resolve merge

This commit is contained in:
Isadora White 2025-04-21 12:11:59 -07:00
commit 748f65a4da
24 changed files with 5983 additions and 310 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Kolby Nottingham
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,913 @@
diff --git a/node_modules/minecraft-data/minecraft-data/data/pc/1.21.1/blocks.json b/node_modules/minecraft-data/minecraft-data/data/pc/1.21.1/blocks.json
index 9d8c578..91e1d77 100644
--- a/node_modules/minecraft-data/minecraft-data/data/pc/1.21.1/blocks.json
+++ b/node_modules/minecraft-data/minecraft-data/data/pc/1.21.1/blocks.json
@@ -1062,7 +1062,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -1088,7 +1088,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -1114,7 +1114,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -1141,7 +1141,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -2905,7 +2905,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -2932,7 +2932,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -2959,7 +2959,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -4998,7 +4998,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -5024,7 +5024,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -5218,7 +5218,7 @@
"resistance": 1200.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -5603,7 +5603,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -5629,7 +5629,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -5655,7 +5655,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -8242,7 +8242,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -8274,7 +8274,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -11717,7 +11717,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -11743,7 +11743,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -11915,7 +11915,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -30948,7 +30948,7 @@
"resistance": 1200.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -30973,7 +30973,7 @@
"resistance": 1200.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -30998,7 +30998,7 @@
"resistance": 1200.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 10,
"filterLight": 15,
@@ -31023,7 +31023,7 @@
"resistance": 1200.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34549,7 +34549,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34576,7 +34576,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34603,7 +34603,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34630,7 +34630,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34657,7 +34657,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34684,7 +34684,7 @@
"resistance": 3.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34711,7 +34711,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34738,7 +34738,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34765,7 +34765,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34792,7 +34792,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34819,7 +34819,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34846,7 +34846,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34873,7 +34873,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34900,7 +34900,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34927,7 +34927,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34954,7 +34954,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -34981,7 +34981,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35008,7 +35008,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35035,7 +35035,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35100,7 +35100,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35165,7 +35165,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35230,7 +35230,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35295,7 +35295,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35338,7 +35338,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35381,7 +35381,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35424,7 +35424,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35467,7 +35467,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35494,7 +35494,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35521,7 +35521,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35548,7 +35548,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35575,7 +35575,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35602,7 +35602,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35629,7 +35629,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35656,7 +35656,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -35683,7 +35683,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35748,7 +35748,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35813,7 +35813,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35878,7 +35878,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35943,7 +35943,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -35986,7 +35986,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -36029,7 +36029,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -36072,7 +36072,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 0,
@@ -36115,7 +36115,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36182,7 +36182,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36249,7 +36249,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36316,7 +36316,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36383,7 +36383,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36450,7 +36450,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36517,7 +36517,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36584,7 +36584,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36651,7 +36651,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36714,7 +36714,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36777,7 +36777,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36840,7 +36840,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36903,7 +36903,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -36966,7 +36966,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37029,7 +37029,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37092,7 +37092,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37155,7 +37155,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37188,7 +37188,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37221,7 +37221,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37254,7 +37254,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37287,7 +37287,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37320,7 +37320,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37353,7 +37353,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37386,7 +37386,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -37419,7 +37419,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37457,7 +37457,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37495,7 +37495,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37533,7 +37533,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37571,7 +37571,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37609,7 +37609,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37647,7 +37647,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37685,7 +37685,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -37723,7 +37723,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": true,
"emitLight": 0,
"filterLight": 0,
@@ -39352,7 +39352,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -39379,7 +39379,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -39406,7 +39406,7 @@
"resistance": 6.0,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,
@@ -39653,7 +39653,7 @@
"resistance": 3.5,
"stackSize": 64,
"diggable": true,
- "material": "incorrect_for_wooden_tool",
+ "material": "mineable/pickaxe",
"transparent": false,
"emitLight": 0,
"filterLight": 15,

View file

