Skip to content

Commit 19bc213

Browse files
authored
Merge pull request #39 from ManualForArchipelago/item_with_values
Add logic for item with values
2 parents f412da1 + cb3c9d3 commit 19bc213

File tree

10 files changed

+490
-270
lines changed

10 files changed

+490
-270
lines changed

schemas/Manual.items.schema.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@
5757
"type": "boolean",
5858
"default": true
5959
},
60+
"value": {
61+
"description": "(Optional) A dictonary of values this item has in the format {\"name\": int,\"otherName\": int} \nUsed with the {ItemValue(Name: int)} in location requires \neg. \"value\": {\"coins\":10} mean this item is worth 10 coins",
62+
"type": "object",
63+
"patternProperties": {
64+
"^.+$": {
65+
"anyOf": [
66+
{
67+
"type": "integer",
68+
"description": "A value that this item has must be 'name':integer \neg. \"coins\": 10"
69+
}
70+
]
71+
}
72+
}
73+
},
6074
"_comment": {"$ref": "#/definitions/comment"}
6175

6276
},

src/DataValidation.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import logging
22
import re
33
import json
4+
from worlds.AutoWorld import World
5+
from BaseClasses import MultiWorld, ItemClassification
6+
47

58
class ValidationError(Exception):
69
pass
@@ -192,6 +195,109 @@ def checkItemsThatShouldBeRequired():
192195
if item["name"] in region_requires:
193196
raise ValidationError("Item %s is required by region %s, but the item is not marked as progression." % (item["name"], region_name))
194197

198+
@staticmethod
199+
def _checkLocationRequiresForItemValueWithRegex(values_requested: dict[str, int], requires) -> dict[str, int]:
200+
if isinstance(requires, str) and 'ItemValue' in requires:
201+
for result in re.findall(r'\{ItemValue\(([^:]*)\:([^)]+)\)\}', requires):
202+
value = result[0].lower().strip()
203+
count = int(result[1])
204+
if not values_requested.get(value):
205+
values_requested[value] = count
206+
else:
207+
values_requested[value] = max(values_requested[value], count)
208+
return values_requested
209+
210+
@staticmethod
211+
def checkIfEnoughItemsForValue():
212+
values_available = {}
213+
values_requested = {}
214+
215+
# First find the biggest values required by locations
216+
for location in DataValidation.location_table:
217+
if "requires" not in location:
218+
continue
219+
220+
# convert to json so we don't have to guess the data type
221+
location_requires = json.dumps(location["requires"])
222+
223+
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, location_requires)
224+
# Second, check region requires for the presence of item name
225+
for region_name in DataValidation.region_table:
226+
region = DataValidation.region_table[region_name]
227+
228+
if "requires" not in region:
229+
continue
230+
231+
# convert to json so we don't have to guess the data type
232+
region_requires = json.dumps(region["requires"])
233+
234+
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, region_requires)
235+
# then if something is requested, we loop items
236+
if values_requested:
237+
238+
# get all the available values with total count
239+
for item in DataValidation.item_table:
240+
# if the item is already progression, no need to check
241+
if not item.get("progression") and not item.get("progression_skip_balancing"):
242+
continue
243+
244+
item_count = item.get('count', None)
245+
if item_count is None: #check with none because 0 == false
246+
item_count = '1'
247+
248+
for key, count in item.get("value", {}).items():
249+
if not values_available.get(key.lower().strip()):
250+
values_available[key] = 0
251+
values_available[key] += int(count) * int(item_count)
252+
253+
# compare whats available vs requested
254+
errors = []
255+
for value, count in values_requested.items():
256+
if values_available.get(value, 0) < count:
257+
errors.append(f" '{value}': {values_available.get(value, 0)} out of the {count} {value} worth of progression items required can be found.")
258+
if errors:
259+
raise ValidationError("There are not enough progression items for the following values: \n" + "\n".join(errors))
260+
261+
@staticmethod
262+
def preFillCheckIfEnoughItemsForValue(world: World, multiworld: MultiWorld):
263+
from .Helpers import get_items_with_value, get_items_for_player
264+
player = world.player
265+
values_requested = {}
266+
267+
for region in multiworld.regions:
268+
if region.player != player:
269+
continue
270+
271+
manualregion = DataValidation.region_table.get(region.name, {})
272+
if "requires" in manualregion and manualregion["requires"]:
273+
region_requires = json.dumps(manualregion["requires"])
274+
275+
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, region_requires)
276+
277+
for location in region.locations:
278+
manualLocation = world.location_name_to_location.get(location.name, {})
279+
if "requires" in manualLocation and manualLocation["requires"]:
280+
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, manualLocation["requires"])
281+
282+
# compare whats available vs requested but only if there's anything requested
283+
if values_requested:
284+
errors = []
285+
existing_items = [item for item in get_items_for_player(multiworld, player) if item.code is not None and
286+
item.classification == ItemClassification.progression or item.classification == ItemClassification.progression_skip_balancing]
287+
288+
for value, val_count in values_requested.items():
289+
items_value = get_items_with_value(world, multiworld, value, player, True)
290+
found_count = 0
291+
if items_value:
292+
for item in existing_items:
293+
if item.name in items_value:
294+
found_count += items_value[item.name]
295+
296+
if found_count < val_count:
297+
errors.append(f" '{value}': {found_count} out of the {val_count} {value} worth of progression items required can be found.")
298+
if errors:
299+
raise ValidationError("There are not enough progression items for the following value(s): \n" + "\n".join(errors))
300+
195301
@staticmethod
196302
def checkRegionsConnectingToOtherRegions():
197303
for region_name in DataValidation.region_table:
@@ -338,7 +444,16 @@ def checkForNonStartingRegionsThatAreUnreachable():
338444
raise ValidationError("The region '%s' is set as a non-starting region, but has no regions that connect to it. It will be inaccessible." % nonstarter)
339445

