Files
pepa-pi-bot/runtime/skills/build-shelter.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

189 lines
6.7 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.build-shelter — place a minimal 3×3×3 hut around the bot's
// recorded base (or current position if no base yet). The blueprint is
// computed once per call as a list of {x,y,z,blockType} targets, then
// the skill places them in order, marking each placed block in the
// owned-blocks ledger. Idempotent: any target that already holds the
// right block is skipped, so the skill is resumable across restarts.
//
// Walls use any *_planks the bot carries (we pick the most-common type).
// The interior keeps the bed slot empty (assumes the bed sits on
// (cx+1, cy, cz) — i.e. one step east of the centre).
//
// Out of scope here: door entity (mineflayer can't reliably place doors
// without recent version checks). The west wall has a 1-block opening
// at head-height the bot can step through.
import { applyProfile, PROFILES } from "../movement-profiles.js";
import { info, warn } from "../log.js";
import { getLocation, setLocation } from "../locations.js";
const SHELTER_NAME = "shelter";
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 pickBuildPlanks(bot) {
const counts = new Map();
for (const item of bot.inventory.items()) {
if (item.name.endsWith("_planks")) {
counts.set(item.name, (counts.get(item.name) ?? 0) + item.count);
}
}
let best = null;
for (const [name, n] of counts) {
if (!best || n > best.n) best = { name, n };
}
return best;
}
// Build a list of {x,y,z, name} targets for a 3×3 footprint × 3-tall
// shelter centred on (cx, cy, cz). Bed slot (cx+1, cy, cz) and head-
// height entry at the west wall (cx-1, cy+1, cz) are left empty.
function blueprint(center, plankName) {
const targets = [];
const { x: cx, y: cy, z: cz } = center;
// Floor: only the corners we don't already have (skip the bed slot).
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
targets.push({ x: cx + dx, y: cy - 1, z: cz + dz, name: plankName });
}
}
// Walls (y = cy and y = cy+1).
for (let dy = 0; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
const isCorner = Math.abs(dx) + Math.abs(dz) === 2;
const isWall = Math.abs(dx) === 1 || Math.abs(dz) === 1;
if (!isWall && !isCorner) continue; // skip interior column
// Leave one wall slot open as a doorway: west wall, head height.
if (dx === -1 && dz === 0 && dy === 1) continue;
// Bed occupies (cx+1, cy, cz) — its second half is at (cx+2, cy, cz)
// which is OUTSIDE this 3×3 footprint. So we don't need to clear it.
targets.push({ x: cx + dx, y: cy + dy, z: cz + dz, name: plankName });
}
}
}
// Roof: full 3×3 at y = cy+2.
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
targets.push({ x: cx + dx, y: cy + 2, z: cz + dz, name: plankName });
}
}
return targets;
}
function blockMatchesName(block, name) {
return block && block.name === name;
}
function findReferenceForPlacement(bot, target) {
// Try the block below the target first (most natural place to stack
// from). If it's air, try sides.
const offsets = [
{ x: 0, y: -1, z: 0, face: { x: 0, y: 1, z: 0 } },
{ x: -1, y: 0, z: 0, face: { x: 1, y: 0, z: 0 } },
{ x: 1, y: 0, z: 0, face: { x: -1, y: 0, z: 0 } },
{ x: 0, y: 0, z: -1, face: { x: 0, y: 0, z: 1 } },
{ x: 0, y: 0, z: 1, face: { x: 0, y: 0, z: -1 } },
{ x: 0, y: 1, z: 0, face: { x: 0, y: -1, z: 0 } },
];
for (const off of offsets) {
const block = bot.blockAt({
x: target.x + off.x,
y: target.y + off.y,
z: target.z + off.z,
});
if (block && block.boundingBox === "block") return { ref: block, face: off.face };
}
return null;
}
export const skill = Object.freeze({
id: "village.build-shelter",
title: "Build a tiny shelter around the bed",
timeoutMs: 5 * 60_000,
preconditions(ctx) {
if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" };
const planks = pickBuildPlanks(ctx.bot);
if (!planks || planks.n < 18) {
return { ok: false, code: "missing_material", detail: `need ≥18 planks (have ${planks?.n ?? 0})` };
}
// Need a base or at least a placed bed nearby; the chooseBase skill
// is responsible for picking the spot first.
const base = getLocation("base") ?? getLocation(SHELTER_NAME);
if (!base) return { ok: false, code: "no_base", detail: "no base location chosen yet" };
return { ok: true };
},
async execute(ctx, { owned } = {}) {
const bot = ctx.bot;
const planks = pickBuildPlanks(bot);
const base = getLocation("base") ?? getLocation(SHELTER_NAME);
const center = { x: base.x, y: base.y, z: base.z };
applyProfile(PROFILES.BUILD, bot);
const targets = blueprint(center, planks.name);
info("action", `village.build-shelter: ${targets.length} blocks (planks=${planks.name})`);
let placed = 0;
let skipped = 0;
for (const target of targets) {
const existing = bot.blockAt(target);
if (blockMatchesName(existing, planks.name)) {
skipped++;
continue;
}
// Re-equip planks each iteration (the bot might have eaten / swapped).
const item = bot.inventory.items().find((i) => i.name === planks.name);
if (!item) {
warn("action", `village.build-shelter: ran out of ${planks.name} mid-build`);
break;
}
try {
await withTimeout(bot.equip(item, "hand"), 3000, "equip plank");
} catch (e) {
warn("action", `village.build-shelter: equip failed: ${e.message}`);
continue;
}
const place = findReferenceForPlacement(bot, target);
if (!place) {
warn("action", `village.build-shelter: no reference block for ${target.x},${target.y},${target.z}`);
continue;
}
try {
await withTimeout(bot.placeBlock(place.ref, place.face), 5000, "placeBlock");
if (owned?.markPlaced) {
owned.markPlaced({
x: target.x, y: target.y, z: target.z,
blockType: planks.name,
skill: "village.build-shelter",
});
}
placed++;
} catch (e) {
warn("action", `village.build-shelter: place ${target.x},${target.y},${target.z} failed: ${e.message}`);
}
}
// Record the shelter location so future skills can find it even if
// base gets re-scored.
setLocation(SHELTER_NAME, { x: center.x, y: center.y, z: center.z, radius: 2, note: `auto-built; ${placed} blocks placed` });
if (placed === 0 && skipped === 0) {
return { ok: false, code: "no_progress", detail: "could not place any blocks", worldDelta: null };
}
return {
ok: true,
code: "done",
detail: { placed, skipped, total: targets.length, plankName: planks.name },
worldDelta: { shelterAt: center, placed },
};
},
});