Files
pepa-pi-bot/runtime/skills/escape-pit-safe.js
T
mayatnikovandClaude Opus 4.7 08ac2e1bc1 feat(v0.3.1): real survival patterns — biome-aware scout, wedge-relocate, escape-pit-safe
Operator reported the bot wandered the same 50×50 patch for 2 hours
without making any progress toward food. Diagnosis showed three root
causes; this commit addresses all five open improvement_requests
the LLM (postmortem + tuner) flagged automatically.

Research basis (`Voyager`, `Plan4MC`, `GITM`, `Mindcraft`):
  - Coverage / commit-to-cardinal exploration when local scan fails
  - Biome-aware strategy switching using a static affordance table
  - Wedge detector above the skill layer that triggers RELOCATE not
    RETRY (per-skill stuck checks reset on re-entry — useless)
  - Time-in-region bbox heuristic + need-duration AND skill-cycle gate

Concrete changes:

1. `runtime/goal/storyline.js`
   - orient_self.completed: added timeout fallback (HP=full + session
     >120s → done) so barren biomes don't block the bot on step 1.
     Closes improvement #2 'Нет навыка оценки когда сменить район'.
   - first_food.suggestSkill: now picks survive.scout-food (new) when
     no passive mob is nearby; falls back to survive.acquire-food only
     when something is in immediate range.

2. `runtime/biome-affordances.js` (new)
   - Static table: 40+ biomes → {has_passive_mobs, has_trees,
     has_water, has_crops, livable}.
   - Unknown biomes return optimistic defaults to avoid regressions.
   - Closes improvement #1 'Нет навыка целевого поиска еды по биому'.

3. `runtime/skills/scout-food.js` (new — survive.scout-food)
   - Tiered strategy: biome check → scan 32 → scan 64 → commit a
     cardinal for 200 blocks rescanning every 16. On cardinal
     exhaustion, returns code:"exhausted" so the curriculum can
     escalate to village.relocate.
   - In barren biomes (desert/ocean/snowy_plains) the scan is
     SKIPPED — bot walks straight toward the nearest neighbour
     biome that affords passive mobs (8-direction biome probe at
     radius 64).

4. `runtime/awareness/wedge-detector.js` (new)
   - Rolling 10-min position bbox tracker. observe() called every
     tick; isWedged() returns true when bbox<50 AND active need
     unmet >5min AND skill cycles ≥3.
   - markRelocationStarted() suppresses further wedge firings
     until the bot has displaced ≥200b — prevents stack overflow
     of relocate calls.
   - Lives ABOVE the skill layer (in runtime/reflex.js), because
     any per-skill stuck check resets on re-entry.

5. `runtime/skills/relocate.js` (new — village.relocate)
   - 300-block walk in least-recently-used cardinal (per-incident
     memory in ctx.recentRelocations).
   - Re-paths every 32 blocks, soft-tolerates pathfinder failures
     (3 consecutive throws → exit with code:"stuck_in_place").
   - Closes improvement #2 + #4 ('low success rate trigger').

6. `runtime/skills/escape-pit-safe.js` (new — recovery.escape-pit-safe)
   - Surveys 4 cardinals AND ceiling height before committing.
     Picks the direction with most open blocks (≥3, no lava).
     Falls through to pillar-up only if ceiling clear ≥4b. Returns
     code:"no_strategy" if both blocked so curriculum can escalate
     to relocate.
   - Closes improvement #3 'Нет навыка для безопасного выхода'.

7. `runtime/reflex.js`
   - Wedge detector wired before manifesto/storyline. If wedge.wedged
     is true, dispatches village.relocate directly and returns —
     bypasses every other branch.
   - ctx.disableWedge flag for tests.

8. `runtime/coach/advisor-trigger.js`
   - LLM provider outage backoff: 3 consecutive http_400 / timeout /
     network_error → suppress advisor for 10 min. Today's TimeWeb
     gpt-5.4-mini was 400'ing for an hour straight; we were spending
     trigger budget on dead calls. Closes improvement implicit gap
     in #5.

9. `runtime/bot.js`
   - Tracks botSpawnedAt; snapshot._sessionMs exposed for storyline
     orient_self timeout fallback.

Tests: 396 green (was 378, +18):
  - runtime/biome-affordances.test.js — 8 tests
  - runtime/awareness/wedge-detector.test.js — 9 tests
  - runtime/goal/storyline.test.js — 1 new test (orient_self timeout)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:01:39 +03:00

