fertig-classic-games/tools/verifyPipePuzzle.js

94 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Headless verification for Pipe Puzzle.
// node tools/verifyPipePuzzle.js
// Exits non-zero on any failure.
//
// 1. Fixture tests: hand-built boards, rotation algebra, win check.
// 2. Generation invariant sweep: for many random puzzles at every
// difficulty, the generated path is a valid Hamiltonian path and the
// solution board is solved while the scrambled board is not.
import {
N, E, S, W, DIRS, OPP, DELTA,
rotateSockets, generatePuzzle, isSolved, wetCells,
boardHasLeak, tileHasLeak, randomHamiltonianPath, gridAdjacent,
cellRC, DIFFICULTIES,
} from '../src/games/pipepuzzle/PipePuzzleLogic.js';
let failures = 0;
function check(name, cond, detail = '') {
if (cond) { console.log(` ok ${name}`); }
else { failures += 1; console.error(`FAIL ${name}${detail ? `${detail}` : ''}`); }
}
// ── 1. Fixtures ─────────────────────────────────────────────────────────────
console.log('\n— Socket algebra —');
check('N/E/S/W flags distinct', new Set([N, E, S, W]).size === 4);
check('OPP is an involution', DIRS.every((d) => OPP[OPP[d]] === d));
check('rotateSockets 4 steps = identity', [1, 2, 4, 8, 3, 5, 6, 10, 12, 9, 15].every((s) => rotateSockets(s, 4) === s));
check('rotateSockets N→E→S→W', rotateSockets(N, 1) === E && rotateSockets(N, 2) === S && rotateSockets(N, 3) === W);
check('rotateSockets elbow NE→ES→SW→WN', rotateSockets(N | E, 1) === (E | S) && rotateSockets(N | E, 2) === (S | W) && rotateSockets(N | E, 3) === (W | N));
// A solved 2×2 board (row-major: 0=(0,0) 1=(0,1) / 2=(1,0) 3=(1,1)):
// source cell0 → cell1 → cell3 → drain cell2.
// Sockets (solution): cell0 = E (source, 1 socket), cell1 = W|S, cell2 = E
// (drain, 1 socket), cell3 = N|W.
{
const n = 2;
const sockets = [E, W | S, E, N | W];
const board = { n, sockets, source: 0, drain: 2 };
check('fixture 2×2 solved', isSolved(board) === true);
check('fixture 2×2 wet = all cells', wetCells(sockets, n, 0).size === 4);
check('fixture 2×2 no leaks', boardHasLeak(sockets, n) === false);
// Rotate the source: E → S. Now it points at cell2 (which has only E),
// so the source leaks and the board is unsolved.
board.sockets = [S, W | S, E, N | W];
check('fixture 2×2 after rotation not solved', isSolved(board) === false);
check('fixture 2×2 after rotation has leak', boardHasLeak(board.sockets, n) === true);
}
// A leaked board: single socket pointing at wall
{
const n = 3;
const sockets = [N, 0, 0, 0, 0, 0, 0, 0, 0];
check('wall socket is a leak', tileHasLeak(sockets, n, 0) === true);
}
// ── 2. Generation invariant sweep ───────────────────────────────────────────
console.log('\n— Generation invariants —');
for (const diff of DIFFICULTIES) {
const n = diff.n;
const samples = 40;
let pathValid = 0, solutionSolved = 0, scrambledUnsolved = 0, tileCounts = 0;
for (let s = 0; s < samples; s++) {
const path = randomHamiltonianPath(n);
const total = n * n;
const isPerm = new Set(path).size === total && path.length === total &&
path.every((i) => Number.isInteger(i) && i >= 0 && i < total);
const adjacent = path.slice(0, -1).every((c, i) => gridAdjacent(c, path[i + 1], n));
if (isPerm && adjacent) pathValid++;
const p = generatePuzzle(n);
// Solution board must be solved.
if (isSolved({ n, sockets: p.solution, source: p.source, drain: p.drain })) solutionSolved++;
// Scrambled board must not already be solved.
if (!isSolved({ n, sockets: p.sockets, source: p.source, drain: p.drain })) scrambledUnsolved++;
// Tile socket counts: 1 for source/drain, 2 for the rest.
const countsOk = p.sockets.every((sk, i) => {
const bits = (x) => { let c = 0; while (x) { x &= x - 1; c++; } return c; };
if (i === p.source || i === p.drain) return bits(sk) === 1;
return bits(sk) === 2;
});
if (countsOk) tileCounts++;
}
check(`${diff.key} (${n}×${n}): path is a valid Hamiltonian path (${pathValid}/${samples})`, pathValid === samples);
check(`${diff.key} (${n}×${n}): solution board is solved (${solutionSolved}/${samples})`, solutionSolved === samples);
check(`${diff.key} (${n}×${n}): scrambled board starts unsolved (${scrambledUnsolved}/${samples})`, scrambledUnsolved === samples);
check(`${diff.key} (${n}×${n}): socket counts 1/1/2…/2 (${tileCounts}/${samples})`, tileCounts === samples);
}
console.log(failures === 0 ? '\nAll Pipe Puzzle checks passed.' : `\n${failures} check(s) FAILED.`);
process.exit(failures === 0 ? 0 : 1);