0.6.0 training session: Oracle Bot, RL combat, Mind's Eye, multilingual pipeline
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>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* mortal_claude.js — Claude experiences Minecraft as a mortal.
|
||||
*
|
||||
* No RCON. No creative mode. No cheats. Just survival.
|
||||
* Accepts commands via stdin, reports observations as JSON.
|
||||
* Can pray to God and use sudo like a real player.
|
||||
*/
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
let alive = true;
|
||||
let spawnCount = 0;
|
||||
|
||||
// ── Observation ──
|
||||
|
||||
function observe() {
|
||||
if (!bot.entity) return { error: 'not spawned' };
|
||||
const pos = bot.entity.position;
|
||||
const obs = {
|
||||
pos: { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) },
|
||||
hp: Math.round(bot.health * 10) / 10,
|
||||
food: bot.food,
|
||||
xp: bot.experience?.level || 0,
|
||||
time: bot.time?.timeOfDay || 0,
|
||||
isDay: (bot.time?.timeOfDay || 0) < 13000,
|
||||
rain: bot.isRaining || false,
|
||||
below: bname(pos.offset(0, -1, 0)),
|
||||
at: bname(pos),
|
||||
};
|
||||
|
||||
// Nearby threats and friendlies
|
||||
obs.mobs = [];
|
||||
obs.players = [];
|
||||
for (const e of Object.values(bot.entities)) {
|
||||
if (e === bot.entity) continue;
|
||||
const d = Math.round(e.position.distanceTo(pos));
|
||||
if (d > 24) continue;
|
||||
if (e.type === 'player') {
|
||||
obs.players.push({ name: e.username, dist: d });
|
||||
} else if (e.type === 'mob' || e.type === 'hostile') {
|
||||
obs.mobs.push({ type: e.name || e.type, dist: d });
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory summary
|
||||
const items = bot.inventory.items();
|
||||
if (items.length) {
|
||||
obs.inventory = items.map(i => `${i.name}x${i.count}`).join(', ');
|
||||
} else {
|
||||
obs.inventory = 'empty';
|
||||
}
|
||||
|
||||
// Armor
|
||||
const armor = [bot.inventory.slots[5], bot.inventory.slots[6], bot.inventory.slots[7], bot.inventory.slots[8]];
|
||||
obs.armor = armor.filter(Boolean).map(a => a.name).join(', ') || 'none';
|
||||
|
||||
// Terrain scan (compact)
|
||||
const terrain = new Map();
|
||||
for (let dx = -3; dx <= 3; dx++) {
|
||||
for (let dz = -3; dz <= 3; dz++) {
|
||||
const b = bot.blockAt(pos.offset(dx, -1, dz));
|
||||
if (b) {
|
||||
const n = b.name;
|
||||
terrain.set(n, (terrain.get(n) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
obs.ground = Object.fromEntries(terrain);
|
||||
|
||||
return obs;
|
||||
}
|
||||
|
||||
function bname(vec) {
|
||||
const b = bot.blockAt(vec);
|
||||
return b ? b.name : '?';
|
||||
}
|
||||
|
||||
// ── Survival actions ──
|
||||
|
||||
function eat() {
|
||||
const food = bot.inventory.items().find(i =>
|
||||
['cooked_beef', 'bread', 'cooked_porkchop', 'cooked_chicken', 'cooked_mutton',
|
||||
'golden_apple', 'apple', 'baked_potato', 'cooked_cod', 'cooked_salmon',
|
||||
'sweet_berries', 'melon_slice', 'carrot', 'potato'].includes(i.name)
|
||||
);
|
||||
if (food) {
|
||||
bot.equip(food, 'hand').then(() => {
|
||||
bot.activateItem();
|
||||
setTimeout(() => bot.deactivateItem(), 1800);
|
||||
}).catch(() => {});
|
||||
return food.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function flee() {
|
||||
// Run away from nearest hostile
|
||||
const pos = bot.entity.position;
|
||||
let nearestMob = null;
|
||||
let nearestDist = Infinity;
|
||||
for (const e of Object.values(bot.entities)) {
|
||||
if (e === bot.entity || e.type === 'player') continue;
|
||||
const d = e.position.distanceTo(pos);
|
||||
if (d < nearestDist && d < 16) {
|
||||
nearestDist = d;
|
||||
nearestMob = e;
|
||||
}
|
||||
}
|
||||
if (nearestMob) {
|
||||
// Run opposite direction
|
||||
const dx = pos.x - nearestMob.position.x;
|
||||
const dz = pos.z - nearestMob.position.z;
|
||||
const len = Math.sqrt(dx * dx + dz * dz) || 1;
|
||||
const target = pos.offset((dx / len) * 10, 0, (dz / len) * 10);
|
||||
bot.lookAt(target);
|
||||
bot.setControlState('forward', true);
|
||||
bot.setControlState('sprint', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
bot.setControlState('sprint', false);
|
||||
}, 4000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function attack() {
|
||||
const pos = bot.entity.position;
|
||||
let nearest = null;
|
||||
let nearestDist = Infinity;
|
||||
for (const e of Object.values(bot.entities)) {
|
||||
if (e === bot.entity || e.type === 'player') continue;
|
||||
const d = e.position.distanceTo(pos);
|
||||
if (d < 4 && d < nearestDist) {
|
||||
nearestDist = d;
|
||||
nearest = e;
|
||||
}
|
||||
}
|
||||
if (nearest) {
|
||||
// Equip best weapon
|
||||
const weapon = bot.inventory.items().find(i => i.name.includes('sword') || i.name.includes('axe'));
|
||||
if (weapon) bot.equip(weapon, 'hand').catch(() => {});
|
||||
bot.attack(nearest);
|
||||
return nearest.name || nearest.type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Events ──
|
||||
|
||||
bot.on('login', () => {
|
||||
out({ event: 'login', name: bot.username });
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
spawnCount++;
|
||||
alive = true;
|
||||
// Wait for chunks
|
||||
setTimeout(() => {
|
||||
const o = observe();
|
||||
out({ event: spawnCount === 1 ? 'first_spawn' : 'respawn', ...o });
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
bot.on('chat', (username, message) => {
|
||||
if (username === bot.username) return;
|
||||
out({ event: 'chat', from: username, msg: message });
|
||||
});
|
||||
|
||||
bot.on('whisper', (username, message) => {
|
||||
out({ event: 'whisper', from: username, msg: message });
|
||||
});
|
||||
|
||||
bot.on('health', () => {
|
||||
if (bot.health <= 5 && alive) {
|
||||
out({ event: 'low_hp', hp: Math.round(bot.health * 10) / 10, food: bot.food });
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('death', () => {
|
||||
alive = false;
|
||||
out({ event: 'death', spawns: spawnCount, msg: 'I died.' });
|
||||
// Auto respawn
|
||||
setTimeout(() => {
|
||||
try { bot.respawn(); } catch (e) {}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
bot.on('entityHurt', (entity) => {
|
||||
if (entity === bot.entity) {
|
||||
out({ event: 'hurt', hp: Math.round(bot.health * 10) / 10 });
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
out({ event: 'kicked', reason: typeof reason === 'object' ? JSON.stringify(reason) : String(reason) });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
bot.on('error', (err) => {
|
||||
out({ event: 'error', msg: err.message });
|
||||
});
|
||||
|
||||
// ── Output ──
|
||||
|
||||
function out(obj) {
|
||||
console.log(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// ── Command interface ──
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
|
||||
rl.on('line', async (line) => {
|
||||
const cmd = line.trim();
|
||||
if (!cmd) return;
|
||||
|
||||
if (cmd === 'look') {
|
||||
out(observe());
|
||||
}
|
||||
else if (cmd === 'eat') {
|
||||
const food = eat();
|
||||
out({ action: 'eat', food: food || 'nothing edible' });
|
||||
}
|
||||
else if (cmd === 'flee') {
|
||||
const fled = flee();
|
||||
out({ action: 'flee', success: fled });
|
||||
if (fled) setTimeout(() => out(observe()), 4500);
|
||||
}
|
||||
else if (cmd === 'fight') {
|
||||
const target = attack();
|
||||
out({ action: 'fight', target: target || 'nothing nearby' });
|
||||
}
|
||||
else if (cmd === 'forward') {
|
||||
bot.setControlState('forward', true);
|
||||
setTimeout(() => { bot.setControlState('forward', false); out(observe()); }, 3000);
|
||||
}
|
||||
else if (cmd === 'back') {
|
||||
bot.setControlState('back', true);
|
||||
setTimeout(() => { bot.setControlState('back', false); out(observe()); }, 2000);
|
||||
}
|
||||
else if (cmd === 'left') {
|
||||
bot.setControlState('left', true);
|
||||
setTimeout(() => { bot.setControlState('left', false); out(observe()); }, 1500);
|
||||
}
|
||||
else if (cmd === 'right') {
|
||||
bot.setControlState('right', true);
|
||||
setTimeout(() => { bot.setControlState('right', false); out(observe()); }, 1500);
|
||||
}
|
||||
else if (cmd === 'jump') {
|
||||
bot.setControlState('jump', true);
|
||||
setTimeout(() => { bot.setControlState('jump', false); }, 500);
|
||||
}
|
||||
else if (cmd === 'sprint') {
|
||||
bot.setControlState('forward', true);
|
||||
bot.setControlState('sprint', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
bot.setControlState('sprint', false);
|
||||
out(observe());
|
||||
}, 5000);
|
||||
}
|
||||
else if (cmd.startsWith('pray ')) {
|
||||
bot.chat(cmd); // sends "pray <message>" to the server
|
||||
out({ action: 'pray', prayer: cmd.slice(5) });
|
||||
}
|
||||
else if (cmd.startsWith('sudo ')) {
|
||||
bot.chat(cmd);
|
||||
out({ action: 'sudo', command: cmd.slice(5) });
|
||||
}
|
||||
else if (cmd.startsWith('say ')) {
|
||||
bot.chat(cmd.slice(4));
|
||||
}
|
||||
else if (cmd === 'inventory') {
|
||||
const items = bot.inventory.items();
|
||||
out({ inventory: items.map(i => ({ name: i.name, count: i.count })) });
|
||||
}
|
||||
else if (cmd === 'quit') {
|
||||
bot.end();
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
out({ error: 'commands: look, eat, flee, fight, forward, back, left, right, jump, sprint, pray <msg>, sudo <cmd>, say <msg>, inventory, quit' });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user