86 lines
23 KiB
JavaScript
86 lines
23 KiB
JavaScript
const test=require('node:test'),assert=require('node:assert/strict'),vm=require('node:vm'),fs=require('node:fs');
|
||
fs.mkdirSync('artifacts',{recursive:true});
|
||
const C=require('../src/core.js'),{Game,SPEEDS}=require('../src/game.js'),M=require('matter-js');
|
||
const box={window:{}};vm.runInNewContext(fs.readFileSync('src/maps.js','utf8'),box);const data=box.window.MAP_DATA;
|
||
const square=(id,x,y)=>({id:String(id),name:'地区'+id,center:[x+50,y+50],rings:[[[x,y],[x+100,y],[x+100,y+100],[x,y+100]]]});
|
||
const level={id:'100',name:'两块',width:200,height:100,regions:[square(0,0,0),square(1,100,0)],edges:[[0,1,0]]};
|
||
function game(l=level,s=0){return new Game(l,420,560,s,()=>{},()=>.5);}
|
||
// Connectivity fixtures start with the complete map inside the tank. Boundary
|
||
// correction deliberately no longer moves other, unrelated pieces in formation.
|
||
function alignWholeMap(g,tx=20,ty=130){g.rescale(Math.min(g.scale,(g.w-tx-20)/(g.level.width*1.06),(g.h-ty-20)/(g.level.height*1.06)));for(let i=0;i<g.level.regions.length;i++)g.createGroup([i],{tx,ty,angle:0});}
|
||
test('相邻但贴错边、方向不一致、非邻接地区均不可合并',()=>{const a={ids:[0],tx:50,ty:100,angle:0};assert.equal(C.canSnap(a,{ids:[1],tx:150,ty:100,angle:0},level.edges,22),null);assert.equal(C.canSnap(a,{ids:[1],tx:50,ty:100,angle:Math.PI/2},level.edges,22),null);assert.equal(C.canSnap(a,{ids:[2],tx:50,ty:100,angle:0},level.edges,22),null);assert.ok(C.canSnap(a,{ids:[1],tx:62,ty:100,angle:0},level.edges,22));});
|
||
test('刚体旋转后按正确方位吸合,并且一局可达通关',()=>{const g=game();g.status='playing';g.queue=[];const pose={tx:300,ty:200,angle:Math.PI/2};g.createGroup([0],pose);g.createGroup([1],{...pose,tx:308});assert.ok(g.tryMerges());assert.equal(g.groups.length,1);assert.equal(g.groups[0].ids.length,2);assert.equal(g.status,'won');assert.equal(g.score,1100);g.destroy();});
|
||
test('跨海独立片可连接,且按跨海倍率计分',()=>{const l={...level,edges:[[0,1,1]]},g=game(l,1);g.status='playing';g.createGroup([0],{tx:10,ty:120,angle:0});g.createGroup([1],{tx:12,ty:120,angle:0});g.tryMerges();assert.equal(g.score,1620);g.destroy();});
|
||
test('出片无重复,暂停冻结计时,四档实际间隔与重力递增',()=>{const g=game();g.start();g.spawn();const clock=g.clock;g.status='paused';g.tick(34);assert.equal(g.clock,clock);g.status='playing';g.nextAt=0;g.tick(16);assert.equal(g.dropped,2);assert.equal(new Set(g.groups.flatMap(x=>x.ids)).size,2);for(let i=1;i<4;i++){assert.ok(SPEEDS[i].interval<SPEEDS[i-1].interval);assert.ok(SPEEDS[i].gravity>SPEEDS[i-1].gravity);}g.destroy();});
|
||
test('左右晃动可反复使用且不扣分',()=>{const g=game();g.start();g.spawn();g.queue=[];g.score=200;for(let i=0;i<20;i++){assert.ok(g.nudge(i%2?-1:1));for(let j=0;j<9;j++)g.tick(16.67);}assert.equal(g.score,200);assert.equal(g.shakeCount,20);g.destroy();});
|
||
test('堆顶有出生保护,落地支撑的堆积稳定堵塞警戒线才判负',()=>{const l={id:'100',name:'真实堆积',width:200,height:300,regions:Array.from({length:4},(_,i)=>square(i,0,0)),edges:[]},g=game(l);g.status='playing';g.queue=[];for(let i=0;i<4;i++){const p=g.createGroup([i],{tx:0,ty:0,angle:0});M.Body.setPosition(p.body,{x:210,y:g.h-(i+.5)*100*g.scale+.05*i});M.Body.setStatic(p.body,true);}for(let i=0;i<100;i++)g.tick(34);assert.equal(g.status,'playing');for(let i=0;i<130;i++)g.tick(34);assert.ok(g.groups.every(p=>p.grounded));assert.equal(g.status,'lost');g.destroy();});
|
||
test('全国每个真实关卡均连通、每片轮廓可生成有限的刚体',()=>{assert.equal(data.levels[0].regions.length,34);assert.equal(data.levels.length,35);let count=0;for(const l of data.levels){const seen=new Set([0]);let changed=true;while(changed){changed=false;for(const e of l.edges){if(seen.has(e[0])!==seen.has(e[1])){seen.add(e[0]);seen.add(e[1]);changed=true;}}}assert.equal(seen.size,l.regions.length,l.name+' must be completable');const g=game(l);for(let i=0;i<l.regions.length;i++){const p=g.createGroup([i],{tx:0,ty:0,angle:0});assert.ok(Number.isFinite(p.body.mass)&&p.body.mass>0,l.name+' '+i);assert.ok(Number.isFinite(p.body.position.x));M.Composite.remove(g.engine.world,p.body);g.groups=[];count++;}g.destroy();}assert.equal(count,529);});
|
||
test('完整的全国地图可全部吸合,海岛无死局',()=>{const g=game(data.levels[0]);g.status='playing';g.queue=[];alignWholeMap(g);for(let i=0;i<40;i++)g.tryMerges();assert.equal(g.status,'won');assert.equal(g.groups.length,1);assert.equal(g.groups[0].ids.length,34);g.destroy();});
|
||
test('窗口缩放保留进度与组合',()=>{const g=game();g.start();g.spawn();g.score=120;g.resize(360,440);assert.equal(g.score,120);assert.equal(g.dropped,1);assert.equal(g.groups.length,1);assert.ok(Number.isFinite(g.groups[0].body.position.x));g.destroy();});
|
||
test('损坏或恶意纪录输入安全降级',()=>{assert.deepEqual(C.safeRecord(null),{});assert.deepEqual(C.safeRecord({'100000:0':123,'x':7,'330100:2':-1}),{'100000:0':123});});
|
||
test('27个省内关卡包含333地级单元及30省直辖地区,全部可拼完',()=>{const prefectures=data.levels.filter(l=>l.kind==='prefecture');assert.equal(prefectures.length,27);assert.equal(prefectures.reduce((n,l)=>n+l.regions.length,0),363);assert.equal(prefectures.reduce((n,l)=>n+l.regions.filter(r=>r.unitType==='prefecture').length,0),333);assert.equal(prefectures.reduce((n,l)=>n+l.regions.filter(r=>r.unitType==='province-direct').length,0),30);for(const l of prefectures){assert.ok(l.regions.every(r=>r.unitType==='prefecture'||r.unitType==='province-direct'),l.name+'仅含省域直接子区域');const g=game(l);g.status='playing';g.queue=[];alignWholeMap(g);for(let i=0;i<l.regions.length+3;i++)g.tryMerges();assert.equal(g.status,'won',l.name+'可通关');g.destroy();}});
|
||
test('任意静止角度均保留,不自动回正或夹回旧倾角范围',()=>{for(const angle of [-1.1,.8,Math.PI/2]){const g=game();g.status='playing';g.queue=[];g.engine.gravity.y=0;const p=g.createGroup([0],{tx:0,ty:0,angle});M.Body.setPosition(p.body,{x:210,y:280});for(let i=0;i<180;i++)g.tick(1000/60);assert.ok(Math.abs(p.body.angle-angle)<1e-8);g.destroy();}});
|
||
test('自由角动量自然衰减后停在新角度,旋转速度仍有上限',()=>{const g=game();g.status='playing';g.queue=[];g.engine.gravity.y=0;const p=g.createGroup([0],{tx:0,ty:0,angle:.6});M.Body.setPosition(p.body,{x:210,y:280});M.Body.setAngularVelocity(p.body,.012);for(let i=0;i<240;i++){g.tick(1000/60);assert.ok(Math.abs(p.body.angularVelocity)<=.0251);}assert.ok(p.body.angle>.9);assert.ok(Math.abs(p.body.angularVelocity)<.001);g.destroy();});
|
||
test('左右按键驱动容器弹簧,并对所有碎片施加惯性,而非只移动选中块',()=>{const g=game();g.start();g.spawn();g.queue=[];const b=g.createGroup([g.groups[0].ids[0]===0?1:0],{tx:180,ty:160,angle:0});const a=g.groups[0];const ax=a.body.position.x,bx=b.body.position.x;assert.ok(g.nudge(-1));g.tick(16.67);assert.ok(g.shake.offset<0);assert.ok(a.body.position.x!==ax);assert.ok(b.body.position.x!==bx);assert.equal(g.score,0);g.status='paused';const offset=g.shake.offset;assert.equal(g.nudge(1),false);g.tick(16.67);assert.equal(g.shake.offset,offset);g.destroy();});
|
||
test('全国完整地图初始宽度可容纳,旋转后过大的组合仍自动缩放并通关',()=>{const l=data.levels[0],g=new Game(l,320,400,0);assert.ok(l.width*g.scale<=g.w-20,'完整全国初始宽度应能放进容器');const scale=g.scale;g.status='playing';g.queue=[];g.createGroup(Array.from({length:33},(_,i)=>i),{tx:10,ty:150,angle:Math.PI/2});g.createGroup([33],{tx:10,ty:150,angle:Math.PI/2});g.tryMerges();assert.equal(g.status,'won');assert.ok(g.scale<scale);const b=g.groups[0].body.bounds;assert.ok(b.max.x-b.min.x<=g.w-19);assert.ok(b.max.y-b.min.y<=g.h-78-23);g.destroy();});
|
||
|
||
test('真实落地会回弹并产生碰撞反馈,落地碎片可被晃动轻轻颠起',()=>{const g=game();g.start();g.spawn();g.queue=[];let sawBounce=false;for(let i=0;i<480;i++){g.tick(16.67);if(g.groups[0].body.velocity.y<-.15)sawBounce=true;}assert.ok(sawBounce);assert.ok(g.collisionCount>0);const body=g.groups[0].body,start=body.position.y;g.nudge(1);assert.ok(body.velocity.y<0,'睡眠中的已落地碎片也会轻弹');let highest=start;for(let i=0;i<45;i++){g.tick(16.67);highest=Math.min(highest,body.position.y);}assert.ok(start-highest>4);assert.ok(start-highest<75);g.destroy();});
|
||
test('反复交替晃动限制平移和旋转速度,不会把碎片无限加速或甩出框外',()=>{const g=game();g.start();g.spawn();g.queue=[];for(let i=0;i<600;i++){if(i%9===0)g.nudge(Math.floor(i/9)%2?-1:1);g.tick(16.67);const b=g.groups[0].body;assert.ok(Math.abs(b.velocity.x)<=7.201);assert.ok(b.velocity.y<=9.001);assert.ok(Math.abs(b.angularVelocity)<=.0251);const real=M.Bounds.create(b.vertices);assert.ok(real.min.x>=-3&&real.max.x<=g.w+3);assert.ok(real.max.y<=g.h+4);}assert.ok(g.collisionCount>0);assert.ok(g.impacts.length<=20);g.destroy();});
|
||
|
||
|
||
test('上晃作用于整个框和每个组合,冷却防连点,暂停时不可操作',()=>{const g=game();g.start();g.spawn();g.queue=[];g.createGroup([g.groups[0].ids[0]===0?1:0],{tx:180,ty:160,angle:0});assert.equal(g.nudgeUp(),true);assert.ok(g.groups.every(p=>p.body.velocity.y<0));assert.equal(g.nudgeUp(),false);g.tick(16.67);assert.ok(g.shake.y<0);g.status='paused';assert.equal(g.nudgeUp(),false);g.destroy();});
|
||
test('四档弹性实际递增且反复三向晃动始终受速度/框体限制',()=>{const {BOUNCES}=require('../src/game.js');assert.equal(BOUNCES.length,4);let heights=[];for(const b of [0,1,3,2]){const g=new Game(level,420,560,0,()=>{},()=>.5,b);g.start();g.spawn();g.queue=[];for(let i=0;i<480;i++)g.tick(16.67);const start=g.groups[0].body.position.y;g.nudgeUp();let highest=start;for(let i=0;i<60;i++){g.tick(16.67);highest=Math.min(highest,g.groups[0].body.position.y);}heights.push(start-highest);assert.equal(g.groups[0].body.restitution,BOUNCES[b].restitution);for(let i=0;i<600;i++){if(i%30===0)g.nudgeUp();if(i%9===0)g.nudge(i%2?1:-1);g.tick(16.67);const body=g.groups[0].body,real=M.Bounds.create(body.vertices);assert.ok(Math.abs(body.velocity.x)<=BOUNCES[b].maxX+.001);assert.ok(Math.abs(body.angularVelocity)<=.0251);assert.ok(real.min.x>=-3&&real.max.x<=g.w+3&&real.max.y<=g.h+4);}g.destroy();}for(let i=1;i<heights.length;i++)assert.ok(heights[i]>heights[i-1]*1.15,heights);});
|
||
test('地级版与全国版继续保持额外七成的相对缩放',()=>{const {baseScale}=require('../src/game.js');for(const l of data.levels.slice(1)){const same={...l,id:'100000'};assert.ok(Math.abs(baseScale(l,320,435)/baseScale(same,320,435)-.7)<.00001);}});
|
||
test('暂停也可主动结束,成绩曲线从零开始、通关含奖励、结束后冻结且只发一次事件',()=>{let ends=0;const g=new Game(level,420,560,0,t=>{if(t==='end')ends++;},()=>.5);g.start();g.spawn();g.queue=[];g.tick(100);g.status='paused';const before=g.clock;g.tick(100);assert.equal(g.clock,before);assert.ok(g.finish());assert.equal(g.status,'ended');assert.equal(g.endReason,'stopped');assert.equal(ends,1);assert.deepEqual(g.history,[{ms:0,score:0},{ms:100,score:0}]);assert.equal(g.finish(),false);g.tick(100);assert.equal(g.clock,100);g.destroy();const won=game();won.status='playing';won.clock=60000;won.queue=[];won.createGroup([0],{tx:20,ty:100,angle:0});won.createGroup([1],{tx:20,ty:100,angle:0});won.tryMerges();assert.equal(won.endReason,'won');assert.deepEqual(won.history,[{ms:0,score:0},{ms:60000,score:1100}]);won.destroy();});
|
||
test('不同弹性的纪录分开存储,仍兼容旧版最低弹性纪录',()=>{assert.deepEqual(C.safeRecord({'100000:0':100,'100000:0:1':200,'100000:0:2':300,'100000:0:4':900}),{'100000:0':100,'100000:0:1':200,'100000:0:2':300});});
|
||
|
||
test('四档下落速度下,实测上晃分别为轻弹、正常和强弹,强弹可颠起至少200px',()=>{const fs=require('node:fs'),results=[];const l={id:'100000',name:'弹跳测试',width:1000,height:1000,regions:[square(0,0,0)],edges:[]};for(let speed=0;speed<4;speed++){let heights=[];for(let bounce=0;bounce<4;bounce++){const g=new Game(l,320,590,speed,()=>{},()=>.5,bounce);g.start();g.spawn();g.queue=[];const body=g.groups[0].body;M.Body.translate(body,{x:0,y:g.h-1-body.bounds.max.y});M.Body.setVelocity(body,{x:0,y:0});M.Sleeping.set(body,true);const start=body.position.y;assert.ok(g.nudgeUp());let highest=start;for(let i=0;i<180;i++){g.tick(16.67);highest=Math.min(highest,body.position.y);}const height=start-highest;heights.push(height);results.push({speed,bounce,height:Number(height.toFixed(1))});g.destroy();}assert.ok(heights[0]>=25&&heights[0]<=55,heights);assert.ok(heights[1]>=85&&heights[1]<=150,heights);assert.ok(heights[2]>=200&&heights[2]<=340,heights);assert.ok(heights[3]>heights[1]*1.15&&heights[3]<heights[2]/1.1,heights);}fs.writeFileSync('artifacts/bounce-probe.json',JSON.stringify({method:'320×590 logical pixels, one supported square, one upward shake, actual physics peak; not real-device performance',results},null,2));});
|
||
|
||
test('左右晃动会缓存请求,碎片在两次150ms按键之间落地也能颠起',()=>{const l={id:'100000',name:'落地时序',width:1000,height:1000,regions:[square(0,0,0)],edges:[]};for(const delay of [0,25,60,100]){const g=new Game(l,320,590,0,()=>{},()=>.5,1);g.start();g.spawn();g.queue=[];g.clock=delay;const p=g.groups[0],b=p.body;M.Body.translate(b,{x:0,y:g.h-6-b.bounds.max.y});M.Body.setVelocity(b,{x:0,y:1});assert.ok(g.nudge(1));assert.equal(g.hopCount,0,'尚未接触时不在空中加速');for(let i=0;i<9;i++)g.tick(1000/60);assert.equal(g.hopCount,1,'下一次按键前已在落地时触发');assert.ok(b.velocity.y<0);g.destroy();}});
|
||
test('过期请求、空中长按和暂停不会凭空产生连续上抛',()=>{const l={id:'100000',name:'时序限制',width:1000,height:1000,regions:[square(0,0,0)],edges:[]},g=new Game(l,320,590,0,()=>{},()=>.5,1);g.start();g.spawn();g.queue=[];g.nudge(1);for(let i=0;i<180;i++)g.tick(1000/60);assert.equal(g.hopCount,0,'初始空中请求已过期');g.status='paused';assert.equal(g.nudge(1),false);const count=g.hopCount;g.tick(100);assert.equal(g.hopCount,count);g.destroy();});
|
||
|
||
test('容器滑动按位置分配力度:附近更强,远处仍动,没有瞬移或单片抓取',()=>{
|
||
const l={id:'100000',name:'位置力度',width:1000,height:1000,regions:[square(0,0,0),square(1,300,0)],edges:[]},g=new Game(l,420,560,0,()=>{},()=>.5,1);g.status='playing';g.queue=[];
|
||
const near=g.createGroup([0],{tx:0,ty:0,angle:0}),far=g.createGroup([1],{tx:0,ty:0,angle:0});M.Body.setPosition(near.body,{x:90,y:200});M.Body.setPosition(far.body,{x:330,y:420});const positions=g.groups.map(p=>({...p.body.position}));
|
||
assert.ok(g.swipe({x:90,y:200},{x:30,y:-10},32));assert.ok(near.body.velocity.x>far.body.velocity.x*3);assert.ok(far.body.velocity.x>0);assert.ok(near.body.velocity.y<far.body.velocity.y&&far.body.velocity.y<0);assert.deepEqual(g.groups.map(p=>({...p.body.position})),positions);assert.equal(g.swipeCount,1);g.tick(16.67);assert.ok(g.shake.offset>0);assert.ok(g.shake.y<0);g.destroy();
|
||
});
|
||
test('同样距离快速划动更有力;轻触、无效输入与暂停不触发晃动',()=>{
|
||
const run=elapsed=>{const g=game();g.start();g.spawn();g.queue=[];const body=g.groups[0].body,p={...body.position};g.swipe(p,{x:24,y:0},elapsed);const vx=body.velocity.x;g.destroy();return vx;};assert.ok(run(16)>run(64)*1.3);
|
||
const g=game();g.start();g.spawn();const p={x:200,y:200};assert.equal(g.swipe(p,{x:.1,y:.1},16),false);assert.equal(g.swipe(p,{x:NaN,y:0},16),false);assert.equal(g.swipe(p,{x:20,y:0},Infinity),false);g.status='paused';assert.equal(g.swipe(p,{x:20,y:0},16),false);assert.equal(g.swipeCount,0);g.destroy();
|
||
});
|
||
test('四档弹性连续任意方向滑动不超速、穿墙、旋转失控,落地滑动能颠起',()=>{
|
||
const l={id:'100000',name:'滑动边界',width:1000,height:1000,regions:[square(0,0,0)],edges:[]};for(let bounce=0;bounce<4;bounce++){
|
||
const g=new Game(l,320,590,0,()=>{},()=>.5,bounce);g.start();g.spawn();g.queue=[];const b=g.groups[0].body;M.Body.translate(b,{x:0,y:g.h-1-b.bounds.max.y});M.Sleeping.set(b,true);g.swipe({...b.position},{x:30,y:0},32);assert.ok(b.velocity.y<0,'落地碎片有上弹反馈');
|
||
for(let i=0;i<600;i++){g.swipe({x:160,y:450},{x:Math.sin(i*.13)*300,y:Math.cos(i*.17)*300},16.67);g.tick(16.67);const real=M.Bounds.create(b.vertices);assert.ok(Math.abs(b.velocity.x)<=g.bounce.maxX+.001);assert.ok(b.velocity.y<=9.001);assert.ok(Math.abs(b.angularVelocity)<=.0251);assert.ok(real.min.x>=-3&&real.max.x<=g.w+3&&real.max.y<=g.h+4);assert.ok(g.impacts.length<=20);}g.destroy();
|
||
}
|
||
});
|
||
|
||
test('每次划动只给一次冲量,短间隔限频,地块方向有分散且随后自由运动',()=>{
|
||
const l={id:'100000',name:'单次惯性',width:1000,height:1000,regions:[square(0,0,0),square(1,300,0)],edges:[]};let seq=0;const g=new Game(l,420,560,0,()=>{},()=>[.1,.9,.9,.1][seq++%4],1);g.status='playing';g.queue=[];
|
||
const a=g.createGroup([0],{tx:0,ty:100,angle:0}),b=g.createGroup([1],{tx:0,ty:100,angle:0});M.Body.setPosition(a.body,{x:120,y:200});M.Body.setPosition(b.body,{x:260,y:200});assert.ok(g.swipe({x:190,y:200},{x:70,y:0},100));assert.ok(a.body.velocity.x>0&&b.body.velocity.x>0);assert.ok(a.body.velocity.y*b.body.velocity.y<0,'相同大方向下有不同偏转');
|
||
const pos={...a.body.position};assert.equal(g.swipe({x:120,y:200},{x:-70,y:0},100),false);for(let i=0;i<15;i++)g.tick(16.67);assert.equal(g.swipeCount,1);assert.notDeepEqual(a.body.position,pos);assert.ok(g.swipe({x:120,y:200},{x:70,y:0},100));assert.equal(g.swipeCount,2);g.destroy();
|
||
});
|
||
|
||
test('待落块先留在预备区,不进入物理世界或落块计数;四档均到点释放',()=>{
|
||
for(let speed=0;speed<4;speed++){const g=new Game(level,420,560,speed,()=>{},()=>.5);g.start();const p=g.pendingView();assert.ok(p);assert.equal(g.dropped,0);assert.equal(g.groups.length,0);assert.ok(p.top>=0&&p.bottom<g.dangerY);g.holdPending(p.serial);g.movePending(60,p.serial);const chosen=g.pending.x,deadline=g.nextAt;while(g.clock<deadline-50)g.tick(Math.min(100,deadline-50-g.clock));assert.equal(g.dropped,0);assert.equal(g.releasePending(p.serial),true);assert.equal(g.dropped,0,'定时模式松手不提前');g.holdPending(p.serial);g.tick(50);assert.equal(g.dropped,1,'到点仍按住也落下');assert.notEqual(g.pending.serial,p.serial);const body=g.groups[0],anchor=g.worldPoint(body,g.level.regions[p.id].center);assert.ok(Math.abs(anchor.x-chosen)<.001);assert.equal(g.movePending(330,p.serial),false);assert.equal(g.releasePending(p.serial),false,'旧手指不会释放下一块');assert.equal(g.dropped,1);g.destroy();}
|
||
});
|
||
test('∞模式等待不自动落下,取消/暂停不释放;仅有效松手释放一次',()=>{
|
||
const g=new Game(level,320,590,4,()=>{},()=>.5,3);g.start();const serial=g.pending.serial;for(let i=0;i<1000;i++)g.tick(100);assert.equal(g.dropped,0);assert.equal(g.pending.serial,serial);assert.equal(g.pendingView().remaining,null);assert.equal(g.releasePending(serial),false);g.holdPending(serial);g.cancelPendingDrag(serial);assert.equal(g.releasePending(serial),false);g.holdPending(serial);g.status='paused';const time=g.clock;g.tick(100);assert.equal(g.clock,time);assert.equal(g.releasePending(serial),false);g.cancelPendingDrag(serial);g.status='playing';g.holdPending(serial);assert.equal(g.releasePending(serial),true);assert.equal(g.dropped,1);assert.equal(g.groups.length,1);assert.equal(g.releasePending(serial),false);assert.equal(g.dropped,1);g.destroy();
|
||
});
|
||
test('预备区调整有真实轮廓左右边界,缩放保留位置比例且暂停不走倒计时',()=>{
|
||
const g=new Game(data.levels[0],320,590,0,()=>{},()=>.5);g.start();const serial=g.pending.serial;g.movePending(-999,serial);const x=g.pending.x;g.spawn(serial);let real=M.Bounds.create(g.groups[0].body.vertices);assert.ok(real.min.x>=7.9);g.movePending(999,g.pending.serial);const second=g.pending.serial;g.spawn(second);real=M.Bounds.create(g.groups[1].body.vertices);assert.ok(real.max.x<=g.w-7.9);const third=g.pending.serial;g.movePending(g.w*.7,third);const ratio=g.pending.x/g.w;g.resize(420,650);assert.ok(Math.abs(g.pending.x/g.w-ratio)<.00001);const before=g.pendingView().remaining;g.status='paused';g.tick(100);assert.equal(g.pendingView().remaining,before);assert.ok(x>=8);g.destroy();
|
||
});
|
||
test('四档弹性按展示顺序递增,新档居中且旧强弹纪录不被挪用',()=>{
|
||
const {BOUNCES,BOUNCE_ORDER}=require('../src/game.js');assert.deepEqual(BOUNCE_ORDER,[0,1,3,2]);for(let i=1;i<BOUNCE_ORDER.length;i++){const a=BOUNCES[BOUNCE_ORDER[i-1]],b=BOUNCES[BOUNCE_ORDER[i]];for(const key of ['restitution','hop','rise','push','maxX'])assert.ok(b[key]>a[key],key);}assert.equal(BOUNCES[2].name,'弹力全开');assert.equal(BOUNCES[3].name,'元气弹跳');assert.deepEqual(C.safeRecord({'100000:4:3':123,'100000:0:2':999,'100000:5:0':5}),{'100000:4:3':123,'100000:0:2':999});
|
||
});
|
||
|
||
test('正确相邻地块小幅错位与倾斜更容易吸合,错边、过远、错向和非邻接仍拒绝',()=>{
|
||
const merged=game();merged.status='playing';merged.queue=[];merged.createGroup([0],{tx:90,ty:140,angle:-.13});merged.createGroup([1],{tx:114,ty:140,angle:.13});assert.equal(merged.tryMerges(),true);assert.equal(merged.status,'won');merged.destroy();
|
||
for(const pose of [{tx:118,ty:140,angle:0},{tx:90,ty:140,angle:.32},{tx:190,ty:140,angle:0}]){const g=game();g.status='playing';g.createGroup([0],{tx:90,ty:140,angle:0});g.createGroup([1],pose);assert.equal(g.tryMerges(),false);assert.equal(g.groups.length,2);g.destroy();}
|
||
const noEdge=game({...level,edges:[]});noEdge.status='playing';noEdge.createGroup([0],{tx:90,ty:140,angle:0});noEdge.createGroup([1],{tx:90,ty:140,angle:0});assert.equal(noEdge.tryMerges(),false);noEdge.destroy();
|
||
});
|
||
|
||
test('直辖市县区、港澳分区与台湾县市均有完整关卡且可通关',()=>{const counts={'110000':16,'120000':16,'310000':16,'500000':38,'810000':18,'820000':8,'710000':20};for(const [id,count] of Object.entries(counts)){const l=data.levels.find(l=>l.id===id);assert.equal(l.regions.length,count);const g=game(l);g.status='playing';g.queue=[];alignWholeMap(g);for(let i=0;i<count+2;i++)g.tryMerges();assert.equal(g.status,'won',l.name+'应可通关');g.destroy();}const tw=data.levels.find(l=>l.id==='710000');for(const name of ['台北市','新北市','高雄市','台东县','澎湖县'])assert.ok(tw.regions.some(r=>r.name===name));assert.ok(tw.regions.every(r=>r.name!=='金门县'&&r.name!=='连江县'));assert.ok(tw.scopeNote.includes('移位示意'));});
|
||
|
||
test('台湾环形县市保留内部空洞,内嵌市不是海岛且不会被县块碰撞填满',()=>{const l=data.levels.find(l=>l.id==='710000');for(const [outer,inner] of [['新北市','台北市'],['嘉义县','嘉义市']]){const a=l.regions.findIndex(r=>r.name===outer),b=l.regions.findIndex(r=>r.name===inner),r=l.regions[a];assert.ok(r.holes&&r.holes.length&&r.physics.length);assert.ok(l.edges.some(e=>((e[0]===a&&e[1]===b)||(e[0]===b&&e[1]===a))&&e[2]===0));const g=game(l),body=g.createGroup([a],{tx:10,ty:100,angle:0}),point=g.worldPoint(body,l.regions[b].label);assert.equal(M.Query.point([body.body],point).length,0,outer+'应留出'+inner+'的位置');g.destroy();}});
|