diff --git a/src/agent/agent.js b/src/agent/agent.js index dfaf425..7ef3e05 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -5,6 +5,7 @@ import { Examples } from '../utils/examples.js'; import { initBot } from '../utils/mcdata.js'; import { sendRequest } from '../utils/gpt.js'; import { containsCommand, commandExists, executeCommand } from './commands/index.js'; +import { ItemGoal } from './item_goal.js'; export class Agent { @@ -13,6 +14,7 @@ export class Agent { this.examples = new Examples(); this.history = new History(this); this.coder = new Coder(this); + this.item_goal = new ItemGoal(this); console.log('Loading examples...'); @@ -171,9 +173,13 @@ export class Agent { this.handleMessage('system', `You died with the final message: '${message}'. Previous actions were stopped and you have respawned. Notify the user and perform any necessary actions.`); } }); + this.bot.on('idle', () => { this.bot.modes.unPauseAll(); - this.coder.executeResume(); + if (this.coder.resume_func != null) + this.coder.executeResume(); + else + this.item_goal.executeNext(); }); // This update loop ensures that each update() is called one at a time, even if it takes longer than the interval @@ -188,6 +194,8 @@ export class Agent { } } }, INTERVAL); + + this.bot.emit('idle'); } isIdle() { diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js new file mode 100644 index 0000000..1f76364 --- /dev/null +++ b/src/agent/item_goal.js @@ -0,0 +1,298 @@ +import * as skills from './library/skills.js'; +import * as world from './library/world.js'; +import * as mc from '../utils/mcdata.js'; + + +class ItemNode { + constructor(bot, name, quantity, wrapper) { + this.bot = bot; + this.name = name; + this.quantity = quantity; + this.wrapper = wrapper; + this.type = ''; + this.source = null; + this.prereq = null; + this.recipe = []; + this.fails = 0; + } + + setRecipe(recipe) { + this.type = 'craft'; + let size = 0; + for (let [key, value] of Object.entries(recipe)) { + this.recipe.push(new ItemWrapper(this.bot, key, value * this.quantity, this.wrapper)); + size += value; + } + if (size > 4) { + this.prereq = new ItemWrapper(this.bot, 'crafting_table', 1, this.wrapper); + } + return this; + } + + setCollectable(source=null, tool=null) { + this.type = 'block'; + if (source) + this.source = source; + else + this.source = this.name; + if (tool) + this.prereq = new ItemWrapper(this.bot, tool, 1, this.wrapper); + return this; + } + + setSmeltable(source) { + this.type = 'smelt'; + this.prereq = new ItemWrapper(this.bot, 'furnace', 1, this.wrapper); + this.source = new ItemWrapper(this.bot, source, this.quantity, this.wrapper); + return this; + } + + setHuntable(animal_source) { + this.type = 'hunt'; + this.source = animal_source; + return this; + } + + getChildren() { + let children = []; + for (let child of this.recipe) { + if (child instanceof ItemWrapper && child.methods.length > 0) { + children.push(child); + } + } + if (this.prereq && this.prereq instanceof ItemWrapper && this.prereq.methods.length > 0) { + children.push(this.prereq); + } + return children; + } + + isReady() { + for (let child of this.getChildren()) { + if (!child.isDone()) { + return false; + } + } + return true; + } + + isDone() { + let qualifying = [this.name]; + if (this.name.includes('pickaxe') || + this.name.includes('axe') || + this.name.includes('shovel') || + this.name.includes('hoe') || + this.name.includes('sword')) { + let material = this.name.split('_')[0]; + let type = this.name.split('_')[1]; + if (material === 'wooden') { + qualifying.push('stone_' + type); + qualifying.push('iron_' + type); + qualifying.push('gold_' + type); + qualifying.push('diamond_' + type); + } else if (material === 'stone') { + qualifying.push('iron_' + type); + qualifying.push('gold_' + type); + qualifying.push('diamond_' + type); + } else if (material === 'iron') { + qualifying.push('gold_' + type); + qualifying.push('diamond_' + type); + } else if (material === 'gold') { + qualifying.push('diamond_' + type); + } + } + for (let item of qualifying) { + if (world.getInventoryCounts(this.bot)[item] >= this.quantity) { + return true; + } + } + return false; + } + + getDepth() { + if (this.isDone()) { + return 0; + } + let depth = 0; + for (let child of this.getChildren()) { + depth = Math.max(depth, child.getDepth()); + } + return depth + 1; + } + + getFails() { + if (this.isDone()) { + return 0; + } + let fails = 0; + for (let child of this.getChildren()) { + fails += child.getFails(); + } + return fails + this.fails; + } + + getNext() { + if (this.isReady()) { + return this; + } + let furthest_depth = -1; + let furthest_child = null; + for (let child of this.getChildren()) { + let depth = child.getDepth(); + if (depth > furthest_depth) { + furthest_depth = depth; + furthest_child = child; + } + } + return furthest_child.getNext(); + } + + async execute() { + if (!this.isReady()) { + this.fails += 1; + return; + } + if (this.type === 'block') { + await skills.collectBlock(this.bot, this.source, this.quantity); + } else if (this.type === 'smelt') { + await skills.smeltItem(this.bot, this.name, this.quantity); + } else if (this.type === 'hunt') { + for (let i = 0; i < this.quantity; i++) { + let res = await skills.attackNearest(this.bot, this.source); + if (!res) break; + } + } else if (this.type === 'craft') { + await skills.craftRecipe(this.bot, this.name, this.quantity); + } + if (!this.isDone()) { + this.fails += 1; + } + } +} + + +class ItemWrapper { + constructor(bot, name, quantity, parent=null) { + this.bot = bot; + this.name = name; + this.quantity = quantity; + this.parent = parent; + this.methods = []; + + if (!this.containsCircularDependency()) { + this.createChildren(); + } + } + + createChildren() { + let recipes = mc.getItemCraftingRecipes(this.name); + if (recipes) { + for (let recipe of recipes) { + this.methods.push(new ItemNode(this.bot, this.name, this.quantity, this).setRecipe(recipe)); + } + } + + let block_source = mc.getItemBlockSource(this.name); + if (block_source) { + let tool = mc.getBlockTool(block_source); + this.methods.push(new ItemNode(this.bot, this.name, this.quantity, this).setCollectable(block_source, tool)); + } + + let smeltingIngredient = mc.getItemSmeltingIngredient(this.name); + if (smeltingIngredient) { + this.methods.push(new ItemNode(this.bot, this.name, this.quantity, this).setSmeltable(smeltingIngredient)); + } + + let animal_source = mc.getItemAnimalSource(this.name); + if (animal_source) { + this.methods.push(new ItemNode(this.bot, this.name, this.quantity, this).setHuntable(animal_source)); + } + } + + containsCircularDependency() { + let p = this.parent; + while (p) { + if (p.name === this.name) { + return true; + } + p = p.parent; + } + return false; + } + + getBestMethod() { + let best_cost = -1; + let best_method = null; + for (let method of this.methods) { + let cost = method.getDepth() + method.getFails(); + if (best_cost == -1 || cost < best_cost) { + best_cost = cost; + best_method = method; + } + } + return best_method + } + + getChildren() { + if (this.methods.length === 0) + return []; + return this.getBestMethod().getChildren(); + } + + isReady() { + if (this.methods.length === 0) + return false; + return this.getBestMethod().isReady(); + } + + isDone() { + if (this.methods.length === 0) + return true; + return this.getBestMethod().isDone(); + } + + getDepth() { + if (this.methods.length === 0) + return 0; + return this.getBestMethod().getDepth(); + } + + getFails() { + if (this.methods.length === 0) + return 0; + return this.getBestMethod().getFails(); + } + + getNext() { + if (this.methods.length === 0) + return null; + return this.getBestMethod().getNext(); + } +} + + +export class ItemGoal { + constructor(agent, timeout=-1) { + this.agent = agent; + this.timeout = timeout; + this.goal = null; + } + + setGoal(goal, quantity=1) { + this.goal = new ItemWrapper(this.agent.bot, goal, quantity); + } + + async executeNext() { + await new Promise(resolve => setTimeout(resolve, 500)); + let next = this.goal.getNext(); + + await this.agent.coder.execute(async () => { + await next.execute(); + }, this.timeout); + + if (next.isDone()) { + console.log(`Successfully obtained ${next.quantity} ${next.name} for goal ${this.goal.name}`); + } else { + console.log(`Failed to obtain ${next.quantity} ${next.name} for goal ${this.goal.name}`); + } + } +} diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index d5abb0b..648cabe 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -35,7 +35,7 @@ function equipHighestAttack(bot) { } -export async function craftRecipe(bot, itemName) { +export async function craftRecipe(bot, itemName, num=1) { /** * Attempt to craft the given item name from a recipe. May craft many items. * @param {MinecraftBot} bot, reference to the minecraft bot. @@ -85,7 +85,7 @@ export async function craftRecipe(bot, itemName) { const recipe = recipes[0]; console.log('crafting...'); - await bot.craft(recipe, 1, craftingTable); + await bot.craft(recipe, num, craftingTable); log(bot, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`); if (placedTable) { await collectBlock(bot, 'crafting_table', 1); @@ -114,7 +114,16 @@ export async function smeltItem(bot, itemName, num=1) { let furnaceBlock = undefined; furnaceBlock = world.getNearestBlock(bot, 'furnace', 6); if (!furnaceBlock){ - log(bot, `There is no furnace nearby.`) + // Try to place furnace + let hasFurnace = world.getInventoryCounts(bot)['furnace'] > 0; + if (hasFurnace) { + let pos = world.getNearestFreeSpace(bot, 1, 6); + await placeBlock(bot, 'furnace', pos.x, pos.y, pos.z); + furnaceBlock = world.getNearestBlock(bot, 'furnace', 6); + } + } + if (!furnaceBlock){ + log(bot, `There is no furnace nearby and you have no furnace.`) return false; } await bot.lookAt(furnaceBlock.position); diff --git a/src/agent/library/world.js b/src/agent/library/world.js index 50cdfcc..2bb00bb 100644 --- a/src/agent/library/world.js +++ b/src/agent/library/world.js @@ -172,7 +172,6 @@ export function getInventoryCounts(bot) { inventory[item.name] += item.count; } } - console.log(inventory) return inventory; } diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 7c8a0a9..613d563 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -42,12 +42,20 @@ export function isHostile(mob) { return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; } -export function getItemId(item) { - return mcdata.itemsByName[item].id; +export function getItemId(itemName) { + let item = mcdata.itemsByName[itemName]; + if (item) { + return item.id; + } + return null; } export function getItemName(itemId) { - return mcdata.items[itemId].name; + let item = mcdata.items[itemId] + if (item) { + return item.name; + } + return null; } export function getAllItems(ignore) { @@ -98,4 +106,80 @@ export function getAllBlockIds(ignore) { export function getAllBiomes() { return mcdata.biomes; +} + +export function getItemCraftingRecipes(itemName) { + let itemId = getItemId(itemName); + if (!mcdata.recipes[itemId]) { + return null; + } + + let recipes = []; + for (let r of mcdata.recipes[itemId]) { + let recipe = {}; + let ingredients = []; + if (r.ingredients) { + ingredients = r.ingredients; + } else if (r.inShape) { + ingredients = r.inShape.flat(); + } + for (let ingredient of ingredients) { + let ingredientName = getItemName(ingredient); + if (ingredientName === null) continue; + if (!recipe[ingredientName]) + recipe[ingredientName] = 0; + recipe[ingredientName]++; + } + recipes.push(recipe); + } + + return recipes; +} + +export function getItemSmeltingIngredient(itemName) { + return { + baked_potato: 'potato', + steak: 'raw_beef', + cooked_chicken: 'raw_chicken', + cooked_cod: 'raw_cod', + cooked_mutton: 'raw_mutton', + cooked_porkchop: 'raw_porkchop', + cooked_rabbit: 'raw_rabbit', + cooked_salmon: 'raw_salmon', + dried_kelp: 'kelp', + iron_ingot: 'raw_iron', + gold_ingot: 'raw_gold', + copper_ingot: 'raw_copper', + glass: 'sand' + }[itemName]; +} + +export function getItemBlockSource(itemName) { + let itemId = getItemId(itemName); + for (let block of getAllBlocks()) { + if (block.drops.includes(itemId)) { + return block.name; + } + } + return null; +} + +export function getItemAnimalSource(itemName) { + return { + raw_beef: 'cow', + raw_chicken: 'chicken', + raw_cod: 'cod', + raw_mutton: 'sheep', + raw_porkchop: 'pig', + raw_rabbit: 'rabbit', + raw_salmon: 'salmon' + }[itemName]; +} + +export function getBlockTool(blockName) { + let block = mcdata.blocksByName[blockName]; + if (!block || !block.harvestTools) { + return null; + } + return getItemName(Object.keys(block.harvestTools)[0]); // Double check first tool is always simplest } \ No newline at end of file