import assert from 'node:assert/strict'; import {createState,start,step,advance,togglePause} from './host.mjs'; const checks=[]; function check(name,run){run();checks.push(name);} function collectAll(s){for(const token of s.tokens){s.player={x:token.x,y:token.y};step(s,{},0.01);}} function quietSurvival(s){s.spawnClock=100;s.hazards=[];for(let i=0;i<201&&s.status==='running';i++)advance(s,{},0.1);} check('Reject unsupported donor mode',()=>assert.throws(()=>createState('other'))); check('Independent collector finishes four tokens',()=>{const s=start('collector');collectAll(s);assert.equal(s.status,'won');assert.equal(s.collected,4);assert.equal(s.hazards.length,0);}); check('Independent arena finishes twenty simulated seconds',()=>{const s=start('arena');quietSurvival(s);assert.equal(s.status,'won');assert.equal(s.tokens.length,0);}); check('Hybrid does not win from collection alone',()=>{const s=start();collectAll(s);assert.equal(s.status,'running');assert.ok(s.remaining>19);assert.equal(s.charges,3);}); check('Hybrid does not win from survival alone',()=>{const s=start();quietSurvival(s);assert.equal(s.status,'running');assert.equal(s.remaining,0);}); check('Hybrid wins when both goals complete',()=>{const s=start();collectAll(s);quietSurvival(s);assert.equal(s.status,'won');}); check('Collection event is idempotent',()=>{const s=start();s.player={x:80,y:80};step(s,{},0.01);step(s,{},0.01);assert.equal(s.collected,1);assert.equal(s.charges,2);assert.equal(s.events.length,0);}); check('Pulse has range and a shared charge cost',()=>{const s=start();s.hazards=[{id:'near',x:330,y:180},{id:'far',x:600,y:180}];step(s,{pulse:true},0.01);assert.equal(s.charges,0);assert.equal(s.defeated,1);assert.equal(s.hazards[0].id,'far');}); check('Held pulse spends once, not once per substep',()=>{const s=start();s.charges=3;advance(s,{pulse:true},0.1);assert.equal(s.charges,2);advance(s,{pulse:true},0.1);assert.equal(s.charges,2);advance(s,{},0.01);advance(s,{pulse:true},0.01);assert.equal(s.charges,1);}); check('Hybrid charge refills only from collection',()=>{const s=start();s.spawnClock=100;for(let i=0;i<50;i++)advance(s,{},0.1);assert.equal(s.charges,1);s.player={x:80,y:80};step(s,{},0.01);assert.equal(s.charges,2);}); check('Arena retains its independent timed recharge',()=>{const s=start('arena');s.spawnClock=100;for(let i=0;i<31;i++)advance(s,{},0.1);assert.equal(s.charges,2);}); check('Contact damages once during invulnerability',()=>{const s=start();s.hazards=[{id:'contact',x:320,y:180}];step(s,{},0.01);assert.equal(s.hp,2);advance(s,{},0.1);assert.equal(s.hp,2);}); check('Zero health loses before a completed goal can win',()=>{const s=start();s.hp=1;s.collected=4;s.remaining=0;s.hazards=[{id:'contact',x:320,y:180}];step(s,{},0.01);assert.equal(s.status,'lost');}); check('One-second movement agrees at15/30/60/120 simulated FPS',()=>{for(const fps of [15,30,60,120]){const s=start('collector');for(let i=0;i{const s=start('collector');advance(s,{right:true,down:true},0.1);assert.ok(Math.abs(Math.hypot(s.player.x-320,s.player.y-180)-18)<1e-7);}); check('Host movement respects all four world boundaries',()=>{for(const [input,x,y] of [[{left:true},14,180],[{right:true},626,180],[{up:true},320,14],[{down:true},320,346]]){const s=start('collector');for(let i=0;i<50;i++)advance(s,input,0.1);assert.ok(Math.abs(s.player.x-x)<1e-7);assert.ok(Math.abs(s.player.y-y)<1e-7);}}); check('Pause freezes time, position and donor state',()=>{const s=start();togglePause(s);const before=structuredClone(s);advance(s,{right:true,pulse:true},0.1);assert.deepEqual(s,before);togglePause(s);advance(s,{right:true},0.1);assert.ok(s.age>0);}); check('Fresh restart restores tokens, hazards, charge and score',()=>{const s=start();collectAll(s);const fresh=start();assert.equal(fresh.collected,0);assert.equal(fresh.hazards.length,0);assert.equal(fresh.charges,1);assert.ok(fresh.tokens.every(t=>!t.collected));}); check('Invalid elapsed time cannot mutate a game',()=>{const s=start(),before=structuredClone(s);for(const value of [NaN,Infinity,-1,0])advance(s,{},value);assert.deepEqual(s,before);}); check('Long stall is bounded to0.1 simulated seconds',()=>{const s=start('collector');advance(s,{right:true},10);assert.ok(Math.abs(s.age-0.1)<1e-8);assert.ok(Math.abs(s.player.x-338)<1e-8);}); check('Hazard wave count is bounded at eight',()=>{const s=start('arena');s.hp=1000;s.player={x:320,y:180};for(let i=0;i<500&&s.status==='running';i++)advance(s,{},0.1);assert.ok(s.hazards.length<=8);assert.ok(s.hazards.length>0);}); check('Terminal result is immutable on later frames',()=>{const s=start('collector');collectAll(s);const before=structuredClone(s);advance(s,{left:true},0.1);assert.deepEqual(s,before);}); console.log(JSON.stringify({result:'passed',runtime:process.version,count:checks.length,checks,scope:'Actual Node execution of two original donor modes and hybrid rules; simulated timing only, no browser/render/hardware/third-party game acceptance.'},null,2));