plancraft 0.1.1__py3-none-any.whl → 0.1.2__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.
environments/recipes.py DELETED
@@ -1,542 +0,0 @@
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)