/** * survival_bot.js — Frame-based AI with reactive micro-scripts between frames. * * The bot runs continuous survival behaviors autonomously: * - Attack nearby hostiles (sword swing every 600ms) * - Eat when hungry or low HP * - Flee when overwhelmed * - Shield when taking ranged damage * * AI frames come via stdin commands that set the HIGH-LEVEL policy. * Between frames, the bot executes the policy autonomously. */ const mineflayer = require('mineflayer'); 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', }); // ── Policy state (set by AI frames, executed by tick loop) ── let policy = { mode: 'survive', // 'survive' | 'explore' | 'flee' | 'idle' fleeHp: 5, // flee threshold fightRange: 5, // engage hostiles within this range exploreDir: null, // {x, z} direction to walk waypoint: null, // {x, y, z} target position }; let alive = false; let deaths = 0; let tickCount = 0; const HOSTILE = new Set([ 'zombie', 'husk', 'skeleton', 'creeper', 'spider', 'cave_spider', 'witch', 'enderman', 'pillager', 'vindicator', 'ravager', 'phantom', 'drowned', 'stray', 'blaze', 'ghast', 'slime', 'magma_cube', 'parched', 'camel_husk', ]); // ── Core tick loop (runs every 600ms — sword cooldown rate) ── setInterval(() => { if (!alive || !bot.entity) return; tickCount++; const pos = bot.entity.position; const hp = bot.health; const food = bot.food; // Find nearest hostile let nearestHostile = null; let nearestDist = Infinity; let hostileCount = 0; for (const e of Object.values(bot.entities)) { if (e === bot.entity) continue; const name = (e.name || e.type || '').toLowerCase(); if (!HOSTILE.has(name)) continue; const d = e.position.distanceTo(pos); if (d > 24) continue; hostileCount++; if (d < nearestDist) { nearestDist = d; nearestHostile = e; } } // ── REACTIVE BEHAVIORS (priority order) ── // 1. CRITICAL: Eat if low HP and have food if (hp < 10 && food < 20 && !bot.food >= 20) { tryEat(); } // 2. FLEE if HP critical or overwhelmed if (hp < policy.fleeHp || (hostileCount >= 3 && hp < 12)) { fleeFrom(nearestHostile); return; } // 3. FIGHT if hostile in range if (nearestHostile && nearestDist < policy.fightRange) { // Equip sword if not already equipWeapon(); // Face and attack bot.lookAt(nearestHostile.position.offset(0, 1, 0)); bot.attack(nearestHostile); // Strafe sideways while fighting (dodge) if (tickCount % 4 < 2) { bot.setControlState('left', true); bot.setControlState('right', false); } else { bot.setControlState('left', false); bot.setControlState('right', true); } return; } // 4. APPROACH if hostile nearby but out of melee range if (nearestHostile && nearestDist < 12 && nearestDist > policy.fightRange && policy.mode === 'survive') { bot.lookAt(nearestHostile.position.offset(0, 1, 0)); bot.setControlState('forward', true); bot.setControlState('sprint', nearestDist > 8); return; } // 5. Stop strafing when not fighting bot.setControlState('left', false); bot.setControlState('right', false); // 6. EXPLORE if no threats if (policy.mode === 'explore' && policy.waypoint) { const dist = pos.distanceTo(new (require('vec3'))(policy.waypoint.x, policy.waypoint.y, policy.waypoint.z)); if (dist > 3) { bot.lookAt(new (require('vec3'))(policy.waypoint.x, pos.y, policy.waypoint.z)); bot.setControlState('forward', true); bot.setControlState('sprint', dist > 10); } else { bot.setControlState('forward', false); bot.setControlState('sprint', false); out({ event: 'arrived', waypoint: policy.waypoint }); policy.waypoint = null; } } else if (policy.mode !== 'idle') { bot.setControlState('forward', false); bot.setControlState('sprint', false); } // 7. EAT if food is low (passive) if (food < 14 && hp < 18) { tryEat(); } // 8. Report status every 10 ticks (~6 seconds) if (tickCount % 10 === 0) { out(observe()); } }, 600); // ── Helper functions ── function equipWeapon() { const sword = bot.inventory.items().find(i => i.name.includes('sword')); if (sword && bot.heldItem?.name !== sword.name) { bot.equip(sword, 'hand').catch(() => {}); } } function tryEat() { const food = bot.inventory.items().find(i => ['cooked_beef', 'bread', 'cooked_porkchop', 'golden_apple', 'baked_potato', 'cooked_chicken', 'apple', 'cooked_cod', 'carrot'].includes(i.name) ); if (food) { bot.equip(food, 'hand').then(() => { bot.activateItem(); setTimeout(() => bot.deactivateItem(), 1800); }).catch(() => {}); } } function fleeFrom(entity) { if (!entity) { // Random direction bot.setControlState('forward', true); bot.setControlState('sprint', true); return; } const pos = bot.entity.position; const dx = pos.x - entity.position.x; const dz = pos.z - entity.position.z; const len = Math.sqrt(dx * dx + dz * dz) || 1; const target = pos.offset((dx / len) * 20, 0, (dz / len) * 20); bot.lookAt(target); bot.setControlState('forward', true); bot.setControlState('sprint', true); bot.setControlState('jump', true); setTimeout(() => bot.setControlState('jump', false), 400); } function observe() { if (!bot.entity) return { alive: false }; 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; if (e.type === 'player') { mobs.push({ type: 'player', name: e.username, dist: d }); } else { mobs.push({ type: name, dist: d, hostile: HOSTILE.has((name || '').toLowerCase()) }); } } // Compact terrain const ground = {}; for (let dx = -2; dx <= 2; dx++) { for (let dz = -2; dz <= 2; dz++) { const b = bot.blockAt(pos.offset(dx, -1, dz)); if (b) ground[b.name] = (ground[b.name] || 0) + 1; } } return { frame: tickCount, 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), ground, policy: policy.mode, deaths, }; } function out(obj) { console.log(JSON.stringify(obj)); } // ── Events ── bot.on('login', () => out({ event: 'login' })); bot.on('spawn', () => { alive = true; deaths++; setTimeout(() => out({ event: deaths === 1 ? 'first_spawn' : 'respawn', ...observe() }), 3000); }); bot.on('death', () => { alive = false; out({ event: 'death', deaths, msg: 'I died.' }); setTimeout(() => { try { bot.respawn(); } catch (e) {} }, 2000); }); bot.on('chat', (username, message) => { if (username === bot.username) return; out({ event: 'chat', from: username, msg: message }); }); bot.on('kicked', (reason) => { out({ event: 'kicked', reason: typeof reason === 'object' ? JSON.stringify(reason) : String(reason) }); process.exit(1); }); // ── AI Frame commands (stdin) ── const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { const cmd = line.trim(); if (cmd === 'look') out(observe()); else if (cmd === 'survive') { policy.mode = 'survive'; out({ policy: 'survive' }); } else if (cmd === 'idle') { policy.mode = 'idle'; stopMoving(); out({ policy: 'idle' }); } else if (cmd === 'flee') { policy.mode = 'survive'; policy.fleeHp = 15; out({ policy: 'flee_mode' }); } else if (cmd.startsWith('goto ')) { const [, x, y, z] = cmd.split(' ').map(Number); policy.mode = 'explore'; policy.waypoint = { x, y, z }; out({ policy: 'explore', waypoint: policy.waypoint }); } else if (cmd.startsWith('pray ')) { bot.chat(cmd); out({ action: 'pray', msg: cmd.slice(5) }); } else if (cmd.startsWith('sudo ')) { bot.chat(cmd); out({ action: 'sudo', msg: cmd.slice(5) }); } else if (cmd.startsWith('say ')) { bot.chat(cmd.slice(4)); } else if (cmd === 'quit') { bot.end(); process.exit(0); } }); function stopMoving() { ['forward', 'back', 'left', 'right', 'sprint', 'jump'].forEach(s => bot.setControlState(s, false)); }