about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-09-09 16:44:09 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2025-09-09 16:44:09 -0400
commit8de745f4d3350ac848c9362a33e223c0ff94fdcf (patch)
tree102db82fd9c7822dd6c0d6e9a555181faf9ba00e
parent6dc4fae2ee9b7b9145a8938e95080dba448cf44a (diff)
downloadlingo2-archipelago-8de745f4d3350ac848c9362a33e223c0ff94fdcf.tar.gz
lingo2-archipelago-8de745f4d3350ac848c9362a33e223c0ff94fdcf.tar.bz2
lingo2-archipelago-8de745f4d3350ac848c9362a33e223c0ff94fdcf.zip
Added symbol shuffle
Also fixed unlocked letters + any double letter cyan doors, and tweaked
some logic related to important panels with symbols on them.
-rw-r--r--apworld/__init__.py1
-rw-r--r--apworld/items.py24
-rw-r--r--apworld/options.py9
-rw-r--r--apworld/player_logic.py54
-rw-r--r--apworld/rules.py2
-rw-r--r--apworld/static_logic.py4
-rw-r--r--data/connections.txtpb20
-rw-r--r--data/ids.yaml19
-rw-r--r--data/maps/the_entry/rooms/Starting Room.txtpb4
-rw-r--r--data/maps/the_great/doors.txtpb5
-rw-r--r--data/maps/the_great/rooms/West Side.txtpb1
-rw-r--r--data/metadata.txtpb22
-rw-r--r--proto/human.proto4
-rw-r--r--tools/assign_ids/main.cpp18
14 files changed, 167 insertions, 20 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index 6eeee74..fc263c0 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -82,6 +82,7 @@ class Lingo2World(World):
82 "shuffle_control_center_colors", 82 "shuffle_control_center_colors",
83 "shuffle_doors", 83 "shuffle_doors",
84 "shuffle_letters", 84 "shuffle_letters",
85 "shuffle_symbols",
85 "victory_condition", 86 "victory_condition",
86 ] 87 ]
87 88
diff --git a/apworld/items.py b/apworld/items.py index 971a709..32568a3 100644 --- a/apworld/items.py +++ b/apworld/items.py
@@ -1,5 +1,29 @@
1from .generated import data_pb2 as data_pb2
1from BaseClasses import Item 2from BaseClasses import Item
2 3
3 4
4class Lingo2Item(Item): 5class Lingo2Item(Item):
5 game: str = "Lingo 2" 6 game: str = "Lingo 2"
7
8
9SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = {
10 data_pb2.PuzzleSymbol.SUN: "Sun Symbol",
11 data_pb2.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
12 data_pb2.PuzzleSymbol.ZERO: "Zero Symbol",
13 data_pb2.PuzzleSymbol.EXAMPLE: "Example Symbol",
14 data_pb2.PuzzleSymbol.BOXES: "Boxes Symbol",
15 data_pb2.PuzzleSymbol.PLANET: "Planet Symbol",
16 data_pb2.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
17 data_pb2.PuzzleSymbol.CROSS: "Cross Symbol",
18 data_pb2.PuzzleSymbol.SWEET: "Sweet Symbol",
19 data_pb2.PuzzleSymbol.GENDER: "Gender Symbol",
20 data_pb2.PuzzleSymbol.AGE: "Age Symbol",
21 data_pb2.PuzzleSymbol.SOUND: "Sound Symbol",
22 data_pb2.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
23 data_pb2.PuzzleSymbol.JOB: "Job Symbol",
24 data_pb2.PuzzleSymbol.STARS: "Stars Symbol",
25 data_pb2.PuzzleSymbol.NULL: "Null Symbol",
26 data_pb2.PuzzleSymbol.EVAL: "Eval Symbol",
27 data_pb2.PuzzleSymbol.LINGO: "Lingo Symbol",
28 data_pb2.PuzzleSymbol.QUESTION: "Question Symbol",
29}
diff --git a/apworld/options.py b/apworld/options.py index 2197b0f..f72e826 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -39,6 +39,14 @@ class ShuffleLetters(Choice):
39 option_item_cyan = 4 39 option_item_cyan = 4
40 40
41 41
42class ShuffleSymbols(Toggle):
43 """
44 If enabled, 19 items will be added to the pool, representing the different symbols that can appear on a panel.
45 Players will be prevented from solving puzzles with symbols on them until all of the required symbols are unlocked.
46 """
47 display_name = "Shuffle Symbols"
48
49
42class KeyholderSanity(Toggle): 50class KeyholderSanity(Toggle):
43 """ 51 """
44 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder. 52 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder.
@@ -102,6 +110,7 @@ class Lingo2Options(PerGameCommonOptions):
102 shuffle_doors: ShuffleDoors 110 shuffle_doors: ShuffleDoors
103 shuffle_control_center_colors: ShuffleControlCenterColors 111 shuffle_control_center_colors: ShuffleControlCenterColors
104 shuffle_letters: ShuffleLetters 112 shuffle_letters: ShuffleLetters
113 shuffle_symbols: ShuffleSymbols
105 keyholder_sanity: KeyholderSanity 114 keyholder_sanity: KeyholderSanity
106 cyan_door_behavior: CyanDoorBehavior 115 cyan_door_behavior: CyanDoorBehavior
107 daedalus_roof_access: DaedalusRoofAccess 116 daedalus_roof_access: DaedalusRoofAccess
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index dbd340c..42b36e6 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -1,6 +1,7 @@
1from enum import IntEnum, auto 1from enum import IntEnum, auto
2 2
3from .generated import data_pb2 as data_pb2 3from .generated import data_pb2 as data_pb2
4from .items import SYMBOL_ITEMS
4from typing import TYPE_CHECKING, NamedTuple 5from typing import TYPE_CHECKING, NamedTuple
5 6
6from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior 7from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior
@@ -23,7 +24,6 @@ class AccessRequirements:
23 items: set[str] 24 items: set[str]
24 progressives: dict[str, int] 25 progressives: dict[str, int]
25 rooms: set[str] 26 rooms: set[str]
26 symbols: set[str]
27 letters: dict[str, int] 27 letters: dict[str, int]
28 cyans: bool 28 cyans: bool
29 29
@@ -34,7 +34,6 @@ class AccessRequirements:
34 self.items = set() 34 self.items = set()
35 self.progressives = dict() 35 self.progressives = dict()
36 self.rooms = set() 36 self.rooms = set()
37 self.symbols = set()
38 self.letters = dict() 37 self.letters = dict()
39 self.cyans = False 38 self.cyans = False
40 self.or_logic = list() 39 self.or_logic = list()
@@ -49,9 +48,6 @@ class AccessRequirements:
49 for room in other.rooms: 48 for room in other.rooms:
50 self.rooms.add(room) 49 self.rooms.add(room)
51 50
52 for symbol in other.symbols:
53 self.symbols.add(symbol)
54
55 for letter, level in other.letters.items(): 51 for letter, level in other.letters.items():
56 self.letters[letter] = max(self.letters.get(letter, 0), level) 52 self.letters[letter] = max(self.letters.get(letter, 0), level)
57 53
@@ -60,6 +56,10 @@ class AccessRequirements:
60 for disjunction in other.or_logic: 56 for disjunction in other.or_logic:
61 self.or_logic.append(disjunction) 57 self.or_logic.append(disjunction)
62 58
59 def is_empty(self) -> bool:
60 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0
61 and not self.cyans and len(self.or_logic) == 0)
62
63 def __repr__(self): 63 def __repr__(self):
64 parts = [] 64 parts = []
65 if len(self.items) > 0: 65 if len(self.items) > 0:
@@ -68,8 +68,6 @@ class AccessRequirements:
68 parts.append(f"progressives={self.progressives}") 68 parts.append(f"progressives={self.progressives}")
69 if len(self.rooms) > 0: 69 if len(self.rooms) > 0:
70 parts.append(f"rooms={self.rooms}") 70 parts.append(f"rooms={self.rooms}")
71 if len(self.symbols) > 0:
72 parts.append(f"symbols={self.symbols}")
73 if len(self.letters) > 0: 71 if len(self.letters) > 0:
74 parts.append(f"letters={self.letters}") 72 parts.append(f"letters={self.letters}")
75 if self.cyans: 73 if self.cyans:
@@ -231,6 +229,10 @@ class Lingo2PlayerLogic:
231 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id, 229 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id,
232 reqs)) 230 reqs))
233 231
232 if self.world.options.shuffle_symbols:
233 for symbol_name in SYMBOL_ITEMS.values():
234 self.real_items.append(symbol_name)
235
234 def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: 236 def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements:
235 if answer is None: 237 if answer is None:
236 if panel_id not in self.panel_reqs: 238 if panel_id not in self.panel_reqs:
@@ -253,25 +255,35 @@ class Lingo2PlayerLogic:
253 self.add_solution_reqs(reqs, answer) 255 self.add_solution_reqs(reqs, answer)
254 elif len(panel.proxies) > 0: 256 elif len(panel.proxies) > 0:
255 possibilities = [] 257 possibilities = []
258 already_filled = False
256 259
257 for proxy in panel.proxies: 260 for proxy in panel.proxies:
258 proxy_reqs = AccessRequirements() 261 proxy_reqs = AccessRequirements()
259 self.add_solution_reqs(proxy_reqs, proxy.answer) 262 self.add_solution_reqs(proxy_reqs, proxy.answer)
260 263
261 possibilities.append(proxy_reqs) 264 if not proxy_reqs.is_empty():
265 possibilities.append(proxy_reqs)
266 else:
267 already_filled = True
268 break
262 269
263 if not any(proxy.answer == panel.answer for proxy in panel.proxies): 270 if not already_filled and not any(proxy.answer == panel.answer for proxy in panel.proxies):
264 proxy_reqs = AccessRequirements() 271 proxy_reqs = AccessRequirements()
265 self.add_solution_reqs(proxy_reqs, panel.answer) 272 self.add_solution_reqs(proxy_reqs, panel.answer)
266 273
267 possibilities.append(proxy_reqs) 274 if not proxy_reqs.is_empty():
275 possibilities.append(proxy_reqs)
276 else:
277 already_filled = True
268 278
269 reqs.or_logic.append(possibilities) 279 if not already_filled:
280 reqs.or_logic.append(possibilities)
270 else: 281 else:
271 self.add_solution_reqs(reqs, panel.answer) 282 self.add_solution_reqs(reqs, panel.answer)
272 283
273 for symbol in panel.symbols: 284 if self.world.options.shuffle_symbols:
274 reqs.symbols.add(symbol) 285 for symbol in panel.symbols:
286 reqs.items.add(SYMBOL_ITEMS.get(symbol))
275 287
276 if panel.HasField("required_door"): 288 if panel.HasField("required_door"):
277 door_reqs = self.get_door_open_reqs(panel.required_door) 289 door_reqs = self.get_door_open_reqs(panel.required_door)
@@ -300,9 +312,16 @@ class Lingo2PlayerLogic:
300 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) 312 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
301 reqs.merge(panel_reqs) 313 reqs.merge(panel_reqs)
302 elif door.complete_at == 1: 314 elif door.complete_at == 1:
303 reqs.or_logic.append([self.get_panel_reqs(proxy.panel, 315 disjunction = []
304 proxy.answer if proxy.HasField("answer") else None) 316 for proxy in door.panels:
305 for proxy in door.panels]) 317 proxy_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
318 if proxy_reqs.is_empty():
319 disjunction.clear()
320 break
321 else:
322 disjunction.append(proxy_reqs)
323 if len(disjunction) > 0:
324 reqs.or_logic.append(disjunction)
306 else: 325 else:
307 # TODO: Handle complete_at > 1 326 # TODO: Handle complete_at > 1
308 pass 327 pass
@@ -316,7 +335,8 @@ class Lingo2PlayerLogic:
316 if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2: 335 if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2:
317 reqs.rooms.add("The Repetitive - Main Room") 336 reqs.rooms.add("The Repetitive - Main Room")
318 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter: 337 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter:
319 reqs.cyans = True 338 if self.world.options.shuffle_letters != ShuffleLetters.option_unlocked:
339 reqs.cyans = True
320 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item: 340 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
321 # There shouldn't be any locations that are cyan doors. 341 # There shouldn't be any locations that are cyan doors.
322 pass 342 pass
diff --git a/apworld/rules.py b/apworld/rules.py index 56486fa..0bff056 100644 --- a/apworld/rules.py +++ b/apworld/rules.py
@@ -18,8 +18,6 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
18 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): 18 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms):
19 return False 19 return False
20 20
21 # TODO: symbols
22
23 for letter_key, letter_level in reqs.letters.items(): 21 for letter_key, letter_level in reqs.letters.items():
24 if not state.has(letter_key, world.player, letter_level): 22 if not state.has(letter_key, world.player, letter_level):
25 return False 23 return False
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 0cc7e55..c112d8e 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -1,4 +1,5 @@
1from .generated import data_pb2 as data_pb2 1from .generated import data_pb2 as data_pb2
2from .items import SYMBOL_ITEMS
2import pkgutil 3import pkgutil
3 4
4class Lingo2StaticLogic: 5class Lingo2StaticLogic:
@@ -64,6 +65,9 @@ class Lingo2StaticLogic:
64 65
65 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" 66 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done"
66 67
68 for symbol_name in SYMBOL_ITEMS.values():
69 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
70
67 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} 71 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()}
68 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} 72 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()}
69 73
diff --git a/data/connections.txtpb b/data/connections.txtpb index a79778f..d718c96 100644 --- a/data/connections.txtpb +++ b/data/connections.txtpb
@@ -841,6 +841,8 @@ connections {
841 } 841 }
842 oneway: true 842 oneway: true
843} 843}
844# Two one-way connections because the CLUE panel only needs to be solved to
845# go from The Great to The Partial.
844connections { 846connections {
845 from { 847 from {
846 port { 848 port {
@@ -856,6 +858,24 @@ connections {
856 name: "GREAT" 858 name: "GREAT"
857 } 859 }
858 } 860 }
861 oneway: true
862}
863connections {
864 from {
865 port {
866 map: "the_partial"
867 room: "Obverse Side"
868 name: "GREAT"
869 }
870 }
871 to {
872 port {
873 map: "the_great"
874 room: "West Side"
875 name: "PARTIAL"
876 }
877 }
878 oneway: true
859} 879}
860connections { 880connections {
861 from { 881 from {
diff --git a/data/ids.yaml b/data/ids.yaml index e2ec985..30a400b 100644 --- a/data/ids.yaml +++ b/data/ids.yaml
@@ -3836,6 +3836,25 @@ endings:
3836 YELLOW: 1206 3836 YELLOW: 1206
3837special: 3837special:
3838 A Job Well Done: 1160 3838 A Job Well Done: 1160
3839 Age Symbol: 2791
3840 Anagram Symbol: 2792
3841 Boxes Symbol: 2793
3842 Cross Symbol: 2794
3843 Eval Symbol: 2795
3844 Example Symbol: 2796
3845 Gender Symbol: 2797
3846 Job Symbol: 2798
3847 Lingo Symbol: 2799
3848 Null Symbol: 2800
3849 Planet Symbol: 2801
3850 Pyramid Symbol: 2802
3851 Question Symbol: 2803
3852 Sound Symbol: 2804
3853 Sparkles Symbol: 2805
3854 Stars Symbol: 2806
3855 Sun Symbol: 2807
3856 Sweet Symbol: 2808
3857 Zero Symbol: 2809
3839progressives: 3858progressives:
3840 Progressive Gold Ending: 2753 3859 Progressive Gold Ending: 2753
3841door_groups: 3860door_groups:
diff --git a/data/maps/the_entry/rooms/Starting Room.txtpb b/data/maps/the_entry/rooms/Starting Room.txtpb index bc77e6d..8e8373b 100644 --- a/data/maps/the_entry/rooms/Starting Room.txtpb +++ b/data/maps/the_entry/rooms/Starting Room.txtpb
@@ -24,7 +24,9 @@ panels {
24 path: "Panels/Entry/front_1" 24 path: "Panels/Entry/front_1"
25 clue: "eye" 25 clue: "eye"
26 answer: "i" 26 answer: "i"
27 symbols: ZERO 27 #symbols: ZERO
28 # This panel blocks getting N1 and T1. We will mod it to be I/I with no symbol
29 # when symbol shuffle is on.
28} 30}
29panels { 31panels {
30 name: "HINT" 32 name: "HINT"
diff --git a/data/maps/the_great/doors.txtpb b/data/maps/the_great/doors.txtpb index f0f2fde..5d0e90d 100644 --- a/data/maps/the_great/doors.txtpb +++ b/data/maps/the_great/doors.txtpb
@@ -508,3 +508,8 @@ doors {
508 receivers: "Panels/General/entry_7/teleportListener" 508 receivers: "Panels/General/entry_7/teleportListener"
509 double_letters: true 509 double_letters: true
510} 510}
511doors {
512 name: "Partial Entrance"
513 type: EVENT
514 panels { room: "West Side" name: "CLUE" }
515}
diff --git a/data/maps/the_great/rooms/West Side.txtpb b/data/maps/the_great/rooms/West Side.txtpb index daf1718..8279e16 100644 --- a/data/maps/the_great/rooms/West Side.txtpb +++ b/data/maps/the_great/rooms/West Side.txtpb
@@ -76,4 +76,5 @@ ports {
76 path: "Meshes/Blocks/Warps/worldport7" 76 path: "Meshes/Blocks/Warps/worldport7"
77 orientation: "east" 77 orientation: "east"
78 # ER with this is weird; make sure to place on the surface 78 # ER with this is weird; make sure to place on the surface
79 required_door { name: "Partial Entrance" }
79} 80}
diff --git a/data/metadata.txtpb b/data/metadata.txtpb new file mode 100644 index 0000000..ef66622 --- /dev/null +++ b/data/metadata.txtpb
@@ -0,0 +1,22 @@
1# Filler item.
2special_names: "A Job Well Done"
3# Symbol items.
4special_names: "Age Symbol"
5special_names: "Anagram Symbol"
6special_names: "Boxes Symbol"
7special_names: "Cross Symbol"
8special_names: "Eval Symbol"
9special_names: "Example Symbol"
10special_names: "Gender Symbol"
11special_names: "Job Symbol"
12special_names: "Lingo Symbol"
13special_names: "Null Symbol"
14special_names: "Planet Symbol"
15special_names: "Pyramid Symbol"
16special_names: "Question Symbol"
17special_names: "Sound Symbol"
18special_names: "Sparkles Symbol"
19special_names: "Stars Symbol"
20special_names: "Sun Symbol"
21special_names: "Sweet Symbol"
22special_names: "Zero Symbol"
diff --git a/proto/human.proto b/proto/human.proto index d48f687..1c5b463 100644 --- a/proto/human.proto +++ b/proto/human.proto
@@ -212,6 +212,10 @@ message HumanDoorGroups {
212 repeated HumanDoorGroup door_groups = 1; 212 repeated HumanDoorGroup door_groups = 1;
213} 213}
214 214
215message HumanGlobalMetadata {
216 repeated string special_names = 1;
217}
218
215message IdMappings { 219message IdMappings {
216 message RoomIds { 220 message RoomIds {
217 map<string, uint64> panels = 1; 221 map<string, uint64> panels = 1;
diff --git a/tools/assign_ids/main.cpp b/tools/assign_ids/main.cpp index ee55338..3e16f78 100644 --- a/tools/assign_ids/main.cpp +++ b/tools/assign_ids/main.cpp
@@ -44,6 +44,7 @@ class AssignIds {
44 ProcessSpecialIds(); 44 ProcessSpecialIds();
45 ProcessProgressivesFile(datadir_path / "progressives.txtpb"); 45 ProcessProgressivesFile(datadir_path / "progressives.txtpb");
46 ProcessDoorGroupsFile(datadir_path / "door_groups.txtpb"); 46 ProcessDoorGroupsFile(datadir_path / "door_groups.txtpb");
47 ProcessGlobalMetadataFile(datadir_path / "metadata.txtpb");
47 48
48 WriteIds(ids_path); 49 WriteIds(ids_path);
49 50
@@ -288,6 +289,23 @@ class AssignIds {
288 } 289 }
289 } 290 }
290 291
292 void ProcessGlobalMetadataFile(std::filesystem::path path) {
293 if (!std::filesystem::exists(path)) {
294 return;
295 }
296
297 auto h_metadata = ReadMessageFromFile<HumanGlobalMetadata>(path.string());
298 auto& specials = *output_.mutable_special();
299
300 for (const std::string& h_special : h_metadata.special_names()) {
301 if (!id_mappings_.special().contains(h_special)) {
302 specials[h_special] = next_id_++;
303 } else {
304 specials[h_special] = id_mappings_.special().at(h_special);
305 }
306 }
307 }
308
291 private: 309 private:
292 void UpdateNextId(const google::protobuf::Map<std::string, uint64_t>& ids) { 310 void UpdateNextId(const google::protobuf::Map<std::string, uint64_t>& ids) {
293 for (const auto& [_, id] : ids) { 311 for (const auto& [_, id] : ids) {