Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbtUPJ24P6vc6tE1x6RS3m
game-kit
The shared infrastructure of Wiz-War and Waving Hands, extracted so the next game starts with all of it in place: the hall, room codes and tables, the Peanut Gallery (and the host's leave for it to talk), table talk, rematches, seats that travel between devices by phrase and come back by the keeper's link when a browser loses them, the keeper's bell, append-only ledgers replayed through a deterministic engine, the rules revision, the reports desk, the deploy script with its determinism gate, nightly rollup and backup, Sentry with Slack alerts, and the visitors, pulse and reports skills for Claude.
The game itself is one file. src/lib/game/index.ts implements GameSpec
(see src/lib/game/spec.ts); the server, the client store, the hall and
the ops tools never look past it. The template ships with a demo game,
High Card, so everything runs before you have written a line.
Beyond the required methods, a game opts into what it needs through
optional hooks, and without one the kit behaves as if it were not there:
validate (refuse a move with a reason), seatsFor (fewer seats for some
options), settled (drafts a player may replace until final), rankable
(a secret ranking before the deal, read by create), botPress and
parting (bots that talk, remember by notes and read the talk in
botInput), and tally / tallyBegun (the game's own lines in the tally).
CONVENTIONS.md says what each does.
Start a game
./new-game.sh <slug> "<Name>" [port] [domain]
That copies template/ to ~/projects/<slug>, fills the name, port and
domain in, initialises git, installs dependencies, and prints the steps
that remain: write the game, fill in the hall and rules and guide, create
the droplet and the Sentry project, deploy. CONVENTIONS.md is copied to
the new project as docs/conventions.md; it is the house style.
Run the demo locally from a new project:
npm run dev:all # server and site together; Ctrl-C stops both
npm run server # the game server on its port
npm run dev -- --host
What is where
template/
src/lib/game/spec.ts the contract a game implements
src/lib/game/index.ts the demo game; replace it
src/lib/net/ client.ts (calls, held seats, socket), room.svelte.ts (the room store), talk.ts, view.ts
src/lib/components/ Hall, Lobby, TableTalk, ReportSlip, Board (demo; replace it)
src/routes/ the hall, /join/[code], /rules, /guide
server/src/ index (routes), rooms (game-agnostic), store (ledger), reports (the desk), tally, ratelimit, and their tests
deploy/ deploy, setup, Caddy, systemd, cron, gate + replay, backup, rollup, pulse, visitors, Slack alert, reports tools
.claude/skills/ <slug>-visitors, <slug>-pulse, <slug>-reports
Placeholders: __SLUG__, __NAME__, __PORT__, __DOMAIN__ are filled by
new-game.sh; __IP__, __SENTRY_DSN__, __SENTRY_PROJECT_ID__,
and __SENTRY_SLACK_INTEGRATION__ wait until those things exist.
What the kit is, and is not
The model is copy and diverge. A new game starts as a copy of the template and then goes its own way; the two games that came before it are not instances of the template and were not rewritten to be. The kit holds the reference copy of the shared parts and the conventions they follow, so a third game starts from the best current version of everything.
Drift is meant to be visible, not prevented:
./drift.sh ../waving-hands # how far each shared file has wandered
drift-map/<slug>.txt pairs files when a game's layout differs from the
template's. When a game grows something every game should have (the
gallery and the reports desk were both born in one game and ported to the
other), bring it back here, with placeholders, so the next game inherits
it, and read the drift before assuming the kit has it.
The contract is simultaneous rounds: every seat that needs input submits, then the round resolves; turn-at-a-time games fit as the case where one seat needs input at a time. A game of single commands with out-of-turn interruptions, Wiz-War's shape, does not fit and keeps its own server.
The hall-wide notice
Every game on the shared host shows the same notice at the top of its pages when there is one: maintenance, a new game, anything the whole family should hear. The keeper sets it on the host,
hall-announce "Kestrel's Hall is down for five minutes of maintenance" --at "2026-09-30 11:00 UTC"
hall-announce --clear
which writes /var/lib/hall/announcement.json; each game's site serves it at
/hall/announcement.json (a handle_path in its Caddy site file), and
Announcement.svelte, in the template's layout, shows it with the time in
each reader's own clock. A reader may dismiss one notice; the next still
shows. hall-announce is installed by gamehall/deploy.sh.
The family
family.json lists every game of Kestrel's Hall: slug, name, address,
one-line pitch, player count and origin, plus hall, the address of the
family's landing page. Each game ships a copy as static/family.json (the
deploy script takes the kit's latest when the kit is beside the game) and
its about panel lists the others under More games in Kestrel's Hall, the
heading linking to the family's landing page. Add a game here, then
deploy the games.
That landing page, https://gamehall.kestrelsnest.social, is one static page
in gamehall/site/ with a card per game, written by hand. It is served by
Caddy on the shared host beside the games (gamehall/gamehall.caddy) and
published with gamehall/deploy.sh 159.203.115.252. A new game is not in
the family until it has its card there too.
Its visitors are counted from Caddy's log by gamehall/ops/ (installed by
the same deploy): gamehall-visitors.sh on the host for today, and a
nightly rollup line in /var/lib/gamehall/rollup.jsonl. The skill
gamehall-visitors reads them. A new game on the shared host is added to
GAMES in gamehall/ops/traffic.py so the digest counts the people the
hall sends it.
The hall also keeps the night on the shared host: hall-nightly.sh rollup
at 00:12 UTC and hall-nightly.sh backup at 07:23 UTC (gamehall.cron)
run the hall's own rollup and every game's rollup and backup found in
/usr/local/bin, and check in with Sentry Crons once for each job, monitors
hall-rollup and hall-backup in the gamehall project, from the URLs in
/root/.hall-sentry-cron-rollup and /root/.hall-sentry-cron-backup. A game's
own cron file and check-ins are for a box of its own.