Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 29 additions & 50 deletions src/DataValidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,31 @@ class DataValidation():

@staticmethod
def checkItemNamesInLocationRequires():
from .Rules import ITEM_REGEX
for location in DataValidation.location_table_with_events:
if "requires" not in location:
continue

if isinstance(location["requires"], str):
# parse user written statement into list of each item
for item in re.findall(r'\|[^|]+\|', location["requires"]):
if item.lower() == "or" or item.lower() == "and" or item == ")" or item == "(":
continue
else:
# if it's a category, validate that the category exists
if '@' in item:
item = item.replace("|", "")
item_parts = item.split(":")
item_name = item

if len(item_parts) > 1:
item_name = item_parts[0]

item_name = item_name[1:]
item_category_exists = len([item for item in DataValidation.item_table_with_events if item_name in item.get('category', [])]) > 0

if not item_category_exists:
raise ValidationError("Item category %s is required by location %s but is misspelled or does not exist." % (item_name, location.get("name")))

continue
for match in ITEM_REGEX.finditer(location["requires"]):
is_category = bool(match.group(1))
item_name = match.group(2)
item_count = (str(match.group(3) or "1")).lstrip(':').strip()

item = item.replace("|", "")
if not item_count.isnumeric() and item_count not in ["all", "half"] and not item_count.endswith('%'):
item_name = match.group(0).strip("|")
logging.debug(f'Invalid item_count "{item_count}" found, reverting to initial item_name "{item_name}"')

item_parts = item.split(":")
item_name = item
# if it's a category, validate that the category exists
if is_category:
item_category_exists = len([item for item in DataValidation.item_table_with_events if item_name in item.get('category', [])]) > 0

if len(item_parts) > 1:
item_name = item_parts[0]
if not item_category_exists:
raise ValidationError("Item category %s is required by location %s but is misspelled or does not exist." % (item_name, location.get("name")))

continue
else:
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0

if not item_exists:
Expand Down Expand Up @@ -96,6 +86,7 @@ def checkItemNamesInLocationRequires():

@staticmethod
def checkItemNamesInRegionRequires():
from .Rules import ITEM_REGEX
for region_name in DataValidation.region_table:
region = DataValidation.region_table[region_name]

Expand All @@ -104,35 +95,23 @@ def checkItemNamesInRegionRequires():

if isinstance(region["requires"], str):
# parse user written statement into list of each item
for item in re.findall(r'\|[^|]+\|', region["requires"]):
if item.lower() == "or" or item.lower() == "and" or item == ")" or item == "(":
continue
else:
# if it's a category, validate that the category exists
if '@' in item:
item = item.replace("|", "")
item_parts = item.split(":")
item_name = item

if len(item_parts) > 1:
item_name = item_parts[0]
for match in ITEM_REGEX.finditer(region["requires"]):
is_category = bool(match.group(1))
item_name = match.group(2)
item_count = (str(match.group(3) or "1")).lstrip(':').strip()

item_name = item_name[1:]
item_category_exists = len([item for item in DataValidation.item_table_with_events if item_name in item.get('category', [])]) > 0
if not item_count.isnumeric() and item_count not in ["all", "half"] and not item_count.endswith('%'):
item_name = match.group(0).strip("|")
logging.debug(f'Invalid item_count "{item_count}" found, reverting to initial item_name "{item_name}"')

if not item_category_exists:
raise ValidationError("Item category %s is required by region %s but is misspelled or does not exist." % (item_name, region_name))
# if it's a category, validate that the category exists
if is_category:
item_category_exists = len([item for item in DataValidation.item_table_with_events if item_name in item.get('category', [])]) > 0

continue

item = item.replace("|", "")

item_parts = item.split(":")
item_name = item

if len(item_parts) > 1:
item_name = item_parts[0]
if not item_category_exists:
raise ValidationError("Item category %s is required by region %s but is misspelled or does not exist." % (item_name, region_name))

else:
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0

if not item_exists:
Expand Down
10 changes: 5 additions & 5 deletions src/Items.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

item_id_to_name: dict[int, str] = {}
item_name_to_item: dict[str, dict] = {}
item_name_groups: dict[str, str] = {}
item_name_groups: dict[str, set[str]] = {}
advancement_item_names: set[str] = set()
lastItemId = -1

Expand Down Expand Up @@ -47,8 +47,8 @@

for c in item.get("category", []):
if c not in item_name_groups:
item_name_groups[c] = []
item_name_groups[c].append(item_name)
item_name_groups[c] = set()
item_name_groups[c].add(item_name)

