v0.4.0 vNext — closed-loop world model + settlement contract (#29)
* feat(v0.4.0): vNext — closed-loop world model + settlement contract
Implements the vNext architecture from the research doc: demote the noisy
multi-rail planner in favour of a closed loop (world truth → invariant check)
plus a single utility-driven goal authority.
L1 services (fix no_drop / silent pathfinder hang first):
- InventoryLedger: diff-based "did I actually get it" verifier; acquire-food
now confirms via ledger.gainedSince instead of the unreliable count/event.
- MotionService.gotoSafe: wall-clock timeout + progress watchdog +
path_update(noPath/timeout) → structured {reached|stuck|timeout|nopath}.
L3 plan — unify the three competing rails (curriculum/manifesto/storyline):
- Settlement Contract: ordered M0–M9 milestones, each invariant-checked
against an authoritative world view (early steps delegate to the proven
curriculum; late game adds farming).
- InvariantChecker + predicate library; GoalManager selects the lowest unmet
milestone via utility argmax (food-urgency preempts, DEPS-style).
- Wired into the scheduler: bot.js precomputes snapshot.contract; reflex.js
consumes it in place of the storyline rail. Manifesto L0 still preempts.
Eval + robustness:
- Village Score (single 0..1 metric) on the snapshot + TUI "build" line.
- survive.dig-in skill + dusk_dig_in mode (exposed at night, no bed → cover).
- approach_block helper (GoalNear + lookAt, avoids GoalLookAtBlock #341).
+28 new tests (450 total green). LLM remains entirely off the tick path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(v0.4.0): finish vNext plan — anti-loop, skill-graph, worldDelta diff, flee→motion
Completes the remaining v0.4.0 plan items and one fix motivated by a live
in-game observation (flee hanging 30s against a persistent zombie).
- flee → MotionService.gotoSafe: structured {stuck|timeout|nopath} in ~4s with
a blind-retreat fallback, instead of the observed 30s pathfinder hang + 3
watchdog replans. Movements setup guarded so it is unit-testable.
- QW5 anti-loop (runtime/anti-loop.js): same skill failing >=3x in 5min →
30min blacklist (reflex shouldSkip) + one-shot improvement_request
(bot.js drainFired -> writeProposal).
- 4.1 closed-loop worldDelta: runSkill snapshots inventory before execute and
attaches the real delta (_invObserved) to every successful result; opt-in
skill.expectGain asserts the claimed gain or returns world_unchanged.
- 3.6 skill-graph (Plan4MC): declarative requires/produces for ~20 skills;
prerequisitesMet/canRun/runnableFrontier; GoalManager annotates suggestions
with blockedBy when prereqs are unmet.
+22 tests (472 total green). Live smoke confirmed dig-in works and no new
errors; flee loop is what this commit's flee migration addresses.
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 #29.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// Shared skill helpers.
|
||||
//
|
||||
// approachBlock — walk up to a target block and look at it, WITHOUT using
|
||||
// pathfinder's GoalLookAtBlock. That goal raycasts collision boxes only
|
||||
// (mineflayer-pathfinder #341), so for non-collision targets — crops, torches,
|
||||
// fences, saplings, buttons — the raycast never hits and the goal is never
|
||||
// "reached". Instead we GoalNear the block and lookAt its centre, which works
|
||||
// regardless of the target's collision shape (research QW4).
|
||||
|
||||
import pathfinderPkg from "mineflayer-pathfinder";
|
||||
const { goals } = pathfinderPkg;
|
||||
|
||||
export function blockPos(target) {
|
||||
const p = target?.position ?? target;
|
||||
if (!p || p.x == null) return null;
|
||||
return { x: Math.floor(p.x), y: Math.floor(p.y), z: Math.floor(p.z) };
|
||||
}
|
||||
|
||||
export function blockCenter(target) {
|
||||
const p = blockPos(target);
|
||||
return p ? { x: p.x + 0.5, y: p.y + 0.5, z: p.z + 0.5 } : null;
|
||||
}
|
||||
|
||||
export function withinReach(botPos, target, reach = 4) {
|
||||
const c = blockCenter(target);
|
||||
if (!botPos || !c) return false;
|
||||
return Math.hypot(botPos.x - c.x, botPos.y - c.y, botPos.z - c.z) <= reach;
|
||||
}
|
||||
|
||||
// Returns { ok, code, distance? }. Uses ctx.motion.gotoSafe when available
|
||||
// (structured, can't hang); falls back to a raw goto otherwise.
|
||||
export async function approachBlock(ctx, target, opts = {}) {
|
||||
const bot = ctx?.bot;
|
||||
const pos = blockPos(target);
|
||||
if (!bot || !pos) return { ok: false, code: "no_target" };
|
||||
|
||||
const reach = opts.reach ?? 3;
|
||||
const reachCheck = opts.reachCheck ?? 4;
|
||||
|
||||
if (!withinReach(bot.entity?.position, target, reachCheck)) {
|
||||
const goal = new goals.GoalNear(pos.x, pos.y, pos.z, reach);
|
||||
let res;
|
||||
if (ctx.motion?.gotoSafe) {
|
||||
res = await ctx.motion.gotoSafe(goal, { timeoutMs: opts.timeoutMs ?? 20_000, label: "approach_block" });
|
||||
} else {
|
||||
try {
|
||||
await bot.pathfinder.goto(goal);
|
||||
res = { ok: true, code: "reached" };
|
||||
} catch (e) {
|
||||
res = { ok: false, code: "error", detail: e?.message ?? String(e) };
|
||||
}
|
||||
}
|
||||
if (!res.ok && !withinReach(bot.entity?.position, target, reachCheck)) {
|
||||
return { ok: false, code: res.code, detail: res.detail ?? null };
|
||||
}
|
||||
}
|
||||
|
||||
// Look at the block centre — NOT GoalLookAtBlock (issue #341).
|
||||
try { await bot.lookAt(blockCenter(target), true); } catch {}
|
||||
|
||||
const here = bot.entity?.position;
|
||||
const c = blockCenter(target);
|
||||
const distance = here && c ? Math.round(Math.hypot(here.x - c.x, here.y - c.y, here.z - c.z) * 10) / 10 : null;
|
||||
return { ok: true, code: "reached", distance };
|
||||
}
|
||||
|
||||
export const _internal = { blockPos, blockCenter, withinReach };
|
||||
Reference in New Issue
Block a user