Bot can never be trapped in rock: nudge, retrace trail, fly out (t72)

This commit is contained in:
2026-10-29 18:25:37 -04:00
parent fc29214162
commit b655d754b9
8 changed files with 196 additions and 15 deletions
+7
View File
@@ -623,3 +623,10 @@ above, including:
count toward the raised nozzle allowance (4,000); only rain counts toward rain's 2,500. count toward the raised nozzle allowance (4,000); only rain counts toward rain's 2,500.
Factory launches remain uncapped. A full nozzle allowance says “Too much in the air.” Factory launches remain uncapped. A full nozzle allowance says “Too much in the air.”
The 4,000 tuning preserves the existing eight-second blown-pile collapse check; 6,000 did not. The 4,000 tuning preserves the existing eight-second blown-pile collapse check; 6,000 did not.
- **Buried bot rescue (t72, 2026-10-29).** The owner chose a small nearest-first nudge,
then the newest still-open visited position with a message, then flight through rock until
open space with a message. This prevents cooling magma from trapping the bot. The nudge
requires fully explored space and prefers up on equal distances. A 256-point trail records
every four grains without per-tick allocations; loading rebuilds it from the bot position.
Searches happen only on burial, capped at 440 nudge and 256 trail probes per escape episode.
+11 -7
View File
@@ -244,7 +244,7 @@ Starter furnace designs and the milestone list shown in the notebook.
- functions: smelterBlueprint - functions: smelterBlueprint
- variables: STARTER_DESIGNS, MILESTONES - variables: STARTER_DESIGNS, MILESTONES
### src/sim/world.js (123 lines) ### src/sim/world.js (124 lines)
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking. World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
- functions: emit, bagCap, discover, sharedArray, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, renderDirtyXY, renderDirtyChunk, renderDirtyIdx, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx - functions: emit, bagCap, discover, sharedArray, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, renderDirtyXY, renderDirtyChunk, renderDirtyIdx, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, renderDirty, awake, hot, hotLvl, powderArea, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, screwLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats - variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, renderDirty, awake, hot, hotLvl, powderArea, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, screwLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
@@ -337,7 +337,7 @@ Trees (background layer), acorns, saplings, and waking freezing water.
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree - functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
- variables: treeBases, saplings, treeDraw - variables: treeBases, saplings, treeDraw
### src/sim/vacuum.js (200 lines) ### src/sim/vacuum.js (203 lines)
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain. The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite - functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
- variables: blowRR, FLY_G, FLY_Q - variables: blowRR, FLY_G, FLY_Q
@@ -355,16 +355,16 @@ Undo (Ctrl+Z) for building, and the bot building ghosts.
- functions: undoNote, undoPush, undoRecord, undoLast, clearAllGhosts, ghostTick, ghostTick0 - functions: undoNote, undoPush, undoRecord, undoLast, clearAllGhosts, ghostTick, ghostTick0
- variables: UNDO_MAX, undoStack, undoRec, ghostMissing, ghostIter - variables: UNDO_MAX, undoStack, undoRec, ghostMissing, ghostIter
### src/sim/bot.js (55 lines) ### src/sim/bot.js (99 lines)
The bot: movement and collision with the world. The bot: movement and collision with the world.
- functions: botBlocked, botOverlapSolid, botSlide, botStep - functions: botBlocked, botOverlapSolid, botTrailRecord, botNudgeOpen, botRescue, botSlide, botStep
- variables: SLIDE_MAX, SLIDE_STEP - variables: BOT_TRAIL_SIZE, botTrailX, botTrailY, botTrailHead, botTrailCount, botEscape, BOT_NUDGES, SLIDE_MAX, SLIDE_STEP
### src/sim/ledger.js (54 lines) ### src/sim/ledger.js (54 lines)
Milestone checks (the ledger) and the sky roof line used for cooling. Milestone checks (the ledger) and the sky roof line used for cooling.
- functions: checkLedger, updateRoof, updateDirtyRoof, scanRoof - functions: checkLedger, updateRoof, updateDirtyRoof, scanRoof
### src/sim/weather.js (87 lines) ### src/sim/weather.js (90 lines)
Weather: clouds from steam, rain and snow, and the world water count. Weather: clouds from steam, rain and snow, and the world water count.
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick - functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
- variables: WBAND, WS, weather - variables: WBAND, WS, weather
@@ -507,7 +507,7 @@ Startup capabilities and title art.
- functions: checkCapabilities, renderCapabilityNotice, canStartGame, buildTitleCells, drawTitle - functions: checkCapabilities, renderCapabilityNotice, canStartGame, buildTitleCells, drawTitle
- variables: REQUIRE_WEBGPU, REQUIRE_THREADS, CAPS, CAPS_CHECK, FONT5, TITLE - variables: REQUIRE_WEBGPU, REQUIRE_THREADS, CAPS, CAPS_CHECK, FONT5, TITLE
### src/ui/saves.js (224 lines) ### src/ui/saves.js (237 lines)
Saves in IndexedDB, world files (export / import) and the blueprint library. Saves in IndexedDB, world files (export / import) and the blueprint library.
- functions: newWorldId, worldWrite, listWorlds, putWorldEntry, worldNewest, deleteWorld, cleanWorldName, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, packBuffers, unpackBuffers, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode - functions: newWorldId, worldWrite, listWorlds, putWorldEntry, worldNewest, deleteWorld, cleanWorldName, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, packBuffers, unpackBuffers, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode
- variables: DB, worldIdTime, worldIdCounter, worldWrites, savingCount - variables: DB, worldIdTime, worldIdCounter, worldWrites, savingCount
@@ -639,6 +639,9 @@ The frame loop and boot.
- `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load. - `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
- `test/t_blow_collapse.js`: Owner's upward nozzle recipe, inline and with real shared-memory helpers. - `test/t_blow_collapse.js`: Owner's upward nozzle recipe, inline and with real shared-memory helpers.
- `test/t_blow_collapse.py`: Straight-up bagged sand nozzle in the real pixel frame loop. - `test/t_blow_collapse.py`: Straight-up bagged sand nozzle in the real pixel frame loop.
- `test/t_blueprint_valid.js`: Shared blueprint codes must be safe before entering the clipboard or ghost queues.
- `test/t_bot_rescue.js`: t72: rescue uses visited space before allowing a bounded escape through rock.
- `test/t_bot_rescue.py`: t72: rescue messages and keyboard escape in the pixel game.
- `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning. - `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
- `test/t_caps.py`: Capability detection, title notices and the required-WebGPU startup gate. - `test/t_caps.py`: Capability detection, title notices and the required-WebGPU startup gate.
- `test/t_conveyors.js`: Screw conveyors that never carry liquid (it flows through), screw and belt throughput, filters. - `test/t_conveyors.js`: Screw conveyors that never carry liquid (it flows through), screw and belt throughput, filters.
@@ -655,6 +658,7 @@ The frame loop and boot.
- `test/t_gpu.py`: Dirty uploads, default limits, warm pipelines, world replacement and device recovery. - `test/t_gpu.py`: Dirty uploads, default limits, warm pipelines, world replacement and device recovery.
- `test/t_gpu_golden.py`: Frozen world references; seven smooth crops corrected for continuous noise in t91. - `test/t_gpu_golden.py`: Frozen world references; seven smooth crops corrected for continuous noise in t91.
- `test/t_gpu_light.py`: The former CPU light behavioural checks, now read from the actual GPU light grid. - `test/t_gpu_light.py`: The former CPU light behavioural checks, now read from the actual GPU light grid.
- `test/t_import_save.js`: Import must preserve the running world before reading or replacing it.
- `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables. - `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables.
- `test/t_lakes.js`: Generated lakes wake without a tool, settle on their own, and have dry tree bases. - `test/t_lakes.js`: Generated lakes wake without a tool, settle on their own, and have dry tree bases.
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state. - `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
+6
View File
@@ -74,6 +74,12 @@ checks waking within eight ticks separately from the probabilistic consumption o
wake/background-tree change. It also checks generated tree bases, startup wakes, and the wake/background-tree change. It also checks generated tree bases, startup wakes, and the
seed-7 lake leveling within two grains after 6,000 undisturbed ticks, fresh and reloaded. seed-7 lake leveling within two grains after 6,000 undisturbed ticks, fresh and reloaded.
`test/t_bot_rescue.js` covers deep basalt burial, nearest/upward and diagonal nudges,
unexplored-pocket rejection, newest open trail points, ring wrap/load reset, bounded searches,
and flight through rock stopping at open space. Run `python3 test/t_bot_rescue.py` for the
focused pixel-browser check: both rescue messages and real keyboard flight out of basalt;
screenshots go to `shots/t72-{retrace,escape,open}.png`.
## Browser tests ## Browser tests
- They need the `.venv` from README.md → Develop; plain `python3` switches to it on its own. - They need the `.venv` from README.md → Develop; plain `python3` switches to it on its own.
+15
View File
@@ -4,6 +4,21 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-29: t72 rescue the bot from buried natural rock (Codex)
- Added explored nearest/up-first nudges, a 256-point typed-array trail and messaged
retrace/fly-through escape. Trail update O(1), allocation-free; searches capped at 696
candidates per burial. Trail resets/rebuilds on load; no worldgen/save format changes.
- Files: src/sim/{bot,world}.js, test/t_bot_rescue.{js,py}, docs/{DESIGN,TESTING,PROJECT_MAP}.md,
tasks/t72* and this log. Regressions failed first; all 39 headless suites pass (18 rescue checks).
- Only browser test: python3 test/t_bot_rescue.py (messages, real keyboard escape, pixel,
no page errors); inspected shots/t72-{retrace,escape,open}.png. No full browser suite.
- tools/perf.py settled/pixel before -> after: median frame interval 16.67 -> 16.67 ms;
CPU frame p50/p95 zoom 2: 9.71/11.34 -> 9.02/10.40 ms, zoom 6: 5.26/7.77 -> 4.19/5.01 ms.
Short samples show no regression, not a proven speedup. Sandbox WebGPU failed; host GPU passed.
Evidence: /tmp/t72-{red,headless,browser,before,after}.log and before/after JSON.
Owner playtest pending. No commit.
## 2026-10-29: t96 wake new-world lakes and keep trees off their banks (Codex) ## 2026-10-29: t96 wake new-world lakes and keep trees off their banks (Codex)
- Wake water after startup reset; omit whole tree candidates on wet/water-connected low banks. - Wake water after startup reset; omit whole tree candidates on wet/water-connected low banks.
+52 -8
View File
@@ -8,6 +8,46 @@ function botOverlapSolid(x, y) {
for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) if (botBlocked(cy * W + cx)) return true; for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) if (botBlocked(cy * W + cx)) return true;
return false; return false;
} }
// Only visited, open positions enter this fixed ring; normal recording is O(1).
const BOT_TRAIL_SIZE = 256;
const botTrailX = new Float64Array(BOT_TRAIL_SIZE), botTrailY = new Float64Array(BOT_TRAIL_SIZE);
let botTrailHead = 0, botTrailCount = 0, botEscape = false;
function botTrailRecord() {
const last = (botTrailHead + BOT_TRAIL_SIZE - 1) % BOT_TRAIL_SIZE;
const dx = bot.x - botTrailX[last], dy = bot.y - botTrailY[last];
if (botTrailCount && dx * dx + dy * dy < BS * BS) return;
botTrailX[botTrailHead] = bot.x; botTrailY[botTrailHead] = bot.y;
botTrailHead = (botTrailHead + 1) % BOT_TRAIL_SIZE;
botTrailCount = Math.min(BOT_TRAIL_SIZE, botTrailCount + 1);
}
// Precompute once: 440 probes within three blocks, nearest first and up on ties.
const BOT_NUDGES = [];
for (let y = -12; y <= 12; y++) for (let x = -12; x <= 12; x++) {
if (x * x + y * y > 0 && x * x + y * y <= 144) BOT_NUDGES.push([x, y]);
}
BOT_NUDGES.sort((a, b) => a[0] * a[0] + a[1] * a[1] - b[0] * b[0] - b[1] * b[1] || a[1] - b[1] || a[0] - b[0]);
function botNudgeOpen(x, y) {
if (botOverlapSolid(x, y)) return false;
for (let cy = Math.floor(y); cy < Math.ceil(y + bot.h); cy++)
for (let cx = Math.floor(x); cx < Math.ceil(x + bot.w); cx++) if (fogSeen(cx, cy) < 255) return false;
return true;
}
function botRescue() {
for (let k = 0; k < BOT_NUDGES.length; k++) {
const x = bot.x + BOT_NUDGES[k][0], y = bot.y + BOT_NUDGES[k][1];
if (botNudgeOpen(x, y)) { bot.x = x; bot.y = y; return; }
}
// At most 256 checks. Discard the buried end so the next retrace stays behind us.
for (let k = 0; k < botTrailCount; k++) {
const i = (botTrailHead + BOT_TRAIL_SIZE - 1 - k) % BOT_TRAIL_SIZE;
if (botOverlapSolid(botTrailX[i], botTrailY[i])) continue;
bot.x = botTrailX[i]; bot.y = botTrailY[i]; bot.vx = bot.vy = 0;
botTrailHead = (i + 1) % BOT_TRAIL_SIZE; botTrailCount -= k;
emit({t: 'toast', text: 'Dug out: back where you came from'}); return;
}
botEscape = true;
emit({t: 'toast', text: 'Dug out: Fly through the rock to open space'});
}
// Flying straight into a lip of rock a grain or two deep at the edge of the bot, it slips // Flying straight into a lip of rock a grain or two deep at the edge of the bot, it slips
// sideways round it instead of stopping dead, so it threads tunnels and holes it nearly fits. // sideways round it instead of stopping dead, so it threads tunnels and holes it nearly fits.
const SLIDE_MAX = 2.5, SLIDE_STEP = 0.34; const SLIDE_MAX = 2.5, SLIDE_STEP = 0.34;
@@ -37,19 +77,23 @@ function botStep(ix, iy, boost) {
bot.vx *= 0.885; bot.vy *= 0.885; bot.vx *= 0.885; bot.vy *= 0.885;
if (ix) bot.face = ix > 0 ? 1 : -1; if (ix) bot.face = ix > 0 ? 1 : -1;
bot.thrust = Math.min(1, Math.hypot(ix, iy)); bot.thrust = Math.min(1, Math.hypot(ix, iy));
if (botOverlapSolid(bot.x, bot.y)) { const buried = botOverlapSolid(bot.x, bot.y);
// unstick: search upward then sideways for a free spot if (!buried) {
for (let k = 1; k < 40; k++) { botEscape = false;
if (!botOverlapSolid(bot.x, bot.y - k)) { bot.y -= k; break; } if (!botTrailCount) botTrailRecord();
if (!botOverlapSolid(bot.x - k, bot.y)) { bot.x -= k; break; } } else if (!botEscape) botRescue();
if (!botOverlapSolid(bot.x + k, bot.y)) { bot.x += k; break; }
}
}
const steps = Math.ceil(Math.max(Math.abs(bot.vx), Math.abs(bot.vy)) / 0.45) || 1; const steps = Math.ceil(Math.max(Math.abs(bot.vx), Math.abs(bot.vy)) / 0.45) || 1;
const sx = bot.vx / steps, sy = bot.vy / steps; const sx = bot.vx / steps, sy = bot.vy / steps;
for (let s = 0; s < steps; s++) { for (let s = 0; s < steps; s++) {
if (botEscape) {
bot.x = Math.max(0, Math.min(W - bot.w, bot.x + sx));
bot.y = Math.max(0, Math.min(H - bot.h, bot.y + sy));
if (!botOverlapSolid(bot.x, bot.y)) botEscape = false;
continue;
}
if (sx) { if (!botOverlapSolid(bot.x + sx, bot.y)) bot.x += sx; else if (!(ix && !iy && botSlide(0, sx))) { bot.vx *= -0.15; } } if (sx) { if (!botOverlapSolid(bot.x + sx, bot.y)) bot.x += sx; else if (!(ix && !iy && botSlide(0, sx))) { bot.vx *= -0.15; } }
if (sy) { if (!botOverlapSolid(bot.x, bot.y + sy)) bot.y += sy; else if (!(iy && !ix && botSlide(1, sy))) { bot.vy *= -0.15; } } if (sy) { if (!botOverlapSolid(bot.x, bot.y + sy)) bot.y += sy; else if (!(iy && !ix && botSlide(1, sy))) { bot.vy *= -0.15; } }
} }
if (!botEscape) botTrailRecord();
// loose grains, liquids and gases pass straight through the bot: it never moves them // loose grains, liquids and gases pass straight through the bot: it never moves them
} }
+1
View File
@@ -46,6 +46,7 @@ function sharedArray(Type, length) {
} }
function alloc(w, h) { function alloc(w, h) {
resetDevConsole(); resetDevConsole();
botTrailHead = botTrailCount = 0; botEscape = false;
W = w; H = h; N = w * h; W = w; H = h; N = w * h;
fogAlloc(); fogAlloc();
CW = Math.ceil(w / 32); CH = Math.ceil(h / 32); NCH = CW * CH; CW = Math.ceil(w / 32); CH = Math.ceil(h / 32); NCH = CW * CH;
+56
View File
@@ -0,0 +1,56 @@
// 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();
+48
View File
@@ -0,0 +1,48 @@
"""t72: rescue messages and keyboard escape in the pixel game."""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / 'tools'))
from use_venv import ensure_playwright; ensure_playwright()
from browser import launch, consume_gpu_option
consume_gpu_option()
ROOT = pathlib.Path(__file__).resolve().parents[1]
with launch(viewport={'width': 1280, 'height': 760}) as (_, page, url):
errors = []
page.on('pageerror', lambda e: errors.append(str(e)))
page.goto(url)
page.evaluate("""() => {
const enter=enterGame;enterGame=()=>{enter();GAME.paused=true;};
SET.look='pixel';SET.workerCap=0;SET.cameraLead=false;SET.autosave=0;
startNewWorld(7,768,600);
}""")
page.wait_for_function('GAME.running', timeout=60000)
page.evaluate("""() => {
mat.fill(EMPTY);flags.fill(0);temp.fill(20);bgm.fill(0);fog.fill(255);
machines=[null];molds=[];ents=[];flyers=[];treeBases=[];saplings=[];
weather.next=1e15;weather.shower=null;surfY.fill(300);updateRoof();
botTrailCount=botTrailHead=0;botEscape=false;bot.vx=bot.vy=0;bot.y=200;
for(let x=200;x<=360;x+=4){bot.x=x;botStep(0,0,false);}
for(let y=140;y<280;y++)for(let x=240;x<450;x++)setCell(y*W+x,BASALT,20,F_NAT);
document.querySelectorAll('.card,.toast').forEach(e=>e.remove());
botStep(0,0,false);drainEvents();
SET.zoom=6;resizeRender();snapCamera();renderDirty.fill(1);GPU.world=null;SM.surf=-1;
}""")
assert page.evaluate('bot.x===232 && !botOverlapSolid(bot.x,bot.y)')
assert page.get_by_text('Dug out: back where you came from', exact=True).is_visible()
page.screenshot(path=str(ROOT / 'shots/t72-retrace.png'))
page.evaluate("""() => {
document.querySelectorAll('.toast').forEach(e=>e.remove());
botTrailCount=botTrailHead=0;bot.x=360;bot.y=200;bot.vx=bot.vy=0;
botStep(0,0,false);drainEvents();snapCamera();
}""")
assert page.get_by_text('Dug out: Fly through the rock to open space', exact=True).is_visible()
page.screenshot(path=str(ROOT / 'shots/t72-escape.png'))
page.keyboard.down('d')
page.evaluate('GAME.paused=false')
page.wait_for_function('bot.x>=450 && !botEscape', timeout=15000)
page.keyboard.up('d')
page.evaluate('GAME.paused=true')
assert page.evaluate('!botOverlapSolid(bot.x,bot.y)')
page.screenshot(path=str(ROOT / 'shots/t72-open.png'))
assert not errors, errors
print('PASS pixel retrace toast, fallback toast, real keyboard flight through basalt and exit; no page errors')