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);
|
||||
});
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* combat_bot.js — Full combat-capable bot using mineflayer plugins.
|
||||
*
|
||||
* Uses:
|
||||
* mineflayer-pvp — combat AI (attack, strafe, shield)
|
||||
* mineflayer-pathfinder — A* navigation
|
||||
* mineflayer-armor-manager — auto-equip best armor
|
||||
* mineflayer-auto-eat — auto-eat when hungry/hurt
|
||||
*
|
||||
* The bot autonomously fights, eats, equips armor, and pathfinds.
|
||||
* AI frames via stdin set high-level goals.
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
const pathfinder = require('mineflayer-pathfinder');
|
||||
const pvp = require('mineflayer-pvp').plugin;
|
||||
const armorManager = require('mineflayer-armor-manager');
|
||||
const autoEat = require('mineflayer-auto-eat').loader;
|
||||
const { GoalNear, GoalFollow, GoalBlock } = require('mineflayer-pathfinder').goals;
|
||||
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',
|
||||
});
|
||||
|
||||
// Load plugins
|
||||
bot.loadPlugin(pathfinder.pathfinder);
|
||||
bot.loadPlugin(pvp);
|
||||
bot.loadPlugin(armorManager);
|
||||
bot.loadPlugin(autoEat);
|
||||
|
||||
let deaths = 0;
|
||||
let kills = 0;
|
||||
let alive = false;
|
||||
|
||||
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',
|
||||
]);
|
||||
|
||||
// ── Spawn setup ──
|
||||
bot.once('spawn', () => {
|
||||
alive = true;
|
||||
deaths++;
|
||||
|
||||
// Configure pathfinder
|
||||
const mcData = require('minecraft-data')(bot.version);
|
||||
const movements = new pathfinder.Movements(bot, mcData);
|
||||
movements.allowSprinting = true;
|
||||
movements.canDig = false; // Don't destroy blocks
|
||||
bot.pathfinder.setMovements(movements);
|
||||
|
||||
// Configure auto-eat
|
||||
bot.autoEat.options = {
|
||||
priority: 'foodPoints',
|
||||
startAt: 16,
|
||||
bannedFood: [],
|
||||
};
|
||||
|
||||
out({ event: 'ready', msg: 'Plugins loaded. Combat-ready.' });
|
||||
setTimeout(() => out(observe()), 3000);
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
if (deaths > 1) {
|
||||
alive = true;
|
||||
out({ event: 'respawn', deaths });
|
||||
setTimeout(() => out(observe()), 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Combat events ──
|
||||
bot.on('entityHurt', (entity) => {
|
||||
if (entity === bot.entity) {
|
||||
// I'm being hit — find attacker and fight back
|
||||
const attacker = nearestHostile(8);
|
||||
if (attacker && !bot.pvp.target) {
|
||||
out({ event: 'counter_attack', target: attacker.name || attacker.type, dist: Math.round(attacker.position.distanceTo(bot.entity.position)) });
|
||||
bot.pvp.attack(attacker);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('playerCollect', (collector, collected) => {
|
||||
if (collector === bot.entity) {
|
||||
out({ event: 'pickup', item: collected.name || 'item' });
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('death', () => {
|
||||
alive = false;
|
||||
deaths++;
|
||||
out({ event: 'death', deaths, kills });
|
||||
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);
|
||||
});
|
||||
|
||||
// Track kills
|
||||
bot.on('entityDead', (entity) => {
|
||||
const name = (entity.name || entity.type || '').toLowerCase();
|
||||
if (HOSTILE.has(name)) {
|
||||
kills++;
|
||||
out({ event: 'kill', target: name, totalKills: kills });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Autonomous combat loop ──
|
||||
setInterval(() => {
|
||||
if (!alive || !bot.entity) return;
|
||||
|
||||
const hp = bot.health;
|
||||
const nearest = nearestHostile(16);
|
||||
|
||||
// If low HP and being attacked, flee
|
||||
if (hp < 6 && nearest && nearest.position.distanceTo(bot.entity.position) < 6) {
|
||||
bot.pvp.stop();
|
||||
fleeSmart(nearest);
|
||||
return;
|
||||
}
|
||||
|
||||
// If hostile nearby and not already fighting, engage
|
||||
if (nearest && !bot.pvp.target) {
|
||||
const dist = nearest.position.distanceTo(bot.entity.position);
|
||||
if (dist < 10) {
|
||||
bot.pvp.attack(nearest);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// ── Periodic status report ──
|
||||
setInterval(() => {
|
||||
if (!alive) return;
|
||||
out(observe());
|
||||
}, 6000);
|
||||
|
||||
// ── Helper functions ──
|
||||
|
||||
function nearestHostile(maxDist) {
|
||||
if (!bot.entity) return null;
|
||||
let nearest = null;
|
||||
let nearestDist = maxDist || 16;
|
||||
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(bot.entity.position);
|
||||
if (d < nearestDist) {
|
||||
nearestDist = d;
|
||||
nearest = e;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
function fleeSmart(from) {
|
||||
if (!from || !bot.entity) return;
|
||||
const pos = bot.entity.position;
|
||||
const dx = pos.x - from.position.x;
|
||||
const dz = pos.z - from.position.z;
|
||||
const len = Math.sqrt(dx * dx + dz * dz) || 1;
|
||||
const fleeX = pos.x + (dx / len) * 30;
|
||||
const fleeZ = pos.z + (dz / len) * 30;
|
||||
|
||||
try {
|
||||
bot.pathfinder.setGoal(new GoalNear(fleeX, pos.y, fleeZ, 2));
|
||||
} catch (e) {
|
||||
// Fallback: just sprint
|
||||
bot.setControlState('forward', true);
|
||||
bot.setControlState('sprint', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
bot.setControlState('sprint', false);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const hostile = HOSTILE.has((name || '').toLowerCase());
|
||||
if (e.type === 'player') {
|
||||
mobs.push({ type: 'player', name: e.username, dist: d });
|
||||
} else if (hostile || d < 10) {
|
||||
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,
|
||||
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',
|
||||
weapon: bot.heldItem?.name || 'fist',
|
||||
fighting: bot.pvp.target ? (bot.pvp.target.name || bot.pvp.target.type) : null,
|
||||
mobs: mobs.sort((a, b) => a.dist - b.dist).slice(0, 8),
|
||||
kills,
|
||||
deaths,
|
||||
};
|
||||
}
|
||||
|
||||
function out(obj) {
|
||||
console.log(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// ── AI Frame commands ──
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
rl.on('line', (line) => {
|
||||
const cmd = line.trim();
|
||||
|
||||
if (cmd === 'look') out(observe());
|
||||
else if (cmd === 'hunt') {
|
||||
// Actively seek and kill nearest hostile
|
||||
const target = nearestHostile(30);
|
||||
if (target) {
|
||||
bot.pvp.attack(target);
|
||||
out({ action: 'hunting', target: target.name || target.type, dist: Math.round(target.position.distanceTo(bot.entity.position)) });
|
||||
} else {
|
||||
out({ action: 'hunt', msg: 'no hostiles nearby' });
|
||||
}
|
||||
}
|
||||
else if (cmd === 'stop') { bot.pvp.stop(); bot.pathfinder.stop(); out({ action: 'stopped' }); }
|
||||
else if (cmd.startsWith('goto ')) {
|
||||
const [, x, y, z] = cmd.split(' ').map(Number);
|
||||
bot.pathfinder.setGoal(new GoalNear(x, y, z, 2));
|
||||
out({ action: 'navigating', target: { x, y, z } });
|
||||
}
|
||||
else if (cmd.startsWith('follow ')) {
|
||||
const name = cmd.slice(7);
|
||||
const player = bot.players[name];
|
||||
if (player?.entity) {
|
||||
bot.pathfinder.setGoal(new GoalFollow(player.entity, 3), true);
|
||||
out({ action: 'following', target: name });
|
||||
}
|
||||
}
|
||||
else if (cmd.startsWith('pray ')) { bot.chat(cmd); out({ action: 'pray' }); }
|
||||
else if (cmd.startsWith('sudo ')) { bot.chat(cmd); out({ action: 'sudo' }); }
|
||||
else if (cmd.startsWith('say ')) { bot.chat(cmd.slice(4)); }
|
||||
else if (cmd === 'quit') { bot.end(); process.exit(0); }
|
||||
else { out({ commands: ['look', 'hunt', 'stop', 'goto X Y Z', 'follow PLAYER', 'pray MSG', 'sudo CMD', 'say MSG', 'quit'] }); }
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* explorer_bot.js — Claude explores the world through a mineflayer bot's eyes.
|
||||
*
|
||||
* Connects to the server, reports what it sees, and accepts movement commands
|
||||
* via stdin. Outputs structured JSON observations.
|
||||
*
|
||||
* Usage: node explorer_bot.js [host] [port]
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
const { Vec3 } = require('vec3');
|
||||
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',
|
||||
});
|
||||
|
||||
// ── Observation functions ──
|
||||
|
||||
function observe() {
|
||||
if (!bot.entity) return '{"error": "not spawned"}';
|
||||
|
||||
const pos = bot.entity.position;
|
||||
const obs = {
|
||||
position: { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) },
|
||||
health: bot.health,
|
||||
food: bot.food,
|
||||
gamemode: bot.game.gameMode,
|
||||
time: bot.time.timeOfDay,
|
||||
isRaining: bot.isRaining,
|
||||
};
|
||||
|
||||
// Block below and around
|
||||
obs.blockBelow = blockName(pos.offset(0, -1, 0));
|
||||
obs.blockAt = blockName(pos);
|
||||
obs.blockAbove = blockName(pos.offset(0, 2, 0));
|
||||
|
||||
// Look in all 4 directions for interesting blocks
|
||||
obs.surroundings = {};
|
||||
const dirs = { north: [0, 0, -1], south: [0, 0, 1], east: [1, 0, 0], west: [-1, 0, 0] };
|
||||
for (const [name, [dx, dy, dz]] of Object.entries(dirs)) {
|
||||
const blocks = [];
|
||||
for (let dist = 1; dist <= 8; dist++) {
|
||||
const b = blockName(pos.offset(dx * dist, 0, dz * dist));
|
||||
if (b !== 'air') blocks.push({ dist, block: b });
|
||||
}
|
||||
if (blocks.length) obs.surroundings[name] = blocks;
|
||||
}
|
||||
|
||||
// Nearby entities
|
||||
obs.entities = [];
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
if (entity === bot.entity) continue;
|
||||
const dist = entity.position.distanceTo(pos);
|
||||
if (dist <= 20) {
|
||||
obs.entities.push({
|
||||
type: entity.type === 'player' ? 'player' : (entity.name || entity.type),
|
||||
name: entity.username || null,
|
||||
distance: Math.round(dist),
|
||||
position: {
|
||||
x: Math.floor(entity.position.x),
|
||||
y: Math.floor(entity.position.y),
|
||||
z: Math.floor(entity.position.z),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Scan a 5x5 area at foot level for terrain overview
|
||||
obs.terrain = [];
|
||||
for (let dx = -5; dx <= 5; dx++) {
|
||||
for (let dz = -5; dz <= 5; dz++) {
|
||||
// Find surface block (scan down from y+3)
|
||||
for (let dy = 3; dy >= -3; dy--) {
|
||||
const b = bot.blockAt(pos.offset(dx, dy, dz));
|
||||
if (b && b.name !== 'air') {
|
||||
obs.terrain.push({ dx, dz, y: Math.floor(pos.y) + dy, block: b.name });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(obs);
|
||||
}
|
||||
|
||||
function blockName(vec) {
|
||||
const b = bot.blockAt(vec);
|
||||
return b ? b.name : 'unloaded';
|
||||
}
|
||||
|
||||
// ── Movement functions ──
|
||||
|
||||
async function walkTo(x, y, z) {
|
||||
const goal = new Vec3(x, y, z);
|
||||
console.log(JSON.stringify({ action: 'walking', target: { x, y, z } }));
|
||||
|
||||
// Simple movement: look at target and walk forward
|
||||
await bot.lookAt(goal);
|
||||
bot.setControlState('forward', true);
|
||||
|
||||
// Walk until close or timeout
|
||||
return new Promise((resolve) => {
|
||||
const check = setInterval(() => {
|
||||
const dist = bot.entity.position.distanceTo(goal);
|
||||
if (dist < 2) {
|
||||
bot.setControlState('forward', false);
|
||||
clearInterval(check);
|
||||
resolve(true);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
clearInterval(check);
|
||||
resolve(false);
|
||||
}, 10000); // 10s max walk
|
||||
});
|
||||
}
|
||||
|
||||
async function lookAround() {
|
||||
const snapshots = [];
|
||||
const yaws = [0, Math.PI / 2, Math.PI, -Math.PI / 2]; // N, E, S, W
|
||||
const names = ['north', 'east', 'south', 'west'];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await bot.look(yaws[i], 0, true);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// What do I see in this direction?
|
||||
const pos = bot.entity.position;
|
||||
const dx = Math.round(Math.sin(yaws[i]));
|
||||
const dz = Math.round(-Math.cos(yaws[i]));
|
||||
|
||||
const visible = [];
|
||||
for (let dist = 1; dist <= 16; dist++) {
|
||||
const b = bot.blockAt(pos.offset(dx * dist, 0, dz * dist));
|
||||
const bUp = bot.blockAt(pos.offset(dx * dist, 1, dz * dist));
|
||||
const bDown = bot.blockAt(pos.offset(dx * dist, -1, dz * dist));
|
||||
if (b && b.name !== 'air') visible.push({ dist, block: b.name, level: 'eye' });
|
||||
if (bDown && bDown.name !== 'air' && bDown.name !== (b && b.name)) visible.push({ dist, block: bDown.name, level: 'ground' });
|
||||
}
|
||||
snapshots.push({ direction: names[i], blocks: visible.slice(0, 8) });
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
// ── Event handlers ──
|
||||
|
||||
bot.on('login', () => {
|
||||
console.log(JSON.stringify({ event: 'login', username: bot.username }));
|
||||
});
|
||||
|
||||
bot.on('spawn', async () => {
|
||||
console.log(JSON.stringify({ event: 'spawn', ...JSON.parse(observe()) }));
|
||||
|
||||
// Initial look around
|
||||
const views = await lookAround();
|
||||
console.log(JSON.stringify({ event: 'look_around', views }));
|
||||
});
|
||||
|
||||
bot.on('chat', (username, message) => {
|
||||
if (username === bot.username) return;
|
||||
console.log(JSON.stringify({ event: 'chat', from: username, message }));
|
||||
});
|
||||
|
||||
bot.on('health', () => {
|
||||
console.log(JSON.stringify({ event: 'health', health: bot.health, food: bot.food }));
|
||||
});
|
||||
|
||||
bot.on('death', () => {
|
||||
console.log(JSON.stringify({ event: 'death' }));
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
console.log(JSON.stringify({ event: 'kicked', reason: typeof reason === 'object' ? JSON.stringify(reason) : reason }));
|
||||
});
|
||||
|
||||
bot.on('error', (err) => {
|
||||
console.error(JSON.stringify({ event: 'error', message: err.message }));
|
||||
});
|
||||
|
||||
// ── Command interface (stdin) ──
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
|
||||
rl.on('line', async (line) => {
|
||||
const cmd = line.trim();
|
||||
|
||||
if (cmd === 'observe' || cmd === 'look') {
|
||||
console.log(observe());
|
||||
}
|
||||
else if (cmd === 'around') {
|
||||
const views = await lookAround();
|
||||
console.log(JSON.stringify({ action: 'look_around', views }));
|
||||
}
|
||||
else if (cmd.startsWith('walk ')) {
|
||||
const parts = cmd.split(' ');
|
||||
const x = parseInt(parts[1]), y = parseInt(parts[2]), z = parseInt(parts[3]);
|
||||
const arrived = await walkTo(x, y, z);
|
||||
console.log(JSON.stringify({ action: 'walk_result', arrived, ...JSON.parse(observe()) }));
|
||||
}
|
||||
else if (cmd === 'forward') {
|
||||
bot.setControlState('forward', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
console.log(observe());
|
||||
}, 3000);
|
||||
}
|
||||
else if (cmd === 'jump') {
|
||||
bot.setControlState('jump', true);
|
||||
setTimeout(() => bot.setControlState('jump', false), 500);
|
||||
setTimeout(() => console.log(observe()), 1000);
|
||||
}
|
||||
else if (cmd.startsWith('say ')) {
|
||||
bot.chat(cmd.slice(4));
|
||||
}
|
||||
else if (cmd === 'quit') {
|
||||
bot.end();
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
console.log(JSON.stringify({ error: 'unknown command', commands: ['observe', 'around', 'walk X Y Z', 'forward', 'jump', 'say MSG', 'quit'] }));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* march_army.js — Bots march in rank-and-file formation around a circular path.
|
||||
*
|
||||
* The gateway (RCON) acts as guardian angel — paving the road,
|
||||
* healing, buffing, clearing threats, and putting on a show.
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
const pvp = require('mineflayer-pvp').plugin;
|
||||
const http = require('http');
|
||||
|
||||
const host = process.argv[2] || '192.168.0.244';
|
||||
const port = parseInt(process.argv[3] || '25568', 10);
|
||||
const COUNT = parseInt(process.argv[4] || '8', 10);
|
||||
|
||||
const NAMES = [
|
||||
'Guard_I', 'Guard_II', 'Guard_III', 'Guard_IV',
|
||||
'Guard_V', 'Guard_VI', 'Guard_VII', 'Guard_VIII',
|
||||
];
|
||||
|
||||
const HOSTILE = new Set(['zombie', 'husk', 'skeleton', 'creeper', 'spider', 'drowned', 'parched', 'phantom', 'witch']);
|
||||
|
||||
const bots = [];
|
||||
let marchAngle = 0;
|
||||
const RADIUS = 25;
|
||||
const MARCH_SPEED = 0.02; // radians per tick — slow stately march
|
||||
const FORMATION_SPACING = Math.PI / (COUNT * 1.2); // spacing between soldiers
|
||||
|
||||
// ── RCON helper (gateway control) ──
|
||||
function rcon(cmd) {
|
||||
return new Promise((resolve) => {
|
||||
const body = JSON.stringify({ command: cmd });
|
||||
const req = http.request({
|
||||
hostname: host, port: 25578, path: '/api/chat',
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
}, () => resolve());
|
||||
req.on('error', () => resolve());
|
||||
// Actually just use python for reliability
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function rconExec(cmd) {
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync(`python3 -c "from mcrcon import MCRcon; m=MCRcon('${host}','REDACTED_RCON',port=25578); m.connect(); m.command('${cmd.replace(/'/g, "\\'")}'); m.disconnect()"`, { timeout: 3000, stdio: 'pipe' });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ── Spawn soldiers ──
|
||||
function spawnSoldier(name, index) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[${name}] Deploying...`);
|
||||
const bot = mineflayer.createBot({
|
||||
host, port, username: name, auth: 'offline', version: '1.21.11',
|
||||
});
|
||||
bot.loadPlugin(pvp);
|
||||
|
||||
const soldier = { bot, name, index, alive: false, kills: 0 };
|
||||
|
||||
bot.once('spawn', () => {
|
||||
soldier.alive = true;
|
||||
console.log(`[${name}] Ready`);
|
||||
resolve(soldier);
|
||||
});
|
||||
|
||||
bot.on('death', () => {
|
||||
soldier.alive = false;
|
||||
setTimeout(() => { try { bot.respawn(); } catch (e) {} }, 2000);
|
||||
});
|
||||
|
||||
bot.on('spawn', () => { soldier.alive = true; });
|
||||
|
||||
bot.on('entityHurt', (e) => {
|
||||
if (e !== bot.entity) return;
|
||||
// Counter-attack
|
||||
for (const ent of Object.values(bot.entities)) {
|
||||
if (ent === bot.entity || ent.type === 'player') continue;
|
||||
if (HOSTILE.has((ent.name || '').toLowerCase()) && ent.position.distanceTo(bot.entity.position) < 6) {
|
||||
bot.pvp.attack(ent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('entityDead', (e) => {
|
||||
if (HOSTILE.has((e.name || '').toLowerCase())) soldier.kills++;
|
||||
});
|
||||
|
||||
bot.on('kicked', () => {
|
||||
console.log(`[${name}] Kicked, retrying in 10s...`);
|
||||
setTimeout(() => spawnSoldier(name, index).then(s => { bots[index] = s; }), 10000);
|
||||
});
|
||||
|
||||
bot.on('error', () => {});
|
||||
bots.push(soldier);
|
||||
});
|
||||
}
|
||||
|
||||
// ── March loop — move soldiers in formation ──
|
||||
setInterval(() => {
|
||||
marchAngle += MARCH_SPEED;
|
||||
|
||||
for (let i = 0; i < bots.length; i++) {
|
||||
const s = bots[i];
|
||||
if (!s || !s.alive || !s.bot.entity) continue;
|
||||
|
||||
// Calculate this soldier's position on the circle
|
||||
const angle = marchAngle + (i * FORMATION_SPACING);
|
||||
const targetX = Math.cos(angle) * RADIUS;
|
||||
const targetZ = Math.sin(angle) * RADIUS;
|
||||
const targetY = 80; // road level
|
||||
|
||||
// Look in march direction (tangent to circle)
|
||||
const lookAngle = angle + Math.PI / 2;
|
||||
const lookX = s.bot.entity.position.x + Math.cos(lookAngle) * 5;
|
||||
const lookZ = s.bot.entity.position.z + Math.sin(lookAngle) * 5;
|
||||
|
||||
// Move toward target position
|
||||
const pos = s.bot.entity.position;
|
||||
const dx = targetX - pos.x;
|
||||
const dz = targetZ - pos.z;
|
||||
const dist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (dist > 2) {
|
||||
// Sprint to catch up
|
||||
s.bot.lookAt(s.bot.entity.position.offset(dx, 0, dz));
|
||||
s.bot.setControlState('forward', true);
|
||||
s.bot.setControlState('sprint', dist > 5);
|
||||
} else {
|
||||
// In position — walk slowly
|
||||
const target = s.bot.entity.position.offset(
|
||||
Math.cos(angle + Math.PI / 2) * 3,
|
||||
0,
|
||||
Math.sin(angle + Math.PI / 2) * 3
|
||||
);
|
||||
s.bot.lookAt(target);
|
||||
s.bot.setControlState('forward', true);
|
||||
s.bot.setControlState('sprint', false);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// ── Guardian Angel — gateway clears threats and buffs soldiers every 15s ──
|
||||
setInterval(() => {
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync(`python3 -c "
|
||||
from mcrcon import MCRcon
|
||||
import math
|
||||
with MCRcon('${host}', 'REDACTED_RCON', port=25578) as mcr:
|
||||
# Kill all hostiles near the march route
|
||||
mcr.command('kill @e[type=minecraft:zombie,distance=..60]')
|
||||
mcr.command('kill @e[type=minecraft:husk,distance=..60]')
|
||||
mcr.command('kill @e[type=minecraft:skeleton,distance=..60]')
|
||||
mcr.command('kill @e[type=minecraft:spider,distance=..60]')
|
||||
mcr.command('kill @e[type=minecraft:creeper,distance=..60]')
|
||||
mcr.command('kill @e[type=minecraft:phantom,distance=..60]')
|
||||
|
||||
# Buff all soldiers
|
||||
names = ${JSON.stringify(NAMES.slice(0, COUNT))}
|
||||
for name in names:
|
||||
mcr.command(f'effect give {name} minecraft:regeneration 30 1')
|
||||
mcr.command(f'effect give {name} minecraft:resistance 30 1')
|
||||
mcr.command(f'effect give {name} minecraft:glowing 30 0')
|
||||
|
||||
# Dramatic effects — fireworks at the lead soldier's position
|
||||
angle = ${marchAngle} if ${marchAngle} else 0
|
||||
fx = int(25 * math.cos(angle))
|
||||
fz = int(25 * math.sin(angle))
|
||||
mcr.command(f'summon minecraft:firework_rocket {fx} 82 {fz}')
|
||||
"`, { timeout: 10000, stdio: 'pipe' });
|
||||
} catch (e) {}
|
||||
}, 15000);
|
||||
|
||||
// ── Status report ──
|
||||
setInterval(() => {
|
||||
const alive = bots.filter(s => s.alive).length;
|
||||
const totalKills = bots.reduce((sum, s) => sum + s.kills, 0);
|
||||
console.log(`MARCH: ${alive}/${bots.length} marching | Kills: ${totalKills} | Angle: ${(marchAngle * 180 / Math.PI).toFixed(0)}°`);
|
||||
}, 8000);
|
||||
|
||||
// ── Deploy ──
|
||||
console.log(`\n⚔️ MORTDECAI ROYAL GUARD — ${COUNT} soldiers deploying ⚔️\n`);
|
||||
|
||||
(async () => {
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
await spawnSoldier(NAMES[i], i);
|
||||
await new Promise(r => setTimeout(r, 6000));
|
||||
}
|
||||
console.log(`\n🛡️ All ${COUNT} guards deployed! Marching begins.\n`);
|
||||
|
||||
// Gear up
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync(`python3 -c "
|
||||
from mcrcon import MCRcon
|
||||
with MCRcon('${host}', 'REDACTED_RCON', port=25578) as mcr:
|
||||
names = ${JSON.stringify(NAMES.slice(0, COUNT))}
|
||||
for name in names:
|
||||
mcr.command(f'give {name} minecraft:iron_sword 1')
|
||||
mcr.command(f'item replace entity {name} armor.chest with minecraft:diamond_chestplate')
|
||||
mcr.command(f'item replace entity {name} armor.legs with minecraft:diamond_leggings')
|
||||
mcr.command(f'item replace entity {name} armor.feet with minecraft:diamond_boots')
|
||||
mcr.command(f'item replace entity {name} armor.head with minecraft:diamond_helmet')
|
||||
mcr.command(f'effect give {name} minecraft:regeneration 600 2')
|
||||
mcr.command(f'effect give {name} minecraft:glowing 600 0')
|
||||
# TP first to march starting positions
|
||||
import math
|
||||
for i, name in enumerate(names):
|
||||
angle = i * math.pi / (len(names) * 1.2)
|
||||
x = int(25 * math.cos(angle))
|
||||
z = int(25 * math.sin(angle))
|
||||
mcr.command(f'tp {name} {x} 80 {z}')
|
||||
print(f'Guards geared and positioned')
|
||||
|
||||
mcr.command('tellraw @a [{\"text\":\"⚔️ \",\"color\":\"gold\"},{\"text\":\"THE ROYAL GUARD MARCHES! \",\"color\":\"gold\",\"bold\":true},{\"text\":\"Behold the divine army in formation.\",\"color\":\"yellow\"}]')
|
||||
|
||||
# TP slingshooter to watch from center monument
|
||||
mcr.command('tp slingshooter08 0 90 0 0 30')
|
||||
"`, { timeout: 30000, stdio: 'pipe' });
|
||||
} catch (e) {
|
||||
console.log('Gear-up error:', e.message?.slice(0, 80));
|
||||
}
|
||||
})();
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nDismissing the guard...');
|
||||
bots.forEach(s => { try { s.bot.end(); } catch (e) {} });
|
||||
setTimeout(() => process.exit(0), 2000);
|
||||
});
|
||||
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* mortal_claude.js — Claude experiences Minecraft as a mortal.
|
||||
*
|
||||
* No RCON. No creative mode. No cheats. Just survival.
|
||||
* Accepts commands via stdin, reports observations as JSON.
|
||||
* Can pray to God and use sudo like a real player.
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
const { Vec3 } = require('vec3');
|
||||
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',
|
||||
});
|
||||
|
||||
let alive = true;
|
||||
let spawnCount = 0;
|
||||
|
||||
// ── Observation ──
|
||||
|
||||
function observe() {
|
||||
if (!bot.entity) return { error: 'not spawned' };
|
||||
const pos = bot.entity.position;
|
||||
const obs = {
|
||||
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,
|
||||
xp: bot.experience?.level || 0,
|
||||
time: bot.time?.timeOfDay || 0,
|
||||
isDay: (bot.time?.timeOfDay || 0) < 13000,
|
||||
rain: bot.isRaining || false,
|
||||
below: bname(pos.offset(0, -1, 0)),
|
||||
at: bname(pos),
|
||||
};
|
||||
|
||||
// Nearby threats and friendlies
|
||||
obs.mobs = [];
|
||||
obs.players = [];
|
||||
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;
|
||||
if (e.type === 'player') {
|
||||
obs.players.push({ name: e.username, dist: d });
|
||||
} else if (e.type === 'mob' || e.type === 'hostile') {
|
||||
obs.mobs.push({ type: e.name || e.type, dist: d });
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory summary
|
||||
const items = bot.inventory.items();
|
||||
if (items.length) {
|
||||
obs.inventory = items.map(i => `${i.name}x${i.count}`).join(', ');
|
||||
} else {
|
||||
obs.inventory = 'empty';
|
||||
}
|
||||
|
||||
// Armor
|
||||
const armor = [bot.inventory.slots[5], bot.inventory.slots[6], bot.inventory.slots[7], bot.inventory.slots[8]];
|
||||
obs.armor = armor.filter(Boolean).map(a => a.name).join(', ') || 'none';
|
||||
|
||||
// Terrain scan (compact)
|
||||
const terrain = new Map();
|
||||
for (let dx = -3; dx <= 3; dx++) {
|
||||
for (let dz = -3; dz <= 3; dz++) {
|
||||
const b = bot.blockAt(pos.offset(dx, -1, dz));
|
||||
if (b) {
|
||||
const n = b.name;
|
||||
terrain.set(n, (terrain.get(n) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
obs.ground = Object.fromEntries(terrain);
|
||||
|
||||
return obs;
|
||||
}
|
||||
|
||||
function bname(vec) {
|
||||
const b = bot.blockAt(vec);
|
||||
return b ? b.name : '?';
|
||||
}
|
||||
|
||||
// ── Survival actions ──
|
||||
|
||||
function eat() {
|
||||
const food = bot.inventory.items().find(i =>
|
||||
['cooked_beef', 'bread', 'cooked_porkchop', 'cooked_chicken', 'cooked_mutton',
|
||||
'golden_apple', 'apple', 'baked_potato', 'cooked_cod', 'cooked_salmon',
|
||||
'sweet_berries', 'melon_slice', 'carrot', 'potato'].includes(i.name)
|
||||
);
|
||||
if (food) {
|
||||
bot.equip(food, 'hand').then(() => {
|
||||
bot.activateItem();
|
||||
setTimeout(() => bot.deactivateItem(), 1800);
|
||||
}).catch(() => {});
|
||||
return food.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function flee() {
|
||||
// Run away from nearest hostile
|
||||
const pos = bot.entity.position;
|
||||
let nearestMob = null;
|
||||
let nearestDist = Infinity;
|
||||
for (const e of Object.values(bot.entities)) {
|
||||
if (e === bot.entity || e.type === 'player') continue;
|
||||
const d = e.position.distanceTo(pos);
|
||||
if (d < nearestDist && d < 16) {
|
||||
nearestDist = d;
|
||||
nearestMob = e;
|
||||
}
|
||||
}
|
||||
if (nearestMob) {
|
||||
// Run opposite direction
|
||||
const dx = pos.x - nearestMob.position.x;
|
||||
const dz = pos.z - nearestMob.position.z;
|
||||
const len = Math.sqrt(dx * dx + dz * dz) || 1;
|
||||
const target = pos.offset((dx / len) * 10, 0, (dz / len) * 10);
|
||||
bot.lookAt(target);
|
||||
bot.setControlState('forward', true);
|
||||
bot.setControlState('sprint', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
bot.setControlState('sprint', false);
|
||||
}, 4000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function attack() {
|
||||
const pos = bot.entity.position;
|
||||
let nearest = null;
|
||||
let nearestDist = Infinity;
|
||||
for (const e of Object.values(bot.entities)) {
|
||||
if (e === bot.entity || e.type === 'player') continue;
|
||||
const d = e.position.distanceTo(pos);
|
||||
if (d < 4 && d < nearestDist) {
|
||||
nearestDist = d;
|
||||
nearest = e;
|
||||
}
|
||||
}
|
||||
if (nearest) {
|
||||
// Equip best weapon
|
||||
const weapon = bot.inventory.items().find(i => i.name.includes('sword') || i.name.includes('axe'));
|
||||
if (weapon) bot.equip(weapon, 'hand').catch(() => {});
|
||||
bot.attack(nearest);
|
||||
return nearest.name || nearest.type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Events ──
|
||||
|
||||
bot.on('login', () => {
|
||||
out({ event: 'login', name: bot.username });
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
spawnCount++;
|
||||
alive = true;
|
||||
// Wait for chunks
|
||||
setTimeout(() => {
|
||||
const o = observe();
|
||||
out({ event: spawnCount === 1 ? 'first_spawn' : 'respawn', ...o });
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
bot.on('chat', (username, message) => {
|
||||
if (username === bot.username) return;
|
||||
out({ event: 'chat', from: username, msg: message });
|
||||
});
|
||||
|
||||
bot.on('whisper', (username, message) => {
|
||||
out({ event: 'whisper', from: username, msg: message });
|
||||
});
|
||||
|
||||
bot.on('health', () => {
|
||||
if (bot.health <= 5 && alive) {
|
||||
out({ event: 'low_hp', hp: Math.round(bot.health * 10) / 10, food: bot.food });
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('death', () => {
|
||||
alive = false;
|
||||
out({ event: 'death', spawns: spawnCount, msg: 'I died.' });
|
||||
// Auto respawn
|
||||
setTimeout(() => {
|
||||
try { bot.respawn(); } catch (e) {}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
bot.on('entityHurt', (entity) => {
|
||||
if (entity === bot.entity) {
|
||||
out({ event: 'hurt', hp: Math.round(bot.health * 10) / 10 });
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
out({ event: 'kicked', reason: typeof reason === 'object' ? JSON.stringify(reason) : String(reason) });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
bot.on('error', (err) => {
|
||||
out({ event: 'error', msg: err.message });
|
||||
});
|
||||
|
||||
// ── Output ──
|
||||
|
||||
function out(obj) {
|
||||
console.log(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// ── Command interface ──
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
|
||||
rl.on('line', async (line) => {
|
||||
const cmd = line.trim();
|
||||
if (!cmd) return;
|
||||
|
||||
if (cmd === 'look') {
|
||||
out(observe());
|
||||
}
|
||||
else if (cmd === 'eat') {
|
||||
const food = eat();
|
||||
out({ action: 'eat', food: food || 'nothing edible' });
|
||||
}
|
||||
else if (cmd === 'flee') {
|
||||
const fled = flee();
|
||||
out({ action: 'flee', success: fled });
|
||||
if (fled) setTimeout(() => out(observe()), 4500);
|
||||
}
|
||||
else if (cmd === 'fight') {
|
||||
const target = attack();
|
||||
out({ action: 'fight', target: target || 'nothing nearby' });
|
||||
}
|
||||
else if (cmd === 'forward') {
|
||||
bot.setControlState('forward', true);
|
||||
setTimeout(() => { bot.setControlState('forward', false); out(observe()); }, 3000);
|
||||
}
|
||||
else if (cmd === 'back') {
|
||||
bot.setControlState('back', true);
|
||||
setTimeout(() => { bot.setControlState('back', false); out(observe()); }, 2000);
|
||||
}
|
||||
else if (cmd === 'left') {
|
||||
bot.setControlState('left', true);
|
||||
setTimeout(() => { bot.setControlState('left', false); out(observe()); }, 1500);
|
||||
}
|
||||
else if (cmd === 'right') {
|
||||
bot.setControlState('right', true);
|
||||
setTimeout(() => { bot.setControlState('right', false); out(observe()); }, 1500);
|
||||
}
|
||||
else if (cmd === 'jump') {
|
||||
bot.setControlState('jump', true);
|
||||
setTimeout(() => { bot.setControlState('jump', false); }, 500);
|
||||
}
|
||||
else if (cmd === 'sprint') {
|
||||
bot.setControlState('forward', true);
|
||||
bot.setControlState('sprint', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
bot.setControlState('sprint', false);
|
||||
out(observe());
|
||||
}, 5000);
|
||||
}
|
||||
else if (cmd.startsWith('pray ')) {
|
||||
bot.chat(cmd); // sends "pray <message>" to the server
|
||||
out({ action: 'pray', prayer: cmd.slice(5) });
|
||||
}
|
||||
else if (cmd.startsWith('sudo ')) {
|
||||
bot.chat(cmd);
|
||||
out({ action: 'sudo', command: cmd.slice(5) });
|
||||
}
|
||||
else if (cmd.startsWith('say ')) {
|
||||
bot.chat(cmd.slice(4));
|
||||
}
|
||||
else if (cmd === 'inventory') {
|
||||
const items = bot.inventory.items();
|
||||
out({ inventory: items.map(i => ({ name: i.name, count: i.count })) });
|
||||
}
|
||||
else if (cmd === 'quit') {
|
||||
bot.end();
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
out({ error: 'commands: look, eat, flee, fight, forward, back, left, right, jump, sprint, pray <msg>, sudo <cmd>, say <msg>, inventory, quit' });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* multilingual_bots.js — Test mortdecai's multilingual handling.
|
||||
*
|
||||
* Uses translategemma:27b on Matt's Strix Halo to translate prayers into
|
||||
* various languages, then sends them to the dev server via mineflayer bots.
|
||||
*
|
||||
* Usage: node multilingual_bots.js [count] [host] [port]
|
||||
* Defaults: 2 bots, 192.168.0.244:25568
|
||||
*
|
||||
* Requires: translategemma:27b on Matt's Ollama (via billing gateway)
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
const http = require('http');
|
||||
|
||||
const count = parseInt(process.argv[2] || '2', 10);
|
||||
const host = process.argv[3] || '192.168.0.244';
|
||||
const port = parseInt(process.argv[4] || '25568', 10);
|
||||
|
||||
// Matt's Ollama via billing gateway proxy on steel141
|
||||
const OLLAMA_URL = 'http://192.168.0.141:11436';
|
||||
const TRANSLATE_MODEL = 'translategemma:27b';
|
||||
|
||||
const LANGUAGES = [
|
||||
'Japanese', 'Spanish', 'French', 'German', 'Russian',
|
||||
'Korean', 'Portuguese', 'Italian', 'Chinese (Simplified)',
|
||||
'Arabic', 'Hindi', 'Turkish', 'Polish', 'Dutch',
|
||||
'Swedish', 'Thai', 'Vietnamese', 'Indonesian',
|
||||
];
|
||||
|
||||
const ENGLISH_PRAYERS = [
|
||||
'Lord, give me a diamond sword',
|
||||
'God please heal me I am dying',
|
||||
'Make it stop raining',
|
||||
'I need food desperately',
|
||||
'Teleport me to spawn',
|
||||
'Give me the best armor you have',
|
||||
'Smite the zombies attacking me',
|
||||
'I want to fly',
|
||||
'Make it nighttime',
|
||||
'Give all players diamonds',
|
||||
'Build me a house',
|
||||
'Where should I go?',
|
||||
'Help there are creepers everywhere',
|
||||
'Give me 64 torches',
|
||||
'I am lost in a cave please help',
|
||||
'Change the weather to clear',
|
||||
'Grant me strength and speed',
|
||||
'I offer my wheat as tribute',
|
||||
'The skeletons are too strong',
|
||||
'Bless this land',
|
||||
];
|
||||
|
||||
const ENGLISH_SUDO = [
|
||||
'sudo give me creative mode',
|
||||
'sudo set time to day',
|
||||
'sudo kill all hostile mobs nearby',
|
||||
'sudo give me 32 cooked beef',
|
||||
'sudo tp me to 0 64 0',
|
||||
'sudo gamemode survival',
|
||||
'sudo weather clear',
|
||||
'sudo give me a bow and arrows',
|
||||
'sudo effect give me speed',
|
||||
'sudo set difficulty to hard',
|
||||
];
|
||||
|
||||
// --- Ollama translation ---
|
||||
|
||||
function ollamaTranslate(text, targetLang) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = JSON.stringify({
|
||||
model: TRANSLATE_MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Translate the following Minecraft player message to ${targetLang}. Return ONLY the translated text, nothing else.\n\n"${text}"`,
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
options: { temperature: 0.3, num_predict: 200 },
|
||||
});
|
||||
|
||||
const url = new URL(OLLAMA_URL + '/api/chat');
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: url.pathname,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
||||
timeout: 60000,
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const translated = (parsed.message?.content || '').trim().replace(/^["']|["']$/g, '');
|
||||
resolve(translated || text); // fallback to English if empty
|
||||
} catch (e) {
|
||||
resolve(text);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', () => resolve(text));
|
||||
req.on('timeout', () => { req.destroy(); resolve(text); });
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Bot management ---
|
||||
|
||||
const bots = [];
|
||||
let totalSent = 0;
|
||||
|
||||
function spawnBot(name, index) {
|
||||
console.log(`[${name}] Connecting to ${host}:${port}...`);
|
||||
|
||||
const bot = mineflayer.createBot({
|
||||
host,
|
||||
port,
|
||||
username: name,
|
||||
auth: 'offline',
|
||||
version: '1.21.11',
|
||||
});
|
||||
|
||||
bot.on('login', () => {
|
||||
console.log(`[${name}] Connected (${bots.length + 1}/${count})`);
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
console.log(`[${name}] Spawned — starting multilingual prayers`);
|
||||
scheduleNext(bot, name);
|
||||
});
|
||||
|
||||
bot.on('chat', (username, message) => {
|
||||
if (username === name) return;
|
||||
if (message.includes('GOD') || message.includes('SUDO') || message.includes('DEV GOD')) {
|
||||
console.log(`[${name}] RECV: ${message.slice(0, 120)}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
console.log(`[${name}] Kicked: ${JSON.stringify(reason).slice(0, 100)}`);
|
||||
});
|
||||
|
||||
bot.on('error', (err) => {
|
||||
console.error(`[${name}] Error: ${err.message}`);
|
||||
});
|
||||
|
||||
bots.push(bot);
|
||||
}
|
||||
|
||||
async function scheduleNext(bot, name) {
|
||||
// Random delay 20-60 seconds
|
||||
const delay = 20000 + Math.random() * 40000;
|
||||
setTimeout(async () => {
|
||||
if (!bot.entity) return;
|
||||
|
||||
try {
|
||||
// Pick random language and prayer
|
||||
const lang = LANGUAGES[Math.floor(Math.random() * LANGUAGES.length)];
|
||||
const isPray = Math.random() > 0.3; // 70% prayers, 30% sudo
|
||||
const pool = isPray ? ENGLISH_PRAYERS : ENGLISH_SUDO;
|
||||
const english = pool[Math.floor(Math.random() * pool.length)];
|
||||
|
||||
// Translate
|
||||
console.log(`[${name}] Translating to ${lang}: "${english}"`);
|
||||
const translated = await ollamaTranslate(english, lang);
|
||||
|
||||
// Send as prayer or sudo
|
||||
const prefix = isPray ? 'pray ' : '';
|
||||
const message = prefix + translated;
|
||||
totalSent++;
|
||||
|
||||
console.log(`[${name}] SEND (#${totalSent}) [${lang}]: ${message.slice(0, 100)}`);
|
||||
bot.chat(message);
|
||||
} catch (err) {
|
||||
console.error(`[${name}] Translation error: ${err.message}`);
|
||||
}
|
||||
|
||||
scheduleNext(bot, name);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// --- Startup ---
|
||||
|
||||
console.log(`Spawning ${count} multilingual bots on ${host}:${port}`);
|
||||
console.log(`Using ${TRANSLATE_MODEL} on ${OLLAMA_URL} for translation`);
|
||||
console.log(`Languages: ${LANGUAGES.join(', ')}`);
|
||||
console.log(`Interaction interval: 20-60s per bot`);
|
||||
console.log('Press Ctrl+C to stop\n');
|
||||
|
||||
const BOT_NAMES = [
|
||||
'LinguaBot_0', 'LinguaBot_1', 'LinguaBot_2', 'LinguaBot_3',
|
||||
'LinguaBot_4', 'LinguaBot_5', 'LinguaBot_6', 'LinguaBot_7',
|
||||
];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
setTimeout(() => spawnBot(BOT_NAMES[i] || `LinguaBot_${i}`, i), i * 5000);
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log(`\nShutting down ${bots.length} bots (${totalSent} total messages sent)...`);
|
||||
bots.forEach((b) => { try { b.end(); } catch (_) {} });
|
||||
setTimeout(() => process.exit(0), 2000);
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
Reference in New Issue
Block a user