diff --git a/bots/andy/assist.json b/bots/andy/assist.json index 2307682..680b4be 100644 --- a/bots/andy/assist.json +++ b/bots/andy/assist.json @@ -2,6 +2,5 @@ "name": "andy", "bio": "You are playing minecraft and assisting other players in tasks.", "memory": "", - "events": [], "turns": [] } \ No newline at end of file diff --git a/bots/template.js b/bots/template.js index 1e61d60..8d82f75 100644 --- a/bots/template.js +++ b/bots/template.js @@ -1,5 +1,5 @@ -import * as skills from '../../../src/agent/skills.js'; -import * as world from '../../../src/agent/world.js'; +import * as skills from '../../../src/agent/library/skills.js'; +import * as world from '../../../src/agent/library/world.js'; import Vec3 from 'vec3'; const log = skills.log; diff --git a/src/agent/agent.js b/src/agent/agent.js index 7ad8ef8..9e331c2 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -1,11 +1,10 @@ +import { History } from './history.js'; +import { Coder } from './coder.js'; +import { initModes } from './modes.js'; +import { Examples } from '../utils/examples.js'; import { initBot } from '../utils/mcdata.js'; import { sendRequest } from '../utils/gpt.js'; -import { History } from './history.js'; -import { Examples } from './examples.js'; -import { Coder } from './coder.js'; -import { containsCommand, commandExists, executeCommand } from './commands.js'; -import { Events } from './events.js'; -import { initModes } from './modes.js'; +import { containsCommand, commandExists, executeCommand } from './commands/index.js'; export class Agent { @@ -21,10 +20,7 @@ export class Agent { this.bot = initBot(name); - this.events = new Events(this, this.history.events); - initModes(this); - this.idle = true; this.bot.on('login', async () => { @@ -63,7 +59,7 @@ export class Agent { this.bot.emit('finished_executing'); } - this.startUpdateLoop(); + this.startEvents(); }); } @@ -130,7 +126,24 @@ export class Agent { this.bot.emit('finished_executing'); } - startUpdateLoop() { + startEvents() { + // Custom events + this.bot.on('time', () => { + if (this.bot.time.timeOfDay == 0) + this.bot.emit('sunrise'); + else if (this.bot.time.timeOfDay == 6000) + this.bot.emit('noon'); + else if (this.bot.time.timeOfDay == 12000) + this.bot.emit('sunset'); + else if (this.bot.time.timeOfDay == 18000) + this.bot.emit('midnight'); + }); + this.bot.on('health', () => { + if (this.bot.health < 20) + this.bot.emit('damaged'); + }); + + // Logging callbacks this.bot.on('error' , (err) => { console.error('Error event!', err); }); @@ -165,4 +178,8 @@ export class Agent { } }, INTERVAL); } + + isIdle() { + return !this.coder.executing && !this.coder.generating; + } } diff --git a/src/agent/coder.js b/src/agent/coder.js index 6c4e6fe..d4d842a 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -1,16 +1,16 @@ import { writeFile, readFile, mkdirSync } from 'fs'; import { sendRequest } from '../utils/gpt.js'; -import { getSkillDocs } from './skill-library.js'; -import { Examples } from './examples.js'; +import { getSkillDocs } from './library/index.js'; +import { Examples } from '../utils/examples.js'; export class Coder { constructor(agent) { this.agent = agent; - this.current_code = ''; this.file_counter = 0; this.fp = '/bots/'+agent.name+'/action-code/'; this.executing = false; + this.generating = false; this.code_template = ''; this.timedout = false; } @@ -59,12 +59,11 @@ export class Coder { console.error('Error writing code execution file: ' + result); return null; } - this.current_code = code; return await import('../..' + this.fp + filename); } santitizeCode(code) { - const remove_strs = ['javascript', 'js'] + const remove_strs = ['Javascript', 'javascript', 'js'] for (let r of remove_strs) { if (code.startsWith(r)) { code = code.slice(r.length); @@ -89,6 +88,15 @@ export class Coder { async generateCode(agent_history) { + // wrapper to prevent overlapping code generation loops + await this.stop(); + this.generating = true; + await this.generateCodeLoop(agent_history); + this.generating = false; + } + + + async generateCodeLoop(agent_history) { let system_message = "You are a minecraft mineflayer bot that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write your code in a codeblock. Example response: ``` // your code here ``` You will then be given a response to your code. If you are satisfied with the response, respond without a codeblock in a conversational way. If something went wrong, write another codeblock and try to fix the problem."; system_message += getSkillDocs(); @@ -99,6 +107,8 @@ export class Coder { let code_return = null; let failures = 0; for (let i=0; i<5; i++) { + if (this.agent.bot.interrupt_code) + return; console.log(messages) let res = await sendRequest(messages, system_message); console.log('Code generation response:', res) @@ -144,12 +154,8 @@ export class Coder { role: 'system', content: code_return.message }); - - if (this.agent.bot.interrupt_code) - return; } - - return + return; } // returns {success: bool, message: string, interrupted: bool, timedout: false} @@ -173,7 +179,6 @@ export class Coder { let interrupted = this.agent.bot.interrupt_code; let timedout = this.timedout; this.clear(); - this.agent.bot.emit("code_terminated"); return {success:true, message: output, interrupted, timedout}; } catch (err) { this.executing = false; @@ -181,11 +186,10 @@ export class Coder { console.error("Code execution triggered catch: " + err); await this.stop(); - let message = this.formatOutput(this.agent.bot); - message += '!!Code threw exception!! Error: ' + err; + + let message = this.formatOutput(this.agent.bot) + '!!Code threw exception!! Error: ' + err; let interrupted = this.agent.bot.interrupt_code; this.clear(); - this.agent.bot.emit("code_terminated"); return {success: false, message, interrupted, timedout: false}; } } @@ -217,7 +221,6 @@ export class Coder { } clear() { - this.current_code = ''; this.agent.bot.output = ''; this.agent.bot.interrupt_code = false; this.timedout = false; diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 236a3db..119de1f 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -1,15 +1,13 @@ -import * as skills from '../skills.js'; -import * as world from '../world.js'; +import * as skills from '../library/skills.js'; + function wrapExecution(func, timeout=-1) { return async function (agent, ...args) { - agent.idle = false; let code_return = await agent.coder.execute(async () => { await func(agent, ...args); }, timeout); if (code_return.interrupted && !code_return.timedout) return; - agent.idle = true; return code_return.message; } } @@ -19,11 +17,7 @@ export const actionsList = [ name: '!newAction', description: 'Perform new and unknown custom behaviors that are not available as a command by writing code.', perform: async function (agent) { - agent.idle = false; - let res = await agent.coder.generateCode(agent.history); - agent.idle = true; - if (res) - return '\n' + res + '\n'; + await agent.coder.generateCode(agent.history); } }, { @@ -32,7 +26,6 @@ export const actionsList = [ perform: async function (agent) { await agent.coder.stop(); agent.coder.clear(); - agent.idle = true; return 'Agent stopped.'; } }, @@ -46,7 +39,7 @@ export const actionsList = [ perform: async function (agent, mode_name, on) { const modes = agent.bot.modes; if (!modes.exists(mode_name)) - return `Mode ${mode_name} does not exist.` + modes.getDocs(); + return `Mode ${mode_name} does not exist.` + modes.getStr(); if (modes.isOn(mode_name) === on) return `Mode ${mode_name} is already ${on ? 'on' : 'off'}.`; modes.setOn(mode_name, on); @@ -69,6 +62,18 @@ export const actionsList = [ await skills.followPlayer(agent.bot, player_name); }) }, + { + name: '!givePlayer', + description: 'Give the specified item to the given player. Ex: !givePlayer("steve", "stone_pickaxe", 1)', + params: { + 'player_name': '(string) The name of the player to give the item to.', + 'item_name': '(string) The name of the item to give.' , + 'num': '(number) The number of items to give.' + }, + perform: wrapExecution(async (agent, player_name, item_name, num) => { + await skills.giveToPlayer(agent.bot, item_name, player_name, num); + }) + }, { name: '!collectBlocks', description: 'Collect the nearest blocks of a given type.', diff --git a/src/agent/commands.js b/src/agent/commands/index.js similarity index 96% rename from src/agent/commands.js rename to src/agent/commands/index.js index 0b13775..f6bbadf 100644 --- a/src/agent/commands.js +++ b/src/agent/commands/index.js @@ -1,6 +1,6 @@ +import { actionsList } from './actions.js'; +import { queryList } from './queries.js'; -import { actionsList } from './commands/actions.js'; -import { queryList } from './commands/queries.js'; const commandList = queryList.concat(actionsList); const commandMap = {}; @@ -98,4 +98,4 @@ export function getCommandDocs() { } } return docs + '*\n'; -} \ No newline at end of file +} diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index 495603e..ec79474 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -1,5 +1,6 @@ -import { getNearestBlock, getNearbyEntityTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from '../world.js'; -import { getAllItems, getBiomeName } from '../../utils/mcdata.js'; +import * as world from '../library/world.js'; +import * as mc from '../../utils/mcdata.js'; + const pad = (str) => { return '\n' + str + '\n'; @@ -18,7 +19,7 @@ export const queryList = [ res += `\n- Position: x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)}`; res += `\n- Health: ${Math.round(bot.health)} / 20`; res += `\n- Hunger: ${Math.round(bot.food)} / 20`; - res += `\n- Biome: ${getBiomeName(bot)}`; + res += `\n- Biome: ${world.getBiomeName(bot)}`; let weather = "clear"; if (bot.rainState > 0) weather = "Rain"; @@ -45,7 +46,7 @@ export const queryList = [ description: "Get your bot's inventory.", perform: function (agent) { let bot = agent.bot; - let inventory = getInventoryCounts(bot); + let inventory = world.getInventoryCounts(bot); let res = 'INVENTORY'; for (const item in inventory) { if (inventory[item] && inventory[item] > 0) @@ -63,7 +64,7 @@ export const queryList = [ perform: function (agent) { let bot = agent.bot; let res = 'NEARBY_BLOCKS'; - let blocks = getNearbyBlockTypes(bot); + let blocks = world.getNearbyBlockTypes(bot); for (let i = 0; i < blocks.length; i++) { res += `\n- ${blocks[i]}`; } @@ -78,9 +79,9 @@ export const queryList = [ description: "Get the craftable items with the bot's inventory.", perform: function (agent) { const bot = agent.bot; - const table = getNearestBlock(bot, 'crafting_table'); + const table = world.getNearestBlock(bot, 'crafting_table'); let res = 'CRAFTABLE_ITEMS'; - for (const item of getAllItems()) { + for (const item of mc.getAllItems()) { let recipes = bot.recipesFor(item.id, null, 1, table); if (recipes.length > 0) { res += `\n- ${item.name}`; @@ -98,10 +99,10 @@ export const queryList = [ perform: function (agent) { let bot = agent.bot; let res = 'NEARBY_ENTITIES'; - for (const entity of getNearbyPlayerNames(bot)) { + for (const entity of world.getNearbyPlayerNames(bot)) { res += `\n- player: ${entity}`; } - for (const entity of getNearbyEntityTypes(bot)) { + for (const entity of world.getNearbyEntityTypes(bot)) { res += `\n- mob: ${entity}`; } if (res == 'NEARBY_ENTITIES') { @@ -114,14 +115,7 @@ export const queryList = [ name: "!modes", description: "Get all available modes and see which are on/off.", perform: function (agent) { - return agent.bot.modes.getDocs(); + return agent.bot.modes.getStr(); } - }, - { - name: "!currentAction", - description: "Get the currently executing code.", - perform: function (agent) { - return pad("Current code:\n`" + agent.coder.current_code +"`"); - } - }, -]; \ No newline at end of file + } +]; diff --git a/src/agent/events.js b/src/agent/events.js deleted file mode 100644 index 015cf17..0000000 --- a/src/agent/events.js +++ /dev/null @@ -1,47 +0,0 @@ -export class Events { - constructor(agent, events) { - this.events = events; - if (agent != null) - this.init(agent, events); - } - - init(agent, events) { - this.events = events; - for (let [event, callback, params] of events) { - if (callback != null) - agent.bot.on(event, this[callback].bind(this, agent, params)); - } - - agent.bot.on('time', () => { - if (agent.bot.time.timeOfDay == 0) - agent.bot.emit('sunrise'); - else if (agent.bot.time.timeOfDay == 6000) - agent.bot.emit('noon'); - else if (agent.bot.time.timeOfDay == 12000) - agent.bot.emit('sunset'); - else if (agent.bot.time.timeOfDay == 18000) - agent.bot.emit('midnight'); - }); - - agent.bot.on('health', () => { - if (agent.bot.health < 20) - agent.bot.emit('damaged'); - }); - } - - async executeCode(agent, code) { - console.log('responding to event with code.'); - agent.coder.queueCode(code); - let code_return = await agent.coder.execute(); - console.log('code return:', code_return.message); - agent.history.add('system', code_return.message); - } - - sendThought(agent, message) { - agent.handleMessage(agent.name, message); - } - - sendChat(agent, message) { - agent.bot.chat(message); - } -} \ No newline at end of file diff --git a/src/agent/history.js b/src/agent/history.js index 0d04ffa..03245ce 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -1,7 +1,7 @@ import { writeFileSync, readFileSync, mkdirSync } from 'fs'; -import { getCommandDocs } from './commands.js'; -import { sendRequest } from '../utils/gpt.js'; import { stringifyTurns } from '../utils/text.js'; +import { sendRequest } from '../utils/gpt.js'; +import { getCommandDocs } from './commands/index.js'; export class History { @@ -14,9 +14,6 @@ export class History { this.bio = ''; this.memory = ''; - // The bot's events - this.events = []; - // Variables for controlling the agent's memory and knowledge this.max_messages = 20; } @@ -91,7 +88,6 @@ export class History { 'name': this.name, 'bio': this.bio, 'memory': this.memory, - 'events': this.events, 'turns': this.turns }; const json_data = JSON.stringify(data, null, 4); @@ -111,7 +107,6 @@ export class History { const obj = JSON.parse(data); this.bio = obj.bio; this.memory = obj.memory; - this.events = obj.events; this.turns = obj.turns; } catch (err) { console.error(`No file for profile '${load_path}' for agent ${this.name}.`); diff --git a/src/agent/skill-library.js b/src/agent/library/index.js similarity index 89% rename from src/agent/skill-library.js rename to src/agent/library/index.js index 3bca956..677dc11 100644 --- a/src/agent/skill-library.js +++ b/src/agent/library/index.js @@ -1,12 +1,6 @@ import * as skills from './skills.js'; import * as world from './world.js'; -export function getSkillDocs() { - let docstring = "\n*SKILL DOCS\nThese skills are javascript functions that can be called when writing actions and skills.\n"; - docstring += docHelper(Object.values(skills), 'skills'); - docstring += docHelper(Object.values(world), 'world'); - return docstring + '*\n'; -} export function docHelper(functions, module_name) { let docstring = ''; @@ -20,6 +14,9 @@ export function docHelper(functions, module_name) { return docstring; } -export function containsCodeBlock(message) { - return message.indexOf('```') !== -1; +export function getSkillDocs() { + let docstring = "\n*SKILL DOCS\nThese skills are javascript functions that can be called when writing actions and skills.\n"; + docstring += docHelper(Object.values(skills), 'skills'); + docstring += docHelper(Object.values(world), 'world'); + return docstring + '*\n'; } diff --git a/src/agent/skills.js b/src/agent/library/skills.js similarity index 89% rename from src/agent/skills.js rename to src/agent/library/skills.js index af9e96b..9cc8583 100644 --- a/src/agent/skills.js +++ b/src/agent/library/skills.js @@ -1,14 +1,39 @@ -import { getItemId, getItemName } from "../utils/mcdata.js"; -import { getNearestBlocks, getNearestBlock, getInventoryCounts, getNearestEntityWhere, getNearbyEntities, getNearbyBlocks } from "./world.js"; +import * as mc from "../../utils/mcdata.js"; +import * as world from "./world.js"; import pf from 'mineflayer-pathfinder'; import Vec3 from 'vec3'; + export function log(bot, message, chat=false) { bot.output += message + '\n'; if (chat) bot.chat(message); } +async function autoLight(bot) { + if (bot.modes.isOn('torch_placing') && !bot.interrupt_code) { + let nearest_torch = world.getNearestBlock(bot, 'torch', 8); + if (!nearest_torch) { + let has_torch = bot.inventory.items().find(item => item.name === 'torch'); + if (has_torch) { + try { + log(bot, `Placing torch at ${bot.entity.position}.`); + await placeBlock(bot, 'torch', bot.entity.position.x, bot.entity.position.y, bot.entity.position.z); + return true; + } catch (err) {return true;} + } + } + } + return false; +} + +function equipHighestAttack(bot) { + let weapons = bot.inventory.items().filter(item => item.name.includes('sword') || item.name.includes('axe') || item.name.includes('pickaxe') || item.name.includes('shovel')); + let weapon = weapons.sort((a, b) => b.attackDamage - a.attackDamage)[0]; + if (weapon) + bot.equip(weapon, 'hand'); +} + export async function craftRecipe(bot, itemName) { /** @@ -19,25 +44,52 @@ export async function craftRecipe(bot, itemName) { * @example * await skills.craftRecipe(bot, "stick"); **/ - let recipes = bot.recipesFor(getItemId(itemName), null, 1, null); // get recipes that don't require a crafting table + let placedTable = false; + + // get recipes that don't require a crafting table + let recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, null); let craftingTable = null; if (!recipes || recipes.length === 0) { - craftingTable = getNearestBlock(bot, 'crafting_table', 6); + + // Look for crafting table + craftingTable = world.getNearestBlock(bot, 'crafting_table', 6); if (craftingTable === null){ - log(bot, `You either do not have enough resources to craft ${itemName} or it requires a crafting table, but there is none nearby.`) - return false; + + // Try to place crafting table + let hasTable = world.getInventoryCounts(bot)['crafting_table'] > 0; + if (hasTable) { + let pos = world.getNearestFreeSpace(bot, 1, 6); + await placeBlock(bot, 'crafting_table', pos.x, pos.y, pos.z); + craftingTable = world.getNearestBlock(bot, 'crafting_table', 6); + if (craftingTable) { + recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable); + placedTable = true; + } + } + else { + log(bot, `You either do not have enough resources to craft ${itemName} or it requires a crafting table.`) + return false; + } + } + else { + recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable); } - recipes = bot.recipesFor(getItemId(itemName), null, 1, craftingTable); } if (!recipes || recipes.length === 0) { log(bot, `You do not have the resources to craft a ${itemName}.`); + if (placedTable) { + await collectBlock(bot, 'crafting_table', 1); + } return false; } - const recipe = recipes[0]; + const recipe = recipes[0]; console.log('crafting...'); await bot.craft(recipe, 1, craftingTable); - log(bot, `Successfully crafted ${itemName}, you now have ${getInventoryCounts(bot)[itemName]} ${itemName}.`); + log(bot, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`); + if (placedTable) { + await collectBlock(bot, 'crafting_table', 1); + } return true; } @@ -60,7 +112,7 @@ export async function smeltItem(bot, itemName, num=1) { } // TODO: allow cobblestone, sand, clay, etc. let furnaceBlock = undefined; - furnaceBlock = getNearestBlock(bot, 'furnace', 6); + furnaceBlock = world.getNearestBlock(bot, 'furnace', 6); if (!furnaceBlock){ log(bot, `There is no furnace nearby.`) return false; @@ -71,14 +123,14 @@ export async function smeltItem(bot, itemName, num=1) { const furnace = await bot.openFurnace(furnaceBlock); // check if the furnace is already smelting something let input_item = furnace.inputItem(); - if (input_item && input_item.type !== getItemId(itemName) && input_item.count > 0) { + if (input_item && input_item.type !== mc.getItemId(itemName) && input_item.count > 0) { // TODO: check if furnace is currently burning fuel. furnace.fuel is always null, I think there is a bug. // This only checks if the furnace has an input item, but it may not be smelting it and should be cleared. - log(bot, `The furnace is currently smelting ${getItemName(input_item.type)}.`); + log(bot, `The furnace is currently smelting ${mc.getItemName(input_item.type)}.`); return false; } // check if the bot has enough items to smelt - let inv_counts = getInventoryCounts(bot); + let inv_counts = world.getInventoryCounts(bot); if (!inv_counts[itemName] || inv_counts[itemName] < num) { log(bot, `You do not have enough ${itemName} to smelt.`); return false; @@ -93,11 +145,11 @@ export async function smeltItem(bot, itemName, num=1) { return false; } await furnace.putFuel(fuel.type, null, put_fuel); - log(bot, `Added ${put_fuel} ${getItemName(fuel.type)} to furnace fuel.`); - console.log(`Added ${put_fuel} ${getItemName(fuel.type)} to furnace fuel.`) + log(bot, `Added ${put_fuel} ${mc.getItemName(fuel.type)} to furnace fuel.`); + console.log(`Added ${put_fuel} ${mc.getItemName(fuel.type)} to furnace fuel.`) } // put the items in the furnace - await furnace.putInput(getItemId(itemName), null, num); + await furnace.putInput(mc.getItemId(itemName), null, num); // wait for the items to smelt let total = 0; let collected_last = true; @@ -128,10 +180,10 @@ export async function smeltItem(bot, itemName, num=1) { return false; } if (total < num) { - log(bot, `Only smelted ${total} ${getItemName(smelted_item.type)}.`); + log(bot, `Only smelted ${total} ${mc.getItemName(smelted_item.type)}.`); return false; } - log(bot, `Successfully smelted ${itemName}, got ${total} ${getItemName(smelted_item.type)}.`); + log(bot, `Successfully smelted ${itemName}, got ${total} ${mc.getItemName(smelted_item.type)}.`); return true; } @@ -143,7 +195,7 @@ export async function clearNearestFurnace(bot) { * @example * await skills.clearNearestFurnace(bot); **/ - let furnaceBlock = getNearestBlock(bot, 'furnace', 6); + let furnaceBlock = world.getNearestBlock(bot, 'furnace', 6); if (!furnaceBlock){ log(bot, `There is no furnace nearby.`) return false; @@ -170,14 +222,6 @@ export async function clearNearestFurnace(bot) { } -function equipHighestAttack(bot) { - let weapons = bot.inventory.items().filter(item => item.name.includes('sword') || item.name.includes('axe') || item.name.includes('pickaxe') || item.name.includes('shovel')); - let weapon = weapons.sort((a, b) => b.attackDamage - a.attackDamage)[0]; - if (weapon) - bot.equip(weapon, 'hand'); -} - - export async function attackNearest(bot, mobType, kill=true) { /** * Attack mob of the given type. @@ -221,7 +265,7 @@ export async function attackEntity(bot, entity, kill=true) { } else { bot.pvp.attack(entity); - while (getNearbyEntities(bot, 16).includes(entity)) { + while (world.getNearbyEntities(bot, 16).includes(entity)) { await new Promise(resolve => setTimeout(resolve, 1000)); if (bot.interrupt_code) { bot.pvp.stop(); @@ -245,7 +289,7 @@ export async function defendSelf(bot, range=8) { * **/ bot.modes.pause('self_defense'); let attacked = false; - let enemy = getNearestEntityWhere(bot, entity => isHostile(entity), range); + let enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), range); while (enemy) { equipHighestAttack(bot); if (bot.entity.position.distanceTo(enemy.position) > 4 && enemy.name !== 'creeper' && enemy.name !== 'phantom') { @@ -257,7 +301,7 @@ export async function defendSelf(bot, range=8) { bot.pvp.attack(enemy); attacked = true; await new Promise(resolve => setTimeout(resolve, 500)); - enemy = getNearestEntityWhere(bot, entity => isHostile(entity), range); + enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), range); if (bot.interrupt_code) { bot.pvp.stop(); return false; @@ -288,7 +332,7 @@ export async function collectBlock(bot, blockType, num=1) { return false; } let collected = 0; - const blocks = getNearestBlocks(bot, blockType, 64, num); + const blocks = world.getNearestBlocks(bot, blockType, 64, num); if (blocks.length === 0) { log(bot, `Could not find any ${blockType} to collect.`); return false; @@ -553,7 +597,7 @@ export async function goToPosition(bot, x, y, z, min_distance=2) { * @param {number} distance, the distance to keep from the position. Defaults to 2. * @returns {Promise} true if the position was reached, false otherwise. * @example - * let position = world.getNearestBlock(bot, "oak_log", 64).position; + * let position = world.world.getNearestBlock(bot, "oak_log", 64).position; * await skills.goToPosition(bot, position.x, position.y, position.x + 20); **/ if (x == null || y == null || z == null) { @@ -616,7 +660,7 @@ export async function followPlayer(bot, username) { while (!bot.interrupt_code) { let acted = false; if (bot.modes.isOn('self_defense')) { - const enemy = getNearestEntityWhere(bot, entity => isHostile(entity), attack_distance); + const enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), attack_distance); if (enemy) { log(bot, `Found ${enemy.name}, attacking!`, true); await defendSelf(bot, 8); @@ -624,7 +668,7 @@ export async function followPlayer(bot, username) { } } if (bot.modes.isOn('hunting')) { - const animal = getNearestEntityWhere(bot, entity => isHuntable(entity), attack_distance); + const animal = world.getNearestEntityWhere(bot, entity => mc.isHuntable(entity), attack_distance); if (animal) { log(bot, `Hunting ${animal.name}!`, true); await attackEntity(bot, animal, true); @@ -676,34 +720,3 @@ export async function goToBed(bot) { log(bot, `You have woken up.`); return true; } - - -export function isHuntable(mob) { - if (!mob || !mob.name) return false; - const animals = ['chicken', 'cod', 'cow', 'llama', 'mooshroom', 'pig', 'pufferfish', 'rabbit', 'salmon', 'sheep', 'squid', 'tropical_fish', 'turtle']; - return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby -} - - -export function isHostile(mob) { - if (!mob || !mob.name) return false; - return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; -} - - -async function autoLight(bot) { - if (bot.modes.isOn('torch_placing') && !bot.interrupt_code) { - let nearest_torch = getNearestBlock(bot, 'torch', 8); - if (!nearest_torch) { - let has_torch = bot.inventory.items().find(item => item.name === 'torch'); - if (has_torch) { - try { - log(bot, `Placing torch at ${bot.entity.position}.`); - await placeBlock(bot, 'torch', bot.entity.position.x, bot.entity.position.y, bot.entity.position.z); - return true; - } catch (err) {return true;} - } - } - } - return false; -} diff --git a/src/agent/world.js b/src/agent/library/world.js similarity index 81% rename from src/agent/world.js rename to src/agent/library/world.js index ea72f48..cee82f0 100644 --- a/src/agent/world.js +++ b/src/agent/library/world.js @@ -1,5 +1,41 @@ -import { getAllBlockIds } from '../utils/mcdata.js'; import pf from 'mineflayer-pathfinder'; +import { getAllBlockIds } from '../../utils/mcdata.js'; + + +export function getNearestFreeSpace(bot, size=1, distance=8) { + /** + * Get the nearest empty space with solid blocks beneath it of the given size. + * @param {Bot} bot - The bot to get the nearest free space for. + * @param {number} size - The (size x size) of the space to find, default 1. + * @param {number} distance - The maximum distance to search, default 8. + * @returns {Vec3} - The south west corner position of the nearest free space. + * @example + * let position = world.getNearestFreeSpace(bot, 1, 8); + **/ + let empty_pos = bot.findBlocks({ + matching: (block) => { + return block && block.name == 'air'; + }, + maxDistance: distance, + count: 1000 + }); + for (let i = 0; i < empty_pos.length; i++) { + let empty = true; + for (let x = 0; x < size; x++) { + for (let z = 0; z < size; z++) { + let top = bot.blockAt(empty_pos[i].offset(x, 0, z)); + let bottom = bot.blockAt(empty_pos[i].offset(x, -1, z)); + if (!top || !top.name == 'air' || !bottom || !bottom.diggable) { + empty = false; + break; + } + } + } + if (empty) { + return empty_pos[i]; + } + } +} export function getNearestBlocks(bot, block_types, distance=16, count=1) { @@ -222,3 +258,15 @@ export async function isClearPath(bot, target) { let path = await bot.pathfinder.getPathTo(movements, goal, 100); return path.status === 'success'; } + +export function getBiomeName(bot) { + /** + * Get the name of the biome the bot is in. + * @param {Bot} bot - The bot to get the biome for. + * @returns {string} - The name of the biome. + * @example + * let biome = world.getBiomeName(bot); + **/ + const biomeId = bot.world.getBiome(bot.entity.position); + return mcdata.biomes[biomeId].name; +} diff --git a/src/agent/modes.js b/src/agent/modes.js index c318b3c..20ae23c 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -1,5 +1,7 @@ -import * as skills from './skills.js'; -import * as world from './world.js'; +import * as skills from './library/skills.js'; +import * as world from './library/world.js'; +import * as mc from '../utils/mcdata.js'; + // a mode is a function that is called every tick to respond immediately to the world // it has the following fields: @@ -20,7 +22,7 @@ const modes = [ active: false, update: async function (agent) { if (this.active) return; - const enemy = world.getNearestEntityWhere(agent.bot, entity => skills.isHostile(entity), 8); + const enemy = world.getNearestEntityWhere(agent.bot, entity => mc.isHostile(entity), 8); if (enemy && await world.isClearPath(agent.bot, enemy)) { agent.bot.chat(`Fighting ${enemy.name}!`); execute(this, agent, async () => { @@ -35,8 +37,8 @@ const modes = [ on: true, active: false, update: async function (agent) { - if (agent.idle) { - const huntable = world.getNearestEntityWhere(agent.bot, entity => skills.isHuntable(entity), 8); + if (agent.isIdle()) { + const huntable = world.getNearestEntityWhere(agent.bot, entity => mc.isHuntable(entity), 8); if (huntable && await world.isClearPath(agent.bot, huntable)) { execute(this, agent, async () => { agent.bot.chat(`Hunting ${huntable.name}!`); @@ -52,8 +54,8 @@ const modes = [ on: true, active: false, update: async function (agent) { - if (agent.idle) { - let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 4); + if (agent.isIdle()) { + let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8); if (item && await world.isClearPath(agent.bot, item)) { execute(this, agent, async () => { // wait 2 seconds for the item to settle @@ -71,7 +73,7 @@ const modes = [ active: false, update: function (agent) { if (this.active) return; - if (agent.idle) { + if (agent.isIdle()) { // TODO: check light level instead of nearby torches, block.light is broken const near_torch = world.getNearestBlock(agent.bot, 'torch', 8); if (!near_torch) { @@ -97,7 +99,7 @@ const modes = [ last_entity: null, next_change: 0, update: function (agent) { - if (agent.idle) { + if (agent.isIdle()) { this.active = true; const entity = agent.bot.nearestEntity(); let entity_in_view = entity && entity.position.distanceTo(agent.bot.entity.position) < 10 && entity.name !== 'enderman'; @@ -132,13 +134,10 @@ const modes = [ async function execute(mode, agent, func, timeout=-1) { mode.active = true; - await agent.coder.stop(); - agent.idle = false; let code_return = await agent.coder.execute(async () => { await func(); }, timeout); mode.active = false; - agent.idle = true; console.log(`Mode ${mode.name} finished executing, code_return: ${code_return.message}`); } @@ -168,7 +167,7 @@ class ModeController { this.modes_map[mode_name].paused = true; } - getDocs() { + getStr() { let res = 'Available Modes:'; for (let mode of this.modes_list) { let on = mode.on ? 'ON' : 'OFF'; @@ -178,7 +177,7 @@ class ModeController { } async update() { - if (this.agent.idle) { + if (this.agent.isIdle()) { // other actions might pause a mode to override it // when idle, unpause all modes for (let mode of this.modes_list) { @@ -200,4 +199,4 @@ class ModeController { export function initModes(agent) { // the mode controller is added to the bot object so it is accessible from anywhere the bot is used agent.bot.modes = new ModeController(agent); -} \ No newline at end of file +} diff --git a/src/agent/examples.js b/src/utils/examples.js similarity index 94% rename from src/agent/examples.js rename to src/utils/examples.js index ee76c85..721ff0b 100644 --- a/src/agent/examples.js +++ b/src/utils/examples.js @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; -import { embed, cosineSimilarity } from '../utils/gpt.js'; -import { stringifyTurns } from '../utils/text.js'; +import { embed, cosineSimilarity } from './gpt.js'; +import { stringifyTurns } from './text.js'; export class Examples { diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 0e5f884..dcd80e5 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -33,6 +33,17 @@ export function initBot(username) { return bot; } +export function isHuntable(mob) { + if (!mob || !mob.name) return false; + const animals = ['chicken', 'cod', 'cow', 'llama', 'mooshroom', 'pig', 'pufferfish', 'rabbit', 'salmon', 'sheep', 'squid', 'tropical_fish', 'turtle']; + return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby +} + +export function isHostile(mob) { + if (!mob || !mob.name) return false; + return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; +} + export function getItemId(item) { return mcdata.itemsByName[item].id; } @@ -55,7 +66,6 @@ export function getAllItems(ignore) { return items; } - export function getAllItemIds(ignore) { const items = getAllItems(ignore); let itemIds = []; @@ -65,7 +75,6 @@ export function getAllItemIds(ignore) { return itemIds; } - export function getAllBlocks(ignore) { if (!ignore) { ignore = []; @@ -80,7 +89,6 @@ export function getAllBlocks(ignore) { return blocks; } - export function getAllBlockIds(ignore) { const blocks = getAllBlocks(ignore); let blockIds = []; @@ -89,8 +97,3 @@ export function getAllBlockIds(ignore) { } return blockIds; } - -export function getBiomeName(bot) { - const biomeId = bot.world.getBiome(bot.entity.position); - return mcdata.biomes[biomeId].name; -}