A game may count its own lines from every game begun, not only those finished

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-29 12:20:03 -04:00
co-authored by Claude Opus 5.5
parent 403232087d
commit d97d709212
4 changed files with 43 additions and 3 deletions
+5 -2
View File
@@ -99,7 +99,8 @@ README.
- **The tally** sits in the about panel: what the ledgers add up to
(tables, games begun and finished, names seated, turns, time at the
table, games with a bot, talk), counted on demand from the files and
cached a minute. A game adds its own lines through `GameSpec.tally`.
cached a minute. A game adds its own lines through `GameSpec.tally` and
`GameSpec.tallyBegun`.
- **Table options** are the host's choices when opening a table (a
variant, a side, a length): the game declares them in `GameSpec.options`,
the hall renders them as selects, the room line records them, and
@@ -157,7 +158,9 @@ if it were never there.
`botInput` receives the same press, so a bot can keep what was agreed.
`parting(press, viewer)` gives the bots' last words once the game is
over. Without `botPress`, private talk to a bot is refused.
- `tally(state)`: the game's own lines for the tally, from a finished game.
- `tally(state)`: the game's own lines for the tally, from a finished game;
`tallyBegun(options)`: lines from every game begun, finished or not, by
its table's options (games begun on each variant).
- `preferences`, `options`: the preferences panel's settings and the
host's table choices (above).
+33
View File
@@ -0,0 +1,33 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { game, type Input, type State } from '../../src/lib/game';
import type { GameSpec } from '../../src/lib/game/spec';
import { Rooms } from './rooms';
import { Store } from './store';
import { Tallies } from './tally';
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'tally-'));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
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 rooms = new Rooms(counted, new Store(dir));
for (let i = 0; i < 2; i++) {
const host = rooms.create('Ann', 2);
rooms.addBot(host.room, host.token);
rooms.begin(host.room, host.token);
}
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 });
});
});
+3 -1
View File
@@ -1,7 +1,8 @@
// The tally: what the ledgers add up to, for the hall's about panel. Read
// from the files on demand and cached for a minute, so a curious visitor
// costs nothing and a busy hall costs one scan a minute. The counts are
// the kit's; a game may add its own lines through GameSpec.tally.
// the kit's; a game may add its own lines through GameSpec.tally (from a
// finished game) and GameSpec.tallyBegun (from any game begun).
import { cleanOptions, type GameSpec, type SeatId } from '../../src/lib/game/spec';
import type { Tally } from '../../src/lib/net/view';
@@ -56,6 +57,7 @@ export class Tallies<State, Input> {
t.gamesBegun += 1;
if (hasBot) t.gamesWithBot += 1;
if (lines[0].createdAt < firstStart) firstStart = lines[0].createdAt;
for (const [k, v] of Object.entries(this.game.tallyBegun?.(cleanOptions(this.game, lines[0].options)) ?? {})) t.byGame[k] = (t.byGame[k] ?? 0) + v;
try {
state = this.game.create(Object.fromEntries(seats.map((s) => [s.id, s.name])), line.seed, line.rules, cleanOptions(this.game, lines[0].options), line.rankings);
} catch {
+2
View File
@@ -139,6 +139,8 @@ export interface GameSpec<State, Input> {
nameOf(state: State, seat: SeatId): string;
/** The game's own lines for the hall's tally, counted from a finished game: a label and how many it adds. */
tally?(state: State): Record<string, number>;
/** The game's own lines counted from every game begun, finished or not, by its table's options (games begun on each variant). */
tallyBegun?(options: TableOptions): Record<string, number>;
}
/**