import { readFileSync } from 'fs'; import { executeCommand } from './commands/index.js'; import { getPosition } from './library/world.js' import settings from '../../settings.js'; import { Vec3 } from 'vec3'; //todo: modify validator code to return an object with valid and score -> do more testing hahah //todo: figure out how to log these things to the same place as bots/histories export class CraftTaskValidator { constructor(data, agent) { this.target = data.target; this.number_of_target = data.number_of_target; this.agent = agent; } validate() { try{ let valid = false; let total_targets = 0; this.agent.bot.inventory.slots.forEach((slot) => { if (slot && slot.name.toLowerCase() === this.target) { total_targets += slot.count; } if (slot && slot.name.toLowerCase() === this.target && slot.count >= this.number_of_target) { valid = true; console.log('Task is complete'); } }); if (total_targets >= this.number_of_target) { valid = true; console.log('Task is complete'); } return valid; } catch (error) { console.error('Error validating task:', error); return false; } } } export class ConstructionTaskValidator { constructor(data, agent) { this.blueprint = new Blueprint(data.blueprint); this.agent = agent; } validate() { try { //todo: somehow make this more of a percentage or something console.log('Validating task...'); let valid = false; let score = 0; let result = this.blueprint.check(this.agent.bot); if (result.mismatches.length === 0) { valid = true; console.log('Task is complete'); } let total_blocks = result.mismatches.length + result.matches.length; score = (result.matches.length / total_blocks) * 100; console.log(`Task is ${score}% complete`); return valid; } catch (error) { console.error('Error validating task:', error); return false; } } } export function resetConstructionWorld(bot, blueprint) { console.log('Resetting world...'); const starting_position = blueprint.levels[0].coordinates; const length = blueprint.levels[0].placement.length + 5; const height = blueprint.levels.length + 5; const width = blueprint.levels[0].placement[0].length + 5; const command = `/fill ${starting_position[0]} ${starting_position[1]} ${starting_position[2]} ${starting_position[0] + width} ${starting_position[1] + height} ${starting_position[2] + length} air`; bot.chat(command); console.log('World reset'); } // export function resetConstructionWorld(bot) { // console.log('Resetting world...'); // const pos = getPosition(bot); // const xOffset = 25; // const zOffset = 25; // const command = `/fill ${Math.floor(pos.x - xOffset)} -60 ${Math.floor(pos.z - zOffset)} ${Math.floor(pos.x + xOffset)} -50 ${Math.floor(pos.z + zOffset)} air`; // console.log('Command:', command); // bot.chat(command); // console.log('World reset'); // } export function checkLevelBlueprint(agent, levelNum) { const blueprint = agent.task.blueprint; const bot = agent.bot; const result = blueprint.checkLevel(bot, levelNum); if (result.mismatches.length === 0) { return `Level ${levelNum} is correct`; } else { let explanation = blueprint.explainLevelDifference(bot, levelNum); return explanation; } } export function checkBlueprint(agent) { console.log('Checking blueprint...'); console.log(agent); const blueprint = agent.task.blueprint; const bot = agent.bot; const result = blueprint.check(bot); if (result.mismatches.length === 0) { return "Blueprint is correct"; } else { let explanation = blueprint.explainBlueprintDifference(bot); return explanation; } } export class Blueprint { constructor(blueprint) { this.data = blueprint; } explain() { var explanation = ""; for (let item of this.data.levels) { var coordinates = item.coordinates; explanation += `Level ${item.level}: `; explanation += `Start at coordinates X: ${coordinates[0]}, Y: ${coordinates[1]}, Z: ${coordinates[2]}`; let placement_string = this._getPlacementString(item.placement); explanation += `\n${placement_string}\n`; } return explanation; } _getPlacementString(placement) { var placement_string = "[\n"; for (let row of placement) { placement_string += "["; for (let i = 0; i < row.length - 1; i++) { let item = row[i]; placement_string += `${item}, `; } let final_item = row[row.length - 1]; placement_string += `${final_item}],\n`; } placement_string += "]"; return placement_string; } explainLevel(levelNum) { const levelData = this.data.levels[levelNum]; var explanation = `Level ${levelData.level} `; explanation += `starting at coordinates X: ${levelData.coordinates[0]}, Y: ${levelData.coordinates[1]}, Z: ${levelData.coordinates[2]}`; let placement_string = this._getPlacementString(levelData.placement); explanation += `\n${placement_string}\n`; return explanation; } explainBlueprintDifference(bot) { var explanation = ""; const levels = this.data.levels; for (let i = 0; i < levels.length; i++) { let level_explanation = this.explainLevelDifference(bot, i); explanation += level_explanation + "\n"; } return explanation; } explainLevelDifference(bot, levelNum) { const results = this.checkLevel(bot, levelNum); const mismatches = results.mismatches; const levelData = this.data.levels[levelNum]; if (mismatches.length === 0) { return `Level ${levelData.level} is complete`; } var explanation = `Level ${levelData.level} `; // explanation += `at coordinates X: ${levelData.coordinates[0]}, Y: ${levelData.coordinates[1]}, Z: ${levelData.coordinates[2]}`; explanation += " requires the following fixes:\n"; for (let item of mismatches) { if (item.actual === 'air') { explanation += `Place ${item.expected} at coordinates X: ${item.coordinates[0]}, Y: ${item.coordinates[1]}, Z: ${item.coordinates[2]}\n`; } else if (item.expected === 'air') { explanation += `Remove the ${item.actual} at coordinates X: ${item.coordinates[0]}, Y: ${item.coordinates[1]}, Z: ${item.coordinates[2]}\n`; } else { explanation += `Replace the ${item.actual} with a ${item.expected} at coordinates X: ${item.coordinates[0]}, Y: ${item.coordinates[1]}, Z: ${item.coordinates[2]} \n`; } } return explanation; } check(bot) { if (!bot || typeof bot !== 'object' || !bot.hasOwnProperty('blockAt')) { throw new Error('Invalid bot object. Expected a mineflayer bot.'); } const levels = this.data.levels; const mismatches = []; const matches = []; for (let i = 0; i < levels.length; i++) { const result = this.checkLevel(bot, i); mismatches.push(...result.mismatches); matches.push(...result.matches); } return { "mismatches": mismatches, "matches": matches }; } checkLevel(bot, levelNum) { const levelData = this.data.levels[levelNum]; const startCoords = levelData.coordinates; const placement = levelData.placement; const mismatches = []; const matches = []; for (let zOffset = 0; zOffset < placement.length; zOffset++) { const row = placement[zOffset]; for (let xOffset = 0; xOffset < row.length; xOffset++) { const blockName = row[xOffset]; const x = startCoords[0] + xOffset; const y = startCoords[1]; const z = startCoords[2] + zOffset; try { const blockAtLocation = bot.blockAt(new Vec3(x, y, z)); if (!blockAtLocation || blockAtLocation.name !== blockName) { mismatches.push({ level: levelData.level, coordinates: [x, y, z], expected: blockName, actual: blockAtLocation ? bot.registry.blocks[blockAtLocation.type].name : 'air' // Assuming air if no block }); } else { matches.push({ level: levelData.level, coordinates: [x, y, z], expected: blockName, actual: blockAtLocation ? bot.registry.blocks[blockAtLocation.type].name : 'air' // Assuming air if no block }); } } catch (err) { console.error(`Error getting block at (${x}, ${y}, ${z}):`, err); return false; // Stop checking if there's an issue getting blocks } } } return { "mismatches": mismatches, "matches": matches }; } } export class Task { constructor(agent, task_path, task_id) { this.agent = agent; this.data = null; this.taskTimeout = 300; this.taskStartTime = Date.now(); this.validator = null; this.reset_function = null; this.blocked_actions = []; if (task_path && task_id) { this.data = this.loadTask(task_path, task_id); this.task_type = this.data.type; if (this.task_type === 'construction' && this.data.blueprint) { this.blueprint = new Blueprint(this.data.blueprint); this.goal = this.data.goal + ' \n' + this.blueprint.explain() + " \n" + "make sure to place the lower levels of the blueprint first"; this.conversation = this.data.conversation + ' \n' + this.blueprint.explain(); } else { this.goal = this.data.goal; this.conversation = this.data.conversation; } this.taskTimeout = this.data.timeout || 300; this.taskStartTime = Date.now(); if (this.task_type === 'construction') { this.validator = new ConstructionTaskValidator(this.data, this.agent); } else if (this.task_type === 'techtree') { this.validator = new CraftTaskValidator(this.data, this.agent); } this.blocked_actions = this.data.blocked_actions || []; if (this.goal) this.blocked_actions.push('!endGoal'); if (this.conversation) this.blocked_actions.push('!endConversation'); console.log('Task loaded:', this.data); } } loadTask(task_path, task_id) { try { const tasksFile = readFileSync(task_path, 'utf8'); const tasks = JSON.parse(tasksFile); const task = tasks[task_id]; console.log('Loaded task:', task); if (!task) { throw new Error(`Task ${task_id} not found`); } if ((!task.agent_count || task.agent_count <= 1) && this.agent.count_id > 0) { task = null; } return task; } catch (error) { console.error('Error loading task:', error); process.exit(1); } } isDone() { if (this.validator && this.validator.validate()) return {"message": 'Task successful', "code": 2}; // TODO check for other terminal conditions // if (this.task.goal && !this.self_prompter.on) // return {"message": 'Agent ended goal', "code": 3}; // if (this.task.conversation && !inConversation()) // return {"message": 'Agent ended conversation', "code": 3}; if (this.taskTimeout) { const elapsedTime = (Date.now() - this.taskStartTime) / 1000; if (elapsedTime >= this.taskTimeout) { console.log('Task timeout reached. Task unsuccessful.'); return {"message": 'Task timeout reached', "code": 4}; } } return false; } async initBotTask() { if (this.data === null) return; let bot = this.agent.bot; let name = this.agent.name; bot.chat(`/clear ${name}`); console.log(`Cleared ${name}'s inventory.`); //wait for a bit so inventory is cleared await new Promise((resolve) => setTimeout(resolve, 500)); if (this.data.agent_count > 1) { var initial_inventory = this.data.initial_inventory[this.agent.count_id.toString()]; console.log("Initial inventory:", initial_inventory); } else if (this.data) { console.log("Initial inventory:", this.data.initial_inventory); var initial_inventory = this.data.initial_inventory; } if ("initial_inventory" in this.data) { console.log("Setting inventory..."); console.log("Inventory to set:", initial_inventory); for (let key of Object.keys(initial_inventory)) { console.log('Giving item:', key); bot.chat(`/give ${name} ${key} ${initial_inventory[key]}`); }; //wait for a bit so inventory is set await new Promise((resolve) => setTimeout(resolve, 500)); console.log("Done giving inventory items."); } // Function to generate random numbers function getRandomOffset(range) { return Math.floor(Math.random() * (range * 2 + 1)) - range; } let human_player_name = null; let available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); // TODO this does not work with command line args // Finding if there is a human player on the server for (const playerName in bot.players) { const player = bot.players[playerName]; if (!available_agents.some((n) => n === playerName)) { console.log('Found human player:', player.username); human_player_name = player.username break; } } // If there are multiple human players, teleport to the first one // teleport near a human player if found by default if (human_player_name) { console.log(`Teleporting ${name} to human ${human_player_name}`) bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player } await new Promise((resolve) => setTimeout(resolve, 200)); // now all bots are teleport on top of each other (which kinda looks ugly) // Thus, we need to teleport them to random distances to make it look better /* Note : We don't want randomness for construction task as the reference point matters a lot. Another reason for no randomness for construction task is because, often times the user would fly in the air, then set a random block to dirt and teleport the bot to stand on that block for starting the construction, This was done by MaxRobinson in one of the youtube videos. */ if (this.data.type !== 'construction') { const pos = getPosition(bot); const xOffset = getRandomOffset(5); const zOffset = getRandomOffset(5); bot.chat(`/tp ${name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); await new Promise((resolve) => setTimeout(resolve, 200)); } if (this.data.agent_count && this.data.agent_count > 1) { // TODO wait for other bots to join await new Promise((resolve) => setTimeout(resolve, 10000)); if (available_agents.length < this.data.agent_count) { console.log(`Missing ${this.data.agent_count - available_agents.length} bot(s).`); this.agent.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); } } if (this.goal) { console.log('Setting goal:', this.goal); await executeCommand(this.agent, `!goal("${this.goal}")`); } if (this.conversation && this.agent.count_id === 0) { let other_name = available_agents.filter(n => n !== name)[0]; await executeCommand(this.agent, `!startConversation("${other_name}", "${this.conversation}")`); } } } export function giveBlueprint(agent, blueprint) { let bot = agent.bot; let name = agent.name; let blueprint_name = blueprint.name; let blueprint_count = blueprint.count; bot.chat(`/clear ${name}`); console.log(`Cleared ${name}'s inventory.`); bot.chat(`/give ${name} ${blueprint_name} ${blueprint_count}`); console.log(`Gave ${name} ${blueprint_count} ${blueprint_name}(s).`); }