From dade03ed647b7b48302c70431d07c1026527f071 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Wed, 30 Jul 2025 02:18:10 -0600 Subject: [PATCH 1/5] create lambda condition prototype --- worlds/crosscode/logic.py | 5 +- worlds/crosscode/types/condition.py | 81 ++++++++++++++++------------- 2 files changed, 48 insertions(+), 38 deletions(-) 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 6145c1beec28..5fdac94c7dee 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 from BaseClasses import CollectionState from ..options import ShopReceiveMode @@ -22,7 +22,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 @@ -30,7 +30,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 @@ -38,32 +38,36 @@ 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 callback - return state.has(self.item_name, player, target) + return lambda state: state.has(self.item_name, player, target) @dataclass class QuestCondition(Condition): quest_name: str - 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(f"{self.quest_name} (Event)", player) @dataclass class LocationCondition(Condition): location_name: str - 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(f"{self.location_name} (Event)", 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. @@ -71,14 +75,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), @@ -89,8 +93,9 @@ 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): @@ -103,18 +108,24 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a 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): @@ -137,7 +148,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: @@ -146,25 +157,25 @@ 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].name, player) + return lambda state: state.has(args["shop_unlock_by_id"][self.item_id].name, player) if args["shop_receive_mode"] == ShopReceiveMode.option_per_shop: - return state.has(args["shop_unlock_by_shop"][self.shop_name].name, player) + return lambda state: state.has(args["shop_unlock_by_shop"][self.shop_name].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].name, player) - return True + return lambda state: state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].name, player) + return lambda _: True @dataclass class BotanicsCompletionCondition(Condition): @@ -180,8 +191,8 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a return collected >= self.amount 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", From 653a0086302df2aa255ef698abfaad94a23f8c62 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Thu, 14 May 2026 02:29:16 -0600 Subject: [PATCH 2/5] add support for new conditions --- worlds/crosscode/types/condition.py | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/worlds/crosscode/types/condition.py b/worlds/crosscode/types/condition.py index 5fdac94c7dee..31b11afb9a9c 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -101,8 +101,9 @@ def satisfied(self, player: int, location: int | None, args: LogicDict) -> typin 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): @@ -133,10 +134,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): @@ -181,14 +181,16 @@ def satisfied(self, player: int, location: int | None, args: LogicDict) -> typin class BotanicsCompletionCondition(Condition): amount: int - 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) - ]) - - return collected >= self.amount + 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 >= self.amount + return satisfied_internal class NeverCondition(Condition): def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: From 84cf980ef5e8a26f66f1f3ae959b42798c3989b8 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Thu, 14 May 2026 03:00:31 -0600 Subject: [PATCH 3/5] pre-cache some f-strings for speed --- worlds/crosscode/types/condition.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/worlds/crosscode/types/condition.py b/worlds/crosscode/types/condition.py index 31b11afb9a9c..5348f1f6bfc6 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -1,6 +1,6 @@ import typing import abc -from dataclasses import dataclass +from dataclasses import dataclass, field from BaseClasses import CollectionState from ..options import ShopReceiveMode @@ -51,16 +51,24 @@ def callback(state: CollectionState) -> bool: @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, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: - return lambda state: state.has(f"{self.quest_name} (Event)", player) + 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, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: - return lambda state: state.has(f"{self.location_name} (Event)", player) + return lambda state: state.has(self.event_name, player) @dataclass class RegionCondition(Condition): From 3cc3980122e5d0ceaf6b43b76f17aef2670e741c Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Thu, 18 Jun 2026 21:37:05 -0600 Subject: [PATCH 4/5] do not emit non-`init` fields --- worlds/crosscode/codegen/ast.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/crosscode/codegen/ast.py b/worlds/crosscode/codegen/ast.py index 4250f264fe70..ebee1269dd00 100644 --- a/worlds/crosscode/codegen/ast.py +++ b/worlds/crosscode/codegen/ast.py @@ -19,7 +19,11 @@ def create_expression_condition(condition: Condition) -> ast.Call: 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()], + keywords=[ + ast.keyword(arg=key, value=ast.Constant(value)) + for key, value in condition.__dict__.items() + if condition.__dataclass_fields__[key].init + ], ) ast.fix_missing_locations(result) From e9b7a0c6e9642089847d2e6b23a7a628e78fef17 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Thu, 18 Jun 2026 22:23:15 -0600 Subject: [PATCH 5/5] add special handling of and/or conditions --- worlds/crosscode/codegen/ast.py | 36 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/worlds/crosscode/codegen/ast.py b/worlds/crosscode/codegen/ast.py index ebee1269dd00..377ea815aed5 100644 --- a/worlds/crosscode/codegen/ast.py +++ b/worlds/crosscode/codegen/ast.py @@ -5,30 +5,50 @@ 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_condition(condition: Condition) -> ast.Call: - """ - Create an expression representing a singular condition. - """ +def create_expression_dataclass(cls) -> ast.Call: result = ast.Call( - func=ast.Name(condition.__class__.__name__), + func=ast.Name(cls.__class__.__name__), args=[], keywords=[ ast.keyword(arg=key, value=ast.Constant(value)) - for key, value in condition.__dict__.items() - if condition.__dataclass_fields__[key].init + 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. + """ + 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 + def create_expression_condition_list(conditions: typing.Optional[list[Condition]]) -> ast.expr: """ Create an expression representing a list of conditions.