← All docs Games for Group · Principles

Principles — what doesn't change

The architecture evolves; rules and contracts get refined as we build and learn. These principles are what drive the refinements — when a piece of the architecture turns out to violate one, the architecture changes, not the principle. If a proposed change conflicts with one of these, that's the signal to stop and rethink.

Each entry: the principle, why we hold it, and what it looks like in practice.

Companions: decisions records the individual calls these principles produced (with rejected alternatives); learnings records the bugs that taught us; the architecture pages describe the system as it stands today.

1. One source of truth — the same pure rules run everywhere

A game's logic exists in exactly one place (game.ts), and that identical file runs on the Convex server, in the browser for pass-and-play, and on the host phone for LAN. Nothing is ported, mirrored, or reimplemented per mode.

Why: two implementations of the same rules will disagree, and in a multiplayer game a disagreement is a desync — the worst class of bug, invisible until two screens show different boards.

In practice: rules are pure isomorphic TypeScript (no React, no browser, no Convex, no I/O). Boards read rules-derived answers from the same exported selectors the reducer validates with — what a player sees as tappable and what the rules accept can never diverge. Derived data is computed on read, never stored (a stored copy is a second source of truth waiting to go stale).

2. Determinism is non-negotiable

All randomness flows through a seeded generator and all time through an injected clock (ctx.rng, ctx.now). Math.random() and Date.now() never appear in game logic.

Why: determinism is what lets principle 1 work — the server and every client can replay the same match to the identical state, dice rolls sync across devices bit-for-bit, and any bug is reproducible from a seed.

In practice: the engine derives a fresh stream per action from (seed, version); tests force exact rolls through the same door (ctxRolling); a game that needs "optimistic" instant moves qualifies only if its player actions never draw randomness.

3. A game is a self-contained folder

Everything a game is lives in src/games/<id>/ — rules, board, tests, copy. Adding a game touches the registries and the catalog and nothing else; a game never edits shared code, other games, the mode shells, or the backend.

Why: isolation is what makes games cheap to add and impossible to break each other. Game after game, the backend still contains zero game-specific lines — that's the property to protect.

In practice: if a game seems to need a change to shared code, that's an engine/contract discussion first, not a reach-in. The scaffolder (bun run new-game) generates the folder shape so the convention is the path of least resistance.

4. Boards are mode-blind

A Board renders state and calls dispatch. It never knows whether a Zustand store, a Convex mutation, or a LAN websocket is on the other side, and it never carries mode chrome (turn banners, hand-off screens, connection dots — the shells own those).

Why: this is what made three modes possible with one implementation per game — and what makes a future fourth mode a shell-only project. The moment a Board branches on mode, every game pays for every mode forever.

5. The contract forces decisions; it never assumes them

Anything every game must handle is a required part of the typed contract, not a convention someone has to remember. What losing a seat means (onPlayerRemoved) is required — even "not supported" is an explicit, visible declaration. Where the game can be played (meta.modes) is declared AND enforced by the pickers and both engines.

Why: conventions rot; types don't. A forgotten convention compiles, tests green, and fails at a party. A missing contract method doesn't compile.

In practice: when a "games should remember to…" rule appears, the question is always: can the compiler or a test make forgetting impossible?

6. Failures are loud, or they will be silent forever

Anything that can drift quietly gets a check that fails in your face: the LAN guest bundle has a freshness test naming the missing games; the catalog has a consistency test; reducers reject junk payloads and stale removals instead of no-oping; a claim raced and lost throws rather than shrugging.

Why: the scar-tissue pattern in learnings is always the same shape — a check that looked green was structurally incapable of failing. When something "passes," ask what it would take for that check to fail. If the answer is "nothing," it proved nothing.

7. A promise a mode makes, the whole experience keeps

LAN says "no internet needed" — so everything a LAN night touches ships in the binary, art included. Same-device says "pass one phone" — so games whose hidden info can't survive a shared screen honestly declare themselves out (sameDevice: false) instead of shipping a broken experience.

Why: a mode's promise is the user's mental model. One quietly-broken corner ("the pictures don't load on the plane") costs more trust than a missing feature ever would.

8. Learnable in a minute, playable by everyone

Every game must be explainable in three or four short lines (the how-to card — three steps, or three steps per mode — is a forcing function). Party-first: big touch targets, honest states, no dead buttons, spectators welcome, a player quitting handled gracefully.

Why: the product is a group of friends around a table or a room code — the group's slowest learner sets the pace. Depth over breadth: fewer games, each bug-free in every mode it claims.

9. History lives in logs; docs describe today

Docs (CLAUDE.md, adding-a-game, the architecture pages) read as if written fresh today — no patch language, no sediment. The two logs (decisions, learnings) carry the dated history: every real fork with its rejected alternatives, every bug that fooled us with why it escaped. Both are updated in the same session as the change, never after.

Why: a doc that narrates its own edits stops being readable; a decision whose "why" is lost gets relitigated; a lesson not written down gets relearned at full price.

10. Prove it where it can actually fail

Web can't fail a UI-thread bug, dev can't fail a production-domain bug, a unit test can't fail a cross-device sync bug. Every change gets validated on the surface where its failure mode is real: worklets on a device, online play with two real clients, LAN with a real guest, releases against production config.

Why: see principle 6 — a green check that can't fail is decoration. The corollary is the testing ladder every game climbs: pure rules under vitest, then the simulator, then cross-device, before it's called done.

11. Challenge the approach, don't just make it work

Fixing a bug or building something new, the moment a better approach — or a flaw in the current one — becomes clear, challenge it instead of pushing forward on the worse path. Patching the symptom, bolting on a workaround, or finishing a plan you've outgrown is usually the wrong first move. A bug is often the design telling you the approach is wrong (fix the cause); a platform constraint is a signal to question our own behaviour, not just bypass it. And redoing or deleting shipped work is not wasted effort when a better design replaces it — never anchor on sunk cost.

Why: the cost of a worse approach compounds — every later feature bends around it — while the cost of changing course is paid once, now. Constraints usually encode intent (Safari won't speak without a gesture because users hate surprise audio); aligning with them reveals a simpler design. Tambola's "read numbers aloud" was auto-played and host-controlled, hit exactly that wall, and became a per-device opt-in the player switches on — deleting a reducer action, a state field, and a lobby setting while fixing the real problem (2026-07-19; decisions).

12. Every design must pass the mode matrix before it ships an opinion

Before any architecture or design proposal is presented, it is checked against the whole system, not just the question that prompted it: where does it live and run in each of the three modes (online / LAN / same-device — LAN is offline by definition, so anything needing a network is impossible there, while same-device is not an offline mode, because reaching the network is a property of the game rather than of the mode), what does it add to each bundle (web client, native app, Convex), who is the match authority in each, how does it hold up for players who return across weeks (not one session), and what is planned next for the feature — ask, because roadmap items change today's constraints. A first draft is for red-teaming, not presenting: find the three strongest objections before anyone else has to.

Why: a proposal that only answers the asked question exports its unchecked assumptions to whoever reviews it — and each miss costs a full correction round-trip. The checks are mechanical precisely so they can't be skipped when the answer "feels" right. Trivia's question bank made the case: a database-backed bank looked clean until the mode matrix showed two of three modes can't reach a database, and a planned read-aloud mode meant thousands of questions had to be authored as speakable from day one (2026-07-21; decisions). Operational form: the design gates in the repo CLAUDE.md.

Games for Group · Principles — what doesn't change. Companions: decisions · learnings · architecture.