about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-10-06 09:58:45 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2025-10-06 09:58:45 -0400
commit9864382c9e4b2015c05214ebc177b410edc24bce (patch)
tree9c9ca3f93abc078a07d8a5a94c83152fd3eee642
parentd861b203529add969038278dba9d2696d1248cc3 (diff)
downloadlingo2-archipelago-cc-colors.tar.gz
lingo2-archipelago-cc-colors.tar.bz2
lingo2-archipelago-cc-colors.zip
trying something cc-colors
-rw-r--r--apworld/regions.py75
-rw-r--r--apworld/rules.py189
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
6from .items import Lingo2Item 6from .items import Lingo2Item
7from .locations import Lingo2Location 7from .locations import Lingo2Location
8from .player_logic import AccessRequirements 8from .player_logic import AccessRequirements
9from .rules import make_location_lambda 9from .rules import make_location_lambda, Lingo2Entrance, ControlCenterColor, CONTROL_CENTER_COLOR_NAMES, Lingo2Region
10 10
11if TYPE_CHECKING: 11if TYPE_CHECKING:
12 from . import Lingo2World 12 from . import Lingo2World
13 13
14 14
15def create_region(room, world: "Lingo2World") -> Region: 15def create_region(room, world: "Lingo2World") -> Region:
16 return Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld) 16 return Lingo2Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld)
17 17
18 18
19def create_locations(room, new_region: Region, world: "Lingo2World", regions: dict[str, Region]): 19def 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
53 53
54def create_regions(world: "Lingo2World"): 54def create_regions(world: "Lingo2World"):
55 regions = { 55 regions = {
56 "Menu": Region("Menu", world.player, world.multiworld) 56 "Menu": Lingo2Region("Menu", world.player, world.multiworld)
57 } 57 }
58 58
59 region_and_room = [] 59 region_and_room = []
@@ -69,7 +69,10 @@ def create_regions(world: "Lingo2World"):
69 for (region, room) in region_and_room: 69 for (region, room) in region_and_room:
70 create_locations(room, region, world, regions) 70 create_locations(room, region, world, regions)
71 71
72 regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") 72 start_game = Lingo2Entrance(world.player, "Start Game", regions["Menu"])
73 start_game.set_lingo2_rule(world, regions, AccessRequirements(), ControlCenterColor.ALL)
74 regions["Menu"].exits.append(start_game)
75 start_game.connect(regions["The Entry - Starting Room"])
73 76
74 for connection in world.static_logic.objects.connections: 77 for connection in world.static_logic.objects.connections:
75 if connection.roof_access and not world.options.daedalus_roof_access: 78 if connection.roof_access and not world.options.daedalus_roof_access:
@@ -87,6 +90,7 @@ def create_regions(world: "Lingo2World"):
87 connection_name = f"{from_region} -> {to_region}" 90 connection_name = f"{from_region} -> {to_region}"
88 91
89 reqs = AccessRequirements() 92 reqs = AccessRequirements()
93 control_center_colors = ControlCenterColor.ALL
90 94
91 if connection.HasField("required_door"): 95 if connection.HasField("required_door"):
92 reqs.merge(world.player_logic.get_door_open_reqs(connection.required_door)) 96 reqs.merge(world.player_logic.get_door_open_reqs(connection.required_door))
@@ -95,6 +99,10 @@ def create_regions(world: "Lingo2World"):
95 wmap = world.static_logic.objects.maps[door.map_id] 99 wmap = world.static_logic.objects.maps[door.map_id]
96 connection_name = f"{connection_name} (using {wmap.name} - {door.name})" 100 connection_name = f"{connection_name} (using {wmap.name} - {door.name})"
97 101
102 if door.HasField("control_center_color"):
103 control_center_colors = control_center_colors & CONTROL_CENTER_COLOR_NAMES.get(
104 door.control_center_color, ControlCenterColor.NONE)
105
98 if connection.HasField("port"): 106 if connection.HasField("port"):
99 port = world.static_logic.objects.ports[connection.port] 107 port = world.static_logic.objects.ports[connection.port]
100 connection_name = f"{connection_name} (via {port.display_name})" 108 connection_name = f"{connection_name} (via {port.display_name})"
@@ -105,6 +113,11 @@ def create_regions(world: "Lingo2World"):
105 if port.HasField("required_door"): 113 if port.HasField("required_door"):
106 reqs.merge(world.player_logic.get_door_open_reqs(port.required_door)) 114 reqs.merge(world.player_logic.get_door_open_reqs(port.required_door))
107 115
116 req_door = world.static_logic.objects.doors[port.required_door]
117 if req_door.HasField("control_center_color"):
118 control_center_colors = control_center_colors & CONTROL_CENTER_COLOR_NAMES.get(
119 req_door.control_center_color, ControlCenterColor.NONE)
120
108 if connection.HasField("painting"): 121 if connection.HasField("painting"):
109 painting = world.static_logic.objects.paintings[connection.painting] 122 painting = world.static_logic.objects.paintings[connection.painting]
110 connection_name = f"{connection_name} (via painting {painting.name})" 123 connection_name = f"{connection_name} (via painting {painting.name})"
@@ -112,6 +125,11 @@ def create_regions(world: "Lingo2World"):
112 if painting.HasField("required_door"): 125 if painting.HasField("required_door"):
113 reqs.merge(world.player_logic.get_door_open_reqs(painting.required_door)) 126 reqs.merge(world.player_logic.get_door_open_reqs(painting.required_door))
114 127
128 req_door = world.static_logic.objects.doors[painting.required_door]
129 if req_door.HasField("control_center_color"):
130 control_center_colors = control_center_colors & CONTROL_CENTER_COLOR_NAMES.get(
131 req_door.control_center_color, ControlCenterColor.NONE)
132
115 if connection.HasField("panel"): 133 if connection.HasField("panel"):
116 proxy = connection.panel 134 proxy = connection.panel
117 reqs.merge(world.player_logic.get_panel_reqs(proxy.panel, 135 reqs.merge(world.player_logic.get_panel_reqs(proxy.panel,
@@ -132,14 +150,14 @@ def create_regions(world: "Lingo2World"):
132 reqs.simplify() 150 reqs.simplify()
133 reqs.remove_room(from_region) 151 reqs.remove_room(from_region)
134 152
135 connection = Entrance(world.player, connection_name, regions[from_region]) 153 entrance = Lingo2Entrance(world.player, connection_name, regions[from_region])
136 connection.access_rule = make_location_lambda(reqs, world, regions) 154 entrance.set_lingo2_rule(world, regions, reqs, control_center_colors)
137 155
138 regions[from_region].exits.append(connection) 156 regions[from_region].exits.append(entrance)
139 connection.connect(regions[to_region]) 157 entrance.connect(regions[to_region])
140 158
141 for region in reqs.get_referenced_rooms(): 159 for region in reqs.get_referenced_rooms():
142 world.multiworld.register_indirect_condition(regions[region], connection) 160 world.multiworld.register_indirect_condition(regions[region], entrance)
143 161
144 world.multiworld.regions += regions.values() 162 world.multiworld.regions += regions.values()
145 163
@@ -160,19 +178,29 @@ def shuffle_entrances(world: "Lingo2World"):
160 connection_name = f"{port_region_name} - {port.display_name}" 178 connection_name = f"{port_region_name} - {port.display_name}"
161 port_id_by_name[connection_name] = port.id 179 port_id_by_name[connection_name] = port.id
162 180
163 entrance = port_region.create_er_target(connection_name) 181 entrance = Lingo2Entrance(world.player, connection_name)
182 entrance.connect(port_region)
164 entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY 183 entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY
165 184
166 er_exit = port_region.create_exit(connection_name) 185 er_exit = Lingo2Entrance(world.player, connection_name, port_region)
186 port_region.exits.append(er_exit)
167 er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY 187 er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY
168 188
189 door_reqs = AccessRequirements()
190 control_center_colors = ControlCenterColor.ALL
169 if port.HasField("required_door"): 191 if port.HasField("required_door"):
170 door_reqs = world.player_logic.get_door_open_reqs(port.required_door) 192 door_reqs = world.player_logic.get_door_open_reqs(port.required_door)
171 er_exit.access_rule = make_location_lambda(door_reqs, world, None)
172 193
173 for region in door_reqs.get_referenced_rooms(): 194 req_door = world.static_logic.objects.doors[port.required_door]
174 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), 195 if req_door.HasField("control_center_color"):
175 er_exit) 196 control_center_colors = CONTROL_CENTER_COLOR_NAMES.get(req_door.control_center_color,
197 ControlCenterColor.NONE)
198
199 er_exit.set_lingo2_rule(world, None, door_reqs, control_center_colors)
200
201 for region in door_reqs.get_referenced_rooms():
202 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
203 er_exit)
176 204
177 er_entrances.append(entrance) 205 er_entrances.append(entrance)
178 er_exits.append(er_exit) 206 er_exits.append(er_exit)
@@ -195,21 +223,26 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"):
195 from_region = world.multiworld.get_region(from_region_name, world.player) 223 from_region = world.multiworld.get_region(from_region_name, world.player)
196 to_region = world.multiworld.get_region(to_region_name, world.player) 224 to_region = world.multiworld.get_region(to_region_name, world.player)
197 225
198 connection = Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region) 226 connection = Lingo2Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region)
199 227
200 reqs = AccessRequirements() 228 reqs = AccessRequirements()
229 control_center_colors = ControlCenterColor.ALL
201 if from_port.HasField("required_door"): 230 if from_port.HasField("required_door"):
202 reqs = world.player_logic.get_door_open_reqs(from_port.required_door).copy() 231 reqs = world.player_logic.get_door_open_reqs(from_port.required_door).copy()
203 232
233 req_door = world.static_logic.objects.doors[from_port.required_door]
234 if req_door.HasField("control_center_color"):
235 control_center_colors = CONTROL_CENTER_COLOR_NAMES.get(req_door.control_center_color,
236 ControlCenterColor.NONE)
237
204 if world.for_tracker: 238 if world.for_tracker:
205 reqs.items.add(f"Worldport {fpid} Entered") 239 reqs.items.add(f"Worldport {fpid} Entered")
206 240
207 if not reqs.is_empty(): 241 connection.set_lingo2_rule(world, None, reqs, control_center_colors)
208 connection.access_rule = make_location_lambda(reqs, world, None)
209 242
210 for region in reqs.get_referenced_rooms(): 243 for region in reqs.get_referenced_rooms():
211 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), 244 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
212 connection) 245 connection)
213 246
214 from_region.exits.append(connection) 247 from_region.exits.append(connection)
215 connection.connect(to_region) 248 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 @@
1from collections.abc import Callable 1from collections.abc import Callable
2from enum import IntFlag, auto
2from typing import TYPE_CHECKING 3from typing import TYPE_CHECKING
3 4
4from BaseClasses import CollectionState, Region 5from BaseClasses import CollectionState, Region, MultiWorld, Entrance
5from .player_logic import AccessRequirements 6from .player_logic import AccessRequirements
7from ..AutoWorld import LogicMixin
6 8
7if TYPE_CHECKING: 9if TYPE_CHECKING:
8 from . import Lingo2World 10 from . import Lingo2World
9 11
10 12
13class ControlCenterColor(IntFlag):
14 RED = auto()
15 BLUE = auto()
16 ORANGE = auto()
17 MAGENTA = auto()
18 PURPLE = auto()
19 GREEN = auto()
20 BROWN = auto()
21 WHITE = auto()
22
23 NONE = 0
24 ALL = RED + BLUE + ORANGE + MAGENTA + PURPLE + GREEN + BROWN + WHITE
25
26
27CONTROL_CENTER_COLOR_NAMES: dict[str, ControlCenterColor] = {
28 "red": ControlCenterColor.RED,
29 "blue": ControlCenterColor.BLUE,
30 "orange": ControlCenterColor.ORANGE,
31 "magenta": ControlCenterColor.MAGENTA,
32 "purple": ControlCenterColor.PURPLE,
33 "green": ControlCenterColor.GREEN,
34 "brown": ControlCenterColor.BROWN,
35 "white": ControlCenterColor.WHITE,
36}
37
38
39class Lingo2Entrance(Entrance):
40 world: "Lingo2World"
41 reqs: AccessRequirements | None
42 required_regions: list[Region]
43 control_center_colors: ControlCenterColor
44
45 def set_lingo2_rule(self, world: "Lingo2World", regions: dict[str, Region] | None, reqs: AccessRequirements,
46 control_center_colors: ControlCenterColor):
47 self.world = world
48
49 if reqs.is_empty():
50 self.reqs = None
51 else:
52 self.reqs = reqs.copy()
53 self.reqs.rooms.clear()
54
55 if regions is None:
56 self.required_regions = [world.multiworld.get_region(room_name, world.player)
57 for room_name in reqs.rooms]
58 else:
59 self.required_regions = [regions[room_name] for room_name in reqs.rooms]
60
61 self.control_center_colors = control_center_colors
62
63 self.access_rule = self._lingo2_rule
64
65 def _lingo2_rule(self, state: CollectionState) -> bool:
66 if self.reqs is not None and not lingo2_can_satisfy_requirements(state, self.reqs, self.required_regions,
67 self.world):
68 return False
69
70 if self.control_center_colors != ControlCenterColor.ALL\
71 and not self.world.options.shuffle_control_center_colors:
72 from_region_state = state.lingo2_get_region_state(self.parent_region)
73
74 if from_region_state.control_center_colors & self.control_center_colors == ControlCenterColor.NONE:
75 return False
76
77 state.lingo2_use_entrance(self)
78 return True
79
80
81class Lingo2Region(Region):
82 def can_reach(self, state) -> bool:
83 if self in state.reachable_regions[self.player]:
84 return True
85
86 if not state.stale[self.player] and not state.lingo2_state[self.player].stale:
87 # if the cache is updated we can use the cache
88 return super().can_reach(state)
89
90 if state.lingo2_state[self.player].stale:
91 state.lingo2_sweep(self.player)
92
93 return super().can_reach(state)
94
95
96class PerPlayerRegionState:
97 control_center_colors: ControlCenterColor
98
99 def __init__(self):
100 self.control_center_colors = ControlCenterColor.NONE
101
102 def copy(self) -> "PerPlayerRegionState":
103 result = PerPlayerRegionState()
104 result.control_center_colors = self.control_center_colors
105 return result
106
107
108class PerPlayerState:
109 world: "Lingo2World"
110 regions: dict[str, PerPlayerRegionState]
111 stale: bool
112 sweeping: bool
113 sweepable_entrances: set[Entrance]
114 check_colors: bool
115
116 def __init__(self, world: "Lingo2World"):
117 self.world = world
118 self.regions = {}
119 self.stale = True
120 self.sweeping = False
121 self.sweepable_entrances = set()
122 self.check_colors = not world.options.shuffle_control_center_colors
123
124 self.regions["Menu"] = PerPlayerRegionState()
125 self.regions["Menu"].control_center_colors = ControlCenterColor.ALL
126
127 def copy(self) -> "PerPlayerState":
128 result = PerPlayerState(self.world)
129 result.regions = {region_name: region_state.copy() for region_name, region_state in self.regions.items()}
130 result.stale = self.stale
131 result.sweeping = self.sweeping
132 result.sweepable_entrances = self.sweepable_entrances.copy()
133 result.check_colors = self.check_colors
134 return result
135
136 def get_region_state(self, region: Region):
137 return self.regions.setdefault(region.name, PerPlayerRegionState())
138
139
140class Lingo2LogicMixin(LogicMixin):
141 multiworld: MultiWorld
142 reachable_regions: dict[int, set[Region]]
143
144 lingo2_state: dict[int, PerPlayerState]
145
146 def init_mixin(self, multiworld: MultiWorld):
147 self.lingo2_state = {player: PerPlayerState(multiworld.worlds[player])
148 for player in multiworld.get_game_players("Lingo 2")}
149
150 def copy_mixin(self, new_state: CollectionState) -> CollectionState:
151 new_state.lingo2_state = {player: pps.copy() for player, pps in self.lingo2_state.items()}
152
153 return new_state
154
155 def lingo2_get_region_state(self, region: Region):
156 return self.lingo2_state[region.player].get_region_state(region)
157
158 def lingo2_use_entrance(self, entrance: Lingo2Entrance):
159 player_state = self.lingo2_state[entrance.player]
160
161 from_region_state = player_state.get_region_state(entrance.parent_region)
162 to_region_state = player_state.get_region_state(entrance.connected_region)
163
164 should_update = False
165
166 if player_state.check_colors:
167 avail_colors = from_region_state.control_center_colors & entrance.control_center_colors
168 if avail_colors & ~to_region_state.control_center_colors != ControlCenterColor.NONE:
169 if to_region_state.control_center_colors != ControlCenterColor.NONE or avail_colors != ControlCenterColor.ALL:
170 #print(f"entrance {entrance.name} takes region from {to_region_state.control_center_colors} to {avail_colors}")
171 pass
172
173 should_update = True
174 to_region_state.control_center_colors = to_region_state.control_center_colors | avail_colors
175
176 if should_update:
177 player_state.stale = True
178
179 for adjacent in entrance.parent_region.exits:
180 player_state.sweepable_entrances.add(adjacent)
181
182 def lingo2_sweep(self, player: int):
183 player_state = self.lingo2_state[player]
184 if player_state.sweeping:
185 return
186 player_state.sweeping = True
187
188 while player_state.sweepable_entrances:
189 next_entrance = player_state.sweepable_entrances.pop()
190 if next_entrance.parent_region in self.reachable_regions[player]:
191 next_entrance.can_reach(self)
192
193 player_state.stale = False
194 player_state.sweeping = False
195
196
11def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region], 197def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region],
12 world: "Lingo2World") -> bool: 198 world: "Lingo2World") -> bool:
13 if not all(state.has(item, world.player) for item in reqs.items): 199 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
53 239
54 return True 240 return True
55 241
242
56def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World", 243def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World",
57 regions: dict[str, Region] | None) -> Callable[[CollectionState], bool]: 244 regions: dict[str, Region] | None) -> Callable[[CollectionState], bool]:
58 # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule 245 # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule