37 lines
12 KiB
JavaScript
37 lines
12 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);}
|
||
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();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.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 g=game();g.start();g.queue=[];const a=g.groups[0];M.Body.setStatic(a.body,true);M.Body.setPosition(a.body,{x:150,y:42});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.equal(g.status,'lost');g.destroy();});
|
||
test('全国每个真实关卡均连通、每片轮廓可生成有限的刚体',()=>{assert.equal(data.levels[0].regions.length,34);assert.equal(data.levels.length,28);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,367);});
|
||
test('完整的全国地图可全部吸合,海岛无死局',()=>{const g=game(data.levels[0]);g.status='playing';g.queue=[];for(let i=0;i<34;i++)g.createGroup([i],{tx:20,ty:100,angle:0});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.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个地级关卡可拼完',()=>{for(const l of data.levels.slice(1)){assert.ok(l.regions.every(r=>Number(r.id)%100===0),l.name+'不得包含县区或县级市');const g=game(l);g.status='playing';g.queue=[];for(let i=0;i<l.regions.length;i++)g.createGroup([i],{tx:25,ty:110,angle:0});for(let i=0;i<l.regions.length+3;i++)g.tryMerges();assert.equal(g.status,'won',l.name+'可通关');g.destroy();}});
|
||
test('碎片轻微翻动受角度上限约束,松手后自动回正',()=>{const g=game();g.start();g.queue=[];const a=g.groups[0];g.nudge(1);for(let i=0;i<200;i++)g.tick(16.67);assert.ok(Math.abs(a.body.angle)<.025);g.destroy();});
|
||
test('左右按键驱动容器弹簧,并对所有碎片施加惯性,而非只移动选中块',()=>{const g=game();g.start();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,435,0);const old=Math.min((320-52)/l.width,435*.48/l.height);assert.ok(g.scale/old>2.2);const scale=g.scale;g.status='playing';g.queue=[];for(let i=0;i<34;i++)g.createGroup([i],{tx:10,ty:150,angle:0});for(let i=0;i<40;i++)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.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.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.angle)<=.1601);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.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,3);let heights=[];for(let b=0;b<3;b++){const g=new Game(level,420,560,0,()=>{},()=>.5,b);g.start();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.angle)<=.1601);assert.ok(real.min.x>=-3&&real.max.x<=g.w+3&&real.max.y<=g.h+4);}g.destroy();}assert.ok(heights[1]>heights[0]*1.5,heights);assert.ok(heights[2]>heights[1]*1.5,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.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:3':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<3;bounce++){const g=new Game(l,320,590,speed,()=>{},()=>.5,bounce);g.start();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);}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.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.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();});
|