// t72: rescue uses visited space before allowing a bounded escape through rock. const {load, suite, flatWorld} = require('./harness'); const {run} = load(), {check, done} = suite(); function setup() { flatWorld(run, 320, 240, 220); run('mat.fill(EMPTY); flags.fill(0); fog.fill(255); EV.length=0; bot.x=24; bot.y=100; bot.vx=bot.vy=0; botStep(0,0,false);'); } function bury() { run('mat.fill(BASALT); flags.fill(F_NAT);'); } function clear(x,y,w=5,h=5) { run(`for(let y=${y};y<${y+h};y++) for(let x=${x};x<${x+w};x++) {mat[y*W+x]=EMPTY;flags[y*W+x]=0;}`); } setup(); run('for(let x=28;x<=140;x+=4){bot.x=x;botStep(0,0,false);}'); bury(); clear(24,100); run('botStep(0,0,false)'); check('deep basalt retraces to the last open pool edge', run('bot.x===24 && bot.y===100')); check('retrace explains the move', run('EV.some(e=>e.text==="Dug out: back where you came from")')); setup(); bury(); clear(24,95); run('botStep(0,0,false)'); check('plain pop to the top is preserved', run('bot.x===24 && bot.y===95')); setup(); bury(); clear(24,95); clear(29,100); run('botStep(0,0,false)'); check('equal-distance nudge prefers up', run('bot.x===24 && bot.y===95')); setup(); bury(); clear(29,105); run('botStep(0,0,false)'); check('nudge finds a diagonal downward opening', run('bot.x===29 && bot.y===105')); setup(); bury(); clear(24,95); run('fog.fill(0);botStep(0,0,false)'); check('unexplored pocket is never chosen', run('bot.x===24 && bot.y===100')); check('buried trail enables escape with a message', run('EV.some(e=>e.text && e.text.includes("Fly through"))')); run('for(let k=0;k<80;k++) botStep(1,0,false)'); check('escape allows movement through solid rock', run('bot.x>55 && bot.y===100')); check('escape message appears once', run('EV.filter(e=>e.t==="toast").length===1')); setup(); bury(); clear(55,90,30,30); run('for(let k=0;k<160;k++) botStep(1,0,false)'); check('escape ends in open space and next rock stops the bot', run('bot.x>=55 && bot.x<=80 && !botOverlapSolid(bot.x,bot.y)')); setup(); run('for(let k=0;k<600;k++){bot.x=24+(k%60)*4;botStep(0,0,false);}'); check('trail storage is fixed and wraps', run('botTrailCount===256 && botTrailX.length===256 && botTrailY.length===256')); run('const savedHead=botTrailHead; for(let k=0;k<60;k++) botStep(0,0,false);'); check('stationary bot adds no points', run('savedHead===botTrailHead')); run('const trailX=botTrailX; alloc(320,240);'); check('new world clears trail and escape without reallocating storage', run('botTrailCount===0 && !botEscape && trailX===botTrailX')); setup(); run('const rescueSave=serializeWorld(); bot.x=100;botStep(0,0,false);deserializeWorld(rescueSave.meta,rescueSave.buffers);botStep(0,0,false);'); check('load rebuilds trail from the restored bot', run('botTrailCount===1 && botTrailX[0]===24 && botTrailY[0]===100')); setup(); run('bot.x=28;botStep(0,0,false);bot.x=140;botStep(0,0,false);'); bury(); clear(24,100); clear(28,100); run('botStep(0,0,false)'); check('retrace chooses the newest open point', run('bot.x===28')); setup(); bury(); run(`const originalOverlap=botOverlapSolid;let rescueChecks=0; botOverlapSolid=(x,y)=>{rescueChecks++;return originalOverlap(x,y);}; botStep(0,0,false); const firstChecks=rescueChecks;rescueChecks=0; botStep(0,0,false);const escapeChecks=rescueChecks;botOverlapSolid=originalOverlap;`); check('rescue work has a fixed per-tick cap', run('firstChecks<=BOT_NUDGES.length+BOT_TRAIL_SIZE+3')); check('active escape does not repeat searches', run('escapeChecks<=2')); setup(); run('const originalRescue=botRescue;let searches=0;botRescue=()=>{searches++;originalRescue();};for(let k=0;k<120;k++)botStep(1,0,false);botRescue=originalRescue;'); check('normal flight never runs rescue searches', run('searches===0')); done();