From 9864382c9e4b2015c05214ebc177b410edc24bce Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 6 Oct 2025 09:58:45 -0400 Subject: trying something --- apworld/rules.py | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) (limited to 'apworld/rules.py') diff --git a/apworld/rules.py b/apworld/rules.py index f859e75..a84cfbb 100644 --- a/apworld/rules.py +++ b/apworld/rules.py @@ -1,13 +1,199 @@ from collections.abc import Callable +from enum import IntFlag, auto from typing import TYPE_CHECKING -from BaseClasses import CollectionState, Region +from BaseClasses import CollectionState, Region, MultiWorld, Entrance from .player_logic import AccessRequirements +from ..AutoWorld import LogicMixin if TYPE_CHECKING: from . import Lingo2World +class ControlCenterColor(IntFlag): + RED = auto() + BLUE = auto() + ORANGE = auto() + MAGENTA = auto() + PURPLE = auto() + GREEN = auto() + BROWN = auto() + WHITE = auto() + + NONE = 0 + ALL = RED + BLUE + ORANGE + MAGENTA + PURPLE + GREEN + BROWN + WHITE + + +CONTROL_CENTER_COLOR_NAMES: dict[str, ControlCenterColor] = { + "red": ControlCenterColor.RED, + "blue": ControlCenterColor.BLUE, + "orange": ControlCenterColor.ORANGE, + "magenta": ControlCenterColor.MAGENTA, + "purple": ControlCenterColor.PURPLE, + "green": ControlCenterColor.GREEN, + "brown": ControlCenterColor.BROWN, + "white": ControlCenterColor.WHITE, +} + + +class Lingo2Entrance(Entrance): + world: "Lingo2World" + reqs: AccessRequirements | None + required_regions: list[Region] + control_center_colors: ControlCenterColor + + def set_lingo2_rule(self, world: "Lingo2World", regions: dict[str, Region] | None, reqs: AccessRequirements, + control_center_colors: ControlCenterColor): + self.world = world + + if reqs.is_empty(): + self.reqs = None + else: + self.reqs = reqs.copy() + self.reqs.rooms.clear() + + if regions is None: + self.required_regions = [world.multiworld.get_region(room_name, world.player) + for room_name in reqs.rooms] + else: + self.required_regions = [regions[room_name] for room_name in reqs.rooms] + + self.control_center_colors = control_center_colors + + self.access_rule = self._lingo2_rule + + def _lingo2_rule(self, state: CollectionState) -> bool: + if self.reqs is not None and not lingo2_can_satisfy_requirements(state, self.reqs, self.required_regions, + self.world): + return False + + if self.control_center_colors != ControlCenterColor.ALL\ + and not self.world.options.shuffle_control_center_colors: + from_region_state = state.lingo2_get_region_state(self.parent_region) + + if from_region_state.control_center_colors & self.control_center_colors == ControlCenterColor.NONE: + return False + + state.lingo2_use_entrance(self) + return True + + +class Lingo2Region(Region): + def can_reach(self, state) -> bool: + if self in state.reachable_regions[self.player]: + return True + + if not state.stale[self.player] and not state.lingo2_state[self.player].stale: + # if the cache is updated we can use the cache + return super().can_reach(state) + + if state.lingo2_state[self.player].stale: + state.lingo2_sweep(self.player) + + return super().can_reach(state) + + +class PerPlayerRegionState: + control_center_colors: ControlCenterColor + + def __init__(self): + self.control_center_colors = ControlCenterColor.NONE + + def copy(self) -> "PerPlayerRegionState": + result = PerPlayerRegionState() + result.control_center_colors = self.control_center_colors + return result + + +class PerPlayerState: + world: "Lingo2World" + regions: dict[str, PerPlayerRegionState] + stale: bool + sweeping: bool + sweepable_entrances: set[Entrance] + check_colors: bool + + def __init__(self, world: "Lingo2World"): + self.world = world + self.regions = {} + self.stale = True + self.sweeping = False + self.sweepable_entrances = set() + self.check_colors = not world.options.shuffle_control_center_colors + + self.regions["Menu"] = PerPlayerRegionState() + self.regions["Menu"].control_center_colors = ControlCenterColor.ALL + + def copy(self) -> "PerPlayerState": + result = PerPlayerState(self.world) + result.regions = {region_name: region_state.copy() for region_name, region_state in self.regions.items()} + result.stale = self.stale + result.sweeping = self.sweeping + result.sweepable_entrances = self.sweepable_entrances.copy() + result.check_colors = self.check_colors + return result + + def get_region_state(self, region: Region): + return self.regions.setdefault(region.name, PerPlayerRegionState()) + + +class Lingo2LogicMixin(LogicMixin): + multiworld: MultiWorld + reachable_regions: dict[int, set[Region]] + + lingo2_state: dict[int, PerPlayerState] + + def init_mixin(self, multiworld: MultiWorld): + self.lingo2_state = {player: PerPlayerState(multiworld.worlds[player]) + for player in multiworld.get_game_players("Lingo 2")} + + def copy_mixin(self, new_state: CollectionState) -> CollectionState: + new_state.lingo2_state = {player: pps.copy() for player, pps in self.lingo2_state.items()} + + return new_state + + def lingo2_get_region_state(self, region: Region): + return self.lingo2_state[region.player].get_region_state(region) + + def lingo2_use_entrance(self, entrance: Lingo2Entrance): + player_state = self.lingo2_state[entrance.player] + + from_region_state = player_state.get_region_state(entrance.parent_region) + to_region_state = player_state.get_region_state(entrance.connected_region) + + should_update = False + + if player_state.check_colors: + avail_colors = from_region_state.control_center_colors & entrance.control_center_colors + if avail_colors & ~to_region_state.control_center_colors != ControlCenterColor.NONE: + if to_region_state.control_center_colors != ControlCenterColor.NONE or avail_colors != ControlCenterColor.ALL: + #print(f"entrance {entrance.name} takes region from {to_region_state.control_center_colors} to {avail_colors}") + pass + + should_update = True + to_region_state.control_center_colors = to_region_state.control_center_colors | avail_colors + + if should_update: + player_state.stale = True + + for adjacent in entrance.parent_region.exits: + player_state.sweepable_entrances.add(adjacent) + + def lingo2_sweep(self, player: int): + player_state = self.lingo2_state[player] + if player_state.sweeping: + return + player_state.sweeping = True + + while player_state.sweepable_entrances: + next_entrance = player_state.sweepable_entrances.pop() + if next_entrance.parent_region in self.reachable_regions[player]: + next_entrance.can_reach(self) + + player_state.stale = False + player_state.sweeping = False + + def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region], world: "Lingo2World") -> bool: if not all(state.has(item, world.player) for item in reqs.items): @@ -53,6 +239,7 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem return True + def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World", regions: dict[str, Region] | None) -> Callable[[CollectionState], bool]: # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule -- cgit 1.4.1