Take turns
Goal
Two seats take one counter per turn from a shared pile. The authority rejects out-of-turn actions and passes the turn after 30 seconds, including room sleep.
Files you will edit
src/game.ts
Steps
In a template shell, use this minimal board definition. For rooms, keep your existing multiplayer configuration; allow seats 0 and 1 only. Space asks to take a counter. The sender comes from the message envelope.
tsimport { createTurns, defineGame, defineMessage, type GameContext } from 'gameable'; const Take = defineMessage('take-counter', (p): p is null => p === null); let pile = 6; let timeouts = 0; const model = { text: { seat: 0, pile: 6, timeouts: 0 } }; const turns = createTurns({ seats: 2, turnSeconds: 30, onTimeout() { timeouts++; }, }); function take(ctx: GameContext, seat: number): void { if (seat < 0 || seat >= 2 || !Number.isInteger(seat)) return; if (!turns.isTurn(seat) || pile === 0) return; pile--; turns.endTurn(ctx); } function ask(ctx: GameContext): void { if (ctx.input.pressed('Space')) ctx.net.send(Take, null); } function board(ctx: GameContext): void { turns.step(ctx); const requests = ctx.net.messages(Take); for (let i = 0; i < requests.length; i++) take(ctx, requests[i].player); if (ctx.net.role === 'solo' && ctx.input.pressed('Space')) { take(ctx, turns.current()); } model.text.seat = turns.current(); model.text.pile = pile; model.text.timeouts = timeouts; if (ctx.net.role === 'solo') ctx.hud.set(model); const players = ctx.players.list; for (let i = 0; i < players.length; i++) players[i].hud.set(model); } function snapshotBoardDocument(): unknown { return { turns: turns.snapshot(), pile, timeouts }; } function restoreBoardDocument(state: unknown): void { const saved = state as { turns: unknown; pile: number; timeouts: number }; turns.restore(saved.turns); pile = saved.pile; timeouts = saved.timeouts; } export function wakeBoard(ctx: GameContext, doc: unknown, sleptSeconds: number): void { restoreBoardDocument(doc); turns.elapse(ctx, sleptSeconds); } export default defineGame({ init(ctx) { turns.reset(); pile = 6; timeouts = 0; if (ctx.net.role !== 'client') turns.start(ctx); }, systems: [ { run: ask, on: 'client' }, { run: board, on: 'authority' }, ], snapshot: snapshotBoardDocument, restore: restoreBoardDocument, });For a room that sleeps between sessions, keep
snapshotBoardDocument()in the room's own document and, when the room wakes, callwakeBoardfrom the authority with that document and the seconds it slept, before ordinary steps resume. Where that document lives and how the wake reaches the game is the multiplayer bundle's job (llms-multiplayer.txt); this recipe only needs the two functions above. The game works without persistence.Save after wake too.
elapseemits timeout callbacks before passing each expired turn; it returns the number consumed. It caps catch-up at 2000 timeouts for two seats, then restarts a full clock. Do not readDate.nowin the guest or snapshot from a system. Validate move legality alongsideisTurnwhen replacing the counter pile with a real board.
Verify
sh
npm run devSolo: each Space changes pile 6 → 5 → 4 and seat 0 → 1 → 0. In two room tabs, seat 1 pressing first leaves pile at 6; seat 0 pressing changes it to 5. Wait 30 seconds: seat changes and timeouts rises by one without reducing pile. For the wake integration, save a fresh 30-second turn and wake after 35 seconds: one timeout fires, seat is 1, and turns.remainingSeconds() is 25 at 60 Hz.