diff --git a/server/src/rooms.test.ts b/server/src/rooms.test.ts index c69ed7c..15636e5 100644 --- a/server/src/rooms.test.ts +++ b/server/src/rooms.test.ts @@ -21,6 +21,9 @@ afterEach(() => { const GOLD_OPENS: Input = { from: square('2c2'), to: square('2c3') }; 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((r) => setImmediate(r)); + /** A table of two: Ann, who opened it and plays Gold, and Bo, the game begun. */ function table(spec: GameSpec = game) { 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/); }); - 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 host = rooms.create('Ann', 2, { side: 'scarlet', setup: 'shifted' }); const bot = rooms.addBot(host.room, host.token); expect(game.botNames).toContain(bot.name); rooms.begin(host.room, host.token); - const s = host.room.state!; - expect(s.players.A.side).toBe('scarlet'); - expect(s.players.B.side).toBe('gold'); - expect(s.setup).toBe('shifted'); - // Gold moves first, and Gold is the drake: it has already moved. - expect(s.moves).toHaveLength(1); + expect(host.room.state!.players.A.side).toBe('scarlet'); + expect(host.room.state!.players.B.side).toBe('gold'); + expect(host.room.state!.setup).toBe('shifted'); + // Gold moves first, and Gold is the drake: it moves once the host's request has been answered, not inside it. + expect(host.room.state!.moves).toHaveLength(0); + await settled(); + expect(host.room.state!.moves).toHaveLength(1); expect(rooms.waitingOn(host.room)).toEqual(['A']); }); @@ -181,6 +185,7 @@ describe('rooms', () => { rooms.addBot(host.room, host.token); rooms.begin(host.room, host.token); const room = host.room; + vi.runOnlyPendingTimers(); expect(room.state!.n).toBe(4); expect(rooms.view(room, 'A').playingOn).toBe(true); vi.advanceTimersByTime(2000); @@ -195,16 +200,18 @@ describe('rooms', () => { }); 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(); rooms.submit(room, ann, GOLD_OPENS, 0); 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(rooms.waitingOn(room)).toEqual(['A']); 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 }); rooms.submit(room, ann, { from: square('2d2'), to: square('2d3') }, 2); + await settled(); expect(room.resolved).toBe(4); }); diff --git a/server/src/rooms.ts b/server/src/rooms.ts index 49697e9..7d4da53 100644 --- a/server/src/rooms.ts +++ b/server/src/rooms.ts @@ -351,8 +351,23 @@ export class Rooms { 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 } } : {}) }); // A game whose first move belongs to a bot must not wait for a human who is not owed a move. - this.driveBots(room); - this.botsSpeak(room, 'phase'); + 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): 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.botsSpeak(room, 'phase'); + }); } /** @@ -538,8 +553,7 @@ export class Rooms { } } this.commit(room, { t: 'turn', at: Date.now(), inputs }); - this.driveBots(room); - this.botsSpeak(room, 'phase'); + this.botsAfterAnswering(room); } /**