Frontend
Building BlockMyBlocks: A balance game for all ages
Reallexi LLC Dev.to (EN Zone)
1 views
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.
Read original: https://dev.to/reallexi_llc_384b040df139/building-blockmyblocks-a-balance-game-for-all-ages-4g84
← Previous
My 3B Model Found a Shortcut. It Took Me Three Fixes to Close It.
Next →
Getting Started with Excel for Data Analytics: From Basics to Data Cleaning.
Related
Comments0
No comments yet — be the first