Files
pepa-pi-bot/runtime/skills/deposit-surplus.js
T
mayatnikovandClaude Opus 4.7 29542f0559 fix(runtime): unstick scheduler + chop + sleep + bed/shelter/farm skills
Recovers the bot from the live-server symptoms reported 2026-05-26:
1) constant supervisor reconnects, 2) chop "clicks once and stops",
3) sleep does nothing without a bed and so blocks night-skipping for
other players, 4) curriculum reflex always fell through to wander.

Supervisor (#38):
- runtime/watch-filter.js: pure predicate excluding *.test.js + the
  supervisor itself; recursive:true so skills/ + social/ edits also
  restart. Burned a working main once when test files counted toward
  the rollback threshold.
- runtime/supervisor.js: watch-triggered restarts no longer count
  toward the crash-loop rollback path. Watcher is now recursive.

Chop / mine (#39):
- runtime/actions.js + runtime/skills/gather-stone.js: replaced raw
  pathfinder.goto + bot.dig with mineflayer-collectblock's
  bot.collectBlock.collect — handles approach, repositioning, LoS,
  dig and pickup as one primitive. Old version "swung once" because
  GoalGetToBlock often parked the bot in leaves above the log.

Sleep + bed (#40):
- runtime/actions.js: sleepInBed now ALSO places a carried bed on
  solid ground next to the bot and sleeps on it. Critical so the bot
  stops blocking player night-skipping the moment it owns a bed.

Bed pipeline (#41):
- runtime/skills/gather-wool.js: gather.wool skill — mines wool block
  if any nearby, otherwise shears or attacks the nearest sheep.
- runtime/skills/craft.js: craftBedSkill (any colour the bot has ≥3
  wool of, plus 3 planks, plus a table).
- runtime/curriculum.js: new milestone survive.bed sits between
  wood.tools and stone.32 so the bot gets a bed BEFORE everything else.
  Test fixture updated to include a red_bed in post-survive.bed stages.

Village / shelter / wheat (#42, #43):
- runtime/skills/build-shelter.js: village.build-shelter — real 3×3×3
  resumable hut blueprint around the recorded base, places one block
  per loop, idempotent so an interrupted build resumes correctly,
  marks each placed block in the owned-blocks ledger.
- runtime/skills/deposit-surplus.js: village.deposit-surplus opens
  the nearest chest and transfers surplus stacks while keeping a
  reserve of tools/food/bed.
- runtime/skills/farm-wheat.js: farm.wheat does one step per call
  (till adjacent-to-water grass, plant seeds, or harvest ripe wheat).
- runtime/curriculum.js: village.shelter milestone after base-site.

Scheduler glitch (root of "always wander"):
- runtime/bot.js: curriculum + locations are now computed BEFORE
  runTick. Previously they were stamped AFTER, so reflex.js saw
  snapshot.curriculum=undefined every tick and fell through to the
  wander fallback. Verified live: scheduler now dispatches
  gather.logs/gather.stone/craft.* by id via runSkill.

Eat-spam:
- runtime/reflex.js: eatReflex now checks inventory for actual food
  and updates lastEatAt on EVERY dispatch (not only successes), so a
  failed eat respects the 5 s cooldown instead of firing every tick.

npm test 123/123. Validated live on play.xmatic.team (curriculum
dispatched gather.logs via runSkill, recover hint switched to wander
when no log in range).

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

128 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// village.deposit-surplus — find the nearest placed chest, open it, and
// transfer any stack the bot is over-carrying (logs, cobble, dirt,
// seeds). Keeps a small "essentials" reserve in inventory so the bot
// keeps its tools, food and bed.
//
// What counts as surplus:
// * any item whose count exceeds RESERVE_PER_NAME (default: keep 8 of
// each named item), UNLESS it's in KEEP_ALWAYS (tools/bed/food).
// * raw materials that look strictly storable (logs/cobble/dirt/sand).
import pathfinderPkg from "mineflayer-pathfinder";
const { pathfinder, goals, Movements } = pathfinderPkg;
import { applyProfile, PROFILES } from "../movement-profiles.js";
import { info, warn } from "../log.js";
const KEEP_ALWAYS_NAME_RE = /(_axe|_pickaxe|_sword|_shovel|_hoe|_bed|bread|cooked_|apple|carrot|potato|wheat_seeds)$/;
const STORABLE_NAME_RE = /(_log$|_stem$|cobblestone|cobbled_deepslate|deepslate|stone$|dirt|sand|gravel|wheat$|_planks$|stick$)/;
const RESERVE_PER_NAME = 8;
let pluginLoaded = new WeakSet();
function ensurePathfinder(bot) {
if (pluginLoaded.has(bot)) return;
bot.loadPlugin(pathfinder);
pluginLoaded.add(bot);
}
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));
}
function pickSurplus(bot) {
const out = [];
// Group inventory items by name, then decide how much to deposit per name.
const grouped = new Map();
for (const item of bot.inventory.items()) {
if (!grouped.has(item.name)) grouped.set(item.name, []);
grouped.get(item.name).push(item);
}
for (const [name, items] of grouped) {
if (KEEP_ALWAYS_NAME_RE.test(name)) continue;
const total = items.reduce((s, i) => s + i.count, 0);
const storable = STORABLE_NAME_RE.test(name);
const reserve = storable ? Math.min(RESERVE_PER_NAME, total) : 0;
const surplus = total - reserve;
if (surplus <= 0) continue;
out.push({ name, surplus, items });
}
return out;
}
export const skill = Object.freeze({
id: "village.deposit-surplus",
title: "Deposit surplus items in a chest",
timeoutMs: 60_000,
preconditions(ctx) {
if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" };
const surplus = pickSurplus(ctx.bot);
if (surplus.length === 0) return { ok: false, code: "nothing_to_deposit", detail: "no surplus stacks" };
const chest = ctx.bot.findBlock({
matching: (b) => b?.name === "chest" || b?.name === "trapped_chest",
maxDistance: 24,
});
if (!chest) return { ok: false, code: "no_chest", detail: "no chest within 24 blocks" };
return { ok: true };
},
async execute(ctx) {
const bot = ctx.bot;
const chest = bot.findBlock({
matching: (b) => b?.name === "chest" || b?.name === "trapped_chest",
maxDistance: 24,
});
if (!chest) return { ok: false, code: "no_chest", detail: "no chest after move", worldDelta: null };
ensurePathfinder(bot);
applyProfile(PROFILES.TRAVEL, bot);
try {
await withTimeout(
bot.pathfinder.goto(new goals.GoalNear(chest.position.x, chest.position.y, chest.position.z, 1)),
30_000,
"goto chest",
);
} catch (e) {
return { ok: false, code: "no_path", detail: e.message, worldDelta: null };
}
let chestHandle;
try {
chestHandle = await withTimeout(bot.openContainer(chest), 8_000, "openChest");
} catch (e) {
return { ok: false, code: "open_failed", detail: e.message, worldDelta: null };
}
let deposited = 0;
const detail = [];
try {
for (const { name, surplus } of pickSurplus(bot)) {
const ref = bot.registry?.itemsByName?.[name];
if (!ref) continue;
try {
await withTimeout(chestHandle.deposit(ref.id, null, surplus), 10_000, `deposit ${name}`);
deposited += surplus;
detail.push(`${name}×${surplus}`);
info("action", `village.deposit-surplus: ${name}×${surplus}`);
} catch (e) {
warn("action", `village.deposit-surplus: ${name} failed: ${e.message}`);
}
}
} finally {
try { await chestHandle.close(); } catch {}
}
if (deposited === 0) {
return { ok: false, code: "deposit_failed", detail: "opened chest but deposited nothing", worldDelta: null };
}
return {
ok: true,
code: "done",
detail: { deposited, items: detail },
worldDelta: { depositedTotal: deposited },
};
},
});