diff options
-rw-r--r-- | apworld/regions.py | 75 | ||||
-rw-r--r-- | 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 | |||
6 | from .items import Lingo2Item | 6 | from .items import Lingo2Item |
7 | from .locations import Lingo2Location | 7 | from .locations import Lingo2Location |
8 | from .player_logic import AccessRequirements | 8 | from .player_logic import AccessRequirements |
9 | from .rules import make_location_lambda | 9 | from .rules import make_location_lambda, Lingo2Entrance, ControlCenterColor, CONTROL_CENTER_COLOR_NAMES, Lingo2Region |
10 | 10 | ||
11 | if TYPE_CHECKING: | 11 | if TYPE_CHECKING: |
12 | from . import Lingo2World | 12 | from . import Lingo2World |
13 | 13 | ||
14 | 14 | ||
15 | def create_region(room, world: "Lingo2World") -> Region: | 15 | def 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 | ||
19 | def create_locations(room, new_region: Region, world: "Lingo2World", regions: dict[str, Region]): | 19 | 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 | |||
53 | 53 | ||
54 | def create_regions(world: "Lingo2World"): | 54 | def 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 @@ | |||
1 | from collections.abc import Callable | 1 | from collections.abc import Callable |
2 | from enum import IntFlag, auto | ||
2 | from typing import TYPE_CHECKING | 3 | from typing import TYPE_CHECKING |
3 | 4 | ||
4 | from BaseClasses import CollectionState, Region | 5 | from BaseClasses import CollectionState, Region, MultiWorld, Entrance |
5 | from .player_logic import AccessRequirements | 6 | from .player_logic import AccessRequirements |
7 | from ..AutoWorld import LogicMixin | ||
6 | 8 | ||
7 | if TYPE_CHECKING: | 9 | if TYPE_CHECKING: |
8 | from . import Lingo2World | 10 | from . import Lingo2World |
9 | 11 | ||
10 | 12 | ||
13 | class 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 | |||
27 | CONTROL_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 | |||
39 | class 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 | |||
81 | class 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 | |||
96 | class 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 | |||
108 | class 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 | |||
140 | class 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 | |||
11 | def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region], | 197 | def 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 | |||
56 | def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World", | 243 | def 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 |