mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
* 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>
92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
import dataclasses
|
|
import importlib
|
|
import operator
|
|
from collections.abc import Callable, Iterable
|
|
from typing import Any, Final, Literal, Self, cast
|
|
|
|
from typing_extensions import override
|
|
|
|
from Options import CommonOptions, Option
|
|
|
|
Operator = Literal["eq", "ne", "gt", "lt", "ge", "le", "contains", "in"]
|
|
|
|
OPERATORS: Final[dict[Operator, Callable[..., bool]]] = {
|
|
"eq": operator.eq,
|
|
"ne": operator.ne,
|
|
"gt": operator.gt,
|
|
"lt": operator.lt,
|
|
"ge": operator.ge,
|
|
"le": operator.le,
|
|
"contains": operator.contains,
|
|
"in": operator.contains,
|
|
}
|
|
OPERATOR_STRINGS: Final[dict[Operator, str]] = {
|
|
"eq": "==",
|
|
"ne": "!=",
|
|
"gt": ">",
|
|
"lt": "<",
|
|
"ge": ">=",
|
|
"le": "<=",
|
|
}
|
|
REVERSE_OPERATORS: Final[tuple[Operator, ...]] = ("in",)
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class OptionFilter:
|
|
option: type[Option[Any]]
|
|
value: Any
|
|
operator: Operator = "eq"
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Returns a JSON compatible dict representation of this option filter"""
|
|
return {
|
|
"option": f"{self.option.__module__}.{self.option.__name__}",
|
|
"value": self.value,
|
|
"operator": self.operator,
|
|
}
|
|
|
|
def check(self, options: CommonOptions) -> bool:
|
|
"""Tests the given options dataclass to see if it passes this option filter"""
|
|
option_name = next(
|
|
(name for name, cls in options.__class__.type_hints.items() if cls is self.option),
|
|
None,
|
|
)
|
|
if option_name is None:
|
|
raise ValueError(f"Cannot find option {self.option.__name__} in options class {options.__class__.__name__}")
|
|
opt = cast(Option[Any] | None, getattr(options, option_name, None))
|
|
if opt is None:
|
|
raise ValueError(f"Invalid option: {option_name}")
|
|
|
|
fn = OPERATORS[self.operator]
|
|
return fn(self.value, opt) if self.operator in REVERSE_OPERATORS else fn(opt, self.value)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
"""Returns a new OptionFilter instance from a dict representation"""
|
|
if "option" not in data or "value" not in data:
|
|
raise ValueError("Missing required value and/or option")
|
|
|
|
option_path = data["option"]
|
|
try:
|
|
option_mod_name, option_cls_name = option_path.rsplit(".", 1)
|
|
option_module = importlib.import_module(option_mod_name)
|
|
option = getattr(option_module, option_cls_name, None)
|
|
except (ValueError, ImportError) as e:
|
|
raise ValueError(f"Cannot parse option '{option_path}'") from e
|
|
if option is None or not issubclass(option, Option):
|
|
raise ValueError(f"Invalid option '{option_path}' returns type '{option}' instead of Option subclass")
|
|
|
|
value = data["value"]
|
|
operator = data.get("operator", "eq")
|
|
return cls(option=cast(type[Option[Any]], option), value=value, operator=operator)
|
|
|
|
@classmethod
|
|
def multiple_from_dict(cls, data: Iterable[dict[str, Any]]) -> tuple[Self, ...]:
|
|
"""Returns a tuple of OptionFilters instances from an iterable of dict representations"""
|
|
return tuple(cls.from_dict(o) for o in data)
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
op = OPERATOR_STRINGS.get(self.operator, self.operator)
|
|
return f"{self.option.__name__} {op} {self.value}"
|