diff --git a/evaluate.sh b/evaluate.sh deleted file mode 100755 index b1cd875..0000000 --- a/evaluate.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -# Initialize variables -args=() -num_experiments=0 -successful=0 -unsuccessful=0 -error=0 - -# Parse arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --num_experiments) - num_experiments="$2" - shift 2 - ;; - *) - args+=("$1") - shift - ;; - esac -done - -# Validate num_experiments -if ! [[ "$num_experiments" =~ ^[0-9]+$ ]] || [[ "$num_experiments" -eq 0 ]]; then - echo "Error: num_experiments must be a positive integer" - echo "Usage: $0 --num_experiments [other arguments]" - exit 1 -fi - -# Run experiments -while (( successful + unsuccessful < num_experiments )); do - node main.js "${args[@]}" - exit_code=$? - - case $exit_code in - 2) ((successful++));; - 3) ((unsuccessful++));; - 4) ((error++));; - *) echo "Unknown exit code: $exit_code";; - esac - - # Calculate success percentage - if [[ $successful -eq 0 && $unsuccessful -eq 0 ]]; then - success_percentage=0 - else - success_percentage=$(echo "scale=2; $successful / ($successful + $unsuccessful) * 100" | bc) - fi - - echo "Success percentage: $success_percentage%" - echo "Total successful: $successful" - echo "Total unsuccessful: $unsuccessful" - echo "Total errors: $error" - echo "Total experiments run: $((successful + unsuccessful))" -done - -# Generate output file with a cleaner name format -date_time=$(date +'%Y-%m-%d_%H-%M-%S') -output_file="${date_time}_results.txt" - -echo "Total experiments: $num_experiments" > "$output_file" -echo "Successful experiments: $successful" >> "$output_file" -echo "Unsuccessful experiments: $unsuccessful" >> "$output_file" -echo "Experiments with errors: $error" >> "$output_file" -echo "Success percentage: $success_percentage%" >> "$output_file" - -echo "Results saved in $output_file" \ No newline at end of file diff --git a/main.js b/main.js index 1054633..9efb4e6 100644 --- a/main.js +++ b/main.js @@ -25,7 +25,6 @@ function parseArguments() { .parse(); } -//todo: modify for multiple agents function getProfiles(args) { return args.profiles || settings.profiles; } @@ -35,7 +34,7 @@ async function main() { const mindServer = createMindServer(); } mainProxy.connect(); - + const args = parseArguments(); const profiles = getProfiles(args); console.log(profiles); diff --git a/src/agent/agent.js b/src/agent/agent.js index 805f560..7b14f3c 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -13,7 +13,7 @@ import { handleTranslation, handleEnglishTranslation } from '../utils/translator import { addViewer } from './viewer.js'; import settings from '../../settings.js'; import { serverProxy } from './agent_proxy.js'; -import { loadTask, initBotTask, TechTreeHarvestValidator } from '../utils/tasks.js'; +import { Task } from './tasks.js'; export class Agent { async start(profile_fp, load_mem=false, init_message=null, count_id=0, task_path=null, task_id=null) { @@ -45,6 +45,9 @@ export class Agent { convoManager.initAgent(this); console.log('Initializing examples...'); await this.prompter.initExamples(); + console.log('Initializing task...'); + this.task = new Task(this, task_path, task_id); + this.blocked_actions = this.task.blocked_actions || []; serverProxy.connect(this); @@ -58,24 +61,6 @@ export class Agent { save_data = this.history.load(); } - // Load task if provided - if (task_path) { - this.task = loadTask(task_path, task_id); - this.taskTimeout = this.task.timeout || 300; - this.taskStartTime = Date.now(); - this.validator = new TechTreeHarvestValidator(this.task, this.bot); - this.blocked_actions = this.task.blocked_actions || []; - if (this.task.goal) - this.blocked_actions.push('!endGoal'); - if (this.task.conversation) - this.blocked_actions.push('!endConversation'); - } else { - this.task = null; - this.taskTimeout = null; - this.validator = null; - this.blocked_actions = []; - } - this.bot.on('login', () => { console.log(this.name, 'logged in!'); @@ -105,8 +90,7 @@ export class Agent { this._setupEventHandlers(save_data, init_message); this.startEvents(); - if (this.task) - initBotTask(this); + this.task.initBotTask(); } catch (error) { console.error('Error in spawn event:', error); @@ -376,6 +360,7 @@ export class Agent { } startEvents() { + // Custom events this.bot.on('time', () => { if (this.bot.time.timeOfDay == 0) this.bot.emit('sunrise'); @@ -454,46 +439,17 @@ export class Agent { this.bot.emit('idle'); // Check for task completion - if (this.task) { + if (this.task.data) { setInterval(() => { - if (this.validator && this.validator.validate()) - this.killBots(); - // TODO check for other terminal conditions - // if (this.task.goal && !this.self_prompter.on) - // this.cleanKill('Agent ended goal', 3); - // if (this.task.conversation && !inConversation()) - // this.cleanKill('Agent ended conversation', 3); - if (this.taskTimeout) { - const elapsedTime = (Date.now() - this.taskStartTime) / 1000; - if (elapsedTime >= this.taskTimeout) { - console.log('Task timeout reached. Task unsuccessful.'); - this.cleanKill('Task unsuccessful: Timeout reached', 3); - } + let res = this.task.isDone(); + if (res) { + // TODO kill other bots + this.cleanKill(res.message, res.code); } - }, 1000); } } - async killBots() { - this.bot.chat('Task completed!'); - this.bot.chat(`/clear @p`); - // Kick other bots - if (this.task && this.task.agent_number) { - const agent_names = this.task.agent_names; - console.log('All agent names:', agent_names); - console.log('My name:', this.name); - const botNames = agent_names.filter(botName => botName !== this.name); - console.log('Kicking bots:', botNames); - botNames.forEach(botName => { - this.bot.chat(`/kick ${botName}`); - console.log(`/kick ${botName}`); - - }); - } - this.cleanKill('Task completed, exiting', 2); - } - async update(delta) { await this.bot.modes.update(); this.self_prompter.update(delta); diff --git a/src/agent/tasks.js b/src/agent/tasks.js new file mode 100644 index 0000000..453125e --- /dev/null +++ b/src/agent/tasks.js @@ -0,0 +1,192 @@ +import { readFileSync } from 'fs'; +import { executeCommand } from './commands/index.js'; +import { getPosition } from './library/world.js' +import settings from '../../settings.js'; + + +export class TaskValidator { + 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 Task { + constructor(agent, task_path, task_id) { + this.agent = agent; + this.data = null; + this.taskTimeout = 300; + this.taskStartTime = Date.now(); + this.validator = null; + this.blocked_actions = []; + if (task_path && task_id) { + this.data = this.loadTask(task_path, task_id); + this.taskTimeout = this.data.timeout || 300; + this.taskStartTime = Date.now(); + this.validator = new TaskValidator(this.data, this.agent); + this.blocked_actions = this.data.blocked_actions || []; + if (this.data.goal) + this.blocked_actions.push('!endGoal'); + if (this.data.conversation) + this.blocked_actions.push('!endConversation'); + } + } + + loadTask(task_path, task_id) { + try { + const tasksFile = readFileSync(task_path, 'utf8'); + const tasks = JSON.parse(tasksFile); + const task = tasks[task_id]; + if (!task) { + throw new Error(`Task ${task_id} not found`); + } + + 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_number > 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 === name)) { + 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) { + 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.data.goal) { + await executeCommand(this.agent, `!goal("${this.data.goal}")`); + } + + if (this.data.conversation && this.agent.count_id === 0) { + let other_name = available_agents.filter(n => n !== name)[0]; + await executeCommand(this.agent, `!startConversation("${other_name}", "${this.data.conversation}")`); + } + } +} diff --git a/src/utils/tasks.js b/src/utils/tasks.js deleted file mode 100644 index 0c6c470..0000000 --- a/src/utils/tasks.js +++ /dev/null @@ -1,149 +0,0 @@ -import { readFileSync } from 'fs'; -import { executeCommand } from '../agent/commands/index.js'; -import { getPosition } from '../agent/library/world.js' -import settings from '../../settings.js'; - -export function loadTask(task_path, task_id) { - try { - const tasksFile = readFileSync(task_path, 'utf8'); - const tasks = JSON.parse(tasksFile); - const task = tasks[task_id]; - if (!task) { - throw new Error(`Task ${task_id} not found`); - } - - return task; - } catch (error) { - console.error('Error loading task:', error); - process.exit(1); - } -} - -export async function initBotTask(agent) { - let bot = agent.bot; - let task = agent.task; - let name = bot.username; - - 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 (task.agent_number > 1) { - var initial_inventory = task.initial_inventory[agent.count_id.toString()]; - console.log("Initial inventory:", initial_inventory); - } else if (task) { - console.log("Initial inventory:", task.initial_inventory); - var initial_inventory = task.initial_inventory; - } - - if ("initial_inventory" in task) { - 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); - - // 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 === name)) { - 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 (task.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 (task.agent_count && task.agent_count > 1) { - await new Promise((resolve) => setTimeout(resolve, 10000)); - if (available_agents.length < task.agent_count) { - console.log(`Missing ${task.agent_count - available_agents.length} bot(s).`); - agent.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); - } - - } - - if (task.goal) { - await executeCommand(agent, `!goal("${task.goal}")`); - } - - if (task.conversation && agent.count_id === 0) { - let other_name = available_agents.filter(n => n !== name)[0]; - await executeCommand(agent, `!startConversation("${other_name}", "${task.conversation}")`); - } -} - -export class TechTreeHarvestValidator { - constructor(task, bot) { - this.target = task.target; - this.number_of_target = task.number_of_target; - this.bot = bot; - } - - validate() { - try{ - let valid = false; - let total_targets = 0; - this.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; - } - } -}