├── .gitignore ├── FAQ.md ├── LICENSE ├── README.md ├── images └── pull.png ├── installation ├── fabric_mods_install.md └── minecraft_instance_install.md ├── requirements.txt ├── setup.py └── voyager ├── __init__.py ├── agents ├── __init__.py ├── action.py ├── critic.py ├── curriculum.py └── skill.py ├── control_primitives ├── .prettierrc.json ├── __init__.py ├── craftHelper.js ├── craftItem.js ├── exploreUntil.js ├── givePlacedItemBack.js ├── killMob.js ├── mineBlock.js ├── placeItem.js ├── shoot.js ├── smeltItem.js ├── useChest.js └── waitForMobRemoved.js ├── control_primitives_context ├── .prettierrc.json ├── __init__.py ├── craftItem.js ├── exploreUntil.js ├── killMob.js ├── mineBlock.js ├── mineflayer.js ├── placeItem.js ├── smeltItem.js └── useChest.js ├── env ├── .gitignore ├── __init__.py ├── bridge.py ├── minecraft_launcher.py ├── mineflayer │ ├── .prettierignore │ ├── .prettierrc.json │ ├── index.js │ ├── lib │ │ ├── observation │ │ │ ├── base.js │ │ │ ├── chests.js │ │ │ ├── inventory.js │ │ │ ├── onChat.js │ │ │ ├── onError.js │ │ │ ├── onSave.js │ │ │ ├── status.js │ │ │ └── voxels.js │ │ ├── skillLoader.js │ │ └── utils.js │ ├── mineflayer-collectblock │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── _config.yml │ │ ├── docs │ │ │ └── api.md │ │ ├── examples │ │ │ ├── collector.js │ │ │ ├── oreMiner.js │ │ │ └── storageBot.js │ │ ├── package.json │ │ ├── src │ │ │ ├── BlockVeins.ts │ │ │ ├── CollectBlock.ts │ │ │ ├── Inventory.ts │ │ │ ├── Targets.ts │ │ │ ├── TaskQueue.ts │ │ │ ├── TemporarySubscriber.ts │ │ │ ├── Util.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ └── package.json └── process_monitor.py ├── prompts ├── __init__.py ├── action_response_format.txt ├── action_template.txt ├── critic.txt ├── curriculum.txt ├── curriculum_qa_step1_ask_questions.txt ├── curriculum_qa_step2_answer_questions.txt ├── curriculum_task_decomposition.txt └── skill.txt ├── utils ├── __init__.py ├── file_utils.py ├── json_utils.py └── record_utils.py └── voyager.py /.gitignore: -------------------------------------------------------------------------------- 1 | ckpt* 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | # env/ 88 | venv/ 89 | # ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Mac 107 | .DS_Store 108 | 109 | # MuJoCo License key 110 | mjkey.txt 111 | 112 | .mujocomanip_temp_model.xml 113 | 114 | # Python IDE 115 | .idea 116 | 117 | # Locally generated files 118 | dump.rdb 119 | *.local.ipynb 120 | runs/ 121 | temp* 122 | debug_* 123 | *.swp 124 | 125 | .tabnine_root 126 | 127 | logs/ 128 | plot_iter.py 129 | *.svg -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | * [I got connection error after I click on the Azure login link and login to Microsoft account.](#i-get-a-connection-error-after-i-click-on-the-azure-login-link-and-login-to-my-microsoft-account) 3 | * [I get `KeyError: 'access_token'` after I copied the link](#i-get-keyerror-accesstoken-after-i-copied-the-link) 4 | * [I got `Subprocess Mineflayer failed to start` error.](#i-get-subprocess-mineflayer-failed-to-start-error) 5 | * [I see the bot left and rejoin the game after each task.](#i-see-the-bot-left-and-rejoin-the-game-after-each-task) 6 | * [How to show the bot's first-person perspective?](#how-to-show-the-bots-first-person-view) 7 | * [Can I use GPT-3.5 instead of GPT-4?](#can-i-use-gpt-35-instead-of-gpt-4) 8 | * [What's the estimated cost of running Voyager?](#whats-the-estimated-cost-of-running-voyager) 9 | 10 | ## I get a connection error after I click on the Azure login link and login to my Microsoft account. 11 | 12 | It's normal that you get a connection refused or 404 error after you log in. You will still see the new URL in your browser. You just need to copy and paste that link. It should contain things like `code=M.C....` in that link. 13 | 14 | ## I get `KeyError: 'access_token'` after I copied the link 15 | 16 | While testing Voyager, we use Redirect URI Type: `Public client/native (mobile & desktop)` in the app registration for Azure Login. However, according to the report in issue [#34](https://github.com/MineDojo/Voyager/issues/34#issuecomment-1567007133), the URI Type was changed to "Web" and it resolved the problem. Feel free to attempt both URI Types to determine which one works for you. 17 | 18 | ## I get `Subprocess Mineflayer failed to start` error. 19 | 20 | There are many reasons that may cause this problem. You can try with following solutions: 21 | 1. Make sure you install nodejs and the dependency packages correctly. You can use the following command to check your installation: 22 | ```bash 23 | cd voyager/env/mineflayer 24 | node index.js 25 | ``` 26 | If you see `Server started on port {PORT}`, then your installation is correct. You can kill the process by `Ctrl+C`. 27 | 2. Make sure you install Fabric correctly. You should be able to select the Fabric version in the Minecraft launcher. 28 | 3. Each Mineflayer process can only listen to one port. If you want to start multiple instances of `Voyager`, you need to manually change the port when initialization: 29 | ```python 30 | from voyager import Voyager 31 | voyager = Voyager( 32 | server_port=3001, # default is 3000 33 | ... 34 | ) 35 | ``` 36 | 37 | ## I see the bot left and rejoin the game after each task. 38 | 39 | After completing each task, we'll reset the environment, which means the bot will exit and rejoin the game. This reset is necessary to synchronize Mineflayer with the Minecraft game. However, certain commands we utilize might result in lag on the Mineflayer side, causing the inventory stored in Mineflayer to differ from the actual inventory in the game. If you wish to prevent the reset, you can use `voyager.learn(reset_env=False)` and consider increasing the `env_wait_ticks` value. This will also provide Mineflayer with additional time to sync with the Minecraft game. 40 | 41 | 42 | ## How to show the bot's first-person view? 43 | 44 | Due to the Mineflayer's limitation, we currently can not directly get the bot's view in the game. Although there's a plugin called [prismarine-viewer](https://github.com/PrismarineJS/prismarine-viewer), the video quality is not good enough, so we opt not to use it. Our demo video is generated by [replay-mod](https://www.replaymod.com/). We start the recording and let the bot play for hours, then come back to the recording and render the view from the bot. 45 | 46 | 47 | ## Can I use GPT-3.5 instead of GPT-4? 48 | 49 | It's highly recommended to use GPT-4. GPT-3.5 falls behind in terms of code quality and reasoning ability compared to GPT-4. Moreover, GPT-3.5 has a limited context length, which means it may provide incomplete responses. If you insist on using GPT-3.5, it is essential to configure it with `skill_manager_retrieval_top_k=0` to disable the skill library. This will reduce the context length of the prompt. 50 | 51 | ## What's the estimated cost of running Voyager? 52 | 53 | Using Voyager for approximately 160 iterations using GPT-4 will cost you around USD 50. It's important to keep a close eye on your OpenAI API expenses and avoid unnecessary spending. Once Voyager begins running, it's recommended to monitor the bot's actions for a period and ensure that it successfully completes some tasks. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 MineDojo Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voyager: An Open-Ended Embodied Agent with Large Language Models 2 |
3 | 4 | [[Website]](https://voyager.minedojo.org/) 5 | [[Arxiv]](https://arxiv.org/abs/2305.16291) 6 | [[PDF]](https://voyager.minedojo.org/assets/documents/voyager.pdf) 7 | [[Tweet]](https://twitter.com/DrJimFan/status/1662115266933972993?s=20) 8 | 9 | [![Python Version](https://img.shields.io/badge/Python-3.9-blue.svg)](https://github.com/MineDojo/Voyager) 10 | [![GitHub license](https://img.shields.io/github/license/MineDojo/Voyager)](https://github.com/MineDojo/Voyager/blob/main/LICENSE) 11 | ______________________________________________________________________ 12 | 13 | 14 | https://github.com/MineDojo/Voyager/assets/25460983/ce29f45b-43a5-4399-8fd8-5dd105fd64f2 15 | 16 | ![](images/pull.png) 17 | 18 | 19 |
20 | 21 | We introduce Voyager, the first LLM-powered embodied lifelong learning agent 22 | in Minecraft that continuously explores the world, acquires diverse skills, and 23 | makes novel discoveries without human intervention. Voyager consists of three 24 | key components: 1) an automatic curriculum that maximizes exploration, 2) an 25 | ever-growing skill library of executable code for storing and retrieving complex 26 | behaviors, and 3) a new iterative prompting mechanism that incorporates environment 27 | feedback, execution errors, and self-verification for program improvement. 28 | Voyager interacts with GPT-4 via blackbox queries, which bypasses the need for 29 | model parameter fine-tuning. The skills developed by Voyager are temporally 30 | extended, interpretable, and compositional, which compounds the agent’s abilities 31 | rapidly and alleviates catastrophic forgetting. Empirically, Voyager shows 32 | strong in-context lifelong learning capability and exhibits exceptional proficiency 33 | in playing Minecraft. It obtains 3.3× more unique items, travels 2.3× longer 34 | distances, and unlocks key tech tree milestones up to 15.3× faster than prior SOTA. 35 | Voyager is able to utilize the learned skill library in a new Minecraft world to 36 | solve novel tasks from scratch, while other techniques struggle to generalize. 37 | 38 | In this repo, we provide Voyager code. This codebase is under [MIT License](LICENSE). 39 | 40 | # Installation 41 | Voyager requires Python ≥ 3.9 and Node.js ≥ 16.13.0. We have tested on Ubuntu 20.04, Windows 11, and macOS. You need to follow the instructions below to install Voyager. 42 | 43 | ## Python Install 44 | ``` 45 | git clone https://github.com/MineDojo/Voyager 46 | cd Voyager 47 | pip install -e . 48 | ``` 49 | 50 | ## Node.js Install 51 | In addition to the Python dependencies, you need to install the following Node.js packages: 52 | ``` 53 | cd voyager/env/mineflayer 54 | npm install -g npx 55 | npm install 56 | cd mineflayer-collectblock 57 | npx tsc 58 | cd .. 59 | npm install 60 | ``` 61 | 62 | ## Minecraft Instance Install 63 | 64 | Voyager depends on Minecraft game. You need to install Minecraft game and set up a Minecraft instance. 65 | 66 | Follow the instructions in [Minecraft Login Tutorial](installation/minecraft_instance_install.md) to set up your Minecraft Instance. 67 | 68 | ## Fabric Mods Install 69 | 70 | You need to install fabric mods to support all the features in Voyager. Remember to use the correct Fabric version of all the mods. 71 | 72 | Follow the instructions in [Fabric Mods Install](installation/fabric_mods_install.md) to install the mods. 73 | 74 | # Getting Started 75 | Voyager uses OpenAI's GPT-4 as the language model. You need to have an OpenAI API key to use Voyager. You can get one from [here](https://platform.openai.com/account/api-keys). 76 | 77 | After the installation process, you can run Voyager by: 78 | ```python 79 | from voyager import Voyager 80 | 81 | # you can also use mc_port instead of azure_login, but azure_login is highly recommended 82 | azure_login = { 83 | "client_id": "YOUR_CLIENT_ID", 84 | "redirect_url": "https://127.0.0.1/auth-response", 85 | "secret_value": "[OPTIONAL] YOUR_SECRET_VALUE", 86 | "version": "fabric-loader-0.14.18-1.19", # the version Voyager is tested on 87 | } 88 | openai_api_key = "YOUR_API_KEY" 89 | 90 | voyager = Voyager( 91 | azure_login=azure_login, 92 | openai_api_key=openai_api_key, 93 | ) 94 | 95 | # start lifelong learning 96 | voyager.learn() 97 | ``` 98 | 99 | * If you are running with `Azure Login` for the first time, it will ask you to follow the command line instruction to generate a config file. 100 | * For `Azure Login`, you also need to select the world and open the world to LAN by yourself. After you run `voyager.learn()` the game will pop up soon, you need to: 101 | 1. Select `Singleplayer` and press `Create New World`. 102 | 2. Set Game Mode to `Creative` and Difficulty to `Peaceful`. 103 | 3. After the world is created, press `Esc` key and press `Open to LAN`. 104 | 4. Select `Allow cheats: ON` and press `Start LAN World`. You will see the bot join the world soon. 105 | 106 | # Resume from a checkpoint during learning 107 | 108 | If you stop the learning process and want to resume from a checkpoint later, you can initialize Voyager by: 109 | ```python 110 | from voyager import Voyager 111 | 112 | voyager = Voyager( 113 | azure_login=azure_login, 114 | openai_api_key=openai_api_key, 115 | ckpt_dir="YOUR_CHECKPOINT_DIR", 116 | resume=True, 117 | ) 118 | ``` 119 | 120 | # FAQ 121 | If you have any questions, please check our [FAQ](FAQ.md) first before opening an issue. 122 | 123 | # Paper and Citation 124 | 125 | If you find our work useful, please consider citing us! 126 | 127 | ```bibtex 128 | @article{wang2023voyager, 129 | title = {Voyager: An Open-Ended Embodied Agent with Large Language Models}, 130 | author = {Guanzhi Wang and Yuqi Xie and Yunfan Jiang and Ajay Mandlekar and Chaowei Xiao and Yuke Zhu and Linxi Fan and Anima Anandkumar}, 131 | year = {2023}, 132 | journal = {arXiv preprint arXiv: Arxiv-2305.16291} 133 | } 134 | ``` 135 | 136 | Disclaimer: This project is strictly for research purposes, and not an official product from NVIDIA. 137 | -------------------------------------------------------------------------------- /images/pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/better-py/annotated-voyager/39bb8738dea8b95b601fe2b868c5442950d87edb/images/pull.png -------------------------------------------------------------------------------- /installation/fabric_mods_install.md: -------------------------------------------------------------------------------- 1 | # Fabric Mods Install 2 | In this tutorial, we will install the Fabric launcher and 5 mods. Remember to use the correct Fabric version that matches your game version (1.19) of all the mods. 3 | 1. You can download the latest Fabric Installer from [here](https://fabricmc.net/use/installer/). For Windows users, just download the `.exe` file. For Mac or Ubuntu users, download the jar file and call `java -jar fabric-installer-0.11.2.jar` to install. Select game version to be `1.19` and loader version to be `0.14.18`. It will automatically detect your Minecraft game install location. 4 | 2. After installing Fabric, you will have a `YOUR_MINECRAFT_GAME_LOCATION/mod` folder. You need to put all the mods under this folder. Also, you will have a `YOUR_MINECRAFT_GAME_LOCATION/versions/fabric-loader-0.14.18-1.19`. This is the version you will run the game with. 5 | 3. Here are 4 mods that can be directly downloaded to `YOUR_MINECRAFT_GAME_LOCATION/mod` folder: 6 | * [Fabric API](https://modrinth.com/mod/fabric-api): Basic Fabric APIs. 7 | * [Mod Menu](https://cdn.modrinth.com/data/mOgUt4GM/versions/4.0.4/modmenu-4.0.4.jar): Used to manage all the mods that you download. 8 | * [Complete Config](https://www.curseforge.com/minecraft/mc-mods/completeconfig/download/3821056): Dependency of server pause. 9 | * [Multi Server Pause](https://www.curseforge.com/minecraft/mc-mods/multiplayer-server-pause-fabric/download/3822586): Used to pause the server when waiting for GPT-4 to reply. 10 | 4. For the last mod [Better Respawn](https://github.com/xieleo5/better-respawn/tree/1.19), you need to manually clone and compile. 11 | 12 | * After you clone the repo, remove the `'forge'` string in the last line of `settings.gradle`. Then run `gradlew build` to compile the mod. You will find the compiled jar file in `better-respawn/fabric/build/libs/better-respawn-fabric-1.19-2.0.0.jar`. Put the jar file to the mod folder. 13 | * You will need a Java Runtime Environment v17+ to build `better-respawn`. Some newer JRE versions will error during build. Find the JRE v17 archive [here](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html). 14 | * After you launch the game, go to `YOUR_MINECRAFT_GAME_LOCATION/config/better-respawn`, and modify the properties file with: 15 | ``` 16 | respawn_block_range=32 17 | max_respawn_distance=32 18 | min_respawn_distance=0 19 | ``` 20 | 5. Don't forget to change the `version` in `azure_login` to `fabric-loader-0.14.18-1.19` that you are using. You can find it under `YOUR_MINECRAFT_GAME_LOCATION/version` folder. 21 | 22 | You can return to [README.md](../README.md#getting-started) and getting started now. 23 | -------------------------------------------------------------------------------- /installation/minecraft_instance_install.md: -------------------------------------------------------------------------------- 1 | # Minecraft Instance Install 2 | To start using Voyager, you should first make sure to have an official [Minecraft](https://www.minecraft.net/) game (version 1.19) installed. 3 | 4 | There are two ways to start a Minecraft instance for Voyager. Sometimes GPT-4 will write an infinite loop that runs forever. In this case, there'll be a request timeout. Using Azure login can automatically resume the running if there's a request timeout. 5 | 6 | ## Option 1: Microsoft Azure Login (Recommended) 7 | Using this method will allow Voyager to automatically resume when there's a request timeout. This is dependent on the [minecraft-launcher-lib](https://minecraft-launcher-lib.readthedocs.io/en/stable/tutorial/microsoft_login.html#let-the-user-log-in) library. 8 | 9 | 1. Sign in to [Azure Portal](https://portal.azure.com/). 10 | 2. Go to [Azure Active Directory](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview). 11 | 3. Click on the `App Registrations` tab on the left panel. 12 | 4. Click on the `New registration` button. 13 | 5. Fill the form with the following values: 14 | - Name: `YOUR_APP_NAME` 15 | - Supported account types: `Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts` 16 | - Redirect URI Type: `Public client/native (mobile & desktop)`, Value: `https://127.0.0.1/auth-response` (If you get `KeyError: 'access_token'` in the end, you can try to change the type to `Web`, see [FAQ](https://github.com/MineDojo/Voyager/blob/main/FAQ.md) for more information) 17 | 6. Click on the `Register` button. 18 | 7. The `Application (client) ID` will be your `client_id`. 19 | 8. [Optional] Go to the `Certificates & Secrets` tab and click on the `New client secret` button. Fill the description by yourself. After you click `Add`, you will see your value, this will be your `secret_value`. 20 | 9. Go to your Minecraft install location `YOUR_MINECRAFT_GAME_LOCATION/versions`, and check all the versions you have. All the folder names are your valid `version` value. 21 | 22 | After these steps, you will finally get your azure_login information: 23 | ```python 24 | azure_login = { 25 | "client_id": "CLIENT_ID FROM STEP 7", 26 | "redirect_url": "https://127.0.0.1/auth-response", 27 | "secret_value": "[OPTIONAL] SECRET_KEY FROM STEP 8", 28 | "version": "MINECRAFT VERSION YOU WANT TO USE", 29 | } 30 | ``` 31 | **Voyager use `fabric-loader-0.14.18-1.19` version to run all the experiments.** You may not have this version currently, you can move on to the [Fabric Mods Install](fabric_mods_install.md#fabric-mods-install) section and follow the instructions there to install the fabric version of the game. 32 | 33 | ## Option 2: Minecraft Official Launcher 34 | 35 | After you install official Minecraft, you should have a Minecraft official launcher, open it, and follow the instructions here: 36 | 1. Select the version you want to play and start the game. 37 | 2. Select `Singleplayer` and create a new world. 38 | 3. Set Game Mode to `Creative` and Difficulty to `Peaceful`. 39 | 4. After the world is created, press `Esc` and select `Open to LAN`. 40 | 5. Select `Allow cheats: ON` and press `Start LAN World`. 41 | 6. You will see a port number in the chat log, that is your `mc-port`, use this number to instantiate Voyager later. 42 | 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm 2 | langchain 3 | javascript 4 | setuptools 5 | openai 6 | chardet 7 | cchardet 8 | chromadb 9 | tiktoken 10 | requests 11 | setuptools 12 | gymnasium 13 | psutil 14 | minecraft_launcher_lib -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import pkg_resources 4 | from setuptools import setup, find_packages 5 | 6 | 7 | PKG_NAME = "voyager" 8 | VERSION = "0.1" 9 | EXTRAS = {} 10 | 11 | 12 | def _read_file(fname): 13 | # this_dir = os.path.abspath(os.path.dirname(__file__)) 14 | # with open(os.path.join(this_dir, fname)) as f: 15 | with pathlib.Path(fname).open() as fp: 16 | return fp.read() 17 | 18 | 19 | def _read_install_requires(): 20 | with pathlib.Path("requirements.txt").open() as fp: 21 | return [ 22 | str(requirement) for requirement in pkg_resources.parse_requirements(fp) 23 | ] 24 | 25 | 26 | def _fill_extras(extras): 27 | if extras: 28 | extras["all"] = list(set([item for group in extras.values() for item in group])) 29 | return extras 30 | 31 | 32 | setup( 33 | name=PKG_NAME, 34 | version=VERSION, 35 | author=f"MineDojo Team", 36 | url="https://github.com/MineDojo/Voyager", 37 | description="research project", 38 | long_description=_read_file("README.md"), 39 | long_description_content_type="text/markdown", 40 | keywords=[ 41 | "Open-Ended Learning", 42 | "Lifelong Learning", 43 | "Embodied Agents", 44 | "Large Language Models", 45 | ], 46 | license="MIT License", 47 | packages=find_packages(include=f"{PKG_NAME}.*"), 48 | include_package_data=True, 49 | zip_safe=False, 50 | install_requires=_read_install_requires(), 51 | extras_require=_fill_extras(EXTRAS), 52 | python_requires=">=3.9", 53 | classifiers=[ 54 | "Development Status :: 5 - Production/Stable", 55 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 56 | "Environment :: Console", 57 | "Programming Language :: Python :: 3.9", 58 | ], 59 | ) 60 | -------------------------------------------------------------------------------- /voyager/__init__.py: -------------------------------------------------------------------------------- 1 | from .voyager import Voyager 2 | -------------------------------------------------------------------------------- /voyager/agents/__init__.py: -------------------------------------------------------------------------------- 1 | from .action import ActionAgent 2 | from .critic import CriticAgent 3 | from .curriculum import CurriculumAgent 4 | from .skill import SkillManager 5 | -------------------------------------------------------------------------------- /voyager/agents/action.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | import voyager.utils as U 5 | from javascript import require 6 | from langchain.chat_models import ChatOpenAI 7 | from langchain.prompts import SystemMessagePromptTemplate 8 | from langchain.schema import AIMessage, HumanMessage, SystemMessage 9 | 10 | from voyager.prompts import load_prompt 11 | from voyager.control_primitives_context import load_control_primitives_context 12 | 13 | 14 | class ActionAgent: 15 | def __init__( 16 | self, 17 | model_name="gpt-3.5-turbo", 18 | temperature=0, 19 | request_timout=120, 20 | ckpt_dir="ckpt", 21 | resume=False, 22 | chat_log=True, 23 | execution_error=True, 24 | ): 25 | self.ckpt_dir = ckpt_dir 26 | self.chat_log = chat_log 27 | self.execution_error = execution_error 28 | U.f_mkdir(f"{ckpt_dir}/action") 29 | if resume: 30 | print(f"\033[32mLoading Action Agent from {ckpt_dir}/action\033[0m") 31 | self.chest_memory = U.load_json(f"{ckpt_dir}/action/chest_memory.json") 32 | else: 33 | self.chest_memory = {} 34 | self.llm = ChatOpenAI( 35 | model_name=model_name, 36 | temperature=temperature, 37 | request_timeout=request_timout, 38 | ) 39 | 40 | def update_chest_memory(self, chests): 41 | for position, chest in chests.items(): 42 | if position in self.chest_memory: 43 | if isinstance(chest, dict): 44 | self.chest_memory[position] = chest 45 | else: 46 | print(f"\033[32mAction Agent saving chest {position}: {chest}\033[0m") 47 | self.chest_memory[position] = chest 48 | U.dump_json(self.chest_memory, f"{self.ckpt_dir}/action/chest_memory.json") 49 | 50 | def render_chest_observation(self): 51 | chests = [] 52 | for chest_position, chest in self.chest_memory.items(): 53 | if isinstance(chest, dict) and len(chest) > 0: 54 | chests.append(f"{chest_position}: {chest}") 55 | for chest_position, chest in self.chest_memory.items(): 56 | if isinstance(chest, dict) and len(chest) == 0: 57 | chests.append(f"{chest_position}: Empty") 58 | for chest_position, chest in self.chest_memory.items(): 59 | if isinstance(chest, str): 60 | assert chest == "Unknown" 61 | chests.append(f"{chest_position}: Unknown items inside") 62 | assert len(chests) == len(self.chest_memory) 63 | if chests: 64 | chests = "\n".join(chests) 65 | return f"Chests:\n{chests}\n\n" 66 | else: 67 | return f"Chests: None\n\n" 68 | 69 | def render_system_message(self, skills=[]): 70 | system_template = load_prompt("action_template") 71 | # FIXME: Hardcoded control_primitives 72 | base_skills = [ 73 | "exploreUntil", 74 | "mineBlock", 75 | "craftItem", 76 | "placeItem", 77 | "smeltItem", 78 | "killMob", 79 | ] 80 | if not self.llm.model_name == "gpt-3.5-turbo": 81 | base_skills += [ 82 | "useChest", 83 | "mineflayer", 84 | ] 85 | programs = "\n\n".join(load_control_primitives_context(base_skills) + skills) 86 | response_format = load_prompt("action_response_format") 87 | system_message_prompt = SystemMessagePromptTemplate.from_template( 88 | system_template 89 | ) 90 | system_message = system_message_prompt.format( 91 | programs=programs, response_format=response_format 92 | ) 93 | assert isinstance(system_message, SystemMessage) 94 | return system_message 95 | 96 | def render_human_message( 97 | self, *, events, code="", task="", context="", critique="" 98 | ): 99 | chat_messages = [] 100 | error_messages = [] 101 | # FIXME: damage_messages is not used 102 | damage_messages = [] 103 | assert events[-1][0] == "observe", "Last event must be observe" 104 | for i, (event_type, event) in enumerate(events): 105 | if event_type == "onChat": 106 | chat_messages.append(event["onChat"]) 107 | elif event_type == "onError": 108 | error_messages.append(event["onError"]) 109 | elif event_type == "onDamage": 110 | damage_messages.append(event["onDamage"]) 111 | elif event_type == "observe": 112 | biome = event["status"]["biome"] 113 | time_of_day = event["status"]["timeOfDay"] 114 | voxels = event["voxels"] 115 | entities = event["status"]["entities"] 116 | health = event["status"]["health"] 117 | hunger = event["status"]["food"] 118 | position = event["status"]["position"] 119 | equipment = event["status"]["equipment"] 120 | inventory_used = event["status"]["inventoryUsed"] 121 | inventory = event["inventory"] 122 | assert i == len(events) - 1, "observe must be the last event" 123 | 124 | observation = "" 125 | 126 | if code: 127 | observation += f"Code from the last round:\n{code}\n\n" 128 | else: 129 | observation += f"Code from the last round: No code in the first round\n\n" 130 | 131 | if self.execution_error: 132 | if error_messages: 133 | error = "\n".join(error_messages) 134 | observation += f"Execution error:\n{error}\n\n" 135 | else: 136 | observation += f"Execution error: No error\n\n" 137 | 138 | if self.chat_log: 139 | if chat_messages: 140 | chat_log = "\n".join(chat_messages) 141 | observation += f"Chat log: {chat_log}\n\n" 142 | else: 143 | observation += f"Chat log: None\n\n" 144 | 145 | observation += f"Biome: {biome}\n\n" 146 | 147 | observation += f"Time: {time_of_day}\n\n" 148 | 149 | if voxels: 150 | observation += f"Nearby blocks: {', '.join(voxels)}\n\n" 151 | else: 152 | observation += f"Nearby blocks: None\n\n" 153 | 154 | if entities: 155 | nearby_entities = [ 156 | k for k, v in sorted(entities.items(), key=lambda x: x[1]) 157 | ] 158 | observation += f"Nearby entities (nearest to farthest): {', '.join(nearby_entities)}\n\n" 159 | else: 160 | observation += f"Nearby entities (nearest to farthest): None\n\n" 161 | 162 | observation += f"Health: {health:.1f}/20\n\n" 163 | 164 | observation += f"Hunger: {hunger:.1f}/20\n\n" 165 | 166 | observation += f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n" 167 | 168 | observation += f"Equipment: {equipment}\n\n" 169 | 170 | if inventory: 171 | observation += f"Inventory ({inventory_used}/36): {inventory}\n\n" 172 | else: 173 | observation += f"Inventory ({inventory_used}/36): Empty\n\n" 174 | 175 | if not ( 176 | task == "Place and deposit useless items into a chest" 177 | or task.startswith("Deposit useless items into the chest at") 178 | ): 179 | observation += self.render_chest_observation() 180 | 181 | observation += f"Task: {task}\n\n" 182 | 183 | if context: 184 | observation += f"Context: {context}\n\n" 185 | else: 186 | observation += f"Context: None\n\n" 187 | 188 | if critique: 189 | observation += f"Critique: {critique}\n\n" 190 | else: 191 | observation += f"Critique: None\n\n" 192 | 193 | return HumanMessage(content=observation) 194 | 195 | def process_ai_message(self, message): 196 | assert isinstance(message, AIMessage) 197 | 198 | retry = 3 199 | error = None 200 | while retry > 0: 201 | try: 202 | babel = require("@babel/core") 203 | babel_generator = require("@babel/generator").default 204 | 205 | code_pattern = re.compile(r"```(?:javascript|js)(.*?)```", re.DOTALL) 206 | code = "\n".join(code_pattern.findall(message.content)) 207 | parsed = babel.parse(code) 208 | functions = [] 209 | assert len(list(parsed.program.body)) > 0, "No functions found" 210 | for i, node in enumerate(parsed.program.body): 211 | if node.type != "FunctionDeclaration": 212 | continue 213 | node_type = ( 214 | "AsyncFunctionDeclaration" 215 | if node["async"] 216 | else "FunctionDeclaration" 217 | ) 218 | functions.append( 219 | { 220 | "name": node.id.name, 221 | "type": node_type, 222 | "body": babel_generator(node).code, 223 | "params": list(node["params"]), 224 | } 225 | ) 226 | # find the last async function 227 | main_function = None 228 | for function in reversed(functions): 229 | if function["type"] == "AsyncFunctionDeclaration": 230 | main_function = function 231 | break 232 | assert ( 233 | main_function is not None 234 | ), "No async function found. Your main function must be async." 235 | assert ( 236 | len(main_function["params"]) == 1 237 | and main_function["params"][0].name == "bot" 238 | ), f"Main function {main_function['name']} must take a single argument named 'bot'" 239 | program_code = "\n\n".join(function["body"] for function in functions) 240 | exec_code = f"await {main_function['name']}(bot);" 241 | return { 242 | "program_code": program_code, 243 | "program_name": main_function["name"], 244 | "exec_code": exec_code, 245 | } 246 | except Exception as e: 247 | retry -= 1 248 | error = e 249 | time.sleep(1) 250 | return f"Error parsing action response (before program execution): {error}" 251 | 252 | def summarize_chatlog(self, events): 253 | def filter_item(message: str): 254 | craft_pattern = r"I cannot make \w+ because I need: (.*)" 255 | craft_pattern2 = ( 256 | r"I cannot make \w+ because there is no crafting table nearby" 257 | ) 258 | mine_pattern = r"I need at least a (.*) to mine \w+!" 259 | if re.match(craft_pattern, message): 260 | return re.match(craft_pattern, message).groups()[0] 261 | elif re.match(craft_pattern2, message): 262 | return "a nearby crafting table" 263 | elif re.match(mine_pattern, message): 264 | return re.match(mine_pattern, message).groups()[0] 265 | else: 266 | return "" 267 | 268 | chatlog = set() 269 | for event_type, event in events: 270 | if event_type == "onChat": 271 | item = filter_item(event["onChat"]) 272 | if item: 273 | chatlog.add(item) 274 | return "I also need " + ", ".join(chatlog) + "." if chatlog else "" 275 | -------------------------------------------------------------------------------- /voyager/agents/critic.py: -------------------------------------------------------------------------------- 1 | from voyager.prompts import load_prompt 2 | from voyager.utils.json_utils import fix_and_parse_json 3 | from langchain.chat_models import ChatOpenAI 4 | from langchain.schema import HumanMessage, SystemMessage 5 | 6 | 7 | class CriticAgent: 8 | def __init__( 9 | self, 10 | model_name="gpt-3.5-turbo", 11 | temperature=0, 12 | request_timout=120, 13 | mode="auto", 14 | ): 15 | self.llm = ChatOpenAI( 16 | model_name=model_name, 17 | temperature=temperature, 18 | request_timeout=request_timout, 19 | ) 20 | assert mode in ["auto", "manual"] 21 | self.mode = mode 22 | 23 | def render_system_message(self): 24 | system_message = SystemMessage(content=load_prompt("critic")) 25 | return system_message 26 | 27 | def render_human_message(self, *, events, task, context, chest_observation): 28 | assert events[-1][0] == "observe", "Last event must be observe" 29 | biome = events[-1][1]["status"]["biome"] 30 | time_of_day = events[-1][1]["status"]["timeOfDay"] 31 | voxels = events[-1][1]["voxels"] 32 | health = events[-1][1]["status"]["health"] 33 | hunger = events[-1][1]["status"]["food"] 34 | position = events[-1][1]["status"]["position"] 35 | equipment = events[-1][1]["status"]["equipment"] 36 | inventory_used = events[-1][1]["status"]["inventoryUsed"] 37 | inventory = events[-1][1]["inventory"] 38 | 39 | for i, (event_type, event) in enumerate(events): 40 | if event_type == "onError": 41 | print(f"\033[31mCritic Agent: Error occurs {event['onError']}\033[0m") 42 | return None 43 | 44 | observation = "" 45 | 46 | observation += f"Biome: {biome}\n\n" 47 | 48 | observation += f"Time: {time_of_day}\n\n" 49 | 50 | if voxels: 51 | observation += f"Nearby blocks: {', '.join(voxels)}\n\n" 52 | else: 53 | observation += f"Nearby blocks: None\n\n" 54 | 55 | observation += f"Health: {health:.1f}/20\n\n" 56 | observation += f"Hunger: {hunger:.1f}/20\n\n" 57 | 58 | observation += f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n" 59 | 60 | observation += f"Equipment: {equipment}\n\n" 61 | 62 | if inventory: 63 | observation += f"Inventory ({inventory_used}/36): {inventory}\n\n" 64 | else: 65 | observation += f"Inventory ({inventory_used}/36): Empty\n\n" 66 | 67 | observation += chest_observation 68 | 69 | observation += f"Task: {task}\n\n" 70 | 71 | if context: 72 | observation += f"Context: {context}\n\n" 73 | else: 74 | observation += f"Context: None\n\n" 75 | 76 | print(f"\033[31m****Critic Agent human message****\n{observation}\033[0m") 77 | return HumanMessage(content=observation) 78 | 79 | def human_check_task_success(self): 80 | confirmed = False 81 | success = False 82 | critique = "" 83 | while not confirmed: 84 | success = input("Success? (y/n)") 85 | success = success.lower() == "y" 86 | critique = input("Enter your critique:") 87 | print(f"Success: {success}\nCritique: {critique}") 88 | confirmed = input("Confirm? (y/n)") in ["y", ""] 89 | return success, critique 90 | 91 | def ai_check_task_success(self, messages, max_retries=5): 92 | if max_retries == 0: 93 | print( 94 | "\033[31mFailed to parse Critic Agent response. Consider updating your prompt.\033[0m" 95 | ) 96 | return False, "" 97 | 98 | if messages[1] is None: 99 | return False, "" 100 | 101 | critic = self.llm(messages).content 102 | print(f"\033[31m****Critic Agent ai message****\n{critic}\033[0m") 103 | try: 104 | response = fix_and_parse_json(critic) 105 | assert response["success"] in [True, False] 106 | if "critique" not in response: 107 | response["critique"] = "" 108 | return response["success"], response["critique"] 109 | except Exception as e: 110 | print(f"\033[31mError parsing critic response: {e} Trying again!\033[0m") 111 | return self.ai_check_task_success( 112 | messages=messages, 113 | max_retries=max_retries - 1, 114 | ) 115 | 116 | def check_task_success( 117 | self, *, events, task, context, chest_observation, max_retries=5 118 | ): 119 | human_message = self.render_human_message( 120 | events=events, 121 | task=task, 122 | context=context, 123 | chest_observation=chest_observation, 124 | ) 125 | 126 | messages = [ 127 | self.render_system_message(), 128 | human_message, 129 | ] 130 | 131 | if self.mode == "human": 132 | return self.human_check_task_success() 133 | elif self.mode == "auto": 134 | return self.ai_check_task_success( 135 | messages=messages, max_retries=max_retries 136 | ) 137 | else: 138 | raise ValueError(f"Invalid critic agent mode: {self.mode}") 139 | -------------------------------------------------------------------------------- /voyager/agents/skill.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import voyager.utils as U 4 | from langchain.chat_models import ChatOpenAI 5 | from langchain.embeddings.openai import OpenAIEmbeddings 6 | from langchain.schema import HumanMessage, SystemMessage 7 | from langchain.vectorstores import Chroma 8 | 9 | from voyager.prompts import load_prompt 10 | from voyager.control_primitives import load_control_primitives 11 | 12 | 13 | class SkillManager: 14 | def __init__( 15 | self, 16 | model_name="gpt-3.5-turbo", 17 | temperature=0, 18 | retrieval_top_k=5, 19 | request_timout=120, 20 | ckpt_dir="ckpt", 21 | resume=False, 22 | ): 23 | self.llm = ChatOpenAI( 24 | model_name=model_name, 25 | temperature=temperature, 26 | request_timeout=request_timout, 27 | ) 28 | U.f_mkdir(f"{ckpt_dir}/skill/code") 29 | U.f_mkdir(f"{ckpt_dir}/skill/description") 30 | U.f_mkdir(f"{ckpt_dir}/skill/vectordb") 31 | # programs for env execution 32 | self.control_primitives = load_control_primitives() 33 | if resume: 34 | print(f"\033[33mLoading Skill Manager from {ckpt_dir}/skill\033[0m") 35 | self.skills = U.load_json(f"{ckpt_dir}/skill/skills.json") 36 | else: 37 | self.skills = {} 38 | self.retrieval_top_k = retrieval_top_k 39 | self.ckpt_dir = ckpt_dir 40 | self.vectordb = Chroma( 41 | collection_name="skill_vectordb", 42 | embedding_function=OpenAIEmbeddings(), 43 | persist_directory=f"{ckpt_dir}/skill/vectordb", 44 | ) 45 | assert self.vectordb._collection.count() == len(self.skills), ( 46 | f"Skill Manager's vectordb is not synced with skills.json.\n" 47 | f"There are {self.vectordb._collection.count()} skills in vectordb but {len(self.skills)} skills in skills.json.\n" 48 | f"Did you set resume=False when initializing the manager?\n" 49 | f"You may need to manually delete the vectordb directory for running from scratch." 50 | ) 51 | 52 | @property 53 | def programs(self): 54 | programs = "" 55 | for skill_name, entry in self.skills.items(): 56 | programs += f"{entry['code']}\n\n" 57 | for primitives in self.control_primitives: 58 | programs += f"{primitives}\n\n" 59 | return programs 60 | 61 | def add_skill(self, program_name, program_code): 62 | skill_description = self.generate_skill_description(program_name, program_code) 63 | print( 64 | f"\033[33mSkill Manager generated description for {program_name}:\n{skill_description}\033[0m" 65 | ) 66 | if program_name in self.skills: 67 | print(f"\033[33mSkill {program_name} already exists. Rewriting!\033[0m") 68 | self.vectordb._collection.delete(ids=[program_name]) 69 | i = 2 70 | while f"{program_name}V{i}.js" in os.listdir(f"{self.ckpt_dir}/skill/code"): 71 | i += 1 72 | dumped_program_name = f"{program_name}V{i}" 73 | else: 74 | dumped_program_name = program_name 75 | self.vectordb.add_texts( 76 | texts=[skill_description], 77 | ids=[program_name], 78 | metadatas=[{"name": program_name}], 79 | ) 80 | self.skills[program_name] = { 81 | "code": program_code, 82 | "description": skill_description, 83 | } 84 | assert self.vectordb._collection.count() == len( 85 | self.skills 86 | ), "vectordb is not synced with skills.json" 87 | U.dump_text( 88 | program_code, f"{self.ckpt_dir}/skill/code/{dumped_program_name}.js" 89 | ) 90 | U.dump_text( 91 | skill_description, 92 | f"{self.ckpt_dir}/skill/description/{dumped_program_name}.txt", 93 | ) 94 | U.dump_json(self.skills, f"{self.ckpt_dir}/skill/skills.json") 95 | self.vectordb.persist() 96 | 97 | def generate_skill_description(self, program_name, program_code): 98 | messages = [ 99 | SystemMessage(content=load_prompt("skill")), 100 | HumanMessage( 101 | content=program_code 102 | + "\n\n" 103 | + f"The main function is `{program_name}`." 104 | ), 105 | ] 106 | skill_description = f" // { self.llm(messages).content}" 107 | return f"async function {program_name}(bot) {{\n{skill_description}\n}}" 108 | 109 | def retrieve_skills(self, query): 110 | k = min(self.vectordb._collection.count(), self.retrieval_top_k) 111 | if k == 0: 112 | return [] 113 | print(f"\033[33mSkill Manager retrieving for {k} skills\033[0m") 114 | docs_and_scores = self.vectordb.similarity_search_with_score(query, k=k) 115 | print( 116 | f"\033[33mSkill Manager retrieved skills: " 117 | f"{', '.join([doc.metadata['name'] for doc, _ in docs_and_scores])}\033[0m" 118 | ) 119 | skills = [] 120 | for doc, _ in docs_and_scores: 121 | skills.append(self.skills[doc.metadata["name"]]["code"]) 122 | return skills 123 | -------------------------------------------------------------------------------- /voyager/control_primitives/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /voyager/control_primitives/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | import os 3 | import voyager.utils as U 4 | 5 | 6 | def load_control_primitives(primitive_names=None): 7 | package_path = pkg_resources.resource_filename("voyager", "") 8 | if primitive_names is None: 9 | primitive_names = [ 10 | primitives[:-3] 11 | for primitives in os.listdir(f"{package_path}/control_primitives") 12 | if primitives.endswith(".js") 13 | ] 14 | primitives = [ 15 | U.load_text(f"{package_path}/control_primitives/{primitive_name}.js") 16 | for primitive_name in primitive_names 17 | ] 18 | return primitives 19 | -------------------------------------------------------------------------------- /voyager/control_primitives/craftHelper.js: -------------------------------------------------------------------------------- 1 | function failedCraftFeedback(bot, name, item, craftingTable) { 2 | const recipes = bot.recipesAll(item.id, null, craftingTable); 3 | if (!recipes.length) { 4 | throw new Error(`No crafting table nearby`); 5 | } else { 6 | const recipes = bot.recipesAll( 7 | item.id, 8 | null, 9 | mcData.blocksByName.crafting_table.id 10 | ); 11 | // find the recipe with the fewest missing ingredients 12 | var min = 999; 13 | var min_recipe = null; 14 | for (const recipe of recipes) { 15 | const delta = recipe.delta; 16 | var missing = 0; 17 | for (const delta_item of delta) { 18 | if (delta_item.count < 0) { 19 | const inventory_item = bot.inventory.findInventoryItem( 20 | mcData.items[delta_item.id].name, 21 | null 22 | ); 23 | if (!inventory_item) { 24 | missing += -delta_item.count; 25 | } else { 26 | missing += Math.max( 27 | -delta_item.count - inventory_item.count, 28 | 0 29 | ); 30 | } 31 | } 32 | } 33 | if (missing < min) { 34 | min = missing; 35 | min_recipe = recipe; 36 | } 37 | } 38 | const delta = min_recipe.delta; 39 | let message = ""; 40 | for (const delta_item of delta) { 41 | if (delta_item.count < 0) { 42 | const inventory_item = bot.inventory.findInventoryItem( 43 | mcData.items[delta_item.id].name, 44 | null 45 | ); 46 | if (!inventory_item) { 47 | message += ` ${-delta_item.count} more ${ 48 | mcData.items[delta_item.id].name 49 | }, `; 50 | } else { 51 | if (inventory_item.count < -delta_item.count) { 52 | message += `${ 53 | -delta_item.count - inventory_item.count 54 | } more ${mcData.items[delta_item.id].name}`; 55 | } 56 | } 57 | } 58 | } 59 | bot.chat(`I cannot make ${name} because I need: ${message}`); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /voyager/control_primitives/craftItem.js: -------------------------------------------------------------------------------- 1 | async function craftItem(bot, name, count = 1) { 2 | // return if name is not string 3 | if (typeof name !== "string") { 4 | throw new Error("name for craftItem must be a string"); 5 | } 6 | // return if count is not number 7 | if (typeof count !== "number") { 8 | throw new Error("count for craftItem must be a number"); 9 | } 10 | const itemByName = mcData.itemsByName[name]; 11 | if (!itemByName) { 12 | throw new Error(`No item named ${name}`); 13 | } 14 | const craftingTable = bot.findBlock({ 15 | matching: mcData.blocksByName.crafting_table.id, 16 | maxDistance: 32, 17 | }); 18 | if (!craftingTable) { 19 | bot.chat("Craft without a crafting table"); 20 | } else { 21 | await bot.pathfinder.goto( 22 | new GoalLookAtBlock(craftingTable.position, bot.world) 23 | ); 24 | } 25 | const recipe = bot.recipesFor(itemByName.id, null, 1, craftingTable)[0]; 26 | if (recipe) { 27 | bot.chat(`I can make ${name}`); 28 | try { 29 | await bot.craft(recipe, count, craftingTable); 30 | bot.chat(`I did the recipe for ${name} ${count} times`); 31 | } catch (err) { 32 | bot.chat(`I cannot do the recipe for ${name} ${count} times`); 33 | } 34 | } else { 35 | failedCraftFeedback(bot, name, itemByName, craftingTable); 36 | _craftItemFailCount++; 37 | if (_craftItemFailCount > 10) { 38 | throw new Error( 39 | "craftItem failed too many times, check chat log to see what happened" 40 | ); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /voyager/control_primitives/exploreUntil.js: -------------------------------------------------------------------------------- 1 | // Explore downward for 60 seconds: exploreUntil(bot, new Vec3(0, -1, 0), 60); 2 | async function exploreUntil( 3 | bot, 4 | direction, 5 | maxTime = 60, 6 | callback = () => { 7 | return false; 8 | } 9 | ) { 10 | if (typeof maxTime !== "number") { 11 | throw new Error("maxTime must be a number"); 12 | } 13 | if (typeof callback !== "function") { 14 | throw new Error("callback must be a function"); 15 | } 16 | const test = callback(); 17 | if (test) { 18 | bot.chat("Explore success."); 19 | return Promise.resolve(test); 20 | } 21 | if (direction.x === 0 && direction.y === 0 && direction.z === 0) { 22 | throw new Error("direction cannot be 0, 0, 0"); 23 | } 24 | if ( 25 | !( 26 | (direction.x === 0 || direction.x === 1 || direction.x === -1) && 27 | (direction.y === 0 || direction.y === 1 || direction.y === -1) && 28 | (direction.z === 0 || direction.z === 1 || direction.z === -1) 29 | ) 30 | ) { 31 | throw new Error( 32 | "direction must be a Vec3 only with value of -1, 0 or 1" 33 | ); 34 | } 35 | maxTime = Math.min(maxTime, 1200); 36 | return new Promise((resolve, reject) => { 37 | const dx = direction.x; 38 | const dy = direction.y; 39 | const dz = direction.z; 40 | 41 | let explorationInterval; 42 | let maxTimeTimeout; 43 | 44 | const cleanUp = () => { 45 | clearInterval(explorationInterval); 46 | clearTimeout(maxTimeTimeout); 47 | bot.pathfinder.setGoal(null); 48 | }; 49 | 50 | const explore = () => { 51 | const x = 52 | bot.entity.position.x + 53 | Math.floor(Math.random() * 20 + 10) * dx; 54 | const y = 55 | bot.entity.position.y + 56 | Math.floor(Math.random() * 20 + 10) * dy; 57 | const z = 58 | bot.entity.position.z + 59 | Math.floor(Math.random() * 20 + 10) * dz; 60 | let goal = new GoalNear(x, y, z); 61 | if (dy === 0) { 62 | goal = new GoalNearXZ(x, z); 63 | } 64 | bot.pathfinder.setGoal(goal); 65 | 66 | try { 67 | const result = callback(); 68 | if (result) { 69 | cleanUp(); 70 | bot.chat("Explore success."); 71 | resolve(result); 72 | } 73 | } catch (err) { 74 | cleanUp(); 75 | reject(err); 76 | } 77 | }; 78 | 79 | explorationInterval = setInterval(explore, 2000); 80 | 81 | maxTimeTimeout = setTimeout(() => { 82 | cleanUp(); 83 | bot.chat("Max exploration time reached"); 84 | resolve(null); 85 | }, maxTime * 1000); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /voyager/control_primitives/givePlacedItemBack.js: -------------------------------------------------------------------------------- 1 | async function givePlacedItemBack(bot, name, position) { 2 | await bot.chat("/gamerule doTileDrops false"); 3 | // iterate name and position 4 | const history = []; 5 | for (let i = 0; i < name.length; i++) { 6 | await givePlacedItemBackSingle(bot, name[i], position[i]); 7 | } 8 | await bot.chat("/gamerule doTileDrops true"); 9 | 10 | async function givePlacedItemBackSingle(bot, name, position) { 11 | bot.chat(`/give bot ${name} 1`); 12 | const x = Math.floor(position.x); 13 | const y = Math.floor(position.y); 14 | const z = Math.floor(position.z); 15 | // loop through 125 blocks around the block 16 | const size = 3; 17 | for (let dx = -size; dx <= size; dx++) { 18 | for (let dy = -size; dy <= size; dy++) { 19 | for (let dz = -size; dz <= size; dz++) { 20 | const block = bot.blockAt(new Vec3(x + dx, y + dy, z + dz)); 21 | if ( 22 | block?.name === name && 23 | !history.includes(block.position) 24 | ) { 25 | await bot.chat( 26 | `/setblock ${x + dx} ${y + dy} ${ 27 | z + dz 28 | } air destroy` 29 | ); 30 | history.push(block.position); 31 | await bot.waitForTicks(20); 32 | return; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /voyager/control_primitives/killMob.js: -------------------------------------------------------------------------------- 1 | async function killMob(bot, mobName, timeout = 300) { 2 | // return if mobName is not string 3 | if (typeof mobName !== "string") { 4 | throw new Error(`mobName for killMob must be a string`); 5 | } 6 | // return if timeout is not number 7 | if (typeof timeout !== "number") { 8 | throw new Error(`timeout for killMob must be a number`); 9 | } 10 | 11 | const weaponsForShooting = [ 12 | "bow", 13 | "crossbow", 14 | "snowball", 15 | "ender_pearl", 16 | "egg", 17 | "splash_potion", 18 | "trident", 19 | ]; 20 | const mainHandItem = bot.inventory.slots[bot.getEquipmentDestSlot("hand")]; 21 | 22 | const entity = bot.nearestEntity( 23 | (entity) => 24 | entity.name === mobName && 25 | // kill mob distance should be slightly bigger than explore distance 26 | entity.position.distanceTo(bot.entity.position) < 48 27 | ); 28 | if (!entity) { 29 | bot.chat(`No ${mobName} nearby, please explore first`); 30 | _killMobFailCount++; 31 | if (_killMobFailCount > 10) { 32 | throw new Error( 33 | `killMob failed too many times, make sure you explore before calling killMob` 34 | ); 35 | } 36 | return; 37 | } 38 | 39 | let droppedItem; 40 | if (mainHandItem && weaponsForShooting.includes(mainHandItem.name)) { 41 | bot.hawkEye.autoAttack(entity, mainHandItem.name); 42 | droppedItem = await waitForMobShot(bot, entity, timeout); 43 | } else { 44 | await bot.pvp.attack(entity); 45 | droppedItem = await waitForMobRemoved(bot, entity, timeout); 46 | } 47 | if (droppedItem) { 48 | await bot.collectBlock.collect(droppedItem, { ignoreNoPath: true }); 49 | } 50 | bot.save(`${mobName}_killed`); 51 | } 52 | -------------------------------------------------------------------------------- /voyager/control_primitives/mineBlock.js: -------------------------------------------------------------------------------- 1 | async function mineBlock(bot, name, count = 1) { 2 | // return if name is not string 3 | if (typeof name !== "string") { 4 | throw new Error(`name for mineBlock must be a string`); 5 | } 6 | if (typeof count !== "number") { 7 | throw new Error(`count for mineBlock must be a number`); 8 | } 9 | const blockByName = mcData.blocksByName[name]; 10 | if (!blockByName) { 11 | throw new Error(`No block named ${name}`); 12 | } 13 | const blocks = bot.findBlocks({ 14 | matching: [blockByName.id], 15 | maxDistance: 32, 16 | count: 1024, 17 | }); 18 | if (blocks.length === 0) { 19 | bot.chat(`No ${name} nearby, please explore first`); 20 | _mineBlockFailCount++; 21 | if (_mineBlockFailCount > 10) { 22 | throw new Error( 23 | "mineBlock failed too many times, make sure you explore before calling mineBlock" 24 | ); 25 | } 26 | return; 27 | } 28 | const targets = []; 29 | for (let i = 0; i < blocks.length; i++) { 30 | targets.push(bot.blockAt(blocks[i])); 31 | } 32 | await bot.collectBlock.collect(targets, { 33 | ignoreNoPath: true, 34 | count: count, 35 | }); 36 | bot.save(`${name}_mined`); 37 | } 38 | -------------------------------------------------------------------------------- /voyager/control_primitives/placeItem.js: -------------------------------------------------------------------------------- 1 | async function placeItem(bot, name, position) { 2 | // return if name is not string 3 | if (typeof name !== "string") { 4 | throw new Error(`name for placeItem must be a string`); 5 | } 6 | // return if position is not Vec3 7 | if (!(position instanceof Vec3)) { 8 | throw new Error(`position for placeItem must be a Vec3`); 9 | } 10 | const itemByName = mcData.itemsByName[name]; 11 | if (!itemByName) { 12 | throw new Error(`No item named ${name}`); 13 | } 14 | const item = bot.inventory.findInventoryItem(itemByName.id); 15 | if (!item) { 16 | bot.chat(`No ${name} in inventory`); 17 | return; 18 | } 19 | const item_count = item.count; 20 | // find a reference block 21 | const faceVectors = [ 22 | new Vec3(0, 1, 0), 23 | new Vec3(0, -1, 0), 24 | new Vec3(1, 0, 0), 25 | new Vec3(-1, 0, 0), 26 | new Vec3(0, 0, 1), 27 | new Vec3(0, 0, -1), 28 | ]; 29 | let referenceBlock = null; 30 | let faceVector = null; 31 | for (const vector of faceVectors) { 32 | const block = bot.blockAt(position.minus(vector)); 33 | if (block?.name !== "air") { 34 | referenceBlock = block; 35 | faceVector = vector; 36 | bot.chat(`Placing ${name} on ${block.name} at ${block.position}`); 37 | break; 38 | } 39 | } 40 | if (!referenceBlock) { 41 | bot.chat( 42 | `No block to place ${name} on. You cannot place a floating block.` 43 | ); 44 | _placeItemFailCount++; 45 | if (_placeItemFailCount > 10) { 46 | throw new Error( 47 | `placeItem failed too many times. You cannot place a floating block.` 48 | ); 49 | } 50 | return; 51 | } 52 | 53 | // You must use try catch to placeBlock 54 | try { 55 | // You must first go to the block position you want to place 56 | await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {})); 57 | // You must equip the item right before calling placeBlock 58 | await bot.equip(item, "hand"); 59 | await bot.placeBlock(referenceBlock, faceVector); 60 | bot.chat(`Placed ${name}`); 61 | bot.save(`${name}_placed`); 62 | } catch (err) { 63 | const item = bot.inventory.findInventoryItem(itemByName.id); 64 | if (item?.count === item_count) { 65 | bot.chat( 66 | `Error placing ${name}: ${err.message}, please find another position to place` 67 | ); 68 | _placeItemFailCount++; 69 | if (_placeItemFailCount > 10) { 70 | throw new Error( 71 | `placeItem failed too many times, please find another position to place.` 72 | ); 73 | } 74 | } else { 75 | bot.chat(`Placed ${name}`); 76 | bot.save(`${name}_placed`); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /voyager/control_primitives/shoot.js: -------------------------------------------------------------------------------- 1 | // shoot 1 pig with a bow: shoot(bot, "bow", "pig"); 2 | async function shoot(bot, weapon, target) { 3 | const validWeapons = [ 4 | "bow", 5 | "crossbow", 6 | "snowball", 7 | "ender_pearl", 8 | "egg", 9 | "splash_potion", 10 | "trident", 11 | ]; 12 | if (!validWeapons.includes(weapon)) { 13 | bot.chat(`${weapon} is not a valid weapon for shooting`); 14 | return; 15 | } 16 | 17 | const weaponItem = mcData.itemsByName[weapon]; 18 | if (!bot.inventory.findInventoryItem(weaponItem.id, null)) { 19 | bot.chat(`No ${weapon} in inventory for shooting`); 20 | return; 21 | } 22 | 23 | const targetEntity = bot.nearestEntity( 24 | (entity) => 25 | entity.name === target 26 | ); 27 | if (!targetEntity) { 28 | bot.chat(`No ${target} nearby`); 29 | return; 30 | } 31 | bot.hawkEye.autoAttack(targetEntity, "bow"); 32 | bot.on('auto_shot_stopped', (target) => { 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /voyager/control_primitives/smeltItem.js: -------------------------------------------------------------------------------- 1 | async function smeltItem(bot, itemName, fuelName, count = 1) { 2 | // return if itemName or fuelName is not string 3 | if (typeof itemName !== "string" || typeof fuelName !== "string") { 4 | throw new Error("itemName or fuelName for smeltItem must be a string"); 5 | } 6 | // return if count is not a number 7 | if (typeof count !== "number") { 8 | throw new Error("count for smeltItem must be a number"); 9 | } 10 | const item = mcData.itemsByName[itemName]; 11 | const fuel = mcData.itemsByName[fuelName]; 12 | if (!item) { 13 | throw new Error(`No item named ${itemName}`); 14 | } 15 | if (!fuel) { 16 | throw new Error(`No item named ${fuelName}`); 17 | } 18 | const furnaceBlock = bot.findBlock({ 19 | matching: mcData.blocksByName.furnace.id, 20 | maxDistance: 32, 21 | }); 22 | if (!furnaceBlock) { 23 | throw new Error("No furnace nearby"); 24 | } else { 25 | await bot.pathfinder.goto( 26 | new GoalLookAtBlock(furnaceBlock.position, bot.world) 27 | ); 28 | } 29 | const furnace = await bot.openFurnace(furnaceBlock); 30 | let success_count = 0; 31 | for (let i = 0; i < count; i++) { 32 | if (!bot.inventory.findInventoryItem(item.id, null)) { 33 | bot.chat(`No ${itemName} to smelt in inventory`); 34 | break; 35 | } 36 | if (furnace.fuelSeconds < 15 && furnace.fuelItem()?.name !== fuelName) { 37 | if (!bot.inventory.findInventoryItem(fuel.id, null)) { 38 | bot.chat(`No ${fuelName} as fuel in inventory`); 39 | break; 40 | } 41 | await furnace.putFuel(fuel.id, null, 1); 42 | await bot.waitForTicks(20); 43 | if (!furnace.fuel && furnace.fuelItem()?.name !== fuelName) { 44 | throw new Error(`${fuelName} is not a valid fuel`); 45 | } 46 | } 47 | await furnace.putInput(item.id, null, 1); 48 | await bot.waitForTicks(12 * 20); 49 | if (!furnace.outputItem()) { 50 | throw new Error(`${itemName} is not a valid input`); 51 | } 52 | await furnace.takeOutput(); 53 | success_count++; 54 | } 55 | furnace.close(); 56 | if (success_count > 0) bot.chat(`Smelted ${success_count} ${itemName}.`); 57 | else { 58 | bot.chat( 59 | `Failed to smelt ${itemName}, please check the fuel and input.` 60 | ); 61 | _smeltItemFailCount++; 62 | if (_smeltItemFailCount > 10) { 63 | throw new Error( 64 | `smeltItem failed too many times, please check the fuel and input.` 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /voyager/control_primitives/useChest.js: -------------------------------------------------------------------------------- 1 | async function getItemFromChest(bot, chestPosition, itemsToGet) { 2 | // return if chestPosition is not Vec3 3 | if (!(chestPosition instanceof Vec3)) { 4 | bot.chat("chestPosition for getItemFromChest must be a Vec3"); 5 | return; 6 | } 7 | await moveToChest(bot, chestPosition); 8 | const chestBlock = bot.blockAt(chestPosition); 9 | const chest = await bot.openContainer(chestBlock); 10 | for (const name in itemsToGet) { 11 | const itemByName = mcData.itemsByName[name]; 12 | if (!itemByName) { 13 | bot.chat(`No item named ${name}`); 14 | continue; 15 | } 16 | 17 | const item = chest.findContainerItem(itemByName.id); 18 | if (!item) { 19 | bot.chat(`I don't see ${name} in this chest`); 20 | continue; 21 | } 22 | try { 23 | await chest.withdraw(item.type, null, itemsToGet[name]); 24 | } catch (err) { 25 | bot.chat(`Not enough ${name} in chest.`); 26 | } 27 | } 28 | await closeChest(bot, chestBlock); 29 | } 30 | 31 | async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) { 32 | // return if chestPosition is not Vec3 33 | if (!(chestPosition instanceof Vec3)) { 34 | throw new Error( 35 | "chestPosition for depositItemIntoChest must be a Vec3" 36 | ); 37 | } 38 | await moveToChest(bot, chestPosition); 39 | const chestBlock = bot.blockAt(chestPosition); 40 | const chest = await bot.openContainer(chestBlock); 41 | for (const name in itemsToDeposit) { 42 | const itemByName = mcData.itemsByName[name]; 43 | if (!itemByName) { 44 | bot.chat(`No item named ${name}`); 45 | continue; 46 | } 47 | const item = bot.inventory.findInventoryItem(itemByName.id); 48 | if (!item) { 49 | bot.chat(`No ${name} in inventory`); 50 | continue; 51 | } 52 | try { 53 | await chest.deposit(item.type, null, itemsToDeposit[name]); 54 | } catch (err) { 55 | bot.chat(`Not enough ${name} in inventory.`); 56 | } 57 | } 58 | await closeChest(bot, chestBlock); 59 | } 60 | 61 | async function checkItemInsideChest(bot, chestPosition) { 62 | // return if chestPosition is not Vec3 63 | if (!(chestPosition instanceof Vec3)) { 64 | throw new Error( 65 | "chestPosition for depositItemIntoChest must be a Vec3" 66 | ); 67 | } 68 | await moveToChest(bot, chestPosition); 69 | const chestBlock = bot.blockAt(chestPosition); 70 | await bot.openContainer(chestBlock); 71 | await closeChest(bot, chestBlock); 72 | } 73 | 74 | async function moveToChest(bot, chestPosition) { 75 | if (!(chestPosition instanceof Vec3)) { 76 | throw new Error( 77 | "chestPosition for depositItemIntoChest must be a Vec3" 78 | ); 79 | } 80 | if (chestPosition.distanceTo(bot.entity.position) > 32) { 81 | bot.chat( 82 | `/tp ${chestPosition.x} ${chestPosition.y} ${chestPosition.z}` 83 | ); 84 | await bot.waitForTicks(20); 85 | } 86 | const chestBlock = bot.blockAt(chestPosition); 87 | if (chestBlock.name !== "chest" || chestBlock.name !== "chest_minecart") { 88 | bot.emit("removeChest", chestPosition); 89 | throw new Error( 90 | `No chest at ${chestPosition}, it is ${chestBlock.name}` 91 | ); 92 | } 93 | await bot.pathfinder.goto( 94 | new GoalLookAtBlock(chestBlock.position, bot.world, {}) 95 | ); 96 | return chestBlock; 97 | } 98 | 99 | async function listItemsInChest(bot, chestBlock) { 100 | const chest = await bot.openContainer(chestBlock); 101 | const items = chest.containerItems(); 102 | if (items.length > 0) { 103 | const itemNames = items.reduce((acc, obj) => { 104 | if (acc[obj.name]) { 105 | acc[obj.name] += obj.count; 106 | } else { 107 | acc[obj.name] = obj.count; 108 | } 109 | return acc; 110 | }, {}); 111 | bot.emit("closeChest", itemNames, chestBlock.position); 112 | } else { 113 | bot.emit("closeChest", {}, chestBlock.position); 114 | } 115 | return chest; 116 | } 117 | 118 | async function closeChest(bot, chestBlock) { 119 | try { 120 | const chest = await listItemsInChest(bot, chestBlock); 121 | await chest.close(); 122 | } catch (err) { 123 | await bot.closeWindow(chestBlock); 124 | } 125 | } 126 | 127 | function itemByName(items, name) { 128 | for (let i = 0; i < items.length; ++i) { 129 | const item = items[i]; 130 | if (item && item.name === name) return item; 131 | } 132 | return null; 133 | } 134 | // 135 | // function itemToString(item) { 136 | // return item && `${item.count} ${item.name}`; 137 | // } 138 | -------------------------------------------------------------------------------- /voyager/control_primitives/waitForMobRemoved.js: -------------------------------------------------------------------------------- 1 | function waitForMobRemoved(bot, entity, timeout = 300) { 2 | return new Promise((resolve, reject) => { 3 | let success = false; 4 | let droppedItem = null; 5 | // Set up timeout 6 | const timeoutId = setTimeout(() => { 7 | success = false; 8 | bot.pvp.stop(); 9 | }, timeout * 1000); 10 | 11 | // Function to handle entityRemoved event 12 | function onEntityGone(e) { 13 | if (e === entity) { 14 | success = true; 15 | clearTimeout(timeoutId); 16 | bot.chat(`Killed ${entity.name}!`); 17 | bot.pvp.stop(); 18 | } 19 | } 20 | 21 | function onItemDrop(item) { 22 | if (entity.position.distanceTo(item.position) <= 1) { 23 | droppedItem = item; 24 | } 25 | } 26 | 27 | function onStoppedAttacking() { 28 | clearTimeout(timeoutId); 29 | bot.removeListener("entityGone", onEntityGone); 30 | bot.removeListener("stoppedAttacking", onStoppedAttacking); 31 | bot.removeListener("itemDrop", onItemDrop); 32 | if (!success) reject(new Error(`Failed to kill ${entity.name}.`)); 33 | else resolve(droppedItem); 34 | } 35 | 36 | // Listen for entityRemoved event 37 | bot.on("entityGone", onEntityGone); 38 | bot.on("stoppedAttacking", onStoppedAttacking); 39 | bot.on("itemDrop", onItemDrop); 40 | }); 41 | } 42 | 43 | 44 | function waitForMobShot(bot, entity, timeout = 300) { 45 | return new Promise((resolve, reject) => { 46 | let success = false; 47 | let droppedItem = null; 48 | // Set up timeout 49 | const timeoutId = setTimeout(() => { 50 | success = false; 51 | bot.hawkEye.stop(); 52 | }, timeout * 1000); 53 | 54 | // Function to handle entityRemoved event 55 | function onEntityGone(e) { 56 | if (e === entity) { 57 | success = true; 58 | clearTimeout(timeoutId); 59 | bot.chat(`Shot ${entity.name}!`); 60 | bot.hawkEye.stop(); 61 | } 62 | } 63 | 64 | function onItemDrop(item) { 65 | if (entity.position.distanceTo(item.position) <= 1) { 66 | droppedItem = item; 67 | } 68 | } 69 | 70 | function onAutoShotStopped() { 71 | clearTimeout(timeoutId); 72 | bot.removeListener("entityGone", onEntityGone); 73 | bot.removeListener("auto_shot_stopped", onAutoShotStopped); 74 | bot.removeListener("itemDrop", onItemDrop); 75 | if (!success) reject(new Error(`Failed to shoot ${entity.name}.`)); 76 | else resolve(droppedItem); 77 | } 78 | 79 | // Listen for entityRemoved event 80 | bot.on("entityGone", onEntityGone); 81 | bot.on("auto_shot_stopped", onAutoShotStopped); 82 | bot.on("itemDrop", onItemDrop); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | import os 3 | import voyager.utils as U 4 | 5 | 6 | def load_control_primitives_context(primitive_names=None): 7 | package_path = pkg_resources.resource_filename("voyager", "") 8 | if primitive_names is None: 9 | primitive_names = [ 10 | primitive[:-3] 11 | for primitive in os.listdir(f"{package_path}/control_primitives_context") 12 | if primitive.endswith(".js") 13 | ] 14 | primitives = [ 15 | U.load_text(f"{package_path}/control_primitives_context/{primitive_name}.js") 16 | for primitive_name in primitive_names 17 | ] 18 | return primitives 19 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/craftItem.js: -------------------------------------------------------------------------------- 1 | // Craft 8 oak_planks from 2 oak_log (do the recipe 2 times): craftItem(bot, "oak_planks", 2); 2 | // You must place a crafting table before calling this function 3 | async function craftItem(bot, name, count = 1) { 4 | const item = mcData.itemsByName[name]; 5 | const craftingTable = bot.findBlock({ 6 | matching: mcData.blocksByName.crafting_table.id, 7 | maxDistance: 32, 8 | }); 9 | await bot.pathfinder.goto( 10 | new GoalLookAtBlock(craftingTable.position, bot.world) 11 | ); 12 | const recipe = bot.recipesFor(item.id, null, 1, craftingTable)[0]; 13 | await bot.craft(recipe, count, craftingTable); 14 | } 15 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/exploreUntil.js: -------------------------------------------------------------------------------- 1 | /* 2 | Explore until find an iron_ore, use Vec3(0, -1, 0) because iron ores are usually underground 3 | await exploreUntil(bot, new Vec3(0, -1, 0), 60, () => { 4 | const iron_ore = bot.findBlock({ 5 | matching: mcData.blocksByName["iron_ore"].id, 6 | maxDistance: 32, 7 | }); 8 | return iron_ore; 9 | }); 10 | 11 | Explore until find a pig, use Vec3(1, 0, 1) because pigs are usually on the surface 12 | let pig = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => { 13 | const pig = bot.nearestEntity((entity) => { 14 | return ( 15 | entity.name === "pig" && 16 | entity.position.distanceTo(bot.entity.position) < 32 17 | ); 18 | }); 19 | return pig; 20 | }); 21 | */ 22 | async function exploreUntil(bot, direction, maxTime = 60, callback) { 23 | /* 24 | Implementation of this function is omitted. 25 | direction: Vec3, can only contain value of -1, 0 or 1 26 | maxTime: number, the max time for exploration 27 | callback: function, early stop condition, will be called each second, exploration will stop if return value is not null 28 | 29 | Return: null if explore timeout, otherwise return the return value of callback 30 | */ 31 | } 32 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/killMob.js: -------------------------------------------------------------------------------- 1 | // Kill a pig and collect the dropped item: killMob(bot, "pig", 300); 2 | async function killMob(bot, mobName, timeout = 300) { 3 | const entity = bot.nearestEntity( 4 | (entity) => 5 | entity.name === mobName && 6 | entity.position.distanceTo(bot.entity.position) < 32 7 | ); 8 | await bot.pvp.attack(entity); 9 | await bot.pathfinder.goto( 10 | new GoalBlock(entity.position.x, entity.position.y, entity.position.z) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/mineBlock.js: -------------------------------------------------------------------------------- 1 | // Mine 3 cobblestone: mineBlock(bot, "stone", 3); 2 | async function mineBlock(bot, name, count = 1) { 3 | const blocks = bot.findBlocks({ 4 | matching: (block) => { 5 | return block.name === name; 6 | }, 7 | maxDistance: 32, 8 | count: count, 9 | }); 10 | const targets = []; 11 | for (let i = 0; i < Math.min(blocks.length, count); i++) { 12 | targets.push(bot.blockAt(blocks[i])); 13 | } 14 | await bot.collectBlock.collect(targets, { ignoreNoPath: true }); 15 | } 16 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/mineflayer.js: -------------------------------------------------------------------------------- 1 | await bot.pathfinder.goto(goal); // A very useful function. This function may change your main-hand equipment. 2 | // Following are some Goals you can use: 3 | 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` 4 | new GoalXZ(x, z); // Useful for long-range goals that don't have a specific Y level. `x` and `z` are `number` 5 | 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` 6 | new GoalFollow(entity, range); // Follow the specified entity within the specified range. `entity` is `Entity`, `range` is `number` 7 | new GoalPlaceBlock(position, bot.world, {}); // Position the bot in order to place a block. `position` is `Vec3` 8 | new GoalLookAtBlock(position, bot.world, {}); // Path into a position where a blockface of the block at position is visible. `position` is `Vec3` 9 | 10 | // These are other Mineflayer functions you can use: 11 | bot.isABed(bedBlock); // Return true if `bedBlock` is a bed 12 | bot.blockAt(position); // Return the block at `position`. `position` is `Vec3` 13 | 14 | // These are other Mineflayer async functions you can use: 15 | 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" 16 | 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. 17 | 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 18 | await bot.sleep(bedBlock); // Sleep until sunrise. You must get to a bed block first 19 | 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 20 | 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` 21 | 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 22 | 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 23 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/placeItem.js: -------------------------------------------------------------------------------- 1 | // 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)); 2 | async function placeItem(bot, name, position) { 3 | const item = bot.inventory.findInventoryItem(mcData.itemsByName[name].id); 4 | // find a reference block 5 | const faceVectors = [ 6 | new Vec3(0, 1, 0), 7 | new Vec3(0, -1, 0), 8 | new Vec3(1, 0, 0), 9 | new Vec3(-1, 0, 0), 10 | new Vec3(0, 0, 1), 11 | new Vec3(0, 0, -1), 12 | ]; 13 | let referenceBlock = null; 14 | let faceVector = null; 15 | for (const vector of faceVectors) { 16 | const block = bot.blockAt(position.minus(vector)); 17 | if (block?.name !== "air") { 18 | referenceBlock = block; 19 | faceVector = vector; 20 | break; 21 | } 22 | } 23 | // You must first go to the block position you want to place 24 | await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {})); 25 | // You must equip the item right before calling placeBlock 26 | await bot.equip(item, "hand"); 27 | await bot.placeBlock(referenceBlock, faceVector); 28 | } 29 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/smeltItem.js: -------------------------------------------------------------------------------- 1 | // Smelt 1 raw_iron into 1 iron_ingot using 1 oak_planks as fuel: smeltItem(bot, "raw_iron", "oak_planks"); 2 | // You must place a furnace before calling this function 3 | async function smeltItem(bot, itemName, fuelName, count = 1) { 4 | const item = mcData.itemsByName[itemName]; 5 | const fuel = mcData.itemsByName[fuelName]; 6 | const furnaceBlock = bot.findBlock({ 7 | matching: mcData.blocksByName.furnace.id, 8 | maxDistance: 32, 9 | }); 10 | await bot.pathfinder.goto( 11 | new GoalLookAtBlock(furnaceBlock.position, bot.world) 12 | ); 13 | const furnace = await bot.openFurnace(furnaceBlock); 14 | for (let i = 0; i < count; i++) { 15 | await furnace.putFuel(fuel.id, null, 1); 16 | await furnace.putInput(item.id, null, 1); 17 | // Wait 12 seconds for the furnace to smelt the item 18 | await bot.waitForTicks(12 * 20); 19 | await furnace.takeOutput(); 20 | } 21 | await furnace.close(); 22 | } 23 | -------------------------------------------------------------------------------- /voyager/control_primitives_context/useChest.js: -------------------------------------------------------------------------------- 1 | // Get a torch from chest at (30, 65, 100): getItemFromChest(bot, new Vec3(30, 65, 100), {"torch": 1}); 2 | // This function will work no matter how far the bot is from the chest. 3 | async function getItemFromChest(bot, chestPosition, itemsToGet) { 4 | await moveToChest(bot, chestPosition); 5 | const chestBlock = bot.blockAt(chestPosition); 6 | const chest = await bot.openContainer(chestBlock); 7 | for (const name in itemsToGet) { 8 | const itemByName = mcData.itemsByName[name]; 9 | const item = chest.findContainerItem(itemByName.id); 10 | await chest.withdraw(item.type, null, itemsToGet[name]); 11 | } 12 | await closeChest(bot, chestBlock); 13 | } 14 | // Deposit a torch into chest at (30, 65, 100): depositItemIntoChest(bot, new Vec3(30, 65, 100), {"torch": 1}); 15 | // This function will work no matter how far the bot is from the chest. 16 | async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) { 17 | await moveToChest(bot, chestPosition); 18 | const chestBlock = bot.blockAt(chestPosition); 19 | const chest = await bot.openContainer(chestBlock); 20 | for (const name in itemsToDeposit) { 21 | const itemByName = mcData.itemsByName[name]; 22 | const item = bot.inventory.findInventoryItem(itemByName.id); 23 | await chest.deposit(item.type, null, itemsToDeposit[name]); 24 | } 25 | await closeChest(bot, chestBlock); 26 | } 27 | // Check the items inside the chest at (30, 65, 100): checkItemInsideChest(bot, new Vec3(30, 65, 100)); 28 | // You only need to call this function once without any action to finish task of checking items inside the chest. 29 | async function checkItemInsideChest(bot, chestPosition) { 30 | await moveToChest(bot, chestPosition); 31 | const chestBlock = bot.blockAt(chestPosition); 32 | await bot.openContainer(chestBlock); 33 | // You must close the chest after opening it if you are asked to open a chest 34 | await closeChest(bot, chestBlock); 35 | } 36 | -------------------------------------------------------------------------------- /voyager/env/.gitignore: -------------------------------------------------------------------------------- 1 | # MCP-Reborn 2 | MCP-Reborn/ 3 | run/ 4 | *.jar 5 | config.json 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | .idea/ 166 | 167 | # Logs 168 | logs 169 | npm-debug.log* 170 | yarn-debug.log* 171 | yarn-error.log* 172 | lerna-debug.log* 173 | .pnpm-debug.log* 174 | 175 | # Diagnostic reports (https://nodejs.org/api/report.html) 176 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 177 | 178 | # Runtime data 179 | pids 180 | *.pid 181 | *.seed 182 | *.pid.lock 183 | 184 | # Directory for instrumented libs generated by jscoverage/JSCover 185 | lib-cov 186 | 187 | # Coverage directory used by tools like istanbul 188 | coverage 189 | *.lcov 190 | 191 | # nyc test coverage 192 | .nyc_output 193 | 194 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 195 | .grunt 196 | 197 | # Bower dependency directory (https://bower.io/) 198 | bower_components 199 | 200 | # node-waf configuration 201 | .lock-wscript 202 | 203 | # Compiled binary addons (https://nodejs.org/api/addons.html) 204 | build/Release 205 | 206 | # Dependency directories 207 | node_modules/ 208 | jspm_packages/ 209 | 210 | # Snowpack dependency directory (https://snowpack.dev/) 211 | web_modules/ 212 | 213 | # TypeScript cache 214 | *.tsbuildinfo 215 | 216 | # Optional npm cache directory 217 | .npm 218 | 219 | # Optional eslint cache 220 | .eslintcache 221 | 222 | # Optional stylelint cache 223 | .stylelintcache 224 | 225 | # Microbundle cache 226 | .rpt2_cache/ 227 | .rts2_cache_cjs/ 228 | .rts2_cache_es/ 229 | .rts2_cache_umd/ 230 | 231 | # Optional REPL history 232 | .node_repl_history 233 | 234 | # Output of 'npm pack' 235 | *.tgz 236 | 237 | # Yarn Integrity file 238 | .yarn-integrity 239 | 240 | # dotenv environment variable files 241 | .env.development.local 242 | .env.test.local 243 | .env.production.local 244 | .env.local 245 | 246 | # parcel-bundler cache (https://parceljs.org/) 247 | .parcel-cache 248 | 249 | # Next.js build output 250 | .next 251 | out 252 | 253 | # Nuxt.js build / generate output 254 | .nuxt 255 | dist 256 | 257 | # Gatsby files 258 | .cache/ 259 | # Comment in the public line in if your project uses Gatsby and not Next.js 260 | # https://nextjs.org/blog/next-9-1#public-directory-support 261 | # public 262 | 263 | # vuepress build output 264 | .vuepress/dist 265 | 266 | # vuepress v2.x temp and cache directory 267 | .temp 268 | 269 | # Docusaurus cache and generated files 270 | .docusaurus 271 | 272 | # Serverless directories 273 | .serverless/ 274 | 275 | # FuseBox cache 276 | .fusebox/ 277 | 278 | # DynamoDB Local files 279 | .dynamodb/ 280 | 281 | # TernJS port file 282 | .tern-port 283 | 284 | # Stores VSCode versions used for testing VSCode extensions 285 | .vscode-test 286 | 287 | # yarn v2 288 | .yarn/cache 289 | .yarn/unplugged 290 | .yarn/build-state.yml 291 | .yarn/install-state.gz 292 | .pnp.* 293 | 294 | package-lock.json -------------------------------------------------------------------------------- /voyager/env/__init__.py: -------------------------------------------------------------------------------- 1 | from .bridge import VoyagerEnv 2 | -------------------------------------------------------------------------------- /voyager/env/bridge.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import time 3 | import warnings 4 | from typing import SupportsFloat, Any, Tuple, Dict 5 | 6 | import requests 7 | import json 8 | 9 | import gymnasium as gym 10 | from gymnasium.core import ObsType 11 | 12 | import voyager.utils as U 13 | 14 | from .minecraft_launcher import MinecraftInstance 15 | from .process_monitor import SubprocessMonitor 16 | 17 | 18 | class VoyagerEnv(gym.Env): 19 | def __init__( 20 | self, 21 | mc_port=None, 22 | azure_login=None, 23 | server_host="http://127.0.0.1", 24 | server_port=3000, 25 | request_timeout=600, 26 | log_path="./logs", 27 | ): 28 | if not mc_port and not azure_login: 29 | raise ValueError("Either mc_port or azure_login must be specified") 30 | if mc_port and azure_login: 31 | warnings.warn( 32 | "Both mc_port and mc_login are specified, mc_port will be ignored" 33 | ) 34 | self.mc_port = mc_port 35 | self.azure_login = azure_login 36 | self.server = f"{server_host}:{server_port}" 37 | self.server_port = server_port 38 | self.request_timeout = request_timeout 39 | self.log_path = log_path 40 | self.mineflayer = self.get_mineflayer_process(server_port) 41 | if azure_login: 42 | self.mc_instance = self.get_mc_instance() 43 | else: 44 | self.mc_instance = None 45 | self.has_reset = False 46 | self.reset_options = None 47 | self.connected = False 48 | self.server_paused = False 49 | 50 | def get_mineflayer_process(self, server_port): 51 | U.f_mkdir(self.log_path, "mineflayer") 52 | file_path = os.path.abspath(os.path.dirname(__file__)) 53 | return SubprocessMonitor( 54 | commands=[ 55 | "node", 56 | U.f_join(file_path, "mineflayer/index.js"), 57 | str(server_port), 58 | ], 59 | name="mineflayer", 60 | ready_match=r"Server started on port (\d+)", 61 | log_path=U.f_join(self.log_path, "mineflayer"), 62 | ) 63 | 64 | def get_mc_instance(self): 65 | print("Creating Minecraft server") 66 | U.f_mkdir(self.log_path, "minecraft") 67 | return MinecraftInstance( 68 | **self.azure_login, 69 | mineflayer=self.mineflayer, 70 | log_path=U.f_join(self.log_path, "minecraft"), 71 | ) 72 | 73 | def check_process(self): 74 | if self.mc_instance and not self.mc_instance.is_running: 75 | # if self.mc_instance: 76 | # self.mc_instance.check_process() 77 | # if not self.mc_instance.is_running: 78 | print("Starting Minecraft server") 79 | self.mc_instance.run() 80 | self.mc_port = self.mc_instance.port 81 | self.reset_options["port"] = self.mc_instance.port 82 | print(f"Server started on port {self.reset_options['port']}") 83 | retry = 0 84 | while not self.mineflayer.is_running: 85 | print("Mineflayer process has exited, restarting") 86 | self.mineflayer.run() 87 | if not self.mineflayer.is_running: 88 | if retry > 3: 89 | raise RuntimeError("Mineflayer process failed to start") 90 | else: 91 | continue 92 | print(self.mineflayer.ready_line) 93 | res = requests.post( 94 | f"{self.server}/start", 95 | json=self.reset_options, 96 | timeout=self.request_timeout, 97 | ) 98 | if res.status_code != 200: 99 | self.mineflayer.stop() 100 | raise RuntimeError( 101 | f"Minecraft server reply with code {res.status_code}" 102 | ) 103 | return res.json() 104 | 105 | def step( 106 | self, 107 | code: str, 108 | programs: str = "", 109 | ) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]: 110 | if not self.has_reset: 111 | raise RuntimeError("Environment has not been reset yet") 112 | self.check_process() 113 | self.unpause() 114 | data = { 115 | "code": code, 116 | "programs": programs, 117 | } 118 | res = requests.post( 119 | f"{self.server}/step", json=data, timeout=self.request_timeout 120 | ) 121 | if res.status_code != 200: 122 | raise RuntimeError("Failed to step Minecraft server") 123 | returned_data = res.json() 124 | self.pause() 125 | return json.loads(returned_data) 126 | 127 | def render(self): 128 | raise NotImplementedError("render is not implemented") 129 | 130 | def reset( 131 | self, 132 | *, 133 | seed=None, 134 | options=None, 135 | ) -> Tuple[ObsType, Dict[str, Any]]: 136 | if options is None: 137 | options = {} 138 | 139 | if options.get("inventory", {}) and options.get("mode", "hard") != "hard": 140 | raise RuntimeError("inventory can only be set when options is hard") 141 | 142 | self.reset_options = { 143 | "port": self.mc_port, 144 | "reset": options.get("mode", "hard"), 145 | "inventory": options.get("inventory", {}), 146 | "equipment": options.get("equipment", []), 147 | "spread": options.get("spread", False), 148 | "waitTicks": options.get("wait_ticks", 5), 149 | "position": options.get("position", None), 150 | } 151 | 152 | self.unpause() 153 | self.mineflayer.stop() 154 | time.sleep(1) # wait for mineflayer to exit 155 | 156 | returned_data = self.check_process() 157 | self.has_reset = True 158 | self.connected = True 159 | # All the reset in step will be soft 160 | self.reset_options["reset"] = "soft" 161 | self.pause() 162 | return json.loads(returned_data) 163 | 164 | def close(self): 165 | self.unpause() 166 | if self.connected: 167 | res = requests.post(f"{self.server}/stop") 168 | if res.status_code == 200: 169 | self.connected = False 170 | if self.mc_instance: 171 | self.mc_instance.stop() 172 | self.mineflayer.stop() 173 | return not self.connected 174 | 175 | def pause(self): 176 | if self.mineflayer.is_running and not self.server_paused: 177 | res = requests.post(f"{self.server}/pause") 178 | if res.status_code == 200: 179 | self.server_paused = True 180 | return self.server_paused 181 | 182 | def unpause(self): 183 | if self.mineflayer.is_running and self.server_paused: 184 | res = requests.post(f"{self.server}/pause") 185 | if res.status_code == 200: 186 | self.server_paused = False 187 | else: 188 | print(res.json()) 189 | return self.server_paused 190 | -------------------------------------------------------------------------------- /voyager/env/minecraft_launcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import minecraft_launcher_lib 5 | import sys 6 | import voyager.utils as U 7 | 8 | from .process_monitor import SubprocessMonitor 9 | 10 | 11 | class MinecraftInstance: 12 | def __init__( 13 | self, 14 | client_id, 15 | redirect_url, 16 | secret_value, 17 | version, 18 | mineflayer, 19 | log_path="logs", 20 | ): 21 | self.client_id = client_id 22 | self.redirect_url = redirect_url 23 | self.secret_value = secret_value 24 | self.version = version 25 | self.log_path = log_path 26 | self.mc_dir = minecraft_launcher_lib.utils.get_minecraft_directory() 27 | self.port = None 28 | 29 | def stop_mineflayer(): 30 | print("Stopping mineflayer") 31 | try: 32 | mineflayer.stop() 33 | except Exception as e: 34 | print(e) 35 | 36 | self.mc_command = self.get_mc_command() 37 | self.mc_process = SubprocessMonitor( 38 | commands=self.mc_command, 39 | name="minecraft", 40 | ready_match=r"Started serving on (\d+)", 41 | log_path=self.log_path, 42 | callback=stop_mineflayer, 43 | callback_match=r"\[Server thread/INFO\]: bot left the game", 44 | finished_callback=stop_mineflayer, 45 | ) 46 | 47 | def get_mineflayer_process(self, server_port): 48 | U.f_mkdir(self.log_path, "../mineflayer") 49 | file_path = os.path.abspath(os.path.dirname(__file__)) 50 | return SubprocessMonitor( 51 | commands=[ 52 | "node", 53 | U.f_join(file_path, "mineflayer/index.js"), 54 | str(server_port), 55 | ], 56 | name="mineflayer", 57 | ready_match=r"Server started on port (\d+)", 58 | log_path=U.f_join(self.log_path, "mineflayer"), 59 | ) 60 | 61 | def get_mc_command(self): 62 | file_path = os.path.abspath(os.path.dirname(__file__)) 63 | if not U.f_exists(file_path, "config.json"): 64 | ( 65 | login_url, 66 | state, 67 | code_verifier, 68 | ) = minecraft_launcher_lib.microsoft_account.get_secure_login_data( 69 | self.client_id, self.redirect_url 70 | ) 71 | print( 72 | f"Please open {login_url} in your browser and copy the url you are redirected into the prompt below." 73 | ) 74 | code_url = input() 75 | 76 | try: 77 | auth_code = ( 78 | minecraft_launcher_lib.microsoft_account.parse_auth_code_url( 79 | code_url, state 80 | ) 81 | ) 82 | except AssertionError: 83 | print("States do not match!") 84 | sys.exit(1) 85 | except KeyError: 86 | print("Url not valid") 87 | sys.exit(1) 88 | 89 | login_data = minecraft_launcher_lib.microsoft_account.complete_login( 90 | self.client_id, 91 | self.secret_value, 92 | self.redirect_url, 93 | auth_code, 94 | code_verifier, 95 | ) 96 | 97 | options = { 98 | "username": login_data["name"], 99 | "uuid": login_data["id"], 100 | "token": login_data["access_token"], 101 | } 102 | U.json_dump(options, file_path, "config.json") 103 | print(f"Login success, save to {U.f_join(file_path, 'config.json')}") 104 | 105 | options = U.json_load(file_path, "config.json") 106 | mc_command = minecraft_launcher_lib.command.get_minecraft_command( 107 | self.version, self.mc_dir, options 108 | ) 109 | 110 | return mc_command 111 | 112 | def run(self): 113 | self.mc_process.run() 114 | pattern = r"Started serving on (\d+)" 115 | match = re.search(pattern, self.mc_process.ready_line) 116 | if match: 117 | self.port = int(match.group(1)) 118 | print("The mc server is listening on port", self.port) 119 | else: 120 | raise RuntimeError("Port not found") 121 | 122 | def stop(self): 123 | self.mc_process.stop() 124 | 125 | @property 126 | def is_running(self): 127 | return self.mc_process.is_running 128 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage -------------------------------------------------------------------------------- /voyager/env/mineflayer/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const express = require("express"); 3 | const bodyParser = require("body-parser"); 4 | const mineflayer = require("mineflayer"); 5 | 6 | const skills = require("./lib/skillLoader"); 7 | const { initCounter, getNextTime } = require("./lib/utils"); 8 | const obs = require("./lib/observation/base"); 9 | const OnChat = require("./lib/observation/onChat"); 10 | const OnError = require("./lib/observation/onError"); 11 | const { Voxels, BlockRecords } = require("./lib/observation/voxels"); 12 | const Status = require("./lib/observation/status"); 13 | const Inventory = require("./lib/observation/inventory"); 14 | const OnSave = require("./lib/observation/onSave"); 15 | const Chests = require("./lib/observation/chests"); 16 | const { plugin: tool } = require("mineflayer-tool"); 17 | 18 | let bot = null; 19 | 20 | const app = express(); 21 | 22 | app.use(bodyParser.json({ limit: "50mb" })); 23 | app.use(bodyParser.urlencoded({ limit: "50mb", extended: false })); 24 | 25 | app.post("/start", (req, res) => { 26 | if (bot) onDisconnect("Restarting bot"); 27 | bot = null; 28 | console.log(req.body); 29 | bot = mineflayer.createBot({ 30 | host: "localhost", // minecraft server ip 31 | port: req.body.port, // minecraft server port 32 | username: "bot", 33 | disableChatSigning: true, 34 | checkTimeoutInterval: 60 * 60 * 1000, 35 | }); 36 | bot.once("error", onConnectionFailed); 37 | 38 | // Event subscriptions 39 | bot.waitTicks = req.body.waitTicks; 40 | bot.globalTickCounter = 0; 41 | bot.stuckTickCounter = 0; 42 | bot.stuckPosList = []; 43 | bot.iron_pickaxe = false; 44 | 45 | bot.on("kicked", onDisconnect); 46 | 47 | // mounting will cause physicsTick to stop 48 | bot.on("mount", () => { 49 | bot.dismount(); 50 | }); 51 | 52 | bot.once("spawn", async () => { 53 | bot.removeListener("error", onConnectionFailed); 54 | let itemTicks = 1; 55 | if (req.body.reset === "hard") { 56 | bot.chat("/clear @s"); 57 | bot.chat("/kill @s"); 58 | const inventory = req.body.inventory ? req.body.inventory : {}; 59 | const equipment = req.body.equipment 60 | ? req.body.equipment 61 | : [null, null, null, null, null, null]; 62 | for (let key in inventory) { 63 | bot.chat(`/give @s minecraft:${key} ${inventory[key]}`); 64 | itemTicks += 1; 65 | } 66 | const equipmentNames = [ 67 | "armor.head", 68 | "armor.chest", 69 | "armor.legs", 70 | "armor.feet", 71 | "weapon.mainhand", 72 | "weapon.offhand", 73 | ]; 74 | for (let i = 0; i < 6; i++) { 75 | if (i === 4) continue; 76 | if (equipment[i]) { 77 | bot.chat( 78 | `/item replace entity @s ${equipmentNames[i]} with minecraft:${equipment[i]}` 79 | ); 80 | itemTicks += 1; 81 | } 82 | } 83 | } 84 | 85 | if (req.body.position) { 86 | bot.chat( 87 | `/tp @s ${req.body.position.x} ${req.body.position.y} ${req.body.position.z}` 88 | ); 89 | } 90 | 91 | // if iron_pickaxe is in bot's inventory 92 | if ( 93 | bot.inventory.items().find((item) => item.name === "iron_pickaxe") 94 | ) { 95 | bot.iron_pickaxe = true; 96 | } 97 | 98 | const { pathfinder } = require("mineflayer-pathfinder"); 99 | const tool = require("mineflayer-tool").plugin; 100 | const collectBlock = require("mineflayer-collectblock").plugin; 101 | const pvp = require("mineflayer-pvp").plugin; 102 | const minecraftHawkEye = require("minecrafthawkeye"); 103 | bot.loadPlugin(pathfinder); 104 | bot.loadPlugin(tool); 105 | bot.loadPlugin(collectBlock); 106 | bot.loadPlugin(pvp); 107 | bot.loadPlugin(minecraftHawkEye); 108 | 109 | // bot.collectBlock.movements.digCost = 0; 110 | // bot.collectBlock.movements.placeCost = 0; 111 | 112 | obs.inject(bot, [ 113 | OnChat, 114 | OnError, 115 | Voxels, 116 | Status, 117 | Inventory, 118 | OnSave, 119 | Chests, 120 | BlockRecords, 121 | ]); 122 | skills.inject(bot); 123 | 124 | if (req.body.spread) { 125 | bot.chat(`/spreadplayers ~ ~ 0 300 under 80 false @s`); 126 | await bot.waitForTicks(bot.waitTicks); 127 | } 128 | 129 | await bot.waitForTicks(bot.waitTicks * itemTicks); 130 | res.json(bot.observe()); 131 | 132 | initCounter(bot); 133 | bot.chat("/gamerule keepInventory true"); 134 | bot.chat("/gamerule doDaylightCycle false"); 135 | }); 136 | 137 | function onConnectionFailed(e) { 138 | console.log(e); 139 | bot = null; 140 | res.status(400).json({ error: e }); 141 | } 142 | function onDisconnect(message) { 143 | if (bot.viewer) { 144 | bot.viewer.close(); 145 | } 146 | bot.end(); 147 | console.log(message); 148 | bot = null; 149 | } 150 | }); 151 | 152 | app.post("/step", async (req, res) => { 153 | // import useful package 154 | let response_sent = false; 155 | function otherError(err) { 156 | console.log("Uncaught Error"); 157 | bot.emit("error", handleError(err)); 158 | bot.waitForTicks(bot.waitTicks).then(() => { 159 | if (!response_sent) { 160 | response_sent = true; 161 | res.json(bot.observe()); 162 | } 163 | }); 164 | } 165 | 166 | process.on("uncaughtException", otherError); 167 | 168 | const mcData = require("minecraft-data")(bot.version); 169 | mcData.itemsByName["leather_cap"] = mcData.itemsByName["leather_helmet"]; 170 | mcData.itemsByName["leather_tunic"] = 171 | mcData.itemsByName["leather_chestplate"]; 172 | mcData.itemsByName["leather_pants"] = 173 | mcData.itemsByName["leather_leggings"]; 174 | mcData.itemsByName["leather_boots"] = mcData.itemsByName["leather_boots"]; 175 | mcData.itemsByName["lapis_lazuli_ore"] = mcData.itemsByName["lapis_ore"]; 176 | mcData.blocksByName["lapis_lazuli_ore"] = mcData.blocksByName["lapis_ore"]; 177 | const { 178 | Movements, 179 | goals: { 180 | Goal, 181 | GoalBlock, 182 | GoalNear, 183 | GoalXZ, 184 | GoalNearXZ, 185 | GoalY, 186 | GoalGetToBlock, 187 | GoalLookAtBlock, 188 | GoalBreakBlock, 189 | GoalCompositeAny, 190 | GoalCompositeAll, 191 | GoalInvert, 192 | GoalFollow, 193 | GoalPlaceBlock, 194 | }, 195 | pathfinder, 196 | Move, 197 | ComputedPath, 198 | PartiallyComputedPath, 199 | XZCoordinates, 200 | XYZCoordinates, 201 | SafeBlock, 202 | GoalPlaceBlockOptions, 203 | } = require("mineflayer-pathfinder"); 204 | const { Vec3 } = require("vec3"); 205 | 206 | // Set up pathfinder 207 | const movements = new Movements(bot, mcData); 208 | bot.pathfinder.setMovements(movements); 209 | 210 | bot.globalTickCounter = 0; 211 | bot.stuckTickCounter = 0; 212 | bot.stuckPosList = []; 213 | 214 | function onTick() { 215 | bot.globalTickCounter++; 216 | if (bot.pathfinder.isMoving()) { 217 | bot.stuckTickCounter++; 218 | if (bot.stuckTickCounter >= 100) { 219 | onStuck(1.5); 220 | bot.stuckTickCounter = 0; 221 | } 222 | } 223 | } 224 | 225 | bot.on("physicTick", onTick); 226 | 227 | // initialize fail count 228 | let _craftItemFailCount = 0; 229 | let _killMobFailCount = 0; 230 | let _mineBlockFailCount = 0; 231 | let _placeItemFailCount = 0; 232 | let _smeltItemFailCount = 0; 233 | 234 | // Retrieve array form post bod 235 | const code = req.body.code; 236 | const programs = req.body.programs; 237 | bot.cumulativeObs = []; 238 | await bot.waitForTicks(bot.waitTicks); 239 | const r = await evaluateCode(code, programs); 240 | process.off("uncaughtException", otherError); 241 | if (r !== "success") { 242 | bot.emit("error", handleError(r)); 243 | } 244 | await returnItems(); 245 | // wait for last message 246 | await bot.waitForTicks(bot.waitTicks); 247 | if (!response_sent) { 248 | response_sent = true; 249 | res.json(bot.observe()); 250 | } 251 | bot.removeListener("physicTick", onTick); 252 | 253 | async function evaluateCode(code, programs) { 254 | // Echo the code produced for players to see it. Don't echo when the bot code is already producing dialog or it will double echo 255 | try { 256 | await eval("(async () => {" + programs + "\n" + code + "})()"); 257 | return "success"; 258 | } catch (err) { 259 | return err; 260 | } 261 | } 262 | 263 | function onStuck(posThreshold) { 264 | const currentPos = bot.entity.position; 265 | bot.stuckPosList.push(currentPos); 266 | 267 | // Check if the list is full 268 | if (bot.stuckPosList.length === 5) { 269 | const oldestPos = bot.stuckPosList[0]; 270 | const posDifference = currentPos.distanceTo(oldestPos); 271 | 272 | if (posDifference < posThreshold) { 273 | teleportBot(); // execute the function 274 | } 275 | 276 | // Remove the oldest time from the list 277 | bot.stuckPosList.shift(); 278 | } 279 | } 280 | 281 | function teleportBot() { 282 | const blocks = bot.findBlocks({ 283 | matching: (block) => { 284 | return block.type === 0; 285 | }, 286 | maxDistance: 1, 287 | count: 27, 288 | }); 289 | 290 | if (blocks) { 291 | // console.log(blocks.length); 292 | const randomIndex = Math.floor(Math.random() * blocks.length); 293 | const block = blocks[randomIndex]; 294 | bot.chat(`/tp @s ${block.x} ${block.y} ${block.z}`); 295 | } else { 296 | bot.chat("/tp @s ~ ~1.25 ~"); 297 | } 298 | } 299 | 300 | function returnItems() { 301 | bot.chat("/gamerule doTileDrops false"); 302 | const crafting_table = bot.findBlock({ 303 | matching: mcData.blocksByName.crafting_table.id, 304 | maxDistance: 128, 305 | }); 306 | if (crafting_table) { 307 | bot.chat( 308 | `/setblock ${crafting_table.position.x} ${crafting_table.position.y} ${crafting_table.position.z} air destroy` 309 | ); 310 | bot.chat("/give @s crafting_table"); 311 | } 312 | const furnace = bot.findBlock({ 313 | matching: mcData.blocksByName.furnace.id, 314 | maxDistance: 128, 315 | }); 316 | if (furnace) { 317 | bot.chat( 318 | `/setblock ${furnace.position.x} ${furnace.position.y} ${furnace.position.z} air destroy` 319 | ); 320 | bot.chat("/give @s furnace"); 321 | } 322 | if (bot.inventoryUsed() >= 32) { 323 | // if chest is not in bot's inventory 324 | if (!bot.inventory.items().find((item) => item.name === "chest")) { 325 | bot.chat("/give @s chest"); 326 | } 327 | } 328 | // if iron_pickaxe not in bot's inventory and bot.iron_pickaxe 329 | if ( 330 | bot.iron_pickaxe && 331 | !bot.inventory.items().find((item) => item.name === "iron_pickaxe") 332 | ) { 333 | bot.chat("/give @s iron_pickaxe"); 334 | } 335 | bot.chat("/gamerule doTileDrops true"); 336 | } 337 | 338 | function handleError(err) { 339 | let stack = err.stack; 340 | if (!stack) { 341 | return err; 342 | } 343 | console.log(stack); 344 | const final_line = stack.split("\n")[1]; 345 | const regex = /:(\d+):\d+\)/; 346 | 347 | const programs_length = programs.split("\n").length; 348 | let match_line = null; 349 | for (const line of stack.split("\n")) { 350 | const match = regex.exec(line); 351 | if (match) { 352 | const line_num = parseInt(match[1]); 353 | if (line_num >= programs_length) { 354 | match_line = line_num - programs_length; 355 | break; 356 | } 357 | } 358 | } 359 | if (!match_line) { 360 | return err.message; 361 | } 362 | let f_line = final_line.match( 363 | /\((?.*):(?\d+):(?\d+)\)/ 364 | ); 365 | if (f_line && f_line.groups && fs.existsSync(f_line.groups.file)) { 366 | const { file, line, pos } = f_line.groups; 367 | const f = fs.readFileSync(file, "utf8").split("\n"); 368 | // let filename = file.match(/(?<=node_modules\\)(.*)/)[1]; 369 | let source = file + `:${line}\n${f[line - 1].trim()}\n `; 370 | 371 | const code_source = 372 | "at " + 373 | code.split("\n")[match_line - 1].trim() + 374 | " in your code"; 375 | return source + err.message + "\n" + code_source; 376 | } else if ( 377 | f_line && 378 | f_line.groups && 379 | f_line.groups.file.includes("") 380 | ) { 381 | const { file, line, pos } = f_line.groups; 382 | let source = 383 | "Your code" + 384 | `:${match_line}\n${code.split("\n")[match_line - 1].trim()}\n `; 385 | let code_source = ""; 386 | if (line < programs_length) { 387 | source = 388 | "In your program code: " + 389 | programs.split("\n")[line - 1].trim() + 390 | "\n"; 391 | code_source = `at line ${match_line}:${code 392 | .split("\n") 393 | [match_line - 1].trim()} in your code`; 394 | } 395 | return source + err.message + "\n" + code_source; 396 | } 397 | return err.message; 398 | } 399 | }); 400 | 401 | app.post("/stop", (req, res) => { 402 | bot.end(); 403 | res.json({ 404 | message: "Bot stopped", 405 | }); 406 | }); 407 | 408 | app.post("/pause", (req, res) => { 409 | if (!bot) { 410 | res.status(400).json({ error: "Bot not spawned" }); 411 | return; 412 | } 413 | bot.chat("/pause"); 414 | bot.waitForTicks(bot.waitTicks).then(() => { 415 | res.json({ message: "Success" }); 416 | }); 417 | }); 418 | 419 | // Server listening to PORT 3000 420 | 421 | const DEFAULT_PORT = 3000; 422 | const PORT = process.argv[2] || DEFAULT_PORT; 423 | app.listen(PORT, () => { 424 | console.log(`Server started on port ${PORT}`); 425 | }); 426 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/base.js: -------------------------------------------------------------------------------- 1 | class Observation { 2 | constructor(bot) { 3 | if (new.target === Observation) { 4 | throw new TypeError( 5 | "Cannot instantiate abstract class Observation" 6 | ); 7 | } 8 | 9 | this.bot = bot; 10 | this.name = "Observation"; 11 | } 12 | 13 | observe() { 14 | throw new TypeError("Method 'observe()' must be implemented."); 15 | } 16 | 17 | reset() {} 18 | } 19 | 20 | function inject(bot, obs_list) { 21 | bot.obsList = []; 22 | bot.cumulativeObs = []; 23 | bot.eventMemory = {}; 24 | obs_list.forEach((obs) => { 25 | bot.obsList.push(new obs(bot)); 26 | }); 27 | bot.event = function (event_name) { 28 | let result = {}; 29 | bot.obsList.forEach((obs) => { 30 | if (obs.name.startsWith("on") && obs.name !== event_name) { 31 | return; 32 | } 33 | result[obs.name] = obs.observe(); 34 | }); 35 | bot.cumulativeObs.push([event_name, result]); 36 | }; 37 | bot.observe = function () { 38 | bot.event("observe"); 39 | const result = bot.cumulativeObs; 40 | bot.cumulativeObs = []; 41 | return JSON.stringify(result); 42 | }; 43 | } 44 | 45 | module.exports = { Observation, inject }; 46 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/chests.js: -------------------------------------------------------------------------------- 1 | const { Observation } = require("./base"); 2 | 3 | class Chests extends Observation { 4 | constructor(bot) { 5 | super(bot); 6 | this.name = "nearbyChests"; 7 | this.chestsItems = {}; 8 | bot.on("closeChest", (chestItems, position) => { 9 | this.chestsItems[position] = chestItems; 10 | }); 11 | bot.on("removeChest", (chestPosition) => { 12 | delete this.chestsItems[chestPosition]; 13 | }); 14 | } 15 | 16 | observe() { 17 | const chests = this.bot.findBlocks({ 18 | matching: this.bot.registry.blocksByName.chest.id, 19 | maxDistance: 16, 20 | count: 999, 21 | }); 22 | chests.forEach((chest) => { 23 | if (!this.chestsItems.hasOwnProperty(chest)) { 24 | this.chestsItems[chest] = "Unknown"; 25 | } 26 | }); 27 | return this.chestsItems; 28 | } 29 | } 30 | 31 | module.exports = Chests; 32 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/inventory.js: -------------------------------------------------------------------------------- 1 | const { Observation } = require("./base"); 2 | 3 | class Inventory extends Observation { 4 | constructor(bot) { 5 | super(bot); 6 | this.name = "inventory"; 7 | } 8 | 9 | observe() { 10 | return listItems(this.bot); 11 | } 12 | } 13 | 14 | function listItems(bot) { 15 | const items = getInventoryItems(bot); 16 | return items.reduce(itemToDict, {}); 17 | } 18 | 19 | function getInventoryItems(bot) { 20 | const inventory = bot.currentWindow || bot.inventory; 21 | return inventory.items(); 22 | } 23 | 24 | function itemToDict(acc, cur) { 25 | if (cur.name && cur.count) { 26 | //if both name and count property are defined 27 | if (acc[cur.name]) { 28 | //if the item is already in the dict 29 | acc[cur.name] += cur.count; 30 | } else { 31 | //if the item is not in the dict 32 | acc[cur.name] = cur.count; 33 | } 34 | } 35 | return acc; 36 | } 37 | 38 | //export modules 39 | module.exports = Inventory; 40 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/onChat.js: -------------------------------------------------------------------------------- 1 | const Observation = require("./base.js").Observation; 2 | 3 | class onChat extends Observation { 4 | constructor(bot) { 5 | super(bot); 6 | this.name = "onChat"; 7 | this.obs = ""; 8 | bot.on("chatEvent", (username, message) => { 9 | // Save entity status to local variable 10 | if (message.startsWith("/")) { 11 | return; 12 | } 13 | 14 | this.obs += message; 15 | this.bot.event(this.name); 16 | }); 17 | } 18 | 19 | observe() { 20 | const result = this.obs; 21 | this.obs = ""; 22 | return result; 23 | } 24 | } 25 | 26 | module.exports = onChat; 27 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/onError.js: -------------------------------------------------------------------------------- 1 | const Observation = require("./base.js").Observation; 2 | 3 | class onError extends Observation { 4 | constructor(bot) { 5 | super(bot); 6 | this.name = "onError"; 7 | this.obs = null; 8 | bot.on("error", (err) => { 9 | // Save entity status to local variable 10 | this.obs = err; 11 | this.bot.event(this.name); 12 | }); 13 | } 14 | 15 | observe() { 16 | const result = this.obs; 17 | this.obs = null; 18 | return result; 19 | } 20 | } 21 | 22 | module.exports = onError; 23 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/onSave.js: -------------------------------------------------------------------------------- 1 | const Observation = require("./base.js").Observation; 2 | 3 | class onSave extends Observation { 4 | constructor(bot) { 5 | super(bot); 6 | this.name = "onSave"; 7 | this.obs = null; 8 | bot.on("save", (eventName) => { 9 | // Save entity status to local variable 10 | this.obs = eventName; 11 | this.bot.event(this.name); 12 | }); 13 | } 14 | 15 | observe() { 16 | const result = this.obs; 17 | this.obs = null; 18 | return result; 19 | } 20 | } 21 | 22 | module.exports = onSave; 23 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/status.js: -------------------------------------------------------------------------------- 1 | const Observation = require("./base.js").Observation; 2 | 3 | class Status extends Observation { 4 | constructor(bot) { 5 | super(bot); 6 | this.name = "status"; 7 | } 8 | 9 | observe() { 10 | return { 11 | health: this.bot.health, 12 | food: this.bot.food, 13 | saturation: this.bot.foodSaturation, 14 | oxygen: this.bot.oxygenLevel, 15 | position: this.bot.entity.position, 16 | velocity: this.bot.entity.velocity, 17 | yaw: this.bot.entity.yaw, 18 | pitch: this.bot.entity.pitch, 19 | onGround: this.bot.entity.onGround, 20 | equipment: this.getEquipment(), 21 | name: this.bot.entity.username, 22 | timeSinceOnGround: this.bot.entity.timeSinceOnGround, 23 | isInWater: this.bot.entity.isInWater, 24 | isInLava: this.bot.entity.isInLava, 25 | isInWeb: this.bot.entity.isInWeb, 26 | isCollidedHorizontally: this.bot.entity.isCollidedHorizontally, 27 | isCollidedVertically: this.bot.entity.isCollidedVertically, 28 | biome: this.bot.blockAt(this.bot.entity.position) 29 | ? this.bot.blockAt(this.bot.entity.position).biome.name 30 | : "None", 31 | entities: this.getEntities(), 32 | timeOfDay: this.getTime(), 33 | inventoryUsed: this.bot.inventoryUsed(), 34 | elapsedTime: this.bot.globalTickCounter, 35 | }; 36 | } 37 | 38 | itemToObs(item) { 39 | if (!item) return null; 40 | return item.name; 41 | } 42 | 43 | getTime() { 44 | const timeOfDay = this.bot.time.timeOfDay; 45 | let time = ""; 46 | if (timeOfDay < 1000) { 47 | time = "sunrise"; 48 | } else if (timeOfDay < 6000) { 49 | time = "day"; 50 | } else if (timeOfDay < 12000) { 51 | time = "noon"; 52 | } else if (timeOfDay < 13000) { 53 | time = "sunset"; 54 | } else if (timeOfDay < 18000) { 55 | time = "night"; 56 | } else if (timeOfDay < 22000) { 57 | time = "midnight"; 58 | } else { 59 | time = "sunrise"; 60 | } 61 | return time; 62 | } 63 | 64 | // For each item in equipment, if it exists, return the name of the item 65 | // otherwise return null 66 | getEquipment() { 67 | const slots = this.bot.inventory.slots; 68 | const mainHand = this.bot.heldItem; 69 | return slots 70 | .slice(5, 9) 71 | .concat(mainHand, slots[45]) 72 | .map(this.itemToObs); 73 | } 74 | 75 | getEntities() { 76 | const entities = this.bot.entities; 77 | if (!entities) return {}; 78 | // keep all monsters in one list, keep other mobs in another list 79 | const mobs = {}; 80 | for (const id in entities) { 81 | const entity = entities[id]; 82 | if (!entity.displayName) continue; 83 | if (entity.name === "player" || entity.name === "item") continue; 84 | if (entity.position.distanceTo(this.bot.entity.position) < 32) { 85 | if (!mobs[entity.name]) { 86 | mobs[entity.name] = entity.position.distanceTo( 87 | this.bot.entity.position 88 | ); 89 | } else if ( 90 | mobs[entity.name] > 91 | entity.position.distanceTo(this.bot.entity.position) 92 | ) { 93 | mobs[entity.name] = entity.position.distanceTo( 94 | this.bot.entity.position 95 | ); 96 | } 97 | } 98 | } 99 | return mobs; 100 | } 101 | } 102 | 103 | module.exports = Status; 104 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/observation/voxels.js: -------------------------------------------------------------------------------- 1 | // Blocks = require("./blocks") 2 | const { Observation } = require("./base"); 3 | 4 | class Voxels extends Observation { 5 | constructor(bot) { 6 | super(bot); 7 | this.name = "voxels"; 8 | } 9 | 10 | observe() { 11 | return Array.from(getSurroundingBlocks(this.bot, 8, 2, 8)); 12 | } 13 | } 14 | 15 | class BlockRecords extends Observation { 16 | constructor(bot) { 17 | super(bot); 18 | this.name = "blockRecords"; 19 | this.records = new Set(); 20 | this.tick = 0; 21 | bot.on("physicsTick", () => { 22 | this.tick++; 23 | if (this.tick >= 100) { 24 | const items = getInventoryItems(this.bot); 25 | getSurroundingBlocks(this.bot, 8, 2, 8).forEach((block) => { 26 | if (!items.has(block)) this.records.add(block); 27 | }); 28 | this.tick = 0; 29 | } 30 | }); 31 | } 32 | 33 | observe() { 34 | return Array.from(this.records); 35 | } 36 | 37 | reset() { 38 | this.records = new Set(); 39 | } 40 | } 41 | 42 | function getSurroundingBlocks(bot, x_distance, y_distance, z_distance) { 43 | const surroundingBlocks = new Set(); 44 | 45 | for (let x = -x_distance; x <= x_distance; x++) { 46 | for (let y = -y_distance; y <= y_distance; y++) { 47 | for (let z = -z_distance; z <= z_distance; z++) { 48 | const block = bot.blockAt(bot.entity.position.offset(x, y, z)); 49 | if (block && block.type !== 0) { 50 | surroundingBlocks.add(block.name); 51 | } 52 | } 53 | } 54 | } 55 | // console.log(surroundingBlocks); 56 | return surroundingBlocks; 57 | } 58 | 59 | function getInventoryItems(bot) { 60 | const items = new Set(); 61 | bot.inventory.items().forEach((item) => { 62 | if (item) items.add(item.name); 63 | }); 64 | return items; 65 | } 66 | 67 | module.exports = { Voxels, BlockRecords }; 68 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/skillLoader.js: -------------------------------------------------------------------------------- 1 | function inject(bot) { 2 | bot._sleep = bot.sleep; 3 | bot.sleep = async (bedBlock) => { 4 | await bot.waitForTicks(20); 5 | await bot._sleep(bedBlock); 6 | await bot.waitForTicks(135); 7 | }; 8 | 9 | bot._fish = bot.fish; 10 | bot.fish = async () => { 11 | if (bot.heldItem?.name !== "fishing_rod") { 12 | bot.chat("I'm not holding a fishing rod!"); 13 | return; 14 | } 15 | let timeout = null; 16 | await Promise.race([ 17 | bot._fish(), 18 | new Promise( 19 | (resolve, reject) => 20 | (timeout = setTimeout(() => { 21 | bot.activateItem(); 22 | reject( 23 | new Error( 24 | "Finishing timeout, make sure you get to and look at a water block!" 25 | ) 26 | ); 27 | }, 60000)) 28 | ), 29 | ]); 30 | clearTimeout(timeout); 31 | await bot.waitForTicks(20); 32 | }; 33 | 34 | bot._consume = bot.consume; 35 | bot.consume = async () => { 36 | // action_count.activateItem++; 37 | await bot._consume(); 38 | await bot.waitForTicks(20); 39 | }; 40 | 41 | bot._useOn = bot.useOn; 42 | bot.useOn = async (entity) => { 43 | if (entity.position.distanceTo(bot.entity.position) > 6) { 44 | bot.chat("Please goto a place near the entity first!"); 45 | return; 46 | } 47 | await bot._useOn(entity); 48 | await bot.waitForTicks(20); 49 | }; 50 | 51 | bot._activateBlock = bot.activateBlock; 52 | bot.activateBlock = async (block) => { 53 | if (block.position.distanceTo(bot.entity.position) > 6) { 54 | bot.chat("Please goto a place near the block first!"); 55 | return; 56 | } 57 | // action_count.activateBlock++; 58 | await bot._activateBlock(block); 59 | }; 60 | 61 | bot._chat = bot.chat; 62 | bot.chat = (message) => { 63 | // action_count.chat++; 64 | bot.emit("chatEvent", "bot", message); 65 | bot._chat(message); 66 | }; 67 | 68 | bot.inventoryUsed = () => { 69 | return bot.inventory.slots.slice(9, 45).filter((item) => item !== null) 70 | .length; 71 | }; 72 | 73 | bot.save = function (eventName) { 74 | bot.emit("save", eventName); 75 | }; 76 | } 77 | 78 | // export all control_primitives 79 | module.exports = { inject }; 80 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/lib/utils.js: -------------------------------------------------------------------------------- 1 | let gameTimeCounter = 0; 2 | let gameTimeList = []; 3 | const initCounter = (bot) => { 4 | gameTimeList = []; 5 | for (let i = 0; i < 13000; i += 1000) { 6 | gameTimeList.push(i); 7 | } 8 | for (let i = 13000; i < 24000; i += 2000) { 9 | gameTimeList.push(i); 10 | } 11 | const timeOfDay = bot.time.timeOfDay; 12 | for (let i = 0; i < gameTimeList.length; i++) { 13 | if (gameTimeList[i] > timeOfDay) { 14 | gameTimeCounter = i - 1; 15 | break; 16 | } 17 | } 18 | }; 19 | 20 | const getNextTime = () => { 21 | gameTimeCounter++; 22 | if (gameTimeCounter >= gameTimeList.length) { 23 | gameTimeCounter = 0; 24 | } 25 | return gameTimeList[gameTimeCounter]; 26 | }; 27 | 28 | module.exports = { 29 | initCounter, 30 | getNextTime, 31 | }; 32 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | lib/ 107 | package-lock.json 108 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TheDudeFromCI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/README.md: -------------------------------------------------------------------------------- 1 |

