纠正仓库内容为衍生项目,标明 campaign-200-r2 最新版
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm';import {solve,graph,canAppend,violations} from '../rules.mjs';
|
||||
const pack=JSON.parse(fs.readFileSync(new URL('../levels.json',import.meta.url),'utf8'));
|
||||
function boot(width=340,storage=new Map(),height=420,withAudio=false){const map=new Map(),draws=[],downloads=[];class Element{constructor(id=''){this.id=id;this.children=[];this.handlers={};this.value='';this.style={};this.className='';this.textContent='';this.hidden=false;this.disabled=false;}replaceChildren(){this.children=[];}append(...e){this.children.push(...e);}addEventListener(k,f){this.handlers[k]=f;}setPointerCapture(){}getBoundingClientRect(){return {left:0,top:0,width,height};}click(){this.onclick?.();if(this.download)downloads.push(this.download);}}
|
||||
const get=id=>{if(!map.has(id))map.set(id,new Element(id));return map.get(id);};const ctx={setTransform(){},clearRect(){draws.length=0;},beginPath(){this.p=[];},moveTo(x,y){this.p.push([x,y]);},lineTo(x,y){this.p.push([x,y]);},quadraticCurveTo(cx,cy,x,y){this.p.push([x,y]);},closePath(){},fill(){draws.push({kind:'fill',colour:this.fillStyle,points:this.p});},stroke(){draws.push({kind:'stroke',colour:this.strokeStyle,points:this.p});},fillText(s,x,y){draws.push({kind:'text',s,x,y});},save(){},restore(){},translate(){},rotate(){},setLineDash(){}};const canvas=get('board');canvas.clientWidth=width;canvas.clientHeight=height;canvas.getContext=()=>ctx;get('screen-play').className='screen hidden';get('screen-select').className='screen';const window={devicePixelRatio:1,handlers:{},addEventListener(k,f){this.handlers[k]=f;}};if(withAudio){let clock=0;window.AudioContext=class{get currentTime(){return clock+=.25;}constructor(){this.state='running';this.destination={};}createOscillator(){return {frequency:{setValueAtTime(){}},connect(){},disconnect(){},start(){},stop(){}};}createGain(){return {gain:{setValueAtTime(){},linearRampToValueAtTime(){},exponentialRampToValueAtTime(){}},connect(){},disconnect(){}};}};}const html=fs.readFileSync(new URL('../index.html',import.meta.url),'utf8');const script=html.match(/<script>([\s\S]*)<\/script>/)[1];const context=vm.createContext({document:{getElementById:get,createElement:()=>new Element()},window,localStorage:{getItem:k=>storage.get(k)||null,setItem:(k,v)=>storage.set(k,v)},performance:{now:()=>1000},console,Blob,URL:{createObjectURL:()=> 'blob:test',revokeObjectURL(){}},setTimeout:f=>f(),clearTimeout(){}});new vm.Script(script).runInContext(context);
|
||||
const snapshot=()=>JSON.parse(JSON.stringify(window.variantSnapshot()));const press=id=>{const [x,y]=snapshot().centers[id];canvas.handlers.pointerdown({preventDefault(){},pointerId:1,clientX:x,clientY:y});canvas.handlers.pointerup({});};const select=n=>{const id=pack.levels[n-1].id;for(const section of get('pack-list').children){const b=section.children[1].children.find(b=>b.levelId===id);if(b){b.click();return;}}throw Error('Missing level '+id);};return {get,draws,canvas,window,snapshot,press,select,storage,downloads};}
|
||||
|
||||
|
||||
test('all 200 revised levels run at narrow phone size with undo/restart/hint',()=>{const ui=boot(296,new Map(),390);assert.equal(ui.get('pack-list').children.length,20);for(const l of pack.levels){ui.select(l.order);ui.get('close-rules').click();const a=solve(l)[0];for(const [x,y]of Object.values(ui.snapshot().centers)){assert.ok(x>=0&&x<=296);assert.ok(y>=0&&y<=390);}ui.press(a[0]);ui.press(a[1]);ui.press(a[0]);assert.deepEqual(ui.snapshot().path,a.slice(0,1));ui.get('restart').click();ui.get('hint').click();for(const id of a)ui.press(id);assert.equal(ui.snapshot().cleared,true,'level '+l.order);ui.get('undo').click();assert.equal(ui.snapshot().cleared,false);ui.press(a.at(-1));assert.equal(ui.snapshot().cleared,true);assert.equal(ui.get('next').hidden,l.order===200);}});
|
||||
test('no colour shapes, no rule chips, no direction-parity overlaps',()=>{const ui=boot();for(const l of pack.levels){assert.ok(!l.cells.some(c=>c.direction&&c.parity));if(!l.features.includes('alternation'))continue;ui.select(l.order);assert.equal(ui.get('constraints').children.length,0);assert.ok(!ui.draws.some(d=>d.kind==='text'&&['○','△'].includes(d.s)));assert.ok(ui.draws.some(d=>d.kind==='fill'&&['#64acf3','#ffae55'].includes(d.colour)));}});
|
||||
test('teacher instructions are in a dismissible rules sheet, not the board header',()=>{const ui=boot();ui.select(9);assert.equal(ui.get('rules-sheet').hidden,false);assert.match(ui.get('rule-text').textContent,/横通道/);ui.get('close-rules').click();assert.equal(ui.get('rules-sheet').hidden,true);assert.equal(ui.get('constraints').children.length,0);});
|
||||
test('wide large hex boards stay playable in a short landscape canvas',()=>{const ui=boot(480,new Map(),290);for(const l of pack.levels.filter(l=>l.geometry==='hex'&&!l.tutorial)){ui.select(l.order);for(const [x,y]of Object.values(ui.snapshot().centers)){assert.ok(x>=0&&x<=480);assert.ok(y>=0&&y<=290);}for(const id of solve(l)[0])ui.press(id);assert.equal(ui.snapshot().cleared,true);}});
|
||||
test('new progress is independent of previous versions and sound settings persist',()=>{const memory=new Map(),ui=boot(340,memory);ui.select(17);ui.get('btn-sound').click();for(const id of solve(pack.levels[16])[0])ui.press(id);const reopened=boot(340,memory);assert.equal(reopened.snapshot().order,17);assert.equal(reopened.snapshot().soundEnabled,false);assert.ok(memory.has('variant:campaign-200-r2:v1'));assert.ok(!memory.has('variant:campaign-200:v1'));});
|
||||
Reference in New Issue
Block a user