plancraft 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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,542 @@
|
|
1
|
+
import glob
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import random
|
5
|
+
from abc import abstractmethod
|
6
|
+
from collections import defaultdict
|
7
|
+
from copy import deepcopy
|
8
|
+
from dataclasses import dataclass
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
|
12
|
+
from plancraft.environments.items import ALL_ITEMS
|
13
|
+
|
14
|
+
|
15
|
+
def clean_item_name(item: str) -> str:
|
16
|
+
return item.replace("minecraft:", "")
|
17
|
+
|
18
|
+
|
19
|
+
# find directory of file
|
20
|
+
dir_path = os.path.dirname(os.path.realpath(__file__))
|
21
|
+
TAG_TO_ITEMS: dict[str, list[str]] = {}
|
22
|
+
for tag_file in glob.glob(f"{dir_path}/tags/*.json"):
|
23
|
+
with open(tag_file) as file:
|
24
|
+
tag = json.load(file)
|
25
|
+
tag_name = tag_file.split("/")[-1].split(".")[0]
|
26
|
+
TAG_TO_ITEMS[tag_name] = [clean_item_name(v) for v in tag["values"]]
|
27
|
+
|
28
|
+
|
29
|
+
def item_to_id(item: str) -> int:
|
30
|
+
if item is None:
|
31
|
+
return len(ALL_ITEMS)
|
32
|
+
return ALL_ITEMS.index(clean_item_name(item))
|
33
|
+
|
34
|
+
|
35
|
+
def id_to_item(item_id: int) -> str:
|
36
|
+
if item_id == len(ALL_ITEMS):
|
37
|
+
return None
|
38
|
+
return ALL_ITEMS[item_id]
|
39
|
+
|
40
|
+
|
41
|
+
def convert_ingredients_to_table(ingredients: list[str]) -> np.array:
|
42
|
+
assert len(ingredients) == 9, "Crafting table must have 9 slots"
|
43
|
+
table = np.zeros((3, 3), dtype=int)
|
44
|
+
for index, item in enumerate(ingredients):
|
45
|
+
x, y = divmod(index, 3)
|
46
|
+
table[x, y] = item_to_id(item)
|
47
|
+
return table
|
48
|
+
|
49
|
+
|
50
|
+
def get_item(item):
|
51
|
+
"""
|
52
|
+
Iterator over the possible items in a recipe object
|
53
|
+
"""
|
54
|
+
if isinstance(item, list):
|
55
|
+
for i in item:
|
56
|
+
yield from get_item(i)
|
57
|
+
if isinstance(item, str):
|
58
|
+
if item.startswith("#"):
|
59
|
+
tag_items = TAG_TO_ITEMS[clean_item_name(item.replace("#", ""))]
|
60
|
+
yield from get_item(tag_items)
|
61
|
+
else:
|
62
|
+
yield clean_item_name(item)
|
63
|
+
if isinstance(item, dict):
|
64
|
+
if "item" in item:
|
65
|
+
yield clean_item_name(item["item"])
|
66
|
+
else:
|
67
|
+
tag_items = TAG_TO_ITEMS[clean_item_name(item["tag"])]
|
68
|
+
yield from get_item(tag_items)
|
69
|
+
|
70
|
+
|
71
|
+
@dataclass
|
72
|
+
class RecipeResult:
|
73
|
+
item: str
|
74
|
+
count: int = 1
|
75
|
+
|
76
|
+
|
77
|
+
class BaseRecipe:
|
78
|
+
@abstractmethod
|
79
|
+
def craft(self, table: np.array) -> RecipeResult:
|
80
|
+
pass
|
81
|
+
|
82
|
+
@abstractmethod
|
83
|
+
def smelt(self, ingredient: str) -> RecipeResult:
|
84
|
+
pass
|
85
|
+
|
86
|
+
@abstractmethod
|
87
|
+
def sample_inputs(self) -> tuple[dict[str, int], set]:
|
88
|
+
pass
|
89
|
+
|
90
|
+
@abstractmethod
|
91
|
+
def can_craft_from_inventory(self, inventory: dict[str, int]) -> bool:
|
92
|
+
pass
|
93
|
+
|
94
|
+
@abstractmethod
|
95
|
+
def craft_from_inventory(self, inventory: dict[str, int]) -> dict[str, int]:
|
96
|
+
pass
|
97
|
+
|
98
|
+
@property
|
99
|
+
def inputs(self) -> set:
|
100
|
+
raise NotImplementedError()
|
101
|
+
|
102
|
+
@property
|
103
|
+
def recipe_type(self) -> str:
|
104
|
+
raise NotImplementedError()
|
105
|
+
|
106
|
+
@property
|
107
|
+
def num_slots(self) -> int:
|
108
|
+
return NotImplementedError()
|
109
|
+
|
110
|
+
def __repr__(self) -> str:
|
111
|
+
pass
|
112
|
+
|
113
|
+
|
114
|
+
class ShapelessRecipe(BaseRecipe):
|
115
|
+
def __init__(self, recipe):
|
116
|
+
# list of counters that represent the different valid inputs
|
117
|
+
self.ingredients: list[dict[str, int]] = []
|
118
|
+
self.add_ingredient(recipe["ingredients"], 0, {})
|
119
|
+
|
120
|
+
self.ingredients_arr = np.stack(
|
121
|
+
[self.convert_ingredients_counter_to_arr(ing) for ing in self.ingredients],
|
122
|
+
axis=0,
|
123
|
+
)
|
124
|
+
|
125
|
+
result_item_name = clean_item_name(recipe["result"]["item"])
|
126
|
+
self.result = RecipeResult(result_item_name, recipe["result"].get("count", 1))
|
127
|
+
|
128
|
+
def add_ingredient(
|
129
|
+
self, ingredients_list: list[dict], index: int, current_counter: dict[str, int]
|
130
|
+
):
|
131
|
+
# Base case: all ingredients are processed, add current variant to the list
|
132
|
+
if index == len(ingredients_list):
|
133
|
+
self.ingredients.append(current_counter)
|
134
|
+
return
|
135
|
+
|
136
|
+
ingredient_names = list(get_item(ingredients_list[index]))
|
137
|
+
if len(ingredient_names) > 1:
|
138
|
+
# If the ingredient has alternatives, recurse for each one
|
139
|
+
# This is the case for fire_charge with coal/charcoal
|
140
|
+
for item_name in ingredient_names:
|
141
|
+
new_counter = deepcopy(current_counter)
|
142
|
+
new_counter[item_name] = new_counter.get(item_name, 0) + 1
|
143
|
+
self.add_ingredient(ingredients_list, index + 1, new_counter)
|
144
|
+
# single acceptable ingredient
|
145
|
+
elif len(ingredient_names) == 1:
|
146
|
+
item_name = ingredient_names[0]
|
147
|
+
current_counter[item_name] = current_counter.get(item_name, 0) + 1
|
148
|
+
self.add_ingredient(ingredients_list, index + 1, current_counter)
|
149
|
+
else:
|
150
|
+
raise ValueError("No item found in ingredient")
|
151
|
+
|
152
|
+
@staticmethod
|
153
|
+
def convert_ingredients_counter_to_arr(
|
154
|
+
ingredients_counter: dict[str, int],
|
155
|
+
) -> np.array:
|
156
|
+
arr = np.zeros(len(ALL_ITEMS) + 1)
|
157
|
+
total_slots = 0
|
158
|
+
for item, count in ingredients_counter.items():
|
159
|
+
arr[item_to_id(item)] = count
|
160
|
+
total_slots += count
|
161
|
+
# account for empty slots
|
162
|
+
arr[len(ALL_ITEMS)] = 9 - total_slots
|
163
|
+
return arr
|
164
|
+
|
165
|
+
def craft(self, table: np.array) -> tuple[RecipeResult, list[int]]:
|
166
|
+
assert table.shape == (3, 3), "Crafting table must have 3x3 shape"
|
167
|
+
table_arr = np.bincount(table.flatten(), minlength=len(ALL_ITEMS) + 1)
|
168
|
+
if (table_arr == self.ingredients_arr).all(axis=1).any():
|
169
|
+
indexes_to_decrement = []
|
170
|
+
for idx, item_id in enumerate(table.flatten()):
|
171
|
+
if item_id != len(ALL_ITEMS):
|
172
|
+
indexes_to_decrement.append(idx)
|
173
|
+
return self.result, indexes_to_decrement
|
174
|
+
|
175
|
+
return None
|
176
|
+
|
177
|
+
def smelt(self, ingredient: str):
|
178
|
+
return None
|
179
|
+
|
180
|
+
def sample_inputs(self) -> tuple[dict[str, int], set]:
|
181
|
+
exclude_set = set()
|
182
|
+
for ingredient_counter in self.ingredients:
|
183
|
+
for ingredient in ingredient_counter.keys():
|
184
|
+
if ingredient is not None:
|
185
|
+
exclude_set.add(ingredient)
|
186
|
+
|
187
|
+
# sample a random ingredients set from the list of possible
|
188
|
+
return deepcopy(random.choice(self.ingredients)), deepcopy(exclude_set)
|
189
|
+
|
190
|
+
@property
|
191
|
+
def inputs(self) -> set:
|
192
|
+
all_inputs = set()
|
193
|
+
for ingredient_counter in self.ingredients:
|
194
|
+
for ingredient in ingredient_counter.keys():
|
195
|
+
if ingredient is not None:
|
196
|
+
all_inputs.add(ingredient)
|
197
|
+
return all_inputs
|
198
|
+
|
199
|
+
@property
|
200
|
+
def recipe_type(self) -> str:
|
201
|
+
return "shapeless"
|
202
|
+
|
203
|
+
def can_craft_from_inventory(self, inventory: dict[str, int]) -> bool:
|
204
|
+
for ingredient_counter in self.ingredients:
|
205
|
+
temp_inventory = deepcopy(inventory)
|
206
|
+
for ingredient, count in ingredient_counter.items():
|
207
|
+
if (
|
208
|
+
ingredient not in temp_inventory
|
209
|
+
or temp_inventory[ingredient] < count
|
210
|
+
):
|
211
|
+
break
|
212
|
+
temp_inventory[ingredient] -= count
|
213
|
+
else:
|
214
|
+
return True
|
215
|
+
return False
|
216
|
+
|
217
|
+
def craft_from_inventory(self, inventory: dict[str, int]) -> dict[str, int]:
|
218
|
+
assert self.can_craft_from_inventory(inventory), "Cannot craft from inventory"
|
219
|
+
for ingredient_counter in self.ingredients:
|
220
|
+
temp_inventory = deepcopy(inventory)
|
221
|
+
for ingredient, count in ingredient_counter.items():
|
222
|
+
if temp_inventory.get(ingredient, 0) < count:
|
223
|
+
break
|
224
|
+
temp_inventory[ingredient] -= count
|
225
|
+
else:
|
226
|
+
new_inventory = deepcopy(inventory)
|
227
|
+
for ingredient, count in ingredient_counter.items():
|
228
|
+
new_inventory[ingredient] -= count
|
229
|
+
if new_inventory[ingredient] == 0:
|
230
|
+
del new_inventory[ingredient]
|
231
|
+
new_inventory[self.result.item] = (
|
232
|
+
new_inventory.get(self.result.item, 0) + self.result.count
|
233
|
+
)
|
234
|
+
return new_inventory
|
235
|
+
|
236
|
+
def __repr__(self):
|
237
|
+
return f"ShapelessRecipe({self.ingredients}, {self.result})"
|
238
|
+
|
239
|
+
def __prompt_repr__(self) -> str:
|
240
|
+
# use to get a simple representation of the recipe for prompting
|
241
|
+
out = []
|
242
|
+
for ingredients in self.ingredients:
|
243
|
+
ingredients_string = ", ".join(
|
244
|
+
[f"{count} {i}" for i, count in ingredients.items()]
|
245
|
+
)
|
246
|
+
out.append(
|
247
|
+
f"{ingredients_string} -> {self.result.count} {self.result.item}"
|
248
|
+
)
|
249
|
+
return "\n".join(out)
|
250
|
+
|
251
|
+
def sample_input_crafting_grid(self) -> list[dict[str, int]]:
|
252
|
+
# sample a random ingredient crafting arrangement to craft item
|
253
|
+
ingredients = deepcopy(random.choice(self.ingredients))
|
254
|
+
|
255
|
+
num_inputs = sum(ingredients.values())
|
256
|
+
crafting_table_slots = random.sample(range(1, 10), num_inputs)
|
257
|
+
|
258
|
+
ingredients_list = []
|
259
|
+
for i, count in ingredients.items():
|
260
|
+
ingredients_list += [i] * count
|
261
|
+
|
262
|
+
crafting_table = []
|
263
|
+
for ingredient, slot in zip(ingredients_list, crafting_table_slots):
|
264
|
+
if ingredient is not None:
|
265
|
+
crafting_table.append({"type": ingredient, "slot": slot, "quantity": 1})
|
266
|
+
|
267
|
+
return crafting_table
|
268
|
+
|
269
|
+
|
270
|
+
class ShapedRecipe(BaseRecipe):
|
271
|
+
def __init__(self, recipe):
|
272
|
+
self.kernel = self.extract_kernel(recipe)
|
273
|
+
|
274
|
+
self.kernel_height = len(self.kernel)
|
275
|
+
self.kernel_width = len(self.kernel[0])
|
276
|
+
self.kernel_size = self.kernel_height * self.kernel_width
|
277
|
+
|
278
|
+
result_item_name = clean_item_name(recipe["result"]["item"])
|
279
|
+
self.result = RecipeResult(result_item_name, recipe["result"].get("count", 1))
|
280
|
+
|
281
|
+
def possible_kernel_positions(self):
|
282
|
+
return [
|
283
|
+
(row, col)
|
284
|
+
for row in range(3 - self.kernel_height + 1)
|
285
|
+
for col in range(3 - self.kernel_width + 1)
|
286
|
+
]
|
287
|
+
|
288
|
+
def extract_kernel(self, recipe) -> np.array:
|
289
|
+
patterns = recipe["pattern"]
|
290
|
+
keys = recipe["key"]
|
291
|
+
|
292
|
+
# Convert pattern symbols to corresponding items ids
|
293
|
+
def symbol_to_items(symbol):
|
294
|
+
if symbol == " ":
|
295
|
+
return set([item_to_id(None)])
|
296
|
+
return set([item_to_id(item) for item in get_item(keys[symbol])])
|
297
|
+
|
298
|
+
# Convert each row of the pattern to a list of possible item lists
|
299
|
+
kernel = [[symbol_to_items(symbol) for symbol in row] for row in patterns]
|
300
|
+
return kernel
|
301
|
+
|
302
|
+
def craft(self, table: np.array) -> tuple[RecipeResult, list[int]]:
|
303
|
+
assert table.shape == (3, 3), "Crafting table must have 3x3 shape"
|
304
|
+
|
305
|
+
count_empty = (table == len(ALL_ITEMS)).sum()
|
306
|
+
should_be_empty_count = 9 - self.kernel_size
|
307
|
+
if count_empty < should_be_empty_count:
|
308
|
+
return None
|
309
|
+
|
310
|
+
for start_row, start_col in self.possible_kernel_positions():
|
311
|
+
# count number of empty slots in kernel
|
312
|
+
count_empty_in_kernel = (
|
313
|
+
table[
|
314
|
+
start_row : start_row + self.kernel_height,
|
315
|
+
start_col : start_col + self.kernel_width,
|
316
|
+
]
|
317
|
+
== len(ALL_ITEMS)
|
318
|
+
).sum()
|
319
|
+
# count number of empty slots outside
|
320
|
+
count_empty_outside_kernel = count_empty - count_empty_in_kernel
|
321
|
+
# check if the number of empty slots outside is correct
|
322
|
+
if count_empty_outside_kernel != should_be_empty_count:
|
323
|
+
continue
|
324
|
+
|
325
|
+
# check that all items in kernel match the table
|
326
|
+
indexes_to_decrement = []
|
327
|
+
for row in range(self.kernel_height):
|
328
|
+
for col in range(self.kernel_width):
|
329
|
+
if (
|
330
|
+
table[start_row + row, start_col + col]
|
331
|
+
not in self.kernel[row][col]
|
332
|
+
):
|
333
|
+
break
|
334
|
+
# add to indexes to decrement if not empty slot
|
335
|
+
if table[start_row + row, start_col + col] != len(ALL_ITEMS):
|
336
|
+
idx = (start_row + row) * 3 + start_col + col
|
337
|
+
indexes_to_decrement.append(idx)
|
338
|
+
else:
|
339
|
+
continue
|
340
|
+
break
|
341
|
+
else:
|
342
|
+
return self.result, indexes_to_decrement
|
343
|
+
return None
|
344
|
+
|
345
|
+
def smelt(self, ingredient: str):
|
346
|
+
return None
|
347
|
+
|
348
|
+
def sample_inputs(self) -> tuple[dict[str, int], set]:
|
349
|
+
input_counter = defaultdict(int)
|
350
|
+
exclude_set = set()
|
351
|
+
for row in self.kernel:
|
352
|
+
for item_set in row:
|
353
|
+
# sample a random item from the set
|
354
|
+
if len(item_set) > 1:
|
355
|
+
item_id = random.choice(list(item_set))
|
356
|
+
# add all items to exclude set
|
357
|
+
exclude_set.update(item_set)
|
358
|
+
# single acceptable ingredient
|
359
|
+
else:
|
360
|
+
item_id = list(item_set)[0]
|
361
|
+
exclude_set.add(item_id)
|
362
|
+
|
363
|
+
# exclude empty slot
|
364
|
+
if id_to_item(item_id) is not None:
|
365
|
+
input_counter[id_to_item(item_id)] += 1
|
366
|
+
|
367
|
+
# convert exclude set to item names
|
368
|
+
exclude_set = {
|
369
|
+
id_to_item(item_id) for item_id in exclude_set if id_to_item(item_id)
|
370
|
+
}
|
371
|
+
return dict(input_counter), exclude_set
|
372
|
+
|
373
|
+
@property
|
374
|
+
def inputs(self) -> set:
|
375
|
+
all_inputs = set()
|
376
|
+
for row in self.kernel:
|
377
|
+
for item_set in row:
|
378
|
+
all_inputs.update(item_set)
|
379
|
+
all_inputs = {
|
380
|
+
id_to_item(item_id) for item_id in all_inputs if id_to_item(item_id)
|
381
|
+
}
|
382
|
+
return all_inputs
|
383
|
+
|
384
|
+
@property
|
385
|
+
def recipe_type(self) -> str:
|
386
|
+
return "shaped"
|
387
|
+
|
388
|
+
def can_craft_from_inventory(self, inventory: dict[str, int]) -> bool:
|
389
|
+
temp_inventory = deepcopy(inventory)
|
390
|
+
for row in self.kernel:
|
391
|
+
for item_set in row:
|
392
|
+
for inventory_item in item_set:
|
393
|
+
item_name = id_to_item(inventory_item)
|
394
|
+
if item_name is None:
|
395
|
+
break
|
396
|
+
elif item_name in temp_inventory and temp_inventory[item_name] > 0:
|
397
|
+
temp_inventory[item_name] -= 1
|
398
|
+
break
|
399
|
+
else:
|
400
|
+
return False
|
401
|
+
return True
|
402
|
+
|
403
|
+
def craft_from_inventory(self, inventory: dict[str, int]) -> dict[str, int]:
|
404
|
+
assert self.can_craft_from_inventory(inventory), "Cannot craft from inventory"
|
405
|
+
new_inventory = deepcopy(inventory)
|
406
|
+
for row in self.kernel:
|
407
|
+
for item_set in row:
|
408
|
+
for inventory_item in item_set:
|
409
|
+
item_name = id_to_item(inventory_item)
|
410
|
+
if (
|
411
|
+
item_name
|
412
|
+
and item_name in new_inventory
|
413
|
+
and new_inventory[item_name] > 0
|
414
|
+
):
|
415
|
+
new_inventory[item_name] -= 1
|
416
|
+
if new_inventory[item_name] == 0:
|
417
|
+
del new_inventory[item_name]
|
418
|
+
break
|
419
|
+
# add result to inventory
|
420
|
+
new_inventory[self.result.item] = (
|
421
|
+
new_inventory.get(self.result.item, 0) + self.result.count
|
422
|
+
)
|
423
|
+
return new_inventory
|
424
|
+
|
425
|
+
def __repr__(self) -> str:
|
426
|
+
return f"ShapedRecipe({self.kernel}, {self.result})"
|
427
|
+
|
428
|
+
def __prompt_repr__(self) -> str:
|
429
|
+
string_kernel = []
|
430
|
+
for row in self.kernel:
|
431
|
+
row_col = []
|
432
|
+
for col in row:
|
433
|
+
valid_items = [str(id_to_item(i)) for i in col]
|
434
|
+
if valid_items[0] == "None":
|
435
|
+
valid_items[0] = "empty"
|
436
|
+
row_col.append("|".join(valid_items))
|
437
|
+
string_kernel.append("\t".join(row_col))
|
438
|
+
result_row = len(self.kernel) // 2
|
439
|
+
string_kernel[result_row] = (
|
440
|
+
string_kernel[result_row] + f" -> {self.result.count} {self.result.item}"
|
441
|
+
)
|
442
|
+
return "\n".join(string_kernel)
|
443
|
+
|
444
|
+
def sample_input_crafting_grid(self) -> list[dict[str, int]]:
|
445
|
+
# sample a random ingredient crafting arrangement to craft item
|
446
|
+
crafting_table = []
|
447
|
+
|
448
|
+
start_row, start_col = random.choice(self.possible_kernel_positions())
|
449
|
+
for x in range(0, len(self.kernel)):
|
450
|
+
for y in range(0, len(self.kernel[0])):
|
451
|
+
i = random.choice(list(self.kernel[x][y]))
|
452
|
+
object_type = id_to_item(i)
|
453
|
+
if object_type is None:
|
454
|
+
continue
|
455
|
+
location_x = start_row + x
|
456
|
+
location_y = start_col + y
|
457
|
+
crafting_table += [
|
458
|
+
{
|
459
|
+
"type": object_type,
|
460
|
+
"slot": location_x * 3 + location_y + 1,
|
461
|
+
"quantity": 1,
|
462
|
+
}
|
463
|
+
]
|
464
|
+
return crafting_table
|
465
|
+
|
466
|
+
|
467
|
+
class SmeltingRecipe(BaseRecipe):
|
468
|
+
def __init__(self, recipe):
|
469
|
+
self.ingredient = set(get_item(recipe["ingredient"]))
|
470
|
+
result_item_name = clean_item_name(recipe["result"])
|
471
|
+
self.result = RecipeResult(result_item_name, 1)
|
472
|
+
|
473
|
+
def smelt(self, ingredient: str) -> RecipeResult:
|
474
|
+
if ingredient in self.ingredient:
|
475
|
+
return self.result
|
476
|
+
return None
|
477
|
+
|
478
|
+
def craft(self, table: np.array):
|
479
|
+
return None
|
480
|
+
|
481
|
+
def can_craft_from_inventory(self, inventory: dict[str, int]) -> bool:
|
482
|
+
return len(set(inventory.keys()).intersection(self.ingredient)) > 0
|
483
|
+
|
484
|
+
def craft_from_inventory(
|
485
|
+
self, inventory: dict[str, int], quantity=1
|
486
|
+
) -> dict[str, int]:
|
487
|
+
assert self.can_craft_from_inventory(inventory), "Cannot craft from inventory"
|
488
|
+
|
489
|
+
new_inventory = deepcopy(inventory)
|
490
|
+
for ingredient in self.ingredient:
|
491
|
+
if ingredient in new_inventory and new_inventory[ingredient] >= quantity:
|
492
|
+
new_inventory[ingredient] -= quantity
|
493
|
+
if new_inventory[ingredient] == 0:
|
494
|
+
del new_inventory[ingredient]
|
495
|
+
new_inventory[self.result.item] = (
|
496
|
+
new_inventory.get(self.result.item, 0) + quantity # count
|
497
|
+
)
|
498
|
+
return new_inventory
|
499
|
+
return None
|
500
|
+
|
501
|
+
def sample_inputs(self) -> tuple[dict[str, int], set]:
|
502
|
+
# sample a random ingredient from the list of possible
|
503
|
+
# return a dict with the ingredient and its count
|
504
|
+
# also return the set of all possible ingredients
|
505
|
+
return {random.choice(deepcopy(list(self.ingredient))): 1}, deepcopy(
|
506
|
+
self.ingredient
|
507
|
+
)
|
508
|
+
|
509
|
+
@property
|
510
|
+
def inputs(self) -> set:
|
511
|
+
return deepcopy(self.ingredient)
|
512
|
+
|
513
|
+
@property
|
514
|
+
def recipe_type(self) -> str:
|
515
|
+
return "smelting"
|
516
|
+
|
517
|
+
def __repr__(self) -> str:
|
518
|
+
return f"SmeltingRecipe({self.ingredient}, {self.result})"
|
519
|
+
|
520
|
+
def __prompt_repr__(self) -> str:
|
521
|
+
# use to get a simple representation of the recipe for prompting
|
522
|
+
out = []
|
523
|
+
for i in self.ingredient:
|
524
|
+
out.append(f"1 {i} -> {self.result.count} {self.result.item}")
|
525
|
+
return "\n".join(out)
|
526
|
+
|
527
|
+
|
528
|
+
def recipe_factory(recipe: dict) -> BaseRecipe:
|
529
|
+
if recipe["type"] == "minecraft:crafting_shapeless":
|
530
|
+
return ShapelessRecipe(recipe)
|
531
|
+
if recipe["type"] == "minecraft:crafting_shaped":
|
532
|
+
return ShapedRecipe(recipe)
|
533
|
+
if recipe["type"] == "minecraft:smelting":
|
534
|
+
return SmeltingRecipe(recipe)
|
535
|
+
|
536
|
+
|
537
|
+
RECIPES: dict[str, list[BaseRecipe]] = defaultdict(list)
|
538
|
+
for f in glob.glob(f"{dir_path}/recipes/*.json"):
|
539
|
+
with open(f) as file:
|
540
|
+
recipe = json.load(file)
|
541
|
+
if r := recipe_factory(recipe):
|
542
|
+
RECIPES[r.result.item].append(r)
|