Conway's Game of Life in Zig written in a weekend with dual build targets and zero external dependencies:
- Terminal - Native executable with ANSI rendering
- Web - WebAssembly + React with canvas rendering
- Zig 0.15.2
- Bun (for web frontend)
zig build runCtrl+C to exit
# Build the WASM module
zig build wasm
# Install web deps and start dev server
cd web
bun install
bun devSpaceto toggle runningsto stepcto clear- Left mouse click to toggle cell
- Mouse drag support
src
├── core.zig # Shared cgol logic
├── main.zig # Entry point for terminal build target
├── terminal.zig # ANSI terminal renderer + utils
└── wasm.zig # WebAssembly exportsBoth build targets support dynamic grid dimensions:
Terminal - On startup, the terminal size is queried via ioctl and calculates the maximum grid that fits within the available rows and columns.
Web - The React frontend measures the viewport and computes grid dimensions to fill the canvas. On window resize, the grid is re-initialized to match the new available space.
This means the simulation automatically scales to use your full screen real estate whether you're running in a small terminal pane or a maximized browser window.
The web frontend avoids copying cell data on every frame by reading directly from WASM linear memory. The wasm.zig module exposes:
export fn getCellsPtr() ?[*]core.Cell {
return if (wasm_grid) |g| g.cells.ptr else null;
}
export fn getCellsLen() usize {
return if (wasm_grid) |g| g.cells.len else 0;
}On the TypeScript side, we create a Uint8Array view into the WASM memory buffer:
export function getCellsArray(wasm: CGOLWasm): Uint8Array {
const ptr = wasm.getCellsPtr();
const len = wasm.getCellsLen();
return new Uint8Array(wasm.memory.buffer, ptr, len);
}This returns a live view; no data is copied. The renderer iterates over this array each frame to draw cells, achieving efficient O(1) access to the entire grid state without serialization overhead.
SPDX-License-Identifier: MIT
Licensed under the MIT License. See LICENSE for full details.