diff --git a/README.md b/README.md index 3798135..a8de2ac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,108 @@ # cletus -Cletus is a Java Edition Minecraft bot built with AI and Mineflayer. The goal is to have Cletus become somewhat autonomous. \ No newline at end of file +Cletus is a Java Edition Minecraft bot built with AI and Mineflayer. The goal is to have Cletus become somewhat autonomous. + +# Cletus Bot – A Learning Minecraft AI + +Cletus is a modular Mineflayer-based Minecraft bot designed to observe, learn, adapt, and evolve through tasks, memory, and AI interaction. It reads signs, listens to players, remembers context, and uses a local AI model to improve over time. + +--- + +## Features +- **State-driven AI**: Uses a dynamic state machine (`/states`) to handle tasks like `Observe`, `ReadFromSign`, `Idle`, `HandleChat`, etc. +- **Memory system**: Persists events, chats, locations, and signs in SQLite via `/memory` +- **Safe zones**: Prevents accidental destruction near homes, beds, bases, and more +- **AI integration**: Uses [Ollama](https://ollama.ai) with `gemma3` or any LLM to chat and rewrite failed scripts +- **Self-growing**: Failed task scripts are auto-sent to AI for improvement and saved + +--- + +## Folder Structure +```txt +├── bot.js # Bot entrypoint +├── config.json # Centralized bot and AI config +├── core/ +│ ├── state-machine.js # Loads and manages dynamic states +│ └── context.js # Shared bot context for state/tasks +├── states/ # Modular AI states (Observe, Idle, etc.) +├── bot-tasks/ # AI or manually written task modules +├── memory/ # Memory modules (chat, signs, events, etc.) +├── lib/ +│ ├── ai-helper.js # AI chat integration +│ └── utils.js # (Optional helper utils) +├── db/ +│ └── memory.db # SQLite database (auto-created) +``` + +--- + +## Config (`config.json`) +```json +{ + "bot": { + "name": "Cletus", + "host": "192.168.1.90", + "port": 25565, + "auth": "offline", + "version": "1.20.4" + }, + "ai": { + "model": "gemma3", + "ollamaUrl": "http://localhost:11434/api/generate", + "persona": "a curious minecraft explorer who learns from the world around them", + "responseLength": 30 + }, + "safeZone": { + "xBound": 50, + "yBound": 30, + "zBound": 50, + "keywords": ["home", "bed", "base", "village", "farm"] + } +} +``` + +--- + +## 🛠 Prerequisites +- [Node.js](https://nodejs.org/) (v16+) +- [Ollama](https://ollama.ai) running locally with `gemma3` or compatible model +- Minecraft server (1.20.4) with bot permissions + +--- + +## Running the Bot +```bash +npm install +node bot.js +``` + +Then in Minecraft: +``` +Say: Cletus, go find a flower +``` + +--- + +## Extending Cletus +- Add new tasks to `/bot-tasks/` +- Add new states to `/states/` +- Memory system will auto-track chat, signs, events +- AI auto-rewrites broken tasks using `chatWithAI()` + +--- + +## AI Improvement Loop +1. You run a task +2. If it fails → AI rewrites the file +3. File is overwritten and reused on next run + +--- + +## Example Tasks +- `find-flower` +- `observe` +- `read-from-sign` +- `idle` + +> Want Cletus to do more? Just ask him in chat. + diff --git a/bot/bot-tasks/TaskTemplate.js b/bot/bot-tasks/TaskTemplate.js new file mode 100644 index 0000000..4995110 --- /dev/null +++ b/bot/bot-tasks/TaskTemplate.js @@ -0,0 +1,55 @@ +// bot-tasks/TaskTemplate.js +const { getBot, getAllowDestruction, setAllowDestruction, getStateMachine } = require('../core/context'); +const db = require('../db'); +const path = require('path'); +const fs = require('fs'); +const { chatWithAI } = require('../lib/ai-helper'); +const config = require('../config.json'); + +// Optional: name of the task (same as filename) +const taskName = 'task-template'; + +module.exports = async function TaskTemplate() { + const bot = getBot(); + + console.log(`[TASK] Starting ${taskName}`); + setAllowDestruction(true); // 🔓 allow digging/building temporarily + + try { + // Your logic here — Example: walk to a random spot + const dx = Math.floor(Math.random() * 10 - 5); + const dz = Math.floor(Math.random() * 10 - 5); + const target = bot.entity.position.offset(dx, 0, dz); + + const { GoalNear } = require('mineflayer-pathfinder').goals; + bot.pathfinder.setGoal(new GoalNear(target.x, target.y, target.z, 1)); + + bot.chat(`I'm working on ${taskName}`); + + // Simulate a completed task + db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'success', 0)`, [taskName]); + + } catch (err) { + console.error(`[TASK] ${taskName} failed:`, err.message); + bot.chat(`Task ${taskName} failed.`); + + // Log failure to DB + db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'failed', -1)`, [taskName]); + + // Optional: Trigger rewrite if consistently failing + const scriptPath = path.join(__dirname, `${taskName}.js`); + const original = fs.readFileSync(scriptPath, 'utf8'); + const prompt = `This script failed:\n${original}\nImprove it to avoid failure. Only return code.`; + + const response = await chatWithAI(prompt, config.ai); + fs.writeFileSync(scriptPath, response); + console.log(`[AI] Rewrote task: ${taskName}`); + } finally { + setAllowDestruction(false); // 🔒 disable again for safety + + // Return to idle after completion + setTimeout(() => { + getStateMachine().transition('Idle'); + }, 10000); + } +}; diff --git a/bot/bot-tasks/craft-items.js b/bot/bot-tasks/craft-items.js deleted file mode 100644 index 61078a3..0000000 --- a/bot/bot-tasks/craft-items.js +++ /dev/null @@ -1,32 +0,0 @@ -// bot-tasks/craft-item.js -module.exports = async function craftItem(bot, db, sayWithPersona) { - - if (!bot.allowDestruction) { - sayWithPersona("i'm not supposed to break things unless you tell me to."); - return; - } - - try { - const itemName = 'planks'; // Placeholder. Later, parse this from command/context. - const count = 4; - - const item = bot.registry.itemsByName[itemName]; - if (!item) { - sayWithPersona(`you tried to craft ${itemName}, but it doesn't even exist.`); - return; - } - - const recipe = bot.recipesFor(item.id, null, 1)[0]; - if (!recipe) { - sayWithPersona(`you don't know how to make ${itemName}. learn it first.`); - return; - } - - sayWithPersona(`you're crafting ${count} ${itemName}. hope you're happy.`); - await bot.craft(recipe, count, null); - } catch (err) { - console.error("craft-item.js failed:", err); - sayWithPersona("you tried to craft something and screwed it up."); - } - }; - \ No newline at end of file diff --git a/bot/bot-tasks/eat-food.js b/bot/bot-tasks/eat-food.js deleted file mode 100644 index bc5e34b..0000000 --- a/bot/bot-tasks/eat-food.js +++ /dev/null @@ -1,57 +0,0 @@ -const { GoalNear } = require('mineflayer-pathfinder').goals; -const memory = require('../lib/memory'); - -module.exports = async function eatFood(bot, db, sayWithPersona) { - try { - const foodItem = bot.inventory.items().find(i => - i.name.includes('bread') || i.name.includes('apple') || i.name.includes('carrot') - ); - - if (foodItem) { - await bot.equip(foodItem, 'hand'); - await bot.consume(); - sayWithPersona("you were hungry, so you ate something. yum."); - return; - } - - sayWithPersona("you're starving and have no food. checking a chest..."); - - memory.getMemory(db, 'food-chest', async (err, pos) => { - if (err || !pos) { - sayWithPersona("you have no idea where food is. you're doomed."); - return; - } - - bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1)); - - const checkChest = setTimeout(async () => { - try { - const chestBlock = bot.blockAt(bot.entity.position.offset(1, 0, 0)); - const chest = await bot.openChest(chestBlock); - const food = chest.containerItems().find(item => - item.name.includes('bread') || item.name.includes('apple') - ); - - if (food) { - await chest.withdraw(food.type, null, 1); - sayWithPersona("you snagged food from the chest. eating now..."); - await bot.equip(food, 'hand'); - await bot.consume(); - } else { - sayWithPersona("you checked the food chest, but it's empty. awesome."); - } - - chest.close(); - clearTimeout(checkChest); - } catch (e) { - sayWithPersona("you fumbled the chest like a fool."); - console.error("eat-food chest error:", e); - } - }, 4000); - }); - - } catch (err) { - console.error("eat-food.js failed:", err); - sayWithPersona("you tried to eat but failed... somehow."); - } -}; diff --git a/bot/bot-tasks/find-flower.js b/bot/bot-tasks/find-flower.js new file mode 100644 index 0000000..4d02825 --- /dev/null +++ b/bot/bot-tasks/find-flower.js @@ -0,0 +1,54 @@ +const { getBot, getStateMachine, setAllowDestruction } = require('../core/context'); +const db = require('../db'); +const path = require('path'); +const fs = require('fs'); +const { chatWithAI } = require('../lib/ai-helper'); +const config = require('../config.json'); + +const taskName = 'find-flower'; + +module.exports = async function FindFlower() { + const bot = getBot(); + console.log(`[TASK] Starting ${taskName}`); + setAllowDestruction(false); + + try { + const flowerNames = [ + 'dandelion', 'poppy', 'blue_orchid', 'allium', + 'azure_bluet', 'red_tulip', 'orange_tulip', 'white_tulip', + 'pink_tulip', 'oxeye_daisy', 'cornflower', 'lily_of_the_valley' + ]; + + const flower = bot.findBlock({ + matching: block => flowerNames.includes(block.name), + maxDistance: 32 + }); + + if (!flower) { + bot.chat("I couldn't find a flower nearby."); + db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'not-found', 0)`, [taskName]); + return; + } + + const { GoalNear } = require('mineflayer-pathfinder').goals; + bot.pathfinder.setGoal(new GoalNear(flower.position.x, flower.position.y, flower.position.z, 1)); + + bot.chat("Heading to a flower..."); + db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'success', 1)`, [taskName]); + + } catch (err) { + console.error(`[TASK] ${taskName} failed:`, err.message); + bot.chat(`Task ${taskName} failed.`); + db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'failed', -1)`, [taskName]); + + const scriptPath = path.join(__dirname, `${taskName}.js`); + const original = fs.readFileSync(scriptPath, 'utf8'); + const prompt = `This script called '${taskName}' failed. Please improve it:\n\n${original}`; + const rewritten = await chatWithAI(prompt, config.ai); + fs.writeFileSync(scriptPath, rewritten); + } finally { + setTimeout(() => { + getStateMachine().transition('Idle'); + }, 10000); + } +}; diff --git a/bot/bot-tasks/find-materials.js b/bot/bot-tasks/find-materials.js deleted file mode 100644 index d28542b..0000000 --- a/bot/bot-tasks/find-materials.js +++ /dev/null @@ -1,44 +0,0 @@ -// bot-tasks/find-materials.js -const { GoalBlock } = require('mineflayer-pathfinder').goals; - -console.log('[TASK] find-materials.js loaded'); - -module.exports = async function findMaterials(bot, db, sayWithPersona) { - - console.log('[TASK EXECUTION] find-materials running'); - sayWithPersona("you asked me to find materials. I'm trying..."); - - if (!bot.allowDestruction) { - sayWithPersona("i'm not supposed to break things unless you tell me to."); - return; - } - - try { - // For now, hardcoded material search — will eventually be inferred from task/command - const keywords = ['log', 'coal', 'iron_ore', 'tree', 'water', 'wood', 'potato', 'carrot']; // Expandable list - const targetBlock = bot.findBlock({ - matching: block => keywords.some(k => block.name.includes(k)), - maxDistance: 32 - }); - - if (!targetBlock) { - sayWithPersona("you looked around and couldn't find any good materials nearby."); - return; - } - - sayWithPersona(`you spotted some ${targetBlock.name}. heading there now.`); - bot.pathfinder.setGoal(new GoalBlock(targetBlock.position.x, targetBlock.position.y, targetBlock.position.z)); - - // Optional: add memory logging - db.run( - `INSERT OR IGNORE INTO memory (label, data) VALUES (?, ?)`, - [`found-${targetBlock.name}`, JSON.stringify(targetBlock.position)], - (err) => { - if (err) console.error("DB insert failed in find-materials:", err); - } - ); - } catch (err) { - console.error("find-materials.js failed:", err); - sayWithPersona("you tried to find materials but got lost in thought."); - } -}; diff --git a/bot/bot-tasks/follow-me.js b/bot/bot-tasks/follow-me.js deleted file mode 100644 index 4487b2b..0000000 --- a/bot/bot-tasks/follow-me.js +++ /dev/null @@ -1,40 +0,0 @@ -const { GoalFollow } = require('mineflayer-pathfinder').goals; - -console.log('[TASK] follow-me.js loaded'); - -module.exports = async function followMe(bot, db, sayWithPersona) { - try { - const lastChat = bot.lastChatMessage?.toLowerCase() ?? ''; - - // Check if the user wants to stop following - if (lastChat.includes('stop') || lastChat.includes('leave me alone') || lastChat.includes('unfollow')) { - bot.pathfinder.setGoal(null); - sayWithPersona("fine. i'll stop following you. whatever."); - return; - } - - const playerUsername = bot.nearestEntity(entity => - entity.type === 'player' && entity.username !== bot.username - )?.username; - - if (!playerUsername) { - sayWithPersona("you want me to follow you, but you're not even here. genius."); - return; - } - - const playerEntity = bot.players[playerUsername]?.entity; - if (!playerEntity) { - sayWithPersona("I can't see you. Are you invisible or just lagging?"); - return; - } - - sayWithPersona(`ugh. fine. following ${playerUsername} around like a lost puppy.`); - - const goal = new GoalFollow(playerEntity, 1); - bot.pathfinder.setGoal(goal, true); - - } catch (err) { - console.error("follow-me.js failed:", err); - sayWithPersona("you told me to follow, but I forgot how legs work."); - } -}; diff --git a/bot/bot-tasks/go-to-bed.js b/bot/bot-tasks/go-to-bed.js deleted file mode 100644 index ef41e9f..0000000 --- a/bot/bot-tasks/go-to-bed.js +++ /dev/null @@ -1,42 +0,0 @@ -const { GoalNear } = require('mineflayer-pathfinder').goals; -const memory = require('../lib/memory'); - -module.exports = async function goToBed(bot, db, sayWithPersona) { - try { - const hour = bot.time.timeOfDay; - if (hour < 13000 || hour > 23999) { - sayWithPersona("someone asked you to sleep, but it's not night."); - return; - } - - memory.getMemory(db, 'bed', (err, pos) => { - if (err || !pos) { - sayWithPersona("you tried to sleep but don't remember where your bed is."); - return; - } - - sayWithPersona("it's nighttime. you're heading to your bed."); - bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1)); - - const sleepCheck = setInterval(() => { - const bed = bot.findBlock({ - matching: block => block.name.includes('bed'), - maxDistance: 4 - }); - if (bed) { - bot.sleep(bed).then(() => { - sayWithPersona("ugh, finally sleeping."); - clearInterval(sleepCheck); - }).catch(() => { - sayWithPersona("you found your bed but couldn't sleep. tragic."); - clearInterval(sleepCheck); - }); - } - }, 2000); - }); - - } catch (err) { - console.error("go-to-bed.js failed:", err); - sayWithPersona("something broke while trying to sleep."); - } -}; diff --git a/bot/bot-tasks/remember-signs.js b/bot/bot-tasks/remember-signs.js deleted file mode 100644 index c3b7f92..0000000 --- a/bot/bot-tasks/remember-signs.js +++ /dev/null @@ -1,40 +0,0 @@ -const memory = require('../lib/memory'); - -module.exports = async function rememberSigns(bot, db, sayWithPersona) { - try { - const signs = bot.findBlocks({ - matching: block => block.name.includes('sign'), - maxDistance: 16, - count: 10 - }); - - for (const pos of signs) { - const block = bot.blockAt(pos); - - // In some Mineflayer versions, use .getSignText(), otherwise use .signText or .getBlockEntityData() - const signText = block.getSignText?.() || block.signText || null; - - if (signText && signText.trim()) { - const label = signText.trim(); - const data = { - x: pos.x, - y: pos.y, - z: pos.z - }; - - memory.saveMemory(db, label, data, (err) => { - if (!err) { - sayWithPersona(`you memorized a place labeled '${label}'.`); - } else { - console.error("Failed to remember sign label:", err); - sayWithPersona("you saw a sign but forgot it instantly. classic."); - } - }); - } - } - - } catch (err) { - console.error("remember-signs.js failed:", err); - sayWithPersona("you tried to read signs and got confused."); - } -}; diff --git a/bot/bot.js b/bot/bot.js index 96c42c9..5f659fa 100644 --- a/bot/bot.js +++ b/bot/bot.js @@ -1,318 +1,89 @@ -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 pathfinder = require('mineflayer-pathfinder').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'); +const { getSafeZones, isInSafeZone } = require('./memory/locations'); +const db = require('./db'); -// === Bot Parameters === -const MC_HOST = 'www.thebytes.net'; -const MC_PORT = 25565; -const MC_AUTH = 'offline'; -const MC_VER = '1.20.4'; +// configuration file. +const config = require('./config.json'); -let currentTask = null; +const { StateMachine } = require('./core/state-machine'); +const context = require('./core/context'); -// === 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 === +// === Bot Config === const bot = mineflayer.createBot({ - host: MC_HOST, - port: MC_PORT, - username: BOT_NAME, - auth: MC_AUTH, - version: MC_VER, + host: config.bot.host, + port: config.bot.port, + username: config.bot.name, + auth: config.bot.auth, + version: config.bot.version, }); bot.loadPlugin(pathfinder); -bot.allowDestruction = false; +context.setBot(bot); +context.setDB(db); -// === Bot Spawned === -bot.on('spawn', () => { - console.log(`${BOT_NAME} spawned.`); +let allowDestruction = false; - 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 +context.setAllowDestruction = (value) => { allowDestruction = value; }; +context.getAllowDestruction = () => allowDestruction; + +const originalDig = bot.dig; + +bot.dig = async function (block, ...args) { + return new Promise((resolve, reject) => { + getSafeZones(db, async (err, zones) => { + if (err) { + console.warn('[SAFEZONE] Error loading zones:', err); + return reject(err); + } + + // Check override first + if (context.getAllowDestruction() === true) { + try { + const result = await originalDig.call(bot, block, ...args); + console.log(`[DIG-OVERRIDE] ${block.name} at ${block.position}`); + return resolve(result); + } catch (digErr) { + return reject(digErr); } } - 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; + // Block dig inside safe zone + if (isInSafeZone(block.position, zones)) { + bot.chat("You're not allowed to dig here — it's a protected zone."); + console.log(`[BLOCKED] Attempted to dig inside safe zone: ${block.name} at ${block.position}`); + return resolve(false); } - } - - 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, ' ')}.`); + // Normal dig 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}`); + const result = await originalDig.call(bot, block, ...args); + console.log(`[DIG] ${block.name} at ${block.position} (allowed)`); + return resolve(result); + } catch (digErr) { + return reject(digErr); } - } else { - console.log(`[NO MATCH]: Responding only.`); - sayWithPersona(message); - } - } + }); + }); +}; + +bot.once('spawn', async () => { + console.log(`${config.bot.name} has spawned.`); + const stateMachine = new StateMachine(path.join(__dirname, 'states')); + context.setStateMachine(stateMachine); + await stateMachine.run('Observe'); }); - - - - - - - -// === 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('chat', (username, message) => { + if (username !== bot.username) { + context.setLastChat({ username, message }); + context.getStateMachine().transition('HandleChat'); + } +}); 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."); - } -}); + context.getStateMachine().transition('ActOnMemory'); +}); \ No newline at end of file diff --git a/bot/bot_old.js b/bot/bot_old.js deleted file mode 100644 index 2de5d6f..0000000 --- a/bot/bot_old.js +++ /dev/null @@ -1,188 +0,0 @@ -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.')); \ No newline at end of file diff --git a/bot/config.json b/bot/config.json new file mode 100644 index 0000000..a2f7354 --- /dev/null +++ b/bot/config.json @@ -0,0 +1,22 @@ +{ + "bot": { + "name": "Cletus", + "host": "192.168.1.90", + "port": 25565, + "auth": "offline", + "version": "1.20.4" + }, + "ai": { + "model": "gemma3", + "ollamaUrl": "http://localhost:11434/api/generate", + "persona": "a curious minecraft explorer who learns from the world around them", + "responseLength": 30 + }, + "safeZone": { + "xBound": 50, + "yBound": 30, + "zBound": 50, + "keywords": ["home", "bed", "base", "village", "farm"] + } + } + \ No newline at end of file diff --git a/bot/core/context.js b/bot/core/context.js new file mode 100644 index 0000000..ebb38e0 --- /dev/null +++ b/bot/core/context.js @@ -0,0 +1,20 @@ +// core/context.js +let bot = null; +let stateMachine = null; +let lastChat = null; + +module.exports = { + setBot: (instance) => { bot = instance; }, + getBot: () => bot, + + setStateMachine: (machine) => { stateMachine = machine; }, + getStateMachine: () => stateMachine, + + setLastChat: (chat) => { lastChat = chat; }, + getLastChat: () => lastChat, + + getContextSnapshot: () => ({ + bot, + lastChat + }) +}; diff --git a/bot/core/state-machine.js b/bot/core/state-machine.js new file mode 100644 index 0000000..a97974d --- /dev/null +++ b/bot/core/state-machine.js @@ -0,0 +1,55 @@ +// core/state-machine.js +const fs = require('fs'); +const path = require('path'); + +class StateMachine { + constructor(statesPath) { + this.statesPath = statesPath; + this.states = new Map(); + this.currentState = null; + this.loadStates(); + } + + loadStates() { + const files = fs.readdirSync(this.statesPath); + for (const file of files) { + if (file.endsWith('.js')) { + const stateName = path.basename(file, '.js'); + const stateModule = require(path.join(this.statesPath, file)); + this.states.set(stateName, stateModule); + } + } + } + + async run(initialState) { + if (!this.states.has(initialState)) { + throw new Error(`Initial state '${initialState}' not found.`); + } + this.currentState = initialState; + await this.executeCurrent(); + } + + async transition(nextState) { + if (!this.states.has(nextState)) { + console.warn(`State '${nextState}' not found. Ignoring.`); + return; + } + this.currentState = nextState; + await this.executeCurrent(); + } + + async executeCurrent() { + const stateFn = this.states.get(this.currentState); + if (typeof stateFn !== 'function') { + console.warn(`State '${this.currentState}' is not executable.`); + return; + } + try { + await stateFn(); + } catch (err) { + console.error(`Error executing state '${this.currentState}':`, err); + } + } +} + +module.exports = { StateMachine }; diff --git a/bot/db.js b/bot/db.js new file mode 100644 index 0000000..37ce1e7 --- /dev/null +++ b/bot/db.js @@ -0,0 +1,35 @@ +// db.js +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); + +const db = new sqlite3.Database(path.join(__dirname, 'db', 'memory.db'), (err) => { + if (err) { + console.error('[DB] Connection error:', err.message); + } else { + console.log('[DB] Connected to SQLite database.'); + } +}); + +db.serialize(() => { + db.run(` + CREATE TABLE IF NOT EXISTS memory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT UNIQUE, + data TEXT + ) + `); + + 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 + ) + `); +}); + +module.exports = db; diff --git a/bot/lib/ai-helper.js b/bot/lib/ai-helper.js index 6c20102..b20a906 100644 --- a/bot/lib/ai-helper.js +++ b/bot/lib/ai-helper.js @@ -1,38 +1,23 @@ +// lib/ai-helper.js const axios = require('axios'); +const config = require('../config.json'); -/** - * Generates a persona-driven response from an LLM and returns the result. - * - * @param {string} message - The input prompt to complete. - * @param {object} options - Persona and LLM settings. - * @returns {Promise} - A trimmed response string. - */ -async function chatWithPersona(message, options) { - const { - botName = 'Cletus', - persona = `a cranky teenager with a sense of dark humor and sarcasm. you have short responses and don't repeat yourself.`, - length = 20, - ollamaUrl = 'http://192.168.1.3:11434/api/generate', - model = 'gemma3' - } = options; +async function chatWithAI(message, overrides = {}) { - const prompt = `You are a minecraft bot named ${botName}. ` + - `You don't know that you're a bot though. Instead, you are a ${persona}. ` + - `Keep your responses around ${length}. Respond to this: ${message}`; + const settings = { ...config.ai, ...overrides }; + const prompt = `You are ${settings.botName}, ${settings.persona}. Keep your responses around ${settings.responseLength} words. Respond to: ${message}`; try { - const response = await axios.post(ollamaUrl, { - model, + const response = await axios.post(settings.ollamaUrl, { + model: settings.model, prompt, stream: false }); - - const aiReply = response.data.response?.trim().replace(/^"|"$/g, ''); - return aiReply || "...What?"; + return response.data.response?.trim() || '...'; } catch (err) { - console.error("AI response failed:", err.message); - return "I'm too moody to respond right now."; + console.error('[AI] Request failed:', err.message); + return 'Im confused.'; } } -module.exports = { chatWithPersona }; \ No newline at end of file +module.exports = { chatWithAI }; diff --git a/bot/lib/log-surroundings.js b/bot/lib/log-surroundings.js index a0c7bf3..e69de29 100644 --- a/bot/lib/log-surroundings.js +++ b/bot/lib/log-surroundings.js @@ -1,39 +0,0 @@ -// lib/log-surroundings.js -const interestingBlocks = [ - 'coal_ore', 'iron_ore', 'diamond_ore', 'gold_ore', 'emerald_ore', - 'redstone_ore', 'lapis_ore', 'copper_ore', - 'oak_log', 'birch_log', 'spruce_log', - 'crafting_table', 'furnace', 'chest', 'anvil', - 'beacon', 'bell', 'lectern', 'enchanting_table', 'portal', - 'villager', 'spawner', 'campfire' - ]; - - module.exports = function logSurroundings(bot, db, sayWithPersona) { - try { - const blocks = bot.findBlocks({ - matching: block => interestingBlocks.includes(block.name), - maxDistance: 12, - count: 10 - }); - - for (const pos of blocks) { - const block = bot.blockAt(pos); - if (!block || !block.name) continue; - - const label = `found-${block.name}-${pos.x},${pos.y},${pos.z}`; - - db.run( - `INSERT OR IGNORE INTO memory (label, data) VALUES (?, ?)`, - [label, JSON.stringify(pos)], - (err) => { - if (err) { - console.error("DB insert failed in logSurroundings:", err); - } - } - ); - } - } catch (err) { - console.error("logSurroundings failed:", err); - sayWithPersona("your brain short-circuited while trying to remember stuff."); - } - }; \ No newline at end of file diff --git a/bot/lib/memory.js b/bot/lib/memory.js deleted file mode 100644 index a09dd20..0000000 --- a/bot/lib/memory.js +++ /dev/null @@ -1,53 +0,0 @@ -// lib/memory.js - -module.exports = { - saveMemory: function (db, label, data, callback = () => {}) { - db.run( - `INSERT OR REPLACE INTO memory (label, data) VALUES (?, ?)`, - [label, JSON.stringify(data)], - callback - ); - }, - - getMemory: function (db, label, callback) { - db.get( - `SELECT data FROM memory WHERE label = ?`, - [label], - (err, row) => { - if (err) return callback(err, null); - if (!row) return callback(null, null); - try { - callback(null, JSON.parse(row.data)); - } catch (e) { - callback(e, null); - } - } - ); - }, - - forgetMemory: function (db, label, callback = () => {}) { - db.run( - `DELETE FROM memory WHERE label = ?`, - [label], - callback - ); - }, - - listMemory: function (db, callback) { - db.all(`SELECT label, data FROM memory`, [], (err, rows) => { - if (err) return callback(err, null); - const parsed = rows.map(row => ({ - label: row.label, - data: (() => { - try { - return JSON.parse(row.data); - } catch { - return row.data; - } - })() - })); - callback(null, parsed); - }); - } -}; - \ No newline at end of file diff --git a/bot/lib/utils.js b/bot/lib/utils.js new file mode 100644 index 0000000..e69de29 diff --git a/bot/memory/chat.js b/bot/memory/chat.js new file mode 100644 index 0000000..c7fc485 --- /dev/null +++ b/bot/memory/chat.js @@ -0,0 +1,35 @@ +// memory/chat.js +const { saveMemory, getMemory, listMemory } = require('./index'); + +function logChatMessage(db, username, message) { + const label = `chat:${username.toLowerCase()}`; + + getMemory(db, label, (err, existing) => { + const history = Array.isArray(existing) ? existing : []; + history.push({ message, timestamp: Date.now() }); + + // Keep the latest 20 messages + const trimmed = history.slice(-20); + saveMemory(db, label, trimmed); + }); +} + +function getLastMessageFrom(db, username, callback) { + const label = `chat:${username.toLowerCase()}`; + getMemory(db, label, (err, history) => { + if (err || !history || history.length === 0) return callback(null, null); + callback(null, history[history.length - 1]); + }); +} + +function listKnownPlayers(db, callback) { + listMemory(db, (err, entries) => { + if (err) return callback(err, []); + const players = entries + .filter(e => e.label.startsWith('chat:')) + .map(e => e.label.replace('chat:', '')); + callback(null, players); + }); +} + +module.exports = { logChatMessage, getLastMessageFrom, listKnownPlayers }; \ No newline at end of file diff --git a/bot/memory/events.js b/bot/memory/events.js new file mode 100644 index 0000000..dd0041c --- /dev/null +++ b/bot/memory/events.js @@ -0,0 +1,31 @@ +// memory/events.js +const { saveMemory, getMemory, listMemory } = require('./index'); + +function logEvent(db, type, data) { + const label = `event:${type}`; + + getMemory(db, label, (err, existing) => { + const events = Array.isArray(existing) ? existing : []; + events.push({ ...data, timestamp: Date.now() }); + + const trimmed = events.slice(-50); // Keep last 50 events per type + saveMemory(db, label, trimmed); + }); +} + +function getRecentEvents(db, type, callback) { + const label = `event:${type}`; + getMemory(db, label, callback); +} + +function listEventTypes(db, callback) { + listMemory(db, (err, entries) => { + if (err) return callback(err, []); + const types = entries + .filter(e => e.label.startsWith('event:')) + .map(e => e.label.replace('event:', '')); + callback(null, types); + }); +} + +module.exports = { logEvent, getRecentEvents, listEventTypes }; \ No newline at end of file diff --git a/bot/memory/index.js b/bot/memory/index.js new file mode 100644 index 0000000..30208b1 --- /dev/null +++ b/bot/memory/index.js @@ -0,0 +1,54 @@ +// memory/index.js +const db = require('../db'); + +function saveMemory(db, label, data, callback = () => {}) { + db.run( + `INSERT OR REPLACE INTO memory (label, data) VALUES (?, ?)`, + [label, JSON.stringify(data)], + callback + ); +} + +function getMemory(db, label, callback) { + db.get( + `SELECT data FROM memory WHERE label = ?`, + [label], + (err, row) => { + if (err) return callback(err, null); + if (!row) return callback(null, null); + try { + callback(null, JSON.parse(row.data)); + } catch (e) { + callback(e, null); + } + } + ); +} + +function forgetMemory(db, label, callback = () => {}) { + db.run(`DELETE FROM memory WHERE label = ?`, [label], callback); +} + +function listMemory(db, callback) { + db.all(`SELECT label, data FROM memory`, [], (err, rows) => { + if (err) return callback(err, null); + const parsed = rows.map(row => ({ + label: row.label, + data: (() => { + try { + return JSON.parse(row.data); + } catch { + return row.data; + } + })() + })); + callback(null, parsed); + }); +} + +module.exports = { + saveMemory, + getMemory, + forgetMemory, + listMemory +}; \ No newline at end of file diff --git a/bot/memory/locations.js b/bot/memory/locations.js new file mode 100644 index 0000000..88010ff --- /dev/null +++ b/bot/memory/locations.js @@ -0,0 +1,59 @@ +// memory/locations.js +const { getMemory, saveMemory } = require('./index'); +const config = require('../config.json'); + +function setSafeZone(db, label, position, bounds = { x: config.safeZone.xBound, y: config.safeZone.yBound, z: config.safeZone.zBound }) { + saveMemory(db, `safezone:${label}`, { center: position, bounds }); +} + +function getSafeZones(db, callback) { + db.all(`SELECT label, data FROM memory WHERE label LIKE 'safezone:%'`, [], (err, rows) => { + if (err) return callback(err, []); + const zones = rows.map(row => ({ + label: row.label, + ...JSON.parse(row.data) + })); + callback(null, zones); + }); +} + +function getHomeZone(db, callback) { + getSafeZones(db, (err, zones) => { + if (err) return callback(err, null); + const home = zones.find(z => z.label === 'safezone:home'); + callback(null, home); + }); +} + +function isInSafeZone(pos, zones) { + return zones.some(zone => { + const { center, bounds } = zone; + return ( + Math.abs(pos.x - center.x) <= bounds.x && + Math.abs(pos.y - center.y) <= bounds.y && + Math.abs(pos.z - center.z) <= bounds.z + ); + }); +} + +module.exports = { + setSafeZone, + getSafeZones, + getHomeZone, + isInSafeZone + }; + + +/* + +use this is areas to determine a safeZone: + +const { getSafeZones, isInSafeZone } = require('../memory/locations'); + +getSafeZones(db, (err, zones) => { + if (isInSafeZone(bot.entity.position, zones)) { + bot.chat("You're inside a safe zone."); + } +}); + +*/ \ No newline at end of file diff --git a/bot/memory/signs.js b/bot/memory/signs.js new file mode 100644 index 0000000..4c7a121 --- /dev/null +++ b/bot/memory/signs.js @@ -0,0 +1,30 @@ +// memory/signs.js +const { saveMemory, getMemory, listMemory } = require('./index'); + +function logSign(db, text, position) { + const label = `sign:${text.toLowerCase().replace(/\s+/g, '_').slice(0, 50)}`; + + saveMemory(db, label, { + text, + position, + timestamp: Date.now() + }); +} + +function getAllSigns(db, callback) { + listMemory(db, (err, entries) => { + if (err) return callback(err, []); + const signs = entries.filter(e => e.label.startsWith('sign:')); + callback(null, signs); + }); +} + +function findSignContaining(db, keyword, callback) { + getAllSigns(db, (err, signs) => { + if (err) return callback(err, null); + const match = signs.find(s => s.data.text.toLowerCase().includes(keyword.toLowerCase())); + callback(null, match || null); + }); +} + +module.exports = { logSign, getAllSigns, findSignContaining }; \ No newline at end of file diff --git a/bot/states/ActOnMemory.js b/bot/states/ActOnMemory.js new file mode 100644 index 0000000..f4ed749 --- /dev/null +++ b/bot/states/ActOnMemory.js @@ -0,0 +1,14 @@ +// states/ActOnMemory.js +const { chatWithAI } = require('../lib/ai-helper'); +const { getBot } = require('../core/context'); +const config = require('../config.json'); + +module.exports = async function ActOnMemory() { + console.log('[STATE] ActOnMemory'); + + const prompt = 'You just died. Based on what you remember, what should you do next?'; + const response = await chatWithAI(prompt, config.ai); + + const bot = getBot(); + bot.chat(response); +}; diff --git a/bot/states/HandleChat.js b/bot/states/HandleChat.js new file mode 100644 index 0000000..90de8ff --- /dev/null +++ b/bot/states/HandleChat.js @@ -0,0 +1,40 @@ +// states/HandleChat.js +const path = require('path'); +const fs = require('fs'); +const { getLastChat, getBot, getDB } = require('../core/context'); +const { logChatMessage } = require('../memory/chat'); +const { chatWithAI } = require('../lib/ai-helper'); +const config = require('../config.json'); + +module.exports = async function HandleChat() { + const bot = getBot(); + const db = getDB(); + const { username, message } = getLastChat(); + const msg = message.toLowerCase(); + + console.log(`[STATE] HandleChat: ${message}`); + logChatMessage(db, username, message); + + // Load available task scripts from the task directory. + const taskDir = path.join(__dirname, '../bot-tasks'); + const availableTasks = fs.readdirSync(taskDir) + .filter(f => f.endsWith('.js')) + .map(f => f.replace('.js', '')); + + const matchedTask = availableTasks.find(task => msg.includes(task.replace(/-/g, ' '))); + + if (matchedTask) { + try { + const task = require(path.join(taskDir, matchedTask)); + bot.chat(`Okay, I’ll try to ${matchedTask.replace(/-/g, ' ')}.`); + await task(); + return; + } catch (err) { + console.error(`[TASK ERROR] ${matchedTask}:`, err.message); + bot.chat(`I couldn't complete the task "${matchedTask}".`); + } + } + + const response = await chatWithAI(message, config.ai); + bot.chat(response); +}; \ No newline at end of file diff --git a/bot/states/Idle.js b/bot/states/Idle.js new file mode 100644 index 0000000..d6ba3a4 --- /dev/null +++ b/bot/states/Idle.js @@ -0,0 +1,73 @@ +// states/Idle.js +const { getBot } = require('../core/context'); +const { GoalNear } = require('mineflayer-pathfinder').goals; +const { getHomeZone } = require('../memory/locations'); +const db = require('../db'); + +module.exports = async function Idle() { + const bot = getBot(); + console.log('[STATE] Idle'); + + getHomeZone(db, async (err, zone) => { + const fallbackCenter = { x: 100, y: 64, z: 100 }; + const fallbackBounds = { x: 20, y: 10, z: 20 }; + + const center = zone?.center || fallbackCenter; + const bounds = zone?.bounds || fallbackBounds; + const safeRadius = Math.min(bounds.x, bounds.z); + + const actionRoll = Math.floor(Math.random() * 3); + + if (actionRoll === 0) { + const grass = bot.findBlock({ + matching: block => block.name === 'tall_grass', + maxDistance: safeRadius + }); + + if (grass) { + await bot.pathfinder.setGoal(new GoalNear(grass.position.x, grass.position.y, grass.position.z, 1)); + try { + await bot.dig(grass); + bot.chat("Trimming grass."); + } catch {} + } + + } else if (actionRoll === 1) { + const crops = bot.findBlocks({ + matching: block => ['wheat', 'carrots', 'potatoes'].includes(block.name), + maxDistance: safeRadius, + count: 5 + }); + + for (const pos of crops) { + const crop = bot.blockAt(pos); + if (crop.metadata === 7) { + await bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1)); + try { + await bot.dig(crop); + bot.chat("Harvesting a crop."); + // TODO: Replanting logic needs to be added here. This might be a state or a task.. not sure yet. + } catch {} + break; + } + } + + } else { + const mob = Object.values(bot.entities).find(e => + e.type === 'mob' && + e.position.distanceTo(bot.entity.position) <= safeRadius && + e.username !== bot.username + ); + + if (mob) { + bot.chat(`Engaging ${mob.name}.`); + bot.attack(mob); + } else { + const dx = Math.floor(Math.random() * safeRadius * 2 - safeRadius); + const dz = Math.floor(Math.random() * safeRadius * 2 - safeRadius); + const pos = bot.entity.position.offset(dx, 0, dz); + bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1)); + } + } + }); +}; diff --git a/bot/states/Observe.js b/bot/states/Observe.js new file mode 100644 index 0000000..6db3181 --- /dev/null +++ b/bot/states/Observe.js @@ -0,0 +1,16 @@ +// states/Observe.js +const { getBot } = require('../core/context'); +const { chatWithAI } = require('../lib/ai-helper'); +const config = require('../config.json'); + +module.exports = async function Observe() { + const bot = getBot(); + console.log('[STATE] Observe'); + + // Example: count nearby entities. I will need to add way more to this observe file later. + const entities = Object.values(bot.entities).filter(e => e.type === 'mob'); + const msg = `I see ${entities.length} mobs nearby. What should I do?`; + const response = await chatWithAI(msg, config.ai); + + bot.chat(response); +}; \ No newline at end of file diff --git a/bot/states/ReadFromSign.js b/bot/states/ReadFromSign.js new file mode 100644 index 0000000..e512af5 --- /dev/null +++ b/bot/states/ReadFromSign.js @@ -0,0 +1,38 @@ +// states/readFromSign.js +const { getBot } = require('../core/context'); +const { chatWithAI } = require('../lib/ai-helper'); +const { setSafeZone } = require('../memory/locations'); +const config = require('../config.json'); +const db = require('../db'); + +module.exports = async function ReadFromSign() { + const bot = getBot(); + console.log('[STATE] ReadFromSign'); + + const signs = bot.findBlocks({ + matching: block => block.name.includes('sign'), + maxDistance: 10, + count: 5 + }); + + for (const pos of signs) { + try { + const text = await bot.readSign(pos); + const keywords = config.safeZone.keywords; + + const matchedKeyword = keywords.find(k => text.toLowerCase().includes(k)); + if (matchedKeyword) { + const standardizedLabel = matchedKeyword.toLowerCase(); + setSafeZone(db, standardizedLabel, pos); + bot.chat(`You set a safe zone for "${standardizedLabel}".`); + } + + const response = await chatWithAI(`You read a sign that says: "${text}". What should you do?`, config.ai); + bot.chat(response); + break; + + } catch (err) { + console.warn('[SIGN] Read failed:', err.message); + } + } +}; diff --git a/bot/states/RestructureScript.js b/bot/states/RestructureScript.js new file mode 100644 index 0000000..a6e023f --- /dev/null +++ b/bot/states/RestructureScript.js @@ -0,0 +1,25 @@ +const fs = require('fs'); +const path = require('path'); +const { chatWithAI } = require('../lib/ai-helper'); +const config = require('../config.json'); + +module.exports = async function RestructureScript() { + console.log('[STATE] RestructureScript'); + + + // Replace with actual context data later. This is place holder for getting a task script that isn't written. + const taskName = 'example-task'; + const scriptPath = path.join(__dirname, '..', 'bot-tasks', `${taskName}.js`); + + if (!fs.existsSync(scriptPath)) { + console.warn(`[AI] Cannot find script: ${taskName}.js`); + return; + } + + const original = fs.readFileSync(scriptPath, 'utf8'); + const prompt = `This script failed repeatedly:\n${original}\nRewrite it to be more reliable. Only return code.`; + const response = await chatWithAI(prompt, config.ai); + + fs.writeFileSync(scriptPath, response); + console.log(`[AI] Rewrote ${taskName}`); +};