Files
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

181 lines
6.0 KiB
JavaScript
Raw Permalink 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.
// farm.wheat — opportunistic wheat farming. The skill does ONE of:
// * plant a wheat seed on a nearby tilled farmland block, OR
// * till a grass/dirt block adjacent to water if we have a hoe and seeds,
// * harvest a fully-grown wheat block.
//
// We don't try to plan a full 3×3 plot in one call — the curriculum can
// dispatch the skill repeatedly and each call makes one block of progress.
// This is consistent with the gather.logs / gather.stone "one block at a
// time" rhythm and keeps each tick observable.
import pathfinderPkg from "mineflayer-pathfinder";
const { pathfinder, goals, Movements } = pathfinderPkg;
import { applyProfile, PROFILES } from "../movement-profiles.js";
import { info, warn } from "../log.js";
const HOE_NAMES = ["wooden_hoe", "stone_hoe", "iron_hoe", "diamond_hoe", "netherite_hoe", "golden_hoe"];
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 getCount(bot, name) {
return bot.inventory.items().reduce((s, i) => (i.name === name ? s + i.count : s), 0);
}
function getItem(bot, name) {
return bot.inventory.items().find((i) => i.name === name) ?? null;
}
function hasHoe(bot) {
return HOE_NAMES.some((n) => getCount(bot, n) > 0);
}
function findHoe(bot) {
for (const n of HOE_NAMES) {
const item = getItem(bot, n);
if (item) return item;
}
return null;
}
function isWaterNear(bot, pos, radius = 4) {
for (let dx = -radius; dx <= radius; dx++) {
for (let dz = -radius; dz <= radius; dz++) {
const b = bot.blockAt({ x: pos.x + dx, y: pos.y, z: pos.z + dz });
if (b?.name === "water") return true;
}
}
return false;
}
export const skill = Object.freeze({
id: "farm.wheat",
title: "Make one step of wheat farming progress",
timeoutMs: 60_000,
preconditions(ctx) {
if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" };
const bot = ctx.bot;
const hasSeeds = getCount(bot, "wheat_seeds") > 0;
// We're happy to dispatch when EITHER seeds+hoe+water available
// OR there's a ripe wheat block to harvest.
const ripeWheat = bot.findBlock({
matching: (b) => b?.name === "wheat" && (b?.metadata === 7 || b?.getProperties?.()?.age === 7),
maxDistance: 24,
});
if (ripeWheat) return { ok: true };
if (!hasSeeds) return { ok: false, code: "no_seeds", detail: "no wheat_seeds in inventory" };
if (!hasHoe(bot)) return { ok: false, code: "missing_tool", detail: "no hoe" };
// Need at least one tillable + water-adjacent block within 16.
const tillable = bot.findBlock({
matching: (b) => (b?.name === "grass_block" || b?.name === "dirt") && isWaterNear(bot, b.position, 4),
maxDistance: 16,
});
if (!tillable) return { ok: false, code: "no_target", detail: "no tillable grass/dirt near water" };
return { ok: true };
},
async execute(ctx) {
const bot = ctx.bot;
ensurePathfinder(bot);
applyProfile(PROFILES.GATHER, bot);
// 1. Harvest ripe wheat if any.
const ripeWheat = bot.findBlock({
matching: (b) => b?.name === "wheat" && (b?.metadata === 7 || b?.getProperties?.()?.age === 7),
maxDistance: 24,
});
if (ripeWheat) {
try {
await withTimeout(
bot.pathfinder.goto(new goals.GoalGetToBlock(ripeWheat.position.x, ripeWheat.position.y, ripeWheat.position.z)),
20_000,
"goto wheat",
);
await withTimeout(bot.dig(ripeWheat), 8_000, "harvest wheat");
info("action", `farm.wheat: harvested wheat at ${ripeWheat.position}`);
return {
ok: true,
code: "done",
detail: { phase: "harvest", at: ripeWheat.position },
worldDelta: { harvestedAt: ripeWheat.position },
};
} catch (e) {
warn("action", `farm.wheat harvest failed: ${e.message}`);
return { ok: false, code: "failed", detail: e.message, worldDelta: null };
}
}
// 2. Plant on existing farmland if any (water-adjacent or not, just farmland exists).
const farmland = bot.findBlock({
matching: (b) => b?.name === "farmland",
maxDistance: 16,
});
const seedItem = getItem(bot, "wheat_seeds");
if (farmland && seedItem) {
try {
await withTimeout(
bot.pathfinder.goto(new goals.GoalNear(farmland.position.x, farmland.position.y, farmland.position.z, 1)),
20_000,
"goto farmland",
);
await withTimeout(bot.equip(seedItem, "hand"), 3000, "equip seeds");
await withTimeout(
bot.placeBlock(farmland, { x: 0, y: 1, z: 0 }),
5000,
"placeBlock(seeds)",
);
return {
ok: true,
code: "done",
detail: { phase: "plant", at: farmland.position },
worldDelta: { plantedAt: farmland.position },
};
} catch (e) {
warn("action", `farm.wheat plant failed: ${e.message}`);
// fall through to tilling
}
}
// 3. Till a grass/dirt block next to water with our hoe.
const tillable = bot.findBlock({
matching: (b) => (b?.name === "grass_block" || b?.name === "dirt") && isWaterNear(bot, b.position, 4),
maxDistance: 16,
});
if (!tillable) {
return { ok: false, code: "no_target", detail: "no tillable block remaining", worldDelta: null };
}
const hoe = findHoe(bot);
try {
await withTimeout(
bot.pathfinder.goto(new goals.GoalNear(tillable.position.x, tillable.position.y, tillable.position.z, 2)),
20_000,
"goto tillable",
);
await withTimeout(bot.equip(hoe, "hand"), 3000, "equip hoe");
// Activate the block (right-click): turns grass/dirt → farmland.
await withTimeout(bot.activateBlock(tillable), 5000, "till");
return {
ok: true,
code: "done",
detail: { phase: "till", at: tillable.position },
worldDelta: { tilledAt: tillable.position },
};
} catch (e) {
warn("action", `farm.wheat till failed: ${e.message}`);
return { ok: false, code: "failed", detail: e.message, worldDelta: null };
}
},
});