diff options
Diffstat (limited to 'apworld/rules.py')
-rw-r--r-- | apworld/rules.py | 189 |
1 files changed, 188 insertions, 1 deletions
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 |