160 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// recovery.escape-pit-safe — multi-strategy escape from a hole / pit.
//
// Why this exists alongside recovery.tunnel-out and survive.pillar-up:
//
// tunnel-out tries to dig sideways through walls — fails on
// unbreakable terrain or when there's no clear horizontal exit.
// pillar-up places blocks under the bot and jumps — fails when
// there's a ceiling block above the column.
//
// Both fail silently in a deep cave or 2×2 hole with a ceiling.
// escape-pit-safe surveys options first, then commits:
//
// 1. Scan 4 cardinals at head height + foot height. Pick the one
// with the closest open path to surface (defined as: column
// with sky visibility above OR ≥3 air blocks horizontal followed
// by stairs / slope up).
// 2. If a clear horizontal path exists → recovery.tunnel-out toward it.
// 3. If no horizontal path BUT ceiling is open above us → pillar-up.
// 4. If both blocked → return "stuck" and let the LLM-advisor flag a
// genuine improvement (e.g. "need water-bucket-MLG", "need to
// mine through stone").
import { info, warn } from "../log.js";
import { runSkill } from "./index.js";
const SCAN_RADIUS = 6;
const HEAD_OFFSET_Y = 1;
function blockNameAt(bot, x, y, z) {
try {
const b = bot.blockAt?.({ x: Math.floor(x), y: Math.floor(y), z: Math.floor(z) });
return b?.name ?? null;
} catch { return null; }
}
function isAir(name) { return name === "air" || name === "cave_air" || name === "void_air"; }
function isWater(name) { return name === "water" || name === "flowing_water"; }
function isLava(name) { return name === "lava" || name === "flowing_lava"; }
function isPassable(name) { return isAir(name) || (name && /carpet|button|torch|grass$/.test(name)); }
// Look up to 32 blocks straight up from (x,y,z). Return distance to first
// non-air block, or Infinity if open all the way (this is a sky-visible
// column we could pillar-up out of).
function ceilingDistance(bot, x, y, z) {
for (let dy = HEAD_OFFSET_Y + 1; dy <= 32; dy++) {
const n = blockNameAt(bot, x, y + dy, z);
if (!n) return dy; // unloaded chunk = no info; treat conservatively
if (!isAir(n)) return dy;
}
return Infinity;
}
// Scan SCAN_RADIUS blocks in each cardinal at head height. Return
// summary per direction: open-block count, lava/water hits, first
// non-air block name.
function scanCardinals(bot) {
const pos = bot?.entity?.position;
if (!pos) return [];
const head = pos.offset?.(0, HEAD_OFFSET_Y, 0) ?? { x: pos.x, y: pos.y + HEAD_OFFSET_Y, z: pos.z };
const dirs = [
{ name: "N", dx: 0, dz: -1 },
{ name: "E", dx: 1, dz: 0 },
{ name: "S", dx: 0, dz: 1 },
{ name: "W", dx: -1, dz: 0 },
];
return dirs.map((d) => {
let openBlocks = 0;
let lavaAt = -1;
let waterAt = -1;
let firstBlock = null;
for (let r = 1; r <= SCAN_RADIUS; r++) {
const x = head.x + d.dx * r;
const z = head.z + d.dz * r;
const n = blockNameAt(bot, x, head.y, z);
if (isLava(n) && lavaAt < 0) lavaAt = r;
if (isWater(n) && waterAt < 0) waterAt = r;
if (isPassable(n)) {
openBlocks++;
if (firstBlock === null) firstBlock = "(air)";
} else {
if (firstBlock === null) firstBlock = n ?? "(unknown)";
break;
}
}
return { ...d, openBlocks, lavaAt, waterAt, firstBlock };
});
}
export const skill = Object.freeze({
id: "recovery.escape-pit-safe",
title: "Escape a pit — survey directions and pick the safest exit",
timeoutMs: 90_000,
preconditions(ctx) {
if (!ctx?.bot?.entity?.position) return { ok: false, code: "no_bot", detail: "bot missing" };
return { ok: true };
},
async execute(ctx) {
const bot = ctx.bot;
const pos = bot.entity.position;
const dirs = scanCardinals(bot);
const ceiling = ceilingDistance(bot, pos.x, pos.y, pos.z);
info(
"action",
`escape-pit-safe: cardinals=${dirs.map((d) => `${d.name}:${d.openBlocks}/${d.firstBlock}`).join(" ")} ceiling=${ceiling === Infinity ? "open" : ceiling + "b"}`,
);
// 1. Choose the direction with most open blocks (≥3) and no lava.
const horizontal = dirs
.filter((d) => d.openBlocks >= 3 && d.lavaAt < 0)
.sort((a, b) => b.openBlocks - a.openBlocks)[0];
if (horizontal) {
info("action", `escape-pit-safe: walking out via ${horizontal.name} (${horizontal.openBlocks}b clear)`);
// Delegate to wander step or simple controlled walk. Easier:
// invoke recovery.tunnel-out with a hint of the chosen direction.
const res = await runSkill("recovery.tunnel-out", ctx, { preferredDir: horizontal.name });
return {
ok: !!res?.ok,
code: res?.code ?? (res?.ok ? "done" : "tunnel_failed"),
detail: { strategy: "horizontal_walk", direction: horizontal.name, tunnelResult: res?.detail },
worldDelta: res?.worldDelta ?? { strategy: "horizontal_walk", direction: horizontal.name },
};
}
// 2. No horizontal exit — try pillar-up only if ceiling is open
// for the bot's height (≥3 blocks: head + 1 build + 1 ceiling slack).
if (ceiling >= 4) {
info("action", `escape-pit-safe: ceiling clear (${ceiling}b), trying pillar-up`);
const res = await runSkill("survive.pillar-up", ctx);
return {
ok: !!res?.ok,
code: res?.code ?? (res?.ok ? "done" : "pillar_failed"),
detail: { strategy: "pillar_up", ceiling, pillarResult: res?.detail },
worldDelta: res?.worldDelta ?? { strategy: "pillar_up" },
};
}
// 3. Both blocked — surrender. The LLM-advisor will see this in
// recent dispatches and can suggest village.relocate or flag a
// new skill request.
warn("action", `escape-pit-safe: no viable strategy (horizontal blocked + ceiling ${ceiling}b)`);
return {
ok: false,
code: "no_strategy",
detail: { horizontal: dirs.map((d) => ({ d: d.name, open: d.openBlocks, lava: d.lavaAt })), ceiling },
worldDelta: null,
};
},
recover(ctx, result) {
if (result.code === "no_strategy") {
return { hint: "wander", reason: "no viable pit-escape; let curriculum try a different action" };
}
return null;
},
});
// Test exports
export const __testing = { scanCardinals, ceilingDistance, isAir, isPassable, SCAN_RADIUS };