mineflayer-collectblock

2 |

A small utility plugin for allowing users to collect blocks using a higher level API.

3 | 4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | --- 14 | ## This is a modified version to better support Voyager 15 | 16 | ## Showcase 17 | 18 | You can see a video of the plugin in action, [here.](https://youtu.be/5T_rcCnNnf4) 19 | The source code of the bot in the video can be seen in the examples folder, [here.](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/examples/collector.js) 20 | 21 | ### Description 22 | 23 | This plugin is a wrapper for mineflayer that allows for easier API usage when collecting blocks or item drops. This plugin is designed to reduce some of the boilerplate code based around the act of pathfinding to a block _(handled by_ ***mineflayer-pathfinder***_)_, selecting the best tool to mine that block _(handled by_ ***mineflayer-tool***_)_, actually mining it, then moving to collect the item drops from that block. This plugin allows for all of that basic concept to be wrapped up into a single API function. 24 | 25 | In addition to the usage above, some additional quality of life features are available in this plugin. These include the ability to automatically deposit items into a chest when the bot's inventory is full, collecting new tools from a chest if the bot doesn't currently have a required tool _(also handled by_ ***mineflayer-tool***_)_, and allowing for queueing of multiple blocks or item drops to the collection task, so they can be processed later. 26 | 27 | ### Getting Started 28 | 29 | This plugin is built using Node and can be installed using: 30 | ```bash 31 | npm install --save mineflayer-collectblock 32 | ``` 33 | 34 | ### Simple Bot 35 | 36 | The brief description goes here. 37 | 38 | ```js 39 | // Create your bot 40 | const mineflayer = require("mineflayer") 41 | const bot = mineflayer.createBot({ 42 | host: 'localhost', 43 | username: 'Player', 44 | }) 45 | let mcData 46 | 47 | // Load collect block 48 | bot.loadPlugin(require('mineflayer-collectblock').plugin) 49 | 50 | async function collectGrass() { 51 | // Find a nearby grass block 52 | const grass = bot.findBlock({ 53 | matching: mcData.blocksByName.grass_block.id, 54 | maxDistance: 64 55 | }) 56 | 57 | if (grass) { 58 | // If we found one, collect it. 59 | try { 60 | await bot.collectBlock.collect(grass) 61 | collectGrass() // Collect another grass block 62 | } catch (err) { 63 | console.log(err) // Handle errors, if any 64 | } 65 | } 66 | } 67 | 68 | // On spawn, start collecting all nearby grass 69 | bot.once('spawn', () => { 70 | mcData = require('minecraft-data')(bot.version) 71 | collectGrass() 72 | }) 73 | ``` 74 | 75 | ### Documentation 76 | 77 | [API](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/docs/api.md) 78 | 79 | [Examples](https://github.com/TheDudeFromCI/mineflayer-collectblock/tree/master/examples) 80 | 81 | ### License 82 | 83 | This project uses the [MIT](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/LICENSE) license. 84 | 85 | ### Contributions 86 | 87 | This project is accepting PRs and Issues. See something you think can be improved? Go for it! Any and all help is highly appreciated! 88 | 89 | For larger changes, it is recommended to discuss these changes in the issues tab before writing any code. It's also preferred to make many smaller PRs than one large one, where applicable. 90 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | Welcome to the *mineflayer-collectblock* API documentation page. 4 | 5 | ## Table of Contents 6 | 7 | - [1. Summary](#1-summary) 8 | - [Properties](#properties) 9 | - [`bot.collectblock.movements: Movements`](#botcollectblockmovements-movements) 10 | - [Functions](#functions) 11 | - [collect](#collect) 12 | - [Options:](#options) 13 | 14 | ## 1. Summary 15 | 16 | The collect block plugin is a utility plugin that can be used to help make collecting blocks and item drops very easy, using only a single API call. No need to worry about pathfinding to the block, selecting the right tool, or moving to pick up the item drop after mining. 17 | 18 | ## Properties 19 | 20 | ### `bot.collectblock.movements: Movements` 21 | 22 | The movements object used by the pathfinder plugin to define the movement configuration. This object is passed to the pathfinder plugin when any API from this plugin is called in order to control how pathfinding should work when collecting the given blocks or item. 23 | 24 | If set to null, the pathfinder plugin movements is not updated. 25 | 26 | Defaults to a new movements object instance. 27 | 28 | ## Functions 29 | 30 | ### collect 31 | 32 | Usage: `bot.collectblock.collect(target: Collectable | Collectable[], options?: CollectOptions, cb: (err?: Error) => void): void` 33 | 34 | Causes the bot to collect the given block, item drop, or list of those. If the target is a block, the bot will move to the block, mine it, and pick up the item drop. If the target is an item drop, the bot will move to the item drop and pick it up. If the target is a list of collectables, the bot will move from target to target in order of closest to furthest and collect each target in turn. 35 | 36 | #### Options: 37 | 38 | * `append: boolean` 39 | 40 | If true, the target(s) will be appended to the existing target list instead of starting a new task. Defaults to false. 41 | 42 | * `ignoreNoPath: boolean` 43 | 44 | If true, errors will not be thrown when a path to the target block cannot be found. The bot will attempt to choose the best available position it can find, instead. Errors are still thrown if the bot cannot interact with the block from it's final location. Defaults to false. 45 | 46 | * `chestLocations: Vec3[]` 47 | 48 | Gets the list of chest locations to use when storing items after the bot's inventory becomes full. If undefined, it defaults to the chest location list on the bot.collectBlock plugin. 49 | 50 | * `itemFilter: ItemFilter` 51 | 52 | When transferring items to a chest, this filter is used to determine what items are allowed to be moved, and what items aren't allowed to be moved. Defaults to the item filter specified on the bot.collectBlock plugin. -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/examples/collector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This bot example show how to direct a bot to collect a specific block type 3 | * or a group of nearby blocks of that type. 4 | */ 5 | 6 | const mineflayer = require('mineflayer') 7 | const collectBlock = require('mineflayer-collectblock').plugin 8 | 9 | if (process.argv.length < 4 || process.argv.length > 6) { 10 | console.log('Usage : node collector.js [] []') 11 | process.exit(1) 12 | } 13 | 14 | const bot = mineflayer.createBot({ 15 | host: process.argv[2], 16 | port: process.argv[3], 17 | username: process.argv[4] || 'collector', 18 | password: process.argv[5] 19 | }) 20 | 21 | bot.loadPlugin(collectBlock) 22 | 23 | let mcData 24 | bot.once('spawn', () => { 25 | mcData = require('minecraft-data')(bot.version) 26 | }) 27 | 28 | bot.on('chat', async (username, message) => { 29 | const args = message.split(' ') 30 | if (args[0] !== 'collect') return 31 | 32 | let count = 1 33 | if (args.length === 3) count = parseInt(args[1]) 34 | 35 | let type = args[1] 36 | if (args.length === 3) type = args[2] 37 | 38 | const blockType = mcData.blocksByName[type] 39 | if (!blockType) { 40 | return 41 | } 42 | 43 | const blocks = bot.findBlocks({ 44 | matching: blockType.id, 45 | maxDistance: 64, 46 | count: count 47 | }) 48 | 49 | if (blocks.length === 0) { 50 | bot.chat("I don't see that block nearby.") 51 | return 52 | } 53 | 54 | const targets = [] 55 | for (let i = 0; i < Math.min(blocks.length, count); i++) { 56 | targets.push(bot.blockAt(blocks[i])) 57 | } 58 | 59 | bot.chat(`Found ${targets.length} ${type}(s)`) 60 | 61 | try { 62 | await bot.collectBlock.collect(targets) 63 | // All blocks have been collected. 64 | bot.chat('Done') 65 | } catch (err) { 66 | // An error occurred, report it. 67 | bot.chat(err.message) 68 | console.log(err) 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/examples/oreMiner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This bot example shows how to collect a vein of ores quickly after only finding a single block. 3 | * This makes it easy to collect a vein of ores or mine a tree without looking for every block in the 4 | * area. 5 | */ 6 | 7 | const mineflayer = require('mineflayer') 8 | const collectBlock = require('mineflayer-collectblock').plugin 9 | 10 | if (process.argv.length < 4 || process.argv.length > 6) { 11 | console.log('Usage : node oreMiner.js [] []') 12 | process.exit(1) 13 | } 14 | 15 | const bot = mineflayer.createBot({ 16 | host: process.argv[2], 17 | port: process.argv[3], 18 | username: process.argv[4] || 'oreMiner', 19 | password: process.argv[5] 20 | }) 21 | 22 | bot.loadPlugin(collectBlock) 23 | 24 | let mcData 25 | bot.once('spawn', () => { 26 | mcData = require('minecraft-data')(bot.version) 27 | }) 28 | 29 | bot.on('chat', async (username, message) => { 30 | const args = message.split(' ') 31 | if (args[0] !== 'collect') return 32 | 33 | const blockType = mcData.blocksByName[args[1]] 34 | if (!blockType) { 35 | bot.chat(`I don't know any blocks named ${args[1]}.`) 36 | return 37 | } 38 | 39 | const block = bot.findBlock({ 40 | matching: blockType.id, 41 | maxDistance: 64 42 | }) 43 | 44 | if (!block) { 45 | bot.chat("I don't see that block nearby.") 46 | return 47 | } 48 | 49 | const targets = bot.collectBlock.findFromVein(block) 50 | try { 51 | await bot.collectBlock.collect(targets) 52 | // All blocks have been collected. 53 | bot.chat('Done') 54 | } catch (err) { 55 | // An error occurred, report it. 56 | bot.chat(err.message) 57 | console.log(err) 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/examples/storageBot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This bot example shows how to use the chest filling mechanic of the plugin. 3 | * Simply provide a given storage chest, and the bot will automatically try and 4 | * store it's inventory in that chest when the bot's inventory becomes full. 5 | */ 6 | 7 | if (process.argv.length < 4 || process.argv.length > 6) { 8 | console.log('Usage : node storageBot.js [] []') 9 | process.exit(1) 10 | } 11 | 12 | // Load your libraries 13 | const mineflayer = require('mineflayer') 14 | const collectBlock = require('mineflayer-collectblock').plugin 15 | 16 | // Create your bot 17 | const bot = mineflayer.createBot({ 18 | host: process.argv[2], 19 | port: parseInt(process.argv[3]), 20 | username: process.argv[4] ? process.argv[4] : 'storageBot', 21 | password: process.argv[5] 22 | }) 23 | 24 | // Load the collect block plugin 25 | bot.loadPlugin(collectBlock) 26 | 27 | // Load mcData on login 28 | let mcData 29 | bot.once('login', () => { 30 | mcData = require('minecraft-data')(bot.version) 31 | }) 32 | 33 | // On spawn, try to find any nearby chests and save those as storage locations. 34 | // When the bot's inventory becomes too full, it will empty it's inventory into 35 | // these chests before collecting more resources. If a chest gets full, it moves 36 | // to the next one in order until it's inventory is empty or it runs out of chests. 37 | bot.once('spawn', () => { 38 | bot.collectBlock.chestLocations = bot.findBlocks({ 39 | matching: mcData.blocksByName.chest.id, 40 | maxDistance: 16, 41 | count: 999999 // Get as many chests as we can 42 | }) 43 | 44 | if (bot.collectBlock.chestLocations.length === 0) { 45 | bot.chat("I don't see any chests nearby.") 46 | } else { 47 | for (const chestPos of bot.collectBlock.chestLocations) { 48 | bot.chat(`I found a chest at ${chestPos}`) 49 | } 50 | } 51 | }) 52 | 53 | // Wait for someone to say something 54 | bot.on('chat', async (username, message) => { 55 | // If the player says something start starts with "collect" 56 | // Otherwise, do nothing 57 | const args = message.split(' ') 58 | if (args[0] !== 'collect') return 59 | 60 | // If the player specifies a number, collect that many. Otherwise, default to 1. 61 | let count = 1 62 | if (args.length === 3) count = parseInt(args[1]) 63 | 64 | // If a number was given the item number is the 3rd arg, not the 2nd. 65 | let type = args[1] 66 | if (args.length === 3) type = args[2] 67 | 68 | // Get the id of that block type for this version of Minecraft. 69 | const blockType = mcData.blocksByName[type] 70 | if (!blockType) { 71 | bot.chat(`I don't know any blocks named ${type}.`) 72 | return 73 | } 74 | 75 | // Find all nearby blocks of that type, up to the given count, within 64 blocks. 76 | const blocks = bot.findBlocks({ 77 | matching: blockType.id, 78 | maxDistance: 64, 79 | count: count 80 | }) 81 | 82 | // Complain if we can't find any nearby blocks of that type. 83 | if (blocks.length === 0) { 84 | bot.chat("I don't see that block nearby.") 85 | return 86 | } 87 | 88 | // Convert the block position array into a block array to pass to collect block. 89 | const targets = [] 90 | for (let i = 0; i < Math.min(blocks.length, count); i++) { 91 | targets.push(bot.blockAt(blocks[i])) 92 | } 93 | 94 | // Announce what we found. 95 | bot.chat(`Found ${targets.length} ${type}(s)`) 96 | 97 | // Tell the bot to collect all of the given blocks in the block list. 98 | try { 99 | await bot.collectBlock.collect(targets) 100 | // All blocks have been collected. 101 | bot.chat('Done') 102 | } catch (err) { 103 | // An error occurred, report it. 104 | bot.chat(err.message) 105 | console.log(err) 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mineflayer-collectblock", 3 | "version": "1.4.1", 4 | "description": "A simple utility plugin for Mineflayer that add a higher level API for collecting blocks.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "ts-standard && tsc && require-self", 9 | "clean": "rm -rf lib", 10 | "test": "test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/TheDudeFromCI/mineflayer-collectblock.git" 15 | }, 16 | "keywords": [ 17 | "mineflayer", 18 | "plugin", 19 | "api", 20 | "utility", 21 | "helper", 22 | "collect" 23 | ], 24 | "author": "TheDudeFromCI", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/TheDudeFromCI/mineflayer-collectblock/issues" 28 | }, 29 | "homepage": "https://github.com/TheDudeFromCI/mineflayer-collectblock#readme", 30 | "dependencies": { 31 | "mineflayer": "^4.0.0", 32 | "mineflayer-pathfinder": "^2.1.1", 33 | "mineflayer-tool": "^1.1.0" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^18.6.4", 37 | "require-self": "^0.2.3", 38 | "ts-standard": "^11.0.0", 39 | "typescript": "^4.1.3" 40 | }, 41 | "files": [ 42 | "lib/**/*" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from 'mineflayer' 2 | import { Block } from 'prismarine-block' 3 | 4 | export function findFromVein (bot: Bot, block: Block, maxBlocks: number, maxDistance: number, floodRadius: number): Block[] { 5 | const targets: Block[] = [] 6 | const open: Block[] = [block] 7 | const type = block.type 8 | const center = block.position 9 | 10 | for (let i = 0; i < maxBlocks; i++) { 11 | const next = open.pop() 12 | if (next == null) break 13 | 14 | targets.push(next) 15 | 16 | for (let x = -floodRadius; x <= floodRadius; x++) { 17 | for (let y = -floodRadius; y <= floodRadius; y++) { 18 | for (let z = -floodRadius; z <= floodRadius; z++) { 19 | const neighborPos = next.position.offset(x, y, z) 20 | if (neighborPos.manhattanDistanceTo(center) > maxDistance) continue 21 | 22 | const neighbor = bot.blockAt(neighborPos) 23 | if (neighbor == null || neighbor.type !== type) continue 24 | 25 | if (targets.includes(neighbor)) continue 26 | if (open.includes(neighbor)) continue 27 | 28 | open.push(neighbor) 29 | } 30 | } 31 | } 32 | } 33 | 34 | return targets 35 | } 36 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/src/Inventory.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from 'mineflayer' 2 | import { Callback } from './CollectBlock' 3 | import { Vec3 } from 'vec3' 4 | import { error } from './Util' 5 | import { Item } from 'prismarine-item' 6 | import { goals } from 'mineflayer-pathfinder' 7 | import { callbackify } from 'util' 8 | 9 | export type ItemFilter = (item: Item) => boolean 10 | 11 | function getClosestChest (bot: Bot, chestLocations: Vec3[]): Vec3 | null { 12 | let chest = null 13 | let distance = 0 14 | 15 | for (const c of chestLocations) { 16 | const dist = c.distanceTo(bot.entity.position) 17 | if (chest == null || dist < distance) { 18 | chest = c 19 | distance = dist 20 | } 21 | } 22 | 23 | if (chest != null) { 24 | chestLocations.splice(chestLocations.indexOf(chest), 1) 25 | } 26 | 27 | return chest 28 | } 29 | 30 | export async function emptyInventoryIfFull (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise { 31 | // @ts-expect-error 32 | if (cb != null) return callbackify(emptyInventoryIfFull)(bot, chestLocations, cb) 33 | if (bot.inventory.emptySlotCount() > 0) return 34 | return await emptyInventory(bot, chestLocations, itemFilter) 35 | } 36 | 37 | export async function emptyInventory (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise { 38 | // @ts-expect-error 39 | if (cb != null) return callbackify(emptyInventory)(bot, chestLocations, cb) 40 | if (chestLocations.length === 0) { 41 | throw error('NoChests', 'There are no defined chest locations!') 42 | } 43 | 44 | // Shallow clone so we can safely remove chests from the list that are full. 45 | chestLocations = [...chestLocations] 46 | 47 | while (true) { 48 | const chest = getClosestChest(bot, chestLocations) 49 | if (chest == null) { 50 | throw error('NoChests', 'All chests are full.') 51 | } 52 | const hasRemaining = await tryEmptyInventory(bot, chest, itemFilter) 53 | if (!hasRemaining) return 54 | } 55 | } 56 | 57 | async function tryEmptyInventory (bot: Bot, chestLocation: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise { 58 | // @ts-expect-error 59 | if (cb != null) return callbackify(tryEmptyInventory)(bot, chestLocation, itemFilter, cb) 60 | await gotoChest(bot, chestLocation) 61 | return await placeItems(bot, chestLocation, itemFilter) 62 | } 63 | 64 | async function gotoChest (bot: Bot, location: Vec3, cb?: Callback): Promise { 65 | // @ts-expect-error 66 | if (cb != null) return callbackify(gotoChest)(bot, location) 67 | await bot.pathfinder.goto(new goals.GoalGetToBlock(location.x, location.y, location.z)) 68 | } 69 | 70 | async function placeItems (bot: Bot, chestPos: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise { 71 | // @ts-expect-error 72 | if (cb != null) return callbackify(placeItems)(bot, chestPos, itemFilter, cb) 73 | const chestBlock = bot.blockAt(chestPos) 74 | if (chestBlock == null) { 75 | throw error('UnloadedChunk', 'Chest is in an unloaded chunk!') 76 | } 77 | const chest = await bot.openChest(chestBlock) 78 | for (const item of bot.inventory.items()) { 79 | if (!itemFilter(item)) continue 80 | if (chest.firstEmptyContainerSlot() === null) { 81 | // We have items that didn't fit. 82 | return true 83 | } 84 | await chest.deposit(item.type, item.metadata, item.count) 85 | } 86 | return false 87 | } 88 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/src/Targets.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from 'mineflayer' 2 | import { Block } from 'prismarine-block' 3 | import { Entity } from 'prismarine-entity' 4 | 5 | export type Collectable = Block | Entity 6 | 7 | export class Targets { 8 | private readonly bot: Bot 9 | private targets: Collectable[] = [] 10 | 11 | constructor (bot: Bot) { 12 | this.bot = bot 13 | } 14 | 15 | appendTargets (targets: Collectable[]): void { 16 | for (const target of targets) { 17 | this.appendTarget(target) 18 | } 19 | } 20 | 21 | appendTarget (target: Collectable): void { 22 | if (this.targets.includes(target)) return 23 | this.targets.push(target) 24 | } 25 | 26 | /** 27 | * Gets the closest target to the bot in this list. 28 | * 29 | * @returns The closest target, or null if there are no targets. 30 | */ 31 | getClosest (): Collectable | null { 32 | let closest: Collectable | null = null 33 | let distance: number = 0 34 | 35 | for (const target of this.targets) { 36 | const dist = target.position.distanceTo(this.bot.entity.position) 37 | 38 | if (closest == null || dist < distance) { 39 | closest = target 40 | distance = dist 41 | } 42 | } 43 | 44 | return closest 45 | } 46 | 47 | get empty (): boolean { 48 | return this.targets.length === 0 49 | } 50 | 51 | clear (): void { 52 | this.targets.length = 0 53 | } 54 | 55 | removeTarget (target: Collectable): void { 56 | const index = this.targets.indexOf(target) 57 | if (index < 0) return 58 | this.targets.splice(index, 1) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts: -------------------------------------------------------------------------------- 1 | import type { Callback } from './index' 2 | export type Task = (cb: Callback) => void 3 | export type SyncTask = () => void 4 | 5 | /** 6 | * A simple utility class for queuing up a series of async tasks to execute. 7 | */ 8 | export class TaskQueue { 9 | private tasks: Task[] = [] 10 | 11 | /** 12 | * If true, the task list will stop executing if one of the tasks throws an error. 13 | */ 14 | readonly stopOnError: boolean = true 15 | 16 | /** 17 | * Adds a new async task to this queue. The provided callback should be executed when 18 | * the async task is complete. 19 | * 20 | * @param task - The async task to add. 21 | */ 22 | add (task: Task): void { 23 | this.tasks.push(task) 24 | } 25 | 26 | /** 27 | * Adds a synchronous task toi this queue. 28 | * 29 | * @param task - The sync task to add. 30 | */ 31 | addSync (task: SyncTask): void { 32 | this.add((cb) => { 33 | try { 34 | task() 35 | cb() 36 | } catch (err: any) { 37 | cb(err) 38 | } 39 | }) 40 | } 41 | 42 | /** 43 | * Runs all tasks currently in this queue and empties the queue. 44 | * 45 | * @param cb - The optional callback to be executed when all tasks in this queue have 46 | * finished executing. 47 | */ 48 | runAll (cb?: Callback): void { 49 | const taskList = this.tasks 50 | this.tasks = [] 51 | 52 | let index = -1 53 | const runNext: () => void = () => { 54 | index++ 55 | if (index >= taskList.length) { 56 | if (cb !== undefined) cb() 57 | return 58 | } 59 | 60 | try { 61 | taskList[index]((err) => { 62 | if (err !== undefined) { 63 | if (cb !== undefined) cb(err) 64 | 65 | if (this.stopOnError) return 66 | } 67 | 68 | runNext() 69 | }) 70 | } catch (err: any) { 71 | if (cb !== undefined) cb(err) 72 | } 73 | } 74 | 75 | runNext() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from 'mineflayer' 2 | 3 | class Subscription { 4 | constructor (readonly eventName: string, readonly callback: Function) {} 5 | } 6 | 7 | export class TemporarySubscriber { 8 | private readonly subscriptions: Subscription[] = [] 9 | 10 | constructor (readonly bot: Bot) {} 11 | 12 | /** 13 | * Adds a new temporary event listener to the bot. 14 | * 15 | * @param event - The event to subscribe to. 16 | * @param callback - The function to execute. 17 | */ 18 | subscribeTo (event: string, callback: Function): void { 19 | this.subscriptions.push(new Subscription(event, callback)) 20 | 21 | // @ts-expect-error 22 | this.bot.on(event, callback) 23 | } 24 | 25 | /** 26 | * Removes all attached event listeners from the bot. 27 | */ 28 | cleanup (): void { 29 | for (const sub of this.subscriptions) { 30 | // @ts-expect-error 31 | this.bot.removeListener(sub.eventName, sub.callback) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/src/Util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a new error object with the given type and message. 3 | * 4 | * @param type - The error type. 5 | * @param message - The error message. 6 | * 7 | * @returns The error object. 8 | */ 9 | export function error (type: string, message: string): Error { 10 | const e = new Error(message) 11 | e.name = type 12 | return e 13 | } 14 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from 'mineflayer' 2 | import { CollectBlock } from './CollectBlock' 3 | import { pathfinder as pathfinderPlugin } from 'mineflayer-pathfinder' 4 | import { plugin as toolPlugin } from 'mineflayer-tool' 5 | 6 | export function plugin (bot: Bot): void { 7 | // @ts-expect-error 8 | bot.collectBlock = new CollectBlock(bot) 9 | 10 | // Load plugins if not loaded manually. 11 | setTimeout(() => loadPathfinderPlugin(bot), 0) 12 | setTimeout(() => loadToolPlugin(bot), 0) 13 | } 14 | 15 | function loadPathfinderPlugin (bot: Bot): void { 16 | if (bot.pathfinder != null) return 17 | bot.loadPlugin(pathfinderPlugin) 18 | } 19 | 20 | function loadToolPlugin (bot: Bot): void { 21 | if (bot.tool != null) return 22 | bot.loadPlugin(toolPlugin) 23 | } 24 | 25 | export { CollectBlock, Callback, CollectOptions } from './CollectBlock' 26 | -------------------------------------------------------------------------------- /voyager/env/mineflayer/mineflayer-collectblock/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | "allowJs": true, /* Allow javascript files to be compiled. */ 10 | "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | "declaration": true, 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./lib", 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | /* Additional Checks */ 35 | "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | /* Experimental Options */ 56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 58 | /* Advanced Options */ 59 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 60 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 61 | }, 62 | "include": [ 63 | "src" 64 | ], 65 | "exclude": [ 66 | "node_modules", 67 | "**/__tests__/*" 68 | ] 69 | } -------------------------------------------------------------------------------- /voyager/env/mineflayer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voyager", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.20.2", 14 | "express": "^4.18.2", 15 | "magic-string": "^0.30.0", 16 | "minecraft-data": "^3.31.0", 17 | "minecrafthawkeye": "^1.3.6", 18 | "mineflayer": "^4.8.1", 19 | "mineflayer-collectblock": "file:mineflayer-collectblock", 20 | "mineflayer-pathfinder": "^2.4.2", 21 | "mineflayer-pvp": "^1.3.2", 22 | "mineflayer-tool": "^1.2.0", 23 | "mocha": "^10.2.0", 24 | "prismarine-biome": "^1.3.0", 25 | "prismarine-block": "^1.16.3", 26 | "prismarine-entity": "^2.2.0", 27 | "prismarine-item": "^1.12.1", 28 | "prismarine-nbt": "^2.2.1", 29 | "prismarine-recipe": "^1.3.1", 30 | "prismarine-viewer": "^1.24.0", 31 | "typescript": "^4.9.5", 32 | "vec3": "^0.1.8" 33 | }, 34 | "devDependencies": { 35 | "prettier": "2.8.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /voyager/env/process_monitor.py: -------------------------------------------------------------------------------- 1 | import time 2 | import re 3 | import warnings 4 | from typing import List 5 | 6 | import psutil 7 | import subprocess 8 | import logging 9 | import threading 10 | 11 | import voyager.utils as U 12 | 13 | 14 | class SubprocessMonitor: 15 | def __init__( 16 | self, 17 | commands: List[str], 18 | name: str, 19 | ready_match: str = r".*", 20 | log_path: str = "logs", 21 | callback_match: str = r"^(?!x)x$", # regex that will never match 22 | callback: callable = None, 23 | finished_callback: callable = None, 24 | ): 25 | self.commands = commands 26 | start_time = time.strftime("%Y%m%d_%H%M%S") 27 | self.name = name 28 | self.logger = logging.getLogger(name) 29 | handler = logging.FileHandler(U.f_join(log_path, f"{start_time}.log")) 30 | formatter = logging.Formatter( 31 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 32 | ) 33 | handler.setFormatter(formatter) 34 | self.logger.addHandler(handler) 35 | self.logger.setLevel(logging.INFO) 36 | self.process = None 37 | self.ready_match = ready_match 38 | self.ready_event = None 39 | self.ready_line = None 40 | self.callback_match = callback_match 41 | self.callback = callback 42 | self.finished_callback = finished_callback 43 | self.thread = None 44 | 45 | def _start(self): 46 | self.logger.info(f"Starting subprocess with commands: {self.commands}") 47 | 48 | self.process = psutil.Popen( 49 | self.commands, 50 | stdout=subprocess.PIPE, 51 | stderr=subprocess.STDOUT, 52 | universal_newlines=True, 53 | ) 54 | print(f"Subprocess {self.name} started with PID {self.process.pid}.") 55 | for line in iter(self.process.stdout.readline, ""): 56 | self.logger.info(line.strip()) 57 | if re.search(self.ready_match, line): 58 | self.ready_line = line 59 | self.logger.info("Subprocess is ready.") 60 | self.ready_event.set() 61 | if re.search(self.callback_match, line): 62 | self.callback() 63 | if not self.ready_event.is_set(): 64 | self.ready_event.set() 65 | warnings.warn(f"Subprocess {self.name} failed to start.") 66 | if self.finished_callback: 67 | self.finished_callback() 68 | 69 | def run(self): 70 | self.ready_event = threading.Event() 71 | self.ready_line = None 72 | self.thread = threading.Thread(target=self._start) 73 | self.thread.start() 74 | self.ready_event.wait() 75 | 76 | def stop(self): 77 | self.logger.info("Stopping subprocess.") 78 | if self.process and self.process.is_running(): 79 | self.process.terminate() 80 | self.process.wait() 81 | 82 | # def __del__(self): 83 | # if self.process.is_running(): 84 | # self.stop() 85 | 86 | @property 87 | def is_running(self): 88 | if self.process is None: 89 | return False 90 | return self.process.is_running() 91 | -------------------------------------------------------------------------------- /voyager/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | import voyager.utils as U 3 | 4 | 5 | def load_prompt(prompt): 6 | package_path = pkg_resources.resource_filename("voyager", "") 7 | return U.load_text(f"{package_path}/prompts/{prompt}.txt") 8 | -------------------------------------------------------------------------------- /voyager/prompts/action_response_format.txt: -------------------------------------------------------------------------------- 1 | Explain: ... 2 | Plan: 3 | 1) ... 4 | 2) ... 5 | 3) ... 6 | ... 7 | Code: 8 | ```javascript 9 | // helper functions (only if needed, try to avoid them) 10 | ... 11 | // main function after the helper functions 12 | async function yourMainFunctionName(bot) { 13 | // ... 14 | } 15 | ``` -------------------------------------------------------------------------------- /voyager/prompts/action_template.txt: -------------------------------------------------------------------------------- 1 | You are a helpful assistant that writes Mineflayer javascript code to complete any Minecraft task specified by me. 2 | 3 | Here are some useful programs written with Mineflayer APIs. 4 | 5 | {programs} 6 | 7 | 8 | At each round of conversation, I will give you 9 | Code from the last round: ... 10 | Execution error: ... 11 | Chat log: ... 12 | Biome: ... 13 | Time: ... 14 | Nearby blocks: ... 15 | Nearby entities (nearest to farthest): 16 | Health: ... 17 | Hunger: ... 18 | Position: ... 19 | Equipment: ... 20 | Inventory (xx/36): ... 21 | Chests: ... 22 | Task: ... 23 | Context: ... 24 | Critique: ... 25 | 26 | You should then respond to me with 27 | 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? 28 | 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. 29 | Code: 30 | 1) Write an async function taking the bot as the only argument. 31 | 2) Reuse the above useful programs as much as possible. 32 | - Use `mineBlock(bot, name, count)` to collect blocks. Do not use `bot.dig` directly. 33 | - Use `craftItem(bot, name, count)` to craft items. Do not use `bot.craft` or `bot.recipesFor` directly. 34 | - Use `smeltItem(bot, name count)` to smelt items. Do not use `bot.openFurnace` directly. 35 | - Use `placeItem(bot, name, position)` to place blocks. Do not use `bot.placeBlock` directly. 36 | - Use `killMob(bot, name, timeout)` to kill mobs. Do not use `bot.attack` directly. 37 | 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. 38 | 4) Functions in the "Code from the last round" section will not be saved or executed. Do not reuse functions listed there. 39 | 5) Anything defined outside a function will be ignored, define all your variables inside your functions. 40 | 6) Call `bot.chat` to show the intermediate progress. 41 | 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). 42 | 8) `maxDistance` should always be 32 for `bot.findBlocks` and `bot.findBlock`. Do not cheat. 43 | 9) Do not write infinite loops or recursive functions. 44 | 10) Do not use `bot.on` or `bot.once` to register event listeners. You definitely do not need them. 45 | 11) Name your function in a meaningful way (can infer the task from the name). 46 | 47 | You should only respond in the format as described below: 48 | RESPONSE FORMAT: 49 | {response_format} 50 | -------------------------------------------------------------------------------- /voyager/prompts/critic.txt: -------------------------------------------------------------------------------- 1 | You are an assistant that assesses my progress of playing Minecraft and provides useful guidance. 2 | 3 | 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. 4 | 5 | I will give you the following information: 6 | 7 | Biome: The biome after the task execution. 8 | Time: The current time. 9 | Nearby blocks: The surrounding blocks. These blocks are not collected yet. However, this is useful for some placing or planting tasks. 10 | Health: My current health. 11 | Hunger: My current hunger level. For eating task, if my hunger level is 20.0, then I successfully ate the food. 12 | Position: My current position. 13 | Equipment: My final equipment. For crafting tasks, I sometimes equip the crafted item. 14 | Inventory (xx/36): My final inventory. For mining and smelting tasks, you only need to check inventory. 15 | Chests: If the task requires me to place items in a chest, you can find chest information here. 16 | Task: The objective I need to accomplish. 17 | Context: The context of the task. 18 | 19 | You should only respond in JSON format as described below: 20 | { 21 | "reasoning": "reasoning", 22 | "success": boolean, 23 | "critique": "critique", 24 | } 25 | Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc. 26 | 27 | Here are some examples: 28 | INPUT: 29 | Inventory (2/36): {'oak_log':2, 'spruce_log':2} 30 | 31 | Task: Mine 3 wood logs 32 | 33 | RESPONSE: 34 | { 35 | "reasoning": "You need to mine 3 wood logs. You have 2 oak logs and 2 spruce logs, which add up to 4 wood logs.", 36 | "success": true, 37 | "critique": "" 38 | } 39 | 40 | INPUT: 41 | Inventory (3/36): {'crafting_table': 1, 'spruce_planks': 6, 'stick': 4} 42 | 43 | Task: Craft a wooden pickaxe 44 | 45 | RESPONSE: 46 | { 47 | "reasoning": "You have enough materials to craft a wooden pickaxe, but you didn't craft it.", 48 | "success": false, 49 | "critique": "Craft a wooden pickaxe with a crafting table using 3 spruce planks and 2 sticks." 50 | } 51 | 52 | INPUT: 53 | Inventory (2/36): {'raw_iron': 5, 'stone_pickaxe': 1} 54 | 55 | Task: Mine 5 iron_ore 56 | 57 | RESPONSE: 58 | { 59 | "reasoning": "Mining iron_ore in Minecraft will get raw_iron. You have 5 raw_iron in your inventory.", 60 | "success": true, 61 | "critique": "" 62 | } 63 | 64 | INPUT: 65 | Biome: plains 66 | 67 | Nearby blocks: stone, dirt, grass_block, grass, farmland, wheat 68 | 69 | Inventory (26/36): ... 70 | 71 | Task: Plant 1 wheat seed. 72 | 73 | RESPONSE: 74 | { 75 | "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.", 76 | "success": true, 77 | "critique": "" 78 | } 79 | 80 | INPUT: 81 | Inventory (11/36): {... ,'rotten_flesh': 1} 82 | 83 | Task: Kill 1 zombie 84 | 85 | Context: ... 86 | 87 | RESPONSE 88 | { 89 | "reasoning": "You have rotten flesh in your inventory, which means you successfully killed one zombie.", 90 | "success": true, 91 | "critique": "" 92 | } 93 | 94 | INPUT: 95 | Hunger: 20.0/20.0 96 | 97 | Inventory (11/36): ... 98 | 99 | Task: Eat 1 ... 100 | 101 | Context: ... 102 | 103 | RESPONSE 104 | { 105 | "reasoning": "For all eating task, if the player's hunger is 20.0, then the player successfully ate the food.", 106 | "success": true, 107 | "critique": "" 108 | } 109 | 110 | INPUT: 111 | Nearby blocks: chest 112 | 113 | 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} 114 | 115 | Chests: 116 | (81, 131, 16): {'andesite': 2, 'dirt': 2, 'cobblestone': 75, 'wooden_pickaxe': 1, 'wooden_sword': 1} 117 | 118 | Task: Deposit useless items into the chest at (81, 131, 16) 119 | 120 | Context: ... 121 | 122 | RESPONSE 123 | { 124 | "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.", 125 | "success": false, 126 | "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." 127 | } -------------------------------------------------------------------------------- /voyager/prompts/curriculum.txt: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | I will give you the following information: 4 | Question 1: ... 5 | Answer: ... 6 | Question 2: ... 7 | Answer: ... 8 | Question 3: ... 9 | Answer: ... 10 | ... 11 | Biome: ... 12 | Time: ... 13 | Nearby blocks: ... 14 | Other blocks that are recently seen: ... 15 | Nearby entities (nearest to farthest): ... 16 | Health: Higher than 15 means I'm healthy. 17 | Hunger: Higher than 15 means I'm not hungry. 18 | Position: ... 19 | Equipment: If I have better armor in my inventory, you should ask me to equip it. 20 | Inventory (xx/36): ... 21 | 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. 22 | Completed tasks so far: ... 23 | Failed tasks that are too hard: ... 24 | 25 | You must follow the following criteria: 26 | 1) You should act as a mentor and guide me to the next task based on my current learning progress. 27 | 2) Please be very specific about what resources I need to collect, what I need to craft, or what mobs I need to kill. 28 | 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. 29 | 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. 30 | 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. 31 | 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. 32 | 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. 33 | 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. 34 | 35 | You should only respond in the format as described below: 36 | RESPONSE FORMAT: 37 | Reasoning: Based on the information I listed above, do reasoning about what the next task should be. 38 | Task: The next task. 39 | 40 | Here's an example response: 41 | Reasoning: The inventory is empty now, chop down a tree to get some wood. 42 | Task: Obtain a wood log. -------------------------------------------------------------------------------- /voyager/prompts/curriculum_qa_step1_ask_questions.txt: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | I will give you the following information: 4 | Biome: ... 5 | Time: ... 6 | Nearby blocks: ... 7 | Other blocks that are recently seen: ... 8 | Nearby entities (nearest to farthest): ... 9 | Health: ... 10 | Hunger: ... 11 | Position: ... 12 | Equipment: ... 13 | Inventory (xx/36): ... 14 | Chests: ... 15 | Completed tasks so far: ... 16 | Failed tasks that are too hard: ... 17 | 18 | You must follow the following criteria: 19 | 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. 20 | 2) Your question should be specific to a concept in Minecraft. 21 | Bad example (the question is too general): 22 | Question: What is the best way to play Minecraft? 23 | Concept: unknown 24 | Bad example (axe is still general, you should specify the type of axe such as wooden axe): 25 | What are the benefits of using an axe to gather resources? 26 | Concept: axe 27 | Good example: 28 | Question: How to make a wooden pickaxe? 29 | Concept: wooden pickaxe 30 | 3) Your questions should be self-contained and not require any context. 31 | Bad example (the question requires the context of my current biome): 32 | Question: What are the blocks that I can find in my current biome? 33 | Concept: unknown 34 | Bad example (the question requires the context of my current inventory): 35 | Question: What are the resources you need the most currently? 36 | Concept: unknown 37 | Bad example (the question requires the context of my current inventory): 38 | Question: Do you have any gold or emerald resources? 39 | Concept: gold 40 | Bad example (the question requires the context of my nearby entities): 41 | Question: Can you see any animals nearby that you can kill for food? 42 | Concept: food 43 | Bad example (the question requires the context of my nearby blocks): 44 | Question: Is there any water source nearby? 45 | Concept: water 46 | Good example: 47 | Question: What are the blocks that I can find in the sparse jungle? 48 | Concept: sparse jungle 49 | 4) Do not ask questions about building tasks (such as building a shelter) since they are too hard for me to do. 50 | 51 | Let's say your current biome is sparse jungle. You can ask questions like: 52 | Question: What are the items that I can find in the sparse jungle? 53 | Concept: sparse jungle 54 | Question: What are the mobs that I can find in the sparse jungle? 55 | Concept: sparse jungle 56 | 57 | Let's say you see a creeper nearby, and you have not defeated a creeper before. You can ask a question like: 58 | Question: How to defeat the creeper? 59 | Concept: creeper 60 | 61 | Let's say your last completed task is "Craft a wooden pickaxe". You can ask a question like: 62 | Question: What are the suggested tasks that I can do after crafting a wooden pickaxe? 63 | Concept: wooden pickaxe 64 | 65 | Here are some more question and concept examples: 66 | Question: What are the ores that I can find in the sparse jungle? 67 | Concept: sparse jungle 68 | (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) 69 | Question: How can you obtain food in the sparse jungle? 70 | Concept: sparse jungle 71 | (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) 72 | Question: How can you use the furnace to upgrade your equipment and make useful items? 73 | Concept: furnace 74 | Question: How to obtain a diamond ore? 75 | Concept: diamond ore 76 | Question: What are the benefits of using a stone pickaxe over a wooden pickaxe? 77 | Concept: stone pickaxe 78 | Question: What are the tools that you can craft using wood planks and sticks? 79 | Concept: wood planks 80 | 81 | You should only respond in the format as described below: 82 | RESPONSE FORMAT: 83 | Reasoning: ... 84 | Question 1: ... 85 | Concept 1: ... 86 | Question 2: ... 87 | Concept 2: ... 88 | Question 3: ... 89 | Concept 3: ... 90 | Question 4: ... 91 | Concept 4: ... 92 | Question 5: ... 93 | Concept 5: ... 94 | ... 95 | -------------------------------------------------------------------------------- /voyager/prompts/curriculum_qa_step2_answer_questions.txt: -------------------------------------------------------------------------------- 1 | You are a helpful assistant that answer my question about Minecraft. 2 | 3 | I will give you the following information: 4 | Question: ... 5 | 6 | You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft. 7 | 1) Start your answer with "Answer: ". 8 | 2) Answer "Answer: Unknown" if you don't know the answer. -------------------------------------------------------------------------------- /voyager/prompts/curriculum_task_decomposition.txt: -------------------------------------------------------------------------------- 1 | You are a helpful assistant that generates a curriculum of subgoals to complete any Minecraft task specified by me. 2 | 3 | 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. 4 | 5 | You must follow the following criteria: 6 | 1) Return a Python list of subgoals that can be completed in order to complete the specified task. 7 | 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]". 8 | 3) Include each level of necessary tools as a subgoal, such as wooden, stone, iron, diamond, etc. 9 | 10 | You should only respond in JSON format as described below: 11 | ["subgoal1", "subgoal2", "subgoal3", ...] 12 | Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc. -------------------------------------------------------------------------------- /voyager/prompts/skill.txt: -------------------------------------------------------------------------------- 1 | You are a helpful assistant that writes a description of the given function written in Mineflayer javascript code. 2 | 3 | 1) Do not mention the function name. 4 | 2) Do not mention anything about `bot.chat` or helper functions. 5 | 3) There might be some helper functions before the main function, but you only need to describe the main function. 6 | 4) Try to summarize the function in no more than 6 sentences. 7 | 5) Your response should be a single line of text. 8 | 9 | For example, if the function is: 10 | 11 | async function mineCobblestone(bot) { 12 | // Check if the wooden pickaxe is in the inventory, if not, craft one 13 | let woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id); 14 | if (!woodenPickaxe) { 15 | bot.chat("Crafting a wooden pickaxe."); 16 | await craftWoodenPickaxe(bot); 17 | woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id); 18 | } 19 | 20 | // Equip the wooden pickaxe if it exists 21 | if (woodenPickaxe) { 22 | await bot.equip(woodenPickaxe, "hand"); 23 | 24 | // Explore until we find a stone block 25 | await exploreUntil(bot, new Vec3(1, -1, 1), 60, () => { 26 | const stone = bot.findBlock({ 27 | matching: mcData.blocksByName["stone"].id, 28 | maxDistance: 32 29 | }); 30 | if (stone) { 31 | return true; 32 | } 33 | }); 34 | 35 | // Mine 8 cobblestone blocks using the wooden pickaxe 36 | bot.chat("Found a stone block. Mining 8 cobblestone blocks."); 37 | await mineBlock(bot, "stone", 8); 38 | bot.chat("Successfully mined 8 cobblestone blocks."); 39 | 40 | // Save the event of mining 8 cobblestone 41 | bot.save("cobblestone_mined"); 42 | } else { 43 | bot.chat("Failed to craft a wooden pickaxe. Cannot mine cobblestone."); 44 | } 45 | } 46 | 47 | The main function is `mineCobblestone`. 48 | 49 | Then you would write: 50 | 51 | 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. -------------------------------------------------------------------------------- /voyager/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .file_utils import * 2 | from .json_utils import * 3 | from .record_utils import EventRecorder 4 | -------------------------------------------------------------------------------- /voyager/utils/file_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | File system utils. 3 | """ 4 | import collections 5 | import os 6 | import pickle 7 | import sys 8 | import errno 9 | import shutil 10 | import glob 11 | 12 | # import pwd 13 | import codecs 14 | import hashlib 15 | import tarfile 16 | import fnmatch 17 | import tempfile 18 | from datetime import datetime 19 | from socket import gethostname 20 | import logging 21 | 22 | 23 | f_ext = os.path.splitext 24 | 25 | f_size = os.path.getsize 26 | 27 | is_file = os.path.isfile 28 | 29 | is_dir = os.path.isdir 30 | 31 | get_dir = os.path.dirname 32 | 33 | 34 | def host_name(): 35 | "Get host name, alias with ``socket.gethostname()``" 36 | return gethostname() 37 | 38 | 39 | def host_id(): 40 | """ 41 | Returns: first part of hostname up to '.' 42 | """ 43 | return host_name().split(".")[0] 44 | 45 | 46 | def utf_open(fname, mode): 47 | """ 48 | Wrapper for codecs.open 49 | """ 50 | return codecs.open(fname, mode=mode, encoding="utf-8") 51 | 52 | 53 | def is_sequence(obj): 54 | """ 55 | Returns: 56 | True if the sequence is a collections.Sequence and not a string. 57 | """ 58 | return isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str) 59 | 60 | 61 | def pack_varargs(args): 62 | """ 63 | Pack *args or a single list arg as list 64 | 65 | def f(*args): 66 | arg_list = pack_varargs(args) 67 | # arg_list is now packed as a list 68 | """ 69 | assert isinstance(args, tuple), "please input the tuple `args` as in *args" 70 | if len(args) == 1 and is_sequence(args[0]): 71 | return args[0] 72 | else: 73 | return args 74 | 75 | 76 | def f_not_empty(*fpaths): 77 | """ 78 | Returns: 79 | True if and only if the file exists and file size > 0 80 | if fpath is a dir, if and only if dir exists and has at least 1 file 81 | """ 82 | fpath = f_join(*fpaths) 83 | if not os.path.exists(fpath): 84 | return False 85 | 86 | if os.path.isdir(fpath): 87 | return len(os.listdir(fpath)) > 0 88 | else: 89 | return os.path.getsize(fpath) > 0 90 | 91 | 92 | def f_expand(fpath): 93 | return os.path.expandvars(os.path.expanduser(fpath)) 94 | 95 | 96 | def f_exists(*fpaths): 97 | return os.path.exists(f_join(*fpaths)) 98 | 99 | 100 | def f_join(*fpaths): 101 | """ 102 | join file paths and expand special symbols like `~` for home dir 103 | """ 104 | fpaths = pack_varargs(fpaths) 105 | fpath = f_expand(os.path.join(*fpaths)) 106 | if isinstance(fpath, str): 107 | fpath = fpath.strip() 108 | return fpath 109 | 110 | 111 | def f_listdir( 112 | *fpaths, 113 | filter_ext=None, 114 | filter=None, 115 | sort=True, 116 | full_path=False, 117 | nonexist_ok=True, 118 | recursive=False, 119 | ): 120 | """ 121 | Args: 122 | full_path: True to return full paths to the dir contents 123 | filter: function that takes in file name and returns True to include 124 | nonexist_ok: True to return [] if the dir is non-existent, False to raise 125 | sort: sort the file names by alphabetical 126 | recursive: True to use os.walk to recursively list files. Note that `filter` 127 | will be applied to the relative path string to the root dir. 128 | e.g. filter will take "a/data1.txt" and "a/b/data3.txt" as input, instead of 129 | just the base file names "data1.txt" and "data3.txt". 130 | if False, will simply call os.listdir() 131 | """ 132 | assert not (filter_ext and filter), "filter_ext and filter are mutually exclusive" 133 | dir_path = f_join(*fpaths) 134 | if not os.path.exists(dir_path) and nonexist_ok: 135 | return [] 136 | if recursive: 137 | files = [ 138 | os.path.join(os.path.relpath(root, dir_path), file) 139 | for root, _, files in os.walk(dir_path) 140 | for file in files 141 | ] 142 | else: 143 | files = os.listdir(dir_path) 144 | if filter is not None: 145 | files = [f for f in files if filter(f)] 146 | elif filter_ext is not None: 147 | files = [f for f in files if f.endswith(filter_ext)] 148 | if sort: 149 | files.sort() 150 | if full_path: 151 | return [os.path.join(dir_path, f) for f in files] 152 | else: 153 | return files 154 | 155 | 156 | def f_mkdir(*fpaths): 157 | """ 158 | Recursively creates all the subdirs 159 | If exist, do nothing. 160 | """ 161 | fpath = f_join(*fpaths) 162 | os.makedirs(fpath, exist_ok=True) 163 | return fpath 164 | 165 | 166 | def f_mkdir_in_path(*fpaths): 167 | """ 168 | fpath is a file, 169 | recursively creates all the parent dirs that lead to the file 170 | If exist, do nothing. 171 | """ 172 | os.makedirs(get_dir(f_join(*fpaths)), exist_ok=True) 173 | 174 | 175 | def last_part_in_path(fpath): 176 | """ 177 | https://stackoverflow.com/questions/3925096/how-to-get-only-the-last-part-of-a-path-in-python 178 | """ 179 | return os.path.basename(os.path.normpath(f_expand(fpath))) 180 | 181 | 182 | def is_abs_path(*fpath): 183 | return os.path.isabs(f_join(*fpath)) 184 | 185 | 186 | def is_relative_path(*fpath): 187 | return not is_abs_path(f_join(*fpath)) 188 | 189 | 190 | def f_time(*fpath): 191 | "File modification time" 192 | return str(os.path.getctime(f_join(*fpath))) 193 | 194 | 195 | def f_append_before_ext(fpath, suffix): 196 | """ 197 | Append a suffix to file name and retain its extension 198 | """ 199 | name, ext = f_ext(fpath) 200 | return name + suffix + ext 201 | 202 | 203 | def f_add_ext(fpath, ext): 204 | """ 205 | Append an extension if not already there 206 | Args: 207 | ext: will add a preceding `.` if doesn't exist 208 | """ 209 | if not ext.startswith("."): 210 | ext = "." + ext 211 | if fpath.endswith(ext): 212 | return fpath 213 | else: 214 | return fpath + ext 215 | 216 | 217 | def f_has_ext(fpath, ext): 218 | "Test if file path is a text file" 219 | _, actual_ext = f_ext(fpath) 220 | return actual_ext == "." + ext.lstrip(".") 221 | 222 | 223 | def f_glob(*fpath): 224 | return glob.glob(f_join(*fpath), recursive=True) 225 | 226 | 227 | def f_remove(*fpath, verbose=False, dry_run=False): 228 | """ 229 | If exist, remove. Supports both dir and file. Supports glob wildcard. 230 | """ 231 | assert isinstance(verbose, bool) 232 | fpath = f_join(fpath) 233 | if dry_run: 234 | print("Dry run, delete:", fpath) 235 | return 236 | for f in glob.glob(fpath): 237 | try: 238 | shutil.rmtree(f) 239 | except OSError as e: 240 | if e.errno == errno.ENOTDIR: 241 | try: 242 | os.remove(f) 243 | except: # final resort safeguard 244 | pass 245 | if verbose: 246 | print(f'Deleted "{fpath}"') 247 | 248 | 249 | def f_copy(fsrc, fdst, ignore=None, include=None, exists_ok=True, verbose=False): 250 | """ 251 | Supports both dir and file. Supports glob wildcard. 252 | """ 253 | fsrc, fdst = f_expand(fsrc), f_expand(fdst) 254 | for f in glob.glob(fsrc): 255 | try: 256 | f_copytree(f, fdst, ignore=ignore, include=include, exist_ok=exists_ok) 257 | except OSError as e: 258 | if e.errno == errno.ENOTDIR: 259 | shutil.copy(f, fdst) 260 | else: 261 | raise 262 | if verbose: 263 | print(f'Copied "{fsrc}" to "{fdst}"') 264 | 265 | 266 | def _f_copytree( 267 | src, 268 | dst, 269 | symlinks=False, 270 | ignore=None, 271 | exist_ok=True, 272 | copy_function=shutil.copy2, 273 | ignore_dangling_symlinks=False, 274 | ): 275 | """Copied from python standard lib shutil.copytree 276 | except that we allow exist_ok 277 | Use f_copytree as entry 278 | """ 279 | names = os.listdir(src) 280 | if ignore is not None: 281 | ignored_names = ignore(src, names) 282 | else: 283 | ignored_names = set() 284 | 285 | os.makedirs(dst, exist_ok=exist_ok) 286 | errors = [] 287 | for name in names: 288 | if name in ignored_names: 289 | continue 290 | srcname = os.path.join(src, name) 291 | dstname = os.path.join(dst, name) 292 | try: 293 | if os.path.islink(srcname): 294 | linkto = os.readlink(srcname) 295 | if symlinks: 296 | # We can't just leave it to `copy_function` because legacy 297 | # code with a custom `copy_function` may rely on copytree 298 | # doing the right thing. 299 | os.symlink(linkto, dstname) 300 | shutil.copystat(srcname, dstname, follow_symlinks=not symlinks) 301 | else: 302 | # ignore dangling symlink if the flag is on 303 | if not os.path.exists(linkto) and ignore_dangling_symlinks: 304 | continue 305 | # otherwise let the copy occurs. copy2 will raise an error 306 | if os.path.isdir(srcname): 307 | _f_copytree( 308 | srcname, dstname, symlinks, ignore, exist_ok, copy_function 309 | ) 310 | else: 311 | copy_function(srcname, dstname) 312 | elif os.path.isdir(srcname): 313 | _f_copytree(srcname, dstname, symlinks, ignore, exist_ok, copy_function) 314 | else: 315 | # Will raise a SpecialFileError for unsupported file types 316 | copy_function(srcname, dstname) 317 | # catch the Error from the recursive copytree so that we can 318 | # continue with other files 319 | except shutil.Error as err: 320 | errors.extend(err.args[0]) 321 | except OSError as why: 322 | errors.append((srcname, dstname, str(why))) 323 | try: 324 | shutil.copystat(src, dst) 325 | except OSError as why: 326 | # Copying file access times may fail on Windows 327 | if getattr(why, "winerror", None) is None: 328 | errors.append((src, dst, str(why))) 329 | if errors: 330 | raise shutil.Error(errors) 331 | return dst 332 | 333 | 334 | def _include_patterns(*patterns): 335 | """Factory function that can be used with copytree() ignore parameter. 336 | 337 | Arguments define a sequence of glob-style patterns 338 | that are used to specify what files to NOT ignore. 339 | Creates and returns a function that determines this for each directory 340 | in the file hierarchy rooted at the source directory when used with 341 | shutil.copytree(). 342 | """ 343 | 344 | def _ignore_patterns(path, names): 345 | keep = set( 346 | name for pattern in patterns for name in fnmatch.filter(names, pattern) 347 | ) 348 | ignore = set( 349 | name 350 | for name in names 351 | if name not in keep and not os.path.isdir(os.path.join(path, name)) 352 | ) 353 | return ignore 354 | 355 | return _ignore_patterns 356 | 357 | 358 | def f_copytree(fsrc, fdst, symlinks=False, ignore=None, include=None, exist_ok=True): 359 | fsrc, fdst = f_expand(fsrc), f_expand(fdst) 360 | assert (ignore is None) or ( 361 | include is None 362 | ), "ignore= and include= are mutually exclusive" 363 | if ignore: 364 | ignore = shutil.ignore_patterns(*ignore) 365 | elif include: 366 | ignore = _include_patterns(*include) 367 | _f_copytree(fsrc, fdst, ignore=ignore, symlinks=symlinks, exist_ok=exist_ok) 368 | 369 | 370 | def f_move(fsrc, fdst): 371 | fsrc, fdst = f_expand(fsrc), f_expand(fdst) 372 | for f in glob.glob(fsrc): 373 | shutil.move(f, fdst) 374 | 375 | 376 | def f_split_path(fpath, normpath=True): 377 | """ 378 | Splits path into a list of its component folders 379 | 380 | Args: 381 | normpath: call os.path.normpath to remove redundant '/' and 382 | up-level references like ".." 383 | """ 384 | if normpath: 385 | fpath = os.path.normpath(fpath) 386 | allparts = [] 387 | while 1: 388 | parts = os.path.split(fpath) 389 | if parts[0] == fpath: # sentinel for absolute paths 390 | allparts.insert(0, parts[0]) 391 | break 392 | elif parts[1] == fpath: # sentinel for relative paths 393 | allparts.insert(0, parts[1]) 394 | break 395 | else: 396 | fpath = parts[0] 397 | allparts.insert(0, parts[1]) 398 | return allparts 399 | 400 | 401 | def get_script_dir(): 402 | """ 403 | Returns: the dir of current script 404 | """ 405 | return os.path.dirname(os.path.realpath(sys.argv[0])) 406 | 407 | 408 | def get_script_file_name(): 409 | """ 410 | Returns: the dir of current script 411 | """ 412 | return os.path.basename(sys.argv[0]) 413 | 414 | 415 | def get_script_self_path(): 416 | """ 417 | Returns: the dir of current script 418 | """ 419 | return os.path.realpath(sys.argv[0]) 420 | 421 | 422 | def get_parent_dir(location, abspath=False): 423 | """ 424 | Args: 425 | location: current directory or file 426 | 427 | Returns: 428 | parent directory absolute or relative path 429 | """ 430 | _path = os.path.abspath if abspath else os.path.relpath 431 | return _path(f_join(location, os.pardir)) 432 | 433 | 434 | def md5_checksum(*fpath): 435 | """ 436 | File md5 signature 437 | """ 438 | hash_md5 = hashlib.md5() 439 | with open(f_join(*fpath), "rb") as f: 440 | for chunk in iter(lambda: f.read(65536), b""): 441 | hash_md5.update(chunk) 442 | return hash_md5.hexdigest() 443 | 444 | 445 | def create_tar(fsrc, output_tarball, include=None, ignore=None, compress_mode="gz"): 446 | """ 447 | Args: 448 | fsrc: source file or folder 449 | output_tarball: output tar file name 450 | compress_mode: "gz", "bz2", "xz" or "" (empty for uncompressed write) 451 | include: include pattern, will trigger copy to temp directory 452 | ignore: ignore pattern, will trigger copy to temp directory 453 | """ 454 | fsrc, output_tarball = f_expand(fsrc), f_expand(output_tarball) 455 | assert compress_mode in ["gz", "bz2", "xz", ""] 456 | src_base = os.path.basename(fsrc) 457 | 458 | tempdir = None 459 | if include or ignore: 460 | tempdir = tempfile.mkdtemp() 461 | tempdest = f_join(tempdir, src_base) 462 | f_copy(fsrc, tempdest, include=include, ignore=ignore) 463 | fsrc = tempdest 464 | 465 | with tarfile.open(output_tarball, "w:" + compress_mode) as tar: 466 | tar.add(fsrc, arcname=src_base) 467 | 468 | if tempdir: 469 | f_remove(tempdir) 470 | 471 | 472 | def extract_tar(source_tarball, output_dir=".", members=None): 473 | """ 474 | Args: 475 | source_tarball: extract members from archive 476 | output_dir: default to current working dir 477 | members: must be a subset of the list returned by getmembers() 478 | """ 479 | source_tarball, output_dir = f_expand(source_tarball), f_expand(output_dir) 480 | with tarfile.open(source_tarball, "r:*") as tar: 481 | tar.extractall(output_dir, members=members) 482 | 483 | 484 | def move_with_backup(*fpath, suffix=".bak"): 485 | """ 486 | Ensures that a path is not occupied. If there is a file, rename it by 487 | adding @suffix. Resursively backs up everything. 488 | 489 | Args: 490 | fpath: file path to clear 491 | suffix: Add to backed up files (default: {'.bak'}) 492 | """ 493 | fpath = str(f_join(*fpath)) 494 | if os.path.exists(fpath): 495 | move_with_backup(fpath + suffix) 496 | shutil.move(fpath, fpath + suffix) 497 | 498 | 499 | def insert_before_ext(name, insert): 500 | """ 501 | log.txt -> log.ep50.txt 502 | """ 503 | name, ext = os.path.splitext(name) 504 | return name + insert + ext 505 | 506 | 507 | def timestamp_file_name(fname): 508 | timestr = datetime.now().strftime("_%H-%M-%S_%m-%d-%y") 509 | return insert_before_ext(fname, timestr) 510 | 511 | 512 | def get_file_lock(*fpath, timeout: int = 15, logging_level="critical"): 513 | """ 514 | NFS-safe filesystem-backed lock. `pip install flufl.lock` 515 | https://flufllock.readthedocs.io/en/stable/apiref.html 516 | 517 | Args: 518 | fpath: should be a path on NFS so that every process can see it 519 | timeout: seconds 520 | """ 521 | from flufl.lock import Lock 522 | 523 | logging.getLogger("flufl.lock").setLevel(logging_level.upper()) 524 | return Lock(f_join(*fpath), lifetime=timeout) 525 | 526 | 527 | def load_pickle(*fpaths): 528 | with open(f_join(*fpaths), "rb") as fp: 529 | return pickle.load(fp) 530 | 531 | 532 | def dump_pickle(data, *fpaths): 533 | with open(f_join(*fpaths), "wb") as fp: 534 | pickle.dump(data, fp) 535 | 536 | 537 | def load_text(*fpaths, by_lines=False): 538 | with open(f_join(*fpaths), "r") as fp: 539 | if by_lines: 540 | return fp.readlines() 541 | else: 542 | return fp.read() 543 | 544 | 545 | def load_text_lines(*fpaths): 546 | return load_text(*fpaths, by_lines=True) 547 | 548 | 549 | def dump_text(s, *fpaths): 550 | with open(f_join(*fpaths), "w") as fp: 551 | fp.write(s) 552 | 553 | 554 | def dump_text_lines(lines: list[str], *fpaths, add_newline=True): 555 | with open(f_join(*fpaths), "w") as fp: 556 | for line in lines: 557 | print(line, file=fp, end="\n" if add_newline else "") 558 | 559 | 560 | # aliases to be consistent with other load_* and dump_* 561 | pickle_load = load_pickle 562 | pickle_dump = dump_pickle 563 | text_load = load_text 564 | read_text = load_text 565 | read_text_lines = load_text_lines 566 | write_text = dump_text 567 | write_text_lines = dump_text_lines 568 | text_dump = dump_text 569 | -------------------------------------------------------------------------------- /voyager/utils/json_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from typing import Any, Dict, Union 4 | from .file_utils import f_join 5 | 6 | 7 | def json_load(*file_path, **kwargs): 8 | file_path = f_join(file_path) 9 | with open(file_path, "r") as fp: 10 | return json.load(fp, **kwargs) 11 | 12 | 13 | def json_loads(string, **kwargs): 14 | return json.loads(string, **kwargs) 15 | 16 | 17 | def json_dump(data, *file_path, **kwargs): 18 | file_path = f_join(file_path) 19 | with open(file_path, "w") as fp: 20 | json.dump(data, fp, **kwargs) 21 | 22 | 23 | def json_dumps(data, **kwargs): 24 | """ 25 | Returns: string 26 | """ 27 | return json.dumps(data, **kwargs) 28 | 29 | 30 | # ---------------- Aliases ----------------- 31 | # add aliases where verb goes first, json_load -> load_json 32 | load_json = json_load 33 | loads_json = json_loads 34 | dump_json = json_dump 35 | dumps_json = json_dumps 36 | 37 | 38 | def extract_char_position(error_message: str) -> int: 39 | """Extract the character position from the JSONDecodeError message. 40 | Args: 41 | error_message (str): The error message from the JSONDecodeError 42 | exception. 43 | Returns: 44 | int: The character position. 45 | """ 46 | import re 47 | 48 | char_pattern = re.compile(r"\(char (\d+)\)") 49 | if match := char_pattern.search(error_message): 50 | return int(match[1]) 51 | else: 52 | raise ValueError("Character position not found in the error message.") 53 | 54 | 55 | def add_quotes_to_property_names(json_string: str) -> str: 56 | """ 57 | Add quotes to property names in a JSON string. 58 | Args: 59 | json_string (str): The JSON string. 60 | Returns: 61 | str: The JSON string with quotes added to property names. 62 | """ 63 | 64 | def replace_func(match): 65 | return f'"{match.group(1)}":' 66 | 67 | property_name_pattern = re.compile(r"(\w+):") 68 | corrected_json_string = property_name_pattern.sub(replace_func, json_string) 69 | 70 | try: 71 | json.loads(corrected_json_string) 72 | return corrected_json_string 73 | except json.JSONDecodeError as e: 74 | raise e 75 | 76 | 77 | def balance_braces(json_string: str) -> str: 78 | """ 79 | Balance the braces in a JSON string. 80 | Args: 81 | json_string (str): The JSON string. 82 | Returns: 83 | str: The JSON string with braces balanced. 84 | """ 85 | 86 | open_braces_count = json_string.count("{") 87 | close_braces_count = json_string.count("}") 88 | 89 | while open_braces_count > close_braces_count: 90 | json_string += "}" 91 | close_braces_count += 1 92 | 93 | while close_braces_count > open_braces_count: 94 | json_string = json_string.rstrip("}") 95 | close_braces_count -= 1 96 | 97 | try: 98 | json.loads(json_string) 99 | return json_string 100 | except json.JSONDecodeError as e: 101 | raise e 102 | 103 | 104 | def fix_invalid_escape(json_str: str, error_message: str) -> str: 105 | while error_message.startswith("Invalid \\escape"): 106 | bad_escape_location = extract_char_position(error_message) 107 | json_str = json_str[:bad_escape_location] + json_str[bad_escape_location + 1 :] 108 | try: 109 | json.loads(json_str) 110 | return json_str 111 | except json.JSONDecodeError as e: 112 | error_message = str(e) 113 | return json_str 114 | 115 | 116 | def correct_json(json_str: str) -> str: 117 | """ 118 | Correct common JSON errors. 119 | Args: 120 | json_str (str): The JSON string. 121 | """ 122 | 123 | try: 124 | json.loads(json_str) 125 | return json_str 126 | except json.JSONDecodeError as e: 127 | error_message = str(e) 128 | if error_message.startswith("Invalid \\escape"): 129 | json_str = fix_invalid_escape(json_str, error_message) 130 | if error_message.startswith( 131 | "Expecting property name enclosed in double quotes" 132 | ): 133 | json_str = add_quotes_to_property_names(json_str) 134 | try: 135 | json.loads(json_str) 136 | return json_str 137 | except json.JSONDecodeError as e: 138 | error_message = str(e) 139 | if balanced_str := balance_braces(json_str): 140 | return balanced_str 141 | return json_str 142 | 143 | 144 | def fix_and_parse_json( 145 | json_str: str, try_to_fix_with_gpt: bool = True 146 | ) -> Union[str, Dict[Any, Any]]: 147 | """Fix and parse JSON string""" 148 | try: 149 | json_str = json_str.replace("\t", "") 150 | return json.loads(json_str) 151 | except json.JSONDecodeError as _: # noqa: F841 152 | json_str = correct_json(json_str) 153 | try: 154 | return json.loads(json_str) 155 | except json.JSONDecodeError as _: # noqa: F841 156 | pass 157 | # Let's do something manually: 158 | # sometimes GPT responds with something BEFORE the braces: 159 | # "I'm sorry, I don't understand. Please try again." 160 | # {"text": "I'm sorry, I don't understand. Please try again.", 161 | # "confidence": 0.0} 162 | # So let's try to find the first brace and then parse the rest 163 | # of the string 164 | try: 165 | brace_index = json_str.index("{") 166 | json_str = json_str[brace_index:] 167 | last_brace_index = json_str.rindex("}") 168 | json_str = json_str[: last_brace_index + 1] 169 | return json.loads(json_str) 170 | except json.JSONDecodeError as e: # noqa: F841 171 | # if try_to_fix_with_gpt: 172 | # print( 173 | # "Warning: Failed to parse AI output, attempting to fix." 174 | # "\n If you see this warning frequently, it's likely that" 175 | # " your prompt is confusing the AI. Try changing it up" 176 | # " slightly." 177 | # ) 178 | # # Now try to fix this up using the ai_functions 179 | # ai_fixed_json = fix_json(json_str, JSON_SCHEMA) 180 | # 181 | # if ai_fixed_json != "failed": 182 | # return json.loads(ai_fixed_json) 183 | # else: 184 | # # This allows the AI to react to the error message, 185 | # # which usually results in it correcting its ways. 186 | # print("Failed to fix ai output, telling the AI.") 187 | # return json_str 188 | # else: 189 | raise e 190 | 191 | 192 | # def fix_json(json_str: str, schema: str) -> str: 193 | # """Fix the given JSON string to make it parseable and fully complient with the provided schema.""" 194 | # 195 | # # Try to fix the JSON using gpt: 196 | # function_string = "def fix_json(json_str: str, schema:str=None) -> str:" 197 | # args = [f"'''{json_str}'''", f"'''{schema}'''"] 198 | # description_string = ( 199 | # "Fixes the provided JSON string to make it parseable" 200 | # " and fully complient with the provided schema.\n If an object or" 201 | # " field specified in the schema isn't contained within the correct" 202 | # " JSON, it is ommited.\n This function is brilliant at guessing" 203 | # " when the format is incorrect." 204 | # ) 205 | # 206 | # # If it doesn't already start with a "`", add one: 207 | # if not json_str.startswith("`"): 208 | # json_str = "```json\n" + json_str + "\n```" 209 | # result_string = call_ai_function( 210 | # function_string, args, description_string, model=cfg.fast_llm_model 211 | # ) 212 | # if cfg.debug: 213 | # print("------------ JSON FIX ATTEMPT ---------------") 214 | # print(f"Original JSON: {json_str}") 215 | # print("-----------") 216 | # print(f"Fixed JSON: {result_string}") 217 | # print("----------- END OF FIX ATTEMPT ----------------") 218 | # 219 | # try: 220 | # json.loads(result_string) # just check the validity 221 | # return result_string 222 | # except: # noqa: E722 223 | # # Get the call stack: 224 | # # import traceback 225 | # # call_stack = traceback.format_exc() 226 | # # print(f"Failed to fix JSON: '{json_str}' "+call_stack) 227 | # return "failed" 228 | -------------------------------------------------------------------------------- /voyager/utils/record_utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .file_utils import * 4 | from .json_utils import * 5 | 6 | 7 | class EventRecorder: 8 | def __init__( 9 | self, 10 | ckpt_dir="ckpt", 11 | resume=False, 12 | init_position=None, 13 | ): 14 | self.ckpt_dir = ckpt_dir 15 | self.item_history = set() 16 | self.item_vs_time = {} 17 | self.item_vs_iter = {} 18 | self.biome_history = set() 19 | self.init_position = init_position 20 | self.position_history = [[0, 0]] 21 | self.elapsed_time = 0 22 | self.iteration = 0 23 | f_mkdir(self.ckpt_dir, "events") 24 | if resume: 25 | self.resume() 26 | 27 | def record(self, events, task): 28 | task = re.sub(r'[\\/:"*?<>| ]', "_", task) 29 | task = task.replace(" ", "_") + time.strftime( 30 | "_%Y%m%d_%H%M%S", time.localtime() 31 | ) 32 | self.iteration += 1 33 | if not self.init_position: 34 | self.init_position = [ 35 | events[0][1]["status"]["position"]["x"], 36 | events[0][1]["status"]["position"]["z"], 37 | ] 38 | for event_type, event in events: 39 | self.update_items(event) 40 | if event_type == "observe": 41 | self.update_elapsed_time(event) 42 | print( 43 | f"\033[96m****Recorder message: {self.elapsed_time} ticks have elapsed****\033[0m\n" 44 | f"\033[96m****Recorder message: {self.iteration} iteration passed****\033[0m" 45 | ) 46 | dump_json(events, f_join(self.ckpt_dir, "events", task)) 47 | 48 | def resume(self, cutoff=None): 49 | self.item_history = set() 50 | self.item_vs_time = {} 51 | self.item_vs_iter = {} 52 | self.elapsed_time = 0 53 | self.position_history = [[0, 0]] 54 | 55 | def get_timestamp(string): 56 | timestamp = "_".join(string.split("_")[-2:]) 57 | return time.mktime(time.strptime(timestamp, "%Y%m%d_%H%M%S")) 58 | 59 | records = f_listdir(self.ckpt_dir, "events") 60 | sorted_records = sorted(records, key=get_timestamp) 61 | for record in sorted_records: 62 | self.iteration += 1 63 | if cutoff and self.iteration > cutoff: 64 | break 65 | events = load_json(f_join(self.ckpt_dir, "events", record)) 66 | if not self.init_position: 67 | self.init_position = ( 68 | events[0][1]["status"]["position"]["x"], 69 | events[0][1]["status"]["position"]["z"], 70 | ) 71 | for event_type, event in events: 72 | self.update_items(event) 73 | self.update_position(event) 74 | if event_type == "observe": 75 | self.update_elapsed_time(event) 76 | 77 | def update_items(self, event): 78 | inventory = event["inventory"] 79 | elapsed_time = event["status"]["elapsedTime"] 80 | biome = event["status"]["biome"] 81 | items = set(inventory.keys()) 82 | new_items = items - self.item_history 83 | self.item_history.update(items) 84 | self.biome_history.add(biome) 85 | if new_items: 86 | if self.elapsed_time + elapsed_time not in self.item_vs_time: 87 | self.item_vs_time[self.elapsed_time + elapsed_time] = [] 88 | self.item_vs_time[self.elapsed_time + elapsed_time].extend(new_items) 89 | if self.iteration not in self.item_vs_iter: 90 | self.item_vs_iter[self.iteration] = [] 91 | self.item_vs_iter[self.iteration].extend(new_items) 92 | 93 | def update_elapsed_time(self, event): 94 | self.elapsed_time += event["status"]["elapsedTime"] 95 | 96 | def update_position(self, event): 97 | position = [ 98 | event["status"]["position"]["x"] - self.init_position[0], 99 | event["status"]["position"]["z"] - self.init_position[1], 100 | ] 101 | if self.position_history[-1] != position: 102 | self.position_history.append(position) 103 | --------------------------------------------------------------------------------