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.md4
-rw-r--r--apworld/__init__.py16
-rw-r--r--apworld/docs/en_Lingo_2.md4
-rw-r--r--apworld/items.py24
-rw-r--r--apworld/options.py9
-rw-r--r--apworld/player_logic.py60
-rw-r--r--apworld/requirements.txt2
-rw-r--r--apworld/rules.py2
-rw-r--r--apworld/static_logic.py14
9 files changed, 111 insertions, 24 deletions
diff --git a/apworld/README.md b/apworld/README.md index 386a2a1..13374b2 100644 --- a/apworld/README.md +++ b/apworld/README.md
@@ -6,7 +6,7 @@ Lingo 2.
6## Installation 6## Installation
7 7
81. Download the Lingo 2 Apworld from 81. Download the Lingo 2 Apworld from
9 [the releases page](https://code.fourisland.com/lingo-archipelago2/about/apworld/CHANGELOG.md). 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 102. If you do not already have it, download and install the
11 [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/). 11 [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/).
123. Double click on `lingo2.apworld` to install it, or copy it manually to the 123. Double click on `lingo2.apworld` to install it, or copy it manually to the
@@ -20,7 +20,7 @@ used.
20 20
21The first file is `data.binpb`, the datafile containing the randomizer logic. 21The first file is `data.binpb`, the datafile containing the randomizer logic.
22You can read about how to generate it on 22You can read about how to generate it on
23[its own README page](https://code.fourisland.com/lingo-archipelago2/about/data/README.md). 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`. 24Once you have it, put it in a subfolder of `apworld` called `generated`.
25 25
26The second generated file is `data_pb2.py`. This file allows Archipelago to read 26The second generated file is `data_pb2.py`. This file allows Archipelago to read
diff --git a/apworld/__init__.py b/apworld/__init__.py index c45e8b3..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
@@ -72,11 +84,13 @@ class Lingo2World(World):
72 "shuffle_control_center_colors", 84 "shuffle_control_center_colors",
73 "shuffle_doors", 85 "shuffle_doors",
74 "shuffle_letters", 86 "shuffle_letters",
87 "shuffle_symbols",
75 "victory_condition", 88 "victory_condition",
76 ] 89 ]
77 90
78 slot_data = { 91 slot_data = {
79 **self.options.as_dict(*slot_options), 92 **self.options.as_dict(*slot_options),
93 "version": [MAJOR_VERSION, MINOR_VERSION],
80 } 94 }
81 95
82 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 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 c94b809..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
@@ -335,7 +355,11 @@ class Lingo2PlayerLogic:
335 355
336 for ending_id in door.endings: 356 for ending_id in door.endings:
337 ending = self.world.static_logic.objects.endings[ending_id] 357 ending = self.world.static_logic.objects.endings[ending_id]
338 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)")
339 363
340 for sub_door_id in door.doors: 364 for sub_door_id in door.doors:
341 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 49ca0a7..dbc395b 100644 --- a/apworld/requirements.txt +++ b/apworld/requirements.txt
@@ -1 +1 @@
protobuf==3.20.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 3f6cdea..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,17 +35,21 @@ 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
@@ -51,9 +61,13 @@ class Lingo2StaticLogic:
51 if keyholder.HasField("key"): 61 if keyholder.HasField("key"):
52 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"
53 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)
54 65
55 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"
56 67
68 for symbol_name in SYMBOL_ITEMS.values():
69 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
70
57 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()}
58 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()}
59 73