From 75428b5a9b98603ade85f64a337605e61fcaf6db Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Mon, 26 Oct 2026 22:38:08 -0400 Subject: [PATCH] The template's room server guards a stale room once, by name; the test's tick helper is named for what it waits on; the port list and the hall's tally lose a set that was always empty Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx --- template/deploy/README.md | 2 +- template/server/src/rooms.test.ts | 12 ++++++------ template/server/src/rooms.ts | 11 +++++++---- template/src/lib/components/Hall.svelte | 3 +-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/template/deploy/README.md b/template/deploy/README.md index a47f821..c388ef7 100644 --- a/template/deploy/README.md +++ b/template/deploy/README.md @@ -18,7 +18,7 @@ service user, /opt/ and /var/lib/ trees, access log small Node process capped at 300 MB by its unit file, and a room is a few kilobytes of ledger, so the host carries several. Each game needs a port of its own on the host: wizwar 8787, waving-hands 8788, hnefatafl 8789, -dip 8790. +dip 8790, wyrm 8791. ## Current production diff --git a/template/server/src/rooms.test.ts b/template/server/src/rooms.test.ts index 9b0a318..8453467 100644 --- a/template/server/src/rooms.test.ts +++ b/template/server/src/rooms.test.ts @@ -17,7 +17,7 @@ afterEach(() => { }); /** The bots move once the request that made their move due has been answered: after this tick. */ -const settled = () => new Promise((r) => setImmediate(r)); +const answered = () => new Promise((r) => setImmediate(r)); /** A table of the demo game: Ann, who opened it, Bo, and Cy when asked for, the game begun. */ function table(spec: GameSpec = game, withCy = false) { @@ -182,7 +182,7 @@ describe('rooms', () => { const cy = rooms.join(host.room, 'Cy'); const room = host.room; rooms.begin(room, host.token); - await settled(); + await answered(); rooms.say(room, host.token, 'Psst.', ['B']); rooms.say(room, cy.token, 'Hello, all.'); const annSees = rooms.view(room, 'A').chat; @@ -192,7 +192,7 @@ describe('rooms', () => { expect(rooms.view(room, '').chat.map((l) => l.text)).toEqual(['Hello, all.']); rooms.submit(room, host.token, { pick: 1 }, 0); rooms.submit(room, cy.token, { pick: 2 }, 0); - await settled(); + await answered(); expect(heard[0]).toEqual(['Round 1.', 'Psst.', 'I hear you.', 'Hello, all.', 'I hear you.']); expect(room.chat.filter((l) => l.note === 'secret').map((l) => l.text)).toEqual(['Round 1.', 'Round 2.']); expect(rooms.view(room, 'A').parting).toEqual([]); @@ -254,13 +254,13 @@ describe('the host and the other seats', () => { rooms.submit(room, ann, { pick: 5 }, 0); rooms.standIn(room, ann, 'B'); // The round waited on Bo alone; the bot plays it as soon as the host has been answered. - await settled(); + await answered(); expect(room.resolved).toBe(1); expect(rooms.waitingOn(room)).toEqual(['A']); expect(() => rooms.seatOf(room, bo)).toThrow(/gave your seat to a bot/); expect(rooms.view(room, 'A').seats[1]).toEqual({ id: 'B', name: 'Bo', bot: true, standIn: true }); rooms.submit(room, ann, { pick: 3 }, 1); - await settled(); + await answered(); expect(room.resolved).toBe(2); }); @@ -320,7 +320,7 @@ describe('the host and the other seats', () => { expect(() => rooms.claim(phrase)).toThrow(/unknown or has expired/); while (!game.over(room.state!)) { rooms.submit(room, ann, { pick: 5 }, room.resolved); - await settled(); + await answered(); } const next = rooms.get(rooms.rematch(room, ann).roomId); expect(next.expected).toEqual(['Bo']); diff --git a/template/server/src/rooms.ts b/template/server/src/rooms.ts index ae7affc..8f7a76d 100644 --- a/template/server/src/rooms.ts +++ b/template/server/src/rooms.ts @@ -361,10 +361,14 @@ export class Rooms { * to hang and the bot's to come at once. The table is told as each move * lands, the same as any other. */ + /** A room forgotten and loaded again drives itself; a timer or tick that wakes with the old one must do nothing. */ + private isTable(room: Room): boolean { + return this.rooms.get(room.id) === room; + } + private botsAfterAnswering(room: Room): void { setImmediate(() => { - // A room forgotten and loaded again drives itself; this one is no longer the table. - if (this.rooms.get(room.id) !== room) return; + if (!this.isTable(room)) return; try { this.driveBots(room); this.botsSpeak(room, 'phase'); @@ -403,8 +407,7 @@ export class Rooms { const hurried = this.hurried.has(room.id); const next = setTimeout(() => { this.pacing.delete(room.id); - // A room forgotten and loaded again drives itself; this one is no longer the table. - if (this.rooms.get(room.id) !== room) return; + if (!this.isTable(room)) return; this.driveBots(room, hurried ? HURRIED_STEP : 1); // Played out: the bots have their say once, as after any round, which ends a game with parting words. if (!this.pacing.has(room.id)) this.botsSpeak(room, 'phase'); diff --git a/template/src/lib/components/Hall.svelte b/template/src/lib/components/Hall.svelte index 3c763fd..8bd05c7 100644 --- a/template/src/lib/components/Hall.svelte +++ b/template/src/lib/components/Hall.svelte @@ -45,14 +45,13 @@ { n: t.turnsPlayed, label: 'turns recorded' }, { n: hours(t.minutesAtTable), label: 'at the table' } ]; - const onCards = new Set([]); const details = [ { n: t.gamesBegun, label: `games begun, at ${t.tablesOpened} ${t.tablesOpened === 1 ? 'table' : 'tables'}` }, { n: t.namesSeated, label: t.namesSeated === 1 ? 'name has taken a seat' : 'names have taken a seat' }, { n: t.turnsPlayed, label: `turns recorded; ${t.longestGameTurns} in the longest game` }, { n: t.gamesWithBot, label: t.gamesWithBot === 1 ? 'game with a bot seated' : 'games with a bot seated' }, ...(t.talkLines ? [{ n: t.talkLines, label: t.talkLines === 1 ? 'line of table talk' : 'lines of table talk' }] : []), - ...Object.entries(t.byGame).filter(([label]) => !onCards.has(label)).map(([label, n]) => ({ n, label })) + ...Object.entries(t.byGame).map(([label, n]) => ({ n, label })) ]; const first = t.firstGameAt ? `The first game began ${new Date(t.firstGameAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}. ` : ''; return { headline, details, note: `${first}Names are counted, not people: one player under two names is two here. Table time counts only the minutes between moves, so a game left open overnight adds nothing for the night.` };