// 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: turn count, each wizard's health, 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 { resolveTurn } from '../src/lib/game/resolve'; import { createGame, type GameState, type TurnInput, type WizardId } from '../src/lib/game/state'; interface Seat { id: WizardId; 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: GameState | null = null; let turns = 0; try { for (const line of lines) { if (line.t === 'seat') seats.push(line); else if (line.t === 'start') state = createGame(Object.fromEntries(seats.map((s) => [s.id, s.name])), line.seed); else if (line.t === 'turn' && state) { state = resolveTurn(state, line.inputs as Record); turns += 1; } } } catch (e) { console.log(`${code}: REFUSED at turn ${turns + 1}: ${e instanceof Error ? e.message : String(e)}`); failures += 1; continue; } let verdict = state ? `${turns} turns, ${state.over ? '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 view = (await res.json()) as { state: GameState | null }; const theirs = view.state; const same = !!theirs && theirs.turn === state.turn && theirs.seats.every((id) => theirs.wizards[id].hp === state!.wizards[id].hp) && (theirs.over?.winner ?? null) === (state.over?.winner ?? null); if (!same) { failures += 1; verdict += `, DIFFERS from the server (server turn ${theirs?.turn}, local turn ${state.turn})`; } 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();