340446

447+
def runPreFillDataValidation(world: World, multiworld: MultiWorld):
448+
validation_errors = []
341449

450+
# check if there is enough items with values
451+
try: DataValidation.preFillCheckIfEnoughItemsForValue(world, multiworld)
452+
except ValidationError as e: validation_errors.append(e)
453+
454+
if validation_errors:
455+
newline = "\n"
456+
raise Exception(f"\nValidationError(s) for pre_fill of player {world.player}: \n\n{newline.join([' - ' + str(validation_error) for validation_error in validation_errors])}\n\n")
342457
# Called during stage_assert_generate
343458
def runGenerationDataValidation() -> None:
344459
validation_errors = []
@@ -358,6 +473,10 @@ def runGenerationDataValidation() -> None:
358473
try: DataValidation.checkItemsThatShouldBeRequired()
359474
except ValidationError as e: validation_errors.append(e)
360475

476+
# check if there's enough Items with values to get to every location requesting it
477+
try: DataValidation.checkIfEnoughItemsForValue()
478+
except ValidationError as e: validation_errors.append(e)
479+
361480
# check that regions that are connected to are correct
362481
try: DataValidation.checkRegionsConnectingToOtherRegions()
363482
except ValidationError as e: validation_errors.append(e)

src/Helpers.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from BaseClasses import MultiWorld
1+
from BaseClasses import MultiWorld, Item
2+
from typing import Optional, List
3+
from worlds.AutoWorld import World
24
from .Data import category_table
35
from .Items import ManualItem
46
from .Locations import ManualLocation
@@ -85,4 +87,36 @@ def _is_manualobject_enabled(multiworld: MultiWorld, player: int, object: any) -
8587
enabled = False
8688
break
8789

88-
return enabled
90+
return enabled
91+
92+
def get_items_for_player(multiworld: MultiWorld, player: int) -> List[Item]:
93+
"""Return list of items of a player including placed items"""
94+
return [i for i in multiworld.get_items() if i.player == player]
95+
96+
def get_items_with_value(world: World, multiworld: MultiWorld, value: str, player: Optional[int] = None, force: bool = False) -> dict[str, int]:
97+
"""Return a dict of every items with a specific value type present in their respective 'value' dict\n
98+
Output in the format 'Item Name': 'value count'\n
99+
Keep a cache of the result and wont redo unless 'force == True'
100+
"""
101+
if player is None:
102+
player = world.player
103+
104+
player_items = get_items_for_player(multiworld, player)
105+
# Just a small check to prevent caching {} if items don't exist yet
106+
if not player_items:
107+
return {value: -1}
108+
109+
value = value.lower().strip()
110+
111+
if not hasattr(world, 'item_values'): #Cache of just the item values
112+
world.item_values = {}
113+
114+
if not world.item_values.get(player):
115+
world.item_values[player] = {}
116+
117+
if value not in world.item_values.get(player, {}).keys() or force:
118+
item_with_values = {i.name: world.item_name_to_item[i.name]['value'].get(value, 0)
119+
for i in player_items if i.code is not None
120+
and i.name in world.item_name_groups.get(f'has_{value}_value', [])}
121+
world.item_values[player][value] = item_with_values
122+
return world.item_values[player].get(value)

src/Items.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
item_name_groups[c] = []
4343
item_name_groups[c].append(item_name)
4444

45+
for v in item.get("value", {}).keys():
46+
group_name = f"has_{v.lower().strip()}_value"
47+
if group_name not in item_name_groups:
48+
item_name_groups[group_name] = []
49+
item_name_groups[group_name].append(item_name)
50+
4551
item_id_to_name[None] = "__Victory__"
4652
item_name_to_id = {name: id for id, name in item_id_to_name.items()}
4753

src/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .Meta import world_description, world_webworld, enable_region_diagram
1414
from .Locations import location_id_to_name, location_name_to_id, location_name_to_location, location_name_groups, victory_names
1515
from .Items import item_id_to_name, item_name_to_id, item_name_to_item, item_name_groups
16-
from .DataValidation import runGenerationDataValidation
16+
from .DataValidation import runGenerationDataValidation, runPreFillDataValidation
1717

1818
from .Regions import create_regions
1919
from .Items import ManualItem
@@ -298,6 +298,10 @@ def generate_basic(self):
298298
from Utils import visualize_regions
299299
visualize_regions(self.multiworld.get_region("Menu", self.player), f"{self.game}_{self.player}.puml")
300300

301+
def pre_fill(self):
302+
# DataValidation after all the hooks are done but before fill
303+
runPreFillDataValidation(self, self.multiworld)
304+
301305
def fill_slot_data(self):
302306
slot_data = before_fill_slot_data({}, self, self.multiworld, self.player)
303307

@@ -386,7 +390,7 @@ def client_data(self):
386390
'player_id': self.player,
387391
'items': self.item_name_to_item,
388392
'locations': self.location_name_to_location,
389-
# todo: extract connections out of mutliworld.get_regions() instead, in case hooks have modified the regions.
393+
# todo: extract connections out of multiworld.get_regions() instead, in case hooks have modified the regions.
390394
'regions': region_table,
391395
'categories': category_table
392396
}

0 commit comments

Comments
 (0)