plancraft 0.1.0__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,215 +0,0 @@
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 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