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