forked from mirror/Archipelago
* initial commit of rules engine * implement most of the stuff * add docs and fill out rest of the functionality * add in explain functions * dedupe items and add more docs * pr feedback and optimization updates * Self is not in typing on 3.10 * fix test * Update docs/rule builder.md Co-authored-by: BadMagic100 <dempsey.sean@outlook.com> * pr feedback * love it when CI gives me different results than local * add composition with bitwise and and or * strongly typed option filtering * skip resolving location parent region * update docs * update typing and add decorator * add string explains * move simplify code to world * add wrapper rule * I may need to abandon the generic typing * missing space for faris * fix hashing for resolved rules * thank u typing extensions ilu * remove bad cacheable check * add decorator to assign hash and rule name * more type crimes... * region access rules are now cached * break compatibility so new features work * update docs * replace decorators with __init_subclass__ * ok now the frozen dataclass is automatic * one more type fix for the road * small fixes and caching tests * play nicer with tests * ok actually fix the tests * add item_mapping for faris * add more state helpers as rules * fix has from list rules * fix can reach location caching and add set completion condition * fix can reach entrance caching * implement HasGroup and HasGroupUnique * add more tests and fix some bugs * Add name arg to create_entrance Co-authored-by: roseasromeo <11944660+roseasromeo@users.noreply.github.com> * fix json dumping option filters * restructure and test serialization * add prop to disable caching * switch to __call__ and revert access_rule changes * update docs and make edge cases match * ruff has lured me into a false sense of security * also unused * fix disabling caching * move filter function to filter class * add more docs * tests for explain functions * Update docs/rule builder.md Co-authored-by: roseasromeo <11944660+roseasromeo@users.noreply.github.com> * chore: Strip out uses of TYPE_CHECKING as much as possible * chore: add empty webworld for test * chore: optimize rule evaluations * remove getattr from hot code paths * testing new cache flags * only clear cache for rules cached as false in collect * update test for new behaviour * do not have rules inherit from each other * update docs on caching * fix name of attribute * make explain messages more colorful * fix issue with combining rules with different options * add convenience functions for filtering * use an operator with higher precedence * name conflicts less with optionfilter * move simplify and instance caching code * update docs * kill resolve_rule * kill true_rule and false_rule * move helpers to base classes * update docs * I really should finish all of my * fix test * rename mixin * fix typos * refactor rule builder into folder for better imports * update docs * do not dupe collectionrule * docs review feedback * missed a file * remove rule_caching_enabled from base World * update docs on caching * shuffle around some docs * use option instead of option.value * add in operator and more testing * rm World = object * test fixes * move cache to logic mixin * keep test rule builder world out of global registry * todone * call register_dependencies automatically * move register deps call to call_single * add filtered_resolution * allow bool opts on filters * fix serialization tests * allow reverse operations --------- Co-authored-by: BadMagic100 <dempsey.sean@outlook.com> Co-authored-by: roseasromeo <11944660+roseasromeo@users.noreply.github.com>
147 lines
6.9 KiB
Python
147 lines
6.9 KiB
Python
from collections import defaultdict
|
|
from typing import ClassVar, cast
|
|
|
|
from typing_extensions import override
|
|
|
|
from BaseClasses import CollectionState, Item, MultiWorld, Region
|
|
from worlds.AutoWorld import LogicMixin, World
|
|
|
|
from .rules import Rule
|
|
|
|
|
|
class CachedRuleBuilderWorld(World):
|
|
"""A World subclass that provides helpers for interacting with the rule builder"""
|
|
|
|
rule_item_dependencies: dict[str, set[int]]
|
|
"""A mapping of item name to set of rule ids"""
|
|
|
|
rule_region_dependencies: dict[str, set[int]]
|
|
"""A mapping of region name to set of rule ids"""
|
|
|
|
rule_location_dependencies: dict[str, set[int]]
|
|
"""A mapping of location name to set of rule ids"""
|
|
|
|
rule_entrance_dependencies: dict[str, set[int]]
|
|
"""A mapping of entrance name to set of rule ids"""
|
|
|
|
item_mapping: ClassVar[dict[str, str]] = {}
|
|
"""A mapping of actual item name to logical item name.
|
|
Useful when there are multiple versions of a collected item but the logic only uses one. For example:
|
|
item = Item("Currency x500"), rule = Has("Currency", count=1000), item_mapping = {"Currency x500": "Currency"}"""
|
|
|
|
rule_caching_enabled: ClassVar[bool] = True
|
|
"""Flag to inform rules that the caching system for this world is enabled. It should not be overridden."""
|
|
|
|
def __init__(self, multiworld: MultiWorld, player: int) -> None:
|
|
super().__init__(multiworld, player)
|
|
self.rule_item_dependencies = defaultdict(set)
|
|
self.rule_region_dependencies = defaultdict(set)
|
|
self.rule_location_dependencies = defaultdict(set)
|
|
self.rule_entrance_dependencies = defaultdict(set)
|
|
|
|
@override
|
|
def register_rule_dependencies(self, resolved_rule: Rule.Resolved) -> None:
|
|
for item_name, rule_ids in resolved_rule.item_dependencies().items():
|
|
self.rule_item_dependencies[item_name] |= rule_ids
|
|
for region_name, rule_ids in resolved_rule.region_dependencies().items():
|
|
self.rule_region_dependencies[region_name] |= rule_ids
|
|
for location_name, rule_ids in resolved_rule.location_dependencies().items():
|
|
self.rule_location_dependencies[location_name] |= rule_ids
|
|
for entrance_name, rule_ids in resolved_rule.entrance_dependencies().items():
|
|
self.rule_entrance_dependencies[entrance_name] |= rule_ids
|
|
|
|
def register_rule_builder_dependencies(self) -> None:
|
|
"""Register all rules that depend on locations or entrances with their dependencies"""
|
|
for location_name, rule_ids in self.rule_location_dependencies.items():
|
|
try:
|
|
location = self.get_location(location_name)
|
|
except KeyError:
|
|
continue
|
|
if not isinstance(location.access_rule, Rule.Resolved):
|
|
continue
|
|
for item_name in location.access_rule.item_dependencies():
|
|
self.rule_item_dependencies[item_name] |= rule_ids
|
|
for region_name in location.access_rule.region_dependencies():
|
|
self.rule_region_dependencies[region_name] |= rule_ids
|
|
|
|
for entrance_name, rule_ids in self.rule_entrance_dependencies.items():
|
|
try:
|
|
entrance = self.get_entrance(entrance_name)
|
|
except KeyError:
|
|
continue
|
|
if not isinstance(entrance.access_rule, Rule.Resolved):
|
|
continue
|
|
for item_name in entrance.access_rule.item_dependencies():
|
|
self.rule_item_dependencies[item_name] |= rule_ids
|
|
for region_name in entrance.access_rule.region_dependencies():
|
|
self.rule_region_dependencies[region_name] |= rule_ids
|
|
|
|
@override
|
|
def collect(self, state: CollectionState, item: Item) -> bool:
|
|
changed = super().collect(state, item)
|
|
if changed and self.rule_item_dependencies:
|
|
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
|
mapped_name = self.item_mapping.get(item.name, "")
|
|
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
|
for rule_id in rule_ids:
|
|
if player_results.get(rule_id, None) is False:
|
|
del player_results[rule_id]
|
|
|
|
return changed
|
|
|
|
@override
|
|
def remove(self, state: CollectionState, item: Item) -> bool:
|
|
changed = super().remove(state, item)
|
|
if not changed:
|
|
return changed
|
|
|
|
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
|
if self.rule_item_dependencies:
|
|
mapped_name = self.item_mapping.get(item.name, "")
|
|
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
|
for rule_id in rule_ids:
|
|
player_results.pop(rule_id, None)
|
|
|
|
# clear all region dependent caches as none can be trusted
|
|
if self.rule_region_dependencies:
|
|
for rule_ids in self.rule_region_dependencies.values():
|
|
for rule_id in rule_ids:
|
|
player_results.pop(rule_id, None)
|
|
|
|
# clear all location dependent caches as they may have lost region access
|
|
if self.rule_location_dependencies:
|
|
for rule_ids in self.rule_location_dependencies.values():
|
|
for rule_id in rule_ids:
|
|
player_results.pop(rule_id, None)
|
|
|
|
# clear all entrance dependent caches as they may have lost region access
|
|
if self.rule_entrance_dependencies:
|
|
for rule_ids in self.rule_entrance_dependencies.values():
|
|
for rule_id in rule_ids:
|
|
player_results.pop(rule_id, None)
|
|
|
|
return changed
|
|
|
|
@override
|
|
def reached_region(self, state: CollectionState, region: Region) -> None:
|
|
super().reached_region(state, region)
|
|
if self.rule_region_dependencies:
|
|
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
|
for rule_id in self.rule_region_dependencies[region.name]:
|
|
player_results.pop(rule_id, None)
|
|
|
|
|
|
class CachedRuleBuilderLogicMixin(LogicMixin):
|
|
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
|
|
rule_builder_cache: dict[int, dict[int, bool]] # pyright: ignore[reportUninitializedInstanceVariable]
|
|
|
|
def init_mixin(self, multiworld: "MultiWorld") -> None:
|
|
players = multiworld.get_all_ids()
|
|
self.rule_builder_cache = {player: {} for player in players}
|
|
|
|
def copy_mixin(self, new_state: "CachedRuleBuilderLogicMixin") -> "CachedRuleBuilderLogicMixin":
|
|
new_state.rule_builder_cache = {
|
|
player: player_results.copy() for player, player_results in self.rule_builder_cache.items()
|
|
}
|
|
return new_state
|