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'); // === SQLite setup === const dbPath = path.join(__dirname, '../db/memory.db'); const db = new sqlite3.Database(dbPath, (err) => { if (err) { console.error('Failed to connect to database:', err); } else { console.log('Connected to SQLite database.'); 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 )`); } }); // === AI Integration (Ollama) === const OLLAMA_URL = 'http://192.168.1.3:11434/api/generate'; const MODEL_NAME = 'gemma3'; // === Create the bot === const bot = mineflayer.createBot({ host: '192.168.1.90', port: 25565, username: 'Cletus', version: '1.20.4', auth: 'offline' }); bot.loadPlugin(pathfinder); bot.on('spawn', () => { console.log('Bot has spawned!'); const defaultMove = new Movements(bot); bot.pathfinder.setMovements(defaultMove); // Passive background wandering when idle setInterval(() => { if (!bot.pathfinder.isMoving() && !bot.targetDigBlock) { const pos = bot.entity.position.offset( Math.floor(Math.random() * 10 - 5), 0, Math.floor(Math.random() * 10 - 5) ); bot.chat("Ugh, wandering again. This server is so boring..."); bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1)); } }, 60000); // every 60 seconds }); async function followPlayer(username) { const target = bot.players[username]?.entity; if (target) { bot.chat(`Ugh, fine. Following ${username}.`); bot.pathfinder.setGoal(new GoalFollow(target, 1), true); db.run(`INSERT INTO tasks (action, parameters, context, outcome, score) VALUES (?, ?, ?, ?, ?)`, ['follow', JSON.stringify({ target: username }), 'Follow player', 'started', 0]); } else { bot.chat("Seriously? I can't even see you."); db.run(`INSERT INTO tasks (action, parameters, context, outcome, score) VALUES (?, ?, ?, ?, ?)`, ['follow', JSON.stringify({ target: username }), 'Follow player', 'target not found', -1]); } } async function exploreArea() { const pos = bot.entity.position.offset( Math.floor(Math.random() * 20 - 10), 0, Math.floor(Math.random() * 20 - 10) ); bot.chat("Exploring... because why not."); bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1)); db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`, ['explore', `Random target: ${pos}`, 'started', 0]); } async function digNearestWood() { const logBlock = bot.findBlock({ matching: block => block.name.includes("log"), maxDistance: 16 }); if (!logBlock) { bot.chat("Wow, no trees? Shocking."); db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`, ['chop_tree', 'No logs nearby', 'failed', -1]); return; } try { bot.chat("Here we go again, chopping a tree."); await bot.dig(logBlock); db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`, ['chop_tree', `Block: ${logBlock.name}`, 'success', 1]); } catch (e) { bot.chat("Can't even chop right now. Figures."); db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`, ['chop_tree', 'Dig error', 'failed', -1]); } } async function handleAICommand(text, username) { const lowered = text.toLowerCase(); if (lowered.includes("follow")) { return followPlayer(username); } if (lowered.includes("explore")) { return exploreArea(); } if (lowered.includes("chop") && lowered.includes("tree")) { return digNearestWood(); } if (lowered.includes("dig") && lowered.includes("dirt")) { const block = bot.blockAt(bot.entity.position.offset(0, -1, 0)); if (block && bot.canDigBlock(block)) { bot.chat("Fine. Digging dirt. Happy now?"); await bot.dig(block); db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`, ['dig_dirt', `Block below: ${block.name}`, 'success', 1]); } else { bot.chat("Wow, I can't even dig here."); db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`, ['dig_dirt', 'nothing to dig', 'fail', -1]); } return; } bot.chat(text); // Fallback } bot.on('chat', async (username, message) => { if (username === bot.username) return; console.log(`${username}: ${message}`); const prompt = ` You are Cletus, a sarcastic teenage Minecraft bot. The player said: "${message}" Determine: 1. If it's small talk, reply sarcastically. 2. If it's a command (dig, mine, follow, chop, build), return an actionable command. Format your answer like this: TYPE: chat - [text to say] OR TYPE: action - [command to execute] `; try { const response = await axios.post(OLLAMA_URL, { model: MODEL_NAME, prompt, stream: false }); const aiReply = response.data?.response?.trim(); if (aiReply.startsWith("TYPE: chat")) { const msg = aiReply.split(" - ")[1]; bot.chat(msg); } else if (aiReply.startsWith("TYPE: action")) { const cmd = aiReply.split(" - ")[1]; await handleAICommand(cmd, username); } else { bot.chat("I'm confused. You confuse me."); } } catch (err) { console.error('AI call failed:', err.message); } }); 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.'));