diff --git a/runtime/actions.js b/runtime/actions.js index 5cbe9b4..896e047 100644 --- a/runtime/actions.js +++ b/runtime/actions.js @@ -412,59 +412,93 @@ export async function chopNearestTree(bot) { // ---- exploration ----------------------------------------------------------- +// wander — probe-then-go. Old version picked a random angle and trusted +// pathfinder; on this server pathfinder routinely times out (the bot is +// in a 3-block corridor, on a tree, in spawn area without good graph) +// and the blind-walk fallback then went in the SAME direction the +// pathfinder couldn't solve. 2026-05-26 fix: try every cardinal +// direction for 800 ms each, measure the actual Δ in-world, then commit +// to the best one for the rest of the budget. export async function wander(bot, radius = 12) { ensurePathfinder(bot); setMovementsForTravel(bot); - // Pick a random offset that's at least 6 blocks away — small enough to be - // safe, large enough to not be a no-op when we're stuck on the same block. + + const trials = await probeCardinalSteps(bot, 800); + const best = trials.reduce((b, t) => (t.dist > b.dist ? t : b), { dist: 0 }); + + // If nothing moved at all, the bot is wedged in all four cardinal + // directions (corner of a corridor, surrounded by leaves, hole). Try + // to jump + go in the most-promising direction (>0 movement) — gravity + // will help us drop to a lower y where there's space. + if (best.dist < 0.5) { + const fallback = trials.reduce((b, t) => (t.dist > b.dist ? t : b), { dist: 0, yaw: 0 }); + info("action", `wander: all cardinals blocked, jumping fallback yaw=${fallback.yaw?.toFixed?.(2)}`); + try { await bot.look(fallback.yaw ?? 0, 0, true); } catch {} + bot.setControlState("forward", true); + bot.setControlState("jump", true); + try { + await new Promise((r) => setTimeout(r, 2_500)); + } finally { + bot.setControlState("forward", false); + bot.setControlState("jump", false); + } + return { ok: true, detail: { mode: "wedged-jump", trials } }; + } + + // Commit to best direction. + const wantDist = Math.max(6, Math.min(radius, best.dist * 4 + 2)); const here = bot.entity.position; - const angle = Math.random() * Math.PI * 2; - const dist = 6 + Math.random() * (radius - 6); - const tx = Math.round(here.x + Math.cos(angle) * dist); - const tz = Math.round(here.z + Math.sin(angle) * dist); + const tx = Math.round(here.x + Math.sin(-best.yaw) * wantDist); + const tz = Math.round(here.z + Math.cos(-best.yaw) * wantDist); const ty = Math.round(here.y); - info("action", `wander: → ${tx},${ty},${tz} (dist=${dist.toFixed(1)})`); + info("action", `wander: best=${best.name}(Δ=${best.dist.toFixed(1)}) → ${tx},${ty},${tz}`); try { await withTimeout( bot.pathfinder.goto(new goals.GoalNear(tx, ty, tz, 2)), - 15_000, + 12_000, `wander(${tx},${tz})`, ); - return { ok: true, detail: { to: { x: tx, y: ty, z: tz } } }; + return { ok: true, detail: { to: { x: tx, y: ty, z: tz }, via: best.name } }; } catch (e) { - warn("action", `wander pathfinder failed: ${e.message} — falling back to blind walk`); - // Blind walk fallback: hold `forward` + `jump` for 3 seconds in - // the chosen direction. Pathfinder sometimes refuses to find a path - // when the bot is wedged in leaves/sand/water or sitting on a tree - // canopy — without this fallback the scheduler would loop wander → - // fail → wander → fail forever. The blind step at least unsticks - // the bot and lets the next tick re-scan blocks. + warn("action", `wander pathfinder failed: ${e.message} — continuing blind in ${best.name}`); + try { await bot.look(best.yaw, 0, true); } catch {} + bot.setControlState("forward", true); + bot.setControlState("jump", true); try { - await blindStepToward(bot, tx, tz, 3_000); - return { ok: true, detail: { to: { x: tx, y: ty, z: tz }, mode: "blind" } }; - } catch (e2) { - warn("action", `wander blind walk also failed: ${e2.message}`); - return { ok: false, detail: `pathfinder: ${e.message}; blind: ${e2.message}` }; + await new Promise((r) => setTimeout(r, 2_500)); + } finally { + bot.setControlState("forward", false); + bot.setControlState("jump", false); } + return { ok: true, detail: { to: { x: tx, y: ty, z: tz }, mode: "blind", via: best.name } }; } } -async function blindStepToward(bot, targetX, targetZ, durationMs) { - try { - const here = bot.entity.position; - const dx = targetX - here.x; - const dz = targetZ - here.z; - // Yaw such that +Z is south (0) and angles go clockwise looking down. - // Mineflayer uses radians. - const yaw = Math.atan2(-dx, -dz); - await bot.look(yaw, 0, true); +const CARDINAL_YAWS = [ + { name: "N", yaw: Math.PI }, + { name: "E", yaw: -Math.PI / 2 }, + { name: "S", yaw: 0 }, + { name: "W", yaw: Math.PI / 2 }, +]; + +async function probeCardinalSteps(bot, durationMs = 800) { + const trials = []; + for (const { name, yaw } of CARDINAL_YAWS) { + try { await bot.look(yaw, 0, true); } catch {} + const before = bot.entity.position.clone(); bot.setControlState("forward", true); - bot.setControlState("jump", true); - await new Promise((r) => setTimeout(r, durationMs)); - } finally { - bot.setControlState("forward", false); - bot.setControlState("jump", false); + try { + await new Promise((r) => setTimeout(r, durationMs)); + } finally { + bot.setControlState("forward", false); + } + const after = bot.entity.position; + const dist = Math.hypot(after.x - before.x, after.z - before.z); + trials.push({ name, yaw, dist }); + // settle physics + await new Promise((r) => setTimeout(r, 150)); } + return trials; } // ---- crafting -------------------------------------------------------------- diff --git a/runtime/compat.test.js b/runtime/compat.test.js index fc5e32d..5438a76 100644 --- a/runtime/compat.test.js +++ b/runtime/compat.test.js @@ -13,20 +13,16 @@ import { isManMadeBlockName, classifyArea, shouldAvoid } from "./claim-avoidance // We can't import owned-blocks.js until config-driven stateDir exists, // so it's tested via an isolated import in a temp dir below. -// 2026-05-26: canDig is FALSE on every profile because bot.dig silently -// fails on the live server (mineflayer #3888 / protocol 775). Once the -// 1.21.4 pin restores real digging, gather/travel/flee profiles can flip -// canDig back to true. -test("movement profile descriptor: gather has canDig=false (silent-dig safeguard)", () => { +test("movement profile descriptor: gather has canDig=true (real dig works)", () => { const d = describeProfile(PROFILES.GATHER); - assert.equal(d.canDig, false); + assert.equal(d.canDig, true); assert.equal(d.canPlace, false); assert.equal(d.allow1by1towers, false); }); -test("movement profile descriptor: flee allows higher drop, canDig=false", () => { +test("movement profile descriptor: flee allows higher drop, canDig=true", () => { const d = describeProfile(PROFILES.FLEE); - assert.equal(d.canDig, false); + assert.equal(d.canDig, true); assert.equal(d.maxDropDown, 8); }); diff --git a/runtime/movement-profiles.js b/runtime/movement-profiles.js index 13e7795..4fa9e7a 100644 --- a/runtime/movement-profiles.js +++ b/runtime/movement-profiles.js @@ -20,17 +20,16 @@ export const PROFILES = Object.freeze({ // Pure descriptors — safe to import without a live bot. // -// canDig is FALSE everywhere by default (2026-05-26). On the live server -// (play.xmatic.team 26.1.2+ViaBackwards 5.9.1) bot.dig silently fails — -// the packet ID table for protocol 775 is wrong in minecraft-data -// (mineflayer#3888) — so pathfinder would schedule paths through -// must-dig blocks the bot can't actually break, and we'd loop. Once -// 1.21.4 pin + lookAt+wait fix is verified live, we can re-enable -// canDig for gather/travel profiles. +// 2026-05-26 update: ground-truth probe (diag.physics) on +// play.xmatic.team confirmed forward + jump work in normal directions +// (forward Δ≈3.4 blocks/1.2s in an unobstructed cardinal, jump ΔY=1.25 +// = vanilla height). The 1.21.4 pin + AuthMe login flow are doing +// their job. canDig is back to true on gather/travel/flee — pathfinder +// needs to be able to break leaves/dirt to actually move around. export const PROFILE_DEFAULTS = Object.freeze({ - [PROFILES.GATHER]: { canDig: false, canPlace: false, allow1by1towers: false }, - [PROFILES.TRAVEL]: { canDig: false, canPlace: false, allow1by1towers: false }, - [PROFILES.FLEE]: { canDig: false, canPlace: false, allow1by1towers: false, maxDropDown: 8 }, + [PROFILES.GATHER]: { canDig: true, canPlace: false, allow1by1towers: false }, + [PROFILES.TRAVEL]: { canDig: true, canPlace: false, allow1by1towers: false }, + [PROFILES.FLEE]: { canDig: true, canPlace: false, allow1by1towers: false, maxDropDown: 8 }, [PROFILES.BUILD]: { canDig: false, canPlace: true, allow1by1towers: true }, [PROFILES.RETURN_TO_BASE]: { canDig: false, canPlace: false, allow1by1towers: false }, }); diff --git a/runtime/skills/diagnose-physics.js b/runtime/skills/diagnose-physics.js index 0dffb83..6d463c5 100644 --- a/runtime/skills/diagnose-physics.js +++ b/runtime/skills/diagnose-physics.js @@ -21,19 +21,45 @@ function withTimeout(promise, ms, label) { return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } -async function probeForward(bot, durationMs = 2_000) { +// Probe forward by trying every cardinal direction in sequence and +// keeping the BEST result. If the bot is wedged on a single side +// (treetop with leaves on one side, open air on the other) we still +// get a true positive instead of a false "BROKEN". +async function probeForward(bot, durationMsPerDir = 1_200) { const before = bot.entity.position.clone(); - try { - bot.setControlState("forward", true); - await new Promise((r) => setTimeout(r, durationMs)); - } finally { - bot.setControlState("forward", false); + const trials = []; + const yaws = [ + { name: "N", yaw: Math.PI }, // -Z + { name: "E", yaw: -Math.PI / 2 }, // +X + { name: "S", yaw: 0 }, // +Z + { name: "W", yaw: Math.PI / 2 }, // -X + ]; + for (const { name, yaw } of yaws) { + try { await bot.look(yaw, 0, true); } catch {} + const t0 = bot.entity.position.clone(); + try { + bot.setControlState("forward", true); + await new Promise((r) => setTimeout(r, durationMsPerDir)); + } finally { + bot.setControlState("forward", false); + } + const t1 = bot.entity.position; + const dx = t1.x - t0.x; + const dz = t1.z - t0.z; + const dist = Math.hypot(dx, dz); + trials.push({ dir: name, dist }); + // settle gravity + await new Promise((r) => setTimeout(r, 300)); } - const after = bot.entity.position; - const dx = after.x - before.x; - const dz = after.z - before.z; - const dist = Math.hypot(dx, dz); - return { before, after: after.clone(), dist, works: dist > 0.5 }; + const best = trials.reduce((b, t) => (t.dist > b.dist ? t : b), { dir: "?", dist: 0 }); + const after = bot.entity.position.clone(); + return { + before, after, + trials, + bestDir: best.dir, + bestDist: best.dist, + works: best.dist > 0.5, + }; } async function probeJump(bot) { @@ -88,7 +114,8 @@ export const skill = Object.freeze({ info("action", "diag.physics: starting probe"); const fwd = await probeForward(bot); - info("action", `diag.physics: forward Δ=${fwd.dist.toFixed(2)} (${fwd.works ? "OK" : "BROKEN"})`); + const trialsStr = fwd.trials?.map((t) => `${t.dir}:${t.dist.toFixed(1)}`).join(" ") ?? "?"; + info("action", `diag.physics: forward bestΔ=${fwd.bestDist.toFixed(2)} (${fwd.bestDir}) trials=[${trialsStr}] (${fwd.works ? "OK" : "BROKEN"})`); const jmp = await probeJump(bot); info("action", `diag.physics: jump ΔY=${jmp.deltaY.toFixed(2)} (${jmp.works ? "OK" : "BROKEN"})`); @@ -96,7 +123,7 @@ export const skill = Object.freeze({ const dig = await probeDig(bot); info("action", `diag.physics: dig ${dig.before ?? "?"}→${dig.after ?? "?"} (${dig.works ? "OK" : "BROKEN"}: ${dig.reason ?? ""})`); - const summary = `physics probe: forward=${fwd.works ? "ok" : "BROKEN"}(Δ${fwd.dist.toFixed(1)}) jump=${jmp.works ? "ok" : "BROKEN"}(Δy${jmp.deltaY.toFixed(1)}) dig=${dig.works ? "ok" : "BROKEN"}(${dig.before ?? "no-target"}→${dig.after ?? "?"})`; + const summary = `physics probe: forward=${fwd.works ? "ok" : "BROKEN"}(best=${fwd.bestDir}@${fwd.bestDist.toFixed(1)}) jump=${jmp.works ? "ok" : "BROKEN"}(Δy${jmp.deltaY.toFixed(1)}) dig=${dig.works ? "ok" : "BROKEN"}(${dig.before ?? "no-target"}→${dig.after ?? "?"})`; appendDiary(summary); return { diff --git a/runtime/skills/explore-far.js b/runtime/skills/explore-far.js index de779f1..526e1c9 100644 --- a/runtime/skills/explore-far.js +++ b/runtime/skills/explore-far.js @@ -62,49 +62,90 @@ export const skill = Object.freeze({ ensurePathfinder(bot); setMovementsForTravel(bot); - const here = bot.entity.position; + // 2026-05-26: probe-then-go. Try all 4 cardinal directions for + // 800 ms each, pick the one where we actually moved, then commit + // a long blind walk in that direction. Pathfinder timeouts on + // this server mean we can't trust GoalNear; cardinal probing + // gives us a free-direction signal cheaply. const dist = Math.max(24, args.distance ?? 48); - const q = args.quadrant ?? nextQuadrant(bot); - const tx = Math.round(here.x + q.x * dist); - const tz = Math.round(here.z + q.z * dist); + const trials = await probeCardinalStep(bot, 800); + const best = trials.reduce((b, t) => (t.dist > b.dist ? t : b), { dist: 0, yaw: 0, name: "?" }); + info("action", `explore.far: cardinal probe trials=${trials.map((t) => `${t.name}:${t.dist.toFixed(1)}`).join(" ")} best=${best.name}`); + + if (best.dist < 0.5) { + // All cardinals blocked. Same wedged-jump as wander. + info("action", "explore.far: wedged — jump fallback"); + try { await bot.look(best.yaw ?? 0, 0, true); } catch {} + bot.setControlState("forward", true); + bot.setControlState("jump", true); + try { + await new Promise((r) => setTimeout(r, 3_000)); + } finally { + bot.setControlState("forward", false); + bot.setControlState("jump", false); + } + return { ok: true, code: "done", detail: { mode: "wedged-jump", trials }, worldDelta: { movedTo: null } }; + } + + const here = bot.entity.position.clone(); + const tx = Math.round(here.x + Math.sin(-best.yaw) * dist); + const tz = Math.round(here.z + Math.cos(-best.yaw) * dist); const ty = Math.round(here.y); - info("action", `explore.far: → ${tx},${ty},${tz} (quad=${q.x},${q.z}, dist=${dist})`); + info("action", `explore.far: walking ${best.name} → ${tx},${ty},${tz}`); try { await withTimeout( bot.pathfinder.goto(new goals.GoalNear(tx, ty, tz, 4)), - 60_000, + 45_000, `explore.far(${tx},${tz})`, ); return { - ok: true, - code: "done", - detail: { to: { x: tx, y: ty, z: tz }, quadrant: q }, + ok: true, code: "done", + detail: { to: { x: tx, y: ty, z: tz }, dir: best.name }, worldDelta: { movedTo: { x: tx, y: ty, z: tz } }, }; } catch (e) { - warn("action", `explore.far failed: ${e.message} — blind walking`); + warn("action", `explore.far pathfinder failed: ${e.message} — continuing blind`); + try { await bot.look(best.yaw, 0, true); } catch {} + bot.setControlState("forward", true); + bot.setControlState("jump", true); try { - const dx = tx - here.x; - const dz = tz - here.z; - const yaw = Math.atan2(-dx, -dz); - await bot.look(yaw, 0, true); - bot.setControlState("forward", true); - bot.setControlState("jump", true); - await new Promise((r) => setTimeout(r, 5_000)); + await new Promise((r) => setTimeout(r, 7_000)); + } finally { bot.setControlState("forward", false); bot.setControlState("jump", false); - return { - ok: true, - code: "done", - detail: { mode: "blind", quadrant: q }, - worldDelta: { movedTo: null }, - }; - } catch (e2) { - bot.setControlState("forward", false); - bot.setControlState("jump", false); - return { ok: false, code: "failed", detail: `${e.message}; blind ${e2.message}`, worldDelta: null }; } + return { + ok: true, code: "done", + detail: { mode: "blind", dir: best.name }, + worldDelta: { movedTo: null }, + }; } }, }); + +const CARDINAL_YAWS = [ + { name: "N", yaw: Math.PI }, + { name: "E", yaw: -Math.PI / 2 }, + { name: "S", yaw: 0 }, + { name: "W", yaw: Math.PI / 2 }, +]; + +async function probeCardinalStep(bot, durationMs = 800) { + const trials = []; + for (const { name, yaw } of CARDINAL_YAWS) { + try { await bot.look(yaw, 0, true); } catch {} + const before = bot.entity.position.clone(); + bot.setControlState("forward", true); + try { + await new Promise((r) => setTimeout(r, durationMs)); + } finally { + bot.setControlState("forward", false); + } + const after = bot.entity.position; + const dist = Math.hypot(after.x - before.x, after.z - before.z); + trials.push({ name, yaw, dist }); + await new Promise((r) => setTimeout(r, 150)); + } + return trials; +}