Phase 7 of plans/autonomous-survival-bot-prd.md. Five small modules
that close the recurring "shared state" and "version-pinned list"
failure modes the PRD flags in §7 and §5.4.
New:
- runtime/movement-profiles.js: named profiles (GATHER, TRAVEL, FLEE,
BUILD, RETURN_TO_BASE) as pure descriptors via PROFILE_DEFAULTS,
plus applyProfile(profile, bot) that hands a fresh Movements to
pathfinder. Avoids the "flee left canDig=false on the shared
Movements, next chop got stuck in canopy" regression.
- runtime/owned-blocks.js: JSONL ledger of blocks this bot placed/
removed (state/<host>/owned-blocks.jsonl); isOwned({x,y,z}) for
O(1) lookups; ensureDir() makes the parent dir lazily.
- runtime/claim-avoidance.js: classifyArea({blocks, isOwned}) returns
player_build / natural_or_owned / insufficient_data based on
man-made block density vs ownership ratio; shouldAvoid(area) helper.
Designed for gather/place skills to call before touching contested
area.
- runtime/skills/compat.test.js: runs runtime/skills/groups.js against
real minecraft-data registries for 1.18.2, 1.20.4, 1.21.5; spot-
checks that pale_oak_log only appears on 1.21+ etc.
- runtime/compat.test.js: 10 tests covering movement descriptors,
isManMadeBlockName, classifyArea, owned-blocks markPlaced/dedup/
isOwned/markRemoved.
npm test now 79/79.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
86 lines
2.7 KiB
JavaScript
86 lines
2.7 KiB
JavaScript
// Owned-blocks ledger. The bot places blocks (crafting table, chest,
|
|
// torch, shelter walls). When it later considers mining something or
|
|
// the claim-avoidance heuristic asks "is this a player build?", it
|
|
// needs to recognise its own work and not treat it as someone else's.
|
|
//
|
|
// Stored under state/<host>/owned-blocks.jsonl as a streaming append-
|
|
// only log keyed by "x,y,z@dimension". On boot we load it into a Set
|
|
// for O(1) lookups. The file is small enough (a few hundred entries
|
|
// for a village) that we don't bother compacting.
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { stateDir } from "./config.js";
|
|
|
|
const LEDGER_PATH = path.join(stateDir, "owned-blocks.jsonl");
|
|
|
|
// Make sure the parent directory exists. Cheap to repeat — fs.mkdirSync
|
|
// with recursive:true is idempotent — and avoids ENOENT in tests that
|
|
// start from a fresh tmp dir.
|
|
function ensureDir() {
|
|
try { fs.mkdirSync(stateDir, { recursive: true }); } catch {}
|
|
}
|
|
|
|
function keyOf(x, y, z, dimension = "overworld") {
|
|
return `${Math.round(x)},${Math.round(y)},${Math.round(z)}@${dimension}`;
|
|
}
|
|
|
|
function loadLedger() {
|
|
const set = new Set();
|
|
try {
|
|
const raw = fs.readFileSync(LEDGER_PATH, "utf8");
|
|
for (const line of raw.split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
try {
|
|
const entry = JSON.parse(trimmed);
|
|
if (entry.action === "place") set.add(keyOf(entry.x, entry.y, entry.z, entry.dimension));
|
|
else if (entry.action === "remove") set.delete(keyOf(entry.x, entry.y, entry.z, entry.dimension));
|
|
} catch {
|
|
// skip malformed line
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (e.code !== "ENOENT") throw e;
|
|
}
|
|
return set;
|
|
}
|
|
|
|
export function createOwnedBlocksLedger() {
|
|
const owned = loadLedger();
|
|
|
|
function append(action, entry) {
|
|
ensureDir();
|
|
const line = JSON.stringify({ ts: new Date().toISOString(), action, ...entry }) + "\n";
|
|
fs.appendFileSync(LEDGER_PATH, line);
|
|
}
|
|
|
|
function markPlaced({ x, y, z, dimension = "overworld", blockType = null, skill = null }) {
|
|
const k = keyOf(x, y, z, dimension);
|
|
if (owned.has(k)) return;
|
|
owned.add(k);
|
|
append("place", { x: Math.round(x), y: Math.round(y), z: Math.round(z), dimension, blockType, skill });
|
|
}
|
|
|
|
function markRemoved({ x, y, z, dimension = "overworld" }) {
|
|
const k = keyOf(x, y, z, dimension);
|
|
if (!owned.has(k)) return;
|
|
owned.delete(k);
|
|
append("remove", { x: Math.round(x), y: Math.round(y), z: Math.round(z), dimension });
|
|
}
|
|
|
|
function isOwned({ x, y, z, dimension = "overworld" }) {
|
|
return owned.has(keyOf(x, y, z, dimension));
|
|
}
|
|
|
|
function size() {
|
|
return owned.size;
|
|
}
|
|
|
|
function snapshot() {
|
|
return new Set(owned);
|
|
}
|
|
|
|
return { markPlaced, markRemoved, isOwned, size, snapshot };
|
|
}
|