about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2026-02-14 13:42:36 -0500
committerStar Rauchenberger <fefferburbia@gmail.com>2026-02-14 13:43:13 -0500
commite38516f2436cb7c403da86d6d588b75644c8cdcf (patch)
tree79db39a0cccd4bf7283782df8911729eae497c77 /apworld
parent8f5184fdb2545b46d930af17e99be76f59516906 (diff)
downloadlingo2-archipelago-e38516f2436cb7c403da86d6d588b75644c8cdcf.tar.gz
lingo2-archipelago-e38516f2436cb7c403da86d6d588b75644c8cdcf.tar.bz2
lingo2-archipelago-e38516f2436cb7c403da86d6d588b75644c8cdcf.zip
Refactored access reqs HEAD main
Diffstat (limited to 'apworld')
-rw-r--r--apworld/locations.py81
-rw-r--r--apworld/player_logic.py169
-rw-r--r--apworld/regions.py40
-rw-r--r--apworld/rules.py243
4 files changed, 297 insertions, 236 deletions
diff --git a/apworld/locations.py b/apworld/locations.py index 174a0dd..c92215e 100644 --- a/apworld/locations.py +++ b/apworld/locations.py
@@ -1,7 +1,12 @@
1from enum import Enum 1from enum import Enum
2from typing import TYPE_CHECKING
2 3
3from BaseClasses import Location, Item 4from BaseClasses import Location, Item, Region, CollectionState, Entrance
4from .items import Lingo2Item 5from .items import Lingo2Item
6from .rules import AccessRequirements
7
8if TYPE_CHECKING:
9 from . import Lingo2World
5 10
6 11
7class LetterPlacementType(Enum): 12class LetterPlacementType(Enum):
@@ -10,13 +15,59 @@ class LetterPlacementType(Enum):
10 FORCE = 2 15 FORCE = 2
11 16
12 17
18def get_required_regions(reqs: AccessRequirements, world: "Lingo2World",
19 regions: dict[str, Region] | None) -> list[Region]:
20 # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule
21 # checking.
22 if regions is not None:
23 return [regions[room_name] for room_name in reqs.rooms]
24 else:
25 return [world.multiworld.get_region(room_name, world.player) for room_name in reqs.rooms]
26
27
13class Lingo2Location(Location): 28class Lingo2Location(Location):
14 game: str = "Lingo 2" 29 game: str = "Lingo 2"
15 30
31 reqs: AccessRequirements | None
32 world: "Lingo2World"
33 required_regions: list[Region]
34
16 port_id: int 35 port_id: int
17 goal: bool 36 goal: bool
18 letter_placement_type: LetterPlacementType 37 letter_placement_type: LetterPlacementType
19 38
39 @classmethod
40 def non_event_location(cls, world: "Lingo2World", code: int, region: Region):
41 result = cls(world.player, world.static_logic.location_id_to_name[code], code, region)
42 result.reqs = None
43 result.world = world
44 result.required_regions = []
45
46 return result
47
48 @classmethod
49 def event_location(cls, world: "Lingo2World", name: str, region: Region):
50 result = cls(world.player, name, None, region)
51 result.reqs = None
52 result.world = world
53 result.required_regions = []
54
55 return result
56
57 def set_access_rule(self, reqs: AccessRequirements, regions: dict[str, Region] | None):
58 self.reqs = reqs
59 self.required_regions = get_required_regions(reqs, self.world, regions)
60 self.access_rule = self._l2_access_rule
61
62 def _l2_access_rule(self, state: CollectionState) -> bool:
63 if self.reqs is not None and not self.reqs.check_access(state, self.world):
64 return False
65
66 if not all(state.can_reach(region) for region in self.required_regions):
67 return False
68
69 return True
70
20 def set_up_letter_rule(self, lpt: LetterPlacementType): 71 def set_up_letter_rule(self, lpt: LetterPlacementType):
21 self.letter_placement_type = lpt 72 self.letter_placement_type = lpt
22 self.item_rule = self._l2_item_rule 73 self.item_rule = self._l2_item_rule
@@ -31,3 +82,31 @@ class Lingo2Location(Location):
31 return not item.is_letter 82 return not item.is_letter
32 83
33 return True 84 return True
85
86
87class Lingo2Entrance(Entrance):
88 reqs: AccessRequirements | None
89 world: "Lingo2World"
90 required_regions: list[Region]
91
92 def __init__(self, world: "Lingo2World", description: str, region: Region):
93 super().__init__(world.player, description, region)
94
95 self.reqs = None
96 self.world = world
97 self.required_regions = []
98
99 def set_access_rule(self, reqs: AccessRequirements, regions: dict[str, Region] | None):
100 self.reqs = reqs
101 self.required_regions = get_required_regions(reqs, self.world, regions)
102 self.access_rule = self._l2_access_rule
103
104 def _l2_access_rule(self, state: CollectionState) -> bool:
105 if self.reqs is not None and not self.reqs.check_access(state, self.world):
106 return False
107
108 if not all(state.can_reach(region) for region in self.required_regions):
109 return False
110
111 return True
112
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index ea74266..2c3e08b 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -6,6 +6,7 @@ from .items import SYMBOL_ITEMS
6from typing import TYPE_CHECKING, NamedTuple 6from typing import TYPE_CHECKING, NamedTuple
7 7
8from .options import ShuffleLetters, CyanDoorBehavior, VictoryCondition, FastTravelAccess 8from .options import ShuffleLetters, CyanDoorBehavior, VictoryCondition, FastTravelAccess
9from .rules import AccessRequirements
9 10
10if TYPE_CHECKING: 11if TYPE_CHECKING:
11 from . import Lingo2World 12 from . import Lingo2World
@@ -21,174 +22,6 @@ def calculate_letter_histogram(solution: str) -> dict[str, int]:
21 return histogram 22 return histogram
22 23
23 24
24class AccessRequirements:
25 items: set[str]
26 progressives: dict[str, int]
27 rooms: set[str]
28 letters: dict[str, int]
29 cyans: bool
30
31 # This is an AND of ORs.
32 or_logic: list[list["AccessRequirements"]]
33
34 # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should
35 # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1.
36 complete_at: int | None
37 possibilities: list["AccessRequirements"]
38
39 def __init__(self):
40 self.items = set()
41 self.progressives = dict()
42 self.rooms = set()
43 self.letters = dict()
44 self.cyans = False
45 self.or_logic = list()
46 self.complete_at = None
47 self.possibilities = list()
48
49 def copy(self) -> "AccessRequirements":
50 reqs = AccessRequirements()
51 reqs.items = self.items.copy()
52 reqs.progressives = self.progressives.copy()
53 reqs.rooms = self.rooms.copy()
54 reqs.letters = self.letters.copy()
55 reqs.cyans = self.cyans
56 reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic]
57 reqs.complete_at = self.complete_at
58 reqs.possibilities = self.possibilities.copy()
59 return reqs
60
61 def merge(self, other: "AccessRequirements"):
62 for item in other.items:
63 self.items.add(item)
64
65 for item, amount in other.progressives.items():
66 self.progressives[item] = max(amount, self.progressives.get(item, 0))
67
68 for room in other.rooms:
69 self.rooms.add(room)
70
71 for letter, level in other.letters.items():
72 self.letters[letter] = max(self.letters.get(letter, 0), level)
73
74 self.cyans = self.cyans or other.cyans
75
76 for disjunction in other.or_logic:
77 self.or_logic.append([sub_req.copy() for sub_req in disjunction])
78
79 if other.complete_at is not None:
80 # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of
81 # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports
82 # conjunctions of requirements.
83 if self.complete_at is not None:
84 print("Merging requirements with complete_at > 1. This is messy and should be avoided!")
85
86 left_req = AccessRequirements()
87 left_req.complete_at = self.complete_at
88 left_req.possibilities = [sub_req.copy() for sub_req in self.possibilities]
89 self.or_logic.append([left_req])
90
91 self.complete_at = None
92 self.possibilities = list()
93
94 right_req = AccessRequirements()
95 right_req.complete_at = other.complete_at
96 right_req.possibilities = [sub_req.copy() for sub_req in other.possibilities]
97 self.or_logic.append([right_req])
98 else:
99 self.complete_at = other.complete_at
100 self.possibilities = [sub_req.copy() for sub_req in other.possibilities]
101
102 def is_empty(self) -> bool:
103 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0
104 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None)
105
106 def __eq__(self, other: "AccessRequirements"):
107 return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and
108 self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and
109 self.complete_at == other.complete_at and self.possibilities == other.possibilities)
110
111 def simplify(self):
112 resimplify = False
113
114 if len(self.or_logic) > 0:
115 old_or_logic = self.or_logic
116
117 def remove_redundant(sub_reqs: "AccessRequirements"):
118 new_reqs = sub_reqs.copy()
119 new_reqs.letters = {l: v for l, v in new_reqs.letters.items() if self.letters.get(l, 0) < v}
120 if new_reqs != sub_reqs:
121 return new_reqs
122 else:
123 return sub_reqs
124
125 self.or_logic = []
126 for disjunction in old_or_logic:
127 new_disjunction = []
128 for ssr in disjunction:
129 new_ssr = remove_redundant(ssr)
130 if not new_ssr.is_empty():
131 new_disjunction.append(new_ssr)
132 else:
133 new_disjunction.clear()
134 break
135 if len(new_disjunction) == 1:
136 self.merge(new_disjunction[0])
137 resimplify = True
138 elif len(new_disjunction) > 1:
139 if all(cjr == new_disjunction[0] for cjr in new_disjunction):
140 self.merge(new_disjunction[0])
141 resimplify = True
142 else:
143 self.or_logic.append(new_disjunction)
144
145 if resimplify:
146 self.simplify()
147
148 def get_referenced_rooms(self):
149 result = set(self.rooms)
150
151 for disjunction in self.or_logic:
152 for sub_req in disjunction:
153 result = result.union(sub_req.get_referenced_rooms())
154
155 for sub_req in self.possibilities:
156 result = result.union(sub_req.get_referenced_rooms())
157
158 return result
159
160 def remove_room(self, room: str):
161 if room in self.rooms:
162 self.rooms.remove(room)
163
164 for disjunction in self.or_logic:
165 for sub_req in disjunction:
166 sub_req.remove_room(room)
167
168 for sub_req in self.possibilities:
169 sub_req.remove_room(room)
170
171 def __repr__(self):
172 parts = []
173 if len(self.items) > 0:
174 parts.append(f"items={self.items}")
175 if len(self.progressives) > 0:
176 parts.append(f"progressives={self.progressives}")
177 if len(self.rooms) > 0:
178 parts.append(f"rooms={self.rooms}")
179 if len(self.letters) > 0:
180 parts.append(f"letters={self.letters}")
181 if self.cyans:
182 parts.append(f"cyans=True")
183 if len(self.or_logic) > 0:
184 parts.append(f"or_logic={self.or_logic}")
185 if self.complete_at is not None:
186 parts.append(f"complete_at={self.complete_at}")
187 if len(self.possibilities) > 0:
188 parts.append(f"possibilities={self.possibilities}")
189 return "AccessRequirements(" + ", ".join(parts) + ")"
190
191
192class PlayerLocation(NamedTuple): 25class PlayerLocation(NamedTuple):
193 code: int | None 26 code: int | None
194 reqs: AccessRequirements 27 reqs: AccessRequirements
diff --git a/apworld/regions.py b/apworld/regions.py index 3996153..313fd02 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -4,10 +4,9 @@ import BaseClasses
4from BaseClasses import Region, ItemClassification, Entrance 4from BaseClasses import Region, ItemClassification, Entrance
5from entrance_rando import randomize_entrances 5from entrance_rando import randomize_entrances
6from .items import Lingo2Item 6from .items import Lingo2Item
7from .locations import Lingo2Location, LetterPlacementType 7from .locations import Lingo2Location, LetterPlacementType, Lingo2Entrance
8from .options import FastTravelAccess 8from .options import FastTravelAccess
9from .player_logic import AccessRequirements 9from .player_logic import AccessRequirements
10from .rules import make_location_lambda
11 10
12if TYPE_CHECKING: 11if TYPE_CHECKING:
13 from . import Lingo2World 12 from . import Lingo2World
@@ -22,9 +21,8 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di
22 reqs = location.reqs.copy() 21 reqs = location.reqs.copy()
23 reqs.remove_room(new_region.name) 22 reqs.remove_room(new_region.name)
24 23
25 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], 24 new_location = Lingo2Location.non_event_location(world, location.code, new_region)
26 location.code, new_region) 25 new_location.set_access_rule(reqs, regions)
27 new_location.access_rule = make_location_lambda(reqs, world, regions)
28 if world.options.restrict_letter_placements: 26 if world.options.restrict_letter_placements:
29 if location.is_letter: 27 if location.is_letter:
30 new_location.set_up_letter_rule(LetterPlacementType.FORCE) 28 new_location.set_up_letter_rule(LetterPlacementType.FORCE)
@@ -33,7 +31,7 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di
33 new_region.locations.append(new_location) 31 new_region.locations.append(new_location)
34 32
35 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): 33 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items():
36 new_location = Lingo2Location(world.player, event_name, None, new_region) 34 new_location = Lingo2Location.event_location(world, event_name, new_region)
37 if world.for_tracker and item_name == "Victory": 35 if world.for_tracker and item_name == "Victory":
38 new_location.goal = True 36 new_location.goal = True
39 37
@@ -47,12 +45,11 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di
47 if port.no_shuffle: 45 if port.no_shuffle:
48 continue 46 continue
49 47
50 new_location = Lingo2Location(world.player, f"Worldport {port.id} Entered", None, new_region) 48 new_location = Lingo2Location.event_location(world, f"Worldport {port.id} Entered", new_region)
51 new_location.port_id = port.id 49 new_location.port_id = port.id
52 50
53 if port.HasField("required_door"): 51 if port.HasField("required_door"):
54 new_location.access_rule = \ 52 new_location.set_access_rule(world.player_logic.get_door_open_reqs(port.required_door), regions)
55 make_location_lambda(world.player_logic.get_door_open_reqs(port.required_door), world, regions)
56 53
57 new_region.locations.append(new_location) 54 new_region.locations.append(new_location)
58 55
@@ -154,8 +151,8 @@ def create_regions(world: "Lingo2World"):
154 # what regions are dead ends. 151 # what regions are dead ends.
155 continue 152 continue
156 153
157 connection = Entrance(world.player, connection_name, regions[from_region]) 154 connection = Lingo2Entrance(world, connection_name, regions[from_region])
158 connection.access_rule = make_location_lambda(reqs, world, regions) 155 connection.set_access_rule(reqs, regions)
159 156
160 regions[from_region].exits.append(connection) 157 regions[from_region].exits.append(connection)
161 connection.connect(regions[to_region]) 158 connection.connect(regions[to_region])
@@ -172,13 +169,15 @@ def create_regions(world: "Lingo2World"):
172 continue 169 continue
173 170
174 connection_name = f"Return to {to_region}" 171 connection_name = f"Return to {to_region}"
175 172 connection = Lingo2Entrance(world, connection_name, regions["Menu"])
176 reqs = AccessRequirements() 173 regions["Menu"].exits.append(connection)
174 connection.connect(regions[to_region])
177 175
178 if world.options.fast_travel_access == FastTravelAccess.option_items: 176 if world.options.fast_travel_access == FastTravelAccess.option_items:
177 reqs = AccessRequirements()
179 reqs.items.add(world.static_logic.get_map_rte_item_name(rte_map_id)) 178 reqs.items.add(world.static_logic.get_map_rte_item_name(rte_map_id))
180 179
181 regions["Menu"].connect(regions[to_region], connection_name, make_location_lambda(reqs, world, None)) 180 connection.set_access_rule(reqs, regions)
182 181
183 world.multiworld.regions += regions.values() 182 world.multiworld.regions += regions.values()
184 183
@@ -211,13 +210,13 @@ def shuffle_entrances(world: "Lingo2World"):
211 from_region = world.multiworld.get_region(from_region_name, world.player) 210 from_region = world.multiworld.get_region(from_region_name, world.player)
212 to_region = world.multiworld.get_region(to_region_name, world.player) 211 to_region = world.multiworld.get_region(to_region_name, world.player)
213 212
214 connection = Entrance(world.player, f"{from_region_name} - {chosen_port.display_name}", from_region) 213 connection = Lingo2Entrance(world, f"{from_region_name} - {chosen_port.display_name}", from_region)
215 from_region.exits.append(connection) 214 from_region.exits.append(connection)
216 connection.connect(to_region) 215 connection.connect(to_region)
217 216
218 if chosen_port.HasField("required_door"): 217 if chosen_port.HasField("required_door"):
219 door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door) 218 door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door)
220 connection.access_rule = make_location_lambda(door_reqs, world, None) 219 connection.set_access_rule(door_reqs, None)
221 220
222 for region in door_reqs.get_referenced_rooms(): 221 for region in door_reqs.get_referenced_rooms():
223 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), 222 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
@@ -233,12 +232,13 @@ def shuffle_entrances(world: "Lingo2World"):
233 entrance = port_region.create_er_target(connection_name) 232 entrance = port_region.create_er_target(connection_name)
234 entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY 233 entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY
235 234
236 er_exit = port_region.create_exit(connection_name) 235 er_exit = Lingo2Entrance(world, connection_name, port_region)
236 port_region.exits.append(er_exit)
237 er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY 237 er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY
238 238
239 if port.HasField("required_door"): 239 if port.HasField("required_door"):
240 door_reqs = world.player_logic.get_door_open_reqs(port.required_door) 240 door_reqs = world.player_logic.get_door_open_reqs(port.required_door)
241 er_exit.access_rule = make_location_lambda(door_reqs, world, None) 241 er_exit.set_access_rule(door_reqs, None)
242 242
243 for region in door_reqs.get_referenced_rooms(): 243 for region in door_reqs.get_referenced_rooms():
244 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),
@@ -265,7 +265,7 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"):
265 from_region = world.multiworld.get_region(from_region_name, world.player) 265 from_region = world.multiworld.get_region(from_region_name, world.player)
266 to_region = world.multiworld.get_region(to_region_name, world.player) 266 to_region = world.multiworld.get_region(to_region_name, world.player)
267 267
268 connection = Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region) 268 connection = Lingo2Entrance(world, f"{from_region_name} - {from_port.display_name}", from_region)
269 269
270 reqs = AccessRequirements() 270 reqs = AccessRequirements()
271 if from_port.HasField("required_door"): 271 if from_port.HasField("required_door"):
@@ -275,7 +275,7 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"):
275 reqs.items.add(f"Worldport {fpid} Entered") 275 reqs.items.add(f"Worldport {fpid} Entered")
276 276
277 if not reqs.is_empty(): 277 if not reqs.is_empty():
278 connection.access_rule = make_location_lambda(reqs, world, None) 278 connection.set_access_rule(reqs, None)
279 279
280 for region in reqs.get_referenced_rooms(): 280 for region in reqs.get_referenced_rooms():
281 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), 281 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
diff --git a/apworld/rules.py b/apworld/rules.py index f859e75..70a76c0 100644 --- a/apworld/rules.py +++ b/apworld/rules.py
@@ -1,66 +1,215 @@
1from collections.abc import Callable
2from typing import TYPE_CHECKING 1from typing import TYPE_CHECKING
3 2
4from BaseClasses import CollectionState, Region 3from BaseClasses import CollectionState
5from .player_logic import AccessRequirements
6 4
7if TYPE_CHECKING: 5if TYPE_CHECKING:
8 from . import Lingo2World 6 from . import Lingo2World
9 7
10 8
11def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region], 9class AccessRequirements:
12 world: "Lingo2World") -> bool: 10 items: set[str]
13 if not all(state.has(item, world.player) for item in reqs.items): 11 progressives: dict[str, int]
14 return False 12 rooms: set[str]
13 letters: dict[str, int]
14 cyans: bool
15 15
16 if not all(state.has(item, world.player, amount) for item, amount in reqs.progressives.items()): 16 # This is an AND of ORs.
17 return False 17 or_logic: list[list["AccessRequirements"]]
18 18
19 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): 19 # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should
20 return False 20 # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1.
21 complete_at: int | None
22 possibilities: list["AccessRequirements"]
21 23
22 if not all(state.can_reach(region) for region in regions): 24 def __init__(self):
23 return False 25 self.items = set()
26 self.progressives = dict()
27 self.rooms = set()
28 self.letters = dict()
29 self.cyans = False
30 self.or_logic = list()
31 self.complete_at = None
32 self.possibilities = list()
24 33
25 for letter_key, letter_level in reqs.letters.items(): 34 def copy(self) -> "AccessRequirements":
26 if not state.has(letter_key, world.player, letter_level): 35 reqs = AccessRequirements()
36 reqs.items = self.items.copy()
37 reqs.progressives = self.progressives.copy()
38 reqs.rooms = self.rooms.copy()
39 reqs.letters = self.letters.copy()
40 reqs.cyans = self.cyans
41 reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic]
42 reqs.complete_at = self.complete_at
43 reqs.possibilities = self.possibilities.copy()
44 return reqs
45
46 def merge(self, other: "AccessRequirements"):
47 for item in other.items:
48 self.items.add(item)
49
50 for item, amount in other.progressives.items():
51 self.progressives[item] = max(amount, self.progressives.get(item, 0))
52
53 for room in other.rooms:
54 self.rooms.add(room)
55
56 for letter, level in other.letters.items():
57 self.letters[letter] = max(self.letters.get(letter, 0), level)
58
59 self.cyans = self.cyans or other.cyans
60
61 for disjunction in other.or_logic:
62 self.or_logic.append([sub_req.copy() for sub_req in disjunction])
63
64 if other.complete_at is not None:
65 # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of
66 # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports
67 # conjunctions of requirements.
68 if self.complete_at is not None:
69 print("Merging requirements with complete_at > 1. This is messy and should be avoided!")
70
71 left_req = AccessRequirements()
72 left_req.complete_at = self.complete_at
73 left_req.possibilities = [sub_req.copy() for sub_req in self.possibilities]
74 self.or_logic.append([left_req])
75
76 self.complete_at = None
77 self.possibilities = list()
78
79 right_req = AccessRequirements()
80 right_req.complete_at = other.complete_at
81 right_req.possibilities = [sub_req.copy() for sub_req in other.possibilities]
82 self.or_logic.append([right_req])
83 else:
84 self.complete_at = other.complete_at
85 self.possibilities = [sub_req.copy() for sub_req in other.possibilities]
86
87 def is_empty(self) -> bool:
88 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0
89 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None)
90
91 def __eq__(self, other: "AccessRequirements"):
92 return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and
93 self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and
94 self.complete_at == other.complete_at and self.possibilities == other.possibilities)
95
96 def simplify(self):
97 resimplify = False
98
99 if len(self.or_logic) > 0:
100 old_or_logic = self.or_logic
101
102 def remove_redundant(sub_reqs: "AccessRequirements"):
103 new_reqs = sub_reqs.copy()
104 new_reqs.letters = {l: v for l, v in new_reqs.letters.items() if self.letters.get(l, 0) < v}
105 if new_reqs != sub_reqs:
106 return new_reqs
107 else:
108 return sub_reqs
109
110 self.or_logic = []
111 for disjunction in old_or_logic:
112 new_disjunction = []
113 for ssr in disjunction:
114 new_ssr = remove_redundant(ssr)
115 if not new_ssr.is_empty():
116 new_disjunction.append(new_ssr)
117 else:
118 new_disjunction.clear()
119 break
120 if len(new_disjunction) == 1:
121 self.merge(new_disjunction[0])
122 resimplify = True
123 elif len(new_disjunction) > 1:
124 if all(cjr == new_disjunction[0] for cjr in new_disjunction):
125 self.merge(new_disjunction[0])
126 resimplify = True
127 else:
128 self.or_logic.append(new_disjunction)
129
130 if resimplify:
131 self.simplify()
132
133 def get_referenced_rooms(self):
134 result = set(self.rooms)
135
136 for disjunction in self.or_logic:
137 for sub_req in disjunction:
138 result = result.union(sub_req.get_referenced_rooms())
139
140 for sub_req in self.possibilities:
141 result = result.union(sub_req.get_referenced_rooms())
142
143 return result
144
145 def remove_room(self, room: str):
146 if room in self.rooms:
147 self.rooms.remove(room)
148
149 for disjunction in self.or_logic:
150 for sub_req in disjunction:
151 sub_req.remove_room(room)
152
153 for sub_req in self.possibilities:
154 sub_req.remove_room(room)
155
156 def __repr__(self):
157 parts = []
158 if len(self.items) > 0:
159 parts.append(f"items={self.items}")
160 if len(self.progressives) > 0:
161 parts.append(f"progressives={self.progressives}")
162 if len(self.rooms) > 0:
163 parts.append(f"rooms={self.rooms}")
164 if len(self.letters) > 0:
165 parts.append(f"letters={self.letters}")
166 if self.cyans:
167 parts.append(f"cyans=True")
168 if len(self.or_logic) > 0:
169 parts.append(f"or_logic={self.or_logic}")
170 if self.complete_at is not None:
171 parts.append(f"complete_at={self.complete_at}")
172 if len(self.possibilities) > 0:
173 parts.append(f"possibilities={self.possibilities}")
174 return "AccessRequirements(" + ", ".join(parts) + ")"
175
176 def check_access(self, state: CollectionState, world: "Lingo2World") -> bool:
177 if not all(state.has(item, world.player) for item in self.items):
27 return False 178 return False
28 179
29 if reqs.cyans: 180 if not all(state.has(item, world.player, amount) for item, amount in self.progressives.items()):
30 if not any(state.has(letter, world.player, amount)
31 for letter, amount in world.player_logic.double_letter_amount.items()):
32 return False 181 return False
33 182
34 if len(reqs.or_logic) > 0: 183 if not all(state.can_reach_region(region_name, world.player) for region_name in self.rooms):
35 if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, [], world) for sub_reqs in subjunction)
36 for subjunction in reqs.or_logic):
37 return False 184 return False
38 185
39 if reqs.complete_at is not None: 186 for letter_key, letter_level in self.letters.items():
40 completed = 0 187 if not state.has(letter_key, world.player, letter_level):
41 checked = 0 188 return False
42 for possibility in reqs.possibilities: 189
43 checked += 1 190 if self.cyans:
44 if lingo2_can_satisfy_requirements(state, possibility, [], world): 191 if not any(state.has(letter, world.player, amount)
45 completed += 1 192 for letter, amount in world.player_logic.double_letter_amount.items()):
46 if completed >= reqs.complete_at: 193 return False
47 break 194
48 elif len(reqs.possibilities) - checked + completed < reqs.complete_at: 195 if len(self.or_logic) > 0:
49 # There aren't enough remaining possibilities for the check to pass. 196 if not all(any(sub_reqs.check_access(state, world) for sub_reqs in subjunction)
197 for subjunction in self.or_logic):
198 return False
199
200 if self.complete_at is not None:
201 completed = 0
202 checked = 0
203 for possibility in self.possibilities:
204 checked += 1
205 if possibility.check_access(state, world):
206 completed += 1
207 if completed >= self.complete_at:
208 break
209 elif len(self.possibilities) - checked + completed < self.complete_at:
210 # There aren't enough remaining possibilities for the check to pass.
211 return False
212 if completed < self.complete_at:
50 return False 213 return False
51 if completed < reqs.complete_at:
52 return False
53 214
54 return True 215 return True
55
56def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World",
57 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
59 # checking.
60 if regions is not None:
61 required_regions = [regions[room_name] for room_name in reqs.rooms]
62 else:
63 required_regions = [world.multiworld.get_region(room_name, world.player) for room_name in reqs.rooms]
64 new_reqs = reqs.copy()
65 new_reqs.rooms.clear()
66 return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world)