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/regions.py | 75 +++++++++++++++------ apworld/rules.py | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 22 deletions(-) diff --git a/apworld/regions.py b/apworld/regions.py index 1215f5a..b5ec9c6 100644 --- a/apworld/regions.py +++ b/apworld/regions.py @@ -6,14 +6,14 @@ from entrance_rando import randomize_entrances from .items import Lingo2Item from .locations import Lingo2Location from .player_logic import AccessRequirements -from .rules import make_location_lambda +from .rules import make_location_lambda, Lingo2Entrance, ControlCenterColor, CONTROL_CENTER_COLOR_NAMES, Lingo2Region if TYPE_CHECKING: from . import Lingo2World def create_region(room, world: "Lingo2World") -> Region: - return Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld) + return Lingo2Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld) def create_locations(room, new_region: Region, world: "Lingo2World", regions: dict[str, Region]): @@ -53,7 +53,7 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di def create_regions(world: "Lingo2World"): regions = { - "Menu": Region("Menu", world.player, world.multiworld) + "Menu": Lingo2Region("Menu", world.player, world.multiworld) } region_and_room = [] @@ -69,7 +69,10 @@ def create_regions(world: "Lingo2World"): for (region, room) in region_and_room: create_locations(room, region, world, regions) - regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") + start_game = Lingo2Entrance(world.player, "Start Game", regions["Menu"]) + start_game.set_lingo2_rule(world, regions, AccessRequirements(), ControlCenterColor.ALL) + regions["Menu"].exits.append(start_game) + start_game.connect(regions["The Entry - Starting Room"]) for connection in world.static_logic.objects.connections: if connection.roof_access and not world.options.daedalus_roof_access: @@ -87,6 +90,7 @@ def create_regions(world: "Lingo2World"): connection_name = f"{from_region} -> {to_region}" reqs = AccessRequirements() + control_center_colors = ControlCenterColor.ALL if connection.HasField("required_door"): reqs.merge(world.player_logic.get_door_open_reqs(connection.required_door)) @@ -95,6 +99,10 @@ def create_regions(world: "Lingo2World"): wmap = world.static_logic.objects.maps[door.map_id] connection_name = f"{connection_name} (using {wmap.name} - {door.name})" + if door.HasField("control_center_color"): + control_center_colors = control_center_colors & CONTROL_CENTER_COLOR_NAMES.get( + door.control_center_color, ControlCenterColor.NONE) + if connection.HasField("port"): port = world.static_logic.objects.ports[connection.port] connection_name = f"{connection_name} (via {port.display_name})" @@ -105,6 +113,11 @@ def create_regions(world: "Lingo2World"): if port.HasField("required_door"): reqs.merge(world.player_logic.get_door_open_reqs(port.required_door)) + req_door = world.static_logic.objects.doors[port.required_door] + if req_door.HasField("control_center_color"): + control_center_colors = control_center_colors & CONTROL_CENTER_COLOR_NAMES.get( + req_door.control_center_color, ControlCenterColor.NONE) + if connection.HasField("painting"): painting = world.static_logic.objects.paintings[connection.painting] connection_name = f"{connection_name} (via painting {painting.name})" @@ -112,6 +125,11 @@ def create_regions(world: "Lingo2World"): if painting.HasField("required_door"): reqs.merge(world.player_logic.get_door_open_reqs(painting.required_door)) + req_door = world.static_logic.objects.doors[painting.required_door] + if req_door.HasField("control_center_color"): + control_center_colors = control_center_colors & CONTROL_CENTER_COLOR_NAMES.get( + req_door.control_center_color, ControlCenterColor.NONE) + if connection.HasField("panel"): proxy = connection.panel reqs.merge(world.player_logic.get_panel_reqs(proxy.panel, @@ -132,14 +150,14 @@ def create_regions(world: "Lingo2World"): reqs.simplify() reqs.remove_room(from_region) - connection = Entrance(world.player, connection_name, regions[from_region]) - connection.access_rule = make_location_lambda(reqs, world, regions) + entrance = Lingo2Entrance(world.player, connection_name, regions[from_region]) + entrance.set_lingo2_rule(world, regions, reqs, control_center_colors) - regions[from_region].exits.append(connection) - connection.connect(regions[to_region]) + regions[from_region].exits.append(entrance) + entrance.connect(regions[to_region]) for region in reqs.get_referenced_rooms(): - world.multiworld.register_indirect_condition(regions[region], connection) + world.multiworld.register_indirect_condition(regions[region], entrance) world.multiworld.regions += regions.values() @@ -160,19 +178,29 @@ def shuffle_entrances(world: "Lingo2World"): connection_name = f"{port_region_name} - {port.display_name}" port_id_by_name[connection_name] = port.id - entrance = port_region.create_er_target(connection_name) + entrance = Lingo2Entrance(world.player, connection_name) + entrance.connect(port_region) entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY - er_exit = port_region.create_exit(connection_name) + er_exit = Lingo2Entrance(world.player, connection_name, port_region) + port_region.exits.append(er_exit) er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY + door_reqs = AccessRequirements() + control_center_colors = ControlCenterColor.ALL if port.HasField("required_door"): door_reqs = world.player_logic.get_door_open_reqs(port.required_door) - er_exit.access_rule = make_location_lambda(door_reqs, world, None) - for region in door_reqs.get_referenced_rooms(): - world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), - er_exit) + req_door = world.static_logic.objects.doors[port.required_door] + if req_door.HasField("control_center_color"): + control_center_colors = CONTROL_CENTER_COLOR_NAMES.get(req_door.control_center_color, + ControlCenterColor.NONE) + + er_exit.set_lingo2_rule(world, None, door_reqs, control_center_colors) + + for region in door_reqs.get_referenced_rooms(): + world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), + er_exit) er_entrances.append(entrance) er_exits.append(er_exit) @@ -195,21 +223,26 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"): from_region = world.multiworld.get_region(from_region_name, world.player) to_region = world.multiworld.get_region(to_region_name, world.player) - connection = Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region) + connection = Lingo2Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region) reqs = AccessRequirements() + control_center_colors = ControlCenterColor.ALL if from_port.HasField("required_door"): reqs = world.player_logic.get_door_open_reqs(from_port.required_door).copy() + req_door = world.static_logic.objects.doors[from_port.required_door] + if req_door.HasField("control_center_color"): + control_center_colors = CONTROL_CENTER_COLOR_NAMES.get(req_door.control_center_color, + ControlCenterColor.NONE) + if world.for_tracker: reqs.items.add(f"Worldport {fpid} Entered") - if not reqs.is_empty(): - connection.access_rule = make_location_lambda(reqs, world, None) + connection.set_lingo2_rule(world, None, reqs, control_center_colors) - for region in reqs.get_referenced_rooms(): - world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), - connection) + for region in reqs.get_referenced_rooms(): + world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), + connection) from_region.exits.append(connection) connection.connect(to_region) 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