From 21a9995f62444f149511a8c9c396f5089317f970 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 12 Feb 2024 16:33:28 -0800 Subject: [PATCH 01/20] init item goals --- src/agent/agent.js | 10 +- src/agent/item_goal.js | 298 ++++++++++++++++++++++++++++++++++++ src/agent/library/skills.js | 15 +- src/agent/library/world.js | 1 - src/utils/mcdata.js | 90 ++++++++++- 5 files changed, 406 insertions(+), 8 deletions(-) create mode 100644 src/agent/item_goal.js 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 From 899aad7f81791c9d9ada36f7f2bfe3fd079799f9 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 12 Feb 2024 17:37:21 -0800 Subject: [PATCH 02/20] saved nodes 2 --- src/agent/item_goal.js | 117 ++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 48 deletions(-) diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js index 1f76364..f3db618 100644 --- a/src/agent/item_goal.js +++ b/src/agent/item_goal.js @@ -4,11 +4,10 @@ import * as mc from '../utils/mcdata.js'; class ItemNode { - constructor(bot, name, quantity, wrapper) { - this.bot = bot; - this.name = name; - this.quantity = quantity; + constructor(manager, wrapper, name) { + this.manager = manager; this.wrapper = wrapper; + this.name = name; this.type = ''; this.source = null; this.prereq = null; @@ -20,11 +19,15 @@ class ItemNode { 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)); + if (this.manager.nodes[key] === undefined) + this.manager.nodes[key] = new ItemWrapper(this.manager, this.wrapper, key); + this.recipe.push([this.manager.nodes[key], value]); size += value; } if (size > 4) { - this.prereq = new ItemWrapper(this.bot, 'crafting_table', 1, this.wrapper); + if (this.manager.nodes['crafting_table'] === undefined) + this.manager.nodes['crafting_table'] = new ItemWrapper(this.manager, this.wrapper, 'crafting_table'); + this.prereq = this.manager.nodes['crafting_table']; } return this; } @@ -35,15 +38,22 @@ class ItemNode { this.source = source; else this.source = this.name; - if (tool) - this.prereq = new ItemWrapper(this.bot, tool, 1, this.wrapper); + if (tool) { + if (this.manager.nodes[tool] === undefined) + this.manager.nodes[tool] = new ItemWrapper(this.manager, this.wrapper, tool); + this.prereq = this.manager.nodes[tool]; + } return this; } - setSmeltable(source) { + setSmeltable(source_item) { this.type = 'smelt'; - this.prereq = new ItemWrapper(this.bot, 'furnace', 1, this.wrapper); - this.source = new ItemWrapper(this.bot, source, this.quantity, this.wrapper); + if (this.manager.nodes['furnace'] === undefined) + this.manager.nodes['furnace'] = new ItemWrapper(this.manager, this.wrapper, 'furnace'); + this.prereq = this.manager.nodes['furnace']; + if (this.manager.nodes[source_item] === undefined) + this.manager.nodes[source_item] = new ItemWrapper(this.manager, this.wrapper, source_item); + this.source = this.manager.nodes[source_item]; return this; } @@ -56,26 +66,26 @@ class ItemNode { getChildren() { let children = []; for (let child of this.recipe) { - if (child instanceof ItemWrapper && child.methods.length > 0) { + if (child[0] instanceof ItemWrapper && child[0].methods.length > 0) { children.push(child); } } if (this.prereq && this.prereq instanceof ItemWrapper && this.prereq.methods.length > 0) { - children.push(this.prereq); + children.push([this.prereq, 1]); } return children; } isReady() { - for (let child of this.getChildren()) { - if (!child.isDone()) { + for (let [child, quantity] of this.getChildren()) { + if (!child.isDone(quantity)) { return false; } } return true; } - isDone() { + isDone(quantity=1) { let qualifying = [this.name]; if (this.name.includes('pickaxe') || this.name.includes('axe') || @@ -101,31 +111,31 @@ class ItemNode { } } for (let item of qualifying) { - if (world.getInventoryCounts(this.bot)[item] >= this.quantity) { + if (world.getInventoryCounts(this.manager.agent.bot)[item] >= quantity) { return true; } } return false; } - getDepth() { - if (this.isDone()) { + getDepth(quantity=1) { + if (this.isDone(quantity)) { return 0; } let depth = 0; - for (let child of this.getChildren()) { - depth = Math.max(depth, child.getDepth()); + for (let [child, quantity] of this.getChildren()) { + depth = Math.max(depth, child.getDepth(quantity)); } return depth + 1; } - getFails() { - if (this.isDone()) { + getFails(quantity=1) { + if (this.isDone(quantity)) { return 0; } let fails = 0; - for (let child of this.getChildren()) { - fails += child.getFails(); + for (let [child, quantity] of this.getChildren()) { + fails += child.getFails(quantity); } return fails + this.fails; } @@ -136,7 +146,7 @@ class ItemNode { } let furthest_depth = -1; let furthest_child = null; - for (let child of this.getChildren()) { + for (let [child, quantity] of this.getChildren()) { let depth = child.getDepth(); if (depth > furthest_depth) { furthest_depth = depth; @@ -151,19 +161,18 @@ class ItemNode { this.fails += 1; return; } + let init_quantity = world.getInventoryCounts(this.manager.agent.bot)[this.name] || 0; if (this.type === 'block') { - await skills.collectBlock(this.bot, this.source, this.quantity); + await skills.collectBlock(this.manager.agent.bot, this.source); } else if (this.type === 'smelt') { - await skills.smeltItem(this.bot, this.name, this.quantity); + await skills.smeltItem(this.manager.agent.bot, this.name); } 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; - } + await skills.attackNearest(this.manager.agent.bot, this.source); } else if (this.type === 'craft') { - await skills.craftRecipe(this.bot, this.name, this.quantity); + await skills.craftRecipe(this.manager.agent.bot, this.name); } - if (!this.isDone()) { + let final_quantity = world.getInventoryCounts(this.manager.agent.bot)[this.name] || 0; + if (final_quantity <= init_quantity) { this.fails += 1; } } @@ -171,10 +180,9 @@ class ItemNode { class ItemWrapper { - constructor(bot, name, quantity, parent=null) { - this.bot = bot; + constructor(manager, parent, name) { + this.manager = manager; this.name = name; - this.quantity = quantity; this.parent = parent; this.methods = []; @@ -187,24 +195,24 @@ class ItemWrapper { 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)); + this.methods.push(new ItemNode(this.manager, this, this.name).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)); + this.methods.push(new ItemNode(this.manager, this, this.name).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)); + this.methods.push(new ItemNode(this.manager, this, this.name).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)); + this.methods.push(new ItemNode(this.manager, this, this.name).setHuntable(animal_source)); } } @@ -244,10 +252,10 @@ class ItemWrapper { return this.getBestMethod().isReady(); } - isDone() { + isDone(quantity=1) { if (this.methods.length === 0) return true; - return this.getBestMethod().isDone(); + return this.getBestMethod().isDone(quantity); } getDepth() { @@ -275,24 +283,37 @@ export class ItemGoal { this.agent = agent; this.timeout = timeout; this.goal = null; + this.quantity = 1; + this.nodes = {}; } setGoal(goal, quantity=1) { - this.goal = new ItemWrapper(this.agent.bot, goal, quantity); + this.quantity = quantity; + if (this.nodes[goal] === undefined) + this.nodes[goal] = new ItemWrapper(this, null, goal); + this.goal = this.nodes[goal]; } async executeNext() { - await new Promise(resolve => setTimeout(resolve, 500)); let next = this.goal.getNext(); + // Prevent unnecessary attempts to obtain blocks that are not nearby + if (next.type === 'block' && !world.getNearbyBlockTypes(this.agent.bot).includes(next.source)) { + next.fails += 1; + await new Promise((resolve) => setTimeout(resolve, 500)); + this.agent.bot.emit('idle'); + return; + } + let init_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; await this.agent.coder.execute(async () => { await next.execute(); }, this.timeout); + let final_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; - if (next.isDone()) { - console.log(`Successfully obtained ${next.quantity} ${next.name} for goal ${this.goal.name}`); + if (final_quantity > init_quantity) { + console.log(`Successfully obtained ${next.name} for goal ${this.goal.name}`); } else { - console.log(`Failed to obtain ${next.quantity} ${next.name} for goal ${this.goal.name}`); + console.log(`Failed to obtain ${next.name} for goal ${this.goal.name}`); } } } From d5e118fa3a66c227654dbd4d70b3914b533d7583 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 12 Feb 2024 17:48:22 -0800 Subject: [PATCH 03/20] optimization and bug fix --- src/agent/agent.js | 1 + src/agent/item_goal.js | 29 +++++++++++++---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 7ef3e05..ff7f1af 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -15,6 +15,7 @@ export class Agent { this.history = new History(this); this.coder = new Coder(this); this.item_goal = new ItemGoal(this); + this.item_goal.setGoal('iron_pickaxe', 1); console.log('Loading examples...'); diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js index f3db618..d78724e 100644 --- a/src/agent/item_goal.js +++ b/src/agent/item_goal.js @@ -118,8 +118,8 @@ class ItemNode { return false; } - getDepth(quantity=1) { - if (this.isDone(quantity)) { + getDepth(q=1) { + if (this.isDone(q)) { return 0; } let depth = 0; @@ -129,8 +129,8 @@ class ItemNode { return depth + 1; } - getFails(quantity=1) { - if (this.isDone(quantity)) { + getFails(q=1) { + if (this.isDone(q)) { return 0; } let fails = 0; @@ -144,16 +144,12 @@ class ItemNode { if (this.isReady()) { return this; } - let furthest_depth = -1; - let furthest_child = null; for (let [child, quantity] of this.getChildren()) { - let depth = child.getDepth(); - if (depth > furthest_depth) { - furthest_depth = depth; - furthest_child = child; - } + let res = child.getNext(); + if (res) + return res; } - return furthest_child.getNext(); + return null; } async execute() { @@ -258,16 +254,16 @@ class ItemWrapper { return this.getBestMethod().isDone(quantity); } - getDepth() { + getDepth(q=1) { if (this.methods.length === 0) return 0; - return this.getBestMethod().getDepth(); + return this.getBestMethod().getDepth(q); } - getFails() { + getFails(q=1) { if (this.methods.length === 0) return 0; - return this.getBestMethod().getFails(); + return this.getBestMethod().getFails(q); } getNext() { @@ -296,6 +292,7 @@ export class ItemGoal { async executeNext() { let next = this.goal.getNext(); + // Prevent unnecessary attempts to obtain blocks that are not nearby if (next.type === 'block' && !world.getNearbyBlockTypes(this.agent.bot).includes(next.source)) { next.fails += 1; From 5a1e57bd42bdfd8b9708316d598d65dab6c05fa8 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 12 Feb 2024 17:54:03 -0800 Subject: [PATCH 04/20] set interruptible --- src/agent/item_goal.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js index d78724e..5db9bce 100644 --- a/src/agent/item_goal.js +++ b/src/agent/item_goal.js @@ -301,10 +301,18 @@ export class ItemGoal { return; } + // Wait for the bot to be idle before attempting to execute the next goal + if (!this.agent.isIdle()) { + await new Promise((resolve) => setTimeout(resolve, 500)); + return; + } + let init_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; + this.agent.coder.interruptible = true; await this.agent.coder.execute(async () => { await next.execute(); }, this.timeout); + this.agent.coder.interruptible = false; let final_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; if (final_quantity > init_quantity) { From 57783fb4ca3f5a0ecba04d4a32f4db267a274835 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 12 Feb 2024 22:43:31 -0800 Subject: [PATCH 05/20] load goals, logic fixes --- bots/andy/assist.json | 1 + bots/andy/survive.json | 19 +++++++ src/agent/agent.js | 2 +- src/agent/history.js | 3 + src/agent/item_goal.js | 122 ++++++++++++++++++++++------------------- 5 files changed, 89 insertions(+), 58 deletions(-) create mode 100644 bots/andy/survive.json diff --git a/bots/andy/assist.json b/bots/andy/assist.json index 680b4be..eb47577 100644 --- a/bots/andy/assist.json +++ b/bots/andy/assist.json @@ -2,5 +2,6 @@ "name": "andy", "bio": "You are playing minecraft and assisting other players in tasks.", "memory": "", + "goals": [], "turns": [] } \ No newline at end of file diff --git a/bots/andy/survive.json b/bots/andy/survive.json new file mode 100644 index 0000000..159bc0b --- /dev/null +++ b/bots/andy/survive.json @@ -0,0 +1,19 @@ +{ + "name": "andy", + "bio": "You are playing minecraft and assisting other players in tasks.", + "memory": "", + "goals": [ + "wooden_pickaxe", + "torch", + "stone_pickaxe", + "stone_sword", + "leather_tunic", + "iron_pickaxe", + "iron_sword", + "iron_helmet", + "iron_boots", + "iron_leggings", + "iron_chestplate" + ], + "turns": [] +} \ No newline at end of file diff --git a/src/agent/agent.js b/src/agent/agent.js index ff7f1af..5fe4633 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -15,11 +15,11 @@ export class Agent { this.history = new History(this); this.coder = new Coder(this); this.item_goal = new ItemGoal(this); - this.item_goal.setGoal('iron_pickaxe', 1); console.log('Loading examples...'); this.history.load(profile); + this.item_goal.setGoals(this.history.goals); await this.examples.load('./src/examples.json'); await this.coder.load(); diff --git a/src/agent/history.js b/src/agent/history.js index 1e020d6..2614531 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -13,6 +13,7 @@ export class History { // These define an agent's long term memory this.bio = ''; this.memory = ''; + this.goals = []; // Variables for controlling the agent's memory and knowledge this.max_messages = 20; @@ -88,6 +89,7 @@ export class History { 'name': this.name, 'bio': this.bio, 'memory': this.memory, + 'goals': this.goals, 'turns': this.turns }; const json_data = JSON.stringify(data, null, 4); @@ -108,6 +110,7 @@ export class History { this.bio = obj.bio; this.memory = obj.memory; this.turns = obj.turns; + this.goals = obj.goals; } catch (err) { console.error(`No file for profile '${load_path}' for agent ${this.name}.`); } diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js index 5db9bce..c130c07 100644 --- a/src/agent/item_goal.js +++ b/src/agent/item_goal.js @@ -18,10 +18,11 @@ class ItemNode { setRecipe(recipe) { this.type = 'craft'; let size = 0; + this.recipe = []; for (let [key, value] of Object.entries(recipe)) { if (this.manager.nodes[key] === undefined) this.manager.nodes[key] = new ItemWrapper(this.manager, this.wrapper, key); - this.recipe.push([this.manager.nodes[key], value]); + this.recipe.push({node: this.manager.nodes[key], quantity: value}); size += value; } if (size > 4) { @@ -64,21 +65,16 @@ class ItemNode { } getChildren() { - let children = []; - for (let child of this.recipe) { - if (child[0] instanceof ItemWrapper && child[0].methods.length > 0) { - children.push(child); - } - } - if (this.prereq && this.prereq instanceof ItemWrapper && this.prereq.methods.length > 0) { - children.push([this.prereq, 1]); + let children = [...this.recipe]; + if (this.prereq) { + children.push({node: this.prereq, quantity: 1}); } return children; } isReady() { - for (let [child, quantity] of this.getChildren()) { - if (!child.isDone(quantity)) { + for (let child of this.getChildren()) { + if (!child.node.isDone(child.quantity)) { return false; } } @@ -123,8 +119,8 @@ class ItemNode { return 0; } let depth = 0; - for (let [child, quantity] of this.getChildren()) { - depth = Math.max(depth, child.getDepth(quantity)); + for (let child of this.getChildren()) { + depth = Math.max(depth, child.node.getDepth(child.quantity)); } return depth + 1; } @@ -134,18 +130,19 @@ class ItemNode { return 0; } let fails = 0; - for (let [child, quantity] of this.getChildren()) { - fails += child.getFails(quantity); + for (let child of this.getChildren()) { + fails += child.node.getFails(child.quantity); } return fails + this.fails; } - getNext() { - if (this.isReady()) { + getNext(q=1) { + if (this.isDone(q)) + return null; + if (this.isReady()) return this; - } - for (let [child, quantity] of this.getChildren()) { - let res = child.getNext(); + for (let child of this.getChildren()) { + let res = child.node.getNext(child.quantity); if (res) return res; } @@ -187,28 +184,37 @@ class ItemWrapper { } } + add_method(method) { + for (let child of method.getChildren()) { + if (child.node.methods.length === 0) + return; + } + this.methods.push(method); + } + + createChildren() { let recipes = mc.getItemCraftingRecipes(this.name); if (recipes) { for (let recipe of recipes) { - this.methods.push(new ItemNode(this.manager, this, this.name).setRecipe(recipe)); + this.add_method(new ItemNode(this.manager, this, this.name).setRecipe(recipe)) } } let block_source = mc.getItemBlockSource(this.name); if (block_source) { let tool = mc.getBlockTool(block_source); - this.methods.push(new ItemNode(this.manager, this, this.name).setCollectable(block_source, tool)); + this.add_method(new ItemNode(this.manager, this, this.name).setCollectable(block_source, tool)); } let smeltingIngredient = mc.getItemSmeltingIngredient(this.name); if (smeltingIngredient) { - this.methods.push(new ItemNode(this.manager, this, this.name).setSmeltable(smeltingIngredient)); + this.add_method(new ItemNode(this.manager, this, this.name).setSmeltable(smeltingIngredient)); } let animal_source = mc.getItemAnimalSource(this.name); if (animal_source) { - this.methods.push(new ItemNode(this.manager, this, this.name).setHuntable(animal_source)); + this.add_method(new ItemNode(this.manager, this, this.name).setHuntable(animal_source)); } } @@ -223,11 +229,11 @@ class ItemWrapper { return false; } - getBestMethod() { + getBestMethod(q=1) { let best_cost = -1; let best_method = null; for (let method of this.methods) { - let cost = method.getDepth() + method.getFails(); + let cost = method.getDepth(q) + method.getFails(q); if (best_cost == -1 || cost < best_cost) { best_cost = cost; best_method = method; @@ -236,40 +242,28 @@ class ItemWrapper { return best_method } - getChildren() { - if (this.methods.length === 0) - return []; - return this.getBestMethod().getChildren(); - } - - isReady() { + isDone(q=1) { if (this.methods.length === 0) return false; - return this.getBestMethod().isReady(); - } - - isDone(quantity=1) { - if (this.methods.length === 0) - return true; - return this.getBestMethod().isDone(quantity); + return this.getBestMethod(q).isDone(q); } getDepth(q=1) { if (this.methods.length === 0) return 0; - return this.getBestMethod().getDepth(q); + return this.getBestMethod(q).getDepth(q); } getFails(q=1) { if (this.methods.length === 0) return 0; - return this.getBestMethod().getFails(q); + return this.getBestMethod(q).getFails(q); } - getNext() { + getNext(q=1) { if (this.methods.length === 0) return null; - return this.getBestMethod().getNext(); + return this.getBestMethod(q).getNext(q); } } @@ -278,20 +272,34 @@ export class ItemGoal { constructor(agent, timeout=-1) { this.agent = agent; this.timeout = timeout; - this.goal = null; - this.quantity = 1; + this.goals = []; this.nodes = {}; } - setGoal(goal, quantity=1) { - this.quantity = quantity; - if (this.nodes[goal] === undefined) - this.nodes[goal] = new ItemWrapper(this, null, goal); - this.goal = this.nodes[goal]; + setGoals(goals) { + this.goals = [] + for (let goal of goals) { + this.goals.push({name: goal, quantity: 1}) + } } async executeNext() { - let next = this.goal.getNext(); + // Get goal by priority + let goal = null; + let inventory = world.getInventoryCounts(this.agent.bot); + for (let g of this.goals) { + if (inventory[g.name] === undefined || inventory[g.name] < g.quantity) { + if (this.nodes[g.name] === undefined) + this.nodes[g.name] = new ItemWrapper(this, null, g.name); + goal = this.nodes[g.name]; + break; + } + } + if (goal === null) + return; + + // Get next goal to execute + let next = goal.getNext(); // Prevent unnecessary attempts to obtain blocks that are not nearby if (next.type === 'block' && !world.getNearbyBlockTypes(this.agent.bot).includes(next.source)) { @@ -302,11 +310,10 @@ export class ItemGoal { } // Wait for the bot to be idle before attempting to execute the next goal - if (!this.agent.isIdle()) { - await new Promise((resolve) => setTimeout(resolve, 500)); + if (!this.agent.isIdle()) return; - } + // Execute the next goal let init_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; this.agent.coder.interruptible = true; await this.agent.coder.execute(async () => { @@ -315,10 +322,11 @@ export class ItemGoal { this.agent.coder.interruptible = false; let final_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; + // Log the result of the goal attempt if (final_quantity > init_quantity) { - console.log(`Successfully obtained ${next.name} for goal ${this.goal.name}`); + console.log(`Successfully obtained ${next.name} for goal ${goal.name}`); } else { - console.log(`Failed to obtain ${next.name} for goal ${this.goal.name}`); + console.log(`Failed to obtain ${next.name} for goal ${goal.name}`); } } } From 6b123c275ea7c31c206c169e3f2060ee2a85636b Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 12 Feb 2024 22:53:26 -0800 Subject: [PATCH 06/20] hunting fix --- bots/andy/survive.json | 2 +- src/agent/item_goal.js | 4 ++-- src/utils/mcdata.js | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bots/andy/survive.json b/bots/andy/survive.json index 159bc0b..4f34cc1 100644 --- a/bots/andy/survive.json +++ b/bots/andy/survive.json @@ -7,7 +7,7 @@ "torch", "stone_pickaxe", "stone_sword", - "leather_tunic", + "leather_chestplate", "iron_pickaxe", "iron_sword", "iron_helmet", diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js index c130c07..e476f76 100644 --- a/src/agent/item_goal.js +++ b/src/agent/item_goal.js @@ -192,7 +192,6 @@ class ItemWrapper { this.methods.push(method); } - createChildren() { let recipes = mc.getItemCraftingRecipes(this.name); if (recipes) { @@ -302,7 +301,8 @@ export class ItemGoal { let next = goal.getNext(); // Prevent unnecessary attempts to obtain blocks that are not nearby - if (next.type === 'block' && !world.getNearbyBlockTypes(this.agent.bot).includes(next.source)) { + if (next.type === 'block' && !world.getNearbyBlockTypes(this.agent.bot).includes(next.source) || + next.type === 'hunt' && !world.getNearbyEntityTypes(this.agent.bot).includes(next.source)) { next.fails += 1; await new Promise((resolve) => setTimeout(resolve, 500)); this.agent.bot.emit('idle'); diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 613d563..b026be2 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -172,7 +172,9 @@ export function getItemAnimalSource(itemName) { raw_mutton: 'sheep', raw_porkchop: 'pig', raw_rabbit: 'rabbit', - raw_salmon: 'salmon' + raw_salmon: 'salmon', + leather: 'cow', + wool: 'sheep' }[itemName]; } From 16c8192872b31c3c71360a8a78f3062873cf6920 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 12 Feb 2024 23:42:19 -0800 Subject: [PATCH 07/20] smelting fixes --- src/agent/item_goal.js | 60 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js index e476f76..e1552f3 100644 --- a/src/agent/item_goal.js +++ b/src/agent/item_goal.js @@ -3,6 +3,21 @@ import * as world from './library/world.js'; import * as mc from '../utils/mcdata.js'; +const blacklist = [ + 'coal_block', + 'iron_block', + 'gold_block', + 'diamond_block', + 'deepslate', + 'blackstone', + 'netherite', + '_wood', + 'stripped_', + 'crimson', + 'warped' +] + + class ItemNode { constructor(manager, wrapper, name) { this.manager = manager; @@ -52,9 +67,15 @@ class ItemNode { if (this.manager.nodes['furnace'] === undefined) this.manager.nodes['furnace'] = new ItemWrapper(this.manager, this.wrapper, 'furnace'); this.prereq = this.manager.nodes['furnace']; + if (this.manager.nodes[source_item] === undefined) this.manager.nodes[source_item] = new ItemWrapper(this.manager, this.wrapper, source_item); - this.source = this.manager.nodes[source_item]; + if (this.manager.nodes['coal'] === undefined) + this.manager.nodes['coal'] = new ItemWrapper(this.manager, this.wrapper, 'coal'); + this.recipe = [ + {node: this.manager.nodes[source_item], quantity: 1}, + {node: this.manager.nodes['coal'], quantity: 1} + ]; return this; } @@ -140,7 +161,7 @@ class ItemNode { if (this.isDone(q)) return null; if (this.isReady()) - return this; + return {node: this, quantity: q}; for (let child of this.getChildren()) { let res = child.node.getNext(child.quantity); if (res) @@ -149,20 +170,27 @@ class ItemNode { return null; } - async execute() { + async execute(quantity=1) { if (!this.isReady()) { this.fails += 1; return; } - let init_quantity = world.getInventoryCounts(this.manager.agent.bot)[this.name] || 0; + let inventory = world.getInventoryCounts(this.manager.agent.bot); + let init_quantity = inventory[this.name] || 0; if (this.type === 'block') { - await skills.collectBlock(this.manager.agent.bot, this.source); + await skills.collectBlock(this.manager.agent.bot, this.source, quantity); } else if (this.type === 'smelt') { - await skills.smeltItem(this.manager.agent.bot, this.name); + let to_smelt_name = this.recipe[0].node.name; + let to_smelt_quantity = Math.min(quantity, inventory[to_smelt_name] || 1); + await skills.smeltItem(this.manager.agent.bot, to_smelt_name, to_smelt_quantity); } else if (this.type === 'hunt') { - await skills.attackNearest(this.manager.agent.bot, this.source); + for (let i=0; i { - await next.execute(); + await next.execute(quantity); }, this.timeout); this.agent.coder.interruptible = false; let final_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; From 214999df2fa41c1b34f88f95005bfd5a10b5d96e Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 27 Feb 2024 14:45:47 -0800 Subject: [PATCH 08/20] wandering for goal, and tool goal fix --- src/agent/agent.js | 9 ++++++++- src/agent/item_goal.js | 24 ++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 5fe4633..e5d0175 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -175,8 +175,15 @@ export class Agent { } }); - this.bot.on('idle', () => { + this.bot.on('idle', async () => { + // Resume all paused modes this.bot.modes.unPauseAll(); + + // Wait a while for inputs before acting independently + await new Promise((resolve) => setTimeout(resolve, 2000)); + if (!this.isIdle()) return; + + // Resume behavior or persue goal if (this.coder.resume_func != null) this.coder.executeResume(); else diff --git a/src/agent/item_goal.js b/src/agent/item_goal.js index e1552f3..3c15ccf 100644 --- a/src/agent/item_goal.js +++ b/src/agent/item_goal.js @@ -309,6 +309,7 @@ export class ItemGoal { this.timeout = timeout; this.goals = []; this.nodes = {}; + this.failed = []; } setGoals(goals) { @@ -321,11 +322,10 @@ export class ItemGoal { async executeNext() { // Get goal by priority let goal = null; - let inventory = world.getInventoryCounts(this.agent.bot); for (let g of this.goals) { - if (inventory[g.name] === undefined || inventory[g.name] < g.quantity) { - if (this.nodes[g.name] === undefined) - this.nodes[g.name] = new ItemWrapper(this, null, g.name); + if (this.nodes[g.name] === undefined) + this.nodes[g.name] = new ItemWrapper(this, null, g.name); + if (!this.nodes[g.name].isDone(g.quantity)) { goal = this.nodes[g.name]; break; } @@ -342,8 +342,20 @@ export class ItemGoal { if (next.type === 'block' && !world.getNearbyBlockTypes(this.agent.bot).includes(next.source) || next.type === 'hunt' && !world.getNearbyEntityTypes(this.agent.bot).includes(next.source)) { next.fails += 1; - await new Promise((resolve) => setTimeout(resolve, 500)); - this.agent.bot.emit('idle'); + + // If the bot has failed to obtain the block before, explore + if (this.failed.includes(next.name)) { + this.failed = this.failed.filter((item) => item !== next.name); + this.agent.coder.interruptible = true; + await this.agent.coder.execute(async () => { + await skills.moveAway(this.agent.bot, 8); + }, this.timeout); + this.agent.coder.interruptible = false; + } else { + this.failed.push(next.name); + await new Promise((resolve) => setTimeout(resolve, 500)); + this.agent.bot.emit('idle'); + } return; } From 2f53a6729b7c28c1cea41c2207518e54ee3c5580 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 5 Mar 2024 12:05:46 -0800 Subject: [PATCH 09/20] npc refactor --- bots/andy/assist.json | 1 - bots/andy/survive.json | 19 ------------------- src/agent/agent.js | 20 ++++++-------------- src/agent/history.js | 9 +++++---- src/agent/npc/controller.js | 26 ++++++++++++++++++++++++++ src/agent/npc/data.js | 18 ++++++++++++++++++ src/agent/{ => npc}/item_goal.js | 6 +++--- 7 files changed, 58 insertions(+), 41 deletions(-) delete mode 100644 bots/andy/survive.json create mode 100644 src/agent/npc/controller.js create mode 100644 src/agent/npc/data.js rename src/agent/{ => npc}/item_goal.js (98%) diff --git a/bots/andy/assist.json b/bots/andy/assist.json index eb47577..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": "", - "goals": [], "turns": [] } \ No newline at end of file diff --git a/bots/andy/survive.json b/bots/andy/survive.json deleted file mode 100644 index 4f34cc1..0000000 --- a/bots/andy/survive.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "andy", - "bio": "You are playing minecraft and assisting other players in tasks.", - "memory": "", - "goals": [ - "wooden_pickaxe", - "torch", - "stone_pickaxe", - "stone_sword", - "leather_chestplate", - "iron_pickaxe", - "iron_sword", - "iron_helmet", - "iron_boots", - "iron_leggings", - "iron_chestplate" - ], - "turns": [] -} \ No newline at end of file diff --git a/src/agent/agent.js b/src/agent/agent.js index 37d81e8..680bf93 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -4,7 +4,7 @@ import { Prompter } from './prompter.js'; import { initModes } from './modes.js'; import { initBot } from '../utils/mcdata.js'; import { containsCommand, commandExists, executeCommand, truncCommandMessage } from './commands/index.js'; -import { ItemGoal } from './item_goal.js'; +import { NPCContoller } from './npc/controller.js'; export class Agent { @@ -13,13 +13,12 @@ export class Agent { this.name = this.prompter.getName(); this.history = new History(this); this.coder = new Coder(this); - this.item_goal = new ItemGoal(this); + this.npc = new NPCContoller(this); await this.prompter.initExamples(); if (load_mem) this.history.load(); - this.item_goal.setGoals(this.history.goals); console.log('Logging in...'); this.bot = initBot(this.name); @@ -178,20 +177,13 @@ export class Agent { }); this.bot.on('idle', async () => { - // Resume all paused modes this.bot.modes.unPauseAll(); - - // Wait a while for inputs before acting independently - await new Promise((resolve) => setTimeout(resolve, 2000)); - if (!this.isIdle()) return; - - // Resume behavior or persue goal - if (this.coder.resume_func != null) - this.coder.executeResume(); - else - this.item_goal.executeNext(); + this.coder.executeResume(); }); + // Init NPC controller + this.npc.init(); + // This update loop ensures that each update() is called one at a time, even if it takes longer than the interval const INTERVAL = 300; setTimeout(async () => { diff --git a/src/agent/history.js b/src/agent/history.js index a55a0db..09479a5 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -1,4 +1,5 @@ -import { writeFileSync, readFileSync, mkdirSync } from 'fs'; +import { writeFileSync, readFileSync } from 'fs'; +import { NPCData } from './npc/data.js'; export class History { @@ -10,7 +11,6 @@ export class History { // These define an agent's long term memory this.memory = ''; - this.goals = []; // Variables for controlling the agent's memory and knowledge this.max_messages = 20; @@ -51,9 +51,10 @@ export class History { let data = { 'name': this.name, 'memory': this.memory, - 'goals': this.goals, 'turns': this.turns }; + if (this.agent.npc.data !== null) + data.npc = this.agent.npc.data.toObject(); const json_data = JSON.stringify(data, null, 4); writeFileSync(this.memory_fp, json_data, (err) => { if (err) { @@ -69,8 +70,8 @@ export class History { const data = readFileSync(this.memory_fp, 'utf8'); const obj = JSON.parse(data); this.memory = obj.memory; + this.agent.npc.data = NPCData.fromObject(obj.npc); this.turns = obj.turns; - this.goals = obj.goals; } catch (err) { console.error(`No memory file '${this.memory_fp}' for agent ${this.name}.`); } diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js new file mode 100644 index 0000000..c8649ff --- /dev/null +++ b/src/agent/npc/controller.js @@ -0,0 +1,26 @@ +import { NPCData } from './data.js'; +import { ItemGoal } from './item_goal.js'; + + +export class NPCContoller { + constructor(agent) { + this.agent = agent; + this.data = NPCData.fromObject(agent.prompter.prompts.npc); + this.item_goal = new ItemGoal(agent); + } + + init() { + if (this.data === null) return; + this.item_goal.setGoals(this.data.goals); + + this.agent.bot.on('idle', async () => { + // Wait a while for inputs before acting independently + await new Promise((resolve) => setTimeout(resolve, 2000)); + if (!this.agent.isIdle()) return; + + // Persue goal + if (this.agent.coder.resume_func === null) + this.item_goal.executeNext(); + }); + } +} \ No newline at end of file diff --git a/src/agent/npc/data.js b/src/agent/npc/data.js new file mode 100644 index 0000000..b0425de --- /dev/null +++ b/src/agent/npc/data.js @@ -0,0 +1,18 @@ +export class NPCData { + constructor() { + this.goals = []; + } + + toObject() { + return { + goals: this.goals + } + } + + static fromObject(obj) { + if (!obj) return null; + let npc = new NPCData(); + npc.goals = obj.goals; + return npc; + } +} \ No newline at end of file diff --git a/src/agent/item_goal.js b/src/agent/npc/item_goal.js similarity index 98% rename from src/agent/item_goal.js rename to src/agent/npc/item_goal.js index 3c15ccf..9b03964 100644 --- a/src/agent/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -1,6 +1,6 @@ -import * as skills from './library/skills.js'; -import * as world from './library/world.js'; -import * as mc from '../utils/mcdata.js'; +import * as skills from '../library/skills.js'; +import * as world from '../library/world.js'; +import * as mc from '../../utils/mcdata.js'; const blacklist = [ From ca7f08d3454a99b23568df79c2291bb2f68cadd7 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 5 Mar 2024 16:40:06 -0800 Subject: [PATCH 10/20] init build goals --- src/agent/agent.js | 2 +- src/agent/library/world.js | 3 +- src/agent/npc/build_goal.js | 80 ++++++++++++++++++++++++ src/agent/npc/construction/shelter.json | 43 +++++++++++++ src/agent/npc/controller.js | 53 +++++++++++++++- src/agent/npc/data.js | 9 ++- src/agent/npc/item_goal.js | 81 ++++++------------------- src/agent/npc/utils.js | 37 +++++++++++ 8 files changed, 239 insertions(+), 69 deletions(-) create mode 100644 src/agent/npc/build_goal.js create mode 100644 src/agent/npc/construction/shelter.json create mode 100644 src/agent/npc/utils.js diff --git a/src/agent/agent.js b/src/agent/agent.js index 1f14e88..8a8d63f 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -174,7 +174,7 @@ export class Agent { console.log('Agent died: ', message); 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(); diff --git a/src/agent/library/world.js b/src/agent/library/world.js index 2bb00bb..9fb51ab 100644 --- a/src/agent/library/world.js +++ b/src/agent/library/world.js @@ -25,11 +25,12 @@ export function getNearestFreeSpace(bot, size=1, distance=8) { 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) { + if (!top || !top.name == 'air' || !bottom || bottom.drops.length == 0 || !bottom.diggable) { empty = false; break; } } + if (!empty) break; } if (empty) { return empty_pos[i]; diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js new file mode 100644 index 0000000..61773ef --- /dev/null +++ b/src/agent/npc/build_goal.js @@ -0,0 +1,80 @@ +import { Vec3 } from 'vec3'; +import * as skills from '../library/skills.js'; +import * as world from '../library/world.js'; +import * as mc from '../../utils/mcdata.js'; + + +export class BuildGoal { + constructor(agent) { + this.agent = agent; + } + + rotateXZ(x, z, orientation, sizex, sizez) { + if (orientation === 0) return [x, z]; + if (orientation === 1) return [z, sizex-x-1]; + if (orientation === 2) return [sizex-x-1, sizez-z-1]; + if (orientation === 3) return [sizez-z-1, x]; + } + + async executeNext(goal, position=null, orientation=null) { + let sizex = goal.blocks[0][0].length; + let sizez = goal.blocks[0].length; + let sizey = goal.blocks.length; + if (!position) { + for (let x = 0; x < sizex - 1; x++) { + position = world.getNearestFreeSpace(this.agent.bot, sizex - x, 16); + if (position) break; + } + } + if (orientation === null) { + orientation = Math.floor(Math.random() * 4); + } + + let inventory = world.getInventoryCounts(this.agent.bot); + let missing = []; + let acted = false; + for (let y = goal.offset; y < sizey+goal.offset; y++) { + for (let z = 0; z < sizez; z++) { + for (let x = 0; x < sizex; x++) { + + let [rx, rz] = this.rotateXZ(x, z, orientation, sizex, sizez); + let ry = y - goal.offset; + let block_name = goal.blocks[ry][rz][rx]; + if (block_name === null || block_name === '') continue; + + let world_pos = new Vec3(position.x + x, position.y + y, position.z + z); + let current_block = this.agent.bot.blockAt(world_pos); + + let res = null; + if (current_block.name !== block_name) { + acted = true; + + if (!this.agent.isIdle()) + return {missing: missing, acted: acted, position: position, orientation: orientation}; + res = await this.agent.coder.execute(async () => { + await skills.breakBlockAt(this.agent.bot, world_pos.x, world_pos.y, world_pos.z); + }); + if (res.interrupted) + return {missing: missing, acted: acted, position: position, orientation: orientation}; + + if (inventory[block_name] > 0) { + + if (!this.agent.isIdle()) + return {missing: missing, acted: acted, position: position, orientation: orientation}; + await this.agent.coder.execute(async () => { + await skills.placeBlock(this.agent.bot, block_name, world_pos.x, world_pos.y, world_pos.z); + }); + if (res.interrupted) + return {missing: missing, acted: acted, position: position, orientation: orientation}; + + } else { + missing.push(block_name); + } + } + } + } + } + return {missing: missing, acted: acted, position: position, orientation: orientation}; + } + +} \ No newline at end of file diff --git a/src/agent/npc/construction/shelter.json b/src/agent/npc/construction/shelter.json new file mode 100644 index 0000000..77e1b0f --- /dev/null +++ b/src/agent/npc/construction/shelter.json @@ -0,0 +1,43 @@ +{ + "name": "shelter", + "offset": -1, + "placement": [ + [1, 1, 1], + [1, 2, 1], + [1, 3, 1], + [2, 3, 1], + [3, 1, 1], + [3, 2, 1], + [3, 3, 1] + ], + "blocks": [ + [ + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"] + ], + [ + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"] + ], + [ + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"] + ], + [ + ["air", "air", "air", "air", "air"], + ["air", "dirt", "dirt", "dirt", "air"], + ["air", "dirt", "dirt", "dirt", "air"], + ["air", "dirt", "dirt", "dirt", "air"], + ["air", "air", "air", "air", "air"] + ] + ] +} \ No newline at end of file diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index c8649ff..dbfa061 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -1,17 +1,32 @@ +import { readdirSync, readFileSync } from 'fs'; import { NPCData } from './data.js'; import { ItemGoal } from './item_goal.js'; +import { BuildGoal } from './build_goal.js'; +import { itemSatisfied } from './utils.js'; export class NPCContoller { constructor(agent) { this.agent = agent; this.data = NPCData.fromObject(agent.prompter.prompts.npc); + this.temp_goals = []; this.item_goal = new ItemGoal(agent); + this.build_goal = new BuildGoal(agent); + this.constructions = {}; } init() { if (this.data === null) return; - this.item_goal.setGoals(this.data.goals); + + for (let file of readdirSync('src/agent/npc/construction')) { + if (file.endsWith('.json')) { + try { + this.constructions[file.slice(0, -5)] = JSON.parse(readFileSync('src/agent/npc/construction/' + file, 'utf8')); + } catch (e) { + console.log('Error reading construction file: ', file); + } + } + } this.agent.bot.on('idle', async () => { // Wait a while for inputs before acting independently @@ -19,8 +34,40 @@ export class NPCContoller { if (!this.agent.isIdle()) return; // Persue goal - if (this.agent.coder.resume_func === null) - this.item_goal.executeNext(); + if (!this.agent.coder.resume_func) + this.executeNext(); }); } + + async executeNext() { + let goals = this.data.goals; + if (this.temp_goals !== null && this.temp_goals.length > 0) { + goals = this.temp_goals.concat(goals); + } + + for (let goal of goals) { + if (this.constructions[goal] === undefined && !itemSatisfied(this.agent.bot, goal)) { + await this.item_goal.executeNext(goal); + break; + } else if (this.constructions[goal]) { + let res = null; + if (this.data.built.hasOwnProperty(goal)) { + res = await this.build_goal.executeNext( + this.constructions[goal], + this.data.built[goal].position, + this.data.built[goal].orientation + ); + } else { + res = await this.build_goal.executeNext(this.constructions[goal]); + this.data.built[goal] = { + name: goal, + position: res.position, + orientation: res.orientation + }; + } + this.temp_goals = res.missing; + if (res.acted) break; + } + } + } } \ No newline at end of file diff --git a/src/agent/npc/data.js b/src/agent/npc/data.js index b0425de..7f03705 100644 --- a/src/agent/npc/data.js +++ b/src/agent/npc/data.js @@ -1,18 +1,23 @@ export class NPCData { constructor() { this.goals = []; + this.built = {}; } toObject() { return { - goals: this.goals + goals: this.goals, + built: this.built } } static fromObject(obj) { if (!obj) return null; let npc = new NPCData(); - npc.goals = obj.goals; + if (obj.goals) + npc.goals = obj.goals; + if (obj.built) + npc.built = obj.built; return npc; } } \ No newline at end of file diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index 9b03964..b94bcb7 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -1,6 +1,7 @@ import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; import * as mc from '../../utils/mcdata.js'; +import { itemSatisfied } from './utils.js'; const blacklist = [ @@ -103,36 +104,9 @@ class ItemNode { } isDone(quantity=1) { - 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.manager.agent.bot)[item] >= quantity) { - return true; - } - } - return false; + if (this.manager.goal.name === this.name) + return false; + return itemSatisfied(this.manager.agent.bot, this.name, quantity); } getDepth(q=1) { @@ -304,37 +278,24 @@ class ItemWrapper { export class ItemGoal { - constructor(agent, timeout=-1) { + constructor(agent) { this.agent = agent; - this.timeout = timeout; - this.goals = []; + this.goal = null; this.nodes = {}; this.failed = []; } - setGoals(goals) { - this.goals = [] - for (let goal of goals) { - this.goals.push({name: goal, quantity: 1}) - } - } - - async executeNext() { - // Get goal by priority - let goal = null; - for (let g of this.goals) { - if (this.nodes[g.name] === undefined) - this.nodes[g.name] = new ItemWrapper(this, null, g.name); - if (!this.nodes[g.name].isDone(g.quantity)) { - goal = this.nodes[g.name]; - break; - } - } - if (goal === null) - return; + async executeNext(item_name) { + if (this.nodes[item_name] === undefined) + this.nodes[item_name] = new ItemWrapper(this, null, item_name); + this.goal = this.nodes[item_name]; // Get next goal to execute - let next_info = goal.getNext(); + let next_info = this.goal.getNext(); + if (!next_info) { + console.log(`Invalid item goal ${this.goal.name}`); + return; + } let next = next_info.node; let quantity = next_info.quantity; @@ -346,11 +307,9 @@ export class ItemGoal { // If the bot has failed to obtain the block before, explore if (this.failed.includes(next.name)) { this.failed = this.failed.filter((item) => item !== next.name); - this.agent.coder.interruptible = true; await this.agent.coder.execute(async () => { await skills.moveAway(this.agent.bot, 8); - }, this.timeout); - this.agent.coder.interruptible = false; + }); } else { this.failed.push(next.name); await new Promise((resolve) => setTimeout(resolve, 500)); @@ -365,18 +324,16 @@ export class ItemGoal { // Execute the next goal let init_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; - this.agent.coder.interruptible = true; await this.agent.coder.execute(async () => { await next.execute(quantity); - }, this.timeout); - this.agent.coder.interruptible = false; + }); let final_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0; // Log the result of the goal attempt if (final_quantity > init_quantity) { - console.log(`Successfully obtained ${next.name} for goal ${goal.name}`); + console.log(`Successfully obtained ${next.name} for goal ${this.goal.name}`); } else { - console.log(`Failed to obtain ${next.name} for goal ${goal.name}`); + console.log(`Failed to obtain ${next.name} for goal ${this.goal.name}`); } } } diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js new file mode 100644 index 0000000..8119b19 --- /dev/null +++ b/src/agent/npc/utils.js @@ -0,0 +1,37 @@ +import * as skills from '../library/skills.js'; +import * as world from '../library/world.js'; +import * as mc from '../../utils/mcdata.js'; + + +export function itemSatisfied(bot, item, quantity=1) { + let qualifying = [item]; + if (item.includes('pickaxe') || + item.includes('axe') || + item.includes('shovel') || + item.includes('hoe') || + item.includes('sword')) { + let material = item.split('_')[0]; + let type = item.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(bot)[item] >= quantity) { + return true; + } + } + return false; +} From 24965b2e06a22e4e549ee7a1124e882314fdc387 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Wed, 6 Mar 2024 11:39:17 -0800 Subject: [PATCH 11/20] generic builds --- src/agent/npc/build_goal.js | 10 +++-- src/agent/npc/construction/shelter.json | 10 ++--- src/agent/npc/controller.js | 14 +++++-- src/agent/npc/utils.js | 49 ++++++++++++++++++++++++- src/utils/mcdata.js | 17 +++++++++ 5 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js index 61773ef..7ca0991 100644 --- a/src/agent/npc/build_goal.js +++ b/src/agent/npc/build_goal.js @@ -2,6 +2,7 @@ import { Vec3 } from 'vec3'; import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; import * as mc from '../../utils/mcdata.js'; +import { blockSatisfied, getTypeOfGeneric } from './utils.js'; export class BuildGoal { @@ -46,7 +47,7 @@ export class BuildGoal { let current_block = this.agent.bot.blockAt(world_pos); let res = null; - if (current_block.name !== block_name) { + if (!blockSatisfied(block_name, current_block)) { acted = true; if (!this.agent.isIdle()) @@ -57,18 +58,19 @@ export class BuildGoal { if (res.interrupted) return {missing: missing, acted: acted, position: position, orientation: orientation}; - if (inventory[block_name] > 0) { + let block_typed = getTypeOfGeneric(this.agent.bot, block_name); + if (inventory[block_typed] > 0) { if (!this.agent.isIdle()) return {missing: missing, acted: acted, position: position, orientation: orientation}; await this.agent.coder.execute(async () => { - await skills.placeBlock(this.agent.bot, block_name, world_pos.x, world_pos.y, world_pos.z); + await skills.placeBlock(this.agent.bot, block_typed, world_pos.x, world_pos.y, world_pos.z); }); if (res.interrupted) return {missing: missing, acted: acted, position: position, orientation: orientation}; } else { - missing.push(block_name); + missing.push(block_typed); } } } diff --git a/src/agent/npc/construction/shelter.json b/src/agent/npc/construction/shelter.json index 77e1b0f..1216665 100644 --- a/src/agent/npc/construction/shelter.json +++ b/src/agent/npc/construction/shelter.json @@ -23,20 +23,20 @@ ["dirt", "air", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"] + ["dirt", "dirt", "door", "dirt", "dirt"] ], [ ["dirt", "dirt", "dirt", "dirt", "dirt"], ["dirt", "air", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"] + ["dirt", "dirt", "", "dirt", "dirt"] ], [ ["air", "air", "air", "air", "air"], - ["air", "dirt", "dirt", "dirt", "air"], - ["air", "dirt", "dirt", "dirt", "air"], - ["air", "dirt", "dirt", "dirt", "air"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], ["air", "air", "air", "air", "air"] ] ] diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index dbfa061..93826e0 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -46,10 +46,16 @@ export class NPCContoller { } for (let goal of goals) { - if (this.constructions[goal] === undefined && !itemSatisfied(this.agent.bot, goal)) { - await this.item_goal.executeNext(goal); - break; - } else if (this.constructions[goal]) { + if (this.constructions[goal] === undefined) { + let quantity = 0; + for (let item of goals) { + if (item === goal) quantity++; + } + if (!itemSatisfied(this.agent.bot, goal, quantity)) { + await this.item_goal.executeNext(goal); + break; + } + } else { let res = null; if (this.data.built.hasOwnProperty(goal)) { res = await this.build_goal.executeNext( diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js index 8119b19..10d1520 100644 --- a/src/agent/npc/utils.js +++ b/src/agent/npc/utils.js @@ -1,8 +1,55 @@ -import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; import * as mc from '../../utils/mcdata.js'; +export function getTypeOfGeneric(bot, block_name) { + if (mc.MATCHING_WOOD_BLOCKS.includes(block_name)) { + + // Return most common wood type in inventory + let type_count = {}; + let max_count = 0; + let max_type = null; + let inventory = world.getInventoryCounts(bot); + for (const item in inventory) { + for (const wood of mc.WOOD_TYPES) { + if (item.includes(wood)) { + if (type_count[wood] === undefined) + type_count[wood] = 0; + type_count[wood] += inventory[item]; + if (type_count[wood] > max_count) { + max_count = type_count[wood]; + max_type = wood; + } + } + } + } + if (max_type !== null) + return max_type + '_' + block_name; + + // Return nearest wood type + let blocks = world.getNearbyBlocks(bot, 32); + for (const block of blocks) { + if (block.name.endsWith('log')) + return block.name.split('_')[0] + '_' + block_name; + } + + // Return oak + return 'oak_' + block_name; + } + return block_name; +} + + +export function blockSatisfied(target_name, block) { + if (target_name == 'dirt') { + return block.name == 'dirt' || block.name == 'grass_block'; + } else if (mc.MATCHING_WOOD_BLOCKS.includes(target_name)) { + return block.name.endsWith(target_name); + } + return block.name == target_name; +} + + export function itemSatisfied(bot, item, quantity=1) { let qualifying = [item]; if (item.includes('pickaxe') || diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index b026be2..6242e61 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -12,6 +12,23 @@ const mc_version = settings.minecraft_version; const mcdata = minecraftData(mc_version); +export const WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak']; +export const MATCHING_WOOD_BLOCKS = [ + 'log', + 'planks', + 'sign', + 'boat', + 'fence_gate', + 'door', + 'fence', + 'slab', + 'stairs', + 'button', + 'pressure_plate', + 'trapdoor' +] + + export function initBot(username) { let bot = createBot({ username: username, From 6571b3c87b162d81c5aba94aa38b8147766831c9 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Wed, 6 Mar 2024 12:18:00 -0800 Subject: [PATCH 12/20] goal quantity --- src/agent/npc/build_goal.js | 6 +++-- src/agent/npc/controller.js | 45 ++++++++++++++++++++----------------- src/agent/npc/data.js | 21 ++++++++++++----- src/agent/npc/item_goal.js | 6 ++--- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js index 7ca0991..e496a30 100644 --- a/src/agent/npc/build_goal.js +++ b/src/agent/npc/build_goal.js @@ -32,7 +32,7 @@ export class BuildGoal { } let inventory = world.getInventoryCounts(this.agent.bot); - let missing = []; + let missing = {}; let acted = false; for (let y = goal.offset; y < sizey+goal.offset; y++) { for (let z = 0; z < sizez; z++) { @@ -70,7 +70,9 @@ export class BuildGoal { return {missing: missing, acted: acted, position: position, orientation: orientation}; } else { - missing.push(block_typed); + if (missing[block_typed] === undefined) + missing[block_typed] = 0; + missing[block_typed]++; } } } diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 93826e0..76c432c 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -40,38 +40,43 @@ export class NPCContoller { } async executeNext() { - let goals = this.data.goals; - if (this.temp_goals !== null && this.temp_goals.length > 0) { - goals = this.temp_goals.concat(goals); - } + // If we need more blocks to complete a building, get those first + let goals = this.temp_goals.concat(this.data.goals); for (let goal of goals) { - if (this.constructions[goal] === undefined) { - let quantity = 0; - for (let item of goals) { - if (item === goal) quantity++; - } - if (!itemSatisfied(this.agent.bot, goal, quantity)) { - await this.item_goal.executeNext(goal); + + // Obtain goal item or block + if (this.constructions[goal.name] === undefined) { + if (!itemSatisfied(this.agent.bot, goal.name, goal.quantity)) { + await this.item_goal.executeNext(goal.name, goal.quantity); break; } - } else { + } + + // Build construction goal + else { let res = null; - if (this.data.built.hasOwnProperty(goal)) { + if (this.data.built.hasOwnProperty(goal.name)) { res = await this.build_goal.executeNext( - this.constructions[goal], - this.data.built[goal].position, - this.data.built[goal].orientation + this.constructions[goal.name], + this.data.built[goal.name].position, + this.data.built[goal.name].orientation ); } else { - res = await this.build_goal.executeNext(this.constructions[goal]); - this.data.built[goal] = { - name: goal, + res = await this.build_goal.executeNext(this.constructions[goal.name]); + this.data.built[goal.name] = { + name: goal.name, position: res.position, orientation: res.orientation }; } - this.temp_goals = res.missing; + this.temp_goals = []; + for (let block_name in res.missing) { + this.temp_goals.push({ + name: block_name, + quantity: res.missing[block_name] + }) + } if (res.acted) break; } } diff --git a/src/agent/npc/data.js b/src/agent/npc/data.js index 7f03705..b160c79 100644 --- a/src/agent/npc/data.js +++ b/src/agent/npc/data.js @@ -5,17 +5,26 @@ export class NPCData { } toObject() { - return { - goals: this.goals, - built: this.built - } + let obj = {}; + if (this.goals.length > 0) + obj.goals = this.goals; + if (Object.keys(this.built).length > 0) + obj.built = this.built; + return obj; } static fromObject(obj) { if (!obj) return null; let npc = new NPCData(); - if (obj.goals) - npc.goals = obj.goals; + if (obj.goals) { + npc.goals = []; + for (let goal of obj.goals) { + if (typeof goal === 'string') + npc.goals.push({name: goal, quantity: 1}); + else + npc.goals.push({name: goal.name, quantity: goal.quantity}); + } + } if (obj.built) npc.built = obj.built; return npc; diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index b94bcb7..dea4a53 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -160,7 +160,7 @@ class ItemNode { } else if (this.type === 'hunt') { for (let i=0; i Date: Wed, 6 Mar 2024 14:25:15 -0800 Subject: [PATCH 13/20] exclude buildings from collecting --- src/agent/library/skills.js | 4 +++- src/agent/library/world.js | 2 +- src/agent/npc/build_goal.js | 47 ++++++++++++++++++++----------------- src/agent/npc/controller.js | 24 ++++++++++++++++++- src/agent/npc/item_goal.js | 13 ++++++---- src/utils/mcdata.js | 7 +++--- 6 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index a3732b9..700cbb7 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -359,11 +359,13 @@ export async function collectBlock(bot, blockType, num=1, exclude=null) { let blocktypes = [blockType]; if (blockType.endsWith('ore')) blocktypes.push('deepslate_'+blockType); + if (blockType === 'dirt') + blocktypes.push('grass_block'); let collected = 0; for (let i=0; i= count) + if (count !== null && blocks.length >= count) break; } } diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js index e496a30..27fc3c2 100644 --- a/src/agent/npc/build_goal.js +++ b/src/agent/npc/build_goal.js @@ -17,6 +17,13 @@ export class BuildGoal { if (orientation === 3) return [sizez-z-1, x]; } + async wrapSkill(func) { + if (!this.agent.isIdle()) + return false; + let res = await this.agent.coder.execute(func); + return !res.interrupted; + } + async executeNext(goal, position=null, orientation=null) { let sizex = goal.blocks[0][0].length; let sizez = goal.blocks[0].length; @@ -47,32 +54,28 @@ export class BuildGoal { let current_block = this.agent.bot.blockAt(world_pos); let res = null; - if (!blockSatisfied(block_name, current_block)) { + if (current_block !== null && !blockSatisfied(block_name, current_block)) { acted = true; - if (!this.agent.isIdle()) - return {missing: missing, acted: acted, position: position, orientation: orientation}; - res = await this.agent.coder.execute(async () => { - await skills.breakBlockAt(this.agent.bot, world_pos.x, world_pos.y, world_pos.z); - }); - if (res.interrupted) - return {missing: missing, acted: acted, position: position, orientation: orientation}; - - let block_typed = getTypeOfGeneric(this.agent.bot, block_name); - if (inventory[block_typed] > 0) { - - if (!this.agent.isIdle()) - return {missing: missing, acted: acted, position: position, orientation: orientation}; - await this.agent.coder.execute(async () => { - await skills.placeBlock(this.agent.bot, block_typed, world_pos.x, world_pos.y, world_pos.z); + if (current_block.name !== 'air') { + res = await this.wrapSkill(async () => { + await skills.breakBlockAt(this.agent.bot, world_pos.x, world_pos.y, world_pos.z); }); - if (res.interrupted) - return {missing: missing, acted: acted, position: position, orientation: orientation}; + if (!res) return {missing: missing, acted: acted, position: position, orientation: orientation}; + } - } else { - if (missing[block_typed] === undefined) - missing[block_typed] = 0; - missing[block_typed]++; + if (block_name !== 'air') { + let block_typed = getTypeOfGeneric(this.agent.bot, block_name); + if (inventory[block_typed] > 0) { + res = await this.wrapSkill(async () => { + await skills.placeBlock(this.agent.bot, block_typed, world_pos.x, world_pos.y, world_pos.z); + }); + if (!res) return {missing: missing, acted: acted, position: position, orientation: orientation}; + } else { + if (missing[block_typed] === undefined) + missing[block_typed] = 0; + missing[block_typed]++; + } } } } diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 76c432c..1ac20ba 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -10,11 +10,30 @@ export class NPCContoller { this.agent = agent; this.data = NPCData.fromObject(agent.prompter.prompts.npc); this.temp_goals = []; - this.item_goal = new ItemGoal(agent); + this.item_goal = new ItemGoal(agent, this.data); this.build_goal = new BuildGoal(agent); this.constructions = {}; } + getBuiltPositions() { + let positions = []; + for (let name in this.data.built) { + let position = this.data.built[name].position; + let offset = this.constructions[name].offset; + let sizex = this.constructions[name].blocks[0][0].length; + let sizez = this.constructions[name].blocks[0].length; + let sizey = this.constructions[name].blocks.length; + for (let y = offset; y < sizey+offset; y++) { + for (let z = 0; z < sizez; z++) { + for (let x = 0; x < sizex; x++) { + positions.push({x: position.x + x, y: position.y + y, z: position.z + z}); + } + } + } + } + return positions; + } + init() { if (this.data === null) return; @@ -80,5 +99,8 @@ export class NPCContoller { if (res.acted) break; } } + + if (this.agent.isIdle()) + this.agent.bot.emit('idle'); } } \ No newline at end of file diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index dea4a53..0c89ce8 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -152,7 +152,7 @@ class ItemNode { let inventory = world.getInventoryCounts(this.manager.agent.bot); let init_quantity = inventory[this.name] || 0; if (this.type === 'block') { - await skills.collectBlock(this.manager.agent.bot, this.source, quantity); + await skills.collectBlock(this.manager.agent.bot, this.source, quantity, this.manager.agent.npc.getBuiltPositions()); } else if (this.type === 'smelt') { let to_smelt_name = this.recipe[0].node.name; let to_smelt_quantity = Math.min(quantity, inventory[to_smelt_name] || 1); @@ -210,10 +210,13 @@ class ItemWrapper { } } - let block_source = mc.getItemBlockSource(this.name); - if (block_source) { - let tool = mc.getBlockTool(block_source); - this.add_method(new ItemNode(this.manager, this, this.name).setCollectable(block_source, tool)); + let block_sources = mc.getItemBlockSources(this.name); + if (block_sources.length > 0 && this.name !== 'torch') { // Do not collect placed torches + for (let block_source of block_sources) { + if (block_source === 'grass_block') continue; // Dirt nodes will collect grass blocks + let tool = mc.getBlockTool(block_source); + this.add_method(new ItemNode(this.manager, this, this.name).setCollectable(block_source, tool)); + } } let smeltingIngredient = mc.getItemSmeltingIngredient(this.name); diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 6242e61..9908649 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -171,14 +171,15 @@ export function getItemSmeltingIngredient(itemName) { }[itemName]; } -export function getItemBlockSource(itemName) { +export function getItemBlockSources(itemName) { let itemId = getItemId(itemName); + let sources = []; for (let block of getAllBlocks()) { if (block.drops.includes(itemId)) { - return block.name; + sources.push(block.name); } } - return null; + return sources; } export function getItemAnimalSource(itemName) { From a7ebe00929056d1f8a53d3a28e144b2d2f34a8f4 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Wed, 6 Mar 2024 15:22:34 -0800 Subject: [PATCH 14/20] crafting bugs --- src/agent/library/world.js | 60 ++++++++++++------------- src/agent/npc/construction/shelter.json | 2 +- src/agent/npc/utils.js | 9 ++-- src/utils/mcdata.js | 16 +++++++ 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/agent/library/world.js b/src/agent/library/world.js index 2c7aa5e..9d6be62 100644 --- a/src/agent/library/world.js +++ b/src/agent/library/world.js @@ -1,5 +1,5 @@ import pf from 'mineflayer-pathfinder'; -import { getAllBlockIds, getAllBiomes } from '../../utils/mcdata.js'; +import * as mc from '../../utils/mcdata.js'; export function getNearestFreeSpace(bot, size=1, distance=8) { @@ -39,26 +39,7 @@ export function getNearestFreeSpace(bot, size=1, distance=8) { } -export function getNearbyBlocks(bot, maxDistance, count=null) { - if (maxDistance == null) maxDistance = 16; - if (count == null) count = 10000; - let positions = bot.findBlocks({matching: getAllBlockIds(['air']), maxDistance: maxDistance, count: count}); - let blocks = []; - for (let i = 0; i < positions.length; i++) { - let block = bot.blockAt(positions[i]); - let distance = positions[i].distanceTo(bot.entity.position); - blocks.push({ block: block, distance: distance }); - } - blocks.sort((a, b) => a.distance - b.distance); - let res = []; - for (let i = 0; i < blocks.length; i++) { - res.push(blocks[i].block); - } - return res; -} - - -export function getNearestBlocks(bot, block_types, distance=16, count=null) { +export function getNearestBlocks(bot, block_types=null, distance=16, count=10000) { /** * Get a list of the nearest blocks of the given types. * @param {Bot} bot - The bot to get the nearest block for. @@ -70,17 +51,32 @@ export function getNearestBlocks(bot, block_types, distance=16, count=null) { * let woodBlocks = world.getNearestBlocks(bot, ['oak_log', 'birch_log'], 16, 1); **/ // if blocktypes is not a list, make it a list - if (!Array.isArray(block_types)) - block_types = [block_types]; - let blocks = []; - for (let block of getNearbyBlocks(bot, distance, count)) { - if (block_types.includes(block.name)) { - blocks.push(block); - if (count !== null && blocks.length >= count) - break; + let block_ids = []; + if (block_types === null) { + block_ids = mc.getAllBlockIds(['air']); + } + else { + if (!Array.isArray(block_types)) + block_types = [block_types]; + for(let block_type of block_types) { + block_ids.push(mc.getBlockId(block_type)); } } - return blocks; + + let positions = bot.findBlocks({matching: block_ids, maxDistance: distance, count: count}); + let blocks = []; + for (let i = 0; i < positions.length; i++) { + let block = bot.blockAt(positions[i]); + let distance = positions[i].distanceTo(bot.entity.position); + blocks.push({ block: block, distance: distance }); + } + blocks.sort((a, b) => a.distance - b.distance); + + let res = []; + for (let i = 0; i < blocks.length; i++) { + res.push(blocks[i].block); + } + return res; } @@ -235,7 +231,7 @@ export function getNearbyBlockTypes(bot, distance=16) { * @example * let blocks = world.getNearbyBlockTypes(bot); **/ - let blocks = getNearbyBlocks(bot, distance); + let blocks = getNearestBlocks(bot, null, distance); let found = []; for (let i = 0; i < blocks.length; i++) { if (!found.includes(blocks[i].name)) { @@ -269,5 +265,5 @@ export function getBiomeName(bot) { * let biome = world.getBiomeName(bot); **/ const biomeId = bot.world.getBiome(bot.entity.position); - return getAllBiomes()[biomeId].name; + return mc.getAllBiomes()[biomeId].name; } diff --git a/src/agent/npc/construction/shelter.json b/src/agent/npc/construction/shelter.json index 1216665..410afb5 100644 --- a/src/agent/npc/construction/shelter.json +++ b/src/agent/npc/construction/shelter.json @@ -20,7 +20,7 @@ ], [ ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "air", "air", "air", "dirt"], + ["dirt", "chest", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], ["dirt", "dirt", "door", "dirt", "dirt"] diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js index 10d1520..b63b544 100644 --- a/src/agent/npc/utils.js +++ b/src/agent/npc/utils.js @@ -27,10 +27,11 @@ export function getTypeOfGeneric(bot, block_name) { return max_type + '_' + block_name; // Return nearest wood type - let blocks = world.getNearbyBlocks(bot, 32); - for (const block of blocks) { - if (block.name.endsWith('log')) - return block.name.split('_')[0] + '_' + block_name; + let log_types = mc.WOOD_TYPES.map((wood) => wood + '_log'); + let blocks = world.getNearestBlocks(bot, log_types, 16, 1); + if (blocks.length > 0) { + let wood = blocks[0].name.split('_')[0]; + return wood + '_' + block_name; } // Return oak diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 9908649..1ffbec7 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -75,6 +75,22 @@ export function getItemName(itemId) { return null; } +export function getBlockId(blockName) { + let block = mcdata.blocksByName[blockName]; + if (block) { + return block.id; + } + return null; +} + +export function getBlockName(blockId) { + let block = mcdata.blocks[blockId] + if (block) { + return block.name; + } + return null; +} + export function getAllItems(ignore) { if (!ignore) { ignore = []; From cc48a38aded592ba8f670385fcfa6e2ea82e8b9b Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Wed, 6 Mar 2024 15:53:22 -0800 Subject: [PATCH 15/20] build bed --- src/agent/npc/construction/shelter.json | 6 ++--- src/agent/npc/item_goal.js | 16 +++++++++++-- src/agent/npc/utils.js | 31 +++++++++++++++++++++++++ src/utils/mcdata.js | 18 ++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/agent/npc/construction/shelter.json b/src/agent/npc/construction/shelter.json index 410afb5..96aec7e 100644 --- a/src/agent/npc/construction/shelter.json +++ b/src/agent/npc/construction/shelter.json @@ -20,8 +20,8 @@ ], [ ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "chest", "air", "air", "dirt"], - ["dirt", "air", "air", "air", "dirt"], + ["dirt", "chest", "air", "bed", "dirt"], + ["dirt", "air", "air", "bed", "dirt"], ["dirt", "air", "air", "air", "dirt"], ["dirt", "dirt", "door", "dirt", "dirt"] ], @@ -30,7 +30,7 @@ ["dirt", "air", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], ["dirt", "air", "air", "air", "dirt"], - ["dirt", "dirt", "", "dirt", "dirt"] + ["dirt", "dirt", "door", "dirt", "dirt"] ], [ ["air", "air", "air", "air", "air"], diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index 0c89ce8..db19dc9 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -15,7 +15,8 @@ const blacklist = [ '_wood', 'stripped_', 'crimson', - 'warped' + 'warped', + 'dye' ] @@ -206,12 +207,23 @@ class ItemWrapper { let recipes = mc.getItemCraftingRecipes(this.name); if (recipes) { for (let recipe of recipes) { + let includes_blacklisted = false; + for (let ingredient in recipe) { + for (let match of blacklist) { + if (ingredient.includes(match)) { + includes_blacklisted = true; + break; + } + } + if (includes_blacklisted) break; + } + if (includes_blacklisted) continue; this.add_method(new ItemNode(this.manager, this, this.name).setRecipe(recipe)) } } let block_sources = mc.getItemBlockSources(this.name); - if (block_sources.length > 0 && this.name !== 'torch') { // Do not collect placed torches + if (block_sources.length > 0 && this.name !== 'torch' && !this.name.includes('bed')) { // Do not collect placed torches or beds for (let block_source of block_sources) { if (block_source === 'grass_block') continue; // Dirt nodes will collect grass blocks let tool = mc.getBlockTool(block_source); diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js index b63b544..a92a38a 100644 --- a/src/agent/npc/utils.js +++ b/src/agent/npc/utils.js @@ -3,6 +3,7 @@ import * as mc from '../../utils/mcdata.js'; export function getTypeOfGeneric(bot, block_name) { + // Get type of wooden block if (mc.MATCHING_WOOD_BLOCKS.includes(block_name)) { // Return most common wood type in inventory @@ -37,6 +38,34 @@ export function getTypeOfGeneric(bot, block_name) { // Return oak return 'oak_' + block_name; } + + // Get type of bed + if (block_name === 'bed') { + + // Return most common wool type in inventory + let type_count = {}; + let max_count = 0; + let max_type = null; + let inventory = world.getInventoryCounts(bot); + for (const item in inventory) { + for (const color of mc.WOOL_COLORS) { + if (item === color + '_wool') { + if (type_count[color] === undefined) + type_count[color] = 0; + type_count[color] += inventory[item]; + if (type_count[color] > max_count) { + max_count = type_count[color]; + max_type = color; + } + } + } + } + if (max_type !== null) + return max_type + '_' + block_name; + + // Return white + return 'white_' + block_name; + } return block_name; } @@ -46,6 +75,8 @@ export function blockSatisfied(target_name, block) { return block.name == 'dirt' || block.name == 'grass_block'; } else if (mc.MATCHING_WOOD_BLOCKS.includes(target_name)) { return block.name.endsWith(target_name); + } else if (target_name == 'bed') { + return block.name.endsWith('bed'); } return block.name == target_name; } diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 1ffbec7..eeeb957 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -27,6 +27,24 @@ export const MATCHING_WOOD_BLOCKS = [ 'pressure_plate', 'trapdoor' ] +export const WOOL_COLORS = [ + 'white', + 'orange', + 'magenta', + 'light_blue', + 'yellow', + 'lime', + 'pink', + 'gray', + 'light_gray', + 'cyan', + 'purple', + 'blue', + 'brown', + 'green', + 'red', + 'black' +] export function initBot(username) { From 83b5f56acc192b104963a76582abd26c8425e284 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Wed, 20 Mar 2024 16:03:16 -0700 Subject: [PATCH 16/20] open doors, sleep at night --- src/agent/library/skills.js | 43 +++++++++++++++ src/agent/npc/build_goal.js | 11 +--- src/agent/npc/controller.js | 102 ++++++++++++++++++++++++++++++++++-- src/agent/npc/data.js | 5 ++ src/agent/npc/utils.js | 8 +++ 5 files changed, 157 insertions(+), 12 deletions(-) diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index adf5912..ac52ec1 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -764,6 +764,49 @@ export async function stay(bot) { return true; } +export async function useDoor(bot, door_pos=null) { + /** + * Use the door at the given position. + * @param {MinecraftBot} bot, reference to the minecraft bot. + * @param {Vec3} door_pos, the position of the door to use. If null, the nearest door will be used. + * @returns {Promise} true if the door was used, false otherwise. + * @example + * let door = world.getNearestBlock(bot, "oak_door", 16).position; + * await skills.useDoor(bot, door); + **/ + if (!door_pos) { + for (let door_type of ['oak_door', 'spruce_door', 'birch_door', 'jungle_door', 'acacia_door', 'dark_oak_door', + 'mangrove_door', 'cherry_door', 'bamboo_door', 'crimson_door', 'warped_door']) { + door_pos = world.getNearestBlock(bot, door_type, 16).position; + if (door_pos) break; + } + } else { + door_pos = Vec3(door_pos.x, door_pos.y, door_pos.z); + } + if (!door_pos) { + log(bot, `Could not find a door to use.`); + return false; + } + + bot.pathfinder.setGoal(new pf.goals.GoalNear(door_pos.x, door_pos.y, door_pos.z, 1)); + await new Promise((resolve) => setTimeout(resolve, 1000)); + while (bot.pathfinder.isMoving()) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + let door_block = bot.blockAt(door_pos); + await bot.lookAt(door_pos); + if (!door_block._properties.open) + await bot.activateBlock(door_block); + + bot.setControlState("forward", true); + await new Promise((resolve) => setTimeout(resolve, 600)); + bot.setControlState("forward", false); + await bot.activateBlock(door_block); + + log(bot, `Used door at ${door_pos}.`); + return true; +} export async function goToBed(bot) { /** diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js index 27fc3c2..3af1e88 100644 --- a/src/agent/npc/build_goal.js +++ b/src/agent/npc/build_goal.js @@ -2,7 +2,7 @@ import { Vec3 } from 'vec3'; import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; import * as mc from '../../utils/mcdata.js'; -import { blockSatisfied, getTypeOfGeneric } from './utils.js'; +import { blockSatisfied, getTypeOfGeneric, rotateXZ } from './utils.js'; export class BuildGoal { @@ -10,13 +10,6 @@ export class BuildGoal { this.agent = agent; } - rotateXZ(x, z, orientation, sizex, sizez) { - if (orientation === 0) return [x, z]; - if (orientation === 1) return [z, sizex-x-1]; - if (orientation === 2) return [sizex-x-1, sizez-z-1]; - if (orientation === 3) return [sizez-z-1, x]; - } - async wrapSkill(func) { if (!this.agent.isIdle()) return false; @@ -45,7 +38,7 @@ export class BuildGoal { for (let z = 0; z < sizez; z++) { for (let x = 0; x < sizex; x++) { - let [rx, rz] = this.rotateXZ(x, z, orientation, sizex, sizez); + let [rx, rz] = rotateXZ(x, z, orientation, sizex, sizez); let ry = y - goal.offset; let block_name = goal.blocks[ry][rz][rx]; if (block_name === null || block_name === '') continue; diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 1ac20ba..5ae358f 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -2,7 +2,10 @@ import { readdirSync, readFileSync } from 'fs'; import { NPCData } from './data.js'; import { ItemGoal } from './item_goal.js'; import { BuildGoal } from './build_goal.js'; -import { itemSatisfied } from './utils.js'; +import { itemSatisfied, rotateXZ } from './utils.js'; +import * as skills from '../library/skills.js'; +import * as world from '../library/world.js'; +import * as mc from '../../utils/mcdata.js'; export class NPCContoller { @@ -53,14 +56,52 @@ export class NPCContoller { if (!this.agent.isIdle()) return; // Persue goal - if (!this.agent.coder.resume_func) + if (!this.agent.coder.resume_func) { this.executeNext(); + this.agent.history.save(); + } }); } async executeNext() { + if (!this.agent.isIdle()) return; + + if (this.agent.bot.time.timeOfDay < 12000) { + // Exit any buildings + let building = this.currentBuilding(); + if (building) { + let door_pos = this.getBuildingDoor(building); + if (door_pos) { + await this.agent.coder.execute(async () => { + await skills.useDoor(this.agent.bot, door_pos); + }); + } + } + + // Work towards goals + await this.executeGoal(); + + } else { + // Return to home + let building = this.currentBuilding(); + if (this.data.home !== null && (building === null || building != this.data.home)) { + let door_pos = this.getBuildingDoor(this.data.home); + await this.agent.coder.execute(async () => { + await skills.useDoor(this.agent.bot, door_pos); + }); + } + + // Go to bed + await this.agent.coder.execute(async () => { + await skills.goToBed(this.agent.bot); + }); + } + } + + async executeGoal() { // If we need more blocks to complete a building, get those first let goals = this.temp_goals.concat(this.data.goals); + this.temp_goals = []; for (let goal of goals) { @@ -89,7 +130,9 @@ export class NPCContoller { orientation: res.orientation }; } - this.temp_goals = []; + if (Object.keys(res.missing).length === 0) { + this.data.home = goal.name; + } for (let block_name in res.missing) { this.temp_goals.push({ name: block_name, @@ -103,4 +146,57 @@ export class NPCContoller { if (this.agent.isIdle()) this.agent.bot.emit('idle'); } + + currentBuilding() { + let bot_pos = this.agent.bot.entity.position; + for (let name in this.data.built) { + let pos = this.data.built[name].position; + let offset = this.constructions[name].offset; + let sizex = this.constructions[name].blocks[0][0].length; + let sizez = this.constructions[name].blocks[0].length; + let sizey = this.constructions[name].blocks.length; + if (this.data.built[name].orientation % 2 === 1) [sizex, sizez] = [sizez, sizex]; + if (bot_pos.x >= pos.x && bot_pos.x < pos.x + sizex && + bot_pos.y >= pos.y + offset && bot_pos.y < pos.y + sizey + offset && + bot_pos.z >= pos.z && bot_pos.z < pos.z + sizez) { + return name; + } + } + return null; + } + + getBuildingDoor(name) { + if (name === null || this.data.built[name] === undefined) return null; + let door_x = null; + let door_z = null; + let door_y = null; + for (let y = 0; y < this.constructions[name].blocks.length; y++) { + for (let z = 0; z < this.constructions[name].blocks[y].length; z++) { + for (let x = 0; x < this.constructions[name].blocks[y][z].length; x++) { + if (this.constructions[name].blocks[y][z][x] !== null && + this.constructions[name].blocks[y][z][x].includes('door')) { + door_x = x; + door_z = z; + door_y = y; + break; + } + } + if (door_x !== null) break; + } + if (door_x !== null) break; + } + if (door_x === null) return null; + + let sizex = this.constructions[name].blocks[0][0].length; + let sizez = this.constructions[name].blocks[0].length; + let orientation = this.data.built[name].orientation; + [door_x, door_z] = rotateXZ(door_x, door_z, orientation, sizex, sizez); + door_y += this.constructions[name].offset; + + return { + x: this.data.built[name].position.x + door_x, + y: this.data.built[name].position.y + door_y, + z: this.data.built[name].position.z + door_z + }; + } } \ No newline at end of file diff --git a/src/agent/npc/data.js b/src/agent/npc/data.js index b160c79..70b59a4 100644 --- a/src/agent/npc/data.js +++ b/src/agent/npc/data.js @@ -2,6 +2,7 @@ export class NPCData { constructor() { this.goals = []; this.built = {}; + this.home = null; } toObject() { @@ -10,6 +11,8 @@ export class NPCData { obj.goals = this.goals; if (Object.keys(this.built).length > 0) obj.built = this.built; + if (this.home) + obj.home = this.home; return obj; } @@ -27,6 +30,8 @@ export class NPCData { } if (obj.built) npc.built = obj.built; + if (obj.home) + npc.home = obj.home; return npc; } } \ No newline at end of file diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js index a92a38a..d5c0528 100644 --- a/src/agent/npc/utils.js +++ b/src/agent/npc/utils.js @@ -114,3 +114,11 @@ export function itemSatisfied(bot, item, quantity=1) { } return false; } + + +export function rotateXZ(x, z, orientation, sizex, sizez) { + if (orientation === 0) return [x, z]; + if (orientation === 1) return [z, sizex-x-1]; + if (orientation === 2) return [sizex-x-1, sizez-z-1]; + if (orientation === 3) return [sizez-z-1, x]; +} From 0d311857047136c0ad75bf8904e3fe6cc91be8eb Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 9 Apr 2024 23:38:59 -0700 Subject: [PATCH 17/20] house and door fixes --- src/agent/npc/construction/hole.json | 38 ++++++++++++++++++++++ src/agent/npc/construction/house.json | 42 ++++++++++++++++++++++++ src/agent/npc/construction/shelter.json | 43 ------------------------- src/agent/npc/controller.js | 37 ++++++++++++++++----- src/agent/npc/utils.js | 2 ++ 5 files changed, 111 insertions(+), 51 deletions(-) create mode 100644 src/agent/npc/construction/hole.json create mode 100644 src/agent/npc/construction/house.json delete mode 100644 src/agent/npc/construction/shelter.json diff --git a/src/agent/npc/construction/hole.json b/src/agent/npc/construction/hole.json new file mode 100644 index 0000000..7a2c1f2 --- /dev/null +++ b/src/agent/npc/construction/hole.json @@ -0,0 +1,38 @@ +{ + "name": "hole", + "offset": -2, + "blocks": [ + [ + ["", "", "", "", ""], + ["", "dirt", "dirt", "dirt", ""], + ["", "dirt", "dirt", "dirt", ""], + ["", "dirt", "dirt", "dirt", ""], + ["", "", "dirt", "", ""], + ["", "", "dirt", "", ""] + ], + [ + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "chest", "bed", "air", "dirt"], + ["dirt", "air", "bed", "air", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "dirt", "door", "dirt", "dirt"], + ["dirt", "dirt", "air", "dirt", "dirt"] + ], + [ + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "torch", "air", "air", "dirt"], + ["dirt", "air", "air", "air", "dirt"], + ["dirt", "dirt", "door", "dirt", "dirt"], + ["air", "air", "air", "air", "air"] + ], + [ + ["air", "air", "air", "air", "air"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["dirt", "dirt", "dirt", "dirt", "dirt"], + ["air", "air", "air", "air", "air"], + ["air", "air", "air", "air", "air"] + ] + ] +} \ No newline at end of file diff --git a/src/agent/npc/construction/house.json b/src/agent/npc/construction/house.json new file mode 100644 index 0000000..0a3677d --- /dev/null +++ b/src/agent/npc/construction/house.json @@ -0,0 +1,42 @@ +{ + "name": "shelter", + "offset": -1, + "blocks": [ + [ + ["", "", "", "", ""], + ["", "planks", "planks", "planks", ""], + ["", "planks", "planks", "planks", ""], + ["", "planks", "planks", "planks", ""], + ["", "planks", "planks", "planks", ""], + ["", "", "planks", "", ""], + ["", "", "", "", ""] + ], + [ + ["log", "planks", "planks", "planks", "log"], + ["planks", "chest", "bed", "air", "planks"], + ["planks", "air", "bed", "air", "planks"], + ["planks", "air", "air", "air", "planks"], + ["planks", "air", "air", "air", "planks"], + ["log", "planks", "door", "planks", "log"], + ["", "air", "air", "air", ""] + ], + [ + ["log", "planks", "planks", "planks", "log"], + ["planks", "torch", "air", "torch", "planks"], + ["planks", "air", "air", "air", "planks"], + ["planks", "air", "air", "air", "planks"], + ["planks", "torch", "air", "torch", "planks"], + ["log", "planks", "door", "planks", "log"], + ["", "air", "air", "air", ""] + ], + [ + ["air", "air", "air", "air", "air"], + ["air", "planks", "planks", "planks", "air"], + ["planks", "planks", "planks", "planks", "planks"], + ["planks", "planks", "planks", "planks", "planks"], + ["air", "planks", "planks", "planks", "air"], + ["air", "air", "air", "air", "air"], + ["", "air", "air", "air", ""] + ] + ] +} \ No newline at end of file diff --git a/src/agent/npc/construction/shelter.json b/src/agent/npc/construction/shelter.json deleted file mode 100644 index 96aec7e..0000000 --- a/src/agent/npc/construction/shelter.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "shelter", - "offset": -1, - "placement": [ - [1, 1, 1], - [1, 2, 1], - [1, 3, 1], - [2, 3, 1], - [3, 1, 1], - [3, 2, 1], - [3, 3, 1] - ], - "blocks": [ - [ - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"] - ], - [ - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "chest", "air", "bed", "dirt"], - ["dirt", "air", "air", "bed", "dirt"], - ["dirt", "air", "air", "air", "dirt"], - ["dirt", "dirt", "door", "dirt", "dirt"] - ], - [ - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "air", "air", "air", "dirt"], - ["dirt", "air", "air", "air", "dirt"], - ["dirt", "air", "air", "air", "dirt"], - ["dirt", "dirt", "door", "dirt", "dirt"] - ], - [ - ["air", "air", "air", "air", "air"], - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["dirt", "dirt", "dirt", "dirt", "dirt"], - ["air", "air", "air", "air", "air"] - ] - ] -} \ No newline at end of file diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 5ae358f..c474efd 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -50,9 +50,25 @@ export class NPCContoller { } } + for (let name in this.constructions) { + let sizez = this.constructions[name].blocks[0].length; + let sizex = this.constructions[name].blocks[0][0].length; + let max_size = Math.max(sizex, sizez); + for (let y = 0; y < this.constructions[name].blocks.length; y++) { + for (let z = 0; z < max_size; z++) { + if (z >= this.constructions[name].blocks[y].length) + this.constructions[name].blocks[y].push([]); + for (let x = 0; x < max_size; x++) { + if (x >= this.constructions[name].blocks[y][z].length) + this.constructions[name].blocks[y][z].push(''); + } + } + } + } + this.agent.bot.on('idle', async () => { // Wait a while for inputs before acting independently - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); if (!this.agent.isIdle()) return; // Persue goal @@ -65,15 +81,19 @@ export class NPCContoller { async executeNext() { if (!this.agent.isIdle()) return; + await this.agent.coder.execute(async () => { + await skills.moveAway(this.agent.bot, 2); + }); - if (this.agent.bot.time.timeOfDay < 12000) { + if (this.agent.bot.time.timeOfDay < 13000) { // Exit any buildings let building = this.currentBuilding(); - if (building) { + if (building == this.data.home) { let door_pos = this.getBuildingDoor(building); if (door_pos) { await this.agent.coder.execute(async () => { await skills.useDoor(this.agent.bot, door_pos); + await skills.moveAway(this.agent.bot, 2); // If the bot is too close to the building it will try to enter again }); } } @@ -96,6 +116,9 @@ export class NPCContoller { await skills.goToBed(this.agent.bot); }); } + + if (this.agent.isIdle()) + this.agent.bot.emit('idle'); } async executeGoal() { @@ -142,9 +165,6 @@ export class NPCContoller { if (res.acted) break; } } - - if (this.agent.isIdle()) - this.agent.bot.emit('idle'); } currentBuilding() { @@ -186,10 +206,11 @@ export class NPCContoller { if (door_x !== null) break; } if (door_x === null) return null; - + let sizex = this.constructions[name].blocks[0][0].length; let sizez = this.constructions[name].blocks[0].length; - let orientation = this.data.built[name].orientation; + let orientation = 4 - this.data.built[name].orientation; // this conversion is opposite + if (orientation == 4) orientation = 0; [door_x, door_z] = rotateXZ(door_x, door_z, orientation, sizex, sizez); door_y += this.constructions[name].offset; diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js index d5c0528..53d8486 100644 --- a/src/agent/npc/utils.js +++ b/src/agent/npc/utils.js @@ -77,6 +77,8 @@ export function blockSatisfied(target_name, block) { return block.name.endsWith(target_name); } else if (target_name == 'bed') { return block.name.endsWith('bed'); + } else if (target_name == 'torch') { + return block.name.includes('torch'); } return block.name == target_name; } From b9f2f74df4f9dc2686424e58b3404a07e0efbd4a Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 19 Apr 2024 14:01:46 -0500 Subject: [PATCH 18/20] fixed torch placer bug --- src/agent/modes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/modes.js b/src/agent/modes.js index 78b62a6..0298e21 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -87,7 +87,7 @@ const modes = [ // TODO: check light level instead of nearby torches, block.light is broken const near_torch = world.getNearestBlock(agent.bot, 'torch', 6); if (!near_torch) { - let torches = agent.bot.inventory.items().filter(item => item.name.includes('torch')); + let torches = agent.bot.inventory.items().filter(item => item.name === 'torch'); if (torches.length > 0) { const torch = torches[0]; const pos = agent.bot.entity.position; From 9a929fc3b292abdb21579890a6d3820e271ca7e2 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 19 Apr 2024 14:02:30 -0500 Subject: [PATCH 19/20] added activate command --- src/agent/commands/actions.js | 8 ++++++++ src/agent/library/skills.js | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 0b8943d..a57a464 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -182,6 +182,14 @@ export const actionsList = [ await skills.goToBed(agent.bot); }) }, + { + name: '!activate', + description: 'Activate the nearest object of a given type.', + params: {'type': '(string) The type of object to activate.'}, + perform: wrapExecution(async (agent, type) => { + await skills.activateNearestBlock(agent.bot, type); + }) + }, { name: '!stay', description: 'Stay in the current location no matter what. Pauses all modes.', diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 829ccbe..0e5f1f8 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -869,3 +869,27 @@ export async function tillAndSow(bot, x, y, z, seedType=null) { } return true; } + +export async function activateNearestBlock(bot, type) { + /** + * Activate the nearest block of the given type. + * @param {MinecraftBot} bot, reference to the minecraft bot. + * @param {string} type, the type of block to activate. + * @returns {Promise} true if the block was activated, false otherwise. + * @example + * await skills.activateNearestBlock(bot, "lever"); + * **/ + let block = world.getNearestBlock(bot, type, 16); + if (!block) { + log(bot, `Could not find any ${type} to activate.`); + return false; + } + if (bot.entity.position.distanceTo(block.position) > 4.5) { + let pos = block.position; + bot.pathfinder.setMovements(new pf.Movements(bot)); + await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); + } + await bot.activateBlock(block); + log(bot, `Activated ${type} at x:${block.position.x.toFixed(1)}, y:${block.position.y.toFixed(1)}, z:${block.position.z.toFixed(1)}.`); + return true; +} From 80664df71baaaf3dc74697d6ceb7e87cbbab4032 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 19 Apr 2024 14:54:16 -0500 Subject: [PATCH 20/20] added andy_npc.json as example --- andy_npc.json | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 andy_npc.json diff --git a/andy_npc.json b/andy_npc.json new file mode 100644 index 0000000..a392812 --- /dev/null +++ b/andy_npc.json @@ -0,0 +1,165 @@ +{ + "name": "andy_npc", + + "model": "gpt-3.5-turbo", + + "conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", + + "coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will recieve it's output. If you are satisfied with the response, respond without a codeblock in a conversational way. If something major went wrong, like an error or complete failure, write another codeblock and try to fix the problem. Minor mistakes are acceptable. Be maximally efficient, creative, and clear. Do not use commands !likeThis, only use codeblocks. Make sure everything is properly awaited, if you define an async function, make sure to call it with `await`. Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, take a deep breath and good luck! \n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nBegin coding:", + + "saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation in your next response. Store information that will help you improve as a Minecraft bot. Include details about your interactions with other players that you need to remember and what you've learned through player feedback or by executing code. Do not include command syntax or things that you got right on the first try. Be extremely brief and use as few words as possible.\nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the memory text: ", + + "npc": { + "goals": [ + "wooden_pickaxe", + "hole", + "stone_axe", + "stone_pickaxe", + "stone_axe", + "house", + "furnace", + "iron_pickaxe", + "iron_axe", + "iron_sword", + "iron_armor" + ] + }, + + "conversation_examples": [ + [ + {"role": "user", "content": "miner_32: Hey! What are you up to?"}, + {"role": "assistant", "content": "Nothing much miner_32, what do you need?"} + ], + + [ + {"role": "user", "content": "grombo_Xx: What do you see?"}, + {"role": "assistant", "content": "Let me see... !nearbyBlocks"}, + {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"}, + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone."} + ], + + [ + {"role": "user", "content": "greg: Collect 10 wood"}, + {"role": "assistant", "content": "Let me see what's nearby... !nearbyBlocks"}, + {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"}, + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs. !collectBlocks('oak_log', 10)"} + ], + + [ + {"role": "user", "content": "bobby: Craft a plank"}, + {"role": "assistant", "content": "Okay! !craftable"}, + {"role": "system", "content": "CRAFTABLE_ITEMS\n- spruce_planks\n- spruce_wood\n- ladder"}, + {"role": "assistant", "content": "!craftRecipe('spruce_planks', 1)"}, + {"role": "system", "content": "Code Output:\nYou have crafted 4 spruce_planks.\nCode execution finished successfully."}, + {"role": "assistant", "content": "I've crafted 4 spruce planks!"} + ], + + [ + {"role": "user", "content": "zZZn98: come here"}, + {"role": "assistant", "content": "On my way! !goToPlayer('zZZn98', 3)"}, + {"role": "system", "content": "Arrived at player."}, + {"role": "assistant", "content": "Here!"}, + {"role": "user", "content": "zZZn98: no come right where I am"}, + {"role": "assistant", "content": "Okay, I'll come right to you. !goToPlayer('zZZn98', 0)"} + ], + + [ + {"role": "user", "content": "hanky: collect some sand and give it to me please"}, + {"role": "assistant", "content": "Collecting sand... !newAction"}, + {"role": "system", "content": "Code Output:\nYou have reached player hanky.\nCode execution finished successfully."}, + {"role": "assistant", "content": "Here!"} + ], + + [ + {"role": "user", "content": "joe: Follow me!"}, + {"role": "assistant", "content": "Sure! !followPlayer('joe', 4)"}, + {"role": "user", "content": "joe: Not so close!"}, + {"role": "assistant", "content": "Okay, I'll keep my distance. !followPlayer('joe', 3)"} + ], + + [ + {"role": "user", "content": "hanky: kill that zombie!"}, + {"role": "assistant", "content": "I'm attacking! !attack('zombie')"}, + {"role": "system", "content": "Code Output:\nNo zombie nearby"}, + {"role": "assistant", "content": "I could not find a zombie nearby."} + ], + + [ + {"role": "user", "content": "abc: stop"}, + {"role": "assistant", "content": "Sure. !stop"}, + {"role": "system", "content": "Agent action stopped."}, + {"role": "assistant", "content": "I've stopped! What next?"} + ], + + [ + {"role": "user", "content": "maya: go to the nearest oak log"}, + {"role": "assistant", "content": "!newAction`"}, + {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, + {"role": "assistant", "content": "Apologies, I made an error. Would you like me to try again?"} + ], + + [ + {"role": "user", "content": "trixy88: craft some sticks"}, + {"role": "assistant", "content": "!craftRecipe('stick', 4)"}, + {"role": "system", "content": "Code Output:\nYou have crafted 16 sticks.\nCode execution finished successfully."}, + {"role": "assistant", "content": "I've crafted 16 sticks!"} + ], + + [ + {"role": "user", "content": "poppy: build a cobblestone house"}, + {"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"} + ], + + [ + {"role": "user", "content": "zib: build a dirt tower"}, + {"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"} + ], + + [ + {"role": "user", "content": "umpire_man: follow me without hunting"}, + {"role": "assistant", "content": "Okay, first I'll turn off hunting mode !setMode('hunting', false)"}, + {"role": "system", "content": "'hunting' mode disabled."}, + {"role": "assistant", "content": "Now I'll follow you !followPlayer('umpire_man')"} + ] + ], + + "coding_examples": [ + [ + {"role": "user", "content": "greg: Collect 10 wood"}, + {"role": "assistant", "content": "```await skills.collectBlock(bot, 'oak_log', 10);\n```"}, + {"role": "system", "content": "Successfully collected 9 wood."}, + {"role": "assistant", "content": "I collected 9 oak logs, what next?"} + ], + [ + {"role": "user", "content": "bobby: cook some chicken"}, + {"role": "assistant", "content": "```\nawait skills.smeltItem(bot, 'chicken', 8);\n```"}, + {"role": "system", "content": "Successfully smelted 8 chicken into 8 cooked_chicken."}, + {"role": "assistant", "content": "I have cooked 8 chicken."} + ], + [ + {"role": "user", "content": "zZZn98: come here"}, + {"role": "assistant", "content": "```await skills.goToPlayer(bot, 'zZZn98');\n```"}, + {"role": "system", "content": "Code execution finished successfully."}, + {"role": "assistant", "content": "Here!"} + ], + [ + {"role": "user", "content": "maya: go to the nearest oak log"}, + {"role": "assistant", "content": "```await skills.goToPosition(bot, nearestOakLog.x, nearestOakLog.y, nearestOakLog.z);\n```"}, + {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, + {"role": "assistant", "content": "Apologies, I made an error. Let me try that again.```\n// I am navigating to the nearest oak log.\nlet position = world.getNearestBlock(bot, 'oak_log', 20);\nawait skills.goToPosition(bot, position.x, position.y, position.z);\n```"}, + {"role": "system", "content": "Arrived at location."}, + {"role": "assistant", "content": "I found an oak log and I am now standing next to it. What next?"} + ], + [ + {"role": "user", "content": "234jeb: build a little tower"}, + {"role": "assistant", "content": "```let pos = bot.entity.position;\nfor (let i = 0; i < 5; i++) {\n await skills.placeBlock(bot, 'dirt', pos.x, pos.y + i, pos.z);\n}\n```"}, + {"role": "system", "content": "Successfully placed 5 dirt."}, + {"role": "assistant", "content": "I built a little tower!"} + ], + [ + {"role": "user", "content": "brug: build a dirt house"}, + {"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n```"} + ] + ] + +} \ No newline at end of file