v0.2.0-rc.3: pillar-up escape + advice in fallback + danger POI (#22)
* v0.2.0-rc.3: pillar-up escape + advice everywhere + danger POI
Closes the gap rc.2 left open. Live observation showed:
- Pi-coach extracted 5 high-quality lessons (do not explore.far at
night near zombies, etc.) but none of them fired (applied_count=0
across the board). Root cause: dispatcher consulted advice only on
the main curriculum path; the bot was falling into the wander/
explore.far FALLBACK after each gather attempt bailed, which
bypassed consult().
- Bot was wedged in a pit on (608, 90) with stone walls. recovery.
tunnel-out kept failing ("Digging aborted") because mining stone
with fists takes ~10s/block; pathfinder watchdog kills it.
This patch:
1. survive.pillar-up (runtime/skills/pillar-up.js) — new escape skill.
Places a placeable block under the bot and jumps onto it; repeats
up to 8 steps. No pickaxe required. Works in dirt/cobble/planks/
sand/gravel/wool/etc. The bot's vertical exit from any pit it can
stand in.
2. Wedged-emergency reflex (runtime/reflex.js). At the top of
curriculumReflex, if noProgressReason is wedged-like AND position
hasn't shifted ≥16 blocks in 60s AND no hostile in 6m AND pillar
block in inventory → dispatch survive.pillar-up. 2-min cooldown
between attempts.
3. consult() now also runs on the WANDER/explore.far fallback path
(runtime/reflex.js curriculumReflex). Pi-coach lessons can finally
take effect. If the fallback skill is overridden to a non-eligible
skill but the bot has a placeable block, falls back to pillar-up.
Outcomes feed reportAdviceOutcome so confidence stays grounded.
4. recordPOI("danger") on death (runtime/coach/postmortem.js). Spatial
memory now flags where the bot died, expires after 6h. POI table
was empty in rc.2.
5. SAFE_OVERRIDES extended (runtime/coach/advice.js): adds
survive.pillar-up and village.choose-base so coach lessons can
route there.
Tests: 255/255 green (+9 pillar-up).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* v0.2.0-rc.3 fixup: relax wedged-escape trigger
Drop the WEDGED_REASONS check — noProgressReason is a string that
may or may not be set when the bot is stuck. Fire pillar-up purely on
"no horizontal progress ≥ 60s, no hostile in 6m, placeable block in
inv". Pillar-up is a constructive no-op when it's not needed (places
one dirt under self) so the false-positive cost is small.
Live observation: rc.3 was deployed and bot was wedged with tunnel-out
repeatedly aborted on stone, but wedged-escape never fired because
the runtime's noProgressReason wasn't in my whitelist. Removing the
gate lets the trigger actually engage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit was merged in pull request #22.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { skill, __testing } from "./pillar-up.js";
|
||||
|
||||
const { pickPillarBlock, inPit, PILLAR_PREFERENCE } = __testing;
|
||||
|
||||
function makePos(x, y, z) {
|
||||
return {
|
||||
x, y, z,
|
||||
offset(dx, dy, dz) { return makePos(x + dx, y + dy, z + dz); },
|
||||
};
|
||||
}
|
||||
function mockBot({ items = [], blocks = {}, pos = makePos(0, 64, 0) } = {}) {
|
||||
const handlers = {};
|
||||
return {
|
||||
entity: { position: pos, yaw: 0, pitch: 0 },
|
||||
inventory: { items: () => items },
|
||||
blockAt(p) { return blocks[`${Math.floor(p.x)},${Math.floor(p.y)},${Math.floor(p.z)}`] ?? { name: "air" }; },
|
||||
on(ev, fn) { handlers[ev] = fn; },
|
||||
setControlState() {},
|
||||
async equip() { return true; },
|
||||
async look() { return true; },
|
||||
async placeBlock() { return true; },
|
||||
};
|
||||
}
|
||||
|
||||
test("pickPillarBlock: chooses preferred block from inventory", () => {
|
||||
const items = [
|
||||
{ name: "stone", count: 4, type: 1 },
|
||||
{ name: "dirt", count: 12, type: 2 },
|
||||
];
|
||||
const chosen = pickPillarBlock(mockBot({ items }));
|
||||
assert.equal(chosen.name, "dirt", "prefers dirt over stone");
|
||||
});
|
||||
|
||||
test("pickPillarBlock: returns null when nothing placeable", () => {
|
||||
const chosen = pickPillarBlock(mockBot({ items: [{ name: "carrot", count: 3 }] }));
|
||||
assert.equal(chosen, null);
|
||||
});
|
||||
|
||||
test("pickPillarBlock: falls back to wood-like names", () => {
|
||||
const chosen = pickPillarBlock(mockBot({ items: [{ name: "oak_planks", count: 5 }] }));
|
||||
assert.equal(chosen.name, "oak_planks");
|
||||
});
|
||||
|
||||
test("inPit: detects walls in cardinal directions", () => {
|
||||
const blocks = {
|
||||
"1,65,0": { name: "stone" },
|
||||
"-1,65,0": { name: "stone" },
|
||||
};
|
||||
const bot = mockBot({ blocks });
|
||||
assert.equal(inPit(bot), true, "two walls = pit");
|
||||
|
||||
const open = mockBot({ blocks: {} });
|
||||
assert.equal(inPit(open), false);
|
||||
});
|
||||
|
||||
test("inPit: detects walls 2 blocks away too", () => {
|
||||
const blocks = {
|
||||
"2,65,0": { name: "stone" },
|
||||
"0,65,-2": { name: "stone" },
|
||||
};
|
||||
const bot = mockBot({ blocks });
|
||||
assert.equal(inPit(bot), true);
|
||||
});
|
||||
|
||||
test("PILLAR_PREFERENCE: dirt is highest priority", () => {
|
||||
assert.equal(PILLAR_PREFERENCE[0], "dirt");
|
||||
assert.ok(PILLAR_PREFERENCE.includes("cobblestone"));
|
||||
});
|
||||
|
||||
test("skill: preconditions fail without placeable block", () => {
|
||||
const ctx = { bot: mockBot({ items: [{ name: "carrot", count: 1 }] }) };
|
||||
const pre = skill.preconditions(ctx);
|
||||
assert.equal(pre.ok, false);
|
||||
assert.equal(pre.code, "missing_material");
|
||||
});
|
||||
|
||||
test("skill: preconditions pass with dirt", () => {
|
||||
const ctx = { bot: mockBot({ items: [{ name: "dirt", count: 8 }] }) };
|
||||
const pre = skill.preconditions(ctx);
|
||||
assert.equal(pre.ok, true);
|
||||
});
|
||||
|
||||
test("skill: id and timeout are sensible", () => {
|
||||
assert.equal(skill.id, "survive.pillar-up");
|
||||
assert.ok(skill.timeoutMs >= 30_000 && skill.timeoutMs <= 90_000);
|
||||
});
|
||||
Reference in New Issue
Block a user