Swarm bots, RCON validation, Haiku distillation complete
Swarm bots (ingame/swarm_bots.js): - 10 survival bots with generated names (SwiftWolf, DarkWolf, etc.) - All bots wander, take damage, auto-respawn, pray when hurt - Gemini + Dolphin(5%) + Multilingual(3%) prompt generation - 20-60s interaction interval per bot Distillation results: - 222 sudo examples via Haiku ($0.28) - 122 god examples via Haiku ($0.37) — with God Soul personality - Total: 344 distilled, $0.65 spent of $5 budget - RCON validation: 74.7% fully valid, 30 real errors out of ~1000 commands validate_distilled.py: - Executes distilled commands on live server via RCON - Distinguishes real errors from benign (no player online) - Tags each example with validation status Dev server switched to Claude Haiku via Anthropic API: - llm_provider: anthropic with $5 budget cap - Auto-fallback to Ollama when budget exhausted - Cost tracking with logging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* 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 player chat messages. Mix "pray" and "sudo" commands.
|
||||
Include: humble prayers, greedy demands, roleplay, questions, typos, slang, edge cases.
|
||||
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 getPrompt() {
|
||||
if (promptPool.length < 5) geminiRefill();
|
||||
if (dolphinPool.length < 3) dolphinRefill();
|
||||
return promptPool.length > 0
|
||||
? promptPool.splice(Math.floor(Math.random() * promptPool.length), 1)[0]
|
||||
: pick(STATIC);
|
||||
}
|
||||
|
||||
// --- 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 {
|
||||
// Random walk: pick a direction and walk for a bit
|
||||
const yaw = Math.random() * Math.PI * 2;
|
||||
bot.look(yaw, 0);
|
||||
|
||||
// Walk forward
|
||||
bot.setControlState('forward', true);
|
||||
setTimeout(() => {
|
||||
bot.setControlState('forward', false);
|
||||
|
||||
// Occasionally jump
|
||||
if (Math.random() < 0.3) {
|
||||
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 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 = dolphinPool.splice(Math.floor(Math.random() * dolphinPool.length), 1)[0];
|
||||
} 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}%)`);
|
||||
Reference in New Issue
Block a user