BlockMyBlocks is out now on Web, iOS, Android. This is how it got made: the decisions, the pipeline, and what I would do differently. What it is A balance game for all ages. Stack the pieces on a deck that tips, hold it steady, and keep going — the levels never run out. Stack the pieces on a deck that tips, hold it steady, and keep going — the levels never run out. Determinism and golden tests The sim is pure and deterministic — time and seed are parameters. That buys the strongest cheap test there is: The catch-up law: simulate(state, a+b) === simulate(simulate(state, a), b) for arbitrary splits. Offline catch-up, tab-restore and server ticks all depend on it. Test it directly with quantised splits (e.g. 120 = 60+60), plus: rewards are integers, no NaN anywhere after a very long run. Golden files: shared/test/golden/.json holds {seed, state0, actions[], expected: {hash per interval, final snapshot}}. The test replays the actions and compares a stable hash of the state at every interval. Any sim change fails golden; --update regenerates only after a human confirms the change is intended and the PR says why. Keep scenarios small but adversarial (empty world, dense world, each subsystem stressed, one long-horizon run). Cross-runtime determinism: the browser smoke computes the same hashes with the same shared sim and must match the server — catches engine/float differences before they become a Scene architecture Rules that keep the frame loop cheap: State reads in useFrame go through store.getState(), never per-instance subscriptions. Selectors are narrow (useWorld(s => s.entities)). HUD is DOM, not canvas. Everything tappable is React DOM for accessibility and crisp text; in-world labels are drei with a hard cap (~40 visible, nearest first). Zero per-frame allocation in steady state: pre-parsed THREE.Color constants (col.copy(c), never col.set('#hex')), module-level scratch vectors, geometry rebuilds gated on a content signature, not on "something changed". Debug handles live on window.game: stats() (draw calls, tris, dpr, tier, fps), mem() (GPU bytes by owner), setQuality, setDpr, setTimeOfDay, governor flag. Test scripts read these; nothing in gameplay Recommended platform collections The generic meta-game set. Add game collections beside them; keep these shapes so payments, social and live-ops code stays portable between games. All timestamps epoch-ms. Collection: Id Fields accounts: account id (nanoid) displayName, isGuest, providers: [{provider: 'guest'\ 'google'\ 'apple'\ 'email', subject}], email?, passwordHash?, locale, country?, createdAt, lastSeenAt, bannedAt?, bannedReason? sessions: opaque 32-byte token (never logged) accountId, deviceId?, createdAt, expiresAt, lastUsedAt wallets: : balance (integer ≥ 0, a cache of the ledger sum), updatedAt wallet_ledger: : accountId, currency, delta, balanceAfter, reason ('collect'\ 'purchase'\ 'gift'\ 'event'\ 'admin'\ 'refund'…), refType?, refId?, at — append-only; the ledger is truth purchases: : accountId, productId, provider ('stripe'\ 'play'\ 'apple'), state ('pending'\ 'granted'\ 'refunded'\ 'failed'), amountCents?, currency?, createdAt, grantedAt?, refundedAt?, rawReceiptRef? entitlements: : key ('season_pass:', 'no_ads', 'pack:'), sourcePurchaseId?, grantedAt, expiresAt? (null = permanent), revokedAt? leaderboard_entries: :: score, meta? (display extras, id-only), updatedAt; periodKey = 'all', ISO week '2026-W35', or an event id flags: flag key value, segments? ({country?, minLevel?, pct?}), version, updatedAt — live-ops overrides on top of content events: event id runtime… Validator-per-message Every message type — WS and REST body alike — has exactly one validator function in shared/protocol.mjs, used by the client before sending and by the server before touching state. Validators are whitelists: the only fields that exist afterwards are the ones they return. Unknown t values and oversized frames (> 16 KB) close the socket; unknown fields are dropped; null from a validator is BAD_PAYLOAD, before any service code runs. There is never a second, looser parser What is next It is live at https://ddkits.github.io/block-my-blocks. If you have shipped something similar, I would like to hear what you would have cut.