The game kit: the shared infrastructure of Wiz-War and Waving Hands as a template

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric Wagoner
2026-09-23 11:00:05 -04:00
co-authored by Claude Fable 5.1
commit 1cd24e3ddd
59 changed files with 7420 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
// One append-only JSONL file per room. A room is its ledger replayed from the
// top: the engine is deterministic given the seed and the recorded inputs, so
// nothing else needs saving.
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import type { SeatId } from '../../src/lib/game/spec';
export type LedgerLine =
| { t: 'room'; id: string; seats: number; createdAt: number }
| { t: 'seat'; id: SeatId; name: string; token: string; bot: boolean }
/** Ledgers from before revisions were recorded resolve under rules 1. */
| { t: 'start'; seed: number; rules?: number }
| { t: 'turn'; at: number; inputs: Record<SeatId, unknown> }
/** Written after the turn that ends the game, for anything that reads ledgers without the engine. */
| { t: 'over'; at: number; winner: SeatId | null; reason: string }
/** A line of table talk from a seated player. */
| { t: 'chat'; at: number; id: SeatId; text: string };
export class Store {
constructor(private dir: string) {
mkdirSync(dir, { recursive: true });
}
private path(roomId: string): string {
return join(this.dir, `${roomId}.jsonl`);
}
append(roomId: string, line: LedgerLine): void {
appendFileSync(this.path(roomId), JSON.stringify(line) + '\n');
}
read(roomId: string): LedgerLine[] {
const path = this.path(roomId);
if (!existsSync(path)) return [];
return readFileSync(path, 'utf8')
.split('\n')
.filter((l) => l.trim())
.map((l) => JSON.parse(l) as LedgerLine);
}
roomIds(): string[] {
return readdirSync(this.dir)
.filter((f) => f.endsWith('.jsonl'))
.map((f) => f.slice(0, -'.jsonl'.length));
}
}