Files
shanhe-puzzle/tests/physics-hotpaths.test.cjs
T
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

29 lines
5.3 KiB
JavaScript

const test=require('node:test'),assert=require('node:assert/strict'),M=require('matter-js'),C=require('../src/core.js'),{Game}=require('../src/game.js');
const rect=(id,x)=>({id:String(id),name:'region'+id,center:[x+50,50],rings:[[[x,0],[x+100,0],[x+100,100],[x,100]]]});
function game(){const concave={id:'0',name:'concave',center:[60,60],rings:[[[0,0],[140,0],[140,35],[35,35],[35,140],[0,140]]]};const g=new Game({id:'fixture',width:1000,height:1000,regions:[concave,rect(1,200),rect(2,400)],edges:[]},420,620,4,()=>{},()=>.5,1,2);g.status='playing';g.queue=[];g.engine.gravity.y=0;return g;}
function penetrating(a,b){for(const x of a.parts.length>1?a.parts.slice(1):[a])for(const y of b.parts.length>1?b.parts.slice(1):[b]){if(!M.Bounds.overlaps(x.bounds,y.bounds))continue;const c=M.Collision.collides(x,y);if(c&&c.depth>2)return true;}return false;}
function pose(body){return {x:body.position.x,y:body.position.y,angle:body.angle};}
function set(body,p){M.Body.setAngle(body,p.angle);M.Body.setPosition(body,{x:p.x,y:p.y});}
test('只读回放SAT与真实Matter凸片碰撞判定一致,涵盖复合凹块和不同插值时刻',()=>{
const g=game(),a=g.createGroup([0],{tx:100,ty:200,angle:0}),b=g.createGroup([1],{tx:30,ty:210,angle:0});let seed=8;const rng=()=>{seed=(seed*1664525+1013904223)>>>0;return seed/4294967296;};let checks=0;
for(let i=0;i<240;i++){const states=[a,b].map(group=>{const before={x:140+rng()*85,y:240+rng()*85,angle:(rng()-.5)*5},after={x:before.x+(rng()-.5)*16,y:before.y+(rng()-.5)*16,angle:before.angle+(rng()-.5)*.05};set(group.body,after);return {g:group,before,after};});
for(const t of [0,.25,.5,.875,1]){states.forEach(s=>set(s.g.body,s.after));const result=g.sampledPairBlocked(states[0],states[1],t);states.forEach(s=>set(s.g.body,{x:s.before.x+(s.after.x-s.before.x)*t,y:s.before.y+(s.after.y-s.before.y)*t,angle:s.before.angle+(s.after.angle-s.before.angle)*t}));assert.equal(result,penetrating(a.body,b.body),'sample '+i+' at '+t);checks++;}
}assert.equal(checks,1200);g.destroy();
});
test('二分试探不改动实际碰撞几何,最终只提交一次姿态且不回放远处自由块',()=>{
const g=game(),a=g.createGroup([1],{tx:20,ty:200,angle:0}),b=g.createGroup([2],{tx:20,ty:200,angle:0}),far=g.createGroup([0],{tx:20,ty:450,angle:0});M.Body.setPosition(a.body,{x:150,y:250});M.Body.setPosition(b.body,{x:220,y:250});g.rememberPoses();M.Body.setPosition(a.body,{x:210,y:250});M.Body.setAngle(a.body,.02);M.Body.translate(far.body,{x:4,y:0});const distant=pose(far.body),angle=M.Body.setAngle,position=M.Body.setPosition,calls=new Map();
M.Body.setAngle=function(body,...args){const c=calls.get(body)||{angle:0,position:0};c.angle++;calls.set(body,c);return angle.call(this,body,...args);};M.Body.setPosition=function(body,...args){const c=calls.get(body)||{angle:0,position:0};c.position++;calls.set(body,c);return position.call(this,body,...args);};
try{g.guardPenetrations();}finally{M.Body.setAngle=angle;M.Body.setPosition=position;}
assert.ok(!penetrating(a.body,b.body));assert.deepEqual(pose(far.body),distant);assert.ok(!calls.has(far.body));for(const c of calls.values()){assert.ok(c.angle<=1);assert.ok(c.position<=1);}g.destroy();
});
test('不变的接触几何复用防穿透结果,旋转和缩放仍重新核对',()=>{
const g=game(),a=g.createGroup([1],{tx:0,ty:200,angle:0}),b=g.createGroup([2],{tx:0,ty:200,angle:0});M.Body.setPosition(a.body,{x:160,y:250});M.Body.setPosition(b.body,{x:180,y:250});const collide=M.Collision.collides;let calls=0;M.Collision.collides=function(...args){calls++;return collide.apply(this,args);};
try{const initial=g.guardPairBlocked(a,b),count=calls;assert.ok(initial);for(let i=0;i<100;i++)assert.equal(g.guardPairBlocked(a,b),initial);assert.equal(calls,count);M.Body.setAngle(a.body,.02);assert.equal(g.guardPairBlocked(a,b),penetrating(a.body,b.body));assert.ok(calls>count);M.Body.translate(a.body,{x:100,y:0});assert.equal(g.guardPairBlocked(a,b),false);M.Body.scale(a.body,4,4);assert.equal(g.guardPairBlocked(a,b),penetrating(a.body,b.body));}finally{M.Collision.collides=collide;g.destroy();}
});
test('邻接结果随组成员和地图边表失效,重复吸附不反复扫描完整边表',()=>{
const g=game();g.level={...g.level,edges:[[0,1,0],[1,2,0]]};const a=[0],b=[1],c=[2],linked=C.linked;let calls=0;C.linked=function(...args){calls++;return linked(...args);};try{for(let i=0;i<200;i++){assert.deepEqual(g.linkedEdge(a,b),[0,1,0]);assert.equal(g.linkedEdge(a,c),null);}assert.equal(calls,2);assert.deepEqual(g.linkedEdge([0,1],c),[1,2,0]);g.level={...g.level,edges:[]};assert.equal(g.linkedEdge(a,b),null);}finally{C.linked=linked;g.destroy();}
});
test('缓存邻接后的吸附结果与原完整扫描一致,三档难度和跨海限制不变',()=>{
for(const difficulty of [0,1,2]){const g=game();g.snapIndex=difficulty;g.snap=require('../src/game.js').SNAP_DIFFICULTIES[difficulty];g.level={...g.level,edges:[[0,1,0],[1,2,1]]};for(let i=0;i<80;i++){const a={ids:[i%2],tx:i*.7,ty:20,angle:i*.01},b={ids:[2],tx:45,ty:40,angle:.22},edge=C.linked(a.ids,b.ids,g.level.edges);let pivot=null;if(edge&&difficulty!==2){const p=g.level.regions[edge[0]].center,q=g.level.regions[edge[1]].center;pivot={x:(p[0]+q[0])*g.scale/2,y:(p[1]+q[1])*g.scale/2};}assert.deepEqual(g.snapPlan(a,b,g.snap.distance),C.canSnap(a,b,g.level.edges,g.snap.distance,g.snap.angle,pivot));}g.destroy();}
});