The table, the hall, the rules and the guide: three boards drawn in Gygax's colours, a drake to play, the article in full, and the pages that walk a player in
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
1 parent
1ef5b23712
commit
b9efe304a0
29 files changed
+1116
-333
No files matched your search
+72
-132
@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { game, type Input, type State } from '../../src/lib/game';
|
||||
import { square } from '../../src/lib/game/board';
|
||||
import type { GameSpec } from '../../src/lib/game/spec';
|
||||
import { Rooms } from './rooms';
|
||||
import { Store } from './store';
|
||||
@@ -16,24 +17,37 @@ afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** 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) {
|
||||
/** Gold's first Warrior step and Scarlet's answer. */
|
||||
const GOLD_OPENS: Input = { from: square('2c2'), to: square('2c3') };
|
||||
const SCARLET_ANSWERS: Input = { from: square('2c7'), to: square('2c6') };
|
||||
|
||||
/** A table of two: Ann, who opened it and plays Gold, and Bo, the game begun. */
|
||||
function table(spec: GameSpec<State, Input> = game) {
|
||||
const rooms = new Rooms(spec, new Store(dir));
|
||||
const host = rooms.create('Ann', 3);
|
||||
const host = rooms.create('Ann', 2);
|
||||
const guest = rooms.join(host.room, 'Bo');
|
||||
const third = withCy ? rooms.join(host.room, 'Cy') : null;
|
||||
rooms.begin(host.room, host.token);
|
||||
return { rooms, room: host.room, ann: host.token, bo: guest.token, cy: third?.token };
|
||||
return { rooms, room: host.room, ann: host.token, bo: guest.token };
|
||||
}
|
||||
|
||||
describe('rooms', () => {
|
||||
it('resolves a round once every seat has moved, and refuses a move for a round already played', () => {
|
||||
it('plays a move as soon as the side to move sends it, and refuses one for a round already played', () => {
|
||||
const { rooms, room, ann, bo } = table();
|
||||
rooms.submit(room, ann, { pick: 5 }, 0);
|
||||
expect(rooms.waitingOn(room)).toEqual(['B']);
|
||||
rooms.submit(room, bo, { pick: 2 }, 0);
|
||||
expect(rooms.waitingOn(room)).toEqual(['A']);
|
||||
expect(() => rooms.submit(room, bo, SCARLET_ANSWERS, 0)).toThrow(/not your move/);
|
||||
rooms.submit(room, ann, GOLD_OPENS, 0);
|
||||
expect(room.resolved).toBe(1);
|
||||
expect(() => rooms.submit(room, ann, { pick: 4 }, 0)).toThrow(/already been played/);
|
||||
expect(rooms.waitingOn(room)).toEqual(['B']);
|
||||
expect(() => rooms.submit(room, ann, GOLD_OPENS, 0)).toThrow(/already been played/);
|
||||
rooms.submit(room, bo, SCARLET_ANSWERS, 1);
|
||||
expect(room.resolved).toBe(2);
|
||||
expect(room.state!.moves).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('refuses a move the rules refuse, with the reason', () => {
|
||||
const { rooms, room, ann } = table();
|
||||
expect(() => rooms.submit(room, ann, { from: square('2c2'), to: square('2c4') }, 0)).toThrow(/Warrior cannot go to 2c4/);
|
||||
expect(() => rooms.submit(room, ann, { from: square('2c7'), to: square('2c6') }, 0)).toThrow(/not one of your pieces/);
|
||||
});
|
||||
|
||||
it("puts the keeper's word in the table's talk though the gallery may not talk, and a restart keeps it", () => {
|
||||
@@ -45,7 +59,7 @@ describe('rooms', () => {
|
||||
expect(new Rooms(game, new Store(dir)).get(room.id).chat.at(-1)).toMatchObject(said);
|
||||
});
|
||||
|
||||
it('gives a seat a new token at the keeper\'s word: the old one opens nothing, and a restart keeps the change', () => {
|
||||
it("gives a seat a new token at the keeper's word: the old one opens nothing, and a restart keeps the change", () => {
|
||||
const { rooms, room, bo } = table();
|
||||
const phrase = rooms.transfer(room, bo).phrase;
|
||||
const { seat, token } = rooms.reseat(room, 'B');
|
||||
@@ -62,13 +76,23 @@ describe('rooms', () => {
|
||||
it('re-seats only a person, and says when each person last acted', () => {
|
||||
const { rooms, room, ann } = table();
|
||||
expect(() => rooms.reseat(room, 'Z')).toThrow(/No person sits/);
|
||||
rooms.submit(room, ann, { pick: 5 }, 0);
|
||||
rooms.submit(room, ann, GOLD_OPENS, 0);
|
||||
const seats = rooms.seatsForKeeper(room);
|
||||
expect(seats.map((s) => [s.id, s.name])).toEqual([['A', 'Ann'], ['B', 'Bo']]);
|
||||
expect(seats[0].lastActed).toBeGreaterThan(0);
|
||||
expect(seats[1].lastActed).toBeNull();
|
||||
});
|
||||
|
||||
it('replays a game from its ledger to the same position after a restart', () => {
|
||||
const { rooms, room, ann, bo } = table();
|
||||
rooms.submit(room, ann, GOLD_OPENS, 0);
|
||||
rooms.submit(room, bo, SCARLET_ANSWERS, 1);
|
||||
const reopened = new Rooms(game, new Store(dir)).get(room.id);
|
||||
expect(reopened.state!.cells).toEqual(room.state!.cells);
|
||||
expect(reopened.state!.toMove).toBe('gold');
|
||||
expect(reopened.resolved).toBe(2);
|
||||
});
|
||||
|
||||
it('leaves a room with an unreadable ledger unloaded and opens the rest', () => {
|
||||
const { room } = table();
|
||||
appendFileSync(join(dir, 'ZZZZ.jsonl'), '{"t":"room","id":"ZZZZ","seats":2,"createdAt":1,"options":{}}\n{"t":"seat", torn');
|
||||
@@ -79,80 +103,40 @@ describe('rooms', () => {
|
||||
errors.mockRestore();
|
||||
});
|
||||
|
||||
it('holds a draft until it is settled, and lets it be replaced until then', () => {
|
||||
type Draft = Input & { final?: boolean };
|
||||
const drafts: GameSpec<State, Input> = {
|
||||
...game,
|
||||
cleanInput: (raw) => ({ ...game.cleanInput(raw), final: (raw as Draft)?.final === true }) as Draft,
|
||||
settled: (input) => (input as Draft).final === true
|
||||
};
|
||||
const { rooms, room, ann, bo } = table(drafts);
|
||||
rooms.submit(room, ann, { pick: 5 }, 0);
|
||||
rooms.submit(room, bo, { pick: 2, final: true }, 0);
|
||||
expect(rooms.waitingOn(room)).toEqual(['A']);
|
||||
rooms.submit(room, ann, { pick: 4 }, 0);
|
||||
expect(room.resolved).toBe(0);
|
||||
rooms.submit(room, ann, { pick: 3, final: true }, 0);
|
||||
expect(room.resolved).toBe(1);
|
||||
expect(room.state!.rounds[0].picks).toEqual({ A: 3, B: 2 });
|
||||
rooms.submit(room, bo, { pick: 1, final: true }, 1);
|
||||
expect(() => rooms.submit(room, bo, { pick: 2, final: true }, 1)).toThrow(/already in/);
|
||||
rooms.submit(room, bo, { pick: 2 }, 1);
|
||||
expect(rooms.waitingOn(room)).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it("shows a seat its own move in waiting and its last move played, and nobody else's", () => {
|
||||
const { rooms, room, ann, bo } = table();
|
||||
rooms.submit(room, ann, { pick: 5 }, 0);
|
||||
expect(rooms.view(room, 'A').yourInput).toEqual({ pick: 5 });
|
||||
expect(rooms.view(room, 'B').yourInput).toBeNull();
|
||||
expect(rooms.view(room, '').yourInput).toBeNull();
|
||||
rooms.submit(room, bo, { pick: 2 }, 0);
|
||||
it("shows a seat its last move played, and nobody else's", () => {
|
||||
const { rooms, room, ann } = table();
|
||||
rooms.submit(room, ann, GOLD_OPENS, 0);
|
||||
expect(rooms.view(room, 'A').yourInput).toBeNull();
|
||||
expect(rooms.view(room, 'A').yourLastInput).toEqual({ pick: 5 });
|
||||
expect(rooms.view(room, 'B').yourLastInput).toEqual({ pick: 2 });
|
||||
expect(rooms.view(room, 'A').yourLastInput).toEqual(GOLD_OPENS);
|
||||
expect(rooms.view(room, 'B').yourLastInput).toBeNull();
|
||||
expect(rooms.view(room, '').yourLastInput).toBeNull();
|
||||
});
|
||||
|
||||
it('shows a private line to its sender and the seats it names, never to anyone else or the gallery', () => {
|
||||
const { rooms, room, ann, bo } = table(game, true);
|
||||
it('shows a private line to its sender and the seat it names, never to the gallery', () => {
|
||||
const { rooms, room, ann, bo } = table();
|
||||
rooms.say(room, ann, 'Everyone, hello.');
|
||||
rooms.say(room, ann, 'Bo, lay a five.', ['B']);
|
||||
rooms.say(room, ann, 'Bo, mind your Dragon.', ['B']);
|
||||
const heard = (seat: string) => rooms.view(room, seat).chat.map((l) => l.text);
|
||||
expect(heard('A')).toEqual(['Everyone, hello.', 'Bo, lay a five.']);
|
||||
expect(heard('B')).toEqual(['Everyone, hello.', 'Bo, lay a five.']);
|
||||
expect(heard('C')).toEqual(['Everyone, hello.']);
|
||||
expect(heard('A')).toEqual(['Everyone, hello.', 'Bo, mind your Dragon.']);
|
||||
expect(heard('B')).toEqual(['Everyone, hello.', 'Bo, mind your Dragon.']);
|
||||
expect(heard('')).toEqual(['Everyone, hello.']);
|
||||
expect(rooms.view(room, 'B').chat[1]).toMatchObject({ id: 'A', to: ['B'], turn: 1 });
|
||||
expect(() => rooms.say(room, bo, 'Anyone?', ['Q'])).toThrow(/Nobody at this table/);
|
||||
});
|
||||
|
||||
it('keeps each ranking secret until the deal, which reads them all from the start line', () => {
|
||||
const dealt: Record<string, string[]>[] = [];
|
||||
const ranked: GameSpec<State, Input> = {
|
||||
...game,
|
||||
rankable: () => [
|
||||
{ value: 'red', label: 'Red' },
|
||||
{ value: 'blue', label: 'Blue' }
|
||||
],
|
||||
create: (names, seed, rules, options, rankings) => {
|
||||
dealt.push(rankings ?? {});
|
||||
return game.create(names, seed, rules, options);
|
||||
}
|
||||
};
|
||||
const rooms = new Rooms(ranked, new Store(dir));
|
||||
const host = rooms.create('Ann', 2);
|
||||
const guest = rooms.join(host.room, 'Bo');
|
||||
const room = host.room;
|
||||
expect(() => rooms.rank(room, host.token, ['red'])).toThrow(/every choice once/);
|
||||
rooms.rank(room, host.token, ['blue', 'red']);
|
||||
expect(rooms.view(room, 'A').yourRanking).toEqual(['blue', 'red']);
|
||||
expect(rooms.view(room, 'B')).toMatchObject({ yourRanking: null, ranked: 1 });
|
||||
rooms.begin(room, host.token);
|
||||
expect(dealt).toEqual([{ A: ['blue', 'red'] }]);
|
||||
expect(() => rooms.rank(room, guest.token, ['red', 'blue'])).toThrow(/ranking is closed/);
|
||||
new Rooms(ranked, new Store(dir));
|
||||
expect(dealt[1]).toEqual({ A: ['blue', 'red'] });
|
||||
it('seats the host on the side they chose, and a drake across from them', () => {
|
||||
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(rooms.waitingOn(host.room)).toEqual(['A']);
|
||||
});
|
||||
|
||||
it('refuses a ranking at a table that asks for none', () => {
|
||||
@@ -161,39 +145,7 @@ describe('rooms', () => {
|
||||
expect(() => rooms.rank(host.room, host.token, ['A'])).toThrow(/asks for no ranking/);
|
||||
});
|
||||
|
||||
it('lets a game give its bots words: when a round opens and when spoken to, with notes kept from every view', () => {
|
||||
const heard: string[][] = [];
|
||||
const talking: GameSpec<State, Input> = {
|
||||
...game,
|
||||
botInput: (state, seat, press) => {
|
||||
heard.push((press ?? []).map((l) => l.text));
|
||||
return game.botInput(state, seat);
|
||||
},
|
||||
botPress: (state, seat, press, people, trigger) =>
|
||||
trigger === 'phase' ? [{ to: people, text: `Round ${game.turn(state)}.`, note: 'secret' }] : [{ to: [press[press.length - 1].id], text: 'I hear you.' }],
|
||||
parting: (_press, viewer) => [{ id: 'B', text: `Farewell, ${viewer}.` }]
|
||||
};
|
||||
const rooms = new Rooms(talking, new Store(dir));
|
||||
const host = rooms.create('Ann', 3);
|
||||
rooms.addBot(host.room, host.token);
|
||||
const cy = rooms.join(host.room, 'Cy');
|
||||
const room = host.room;
|
||||
rooms.begin(room, host.token);
|
||||
rooms.say(room, host.token, 'Psst.', ['B']);
|
||||
rooms.say(room, cy.token, 'Hello, all.');
|
||||
const annSees = rooms.view(room, 'A').chat;
|
||||
expect(annSees.map((l) => l.text)).toEqual(['Round 1.', 'Psst.', 'I hear you.', 'Hello, all.']);
|
||||
expect(annSees.some((l) => 'note' in l)).toBe(false);
|
||||
expect(rooms.view(room, 'C').chat.map((l) => l.text)).toEqual(['Round 1.', 'Hello, all.', 'I hear you.']);
|
||||
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);
|
||||
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([]);
|
||||
});
|
||||
|
||||
it('refuses private talk to a bot when the game gives bots no words', () => {
|
||||
it('refuses private talk to a drake, which reads none', () => {
|
||||
const rooms = new Rooms(game, new Store(dir));
|
||||
const host = rooms.create('Ann', 2);
|
||||
rooms.addBot(host.room, host.token);
|
||||
@@ -243,28 +195,20 @@ 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", () => {
|
||||
it("gives a person's seat to a drake, 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.submit(room, ann, GOLD_OPENS, 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);
|
||||
// The move was Bo's; the drake made it.
|
||||
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);
|
||||
expect(room.resolved).toBe(4);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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/);
|
||||
@@ -274,15 +218,15 @@ describe('the host and the other seats', () => {
|
||||
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', () => {
|
||||
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.submit(room, ann, GOLD_OPENS, 0);
|
||||
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 });
|
||||
});
|
||||
@@ -305,14 +249,10 @@ describe('the host and the other seats', () => {
|
||||
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', () => {
|
||||
it('voids a phrase for the seat once a drake stands 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']);
|
||||
});
|
||||
});
|
||||
+5
-5
@@ -194,7 +194,7 @@ export class Rooms<State, Input> {
|
||||
if (!token) throw new RoomError('That token opens no seat at this game.', 403);
|
||||
const given = Buffer.from(hashToken(token), 'hex');
|
||||
const seat = room.seats.find((s) => s.tokenHash.length === given.length * 2 && timingSafeEqual(Buffer.from(s.tokenHash, 'hex'), given));
|
||||
if (seat?.standIn) throw new RoomError('The host gave your seat to a bot, which plays on in your place. Ask them for a transfer phrase to take it back.', 403);
|
||||
if (seat?.standIn) throw new RoomError('The host gave your seat to a drake, which plays on in your place. Ask them for a transfer phrase to take it back.', 403);
|
||||
if (!seat) throw new RoomError('That token opens no seat at this game.', 403);
|
||||
return seat;
|
||||
}
|
||||
@@ -412,7 +412,7 @@ export class Rooms<State, Input> {
|
||||
/** The host seats a bot in an empty chair. */
|
||||
addBot(room: Room<State>, token: string | undefined): Seat {
|
||||
const host = this.seatOf(room, token);
|
||||
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may seat a bot.', 403);
|
||||
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may seat a drake.', 403);
|
||||
if (room.state) throw new RoomError('The game has begun; no more seats are taken.', 409);
|
||||
const taken = new Set(room.seats.map((s) => s.name.toLowerCase()));
|
||||
const pool = this.game.botNames.filter((n) => !taken.has(n.toLowerCase()));
|
||||
@@ -456,7 +456,7 @@ export class Rooms<State, Input> {
|
||||
*/
|
||||
standIn(room: Room<State>, token: string | undefined, seatId: unknown): void {
|
||||
const host = this.seatOf(room, token);
|
||||
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may give a seat to a bot.', 403);
|
||||
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may give a seat to a drake.', 403);
|
||||
if (!room.state || this.game.over(room.state)) throw new RoomError('Only a game under way needs a stand-in.', 409);
|
||||
if (seatId === host.id) throw new RoomError('The host keeps their own seat.', 409);
|
||||
const seat = room.seats.find((s) => s.id === seatId && !s.bot);
|
||||
@@ -485,10 +485,10 @@ export class Rooms<State, Input> {
|
||||
/** The host sends a bot away before the game begins. People leave by not coming back. */
|
||||
unseat(room: Room<State>, token: string | undefined, seatId: unknown): void {
|
||||
const host = this.seatOf(room, token);
|
||||
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may send a bot away.', 403);
|
||||
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may send a drake away.', 403);
|
||||
if (room.state) throw new RoomError('The game has begun; the table is set.', 409);
|
||||
const seat = room.seats.find((s) => s.id === seatId);
|
||||
if (!seat || !seat.bot) throw new RoomError('Only a bot can be sent away.', 400);
|
||||
if (!seat || !seat.bot) throw new RoomError('Only a drake can be sent away.', 400);
|
||||
this.commit(room, { t: 'unseat', id: seat.id });
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ afterEach(() => {
|
||||
|
||||
describe('the tally', () => {
|
||||
it("adds a game's own lines from every game begun, finished or not", () => {
|
||||
const counted: GameSpec<State, Input> = { ...game, tallyBegun: () => ({ 'games of High Card begun': 1 }) };
|
||||
const counted: GameSpec<State, Input> = { ...game, tallyBegun: () => ({ 'games of Wyrm begun': 1 }) };
|
||||
const rooms = new Rooms(counted, new Store(dir));
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const host = rooms.create('Ann', 2);
|
||||
@@ -28,6 +28,6 @@ describe('the tally', () => {
|
||||
rooms.create('Bo', 2);
|
||||
const t = new Tallies(counted, new Store(dir)).get();
|
||||
expect(t).toMatchObject({ tablesOpened: 3, gamesBegun: 2, gamesFinished: 0 });
|
||||
expect(t.byGame).toEqual({ 'games of High Card begun': 2 });
|
||||
expect(t.byGame).toEqual({ 'games of Wyrm begun': 2 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user