Gallery talk, the keeper's bell, rematches, seats that travel by phrase, and seats doubted before dropped

Brought over from the game kit, where they were ported from Wiz-War. The
host may let the Peanut Gallery talk; a watcher signs a name no seat
holds. A seated wizard may challenge Kestrel, the keeper: a seat is held,
the call rings Sentry once per table, and the lobby says what hour it is
where the keeper lives. A finished table may call for a rematch with the
same company and bots, seats held. Transfer seat mints four words good
for ten minutes and one use; the hall claims them. The hall forgets a
held seat only on the second refusal in a row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric WagonerandClaude Fable 5.1 committed 2026-09-23 12:27:21 -04:00
1 parent 5b169d7131
commit e5c240d970
11 files changed
+576 -32

No files matched your search

+10 -3
View File
@@ -7,7 +7,8 @@ import { join } from 'node:path';
import type { TurnInput, WizardId } from '../../src/lib/game/state';
export type LedgerLine =
| { t: 'room'; id: string; seats: number; createdAt: number }
/** `rematchOf` and `expected` mark a table opened for a rematch: whose it follows, and whose seats are held. */
| { t: 'room'; id: string; seats: number; createdAt: number; rematchOf?: string; expected?: string[] }
/** A seat holds only the hash of its token; the token itself goes once to the browser that earned it. Ledgers written before hashing carry `token` and are read once more. */
| { t: 'seat'; id: WizardId; name: string; tokenHash?: string; token?: string; bot: boolean }
/** One seat's move for the turn in progress, so a restart keeps it. The turn line that follows carries every input again. */
@@ -19,8 +20,14 @@ export type LedgerLine =
| { t: 'turn'; at: number; inputs: Record<WizardId, TurnInput> }
/** Written after the turn that ends the duel, for anything that reads ledgers without the engine. */
| { t: 'over'; at: number; winner: WizardId | null; reason: string }
/** A line of table talk from a seated wizard. */
| { t: 'chat'; at: number; id: WizardId; text: string };
/** A line of table talk: from a seat, or, with `name`, from the Peanut Gallery (id SPECTATOR). */
| { t: 'chat'; at: number; id: WizardId; text: string; name?: string }
/** The host let the gallery talk, or hushed it. */
| { t: 'galleryTalk'; at: number; on: boolean; by: WizardId }
/** A seat is held for the keeper of the site, called to the table. */
| { t: 'challenge'; at: number; by: WizardId; keeper: string }
/** A finished table called for a rematch: where it went, and who called. */
| { t: 'rematch'; at: number; to: string; by: WizardId };
export class Store {
constructor(private dir: string) {