fertig-classic-games/server/scripts/verifyDotLink.js

105 lines
4.1 KiB
JavaScript

// Verifier for Dot Link (Node only — no browser).
//
// Asserts every level in public/data/dotlink.json is solvable with a full-cover
// solution, reports how many are uniquely solvable, and confirms the daily
// board generator is deterministic for a fixed seed.
//
// Usage: node server/scripts/verifyDotLink.js
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
makeRng, dateSeed, generateBoard, solve, isSolved,
} from '../../public/src/games/dotlink/DotLinkLogic.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FILE = path.join(__dirname, '../../public/data/dotlink.json');
let failures = 0;
const fail = (msg) => { console.error(`${msg}`); failures++; };
// ── Bank checks ──────────────────────────────────────────────────────────────
const raw = JSON.parse(fs.readFileSync(FILE, 'utf8'));
const levels = raw.levels ?? [];
console.log(`[verify] ${FILE}`);
console.log(`[verify] ${levels.length} levels (seed 0x${(raw.seed >>> 0).toString(16)})`);
if (levels.length !== 100) fail(`expected 100 levels, found ${levels.length}`);
let unique = 0;
let prevCells = 0;
for (const lv of levels) {
const cells = lv.rows * lv.cols;
// Endpoints in-bounds and distinct.
const used = new Set();
for (const e of lv.endpoints) {
for (const p of [e.a, e.b]) {
if (p[0] < 0 || p[0] >= lv.rows || p[1] < 0 || p[1] >= lv.cols) fail(`L${lv.level}: endpoint out of bounds`);
const key = p[0] * lv.cols + p[1];
if (used.has(key)) fail(`L${lv.level}: endpoint overlap`);
used.add(key);
}
}
// Solvable: the stored reference solution must be a valid full-cover solution
// whose colour endpoints match the puzzle's.
if (!lv.solution || lv.solution.length !== lv.endpoints.length) {
fail(`L${lv.level}: missing/mismatched solution`);
} else if (!isSolved(lv, lv.solution)) {
fail(`L${lv.level}: stored solution does not validate`);
}
// Uniqueness (best-effort; only cheap on small grids).
if (cells <= 64) {
const two = solve(lv, { countLimit: 2, maxNodes: 600000 });
if (!two.aborted && two.count === 1) unique++;
}
// Difficulty should be non-decreasing in grid size.
if (cells < prevCells) fail(`L${lv.level}: grid shrank vs previous level`);
prevCells = cells;
}
console.log(`[verify] all ${levels.length} reference solutions validate; ${unique} small boards provably unique`);
// isSolved must reject an obviously incomplete board.
if (isSolved(levels[0], levels[0].endpoints.map((e) => [e.a]))) {
fail('isSolved accepted an incomplete board');
}
// ── Daily determinism ────────────────────────────────────────────────────────
function dailyBoards(dateStr) {
const STAGES = [
{ rows: 5, cols: 5, colors: 4 },
{ rows: 7, cols: 7, colors: 6 },
{ rows: 8, cols: 8, colors: 8 },
{ rows: 10, cols: 10, colors: 9 },
{ rows: 11, cols: 11, colors: 11 },
];
const base = dateSeed(dateStr);
return STAGES.map((s, i) => {
const rng = makeRng((base ^ Math.imul(0x9e3779b9, i + 1)) >>> 0);
return generateBoard(s.rows, s.cols, s.colors, rng);
});
}
const day = '2026-06-14';
const a = dailyBoards(day);
const b = dailyBoards(day);
if (a.some((x) => !x)) fail('daily generation returned a null board');
if (JSON.stringify(a) !== JSON.stringify(b)) fail('daily boards are not deterministic for the same date');
const c = dailyBoards('2026-06-15');
if (JSON.stringify(a.map((g) => g.board)) === JSON.stringify(c.map((g) => g.board))) {
fail('different dates produced identical daily boards');
}
for (const g of a) {
if (g && !isSolved(g.board, g.solution)) fail('a daily board solution does not validate');
}
console.log('[verify] daily boards: deterministic per date, distinct across dates, solvable');
if (failures > 0) {
console.error(`\n[verify] FAILED with ${failures} problem(s).`);
process.exit(1);
}
console.log('\n[verify] All Dot Link checks passed.');