# House conventions for a game in this kit Every game built from the kit shares one shape, so a player who has been at one table knows the next, and the keeper runs them all the same way. This is the shape. When a game strays from it, the reason should be in the game's README. ## The vocabulary - **The hall** is the landing page (`/`). Name, *Play now against the bot*, *Open a table*, *or join one* with a four-letter code, then *your games* (the ledger of tables this browser holds a seat at, with whose move it is and unread talk), *your reports to the keeper*, and *about this game — a labor of love*. - **A table** is a room. Its code is four letters or digits from an alphabet with no look-alikes, and its link is `/join/CODE`. The masthead button reads *invite friends · room CODE* and copies the link. Whoever opened the table is **the host**; they may *Seat a bot* and they *Begin* when the company suits them. A full table begins by itself. - **The Peanut Gallery** is where a link takes anyone without a seat: a read-only view showing only what every seat could see, counted for the table (*3 in the gallery*). While the table is laid, a watcher is offered a seat first; the gallery is the afterthought. - **Table talk** is the chat panel, seated players only. Each line is a ledger line, so it replays with the game, and the hall counts lines said while you were away. - **The ledger** is the append-only JSONL file that *is* the game: room, seats, start (seed and rules revision), turns (inputs only), talk, over. A room is its ledger replayed through a deterministic engine; nothing else is saved. Ledgers survive deploys and restarts and are backed up nightly. - **Report** in the masthead opens the slip: what happened, what you expected, a screenshot if you have one. It is pinned to the room and the round. **The keeper** replies; the reply appears under the report in the player's hall, and the player may answer. Every report and answer rings the keeper's phone through Sentry; the desk's own replies ring nothing. - **The rules page** (`/rules`) is the game's text, structured for reading mid-game, with the original source verbatim in collapsible sections. The engine follows that text and nothing else; a divergence is a bug. The page ends with *send word*. - **How to play** (`/guide`) walks the page in the game's own voice, one screenshot per section, from the hall to a first game. - **Send word** is the contact paragraph: mail, Bluesky and Mastodon, and where the keeper roosts. It lives in the about panel; the colophons, the rules page and the guide each carry a one-line mail link. ## The engine - Pure and deterministic: `create(names, seed, rules)` and `resolve(state, inputs)` are functions of their arguments with a seeded generator inside the state. Bots are functions of the state too. This is what lets a ledger be the whole record. - Simultaneous by default: every seat that `needsInput` submits, then the round resolves at once, bots included. A seat that owes an extra turn alone (a time stop, a bonus move) is just a state where only it needs input; the server resolves rounds owed only to bots straight away. - Hidden information is the engine's business: `view(state, viewer)` returns what one seat may see, and the SPECTATOR view is the intersection of everyone's. The server sends nothing else. - **The rules revision** is stamped on every game at its start line. Bump it only when the deploy gate shows a fix changes how an already-played round resolves; keep the old path behind `state.rules < N` and pin both with tests. A fix that diverges from no ledger ships without a bump. ## The server One Node process per game, on its own port, run from source with `tsx` as a sandboxed systemd service, behind Caddy `handle` blocks that route `/api` and `/ws` before the static files. Rooms in memory are evicted after a week idle and come back from disk on the next visit. Rate limits sit on the doors a stranger can knock on (rooms, seats, talk, the desk); a table of friends never nears them. Seat tokens never leave the server except to the browser that earned them; a shared link carries none. ## Operations Every game gets the same tools, installed by `deploy.sh` on every deploy: - `deploy/deploy.sh `: checks, tests, build, **the determinism gate** (every production ledger replayed with the engine about to ship and compared with the server; a DIFFERS or REFUSED stops the deploy), rsync, restart. Hashed assets from the previous week stay so an open page can still fetch what it was built against. - `-visitors.sh` (skill `-visitors`): who is here now, who came today, every room opened and how far it got. Names seated for the first time are marked NEW. - `-pulse.sh` (skill `-pulse`): service health, journal errors, the rollup's last week, the backup's last word, the box's vitals. - `-rollup.sh` nightly at 00:12 UTC: one JSON line a day of counts, no addresses. `-backup.sh` nightly at 07:23 UTC: rclone to the shared Space, mirror plus dated snapshots pruned after 90 days. Both check in with Sentry Crons so a missed night is noticed. - The reports desk (skill `-reports`): `pull-reports.sh` mirrors the desk to the Desktop with a digest; `report-reply.sh "text"` answers with `resolved`, `by-design` or `open`. A fix goes live before its reply goes out. - Sentry: one project per game; `sentry-slack-alert.sh` routes every issue to `#-notifications`, run once by the keeper with their own token. ## The look Each game chooses its own palette and typeface in `src/app.css`; the components name only the tokens. Keep the page quiet: one accent, no animation that does not answer an action, the game itself as the hero. No emoji in the interface; the words do the work. Phone width first: a sticky bar for the move if the board runs long, lists that shorten, gutters of 16px, no horizontal scroll. ## Things learned the hard way - Caddy's `try_files` rewrites `/api` before `reverse_proxy` unless the proxy sits in its own `handle` block first. - `npm install --omit=optional` on the droplet breaks Vite; the server carries its own small `package.json` and installs only that. - A dynamic `/join/[code]` route must set `prerender = false`. - Two tabs on one origin share a seat; test a second player from `127.0.0.1` against `localhost` (Vite needs `--host`). - Svelte proxies do not `structuredClone`: hand the engine `$state.snapshot`. - Old saved states lack fields added later: backfill on load, and bump the save version when the shape changes. - A token typed into a session lands in the transcript; run token-bearing scripts yourself and revoke afterwards.