5b28002001
Major changes from this session: Training: - 0.6.0 training running: 9B on steel141 3090 Ti, 27B on rented H100 NVL - 7,256 merged training examples (up from 3,183) - New training data: failure modes (85), midloop messaging (27), prompt injection defense (29), personality (32), gold from quarantine bank (232), new tool examples (30), claude's own experience (10) - All training data RCON-validated at 100% pass rate - Bake-off: gemma3:27b 66%, qwen3.5:27b 61%, translategemma:27b 56% Oracle Bot (Mind's Eye): - Invisible spectator bot (mineflayer) streams world state via WebSocket - HTML5 Canvas frontend at mind.mortdec.ai - Real-time tool trace visualization with expandable entries - Streaming model tokens during inference - Gateway integration: fire-and-forget POST /trace on every tool call Reinforcement Learning: - Gymnasium environment wrapping mineflayer bot (minecraft_env.py) - PPO training via Stable Baselines3 (10K param policy network) - Behavioral cloning pretraining (97.5% accuracy on expert policy) - Infinite training loop with auto-restart and checkpoint resume - Bot learns combat, survival, navigation from raw experience Bot Army: - 8-soldier marching formation with autonomous combat - Combat bots using mineflayer-pvp, pathfinder, armor-manager - Multilingual prayer bots via translategemma:27b (18 languages) - Frame-based AI architecture: LLM planner + reactive micro-scripts Infrastructure: - Fixed mattpc.sethpc.xyz billing gateway (API key + player list parser) - Billing gateway now tracks all LAN traffic (LAN auto-auth) - Gateway fallback for empty god-mode responses - Updated mortdec.ai landing page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
233 lines
6.9 KiB
JavaScript
233 lines
6.9 KiB
JavaScript
/**
|
|
* explorer_bot.js — Claude explores the world through a mineflayer bot's eyes.
|
|
*
|
|
* Connects to the server, reports what it sees, and accepts movement commands
|
|
* via stdin. Outputs structured JSON observations.
|
|
*
|
|
* Usage: node explorer_bot.js [host] [port]
|
|
*/
|
|
|
|
const mineflayer = require('mineflayer');
|
|
const { Vec3 } = require('vec3');
|
|
const readline = require('readline');
|
|
|
|
const host = process.argv[2] || '192.168.0.244';
|
|
const port = parseInt(process.argv[3] || '25567', 10);
|
|
|
|
const bot = mineflayer.createBot({
|
|
host,
|
|
port,
|
|
username: 'ClaudeBot',
|
|
auth: 'offline',
|
|
version: '1.21.11',
|
|
});
|
|
|
|
// ── Observation functions ──
|
|
|
|
function observe() {
|
|
if (!bot.entity) return '{"error": "not spawned"}';
|
|
|
|
const pos = bot.entity.position;
|
|
const obs = {
|
|
position: { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) },
|
|
health: bot.health,
|
|
food: bot.food,
|
|
gamemode: bot.game.gameMode,
|
|
time: bot.time.timeOfDay,
|
|
isRaining: bot.isRaining,
|
|
};
|
|
|
|
// Block below and around
|
|
obs.blockBelow = blockName(pos.offset(0, -1, 0));
|
|
obs.blockAt = blockName(pos);
|
|
obs.blockAbove = blockName(pos.offset(0, 2, 0));
|
|
|
|
// Look in all 4 directions for interesting blocks
|
|
obs.surroundings = {};
|
|
const dirs = { north: [0, 0, -1], south: [0, 0, 1], east: [1, 0, 0], west: [-1, 0, 0] };
|
|
for (const [name, [dx, dy, dz]] of Object.entries(dirs)) {
|
|
const blocks = [];
|
|
for (let dist = 1; dist <= 8; dist++) {
|
|
const b = blockName(pos.offset(dx * dist, 0, dz * dist));
|
|
if (b !== 'air') blocks.push({ dist, block: b });
|
|
}
|
|
if (blocks.length) obs.surroundings[name] = blocks;
|
|
}
|
|
|
|
// Nearby entities
|
|
obs.entities = [];
|
|
for (const entity of Object.values(bot.entities)) {
|
|
if (entity === bot.entity) continue;
|
|
const dist = entity.position.distanceTo(pos);
|
|
if (dist <= 20) {
|
|
obs.entities.push({
|
|
type: entity.type === 'player' ? 'player' : (entity.name || entity.type),
|
|
name: entity.username || null,
|
|
distance: Math.round(dist),
|
|
position: {
|
|
x: Math.floor(entity.position.x),
|
|
y: Math.floor(entity.position.y),
|
|
z: Math.floor(entity.position.z),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// Scan a 5x5 area at foot level for terrain overview
|
|
obs.terrain = [];
|
|
for (let dx = -5; dx <= 5; dx++) {
|
|
for (let dz = -5; dz <= 5; dz++) {
|
|
// Find surface block (scan down from y+3)
|
|
for (let dy = 3; dy >= -3; dy--) {
|
|
const b = bot.blockAt(pos.offset(dx, dy, dz));
|
|
if (b && b.name !== 'air') {
|
|
obs.terrain.push({ dx, dz, y: Math.floor(pos.y) + dy, block: b.name });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(obs);
|
|
}
|
|
|
|
function blockName(vec) {
|
|
const b = bot.blockAt(vec);
|
|
return b ? b.name : 'unloaded';
|
|
}
|
|
|
|
// ── Movement functions ──
|
|
|
|
async function walkTo(x, y, z) {
|
|
const goal = new Vec3(x, y, z);
|
|
console.log(JSON.stringify({ action: 'walking', target: { x, y, z } }));
|
|
|
|
// Simple movement: look at target and walk forward
|
|
await bot.lookAt(goal);
|
|
bot.setControlState('forward', true);
|
|
|
|
// Walk until close or timeout
|
|
return new Promise((resolve) => {
|
|
const check = setInterval(() => {
|
|
const dist = bot.entity.position.distanceTo(goal);
|
|
if (dist < 2) {
|
|
bot.setControlState('forward', false);
|
|
clearInterval(check);
|
|
resolve(true);
|
|
}
|
|
}, 200);
|
|
|
|
setTimeout(() => {
|
|
bot.setControlState('forward', false);
|
|
clearInterval(check);
|
|
resolve(false);
|
|
}, 10000); // 10s max walk
|
|
});
|
|
}
|
|
|
|
async function lookAround() {
|
|
const snapshots = [];
|
|
const yaws = [0, Math.PI / 2, Math.PI, -Math.PI / 2]; // N, E, S, W
|
|
const names = ['north', 'east', 'south', 'west'];
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
await bot.look(yaws[i], 0, true);
|
|
await new Promise(r => setTimeout(r, 500));
|
|
|
|
// What do I see in this direction?
|
|
const pos = bot.entity.position;
|
|
const dx = Math.round(Math.sin(yaws[i]));
|
|
const dz = Math.round(-Math.cos(yaws[i]));
|
|
|
|
const visible = [];
|
|
for (let dist = 1; dist <= 16; dist++) {
|
|
const b = bot.blockAt(pos.offset(dx * dist, 0, dz * dist));
|
|
const bUp = bot.blockAt(pos.offset(dx * dist, 1, dz * dist));
|
|
const bDown = bot.blockAt(pos.offset(dx * dist, -1, dz * dist));
|
|
if (b && b.name !== 'air') visible.push({ dist, block: b.name, level: 'eye' });
|
|
if (bDown && bDown.name !== 'air' && bDown.name !== (b && b.name)) visible.push({ dist, block: bDown.name, level: 'ground' });
|
|
}
|
|
snapshots.push({ direction: names[i], blocks: visible.slice(0, 8) });
|
|
}
|
|
return snapshots;
|
|
}
|
|
|
|
// ── Event handlers ──
|
|
|
|
bot.on('login', () => {
|
|
console.log(JSON.stringify({ event: 'login', username: bot.username }));
|
|
});
|
|
|
|
bot.on('spawn', async () => {
|
|
console.log(JSON.stringify({ event: 'spawn', ...JSON.parse(observe()) }));
|
|
|
|
// Initial look around
|
|
const views = await lookAround();
|
|
console.log(JSON.stringify({ event: 'look_around', views }));
|
|
});
|
|
|
|
bot.on('chat', (username, message) => {
|
|
if (username === bot.username) return;
|
|
console.log(JSON.stringify({ event: 'chat', from: username, message }));
|
|
});
|
|
|
|
bot.on('health', () => {
|
|
console.log(JSON.stringify({ event: 'health', health: bot.health, food: bot.food }));
|
|
});
|
|
|
|
bot.on('death', () => {
|
|
console.log(JSON.stringify({ event: 'death' }));
|
|
});
|
|
|
|
bot.on('kicked', (reason) => {
|
|
console.log(JSON.stringify({ event: 'kicked', reason: typeof reason === 'object' ? JSON.stringify(reason) : reason }));
|
|
});
|
|
|
|
bot.on('error', (err) => {
|
|
console.error(JSON.stringify({ event: 'error', message: err.message }));
|
|
});
|
|
|
|
// ── Command interface (stdin) ──
|
|
|
|
const rl = readline.createInterface({ input: process.stdin });
|
|
|
|
rl.on('line', async (line) => {
|
|
const cmd = line.trim();
|
|
|
|
if (cmd === 'observe' || cmd === 'look') {
|
|
console.log(observe());
|
|
}
|
|
else if (cmd === 'around') {
|
|
const views = await lookAround();
|
|
console.log(JSON.stringify({ action: 'look_around', views }));
|
|
}
|
|
else if (cmd.startsWith('walk ')) {
|
|
const parts = cmd.split(' ');
|
|
const x = parseInt(parts[1]), y = parseInt(parts[2]), z = parseInt(parts[3]);
|
|
const arrived = await walkTo(x, y, z);
|
|
console.log(JSON.stringify({ action: 'walk_result', arrived, ...JSON.parse(observe()) }));
|
|
}
|
|
else if (cmd === 'forward') {
|
|
bot.setControlState('forward', true);
|
|
setTimeout(() => {
|
|
bot.setControlState('forward', false);
|
|
console.log(observe());
|
|
}, 3000);
|
|
}
|
|
else if (cmd === 'jump') {
|
|
bot.setControlState('jump', true);
|
|
setTimeout(() => bot.setControlState('jump', false), 500);
|
|
setTimeout(() => console.log(observe()), 1000);
|
|
}
|
|
else if (cmd.startsWith('say ')) {
|
|
bot.chat(cmd.slice(4));
|
|
}
|
|
else if (cmd === 'quit') {
|
|
bot.end();
|
|
process.exit(0);
|
|
}
|
|
else {
|
|
console.log(JSON.stringify({ error: 'unknown command', commands: ['observe', 'around', 'walk X Y Z', 'forward', 'jump', 'say MSG', 'quit'] }));
|
|
}
|
|
});
|