Play Zigfall in your browser: https://dnouri.github.io/Zigfall/
Zigfall is a falling-block game with solo play, local two-player on one keyboard, and browser invite-link P2P. For online play, open the browser version, press 3 to host, copy the invite, and send it to another player. The match runs between the two browsers: Zigfall exchanges inputs, keeps both games deterministic, and cross-checks final state/results without a central game server.
The same Zig game core also runs as a native desktop build with solo play and local two-player on one keyboard. Rendering uses raylib-zig; the rules live separately so the core mechanics can be tested without opening a window.
Important
Zigfall is an independent open-source project and is not affiliated with or endorsed by any game publisher or rights holder.
Open https://dnouri.github.io/Zigfall/ in a browser. The host presses 3 to create an online room, then presses C to copy the invite URL. Send that link to the other player; opening a URL with ?join=<room> puts the joiner into the match as player 2.
Online play is browser-only invite-link P2P. Native builds still do solo and local two-player, but they do not do online networking. The browser build uses public relays to find peers and WebRTC for the connection. Zigfall does not have accounts or matchmaking, and there is no lobby or central server running the match. It also does not bundle TURN, so some networks will not connect.
Solo play works on desktop and in the browser. Local two-player versus also works on one keyboard in both builds.
Browsers also keep a small local profile card. It has a nickname, a random local player ID, Local rating, and W-L-D. It is display-only metadata stored in that browser, not a trusted identity and not a global ranking. Completed online matches update local stats only after both peers finish and their final-state/result reports agree. This is a deterministic peer cross-check, not anti-cheat and not a server-authenticated ranking. Disconnects, desyncs, and result disagreements/timeouts do not count.
The board is 10x40, with 20 visible rows. Pieces come from a seven-bag randomizer with a five-piece queue. Hold, ghost, gravity, and lock delay are all in. Rotation has SRS-style kicks plus 180. Clears track T-spins, back-to-back, combos, and perfect clears. Versus play adds garbage.
| Action | Key(s) |
|---|---|
| Solo mode | 1 |
| Local two-player mode | 2 |
| Host browser online match | 3 |
| Copy invite while hosting online | C |
| Move | Left / Right |
| Soft drop | Down |
| Hard drop | Space |
| Rotate clockwise | X or Up |
| Rotate counter-clockwise | Z |
| Rotate 180 degrees | A |
| Hold | C or Left Shift |
| Pause / resume | P |
| Restart local game | R |
| Close desktop window | Esc |
In an online match both players use that single-player control set. R is deliberately a no-op online for now because there is no rematch flow yet.
Local two-player controls
| Action | Player 1 | Player 2 |
|---|---|---|
| Move | A / D |
Left / Right |
| Soft drop | S |
Down |
| Hard drop | Space | Enter |
| Rotate clockwise | W |
Up |
| Rotate counter-clockwise | Q |
. |
| Rotate 180 degrees | E |
/ |
| Hold | Left Shift | Right Shift |
P pauses or resumes, R restarts, and Esc closes the desktop window.
- Online play is browser-only. Native builds do not support online networking.
- There are no accounts or matchmaking. There is no global leaderboard or central match server.
- There is no rematch flow yet.
- Zigfall does not bundle TURN or a controlled signaling service. Browser matches depend on public relays, WebRTC/ICE, and the players' networks.
- Local profile cards and Local rating are browser-local and display-only. They are not trusted, not anti-cheat, and not server-authenticated rankings. Only completed matches with matching peer final-state/result reports update local W-L-D; disconnects, desyncs, and result disagreements/timeouts do not.
src/game.zig contains the deterministic game rules and can be unit-tested without opening a window. src/main.zig is the raylib-facing shell for native and Emscripten builds. The split is intentional: rendering and input should be boring wrappers around a game state that tests can drive directly.
Native and web builds share the Zig core. The web artifact adds a custom shell and small JavaScript helpers. Those helpers handle invite links, browser-local profiles, and Trystero/WebRTC transport. They are copied as local static files by build.zig and loaded before WASM main starts.
For online duels, the host is setup authority for the room and seed, then both browsers run a conservative lockstep match by exchanging inputs. Periodic state hashes are compared to catch desyncs. Match results are only applied to the local profile when both peers finish and the exchanged final outcome, frame cursor, and state hash agree. Extra peers are treated as a busy-room condition rather than silently joining the match.
- Zig 0.16.0
- Network access for the first pinned dependency fetch; dependencies are recorded in
build.zig.zon - The first wasm build may also fetch and cache the Emscripten SDK/toolchain through the pinned Zig/Emscripten dependency flow
- Optional Node.js for JavaScript checks or intentional vendor-bundle regeneration
No local raylib-zig checkout is required. Normal Zig native/web builds and deploys do not run npm; native builds also do not need Trystero or browser APIs.
Fetch dependencies, format, test, and build the native desktop app:
zig build --fetch=needed
zig fmt build.zig build.zig.zon src/*.zig
zig build test
zig build
zig build -Doptimize=ReleaseFast
zig build runNative builds install the executable to zig-out/bin/zigfall (zig-out/bin/zigfall.exe for Windows targets). They also install the legal bundle under zig-out/share/zigfall/. That bundle contains the project license, third-party notices, tracked license snapshots, and the shared browser-vendor notice files.
When a display is available, this is a quick smoke test:
timeout 3 zig build runCross-target builds use the same pinned dependency set. For example:
zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFastLinux cross targets may also need a target sysroot or system libraries for raylib's OpenGL/X11 backend.
Maintainer CI/deployment checks
These mirror the checks that are useful before publishing a Pages build:
zig fmt build.zig build.zig.zon src/*.zig --check
zig build test --summary all
zig build --summary all
zig build -Dtarget=wasm32-emscripten -Doptimize=ReleaseSmall --summary all
for file in web/zigfall_transport.mjs web/zigfall_transport_emscripten.js \
web/zigfall_invite.mjs web/zigfall_invite_emscripten.js \
web/zigfall_profile.mjs web/zigfall_profile_emscripten.js \
web/vendor/patch-trystero-bundle.mjs web/vendor/trystero-nostr.bundle.mjs \
tools/test_trystero_pkt_limit.mjs tools/test_zigfall_transport_adapter.mjs \
tools/test_zigfall_invite.mjs tools/test_zigfall_profile.mjs \
tools/test_emscripten_shims.mjs; do
node --check "$file"
done
node tools/test_trystero_pkt_limit.mjs
node tools/test_zigfall_transport_adapter.mjs
node tools/test_zigfall_invite.mjs
node tools/test_zigfall_profile.mjs
node tools/test_emscripten_shims.mjs
(cd web/vendor && sha256sum -c trystero-nostr.bundle.mjs.sha256)The Pages workflow also verifies the post-build web artifact manifest before upload.
Build the Emscripten HTML/WASM output with:
zig build -Dtarget=wasm32-emscripten -Doptimize=ReleaseSmallTo launch it with Emscripten's emrun helper:
zig build -Dtarget=wasm32-emscripten -Doptimize=ReleaseSmall runThe generated web files are installed to zig-out/web/. The runtime pieces are zigfall.html, zigfall.js, zigfall.wasm, the three zigfall_*.mjs helpers, and the vendored Trystero bundle. The bundle checksum and vendor notices are copied with it. The same legal bundle is copied there too.
The GitHub Pages workflow prepares zig-out/web for upload. It creates index.html, adds .nojekyll, verifies the Trystero bundle checksum, and checks the runtime/legal file manifest.
The production page imports bundled local JavaScript files, not a runtime CDN. Normal native zig build does not use Node or npm. It also does not load Trystero or browser APIs. After Zig dependencies have been fetched, native builds do not need network access. Runtime online play still depends on browser network access, public relays, and WebRTC/ICE/NAT behavior.
The web shell also installs a keyboard handler so Space, Enter, arrow keys, and Slash (/) do not scroll the page or open quick-find while the game is active. Focused links and form controls are left alone.
Zigfall is GPL-3.0-or-later; see LICENSE. Third-party component notices are summarized in THIRD_PARTY_NOTICES.md. Packaged native and web artifacts also include the tracked licenses/ snapshots and web vendor/ notices.
The original single-player/native Zig/raylib-zig baseline was AI-assisted and is preserved in Git history at cd6e23a. Browser multiplayer came after that, along with invite links, profile cards, deployment packaging, and notices.