about summary refs log tree commit diff stats
path: root/apworld/rules.py
diff options
context:
space:
mode:
Diffstat (limited to 'apworld/rules.py')
-rw-r--r--apworld/rules.py189
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 @@
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