Merge pull request #44 from kolbytn/goals

Automatic Busy Work
This commit is contained in:
Kolby Nottingham 2024-04-19 13:02:16 -07:00 committed by GitHub
commit 65aa0ba35a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1235 additions and 1 deletions

165
andy_npc.json Normal file
View file

@ -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```"}
]
]
}

View file

@ -4,6 +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 { NPCContoller } from './npc/controller.js';
export class Agent {
@ -12,6 +13,8 @@ export class Agent {
this.name = this.prompter.getName();
this.history = new History(this);
this.coder = new Coder(this);
this.npc = new NPCContoller(this);
await this.prompter.initExamples();
if (load_mem)
@ -177,6 +180,9 @@ export class Agent {
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 () => {
@ -189,6 +195,8 @@ export class Agent {
}
}
}, INTERVAL);
this.bot.emit('idle');
}
isIdle() {

View file

@ -1,4 +1,5 @@
import { writeFileSync, readFileSync } from 'fs';
import { NPCData } from './npc/data.js';
export class History {
@ -52,6 +53,8 @@ export class History {
'memory': this.memory,
'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) {
@ -67,6 +70,7 @@ 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;
} catch (err) {
console.error(`No memory file '${this.memory_fp}' for agent ${this.name}.`);

View file

@ -777,6 +777,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<boolean>} 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) {
/**

View file

@ -266,4 +266,4 @@ export function getBiomeName(bot) {
**/
const biomeId = bot.world.getBiome(bot.entity.position);
return mc.getAllBiomes()[biomeId].name;
}
}

View file

@ -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';
import { blockSatisfied, getTypeOfGeneric, rotateXZ } from './utils.js';
export class BuildGoal {
constructor(agent) {
this.agent = agent;
}
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;
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] = 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 !== null && !blockSatisfied(block_name, current_block)) {
acted = true;
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) return {missing: missing, acted: acted, position: position, orientation: orientation};
}
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]++;
}
}
}
}
}
}
return {missing: missing, acted: acted, position: position, orientation: orientation};
}
}

View file

@ -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"]
]
]
}

View file

@ -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", ""]
]
]
}

223
src/agent/npc/controller.js Normal file
View file

@ -0,0 +1,223 @@
import { readdirSync, readFileSync } from 'fs';
import { NPCData } from './data.js';
import { ItemGoal } from './item_goal.js';
import { BuildGoal } from './build_goal.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 {
constructor(agent) {
this.agent = agent;
this.data = NPCData.fromObject(agent.prompter.prompts.npc);
this.temp_goals = [];
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;
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);
}
}
}
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, 5000));
if (!this.agent.isIdle()) return;
// Persue goal
if (!this.agent.coder.resume_func) {
this.executeNext();
this.agent.history.save();
}
});
}
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 < 13000) {
// Exit any buildings
let building = this.currentBuilding();
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
});
}
}
// 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);
});
}
if (this.agent.isIdle())
this.agent.bot.emit('idle');
}
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) {
// 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;
}
}
// Build construction goal
else {
let res = null;
if (this.data.built.hasOwnProperty(goal.name)) {
res = await this.build_goal.executeNext(
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.name]);
this.data.built[goal.name] = {
name: goal.name,
position: res.position,
orientation: res.orientation
};
}
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,
quantity: res.missing[block_name]
})
}
if (res.acted) break;
}
}
}
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 = 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;
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
};
}
}

37
src/agent/npc/data.js Normal file
View file

@ -0,0 +1,37 @@
export class NPCData {
constructor() {
this.goals = [];
this.built = {};
this.home = null;
}
toObject() {
let obj = {};
if (this.goals.length > 0)
obj.goals = this.goals;
if (Object.keys(this.built).length > 0)
obj.built = this.built;
if (this.home)
obj.home = this.home;
return obj;
}
static fromObject(obj) {
if (!obj) return null;
let npc = new NPCData();
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;
if (obj.home)
npc.home = obj.home;
return npc;
}
}

354
src/agent/npc/item_goal.js Normal file
View file

@ -0,0 +1,354 @@
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 = [
'coal_block',
'iron_block',
'gold_block',
'diamond_block',
'deepslate',
'blackstone',
'netherite',
'_wood',
'stripped_',
'crimson',
'warped',
'dye'
]
class ItemNode {
constructor(manager, wrapper, name) {
this.manager = manager;
this.wrapper = wrapper;
this.name = name;
this.type = '';
this.source = null;
this.prereq = null;
this.recipe = [];
this.fails = 0;
}
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({node: this.manager.nodes[key], quantity: value});
size += value;
}
if (size > 4) {
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;
}
setCollectable(source=null, tool=null) {
this.type = 'block';
if (source)
this.source = source;
else
this.source = this.name;
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_item) {
this.type = 'smelt';
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);
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;
}
setHuntable(animal_source) {
this.type = 'hunt';
this.source = animal_source;
return this;
}
getChildren() {
let children = [...this.recipe];
if (this.prereq) {
children.push({node: this.prereq, quantity: 1});
}
return children;
}
isReady() {
for (let child of this.getChildren()) {
if (!child.node.isDone(child.quantity)) {
return false;
}
}
return true;
}
isDone(quantity=1) {
if (this.manager.goal.name === this.name)
return false;
return itemSatisfied(this.manager.agent.bot, this.name, quantity);
}
getDepth(q=1) {
if (this.isDone(q)) {
return 0;
}
let depth = 0;
for (let child of this.getChildren()) {
depth = Math.max(depth, child.node.getDepth(child.quantity));
}
return depth + 1;
}
getFails(q=1) {
if (this.isDone(q)) {
return 0;
}
let fails = 0;
for (let child of this.getChildren()) {
fails += child.node.getFails(child.quantity);
}
return fails + this.fails;
}
getNext(q=1) {
if (this.isDone(q))
return null;
if (this.isReady())
return {node: this, quantity: q};
for (let child of this.getChildren()) {
let res = child.node.getNext(child.quantity);
if (res)
return res;
}
return null;
}
async execute(quantity=1) {
if (!this.isReady()) {
this.fails += 1;
return;
}
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, 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);
await skills.smeltItem(this.manager.agent.bot, to_smelt_name, to_smelt_quantity);
} else if (this.type === 'hunt') {
for (let i=0; i<quantity; i++) {
res = await skills.attackNearest(this.manager.agent.bot, this.source);
if (!res || this.manager.agent.bot.interrupt_code)
break;
}
} else if (this.type === 'craft') {
await skills.craftRecipe(this.manager.agent.bot, this.name, quantity);
}
let final_quantity = world.getInventoryCounts(this.manager.agent.bot)[this.name] || 0;
if (final_quantity <= init_quantity) {
this.fails += 1;
}
}
}
class ItemWrapper {
constructor(manager, parent, name) {
this.manager = manager;
this.name = name;
this.parent = parent;
this.methods = [];
let blacklisted = false;
for (let match of blacklist) {
if (name.includes(match)) {
blacklisted = true;
break;
}
}
if (!blacklisted && !this.containsCircularDependency()) {
this.createChildren();
}
}
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) {
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' && !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);
this.add_method(new ItemNode(this.manager, this, this.name).setCollectable(block_source, tool));
}
}
let smeltingIngredient = mc.getItemSmeltingIngredient(this.name);
if (smeltingIngredient) {
this.add_method(new ItemNode(this.manager, this, this.name).setSmeltable(smeltingIngredient));
}
let animal_source = mc.getItemAnimalSource(this.name);
if (animal_source) {
this.add_method(new ItemNode(this.manager, this, this.name).setHuntable(animal_source));
}
}
containsCircularDependency() {
let p = this.parent;
while (p) {
if (p.name === this.name) {
return true;
}
p = p.parent;
}
return false;
}
getBestMethod(q=1) {
let best_cost = -1;
let best_method = null;
for (let method of this.methods) {
let cost = method.getDepth(q) + method.getFails(q);
if (best_cost == -1 || cost < best_cost) {
best_cost = cost;
best_method = method;
}
}
return best_method
}
isDone(q=1) {
if (this.methods.length === 0)
return false;
return this.getBestMethod(q).isDone(q);
}
getDepth(q=1) {
if (this.methods.length === 0)
return 0;
return this.getBestMethod(q).getDepth(q);
}
getFails(q=1) {
if (this.methods.length === 0)
return 0;
return this.getBestMethod(q).getFails(q);
}
getNext(q=1) {
if (this.methods.length === 0)
return null;
return this.getBestMethod(q).getNext(q);
}
}
export class ItemGoal {
constructor(agent) {
this.agent = agent;
this.goal = null;
this.nodes = {};
this.failed = [];
}
async executeNext(item_name, item_quantity=1) {
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 = this.goal.getNext(item_quantity);
if (!next_info) {
console.log(`Invalid item goal ${this.goal.name}`);
return;
}
let next = next_info.node;
let quantity = next_info.quantity;
// Prevent unnecessary attempts to obtain blocks that are not nearby
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;
// 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);
await this.agent.coder.execute(async () => {
await skills.moveAway(this.agent.bot, 8);
});
} else {
this.failed.push(next.name);
await new Promise((resolve) => setTimeout(resolve, 500));
this.agent.bot.emit('idle');
}
return;
}
// Wait for the bot to be idle before attempting to execute the next goal
if (!this.agent.isIdle())
return;
// Execute the next goal
let init_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0;
await this.agent.coder.execute(async () => {
await next.execute(quantity);
});
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}`);
} else {
console.log(`Failed to obtain ${next.name} for goal ${this.goal.name}`);
}
}
}

126
src/agent/npc/utils.js Normal file
View file

@ -0,0 +1,126 @@
import * as world from '../library/world.js';
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
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 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
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;
}
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);
} 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;
}
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;
}
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];
}

View file

@ -12,6 +12,41 @@ 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 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) {
let bot = createBot({
username: username,
@ -122,4 +157,83 @@ 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 getItemBlockSources(itemName) {
let itemId = getItemId(itemName);
let sources = [];
for (let block of getAllBlocks()) {
if (block.drops.includes(itemId)) {
sources.push(block.name);
}
}
return sources;
}
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',
leather: 'cow',
wool: 'sheep'
}[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
}