v0.2.0-rc.3: pillar-up escape + advice in fallback + danger POI #22
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pepa-pi-bot",
|
||||
"version": "0.2.0-rc.2",
|
||||
"version": "0.2.0-rc.3",
|
||||
"private": true,
|
||||
"description": "An autonomous, self-extending Minecraft player powered by Pi and Mineflayer.",
|
||||
"license": "MIT",
|
||||
@@ -16,7 +16,7 @@
|
||||
"tui": "tsx tui/tui.tsx",
|
||||
"propose:apply": "node scripts/propose-apply.js",
|
||||
"stop": "bash scripts/stop.sh",
|
||||
"test": "node --test runtime/skills/contract.test.js runtime/skills/groups.test.js runtime/skills/compat.test.js runtime/skills/recovery-tunnel-out.test.js runtime/curriculum.test.js runtime/social/social.test.js runtime/social/conversation.test.js runtime/social/chat-history.test.js runtime/social/reply-pi.test.js runtime/stuck-incident.test.js runtime/compat.test.js runtime/reflex.test.js runtime/base-site.test.js runtime/locations.test.js runtime/watch-filter.test.js runtime/world-journal.test.js runtime/scenario-memory.test.js runtime/critic.test.js runtime/skill-library.test.js runtime/modes.test.js runtime/pathfinder-watchdog.test.js runtime/knowledge/knowledge.test.js runtime/coach/postmortem.test.js runtime/coach/advice.test.js runtime/coach/reflect.test.js runtime/persona/chatter.test.js scripts/edit-scope.test.js scripts/lint-patch.test.js"
|
||||
"test": "node --test runtime/skills/contract.test.js runtime/skills/groups.test.js runtime/skills/compat.test.js runtime/skills/recovery-tunnel-out.test.js runtime/skills/pillar-up.test.js runtime/curriculum.test.js runtime/social/social.test.js runtime/social/conversation.test.js runtime/social/chat-history.test.js runtime/social/reply-pi.test.js runtime/stuck-incident.test.js runtime/compat.test.js runtime/reflex.test.js runtime/base-site.test.js runtime/locations.test.js runtime/watch-filter.test.js runtime/world-journal.test.js runtime/scenario-memory.test.js runtime/critic.test.js runtime/skill-library.test.js runtime/modes.test.js runtime/pathfinder-watchdog.test.js runtime/knowledge/knowledge.test.js runtime/coach/postmortem.test.js runtime/coach/advice.test.js runtime/coach/reflect.test.js runtime/persona/chatter.test.js scripts/edit-scope.test.js scripts/lint-patch.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.10.0",
|
||||
|
||||
@@ -20,10 +20,12 @@ const SAFE_OVERRIDES = new Set([
|
||||
"survive.flee",
|
||||
"survive.sleep",
|
||||
"survive.eat",
|
||||
"survive.pillar-up",
|
||||
"recovery.tunnel-out",
|
||||
"explore.far",
|
||||
"explore.wander",
|
||||
"village.build-shelter",
|
||||
"village.choose-base",
|
||||
]);
|
||||
|
||||
// Pi-coach occasionally suggests prefer_skill values that are mode names
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
unanalysedDeaths,
|
||||
record as recordLesson,
|
||||
poiNearby,
|
||||
recordPOI,
|
||||
} from "../knowledge/index.js";
|
||||
import { info, warn } from "../log.js";
|
||||
|
||||
@@ -57,6 +58,18 @@ export function attach(bot, ctx = {}) {
|
||||
if (!death) return;
|
||||
const deathId = insertDeath(death);
|
||||
info("coach", `death recorded id=${deathId ?? "-"} cause=${death.cause} hostile=${death.hostile ?? "?"} skill=${death.lastSkill ?? "?"}`);
|
||||
// v0.2.0-rc.3 — mark this spot as a danger POI so spatial recall
|
||||
// surfaces it next time the bot comes near. Expires after 6 hours
|
||||
// so the danger doesn't outlive its relevance.
|
||||
if (typeof death.x === "number" && typeof death.z === "number") {
|
||||
recordPOI({
|
||||
kind: "danger",
|
||||
name: death.hostile ?? death.cause ?? "death",
|
||||
x: death.x, y: death.y ?? 64, z: death.z,
|
||||
expiresAt: Date.now() + 6 * 3600_000,
|
||||
notes: `death id=${deathId} cause=${death.cause}`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
warn("coach", `captureDeath failed: ${e?.message ?? e}`);
|
||||
}
|
||||
|
||||
+83
-1
@@ -370,6 +370,52 @@ function metricRecoverySkill(ctx, plannedSkillId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// v0.2.0-rc.3 — wedged-emergency escape. When the bot has not made
|
||||
// meaningful horizontal progress for ≥ 60s AND there's no immediate
|
||||
// hostile (defendReflex would have handled it) AND a placeable block
|
||||
// is in inventory, dispatch survive.pillar-up to climb vertically out
|
||||
// of pit terrain. Breaks the tunnel-out-fail-fall-back-to-flee loop
|
||||
// observed live in the rc.2 deploy. noProgressReason is a hint, not
|
||||
// required — pillar-up only writes blocks underneath, so even if the
|
||||
// real cause is something else, the worst case is +1 dirt placed.
|
||||
const WEDGED_MIN_MS = 60_000;
|
||||
|
||||
function wedgedEscapeSkill(ctx) {
|
||||
const s = ctx.snapshot;
|
||||
if (!s) return null;
|
||||
if (s.closestHostile && (s.closestHostile.distance ?? Infinity) < 6) return null;
|
||||
const lastMove = ctx.lastSignificantMoveAt ?? 0;
|
||||
if (!lastMove) return null; // need at least one tick of position tracking
|
||||
if (Date.now() - lastMove < WEDGED_MIN_MS) return null;
|
||||
// Last attempted escape was recent? give it room.
|
||||
if (ctx.skillBackoff?.["survive.pillar-up"] && Date.now() < ctx.skillBackoff["survive.pillar-up"]) {
|
||||
return null;
|
||||
}
|
||||
const pillar = getSkill("survive.pillar-up");
|
||||
if (!pillar) return null;
|
||||
const pre = pillar.preconditions(ctx);
|
||||
if (!pre.ok) return null;
|
||||
return "survive.pillar-up";
|
||||
}
|
||||
|
||||
function trackSignificantMovement(ctx) {
|
||||
const s = ctx.snapshot;
|
||||
const pos = s?.position;
|
||||
if (!pos) return;
|
||||
const last = ctx.lastSignificantPos;
|
||||
if (!last) {
|
||||
ctx.lastSignificantPos = { x: pos.x, z: pos.z };
|
||||
ctx.lastSignificantMoveAt = Date.now();
|
||||
return;
|
||||
}
|
||||
const dx = pos.x - last.x;
|
||||
const dz = pos.z - last.z;
|
||||
if (dx * dx + dz * dz >= 16) {
|
||||
ctx.lastSignificantPos = { x: pos.x, z: pos.z };
|
||||
ctx.lastSignificantMoveAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
function curriculumReflex(ctx) {
|
||||
const s = ctx.snapshot;
|
||||
if (!s.connected) return { action: "noop" };
|
||||
@@ -378,6 +424,17 @@ function curriculumReflex(ctx) {
|
||||
const since = Date.now() - (ctx.lastCurriculumAt ?? 0);
|
||||
if (since < CURRICULUM_COOLDOWN_MS) return { action: "noop" };
|
||||
|
||||
trackSignificantMovement(ctx);
|
||||
const wedged = wedgedEscapeSkill(ctx);
|
||||
if (wedged) {
|
||||
ctx.lastCurriculumAt = Date.now();
|
||||
ctx.skillBackoff = ctx.skillBackoff ?? {};
|
||||
// Don't pillar-up every tick — cool off for 2 min after each attempt.
|
||||
ctx.skillBackoff["survive.pillar-up"] = Date.now() + 2 * 60_000;
|
||||
ctx.dispatch(() => runSkill(wedged, ctx), wedged, {});
|
||||
return { action: "dispatched", kind: "curriculum-wedged-escape", label: wedged };
|
||||
}
|
||||
|
||||
const plan = s.curriculum?.plan;
|
||||
const wanderHintUntil = ctx.skillBackoff?.["__wander_hint__"] ?? 0;
|
||||
const wantWander = wanderHintUntil && Date.now() < wanderHintUntil;
|
||||
@@ -421,7 +478,32 @@ function curriculumReflex(ctx) {
|
||||
// explore.far so the bot actually leaves the patch it's stuck in.
|
||||
if (!plan?.skillId || wantWander) {
|
||||
ctx.lastCurriculumAt = Date.now();
|
||||
if (wantWander && consecutiveWanderHints >= 1) {
|
||||
const fallbackId = wantWander && consecutiveWanderHints >= 1 ? "explore.far" : "wander";
|
||||
// v0.2.0-rc.3 — consult advice on the FALLBACK dispatch too. Without
|
||||
// this, Pi-coach lessons like "do not explore.far after a zombie
|
||||
// sighting" never fire (the bot keeps falling into the fallback path
|
||||
// after each curriculum skill bails on no_target / wander_hint).
|
||||
const fbAdvice = consultAdvice({ plannedSkillId: fallbackId === "wander" ? "explore.far" : fallbackId, snapshot: ctx.snapshot });
|
||||
if (fbAdvice.action === "override" && fbAdvice.overrideSkillId) {
|
||||
ctx.dispatch(() => runSkill(fbAdvice.overrideSkillId, ctx), fbAdvice.overrideSkillId, {
|
||||
onComplete: (res) => reportAdviceOutcome({ lessonId: fbAdvice.lessonId, succeeded: !!res?.ok }),
|
||||
});
|
||||
return { action: "dispatched", kind: "curriculum-fallback-advice-override", label: fbAdvice.overrideSkillId, lessonId: fbAdvice.lessonId };
|
||||
}
|
||||
if (fbAdvice.action === "avoid") {
|
||||
// Lesson says don't do the fallback either. Try pillar-up as a
|
||||
// constructive last resort if we have a placeable block — otherwise
|
||||
// just idle for a tick so the next loop can re-evaluate.
|
||||
const pillarSkill = getSkill("survive.pillar-up");
|
||||
if (pillarSkill && pillarSkill.preconditions(ctx).ok) {
|
||||
ctx.dispatch(() => runSkill("survive.pillar-up", ctx), "survive.pillar-up", {
|
||||
onComplete: (res) => reportAdviceOutcome({ lessonId: fbAdvice.lessonId, succeeded: !!res?.ok }),
|
||||
});
|
||||
return { action: "dispatched", kind: "curriculum-fallback-pillar-up", label: "survive.pillar-up", lessonId: fbAdvice.lessonId };
|
||||
}
|
||||
return { action: "noop", kind: "curriculum-fallback-advice-avoid", label: fallbackId, lessonId: fbAdvice.lessonId };
|
||||
}
|
||||
if (fallbackId === "explore.far") {
|
||||
ctx.dispatch(() => runSkill("explore.far", ctx), "explore.far", {});
|
||||
return { action: "dispatched", kind: "curriculum-explore-far", label: "explore.far" };
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { skill as exploreFar } from "./explore-far.js";
|
||||
import { skill as flee } from "./flee.js";
|
||||
import { skill as sleep } from "./sleep.js";
|
||||
import { skill as tunnelOut } from "./recovery-tunnel-out.js";
|
||||
import { skill as pillarUp } from "./pillar-up.js";
|
||||
import { skill as diagPhysics } from "./diagnose-physics.js";
|
||||
import { skill as diagScan, matchSkill as diagMatch } from "./diagnose-scan.js";
|
||||
import { skill as gatherStone } from "./gather-stone.js";
|
||||
@@ -72,6 +73,7 @@ register(exploreFar);
|
||||
register(flee);
|
||||
register(sleep);
|
||||
register(tunnelOut);
|
||||
register(pillarUp);
|
||||
register(diagPhysics);
|
||||
register(diagScan);
|
||||
register(diagMatch);
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
// survive.pillar-up — escape a pit by placing blocks underneath the bot
|
||||
// and jumping onto them. The classic Minecraft "pillaring" technique:
|
||||
// look straight down, jump, place block at feet, repeat. Works with
|
||||
// any solid placeable block (dirt, cobblestone, sand, etc.). Needs
|
||||
// NO pickaxe — this is the bot's escape hatch when tunnel-out keeps
|
||||
// failing because the surrounding terrain is unbreakable without tools.
|
||||
//
|
||||
// Strategy:
|
||||
// 1. Find a placeable solid block in inventory (dirt > cobblestone >
|
||||
// stone > netherrack > anything solid).
|
||||
// 2. Equip it.
|
||||
// 3. Look straight down.
|
||||
// 4. Loop up to MAX_PILLAR steps:
|
||||
// - Jump (control:on then off after delay).
|
||||
// - In the jump apex, place block on the block below the bot.
|
||||
// - Land on the new block.
|
||||
// - Check we actually rose +1 in Y.
|
||||
// - Stop early if we cleared open sky above (yaw test).
|
||||
|
||||
import pathfinderPkg from "mineflayer-pathfinder";
|
||||
const { pathfinder } = pathfinderPkg;
|
||||
import { info, warn } from "../log.js";
|
||||
|
||||
const PILLAR_PREFERENCE = [
|
||||
"dirt", "cobblestone", "stone", "andesite", "diorite", "granite",
|
||||
"cobbled_deepslate", "deepslate", "sand", "gravel",
|
||||
"oak_planks", "spruce_planks", "birch_planks", "dark_oak_planks",
|
||||
"jungle_planks", "acacia_planks", "mangrove_planks", "cherry_planks",
|
||||
"netherrack",
|
||||
];
|
||||
|
||||
let pathfinderLoaded = new WeakSet();
|
||||
function ensurePathfinder(bot) {
|
||||
if (pathfinderLoaded.has(bot)) return;
|
||||
try { bot.loadPlugin(pathfinder); pathfinderLoaded.add(bot); } catch {}
|
||||
}
|
||||
|
||||
function pickPillarBlock(bot) {
|
||||
const items = bot.inventory?.items?.() ?? [];
|
||||
for (const name of PILLAR_PREFERENCE) {
|
||||
const found = items.find((i) => i.name === name && i.count > 0);
|
||||
if (found) return found;
|
||||
}
|
||||
// Fallback: any block that looks placeable (has _block suffix or known names).
|
||||
const fallback = items.find((i) =>
|
||||
/^(.*_planks|.*_log|.*_wool|cobble|netherrack|stone|dirt|sand|gravel|terracotta|wood)$/i.test(i.name),
|
||||
);
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function placeBlockAtFeet(bot) {
|
||||
// Find the block directly below the bot (referenceBlock for placement).
|
||||
const pos = bot.entity?.position;
|
||||
if (!pos) return { ok: false, reason: "no position" };
|
||||
const below = bot.blockAt(pos.offset(0, -1, 0));
|
||||
if (!below) return { ok: false, reason: "no block below" };
|
||||
if (below.name === "air" || below.name === "cave_air" || below.name === "void_air") {
|
||||
// We're already mid-air — can't place on air. Need to land first.
|
||||
return { ok: false, reason: "below is air" };
|
||||
}
|
||||
try {
|
||||
// placeBlock direction: place on top face (vec3(0, 1, 0))
|
||||
await bot.placeBlock(below, { x: 0, y: 1, z: 0 });
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
return { ok: false, reason: e?.message ?? String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
async function pillarStep(bot) {
|
||||
const startY = bot.entity?.position?.y ?? 0;
|
||||
|
||||
// Look straight down so placement reference is correct.
|
||||
try { await bot.look(bot.entity.yaw, Math.PI / 2, true); } catch {}
|
||||
|
||||
// Jump — engage control, hold briefly, release. Mineflayer's setControlState
|
||||
// handles the jump for us.
|
||||
bot.setControlState("jump", true);
|
||||
await sleep(80);
|
||||
bot.setControlState("jump", false);
|
||||
|
||||
// In the apex (~200-300ms), try to place. Brief wait so we're airborne.
|
||||
await sleep(200);
|
||||
|
||||
const place = await placeBlockAtFeet(bot);
|
||||
if (!place.ok) {
|
||||
// Likely either still on ground or in air — retry once with longer wait.
|
||||
await sleep(150);
|
||||
const retry = await placeBlockAtFeet(bot);
|
||||
if (!retry.ok) return { ok: false, reason: retry.reason };
|
||||
}
|
||||
|
||||
// Wait for the bot to settle on the new block.
|
||||
await sleep(400);
|
||||
const endY = bot.entity?.position?.y ?? startY;
|
||||
const climbed = endY - startY;
|
||||
return { ok: climbed >= 0.5, climbed };
|
||||
}
|
||||
|
||||
function inPit(bot) {
|
||||
// Heuristic "we're in a pit": there's a solid block within 3 blocks
|
||||
// in at least 2 of the 4 cardinal directions at head height.
|
||||
const pos = bot.entity?.position;
|
||||
if (!pos) return false;
|
||||
const head = pos.offset(0, 1, 0);
|
||||
let walls = 0;
|
||||
for (const [dx, dz] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
||||
for (let r = 1; r <= 2; r++) {
|
||||
const b = bot.blockAt?.(head.offset(dx * r, 0, dz * r));
|
||||
if (b && b.name !== "air" && b.name !== "cave_air") {
|
||||
walls += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return walls >= 2;
|
||||
}
|
||||
|
||||
export const skill = Object.freeze({
|
||||
id: "survive.pillar-up",
|
||||
title: "Pillar up to escape a pit",
|
||||
timeoutMs: 45_000,
|
||||
preconditions(ctx) {
|
||||
if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" };
|
||||
const block = pickPillarBlock(ctx.bot);
|
||||
if (!block) {
|
||||
return { ok: false, code: "missing_material", detail: "no placeable block in inventory (dirt/cobble/planks/etc.)" };
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
async execute(ctx, args = {}) {
|
||||
const bot = ctx.bot;
|
||||
ensurePathfinder(bot);
|
||||
// Stop any active pathing so we control movement.
|
||||
try { bot.pathfinder?.setGoal?.(null); } catch {}
|
||||
|
||||
// Pick + equip a pillar block.
|
||||
const block = pickPillarBlock(bot);
|
||||
if (!block) {
|
||||
return { ok: false, code: "missing_material", detail: "no placeable block", worldDelta: null };
|
||||
}
|
||||
try {
|
||||
await bot.equip(block, "hand");
|
||||
} catch (e) {
|
||||
return { ok: false, code: "failed", detail: `equip failed: ${e?.message ?? e}`, worldDelta: null };
|
||||
}
|
||||
info("action", `pillar-up: using ${block.name} (x${block.count}) to climb`);
|
||||
|
||||
const maxSteps = Math.max(2, Math.min(args?.maxSteps ?? 8, 16));
|
||||
const startY = bot.entity?.position?.y ?? 0;
|
||||
let placed = 0;
|
||||
let lastReason = null;
|
||||
|
||||
for (let i = 0; i < maxSteps; i++) {
|
||||
const step = await pillarStep(bot);
|
||||
if (step.ok) {
|
||||
placed += 1;
|
||||
const inv = bot.inventory?.items?.().find((it) => it.type === block.type);
|
||||
if (!inv || inv.count <= 0) {
|
||||
info("action", `pillar-up: out of ${block.name} after ${placed} steps`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
lastReason = step.reason;
|
||||
warn("action", `pillar-up: step ${i + 1} failed (${step.reason})`);
|
||||
// Retry a couple times before giving up — placement timing is finicky.
|
||||
if (i < 2) continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try { bot.setControlState("jump", false); } catch {}
|
||||
const endY = bot.entity?.position?.y ?? startY;
|
||||
const climbed = endY - startY;
|
||||
const stillPit = inPit(bot);
|
||||
|
||||
if (placed === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "no_progress",
|
||||
detail: lastReason ?? "could not place any blocks",
|
||||
worldDelta: null,
|
||||
};
|
||||
}
|
||||
if (stillPit && climbed < 2) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "still_pitted",
|
||||
detail: { placed, climbed, lastReason },
|
||||
worldDelta: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
code: "done",
|
||||
detail: { placed, climbed, blockType: block.name },
|
||||
worldDelta: { climbedY: climbed, mode: "pillar-up" },
|
||||
};
|
||||
},
|
||||
validate(ctx, result) {
|
||||
return result.ok && (result.worldDelta?.climbedY ?? 0) >= 1;
|
||||
},
|
||||
recover(ctx, result) {
|
||||
if (result.code === "missing_material") {
|
||||
return { hint: "wander", reason: "no pillar block; need to gather dirt or cobble" };
|
||||
}
|
||||
if (result.code === "still_pitted") {
|
||||
return { hint: "wander", reason: "pillar-up didn't escape; try tunnel-out next" };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Test exports
|
||||
export const __testing = { pickPillarBlock, inPit, PILLAR_PREFERENCE };
|
||||
@@ -0,0 +1,88 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { skill, __testing } from "./pillar-up.js";
|
||||
|
||||
const { pickPillarBlock, inPit, PILLAR_PREFERENCE } = __testing;
|
||||
|
||||
function makePos(x, y, z) {
|
||||
return {
|
||||
x, y, z,
|
||||
offset(dx, dy, dz) { return makePos(x + dx, y + dy, z + dz); },
|
||||
};
|
||||
}
|
||||
function mockBot({ items = [], blocks = {}, pos = makePos(0, 64, 0) } = {}) {
|
||||
const handlers = {};
|
||||
return {
|
||||
entity: { position: pos, yaw: 0, pitch: 0 },
|
||||
inventory: { items: () => items },
|
||||
blockAt(p) { return blocks[`${Math.floor(p.x)},${Math.floor(p.y)},${Math.floor(p.z)}`] ?? { name: "air" }; },
|
||||
on(ev, fn) { handlers[ev] = fn; },
|
||||
setControlState() {},
|
||||
async equip() { return true; },
|
||||
async look() { return true; },
|
||||
async placeBlock() { return true; },
|
||||
};
|
||||
}
|
||||
|
||||
test("pickPillarBlock: chooses preferred block from inventory", () => {
|
||||
const items = [
|
||||
{ name: "stone", count: 4, type: 1 },
|
||||
{ name: "dirt", count: 12, type: 2 },
|
||||
];
|
||||
const chosen = pickPillarBlock(mockBot({ items }));
|
||||
assert.equal(chosen.name, "dirt", "prefers dirt over stone");
|
||||
});
|
||||
|
||||
test("pickPillarBlock: returns null when nothing placeable", () => {
|
||||
const chosen = pickPillarBlock(mockBot({ items: [{ name: "carrot", count: 3 }] }));
|
||||
assert.equal(chosen, null);
|
||||
});
|
||||
|
||||
test("pickPillarBlock: falls back to wood-like names", () => {
|
||||
const chosen = pickPillarBlock(mockBot({ items: [{ name: "oak_planks", count: 5 }] }));
|
||||
assert.equal(chosen.name, "oak_planks");
|
||||
});
|
||||
|
||||
test("inPit: detects walls in cardinal directions", () => {
|
||||
const blocks = {
|
||||
"1,65,0": { name: "stone" },
|
||||
"-1,65,0": { name: "stone" },
|
||||
};
|
||||
const bot = mockBot({ blocks });
|
||||
assert.equal(inPit(bot), true, "two walls = pit");
|
||||
|
||||
const open = mockBot({ blocks: {} });
|
||||
assert.equal(inPit(open), false);
|
||||
});
|
||||
|
||||
test("inPit: detects walls 2 blocks away too", () => {
|
||||
const blocks = {
|
||||
"2,65,0": { name: "stone" },
|
||||
"0,65,-2": { name: "stone" },
|
||||
};
|
||||
const bot = mockBot({ blocks });
|
||||
assert.equal(inPit(bot), true);
|
||||
});
|
||||
|
||||
test("PILLAR_PREFERENCE: dirt is highest priority", () => {
|
||||
assert.equal(PILLAR_PREFERENCE[0], "dirt");
|
||||
assert.ok(PILLAR_PREFERENCE.includes("cobblestone"));
|
||||
});
|
||||
|
||||
test("skill: preconditions fail without placeable block", () => {
|
||||
const ctx = { bot: mockBot({ items: [{ name: "carrot", count: 1 }] }) };
|
||||
const pre = skill.preconditions(ctx);
|
||||
assert.equal(pre.ok, false);
|
||||
assert.equal(pre.code, "missing_material");
|
||||
});
|
||||
|
||||
test("skill: preconditions pass with dirt", () => {
|
||||
const ctx = { bot: mockBot({ items: [{ name: "dirt", count: 8 }] }) };
|
||||
const pre = skill.preconditions(ctx);
|
||||
assert.equal(pre.ok, true);
|
||||
});
|
||||
|
||||
test("skill: id and timeout are sensible", () => {
|
||||
assert.equal(skill.id, "survive.pillar-up");
|
||||
assert.ok(skill.timeoutMs >= 30_000 && skill.timeoutMs <= 90_000);
|
||||
});
|
||||
Reference in New Issue
Block a user