@ -38,13 +38,13 @@ const settings = {
"code_timeout_mins": -1, // minutes code is allowed to run. -1 for no timeout
"relevant_docs_count": 5, // number of relevant code function docs to select for prompting. -1 for all
"max_messages": process.env.MAX_MESSAGES || 15, // max number of messages to keep in context
"num_examples": process.env.NUM_EXAMPLES || 2, // number of examples to give to the model
"max_messages": 15, // max number of messages to keep in context
"num_examples": 2, // number of examples to give to the model
"max_commands": -1, // max number of commands that can be used in consecutive responses. -1 for no limit
"verbose_commands": true, // show full command syntax
"narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
"chat_bot_messages": true, // publicly chat messages to other bots
"log_all_prompts": process.env.LOG_ALL || true, // log all prompts to file
"log_all_prompts": false, // log ALL prompts to file
}
// these environment variables override certain settings
@ -63,4 +63,14 @@ if (process.env.INSECURE_CODING) {
if (process.env.BLOCKED_ACTIONS) {
settings.blocked_actions = JSON.parse(process.env.BLOCKED_ACTIONS);
}
if (process.env.MAX_MESSAGES) {
settings.max_messages = process.env.MAX_MESSAGES;
}
if (process.env.NUM_EXAMPLES) {
settings.num_examples = process.env.NUM_EXAMPLES;
}
if (process.env.LOG_ALL) {
settings.log_all_prompts = process.env.LOG_ALL;
}
export default settings;

View file

@ -14,7 +14,7 @@ import { handleTranslation, handleEnglishTranslation } from '../utils/translator
import { addBrowserViewer } from './vision/browser_viewer.js';
import settings from '../../settings.js';
import { serverProxy } from './agent_proxy.js';
import { Task } from './tasks.js';
import { Task } from './tasks/tasks.js';
import { say } from './speak.js';
export class Agent {
@ -62,7 +62,6 @@ export class Agent {
} else {
taskStart = Date.now();
}
// incorporate new restart time into task
this.task = new Task(this, task_path, task_id, taskStart);
this.blocked_actions = settings.blocked_actions.concat(this.task.blocked_actions || []);
blacklistCommands(this.blocked_actions);
@ -105,8 +104,10 @@ export class Agent {
this.startEvents();
if (!load_mem) {
if (task_path !== null) {
this.task.initBotTask();
}
}
await new Promise((resolve) => setTimeout(resolve, 10000));
this.checkAllPlayersPresent();

View file

@ -2,7 +2,7 @@ import * as world from '../library/world.js';
import * as mc from '../../utils/mcdata.js';
import { getCommandDocs } from './index.js';
import convoManager from '../conversation.js';
import { checkLevelBlueprint, checkBlueprint } from '../task_types/construction_tasks.js';
import { checkLevelBlueprint, checkBlueprint } from '../tasks/construction_tasks.js';
import { load } from 'cheerio';
const pad = (str) => {

View file

@ -1054,6 +1054,7 @@ export function blueprintToTask(blueprint_data, num_agents) {
}
let give_agent = 0;
console.log("materials", blueprint_data.materials)
for (const key of Object.keys(blueprint_data.materials)) {
initialInventory[JSON.stringify(give_agent)][key] = blueprint_data.materials[key];
give_agent = (give_agent + 1) % num_agents;
@ -1063,7 +1064,7 @@ export function blueprintToTask(blueprint_data, num_agents) {
type: "construction",
goal: "Make a structure with the blueprint below",
conversation: "Let's share materials and make a structure with the blueprint",
agent_count: 2,
agent_count: num_agents,
blueprint: blueprint_data,
initial_inventory: initialInventory,
};

View file

@ -1,10 +1,9 @@
import { readFileSync , writeFileSync, existsSync} from 'fs';
import { executeCommand } from './commands/index.js';
import { getPosition } from './library/world.js';
import settings from '../../settings.js';
import { Vec3 } from 'vec3';
import { ConstructionTaskValidator, Blueprint } from './task_types/construction_tasks.js';
import { CookingTaskInitiator } from './task_types/cooking_tasks.js';
import { executeCommand } from '../commands/index.js';
import { getPosition } from '../library/world.js';
import settings from '../../../settings.js';
import { ConstructionTaskValidator, Blueprint } from './construction_tasks.js';
import { CookingTaskInitiator } from './cooking_tasks.js';
const PROGRESS_FILE = './hells_kitchen_progress.json';
@ -237,27 +236,23 @@ export class Task {
constructor(agent, task_path, task_id, taskStartTime = null) {
this.agent = agent;
this.data = null;
console.log("task start time", taskStartTime);
if (taskStartTime !== null)
this.taskStartTime = taskStartTime;
else
this.taskStartTime = Date.now();
console.log(this.taskStartTime);
this.validator = null;
this.reset_function = null;
this.blocked_actions = [];
this.task_id = task_id;
console.log('Task ID:', task_id);
if (task_path && task_id) {
console.log('Starting task', task_id);
if (task_id.endsWith('hells_kitchen')) {
// Reset hells_kitchen progress when a new task starts
if (task_id && task_id.endsWith('hells_kitchen')) {
hellsKitchenProgressManager.resetTask(task_id);
console.log('Reset Hells Kitchen progress for new task');
}
if (task_path && task_id) {
this.data = this.loadTask(task_path, task_id);
this.task_type = this.data.type;
if (this.task_type === 'construction' && this.data.blueprint) {
@ -292,6 +287,9 @@ export class Task {
if (this.conversation)
this.blocked_actions.push('!endConversation');
}
else {
console.log('No task.');
}
this.name = this.agent.name;
this.available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name);

View file

@ -21,7 +21,7 @@ export class GPT {
async sendRequest(turns, systemMessage, stop_seq='***') {
let messages = [{'role': 'system', 'content': systemMessage}].concat(turns);
messages = strictFormat(messages);
const pack = {
model: this.model_name || "gpt-3.5-turbo",
messages,
@ -29,7 +29,6 @@ export class GPT {
...(this.params || {})
};
if (this.model_name.includes('o1')) {
pack.messages = strictFormat(messages);
delete pack.stop;
}

View file

@ -52,6 +52,24 @@ export class OpenRouter {
return res;
}
async sendVisionRequest(messages, systemMessage, imageBuffer) {
const imageMessages = [...messages];
imageMessages.push({
role: "user",
content: [
{ type: "text", text: systemMessage },
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`
}
}
]
});
return this.sendRequest(imageMessages, systemMessage);
}
async embed(text) {
throw new Error('Embeddings are not supported by Openrouter.');
}

View file

@ -8,6 +8,13 @@ import argparse
from tqdm import tqdm
import glob
# Calculate project root directory
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Define output directory for analysis results
analysis_output_dir = os.path.join(project_root, "experiments", "analysis_results")
# Ensure the output directory exists
os.makedirs(analysis_output_dir, exist_ok=True)
def download_s3_folders(bucket_name, s3_prefix, local_base_dir):
"""
Downloads groups of folders from S3 based on the next level of prefixes.
@ -23,6 +30,10 @@ def download_s3_folders(bucket_name, s3_prefix, local_base_dir):
s3_client = boto3.client('s3')
downloaded_folders = []
# Ensure local_base_dir is relative to project root if not absolute
if not os.path.isabs(local_base_dir):
local_base_dir = os.path.join(project_root, local_base_dir)
try:
# List objects with the prefix, delimited by '/' to find sub-prefixes (folders)
response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=s3_prefix, Delimiter='/')
@ -207,42 +218,61 @@ def aggregate_results(local_folders):
}
def get_immediate_subdirectories(a_dir):
# Ensure a_dir is relative to project root if not absolute
if not os.path.isabs(a_dir):
a_dir = os.path.join(project_root, a_dir)
return [os.path.join(a_dir, name) for name in os.listdir(a_dir)
if os.path.isdir(os.path.join(a_dir, name))]
# --- Main Execution ---
if __name__ == "__main__":
# 1. Download folders from AWS
# 1. Download folders from AWS or use local directory
parser = argparse.ArgumentParser()
parser.add_argument('--s3_download', action="store_true", help='Download folders from S3')
parser.add_argument('--aws_bucket_name', default="mindcraft" , type=str, help='AWS bucket name')
parser.add_argument('--s3_folder_prefix', default="", type=str, help='S3 folder prefix')
parser.add_argument('--local_download_dir', default="results/", type=str, help='Local download directory')
# Change default input dir to 'experiments' relative to project root
parser.add_argument('--local_download_dir', default="experiments", type=str, help='Local directory containing results (relative to project root)')
args = parser.parse_args()
AWS_BUCKET_NAME = args.aws_bucket_name
S3_FOLDER_PREFIX = args.s3_folder_prefix
if args.local_download_dir != "":
LOCAL_DOWNLOAD_DIR = args.local_download_dir + f"/{S3_FOLDER_PREFIX.replace('/', '_')}"
# Resolve local_download_dir relative to project root
local_download_dir_abs = args.local_download_dir
if not os.path.isabs(local_download_dir_abs):
local_download_dir_abs = os.path.join(project_root, local_download_dir_abs)
# Construct LOCAL_DOWNLOAD_DIR based on the absolute path
if args.local_download_dir != "": # Original check seems redundant now, but kept logic
LOCAL_DOWNLOAD_DIR = local_download_dir_abs # Already includes prefix if s3_download
if args.s3_download and S3_FOLDER_PREFIX: # Append S3 prefix if downloading
LOCAL_DOWNLOAD_DIR = os.path.join(local_download_dir_abs, S3_FOLDER_PREFIX.replace('/', '_').rstrip('_'))
else:
LOCAL_DOWNLOAD_DIR = args.local_download_dir
LOCAL_DOWNLOAD_DIR = local_download_dir_abs # Should not happen with default
if (args.s3_download):
print(f"Downloading folders from s3://{args.aws_bucket_name}/{args.s3_folder_prefix} to {args.local_download_dir}...")
folders = download_s3_folders(args.aws_bucket_name, args.s3_folder_prefix, args.local_download_dir)
print(f"Downloading folders from s3://{AWS_BUCKET_NAME}/{S3_FOLDER_PREFIX} to {LOCAL_DOWNLOAD_DIR}...")
# Pass the absolute base path for downloads
folders = download_s3_folders(AWS_BUCKET_NAME, S3_FOLDER_PREFIX, local_download_dir_abs)
else:
folders = get_immediate_subdirectories(args.local_download_dir)
folders = get_immediate_subdirectories(local_download_dir_abs)
print(folders)
if not folders:
print("No folders found or downloaded. Exiting.")
exit()
results = aggregate_results(folders)
print(results)
# Save results to a file
os.makedirs(LOCAL_DOWNLOAD_DIR, exist_ok=True)
with open(LOCAL_DOWNLOAD_DIR + "/results.txt", "w") as file:
# Hardcode output path within experiments/analysis_results/
results_file_path = os.path.join(analysis_output_dir, "analyse_results_output.txt")
with open(results_file_path, "w") as file:
file.write("Results\n")
for key, value in results.items():
file.write(f"{key}: {value}\n")
print("Results saved to results.txt")
print(f"Results saved to {results_file_path}")
# if not downloaded_local_folders:
# print("No folders downloaded. Exiting.")
# exit()

View file

@ -3,6 +3,16 @@ import json
from collections import defaultdict
from prettytable import PrettyTable
import re
import argparse
import pandas as pd
import glob
# Calculate project root directory
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Define output directory for analysis results
analysis_output_dir = os.path.join(project_root, "experiments", "analysis_results")
# Ensure the output directory exists
os.makedirs(analysis_output_dir, exist_ok=True)
def extract_success_scores(folders, model_names):
assert len(folders) == len(model_names), "Folders and model names lists must have the same length."
@ -173,7 +183,49 @@ def extract_success_scores(folders, model_names):
display_table("Average Success Score by Room", avg_room_scores)
display_table("Average Success Score by (Material, Room) Tuples", avg_material_room_scores, tuple_keys=True)
# Example usage
folders = ["experiments/gpt-4o_construction_tasks", "experiments/claude-3-5-sonnet-latest_construction_tasks"]
model_names = ["GPT-4o", "Claude 3.5 sonnet"]
extract_success_scores(folders, model_names)
def analyze_construction_log(log_file):
# ... existing code ...
pass
def main():
parser = argparse.ArgumentParser(description='Analyze construction task logs.')
# Change default input dir to 'experiments' relative to project root
parser.add_argument('--log_dir', type=str, default='experiments',
help='Directory containing the log files (relative to project root)')
# Removed --output_file argument
# parser.add_argument('--output_file', type=str, default='construction_analysis_results.csv',
# help='Output CSV file name (relative to project root)')
args = parser.parse_args()
# Resolve log_dir path relative to project root
log_dir_abs = args.log_dir
if not os.path.isabs(log_dir_abs):
log_dir_abs = os.path.join(project_root, log_dir_abs)
# Hardcode output file path
output_file_abs = os.path.join(analysis_output_dir, "construction_analysis.csv")
all_results = []
# Use absolute log directory path
log_pattern = os.path.join(log_dir_abs, '*.json')
print(f"Searching for logs in: {log_pattern}")
log_files_found = glob.glob(log_pattern)
print(f"Found {len(log_files_found)} log files.")
for log_file in log_files_found:
results = analyze_construction_log(log_file)
if results:
all_results.append(results)
if all_results:
df = pd.DataFrame(all_results)
# Ensure the output directory exists (already done at top)
# os.makedirs(os.path.dirname(output_file_abs), exist_ok=True)
# Save to hardcoded absolute output file path
df.to_csv(output_file_abs, index=False)
print(f"Analysis complete. Results saved to {output_file_abs}")
else:
print("No results generated from log files.")
if __name__ == "__main__":
main()

View file

@ -3,6 +3,16 @@ import json
import re
from collections import defaultdict
from prettytable import PrettyTable
import pandas as pd
import glob
import argparse
# Calculate project root directory
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Define output directory for analysis results
analysis_output_dir = os.path.join(project_root, "experiments", "analysis_results")
# Ensure the output directory exists
os.makedirs(analysis_output_dir, exist_ok=True)
def extract_cooking_items(exp_dir):
"""Extract cooking items from experiment directory name."""
@ -359,66 +369,52 @@ def generate_item_blocked_data(experiments_root):
return item_blocked_data, ignored_tasks
def analyze_cooking_log(log_file):
# Placeholder for the actual analysis logic if it exists
# This function needs to be implemented based on the script's purpose
print(f"Analyzing {log_file}...") # Example print
# Example: return a dictionary of results
return {"file": os.path.basename(log_file), "score": 1} # Dummy result
def main():
# Define lists for model directories and corresponding model names
model_dirs = [
"experiments/gpt-4o_2agent_NEW_cooking_tasks",
# "experiments/claude-3-5-sonnet_2agent_NEW_cooking_tasks",
# "experiments/claude-3-5-sonnet_3agent_NEW_cooking_tasks",
"experiments/gpt-4o_3agent_NEW_cooking_tasks",
# "experiments/1_claude-3-5-sonnet_4agents_NEW_cooking_tasks",
"experiments/gpt-4o_4agents_NEW_cooking_tasks",
"experiments/gpt-4o_5agents_NEW_cooking_tasks",
# "experiments/"
]
model_names = [
"GPT-4o-2agent",
# "Claude-3.5-2agent",
"GPT-4o-3agent",
# "Claude-3.5-3agent",
# "Claude-3.5-4agent",
"GPT-4o-4agent",
"GPT-4o-5agent",
# "Another-Model"
]
parser = argparse.ArgumentParser(description='Analyze cooking task logs.')
# Change default input dir to 'experiments' relative to project root
parser.add_argument('--log_dir', type=str, default='experiments',
help='Directory containing the log files (relative to project root)')
# Removed --output_file argument
# parser.add_argument('--output_file', type=str, default='cooking_analysis_results.csv',
# help='Output CSV file name (relative to project root)')
args = parser.parse_args()
# Ensure both lists are of the same size
if len(model_dirs) != len(model_names):
print("Error: The number of model directories and model names must be the same.")
return
# Resolve log_dir path relative to project root
log_dir_abs = args.log_dir
if not os.path.isabs(log_dir_abs):
log_dir_abs = os.path.join(project_root, log_dir_abs)
# Analyze each model directory
models_blocked_results = {}
models_item_results = {}
all_cooking_items = set()
total_ignored_tasks = 0
# Hardcode output file path
output_file_abs = os.path.join(analysis_output_dir, "cooking_analysis.csv")
for model_dir, model_name in zip(model_dirs, model_names):
print(f"Analyzing {model_name} experiments in: {model_dir}")
all_results = []
# Use absolute log directory path
log_pattern = os.path.join(log_dir_abs, '*.json')
print(f"Searching for logs in: {log_pattern}")
log_files_found = glob.glob(log_pattern)
print(f"Found {len(log_files_found)} log files.")
blocked_results, item_results, unique_items, ignored_tasks = analyze_experiments(model_dir, model_name)
for log_file in log_files_found:
results = analyze_cooking_log(log_file)
if results:
all_results.append(results) # Append the results dictionary
models_blocked_results[model_name] = blocked_results
models_item_results[model_name] = item_results
all_cooking_items.update(unique_items)
total_ignored_tasks += len(ignored_tasks)
if ignored_tasks:
print(f" - {model_name}: Ignored {len(ignored_tasks)} tasks with no score information.")
# Print summary of ignored tasks
if total_ignored_tasks > 0:
print(f"\nTotal ignored tasks (missing score information): {total_ignored_tasks}")
# Print the comparison tables
print_model_comparison_blocked(models_blocked_results)
print_model_comparison_items(models_item_results, all_cooking_items)
# Print overall statistics
print("\nUnique Cooking Items Found:")
print("=" * 60)
print(", ".join(sorted(all_cooking_items)))
print(f"Total unique items: {len(all_cooking_items)}")
if all_results:
df = pd.DataFrame(all_results)
# Ensure the output directory exists
os.makedirs(os.path.dirname(output_file_abs), exist_ok=True)
# Save to hardcoded absolute output file path
df.to_csv(output_file_abs, index=False)
print(f"Analysis complete. Results saved to {output_file_abs}")
else:
print("No results generated from log files.")
if __name__ == "__main__":
main()

View file

@ -8,6 +8,14 @@ import argparse
from tqdm import tqdm
import glob
from prettytable import PrettyTable
import pandas as pd
# Calculate project root directory
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Define output directory for analysis results
analysis_output_dir = os.path.join(project_root, "experiments", "analysis_results")
# Ensure the output directory exists
os.makedirs(analysis_output_dir, exist_ok=True)
def download_s3_folders(bucket_name, s3_prefix, local_base_dir):
"""
@ -24,6 +32,10 @@ def download_s3_folders(bucket_name, s3_prefix, local_base_dir):
s3_client = boto3.client('s3')
downloaded_folders = []
# Ensure local_base_dir is relative to project root if not absolute
if not os.path.isabs(local_base_dir):
local_base_dir = os.path.join(project_root, local_base_dir)
try:
# List objects with the prefix, delimited by '/' to find sub-prefixes (folders)
response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=s3_prefix, Delimiter='/')
@ -69,15 +81,16 @@ def analyze_json_file(file_path):
file_path (str): Path to the JSON file.
Returns:
str or None: The task outcome string if found, otherwise None.
bool: True if task was successful, False otherwise.
"""
try:
with open(file_path, 'r') as f:
data = json.load(f)
if 'turns' in data and isinstance(data['turns'], list):
for turn in reversed(data['turns']): # Check turns from the end
for turn in data['turns']: # Check all turns, not just from the end
if turn.get('role') == 'system' and isinstance(turn.get('content'), str):
if "Task successful ended with code : 2" in turn['content'] or "Task ended with score : 1" in turn["content"] or "Task ended in score: 1" in turn["content"]:
# print(f"Success found in {file_path}")
return True
return False
except FileNotFoundError:
@ -93,18 +106,17 @@ def analyze_json_file(file_path):
def extract_result(folder_path):
folder_name = os.path.basename(folder_path)
json_files = glob.glob(os.path.join(folder_path, "*.json"))
assert len(json_files) == 2, f"Expected 2 json files in {folder_name}, found {len(json_files)}"
if not json_files:
print(f"No JSON files found in {folder_name}")
return None
else:
outcome = False
# Check each JSON file in the folder for success indication
for json_file in json_files:
outcome = analyze_json_file(json_file)
if outcome:
if outcome: # If any file indicates success, return True
return True
return False
return False # Return False only if no files indicate success
def is_base(folder_path):
return "full_plan" in folder_path and "depth_0" in folder_path and "missing" not in folder_path
@ -240,6 +252,9 @@ def aggregate_results(local_folders):
}
def get_immediate_subdirectories(a_dir):
# Ensure a_dir is relative to project root if not absolute
if not os.path.isabs(a_dir):
a_dir = os.path.join(project_root, a_dir)
return [os.path.join(a_dir, name) for name in os.listdir(a_dir)
if os.path.isdir(os.path.join(a_dir, name))]
@ -285,29 +300,49 @@ def create_pretty_tables(results):
return overall_table.get_string() + "\n\n" + depth_table.get_string() + "\n\n" + plan_table.get_string()
# --- Main Execution ---
if __name__ == "__main__":
# 1. Download folders from AWS
def analyze_crafting_log(log_file):
# ... existing code ...
pass
def main():
# 1. Download folders from AWS or use local directory
parser = argparse.ArgumentParser()
parser.add_argument('--s3_download', action="store_true", help='Download folders from S3')
parser.add_argument('--aws_bucket_name', default="mindcraft" , type=str, help='AWS bucket name')
parser.add_argument('--s3_folder_prefix', default="", type=str, help='S3 folder prefix')
parser.add_argument('--local_download_dir', default="results/", type=str, help='Local download directory')
# Change default input dir to 'experiments' relative to project root
parser.add_argument('--local_download_dir', default="experiments", type=str, help='Local directory containing results (relative to project root)')
args = parser.parse_args()
AWS_BUCKET_NAME = args.aws_bucket_name
S3_FOLDER_PREFIX = args.s3_folder_prefix
# Resolve local_download_dir relative to project root
local_download_dir_abs = args.local_download_dir
if not os.path.isabs(local_download_dir_abs):
local_download_dir_abs = os.path.join(project_root, local_download_dir_abs)
# Construct LOCAL_DOWNLOAD_DIR based on the absolute path
# This directory will be used for results aggregation and saving output files
if args.local_download_dir != "":
LOCAL_DOWNLOAD_DIR = args.local_download_dir + f"/{S3_FOLDER_PREFIX.replace('/', '_')}"
LOCAL_DOWNLOAD_DIR = local_download_dir_abs # Base results directory
if args.s3_download and S3_FOLDER_PREFIX: # Append S3 prefix if downloading to keep results separate
LOCAL_DOWNLOAD_DIR = os.path.join(local_download_dir_abs, S3_FOLDER_PREFIX.replace('/', '_').rstrip('_'))
else:
LOCAL_DOWNLOAD_DIR = args.local_download_dir
LOCAL_DOWNLOAD_DIR = local_download_dir_abs # Should not happen with default
if (args.s3_download):
print(f"Downloading folders from s3://{args.aws_bucket_name}/{args.s3_folder_prefix} to {args.local_download_dir}...")
folders = download_s3_folders(args.aws_bucket_name, args.s3_folder_prefix, args.local_download_dir)
print(f"Downloading folders from s3://{AWS_BUCKET_NAME}/{S3_FOLDER_PREFIX} to {LOCAL_DOWNLOAD_DIR}...")
# Pass the absolute base path for downloads, download_s3_folders handles subfolder creation
folders = download_s3_folders(AWS_BUCKET_NAME, S3_FOLDER_PREFIX, local_download_dir_abs)
else:
folders = get_immediate_subdirectories(args.local_download_dir)
print(folders)
# Use the absolute path to get subdirectories
folders = get_immediate_subdirectories(local_download_dir_abs)
print(f"Found local folders: {folders}")
if not folders:
print("No folders found or downloaded. Exiting.")
exit()
results = aggregate_results(folders)
print(results)
@ -316,17 +351,29 @@ if __name__ == "__main__":
tables_output = create_pretty_tables(results)
print("\n" + tables_output)
# Save results to files
os.makedirs(LOCAL_DOWNLOAD_DIR, exist_ok=True)
# Save results to files within the hardcoded experiments/analysis_results/ directory
# os.makedirs(LOCAL_DOWNLOAD_DIR, exist_ok=True) # Output dir created at top
# Save raw results
with open(LOCAL_DOWNLOAD_DIR + "/results.txt", "w") as file:
# Determine filename based on S3 prefix or local dir name if possible
if S3_FOLDER_PREFIX:
results_filename_base = S3_FOLDER_PREFIX.replace('/', '_').rstrip('_')
else:
results_filename_base = os.path.basename(local_download_dir_abs) if local_download_dir_abs else "local"
results_filename_base = f"crafting_analysis_{results_filename_base}"
results_file_path = os.path.join(analysis_output_dir, f"{results_filename_base}_results.txt")
with open(results_file_path, "w") as file:
file.write("Results\n")
for key, value in results.items():
file.write(f"{key}: {value}\n")
# Save pretty tables
with open(LOCAL_DOWNLOAD_DIR + "/results_tables.txt", "w") as file:
tables_file_path = os.path.join(analysis_output_dir, f"{results_filename_base}_tables.txt")
with open(tables_file_path, "w") as file:
file.write(tables_output)
print("Results saved to results.txt and tables saved to results_tables.txt")
print(f"Results saved to {results_file_path} and tables saved to {tables_file_path}")
if __name__ == "__main__":
main()

View file

@ -31,4 +31,4 @@ The generation code is documented to help with customization.
- `profiles/task_construct.json` - Default configuration profile
- `tasks/construction_tasks/test_multiagent_construction_tasks.json` - Training task definitions (initalized with 5 variants)
- `tasks/construction_tasks/test_multiagent_construction_tasks.json` - Test task definitions (initalized with 1 variant)
- `src/agent/task_types/construction_tasks.js` - Blueprint Class, Construction Validation Class, and Procedural Generation Function
- `src/agent/tasks/construction_tasks.js` - Blueprint Class, Construction Validation Class, and Procedural Generation Function

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,699 @@
{
"pyramid_three_agents": {
"type": "construction",
"goal": "Make a structure with the blueprint below",
"conversation": "Let's share materials and make a structure with the blueprint",
"agent_count": 3,
"blueprint": {
"materials": {
"polished_granite": 1,
"gold_block": 27,
"stone_bricks": 41,
"polished_andesite": 34,
"quartz_block": 16,
"stone": 23,
"polished_diorite": 21,
"quartz_pillar": 2,
"glowstone": 3
},
"levels": [
{
"level": 0,
"coordinates": [
-60,
-60,
6
],
"placement": [
[
"polished_granite",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"gold_block",
"stone_bricks",
"polished_andesite",
"gold_block",
"quartz_block",
"polished_andesite",
"gold_block",
"stone_bricks",
"gold_block"
],
[
"air",
"stone_bricks",
"polished_andesite",
"stone",
"polished_diorite",
"polished_andesite",
"stone",
"stone_bricks",
"polished_diorite",
"gold_block"
],
[
"air",
"polished_andesite",
"stone",
"polished_diorite",
"polished_andesite",
"stone",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks"
],
[
"air",
"gold_block",
"polished_diorite",
"polished_andesite",
"stone",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks",
"polished_andesite"
],
[
"air",
"quartz_block",
"polished_andesite",
"stone",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks",
"polished_andesite",
"quartz_block"
],
[
"air",
"polished_andesite",
"stone",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks",
"polished_andesite",
"polished_diorite",
"stone_bricks"
],
[
"air",
"gold_block",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks",
"polished_andesite",
"polished_diorite",
"stone_bricks",
"polished_andesite"
],
[
"air",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks",
"polished_andesite",
"polished_diorite",
"stone_bricks",
"polished_andesite",
"gold_block"
],
[
"air",
"gold_block",
"gold_block",
"stone_bricks",
"polished_andesite",
"quartz_block",
"stone_bricks",
"polished_andesite",
"gold_block",
"gold_block"
]
]
},
{
"level": 1,
"coordinates": [
-60,
-59,
6
],
"placement": [
[
"quartz_pillar",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"gold_block",
"stone_bricks",
"polished_andesite",
"quartz_block",
"stone_bricks",
"polished_andesite",
"gold_block",
"air"
],
[
"air",
"air",
"stone_bricks",
"stone",
"polished_diorite",
"polished_andesite",
"stone",
"stone_bricks",
"stone_bricks",
"air"
],
[
"air",
"air",
"polished_andesite",
"polished_diorite",
"polished_andesite",
"stone",
"stone_bricks",
"polished_diorite",
"polished_andesite",
"air"
],
[
"air",
"air",
"quartz_block",
"polished_andesite",
"stone",
"glowstone",
"polished_diorite",
"stone",
"quartz_block",
"air"
],
[
"air",
"air",
"stone_bricks",
"stone",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks",
"stone_bricks",
"air"
],
[
"air",
"air",
"polished_andesite",
"stone_bricks",
"polished_diorite",
"stone",
"stone_bricks",
"polished_andesite",
"polished_andesite",
"air"
],
[
"air",
"air",
"gold_block",
"stone_bricks",
"polished_andesite",
"quartz_block",
"stone_bricks",
"polished_andesite",
"gold_block",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
]
]
},
{
"level": 2,
"coordinates": [
-60,
-58,
6
],
"placement": [
[
"quartz_pillar",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"gold_block",
"stone_bricks",
"quartz_block",
"gold_block",
"gold_block",
"air",
"air"
],
[
"air",
"air",
"air",
"stone_bricks",
"polished_diorite",
"polished_andesite",
"stone",
"polished_andesite",
"air",
"air"
],
[
"air",
"air",
"air",
"quartz_block",
"polished_andesite",
"glowstone",
"stone_bricks",
"quartz_block",
"air",
"air"
],
[
"air",
"air",
"air",
"gold_block",
"stone",
"stone_bricks",
"polished_diorite",
"stone_bricks",
"air",
"air"
],
[
"air",
"air",
"air",
"gold_block",
"polished_andesite",
"quartz_block",
"stone_bricks",
"gold_block",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
]
]
},
{
"level": 3,
"coordinates": [
-60,
-57,
6
],
"placement": [
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"gold_block",
"quartz_block",
"gold_block",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"quartz_block",
"glowstone",
"quartz_block",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"gold_block",
"quartz_block",
"gold_block",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
]
]
},
{
"level": 4,
"coordinates": [
-60,
-56,
6
],
"placement": [
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"gold_block",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
],
[
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air",
"air"
]
]
}
]
},
"initial_inventory": {
"0": {
"diamond_pickaxe": 1,
"diamond_axe": 1,
"diamond_shovel": 1,
"polished_granite": 1,
"polished_andesite": 34,
"polished_diorite": 21
},
"1": {
"diamond_pickaxe": 1,
"diamond_axe": 1,
"diamond_shovel": 1,
"gold_block": 27,
"quartz_block": 16,
"quartz_pillar": 2
},
"2": {
"diamond_pickaxe": 1,
"diamond_axe": 1,
"diamond_shovel": 1,
"stone_bricks": 41,
"stone": 23,
"glowstone": 3
}
}
}
}

View file

@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
import {proceduralGeneration} from "../../src/agent/task_types/construction_tasks.js";
import {proceduralGeneration} from "../../src/agent/tasks/construction_tasks.js";
//note 'main' (script to run generation of tasks) is at bottom of page

View file

@ -1,5 +1,5 @@
import mineflayer from 'mineflayer';
import { worldToBlueprint, blueprintToTask } from '../../src/agent/task_types/construction_tasks.js';
import { worldToBlueprint, blueprintToTask } from '../../src/agent/tasks/construction_tasks.js';
import fs from 'fs';
import { start } from 'repl';
@ -14,32 +14,33 @@ bot.on('spawn', async () => {
console.log("Bot spawned. Starting blueprint check...");
// set this to be minX, minY, minZ
const startCoord = {
x: -60,
x: -124,
y: 1,
z: 6,
z: 133,
}
bot.chat(`/tp andy ${startCoord.x} ${startCoord.y} ${startCoord.z}`);
const yOffset = 5;
const xOffset = 10;
const zOffset = 10;
const yOffset = 2;
const xOffset = 30;
const zOffset = 20;
const taskFilePath = '/Users/isadorawhite/izzy_mindcraft/mindcraft/tasks/construction_tasks/custom/pyramid.json';
const task_name = "pyramid";
const taskFilePath = '/Users/isadorawhite/izzy_mindcraft/mindcraft/tasks/construction_tasks/custom/flower_three_agents.json';
const task_name = "flower_three_agents";
setTimeout(async () => {
let task_blueprint = await worldToBlueprint(startCoord, yOffset, xOffset, zOffset, bot);
for (const level of task_blueprint.levels) {
for (let i = 0; i < task_blueprint.levels.length; i++) {
// Perform operations on each level
const level = task_blueprint.levels[i];
console.log("Level coordinates:", level.coordinates);
const new_coordinates = [level.coordinates[0], -60, level.coordinates[2]];
const new_coordinates = [level.coordinates[0], -60 + i, level.coordinates[2]];
level.coordinates = new_coordinates;
console.log("New coordinates:", level.coordinates);
}
console.log("Blueprint generated:", task_blueprint.levels[0].coordinates);
const task = blueprintToTask(task_blueprint, 2);
const task = blueprintToTask(task_blueprint, 3);
const task_collection = {}
task_collection[task_name] = task;

View file

@ -16,6 +16,11 @@ import socket
from tqdm import tqdm
import boto3
# Calculate project root directory
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Define tasks directory
tasks_dir = os.path.dirname(os.path.abspath(__file__))
BLOCKED_ACTIONS_COOKING = [
'!activate', '!attackPlayer', '!checkBlueprint', '!checkBlueprintLevel',
'!clearChat', '!clearFurnace', '!consume', '!craftable', '!discard',
@ -57,7 +62,6 @@ def analyze_json_file(file_path):
for turn in data["turns"]:
if turn.get("role") == "system" and "content" in turn:
if isinstance(turn["content"], str) and "Task ended with score : " in turn["content"]:
score_found = True
if "Task ended with score : 1" in turn["content"]:
return 1
elif "Task ended with score : 0" in turn["content"]:
@ -66,7 +70,8 @@ def analyze_json_file(file_path):
score = float(turn["content"].split(":")[-1].strip())
return score
return False
return None
except FileNotFoundError:
print(f"Error: File not found: {file_path}")
return None
@ -86,11 +91,14 @@ def extract_result(folder_path):
return None
else:
score = None
curr_score = 0
for json_file in json_files:
score = analyze_json_file(json_file)
if score is not None:
return score
return 0
max_score = max(score, curr_score)
curr_score = max_score
return curr_score
def aggregate_results(local_folders):
"""
@ -106,24 +114,98 @@ def aggregate_results(local_folders):
total = 0
successful = 0
successful_tasks = []
task_type = local_folders[0].split("/")[-2]
if "cooking" in task_type:
task_type = "cooking"
elif "techtree" in task_type:
task_type = "techtree"
elif "construction" in task_type:
task_type = "construction"
for folder_path in tqdm(local_folders):
folder_name = os.path.basename(folder_path)
try:
result = extract_result(folder_path)
if result == 1:
successful_tasks.append(folder_name)
if result is not None:
total += 1
successful += result
except Exception as e:
print(f"Error processing {folder_name}: {e}")
successful_tasks.sort()
if task_type == "construction":
successful = successful / total
return {
"total": total,
"successful": successful,
}
def check_folder_results(folder_path):
"""
Evaluate all JSON files in a folder and its subfolders and calculate success metrics.
Args:
folder_path (str): Path to the folder containing JSON log files.
Returns:
dict: A dictionary with success metrics.
"""
print(f"Checking results in folder: {folder_path}")
# Check if the folder exists
if not os.path.exists(folder_path):
print(f"Error: Folder not found: {folder_path}")
return None
# Find all subfolders (task IDs) in the given folder
if os.path.isdir(folder_path):
subfolders = [f for f in glob.glob(os.path.join(folder_path, "*")) if os.path.isdir(f)]
if subfolders:
# If there are subfolders, evaluate each subfolder
print(f"Found {len(subfolders)} subfolders to evaluate")
results = aggregate_results(subfolders)
else:
# If no subfolders, treat the folder itself as a results folder
print("No subfolders found, evaluating the folder itself")
results = aggregate_results([folder_path])
# Calculate success rate
if results["total"] > 0:
results["success_rate"] = results["successful"] / results["total"]
else:
results["success_rate"] = 0.0
# Print summary
print("\n=== Evaluation Results ===")
print(f"Total tasks evaluated: {results['total']}")
if "construction" not in folder_path:
print(f"Successful tasks: {results['successful']}")
if "construction" not in folder_path:
print(f"Success rate: {results['success_rate']:.2f}")
else:
print(f"Success rate: {results['successful']:.2f}")
return results
else:
print(f"Error: {folder_path} is not a directory")
return None
def read_settings(file_path):
"""Read and parse the settings.js file to get agent profiles."""
# Ensure file_path is absolute or relative to project_root
if not os.path.isabs(file_path):
file_path = os.path.join(project_root, file_path)
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
@ -151,7 +233,10 @@ def read_settings(file_path):
def update_keys_json():
"""Update the keys.json file with the specified key-value pair."""
with open("keys.example.json", 'r', encoding='utf-8') as file:
keys_example_path = os.path.join(project_root, "keys.example.json")
keys_path = os.path.join(project_root, "keys.json")
with open(keys_example_path, 'r', encoding='utf-8') as file:
content = file.read()
data = json.loads(content)
@ -161,7 +246,7 @@ def update_keys_json():
if env_value: # If the variable exists, update it
data[key] = env_value
with open("keys.json", 'w', encoding='utf-8') as file:
with open(keys_path, 'w', encoding='utf-8') as file:
json.dump(data, file, indent=4)
def set_environment_variable_tmux_session(session_name, key, value):
@ -186,6 +271,14 @@ def launch_parallel_experiments(task_path,
block_conversation=False,
run_in_tmux=True):
# Resolve relative template_profile path
if not os.path.isabs(template_profile):
template_profile = os.path.join(project_root, template_profile)
# Resolve relative task_path path
if not os.path.isabs(task_path):
task_path = os.path.join(project_root, task_path)
with open(task_path, 'r', encoding='utf-8') as file:
content = file.read()
json_data = json.loads(content)
@ -294,19 +387,16 @@ def launch_server_experiment(task_path,
block_conversation=False,
run_in_tmux=True):
"""
Launch a Minecraft server and run experiments on it.
@param task_path: Path to the task file
@param task_ids: IDs of the tasks to run
@param num_exp: Number of experiments to run
@param server: Tuple containing server path and port
@param experiments_folder: Folder to store experiment results
@param exp_name: Name of the experiment for wandb dataset
@param num_agents: Number of agents to run
@param model: Model to use for the agents
@param s3: Boolean flag to enable S3 upload
@param bucket_name: Name of the S3 bucket
"""
# Resolve relative template_profile path
if not os.path.isabs(template_profile):
template_profile = os.path.join(project_root, template_profile)
# Resolve relative task_path path
if not os.path.isabs(task_path):
task_path = os.path.join(project_root, task_path)
experiments_folder = os.path.join(project_root, experiments_folder)
server_path, server_port = server
edit_file(os.path.join(server_path, "server.properties"), {"server-port": server_port})
mindserver_port = server_port - 55916 + 8080
@ -446,55 +536,66 @@ def run_script(task_path,
s3_path="mindcraft-experiments",
session_name="0",
run_in_tmux=True,):
script_content = ""
# Resolve relative task_path path
if not os.path.isabs(task_path):
task_path = os.path.join(project_root, task_path)
# Resolve relative experiments_folder path
if not os.path.isabs(experiments_folder):
experiments_folder = os.path.join(project_root, experiments_folder)
# Resolve relative server_path path
if not os.path.isabs(server_path):
server_path = os.path.join(project_root, server_path)
# Construct command (assuming main.js is in root)
main_js_path = os.path.join(project_root, "main.js")
for exp in range(num_exp):
for task_id in task_ids:
# Create a separate folder for each task_id
task_folder = os.path.join(experiments_folder, str(task_id))
os.makedirs(task_folder, exist_ok=True)
assert os.path.exists(task_folder), f"Directory {task_folder} was not created"
print(f"Created directory: {task_folder}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
exp_folder = os.path.join(experiments_folder, f"{task_id}_{exp}_{timestamp}")
# Need to create the folder first if using subprocess and cwd
os.makedirs(exp_folder, exist_ok=True)
cmd = [
"node", main_js_path,
"--task_path", task_path,
"--task_id", task_id,
"--agent_name", agent_names[0],
"--agent_name", agent_names[1],
"--server", server_path,
"--logs_path", exp_folder, # Ensure logs_path is absolute or handled by main.js relative to root
]
cmd = f"node main.js --task_path \'{task_path}\' --task_id {task_id}"
cp_cmd = f"cp {agent_names[0]}.json {server_path}bots/{agent_names[0]}/profile.json"
for _ in range(num_exp):
script_content += f"{cmd}\n"
script_content += "sleep 2\n"
for agent in agent_names:
agent_file_path = os.path.join(task_folder, f"{agent}_{_}.json")
script_content += f"echo 'Saving to {agent_file_path}'\n"
cp_cmd = f"cp bots/{agent}/memory.json {agent_file_path}"
script_content += f"echo '{cp_cmd}'\n"
script_content += f"{cp_cmd}\n"
script_content += "sleep 1\n"
if s3:
s3_cmd = f"aws s3 cp {agent_file_path} s3://{s3_path}/{task_id}/{agent}_{_}.json"
script_content += f"echo 'Uploading {agent_file_path} to S3'\n"
script_content += f"echo '{s3_cmd}'\n"
script_content += f"{s3_cmd}\n"
script_content += "sleep 1\n"
script_content += f"sleep 10\n"
if s3:
for agent in agent_names:
script_content += f"aws s3 cp bots/{agent} s3://{s3_path}/bots/{agent} --recursive\n"
cmd.extend(["--s3", "--s3_path", s3_path])
# Create a temporary shell script file
script_file = f"./tmp/experiment_script_{session_name}.sh"
make_script_file_and_run(script_content, script_file, session_name=session_name, run_in_tmux=run_in_tmux)
script_content = " ".join(cmd)
make_script_file_and_run(script_content, file_name=f"exp_{exp}_{task_id}_{timestamp}.sh", session_name=session_name, run_in_tmux=run_in_tmux)
print(f"Launched Experiment {exp+1}/{num_exp} for Task {task_id}")
time.sleep(1) # Stagger launches
def make_ops(agent_names, session_name):
"""Make the agents operators in the Minecraft world."""
print('Making agents operators...')
cmd = f"node main.js --task_path tasks/example_tasks.json --task_id debug_{len(agent_names)}_agent_timeout"
# Construct path to example tasks relative to project_root
example_task_path = os.path.join(project_root, "tasks/example_tasks.json")
cmd = f"node {os.path.join(project_root, 'main.js')} --task_path {example_task_path} --task_id debug_{len(agent_names)}_agent_timeout"
subprocess.run(["tmux", "send-keys", "-t", session_name, cmd, "C-m"])
subprocess.run(["tmux", "send-keys", "-t", session_name, cmd, "C-m"], cwd=project_root)
time.sleep(30)
subprocess.run(["tmux", "send-keys", "-t", "server_" + session_name, f"/op @a", "C-m"])
agents_op = check_agent_ops(agent_names, ops_file=f"./server_data_{session_name}/ops.json")
# Check ops file inside the correct tasks/server_data/X directory
ops_file_path = os.path.join(tasks_dir, "server_data", session_name, "ops.json")
agents_op = check_agent_ops(agent_names, ops_file=ops_file_path)
if agents_op:
print("Agents are operators! You are good to go :D")
else:
@ -502,6 +603,15 @@ def make_ops(agent_names, session_name):
make_ops(agent_names, session_name)
def check_agent_ops(agent_names, ops_file="ops.json"):
"""Check if agents are OPs on the server."""
# ops_file path is now provided absolute by caller (make_ops)
# if not os.path.isabs(ops_file):
# ops_file = os.path.join(project_root, ops_file) # OLD LOGIC
if not os.path.exists(ops_file):
print(f"Error: ops.json file not found: {ops_file}")
return False
with open(ops_file, "r") as f:
ops_data = json.load(f)
@ -516,26 +626,39 @@ def make_script_file_and_run(script_content,
file_name,
session_name="0",
run_in_tmux=True):
script_dir = os.path.dirname(file_name)
os.makedirs(script_dir, exist_ok=True)
# Create script inside tasks/tmp/
script_base_dir = os.path.join(tasks_dir, "tmp")
os.makedirs(script_base_dir, exist_ok=True)
script_abs_path = os.path.join(script_base_dir, file_name)
script_dir = os.path.dirname(script_abs_path)
# os.makedirs(script_dir, exist_ok=True) # Already handled by script_base_dir creation
assert os.path.exists(script_dir), f"Script directory {script_dir} was not created"
print(f"Created script directory: {script_dir}")
# Call the function before writing the script file
with open(file_name, 'w') as f:
with open(script_abs_path, 'w') as f:
f.write(script_content)
assert os.path.exists(file_name), f"Script file {file_name} was not created"
assert os.path.exists(script_abs_path), f"Script file {script_abs_path} was not created"
script_file_run = "bash " + file_name
script_file_run = "bash " + script_abs_path
# Execute the shell script using subprocess
# Run subprocess from project_root so node main.js etc work
if run_in_tmux:
subprocess.run(["tmux", "send-keys", "-t", session_name, script_file_run, "C-m"])
subprocess.run(["tmux", "send-keys", "-t", session_name, script_file_run, "C-m"], cwd=project_root)
else:
subprocess.run(script_file_run.split())
subprocess.run(script_file_run.split(), cwd=project_root)
def make_profiles(agent_names, models, apis, template_profile="profiles/collab_profile.json", url="http://127.0.0.1:8000/v1"):
assert len(agent_names) == len(models)
"""Generate profile JSON files for each agent."""
# Resolve relative template_profile path relative to project_root
if template_profile.startswith("profiles/") and not os.path.isabs(template_profile):
template_profile = os.path.join(project_root, template_profile)
elif not os.path.isabs(template_profile):
# Assume relative to tasks dir if not in profiles/ structure
template_profile = os.path.join(tasks_dir, template_profile)
with open(template_profile, 'r') as f:
content = f.read()
@ -559,19 +682,34 @@ def make_profiles(agent_names, models, apis, template_profile="profiles/collab_p
else:
profile["model"] = models[index]
with open(f"{agent_names[index]}.json", 'w') as f:
json.dump(profile, f, indent=4)
# Save profiles inside tasks/profiles/
profiles_output_dir = os.path.join(tasks_dir, "profiles")
os.makedirs(profiles_output_dir, exist_ok=True)
profile_name = f"{agent_names[index]}.json"
profile_path = os.path.join(profiles_output_dir, profile_name)
with open(profile_path, 'w', encoding='utf-8') as outfile:
json.dump(profile, outfile, indent=4)
def create_server_files(source_path, num_copies, world_name="Forest"):
"""Create multiple copies of server files for parallel experiments."""
print("Creating server files...")
print(num_copies)
servers = []
"""Create multiple copies of the server files inside tasks/server_data."""
servers = [] # Define servers list
# Ensure source_path is relative to project_root if not absolute
if not os.path.isabs(source_path):
source_path = os.path.join(project_root, source_path)
# Base dir inside tasks/
server_base_dir = os.path.join(tasks_dir, "server_data")
os.makedirs(server_base_dir, exist_ok=True)
for i in range(num_copies):
dest_path = f"./server_data_{i}/"
# Server copies go into tasks/server_data/0/, tasks/server_data/1/, etc.
dest_path = os.path.join(server_base_dir, str(i))
copy_server_files(source_path, dest_path)
print(dest_path)
edit_file(dest_path + "server.properties", {"server-port": 55916 + i,
# Adjust path for edit_file
server_prop_path = os.path.join(dest_path, "server.properties")
edit_file(server_prop_path, {"server-port": 55916 + i,
"level-name": world_name})
# edit_server_properties_file(dest_path, 55916 + i)
servers.append((dest_path, 55916 + i))
@ -593,13 +731,24 @@ def edit_file(file, content_dict):
print(f"Error editing file {file}: {e}")
def clean_up_server_files(num_copies):
"""Delete server files from multiple locations."""
"""Delete server files from multiple locations within tasks/server_data."""
server_base_dir = os.path.join(tasks_dir, "server_data")
for i in range(num_copies):
dest_path = f"./server_data_{i}/"
# Target paths like tasks/server_data/0/
dest_path = os.path.join(server_base_dir, str(i))
delete_server_files(dest_path)
def copy_server_files(source_path, dest_path):
"""Copy server files to the specified location."""
"""Copy server files from source to destination (dest assumed relative to tasks_dir if not absolute)."""
# Ensure source_path is relative to project_root if not absolute
if not os.path.isabs(source_path):
source_path = os.path.join(project_root, source_path)
# Destination path is now expected inside tasks/server_data/, handled by caller (create_server_files)
# if not os.path.isabs(dest_path):
# dest_path = os.path.join(project_root, dest_path) # OLD LOGIC
if os.path.exists(dest_path):
shutil.rmtree(dest_path)
try:
shutil.copytree(source_path, dest_path)
print(f"Server files copied to {dest_path}")
@ -624,12 +773,13 @@ def check_same_files(d1, d2):
return True
def delete_server_files(dest_path):
"""Delete server files from the specified location."""
try:
"""Delete server files at the destination path (assumed relative to tasks_dir if not absolute)."""
# Path is now expected inside tasks/server_data/, handled by callers
# if not os.path.isabs(dest_path):
# dest_path = os.path.join(project_root, dest_path) # OLD LOGIC
if os.path.exists(dest_path):
shutil.rmtree(dest_path)
print(f"Server files deleted from {dest_path}")
except Exception as e:
print(f"Error deleting server files: {e}")
if not os.path.exists(dest_path):
print("Server files deleted successfully.")
# else:
@ -638,15 +788,25 @@ def delete_server_files(dest_path):
def launch_world(server_path="./server_data/", agent_names=["andy", "jill"], session_name="server", port=55916):
"""Launch the Minecraft world."""
print(f"Launching Minecraft world with port {port}...")
cmd = f"cd {server_path} && java -jar server.jar"
"""Launch the Minecraft server world (server assumed inside tasks/server_data)."""
# Ensure path is relative to tasks_dir if not absolute (expecting tasks/server_data/X)
if not os.path.isabs(server_path):
server_path = os.path.join(tasks_dir, server_path)
ops_file = os.path.join(server_path, "ops.json") # ops.json inside specific server dir
check_agent_ops(agent_names, ops_file=ops_file)
# Launch server using tmux (cwd should be the server_path itself)
java_cmd = f"java -jar server.jar nogui"
# Create tmux session for the server
subprocess.run(['tmux', 'new-session', '-d', '-s', session_name], check=True)
subprocess.run(["tmux", "send-keys", "-t", session_name, cmd, "C-m"])
time.sleep(10)
# Send command to the server session, running from its directory
subprocess.run(["tmux", "send-keys", "-t", session_name, java_cmd, "C-m"], cwd=server_path)
print(f"Launched Minecraft world in session {session_name} from {server_path} on port {port}...")
# Add a delay and check if server started
time.sleep(20) # Increased delay
if not test_server_running(port):
print("Server failed to start. Retrying...")
launch_world(server_path, agent_names, session_name, port)
print(f"Warning: Server on port {port} didn't seem to start correctly after launch.")
def test_server_running(port=55916):
host = 'localhost'
@ -667,73 +827,69 @@ def kill_world(session_name="server"):
subprocess.run(["tmux", "kill-session", "-t", session_name])
def detach_process(command):
"""
Launches a subprocess and detaches from it, allowing it to run independently.
Args:
command: A list of strings representing the command to execute, e.g., ['python', 'my_script.py'].
"""
try:
# Create a new process group so the child doesn't get signals intended for the parent.
# This is crucial for proper detachment.
kwargs = {}
if sys.platform == 'win32':
kwargs.update(creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) # Windows specific
process = subprocess.Popen(command,
stdin=subprocess.PIPE, # Prevent stdin blocking
stdout=subprocess.PIPE, # Redirect stdout
stderr=subprocess.PIPE, # Redirect stderr
close_fds=True, # Close open file descriptors
**kwargs)
print(f"Process launched with PID: {process.pid}")
return process.pid # Return the PID of the detached process
except FileNotFoundError:
print(f"Error: Command not found: {command}")
return None
except Exception as e:
print(f"An error occurred: {e}")
return None
"""Detach a process using tmux."""
# Assume commands are run from project root if needed elsewhere
process = subprocess.Popen(command, shell=True, preexec_fn=os.setsid) # Example, might need cwd
def main():
# edit_settings("settings.js", {"profiles": ["./andy.json", "./jill.json"], "port": 55917})
# edit_server_properties_file("../server_data/", 55917)
parser = argparse.ArgumentParser(description='Run Minecraft AI agent experiments')
parser.add_argument('--no_launch_world', action='store_true', help='Do not launch the Minecraft world')
parser.add_argument('--task_path', default="multiagent_crafting_tasks.json", help='Path to the task file')
parser.add_argument('--num_agents', default=2, type=int, help='Number of agents to run')
parser.add_argument('--num_exp', default=1, type=int, help='Number of experiments to run')
parser.add_argument('--num_parallel', default=1, type=int, help='Number of parallel servers to run')
parser.add_argument('--exp_name', default="exp", help='Name of the experiment')
parser.add_argument('--s3', action='store_true', help='Whether to upload to s3')
parser.add_argument('--bucket_name', default="mindcraft-experiments", help='Name of the s3 bucket')
parser.add_argument('--add_keys', action='store_true', help='Create the keys.json to match the environment variables')
parser.add_argument('--template_profile', default="profiles/tasks/collab_profile.json", help='Model to use for the agents')
parser.add_argument('--model', default="gpt-4o-mini", help='Model to use for the agents')
parser.add_argument('--api', default="openai", help='API to use for the agents')
# parser.add_argument('--world_name', default="Forest", help='Name of the world')
parser.add_argument('--insecure_coding', action='store_true', help='Enable insecure coding')
parser.add_argument('--url', default="http://127.0.0.1:8000/v1")
parser.add_argument('--max_messages', default=15, type=int, help='Maximum number of messages before summarizing')
parser.add_argument('--num_examples', default=2, type=int, help='Maximum number of turns before summarizing')
parser.add_argument('--no-pruning', action='store_true', help='Disable pruning of the actions')
parser = argparse.ArgumentParser(description="Evaluate MindCraft tasks")
parser.add_argument("--task_path", type=str, default="tasks/example_tasks.json", help="Path to the task file or directory (relative to project root)")
parser.add_argument("--task_ids", type=str, nargs="+", default=None, help="Specific task IDs to run")
parser.add_argument("--num_exp", type=int, default=1, help="Number of experiments per task")
parser.add_argument("--num_agents", type=int, default=2, help="Number of agents")
parser.add_argument("--model", type=str, default="gpt-4o-mini", help="Model name")
parser.add_argument("--api", type=str, default="openai", help="API provider")
parser.add_argument("--num_parallel", type=int, default=1, help="Number of parallel experiments")
parser.add_argument("--s3", action="store_true", help="Use S3 for storage")
parser.add_argument("--bucket_name", type=str, default="mindcraft-experiments", help="S3 bucket name")
parser.add_argument("--template_profile", type=str, default="profiles/tasks/collab_profile.json", help="Template profile path")
parser.add_argument("--insecure_coding", action="store_true", help="Allow insecure coding practices")
parser.add_argument("--url", type=str, default="http://127.0.0.1:8000/v1", help="API URL")
parser.add_argument("--check_results", action="store_true", help="Only check results in the specified folder")
parser.add_argument("--servers", type=str, nargs="+", default=["local"], help="List of server directories (e.g., 0 1 2 for server_data/0, server_data/1, etc.) or 'local' for parallel local runs")
parser.add_argument("--exp_name", type=str, default="exp", help="Experiment name prefix")
parser.add_argument("--s3_path", type=str, default="", help="S3 path prefix")
parser.add_argument("--max_messages", type=int, default=15, help="Maximum messages per agent")
parser.add_argument("--num_examples", type=int, default=2, help="Number of examples for few-shot learning")
parser.add_argument("--no_pruning", action="store_true", help="Disable pruning")
parser.add_argument("--block_conversation", action="store_true", help="Block agent conversation actions")
parser.add_argument("--run_in_tmux", action="store_false", help="Run experiment directly without tmux") # Default is True
args = parser.parse_args()
print(args)
if not args.no_launch_world:
try:
subprocess.run(['tmux', 'kill-server'], check=True)
except:
print("No tmux session to kill")
# delete all server files
if not args.no_launch_world:
# Resolve relative paths provided as arguments or defaults (relative to project root)
if not os.path.isabs(args.task_path):
args.task_path = os.path.join(project_root, args.task_path)
if not os.path.isabs(args.template_profile):
# Special handling for default profile path relative to project root
if args.template_profile.startswith("profiles/"):
args.template_profile = os.path.join(project_root, args.template_profile)
else: # Assume relative to tasks dir otherwise
args.template_profile = os.path.join(tasks_dir, args.template_profile)
if args.check_results:
# Hardcode check_folder_results to read from project_root/experiments
check_dir = os.path.join(project_root, "experiments")
check_folder_results(check_dir)
return
# Default server source path relative to project_root
default_server_source = os.path.join(project_root, "server_data")
if not args.run_in_tmux: # Assuming this corresponds to needing server files
# Pass default_server_source to create_server_files
servers = create_server_files(default_server_source, args.num_parallel, world_name="Forest") # Example world name
# The rest of the logic might need adjustment if not using tmux
else:
# Logic for when run_in_tmux is True (perhaps no server creation needed here?)
# Or maybe create_server_files should always run? Adjusting based on original logic
# Let's assume server files are always needed for parallel runs
servers = create_server_files(default_server_source, args.num_parallel, world_name="Forest") # Example world name
# delete all server files (now inside tasks/server_data)
# The clean_up_server_files function now uses the correct base path
clean_up_server_files(args.num_parallel)
if args.add_keys:
if hasattr(args, 'add_keys') and args.add_keys: # Check if arg exists before using
update_keys_json()
launch_parallel_experiments(args.task_path,
@ -751,7 +907,8 @@ def main():
max_messages=args.max_messages,
num_examples=args.num_examples,
no_pruning=args.no_pruning,
run_in_tmux=not args.no_launch_world)
block_conversation=args.block_conversation,
run_in_tmux=not args.run_in_tmux)
if __name__ == "__main__":
main()

View file

@ -10,6 +10,15 @@ import tqdm
from analyse_results import extract_result, get_immediate_subdirectories, analyze_json_file
import glob
# Calculate project root directory
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Define tasks directory
tasks_dir = os.path.dirname(os.path.abspath(__file__))
# Define paths relative to project root (for reading)
LOGS_DIR = os.path.join(project_root, "logs")
EXPERIMENTS_DIR = os.path.join(project_root, "experiments")
BOTS_DIR = os.path.join(project_root, "bots")
"""
This script is intended to run the evaluation script multiple times and then automatically aggregate the
@ -71,31 +80,42 @@ def identify_success_folders(download_dir, num_agents):
def run_data_collection(args):
# Set up directories
LOGS_DIR = Path("logs")
SUCCESSFUL_DIR = Path(f"successful_run_logs_{datetime.now().strftime('%Y-%m-%d')}")
FULL_RUN_LOGS_DIR = Path(f"full_run_logs_{datetime.now().strftime('%Y-%m-%d')}")
EXPERIMENTS_DIR = Path("experiments")
BOTS_DIR = Path("bots")
LOGS_DIR.mkdir(exist_ok=True)
# Set up output directories inside tasks/
timestamp_str = datetime.now().strftime('%Y-%m-%d_%H%M%S') # Add time to avoid overwrite
SUCCESSFUL_DIR = Path(os.path.join(tasks_dir, f"successful_run_logs_{timestamp_str}"))
FULL_RUN_LOGS_DIR = Path(os.path.join(tasks_dir, f"full_run_logs_{timestamp_str}"))
# Input/state dirs (relative to project root)
logs_dir_path = Path(LOGS_DIR)
experiments_dir_path = Path(EXPERIMENTS_DIR)
bots_dir_path = Path(BOTS_DIR)
logs_dir_path.mkdir(exist_ok=True)
SUCCESSFUL_DIR.mkdir(exist_ok=True)
FULL_RUN_LOGS_DIR.mkdir(exist_ok=True)
# Parse tasks and repetitions
# Parse tasks and repetitions, ensuring paths are relative to project root
TASKS_TO_RUN = []
for task_spec in args.tasks:
parts = task_spec.split(':')
if len(parts) == 2:
task_path, repeats = parts[0], int(parts[1])
# Resolve task_path relative to project root
if not os.path.isabs(task_path):
task_path = os.path.join(project_root, task_path)
TASKS_TO_RUN.append((task_path, repeats))
else:
print(f"Warning: Invalid task specification '{task_spec}', expected format 'path:repeats'")
# First clear anything named Andy_ or Jill_ from the bots/ folder
for bot_dir in BOTS_DIR.glob("*"):
# Clear temp agent dirs from project_root/bots/
for bot_dir in bots_dir_path.glob("*"):
if bot_dir.name.startswith(("Andy_", "Jill_", "agent_")):
shutil.rmtree(bot_dir)
# Resolve eval_script path
eval_script_path = args.eval_script
if not os.path.isabs(eval_script_path):
eval_script_path = os.path.join(project_root, eval_script_path)
run_counter = 1
for task_path, repeats in TASKS_TO_RUN:
for rep in range(repeats):
@ -103,38 +123,40 @@ def run_data_collection(args):
print(f"\n Starting {task_path} (rep {rep + 1}/{repeats}) -> {run_id}")
# Track start time to locate experiment folder
before = set(EXPERIMENTS_DIR.glob("*"))
# Ensure EXPERIMENTS_DIR is treated as Path object if needed
before = set(experiments_dir_path.glob("*"))
# Run evaluation
# Run evaluation using the resolved eval_script_path
# Run from project root
subprocess.run([
"python", args.eval_script,
"python", eval_script_path,
"--api", args.api,
"--model", args.model,
"--task_path", task_path,
"--task_path", task_path, # task_path is already absolute or resolved
"--num_agents", str(args.num_agents),
"--num_parallel", str(args.num_parallel)
], check=True)
], check=True, cwd=project_root)
# Wait for experiment folder to appear
# Wait for experiment folder to appear in project_root/experiments/
time.sleep(20) # avoid race condition
after = set(EXPERIMENTS_DIR.glob("*"))
after = set(experiments_dir_path.glob("*"))
new_experiments = list(after - before)
assert len(new_experiments) == 1, f"Expected one new experiment folder, found {len(new_experiments)}"
experiment_dir = new_experiments[0]
print(f"Found experiment folder: {experiment_dir}")
# Identify successful experiments
# Identify successful experiments from project_root/experiments/...
successful_exp_list = identify_success_folders(experiment_dir, args.num_agents)
# Save successful logs and results
# Save successful logs and results (read from project_root/bots, write to tasks/successful_...)
success_output_dir = SUCCESSFUL_DIR / run_id
success_output_dir.mkdir(parents=True, exist_ok=True)
# Identify the ones that are successful
for exp_path in successful_exp_list:
exp_name = os.path.basename(exp_path)
# For each agent, find and copy their logs for this successful experiment
for bot_dir in BOTS_DIR.glob("*"):
for bot_dir in bots_dir_path.glob("*"):
if bot_dir.name.startswith(("Andy_", "Jill_", "agent_")):
agent_logs_dir = bot_dir / "logs"
if agent_logs_dir.exists():
@ -147,10 +169,10 @@ def run_data_collection(args):
shutil.copytree(exp_dir, dest_dir)
print(f"Copied successful log directory: {exp_dir} -> {dest_dir}")
# Move full logs to the full logs dir, aka anything named Jill_ or Andy_
# Move full logs to the full logs dir (read from project_root/bots, write to tasks/full_...)
full_logs_dir = FULL_RUN_LOGS_DIR / run_id
full_logs_dir.mkdir(parents=True, exist_ok=True)
for bot_dir in BOTS_DIR.glob("*"):
for bot_dir in bots_dir_path.glob("*"):
if bot_dir.name.startswith(("Andy_", "Jill_", "agent_")):
# bot_dir is already the full path, no need for agent_dir
dest_dir = full_logs_dir / bot_dir.name
@ -164,7 +186,7 @@ def run_data_collection(args):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run multiple evaluations and collect successful logs")
parser.add_argument("--eval_script", default="evaluation_script.py", help="Path to evaluation script")
parser.add_argument("--eval_script", default="tasks/evaluation_script.py", help="Path to evaluation script relative to project root")
parser.add_argument("--api", default="vllm", help="API to use")
parser.add_argument("--model", default="meta-llama/Meta-Llama-3-8B-Instruct", help="Model to use")
parser.add_argument("--num_agents", type=int, default=2, help="Number of agents")