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,252 @@
|
||||
/**
|
||||
* bot_army.js — Spawn an army of combat bots.
|
||||
*
|
||||
* Each bot runs autonomous combat with mineflayer-pvp.
|
||||
* They attack all hostiles but never each other.
|
||||
* Reports collective stats.
|
||||
*
|
||||
* Usage: node bot_army.js [count] [host] [port]
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
const pvp = require('mineflayer-pvp').plugin;
|
||||
const armorManager = require('mineflayer-armor-manager');
|
||||
const pathfinder = require('mineflayer-pathfinder');
|
||||
const { GoalNear } = require('mineflayer-pathfinder').goals;
|
||||
|
||||
const count = parseInt(process.argv[2] || '8', 10);
|
||||
const host = process.argv[3] || '192.168.0.244';
|
||||
const port = parseInt(process.argv[4] || '25567', 10);
|
||||
|
||||
const HOSTILE = new Set([
|
||||
'zombie', 'husk', 'skeleton', 'creeper', 'spider', 'cave_spider',
|
||||
'witch', 'enderman', 'pillager', 'vindicator', 'phantom',
|
||||
'drowned', 'stray', 'blaze', 'ghast', 'slime', 'magma_cube',
|
||||
'parched', 'camel_husk', 'zombie_villager',
|
||||
]);
|
||||
|
||||
const NAMES = [
|
||||
'Soldier_Alpha', 'Soldier_Bravo', 'Soldier_Charlie', 'Soldier_Delta',
|
||||
'Soldier_Echo', 'Soldier_Foxtrot', 'Soldier_Golf', 'Soldier_Hotel',
|
||||
'Soldier_India', 'Soldier_Juliet', 'Soldier_Kilo', 'Soldier_Lima',
|
||||
'Soldier_Mike', 'Soldier_November', 'Soldier_Oscar', 'Soldier_Papa',
|
||||
];
|
||||
|
||||
// Track all bot names so they don't attack each other
|
||||
const armyNames = new Set(NAMES.slice(0, count));
|
||||
armyNames.add('ClaudeBot');
|
||||
armyNames.add('slingshooter08');
|
||||
armyNames.add('OracleBot');
|
||||
|
||||
const army = [];
|
||||
let totalKills = 0;
|
||||
let totalDeaths = 0;
|
||||
|
||||
function spawnSoldier(name, index) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log(`[${name}] Spawning...`);
|
||||
|
||||
const bot = mineflayer.createBot({
|
||||
host, port,
|
||||
username: name,
|
||||
auth: 'offline',
|
||||
version: '1.21.11',
|
||||
});
|
||||
|
||||
bot.loadPlugin(pvp);
|
||||
bot.loadPlugin(armorManager);
|
||||
bot.loadPlugin(pathfinder.pathfinder);
|
||||
|
||||
const soldier = { bot, name, kills: 0, deaths: 0, alive: false };
|
||||
|
||||
bot.once('spawn', () => {
|
||||
soldier.alive = true;
|
||||
|
||||
// Configure pathfinder
|
||||
try {
|
||||
const mcData = require('minecraft-data')(bot.version);
|
||||
const movements = new pathfinder.Movements(bot, mcData);
|
||||
movements.allowSprinting = true;
|
||||
movements.canDig = false;
|
||||
bot.pathfinder.setMovements(movements);
|
||||
} catch (e) {}
|
||||
|
||||
console.log(`[${name}] Ready at (${Math.floor(bot.entity.position.x)}, ${Math.floor(bot.entity.position.y)}, ${Math.floor(bot.entity.position.z)})`);
|
||||
resolve(soldier);
|
||||
});
|
||||
|
||||
// Respawn on death
|
||||
bot.on('death', () => {
|
||||
soldier.alive = false;
|
||||
soldier.deaths++;
|
||||
totalDeaths++;
|
||||
console.log(`[${name}] DIED (kills: ${soldier.kills}, deaths: ${soldier.deaths})`);
|
||||
setTimeout(() => { try { bot.respawn(); } catch (e) {} }, 2000);
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
if (soldier.deaths > 0) {
|
||||
soldier.alive = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Track kills
|
||||
bot.on('entityDead', (entity) => {
|
||||
const ename = (entity.name || entity.type || '').toLowerCase();
|
||||
if (HOSTILE.has(ename)) {
|
||||
soldier.kills++;
|
||||
totalKills++;
|
||||
}
|
||||
});
|
||||
|
||||
// Counter-attack when hit
|
||||
bot.on('entityHurt', (entity) => {
|
||||
if (entity !== bot.entity) return;
|
||||
const attacker = findNearestHostile(bot, 8);
|
||||
if (attacker && !bot.pvp.target) {
|
||||
bot.pvp.attack(attacker);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
console.log(`[${name}] Kicked: ${JSON.stringify(reason).slice(0, 80)}`);
|
||||
});
|
||||
|
||||
bot.on('error', (err) => {
|
||||
console.error(`[${name}] Error: ${err.message}`);
|
||||
});
|
||||
|
||||
army.push(soldier);
|
||||
}, index * 3000); // Stagger spawns by 3 seconds
|
||||
});
|
||||
}
|
||||
|
||||
function findNearestHostile(bot, maxDist) {
|
||||
if (!bot.entity) return null;
|
||||
let nearest = null;
|
||||
let nearestDist = maxDist;
|
||||
for (const e of Object.values(bot.entities)) {
|
||||
if (e === bot.entity) continue;
|
||||
// Never attack army members or players
|
||||
if (e.type === 'player') continue;
|
||||
const ename = (e.name || e.type || '').toLowerCase();
|
||||
if (!HOSTILE.has(ename)) continue;
|
||||
const d = e.position.distanceTo(bot.entity.position);
|
||||
if (d < nearestDist) {
|
||||
nearestDist = d;
|
||||
nearest = e;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
// ── Autonomous combat loop for all bots ──
|
||||
setInterval(() => {
|
||||
for (const soldier of army) {
|
||||
if (!soldier.alive || !soldier.bot.entity) continue;
|
||||
const bot = soldier.bot;
|
||||
const hp = bot.health;
|
||||
|
||||
// Find nearest hostile
|
||||
const target = findNearestHostile(bot, 16);
|
||||
|
||||
// Flee if critical
|
||||
if (hp < 5 && target) {
|
||||
bot.pvp.stop();
|
||||
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));
|
||||
bot.setControlState('forward', true);
|
||||
bot.setControlState('sprint', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
bot.setControlState('sprint', false);
|
||||
}, 3000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attack nearest hostile
|
||||
if (target && !bot.pvp.target) {
|
||||
bot.pvp.attack(target);
|
||||
}
|
||||
|
||||
// If no target but pvp active, stop
|
||||
if (!target && bot.pvp.target) {
|
||||
bot.pvp.stop();
|
||||
}
|
||||
}
|
||||
}, 800);
|
||||
|
||||
// ── Status report every 10 seconds ──
|
||||
setInterval(() => {
|
||||
const alive = army.filter(s => s.alive).length;
|
||||
const fighting = army.filter(s => s.bot.pvp?.target).length;
|
||||
console.log(`\n=== ARMY STATUS: ${alive}/${army.length} alive | ${fighting} fighting | Kills: ${totalKills} | Deaths: ${totalDeaths} ===`);
|
||||
for (const s of army) {
|
||||
if (!s.bot.entity) continue;
|
||||
const pos = s.bot.entity.position;
|
||||
const target = s.bot.pvp?.target;
|
||||
const status = !s.alive ? 'DEAD' : target ? `FIGHTING ${target.name || target.type}` : 'PATROLLING';
|
||||
console.log(` [${s.name}] HP:${Math.round(s.bot.health)} K:${s.kills} D:${s.deaths} — ${status}`);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// ── Startup ──
|
||||
console.log(`\n╔════════════════════════════════════════╗`);
|
||||
console.log(`║ MORTDECAI BOT ARMY — ${count} SOLDIERS ║`);
|
||||
console.log(`║ Target: ${host}:${port} ║`);
|
||||
console.log(`║ Rules: Kill all hostiles, protect allies ║`);
|
||||
console.log(`╚════════════════════════════════════════╝\n`);
|
||||
|
||||
// Whitelist soldiers (skip if dev server / no whitelist)
|
||||
const { execSync } = require('child_process');
|
||||
const rconPort = port === 25567 ? 25577 : 25578;
|
||||
const rconPass = port === 25567 ? 'REDACTED_RCON' : 'REDACTED_RCON';
|
||||
try {
|
||||
for (let i = 0; i < count; i++) {
|
||||
execSync(`python3 -c "from mcrcon import MCRcon; m=MCRcon('${host}','${rconPass}',port=${rconPort}); m.connect(); m.command('whitelist add ${NAMES[i]}'); m.disconnect()"`, { timeout: 5000, stdio: 'pipe' });
|
||||
}
|
||||
console.log(`Whitelisted ${count} soldiers`);
|
||||
} catch (e) {
|
||||
console.log('Whitelist skipped (dev server or already done)');
|
||||
}
|
||||
|
||||
// Spawn soldiers
|
||||
(async () => {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await spawnSoldier(NAMES[i], 0);
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
console.log(`\nAll ${count} soldiers deployed!`);
|
||||
|
||||
// Gear them all up via RCON
|
||||
try {
|
||||
execSync(`python3 -c "
|
||||
from mcrcon import MCRcon
|
||||
with MCRcon('${host}', '${rconPass}', port=${rconPort}) as mcr:
|
||||
soldiers = ${JSON.stringify(NAMES.slice(0, count))}
|
||||
for name in soldiers:
|
||||
mcr.command(f'give {name} minecraft:iron_sword 1')
|
||||
mcr.command(f'give {name} minecraft:cooked_beef 16')
|
||||
mcr.command(f'item replace entity {name} armor.chest with minecraft:iron_chestplate')
|
||||
mcr.command(f'item replace entity {name} armor.legs with minecraft:iron_leggings')
|
||||
mcr.command(f'item replace entity {name} armor.feet with minecraft:iron_boots')
|
||||
mcr.command(f'item replace entity {name} armor.head with minecraft:iron_helmet')
|
||||
mcr.command(f'effect give {name} minecraft:regeneration 600 1')
|
||||
print(f'Geared up {len(soldiers)} soldiers')
|
||||
"`, { timeout: 30000 });
|
||||
} catch (e) {
|
||||
console.log('Gear-up error:', e.message?.slice(0, 80));
|
||||
}
|
||||
})();
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log(`\n\nFinal score: ${totalKills} kills, ${totalDeaths} deaths`);
|
||||
console.log('Disconnecting army...');
|
||||
army.forEach(s => { try { s.bot.end(); } catch (e) {} });
|
||||
setTimeout(() => process.exit(0), 2000);
|
||||
});
|
||||
Reference in New Issue
Block a user