#Just lowercase the values here to remove all the .lower.strip down the line
item['value'] = {k.lower().strip(): v
Expand All @@ -57,8 +57,8 @@
for v in item.get("value", {}).keys():
group_name = f"has_{v}_value"
if group_name not in item_name_groups:
item_name_groups[group_name] = []
item_name_groups[group_name].append(item_name)
item_name_groups[group_name] = set()
item_name_groups[group_name].add(item_name)

item_id_to_name[None] = "__Victory__"
item_name_to_id = {name: id for id, name in item_id_to_name.items()}
Expand Down
12 changes: 9 additions & 3 deletions src/Locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@

location_id_to_name: dict[int, str] = {}
location_name_to_location: dict[str, dict[str, Any]] = {}
location_name_groups: dict[str, list[str]] = {}
location_name_groups: dict[str, set[str]] = {}
event_name_to_event: dict[str, dict[str, Any]] = {}
event_name_groups: dict[str, set[str]] = {}

for loc in location_table:
loc_name = loc.get("name", f"Unnamed Location {loc['id']}")
Expand All @@ -56,8 +57,8 @@

for c in loc.get("category", []):
if c not in location_name_groups:
location_name_groups[c] = []
location_name_groups[c].append(loc_name)
location_name_groups[c] = set()
location_name_groups[c].add(loc_name)


# location_id_to_name[None] = "__Manual Game Complete__"
Expand All @@ -83,6 +84,11 @@
if 'region' not in event:
event_name_to_event[event_name]['region'] = "Manual"
event_table[key]['region'] = "Manual"
for c in event.get("category", []):
if c not in event_name_groups:
event_name_groups[c] = set()
event_name_groups[c].add(event['name'])

id += 1

######################
Expand Down
98 changes: 48 additions & 50 deletions src/Rules.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dataclasses
import inspect
from typing import TYPE_CHECKING, Any, Callable, Optional
from typing import TYPE_CHECKING, Any, Callable, Optional, Any
from enum import IntEnum
from operator import eq, ge, le

Expand Down Expand Up @@ -62,7 +62,7 @@ def construct_logic_error(location_or_region: dict, source: LogicErrorSource) ->
def infix_to_postfix(expr: str, location: dict) -> str:
prec: dict[str, int] = {"&": 2, "|": 2, "!": 3}
stack: list[str] = []
postfix = ""
postfix: str = ""

try:
for c in expr:
Expand All @@ -87,8 +87,8 @@ def infix_to_postfix(expr: str, location: dict) -> str:
return postfix


def evaluate_postfix(expr: str, location: str) -> bool:
stack = []
def evaluate_postfix(expr: str, location: dict) -> bool:
stack: list[bool] = []

try:
for c in expr:
Expand Down Expand Up @@ -116,28 +116,28 @@ def evaluate_postfix(expr: str, location: str) -> bool:
return stack.pop()

def set_rules(world: "ManualWorld", multiworld: MultiWorld, player: int):
def evaluate_nonnumeric_count(item_name: str, item_count: str, is_category: bool, area: dict) -> int:
def evaluate_nonnumeric_count(item_base: str, item_name: str, item_count: str, is_category: bool, area: dict) -> tuple[str, int]:
item_count = item_count.strip()
if item_count.isnumeric():
return int(item_count)
return item_name, int(item_count)

items_counts = world.get_item_counts(player, only_progression=True)
if is_category:
category_items = [item for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]]
category_items += [event for event in world.event_name_to_event.values() if "category" in event and item_name in event["category"]]
total_count = sum([items_counts.get(category_item["name"], 0) for category_item in category_items])
total_count = sum([items_counts.get(item, 0) for item in world.item_and_event_name_groups.get(item_name, set())])
else:
total_count = items_counts.get(item_name, 0)

if item_count == 'all':
return total_count
count = total_count
elif item_count == 'half':
return int(total_count / 2)
count = int(total_count / 2)
elif item_count.endswith('%') and len(item_count) > 1:
percent = clamp(float(item_count[:-1]) / 100, 0, 1)
return math.ceil(total_count * percent)

raise ValueError(f"Invalid item count `{item_name}` in {area}.")
count = math.ceil(total_count * percent)
# If invalid count assume its actually part of the item name
else:
item_name = item_base.strip("|")
count = 1
return item_name, count

def construct_rule_from_string(area: dict) -> "rule_builder.rules.Rule | None":
if not use_rulebuilder:
Expand All @@ -152,7 +152,7 @@ def construct_rule_from_string(area: dict) -> "rule_builder.rules.Rule | None":
def recursively_tokenize_manual_rule(partial: str) -> "rule_builder.rules.Rule | None":
if not partial:
return rule_builder.rules.True_()
rule = None
rule: Rule | None = None
remaining = ''
partial = partial.strip()
if match := ITEM_REGEX.match(partial):
Expand All @@ -163,10 +163,10 @@ def recursively_tokenize_manual_rule(partial: str) -> "rule_builder.rules.Rule |
if item_count.isnumeric():
count = int(item_count)
else:
count = evaluate_nonnumeric_count(item_name, item_count, is_category, area)
item_name, count = evaluate_nonnumeric_count(match.group(0), item_name, item_count, is_category, area)

