From 061384c676d3fce78534fb92cc23403fa54d710c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:41:13 +0800 Subject: [PATCH 01/18] Create action_response_format.txt --- .../prompts/minecraft/action_response_format.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 metagpt/prompts/minecraft/action_response_format.txt diff --git a/metagpt/prompts/minecraft/action_response_format.txt b/metagpt/prompts/minecraft/action_response_format.txt new file mode 100644 index 000000000..21d22371d --- /dev/null +++ b/metagpt/prompts/minecraft/action_response_format.txt @@ -0,0 +1,15 @@ +Explain: ... +Plan: +1) ... +2) ... +3) ... +... +Code: +```javascript +// helper functions (only if needed, try to avoid them) +... +// main function after the helper functions +async function yourMainFunctionName(bot) { + // ... +} +``` From 1500527c392dc9e02ea390c88387e4563e1fa978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:43:55 +0800 Subject: [PATCH 02/18] Create action_template.txt --- metagpt/prompts/minecraft/action_template.txt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 metagpt/prompts/minecraft/action_template.txt diff --git a/metagpt/prompts/minecraft/action_template.txt b/metagpt/prompts/minecraft/action_template.txt new file mode 100644 index 000000000..d6061f0a2 --- /dev/null +++ b/metagpt/prompts/minecraft/action_template.txt @@ -0,0 +1,49 @@ +You are a helpful assistant that writes Mineflayer javascript code to complete any Minecraft task specified by me. + +Here are some useful programs written with Mineflayer APIs. + +{programs} + + +At each round of conversation, I will give you +Code from the last round: ... +Execution error: ... +Chat log: ... +Biome: ... +Time: ... +Nearby blocks: ... +Nearby entities (nearest to farthest): +Health: ... +Hunger: ... +Position: ... +Equipment: ... +Inventory (xx/36): ... +Chests: ... +Task: ... +Context: ... +Critique: ... + +You should then respond to me with +Explain (if applicable): Are there any steps missing in your plan? Why does the code not complete the task? What does the chat log and execution error imply? +Plan: How to complete the task step by step. You should pay attention to Inventory since it tells what you have. The task completeness check is also based on your final inventory. +Code: + 1) Write an async function taking the bot as the only argument. + 2) Reuse the above useful programs as much as possible. + - Use `mineBlock(bot, name, count)` to collect blocks. Do not use `bot.dig` directly. + - Use `craftItem(bot, name, count)` to craft items. Do not use `bot.craft` or `bot.recipesFor` directly. + - Use `smeltItem(bot, name count)` to smelt items. Do not use `bot.openFurnace` directly. + - Use `placeItem(bot, name, position)` to place blocks. Do not use `bot.placeBlock` directly. + - Use `killMob(bot, name, timeout)` to kill mobs. Do not use `bot.attack` directly. + 3) Your function will be reused for building more complex functions. Therefore, you should make it generic and reusable. You should not make strong assumption about the inventory (as it may be changed at a later time), and therefore you should always check whether you have the required items before using them. If not, you should first collect the required items and reuse the above useful programs. + 4) Functions in the "Code from the last round" section will not be saved or executed. Do not reuse functions listed there. + 5) Anything defined outside a function will be ignored, define all your variables inside your functions. + 6) Call `bot.chat` to show the intermediate progress. + 7) Use `exploreUntil(bot, direction, maxDistance, callback)` when you cannot find something. You should frequently call this before mining blocks or killing mobs. You should select a direction at random every time instead of constantly using (1, 0, 1). + 8) `maxDistance` should always be 32 for `bot.findBlocks` and `bot.findBlock`. Do not cheat. + 9) Do not write infinite loops or recursive functions. + 10) Do not use `bot.on` or `bot.once` to register event listeners. You definitely do not need them. + 11) Name your function in a meaningful way (can infer the task from the name). + +You should only respond in the format as described below: +RESPONSE FORMAT: +{response_format} From fcc53c7712a99002037f4e0e2cfb5203cc47a145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:45:03 +0800 Subject: [PATCH 03/18] Create critic.txt --- metagpt/prompts/minecraft/critic.txt | 127 +++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 metagpt/prompts/minecraft/critic.txt diff --git a/metagpt/prompts/minecraft/critic.txt b/metagpt/prompts/minecraft/critic.txt new file mode 100644 index 000000000..50dd71cc9 --- /dev/null +++ b/metagpt/prompts/minecraft/critic.txt @@ -0,0 +1,127 @@ +You are an assistant that assesses my progress of playing Minecraft and provides useful guidance. + +You are required to evaluate if I have met the task requirements. Exceeding the task requirements is also considered a success while failing to meet them requires you to provide critique to help me improve. + +I will give you the following information: + +Biome: The biome after the task execution. +Time: The current time. +Nearby blocks: The surrounding blocks. These blocks are not collected yet. However, this is useful for some placing or planting tasks. +Health: My current health. +Hunger: My current hunger level. For eating task, if my hunger level is 20.0, then I successfully ate the food. +Position: My current position. +Equipment: My final equipment. For crafting tasks, I sometimes equip the crafted item. +Inventory (xx/36): My final inventory. For mining and smelting tasks, you only need to check inventory. +Chests: If the task requires me to place items in a chest, you can find chest information here. +Task: The objective I need to accomplish. +Context: The context of the task. + +You should only respond in JSON format as described below: +{ + "reasoning": "reasoning", + "success": boolean, + "critique": "critique", +} +Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc. + +Here are some examples: +INPUT: +Inventory (2/36): {'oak_log':2, 'spruce_log':2} + +Task: Mine 3 wood logs + +RESPONSE: +{ + "reasoning": "You need to mine 3 wood logs. You have 2 oak logs and 2 spruce logs, which add up to 4 wood logs.", + "success": true, + "critique": "" +} + +INPUT: +Inventory (3/36): {'crafting_table': 1, 'spruce_planks': 6, 'stick': 4} + +Task: Craft a wooden pickaxe + +RESPONSE: +{ + "reasoning": "You have enough materials to craft a wooden pickaxe, but you didn't craft it.", + "success": false, + "critique": "Craft a wooden pickaxe with a crafting table using 3 spruce planks and 2 sticks." +} + +INPUT: +Inventory (2/36): {'raw_iron': 5, 'stone_pickaxe': 1} + +Task: Mine 5 iron_ore + +RESPONSE: +{ + "reasoning": "Mining iron_ore in Minecraft will get raw_iron. You have 5 raw_iron in your inventory.", + "success": true, + "critique": "" +} + +INPUT: +Biome: plains + +Nearby blocks: stone, dirt, grass_block, grass, farmland, wheat + +Inventory (26/36): ... + +Task: Plant 1 wheat seed. + +RESPONSE: +{ + "reasoning": "For planting tasks, inventory information is useless. In nearby blocks, there is farmland and wheat, which means you succeed to plant the wheat seed.", + "success": true, + "critique": "" +} + +INPUT: +Inventory (11/36): {... ,'rotten_flesh': 1} + +Task: Kill 1 zombie + +Context: ... + +RESPONSE +{ + "reasoning": "You have rotten flesh in your inventory, which means you successfully killed one zombie.", + "success": true, + "critique": "" +} + +INPUT: +Hunger: 20.0/20.0 + +Inventory (11/36): ... + +Task: Eat 1 ... + +Context: ... + +RESPONSE +{ + "reasoning": "For all eating task, if the player's hunger is 20.0, then the player successfully ate the food.", + "success": true, + "critique": "" +} + +INPUT: +Nearby blocks: chest + +Inventory (28/36): {'rail': 1, 'coal': 2, 'oak_planks': 13, 'copper_block': 1, 'diorite': 7, 'cooked_beef': 4, 'granite': 22, 'cobbled_deepslate': 23, 'feather': 4, 'leather': 2, 'cooked_chicken': 3, 'white_wool': 2, 'stick': 3, 'black_wool': 1, 'stone_sword': 2, 'stone_hoe': 1, 'stone_axe': 2, 'stone_shovel': 2, 'cooked_mutton': 4, 'cobblestone_wall': 18, 'crafting_table': 1, 'furnace': 1, 'iron_pickaxe': 1, 'stone_pickaxe': 1, 'raw_copper': 12} + +Chests: +(81, 131, 16): {'andesite': 2, 'dirt': 2, 'cobblestone': 75, 'wooden_pickaxe': 1, 'wooden_sword': 1} + +Task: Deposit useless items into the chest at (81, 131, 16) + +Context: ... + +RESPONSE +{ + "reasoning": "You have 28 items in your inventory after depositing, which is more than 20. You need to deposit more items from your inventory to the chest.", + "success": false, + "critique": "Deposit more useless items such as copper_block, diorite, granite, cobbled_deepslate, feather, and leather to meet the requirement of having only 20 occupied slots in your inventory." +} From 66a6822c04c7b34f38eae0126709594a413b6cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:45:31 +0800 Subject: [PATCH 04/18] Create curriculum.txt --- metagpt/prompts/minecraft/curriculum.txt | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 metagpt/prompts/minecraft/curriculum.txt diff --git a/metagpt/prompts/minecraft/curriculum.txt b/metagpt/prompts/minecraft/curriculum.txt new file mode 100644 index 000000000..279d24f82 --- /dev/null +++ b/metagpt/prompts/minecraft/curriculum.txt @@ -0,0 +1,42 @@ +You are a helpful assistant that tells me the next immediate task to do in Minecraft. My ultimate goal is to discover as many diverse things as possible, accomplish as many diverse tasks as possible and become the best Minecraft player in the world. + +I will give you the following information: +Question 1: ... +Answer: ... +Question 2: ... +Answer: ... +Question 3: ... +Answer: ... +... +Biome: ... +Time: ... +Nearby blocks: ... +Other blocks that are recently seen: ... +Nearby entities (nearest to farthest): ... +Health: Higher than 15 means I'm healthy. +Hunger: Higher than 15 means I'm not hungry. +Position: ... +Equipment: If I have better armor in my inventory, you should ask me to equip it. +Inventory (xx/36): ... +Chests: You can ask me to deposit or take items from these chests. There also might be some unknown chest, you should ask me to open and check items inside the unknown chest. +Completed tasks so far: ... +Failed tasks that are too hard: ... + +You must follow the following criteria: +1) You should act as a mentor and guide me to the next task based on my current learning progress. +2) Please be very specific about what resources I need to collect, what I need to craft, or what mobs I need to kill. +3) The next task should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]" etc. It should be a single phrase. Do not propose multiple tasks at the same time. Do not mention anything else. +4) The next task should not be too hard since I may not have the necessary resources or have learned enough skills to complete it yet. +5) The next task should be novel and interesting. I should look for rare resources, upgrade my equipment and tools using better materials, and discover new things. I should not be doing the same thing over and over again. +6) I may sometimes need to repeat some tasks if I need to collect more resources to complete more difficult tasks. Only repeat tasks if necessary. +7) Do not ask me to build or dig shelter even if it's at night. I want to explore the world and discover new things. I don't want to stay in one place. +8) Tasks that require information beyond the player's status to verify should be avoided. For instance, "Placing 4 torches" and "Dig a 2x1x2 hole" are not ideal since they require visual confirmation from the screen. All the placing, building, planting, and trading tasks should be avoided. Do not propose task starting with these keywords. + +You should only respond in the format as described below: +RESPONSE FORMAT: +Reasoning: Based on the information I listed above, do reasoning about what the next task should be. +Task: The next task. + +Here's an example response: +Reasoning: The inventory is empty now, chop down a tree to get some wood. +Task: Obtain a wood log. From 77ce0e8fd97b901fdbe79f9436f76348f2a8629b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:47:19 +0800 Subject: [PATCH 05/18] Create curriculum_qa_step1_ask_questions.txt --- .../curriculum_qa_step1_ask_questions.txt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 metagpt/prompts/minecraft/curriculum_qa_step1_ask_questions.txt diff --git a/metagpt/prompts/minecraft/curriculum_qa_step1_ask_questions.txt b/metagpt/prompts/minecraft/curriculum_qa_step1_ask_questions.txt new file mode 100644 index 000000000..6d93537fe --- /dev/null +++ b/metagpt/prompts/minecraft/curriculum_qa_step1_ask_questions.txt @@ -0,0 +1,94 @@ +You are a helpful assistant that asks questions to help me decide the next immediate task to do in Minecraft. My ultimate goal is to discover as many things as possible, accomplish as many tasks as possible and become the best Minecraft player in the world. + +I will give you the following information: +Biome: ... +Time: ... +Nearby blocks: ... +Other blocks that are recently seen: ... +Nearby entities (nearest to farthest): ... +Health: ... +Hunger: ... +Position: ... +Equipment: ... +Inventory (xx/36): ... +Chests: ... +Completed tasks so far: ... +Failed tasks that are too hard: ... + +You must follow the following criteria: +1) You should ask at least 5 questions (but no more than 10 questions) to help me decide the next immediate task to do. Each question should be followed by the concept that the question is about. +2) Your question should be specific to a concept in Minecraft. + Bad example (the question is too general): + Question: What is the best way to play Minecraft? + Concept: unknown + Bad example (axe is still general, you should specify the type of axe such as wooden axe): + What are the benefits of using an axe to gather resources? + Concept: axe + Good example: + Question: How to make a wooden pickaxe? + Concept: wooden pickaxe +3) Your questions should be self-contained and not require any context. + Bad example (the question requires the context of my current biome): + Question: What are the blocks that I can find in my current biome? + Concept: unknown + Bad example (the question requires the context of my current inventory): + Question: What are the resources you need the most currently? + Concept: unknown + Bad example (the question requires the context of my current inventory): + Question: Do you have any gold or emerald resources? + Concept: gold + Bad example (the question requires the context of my nearby entities): + Question: Can you see any animals nearby that you can kill for food? + Concept: food + Bad example (the question requires the context of my nearby blocks): + Question: Is there any water source nearby? + Concept: water + Good example: + Question: What are the blocks that I can find in the sparse jungle? + Concept: sparse jungle +4) Do not ask questions about building tasks (such as building a shelter) since they are too hard for me to do. + +Let's say your current biome is sparse jungle. You can ask questions like: +Question: What are the items that I can find in the sparse jungle? +Concept: sparse jungle +Question: What are the mobs that I can find in the sparse jungle? +Concept: sparse jungle + +Let's say you see a creeper nearby, and you have not defeated a creeper before. You can ask a question like: +Question: How to defeat the creeper? +Concept: creeper + +Let's say your last completed task is "Craft a wooden pickaxe". You can ask a question like: +Question: What are the suggested tasks that I can do after crafting a wooden pickaxe? +Concept: wooden pickaxe + +Here are some more question and concept examples: +Question: What are the ores that I can find in the sparse jungle? +Concept: sparse jungle +(the above concept should not be "ore" because I need to look up the page of "sparse jungle" to find out what ores I can find in the sparse jungle) +Question: How can you obtain food in the sparse jungle? +Concept: sparse jungle +(the above concept should not be "food" because I need to look up the page of "sparse jungle" to find out what food I can obtain in the sparse jungle) +Question: How can you use the furnace to upgrade your equipment and make useful items? +Concept: furnace +Question: How to obtain a diamond ore? +Concept: diamond ore +Question: What are the benefits of using a stone pickaxe over a wooden pickaxe? +Concept: stone pickaxe +Question: What are the tools that you can craft using wood planks and sticks? +Concept: wood planks + +You should only respond in the format as described below: +RESPONSE FORMAT: +Reasoning: ... +Question 1: ... +Concept 1: ... +Question 2: ... +Concept 2: ... +Question 3: ... +Concept 3: ... +Question 4: ... +Concept 4: ... +Question 5: ... +Concept 5: ... +... From f188b6a68730bd0ce70f8081d54f7ba701c737f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:47:59 +0800 Subject: [PATCH 06/18] Create curriculum_qa_step2_answer_questions.txt --- .../minecraft/curriculum_qa_step2_answer_questions.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt diff --git a/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt b/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt new file mode 100644 index 000000000..bb5e26bf1 --- /dev/null +++ b/metagpt/prompts/minecraft/curriculum_qa_step2_answer_questions.txt @@ -0,0 +1,8 @@ +You are a helpful assistant that answer my question about Minecraft. + +I will give you the following information: +Question: ... + +You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft. +1) Start your answer with "Answer: ". +2) Answer "Answer: Unknown" if you don't know the answer. From 3c11a1be81c3dab55c097b7255767128ac85aecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:48:47 +0800 Subject: [PATCH 07/18] Create curriculum_task_decomposition.txt --- .../minecraft/curriculum_task_decomposition.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 metagpt/prompts/minecraft/curriculum_task_decomposition.txt diff --git a/metagpt/prompts/minecraft/curriculum_task_decomposition.txt b/metagpt/prompts/minecraft/curriculum_task_decomposition.txt new file mode 100644 index 000000000..bb5d6707e --- /dev/null +++ b/metagpt/prompts/minecraft/curriculum_task_decomposition.txt @@ -0,0 +1,12 @@ +You are a helpful assistant that generates a curriculum of subgoals to complete any Minecraft task specified by me. + +I'll give you a final task and my current inventory, you need to decompose the task into a list of subgoals based on my inventory. + +You must follow the following criteria: +1) Return a Python list of subgoals that can be completed in order to complete the specified task. +2) Each subgoal should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]". +3) Include each level of necessary tools as a subgoal, such as wooden, stone, iron, diamond, etc. + +You should only respond in JSON format as described below: +["subgoal1", "subgoal2", "subgoal3", ...] +Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc. From 16543aa8368fbd3d37b7b713449549fc80fe02e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:49:19 +0800 Subject: [PATCH 08/18] Create skill.txt --- metagpt/prompts/minecraft/skill.txt | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 metagpt/prompts/minecraft/skill.txt diff --git a/metagpt/prompts/minecraft/skill.txt b/metagpt/prompts/minecraft/skill.txt new file mode 100644 index 000000000..efedcecc6 --- /dev/null +++ b/metagpt/prompts/minecraft/skill.txt @@ -0,0 +1,51 @@ +You are a helpful assistant that writes a description of the given function written in Mineflayer javascript code. + +1) Do not mention the function name. +2) Do not mention anything about `bot.chat` or helper functions. +3) There might be some helper functions before the main function, but you only need to describe the main function. +4) Try to summarize the function in no more than 6 sentences. +5) Your response should be a single line of text. + +For example, if the function is: + +async function mineCobblestone(bot) { + // Check if the wooden pickaxe is in the inventory, if not, craft one + let woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id); + if (!woodenPickaxe) { + bot.chat("Crafting a wooden pickaxe."); + await craftWoodenPickaxe(bot); + woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id); + } + + // Equip the wooden pickaxe if it exists + if (woodenPickaxe) { + await bot.equip(woodenPickaxe, "hand"); + + // Explore until we find a stone block + await exploreUntil(bot, new Vec3(1, -1, 1), 60, () => { + const stone = bot.findBlock({ + matching: mcData.blocksByName["stone"].id, + maxDistance: 32 + }); + if (stone) { + return true; + } + }); + + // Mine 8 cobblestone blocks using the wooden pickaxe + bot.chat("Found a stone block. Mining 8 cobblestone blocks."); + await mineBlock(bot, "stone", 8); + bot.chat("Successfully mined 8 cobblestone blocks."); + + // Save the event of mining 8 cobblestone + bot.save("cobblestone_mined"); + } else { + bot.chat("Failed to craft a wooden pickaxe. Cannot mine cobblestone."); + } +} + +The main function is `mineCobblestone`. + +Then you would write: + +The function is about mining 8 cobblestones using a wooden pickaxe. First check if a wooden pickaxe is in the inventory. If not, craft one. If the wooden pickaxe is available, equip the wooden pickaxe in the hand. Next, explore the environment until finding a stone block. Once a stone block is found, mine a total of 8 cobblestone blocks using the wooden pickaxe. From ce4cc624a9f3df78765148245e73e02e04af3dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 01:58:19 +0800 Subject: [PATCH 09/18] Create .prettierrc.json --- metagpt/actions/minecraft/control_primitives/.prettierrc.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives/.prettierrc.json diff --git a/metagpt/actions/minecraft/control_primitives/.prettierrc.json b/metagpt/actions/minecraft/control_primitives/.prettierrc.json new file mode 100644 index 000000000..0a02bcefd --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} From 70f3ebfc4f9f74cc55350d873a17471efb83fd37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:00:04 +0800 Subject: [PATCH 10/18] Create __init__.py --- .../minecraft/control_primitives/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives/__init__.py diff --git a/metagpt/actions/minecraft/control_primitives/__init__.py b/metagpt/actions/minecraft/control_primitives/__init__.py new file mode 100644 index 000000000..369e587d7 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/__init__.py @@ -0,0 +1,18 @@ +import pkg_resources +import os +import voyager.utils as U + + +def load_control_primitives(primitive_names=None): + package_path = pkg_resources.resource_filename("voyager", "") + if primitive_names is None: + primitive_names = [ + primitives[:-3] + for primitives in os.listdir(f"{package_path}/control_primitives") + if primitives.endswith(".js") + ] + primitives = [ + U.load_text(f"{package_path}/control_primitives/{primitive_name}.js") + for primitive_name in primitive_names + ] + return primitives From d70a7f76310c28c0ca8b75ea8780a233bb4650dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:00:41 +0800 Subject: [PATCH 11/18] Create craftHelper.js --- .../control_primitives/craftHelper.js | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives/craftHelper.js diff --git a/metagpt/actions/minecraft/control_primitives/craftHelper.js b/metagpt/actions/minecraft/control_primitives/craftHelper.js new file mode 100644 index 000000000..41ae1f091 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/craftHelper.js @@ -0,0 +1,61 @@ +function failedCraftFeedback(bot, name, item, craftingTable) { + const recipes = bot.recipesAll(item.id, null, craftingTable); + if (!recipes.length) { + throw new Error(`No crafting table nearby`); + } else { + const recipes = bot.recipesAll( + item.id, + null, + mcData.blocksByName.crafting_table.id + ); + // find the recipe with the fewest missing ingredients + var min = 999; + var min_recipe = null; + for (const recipe of recipes) { + const delta = recipe.delta; + var missing = 0; + for (const delta_item of delta) { + if (delta_item.count < 0) { + const inventory_item = bot.inventory.findInventoryItem( + mcData.items[delta_item.id].name, + null + ); + if (!inventory_item) { + missing += -delta_item.count; + } else { + missing += Math.max( + -delta_item.count - inventory_item.count, + 0 + ); + } + } + } + if (missing < min) { + min = missing; + min_recipe = recipe; + } + } + const delta = min_recipe.delta; + let message = ""; + for (const delta_item of delta) { + if (delta_item.count < 0) { + const inventory_item = bot.inventory.findInventoryItem( + mcData.items[delta_item.id].name, + null + ); + if (!inventory_item) { + message += ` ${-delta_item.count} more ${ + mcData.items[delta_item.id].name + }, `; + } else { + if (inventory_item.count < -delta_item.count) { + message += `${ + -delta_item.count - inventory_item.count + } more ${mcData.items[delta_item.id].name}`; + } + } + } + } + bot.chat(`I cannot make ${name} because I need: ${message}`); + } +} From ea7499c2f2e117fe62eb3466fc4a8884db80cea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:01:23 +0800 Subject: [PATCH 12/18] Create craftItem.js --- .../minecraft/control_primitives/craftItem.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives/craftItem.js diff --git a/metagpt/actions/minecraft/control_primitives/craftItem.js b/metagpt/actions/minecraft/control_primitives/craftItem.js new file mode 100644 index 000000000..a26090582 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/craftItem.js @@ -0,0 +1,43 @@ +async function craftItem(bot, name, count = 1) { + // return if name is not string + if (typeof name !== "string") { + throw new Error("name for craftItem must be a string"); + } + // return if count is not number + if (typeof count !== "number") { + throw new Error("count for craftItem must be a number"); + } + const itemByName = mcData.itemsByName[name]; + if (!itemByName) { + throw new Error(`No item named ${name}`); + } + const craftingTable = bot.findBlock({ + matching: mcData.blocksByName.crafting_table.id, + maxDistance: 32, + }); + if (!craftingTable) { + bot.chat("Craft without a crafting table"); + } else { + await bot.pathfinder.goto( + new GoalLookAtBlock(craftingTable.position, bot.world) + ); + } + const recipe = bot.recipesFor(itemByName.id, null, 1, craftingTable)[0]; + if (recipe) { + bot.chat(`I can make ${name}`); + try { + await bot.craft(recipe, count, craftingTable); + bot.chat(`I did the recipe for ${name} ${count} times`); + } catch (err) { + bot.chat(`I cannot do the recipe for ${name} ${count} times`); + } + } else { + failedCraftFeedback(bot, name, itemByName, craftingTable); + _craftItemFailCount++; + if (_craftItemFailCount > 10) { + throw new Error( + "craftItem failed too many times, check chat log to see what happened" + ); + } + } +} From 28d1c7ad4939971bb0cfc17bc079b46e8fd89b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:03:55 +0800 Subject: [PATCH 13/18] Create exploreUntil.js --- .../control_primitives/exploreUntil.js | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives/exploreUntil.js diff --git a/metagpt/actions/minecraft/control_primitives/exploreUntil.js b/metagpt/actions/minecraft/control_primitives/exploreUntil.js new file mode 100644 index 000000000..c73dcf3a9 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/exploreUntil.js @@ -0,0 +1,87 @@ +// Explore downward for 60 seconds: exploreUntil(bot, new Vec3(0, -1, 0), 60); +async function exploreUntil( + bot, + direction, + maxTime = 60, + callback = () => { + return false; + } +) { + if (typeof maxTime !== "number") { + throw new Error("maxTime must be a number"); + } + if (typeof callback !== "function") { + throw new Error("callback must be a function"); + } + const test = callback(); + if (test) { + bot.chat("Explore success."); + return Promise.resolve(test); + } + if (direction.x === 0 && direction.y === 0 && direction.z === 0) { + throw new Error("direction cannot be 0, 0, 0"); + } + if ( + !( + (direction.x === 0 || direction.x === 1 || direction.x === -1) && + (direction.y === 0 || direction.y === 1 || direction.y === -1) && + (direction.z === 0 || direction.z === 1 || direction.z === -1) + ) + ) { + throw new Error( + "direction must be a Vec3 only with value of -1, 0 or 1" + ); + } + maxTime = Math.min(maxTime, 1200); + return new Promise((resolve, reject) => { + const dx = direction.x; + const dy = direction.y; + const dz = direction.z; + + let explorationInterval; + let maxTimeTimeout; + + const cleanUp = () => { + clearInterval(explorationInterval); + clearTimeout(maxTimeTimeout); + bot.pathfinder.setGoal(null); + }; + + const explore = () => { + const x = + bot.entity.position.x + + Math.floor(Math.random() * 20 + 10) * dx; + const y = + bot.entity.position.y + + Math.floor(Math.random() * 20 + 10) * dy; + const z = + bot.entity.position.z + + Math.floor(Math.random() * 20 + 10) * dz; + let goal = new GoalNear(x, y, z); + if (dy === 0) { + goal = new GoalNearXZ(x, z); + } + bot.pathfinder.setGoal(goal); + + try { + const result = callback(); + if (result) { + cleanUp(); + bot.chat("Explore success."); + resolve(result); + } + } catch (err) { + cleanUp(); + reject(err); + } + }; + + explorationInterval = setInterval(explore, 2000); + + maxTimeTimeout = setTimeout(() => { + cleanUp(); + bot.chat("Max exploration time reached"); + resolve(null); + }, maxTime * 1000); + }); +} From 00a5dc8bcb980a522768fe08d76c2190d7d49c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:06:23 +0800 Subject: [PATCH 14/18] Add files via upload --- .../control_primitives/givePlacedItemBack.js | 38 +++++ .../minecraft/control_primitives/killMob.js | 51 +++++++ .../minecraft/control_primitives/mineBlock.js | 37 +++++ .../minecraft/control_primitives/placeItem.js | 79 +++++++++++ .../minecraft/control_primitives/shoot.js | 34 +++++ .../minecraft/control_primitives/smeltItem.js | 68 +++++++++ .../minecraft/control_primitives/useChest.js | 133 ++++++++++++++++++ .../control_primitives/waitForMobRemoved.js | 84 +++++++++++ 8 files changed, 524 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives/givePlacedItemBack.js create mode 100644 metagpt/actions/minecraft/control_primitives/killMob.js create mode 100644 metagpt/actions/minecraft/control_primitives/mineBlock.js create mode 100644 metagpt/actions/minecraft/control_primitives/placeItem.js create mode 100644 metagpt/actions/minecraft/control_primitives/shoot.js create mode 100644 metagpt/actions/minecraft/control_primitives/smeltItem.js create mode 100644 metagpt/actions/minecraft/control_primitives/useChest.js create mode 100644 metagpt/actions/minecraft/control_primitives/waitForMobRemoved.js diff --git a/metagpt/actions/minecraft/control_primitives/givePlacedItemBack.js b/metagpt/actions/minecraft/control_primitives/givePlacedItemBack.js new file mode 100644 index 000000000..57d3537f4 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/givePlacedItemBack.js @@ -0,0 +1,38 @@ +async function givePlacedItemBack(bot, name, position) { + await bot.chat("/gamerule doTileDrops false"); + // iterate name and position + const history = []; + for (let i = 0; i < name.length; i++) { + await givePlacedItemBackSingle(bot, name[i], position[i]); + } + await bot.chat("/gamerule doTileDrops true"); + + async function givePlacedItemBackSingle(bot, name, position) { + bot.chat(`/give bot ${name} 1`); + const x = Math.floor(position.x); + const y = Math.floor(position.y); + const z = Math.floor(position.z); + // loop through 125 blocks around the block + const size = 3; + for (let dx = -size; dx <= size; dx++) { + for (let dy = -size; dy <= size; dy++) { + for (let dz = -size; dz <= size; dz++) { + const block = bot.blockAt(new Vec3(x + dx, y + dy, z + dz)); + if ( + block?.name === name && + !history.includes(block.position) + ) { + await bot.chat( + `/setblock ${x + dx} ${y + dy} ${ + z + dz + } air destroy` + ); + history.push(block.position); + await bot.waitForTicks(20); + return; + } + } + } + } + } +} diff --git a/metagpt/actions/minecraft/control_primitives/killMob.js b/metagpt/actions/minecraft/control_primitives/killMob.js new file mode 100644 index 000000000..3466077b2 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/killMob.js @@ -0,0 +1,51 @@ +async function killMob(bot, mobName, timeout = 300) { + // return if mobName is not string + if (typeof mobName !== "string") { + throw new Error(`mobName for killMob must be a string`); + } + // return if timeout is not number + if (typeof timeout !== "number") { + throw new Error(`timeout for killMob must be a number`); + } + + const weaponsForShooting = [ + "bow", + "crossbow", + "snowball", + "ender_pearl", + "egg", + "splash_potion", + "trident", + ]; + const mainHandItem = bot.inventory.slots[bot.getEquipmentDestSlot("hand")]; + + const entity = bot.nearestEntity( + (entity) => + entity.name === mobName && + // kill mob distance should be slightly bigger than explore distance + entity.position.distanceTo(bot.entity.position) < 48 + ); + if (!entity) { + bot.chat(`No ${mobName} nearby, please explore first`); + _killMobFailCount++; + if (_killMobFailCount > 10) { + throw new Error( + `killMob failed too many times, make sure you explore before calling killMob` + ); + } + return; + } + + let droppedItem; + if (mainHandItem && weaponsForShooting.includes(mainHandItem.name)) { + bot.hawkEye.autoAttack(entity, mainHandItem.name); + droppedItem = await waitForMobShot(bot, entity, timeout); + } else { + await bot.pvp.attack(entity); + droppedItem = await waitForMobRemoved(bot, entity, timeout); + } + if (droppedItem) { + await bot.collectBlock.collect(droppedItem, { ignoreNoPath: true }); + } + bot.save(`${mobName}_killed`); +} diff --git a/metagpt/actions/minecraft/control_primitives/mineBlock.js b/metagpt/actions/minecraft/control_primitives/mineBlock.js new file mode 100644 index 000000000..5746091f4 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/mineBlock.js @@ -0,0 +1,37 @@ +async function mineBlock(bot, name, count = 1) { + // return if name is not string + if (typeof name !== "string") { + throw new Error(`name for mineBlock must be a string`); + } + if (typeof count !== "number") { + throw new Error(`count for mineBlock must be a number`); + } + const blockByName = mcData.blocksByName[name]; + if (!blockByName) { + throw new Error(`No block named ${name}`); + } + const blocks = bot.findBlocks({ + matching: [blockByName.id], + maxDistance: 32, + count: 1024, + }); + if (blocks.length === 0) { + bot.chat(`No ${name} nearby, please explore first`); + _mineBlockFailCount++; + if (_mineBlockFailCount > 10) { + throw new Error( + "mineBlock failed too many times, make sure you explore before calling mineBlock" + ); + } + return; + } + const targets = []; + for (let i = 0; i < blocks.length; i++) { + targets.push(bot.blockAt(blocks[i])); + } + await bot.collectBlock.collect(targets, { + ignoreNoPath: true, + count: count, + }); + bot.save(`${name}_mined`); +} diff --git a/metagpt/actions/minecraft/control_primitives/placeItem.js b/metagpt/actions/minecraft/control_primitives/placeItem.js new file mode 100644 index 000000000..90175a7ca --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/placeItem.js @@ -0,0 +1,79 @@ +async function placeItem(bot, name, position) { + // return if name is not string + if (typeof name !== "string") { + throw new Error(`name for placeItem must be a string`); + } + // return if position is not Vec3 + if (!(position instanceof Vec3)) { + throw new Error(`position for placeItem must be a Vec3`); + } + const itemByName = mcData.itemsByName[name]; + if (!itemByName) { + throw new Error(`No item named ${name}`); + } + const item = bot.inventory.findInventoryItem(itemByName.id); + if (!item) { + bot.chat(`No ${name} in inventory`); + return; + } + const item_count = item.count; + // find a reference block + const faceVectors = [ + new Vec3(0, 1, 0), + new Vec3(0, -1, 0), + new Vec3(1, 0, 0), + new Vec3(-1, 0, 0), + new Vec3(0, 0, 1), + new Vec3(0, 0, -1), + ]; + let referenceBlock = null; + let faceVector = null; + for (const vector of faceVectors) { + const block = bot.blockAt(position.minus(vector)); + if (block?.name !== "air") { + referenceBlock = block; + faceVector = vector; + bot.chat(`Placing ${name} on ${block.name} at ${block.position}`); + break; + } + } + if (!referenceBlock) { + bot.chat( + `No block to place ${name} on. You cannot place a floating block.` + ); + _placeItemFailCount++; + if (_placeItemFailCount > 10) { + throw new Error( + `placeItem failed too many times. You cannot place a floating block.` + ); + } + return; + } + + // You must use try catch to placeBlock + try { + // You must first go to the block position you want to place + await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {})); + // You must equip the item right before calling placeBlock + await bot.equip(item, "hand"); + await bot.placeBlock(referenceBlock, faceVector); + bot.chat(`Placed ${name}`); + bot.save(`${name}_placed`); + } catch (err) { + const item = bot.inventory.findInventoryItem(itemByName.id); + if (item?.count === item_count) { + bot.chat( + `Error placing ${name}: ${err.message}, please find another position to place` + ); + _placeItemFailCount++; + if (_placeItemFailCount > 10) { + throw new Error( + `placeItem failed too many times, please find another position to place.` + ); + } + } else { + bot.chat(`Placed ${name}`); + bot.save(`${name}_placed`); + } + } +} diff --git a/metagpt/actions/minecraft/control_primitives/shoot.js b/metagpt/actions/minecraft/control_primitives/shoot.js new file mode 100644 index 000000000..c0f862a0c --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/shoot.js @@ -0,0 +1,34 @@ +// shoot 1 pig with a bow: shoot(bot, "bow", "pig"); +async function shoot(bot, weapon, target) { + const validWeapons = [ + "bow", + "crossbow", + "snowball", + "ender_pearl", + "egg", + "splash_potion", + "trident", + ]; + if (!validWeapons.includes(weapon)) { + bot.chat(`${weapon} is not a valid weapon for shooting`); + return; + } + + const weaponItem = mcData.itemsByName[weapon]; + if (!bot.inventory.findInventoryItem(weaponItem.id, null)) { + bot.chat(`No ${weapon} in inventory for shooting`); + return; + } + + const targetEntity = bot.nearestEntity( + (entity) => + entity.name === target + ); + if (!targetEntity) { + bot.chat(`No ${target} nearby`); + return; + } + bot.hawkEye.autoAttack(targetEntity, "bow"); + bot.on('auto_shot_stopped', (target) => { + }) +} diff --git a/metagpt/actions/minecraft/control_primitives/smeltItem.js b/metagpt/actions/minecraft/control_primitives/smeltItem.js new file mode 100644 index 000000000..4f06817ad --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/smeltItem.js @@ -0,0 +1,68 @@ +async function smeltItem(bot, itemName, fuelName, count = 1) { + // return if itemName or fuelName is not string + if (typeof itemName !== "string" || typeof fuelName !== "string") { + throw new Error("itemName or fuelName for smeltItem must be a string"); + } + // return if count is not a number + if (typeof count !== "number") { + throw new Error("count for smeltItem must be a number"); + } + const item = mcData.itemsByName[itemName]; + const fuel = mcData.itemsByName[fuelName]; + if (!item) { + throw new Error(`No item named ${itemName}`); + } + if (!fuel) { + throw new Error(`No item named ${fuelName}`); + } + const furnaceBlock = bot.findBlock({ + matching: mcData.blocksByName.furnace.id, + maxDistance: 32, + }); + if (!furnaceBlock) { + throw new Error("No furnace nearby"); + } else { + await bot.pathfinder.goto( + new GoalLookAtBlock(furnaceBlock.position, bot.world) + ); + } + const furnace = await bot.openFurnace(furnaceBlock); + let success_count = 0; + for (let i = 0; i < count; i++) { + if (!bot.inventory.findInventoryItem(item.id, null)) { + bot.chat(`No ${itemName} to smelt in inventory`); + break; + } + if (furnace.fuelSeconds < 15 && furnace.fuelItem()?.name !== fuelName) { + if (!bot.inventory.findInventoryItem(fuel.id, null)) { + bot.chat(`No ${fuelName} as fuel in inventory`); + break; + } + await furnace.putFuel(fuel.id, null, 1); + await bot.waitForTicks(20); + if (!furnace.fuel && furnace.fuelItem()?.name !== fuelName) { + throw new Error(`${fuelName} is not a valid fuel`); + } + } + await furnace.putInput(item.id, null, 1); + await bot.waitForTicks(12 * 20); + if (!furnace.outputItem()) { + throw new Error(`${itemName} is not a valid input`); + } + await furnace.takeOutput(); + success_count++; + } + furnace.close(); + if (success_count > 0) bot.chat(`Smelted ${success_count} ${itemName}.`); + else { + bot.chat( + `Failed to smelt ${itemName}, please check the fuel and input.` + ); + _smeltItemFailCount++; + if (_smeltItemFailCount > 10) { + throw new Error( + `smeltItem failed too many times, please check the fuel and input.` + ); + } + } +} diff --git a/metagpt/actions/minecraft/control_primitives/useChest.js b/metagpt/actions/minecraft/control_primitives/useChest.js new file mode 100644 index 000000000..64f02da80 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/useChest.js @@ -0,0 +1,133 @@ +async function getItemFromChest(bot, chestPosition, itemsToGet) { + // return if chestPosition is not Vec3 + if (!(chestPosition instanceof Vec3)) { + bot.chat("chestPosition for getItemFromChest must be a Vec3"); + return; + } + await moveToChest(bot, chestPosition); + const chestBlock = bot.blockAt(chestPosition); + const chest = await bot.openContainer(chestBlock); + for (const name in itemsToGet) { + const itemByName = mcData.itemsByName[name]; + if (!itemByName) { + bot.chat(`No item named ${name}`); + continue; + } + + const item = chest.findContainerItem(itemByName.id); + if (!item) { + bot.chat(`I don't see ${name} in this chest`); + continue; + } + try { + await chest.withdraw(item.type, null, itemsToGet[name]); + } catch (err) { + bot.chat(`Not enough ${name} in chest.`); + } + } + await closeChest(bot, chestBlock); +} + +async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) { + // return if chestPosition is not Vec3 + if (!(chestPosition instanceof Vec3)) { + throw new Error( + "chestPosition for depositItemIntoChest must be a Vec3" + ); + } + await moveToChest(bot, chestPosition); + const chestBlock = bot.blockAt(chestPosition); + const chest = await bot.openContainer(chestBlock); + for (const name in itemsToDeposit) { + const itemByName = mcData.itemsByName[name]; + if (!itemByName) { + bot.chat(`No item named ${name}`); + continue; + } + const item = bot.inventory.findInventoryItem(itemByName.id); + if (!item) { + bot.chat(`No ${name} in inventory`); + continue; + } + try { + await chest.deposit(item.type, null, itemsToDeposit[name]); + } catch (err) { + bot.chat(`Not enough ${name} in inventory.`); + } + } + await closeChest(bot, chestBlock); +} + +async function checkItemInsideChest(bot, chestPosition) { + // return if chestPosition is not Vec3 + if (!(chestPosition instanceof Vec3)) { + throw new Error( + "chestPosition for depositItemIntoChest must be a Vec3" + ); + } + await moveToChest(bot, chestPosition); + const chestBlock = bot.blockAt(chestPosition); + await bot.openContainer(chestBlock); + await closeChest(bot, chestBlock); +} + +async function moveToChest(bot, chestPosition) { + if (!(chestPosition instanceof Vec3)) { + throw new Error( + "chestPosition for depositItemIntoChest must be a Vec3" + ); + } + if (chestPosition.distanceTo(bot.entity.position) > 32) { + bot.chat( + `/tp ${chestPosition.x} ${chestPosition.y} ${chestPosition.z}` + ); + await bot.waitForTicks(20); + } + const chestBlock = bot.blockAt(chestPosition); + if (chestBlock.name !== "chest") { + bot.emit("removeChest", chestPosition); + throw new Error( + `No chest at ${chestPosition}, it is ${chestBlock.name}` + ); + } + await bot.pathfinder.goto( + new GoalLookAtBlock(chestBlock.position, bot.world, {}) + ); + return chestBlock; +} + +async function listItemsInChest(bot, chestBlock) { + const chest = await bot.openContainer(chestBlock); + const items = chest.containerItems(); + if (items.length > 0) { + const itemNames = items.reduce((acc, obj) => { + if (acc[obj.name]) { + acc[obj.name] += obj.count; + } else { + acc[obj.name] = obj.count; + } + return acc; + }, {}); + bot.emit("closeChest", itemNames, chestBlock.position); + } else { + bot.emit("closeChest", {}, chestBlock.position); + } + return chest; +} + +async function closeChest(bot, chestBlock) { + try { + const chest = await listItemsInChest(bot, chestBlock); + await chest.close(); + } catch (err) { + await bot.closeWindow(chestBlock); + } +} + +function itemByName(items, name) { + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + if (item && item.name === name) return item; + } + return null; +} diff --git a/metagpt/actions/minecraft/control_primitives/waitForMobRemoved.js b/metagpt/actions/minecraft/control_primitives/waitForMobRemoved.js new file mode 100644 index 000000000..fa83d4310 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives/waitForMobRemoved.js @@ -0,0 +1,84 @@ +function waitForMobRemoved(bot, entity, timeout = 300) { + return new Promise((resolve, reject) => { + let success = false; + let droppedItem = null; + // Set up timeout + const timeoutId = setTimeout(() => { + success = false; + bot.pvp.stop(); + }, timeout * 1000); + + // Function to handle entityRemoved event + function onEntityGone(e) { + if (e === entity) { + success = true; + clearTimeout(timeoutId); + bot.chat(`Killed ${entity.name}!`); + bot.pvp.stop(); + } + } + + function onItemDrop(item) { + if (entity.position.distanceTo(item.position) <= 1) { + droppedItem = item; + } + } + + function onStoppedAttacking() { + clearTimeout(timeoutId); + bot.removeListener("entityGone", onEntityGone); + bot.removeListener("stoppedAttacking", onStoppedAttacking); + bot.removeListener("itemDrop", onItemDrop); + if (!success) reject(new Error(`Failed to kill ${entity.name}.`)); + else resolve(droppedItem); + } + + // Listen for entityRemoved event + bot.on("entityGone", onEntityGone); + bot.on("stoppedAttacking", onStoppedAttacking); + bot.on("itemDrop", onItemDrop); + }); +} + + +function waitForMobShot(bot, entity, timeout = 300) { + return new Promise((resolve, reject) => { + let success = false; + let droppedItem = null; + // Set up timeout + const timeoutId = setTimeout(() => { + success = false; + bot.hawkEye.stop(); + }, timeout * 1000); + + // Function to handle entityRemoved event + function onEntityGone(e) { + if (e === entity) { + success = true; + clearTimeout(timeoutId); + bot.chat(`Shot ${entity.name}!`); + bot.hawkEye.stop(); + } + } + + function onItemDrop(item) { + if (entity.position.distanceTo(item.position) <= 1) { + droppedItem = item; + } + } + + function onAutoShotStopped() { + clearTimeout(timeoutId); + bot.removeListener("entityGone", onEntityGone); + bot.removeListener("auto_shot_stopped", onAutoShotStopped); + bot.removeListener("itemDrop", onItemDrop); + if (!success) reject(new Error(`Failed to shoot ${entity.name}.`)); + else resolve(droppedItem); + } + + // Listen for entityRemoved event + bot.on("entityGone", onEntityGone); + bot.on("auto_shot_stopped", onAutoShotStopped); + bot.on("itemDrop", onItemDrop); + }); +} From 411f5c21cd48f9ffae1b6f51ba4d03a7cb0bdec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:08:44 +0800 Subject: [PATCH 15/18] Create __init__.py --- .../control_primitives_context/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives_context/__init__.py diff --git a/metagpt/actions/minecraft/control_primitives_context/__init__.py b/metagpt/actions/minecraft/control_primitives_context/__init__.py new file mode 100644 index 000000000..8126c39ff --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/__init__.py @@ -0,0 +1,18 @@ +import pkg_resources +import os +import voyager.utils as U + + +def load_control_primitives_context(primitive_names=None): + package_path = pkg_resources.resource_filename("voyager", "") + if primitive_names is None: + primitive_names = [ + primitive[:-3] + for primitive in os.listdir(f"{package_path}/control_primitives_context") + if primitive.endswith(".js") + ] + primitives = [ + U.load_text(f"{package_path}/control_primitives_context/{primitive_name}.js") + for primitive_name in primitive_names + ] + return primitives From c43043fcc6358f34d84be6587c238be90e877c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:09:46 +0800 Subject: [PATCH 16/18] Add files via upload --- .../control_primitives_context/craftItem.js | 14 ++++++++ .../exploreUntil.js | 31 ++++++++++++++++ .../control_primitives_context/killMob.js | 12 +++++++ .../control_primitives_context/mineBlock.js | 15 ++++++++ .../control_primitives_context/mineflayer.js | 22 ++++++++++++ .../control_primitives_context/placeItem.js | 28 +++++++++++++++ .../control_primitives_context/smeltItem.js | 22 ++++++++++++ .../control_primitives_context/useChest.js | 35 +++++++++++++++++++ 8 files changed, 179 insertions(+) create mode 100644 metagpt/actions/minecraft/control_primitives_context/craftItem.js create mode 100644 metagpt/actions/minecraft/control_primitives_context/exploreUntil.js create mode 100644 metagpt/actions/minecraft/control_primitives_context/killMob.js create mode 100644 metagpt/actions/minecraft/control_primitives_context/mineBlock.js create mode 100644 metagpt/actions/minecraft/control_primitives_context/mineflayer.js create mode 100644 metagpt/actions/minecraft/control_primitives_context/placeItem.js create mode 100644 metagpt/actions/minecraft/control_primitives_context/smeltItem.js create mode 100644 metagpt/actions/minecraft/control_primitives_context/useChest.js diff --git a/metagpt/actions/minecraft/control_primitives_context/craftItem.js b/metagpt/actions/minecraft/control_primitives_context/craftItem.js new file mode 100644 index 000000000..806811d46 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/craftItem.js @@ -0,0 +1,14 @@ +// Craft 8 oak_planks from 2 oak_log (do the recipe 2 times): craftItem(bot, "oak_planks", 2); +// You must place a crafting table before calling this function +async function craftItem(bot, name, count = 1) { + const item = mcData.itemsByName[name]; + const craftingTable = bot.findBlock({ + matching: mcData.blocksByName.crafting_table.id, + maxDistance: 32, + }); + await bot.pathfinder.goto( + new GoalLookAtBlock(craftingTable.position, bot.world) + ); + const recipe = bot.recipesFor(item.id, null, 1, craftingTable)[0]; + await bot.craft(recipe, count, craftingTable); +} diff --git a/metagpt/actions/minecraft/control_primitives_context/exploreUntil.js b/metagpt/actions/minecraft/control_primitives_context/exploreUntil.js new file mode 100644 index 000000000..55c62a453 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/exploreUntil.js @@ -0,0 +1,31 @@ +/* +Explore until find an iron_ore, use Vec3(0, -1, 0) because iron ores are usually underground +await exploreUntil(bot, new Vec3(0, -1, 0), 60, () => { + const iron_ore = bot.findBlock({ + matching: mcData.blocksByName["iron_ore"].id, + maxDistance: 32, + }); + return iron_ore; +}); + +Explore until find a pig, use Vec3(1, 0, 1) because pigs are usually on the surface +let pig = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => { + const pig = bot.nearestEntity((entity) => { + return ( + entity.name === "pig" && + entity.position.distanceTo(bot.entity.position) < 32 + ); + }); + return pig; +}); +*/ +async function exploreUntil(bot, direction, maxTime = 60, callback) { + /* + Implementation of this function is omitted. + direction: Vec3, can only contain value of -1, 0 or 1 + maxTime: number, the max time for exploration + callback: function, early stop condition, will be called each second, exploration will stop if return value is not null + + Return: null if explore timeout, otherwise return the return value of callback + */ +} diff --git a/metagpt/actions/minecraft/control_primitives_context/killMob.js b/metagpt/actions/minecraft/control_primitives_context/killMob.js new file mode 100644 index 000000000..670ca9753 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/killMob.js @@ -0,0 +1,12 @@ +// Kill a pig and collect the dropped item: killMob(bot, "pig", 300); +async function killMob(bot, mobName, timeout = 300) { + const entity = bot.nearestEntity( + (entity) => + entity.name === mobName && + entity.position.distanceTo(bot.entity.position) < 32 + ); + await bot.pvp.attack(entity); + await bot.pathfinder.goto( + new GoalBlock(entity.position.x, entity.position.y, entity.position.z) + ); +} diff --git a/metagpt/actions/minecraft/control_primitives_context/mineBlock.js b/metagpt/actions/minecraft/control_primitives_context/mineBlock.js new file mode 100644 index 000000000..c6a7559e6 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/mineBlock.js @@ -0,0 +1,15 @@ +// Mine 3 cobblestone: mineBlock(bot, "stone", 3); +async function mineBlock(bot, name, count = 1) { + const blocks = bot.findBlocks({ + matching: (block) => { + return block.name === name; + }, + maxDistance: 32, + count: count, + }); + const targets = []; + for (let i = 0; i < Math.min(blocks.length, count); i++) { + targets.push(bot.blockAt(blocks[i])); + } + await bot.collectBlock.collect(targets, { ignoreNoPath: true }); +} diff --git a/metagpt/actions/minecraft/control_primitives_context/mineflayer.js b/metagpt/actions/minecraft/control_primitives_context/mineflayer.js new file mode 100644 index 000000000..43217885c --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/mineflayer.js @@ -0,0 +1,22 @@ +await bot.pathfinder.goto(goal); // A very useful function. This function may change your main-hand equipment. +// Following are some Goals you can use: +new GoalNear(x, y, z, range); // Move the bot to a block within the specified range of the specified block. `x`, `y`, `z`, and `range` are `number` +new GoalXZ(x, z); // Useful for long-range goals that don't have a specific Y level. `x` and `z` are `number` +new GoalGetToBlock(x, y, z); // Not get into the block, but get directly adjacent to it. Useful for fishing, farming, filling bucket, and beds. `x`, `y`, and `z` are `number` +new GoalFollow(entity, range); // Follow the specified entity within the specified range. `entity` is `Entity`, `range` is `number` +new GoalPlaceBlock(position, bot.world, {}); // Position the bot in order to place a block. `position` is `Vec3` +new GoalLookAtBlock(position, bot.world, {}); // Path into a position where a blockface of the block at position is visible. `position` is `Vec3` + +// These are other Mineflayer functions you can use: +bot.isABed(bedBlock); // Return true if `bedBlock` is a bed +bot.blockAt(position); // Return the block at `position`. `position` is `Vec3` + +// These are other Mineflayer async functions you can use: +await bot.equip(item, destination); // Equip the item in the specified destination. `item` is `Item`, `destination` can only be "hand", "head", "torso", "legs", "feet", "off-hand" +await bot.consume(); // Consume the item in the bot's hand. You must equip the item to consume first. Useful for eating food, drinking potions, etc. +await bot.fish(); // Let bot fish. Before calling this function, you must first get to a water block and then equip a fishing rod. The bot will automatically stop fishing when it catches a fish +await bot.sleep(bedBlock); // Sleep until sunrise. You must get to a bed block first +await bot.activateBlock(block); // This is the same as right-clicking a block in the game. Useful for buttons, doors, etc. You must get to the block first +await bot.lookAt(position); // Look at the specified position. You must go near the position before you look at it. To fill bucket with water, you must lookAt first. `position` is `Vec3` +await bot.activateItem(); // This is the same as right-clicking to use the item in the bot's hand. Useful for using buckets, etc. You must equip the item to activate first +await bot.useOn(entity); // This is the same as right-clicking an entity in the game. Useful for shearing sheep, equipping harnesses, etc. You must get to the entity first diff --git a/metagpt/actions/minecraft/control_primitives_context/placeItem.js b/metagpt/actions/minecraft/control_primitives_context/placeItem.js new file mode 100644 index 000000000..99e06089c --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/placeItem.js @@ -0,0 +1,28 @@ +// Place a crafting_table near the player, Vec3(1, 0, 0) is just an example, you shouldn't always use that: placeItem(bot, "crafting_table", bot.entity.position.offset(1, 0, 0)); +async function placeItem(bot, name, position) { + const item = bot.inventory.findInventoryItem(mcData.itemsByName[name].id); + // find a reference block + const faceVectors = [ + new Vec3(0, 1, 0), + new Vec3(0, -1, 0), + new Vec3(1, 0, 0), + new Vec3(-1, 0, 0), + new Vec3(0, 0, 1), + new Vec3(0, 0, -1), + ]; + let referenceBlock = null; + let faceVector = null; + for (const vector of faceVectors) { + const block = bot.blockAt(position.minus(vector)); + if (block?.name !== "air") { + referenceBlock = block; + faceVector = vector; + break; + } + } + // You must first go to the block position you want to place + await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {})); + // You must equip the item right before calling placeBlock + await bot.equip(item, "hand"); + await bot.placeBlock(referenceBlock, faceVector); +} diff --git a/metagpt/actions/minecraft/control_primitives_context/smeltItem.js b/metagpt/actions/minecraft/control_primitives_context/smeltItem.js new file mode 100644 index 000000000..0a3c76257 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/smeltItem.js @@ -0,0 +1,22 @@ +// Smelt 1 raw_iron into 1 iron_ingot using 1 oak_planks as fuel: smeltItem(bot, "raw_iron", "oak_planks"); +// You must place a furnace before calling this function +async function smeltItem(bot, itemName, fuelName, count = 1) { + const item = mcData.itemsByName[itemName]; + const fuel = mcData.itemsByName[fuelName]; + const furnaceBlock = bot.findBlock({ + matching: mcData.blocksByName.furnace.id, + maxDistance: 32, + }); + await bot.pathfinder.goto( + new GoalLookAtBlock(furnaceBlock.position, bot.world) + ); + const furnace = await bot.openFurnace(furnaceBlock); + for (let i = 0; i < count; i++) { + await furnace.putFuel(fuel.id, null, 1); + await furnace.putInput(item.id, null, 1); + // Wait 12 seconds for the furnace to smelt the item + await bot.waitForTicks(12 * 20); + await furnace.takeOutput(); + } + await furnace.close(); +} diff --git a/metagpt/actions/minecraft/control_primitives_context/useChest.js b/metagpt/actions/minecraft/control_primitives_context/useChest.js new file mode 100644 index 000000000..e80af3fd9 --- /dev/null +++ b/metagpt/actions/minecraft/control_primitives_context/useChest.js @@ -0,0 +1,35 @@ +// Get a torch from chest at (30, 65, 100): getItemFromChest(bot, new Vec3(30, 65, 100), {"torch": 1}); +// This function will work no matter how far the bot is from the chest. +async function getItemFromChest(bot, chestPosition, itemsToGet) { + await moveToChest(bot, chestPosition); + const chestBlock = bot.blockAt(chestPosition); + const chest = await bot.openContainer(chestBlock); + for (const name in itemsToGet) { + const itemByName = mcData.itemsByName[name]; + const item = chest.findContainerItem(itemByName.id); + await chest.withdraw(item.type, null, itemsToGet[name]); + } + await closeChest(bot, chestBlock); +} +// Deposit a torch into chest at (30, 65, 100): depositItemIntoChest(bot, new Vec3(30, 65, 100), {"torch": 1}); +// This function will work no matter how far the bot is from the chest. +async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) { + await moveToChest(bot, chestPosition); + const chestBlock = bot.blockAt(chestPosition); + const chest = await bot.openContainer(chestBlock); + for (const name in itemsToDeposit) { + const itemByName = mcData.itemsByName[name]; + const item = bot.inventory.findInventoryItem(itemByName.id); + await chest.deposit(item.type, null, itemsToDeposit[name]); + } + await closeChest(bot, chestBlock); +} +// Check the items inside the chest at (30, 65, 100): checkItemInsideChest(bot, new Vec3(30, 65, 100)); +// You only need to call this function once without any action to finish task of checking items inside the chest. +async function checkItemInsideChest(bot, chestPosition) { + await moveToChest(bot, chestPosition); + const chestBlock = bot.blockAt(chestPosition); + await bot.openContainer(chestBlock); + // You must close the chest after opening it if you are asked to open a chest + await closeChest(bot, chestBlock); +} From b090a628a0741562f7546b53a1f82276c92cb197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:13:36 +0800 Subject: [PATCH 17/18] Update __init__.py --- metagpt/actions/minecraft/control_primitives/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/minecraft/control_primitives/__init__.py b/metagpt/actions/minecraft/control_primitives/__init__.py index 369e587d7..2446d087c 100644 --- a/metagpt/actions/minecraft/control_primitives/__init__.py +++ b/metagpt/actions/minecraft/control_primitives/__init__.py @@ -4,15 +4,15 @@ import voyager.utils as U def load_control_primitives(primitive_names=None): - package_path = pkg_resources.resource_filename("voyager", "") + package_path = pkg_resources.resource_filename("metagpt", "") if primitive_names is None: primitive_names = [ primitives[:-3] - for primitives in os.listdir(f"{package_path}/control_primitives") + for primitives in os.listdir(f"{package_path}/actions/minecraft/control_primitives") if primitives.endswith(".js") ] primitives = [ - U.load_text(f"{package_path}/control_primitives/{primitive_name}.js") + U.load_text(f"{package_path}/actions/minecraft/control_primitives/{primitive_name}.js") for primitive_name in primitive_names ] return primitives From 8f324ba91ba4f2549d99e8b21c5337378632886c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=8A=B2=E5=AE=87?= <137690584+isaacJinyu@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:15:35 +0800 Subject: [PATCH 18/18] Update __init__.py --- .../minecraft/control_primitives_context/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/minecraft/control_primitives_context/__init__.py b/metagpt/actions/minecraft/control_primitives_context/__init__.py index 8126c39ff..2bee5e3a8 100644 --- a/metagpt/actions/minecraft/control_primitives_context/__init__.py +++ b/metagpt/actions/minecraft/control_primitives_context/__init__.py @@ -4,15 +4,15 @@ import voyager.utils as U def load_control_primitives_context(primitive_names=None): - package_path = pkg_resources.resource_filename("voyager", "") + package_path = pkg_resources.resource_filename("metagpt", "") if primitive_names is None: primitive_names = [ primitive[:-3] - for primitive in os.listdir(f"{package_path}/control_primitives_context") + for primitive in os.listdir(f"{package_path}/actions/minecraft/control_primitives_context") if primitive.endswith(".js") ] primitives = [ - U.load_text(f"{package_path}/control_primitives_context/{primitive_name}.js") + U.load_text(f"{package_path}/actions/minecraft/control_primitives_context/{primitive_name}.js") for primitive_name in primitive_names ] return primitives