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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
Eric WagonerandClaude Fable 5.1 committed 2026-10-26 22:38:08 -04:00
1 parent 62cb00be33
commit 75428b5a9b
4 files changed
+15 -13

No files matched your search

+6 -6
View File
@@ -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<void>((r) => setImmediate(r));
const answered = () => new Promise<void>((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<State, Input> = 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']);
+7 -4
View File
@@ -361,10 +361,14 @@ export class Rooms<State, Input> {
* 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<State>): boolean {
return this.rooms.get(room.id) === room;
}
private botsAfterAnswering(room: Room<State>): 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<State, Input> {
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');