From dade03ed647b7b48302c70431d07c1026527f071 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Wed, 30 Jul 2025 02:18:10 -0600 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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. From de411e81f6a9b5bc7cb260442ea60cc385677736 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Sat, 22 Aug 2026 22:51:01 -0600 Subject: [PATCH 6/6] remove logicdict; replace with world reference --- worlds/crosscode/logic.py | 10 ++-- worlds/crosscode/types/condition.py | 82 +++++++++++++---------------- worlds/crosscode/world.py | 44 ++++++---------- 3 files changed, 60 insertions(+), 76 deletions(-) diff --git a/worlds/crosscode/logic.py b/worlds/crosscode/logic.py index cf3a1292a926..de7ce9e252c6 100644 --- a/worlds/crosscode/logic.py +++ b/worlds/crosscode/logic.py @@ -2,19 +2,23 @@ This module contains various logic functions """ +from __future__ import annotations import typing from BaseClasses import CollectionState -from .types.condition import Condition, LogicDict +from .types.condition import Condition + +if typing.TYPE_CHECKING: + from .world import CrossCodeWorld def condition_satisfied( player: int, conditions: list[Condition], location: int | None, - cond_args: LogicDict + world: CrossCodeWorld ) -> typing.Callable[[CollectionState], bool]: """ Factory function. Return value is a rule that checks whether all the conditions are satisfied. """ - callbacks = [c.satisfied(player, location, cond_args) for c in conditions] + callbacks = [c.satisfied(player, location, world) for c in conditions] 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 7d872ae84bfb..cd7cbc338f1d 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -1,3 +1,4 @@ +from __future__ import annotations import typing import abc from dataclasses import dataclass, field @@ -7,23 +8,12 @@ from .items import ItemPoolEntry -class LogicDict(typing.TypedDict): - mode: str - variables: dict[str, list[str]] - variable_definitions: dict[str, dict[str, list["Condition"]]] - keyrings: set[str] - item_progressive_replacements: dict[str, list[tuple[str, int]]] - chest_clearance_levels: dict[int, str] - shop_receive_mode: int | None - shop_unlock_by_id: dict[int, ItemPoolEntry] - shop_unlock_by_shop: dict[str, ItemPoolEntry] - shop_unlock_by_shop_and_id: dict[tuple[str, int], ItemPoolEntry] - region_botanics_amounts: dict[str, int] - botanics_completion_amount: int +if typing.TYPE_CHECKING: + from ..world import CrossCodeWorld class Condition(abc.ABC): @abc.abstractmethod - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: pass @dataclass @@ -31,12 +21,12 @@ class ItemCondition(Condition): item_name: str amount: int = 1 - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: target = self.amount - if self.item_name in args["keyrings"]: + if self.item_name in world.keyrings: target = 1 - replacements = args["item_progressive_replacements"] + replacements = world.pools.item_progressive_replacements if self.item_name in replacements: def callback(state: CollectionState) -> bool: @@ -57,7 +47,7 @@ class QuestCondition(Condition): 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]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: return lambda state: state.has(self.event_name, player) @dataclass @@ -68,7 +58,7 @@ class LocationCondition(Condition): 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]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: return lambda state: state.has(self.event_name, player) @dataclass @@ -76,13 +66,13 @@ class RegionCondition(Condition): target_mode: typing.Optional[str] region_name: str - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> 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. # otherwise, if the target mode does not match, return true (assume that it's being ANDed with other conditions) - mode: str = args["mode"] + mode: str = world.logic_mode if self.target_mode is None or mode == self.target_mode: return lambda state: state.can_reach_region(self.region_name, player) @@ -90,7 +80,7 @@ def satisfied(self, player: int, location: int | None, args: LogicDict) -> typin @dataclass class AnyElementCondition(Condition): - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: return lambda state: any([ state.has("Heat", player), state.has("Cold", player), @@ -102,25 +92,25 @@ def satisfied(self, player: int, location: int | None, args: LogicDict) -> typin class OrCondition(Condition): subconditions: list[Condition] - 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] + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: + callbacks = [x.satisfied(player, location, world) 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, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: - callbacks = [x.satisfied(player, location, args) for x in self.subconditions] + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: + callbacks = [x.satisfied(player, location, world) for x in self.subconditions] return lambda state: all(map(lambda x: x(state), callbacks)) @dataclass class VariableCondition(Condition): name: str - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: - variables = args["variables"] - variable_definitions = args["variable_definitions"] + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: + variables = world.variables + variable_definitions = world.world_data.variable_definitions if self.name not in variables: return lambda _: True @@ -128,7 +118,7 @@ def satisfied(self, player: int, location: int | None, args: LogicDict) -> typin callbacks = sum( [ [ - x.satisfied(player, location, args) + x.satisfied(player, location, world) for x in variable_definitions[self.name][value] ] for value in variables[self.name] ], @@ -143,8 +133,8 @@ class VariableEntryCondition(Condition): value: str desired: bool - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: - valid = (self.value in args["variables"][self.name]) == self.desired + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: + valid = (self.value in world.variables[self.name]) == self.desired return lambda _: valid @dataclass @@ -157,8 +147,8 @@ class ChestKeyCondition(Condition): "Gold": "Radiant Key", } - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: - chest_levels = args["chest_clearance_levels"] + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: + chest_levels = world.chest_clearance_levels if location is None: raise RuntimeError("An event cannot have a chest key condition") @@ -175,34 +165,34 @@ class ShopSlotCondition(Condition): shop_name: str item_id: int - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: - if args["shop_receive_mode"] is None: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: + if world.shop_receive_mode is None: return lambda _: True - if args["shop_receive_mode"] == ShopReceiveMode.option_per_item_type: - 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 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 lambda state: state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].item.name, player) + if world.shop_receive_mode == ShopReceiveMode.option_per_item_type: + return lambda state: state.has(world.world_data.shop_unlock_by_id[self.item_id].item.name, player) + if world.shop_receive_mode == ShopReceiveMode.option_per_shop: + return lambda state: state.has(world.world_data.shop_unlock_by_shop[self.shop_name].item.name, player) + if world.shop_receive_mode == ShopReceiveMode.option_per_slot: + return lambda state: state.has(world.world_data.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, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: def satisfied_internal(state: CollectionState): collected = sum([ amount - for region, amount in args["region_botanics_amounts"].items() + for region, amount in world.world_data.region_botanics_amounts[world.logic_mode].items() if state.can_reach_region(region, player) ]) - return collected / args["botanics_completion_amount"] >= self.amount + return collected / world.options.botanics_completion_amount >= self.amount return satisfied_internal class NeverCondition(Condition): - def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]: + def satisfied(self, player: int, location: int | None, world: CrossCodeWorld) -> typing.Callable[[CollectionState], bool]: return lambda _: False __all__ = [ diff --git a/worlds/crosscode/world.py b/worlds/crosscode/world.py index 8b099984c515..0484be060276 100644 --- a/worlds/crosscode/world.py +++ b/worlds/crosscode/world.py @@ -1,5 +1,4 @@ """ - This module contains the world class for CrossCode. """ @@ -21,7 +20,7 @@ from .types.items import ItemData, CrossCodeItem from .types.locations import CrossCodeLocation, LocationData -from .types.condition import LogicDict, Condition, LocationCondition +from .types.condition import Condition, LocationCondition from .types.world import WorldData from .types.regions import RegionsData from .types.metadata import IncludeOptions @@ -97,6 +96,10 @@ class CrossCodeWorld(World): rhombus_hub_unlock: bool + keyrings: set[str] + shop_receive_mode: int | None + chest_clearance_levels: dict[int, str] + pre_fill_specific_dungeons_names: dict[str, set[str]] pre_fill_any_dungeon_names: set[str] @@ -106,8 +109,6 @@ class CrossCodeWorld(World): dungeon_location_list: dict[str, set[CrossCodeLocation]] dungeon_areas: typing.ClassVar[set[str]] = {"cold-dng", "heat-dng", "shock-dng", "wave-dng", "tree-dng", "final-dng"} - logic_dict: LogicDict - location_events: dict[str, Location] variables: dict[str, list[str]] @@ -421,27 +422,16 @@ def generate_early(self): self.pre_fill_any_dungeon_names ) - self.logic_dict: LogicDict = { - "mode": self.logic_mode, - "variables": self.variables, - "variable_definitions": self.world_data.variable_definitions, - "keyrings": self.world_data.keyring_items if self.options.keyrings.value else set(), - "item_progressive_replacements": self.pools.item_progressive_replacements, - "chest_clearance_levels": {}, - "shop_receive_mode": self.options.shop_receive_mode.value if self.options.shop_rando.value else None, - "shop_unlock_by_id": self.world_data.shop_unlock_by_id, - "shop_unlock_by_shop": self.world_data.shop_unlock_by_shop, - "shop_unlock_by_shop_and_id": self.world_data.shop_unlock_by_shop_and_id, - "region_botanics_amounts": self.world_data.region_botanics_amounts[self.logic_mode], - "botanics_completion_amount": self.options.botanics_completion_amount.value, - } + self.keyrings = self.world_data.keyring_items if self.options.keyrings.value else set() + self.shop_receive_mode = self.options.shop_receive_mode.value if self.options.shop_rando.value else None + self.chest_clearance_levels = {} # Universal Tracker support # Anything that is generated in a non-standard fashion on my end has to be brought back into scope here from slot data. if hasattr(self.multiworld, "re_gen_passthrough"): slot_data: SlotData = self.multiworld.re_gen_passthrough["CrossCode"] # Reinterpret the JSON chest clearance levels dict (a string -> string mapping) as an int -> int mapping instead. - self.logic_dict["chest_clearance_levels"] = {int(combo_id): clearance for combo_id, clearance in slot_data["options"]["chestClearanceLevels"].items()} + self.chest_clearance_levels = {int(combo_id): clearance for combo_id, clearance in slot_data["options"]["chestClearanceLevels"].items()} @classmethod def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World: @@ -478,7 +468,7 @@ def add_location(self, data: LocationData, region: Region): cum_weights=self._chest_lock_weights )[0] - self.logic_dict["chest_clearance_levels"][data.code] = clearance + self.chest_clearance_levels[data.code] = clearance def create_shops(self): # don't filter the shop pool - the regions must always exist to prevent generation errors @@ -490,7 +480,7 @@ def create_shops(self): self.region_dict[from_region].connect( region, f"{from_region} => {shop_name}", - condition_satisfied(self.player, shop.access.cond, None, self.logic_dict) if shop.access.cond else None + condition_satisfied(self.player, shop.access.cond, None, self) if shop.access.cond else None ) if self.options.shop_send_mode == ShopSendMode.option_per_slot: @@ -514,7 +504,7 @@ def create_regions(self): self.region_dict[conn.region_from].connect( self.region_dict[conn.region_to], f"{conn.region_from} => {conn.region_to}", - condition_satisfied(self.player, conn.cond, None, self.logic_dict) if conn.cond is not None else None + condition_satisfied(self.player, conn.cond, None, self) if conn.cond is not None else None ) self.create_event_conditions(conn.cond) @@ -562,7 +552,7 @@ def create_regions(self): self.player, goal.condition if goal.condition is not None else [], None, - self.logic_dict + self ) ) self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) @@ -653,7 +643,7 @@ def set_rules(self): if not isinstance(loc, CrossCodeLocation): continue if loc.data.access.cond is not None: - add_rule(loc, condition_satisfied(self.player, loc.data.access.cond, loc.data.code, self.logic_dict)) + add_rule(loc, condition_satisfied(self.player, loc.data.access.cond, loc.data.code, self)) def get_pre_fill_items(self) -> list[Item]: pre_fill_items = self.pre_fill_any_dungeon.copy() @@ -758,7 +748,7 @@ def priority(item: CrossCodeItem) -> int: def extend_hint_information(self, hint_data: dict[int, dict[int, str]]): ours = hint_data.setdefault(self.player, {}) - for loc, clearance in self.logic_dict["chest_clearance_levels"].items(): + for loc, clearance in self.chest_clearance_levels.items(): ours[loc] = clearance def fill_slot_data(self) -> SlotData: @@ -793,7 +783,7 @@ def fill_slot_data(self) -> SlotData: "meteorPassage": bool(self.options.vw_meteor_passage.value), "closedGaia": self.options.closed_gaia.value, "vtSkip": bool(self.options.vt_skip.value), - "keyrings": [self.world_data.single_items_dict[name].item_id for name in self.logic_dict["keyrings"]], + "keyrings": [self.world_data.single_items_dict[name].item_id for name in self.keyrings], "chestReveal": bool(self.options.chest_reveal.value), "allowBoosterGrinding": bool(self.options.allow_booster_grinding), "questRando": bool(self.options.quest_rando.value), @@ -804,7 +794,7 @@ def fill_slot_data(self) -> SlotData: "shopSendMode": shop_send_mode_string, "shopReceiveMode": shop_receive_mode_string, "shopDialogHints": bool(self.options.shop_dialog_hints.value), - "chestClearanceLevels": self.logic_dict["chest_clearance_levels"], + "chestClearanceLevels": self.chest_clearance_levels, "botanicsCompletionAmount": self.options.botanics_completion_amount.value, } }