The wedge wasn't only in the manifesto layer; several mechanical bugs kept the bot in a dead random-walk: - storyline / manifesto / curriculum: "local food" now means an edible passive mob within <=32 blocks. A distant chicken or a cod no longer fools the bot into dispatching acquire-food (which then fails on no_path). Long-range food goes through scout-food instead. - scout-food: partial approach to a target now counts as progress (approached_target, e.g. moved:14); a blocked heading is NOT counted as movement; added blind/tunnel fallback so it doesn't die when the pathfinder can't route cleanly. - acquire-food: on no_path it now also tries a blind/tunnel approach to the animal; no_drop routes back into food scouting instead of giving up. - explore.far / relocate / flee: fewer false "done" results (micro-steps no longer counted as success), more genuine escapes from stuck. - scripts/show-story.js: live IPC now actually renders the current storyline step. Verification: scripts/lint-patch.js clean; npm test 404/404 green; bot relaunched in tmux `pepa`. Live logs show real progress — bot switched to survive.scout-food, approached the chicken (approached_target moved:14), then reached survive.acquire-food: hunting chicken. Food isn't fully closed yet but the remaining issue is concrete pickup/drop, not dead random-walk. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
117 lines
3.4 KiB
JavaScript
117 lines
3.4 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { pickActiveNeed, describeActiveNeed, _resetForTest } from "./state.js";
|
|
|
|
function snap(overrides = {}) {
|
|
return {
|
|
connected: true,
|
|
health: 20,
|
|
food: 20,
|
|
hasFood: false,
|
|
inventory: {},
|
|
equipment: { hand: null, head: null, torso: null, legs: null, feet: null },
|
|
nearbyBlocks: {},
|
|
hazards: { footBlock: "grass_block", belowBlock: "dirt", headBlock: "air" },
|
|
isDay: true,
|
|
hostileCount: 0,
|
|
closestHostile: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("pickActiveNeed: disconnected → null", () => {
|
|
_resetForTest();
|
|
assert.equal(pickActiveNeed({ connected: false }), null);
|
|
assert.equal(pickActiveNeed(null), null);
|
|
});
|
|
|
|
test("pickActiveNeed: fresh spawn → L0 alive if zero food", () => {
|
|
_resetForTest();
|
|
const a = pickActiveNeed(snap({ food: 0 }));
|
|
assert.equal(a.need.id, "alive");
|
|
assert.equal(a.skillId, "survive.scout-food");
|
|
});
|
|
|
|
test("pickActiveNeed: hp ok, no food in inventory → L1 food (scout)", () => {
|
|
_resetForTest();
|
|
const a = pickActiveNeed(snap());
|
|
assert.equal(a.need.id, "food");
|
|
assert.equal(a.skillId, "survive.scout-food");
|
|
});
|
|
|
|
test("pickActiveNeed: food covered → L2 tools_wood (gather logs)", () => {
|
|
_resetForTest();
|
|
const a = pickActiveNeed(snap({ inventory: { bread: 8 } }));
|
|
assert.equal(a.need.id, "tools_wood");
|
|
assert.equal(a.skillId, "gather.logs");
|
|
});
|
|
|
|
test("pickActiveNeed: tools wood done → L3 shelter (gather wool)", () => {
|
|
_resetForTest();
|
|
const a = pickActiveNeed(snap({
|
|
inventory: {
|
|
bread: 8,
|
|
wooden_pickaxe: 1, wooden_axe: 1, wooden_sword: 1,
|
|
},
|
|
}));
|
|
assert.equal(a.need.id, "shelter_basic");
|
|
// no wool, no bed → gather.wool
|
|
assert.equal(a.skillId, "gather.wool");
|
|
});
|
|
|
|
test("pickActiveNeed: shelter done → L4 tools_stone", () => {
|
|
_resetForTest();
|
|
const a = pickActiveNeed(snap({
|
|
nearbyBlocks: { beds: 1 },
|
|
inventory: {
|
|
bread: 8,
|
|
wooden_pickaxe: 1, wooden_axe: 1, wooden_sword: 1,
|
|
},
|
|
}));
|
|
assert.equal(a.need.id, "tools_stone");
|
|
assert.equal(a.skillId, "gather.stone");
|
|
});
|
|
|
|
test("pickActiveNeed: armor pursue=null → ladder skips to food_security", () => {
|
|
_resetForTest();
|
|
// Everything up through tools_stone satisfied, no armor (pursue=null).
|
|
// Should advance to food_security, not stall.
|
|
const a = pickActiveNeed(snap({
|
|
nearbyBlocks: { beds: 1 },
|
|
inventory: {
|
|
bread: 8,
|
|
wooden_pickaxe: 1, wooden_axe: 1, wooden_sword: 1,
|
|
stone_pickaxe: 1, stone_axe: 1, stone_sword: 1,
|
|
},
|
|
}));
|
|
assert.equal(a.need.id, "food_security");
|
|
assert.ok(a.blockedNeeds.some((b) => b.id === "armor_basic"), "armor_basic recorded as blocked");
|
|
});
|
|
|
|
test("pickActiveNeed: hostile imminent + low HP → L0 takes over", () => {
|
|
_resetForTest();
|
|
const a = pickActiveNeed(snap({
|
|
health: 6,
|
|
closestHostile: { name: "creeper", distance: 3 },
|
|
inventory: { bread: 8, wooden_pickaxe: 1, wooden_axe: 1, wooden_sword: 1 },
|
|
nearbyBlocks: { beds: 1 },
|
|
}));
|
|
assert.equal(a.need.id, "alive");
|
|
assert.equal(a.skillId, "survive.flee");
|
|
});
|
|
|
|
test("describeActiveNeed: returns 'L<n> <id> → <skill>'", () => {
|
|
_resetForTest();
|
|
const s = describeActiveNeed(snap());
|
|
assert.match(s, /^L1 food → /);
|
|
});
|
|
|
|
test("pickActiveNeed: caches within TTL — same snapshot ref returns same result", () => {
|
|
_resetForTest();
|
|
const s = snap();
|
|
const a = pickActiveNeed(s);
|
|
const b = pickActiveNeed(s);
|
|
assert.equal(a, b, "second call returns cached object");
|
|
});
|