Improve task.js with better modularized code

This commit is contained in:
Ayush Maniar 2025-02-23 02:50:11 -08:00
parent 2f8248955e
commit 1ba5f130f3

View file

@ -1,41 +1,115 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { executeCommand } from './commands/index.js'; import { executeCommand } from './commands/index.js';
import { getPosition } from './library/world.js' import { getPosition } from './library/world.js';
import settings from '../../settings.js'; import settings from '../../settings.js';
import { CraftTaskInitiator } from './task_types/crafting_tasks.js';
import { CookingTaskInitiator } from './task_types/cooking_tasks.js';
/**
export class TaskValidator { * Validates the presence of required items in an agent's inventory
constructor(data, agent) { * @param {Object} data - Task data containing target and quantity information
this.target = data.target; * @param {Object} agent - Agent object with bot inventory
this.number_of_target = data.number_of_target; * @returns {Object} Validation result with success status and missing items
this.agent = agent; */
function checkItemPresence(data, agent) {
// Helper function to check if target is a dictionary with quantities
function isTargetDictionaryWithQuantities(target) {
return typeof target === 'object' &&
!Array.isArray(target) &&
target !== null &&
Object.values(target).every(value => typeof value === 'number');
}
// Convert any target format into a standardized dictionary
function normalizeTargets(target) {
if (typeof target === 'string') {
// Single target case
return { [target]: 1 };
} else if (Array.isArray(target)) {
// Array case - convert to dictionary with default quantity 1
return target.reduce((acc, item) => {
acc[item] = 1;
return acc;
}, {});
} else if (typeof target === 'object' && target !== null) {
// Already a dictionary - return as is
return target;
}
throw new Error('Invalid target format');
}
// Normalize quantities to match target format
function normalizeQuantities(targets, quantities) {
if (quantities === undefined) {
// If no quantities specified, default to 1 for each target
return Object.keys(targets).reduce((acc, key) => {
acc[key] = 1;
return acc;
}, {});
} else if (typeof quantities === 'number') {
// If single number provided, apply to all targets
return Object.keys(targets).reduce((acc, key) => {
acc[key] = quantities;
return acc;
}, {});
} else if (typeof quantities === 'object' && quantities !== null) {
// If quantities dictionary provided, use it directly
return quantities;
}
throw new Error('Invalid number_of_target format');
} }
validate() {
try { try {
let valid = false; // First normalize targets to always have a consistent format
let total_targets = 0; const targets = normalizeTargets(data.target);
this.agent.bot.inventory.slots.forEach((slot) => {
if (slot && slot.name.toLowerCase() === this.target) { // Determine the required quantities
total_targets += slot.count; const requiredQuantities = isTargetDictionaryWithQuantities(data.target)
} ? data.target
if (slot && slot.name.toLowerCase() === this.target && slot.count >= this.number_of_target) { : normalizeQuantities(targets, data.number_of_target);
valid = true;
console.log('Task is complete'); // Count items in inventory
const inventoryCount = {};
agent.bot.inventory.slots.forEach((slot) => {
if (slot) {
const itemName = slot.name.toLowerCase();
inventoryCount[itemName] = (inventoryCount[itemName] || 0) + slot.count;
} }
}); });
if (total_targets >= this.number_of_target) {
valid = true; // Check if all required items are present in sufficient quantities
console.log('Task is complete'); const missingItems = [];
} let allTargetsMet = true;
return valid;
} catch (error) { for (const [item, requiredCount] of Object.entries(requiredQuantities)) {
console.error('Error validating task:', error); const itemName = item.toLowerCase();
return false; const currentCount = inventoryCount[itemName] || 0;
}
if (currentCount < requiredCount) {
allTargetsMet = false;
missingItems.push({
item: itemName,
required: requiredCount,
current: currentCount,
missing: requiredCount - currentCount
});
} }
} }
return {
success: allTargetsMet,
missingItems: missingItems
};
} catch (error) {
console.error('Error checking item presence:', error);
return {
success: false,
missingItems: [],
error: error.message
};
}
}
export class Task { export class Task {
constructor(agent, task_path, task_id) { constructor(agent, task_path, task_id) {
@ -49,7 +123,18 @@ export class Task {
this.data = this.loadTask(task_path, task_id); this.data = this.loadTask(task_path, task_id);
this.taskTimeout = this.data.timeout || 300; this.taskTimeout = this.data.timeout || 300;
this.taskStartTime = Date.now(); this.taskStartTime = Date.now();
this.validator = new TaskValidator(this.data, this.agent); this.task_type = this.data.type;
// Set validator based on task_type
if (this.task_type === 'cooking' || this.task_type === 'techtree') {
this.validator = () => {
const result = checkItemPresence(this.data, this.agent);
return result.success;
};
} else {
this.validator = null;
}
this.blocked_actions = this.data.blocked_actions || []; this.blocked_actions = this.data.blocked_actions || [];
this.restrict_to_inventory = !!this.data.restrict_to_inventory; this.restrict_to_inventory = !!this.data.restrict_to_inventory;
if (this.data.goal) if (this.data.goal)
@ -57,6 +142,9 @@ export class Task {
if (this.data.conversation) if (this.data.conversation)
this.blocked_actions.push('!endConversation'); this.blocked_actions.push('!endConversation');
} }
this.name = this.agent.name;
this.available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name);
} }
loadTask(task_path, task_id) { loadTask(task_path, task_id) {
@ -79,13 +167,9 @@ export class Task {
} }
isDone() { isDone() {
if (this.validator && this.validator.validate()) if (this.validator && this.validator())
return {"message": 'Task successful', "code": 2}; 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) { if (this.taskTimeout) {
const elapsedTime = (Date.now() - this.taskStartTime) / 1000; const elapsedTime = (Date.now() - this.taskStartTime) / 1000;
if (elapsedTime >= this.taskTimeout) { if (elapsedTime >= this.taskTimeout) {
@ -97,89 +181,62 @@ export class Task {
} }
async initBotTask() { async initBotTask() {
if (this.data === null) await this.agent.bot.chat(`/clear ${this.name}`);
return; console.log(`Cleared ${this.name}'s inventory.`);
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 //wait for a bit so inventory is cleared
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
let initial_inventory = null;
if (this.data.agent_count > 1) { if (this.data === null)
initial_inventory = this.data.initial_inventory[this.agent.count_id.toString()]; return;
console.log("Initial inventory:", initial_inventory);
} else if (this.data) { if (this.task_type === 'techtree') {
console.log("Initial inventory:", this.data.initial_inventory); this.initiator = new CraftTaskInitiator(this.data, this.agent);
initial_inventory = this.data.initial_inventory; } else if (this.task_type === 'cooking') {
this.initiator = new CookingTaskInitiator(this.data, this.agent);
} else {
this.initiator = null;
} }
if ("initial_inventory" in this.data) { await this.teleportBots();
//wait for a bit so bots are teleported
await new Promise((resolve) => setTimeout(resolve, 3000));
if (this.data.initial_inventory) {
console.log("Setting inventory..."); console.log("Setting inventory...");
console.log("Inventory to set:", initial_inventory); let initialInventory = {};
for (let key of Object.keys(initial_inventory)) {
console.log('Giving item:', key); // Handle multi-agent inventory assignment
bot.chat(`/give ${name} ${key} ${initial_inventory[key]}`); if (this.data.agent_count > 1) {
}; initialInventory = this.data.initial_inventory[this.agent.count_id.toString()] || {};
//wait for a bit so inventory is set console.log("Initial inventory for agent", this.agent.count_id, ":", initialInventory);
} else {
initialInventory = this.data.initial_inventory;
console.log("Initial inventory:", initialInventory);
}
// Assign inventory items
for (let key of Object.keys(initialInventory)) {
const itemName = key.toLowerCase();
const quantity = initialInventory[key];
await this.agent.bot.chat(`/give ${this.name} ${itemName} ${quantity}`);
console.log(`Gave ${this.name} ${quantity} ${itemName}`);
}
// Wait briefly for inventory commands to complete
await new Promise((resolve) => setTimeout(resolve, 500)); 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; if (this.initiator) {
let available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); // TODO this does not work with command line args await this.initiator.init();
// 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) { if (this.data.agent_count && this.data.agent_count > 1) {
// TODO wait for other bots to join // TODO wait for other bots to join
await new Promise((resolve) => setTimeout(resolve, 10000)); await new Promise((resolve) => setTimeout(resolve, 10000));
if (available_agents.length < this.data.agent_count) { if (this.available_agents.length < this.data.agent_count) {
console.log(`Missing ${this.data.agent_count - available_agents.length} bot(s).`); console.log(`Missing ${this.data.agent_count - this.available_agents.length} bot(s).`);
this.agent.killAll(); this.agent.killAll();
} }
} }
@ -189,8 +246,43 @@ export class Task {
} }
if (this.data.conversation && this.agent.count_id === 0) { if (this.data.conversation && this.agent.count_id === 0) {
let other_name = available_agents.filter(n => n !== name)[0]; let other_name = this.available_agents.filter(n => n !== this.name)[0];
await executeCommand(this.agent, `!startConversation("${other_name}", "${this.data.conversation}")`); await executeCommand(this.agent, `!startConversation("${other_name}", "${this.data.conversation}")`);
} }
} }
async teleportBots() {
console.log('\n\n\n\n\nTeleporting bots');
function getRandomOffset(range) {
return Math.floor(Math.random() * (range * 2 + 1)) - range;
}
let human_player_name = null;
let bot = this.agent.bot;
// Finding if there is a human player on the server
for (const playerName in bot.players) {
const player = bot.players[playerName];
if (!this.available_agents.some((n) => n === playerName)) {
console.log('Found human player:', player.username);
human_player_name = player.username
break;
}
}
if (human_player_name) {
console.log(`Teleporting ${this.name} to human ${human_player_name}`)
bot.chat(`/tp ${this.name} ${human_player_name}`)
}
await new Promise((resolve) => setTimeout(resolve, 200));
if (this.data.type !== 'construction') {
const pos = getPosition(bot);
const xOffset = getRandomOffset(5);
const zOffset = getRandomOffset(5);
bot.chat(`/tp ${this.name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`);
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
} }