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; +}