From 8de745f4d3350ac848c9362a33e223c0ff94fdcf Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 9 Sep 2025 16:44:09 -0400 Subject: Added symbol shuffle Also fixed unlocked letters + any double letter cyan doors, and tweaked some logic related to important panels with symbols on them. --- apworld/__init__.py | 1 + apworld/items.py | 24 ++++++++++++++++++++++ apworld/options.py | 9 +++++++++ apworld/player_logic.py | 54 +++++++++++++++++++++++++++++++++---------------- apworld/rules.py | 2 -- apworld/static_logic.py | 4 ++++ 6 files changed, 75 insertions(+), 19 deletions(-) (limited to 'apworld') 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): "shuffle_control_center_colors", "shuffle_doors", "shuffle_letters", + "shuffle_symbols", "victory_condition", ] 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 @@ +from .generated import data_pb2 as data_pb2 from BaseClasses import Item class Lingo2Item(Item): game: str = "Lingo 2" + + +SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = { + data_pb2.PuzzleSymbol.SUN: "Sun Symbol", + data_pb2.PuzzleSymbol.SPARKLES: "Sparkles Symbol", + data_pb2.PuzzleSymbol.ZERO: "Zero Symbol", + data_pb2.PuzzleSymbol.EXAMPLE: "Example Symbol", + data_pb2.PuzzleSymbol.BOXES: "Boxes Symbol", + data_pb2.PuzzleSymbol.PLANET: "Planet Symbol", + data_pb2.PuzzleSymbol.PYRAMID: "Pyramid Symbol", + data_pb2.PuzzleSymbol.CROSS: "Cross Symbol", + data_pb2.PuzzleSymbol.SWEET: "Sweet Symbol", + data_pb2.PuzzleSymbol.GENDER: "Gender Symbol", + data_pb2.PuzzleSymbol.AGE: "Age Symbol", + data_pb2.PuzzleSymbol.SOUND: "Sound Symbol", + data_pb2.PuzzleSymbol.ANAGRAM: "Anagram Symbol", + data_pb2.PuzzleSymbol.JOB: "Job Symbol", + data_pb2.PuzzleSymbol.STARS: "Stars Symbol", + data_pb2.PuzzleSymbol.NULL: "Null Symbol", + data_pb2.PuzzleSymbol.EVAL: "Eval Symbol", + data_pb2.PuzzleSymbol.LINGO: "Lingo Symbol", + data_pb2.PuzzleSymbol.QUESTION: "Question Symbol", +} 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): option_item_cyan = 4 +class ShuffleSymbols(Toggle): + """ + If enabled, 19 items will be added to the pool, representing the different symbols that can appear on a panel. + Players will be prevented from solving puzzles with symbols on them until all of the required symbols are unlocked. + """ + display_name = "Shuffle Symbols" + + class KeyholderSanity(Toggle): """ If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder. @@ -102,6 +110,7 @@ class Lingo2Options(PerGameCommonOptions): shuffle_doors: ShuffleDoors shuffle_control_center_colors: ShuffleControlCenterColors shuffle_letters: ShuffleLetters + shuffle_symbols: ShuffleSymbols keyholder_sanity: KeyholderSanity cyan_door_behavior: CyanDoorBehavior 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 @@ from enum import IntEnum, auto from .generated import data_pb2 as data_pb2 +from .items import SYMBOL_ITEMS from typing import TYPE_CHECKING, NamedTuple from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior @@ -23,7 +24,6 @@ class AccessRequirements: items: set[str] progressives: dict[str, int] rooms: set[str] - symbols: set[str] letters: dict[str, int] cyans: bool @@ -34,7 +34,6 @@ class AccessRequirements: self.items = set() self.progressives = dict() self.rooms = set() - self.symbols = set() self.letters = dict() self.cyans = False self.or_logic = list() @@ -49,9 +48,6 @@ class AccessRequirements: for room in other.rooms: self.rooms.add(room) - for symbol in other.symbols: - self.symbols.add(symbol) - for letter, level in other.letters.items(): self.letters[letter] = max(self.letters.get(letter, 0), level) @@ -60,6 +56,10 @@ class AccessRequirements: for disjunction in other.or_logic: self.or_logic.append(disjunction) + def is_empty(self) -> bool: + return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 + and not self.cyans and len(self.or_logic) == 0) + def __repr__(self): parts = [] if len(self.items) > 0: @@ -68,8 +68,6 @@ class AccessRequirements: parts.append(f"progressives={self.progressives}") if len(self.rooms) > 0: parts.append(f"rooms={self.rooms}") - if len(self.symbols) > 0: - parts.append(f"symbols={self.symbols}") if len(self.letters) > 0: parts.append(f"letters={self.letters}") if self.cyans: @@ -231,6 +229,10 @@ class Lingo2PlayerLogic: self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id, reqs)) + if self.world.options.shuffle_symbols: + for symbol_name in SYMBOL_ITEMS.values(): + self.real_items.append(symbol_name) + def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: if answer is None: if panel_id not in self.panel_reqs: @@ -253,25 +255,35 @@ class Lingo2PlayerLogic: self.add_solution_reqs(reqs, answer) elif len(panel.proxies) > 0: possibilities = [] + already_filled = False for proxy in panel.proxies: proxy_reqs = AccessRequirements() self.add_solution_reqs(proxy_reqs, proxy.answer) - possibilities.append(proxy_reqs) + if not proxy_reqs.is_empty(): + possibilities.append(proxy_reqs) + else: + already_filled = True + break - if not any(proxy.answer == panel.answer for proxy in panel.proxies): + if not already_filled and not any(proxy.answer == panel.answer for proxy in panel.proxies): proxy_reqs = AccessRequirements() self.add_solution_reqs(proxy_reqs, panel.answer) - possibilities.append(proxy_reqs) + if not proxy_reqs.is_empty(): + possibilities.append(proxy_reqs) + else: + already_filled = True - reqs.or_logic.append(possibilities) + if not already_filled: + reqs.or_logic.append(possibilities) else: self.add_solution_reqs(reqs, panel.answer) - for symbol in panel.symbols: - reqs.symbols.add(symbol) + if self.world.options.shuffle_symbols: + for symbol in panel.symbols: + reqs.items.add(SYMBOL_ITEMS.get(symbol)) if panel.HasField("required_door"): door_reqs = self.get_door_open_reqs(panel.required_door) @@ -300,9 +312,16 @@ class Lingo2PlayerLogic: panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) reqs.merge(panel_reqs) elif door.complete_at == 1: - reqs.or_logic.append([self.get_panel_reqs(proxy.panel, - proxy.answer if proxy.HasField("answer") else None) - for proxy in door.panels]) + disjunction = [] + for proxy in door.panels: + proxy_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) + if proxy_reqs.is_empty(): + disjunction.clear() + break + else: + disjunction.append(proxy_reqs) + if len(disjunction) > 0: + reqs.or_logic.append(disjunction) else: # TODO: Handle complete_at > 1 pass @@ -316,7 +335,8 @@ class Lingo2PlayerLogic: if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2: reqs.rooms.add("The Repetitive - Main Room") elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter: - reqs.cyans = True + if self.world.options.shuffle_letters != ShuffleLetters.option_unlocked: + reqs.cyans = True elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item: # There shouldn't be any locations that are cyan doors. 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 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): return False - # TODO: symbols - for letter_key, letter_level in reqs.letters.items(): if not state.has(letter_key, world.player, letter_level): 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 @@ from .generated import data_pb2 as data_pb2 +from .items import SYMBOL_ITEMS import pkgutil class Lingo2StaticLogic: @@ -64,6 +65,9 @@ class Lingo2StaticLogic: self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" + for symbol_name in SYMBOL_ITEMS.values(): + self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name + self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} -- cgit 1.4.1