plancraft 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,212 +0,0 @@
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()
environments/items.py DELETED
@@ -1,10 +0,0 @@
1
- import json
2
- import os
3
-
4
-
5
- # originally mc_constants.1.16.json
6
- path = os.path.join(os.path.dirname(__file__), "constants.json")
7
- all_data = json.load(open(path))
8
-
9
-
10
- ALL_ITEMS = [item["type"] for item in all_data["items"]]
environments/planner.py DELETED
@@ -1,109 +0,0 @@
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