A falling-sand sandbox in the browser — paint ~14 materials and watch them flow, burn, dissolve, and react. Built with React + TypeScript + Vite, with the entire 60fps simulation running on a plain canvas outside React's render cycle.
▶ Play it live — multiplayer, start a room and share the link
▶ Single-player mirror (GitHub Pages, no relay)
- 14 materials across powders, liquids, solids, and energy: sand, water, oil, acid, lava, stone, wood, plant, ice, fire, smoke, steam, gunpowder, walls.
- Real reactions:
- 🌋 Lava + water → stone + steam
- 🔥 Fire spreads through wood / oil / plants and gets doused by water (→ steam)
- 🧪 Acid dissolves solids and powders
- 🌱 Plants creep along water
- 🧊 Ice melts near heat and slowly freezes adjacent water
- 💥 Gunpowder chain-detonates
- Density-based layering — oil floats on water, lava sinks through everything, sand rests under water.
- Real-time bloom/glow on fire and lava.
- Buttery 120 fps thanks to a dirty-chunk scheduler that only simulates regions where something is actually moving.
| Action | Input |
|---|---|
| Draw | Drag on the canvas |
| Erase | Right-click drag |
| Faucet | Hold the pointer still |
| Select material | Number / letter keys (1 sand, 3 water, 6 lava, F fire, …) |
| Pause / play | Space |
| Clear | C |
| Toggle glow | G |
| Brush size | [ and ] |
npm install
npm run dev # http://localhost:5173npm run build # typecheck + production build into dist/
npm run preview # serve the production buildnpm run server # the relay, ws://localhost:8787
npm run dev # in another terminalOpen the app, start a room from the pill in the title bar, and share the
?room=CODE link. The relay holds rooms in memory only; there is no database
and no account.
Rooms run deterministic lockstep: every client simulates the same grid and only paint strokes cross the network, stamped with the tick at which everyone applies them. Streaming grid deltas instead would be megabytes per second at 200x150 cells and 75 ticks per second; strokes are tens of bytes.
The price is that the simulation must be bit-identical everywhere, so all randomness comes from one seeded generator whose state travels inside a serialized snapshot, and every mutation — paint, magnet, lightning, clear, scene load, pause — goes over the wire rather than being applied locally first. Room speed is pinned to 1x for the same reason. Clients checksum their grid every 300 ticks; a mismatch pulls a fresh snapshot from the oldest peer and replays the inputs since.
Deployed at powder.ssscribe.app: the static
bundle behind Caddy, the relay as a systemd service reachable only through the
proxy at /relay. The GitHub Pages build has no relay, so it is single-player.
docs/multiplayer-protocol.md is the full specification. server/ is a plain
ws relay that stamps, orders and forwards opaque blobs; it never parses a
simulation event.
Known limits in this version: no automatic rejoin after a dropped socket, and a client joining after someone picked a gallery scene gets the room's grid but keeps its own lighting toggles.
The interesting architectural choice: React owns the chrome, an imperative core owns the frame.
src/
sim/materials.ts # material IDs + property tables (density, flammability) + UI palette
sim/Simulation.ts # the engine: cellular-automaton rules, reactions, chunk scheduler, renderer
useSimulation.ts # the rAF loop + pointer/keyboard input — the bridge between React and the sim
App.tsx # toolbar / canvas / controls UI
- No
setStatein the hot loop. The render loop reads a mutableuseRefconfig object, so changing the brush or material never triggers a React re-render. React state is just a mirror for displaying the toolbar. - Dirty-chunk scheduling. The grid is split into 16×16 chunks; each frame only simulates chunks flagged active, and any cell that changes wakes its neighborhood for the next frame. Settled regions cost nothing — the same trick the game Noita uses.
- Data-driven materials. Each material is an ID plus a row of properties. Complex behavior (pyramids, oil/water separation, fire fronts) emerges from a handful of local rules.
MIT

