about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
Diffstat (limited to 'apworld')
-rw-r--r--apworld/README.md48
-rw-r--r--apworld/__init__.py18
-rw-r--r--apworld/docs/en_Lingo_2.md4
-rw-r--r--apworld/items.py24
-rw-r--r--apworld/options.py40
-rw-r--r--apworld/player_logic.py122
-rw-r--r--apworld/requirements.txt2
-rw-r--r--apworld/rules.py2
-rw-r--r--apworld/static_logic.py17
9 files changed, 245 insertions, 32 deletions
diff --git a/apworld/README.md b/apworld/README.md new file mode 100644 index 0000000..13374b2 --- /dev/null +++ b/apworld/README.md
@@ -0,0 +1,48 @@
1# Lingo 2 Apworld
2
3The Lingo 2 Apworld allows you to generate Archipelago Multiworlds containing
4Lingo 2.
5
6## Installation
7
81. Download the Lingo 2 Apworld from
9 [the releases page](https://code.fourisland.com/lingo2-archipelago/about/apworld/CHANGELOG.md).
102. If you do not already have it, download and install the
11 [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/).
123. Double click on `lingo2.apworld` to install it, or copy it manually to the
13 `custom_worlds` folder of your Archipelago installation.
14
15## Running from source
16
17The apworld is mostly written in Python, which does not need to be compiled.
18However, there are two files that need to be generated before the apworld can be
19used.
20
21The first file is `data.binpb`, the datafile containing the randomizer logic.
22You can read about how to generate it on
23[its own README page](https://code.fourisland.com/lingo2-archipelago/about/data/README.md).
24Once you have it, put it in a subfolder of `apworld` called `generated`.
25
26The second generated file is `data_pb2.py`. This file allows Archipelago to read
27the datafile. We use `protoc`, the Protocol Buffer compiler, to generate it. As
28of 0.6.3, Archipelago has protobuf 3.20.3 packaged with it, which means we need
29to compile our proto file with a similar version.
30
31If you followed the steps to generate `data.binpb` and compiled the `datapacker`
32tool yourself, you will already have protobuf version 3.21.12 installed through
33vcpkg. You can then run a command similar to this in order to generate the
34python file.
35
36```shell
37.\out\build\x64-Debug\vcpkg_installed\x64-windows\tools\protobuf\protoc.exe -Iproto\ ^
38 --python_out=apworld\generated\ .\proto\data.proto
39```
40
41The exact path to `protoc.exe` is going to depend on where vcpkg installed its
42packages. The above location is where Visual Studio will probably put it.
43
44After generating those two files, the apworld should be functional. You can copy
45it into an Archipelago source tree (rename the folder `apworld` to `lingo2` if
46you do so) if you want to edit/debug the code. Otherwise, you can zip up the
47folder and rename it to `lingo2.apworld` in order to package it for
48distribution.
diff --git a/apworld/__init__.py b/apworld/__init__.py index 8e3066d..8051e0f 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -1,7 +1,7 @@
1""" 1"""
2Archipelago init file for Lingo 2 2Archipelago init file for Lingo 2
3""" 3"""
4from BaseClasses import ItemClassification, Item 4from BaseClasses import ItemClassification, Item, Tutorial
5from worlds.AutoWorld import WebWorld, World 5from worlds.AutoWorld import WebWorld, World
6from .items import Lingo2Item 6from .items import Lingo2Item
7from .options import Lingo2Options 7from .options import Lingo2Options
@@ -9,10 +9,20 @@ from .player_logic import Lingo2PlayerLogic
9from .regions import create_regions 9from .regions import create_regions
10from .static_logic import Lingo2StaticLogic 10from .static_logic import Lingo2StaticLogic
11 11
12MAJOR_VERSION = 1
13MINOR_VERSION = 0
12 14
13class Lingo2WebWorld(WebWorld): 15class Lingo2WebWorld(WebWorld):
14 rich_text_options_doc = True 16 rich_text_options_doc = True
15 theme = "grass" 17 theme = "grass"
18 tutorials = [Tutorial(
19 "Multiworld Setup Guide",
20 "A guide to playing Lingo 2 with Archipelago.",
21 "English",
22 "en_Lingo_2.md",
23 "setup/en",
24 ["hatkirby"]
25 )]
16 26
17 27
18class Lingo2World(World): 28class Lingo2World(World):
@@ -32,6 +42,8 @@ class Lingo2World(World):
32 static_logic = Lingo2StaticLogic() 42 static_logic = Lingo2StaticLogic()
33 item_name_to_id = static_logic.item_name_to_id 43 item_name_to_id = static_logic.item_name_to_id
34 location_name_to_id = static_logic.location_name_to_id 44 location_name_to_id = static_logic.location_name_to_id
45 item_name_groups = static_logic.item_name_groups
46 location_name_groups = static_logic.location_name_groups
35 47
36 player_logic: Lingo2PlayerLogic 48 player_logic: Lingo2PlayerLogic
37 49
@@ -66,15 +78,19 @@ class Lingo2World(World):
66 78
67 def fill_slot_data(self): 79 def fill_slot_data(self):
68 slot_options = [ 80 slot_options = [
81 "cyan_door_behavior",
69 "daedalus_roof_access", 82 "daedalus_roof_access",
70 "keyholder_sanity", 83 "keyholder_sanity",
84 "shuffle_control_center_colors",
71 "shuffle_doors", 85 "shuffle_doors",
72 "shuffle_letters", 86 "shuffle_letters",
87 "shuffle_symbols",
73 "victory_condition", 88 "victory_condition",
74 ] 89 ]
75 90
76 slot_data = { 91 slot_data = {
77 **self.options.as_dict(*slot_options), 92 **self.options.as_dict(*slot_options),
93 "version": [MAJOR_VERSION, MINOR_VERSION],
78 } 94 }
79 95
80 return slot_data 96 return slot_data
diff --git a/apworld/docs/en_Lingo_2.md b/apworld/docs/en_Lingo_2.md new file mode 100644 index 0000000..977795a --- /dev/null +++ b/apworld/docs/en_Lingo_2.md
@@ -0,0 +1,4 @@
1# Lingo 2
2
3See [the project README](https://code.fourisland.com/lingo2-archipelago/about/)
4for installation instructions and frequently asked questions. \ No newline at end of file
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 f7dc5bd..f72e826 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -8,6 +8,14 @@ class ShuffleDoors(Toggle):
8 display_name = "Shuffle Doors" 8 display_name = "Shuffle Doors"
9 9
10 10
11class ShuffleControlCenterColors(Toggle):
12 """
13 Some doors open after solving the COLOR panel in the Control Center. If this option is enabled, these doors will
14 instead open upon receiving an item.
15 """
16 display_name = "Shuffle Control Center Colors"
17
18
11class ShuffleLetters(Choice): 19class ShuffleLetters(Choice):
12 """ 20 """
13 Controls how letter unlocks are handled. Note that H1, I1, N1, and T1 will always be present at their vanilla 21 Controls how letter unlocks are handled. Note that H1, I1, N1, and T1 will always be present at their vanilla
@@ -31,6 +39,14 @@ class ShuffleLetters(Choice):
31 option_item_cyan = 4 39 option_item_cyan = 4
32 40
33 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
34class KeyholderSanity(Toggle): 50class KeyholderSanity(Toggle):
35 """ 51 """
36 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.
@@ -40,6 +56,27 @@ class KeyholderSanity(Toggle):
40 display_name = "Keyholder Sanity" 56 display_name = "Keyholder Sanity"
41 57
42 58
59class CyanDoorBehavior(Choice):
60 """
61 Cyan-colored doors usually only open upon unlocking double letters. Some panels also only appear upon unlocking
62 double letters. This option determines how these unlocks should behave.
63
64 - **Collect H2**: In the base game, H2 is the first double letter you are intended to collect, so cyan doors only
65 open when you collect the H2 pickup in The Repetitive. Collecting the actual pickup is still required even with
66 remote letter shuffle enabled.
67 - **Any Double Letter**: Cyan doors will open when you have unlocked any cyan letter on your keyboard. In letter
68 shuffle, this means receiving a cyan letter, not picking up a cyan letter collectable.
69 - **Item**: Cyan doors will be grouped together in a single item.
70
71 Note that some cyan doors are impacted by door shuffle (e.g. the entrance to The Tower). When door shuffle is
72 enabled, these doors won't be affected by the value of this option.
73 """
74 display_name = "Cyan Door Behavior"
75 option_collect_h2 = 0
76 option_any_double_letter = 1
77 option_item = 2
78
79
43class DaedalusRoofAccess(Toggle): 80class DaedalusRoofAccess(Toggle):
44 """ 81 """
45 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus 82 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus
@@ -71,7 +108,10 @@ class VictoryCondition(Choice):
71@dataclass 108@dataclass
72class Lingo2Options(PerGameCommonOptions): 109class Lingo2Options(PerGameCommonOptions):
73 shuffle_doors: ShuffleDoors 110 shuffle_doors: ShuffleDoors
111 shuffle_control_center_colors: ShuffleControlCenterColors
74 shuffle_letters: ShuffleLetters 112 shuffle_letters: ShuffleLetters
113 shuffle_symbols: ShuffleSymbols
75 keyholder_sanity: KeyholderSanity 114 keyholder_sanity: KeyholderSanity
115 cyan_door_behavior: CyanDoorBehavior
76 daedalus_roof_access: DaedalusRoofAccess 116 daedalus_roof_access: DaedalusRoofAccess
77 victory_condition: VictoryCondition 117 victory_condition: VictoryCondition
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 5cb9011..42b36e6 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -1,9 +1,10 @@
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 7from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior
7 8
8if TYPE_CHECKING: 9if TYPE_CHECKING:
9 from . import Lingo2World 10 from . import Lingo2World
@@ -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:
@@ -123,16 +121,57 @@ class Lingo2PlayerLogic:
123 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) 121 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1)
124 self.real_items.append(progressive.name) 122 self.real_items.append(progressive.name)
125 123
124 for door_group in world.static_logic.objects.door_groups:
125 if door_group.type == data_pb2.DoorGroupType.CONNECTOR:
126 if not self.world.options.shuffle_doors:
127 continue
128 elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR:
129 if not self.world.options.shuffle_control_center_colors:
130 continue
131 elif door_group.type == data_pb2.DoorGroupType.SHUFFLE_GROUP:
132 if not self.world.options.shuffle_doors:
133 continue
134 else:
135 continue
136
137 for door in door_group.doors:
138 self.item_by_door[door] = (door_group.name, 1)
139
140 self.real_items.append(door_group.name)
141
126 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled 142 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled
127 # before we calculate any access requirements. 143 # before we calculate any access requirements.
128 for door in world.static_logic.objects.doors: 144 for door in world.static_logic.objects.doors:
129 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: 145 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
130 if door.id in self.item_by_door: 146 continue
147
148 if door.id in self.item_by_door:
149 continue
150
151 if (door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and
152 not self.world.options.shuffle_doors):
153 continue
154
155 if (door.type == data_pb2.DoorType.CONTROL_CENTER_COLOR and
156 not self.world.options.shuffle_control_center_colors):
157 continue
158
159 door_item_name = self.world.static_logic.get_door_item_name(door)
160 self.item_by_door[door.id] = (door_item_name, 1)
161 self.real_items.append(door_item_name)
162
163 # We handle cyan_door_behavior = Item after door shuffle, because cyan doors that are impacted by door shuffle
164 # should be exempt from cyan_door_behavior.
165 if world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
166 for door_group in world.static_logic.objects.door_groups:
167 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS:
131 continue 168 continue
132 169
133 door_item_name = self.world.static_logic.get_door_item_name(door) 170 for door in door_group.doors:
134 self.item_by_door[door.id] = (door_item_name, 1) 171 if not door in self.item_by_door:
135 self.real_items.append(door_item_name) 172 self.item_by_door[door] = (door_group.name, 1)
173
174 self.real_items.append(door_group.name)
136 175
137 for door in world.static_logic.objects.doors: 176 for door in world.static_logic.objects.doors:
138 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 177 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
@@ -190,6 +229,10 @@ class Lingo2PlayerLogic:
190 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,
191 reqs)) 230 reqs))
192 231
232 if self.world.options.shuffle_symbols:
233 for symbol_name in SYMBOL_ITEMS.values():
234 self.real_items.append(symbol_name)
235
193 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:
194 if answer is None: 237 if answer is None:
195 if panel_id not in self.panel_reqs: 238 if panel_id not in self.panel_reqs:
@@ -212,25 +255,35 @@ class Lingo2PlayerLogic:
212 self.add_solution_reqs(reqs, answer) 255 self.add_solution_reqs(reqs, answer)
213 elif len(panel.proxies) > 0: 256 elif len(panel.proxies) > 0:
214 possibilities = [] 257 possibilities = []
258 already_filled = False
215 259
216 for proxy in panel.proxies: 260 for proxy in panel.proxies:
217 proxy_reqs = AccessRequirements() 261 proxy_reqs = AccessRequirements()
218 self.add_solution_reqs(proxy_reqs, proxy.answer) 262 self.add_solution_reqs(proxy_reqs, proxy.answer)
219 263
220 possibilities.append(proxy_reqs) 264 if not proxy_reqs.is_empty():
265 possibilities.append(proxy_reqs)
266 else:
267 already_filled = True
268 break
221 269
222 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):
223 proxy_reqs = AccessRequirements() 271 proxy_reqs = AccessRequirements()
224 self.add_solution_reqs(proxy_reqs, panel.answer) 272 self.add_solution_reqs(proxy_reqs, panel.answer)
225 273
226 possibilities.append(proxy_reqs) 274 if not proxy_reqs.is_empty():
275 possibilities.append(proxy_reqs)
276 else:
277 already_filled = True
227 278
228 reqs.or_logic.append(possibilities) 279 if not already_filled:
280 reqs.or_logic.append(possibilities)
229 else: 281 else:
230 self.add_solution_reqs(reqs, panel.answer) 282 self.add_solution_reqs(reqs, panel.answer)
231 283
232 for symbol in panel.symbols: 284 if self.world.options.shuffle_symbols:
233 reqs.symbols.add(symbol) 285 for symbol in panel.symbols:
286 reqs.items.add(SYMBOL_ITEMS.get(symbol))
234 287
235 if panel.HasField("required_door"): 288 if panel.HasField("required_door"):
236 door_reqs = self.get_door_open_reqs(panel.required_door) 289 door_reqs = self.get_door_open_reqs(panel.required_door)
@@ -259,9 +312,16 @@ class Lingo2PlayerLogic:
259 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)
260 reqs.merge(panel_reqs) 313 reqs.merge(panel_reqs)
261 elif door.complete_at == 1: 314 elif door.complete_at == 1:
262 reqs.or_logic.append([self.get_panel_reqs(proxy.panel, 315 disjunction = []
263 proxy.answer if proxy.HasField("answer") else None) 316 for proxy in door.panels:
264 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)
265 else: 325 else:
266 # TODO: Handle complete_at > 1 326 # TODO: Handle complete_at > 1
267 pass 327 pass
@@ -272,12 +332,14 @@ class Lingo2PlayerLogic:
272 self.add_solution_reqs(reqs, door.control_center_color) 332 self.add_solution_reqs(reqs, door.control_center_color)
273 333
274 if door.double_letters: 334 if door.double_letters:
275 if self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla, 335 if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2:
276 ShuffleLetters.option_vanilla_cyan]:
277 reqs.rooms.add("The Repetitive - Main Room") 336 reqs.rooms.add("The Repetitive - Main Room")
278 elif self.world.options.shuffle_letters in [ShuffleLetters.option_progressive, 337 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter:
279 ShuffleLetters.option_item_cyan]: 338 if self.world.options.shuffle_letters != ShuffleLetters.option_unlocked:
280 reqs.cyans = True 339 reqs.cyans = True
340 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
341 # There shouldn't be any locations that are cyan doors.
342 pass
281 343
282 for keyholder_uses in door.keyholders: 344 for keyholder_uses in door.keyholders:
283 key_name = keyholder_uses.key.upper() 345 key_name = keyholder_uses.key.upper()
@@ -293,7 +355,11 @@ class Lingo2PlayerLogic:
293 355
294 for ending_id in door.endings: 356 for ending_id in door.endings:
295 ending = self.world.static_logic.objects.endings[ending_id] 357 ending = self.world.static_logic.objects.endings[ending_id]
296 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)") 358
359 if self.world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name:
360 reqs.items.add("Victory")
361 else:
362 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)")
297 363
298 for sub_door_id in door.doors: 364 for sub_door_id in door.doors:
299 sub_reqs = self.get_door_open_reqs(sub_door_id) 365 sub_reqs = self.get_door_open_reqs(sub_door_id)
diff --git a/apworld/requirements.txt b/apworld/requirements.txt index b701d11..dbc395b 100644 --- a/apworld/requirements.txt +++ b/apworld/requirements.txt
@@ -1 +1 @@
protobuf>=5.29.3 \ No newline at end of file protobuf==3.20.3
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 b699d59..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:
@@ -8,9 +9,14 @@ class Lingo2StaticLogic:
8 item_name_to_id: dict[str, int] 9 item_name_to_id: dict[str, int]
9 location_name_to_id: dict[str, int] 10 location_name_to_id: dict[str, int]
10 11
12 item_name_groups: dict[str, list[str]]
13 location_name_groups: dict[str, list[str]]
14
11 def __init__(self): 15 def __init__(self):
12 self.item_id_to_name = {} 16 self.item_id_to_name = {}
13 self.location_id_to_name = {} 17 self.location_id_to_name = {}
18 self.item_name_groups = {}
19 self.location_name_groups = {}
14 20
15 file = pkgutil.get_data(__name__, "generated/data.binpb") 21 file = pkgutil.get_data(__name__, "generated/data.binpb")
16 self.objects = data_pb2.AllObjects() 22 self.objects = data_pb2.AllObjects()
@@ -29,28 +35,39 @@ class Lingo2StaticLogic:
29 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" 35 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}"
30 location_name = f"{self.get_room_object_map_name(letter)} - {letter_name}" 36 location_name = f"{self.get_room_object_map_name(letter)} - {letter_name}"
31 self.location_id_to_name[letter.ap_id] = location_name 37 self.location_id_to_name[letter.ap_id] = location_name
38 self.location_name_groups.setdefault("Letters", []).append(location_name)
32 39
33 if not letter.level2: 40 if not letter.level2:
34 self.item_id_to_name[letter.ap_id] = letter.key.upper() 41 self.item_id_to_name[letter.ap_id] = letter.key.upper()
42 self.item_name_groups.setdefault("Letters", []).append(letter.key.upper())
35 43
36 for mastery in self.objects.masteries: 44 for mastery in self.objects.masteries:
37 location_name = f"{self.get_room_object_map_name(mastery)} - Mastery" 45 location_name = f"{self.get_room_object_map_name(mastery)} - Mastery"
38 self.location_id_to_name[mastery.ap_id] = location_name 46 self.location_id_to_name[mastery.ap_id] = location_name
47 self.location_name_groups.setdefault("Masteries", []).append(location_name)
39 48
40 for ending in self.objects.endings: 49 for ending in self.objects.endings:
41 location_name = f"{self.get_room_object_map_name(ending)} - {ending.name.title()} Ending" 50 location_name = f"{self.get_room_object_map_name(ending)} - {ending.name.title()} Ending"
42 self.location_id_to_name[ending.ap_id] = location_name 51 self.location_id_to_name[ending.ap_id] = location_name
52 self.location_name_groups.setdefault("Endings", []).append(location_name)
43 53
44 for progressive in self.objects.progressives: 54 for progressive in self.objects.progressives:
45 self.item_id_to_name[progressive.ap_id] = progressive.name 55 self.item_id_to_name[progressive.ap_id] = progressive.name
46 56
57 for door_group in self.objects.door_groups:
58 self.item_id_to_name[door_group.ap_id] = door_group.name
59
47 for keyholder in self.objects.keyholders: 60 for keyholder in self.objects.keyholders:
48 if keyholder.HasField("key"): 61 if keyholder.HasField("key"):
49 location_name = f"{self.get_room_object_location_prefix(keyholder)} - {keyholder.key.upper()} Keyholder" 62 location_name = f"{self.get_room_object_location_prefix(keyholder)} - {keyholder.key.upper()} Keyholder"
50 self.location_id_to_name[keyholder.ap_id] = location_name 63 self.location_id_to_name[keyholder.ap_id] = location_name
64 self.location_name_groups.setdefault("Keyholders", []).append(location_name)
51 65
52 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"
53 67
68 for symbol_name in SYMBOL_ITEMS.values():
69 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
70
54 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()}
55 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()}
56 73