fertig-classic-games/tools/verifyTents.js

179 lines
7.6 KiB
JavaScript
Raw Permalink 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.

// Verifier for Tents & Trees (Node only — no browser).
//
// 1. Unit-tests the solver against hand-built boards (unique / multi / no
// solution, and the no-touch rule).
// 2. Unit-tests the play-state helpers (toggle, diagnose, solve, answer).
// 3. Generation soak: produces puzzles for every difficulty and re-verifies
// each one independently (validity + uniqueness).
//
// Usage: node tools/verifyTents.js
import {
DIFFICULTIES, DIFFICULTY_ORDER, countSolutions, generatePuzzle,
newGame, toggleTent, diagnose, isSolved, solutionTents, keyOf,
} from '../src/games/tents/TentsLogic.js';
let passes = 0;
let failures = 0;
function check(name, cond, detail = '') {
if (cond) { passes++; console.log(` ok ${name}`); }
else { failures++; console.error(`FAIL ${name}${detail ? `${detail}` : ''}`); }
}
// ── Solver unit tests ────────────────────────────────────────────────────────
console.log('[verify] solver');
// Two trees far apart, each with a single candidate: exactly one solution.
{
// 4×4: trees at (1,1) and (2,2) would share diagonal cells; use (0,0) & (3,3).
const trees = [[0, 0], [3, 3]];
const sols = countSolutions(trees, 4, { limit: 5 });
// (0,0)'s candidates: (1,0),(0,1); (3,3)'s: (2,3),(3,2). None touch → 4 solutions.
check('disjoint trees → 4 solutions', sols.length === 4, `got ${sols.length}`);
}
{
// 4×4: trees at (1,1) and (2,2) — diagonal neighbours. Their candidate sets
// overlap in touching cells; tent for one blocks the other's options.
const trees = [[1, 1], [2, 2]];
const sols = countSolutions(trees, 4, { limit: 5 });
// Each tree's candidates: (0,1),(1,0),(1,2) and (2,1),(3,2),(2,3).
// Valid pairs must not touch (8-way): (0,1)&(2,3)? dist (2,2) ok → no touch.
// Count them all: any combo where neither touches the other.
check('diagonal trees → 0 or more, each valid', sols.length >= 0, `got ${sols.length}`);
for (const sol of sols) {
const [a, b] = sol;
const touch = Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1])) <= 1;
check(`solution ${JSON.stringify(sol)} tents do not touch`, !touch);
}
}
{
// Tree in a corner with a tree right beside it: (0,0) & (0,1).
// (0,0)'s tent: (1,0) or (0,1)=tree → only (1,0). (0,1)'s tent: (0,0) tree,
// (0,2), (1,1). (1,0) touches (1,1) and (0,2)? (1,0)-(0,2): Δ(1,2) no touch.
// (1,0)-(1,1): touch. So (0,1)'s tent must be (0,2). One solution.
const sols = countSolutions([[0, 0], [0, 1]], 4, { limit: 5 });
check('corner pair → exactly 1 solution', sols.length === 1, `got ${sols.length}`);
check('corner pair solution', JSON.stringify(sols[0]) === JSON.stringify([[1, 0], [0, 2]]),
JSON.stringify(sols[0]));
}
{
// A tree walled in by trees (no empty orthogonal neighbour) → no solutions.
const trees = [[1, 1], [0, 1], [2, 1], [1, 0]];
const sols = countSolutions(trees, 4, { limit: 5 });
check('walled-in tree → no solutions', sols.length === 0, `got ${sols.length}`);
}
{
// Three in a row: trees (1,1),(2,1),(3,1) on 5×5.
// (1,1) tents: (0,1),(1,0),(1,2). (2,1): (2,0),(2,2). (3,1): (3,0),(3,2),(4,1).
// Must be non-touching. (2,1)'s only options (2,0)/(2,2) touch (1,0)/(1,2)
// diagonally and (3,0)/(3,2) diagonally → whichever chosen blocks both
// neighbours' matching side. Check solver finds the true count.
const sols = countSolutions([[1, 1], [2, 1], [3, 1]], 5, { limit: 10 });
check('three-in-row solvable', sols.length > 0, `got ${sols.length}`);
for (const sol of sols) {
let ok = true;
for (let i = 0; i < sol.length && ok; i++)
for (let j = i + 1; j < sol.length; j++)
if (Math.max(Math.abs(sol[i][0] - sol[j][0]), Math.abs(sol[i][1] - sol[j][1])) <= 1) ok = false;
check(`3-in-row solution ${JSON.stringify(sol)} non-touching`, ok);
}
}
// ── Play-state unit tests ────────────────────────────────────────────────────
console.log('[verify] play state');
{
// Fixed 4×4 puzzle with a known unique solution.
const trees = [[0, 0], [0, 1]];
const sols = countSolutions(trees, 4, { limit: 5 });
check('fixture has unique solution', sols.length === 1, `got ${sols.length}`);
const puzzle = {
difficulty: 'test', size: 4, trees,
rowCounts: [0, 0, 0, 0].map((_, r) => sols[0].filter(([, rr]) => rr === r).length),
colCounts: [0, 0, 0, 0].map((_, c) => sols[0].filter(([cc]) => cc === c).length),
solution: sols[0],
};
const g = newGame(puzzle);
check('clicking a tree is a no-op', toggleTent(g, 0, 0).changed === false);
check('place then remove round-trips',
toggleTent(g, 1, 0).placed === true && toggleTent(g, 1, 0).placed === false);
// Wrong tent (not beside any tree).
toggleTent(g, 3, 3);
let d = diagnose(g);
check('lonely tent flagged', d.badTents.has(keyOf(3, 3)));
// Two tents touching.
toggleTent(g, 1, 0);
toggleTent(g, 1, 1);
d = diagnose(g);
check('touching tents flagged', d.badTents.has(keyOf(1, 0)) && d.badTents.has(keyOf(1, 1)));
// Over-count a row: row 0 count is 1; add a second tent in row 0.
g.tents.add(keyOf(3, 0));
d = diagnose(g);
check('overfull row flagged', d.badRows.has(0));
check('not solved while broken', isSolved(g) === false);
// Solve it exactly.
g.tents.clear();
for (const [c, r] of puzzle.solution) g.tents.add(keyOf(c, r));
d = diagnose(g);
check('solution has no violations',
d.badTents.size + d.badTrees.size + d.badRows.size + d.badCols.size === 0);
check('solution counts as solved', isSolved(g) === true);
// Two tents on one tree → bad tree.
g.tents.add(keyOf(1, 1));
d = diagnose(g);
check('double-claimed tree flagged', d.badTrees.has(keyOf(0, 1)));
check('no longer solved', isSolved(g) === false);
}
// ── Generation soak ──────────────────────────────────────────────────────────
console.log('[verify] generation');
for (const key of DIFFICULTY_ORDER) {
const def = DIFFICULTIES[key];
const t0 = Date.now();
let puzzles = 0;
for (let i = 0; i < 12; i++) {
const p = generatePuzzle(key);
puzzles++;
// Independent re-verification.
// Independent re-verification: (trees + edge counts) must admit exactly
// one solution, and it must be the shipped one.
const sols = countSolutions(p.trees, p.size, { limit: 2, rowCounts: p.rowCounts, colCounts: p.colCounts });
const okUnique = sols.length === 1 && JSON.stringify(sols[0]) === JSON.stringify(p.solution);
check(`${key}: puzzle ${i} unique & matches solution`, okUnique,
`sols=${sols.length}`);
const d = (function () {
const g = newGame(p);
for (const [c, r] of p.solution) g.tents.add(keyOf(c, r));
return { g, d: diagnose(g) };
})();
check(`${key}: puzzle ${i} solution is valid`,
d.d.badTents.size + d.d.badTrees.size + d.d.badRows.size + d.d.badCols.size === 0
&& isSolved(d.g));
check(`${key}: puzzle ${i} has ${def.trees} trees`, p.trees.length === def.trees,
`got ${p.trees.length}`);
check(`${key}: puzzle ${i} counts sum to tree count`,
p.rowCounts.reduce((a, b) => a + b, 0) === p.trees.length
&& p.colCounts.reduce((a, b) => a + b, 0) === p.trees.length);
}
const ms = Date.now() - t0;
console.log(` · ${key}: ${puzzles} puzzles in ${ms} ms (${(ms / 12).toFixed(1)} ms avg)`);
}
console.log(`\n[verify] ${passes} passed, ${failures} failed`);
process.exit(failures ? 1 : 0);