Files
Seth 5b28002001 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>
2026-03-22 20:22:50 -04:00

179 lines
5.2 KiB
JavaScript

/**
* rl_bot.js — Minimal mineflayer bot for RL training.
*
* Communicates via stdin (actions) / stdout (observations) as JSON.
* Designed to be spawned by minecraft_env.py as a subprocess.
*
* Usage: node rl_bot.js [host] [port] [username]
*/
const mineflayer = require('mineflayer');
const pvp = require('mineflayer-pvp').plugin;
const readline = require('readline');
const host = process.argv[2] || '192.168.0.244';
const port = parseInt(process.argv[3] || '25568', 10);
const username = process.argv[4] || 'RLBot';
const HOSTILE = new Set([
'zombie', 'husk', 'skeleton', 'creeper', 'spider', 'cave_spider',
'witch', 'enderman', 'drowned', 'stray', 'phantom', 'parched',
'camel_husk', 'slime', 'magma_cube',
]);
const bot = mineflayer.createBot({ host, port, username, auth: 'offline', version: '1.21.11' });
bot.loadPlugin(pvp);
let alive = false;
let kills = 0;
let deaths = 0;
let died = false;
function out(obj) {
try { process.stdout.write(JSON.stringify(obj) + '\n'); } catch (e) {}
}
function observe() {
if (!bot.entity) return { hp: 0, died: true };
const pos = bot.entity.position;
const mobs = [];
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;
const name = (e.name || e.type || '').toLowerCase();
const hostile = HOSTILE.has(name);
if (e.type === 'player') continue; // ignore players
if (hostile || d < 8) {
mobs.push({ type: name, dist: d, hostile });
}
}
return {
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,
day: (bot.time?.timeOfDay || 0) < 13000,
below: bot.blockAt(pos.offset(0, -1, 0))?.name || '?',
inv: bot.inventory.items().map(i => `${i.name}x${i.count}`).join(', ') || 'empty',
armor: [5, 6, 7, 8].map(s => bot.inventory.slots[s]?.name).filter(Boolean).join(', ') || 'none',
mobs: mobs.sort((a, b) => a.dist - b.dist).slice(0, 8),
kills,
deaths,
died,
};
}
function nearestHostile(maxDist) {
if (!bot.entity) return null;
let nearest = null;
let nd = maxDist || 16;
for (const e of Object.values(bot.entities)) {
if (e === bot.entity || e.type === 'player') continue;
if (!HOSTILE.has((e.name || '').toLowerCase())) continue;
const d = e.position.distanceTo(bot.entity.position);
if (d < nd) { nd = d; nearest = e; }
}
return nearest;
}
function stopMoving() {
['forward', 'back', 'left', 'right', 'sprint', 'jump'].forEach(s => bot.setControlState(s, false));
}
// ── Events ──
bot.once('spawn', () => {
alive = true;
died = false;
out({ event: 'ready', ...observe() });
});
bot.on('spawn', () => { alive = true; died = false; });
bot.on('death', () => {
alive = false;
died = true;
deaths++;
out({ event: 'death', ...observe() });
setTimeout(() => { try { bot.respawn(); } catch (e) {} }, 2000);
});
bot.on('entityDead', (e) => {
if (HOSTILE.has((e.name || '').toLowerCase())) kills++;
});
bot.on('kicked', () => process.exit(1));
bot.on('error', () => {});
// ── Actions (from stdin) ──
const rl = readline.createInterface({ input: process.stdin });
rl.on('line', (line) => {
const cmd = line.trim();
died = false; // reset per-tick death flag
if (cmd === 'observe') {
out(observe());
}
else if (cmd === 'forward') {
stopMoving();
bot.setControlState('forward', true);
setTimeout(() => { bot.setControlState('forward', false); out(observe()); }, 500);
}
else if (cmd === 'fight') {
const target = nearestHostile(6);
if (target) {
const weapon = bot.inventory.items().find(i => i.name.includes('sword'));
if (weapon) bot.equip(weapon, 'hand').catch(() => {});
bot.lookAt(target.position.offset(0, 1, 0));
bot.attack(target);
}
out(observe());
}
else if (cmd === 'flee') {
const target = nearestHostile(16);
if (target) {
const pos = bot.entity.position;
const dx = pos.x - target.position.x;
const dz = pos.z - target.position.z;
const len = Math.sqrt(dx * dx + dz * dz) || 1;
bot.lookAt(pos.offset((dx / len) * 10, 0, (dz / len) * 10));
}
stopMoving();
bot.setControlState('forward', true);
bot.setControlState('sprint', true);
bot.setControlState('jump', true);
setTimeout(() => {
bot.setControlState('jump', false);
setTimeout(() => { stopMoving(); out(observe()); }, 400);
}, 200);
}
else if (cmd === 'eat') {
const food = bot.inventory.items().find(i =>
['cooked_beef', 'bread', 'cooked_porkchop', 'golden_apple',
'cooked_chicken', 'apple', 'baked_potato', 'carrot'].includes(i.name)
);
if (food) {
bot.equip(food, 'hand').then(() => {
bot.activateItem();
setTimeout(() => { bot.deactivateItem(); out(observe()); }, 1600);
}).catch(() => out(observe()));
} else {
out(observe());
}
}
else if (cmd === 'sprint') {
stopMoving();
bot.setControlState('forward', true);
bot.setControlState('sprint', true);
setTimeout(() => { stopMoving(); out(observe()); }, 500);
}
else if (cmd === 'idle') {
stopMoving();
out(observe());
}
else if (cmd === 'quit') {
bot.end();
process.exit(0);
}
});