Files
d2e52a1b79 feat(runtime): compatibility hardening (Phase 7) (#18)
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>
2026-05-25 22:39:38 +03:00

95 lines
2.6 KiB
JavaScript

// "Does this look like a player build?" heuristic. Pure function;
// caller passes a list of nearby blocks and an isOwned predicate.
//
// Approach (intentionally conservative — false positives are fine):
// * Count the man-made blocks within the test set. Man-made =
// processed wood, stone bricks, smooth stone, glass, wool, etc.
// Things that don't naturally generate without effort.
// * Count owned-by-this-bot blocks.
// * If man-made density is high AND the area is not predominantly
// owned by this bot → flag as player_build. The skill code
// should refuse to dig/place there.
//
// We don't try to detect ages, signs, or claim plugins — heuristics
// only. The cost of false positives is "bot wandered around a
// rectangular hut", which is fine; the cost of false negatives is
// "bot griefed a player base", which is unacceptable.
const MAN_MADE_PREFIXES = [
"_planks",
"stone_bricks",
"smooth_stone",
"polished_",
"chiseled_",
"glass",
"wool",
"terracotta",
"concrete",
"sandstone_stairs",
"_slab",
"_stairs",
"_fence",
"_door",
"_trapdoor",
"crafting_table",
"furnace",
"chest",
"barrel",
"shulker_box",
"anvil",
"enchanting_table",
"bookshelf",
"ladder",
];
const MAN_MADE_EXACT = new Set([
"crafting_table",
"furnace",
"chest",
"barrel",
"bookshelf",
"anvil",
"enchanting_table",
"ladder",
"bell",
"lectern",
"composter",
"smoker",
"blast_furnace",
"loom",
"cartography_table",
"fletching_table",
"stonecutter",
"grindstone",
]);
export function isManMadeBlockName(name) {
if (!name) return false;
if (MAN_MADE_EXACT.has(name)) return true;
return MAN_MADE_PREFIXES.some((p) => name.includes(p));
}
export function classifyArea({ blocks, isOwned, minSamples = 5, manMadeThreshold = 0.4 }) {
if (!Array.isArray(blocks) || blocks.length < minSamples) {
return { verdict: "insufficient_data", manMade: 0, owned: 0, total: blocks?.length ?? 0 };
}
let manMade = 0;
let owned = 0;
for (const b of blocks) {
if (!b || !b.name) continue;
if (isManMadeBlockName(b.name)) manMade++;
if (b.position && typeof isOwned === "function" && isOwned(b.position)) owned++;
}
const total = blocks.length;
const manMadeRatio = manMade / total;
const ownedRatio = owned / total;
if (manMadeRatio >= manMadeThreshold && ownedRatio < manMadeRatio * 0.6) {
return { verdict: "player_build", manMade, owned, total, manMadeRatio, ownedRatio };
}
return { verdict: "natural_or_owned", manMade, owned, total, manMadeRatio, ownedRatio };
}
export function shouldAvoid(area) {
return area?.verdict === "player_build";
}