The host has a hand on the other seats: once the game begins, Seats makes a transfer phrase for another person's seat, which gives it a new token when claimed, and gives the seat of a player who cannot go on to a bot, which plays the move the seat owes at once and speaks nothing in their name; the person's token opens nothing while it stands in, their page goes to the gallery with the reason and a claim box, and a rematch holds their seat without the stand-in; the server pings every socket and closes one a dead browser left open

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric WagonerandClaude Opus 5.5 committed 2026-10-16 19:04:33 -04:00
1 parent c439c15e36
commit 09a8b675ad
15 files changed
+462 -35

No files matched your search

+75
View File
@@ -231,3 +231,78 @@ describe('rooms', () => {
});
});
});
describe('the host and the other seats', () => {
it("gives a person's seat to a bot, which plays it at once, and the person's token opens nothing", () => {
const { rooms, room, ann, bo } = table();
rooms.submit(room, ann, { pick: 5 }, 0);
rooms.standIn(room, ann, 'B');
// The round waited on Bo alone; the bot played it.
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);
expect(room.resolved).toBe(2);
});
it('keeps a move the person made before the bot stood in', () => {
const { rooms, room, ann, bo } = table();
rooms.submit(room, bo, { pick: 2 }, 0);
rooms.standIn(room, ann, 'B');
rooms.submit(room, ann, { pick: 5 }, 0);
expect(room.state!.rounds[0].picks).toEqual({ A: 5, B: 2 });
});
it('is the host\'s call alone, in a game under way, for a person other than the host', () => {
const { rooms, room, ann, bo } = table();
expect(() => rooms.standIn(room, bo, 'A')).toThrow(/Only the player who opened/);
expect(() => rooms.standIn(room, ann, 'A')).toThrow(/keeps their own seat/);
expect(() => rooms.standIn(room, ann, 'Z')).toThrow(/No person sits/);
const lobby = rooms.create('Ann', 2);
rooms.join(lobby.room, 'Bo');
expect(() => rooms.standIn(lobby.room, lobby.token, 'B')).toThrow(/under way/);
});
it('gives the seat back by the host\'s phrase, with a new token, and a restart keeps both changes', () => {
const { rooms, room, ann, bo } = table();
rooms.standIn(room, ann, 'B');
expect(new Rooms(game, new Store(dir)).get(room.id).seats[1]).toMatchObject({ bot: true, standIn: true });
const claimed = rooms.claim(rooms.hostPhrase(room, ann, 'B').phrase);
expect(claimed.seat).toBe('B');
expect(rooms.seatOf(room, claimed.token).name).toBe('Bo');
expect(() => rooms.seatOf(room, bo)).toThrow(/opens no seat/);
expect(rooms.waitingOn(room)).toContain('B');
const reopened = new Rooms(game, new Store(dir)).get(room.id);
expect(reopened.seats[1]).toEqual({ id: 'B', name: 'Bo', tokenHash: room.seats[1].tokenHash, bot: false });
});
it("re-seats a person whose browser lost the seat, by the host's phrase, once", () => {
const { rooms, room, ann, bo } = table();
const { phrase } = rooms.hostPhrase(room, ann, 'B');
const claimed = rooms.claim(phrase);
expect(claimed.token).not.toBe(bo);
expect(() => rooms.seatOf(room, bo)).toThrow(/opens no seat/);
expect(() => rooms.claim(phrase)).toThrow(/unknown or has expired/);
});
it("makes a phrase only for the host, and only for another person's seat", () => {
const { rooms, room, ann, bo } = table();
expect(() => rooms.hostPhrase(room, bo, 'A')).toThrow(/Only the player who opened/);
expect(() => rooms.hostPhrase(room, ann, 'A')).toThrow(/No other person/);
const withBot = rooms.create('Ann', 2);
const bot = rooms.addBot(withBot.room, withBot.token);
expect(() => rooms.hostPhrase(withBot.room, withBot.token, bot.id)).toThrow(/No other person/);
});
it('voids a phrase for the seat once a bot stands in, and holds the person\'s seat at a rematch without the stand-in', () => {
const { rooms, room, ann } = table();
const { phrase } = rooms.hostPhrase(room, ann, 'B');
rooms.standIn(room, ann, 'B');
expect(() => rooms.claim(phrase)).toThrow(/unknown or has expired/);
while (!game.over(room.state!)) rooms.submit(room, ann, { pick: 5 }, room.resolved);
const next = rooms.get(rooms.rematch(room, ann).roomId);
expect(next.expected).toEqual(['Bo']);
expect(next.seats.map((s) => s.name)).toEqual(['Ann']);
});
});