about summary refs log tree commit diff stats
path: root/apworld/player_logic.py
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-09-06 09:19:22 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2025-09-06 09:19:22 -0400
commitebda0b634c2396338b86b45128bf507c967e88a7 (patch)
tree80324582c52350c262dd4b5b7a824401ce6ebe4a /apworld/player_logic.py
parentcc58fb28a7825f562c874b56fa08656096cc6a6c (diff)
downloadlingo2-archipelago-ebda0b634c2396338b86b45128bf507c967e88a7.tar.gz
lingo2-archipelago-ebda0b634c2396338b86b45128bf507c967e88a7.tar.bz2
lingo2-archipelago-ebda0b634c2396338b86b45128bf507c967e88a7.zip
[Apworld] Added letter shuffle
Diffstat (limited to 'apworld/player_logic.py')
-rw-r--r--apworld/player_logic.py109
1 files changed, 83 insertions, 26 deletions
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index dc1bdf0..5cb9011 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -1,7 +1,9 @@
1from enum import IntEnum, auto
2
1from .generated import data_pb2 as data_pb2 3from .generated import data_pb2 as data_pb2
2from typing import TYPE_CHECKING, NamedTuple 4from typing import TYPE_CHECKING, NamedTuple
3 5
4from .options import VictoryCondition 6from .options import VictoryCondition, ShuffleLetters
5 7
6if TYPE_CHECKING: 8if TYPE_CHECKING:
7 from . import Lingo2World 9 from . import Lingo2World
@@ -14,10 +16,6 @@ def calculate_letter_histogram(solution: str) -> dict[str, int]:
14 real_l = l.upper() 16 real_l = l.upper()
15 histogram[real_l] = min(histogram.get(real_l, 0) + 1, 2) 17 histogram[real_l] = min(histogram.get(real_l, 0) + 1, 2)
16 18
17 for free_letter in "HINT":
18 if histogram.get(free_letter, 0) == 1:
19 del histogram[free_letter]
20
21 return histogram 19 return histogram
22 20
23 21
@@ -27,6 +25,7 @@ class AccessRequirements:
27 rooms: set[str] 25 rooms: set[str]
28 symbols: set[str] 26 symbols: set[str]
29 letters: dict[str, int] 27 letters: dict[str, int]
28 cyans: bool
30 29
31 # This is an AND of ORs. 30 # This is an AND of ORs.
32 or_logic: list[list["AccessRequirements"]] 31 or_logic: list[list["AccessRequirements"]]
@@ -37,14 +36,9 @@ class AccessRequirements:
37 self.rooms = set() 36 self.rooms = set()
38 self.symbols = set() 37 self.symbols = set()
39 self.letters = dict() 38 self.letters = dict()
39 self.cyans = False
40 self.or_logic = list() 40 self.or_logic = list()
41 41
42 def add_solution(self, solution: str):
43 histogram = calculate_letter_histogram(solution)
44
45 for l, a in histogram.items():
46 self.letters[l] = max(self.letters.get(l, 0), histogram.get(l))
47
48 def merge(self, other: "AccessRequirements"): 42 def merge(self, other: "AccessRequirements"):
49 for item in other.items: 43 for item in other.items:
50 self.items.add(item) 44 self.items.add(item)
@@ -61,6 +55,8 @@ class AccessRequirements:
61 for letter, level in other.letters.items(): 55 for letter, level in other.letters.items():
62 self.letters[letter] = max(self.letters.get(letter, 0), level) 56 self.letters[letter] = max(self.letters.get(letter, 0), level)
63 57
58 self.cyans = self.cyans or other.cyans
59
64 for disjunction in other.or_logic: 60 for disjunction in other.or_logic:
65 self.or_logic.append(disjunction) 61 self.or_logic.append(disjunction)
66 62
@@ -76,6 +72,8 @@ class AccessRequirements:
76 parts.append(f"symbols={self.symbols}") 72 parts.append(f"symbols={self.symbols}")
77 if len(self.letters) > 0: 73 if len(self.letters) > 0:
78 parts.append(f"letters={self.letters}") 74 parts.append(f"letters={self.letters}")
75 if self.cyans:
76 parts.append(f"cyans=True")
79 if len(self.or_logic) > 0: 77 if len(self.or_logic) > 0:
80 parts.append(f"or_logic={self.or_logic}") 78 parts.append(f"or_logic={self.or_logic}")
81 return f"AccessRequirements({", ".join(parts)})" 79 return f"AccessRequirements({", ".join(parts)})"
@@ -86,6 +84,12 @@ class PlayerLocation(NamedTuple):
86 reqs: AccessRequirements 84 reqs: AccessRequirements
87 85
88 86
87class LetterBehavior(IntEnum):
88 VANILLA = auto()
89 ITEM = auto()
90 UNLOCKED = auto()
91
92
89class Lingo2PlayerLogic: 93class Lingo2PlayerLogic:
90 world: "Lingo2World" 94 world: "Lingo2World"
91 95
@@ -100,6 +104,8 @@ class Lingo2PlayerLogic:
100 104
101 real_items: list[str] 105 real_items: list[str]
102 106
107 double_letter_amount: dict[str, int]
108
103 def __init__(self, world: "Lingo2World"): 109 def __init__(self, world: "Lingo2World"):
104 self.world = world 110 self.world = world
105 self.locations_by_room = {} 111 self.locations_by_room = {}
@@ -109,6 +115,7 @@ class Lingo2PlayerLogic:
109 self.proxy_reqs = dict() 115 self.proxy_reqs = dict()
110 self.door_reqs = dict() 116 self.door_reqs = dict()
111 self.real_items = list() 117 self.real_items = list()
118 self.double_letter_amount = dict()
112 119
113 if self.world.options.shuffle_doors: 120 if self.world.options.shuffle_doors:
114 for progressive in world.static_logic.objects.progressives: 121 for progressive in world.static_logic.objects.progressives:
@@ -135,14 +142,20 @@ class Lingo2PlayerLogic:
135 for letter in world.static_logic.objects.letters: 142 for letter in world.static_logic.objects.letters:
136 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, 143 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id,
137 AccessRequirements())) 144 AccessRequirements()))
145 behavior = self.get_letter_behavior(letter.key, letter.level2)
146 if behavior == LetterBehavior.VANILLA:
147 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}"
148 event_name = f"{letter_name} (Collected)"
149 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()
138 150
139 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" 151 if letter.level2:
140 event_name = f"{letter_name} (Collected)" 152 event_name = f"{letter_name} (Double Collected)"
141 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper() 153 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()
154 elif behavior == LetterBehavior.ITEM:
155 self.real_items.append(letter.key.upper())
142 156
143 if letter.level2: 157 if behavior != LetterBehavior.UNLOCKED:
144 event_name = f"{letter_name} (Double Collected)" 158 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1
145 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()
146 159
147 for mastery in world.static_logic.objects.masteries: 160 for mastery in world.static_logic.objects.masteries:
148 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, 161 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id,
@@ -170,7 +183,9 @@ class Lingo2PlayerLogic:
170 for keyholder in world.static_logic.objects.keyholders: 183 for keyholder in world.static_logic.objects.keyholders:
171 if keyholder.HasField("key"): 184 if keyholder.HasField("key"):
172 reqs = AccessRequirements() 185 reqs = AccessRequirements()
173 reqs.letters[keyholder.key.upper()] = 1 186
187 if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED:
188 reqs.letters[keyholder.key.upper()] = 1
174 189
175 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id, 190 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id,
176 reqs)) 191 reqs))
@@ -194,25 +209,25 @@ class Lingo2PlayerLogic:
194 reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.room_id)) 209 reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.room_id))
195 210
196 if answer is not None: 211 if answer is not None:
197 reqs.add_solution(answer) 212 self.add_solution_reqs(reqs, answer)
198 elif len(panel.proxies) > 0: 213 elif len(panel.proxies) > 0:
199 possibilities = [] 214 possibilities = []
200 215
201 for proxy in panel.proxies: 216 for proxy in panel.proxies:
202 proxy_reqs = AccessRequirements() 217 proxy_reqs = AccessRequirements()
203 proxy_reqs.add_solution(proxy.answer) 218 self.add_solution_reqs(proxy_reqs, proxy.answer)
204 219
205 possibilities.append(proxy_reqs) 220 possibilities.append(proxy_reqs)
206 221
207 if not any(proxy.answer == panel.answer for proxy in panel.proxies): 222 if not any(proxy.answer == panel.answer for proxy in panel.proxies):
208 proxy_reqs = AccessRequirements() 223 proxy_reqs = AccessRequirements()
209 proxy_reqs.add_solution(panel.answer) 224 self.add_solution_reqs(proxy_reqs, panel.answer)
210 225
211 possibilities.append(proxy_reqs) 226 possibilities.append(proxy_reqs)
212 227
213 reqs.or_logic.append(possibilities) 228 reqs.or_logic.append(possibilities)
214 else: 229 else:
215 reqs.add_solution(panel.answer) 230 self.add_solution_reqs(reqs, panel.answer)
216 231
217 for symbol in panel.symbols: 232 for symbol in panel.symbols:
218 reqs.symbols.add(symbol) 233 reqs.symbols.add(symbol)
@@ -254,15 +269,20 @@ class Lingo2PlayerLogic:
254 if door.HasField("control_center_color"): 269 if door.HasField("control_center_color"):
255 # TODO: Logic for ensuring two CC states aren't needed at once. 270 # TODO: Logic for ensuring two CC states aren't needed at once.
256 reqs.rooms.add("Control Center - Main Area") 271 reqs.rooms.add("Control Center - Main Area")
257 reqs.add_solution(door.control_center_color) 272 self.add_solution_reqs(reqs, door.control_center_color)
258 273
259 if door.double_letters: 274 if door.double_letters:
260 # TODO: When letter shuffle is on, change this to require any double letter instead. 275 if self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla,
261 reqs.rooms.add("The Repetitive - Main Room") 276 ShuffleLetters.option_vanilla_cyan]:
277 reqs.rooms.add("The Repetitive - Main Room")
278 elif self.world.options.shuffle_letters in [ShuffleLetters.option_progressive,
279 ShuffleLetters.option_item_cyan]:
280 reqs.cyans = True
262 281
263 for keyholder_uses in door.keyholders: 282 for keyholder_uses in door.keyholders:
264 key_name = keyholder_uses.key.upper() 283 key_name = keyholder_uses.key.upper()
265 if key_name not in reqs.letters: 284 if (self.get_letter_behavior(keyholder_uses.key, False) != LetterBehavior.UNLOCKED
285 and key_name not in reqs.letters):
266 reqs.letters[key_name] = 1 286 reqs.letters[key_name] = 1
267 287
268 keyholder = self.world.static_logic.objects.keyholders[keyholder_uses.keyholder] 288 keyholder = self.world.static_logic.objects.keyholders[keyholder_uses.keyholder]
@@ -296,3 +316,40 @@ class Lingo2PlayerLogic:
296 return reqs 316 return reqs
297 else: 317 else:
298 return self.get_door_reqs(door_id) 318 return self.get_door_reqs(door_id)
319
320 def get_letter_behavior(self, letter: str, level2: bool) -> LetterBehavior:
321 if self.world.options.shuffle_letters == ShuffleLetters.option_unlocked:
322 return LetterBehavior.UNLOCKED
323
324 if self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla_cyan, ShuffleLetters.option_item_cyan]:
325 if level2:
326 if self.world.options.shuffle_letters == ShuffleLetters.option_vanilla_cyan:
327 return LetterBehavior.VANILLA
328 else:
329 return LetterBehavior.ITEM
330 else:
331 return LetterBehavior.UNLOCKED
332
333 if not level2 and letter in ["h", "i", "n", "t"]:
334 return LetterBehavior.UNLOCKED
335
336 if self.world.options.shuffle_letters == ShuffleLetters.option_progressive:
337 return LetterBehavior.ITEM
338
339 return LetterBehavior.VANILLA
340
341 def add_solution_reqs(self, reqs: AccessRequirements, solution: str):
342 histogram = calculate_letter_histogram(solution)
343
344 for l, a in histogram.items():
345 needed = min(a, 2)
346 level2 = (needed == 2)
347
348 if level2 and self.get_letter_behavior(l.lower(), True) == LetterBehavior.UNLOCKED:
349 needed = 1
350
351 if self.get_letter_behavior(l.lower(), False) == LetterBehavior.UNLOCKED:
352 needed = needed - 1
353
354 if needed > 0:
355 reqs.letters[l] = max(reqs.letters.get(l, 0), needed)