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>
283 lines
8.6 KiB
JavaScript
283 lines
8.6 KiB
JavaScript
/**
|
|
* 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));
|
|
}
|