plancraft 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- environments/__init__.py +0 -0
- environments/actions.py +218 -0
- environments/env_real.py +315 -0
- environments/env_symbolic.py +215 -0
- environments/items.py +10 -0
- environments/planner.py +109 -0
- environments/recipes.py +542 -0
- environments/sampler.py +224 -0
- models/__init__.py +21 -0
- models/act.py +184 -0
- models/base.py +152 -0
- models/bbox_model.py +492 -0
- models/dummy.py +54 -0
- models/few_shot_images/__init__.py +16 -0
- models/generators.py +483 -0
- models/oam.py +284 -0
- models/oracle.py +268 -0
- models/prompts.py +158 -0
- models/react.py +98 -0
- models/utils.py +289 -0
- plancraft-0.1.0.dist-info/LICENSE +21 -0
- plancraft-0.1.0.dist-info/METADATA +53 -0
- plancraft-0.1.0.dist-info/RECORD +26 -0
- plancraft-0.1.0.dist-info/WHEEL +5 -0
- plancraft-0.1.0.dist-info/top_level.txt +3 -0
- train/dataset.py +187 -0
@@ -0,0 +1,215 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from plancraft.environments.actions import SymbolicAction
|
5
|
+
from plancraft.environments.recipes import (
|
6
|
+
RECIPES,
|
7
|
+
ShapedRecipe,
|
8
|
+
ShapelessRecipe,
|
9
|
+
SmeltingRecipe,
|
10
|
+
convert_ingredients_to_table,
|
11
|
+
)
|
12
|
+
from plancraft.environments.sampler import MAX_STACK_SIZE
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class PseudoActionSpace:
|
18
|
+
def no_op(self):
|
19
|
+
return {
|
20
|
+
"inventory_command": (0, 0, 0),
|
21
|
+
}
|
22
|
+
|
23
|
+
|
24
|
+
class SymbolicPlancraft:
|
25
|
+
def __init__(self, inventory: list[dict] = [], recipes=RECIPES, **kwargs):
|
26
|
+
self.inventory = inventory
|
27
|
+
self.reset_state()
|
28
|
+
self.table_indexes = list(range(1, 10))
|
29
|
+
self.output_index = 0
|
30
|
+
|
31
|
+
self.action_space = PseudoActionSpace()
|
32
|
+
|
33
|
+
self.recipes = recipes
|
34
|
+
|
35
|
+
self.smelting_recipes = []
|
36
|
+
|
37
|
+
self.crafting_recipes = []
|
38
|
+
|
39
|
+
for recipe_list in recipes.values():
|
40
|
+
for recipe in recipe_list:
|
41
|
+
if isinstance(recipe, SmeltingRecipe):
|
42
|
+
self.smelting_recipes.append(recipe)
|
43
|
+
elif isinstance(recipe, (ShapelessRecipe, ShapedRecipe)):
|
44
|
+
self.crafting_recipes.append(recipe)
|
45
|
+
|
46
|
+
def reset_state(self):
|
47
|
+
self.state = {i: {"type": "air", "quantity": 0} for i in range(46)}
|
48
|
+
# initialise inventory
|
49
|
+
for item in self.inventory:
|
50
|
+
self.state[item["slot"]] = {
|
51
|
+
"type": item["type"],
|
52
|
+
"quantity": item["quantity"],
|
53
|
+
}
|
54
|
+
|
55
|
+
def step(self, action: Optional[SymbolicAction | dict]):
|
56
|
+
if action is None:
|
57
|
+
state_list = [
|
58
|
+
{"type": item["type"], "quantity": item["quantity"], "index": idx}
|
59
|
+
for idx, item in self.state.items()
|
60
|
+
]
|
61
|
+
return {"inventory": state_list}, 0, False, {}
|
62
|
+
|
63
|
+
# action_dict = action.to_action_dict()
|
64
|
+
if not isinstance(action, dict):
|
65
|
+
action = action.to_action_dict()
|
66
|
+
|
67
|
+
if "inventory_command" in action:
|
68
|
+
# do inventory command (move)
|
69
|
+
slot, slot_to, quantity = action["inventory_command"]
|
70
|
+
self.move_item(slot, slot_to, quantity)
|
71
|
+
elif "smelt" in action:
|
72
|
+
# do smelt
|
73
|
+
slot, slot_to, quantity = action["smelt"]
|
74
|
+
self.smelt_item(slot, slot_to, quantity)
|
75
|
+
else:
|
76
|
+
raise ValueError("Invalid action")
|
77
|
+
# logger.warn("Cannot parse action for Symbolic action")
|
78
|
+
|
79
|
+
self.clean_state()
|
80
|
+
|
81
|
+
# convert to list for same format as minerl
|
82
|
+
state_list = [
|
83
|
+
{"type": item["type"], "quantity": item["quantity"], "index": idx}
|
84
|
+
for idx, item in self.state.items()
|
85
|
+
]
|
86
|
+
|
87
|
+
return {"inventory": state_list}, 0, False, {}
|
88
|
+
|
89
|
+
def clean_state(self):
|
90
|
+
# reset slot type if quantity is 0
|
91
|
+
for i in self.state.keys():
|
92
|
+
if self.state[i]["quantity"] == 0:
|
93
|
+
self.state[i]["type"] = "air"
|
94
|
+
|
95
|
+
def move_item(self, slot_from: int, slot_to: int, quantity: int):
|
96
|
+
if slot_from == slot_to or quantity < 1 or slot_to == 0:
|
97
|
+
return
|
98
|
+
# slot outside of inventory
|
99
|
+
if slot_from not in self.state or slot_to not in self.state:
|
100
|
+
return
|
101
|
+
# not enough
|
102
|
+
if self.state[slot_from]["quantity"] < quantity:
|
103
|
+
return
|
104
|
+
|
105
|
+
item = self.state[slot_from]
|
106
|
+
|
107
|
+
# slot to is not empty or is the same type as item
|
108
|
+
if (self.state[slot_to]["type"] == "air") or (
|
109
|
+
self.state[slot_to]["quantity"] <= 0
|
110
|
+
):
|
111
|
+
self.state[slot_to] = {"type": item["type"], "quantity": quantity}
|
112
|
+
self.state[slot_from]["quantity"] -= quantity
|
113
|
+
elif self.state[slot_to]["type"] == item["type"] and (
|
114
|
+
MAX_STACK_SIZE[item["type"]] >= self.state[slot_to]["quantity"] + quantity
|
115
|
+
):
|
116
|
+
# check if the quantity exceeds the max stack size
|
117
|
+
self.state[slot_to]["quantity"] += quantity
|
118
|
+
self.state[slot_from]["quantity"] -= quantity
|
119
|
+
else:
|
120
|
+
return
|
121
|
+
|
122
|
+
# reset slot if quantity is 0
|
123
|
+
if self.state[slot_from]["quantity"] == 0:
|
124
|
+
self.state[slot_from] = {"type": "air", "quantity": 0}
|
125
|
+
|
126
|
+
# use up ingredients
|
127
|
+
if slot_from == 0:
|
128
|
+
self.use_ingredients()
|
129
|
+
|
130
|
+
# populate craft slot if ingredients in crafting table have changed
|
131
|
+
if slot_to < 10 or slot_from < 10:
|
132
|
+
self.populate_craft_slot_craft_item()
|
133
|
+
|
134
|
+
def smelt_item(self, slot_from: int, slot_to: int, quantity: int):
|
135
|
+
if quantity < 1 or slot_to == 0 or slot_from == slot_to or slot_from == 0:
|
136
|
+
return # skip if quantity is less than 1
|
137
|
+
|
138
|
+
if slot_from not in self.state or slot_to not in self.state:
|
139
|
+
return # handle slot out of bounds or invalid slot numbers
|
140
|
+
|
141
|
+
item = self.state[slot_from]
|
142
|
+
if item["quantity"] < quantity or item["type"] == "air":
|
143
|
+
return # skip if the slot from is empty or does not have enough items
|
144
|
+
|
145
|
+
for recipe in self.smelting_recipes:
|
146
|
+
if output := recipe.smelt(item["type"]):
|
147
|
+
output_type = output.item
|
148
|
+
# Check if the destination slot is empty or has the same type of item as the output
|
149
|
+
if self.state[slot_to]["type"] == "air":
|
150
|
+
self.state[slot_to] = {"type": output_type, "quantity": quantity}
|
151
|
+
self.state[slot_from]["quantity"] -= quantity
|
152
|
+
break
|
153
|
+
elif self.state[slot_to]["type"] == output_type and (
|
154
|
+
MAX_STACK_SIZE[output_type]
|
155
|
+
>= self.state[slot_to]["quantity"] + quantity
|
156
|
+
): # assuming max stack size is 64
|
157
|
+
self.state[slot_to]["quantity"] += quantity
|
158
|
+
self.state[slot_from]["quantity"] -= quantity
|
159
|
+
break
|
160
|
+
else:
|
161
|
+
return # No space or type mismatch in slot_to
|
162
|
+
|
163
|
+
# Clean up if the source slot is depleted
|
164
|
+
if self.state[slot_from] == 0:
|
165
|
+
self.state[slot_from] = {"type": "air", "quantity": 0}
|
166
|
+
|
167
|
+
if slot_to < 10 or slot_from < 10:
|
168
|
+
self.populate_craft_slot_craft_item()
|
169
|
+
|
170
|
+
def populate_craft_slot_craft_item(self):
|
171
|
+
# get ingredients from crafting table
|
172
|
+
ingredients = []
|
173
|
+
for i in self.table_indexes:
|
174
|
+
if self.state[i]["type"] != "air" and self.state[i]["quantity"] > 0:
|
175
|
+
ingredients.append(self.state[i]["type"])
|
176
|
+
else:
|
177
|
+
ingredients.append(None)
|
178
|
+
table = convert_ingredients_to_table(ingredients)
|
179
|
+
|
180
|
+
# check if any of the crafting recipes match the ingredients
|
181
|
+
for recipe in self.crafting_recipes:
|
182
|
+
if result := recipe.craft(table):
|
183
|
+
result, indexes = result
|
184
|
+
self.ingredients_idxs = indexes
|
185
|
+
self.state[self.output_index] = {
|
186
|
+
"type": result.item,
|
187
|
+
"quantity": result.count,
|
188
|
+
}
|
189
|
+
return
|
190
|
+
|
191
|
+
self.ingredients_idxs = []
|
192
|
+
self.state[self.output_index] = {"type": "air", "quantity": 0}
|
193
|
+
|
194
|
+
def use_ingredients(self):
|
195
|
+
# remove used ingredients from crafting table
|
196
|
+
for idx in self.ingredients_idxs:
|
197
|
+
self.state[idx + 1]["quantity"] -= 1
|
198
|
+
if self.state[idx + 1]["quantity"] <= 0:
|
199
|
+
self.state[idx + 1] = {"type": "air", "quantity": 0}
|
200
|
+
self.ingredients_idxs = []
|
201
|
+
|
202
|
+
def reset(self):
|
203
|
+
self.reset_state()
|
204
|
+
return self.state
|
205
|
+
|
206
|
+
def fast_reset(self, new_inventory: list[dict]):
|
207
|
+
self.inventory = new_inventory
|
208
|
+
self.reset_state()
|
209
|
+
return self.state
|
210
|
+
|
211
|
+
def render(self):
|
212
|
+
print(f"state: {self.state}")
|
213
|
+
|
214
|
+
def close(self):
|
215
|
+
self.reset_state()
|
environments/items.py
ADDED
environments/planner.py
ADDED
@@ -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
|