const logSurroundings = require('./lib/log-surroundings'); const memory = require('./lib/memory'); const { chatWithPersona } = require('./lib/ai-helper'); const mineflayer = require('mineflayer'); const { pathfinder, Movements, goals: { GoalNear } } = require('mineflayer-pathfinder'); const fs = require('fs'); const path = require('path'); const sqlite3 = require('sqlite3').verbose(); const axios = require('axios'); // === AI Integration (Ollama) === const OLLAMA_URL = 'http://192.168.1.3:11434/api/generate'; const MODEL_NAME = 'gemma3'; const BOT_NAME = 'Cletus'; const TASK_DIR = path.join(__dirname, 'bot-tasks'); const DB_PATH = path.join(__dirname, 'db', 'memory.db'); // === Bot Parameters === const MC_HOST = 'www.thebytes.net'; const MC_PORT = 25565; const MC_AUTH = 'offline'; const MC_VER = '1.20.4'; let currentTask = null; // === Prompts === const REFINE_PROMPT = ` Only provide code, do not explain the code or provide a response that isn’t part of the code. As I refine my AI mineflayer bot, I need to revise the logic for the bot’s action of `; // === Prompt Builder for Responses === //const PB_PERSONA = `a cranky teenager with a sense of dark humor and sarcasm. you have short responses and don't repeat yourself.`; const PB_PERSONA = `a minecraft player. you have short responses and don't repeat yourself.`; const PB_RESPONSE_LENGTH = 20; const STANDARD_PROMPT = `You are a minecraft bot named ${BOT_NAME}. ` + `You don't know that you're a bot though. Instead, you think you are ${PB_PERSONA}. ` + `Keep your responses around ${PB_RESPONSE_LENGTH}. Respond to this: `; // === DB Setup === const db = new sqlite3.Database(DB_PATH, (err) => { if (err) { console.error('DB Error:', err); return; } console.log('Connected to DB'); db.run(` CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, action TEXT, parameters TEXT, context TEXT, outcome TEXT, score INTEGER DEFAULT 0 ) `, err => { if (err) console.error('Error creating tasks table:', err); }); db.run(` CREATE TABLE IF NOT EXISTS memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT UNIQUE, data TEXT ) `, err => { if (err) console.error('Error creating tasks table:', err); }); }); // ==== Bot Task Management and Review ============================================================================== // === Dynamic Task Loader === function getAvailableTasks() { const files = fs.readdirSync(TASK_DIR).filter(f => f.endsWith('.js')); const tasks = {}; for (const file of files) { const taskName = file.replace('.js', ''); tasks[taskName] = require(path.join(TASK_DIR, file)); } return tasks; } // === AI Rewrite Trigger === async function requestTaskRewrite(action, scriptText) { const prompt = `${REFINE_PROMPT}${action}. ${scriptText} `.trim(); try { const response = await axios.post(OLLAMA_URL, { model: MODEL_NAME, prompt, stream: false }); const code = response.data.response; const taskPath = path.join(TASK_DIR, `${action}.js`); fs.writeFileSync(taskPath, code, 'utf8'); console.log(`Rewrote task script for ${action}`); } catch (err) { console.error(`AI rewrite failed for ${action}:`, err.message); } } // ==== Helper Functions ========================================================================================== const personaOptions = { botName: BOT_NAME, persona: PB_PERSONA, length: PB_RESPONSE_LENGTH, ollamaUrl: OLLAMA_URL, model: MODEL_NAME }; let lastSpokenTime = 0; function sayWithPersona(message) { const now = Date.now(); if (now - lastSpokenTime < 4000) return; // 4s cooldown lastSpokenTime = now; chatWithPersona(message, personaOptions).then(response => bot.chat(response)); } function startWandering() { const wanderInterval = 10000 + Math.random() * 5000; // 10–15 seconds setTimeout(() => { if (!bot.pathfinder.isMoving()) { const dx = Math.floor(Math.random() * 10 - 5); const dy = Math.floor(Math.random() * 4 - 2); // small vertical variance const dz = Math.floor(Math.random() * 10 - 5); const dest = bot.entity.position.offset(dx, dy, dz); if (Math.floor(Math.random() * 10) + 1 === 5) { sayWithPersona("you are wandering"); } bot.pathfinder.setGoal(new GoalNear(dest.x, dest.y, dest.z, 1)); } if (!bot.allowDestruction) { bot.stopDigging(); // if somehow started } // Recurse to keep wandering startWandering(); }, wanderInterval); } // ==== Bot Creation and Automation =============================================================================== // === Bot Creation === const bot = mineflayer.createBot({ host: MC_HOST, port: MC_PORT, username: BOT_NAME, auth: MC_AUTH, version: MC_VER, }); bot.loadPlugin(pathfinder); bot.allowDestruction = false; // === Bot Spawned === bot.on('spawn', () => { console.log(`${BOT_NAME} spawned.`); if (isRecoveringItems && lastDeathLocation) { sayWithPersona("you just respawned from dying, and you are going to try and get your stuff back. "); // Set goal to walk back to where Cletus died const goal = new GoalNear(lastDeathLocation.x, lastDeathLocation.y, lastDeathLocation.z, 2); bot.pathfinder.setGoal(goal); const recoverTimeout = setTimeout(() => { if (bot.entity.position.distanceTo(lastDeathLocation) <= 3) { sayWithPersona("after dying, you actually found your stuff. "); isRecoveringItems = false; recoveryFails = 0; } else { sayWithPersona("after respawning and looking for your dropped items, you can't find the stuff. "); recoveryFails++; if (recoveryFails >= 2) { sayWithPersona("after respawning multiple times in attempt to locate your dropped items from death, you give up. "); isRecoveringItems = false; lastDeathLocation = null; bot.pathfinder.setGoal(null); // stop movement on failure } } clearTimeout(recoverTimeout); }, 15000); // Try for 15 seconds } const defaultMove = new Movements(bot); bot.pathfinder.setMovements(defaultMove); // Passive idle wandering startWandering(); }); // === Bot Chat Listener === bot.on('chat', async (username, message) => { if (username !== BOT_NAME) { bot.lastChatMessage = message; const messageLower = message.toLowerCase(); const tasks = getAvailableTasks(); if (messageLower.includes('stop that') || messageLower.includes("don't do that")) { if (currentTask) { sayWithPersona("you have been asked to stop doing " + `${currentTask}.`); // Log negative feedback db.get(`SELECT score FROM tasks WHERE action = ? ORDER BY timestamp DESC LIMIT 1`, [currentTask], (err, row) => { let score = row?.score ?? 0; score = Math.min(5, score + 1); db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'interrupted', ?)`, [currentTask, score]); if (score >= 5) { const fs = require('fs'); const path = require('path'); const script = fs.readFileSync(path.join(TASK_DIR, `${currentTask}.js`), 'utf8'); requestTaskRewrite(currentTask, script); } }); // Cancel current goal (pathfinder) bot.pathfinder.setGoal(null); currentTask = null; return; } else { sayWithPersona("you've been asked to stop doing whatever you are doing."); return; } } console.log(`[CHAT] ${username}: ${messageLower}`); console.log(`[TASK FILES LOADED]: ${Object.keys(tasks).join(', ')}`); const matchedTask = Object.keys(tasks).find(taskName => messageLower.includes(taskName.replace(/-/g, ' ')) ); if (matchedTask) { console.log(`[TASK MATCHED]: ${matchedTask}`); sayWithPersona("you were asked to " + `${matchedTask.replace(/-/g, ' ')}.`); try { await tasks[matchedTask](bot, db, sayWithPersona); db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'success', 0)`, [matchedTask]); } catch (err) { console.error(`Task ${matchedTask} failed:`, err.message); sayWithPersona("you failed at the task of " + `${matchedTask}`); } } else { console.log(`[NO MATCH]: Responding only.`); sayWithPersona(message); } } }); // === Lifecycle & Error Events === bot.on('error', (err) => console.error('Bot error:', err)); bot.on('kicked', (reason) => console.log('Bot was kicked:', reason)); bot.on('end', () => console.log('Bot has disconnected.')); // === Reaction to Death === let lastDeathLocation = null; let isRecoveringItems = false; let recoveryFails = 0; bot.on('death', () => { console.log('Cletus died.'); let times = recoveryFails == 0 ? "." : " again."; sayWithPersona("you just died" + `${times}`); // Save death location lastDeathLocation = bot.entity.position.clone(); isRecoveringItems = true; recoveryFails = 0; }); // === Reaction to Being Attacked === bot.on('entityHurt', (entity) => { if (entity === bot.entity) { sayWithPersona("you got hurt."); } }); // Optional: log attacker bot.on('entitySwingArm', (entity) => { if (entity.position.distanceTo(bot.entity.position) < 3) { const attacker = entity.username || entity.name; sayWithPersona(`${attacker}` + "has just hit you."); } });