import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {graph,sideBetween,directionAt,violations,solve,canAppend,doorOpen,doorLatched} from '../rules.mjs';
const pack=JSON.parse(fs.readFileSync(new URL('../levels.json',import.meta.url)));
test('a large neighbor above the channel uses its shared upper side rather than its left coordinate',()=>{const l={geometry:'blocks',features:['big','direction'],cells:[{id:'above',x:0,y:0,w:2,h:1},{id:'channel',x:1,y:1,w:1,h:1,direction:'du'},{id:'below',x:1,y:2,w:1,h:1}]};assert.equal(sideBetween(l,'channel','above'),'u');assert.equal(directionAt(l,['above','channel','below'],1),'du');assert.equal(violations(l,['above','channel','below']).length,0);});
test('all rule combinations use shared solver and append logic',()=>{for(const l of pack.levels){const p=solve(l)[0];for(let i=0;i
{const d={edge:['a','b'],initialOpen:false};assert.equal(doorOpen(d,[]),false);assert.equal(doorOpen(d,['a']),true);assert.equal(doorOpen(d,['a','b','c','d']),true);assert.equal(doorLatched(d,['a','b']),true);assert.equal(doorLatched(d,['a']),false);});
// Independent exhaustive adjacency+rule enumeration for the smallest board of each mechanism family.
function enumerate(l){const g=graph(l),results=[],p=[];function walk(id){p.push(id);if(p.length===l.cells.length){if(!violations(l,p).length)results.push([...p]);}else for(const j of g.adj[g.ids.get(id)]){const n=l.cells[j].id;if(!p.includes(n))walk(n);}p.pop();}for(const c of l.cells)walk(c.id);return results;}
test('small boards exhaustively enumerate all starts without pruning',()=>{const candidates=pack.levels.filter(l=>l.cells.length<=8);for(const l of candidates){const expected=enumerate(l).map(p=>p.join()).sort();const actual=solve(l,{limit:Infinity}).map(p=>p.join()).sort();assert.deepEqual(actual,expected,'level '+l.order);}});