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>
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