from collections.abc import Callable from enum import IntFlag, auto from typing import TYPE_CHECKING 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): return False if not all(state.has(item, world.player, amount) for item, amount in reqs.progressives.items()): return False if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): return False if not all(state.can_reach(region) for region in regions): return False for letter_key, letter_level in reqs.letters.items(): if not state.has(letter_key, world.player, letter_level): return False if reqs.cyans: if not any(state.has(letter, world.player, amount) for letter, amount in world.player_logic.double_letter_amount.items()): return False if len(reqs.or_logic) > 0: if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, [], world) for sub_reqs in subjunction) for subjunction in reqs.or_logic): return False if reqs.complete_at is not None: completed = 0 checked = 0 for possibility in reqs.possibilities: checked += 1 if lingo2_can_satisfy_requirements(state, possibility, [], world): completed += 1 if completed >= reqs.complete_at: break elif len(reqs.possibilities) - checked + completed < reqs.complete_at: # There aren't enough remaining possibilities for the check to pass. return False if completed < reqs.complete_at: return False 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 # checking. if regions is not None: required_regions = [regions[room_name] for room_name in reqs.rooms] else: required_regions = [world.multiworld.get_region(room_name, world.player) for room_name in reqs.rooms] new_reqs = reqs.copy() new_reqs.rooms.clear() return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world)