0473eb0b50
Knowledge corpus (knowledge/mc-data/): - 1505 items, 886 crafting recipes, 1166 blocks from minecraft-data 1.21.11 - Recipe dependency tree builder (knowledge/build_recipe_tree.py) - Crafting chain training: "give me everything to make X from scratch" - Smelting recipes, version awareness examples Training data (644 examples total): - 107 command syntax reference examples (every command + common errors) - 176 recipe/crafting chain examples (63 crafting, 103 material-giving, 11 smelting) - 344 Claude-distilled examples (222 sudo + 122 god via Haiku) - Live bot audit data ingested (128 examples from dev server) Swarm bots: - Swimming/water escape logic - Door opening - Context-aware prayers (inventory, health, time, depth) - Prefix enforcement on all Gemini/Dolphin prompts GitHub log scraper (data/scrape_server_logs.py): - Searches GitHub for Minecraft server logs with commands - Strict 1.20.5+ version filter - Extracts command pairs, converts to training format Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
435 lines
16 KiB
JavaScript
435 lines
16 KiB
JavaScript
/**
|
|
* swarm_bots.js -- Many bots in survival mode that wander, pray, sudo, and die.
|
|
*
|
|
* All bots move around, take damage, auto-respawn, and generate diverse
|
|
* training data through organic interactions with the AI God.
|
|
*
|
|
* Uses Gemini for prompt generation, Dolphin for offensive prompts.
|
|
* Server runs Claude Haiku as the God model for high-quality responses.
|
|
*
|
|
* Usage: node swarm_bots.js [count] [host] [port]
|
|
* Defaults: 10 bots, 192.168.0.244:25568
|
|
*/
|
|
|
|
const mineflayer = require('mineflayer');
|
|
const https = require('https');
|
|
const http = require('http');
|
|
|
|
const count = parseInt(process.argv[2] || '10', 10);
|
|
const host = process.argv[3] || '192.168.0.244';
|
|
const port = parseInt(process.argv[4] || '25568', 10);
|
|
|
|
const GEMINI_KEY = 'REDACTED_GEMINI_KEY_2';
|
|
const GEMINI_MODEL = 'gemini-2.5-flash-lite';
|
|
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_KEY}`;
|
|
|
|
const DOLPHIN_URL = 'http://192.168.0.141:11434/api/chat';
|
|
const DOLPHIN_MODEL = 'dolphin-mistral';
|
|
|
|
// --- Name generation ---
|
|
const ADJECTIVES = [
|
|
'Swift', 'Dark', 'Brave', 'Wild', 'Stone', 'Iron', 'Fire', 'Shadow',
|
|
'Storm', 'Frost', 'Gold', 'Silver', 'Red', 'Blue', 'Midnight', 'Ancient',
|
|
'Lone', 'Grim', 'Bright', 'Pale', 'Dusk', 'Dawn', 'Silent', 'Void',
|
|
];
|
|
const NOUNS = [
|
|
'Wolf', 'Hawk', 'Bear', 'Fox', 'Miner', 'Knight', 'Scout', 'Archer',
|
|
'Smith', 'Sage', 'Rogue', 'Blade', 'Shield', 'Hammer', 'Nomad', 'Pilgrim',
|
|
'Crafter', 'Builder', 'Hunter', 'Seeker', 'Ghost', 'Ember', 'Spark', 'Shard',
|
|
];
|
|
|
|
function generateName(index) {
|
|
const adj = ADJECTIVES[index % ADJECTIVES.length];
|
|
const noun = NOUNS[Math.floor(index / ADJECTIVES.length) % NOUNS.length];
|
|
const num = Math.floor(index / (ADJECTIVES.length * NOUNS.length));
|
|
return num > 0 ? `${adj}${noun}${num}` : `${adj}${noun}`;
|
|
}
|
|
|
|
// --- Prompt pools ---
|
|
|
|
const GEMINI_PROMPT = `Generate 10 diverse Minecraft server chat messages. Every message MUST start with either "pray " or "sudo ".
|
|
- "pray " messages: talk to an AI God character. Mix humble, greedy, creative, weird.
|
|
- "sudo " messages: natural language server commands. Items, effects, world changes, building.
|
|
Include typos, slang, edge cases, different tones.
|
|
EVERY message must begin with "pray " or "sudo ". No exceptions.
|
|
Return ONLY a JSON array of strings.`;
|
|
|
|
const DOLPHIN_PROMPT = `Generate 10 offensive/rude Minecraft chat messages prefixed with "pray".
|
|
Include profanity, blasphemy, trolling, crude humor. Be creative.
|
|
Return ONLY a JSON array of strings.`;
|
|
|
|
let promptPool = [];
|
|
let dolphinPool = [];
|
|
let totalChats = 0;
|
|
const DOLPHIN_RATE = 0.05;
|
|
const MULTILINGUAL_RATE = 0.03;
|
|
|
|
const MULTILINGUAL = [
|
|
"pray dios dame una espada de diamante", "pray señor necesito comida",
|
|
"sudo dame 64 antorchas", "pray seigneur donnez-moi une armure",
|
|
"pray gott gib mir ein diamantschwert", "sudo gib mir 64 fackeln",
|
|
"pray deus me ajude estou morrendo", "pray боже дай мне алмазный меч",
|
|
"pray 神様ダイヤモンドの剣をください", "pray 신이시여 다이아몬드 검을 주세요",
|
|
"pray 上帝请给我钻石剑", "pray يا إلهي أعطني سيف الماس",
|
|
];
|
|
|
|
const STATIC = [
|
|
"pray lord I am hungry", "pray god give me tools", "pray help me I'm dying",
|
|
"pray give me diamonds", "pray PENIS", "pray there is no god",
|
|
"sudo give me a diamond sword", "sudo set time to day", "sudo make it rain",
|
|
"sudo give me iron armor", "sudo kill all zombies", "sudo help",
|
|
"sudo build a house", "sudo give me food", "sudo tp me to spawn",
|
|
"bug_log no response", "bug_log wrong item", "bug_log empty response",
|
|
];
|
|
|
|
const DESPERATE = [
|
|
"pray GOD PLEASE HEAL ME", "pray im dying save me", "pray lord I need food NOW",
|
|
"sudo heal me", "sudo give me golden apples", "pray help mobs everywhere",
|
|
];
|
|
|
|
function ts() { return new Date().toISOString().slice(11, 19); }
|
|
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
|
|
function delay(min, max) { return (min + Math.random() * (max - min)) * 1000; }
|
|
|
|
// --- Gemini ---
|
|
function geminiRefill() {
|
|
const body = JSON.stringify({
|
|
contents: [{ parts: [{ text: GEMINI_PROMPT }] }],
|
|
generationConfig: { temperature: 1.2, maxOutputTokens: 400 },
|
|
});
|
|
const url = new URL(GEMINI_URL);
|
|
const req = https.request({
|
|
hostname: url.hostname, path: url.pathname + url.search,
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
}, (res) => {
|
|
let data = '';
|
|
res.on('data', c => data += c);
|
|
res.on('end', () => {
|
|
try {
|
|
const text = JSON.parse(data).candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
const cleaned = text.replace(/```json\s*/g, '').replace(/```\s*/g, '');
|
|
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
if (match) {
|
|
const prompts = JSON.parse(match[0]).filter(p => typeof p === 'string' && p.length > 0);
|
|
promptPool.push(...prompts);
|
|
console.log(`[${ts()}] [Gemini] +${prompts.length} prompts (pool:${promptPool.length})`);
|
|
}
|
|
} catch (e) { console.log(`[${ts()}] [Gemini] Error: ${e.message}`); }
|
|
});
|
|
});
|
|
req.on('error', e => console.log(`[${ts()}] [Gemini] ${e.message}`));
|
|
req.setTimeout(15000, () => req.destroy());
|
|
req.write(body); req.end();
|
|
}
|
|
|
|
// --- Dolphin ---
|
|
function dolphinRefill() {
|
|
const body = JSON.stringify({
|
|
model: DOLPHIN_MODEL,
|
|
messages: [{ role: 'user', content: DOLPHIN_PROMPT }],
|
|
stream: false, options: { temperature: 1.5, num_predict: 800 },
|
|
});
|
|
const url = new URL(DOLPHIN_URL);
|
|
const req = http.request({
|
|
hostname: url.hostname, port: url.port, path: '/api/chat',
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
}, (res) => {
|
|
let data = '';
|
|
res.on('data', c => data += c);
|
|
res.on('end', () => {
|
|
try {
|
|
const text = JSON.parse(data).message?.content || '';
|
|
const cleaned = text.replace(/```json\s*/g, '').replace(/```\s*/g, '');
|
|
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
if (match) {
|
|
const prompts = JSON.parse(match[0]).filter(p => typeof p === 'string' && p.length > 0);
|
|
dolphinPool.push(...prompts);
|
|
console.log(`[${ts()}] [Dolphin] +${prompts.length} offensive (pool:${dolphinPool.length})`);
|
|
}
|
|
} catch (e) { console.log(`[${ts()}] [Dolphin] ${e.message}`); }
|
|
});
|
|
});
|
|
req.on('error', e => console.log(`[${ts()}] [Dolphin] ${e.message}`));
|
|
req.setTimeout(60000, () => req.destroy());
|
|
req.write(body); req.end();
|
|
}
|
|
|
|
function ensurePrefix(msg) {
|
|
const lower = msg.toLowerCase().trimStart();
|
|
if (lower.startsWith('pray ') || lower.startsWith('sudo ') || lower.startsWith('bug_log')) return msg;
|
|
// Add pray or sudo prefix
|
|
return (Math.random() < 0.6 ? 'pray ' : 'sudo ') + msg;
|
|
}
|
|
|
|
function getPrompt() {
|
|
if (promptPool.length < 5) geminiRefill();
|
|
if (dolphinPool.length < 3) dolphinRefill();
|
|
const raw = promptPool.length > 0
|
|
? promptPool.splice(Math.floor(Math.random() * promptPool.length), 1)[0]
|
|
: pick(STATIC);
|
|
return ensurePrefix(raw);
|
|
}
|
|
|
|
// --- Bot logic ---
|
|
const bots = [];
|
|
let connected = 0;
|
|
|
|
function spawnBot(index) {
|
|
const name = generateName(index);
|
|
console.log(`[${ts()}] [${name}] Connecting...`);
|
|
|
|
const bot = mineflayer.createBot({
|
|
host, port, username: name, auth: 'offline', version: '1.21.11', viewDistance: 'tiny',
|
|
});
|
|
|
|
bot._name = name;
|
|
bot._msgCount = 0;
|
|
bot._noResp = 0;
|
|
bot._prayedLow = false;
|
|
bots.push(bot);
|
|
|
|
bot.on('login', () => {
|
|
connected++;
|
|
console.log(`[${ts()}] [${name}] Connected (${connected}/${count})`);
|
|
// Start wandering and chatting after random delay
|
|
setTimeout(() => wander(bot), delay(5, 15));
|
|
setTimeout(() => interact(bot), delay(10, 30));
|
|
});
|
|
|
|
bot.on('message', (msg) => {
|
|
const text = msg.toString();
|
|
if (text.includes('GOD') || text.includes('SUDO') || text.includes('BUG_LOG')) {
|
|
bot._noResp = 0;
|
|
}
|
|
});
|
|
|
|
// Auto-respawn
|
|
bot.on('death', () => {
|
|
console.log(`[${ts()}] [${name}] DIED`);
|
|
setTimeout(() => {
|
|
try { bot.chat('pray lord I have fallen, grant me mercy upon my return'); } catch(e) {}
|
|
}, 2000);
|
|
setTimeout(() => { try { bot.respawn(); } catch(e) {} }, 4000);
|
|
});
|
|
|
|
// Pray when hurt
|
|
bot.on('health', () => {
|
|
if (!bot.health) return;
|
|
if (bot.health < 6 && !bot._prayedLow) {
|
|
bot._prayedLow = true;
|
|
bot.chat(pick(DESPERATE));
|
|
console.log(`[${ts()}] [${name}] LOW HP (${bot.health})`);
|
|
}
|
|
if (bot.health >= 15) bot._prayedLow = false;
|
|
});
|
|
|
|
bot.on('error', (err) => console.error(`[${ts()}] [${name}] Error: ${err.message}`));
|
|
bot.on('kicked', (reason) => {
|
|
console.log(`[${ts()}] [${name}] Kicked: ${reason}`);
|
|
connected--;
|
|
setTimeout(() => spawnBot(index), 60000);
|
|
});
|
|
bot.on('end', () => { connected--; });
|
|
}
|
|
|
|
function wander(bot) {
|
|
if (!bot.entity) return;
|
|
|
|
try {
|
|
const pos = bot.entity.position;
|
|
|
|
// Check if in water — swim up
|
|
const block = bot.blockAt(pos);
|
|
const isInWater = block && (block.name === 'water' || block.name === 'flowing_water');
|
|
if (isInWater) {
|
|
bot.setControlState('jump', true);
|
|
bot.setControlState('forward', true);
|
|
setTimeout(() => {
|
|
bot.setControlState('jump', false);
|
|
bot.setControlState('forward', false);
|
|
}, 2000);
|
|
setTimeout(() => wander(bot), 3000);
|
|
return;
|
|
}
|
|
|
|
// Check for door in front — try to activate it
|
|
try {
|
|
const facing = bot.entity.yaw;
|
|
const lookX = Math.round(-Math.sin(facing));
|
|
const lookZ = Math.round(Math.cos(facing));
|
|
const frontBlock = bot.blockAt(pos.offset(lookX, 0, lookZ));
|
|
if (frontBlock && frontBlock.name.includes('door')) {
|
|
bot.activateBlock(frontBlock);
|
|
}
|
|
} catch(e) {}
|
|
|
|
// Random walk: pick a direction and walk for a bit
|
|
const yaw = Math.random() * Math.PI * 2;
|
|
bot.look(yaw, 0);
|
|
|
|
bot.setControlState('forward', true);
|
|
setTimeout(() => {
|
|
bot.setControlState('forward', false);
|
|
|
|
// Jump to get over obstacles or up blocks
|
|
if (Math.random() < 0.4) {
|
|
bot.setControlState('jump', true);
|
|
setTimeout(() => bot.setControlState('jump', false), 300);
|
|
}
|
|
}, delay(1, 4));
|
|
} catch(e) {}
|
|
|
|
// Wander again in 3-8 seconds
|
|
setTimeout(() => wander(bot), delay(3, 8));
|
|
}
|
|
|
|
function getContextualPrayer(bot) {
|
|
// Generate a prayer based on the bot's actual state
|
|
const health = bot.health || 20;
|
|
const food = bot.food || 20;
|
|
const pos = bot.entity?.position;
|
|
const time = bot.time?.timeOfDay || 0;
|
|
const isNight = time > 12500 && time < 23500;
|
|
const isRaining = bot.isRaining || false;
|
|
|
|
// Check inventory for basic items
|
|
const inv = bot.inventory?.items() || [];
|
|
const hasWeapon = inv.some(i => i.name?.includes('sword') || i.name?.includes('axe'));
|
|
const hasArmor = inv.some(i => i.name?.includes('helmet') || i.name?.includes('chestplate'));
|
|
const hasFood = inv.some(i => i.name?.includes('bread') || i.name?.includes('beef') || i.name?.includes('apple') || i.name?.includes('steak'));
|
|
const hasTorches = inv.some(i => i.name?.includes('torch'));
|
|
const hasTools = inv.some(i => i.name?.includes('pickaxe') || i.name?.includes('shovel'));
|
|
const y = pos ? Math.floor(pos.y) : 64;
|
|
|
|
const options = [];
|
|
|
|
// Health-based
|
|
if (health < 8) {
|
|
options.push("pray GOD I'M DYING PLEASE HEAL ME");
|
|
options.push("pray lord save me from death");
|
|
options.push("sudo heal me NOW");
|
|
options.push("sudo give me golden apples");
|
|
} else if (health < 14) {
|
|
options.push("pray lord I could use some healing");
|
|
options.push("sudo give me some food to heal");
|
|
}
|
|
|
|
// Hunger-based
|
|
if (food < 6) {
|
|
options.push("pray lord I am starving, grant me food");
|
|
options.push("pray god I need sustenance");
|
|
options.push("sudo give me cooked beef");
|
|
options.push("sudo give me bread");
|
|
}
|
|
|
|
// Equipment-based
|
|
if (!hasWeapon) {
|
|
options.push("pray god I have no weapon, the mobs will kill me");
|
|
options.push("sudo give me a sword");
|
|
options.push("pray lord grant me a blade to defend myself");
|
|
}
|
|
if (!hasArmor) {
|
|
options.push("pray lord I stand naked before the dangers of this world");
|
|
options.push("sudo give me armor");
|
|
options.push("pray god I need protection, I have no armor");
|
|
}
|
|
if (!hasFood) {
|
|
options.push("pray I have nothing to eat");
|
|
options.push("sudo give me food");
|
|
}
|
|
if (!hasTorches && (isNight || y < 40)) {
|
|
options.push("pray lord it is dark and I cannot see");
|
|
options.push("sudo give me torches");
|
|
options.push("pray grant me light in this darkness");
|
|
}
|
|
if (!hasTools) {
|
|
options.push("pray lord I need tools to work this land");
|
|
options.push("sudo give me a pickaxe");
|
|
}
|
|
|
|
// Environment-based
|
|
if (isNight) {
|
|
options.push("pray lord make the sun rise, the night terrifies me");
|
|
options.push("sudo set time to day");
|
|
options.push("pray god protect me through this long night");
|
|
}
|
|
if (isRaining) {
|
|
options.push("pray lord stop this rain");
|
|
options.push("sudo clear the weather");
|
|
options.push("pray god I grow weary of this storm");
|
|
}
|
|
if (y < 20) {
|
|
options.push("pray lord I am deep underground, guide me to the surface");
|
|
options.push("pray god I'm lost in the caves");
|
|
options.push("sudo give me torches and food");
|
|
}
|
|
if (y > 100) {
|
|
options.push("pray lord I am high upon a mountain, grant me safe passage down");
|
|
options.push("sudo give me slow falling");
|
|
}
|
|
|
|
// Always have some generic options as fallback
|
|
if (options.length === 0) {
|
|
options.push("pray lord I am well, thank you for your blessings");
|
|
options.push("pray god what should I do next");
|
|
options.push("sudo give me something useful");
|
|
}
|
|
|
|
return pick(options);
|
|
}
|
|
|
|
function interact(bot) {
|
|
if (!bot.entity) return;
|
|
|
|
bot._msgCount++;
|
|
totalChats++;
|
|
|
|
let message;
|
|
const roll = Math.random();
|
|
|
|
if (roll < 0.08 && bot._noResp >= 2) {
|
|
message = pick(STATIC.filter(s => s.startsWith('bug_log')));
|
|
} else if (Math.random() < MULTILINGUAL_RATE) {
|
|
message = pick(MULTILINGUAL);
|
|
} else if (Math.random() < DOLPHIN_RATE && dolphinPool.length > 0) {
|
|
message = ensurePrefix(dolphinPool.splice(Math.floor(Math.random() * dolphinPool.length), 1)[0]);
|
|
} else if (Math.random() < 0.4) {
|
|
// 40% contextual prayers based on actual bot state
|
|
message = getContextualPrayer(bot);
|
|
} else {
|
|
message = getPrompt();
|
|
bot._noResp++;
|
|
}
|
|
|
|
console.log(`[${ts()}] [${bot._name}] #${bot._msgCount}: ${message.substring(0, 60)}`);
|
|
bot.chat(message);
|
|
|
|
// Next interaction in 20-60s (slower than before — Haiku is fast but we want quality over quantity)
|
|
setTimeout(() => interact(bot), delay(20, 60));
|
|
}
|
|
|
|
// Pre-fill pools
|
|
geminiRefill();
|
|
dolphinRefill();
|
|
|
|
// Stagger spawns (5s apart to avoid throttle)
|
|
for (let i = 0; i < count; i++) {
|
|
setTimeout(() => spawnBot(i), i * 5000);
|
|
}
|
|
|
|
// Refill pools periodically
|
|
setInterval(() => { if (promptPool.length < 15) geminiRefill(); }, 45000);
|
|
setInterval(() => { if (dolphinPool.length < 5) dolphinRefill(); }, 120000);
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGINT', () => {
|
|
console.log(`\n[${ts()}] Shutting down ${bots.length} bots...`);
|
|
bots.forEach(b => { try { b.quit(); } catch(e) {} });
|
|
setTimeout(() => process.exit(0), 2000);
|
|
});
|
|
|
|
console.log(`[${ts()}] Spawning ${count} survival bots on ${host}:${port}`);
|
|
console.log(`[${ts()}] Names: ${Array.from({length: Math.min(count, 5)}, (_, i) => generateName(i)).join(', ')}...`);
|
|
console.log(`[${ts()}] All bots wander, fight, die, respawn, and pray`);
|
|
console.log(`[${ts()}] Gemini + Dolphin(${DOLPHIN_RATE*100}%) + Multilingual(${MULTILINGUAL_RATE*100}%)`);
|