Files

31 lines
5.2 KiB
JavaScript

import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm';import {definitions} from '../tools/definitions.mjs';import {solve,graph} from '../tools/engine-template.mjs';
function boot(mode,width=340){const draws=[],elements=new Map();let context;
class Element{constructor(id=''){this.id=id;this.children=[];this.handlers={};this.hidden=false;this.disabled=false;this.value='';this.textContent='';this.className='';}replaceChildren(){this.children=[];}append(e){this.children.push(e);}addEventListener(n,f){this.handlers[n]=f;}setPointerCapture(){}getBoundingClientRect(){return {left:0,top:0,width,height:420};}click(){this.onclick?.();}}
const ctx={globalAlpha:1,lineWidth:1,font:'',fillStyle:'',strokeStyle:'',textAlign:'center',textBaseline:'middle',setTransform(){},clearRect(){draws.length=0;},beginPath(){this.p=[];},moveTo(x,y){this.p.push([x,y]);},lineTo(x,y){this.p.push([x,y]);},closePath(){},fill(){draws.push({kind:'fill',points:this.p,color:this.fillStyle});},stroke(){draws.push({kind:'stroke',points:this.p,color:this.strokeStyle});},fillText(s,x,y){draws.push({kind:'text',text:s,x,y,font:this.font});},save(){},restore(){},translate(){},rotate(){},setLineDash(){}};
function get(id){if(!elements.has(id))elements.set(id,new Element(id));return elements.get(id);}
const canvas=get('board');canvas.clientWidth=width;canvas.clientHeight=420;canvas.getContext=()=>ctx;
const document={getElementById:get,createElement:()=>new Element()},window={devicePixelRatio:1,addEventListener(){}};
context=vm.createContext({document,window,localStorage:{getItem:()=>null,setItem(){}},performance:{now:()=>100},setTimeout,URL,Blob,console});
const html=fs.readFileSync(new URL('../modes/'+mode+'/play.html',import.meta.url),'utf8');const script=html.match(/<script>([\s\S]*)<\/script>/)[1];new vm.Script(script).runInContext(context);
const snapshot=()=>JSON.parse(JSON.stringify(window.variantSnapshot()));
function press(id){const p=snapshot().centers[id];canvas.handlers.pointerdown({preventDefault(){},pointerId:1,clientX:p[0],clientY:p[1]});canvas.handlers.pointerup({});}
return {get,snapshot,press,canvas,draws,width};
}
for(const def of definitions)test('embedded page lifecycle and all 8 levels '+def.id,()=>{const ui=boot(def.id);const pack=JSON.parse(fs.readFileSync(new URL('../modes/'+def.id+'/levels.json',import.meta.url),'utf8'));
for(let i=0;i<8;i++){ui.get('difficulties').children[Math.floor(i/2)].click();ui.get('levels').children[i%2].click();const l=pack.levels[i],p=solve(l)[0];assert.equal(ui.snapshot().level,l.id);assert.equal(ui.get('levels').children.length,2);assert.equal(ui.get('difficulties').children.length,4);
const before=ui.snapshot();for(const center of Object.values(before.centers)){assert.ok(center[0]>=0&&center[0]<=ui.width);assert.ok(center[1]>=0&&center[1]<=420);}assert.ok(before.unit>=20);
ui.get('hint').click();assert.match(ui.get('status').textContent,/黄色/);
ui.press(p[0]);ui.press(p[1]);assert.deepEqual(ui.snapshot().path,p.slice(0,2));ui.press(p[0]);assert.deepEqual(ui.snapshot().path,p.slice(0,1));ui.get('undo').click();assert.deepEqual(ui.snapshot().path,[]);
for(const id of p)ui.press(id);assert.equal(ui.snapshot().cleared,true);assert.equal(ui.get('remaining').textContent,'完成 ✓');
ui.get('undo').click();assert.equal(ui.snapshot().cleared,false);assert.equal(ui.snapshot().path.length,p.length-1);ui.press(p.at(-1));assert.equal(ui.snapshot().cleared,true);
ui.get('restart').click();assert.deepEqual(ui.snapshot().path,[]);assert.equal(ui.snapshot().cleared,false);assert.ok(ui.snapshot().openedGates.every(v=>!v));
if(l.geometry==='blocks')assert.ok(l.cells.some(c=>c.w>1));
}
});
test('fast drag samples every crossed cell, release preserves prefix',()=>{const ui=boot('relative'),p=solve(JSON.parse(fs.readFileSync(new URL('../modes/relative/levels.json',import.meta.url),'utf8')).levels[0])[0];const start=ui.snapshot().centers[p[0]],next=ui.snapshot().centers[p[1]];ui.canvas.handlers.pointerdown({preventDefault(){},pointerId:1,clientX:start[0],clientY:start[1]});ui.canvas.handlers.pointermove({preventDefault(){},clientX:next[0],clientY:next[1]});ui.canvas.handlers.pointerup({});assert.deepEqual(ui.snapshot().path,p.slice(0,2));});
test('real UI ignores closed door, opens after switch and restores on undo',()=>{const pack=JSON.parse(fs.readFileSync(new URL('../modes/gates/levels.json',import.meta.url),'utf8'));const ui=boot('gates');const l=pack.levels[0],p=solve(l)[0],gate=l.gates[0];for(const id of p)ui.press(id);assert.ok(ui.snapshot().openedGates[0]);ui.get('restart').click();assert.equal(ui.snapshot().openedGates[0],false);
// Follow a geometrically valid prefix reaching one gate side without its switch.
const g=graph(l),prefixes=[];function find(ids){if(gate.edge.includes(ids.at(-1))&&!ids.includes(gate.switchCell)){prefixes.push(ids);return;}for(const j of g.adj[g.ids.get(ids.at(-1))]){const id=l.cells[j].id;if(!ids.includes(id)&&id!==gate.switchCell)find([...ids,id]);}}
find([p[0]]);if(prefixes.length){for(const id of prefixes[0])ui.press(id);const before=ui.snapshot().path;const other=gate.edge.find(id=>id!==before.at(-1));ui.press(other);assert.deepEqual(ui.snapshot().path,before);}
});