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>
190 lines
5.6 KiB
JavaScript
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,
|
|
};
|
|
}
|