// 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.');