Begin Hnefatafl from the game kit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
// 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 <ledger-dir> [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;
|
||||
bot: boolean;
|
||||
}
|
||||
|
||||
const [dir, host] = process.argv.slice(2);
|
||||
if (!dir) {
|
||||
console.error('usage: replay-ledgers.ts <ledger-dir> [https://host]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
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<typeof game.create> | 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';
|
||||
if (host && state) {
|
||||
// The gallery's view carries the round and the outcome, which is all the comparison needs.
|
||||
const res = await fetch(`${host}/api/rooms/${code}`);
|
||||
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();
|
||||
Reference in New Issue
Block a user