import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {definitions} from '../tools/definitions.mjs';import {solve,canAppend,violations,hint,assertStructure} from '../tools/engine-template.mjs'; const packs=definitions.map(d=>JSON.parse(fs.readFileSync(new URL('../modes/'+d.id+'/levels.json',import.meta.url),'utf8'))); // Independent tiny-board reference: its own geometry and full-path rule checks. function reference(level){const cells=level.cells,at=new Map(cells.map(c=>[c.id,c]));function occupied(c){if(c.shape)return c.shape.map(([x,y])=>[c.x+x,c.y+y]);let u=[];for(let y=0;y<(c.h||1);y++)for(let x=0;x<(c.w||1);x++)u.push([c.x+x,c.y+y]);return u;} const adjacent=(a,b)=>level.geometry==='hex'?[[1,0],[-1,0],[0,1],[0,-1],[1,-1],[-1,1]].some(([q,r])=>b.q-a.q===q&&b.r-a.r===r):occupied(a).some(([x,y])=>occupied(b).some(([xx,yy])=>Math.abs(xx-x)+Math.abs(yy-y)===1)); const adj=new Map(cells.map(c=>[c.id,cells.filter(b=>b!==c&&adjacent(c,b)).map(c=>c.id)]));const answers=[],p=[]; function accept(){const pos=id=>p.indexOf(id)+1;for(const c of cells)if(c.clue&&pos(c.id)!==c.clue)return false;for(const r of level.relations||[])if(pos(r.to)-pos(r.from)!==r.delta)return false;for(const chain of level.orderChains||[])for(let i=1;i=pos(chain[i]))return false;for(const w of level.windows||[])if(pos(w.cell)w.max)return false; for(const c of cells)if(c.direction){const i=pos(c.id)-1;if(i===0||i===p.length-1)return false;const a=at.get(p[i-1]),b=c,d=at.get(p[i+1]);const straight=(a.x===b.x&&b.x===d.x)||(a.y===b.y&&b.y===d.y);if(straight!==(c.direction==='straight'))return false;} if(level.mode.includes('alternation'))for(let i=1;ii)return false;return true;} function walk(id){p.push(id);if(p.length===cells.length){if(accept())answers.push([...p]);}else for(const next of adj.get(id))if(!p.includes(next))walk(next);p.pop();}for(const c of cells)walk(c.id);return answers; } for(const pack of packs)for(const level of pack.levels.slice(0,2))test('independent exhaustive reference '+pack.mode+' '+level.id,()=>{assert.deepEqual(solve(level,{limit:Infinity}),reference(level));}); test('all 56 author routes playable and undo states recomputed',()=>{let n=0;for(const pack of packs)for(const l of pack.levels){assertStructure(l);const p=solve(l)[0];for(let i=0;i{const l={mode:'relative',geometry:'square',cells:[0,1,2,3,4].map(i=>({id:''+i,x:i,y:0,w:1,h:1})),relations:[{from:'0',to:'4',delta:4}]};assert.equal(violations(l,['0','1','2','3','4']).length,0);l.relations[0].delta=3;assert.ok(violations(l,['0','1','2','3','4']).length);}); test('window inclusive edges, expiration and shape rejection',()=>{const l={mode:'window',geometry:'square',cells:[0,1,2,3,4].map(i=>({id:''+i,x:i,y:0})),windows:[{cell:'2',min:2,max:4}]};assert.equal(violations(l,['0','2']).length,0);assert.equal(violations(l,['0','1','3','2']).length,0);assert.ok(violations(l,['0','1','3','4']).length);l.windows[0].max=2;assert.throws(()=>assertStructure(l));}); test('direction waits for exit, excludes both endpoints',()=>{const l={mode:'direction',geometry:'square',cells:[{id:'a',x:0,y:0},{id:'b',x:1,y:0,direction:'turn'},{id:'c',x:1,y:1}]};assert.equal(violations(l,['a','b']).length,0);assert.equal(violations(l,['a','b','c']).length,0);assert.ok(violations(l,['b']).length);assert.ok(violations(l,['a','c','b']).length);}); test('closed gate cannot open itself on arrival; undo closes it',()=>{const l={mode:'gates',geometry:'square',cells:[{id:'a',x:0,y:0},{id:'b',x:1,y:0},{id:'s',x:0,y:1},{id:'d',x:1,y:1}],gates:[{edge:['a','b'],switchCell:'s'}]};assert.equal(canAppend(l,['a'],'b'),false);assert.equal(canAppend(l,['s','d','a'],'b'),true);assert.equal(canAppend(l,['a'],'b'),false);l.gates[0].switchCell='b';assert.equal(canAppend(l,['a'],'b'),false);}); test('dynamic connectivity retains future door opportunities',()=>{const l=packs.find(p=>p.mode==='gates').levels[6];assert.deepEqual(solve(l,{prune:true}),solve(l,{prune:false}));}); test('alternating classes are hard transitions; whole block counts once',()=>{const l=packs.find(p=>p.mode==='block-alternation').levels[0];const p=solve(l)[0];assert.equal(p.length,l.cells.length);const g=l.cells.find(c=>c.w>1);assert.ok(g);const target=l.cells.find(c=>c.category===g.category&&c.id!==g.id);assert.equal(canAppend(l,[g.id],target.id),false);}); test('reject overlap, cycles and nonexistent edges',()=>{assert.throws(()=>assertStructure({mode:'order',geometry:'square',cells:[{id:'a',x:0,y:0},{id:'b',x:0,y:0},{id:'c',x:1,y:0}]}));const l={mode:'order',geometry:'square',cells:[0,1,2].map(i=>({id:''+i,x:i,y:0})),orderChains:[['0','1'],['1','0']]};assert.throws(()=>assertStructure(l));delete l.orderChains;l.gates=[{edge:['0','2'],switchCell:'1'}];assert.throws(()=>assertStructure(l));});