Massive Overhaul. Added state machine.
All checks were successful
Deploy Cletus Bot / deploy (push) Successful in 26s

This commit is contained in:
roberts 2025-05-10 12:24:59 -05:00
parent e6c01197ed
commit 2a9f9159bd
30 changed files with 838 additions and 856 deletions

105
README.md
View file

@ -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.
# 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.

View file

@ -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);
}
};

View file

@ -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.");
}
};

View file

@ -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.");
}
};

View file

@ -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);
}
};

View file

@ -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.");
}
};

View file

@ -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.");
}
};

View file

@ -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.");
}
};

View file

@ -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.");
}
};

View file

@ -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 isnt part of the code.
As I refine my AI mineflayer bot, I need to revise the logic for the bots 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; // 1015 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. ");
context.setAllowDestruction = (value) => { allowDestruction = value; };
context.getAllowDestruction = () => allowDestruction;
// 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 originalDig = bot.dig;
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++;
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);
}
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
// 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');
});

View file

@ -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.'));

22
bot/config.json Normal file
View file

@ -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"]
}
}

20
bot/core/context.js Normal file
View file

@ -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
})
};

55
bot/core/state-machine.js Normal file
View file

@ -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 };

35
bot/db.js Normal file
View file

@ -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;

View file

@ -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<string>} - 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 };
module.exports = { chatWithAI };

View file

@ -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.");
}
};

View file

@ -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);
});
}
};

0
bot/lib/utils.js Normal file
View file

35
bot/memory/chat.js Normal file
View file

@ -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 };

31
bot/memory/events.js Normal file
View file

@ -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 };

54
bot/memory/index.js Normal file
View file

@ -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
};

59
bot/memory/locations.js Normal file
View file

@ -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.");
}
});
*/

30
bot/memory/signs.js Normal file
View file

@ -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 };

14
bot/states/ActOnMemory.js Normal file
View file

@ -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);
};

40
bot/states/HandleChat.js Normal file
View file

@ -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, Ill 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);
};

73
bot/states/Idle.js Normal file
View file

@ -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));
}
}
});
};

16
bot/states/Observe.js Normal file
View file

@ -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);
};

View file

@ -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);
}
}
};

View file

@ -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}`);
};