/** * 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); } });