Files
mayatnikovandClaude Opus 4.7 86e5294bb8 chore: snapshot pre-v0.2.0 WIP (pathfinder/reflex/metrics/skills improvements)
Baseline for the v0.2.0 self-learning iteration. All 205 tests pass on this
state. Subsequent commits in this branch layer the knowledge base,
post-mortem coach, and persona narration on top.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:05:12 +03:00

190 lines
5.6 KiB
JavaScript

// Build a compact, JSON-safe snapshot of the world around the bot. Used both
// for reflex decisions and for periodic IPC STATUS events.
import { foods } from "./skills/groups.js";
import { findBlocksByName } from "./perception.js";
function vec3ToObj(v) {
if (!v) return null;
return { x: Math.round(v.x * 100) / 100, y: Math.round(v.y * 100) / 100, z: Math.round(v.z * 100) / 100 };
}
const HOSTILE = new Set([
"zombie",
"skeleton",
"creeper",
"spider",
"witch",
"pillager",
"vindicator",
"husk",
"stray",
"drowned",
"phantom",
"enderman",
"slime",
"magma_cube",
"hoglin",
"piglin_brute",
"ravager",
"warden",
"breeze",
"bogged",
]);
const PASSIVE = new Set([
"cow",
"pig",
"chicken",
"sheep",
"rabbit",
"mooshroom",
"cod",
"salmon",
]);
const INTERESTING_BLOCK_GROUPS = Object.freeze({
logs: ["oak_log", "dark_oak_log", "spruce_log", "birch_log", "jungle_log", "acacia_log", "mangrove_log", "cherry_log", "pale_oak_log"],
stone: ["stone", "cobblestone", "deepslate", "cobbled_deepslate", "andesite", "diorite", "granite"],
water: ["water"],
lava: ["lava", "fire", "soul_fire"],
beds: ["white_bed", "orange_bed", "magenta_bed", "light_blue_bed", "yellow_bed", "lime_bed", "pink_bed", "gray_bed", "light_gray_bed", "cyan_bed", "purple_bed", "blue_bed", "brown_bed", "green_bed", "red_bed", "black_bed"],
storage: ["chest", "trapped_chest", "barrel"],
wool: ["white_wool", "orange_wool", "magenta_wool", "light_blue_wool", "yellow_wool", "lime_wool", "pink_wool", "gray_wool", "light_gray_wool", "cyan_wool", "purple_wool", "blue_wool", "brown_wool", "green_wool", "red_wool", "black_wool"],
crops: ["wheat", "carrots", "potatoes", "beetroots", "sweet_berry_bush"],
coal: ["coal_ore", "deepslate_coal_ore"],
});
function inventoryCounts(bot) {
return (bot.inventory?.items?.() ?? []).reduce((acc, item) => {
acc[item.name] = (acc[item.name] ?? 0) + item.count;
return acc;
}, {});
}
function countInterestingBlocks(bot, pos, radius = 16) {
const out = {};
for (const [kind, names] of Object.entries(INTERESTING_BLOCK_GROUPS)) {
let positions = [];
try {
positions = findBlocksByName(bot, names, { maxDistance: radius, count: 16 });
} catch {
positions = [];
}
if (positions.length === 0) continue;
const nearest = positions
.map((p) => ({ position: vec3ToObj(p), distance: Math.round(Math.hypot(p.x - pos.x, p.z - pos.z) * 10) / 10 }))
.sort((a, b) => a.distance - b.distance)[0];
out[kind] = { count: positions.length, nearest };
}
return out;
}
function carriedEquipment(bot) {
const slots = bot.inventory?.slots ?? [];
return {
hand: bot.heldItem?.name ?? null,
head: slots[5]?.name ?? null,
torso: slots[6]?.name ?? null,
legs: slots[7]?.name ?? null,
feet: slots[8]?.name ?? null,
};
}
function hasEdibleFood(bot, inventory) {
const allowed = foods(bot);
return Object.keys(inventory ?? {}).some((name) => allowed.has(name));
}
function entityDistance(e, pos) {
try {
return Math.round(e.position.distanceTo(pos) * 10) / 10;
} catch {
return null;
}
}
function entitySnapshot(e, pos) {
return {
name: e.username ?? e.name ?? e.displayName ?? "?",
type: e.type ?? null,
distance: entityDistance(e, pos),
position: vec3ToObj(e.position),
};
}
function blockNameAt(bot, pos) {
try {
return bot.blockAt(pos)?.name ?? null;
} catch {
return null;
}
}
export function snapshot(bot) {
if (!bot || !bot.entity) {
return { connected: false };
}
const pos = bot.entity.position;
const entities = Object.values(bot.entities || {});
const players = entities.filter((e) => e.type === "player" && e.username && e.username !== bot.username);
const hostiles = entities.filter((e) => HOSTILE.has((e.name || "").toLowerCase()));
const closestHostile = hostiles.reduce((best, e) => {
const d = e.position.distanceTo(pos);
return !best || d < best.d ? { d, e } : best;
}, null);
const inventory = inventoryCounts(bot);
const nearbyBlocks = countInterestingBlocks(bot, pos);
const droppedItems = entities
.filter((e) => e.type === "object" || e.name === "item")
.filter((e) => e.position && e.position.distanceTo(pos) <= 24)
.map((e) => entitySnapshot(e, pos))
.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity))
.slice(0, 12);
const passives = entities
.filter((e) => PASSIVE.has((e.name || "").toLowerCase()) && e.position)
.map((e) => entitySnapshot(e, pos))
.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity))
.slice(0, 12);
const footBlock = blockNameAt(bot, pos);
const belowBlock = blockNameAt(bot, pos.offset(0, -1, 0));
const headBlock = blockNameAt(bot, pos.offset(0, 1, 0));
const hazards = {
lavaNearby: !!nearbyBlocks.lava,
inFluid: footBlock === "water" || footBlock === "lava",
footBlock,
belowBlock,
headBlock,
};
return {
connected: true,
username: bot.username,
position: vec3ToObj(pos),
health: bot.health,
food: bot.food,
saturation: bot.foodSaturation,
experience: bot.experience?.level,
time: bot.time?.timeOfDay,
isDay: bot.time?.isDay,
weather: { rain: bot.isRaining, thunder: bot.thundering },
dimension: bot.game?.dimension,
inventory,
hasFood: hasEdibleFood(bot, inventory),
equipment: carriedEquipment(bot),
nearbyBlocks,
nearbyEntities: {
passives,
droppedItems,
},
hazards,
biome: bot.blockAt?.(pos)?.biome?.name ?? bot.blockAt?.(pos)?.biome ?? null,
players: players.map((p) => ({ name: p.username, distance: Math.round(p.position.distanceTo(pos)) })),
hostileCount: hostiles.length,
closestHostile: closestHostile
? { name: closestHostile.e.name, distance: Math.round(closestHostile.d * 10) / 10 }
: null,
};
}