// Replays every ledger in a directory with THIS checkout's engine and, when // a server address is given, compares the result with what that server is // showing for the room: the round reached and the outcome. A ledger the // engine cannot replay, or replays differently, is reported and fails the // run. Used by deploy/verify-ledgers.sh. // tsx deploy/replay-ledgers.ts [https://host] import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { game } from '../src/lib/game'; import type { SeatId } from '../src/lib/game/spec'; interface Seat { id: SeatId; name: string; token: string; bot: boolean; } const [dir, host] = process.argv.slice(2); if (!dir) { console.error('usage: replay-ledgers.ts [https://host]'); process.exit(2); } async function main(): Promise { const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl')); let failures = 0; for (const file of files) { const code = file.slice(0, -'.jsonl'.length); const lines = readFileSync(join(dir, file), 'utf8') .split('\n') .filter((l) => l.trim()) .map((l) => JSON.parse(l)); const seats: Seat[] = []; let state: ReturnType | null = null; let turns = 0; try { for (const line of lines) { if (line.t === 'seat') seats.push(line); else if (line.t === 'unseat') seats.splice(seats.findIndex((s) => s.id === line.id), 1); else if (line.t === 'start') state = game.create(Object.fromEntries(seats.map((s) => [s.id, s.name])), line.seed, line.rules ?? 1); else if (line.t === 'turn' && state) { state = game.resolve(state, line.inputs); turns += 1; } } } catch (e) { console.log(`${code}: REFUSED at turn ${turns + 1}: ${e instanceof Error ? e.message : String(e)}`); failures += 1; continue; } const outcome = state ? game.over(state) : null; let verdict = state ? `${turns} turns, ${outcome ? 'over' : 'in play'}` : 'not started'; const human = seats.find((s) => !s.bot); if (host && state && human) { const res = await fetch(`${host}/api/rooms/${code}?token=${encodeURIComponent(human.token)}`); if (!res.ok) { verdict += `, server ${res.status}`; } else { const theirs = (await res.json()) as { turn: number | null; over: { winner: SeatId | null } | null }; const same = theirs.turn === game.turn(state) && (theirs.over?.winner ?? null) === (outcome?.winner ?? null); if (!same) { failures += 1; verdict += `, DIFFERS from the server (server round ${theirs.turn}, local round ${game.turn(state)})`; } else verdict += ', matches the server'; } } console.log(`${code}: ${verdict}`); } console.log(`${files.length} ledger${files.length === 1 ? '' : 's'}, ${failures} problem${failures === 1 ? '' : 's'}`); process.exit(failures ? 1 : 0); } void main();