diff --git a/worlds/crosscode/codegen/ast.py b/worlds/crosscode/codegen/ast.py index 4250f264fe70..377ea815aed5 100644 --- a/worlds/crosscode/codegen/ast.py +++ b/worlds/crosscode/codegen/ast.py @@ -5,22 +5,46 @@ import typing import ast -from ..types.condition import Condition +from ..types.condition import AndCondition, Condition, OrCondition, QuestCondition from ..types.locations import AccessInfo, LocationData from ..types.regions import Goal, RegionConnection from ..types.items import ItemData, ItemPoolEntry, ProgressiveChainEntry, SingleItemData from ..types.shops import ShopData +def create_expression_dataclass(cls) -> ast.Call: + result = ast.Call( + func=ast.Name(cls.__class__.__name__), + args=[], + keywords=[ + ast.keyword(arg=key, value=ast.Constant(value)) + for key, value in cls.__dict__.items() + if cls.__dataclass_fields__[key].init + ], + ) + ast.fix_missing_locations(result) + + return result + def create_expression_condition(condition: Condition) -> ast.Call: """ Create an expression representing a singular condition. """ - result = ast.Call( - func=ast.Name(condition.__class__.__name__), - args=[], - keywords=[ast.keyword(arg=key, value=ast.Constant(value)) for key, value in condition.__dict__.items()], - ) + if isinstance(condition, OrCondition) or isinstance(condition, AndCondition): + # we handle these conditions in a special way, since their lists cannot be encoded by ast.Constant + result = ast.Call( + func=ast.Name(condition.__class__.__name__), + args=[], + keywords=[ + ast.keyword( + arg="subconditions", + value=create_expression_condition_list(condition.subconditions) + ) + ], + ) + else: + # this block should handle most cases, unless you make a condition that has a complex type + result = create_expression_dataclass(condition) ast.fix_missing_locations(result) return result diff --git a/worlds/crosscode/logic.py b/worlds/crosscode/logic.py index 05811468b6f1..cf3a1292a926 100644 --- a/worlds/crosscode/logic.py +++ b/worlds/crosscode/logic.py @@ -15,7 +15,6 @@ def condition_satisfied( """ Factory function. Return value is a rule that checks whether all the conditions are satisfied. """ - def conditions_satisfied_internal(state: CollectionState) -> bool: - return all(c.satisfied(state, player, location, cond_args) for c in conditions) + callbacks = [c.satisfied(player, location, cond_args) for c in conditions] - return conditions_satisfied_internal + return lambda state: all(map(lambda x: x(state), callbacks)) diff --git a/worlds/crosscode/types/condition.py b/worlds/crosscode/types/condition.py index dc383b4396fa..7d872ae84bfb 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -1,6 +1,6 @@ import typing import abc -from dataclasses import field, dataclass +from dataclasses import dataclass, field from BaseClasses import CollectionState from ..options import ShopReceiveMode @@ -23,7 +23,7 @@ class LogicDict(typing.TypedDict): class Condition(abc.ABC): @abc.abstractmethod - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: pass @dataclass @@ -31,7 +31,7 @@ class ItemCondition(Condition): item_name: str amount: int = 1 - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: target = self.amount if self.item_name in args["keyrings"]: target = 1 @@ -39,32 +39,44 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a replacements = args["item_progressive_replacements"] if self.item_name in replacements: - for prog_item_name, quantity in replacements[self.item_name]: - if state.has(prog_item_name, player, quantity): - return True + def callback(state: CollectionState) -> bool: + for prog_item_name, quantity in replacements[self.item_name]: + if state.has(prog_item_name, player, quantity): + return True + return state.has(self.item_name, player, target) - return state.has(self.item_name, player, target) + return callback + + return lambda state: state.has(self.item_name, player, target) @dataclass class QuestCondition(Condition): quest_name: str + event_name: str = field(init=False) + + def __post_init__(self): + self.event_name = f"{self.quest_name} (Event)" - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - return state.has(f"{self.quest_name} (Event)", player) + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + return lambda state: state.has(self.event_name, player) @dataclass class LocationCondition(Condition): location_name: str + event_name: str = field(init=False) + + def __post_init__(self): + self.event_name = f"{self.location_name} (Event)" - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - return state.has(f"{self.location_name} (Event)", player) + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + return lambda state: state.has(self.event_name, player) @dataclass class RegionCondition(Condition): target_mode: typing.Optional[str] region_name: str - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: # target_mode == None means that it matches in all modes. # so if target_mode == None, check if we can reach that region. # else, if the target mode matches, also check if we can reach that region. @@ -72,14 +84,14 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a mode: str = args["mode"] if self.target_mode is None or mode == self.target_mode: - return state.can_reach_region(self.region_name, player) + return lambda state: state.can_reach_region(self.region_name, player) - return True + return lambda _: True @dataclass class AnyElementCondition(Condition): - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - return any([ + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + return lambda state: any([ state.has("Heat", player), state.has("Cold", player), state.has("Shock", player), @@ -90,32 +102,40 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a class OrCondition(Condition): subconditions: list[Condition] - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - return any(map(lambda x: x.satisfied(state, player, location, args), self.subconditions)) + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + callbacks = [x.satisfied(player, location, args) for x in self.subconditions] + return lambda state: any(map(lambda x: x(state), callbacks)) @dataclass class AndCondition(Condition): subconditions: list[Condition] - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - return all(map(lambda x: x.satisfied(state, player, location, args), self.subconditions)) + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + callbacks = [x.satisfied(player, location, args) for x in self.subconditions] + return lambda state: all(map(lambda x: x(state), callbacks)) @dataclass class VariableCondition(Condition): name: str - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: variables = args["variables"] variable_definitions = args["variable_definitions"] if self.name not in variables: - return True + return lambda _: True - for value in variables[self.name]: - if not all(map(lambda c: c.satisfied(state, player, location, args), variable_definitions[self.name][value])): - return False + callbacks = sum( + [ + [ + x.satisfied(player, location, args) + for x in variable_definitions[self.name][value] + ] for value in variables[self.name] + ], + start=[] + ) - return True + return lambda state: all(map(lambda x: x(state), callbacks)) @dataclass class VariableEntryCondition(Condition): @@ -123,10 +143,9 @@ class VariableEntryCondition(Condition): value: str desired: bool - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - variables = args["variables"] - - return (self.value in variables[self.name]) == self.desired + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + valid = (self.value in args["variables"][self.name]) == self.desired + return lambda _: valid @dataclass class ChestKeyCondition(Condition): @@ -138,7 +157,7 @@ class ChestKeyCondition(Condition): "Gold": "Radiant Key", } - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: chest_levels = args["chest_clearance_levels"] if location is None: @@ -147,42 +166,44 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a level: str = chest_levels.get(location, self.default_level) if level == "Default": - return True + return lambda _: True - return state.has(ChestKeyCondition.clearance_items[level], player) + return lambda state: state.has(ChestKeyCondition.clearance_items[level], player) @dataclass class ShopSlotCondition(Condition): shop_name: str item_id: int - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: if args["shop_receive_mode"] is None: - return True + return lambda _: True if args["shop_receive_mode"] == ShopReceiveMode.option_per_item_type: - return state.has(args["shop_unlock_by_id"][self.item_id].item.name, player) + return lambda state: state.has(args["shop_unlock_by_id"][self.item_id].item.name, player) if args["shop_receive_mode"] == ShopReceiveMode.option_per_shop: - return state.has(args["shop_unlock_by_shop"][self.shop_name].item.name, player) + return lambda state: state.has(args["shop_unlock_by_shop"][self.shop_name].item.name, player) if args["shop_receive_mode"] == ShopReceiveMode.option_per_slot: - return state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].item.name, player) - return True + return lambda state: state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].item.name, player) + return lambda _: True @dataclass class BotanicsCompletionCondition(Condition): amount: float - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - collected = sum([ - amount - for region, amount in args["region_botanics_amounts"].items() - if state.can_reach_region(region, player) - ]) + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + def satisfied_internal(state: CollectionState): + collected = sum([ + amount + for region, amount in args["region_botanics_amounts"].items() + if state.can_reach_region(region, player) + ]) - return collected / args["botanics_completion_amount"] >= self.amount + return collected / args["botanics_completion_amount"] >= self.amount + return satisfied_internal class NeverCondition(Condition): - def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - return False + def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + return lambda _: False __all__ = [ "Condition",