The drake moves once the player has been answered, so a move lands at once and the reply follows

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 17:30:11 -04:00
1 parent 7545985d98
commit 2ef7682dbd
2 files changed
+32 -11

No files matched your search

+16 -9
View File
@@ -21,6 +21,9 @@ afterEach(() => {
const GOLD_OPENS: Input = { from: square('2c2'), to: square('2c3') }; const GOLD_OPENS: Input = { from: square('2c2'), to: square('2c3') };
const SCARLET_ANSWERS: Input = { from: square('2c7'), to: square('2c6') }; const SCARLET_ANSWERS: Input = { from: square('2c7'), to: square('2c6') };
/** 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));
/** A table of two: Ann, who opened it and plays Gold, and Bo, the game begun. */ /** A table of two: Ann, who opened it and plays Gold, and Bo, the game begun. */
function table(spec: GameSpec<State, Input> = game) { function table(spec: GameSpec<State, Input> = game) {
const rooms = new Rooms(spec, new Store(dir)); const rooms = new Rooms(spec, new Store(dir));
@@ -124,18 +127,19 @@ describe('rooms', () => {
expect(() => rooms.say(room, bo, 'Anyone?', ['Q'])).toThrow(/Nobody at this table/); expect(() => rooms.say(room, bo, 'Anyone?', ['Q'])).toThrow(/Nobody at this table/);
}); });
it('seats the host on the side they chose, and a drake across from them', () => { it('seats the host on the side they chose, and a drake across from them, which moves once the host has been answered', async () => {
const rooms = new Rooms(game, new Store(dir)); const rooms = new Rooms(game, new Store(dir));
const host = rooms.create('Ann', 2, { side: 'scarlet', setup: 'shifted' }); const host = rooms.create('Ann', 2, { side: 'scarlet', setup: 'shifted' });
const bot = rooms.addBot(host.room, host.token); const bot = rooms.addBot(host.room, host.token);
expect(game.botNames).toContain(bot.name); expect(game.botNames).toContain(bot.name);
rooms.begin(host.room, host.token); rooms.begin(host.room, host.token);
const s = host.room.state!; expect(host.room.state!.players.A.side).toBe('scarlet');
expect(s.players.A.side).toBe('scarlet'); expect(host.room.state!.players.B.side).toBe('gold');
expect(s.players.B.side).toBe('gold'); expect(host.room.state!.setup).toBe('shifted');
expect(s.setup).toBe('shifted'); // Gold moves first, and Gold is the drake: it moves once the host's request has been answered, not inside it.
// Gold moves first, and Gold is the drake: it has already moved. expect(host.room.state!.moves).toHaveLength(0);
expect(s.moves).toHaveLength(1); await settled();
expect(host.room.state!.moves).toHaveLength(1);
expect(rooms.waitingOn(host.room)).toEqual(['A']); expect(rooms.waitingOn(host.room)).toEqual(['A']);
}); });
@@ -181,6 +185,7 @@ describe('rooms', () => {
rooms.addBot(host.room, host.token); rooms.addBot(host.room, host.token);
rooms.begin(host.room, host.token); rooms.begin(host.room, host.token);
const room = host.room; const room = host.room;
vi.runOnlyPendingTimers();
expect(room.state!.n).toBe(4); expect(room.state!.n).toBe(4);
expect(rooms.view(room, 'A').playingOn).toBe(true); expect(rooms.view(room, 'A').playingOn).toBe(true);
vi.advanceTimersByTime(2000); vi.advanceTimersByTime(2000);
@@ -195,16 +200,18 @@ describe('rooms', () => {
}); });
describe('the host and the other seats', () => { describe('the host and the other seats', () => {
it("gives a person's seat to a drake, which plays it at once, and the person's token opens nothing", () => { it("gives a person's seat to a drake, which plays it at once, and the person's token opens nothing", async () => {
const { rooms, room, ann, bo } = table(); const { rooms, room, ann, bo } = table();
rooms.submit(room, ann, GOLD_OPENS, 0); rooms.submit(room, ann, GOLD_OPENS, 0);
rooms.standIn(room, ann, 'B'); rooms.standIn(room, ann, 'B');
// The move was Bo's; the drake made it. // The move was Bo's; the drake makes it as soon as the host has been answered.
await settled();
expect(room.resolved).toBe(2); expect(room.resolved).toBe(2);
expect(rooms.waitingOn(room)).toEqual(['A']); expect(rooms.waitingOn(room)).toEqual(['A']);
expect(() => rooms.seatOf(room, bo)).toThrow(/gave your seat to a drake/); expect(() => rooms.seatOf(room, bo)).toThrow(/gave your seat to a drake/);
expect(rooms.view(room, 'A').seats[1]).toEqual({ id: 'B', name: 'Bo', bot: true, standIn: true }); expect(rooms.view(room, 'A').seats[1]).toEqual({ id: 'B', name: 'Bo', bot: true, standIn: true });
rooms.submit(room, ann, { from: square('2d2'), to: square('2d3') }, 2); rooms.submit(room, ann, { from: square('2d2'), to: square('2d3') }, 2);
await settled();
expect(room.resolved).toBe(4); expect(room.resolved).toBe(4);
}); });
+16 -2
View File
@@ -351,8 +351,23 @@ export class Rooms<State, Input> {
if (room.seats.length < this.game.minSeats) throw new RoomError(`The game needs at least ${this.game.minSeats} players.`, 409); if (room.seats.length < this.game.minSeats) throw new RoomError(`The game needs at least ${this.game.minSeats} players.`, 409);
this.commit(room, { t: 'start', seed: newSeed(), rules: this.game.currentRules, ...(Object.keys(room.rankings).length ? { rankings: { ...room.rankings } } : {}) }); this.commit(room, { t: 'start', seed: newSeed(), rules: this.game.currentRules, ...(Object.keys(room.rankings).length ? { rankings: { ...room.rankings } } : {}) });
// A game whose first move belongs to a bot must not wait for a human who is not owed a move. // A game whose first move belongs to a bot must not wait for a human who is not owed a move.
this.botsAfterAnswering(room);
}
/**
* The bots' moves, once the request that made them due has been answered.
* A bot that thinks for a second or two would otherwise hold up the reply
* to the person whose move it is answering, so their own move would seem
* to hang and the bot's to come at once. The table is told as each move
* lands, the same as any other.
*/
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;
this.driveBots(room); this.driveBots(room);
this.botsSpeak(room, 'phase'); this.botsSpeak(room, 'phase');
});
} }
/** /**
@@ -538,8 +553,7 @@ export class Rooms<State, Input> {
} }
} }
this.commit(room, { t: 'turn', at: Date.now(), inputs }); this.commit(room, { t: 'turn', at: Date.now(), inputs });
this.driveBots(room); this.botsAfterAnswering(room);
this.botsSpeak(room, 'phase');
} }
/** /**