From d97d7092125078cc7619543389b7771b6db4dec5 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Tue, 29 Sep 2026 12:20:03 -0400 Subject: [PATCH] A game may count its own lines from every game begun, not only those finished Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- CONVENTIONS.md | 7 +++++-- template/server/src/tally.test.ts | 33 +++++++++++++++++++++++++++++++ template/server/src/tally.ts | 4 +++- template/src/lib/game/spec.ts | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 template/server/src/tally.test.ts diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 0beb5e3..50393d8 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -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). diff --git a/template/server/src/tally.test.ts b/template/server/src/tally.test.ts new file mode 100644 index 0000000..afbca1b --- /dev/null +++ b/template/server/src/tally.test.ts @@ -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 = { ...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 }); + }); +}); diff --git a/template/server/src/tally.ts b/template/server/src/tally.ts index b7217d3..88a58f6 100644 --- a/template/server/src/tally.ts +++ b/template/server/src/tally.ts @@ -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 { 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 { diff --git a/template/src/lib/game/spec.ts b/template/src/lib/game/spec.ts index 0938f7c..cde21fe 100644 --- a/template/src/lib/game/spec.ts +++ b/template/src/lib/game/spec.ts @@ -139,6 +139,8 @@ export interface GameSpec { 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; + /** 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; } /**