fix(runtime): unstick wander loop + chop radius + explore.far skill

Follow-up to the iteration-1 fixes. Live smoke on play.xmatic.team
revealed the bot was spawning into a tree-less plain (no log within
32 blocks of spawn), looping wander→gather→no_target→wander
forever inside a 16-block box.

- runtime/actions.js: chopNearestTree search radius 32 → 64 (still no
  trees on this spawn, but a normal biome will be served well by it).
  wander now has a blind-walk fallback when pathfinder times out
  (look+forward+jump for 3 s) so the bot at least unsticks from leaves
  or pillars. Pathfinder timeout reduced 30 s → 15 s.
- runtime/skills/explore-far.js: new explore.far skill — walks ~48
  blocks in a quadrant (NE/SE/SW/NW, rotating per call) so successive
  hints actually circle the spawn instead of bouncing in place. Blind
  walk fallback included.
- runtime/reflex.js: when the scheduler is told to wander twice in a
  row by gather.* recover hints, it now dispatches explore.far instead
  so the bot actually leaves the patch it's stuck in. Resets the
  consecutiveWanderHints counter on any success.
- runtime/reflex.js (sleep): no longer dispatches when the bot has
  neither a bed in inventory NOR a known shelter/base location —
  saved one dispatch + 5-min cooldown per restart at night.
- runtime/reflex.js (eat): inventory check + lastEatAt always updated
  fix the eat-spam loop observed live (every tick fired "eat" → "no
  food in inventory" → again).
- runtime/skills/chop-logs.js: recognise "no log within ..." as
  no_target so the recover hint switches the bot to wander/explore.

npm test 124/124.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 11:18:36 +03:00
co-authored by Claude Opus 4.7
parent 29542f0559
commit 19dc8e12c6
6 changed files with 206 additions and 9 deletions
+20 -1
View File
@@ -66,12 +66,16 @@ test("defend wins over curriculum when hostile in melee", () => {
assert.match(dispatches[0].label, /attack zombie/);
});
test("eat wins over curriculum when food low and bot has food", () => {
test("eat wins over curriculum when food low and bot has food in inventory", () => {
const { ctx, dispatches } = makeCtx({
snapshot: {
connected: true,
health: 20,
food: 10,
// 2026-05-26: eat reflex now requires actual food in inventory
// to avoid the eat-spam loop that fired every tick on empty
// inventory.
inventory: { bread: 1 },
curriculum: { plan: { skillId: "gather.logs" } },
},
});
@@ -80,6 +84,21 @@ test("eat wins over curriculum when food low and bot has food", () => {
assert.equal(dispatches[0].label, "eat");
});
test("eat reflex does NOT dispatch when no food in inventory (no spam)", () => {
const { ctx, dispatches } = makeCtx({
snapshot: {
connected: true,
health: 20,
food: 10,
inventory: { dirt: 1 },
curriculum: { plan: { skillId: "gather.logs" } },
},
});
const out = runTick(ctx);
// Falls through to curriculum.
assert.equal(out.reflex, "curriculum");
});
test("curriculum dispatches suggested skill by id", () => {
const { ctx, dispatches } = makeCtx({
snapshot: {