Files
pepa-pi-bot/runtime/skills/diagnose-physics.js
T
mayatnikovandClaude Opus 4.7 86f0c1799e fix(runtime): pin MC_VERSION=1.21.4 + ground-truth probe + close-loop dig
Two-pronged response to user-confirmed "bot stands still, doesn't actually
chop" on play.xmatic.team:

1. Pin protocol — .env now sets MC_VERSION=1.21.4. minecraft-data has
   wrong packet ID mappings for protocol 775 (server 26.1.2 via
   ViaBackwards 5.9.1) — see mineflayer#3888 and #3717. 1.21.5 also has
   an enchants decoder bug that breaks bot.dig. 1.21.4 is the last
   protocol mineflayer 4.37.1 can speak cleanly through VIA.

2. Don't trust dig success — runtime/actions.js chopNearestTree and
   runtime/skills/gather-stone.js now lookAt(face center)+forceLook,
   await collectBlock, then re-read the target block. If the log/stone
   is STILL there, return ok:false code:"silent_dig_failure" and
   blacklist the position. Prevents the curriculum from reporting
   "wood.16 in progress" while the world hasn't actually changed.

3. Defensive default — runtime/movement-profiles.js: canDig=false on
   every profile until dig is confirmed working live. Otherwise
   pathfinder schedules paths through must-dig blocks and the bot loops.

4. Ground-truth probe — runtime/skills/diagnose-physics.js dispatches
   forward/jump/dig probes and writes the result to the diary. New
   IPC command cmd:run-skill lets the operator (or a future curriculum
   trigger) fire any skill on demand; it waits for the current action
   to finish before dispatching. /tmp/pepa-runskill.mjs is a one-shot
   client.

Live probe on play.xmatic.team confirmed: forward Δ=0.003 over 2s
(BROKEN — server rejects movement packets), jump ΔY=0.42 (likely
physics jitter, not a real jump). Strongly suggests an anti-cheat
plugin gating bot-style movements server-side — beyond protocol pin.

npm test 124/124.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:51:43 +03:00

120 lines
4.1 KiB
JavaScript

// diag.physics — one-shot ground truth probe. Tests whether
// setControlState("forward"), setControlState("jump"), and bot.dig
// actually do anything on this server. Writes results to the diary so
// the operator can inspect later. NOT for production loops — it has
// side effects (1-2 blocks moved if forward works) and we don't want
// to dispatch it every tick.
//
// Triggered by: explicit IPC command (cmd:probe-physics) or by the
// curriculum once if the bot has been "working" but its position
// hasn't changed by more than 4 blocks in 5 min. We add the curriculum
// trigger in a follow-up.
import { info, warn } from "../log.js";
import { appendDiary } from "../state-store.js";
function withTimeout(promise, ms, label) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
async function probeForward(bot, durationMs = 2_000) {
const before = bot.entity.position.clone();
try {
bot.setControlState("forward", true);
await new Promise((r) => setTimeout(r, durationMs));
} finally {
bot.setControlState("forward", false);
}
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 };
}
async function probeJump(bot) {
const startY = bot.entity.position.y;
let maxY = startY;
const start = Date.now();
try {
bot.setControlState("jump", true);
while (Date.now() - start < 800) {
await new Promise((r) => setTimeout(r, 50));
if (bot.entity.position.y > maxY) maxY = bot.entity.position.y;
}
} finally {
bot.setControlState("jump", false);
}
return { startY, maxY, deltaY: maxY - startY, works: maxY - startY > 0.4 };
}
async function probeDig(bot) {
const ds = ["sand", "dirt", "grass_block", "stone", "cobblestone", "gravel", "oak_log", "oak_leaves", "leaves"];
const target = bot.findBlock({
matching: (b) => b && b.position && ds.includes(b.name) && b.position.y >= bot.entity.position.y - 3,
maxDistance: 6,
});
if (!target) return { works: false, reason: "no soft block within 6 to test on" };
const tPos = target.position.clone();
const tName = target.name;
try {
await withTimeout(bot.lookAt(tPos.offset(0.5, 0.5, 0.5), true), 2_000, "lookAt");
} catch {}
try {
await withTimeout(bot.dig(target), 12_000, "dig");
} catch (e) {
return { works: false, reason: `dig threw: ${e.message}`, name: tName, at: tPos };
}
const after = bot.blockAt(tPos);
const gone = !after || after.name === "air" || after.name === "cave_air" || after.name === "void_air";
return { works: gone, before: tName, after: after?.name ?? "(none)", at: tPos };
}
export const skill = Object.freeze({
id: "diag.physics",
title: "Probe forward/jump/dig and report",
timeoutMs: 30_000,
preconditions(ctx) {
if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" };
return { ok: true };
},
async execute(ctx) {
const bot = ctx.bot;
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 jmp = await probeJump(bot);
info("action", `diag.physics: jump ΔY=${jmp.deltaY.toFixed(2)} (${jmp.works ? "OK" : "BROKEN"})`);
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 ?? "?"})`;
appendDiary(summary);
return {
ok: true,
code: "done",
detail: {
forward: fwd,
jump: jmp,
dig: dig,
},
worldDelta: {
probe: {
forwardWorks: fwd.works,
jumpWorks: jmp.works,
digWorks: dig.works,
},
},
};
},
});