From 3d03fcd82991d201f32a8313d4b44a4b17de4526 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 7 Feb 2026 13:24:58 -0500 Subject: Add restrict_letter_placements option --- apworld/__init__.py | 10 ++++++++-- apworld/items.py | 5 ++++- apworld/locations.py | 27 ++++++++++++++++++++++++++- apworld/options.py | 12 ++++++++++++ apworld/player_logic.py | 13 +++++++++++-- apworld/regions.py | 7 ++++++- 6 files changed, 67 insertions(+), 7 deletions(-) diff --git a/apworld/__init__.py b/apworld/__init__.py index 42350bc..ba5d7ea 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py @@ -7,7 +7,7 @@ from BaseClasses import ItemClassification, Item, Tutorial from Options import OptionError from settings import Group, UserFilePath from worlds.AutoWorld import WebWorld, World -from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS +from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS, ALL_LETTERS_UPPER from .options import Lingo2Options from .player_logic import Lingo2PlayerLogic from .regions import create_regions, shuffle_entrances, connect_ports_from_ut @@ -70,6 +70,9 @@ class Lingo2World(World): self.player_logic = Lingo2PlayerLogic(self) self.port_pairings = {} + if self.options.restrict_letter_placements: + self.options.local_items.value |= set(ALL_LETTERS_UPPER) + def create_regions(self): if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: self.player_logic.rte_mapping = [self.world.static_logic.map_id_by_name[map_name] @@ -128,11 +131,14 @@ class Lingo2World(World): self.push_precollected(self.create_item(name)) def create_item(self, name: str) -> Item: - return Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else + item = Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else ItemClassification.progression, self.item_name_to_id.get(name), self.player) + item.is_letter = (name in ALL_LETTERS_UPPER) + return item + def set_rules(self): self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) diff --git a/apworld/items.py b/apworld/items.py index 28158c3..143ccb1 100644 --- a/apworld/items.py +++ b/apworld/items.py @@ -5,6 +5,8 @@ from BaseClasses import Item class Lingo2Item(Item): game: str = "Lingo 2" + is_letter: bool + SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = { data_pb2.PuzzleSymbol.SUN: "Sun Symbol", @@ -28,4 +30,5 @@ SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = { data_pb2.PuzzleSymbol.QUESTION: "Question Symbol", } -ANTI_COLLECTABLE_TRAPS: list[str] = [f"Anti {letter}" for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"] +ALL_LETTERS_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +ANTI_COLLECTABLE_TRAPS: list[str] = [f"Anti {letter}" for letter in ALL_LETTERS_UPPER] diff --git a/apworld/locations.py b/apworld/locations.py index 3d619dc..174a0dd 100644 --- a/apworld/locations.py +++ b/apworld/locations.py @@ -1,4 +1,13 @@ -from BaseClasses import Location +from enum import Enum + +from BaseClasses import Location, Item +from .items import Lingo2Item + + +class LetterPlacementType(Enum): + ANY = 0 + DISALLOW = 1 + FORCE = 2 class Lingo2Location(Location): @@ -6,3 +15,19 @@ class Lingo2Location(Location): port_id: int goal: bool + letter_placement_type: LetterPlacementType + + def set_up_letter_rule(self, lpt: LetterPlacementType): + self.letter_placement_type = lpt + self.item_rule = self._l2_item_rule + + def _l2_item_rule(self, item: Item) -> bool: + if not isinstance(item, Lingo2Item): + return True + + if self.letter_placement_type == LetterPlacementType.FORCE: + return item.is_letter + elif self.letter_placement_type == LetterPlacementType.DISALLOW: + return not item.is_letter + + return True diff --git a/apworld/options.py b/apworld/options.py index 063af21..6fe6d8d 100644 --- a/apworld/options.py +++ b/apworld/options.py @@ -44,6 +44,17 @@ class ShuffleLetters(Choice): option_item_cyan = 4 +class RestrictLetterPlacements(Toggle): + """ + If enabled, letter items will be shuffled among letter locations in your local world. Shuffle Letters must be set to + Progressive or Item Cyan for this to be useful. + + WARNING: This option may slow down generation. Additionally, it is only reliable with Shuffle Letters set to Item + Cyan. When set to Progressive, Shuffle Doors and Shuffle Symbols must be turned off. + """ + display_name = "Restrict Letter Placements" + + class ShuffleSymbols(Toggle): """ If enabled, 19 items will be added to the pool, representing the different symbols that can appear on a panel. @@ -251,6 +262,7 @@ class Lingo2Options(PerGameCommonOptions): shuffle_control_center_colors: ShuffleControlCenterColors shuffle_gallery_paintings: ShuffleGalleryPaintings shuffle_letters: ShuffleLetters + restrict_letter_placements: RestrictLetterPlacements shuffle_symbols: ShuffleSymbols shuffle_worldports: ShuffleWorldports keyholder_sanity: KeyholderSanity diff --git a/apworld/player_logic.py b/apworld/player_logic.py index a02856e..7bfd49f 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -192,6 +192,7 @@ class AccessRequirements: class PlayerLocation(NamedTuple): code: int | None reqs: AccessRequirements + is_letter: bool = False class LetterBehavior(IntEnum): @@ -295,6 +296,12 @@ class Lingo2PlayerLogic: self.shuffled_doors.update(set(door.id for door in world.static_logic.objects.doors if door.map_id == game_map.id and door.daedalus_only_allow)) + if (world.options.restrict_letter_placements + and world.options.shuffle_letters == ShuffleLetters.option_progressive + and (world.options.shuffle_doors or world.options.shuffle_symbols)): + raise OptionError(f"When Restrict Letter Placements is enabled and Shuffle Letters is set to Progressive, " + f"both Shuffle Doors and Shuffle Symbols must be disabled (Player {world.player}).") + maximum_masteries = 13 + len(world.options.enable_gift_maps.value) if world.options.enable_icarus: maximum_masteries += 1 @@ -406,9 +413,11 @@ class Lingo2PlayerLogic: if not self.should_shuffle_room(letter.room_id): continue - self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, - AccessRequirements())) behavior = self.get_letter_behavior(letter.key, letter.level2) + + self.locations_by_room.setdefault(letter.room_id, []).append( + PlayerLocation(letter.ap_id, AccessRequirements(), behavior == LetterBehavior.ITEM)) + if behavior == LetterBehavior.VANILLA: if not world.for_tracker: letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" diff --git a/apworld/regions.py b/apworld/regions.py index 500139f..076c143 100644 --- a/apworld/regions.py +++ b/apworld/regions.py @@ -4,7 +4,7 @@ import BaseClasses from BaseClasses import Region, ItemClassification, Entrance from entrance_rando import randomize_entrances from .items import Lingo2Item -from .locations import Lingo2Location +from .locations import Lingo2Location, LetterPlacementType from .options import FastTravelAccess from .player_logic import AccessRequirements from .rules import make_location_lambda @@ -25,6 +25,11 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], location.code, new_region) new_location.access_rule = make_location_lambda(reqs, world, regions) + if world.options.restrict_letter_placements: + if location.is_letter: + new_location.set_up_letter_rule(LetterPlacementType.FORCE) + else: + new_location.set_up_letter_rule(LetterPlacementType.DISALLOW) new_region.locations.append(new_location) for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): -- cgit 1.4.1