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