Files
shanhe-puzzle/tests/difficulty.test.cjs
codex-bot 4845826deb
Shanhe source and offline package checks / verify (pull_request) Failing after 5s
feat: sync Shanhe 2.2 source and add remote package verification
2026-11-01 17:13:46 +08:00

57 lines
7.0 KiB
JavaScript

const test=require('node:test'),assert=require('node:assert/strict'),M=require('matter-js');
const C=require('../src/core.js'),{Game,SNAP_DIFFICULTIES}=require('../src/game.js');
const rect=(id,x,w=100)=>({id:String(id),name:'块'+id,center:[x+w/2,50],rings:[[[x,0],[x+w,0],[x+w,100],[x,100]]]});
const level={id:'test',name:'吸附难度',width:1000,height:1000,regions:[rect(0,0),rect(1,100),rect(2,300,10)],edges:[[0,1,0]]};
function game(snap){const g=new Game(level,420,620,4,()=>{},()=>.5,1,snap);g.status='playing';g.queue=[];return g;}
function roughPoses(g,angle,distance){
const pivot={x:100*g.scale,y:50*g.scale},a={ids:[0],angle:.7,tx:80,ty:200},b={ids:[1],angle:.7+angle};
const ap=C.rotate(pivot,a.angle),bp=C.rotate(pivot,b.angle);b.tx=a.tx+ap.x-bp.x+distance;b.ty=a.ty+ap.y-bp.y;return {a,b};
}
test('吸附三档距离和角度逐级收紧,最高难度保留原26/45px与16°',()=>{
assert.equal(SNAP_DIFFICULTIES.length,3);for(let i=0;i<2;i++)for(const key of ['distance','magnet','angle'])assert.ok(SNAP_DIFFICULTIES[i][key]>SNAP_DIFFICULTIES[i+1][key]);
assert.deepEqual(SNAP_DIFFICULTIES[2],{name:'严丝合缝',distance:26,magnet:45,angle:.28,note:'原有难度:更近的距离、更准确的方向。'});
const g=game();assert.equal(g.snapIndex,2);g.destroy();
});
test('简单档在60px和约43°误差仍能吸合,普通和困难不会提前合并',()=>{
for(let i=0;i<3;i++){const g=game(i),p=roughPoses(g,.75,60);const plan=g.snapPlan(p.a,p.b,g.snap.distance);assert.equal(!!plan,i===0);if(plan)assert.ok(Math.abs(plan.distance-60)<1e-8);g.destroy();}
});
test('普通档在38px和约26°误差可合并,困难档仍要求原有方向',()=>{
for(let i=0;i<3;i++){const g=game(i),p=roughPoses(g,.45,38);assert.equal(!!g.snapPlan(p.a,p.b,g.snap.distance),i<2);g.destroy();}
});
test('轻松和普通进一步放宽位置与方向,近邻牵引比困难档更强',()=>{
for(const [index,angle,distance] of [[0,.95,80],[1,.63,50]]){const g=game(index),p=roughPoses(g,angle,distance);assert.ok(g.snapPlan(p.a,p.b,g.snap.distance));g.destroy();}
const forces=[];for(let index=0;index<3;index++){const g=game(index),p=roughPoses(g,0,(g.snap.distance+g.snap.magnet)/2),a=g.createGroup([0],p.a);g.createGroup([1],p.b);g.attractNearby();forces.push(Math.abs(a.body.force.x));g.destroy();}assert.ok(forces[0]>forces[1]&&forces[1]>forces[2]);
});
test('宽松和普通档可真正合并方向接近的邻块,继承自然角度而非自动回正',()=>{
for(const [index,angle,distance] of [[0,.75,60],[1,.45,38]]){const g=game(index),p=roughPoses(g,angle,distance);g.createGroup([0],p.a);g.createGroup([1],p.b);assert.equal(g.tryMerges(),true);assert.equal(g.groups.length,1);assert.equal(g.groups[0].ids.length,2);assert.equal(g.score,100);assert.ok(Math.abs(g.groups[0].body.angle-p.b.angle)<1e-8);g.destroy();}
});
test('宽松档围绕相邻地区判断,移动源地图坐标原点不会改变结果',()=>{
const g=game(0),p=roughPoses(g,.8,65),plan=g.snapPlan(p.a,p.b,g.snap.distance);assert.ok(plan);
const shifted={...level,regions:level.regions.map(r=>({...r,center:[r.center[0]+5000,r.center[1]+7000]}))};
const other=new Game(shifted,420,620,4,()=>{},()=>.5,1,0);other.scale=g.scale;
for(const pose of [p.a,p.b]){const delta=C.rotate({x:5000*g.scale,y:7000*g.scale},pose.angle);pose.tx-=delta.x;pose.ty-=delta.y;}
assert.ok(Math.abs(other.snapPlan(p.a,p.b,68).distance-plan.distance)<1e-8);g.destroy();other.destroy();
});
test('三档都拒绝非邻接、反向及超过各自范围的地块',()=>{
for(let i=0;i<3;i++){const g=game(i);for(const [angle,distance] of [[Math.PI,0],[0,g.snap.magnet+.1]]){const p=roughPoses(g,angle,distance);assert.equal(g.snapPlan(p.a,p.b,g.snap.magnet),null);}const p=roughPoses(g,0,0);p.b.ids=[2];assert.equal(g.snapPlan(p.a,p.b,g.snap.magnet),null);g.destroy();}
});
test('三档远距靠拢施加双向力,不自动旋转或直接拉动,简单档支持100px',()=>{
for(const [index,distance,angle] of [[0,100,.7],[1,65,.4],[2,40,0]]){const g=game(index),p=roughPoses(g,angle,distance),a=g.createGroup([0],p.a),b=g.createGroup([1],p.b),positions=g.groups.map(x=>({...x.body.position}));g.attractNearby();assert.equal(g.magnetPairs,1);assert.deepEqual(g.groups.map(x=>({...x.body.position})),positions);assert.ok(Math.abs(a.body.force.x+b.body.force.x)<1e-10);assert.ok(Math.abs(a.body.force.y+b.body.force.y)<1e-10);assert.equal(a.body.torque,0);assert.equal(b.body.torque,0);g.destroy();}
});
test('宽松档仍检查吸合目标是否被无关地块占用,取消时保留原块与得分',()=>{
for(const index of [0,1]){const g=game(index),pose={tx:80,ty:200,angle:0},a=g.createGroup([0],pose),b=g.createGroup([1],{...pose,tx:35+pose.tx}),c=g.createGroup([2],pose);M.Body.setPosition(c.body,{x:(a.body.bounds.max.x+b.body.bounds.min.x)/2,y:a.body.position.y});const pos={...c.body.position};assert.ok(g.snapPlan(g.pose(a),g.pose(b),g.snap.distance));assert.equal(g.tryMerges(),false);assert.equal(g.groups.length,3);assert.equal(g.score,0);assert.deepEqual(c.body.position,pos);g.destroy();}
});
test('所有新难度配合四档弹性均为随心落子,等待不触发下落',()=>{
for(let snap=0;snap<3;snap++)for(let bounce=0;bounce<4;bounce++){const g=new Game(level,420,620,4,()=>{},()=>.5,bounce,snap);g.start();for(let i=0;i<600;i++)g.tick(100);assert.equal(g.dropped,0);assert.equal(g.pendingView().remaining,null);const serial=g.pending.serial;g.holdPending(serial);g.releasePending(serial);assert.equal(g.dropped,1);g.destroy();}
});
test('吸附难度的独立纪录键可校验,越界和恶意键不保留',()=>{
assert.deepEqual(C.safeRecord({'100000:4:1:s0':25.9,'100000:4:1:s1':50,'100000:4:1:s2':75,'100000:4:1:s3':100,'100000:4:7:s0':200,'__proto__':100,'100000:4:1':300}),{'100000:4:1:s0':25,'100000:4:1:s1':50,'100000:4:1:s2':75,'100000:4:1':300});
});
test('宽松和普通吸附下,湖北完整队列连续晃动不产生深度穿插',()=>{
const fs=require('node:fs'),vm=require('node:vm'),box={window:{}};vm.runInNewContext(fs.readFileSync('src/maps.js','utf8'),box);const hubei=box.window.MAP_DATA.levels.find(l=>l.id==='420000');
for(const index of [0,1]){let seed=19+index;const rng=()=>{seed=(seed*1664525+1013904223)>>>0;return seed/4294967296;},g=new Game(hubei,340,560,4,()=>{},rng,1,index);g.start();
for(let i=0;i<1800&&g.status==='playing';i++){if(g.pending&&i%55===0){const serial=g.pending.serial;g.movePending(30+rng()*280,serial);g.holdPending(serial);g.releasePending(serial);}if(i%13===0&&g.groups.length)g.swipe({x:40+rng()*260,y:260+rng()*270},{x:(rng()-.5)*140,y:(rng()-.7)*140},60);g.tick(1000/60);
if(i%12===0)for(let a=0;a<g.groups.length;a++)for(let b=0;b<a;b++)for(const pa of (g.groups[a].body.parts.length>1?g.groups[a].body.parts.slice(1):g.groups[a].body.parts))for(const pb of (g.groups[b].body.parts.length>1?g.groups[b].body.parts.slice(1):g.groups[b].body.parts)){if(!M.Bounds.overlaps(pa.bounds,pb.bounds))continue;const collision=M.Collision.collides(pa,pb);assert.ok(!collision||collision.depth<=3,'难度 '+index+' 步 '+i+' 深度 '+(collision&&collision.depth));}}
assert.equal(g.dropped,17);g.destroy();}
});