import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs'; import {boot} from './qa/runtime-harness.mjs';import {solve} from './src/upstream/rules.mjs';import {Haptics} from './src/haptics.mjs';import {FEEDBACK} from './src/feedback-params.mjs';import {feedback} from './src/feedback.mjs';import {renderBrightBoard,landingScale,checkpointScale} from './src/bright-classroom/board-renderer.mjs'; const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url))),levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id)); test('all 200 levels: valid numeric, parity and direction arrivals play checkpoint pitch; final arrival only completes',()=>{ const ui=boot();let numeric=0,parity=0,direction=0; for(let i=0;ic.id===id);ui.cell(id);const e=ui.api.snapshot().feedback.events.at(-1),last=step===route.length,node=(cell.clue>1&&cell.clue===step)||cell.parity||cell.direction;assert.equal(e.cell,id);assert.equal(e.kind,last?'complete':node?'checkpoint':'move','level '+(i+1)+' step '+step);assert.equal(ui.played.at(-1),last?'audio/complete.wav':node?'audio/checkpoint-'+step+'.wav':'audio/move-'+step+'.wav');if(node&&!last){assert.equal(e.ribbon,Boolean(cell.clue>1&&cell.clue===step));if(cell.clue>1)numeric++;if(cell.parity)parity++;if(cell.direction)direction++;}}} assert.ok(numeric>0&&parity>0&&direction>0);}); test('drag back and undo button both vibrate lightly; disabling vibration suppresses both',()=>{ const ui=boot(),route=solve(levels[3])[0];ui.select(3);for(const id of route.slice(0,3)){ui.advance(80);ui.cell(id);}ui.advance(61);let n=ui.vibrated.length;ui.cell(route[1]);assert.equal(ui.api.snapshot().path.length,2);assert.equal(ui.vibrated.length,n+1);assert.equal(ui.vibrated.at(-1).type,'light');ui.advance(61);ui.button('撤销');assert.equal(ui.vibrated.length,n+2);assert.equal(ui.vibrated.at(-1).type,'light');ui.button('设置');ui.button('振动:开');ui.button('完成');ui.advance(80);ui.cell(route[1]);n=ui.vibrated.length;ui.advance(80);ui.button('撤销');assert.equal(ui.vibrated.length,n);assert.equal(ui.api.snapshot().music.playing,'play');}); test('haptic cadence is approximately 15 percent denser without changing light or medium grades',()=>{let now=0;const calls=[],h=new Haptics({vibrateShort:o=>calls.push(o.type)},{getItem:()=>null},{now:()=>now});h.play('move');now=60;h.play('undo');assert.deepEqual(calls,['light']);now=61;h.play('undo');assert.deepEqual(calls,['light','light']);h.play('checkpoint');assert.equal(calls.at(-1),'medium');assert.ok(Math.abs(70/FEEDBACK.hapticGapMs-1.15)<.01);}); test('regular and special scale settle at 90ms with a larger special peak',()=>{assert.equal(FEEDBACK.moveMs,90);assert.equal(FEEDBACK.checkpointMs,90);assert.equal(landingScale(45),1.025);assert.equal(checkpointScale(45),1.065);for(const t of [-10,0,90,1000]){assert.equal(landingScale(t),1);assert.equal(checkpointScale(t),1);}}); test('rendered undo target and nonnumeric checkpoint animate immediately without adding ribbons',t=>{ const old=feedback.now;t.after(()=>{feedback.reset();feedback.now=old;});const level=levels[3],route=solve(level)[0],scales=[],ctx=new Proxy({measureText:()=>({width:20}),scale:(x,y)=>scales.push([x,y])},{get:(o,k)=>o[k]??(()=>{}),set:(o,k,v)=>(o[k]=v,true)});feedback.reset();feedback.now=()=>1000;feedback.emit('undo',level,route[1],1,{from:route[0]});renderBrightBoard(ctx,level,route.slice(0,1),375,400,{now:1045});assert.ok(scales.some(([x])=>x===1.025));scales.length=0;feedback.now=()=>1100;feedback.emit('checkpoint',level,route[0],1,{ribbon:false});const scene=renderBrightBoard(ctx,level,route.slice(0,1),375,400,{now:1145});assert.ok(scales.some(([x])=>x===1.065));assert.equal(scene.bursts.length,0);}); test('quick undo cancels removed node rebound, including last-step removal',t=>{const old=feedback.now;t.after(()=>{feedback.reset();feedback.now=old;});const level=levels[3],route=solve(level)[0],scales=[],ctx=new Proxy({measureText:()=>({width:20}),scale:x=>scales.push(x)},{get:(o,k)=>o[k]??(()=>{}),set:(o,k,v)=>(o[k]=v,true)});feedback.reset();feedback.now=()=>1000;feedback.emit('checkpoint',level,route[1],2,{ribbon:false});feedback.now=()=>1010;feedback.emit('undo',level,route[1],1,{from:route[0]});renderBrightBoard(ctx,level,route.slice(0,1),375,400,{now:1045});assert.equal(scales.length,1);assert.ok(scales[0]>1&&scales[0]<=1.025);scales.length=0;feedback.now=()=>1100;feedback.emit('undo',level,route[0],0,{});renderBrightBoard(ctx,level,[],375,400,{now:1145});assert.deepEqual(scales,[1.025]);});