if is_category:
rule = rule_builder.rules.HasGroup(item_name, count)
rule = rule_builder.rules.HasFromList(*world.item_and_event_name_groups.get(item_name, set()), count=count)
Comment thread
nicopop marked this conversation as resolved.
else:
rule = rule_builder.rules.Has(item_name, count)
remaining = partial[len(match.group(0)):]
Expand Down Expand Up @@ -202,12 +202,23 @@ def recursively_tokenize_manual_rule(partial: str) -> "rule_builder.rules.Rule |

if rule is None:
if not rule_class:
print(f'Warning: Could not find Rule implmenentation of {func_name}.')
print(f'Warning: Could not find Rule implementation of {func_name}.')
# By returning None, we're saying "This entire requires string can't be done with a Rule. Fall back to the pre-rb lambdas"
return None

rule = rule_class(*func_args)
remaining = partial[len(match.group(0)):]
elif partial[0] == "(":
func_founds: dict[int, str] = {}
id: int = 0
for match in FUNCTION_REGEX.finditer(partial):
if match.group(0) not in partial:
# already done all of them
continue
func_founds[id] = match.group(0)
# looks like : {{Function#0}}
partial = partial.replace(match.group(0), f"{{{{Function#{id}}}}}")
id += 1
inner = ''
queue = list(partial[1:])
stack = 1
Expand All @@ -219,8 +230,11 @@ def recursively_tokenize_manual_rule(partial: str) -> "rule_builder.rules.Rule |
stack -= 1
else:
inner += c
rule = recursively_tokenize_manual_rule(inner)
remaining = "".join(queue)
for id, func in func_founds.items():
remaining = remaining.replace(f"{{{{Function#{id}}}}}", func)
inner = inner.replace(f"{{{{Function#{id}}}}}", func)
rule = recursively_tokenize_manual_rule(inner)
else:
print(f'Could not convert {partial} into a Rule')
return None
Expand Down Expand Up @@ -303,44 +317,28 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int =
# parse user written statement into list of each item
for match in ITEM_REGEX.finditer(requires_list):
item_base = match.group(0)
is_category = match.group(1)
is_category = bool(match.group(1))
item_name = match.group(2)
item_count = match.group(3)
require_type = 'item'

if item_base not in requires_list:
# previous instance of this item was already processed
continue

if is_category:
require_type = 'category'

if not item_count:
item_count = "1"
item_count = item_count.lstrip(':')

total = 0

if require_type == 'category':
category_items = [item for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]]
category_items += [event for event in world.event_name_to_event.values() if "category" in event and item_name in event["category"]]
numeric_count = evaluate_nonnumeric_count(item_name, item_count, True, area)

for category_item in category_items:
total += state.count(category_item["name"], player)

if total >= numeric_count:
requires_list = requires_list.replace(item_base, "1")
elif require_type == 'item':
numeric_count = evaluate_nonnumeric_count(item_name, item_count, False, area)
item_name, numeric_count = evaluate_nonnumeric_count(item_base, item_name, item_count, is_category, area)

total = state.count(item_name, player)

if total >= numeric_count:
requires_list = requires_list.replace(item_base, "1")
if is_category:
found = state.has_from_list(world.item_and_event_name_groups.get(item_name, set()), player, numeric_count)
else:
raise ValueError(f'Unknown require_type {require_type}')
found = state.has(item_name, player, numeric_count)

if total < numeric_count:
if found:
requires_list = requires_list.replace(item_base, "1")
else:
requires_list = requires_list.replace(item_base, "0")

requires_list = AND_REGEX.sub('&', requires_list, count=0)
Expand Down Expand Up @@ -426,7 +424,10 @@ def fullLocationOrRegionCheck(state: CollectionState, area: dict):
used_location_names.extend([l.name for l in multiworld.get_region(region, player).locations])
for exitRegion in multiworld.get_region(region, player).entrances:
extra = extra_entrance_rules.get(exitRegion.name, {})
rb_rule = construct_rule_from_string(regionMap[region])
area = regionMap[region]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're reassigning ["name"], this should probably be a .copy()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its what we did before RB, I just moved it here so errors after this part of the code have a name and are detected as a region.
one thing we could do instead is move that code to be in regions.py so that they use the exact region name instead of region A to B thing they have currently (if i remember correctly)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm aware that it's something we're already doing, that's why I approved regardless.

But when you moved it it brought it to my attention as a thing we probably shouldn't be doing.

area["name"] = exitRegion.name
area['is_region'] = True
rb_rule = construct_rule_from_string(area)
if rb_rule is not None:
if extra:
rb_extra_rule = construct_rule_from_string(extra)
Expand All @@ -435,10 +436,7 @@ def fullLocationOrRegionCheck(state: CollectionState, area: dict):
rb_rule = rb_rule & rb_extra_rule
world.set_rule(world.get_entrance(exitRegion.name), rb_rule)
else:
def fullRegionCheck(state: CollectionState, region=regionMap[region], region_name=exitRegion.name):
region['name'] = region_name
region['is_region'] = True

def fullRegionCheck(state: CollectionState, region=area):
return fullLocationOrRegionCheck(state, region)

add_rule(world.get_entrance(exitRegion.name), fullRegionCheck)
Expand Down
Loading
Loading