plancraft 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plancraft/__init__.py +0 -0
- plancraft/config.py +155 -0
- plancraft/environments/__init__.py +0 -0
- plancraft/environments/actions.py +218 -0
- plancraft/environments/env_real.py +316 -0
- plancraft/environments/env_symbolic.py +212 -0
- plancraft/environments/items.py +10 -0
- plancraft/environments/planner.py +109 -0
- plancraft/environments/recipes.py +542 -0
- plancraft/environments/sampler.py +224 -0
- plancraft/evaluator.py +273 -0
- plancraft/models/__init__.py +21 -0
- plancraft/models/act.py +184 -0
- plancraft/models/base.py +152 -0
- plancraft/models/bbox_model.py +492 -0
- plancraft/models/dummy.py +54 -0
- plancraft/models/few_shot_images/__init__.py +16 -0
- plancraft/models/generators.py +480 -0
- plancraft/models/oam.py +283 -0
- plancraft/models/oracle.py +265 -0
- plancraft/models/prompts.py +158 -0
- plancraft/models/react.py +93 -0
- plancraft/models/utils.py +289 -0
- plancraft/train/dataset.py +187 -0
- plancraft/utils.py +84 -0
- {plancraft-0.1.2.dist-info → plancraft-0.1.3.dist-info}/METADATA +1 -1
- plancraft-0.1.3.dist-info/RECORD +30 -0
- plancraft-0.1.3.dist-info/top_level.txt +1 -0
- plancraft-0.1.2.dist-info/RECORD +0 -5
- plancraft-0.1.2.dist-info/top_level.txt +0 -1
- {plancraft-0.1.2.dist-info → plancraft-0.1.3.dist-info}/LICENSE +0 -0
- {plancraft-0.1.2.dist-info → plancraft-0.1.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from plancraft.environments.actions import SymbolicAction
|
4
|
+
from plancraft.environments.recipes import (
|
5
|
+
RECIPES,
|
6
|
+
ShapedRecipe,
|
7
|
+
ShapelessRecipe,
|
8
|
+
SmeltingRecipe,
|
9
|
+
convert_ingredients_to_table,
|
10
|
+
)
|
11
|
+
from plancraft.environments.sampler import MAX_STACK_SIZE
|
12
|
+
|
13
|
+
|
14
|
+
class PseudoActionSpace:
|
15
|
+
def no_op(self):
|
16
|
+
return {
|
17
|
+
"inventory_command": (0, 0, 0),
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
class SymbolicPlancraft:
|
22
|
+
def __init__(self, inventory: list[dict] = [], recipes=RECIPES, **kwargs):
|
23
|
+
self.inventory = inventory
|
24
|
+
self.reset_state()
|
25
|
+
self.table_indexes = list(range(1, 10))
|
26
|
+
self.output_index = 0
|
27
|
+
|
28
|
+
self.action_space = PseudoActionSpace()
|
29
|
+
|
30
|
+
self.recipes = recipes
|
31
|
+
|
32
|
+
self.smelting_recipes = []
|
33
|
+
|
34
|
+
self.crafting_recipes = []
|
35
|
+
|
36
|
+
for recipe_list in recipes.values():
|
37
|
+
for recipe in recipe_list:
|
38
|
+
if isinstance(recipe, SmeltingRecipe):
|
39
|
+
self.smelting_recipes.append(recipe)
|
40
|
+
elif isinstance(recipe, (ShapelessRecipe, ShapedRecipe)):
|
41
|
+
self.crafting_recipes.append(recipe)
|
42
|
+
|
43
|
+
def reset_state(self):
|
44
|
+
self.state = {i: {"type": "air", "quantity": 0} for i in range(46)}
|
45
|
+
# initialise inventory
|
46
|
+
for item in self.inventory:
|
47
|
+
self.state[item["slot"]] = {
|
48
|
+
"type": item["type"],
|
49
|
+
"quantity": item["quantity"],
|
50
|
+
}
|
51
|
+
|
52
|
+
def step(self, action: Optional[SymbolicAction | dict]):
|
53
|
+
if action is None:
|
54
|
+
state_list = [
|
55
|
+
{"type": item["type"], "quantity": item["quantity"], "index": idx}
|
56
|
+
for idx, item in self.state.items()
|
57
|
+
]
|
58
|
+
return {"inventory": state_list}, 0, False, {}
|
59
|
+
|
60
|
+
# action_dict = action.to_action_dict()
|
61
|
+
if not isinstance(action, dict):
|
62
|
+
action = action.to_action_dict()
|
63
|
+
|
64
|
+
if "inventory_command" in action:
|
65
|
+
# do inventory command (move)
|
66
|
+
slot, slot_to, quantity = action["inventory_command"]
|
67
|
+
self.move_item(slot, slot_to, quantity)
|
68
|
+
elif "smelt" in action:
|
69
|
+
# do smelt
|
70
|
+
slot, slot_to, quantity = action["smelt"]
|
71
|
+
self.smelt_item(slot, slot_to, quantity)
|
72
|
+
else:
|
73
|
+
raise ValueError("Invalid action")
|
74
|
+
# logger.warn("Cannot parse action for Symbolic action")
|
75
|
+
|
76
|
+
self.clean_state()
|
77
|
+
|
78
|
+
# convert to list for same format as minerl
|
79
|
+
state_list = [
|
80
|
+
{"type": item["type"], "quantity": item["quantity"], "index": idx}
|
81
|
+
for idx, item in self.state.items()
|
82
|
+
]
|
83
|
+
|
84
|
+
return {"inventory": state_list}, 0, False, {}
|
85
|
+
|
86
|
+
def clean_state(self):
|
87
|
+
# reset slot type if quantity is 0
|
88
|
+
for i in self.state.keys():
|
89
|
+
if self.state[i]["quantity"] == 0:
|
90
|
+
self.state[i]["type"] = "air"
|
91
|
+
|
92
|
+
def move_item(self, slot_from: int, slot_to: int, quantity: int):
|
93
|
+
if slot_from == slot_to or quantity < 1 or slot_to == 0:
|
94
|
+
return
|
95
|
+
# slot outside of inventory
|
96
|
+
if slot_from not in self.state or slot_to not in self.state:
|
97
|
+
return
|
98
|
+
# not enough
|
99
|
+
if self.state[slot_from]["quantity"] < quantity:
|
100
|
+
return
|
101
|
+
|
102
|
+
item = self.state[slot_from]
|
103
|
+
|
104
|
+
# slot to is not empty or is the same type as item
|
105
|
+
if (self.state[slot_to]["type"] == "air") or (
|
106
|
+
self.state[slot_to]["quantity"] <= 0
|
107
|
+
):
|
108
|
+
self.state[slot_to] = {"type": item["type"], "quantity": quantity}
|
109
|
+
self.state[slot_from]["quantity"] -= quantity
|
110
|
+
elif self.state[slot_to]["type"] == item["type"] and (
|
111
|
+
MAX_STACK_SIZE[item["type"]] >= self.state[slot_to]["quantity"] + quantity
|
112
|
+
):
|
113
|
+
# check if the quantity exceeds the max stack size
|
114
|
+
self.state[slot_to]["quantity"] += quantity
|
115
|
+
self.state[slot_from]["quantity"] -= quantity
|
116
|
+
else:
|
117
|
+
return
|
118
|
+
|
119
|
+
# reset slot if quantity is 0
|
120
|
+
if self.state[slot_from]["quantity"] == 0:
|
121
|
+
self.state[slot_from] = {"type": "air", "quantity": 0}
|
122
|
+
|
123
|
+
# use up ingredients
|
124
|
+
if slot_from == 0:
|
125
|
+
self.use_ingredients()
|
126
|
+
|
127
|
+
# populate craft slot if ingredients in crafting table have changed
|
128
|
+
if slot_to < 10 or slot_from < 10:
|
129
|
+
self.populate_craft_slot_craft_item()
|
130
|
+
|
131
|
+
def smelt_item(self, slot_from: int, slot_to: int, quantity: int):
|
132
|
+
if quantity < 1 or slot_to == 0 or slot_from == slot_to or slot_from == 0:
|
133
|
+
return # skip if quantity is less than 1
|
134
|
+
|
135
|
+
if slot_from not in self.state or slot_to not in self.state:
|
136
|
+
return # handle slot out of bounds or invalid slot numbers
|
137
|
+
|
138
|
+
item = self.state[slot_from]
|
139
|
+
if item["quantity"] < quantity or item["type"] == "air":
|
140
|
+
return # skip if the slot from is empty or does not have enough items
|
141
|
+
|
142
|
+
for recipe in self.smelting_recipes:
|
143
|
+
if output := recipe.smelt(item["type"]):
|
144
|
+
output_type = output.item
|
145
|
+
# Check if the destination slot is empty or has the same type of item as the output
|
146
|
+
if self.state[slot_to]["type"] == "air":
|
147
|
+
self.state[slot_to] = {"type": output_type, "quantity": quantity}
|
148
|
+
self.state[slot_from]["quantity"] -= quantity
|
149
|
+
break
|
150
|
+
elif self.state[slot_to]["type"] == output_type and (
|
151
|
+
MAX_STACK_SIZE[output_type]
|
152
|
+
>= self.state[slot_to]["quantity"] + quantity
|
153
|
+
): # assuming max stack size is 64
|
154
|
+
self.state[slot_to]["quantity"] += quantity
|
155
|
+
self.state[slot_from]["quantity"] -= quantity
|
156
|
+
break
|
157
|
+
else:
|
158
|
+
return # No space or type mismatch in slot_to
|
159
|
+
|
160
|
+
# Clean up if the source slot is depleted
|
161
|
+
if self.state[slot_from] == 0:
|
162
|
+
self.state[slot_from] = {"type": "air", "quantity": 0}
|
163
|
+
|
164
|
+
if slot_to < 10 or slot_from < 10:
|
165
|
+
self.populate_craft_slot_craft_item()
|
166
|
+
|
167
|
+
def populate_craft_slot_craft_item(self):
|
168
|
+
# get ingredients from crafting table
|
169
|
+
ingredients = []
|
170
|
+
for i in self.table_indexes:
|
171
|
+
if self.state[i]["type"] != "air" and self.state[i]["quantity"] > 0:
|
172
|
+
ingredients.append(self.state[i]["type"])
|
173
|
+
else:
|
174
|
+
ingredients.append(None)
|
175
|
+
table = convert_ingredients_to_table(ingredients)
|
176
|
+
|
177
|
+
# check if any of the crafting recipes match the ingredients
|
178
|
+
for recipe in self.crafting_recipes:
|
179
|
+
if result := recipe.craft(table):
|
180
|
+
result, indexes = result
|
181
|
+
self.ingredients_idxs = indexes
|
182
|
+
self.state[self.output_index] = {
|
183
|
+
"type": result.item,
|
184
|
+
"quantity": result.count,
|
185
|
+
}
|
186
|
+
return
|
187
|
+
|
188
|
+
self.ingredients_idxs = []
|
189
|
+
self.state[self.output_index] = {"type": "air", "quantity": 0}
|
190
|
+
|
191
|
+
def use_ingredients(self):
|
192
|
+
# remove used ingredients from crafting table
|
193
|
+
for idx in self.ingredients_idxs:
|
194
|
+
self.state[idx + 1]["quantity"] -= 1
|
195
|
+
if self.state[idx + 1]["quantity"] <= 0:
|
196
|
+
self.state[idx + 1] = {"type": "air", "quantity": 0}
|
197
|
+
self.ingredients_idxs = []
|
198
|
+
|
199
|
+
def reset(self):
|
200
|
+
self.reset_state()
|
201
|
+
return self.state
|
202
|
+
|
203
|
+
def fast_reset(self, new_inventory: list[dict]):
|
204
|
+
self.inventory = new_inventory
|
205
|
+
self.reset_state()
|
206
|
+
return self.state
|
207
|
+
|
208
|
+
def render(self):
|
209
|
+
print(f"state: {self.state}")
|
210
|
+
|
211
|
+
def close(self):
|
212
|
+
self.reset_state()
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
import networkx as nx
|
4
|
+
|
5
|
+
from plancraft.environments.recipes import RECIPES, BaseRecipe
|
6
|
+
|
7
|
+
RECIPE_GRAPH = nx.DiGraph()
|
8
|
+
|
9
|
+
for item, recipes in RECIPES.items():
|
10
|
+
for recipe in recipes:
|
11
|
+
RECIPE_GRAPH.add_node(recipe.result.item)
|
12
|
+
for ingredient in recipe.inputs:
|
13
|
+
RECIPE_GRAPH.add_node(ingredient)
|
14
|
+
RECIPE_GRAPH.add_edge(ingredient, recipe.result.item)
|
15
|
+
|
16
|
+
|
17
|
+
def get_ancestors(target: str):
|
18
|
+
return list(nx.ancestors(RECIPE_GRAPH, source=target))
|
19
|
+
|
20
|
+
|
21
|
+
def optimal_planner(
|
22
|
+
target: str,
|
23
|
+
inventory: dict[str, int],
|
24
|
+
steps=[],
|
25
|
+
best_steps=None,
|
26
|
+
max_steps=40,
|
27
|
+
timeout=30,
|
28
|
+
) -> list[tuple[BaseRecipe, dict[str, int]]]:
|
29
|
+
"""
|
30
|
+
Optimal planner for crafting the target item from the given inventory.
|
31
|
+
|
32
|
+
Uses depth-first search with memoization to find the shortest path of crafting steps.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
target: The target item to craft.
|
36
|
+
inventory: The current inventory.
|
37
|
+
steps: The current path of crafting steps.
|
38
|
+
best_steps: The best path of crafting steps found so far.
|
39
|
+
max_steps: The maximum number of steps to take.
|
40
|
+
timeout: The maximum time to spend searching for a solution.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
list of tuples of (recipe, inventory) for each step in the optimal path.
|
44
|
+
"""
|
45
|
+
|
46
|
+
memo = {}
|
47
|
+
# only look at recipes that are ancestors of the target
|
48
|
+
ancestors = get_ancestors(target)
|
49
|
+
# sort to put the closest ancestors first
|
50
|
+
ancestors = sorted(
|
51
|
+
ancestors,
|
52
|
+
key=lambda x: nx.shortest_path_length(RECIPE_GRAPH, source=x, target=target),
|
53
|
+
)
|
54
|
+
|
55
|
+
time_now = time.time()
|
56
|
+
|
57
|
+
def dfs(starting_inventory, steps, best_steps):
|
58
|
+
# If we have exceeded the timeout, return the best known path so far.
|
59
|
+
if time.time() - time_now > timeout:
|
60
|
+
raise TimeoutError("Timeout exceeded")
|
61
|
+
|
62
|
+
memo_key = (frozenset(starting_inventory.items()), len(steps))
|
63
|
+
if memo_key in memo:
|
64
|
+
return memo[memo_key]
|
65
|
+
|
66
|
+
if best_steps is not None and len(steps) >= len(best_steps):
|
67
|
+
# If we already have a shorter or equally short solution, do not proceed further.
|
68
|
+
return best_steps
|
69
|
+
|
70
|
+
if len(steps) > max_steps:
|
71
|
+
# If we have already exceeded the maximum number of steps, do not proceed further.
|
72
|
+
return best_steps
|
73
|
+
|
74
|
+
if target in starting_inventory and starting_inventory[target] > 0:
|
75
|
+
# If the target item is already in the inventory in the required amount, return the current path.
|
76
|
+
if best_steps is None or len(steps) < len(best_steps):
|
77
|
+
return steps
|
78
|
+
return best_steps
|
79
|
+
|
80
|
+
for recipe_name in [target] + ancestors:
|
81
|
+
# skip if already have 9 of the item
|
82
|
+
if starting_inventory.get(recipe_name, 0) >= 9:
|
83
|
+
continue
|
84
|
+
# TODO prevent looping between equivalent recipes (coal <-> coal_block)
|
85
|
+
for recipe in RECIPES[recipe_name]:
|
86
|
+
if recipe.can_craft_from_inventory(starting_inventory):
|
87
|
+
# Craft this item and update the inventory.
|
88
|
+
new_inventory = recipe.craft_from_inventory(starting_inventory)
|
89
|
+
# Add this step to the path.
|
90
|
+
new_steps = steps + [(recipe, new_inventory)]
|
91
|
+
|
92
|
+
# Recursively try to craft the target item with the updated inventory.
|
93
|
+
candidate_steps = dfs(new_inventory, new_steps, best_steps)
|
94
|
+
|
95
|
+
# Update the best known path if the candidate path is better.
|
96
|
+
if candidate_steps is not None and (
|
97
|
+
best_steps is None or len(candidate_steps) < len(best_steps)
|
98
|
+
):
|
99
|
+
best_steps = candidate_steps
|
100
|
+
|
101
|
+
memo[memo_key] = best_steps
|
102
|
+
return best_steps
|
103
|
+
|
104
|
+
try:
|
105
|
+
path = dfs(inventory, steps, best_steps)
|
106
|
+
return path
|
107
|
+
|
108
|
+
except TimeoutError:
|
109
|
+
return None
|