cletus/bot/bot.js
2025-05-09 15:53:19 -05:00

317 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const logSurroundings = require('./lib/log-surroundings');
const memory = require('./lib/memory');
const { chatWithPersona } = require('./lib/ai-helper');
const mineflayer = require('mineflayer');
const { pathfinder, Movements, goals: { GoalNear } } = require('mineflayer-pathfinder');
const fs = require('fs');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const axios = require('axios');
// === AI Integration (Ollama) ===
const OLLAMA_URL = 'http://192.168.1.3:11434/api/generate';
const MODEL_NAME = 'gemma3';
const BOT_NAME = 'Cletus';
const TASK_DIR = path.join(__dirname, 'bot-tasks');
const DB_PATH = path.join(__dirname, 'db', 'memory.db');
// === Bot Parameters ===
const MC_HOST = 'www.thebytes.net';
const MC_PORT = 25565;
const MC_AUTH = 'offline';
const MC_VER = '1.20.4';
let currentTask = null;
// === Prompts ===
const REFINE_PROMPT = `
Only provide code, do not explain the code or provide a response that 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_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 ===
const bot = mineflayer.createBot({
host: MC_HOST,
port: MC_PORT,
username: BOT_NAME,
auth: MC_AUTH,
version: MC_VER,
});
bot.loadPlugin(pathfinder);
bot.allowDestruction = false;
// === Bot Spawned ===
bot.on('spawn', () => {
console.log(`${BOT_NAME} spawned.`);
if (isRecoveringItems && lastDeathLocation) {
sayWithPersona("you just respawned from dying, and you are going to try and get your stuff back. ");
// Set goal to walk back to where Cletus died
const goal = new GoalNear(lastDeathLocation.x, lastDeathLocation.y, lastDeathLocation.z, 2);
bot.pathfinder.setGoal(goal);
const recoverTimeout = setTimeout(() => {
if (bot.entity.position.distanceTo(lastDeathLocation) <= 3) {
sayWithPersona("after dying, you actually found your stuff. ");
isRecoveringItems = false;
recoveryFails = 0;
} else {
sayWithPersona("after respawning and looking for your dropped items, you can't find the stuff. ");
recoveryFails++;
if (recoveryFails >= 2) {
sayWithPersona("after respawning multiple times in attempt to locate your dropped items from death, you give up. ");
isRecoveringItems = false;
lastDeathLocation = null;
bot.pathfinder.setGoal(null); // stop movement on failure
}
}
clearTimeout(recoverTimeout);
}, 15000); // Try for 15 seconds
}
const defaultMove = new Movements(bot);
bot.pathfinder.setMovements(defaultMove);
// Passive idle wandering
startWandering();
});
// === Bot Chat Listener ===
bot.on('chat', async (username, message) => {
if (username !== BOT_NAME) {
bot.lastChatMessage = message;
const messageLower = message.toLowerCase();
const tasks = getAvailableTasks();
if (messageLower.includes('stop that') || messageLower.includes("don't do that")) {
if (currentTask) {
sayWithPersona("you have been asked to stop doing " + `${currentTask}.`);
// Log negative feedback
db.get(`SELECT score FROM tasks WHERE action = ? ORDER BY timestamp DESC LIMIT 1`, [currentTask], (err, row) => {
let score = row?.score ?? 0;
score = Math.min(5, score + 1);
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'interrupted', ?)`, [currentTask, score]);
if (score >= 5) {
const fs = require('fs');
const path = require('path');
const script = fs.readFileSync(path.join(TASK_DIR, `${currentTask}.js`), 'utf8');
requestTaskRewrite(currentTask, script);
}
});
// Cancel current goal (pathfinder)
bot.pathfinder.setGoal(null);
currentTask = null;
return;
} else {
sayWithPersona("you've been asked to stop doing whatever you are doing.");
return;
}
}
console.log(`[CHAT] ${username}: ${messageLower}`);
console.log(`[TASK FILES LOADED]: ${Object.keys(tasks).join(', ')}`);
const matchedTask = Object.keys(tasks).find(taskName =>
messageLower.includes(taskName.replace(/-/g, ' '))
);
if (matchedTask) {
console.log(`[TASK MATCHED]: ${matchedTask}`);
sayWithPersona("you were asked to " + `${matchedTask.replace(/-/g, ' ')}.`);
try {
await tasks[matchedTask](bot, db, sayWithPersona);
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'success', 0)`, [matchedTask]);
} catch (err) {
console.error(`Task ${matchedTask} failed:`, err.message);
sayWithPersona("you failed at the task of " + `${matchedTask}`);
}
} else {
console.log(`[NO MATCH]: Responding only.`);
sayWithPersona(message);
}
}
});
// === Lifecycle & Error Events ===
bot.on('error', (err) => console.error('Bot error:', err));
bot.on('kicked', (reason) => console.log('Bot was kicked:', reason));
bot.on('end', () => console.log('Bot has disconnected.'));
// === Reaction to Death ===
let lastDeathLocation = null;
let isRecoveringItems = false;
let recoveryFails = 0;
bot.on('death', () => {
console.log('Cletus died.');
let times = recoveryFails == 0 ? "." : " again.";
sayWithPersona("you just died" + `${times}`);
// Save death location
lastDeathLocation = bot.entity.position.clone();
isRecoveringItems = true;
recoveryFails = 0;
});
// === Reaction to Being Attacked ===
bot.on('entityHurt', (entity) => {
if (entity === bot.entity) {
sayWithPersona("you got hurt.");
}
});
// Optional: log attacker
bot.on('entitySwingArm', (entity) => {
if (entity.position.distanceTo(bot.entity.position) < 3) {
const attacker = entity.username || entity.name;
sayWithPersona(`${attacker}` + "has just hit you.");
}
});