From 5a0df5470df4c991048b61f9bae59ba6868060df Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 12:31:16 -0500 Subject: [PATCH 01/19] Lingo: Flatten location reqs before fill --- worlds/lingo/player_logic.py | 86 +++++++++++++++++++++++++++++------- worlds/lingo/rules.py | 69 ++++++++++------------------- 2 files changed, 92 insertions(+), 63 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index abb975e020ae..a312f8c57e95 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -1,21 +1,40 @@ -from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING +from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING from .items import ALL_ITEM_TABLE from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, VictoryCondition from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \ - RoomAndPanel + RoomAndDoor, RoomAndPanel from .testing import LingoTestOptions if TYPE_CHECKING: from . import LingoWorld +class AccessRequirements: + rooms: Set[str] + doors: Set[RoomAndDoor] + colors: Set[str] + + def __init__(self): + self.rooms = set() + self.doors = set() + self.colors = set() + + def merge(self, other: "AccessRequirements"): + self.rooms |= other.rooms + self.doors |= other.doors + self.colors |= other.colors + + def __str__(self): + return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})" + + class PlayerLocation(NamedTuple): name: str code: Optional[int] = None - panels: List[RoomAndPanel] = [] + access: AccessRequirements = () class LingoPlayerLogic: @@ -39,8 +58,18 @@ class LingoPlayerLogic: FORCED_GOOD_ITEM: str - def add_location(self, room: str, loc: PlayerLocation): - self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) + PANEL_REQS: Dict[str, Dict[str, AccessRequirements]] + + def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel]): + access_reqs = AccessRequirements() + for panel in panels: + if panel.room is not None and panel.room != room: + access_reqs.rooms.add(panel.room) + + sub_access_reqs = self.calculate_panel_requirements(room if panel.room is None else panel.room, panel.panel) + access_reqs.merge(sub_access_reqs) + + self.LOCATIONS_BY_ROOM.setdefault(room, []).append(PlayerLocation(name, code, access_reqs)) def set_door_item(self, room: str, door: str, item: str): self.ITEM_BY_DOOR.setdefault(room, {})[door] = item @@ -67,6 +96,7 @@ def __init__(self, world: "LingoWorld"): self.LEVEL_2_LOCATION = "" self.PAINTING_MAPPING = {} self.FORCED_GOOD_ITEM = "" + self.PANEL_REQS = {} door_shuffle = world.options.shuffle_doors color_shuffle = world.options.shuffle_colors @@ -85,7 +115,7 @@ def __init__(self, world: "LingoWorld"): for door_name, door_data in room_data.items(): if door_shuffle == ShuffleDoors.option_none: itemloc_name = f"{room_name} - {door_name} (Opened)" - self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels)) + self.add_location(room_name, itemloc_name, None, door_data.panels) self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name self.set_door_item(room_name, door_name, itemloc_name) else: @@ -98,7 +128,7 @@ def __init__(self, world: "LingoWorld"): self.handle_non_grouped_door(room_name, door_data, world) if door_data.event: - self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels)) + self.add_location(room_name, door_data.item_name, None, door_data.panels) self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") @@ -108,14 +138,12 @@ def __init__(self, world: "LingoWorld"): for panel_name, panel_data in room_data.items(): if panel_data.achievement: event_name = room_name + " - " + panel_name + " (Achieved)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)])) + self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: event_name = room_name + " - " + panel_name + " (Counted)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)])) + self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need @@ -125,20 +153,19 @@ def __init__(self, world: "LingoWorld"): if victory_condition == VictoryCondition.option_the_end: self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" - self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)")) + self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, []) self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" elif victory_condition == VictoryCondition.option_the_master: self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" - self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, [])) + self.add_location("Orange Tower Seventh Floor", self.MASTERY_LOCATION, None, []) self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" elif victory_condition == VictoryCondition.option_level_2: self.VICTORY_CONDITION = "Second Room - LEVEL 2" self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" - self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None, - [RoomAndPanel("Second Room", "LEVEL 2")])) + self.add_location("Second Room", self.LEVEL_2_LOCATION, None, [RoomAndPanel("Second Room", "LEVEL 2")]) self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" # Instantiate all real locations. @@ -153,8 +180,7 @@ def __init__(self, world: "LingoWorld"): if location_classification not in location_data.classification: continue - self.add_location(location_data.room, PlayerLocation(location_name, location_data.code, - location_data.panels)) + self.add_location(location_data.room, location_name, location_data.code, location_data.panels) self.REAL_LOCATIONS.append(location_name) # Instantiate all real items. @@ -294,3 +320,29 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: return False return True + + def calculate_panel_requirements(self, room: str, panel: str): + if panel not in self.PANEL_REQS.setdefault(room, {}): + access_reqs = AccessRequirements() + panel_object = PANELS_BY_ROOM[room][panel] + + for req_room in panel_object.required_rooms: + access_reqs.rooms.add(req_room) + + for req_door in panel_object.required_doors: + access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door)) + + for color in panel_object.colors: + access_reqs.colors.add(color) + + for req_panel in panel_object.required_panels: + if req_panel.room is not None and req_panel.room != room: + access_reqs.rooms.add(req_panel.room) + + sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, req_panel.panel) + access_reqs.merge(sub_access_reqs) + + self.PANEL_REQS[room][panel] = access_reqs + + return self.PANEL_REQS[room][panel] + diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index d59b8a1ef78a..6091baedc5b3 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -1,9 +1,8 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState -from .options import VictoryCondition from .player_logic import LingoPlayerLogic, PlayerLocation -from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor +from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor if TYPE_CHECKING: from . import LingoWorld @@ -14,7 +13,7 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, if door is None: return True - return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic) + return _lingo_can_open_door(state, room if door.room is None else door.room, door.door, player, player_logic) def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic): @@ -34,13 +33,21 @@ def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: return True -def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld", +def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): - for panel in location.panels: - panel_room = room_name if panel.room is None else panel.room - if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic): + for req_room in location.access.rooms: + if not state.can_reach(req_room, "Region", world.player): + return False + + for req_door in location.access.doors: + if not _lingo_can_open_door(state, req_door.room, req_door.door, world.player, player_logic): return False + if len(location.access.colors) > 0 and world.options.shuffle_colors: + for color in location.access.colors: + if not state.has(color.capitalize(), world.player): + return False + return True @@ -48,8 +55,11 @@ def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) -def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int, - player_logic: LingoPlayerLogic): +def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): + return state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1) + + +def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: int, player_logic: LingoPlayerLogic): """ Determines whether a door can be opened """ @@ -61,44 +71,11 @@ def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, doo return state.has(item_name, player) -def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld", - player_logic: LingoPlayerLogic): - """ - Determines whether a panel can be solved - """ - if start_room != room and not state.can_reach(room, "Region", world.player): - return False - - if room == "Second Room" and panel == "ANOTHER TRY" \ - and world.options.victory_condition == VictoryCondition.option_level_2 \ - and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1): - return False - - panel_object = PANELS_BY_ROOM[room][panel] - for req_room in panel_object.required_rooms: - if not state.can_reach(req_room, "Region", world.player): - return False - - for req_door in panel_object.required_doors: - if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room, - req_door.door, world.player, player_logic): - return False - - for req_panel in panel_object.required_panels: - if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room, - req_panel.panel, world, player_logic): - return False - - if len(panel_object.colors) > 0 and world.options.shuffle_colors: - for color in panel_object.colors: - if not state.has(color.capitalize(), world.player): - return False - - return True - - def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic): if location.name == player_logic.MASTERY_LOCATION: return lambda state: lingo_can_use_mastery_location(state, world) - return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic) + if location.name == player_logic.LEVEL_2_LOCATION: + return lambda state: lingo_can_use_level_2_location(state, world) + + return lambda state: lingo_can_use_location(state, location, world, player_logic) From 0f04e07c9c319b305e8692d7f29765b05a8a7aa6 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 10 Nov 2023 12:54:30 -0500 Subject: [PATCH 02/19] Coalesce counted events to color granularity --- worlds/lingo/locations.py | 1 + worlds/lingo/player_logic.py | 44 ++++++++++++++++++++++++++++++------ worlds/lingo/regions.py | 1 + worlds/lingo/rules.py | 5 +++- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py index 5903d603ec4f..685dc3537c0a 100644 --- a/worlds/lingo/locations.py +++ b/worlds/lingo/locations.py @@ -34,6 +34,7 @@ class LingoLocation(Location): Location from the game Lingo """ game: str = "Lingo" + counting_panels: int = 0 ALL_LOCATION_TABLE: Dict[str, LocationData] = {} diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index a312f8c57e95..fc781c25825d 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -35,6 +35,7 @@ class PlayerLocation(NamedTuple): name: str code: Optional[int] = None access: AccessRequirements = () + counting_panels: int = 0 class LingoPlayerLogic: @@ -132,8 +133,7 @@ def __init__(self, world: "LingoWorld"): self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") - # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also - # create events for each counting panel, so that we can determine when LEVEL 2 is accessible. + # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. for room_name, room_data in PANELS_BY_ROOM.items(): for panel_name, panel_data in room_data.items(): if panel_data.achievement: @@ -141,11 +141,6 @@ def __init__(self, world: "LingoWorld"): self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" - if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: - event_name = room_name + " - " + panel_name + " (Counted)" - self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) - self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" - # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" @@ -168,6 +163,8 @@ def __init__(self, world: "LingoWorld"): self.add_location("Second Room", self.LEVEL_2_LOCATION, None, [RoomAndPanel("Second Room", "LEVEL 2")]) self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" + self.create_panel_hunt_events(world) + # Instantiate all real locations. location_classification = LocationClassification.normal if location_checks == LocationChecks.option_reduced: @@ -346,3 +343,36 @@ def calculate_panel_requirements(self, room: str, panel: str): return self.PANEL_REQS[room][panel] + def create_panel_hunt_events(self, world: "LingoWorld"): + for room_name, room_data in PANELS_BY_ROOM.items(): + unhindered_panels_by_color: dict[Optional[str], int] = {} + + for panel_name, panel_data in room_data.items(): + # We won't count non-counting panels. + if panel_data.non_counting: + continue + + # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will + # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. + if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ + or len(panel_data.required_rooms) > 0\ + or (world.options.shuffle_colors and len(panel_data.colors) > 1): + event_name = room_name + " - " + panel_name + " (Counted)" + self.add_location(room_name, PlayerLocation(event_name, None, + [RoomAndPanel(room_name, panel_name)], None, 1)) + self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" + else: + if len(panel_data.colors) == 0 or not world.options.shuffle_colors: + color = None + else: + color = panel_data.colors[0] + + unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1 + + for color, panel_count in unhindered_panels_by_color.items(): + if color is None: + event_name = room_name + " - " + str(panel_count) + " White Panels (Counted)" + else: + event_name = room_name + " - " + str(panel_count) + " " + color.capitalize() + " Panels (Counted)" + self.add_location(room_name, PlayerLocation(event_name, None, [], color, panel_count)) + self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panels Solved" diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index e5f947de05e4..b462c05186de 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -15,6 +15,7 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi new_region = Region(room.name, world.player, world.multiworld) for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): new_location = LingoLocation(world.player, location.name, location.code, new_region) + new_location.counting_panels = location.counting_panels new_location.access_rule = make_location_lambda(location, room.name, world, player_logic) new_region.locations.append(new_location) if location.name in player_logic.EVENT_LOC_TO_ITEM: diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 6091baedc5b3..9121e1ae1a92 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -38,6 +38,9 @@ def lingo_can_use_location(state: CollectionState, location: PlayerLocation, wor for req_room in location.access.rooms: if not state.can_reach(req_room, "Region", world.player): return False + if location.color is not None and world.options.shuffle_colors: + if not state.has(location.color.capitalize(), world.player): + return False for req_door in location.access.doors: if not _lingo_can_open_door(state, req_door.room, req_door.door, world.player, player_logic): @@ -56,7 +59,7 @@ def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): - return state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1) + return sum([location.counting_panels for location in state.locations_checked]) >= world.options.level_2_requirement.value - 1 def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: int, player_logic: LingoPlayerLogic): From 0d5006c3d84f9c05833e1325a22a34e220e8f1f6 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 13:01:32 -0500 Subject: [PATCH 03/19] Fixed stuff from the cherrypick --- worlds/lingo/player_logic.py | 26 ++++++++++++++++---------- worlds/lingo/rules.py | 6 ++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index fc781c25825d..4f156849052a 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -4,8 +4,8 @@ from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, VictoryCondition from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ - PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \ - RoomAndDoor, RoomAndPanel + PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ + RoomAndPanel from .testing import LingoTestOptions if TYPE_CHECKING: @@ -62,15 +62,21 @@ class LingoPlayerLogic: PANEL_REQS: Dict[str, Dict[str, AccessRequirements]] def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel]): + counting_panels = 0 access_reqs = AccessRequirements() for panel in panels: if panel.room is not None and panel.room != room: access_reqs.rooms.add(panel.room) - sub_access_reqs = self.calculate_panel_requirements(room if panel.room is None else panel.room, panel.panel) + panel_room = room if panel.room is None else panel.room + panel_object = PANELS_BY_ROOM[panel_room][panel.panel] + if not panel_object.non_counting: + counting_panels += 1 + + sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel) access_reqs.merge(sub_access_reqs) - self.LOCATIONS_BY_ROOM.setdefault(room, []).append(PlayerLocation(name, code, access_reqs)) + self.LOCATIONS_BY_ROOM.setdefault(room, []).append(PlayerLocation(name, code, access_reqs, counting_panels)) def set_door_item(self, room: str, door: str, item: str): self.ITEM_BY_DOOR.setdefault(room, {})[door] = item @@ -144,7 +150,7 @@ def __init__(self, world: "LingoWorld"): # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" - self.LEVEL_2_LOCATION = "N/A" + self.LEVEL_2_LOCATION = "Second Room - LEVEL 2" if victory_condition == VictoryCondition.option_the_end: self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" @@ -345,7 +351,7 @@ def calculate_panel_requirements(self, room: str, panel: str): def create_panel_hunt_events(self, world: "LingoWorld"): for room_name, room_data in PANELS_BY_ROOM.items(): - unhindered_panels_by_color: dict[Optional[str], int] = {} + unhindered_panels_by_color: dict[Optional[str], List[str]] = {} for panel_name, panel_data in room_data.items(): # We won't count non-counting panels. @@ -358,8 +364,7 @@ def create_panel_hunt_events(self, world: "LingoWorld"): or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1): event_name = room_name + " - " + panel_name + " (Counted)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)], None, 1)) + self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" else: if len(panel_data.colors) == 0 or not world.options.shuffle_colors: @@ -367,12 +372,13 @@ def create_panel_hunt_events(self, world: "LingoWorld"): else: color = panel_data.colors[0] - unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1 + unhindered_panels_by_color.setdefault(color, []).append(panel_name) for color, panel_count in unhindered_panels_by_color.items(): if color is None: event_name = room_name + " - " + str(panel_count) + " White Panels (Counted)" else: event_name = room_name + " - " + str(panel_count) + " " + color.capitalize() + " Panels (Counted)" - self.add_location(room_name, PlayerLocation(event_name, None, [], color, panel_count)) + self.add_location(room_name, event_name, None, + [RoomAndPanel(room_name, panel_name) for panel_name in panel_count]) self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panels Solved" diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 9121e1ae1a92..16fa0129fe80 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -38,9 +38,6 @@ def lingo_can_use_location(state: CollectionState, location: PlayerLocation, wor for req_room in location.access.rooms: if not state.can_reach(req_room, "Region", world.player): return False - if location.color is not None and world.options.shuffle_colors: - if not state.has(location.color.capitalize(), world.player): - return False for req_door in location.access.doors: if not _lingo_can_open_door(state, req_door.room, req_door.door, world.player, player_logic): @@ -59,7 +56,8 @@ def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): - return sum([location.counting_panels for location in state.locations_checked]) >= world.options.level_2_requirement.value - 1 + return sum([location.counting_panels for location in state.locations_checked])\ + >= world.options.level_2_requirement.value def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: int, player_logic: LingoPlayerLogic): From a2287e26330b9ea1532aa14a185aef420d0b7b48 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 13:02:57 -0500 Subject: [PATCH 04/19] Re-disable LEVEL 2 panel hunt when it's not the wincon --- worlds/lingo/player_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 4f156849052a..15fa34f863aa 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -150,7 +150,7 @@ def __init__(self, world: "LingoWorld"): # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" - self.LEVEL_2_LOCATION = "Second Room - LEVEL 2" + self.LEVEL_2_LOCATION = "N/A" if victory_condition == VictoryCondition.option_the_end: self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" @@ -169,7 +169,7 @@ def __init__(self, world: "LingoWorld"): self.add_location("Second Room", self.LEVEL_2_LOCATION, None, [RoomAndPanel("Second Room", "LEVEL 2")]) self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" - self.create_panel_hunt_events(world) + self.create_panel_hunt_events(world) # Instantiate all real locations. location_classification = LocationClassification.normal From 9388a4ae0c7a260dcd5ad1a23eeb9ce427635d69 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 13:40:39 -0500 Subject: [PATCH 05/19] Converted some stuff to formatted strings --- worlds/lingo/player_logic.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 15fa34f863aa..ae824b745e06 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -135,15 +135,16 @@ def __init__(self, world: "LingoWorld"): self.handle_non_grouped_door(room_name, door_data, world) if door_data.event: + event_item_name = f"{door_data.item_name} (Opened)" self.add_location(room_name, door_data.item_name, None, door_data.panels) - self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" - self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") + self.EVENT_LOC_TO_ITEM[door_data.item_name] = event_item_name + self.set_door_item(room_name, door_name, event_item_name) # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. for room_name, room_data in PANELS_BY_ROOM.items(): for panel_name, panel_data in room_data.items(): if panel_data.achievement: - event_name = room_name + " - " + panel_name + " (Achieved)" + event_name = f"{room_name} - {panel_name} (Achieved)" self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" @@ -363,7 +364,7 @@ def create_panel_hunt_events(self, world: "LingoWorld"): if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1): - event_name = room_name + " - " + panel_name + " (Counted)" + event_name = f"{room_name} - {panel_name} (Counted)" self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" else: @@ -376,9 +377,9 @@ def create_panel_hunt_events(self, world: "LingoWorld"): for color, panel_count in unhindered_panels_by_color.items(): if color is None: - event_name = room_name + " - " + str(panel_count) + " White Panels (Counted)" + event_name = f"{room_name} - {len(panel_count)} White Panels (Counted)" else: - event_name = room_name + " - " + str(panel_count) + " " + color.capitalize() + " Panels (Counted)" + event_name = f"{room_name} - {len(panel_count)} {color.capitalize()} Panels (Counted)" self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name) for panel_name in panel_count]) self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panels Solved" From 7f46151cc79d205fbd560db86ff8685621bdee29 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 14:00:17 -0500 Subject: [PATCH 06/19] Also flatten event door requirements --- worlds/lingo/player_logic.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index ae824b745e06..f4b521d0542d 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -60,6 +60,7 @@ class LingoPlayerLogic: FORCED_GOOD_ITEM: str PANEL_REQS: Dict[str, Dict[str, AccessRequirements]] + DOOR_REQS: Dict[str, Dict[str, AccessRequirements]] def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel]): counting_panels = 0 @@ -104,6 +105,7 @@ def __init__(self, world: "LingoWorld"): self.PAINTING_MAPPING = {} self.FORCED_GOOD_ITEM = "" self.PANEL_REQS = {} + self.DOOR_REQS = {} door_shuffle = world.options.shuffle_doors color_shuffle = world.options.shuffle_colors @@ -334,7 +336,13 @@ def calculate_panel_requirements(self, room: str, panel: str): access_reqs.rooms.add(req_room) for req_door in panel_object.required_doors: - access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door)) + door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door] + if door_object.event: + sub_access_reqs = self.calculate_door_requirements( + room if req_door.room is None else req_door.room, req_door.door) + access_reqs.merge(sub_access_reqs) + else: + access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door)) for color in panel_object.colors: access_reqs.colors.add(color) @@ -343,13 +351,31 @@ def calculate_panel_requirements(self, room: str, panel: str): if req_panel.room is not None and req_panel.room != room: access_reqs.rooms.add(req_panel.room) - sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, req_panel.panel) + sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, + req_panel.panel) access_reqs.merge(sub_access_reqs) self.PANEL_REQS[room][panel] = access_reqs return self.PANEL_REQS[room][panel] + def calculate_door_requirements(self, room: str, door: str): + if door not in self.DOOR_REQS.setdefault(room, {}): + access_reqs = AccessRequirements() + door_object = DOORS_BY_ROOM[room][door] + + for req_panel in door_object.panels: + if req_panel.room is not None and req_panel.room != room: + access_reqs.rooms.add(req_panel.room) + + sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, + req_panel.panel) + access_reqs.merge(sub_access_reqs) + + self.DOOR_REQS[room][door] = access_reqs + + return self.DOOR_REQS[room][door] + def create_panel_hunt_events(self, world: "LingoWorld"): for room_name, room_data in PANELS_BY_ROOM.items(): unhindered_panels_by_color: dict[Optional[str], List[str]] = {} From 5355852465078cd721a017b3ce3ba6ac3236ce3d Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 14:22:01 -0500 Subject: [PATCH 07/19] Added some documentation --- worlds/lingo/player_logic.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index f4b521d0542d..2bb8afeea4ea 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -63,6 +63,10 @@ class LingoPlayerLogic: DOOR_REQS: Dict[str, Dict[str, AccessRequirements]] def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel]): + """ + Creates a location. This function determines the access requirements for the location by combining and + flattening the requirements for each of the given panels. + """ counting_panels = 0 access_reqs = AccessRequirements() for panel in panels: @@ -328,6 +332,11 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: return True def calculate_panel_requirements(self, room: str, panel: str): + """ + Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in + the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required + by this panel. Memoization is used so that no panel is evaluated more than once. + """ if panel not in self.PANEL_REQS.setdefault(room, {}): access_reqs = AccessRequirements() panel_object = PANELS_BY_ROOM[room][panel] @@ -360,6 +369,9 @@ def calculate_panel_requirements(self, room: str, panel: str): return self.PANEL_REQS[room][panel] def calculate_door_requirements(self, room: str, door: str): + """ + Similar to calculate_panel_requirements, but for event doors. + """ if door not in self.DOOR_REQS.setdefault(room, {}): access_reqs = AccessRequirements() door_object = DOORS_BY_ROOM[room][door] @@ -377,6 +389,18 @@ def calculate_door_requirements(self, room: str, door: str): return self.DOOR_REQS[room][door] def create_panel_hunt_events(self, world: "LingoWorld"): + """ + Creates the event locations/items used for determining access to the LEVEL 2 panel. Instead of creating an event + for every single counting panel in the game, we try to coalesce panels with identical access rules into the same + event. Right now, this means the following: + + When color shuffle is off, panels in a room with no extra access requirements (room, door, or other panel) are + all coalesced into one event. + + When color shuffle is on, single-colored panels (including white) in a room are combined into one event per + color. Multicolored panels and panels with any extra access requirements are not coalesced, and will each + receive their own event. + """ for room_name, room_data in PANELS_BY_ROOM.items(): unhindered_panels_by_color: dict[Optional[str], List[str]] = {} From fb4001a41c038356943c52ddf5d8ffa691aabadf Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 16:08:31 -0500 Subject: [PATCH 08/19] Stop double-counting panels We shouldn't be assigning counts to non-event items. --- worlds/lingo/player_logic.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 2bb8afeea4ea..f4ded4f30b7b 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -67,21 +67,16 @@ def add_location(self, room: str, name: str, code: Optional[int], panels: List[R Creates a location. This function determines the access requirements for the location by combining and flattening the requirements for each of the given panels. """ - counting_panels = 0 access_reqs = AccessRequirements() for panel in panels: if panel.room is not None and panel.room != room: access_reqs.rooms.add(panel.room) panel_room = room if panel.room is None else panel.room - panel_object = PANELS_BY_ROOM[panel_room][panel.panel] - if not panel_object.non_counting: - counting_panels += 1 - sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel) access_reqs.merge(sub_access_reqs) - self.LOCATIONS_BY_ROOM.setdefault(room, []).append(PlayerLocation(name, code, access_reqs, counting_panels)) + self.LOCATIONS_BY_ROOM.setdefault(room, []).append(PlayerLocation(name, code, access_reqs, 0)) def set_door_item(self, room: str, door: str, item: str): self.ITEM_BY_DOOR.setdefault(room, {})[door] = item @@ -402,7 +397,7 @@ def create_panel_hunt_events(self, world: "LingoWorld"): receive their own event. """ for room_name, room_data in PANELS_BY_ROOM.items(): - unhindered_panels_by_color: dict[Optional[str], List[str]] = {} + unhindered_panels_by_color: dict[Optional[str], int] = {} for panel_name, panel_data in room_data.items(): # We won't count non-counting panels. @@ -415,21 +410,25 @@ def create_panel_hunt_events(self, world: "LingoWorld"): or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1): event_name = f"{room_name} - {panel_name} (Counted)" - self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" + self.LOCATIONS_BY_ROOM.setdefault(room_name, []).append( + PlayerLocation(event_name, None, self.calculate_panel_requirements(room_name, panel_name), 1)) else: if len(panel_data.colors) == 0 or not world.options.shuffle_colors: color = None else: color = panel_data.colors[0] - unhindered_panels_by_color.setdefault(color, []).append(panel_name) + unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1 for color, panel_count in unhindered_panels_by_color.items(): + access_reqs = AccessRequirements() if color is None: - event_name = f"{room_name} - {len(panel_count)} White Panels (Counted)" + event_name = f"{room_name} - {panel_count} White Panels (Counted)" else: - event_name = f"{room_name} - {len(panel_count)} {color.capitalize()} Panels (Counted)" - self.add_location(room_name, event_name, None, - [RoomAndPanel(room_name, panel_name) for panel_name in panel_count]) + event_name = f"{room_name} - {panel_count} {color.capitalize()} Panels (Counted)" + access_reqs.colors.add(color) + + self.LOCATIONS_BY_ROOM.setdefault(room_name, []).append(PlayerLocation(event_name, None, access_reqs, + panel_count)) self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panels Solved" From a756770ab9949a82c8b1a635c3114cd2ca5ed3ab Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 16:29:47 -0500 Subject: [PATCH 09/19] Some fixes from review comments --- worlds/lingo/player_logic.py | 6 +++--- worlds/lingo/regions.py | 2 +- worlds/lingo/rules.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index f4ded4f30b7b..8cee72c92051 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -33,9 +33,9 @@ def __str__(self): class PlayerLocation(NamedTuple): name: str - code: Optional[int] = None - access: AccessRequirements = () - counting_panels: int = 0 + code: Optional[int] + access: AccessRequirements + counting_panels: int class LingoPlayerLogic: diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index b462c05186de..18cb0e9e50ce 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -16,7 +16,7 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): new_location = LingoLocation(world.player, location.name, location.code, new_region) new_location.counting_panels = location.counting_panels - new_location.access_rule = make_location_lambda(location, room.name, world, player_logic) + new_location.access_rule = make_location_lambda(location, world, player_logic) new_region.locations.append(new_location) if location.name in player_logic.EVENT_LOC_TO_ITEM: event_name = player_logic.EVENT_LOC_TO_ITEM[location.name] diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 16fa0129fe80..4adcb4f7d2b8 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -56,7 +56,7 @@ def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): - return sum([location.counting_panels for location in state.locations_checked])\ + return sum(location.counting_panels for location in state.locations_checked)\ >= world.options.level_2_requirement.value @@ -72,7 +72,7 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: i return state.has(item_name, player) -def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic): +def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): if location.name == player_logic.MASTERY_LOCATION: return lambda state: lingo_can_use_mastery_location(state, world) From c920ee0e337be89fac4ffb6e01abd285fcbdf3d0 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 16:32:45 -0500 Subject: [PATCH 10/19] Fix naming convention of LingoPlayerLogic instance variables --- worlds/lingo/__init__.py | 10 +-- worlds/lingo/player_logic.py | 132 +++++++++++++++++------------------ worlds/lingo/regions.py | 8 +-- worlds/lingo/rules.py | 8 +-- 4 files changed, 79 insertions(+), 79 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 1f426c92f24a..788f61beb977 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -55,14 +55,14 @@ def create_regions(self): create_regions(self, self.player_logic) def create_items(self): - pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS] + pool = [self.create_item(name) for name in self.player_logic.real_items] - if self.player_logic.FORCED_GOOD_ITEM != "": - new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM) + if self.player_logic.forced_good_item != "": + new_item = self.create_item(self.player_logic.forced_good_item) location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player) location_obj.place_locked_item(new_item) - item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool) + item_difference = len(self.player_logic.real_locations) - len(pool) if item_difference: trap_percentage = self.options.trap_percentage traps = int(item_difference * trap_percentage / 100.0) @@ -107,6 +107,6 @@ def fill_slot_data(self): } if self.options.shuffle_paintings: - slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING + slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping return slot_data diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 8cee72c92051..b79b22bb4d54 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -43,24 +43,24 @@ class LingoPlayerLogic: Defines logic after a player's options have been applied """ - ITEM_BY_DOOR: Dict[str, Dict[str, str]] + item_by_door: Dict[str, Dict[str, str]] - LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]] - REAL_LOCATIONS: List[str] + locations_by_room: Dict[str, List[PlayerLocation]] + real_locations: List[str] - EVENT_LOC_TO_ITEM: Dict[str, str] - REAL_ITEMS: List[str] + event_loc_to_item: Dict[str, str] + real_items: List[str] - VICTORY_CONDITION: str - MASTERY_LOCATION: str - LEVEL_2_LOCATION: str + victory_condition: str + mastery_location: str + level_2_location: str - PAINTING_MAPPING: Dict[str, str] + painting_mapping: Dict[str, str] - FORCED_GOOD_ITEM: str + forced_good_item: str - PANEL_REQS: Dict[str, Dict[str, AccessRequirements]] - DOOR_REQS: Dict[str, Dict[str, AccessRequirements]] + panel_reqs: Dict[str, Dict[str, AccessRequirements]] + door_reqs: Dict[str, Dict[str, AccessRequirements]] def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel]): """ @@ -76,10 +76,10 @@ def add_location(self, room: str, name: str, code: Optional[int], panels: List[R sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel) access_reqs.merge(sub_access_reqs) - self.LOCATIONS_BY_ROOM.setdefault(room, []).append(PlayerLocation(name, code, access_reqs, 0)) + self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs, 0)) def set_door_item(self, room: str, door: str, item: str): - self.ITEM_BY_DOOR.setdefault(room, {})[door] = item + self.item_by_door.setdefault(room, {})[door] = item def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: @@ -88,23 +88,23 @@ def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "Lingo else: progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name self.set_door_item(room_name, door_data.name, progressive_item_name) - self.REAL_ITEMS.append(progressive_item_name) + self.real_items.append(progressive_item_name) else: self.set_door_item(room_name, door_data.name, door_data.item_name) def __init__(self, world: "LingoWorld"): - self.ITEM_BY_DOOR = {} - self.LOCATIONS_BY_ROOM = {} - self.REAL_LOCATIONS = [] - self.EVENT_LOC_TO_ITEM = {} - self.REAL_ITEMS = [] - self.VICTORY_CONDITION = "" - self.MASTERY_LOCATION = "" - self.LEVEL_2_LOCATION = "" - self.PAINTING_MAPPING = {} - self.FORCED_GOOD_ITEM = "" - self.PANEL_REQS = {} - self.DOOR_REQS = {} + self.item_by_door = {} + self.locations_by_room = {} + self.real_locations = [] + self.event_loc_to_item = {} + self.real_items = [] + self.victory_condition = "" + self.mastery_location = "" + self.level_2_location = "" + self.painting_mapping = {} + self.forced_good_item = "" + self.panel_reqs = {} + self.door_reqs = {} door_shuffle = world.options.shuffle_doors color_shuffle = world.options.shuffle_colors @@ -124,7 +124,7 @@ def __init__(self, world: "LingoWorld"): if door_shuffle == ShuffleDoors.option_none: itemloc_name = f"{room_name} - {door_name} (Opened)" self.add_location(room_name, itemloc_name, None, door_data.panels) - self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name + self.event_loc_to_item[itemloc_name] = itemloc_name self.set_door_item(room_name, door_name, itemloc_name) else: # This line is duplicated from StaticLingoItems @@ -138,7 +138,7 @@ def __init__(self, world: "LingoWorld"): if door_data.event: event_item_name = f"{door_data.item_name} (Opened)" self.add_location(room_name, door_data.item_name, None, door_data.panels) - self.EVENT_LOC_TO_ITEM[door_data.item_name] = event_item_name + self.event_loc_to_item[door_data.item_name] = event_item_name self.set_door_item(room_name, door_name, event_item_name) # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. @@ -147,29 +147,29 @@ def __init__(self, world: "LingoWorld"): if panel_data.achievement: event_name = f"{room_name} - {panel_name} (Achieved)" self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) - self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" + self.event_loc_to_item[event_name] = "Mastery Achievement" # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. - self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" - self.LEVEL_2_LOCATION = "N/A" + self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" + self.level_2_location = "N/A" if victory_condition == VictoryCondition.option_the_end: - self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" + self.victory_condition = "Orange Tower Seventh Floor - THE END" self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, []) - self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" + self.event_loc_to_item["The End (Solved)"] = "Victory" elif victory_condition == VictoryCondition.option_the_master: - self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" - self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" + self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" + self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements" - self.add_location("Orange Tower Seventh Floor", self.MASTERY_LOCATION, None, []) - self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" + self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, []) + self.event_loc_to_item[self.mastery_location] = "Victory" elif victory_condition == VictoryCondition.option_level_2: - self.VICTORY_CONDITION = "Second Room - LEVEL 2" - self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" + self.victory_condition = "Second Room - LEVEL 2" + self.level_2_location = "Second Room - Unlock Level 2" - self.add_location("Second Room", self.LEVEL_2_LOCATION, None, [RoomAndPanel("Second Room", "LEVEL 2")]) - self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" + self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")]) + self.event_loc_to_item[self.level_2_location] = "Victory" self.create_panel_hunt_events(world) @@ -181,17 +181,17 @@ def __init__(self, world: "LingoWorld"): location_classification = LocationClassification.insanity for location_name, location_data in ALL_LOCATION_TABLE.items(): - if location_name != self.VICTORY_CONDITION: + if location_name != self.victory_condition: if location_classification not in location_data.classification: continue self.add_location(location_data.room, location_name, location_data.code, location_data.panels) - self.REAL_LOCATIONS.append(location_name) + self.real_locations.append(location_name) # Instantiate all real items. for name, item in ALL_ITEM_TABLE.items(): if item.should_include(world): - self.REAL_ITEMS.append(name) + self.real_items.append(name) # Create the paintings mapping, if painting shuffle is on. if painting_shuffle: @@ -232,7 +232,7 @@ def __init__(self, world: "LingoWorld"): continue # If painting shuffle is on, we only want to consider paintings that actually go somewhere. - if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys(): + if painting_shuffle and painting_obj.id not in self.painting_mapping.keys(): continue pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] @@ -257,12 +257,12 @@ def __init__(self, world: "LingoWorld"): good_item_options.remove(item) if len(good_item_options) > 0: - self.FORCED_GOOD_ITEM = world.random.choice(good_item_options) - self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM) - self.REAL_LOCATIONS.remove("Second Room - Good Luck") + self.forced_good_item = world.random.choice(good_item_options) + self.real_items.remove(self.forced_good_item) + self.real_locations.remove("Second Room - Good Luck") def randomize_paintings(self, world: "LingoWorld") -> bool: - self.PAINTING_MAPPING.clear() + self.painting_mapping.clear() door_shuffle = world.options.shuffle_doors @@ -284,7 +284,7 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: if painting.exit_only and painting.required] req_entrances = world.random.sample(req_enterable, len(req_exits)) - self.PAINTING_MAPPING = dict(zip(req_entrances, req_exits)) + self.painting_mapping = dict(zip(req_entrances, req_exits)) # Next, determine the rest of the exit paintings. exitable = [painting_id for painting_id, painting in PAINTINGS.items() @@ -303,23 +303,23 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: for warp_exit in nonreq_exits: warp_enter = world.random.choice(chosen_entrances) chosen_entrances.remove(warp_enter) - self.PAINTING_MAPPING[warp_enter] = warp_exit + self.painting_mapping[warp_enter] = warp_exit # Assign each of the remaining entrances to any required or non-required exit. for warp_enter in chosen_entrances: warp_exit = world.random.choice(chosen_exits) - self.PAINTING_MAPPING[warp_enter] = warp_exit + self.painting_mapping[warp_enter] = warp_exit # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall # is forced to point to the vanilla exit. - if "eye_painting_2" not in self.PAINTING_MAPPING.keys(): - self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2" + if "eye_painting_2" not in self.painting_mapping.keys(): + self.painting_mapping["eye_painting"] = "eye_painting_2" # Just for sanity's sake, ensure that all required painting rooms are accessed. for painting_id, painting in PAINTINGS.items(): - if painting_id not in self.PAINTING_MAPPING.values() \ + if painting_id not in self.painting_mapping.values() \ and (painting.required or (painting.required_when_no_doors and door_shuffle == ShuffleDoors.option_none)): return False @@ -332,7 +332,7 @@ def calculate_panel_requirements(self, room: str, panel: str): the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required by this panel. Memoization is used so that no panel is evaluated more than once. """ - if panel not in self.PANEL_REQS.setdefault(room, {}): + if panel not in self.panel_reqs.setdefault(room, {}): access_reqs = AccessRequirements() panel_object = PANELS_BY_ROOM[room][panel] @@ -359,15 +359,15 @@ def calculate_panel_requirements(self, room: str, panel: str): req_panel.panel) access_reqs.merge(sub_access_reqs) - self.PANEL_REQS[room][panel] = access_reqs + self.panel_reqs[room][panel] = access_reqs - return self.PANEL_REQS[room][panel] + return self.panel_reqs[room][panel] def calculate_door_requirements(self, room: str, door: str): """ Similar to calculate_panel_requirements, but for event doors. """ - if door not in self.DOOR_REQS.setdefault(room, {}): + if door not in self.door_reqs.setdefault(room, {}): access_reqs = AccessRequirements() door_object = DOORS_BY_ROOM[room][door] @@ -379,9 +379,9 @@ def calculate_door_requirements(self, room: str, door: str): req_panel.panel) access_reqs.merge(sub_access_reqs) - self.DOOR_REQS[room][door] = access_reqs + self.door_reqs[room][door] = access_reqs - return self.DOOR_REQS[room][door] + return self.door_reqs[room][door] def create_panel_hunt_events(self, world: "LingoWorld"): """ @@ -410,8 +410,8 @@ def create_panel_hunt_events(self, world: "LingoWorld"): or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1): event_name = f"{room_name} - {panel_name} (Counted)" - self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" - self.LOCATIONS_BY_ROOM.setdefault(room_name, []).append( + self.event_loc_to_item[event_name] = "Counting Panel Solved" + self.locations_by_room.setdefault(room_name, []).append( PlayerLocation(event_name, None, self.calculate_panel_requirements(room_name, panel_name), 1)) else: if len(panel_data.colors) == 0 or not world.options.shuffle_colors: @@ -429,6 +429,6 @@ def create_panel_hunt_events(self, world: "LingoWorld"): event_name = f"{room_name} - {panel_count} {color.capitalize()} Panels (Counted)" access_reqs.colors.add(color) - self.LOCATIONS_BY_ROOM.setdefault(room_name, []).append(PlayerLocation(event_name, None, access_reqs, + self.locations_by_room.setdefault(room_name, []).append(PlayerLocation(event_name, None, access_reqs, panel_count)) - self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panels Solved" + self.event_loc_to_item[event_name] = "Counting Panels Solved" diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 18cb0e9e50ce..8a69e3c08336 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -13,13 +13,13 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: new_region = Region(room.name, world.player, world.multiworld) - for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): + for location in player_logic.locations_by_room.get(room.name, {}): new_location = LingoLocation(world.player, location.name, location.code, new_region) new_location.counting_panels = location.counting_panels new_location.access_rule = make_location_lambda(location, world, player_logic) new_region.locations.append(new_location) - if location.name in player_logic.EVENT_LOC_TO_ITEM: - event_name = player_logic.EVENT_LOC_TO_ITEM[location.name] + if location.name in player_logic.event_loc_to_item: + event_name = player_logic.event_loc_to_item[location.name] event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) new_location.place_locked_item(event_item) @@ -86,7 +86,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") if painting_shuffle: - for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items(): + for warp_enter, warp_exit in player_logic.painting_mapping.items(): connect_painting(regions, warp_enter, warp_exit, world, player_logic) world.multiworld.regions += regions.values() diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 4adcb4f7d2b8..af17402d8d7d 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -27,7 +27,7 @@ def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: ["Outside The Agreeable", "Tenacious Entrance"] ] for entrance in fake_pilgrimage: - if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player): + if not state.has(player_logic.item_by_door[entrance[0]][entrance[1]], player): return False return True @@ -64,7 +64,7 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: i """ Determines whether a door can be opened """ - item_name = player_logic.ITEM_BY_DOOR[room][door] + item_name = player_logic.item_by_door[room][door] if item_name in PROGRESSIVE_ITEMS: progression = PROGRESSION_BY_ROOM[room][door] return state.has(item_name, player, progression.index) @@ -73,10 +73,10 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: i def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): - if location.name == player_logic.MASTERY_LOCATION: + if location.name == player_logic.mastery_location: return lambda state: lingo_can_use_mastery_location(state, world) - if location.name == player_logic.LEVEL_2_LOCATION: + if location.name == player_logic.level_2_location: return lambda state: lingo_can_use_level_2_location(state, world) return lambda state: lingo_can_use_location(state, location, world, player_logic) From 0aa02ac8f4ef0ab3610de39099f6ffcb0bd6422c Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 Nov 2023 17:07:27 -0500 Subject: [PATCH 11/19] Maintain panel count via items instead of locations --- worlds/lingo/__init__.py | 12 ++++++++++++ worlds/lingo/locations.py | 1 - worlds/lingo/player_logic.py | 12 +++++------- worlds/lingo/regions.py | 1 - worlds/lingo/rules.py | 3 +-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 788f61beb977..daa2f96f2dc6 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -110,3 +110,15 @@ def fill_slot_data(self): slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping return slot_data + + def collect(self, state, item: Item) -> bool: + if item.name.endswith("Counting Panels Solved"): + state.prog_items[self.player]["COUNTING PANELS"] += int(item.name.rstrip(" Counting Panels Solved")) + return True + return super().collect(state, item) + + def remove(self, state, item: Item) -> bool: + if item.name.endswith("Counting Panels Solved"): + state.prog_items[self.player]["COUNTING PANELS"] -= int(item.name.rstrip(" Counting Panels Solved")) + return True + return super().remove(state, item) diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py index 685dc3537c0a..5903d603ec4f 100644 --- a/worlds/lingo/locations.py +++ b/worlds/lingo/locations.py @@ -34,7 +34,6 @@ class LingoLocation(Location): Location from the game Lingo """ game: str = "Lingo" - counting_panels: int = 0 ALL_LOCATION_TABLE: Dict[str, LocationData] = {} diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b79b22bb4d54..dad68f8b5ca4 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -35,7 +35,6 @@ class PlayerLocation(NamedTuple): name: str code: Optional[int] access: AccessRequirements - counting_panels: int class LingoPlayerLogic: @@ -76,7 +75,7 @@ def add_location(self, room: str, name: str, code: Optional[int], panels: List[R sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel) access_reqs.merge(sub_access_reqs) - self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs, 0)) + self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs)) def set_door_item(self, room: str, door: str, item: str): self.item_by_door.setdefault(room, {})[door] = item @@ -410,9 +409,9 @@ def create_panel_hunt_events(self, world: "LingoWorld"): or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1): event_name = f"{room_name} - {panel_name} (Counted)" - self.event_loc_to_item[event_name] = "Counting Panel Solved" + self.event_loc_to_item[event_name] = "1 Counting Panels Solved" self.locations_by_room.setdefault(room_name, []).append( - PlayerLocation(event_name, None, self.calculate_panel_requirements(room_name, panel_name), 1)) + PlayerLocation(event_name, None, self.calculate_panel_requirements(room_name, panel_name))) else: if len(panel_data.colors) == 0 or not world.options.shuffle_colors: color = None @@ -429,6 +428,5 @@ def create_panel_hunt_events(self, world: "LingoWorld"): event_name = f"{room_name} - {panel_count} {color.capitalize()} Panels (Counted)" access_reqs.colors.add(color) - self.locations_by_room.setdefault(room_name, []).append(PlayerLocation(event_name, None, access_reqs, - panel_count)) - self.event_loc_to_item[event_name] = "Counting Panels Solved" + self.locations_by_room.setdefault(room_name, []).append(PlayerLocation(event_name, None, access_reqs)) + self.event_loc_to_item[event_name] = f"{panel_count} Counting Panels Solved" diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 8a69e3c08336..47a94e88b8ff 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -15,7 +15,6 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi new_region = Region(room.name, world.player, world.multiworld) for location in player_logic.locations_by_room.get(room.name, {}): new_location = LingoLocation(world.player, location.name, location.code, new_region) - new_location.counting_panels = location.counting_panels new_location.access_rule = make_location_lambda(location, world, player_logic) new_region.locations.append(new_location) if location.name in player_logic.event_loc_to_item: diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index af17402d8d7d..6abe03fbfd17 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -56,8 +56,7 @@ def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): - return sum(location.counting_panels for location in state.locations_checked)\ - >= world.options.level_2_requirement.value + return state.has("COUNTING PANELS", world.player, world.options.level_2_requirement.value) def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: int, player_logic: LingoPlayerLogic): From 16c7d9359545f7e94982346e6b567d0bbb7b4136 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 20 Nov 2023 13:10:40 -0500 Subject: [PATCH 12/19] Fixed ANOTHER TRY behaviour in panelsanity This change accidentally broke how ANOTHER TRY works in panelsanity. It needs to stay unavailable until the panel hunt is completed, just like LEVEL 2. This commit fixes that. --- worlds/lingo/LL1.yaml | 2 -- worlds/lingo/rules.py | 6 ++++-- worlds/lingo/test/TestPanelsanity.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 worlds/lingo/test/TestPanelsanity.py diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/LL1.yaml index f8b07b86514b..f71bb639d91f 100644 --- a/worlds/lingo/LL1.yaml +++ b/worlds/lingo/LL1.yaml @@ -377,8 +377,6 @@ tag: forbid non_counting: True check: True - required_panel: - - panel: ANOTHER TRY doors: Exit Door: id: Entry Room Area Doors/Door_hi_high diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 6abe03fbfd17..2c783fac4552 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState +from .options import VictoryCondition from .player_logic import LingoPlayerLogic, PlayerLocation from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor @@ -56,7 +57,7 @@ def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): - return state.has("COUNTING PANELS", world.player, world.options.level_2_requirement.value) + return state.has("COUNTING PANELS", world.player, world.options.level_2_requirement.value - 1) def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: int, player_logic: LingoPlayerLogic): @@ -75,7 +76,8 @@ def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_l if location.name == player_logic.mastery_location: return lambda state: lingo_can_use_mastery_location(state, world) - if location.name == player_logic.level_2_location: + if world.options.victory_condition == VictoryCondition.option_level_2\ + and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location): return lambda state: lingo_can_use_level_2_location(state, world) return lambda state: lingo_can_use_location(state, location, world, player_logic) diff --git a/worlds/lingo/test/TestPanelsanity.py b/worlds/lingo/test/TestPanelsanity.py new file mode 100644 index 000000000000..34c1b3815a46 --- /dev/null +++ b/worlds/lingo/test/TestPanelsanity.py @@ -0,0 +1,19 @@ +from . import LingoTestBase + + +class TestPanelHunt(LingoTestBase): + options = { + "shuffle_doors": "complex", + "location_checks": "insanity", + "victory_condition": "level_2", + "level_2_requirement": "15" + } + + def test_another_try(self) -> None: + self.collect_by_name("The Traveled - Entrance") # idk why this is needed + self.assertFalse(self.can_reach_location("Second Room - ANOTHER TRY")) + self.assertFalse(self.can_reach_location("Second Room - Unlock Level 2")) + + self.collect_by_name("Second Room - Exit Door") + self.assertTrue(self.can_reach_location("Second Room - ANOTHER TRY")) + self.assertTrue(self.can_reach_location("Second Room - Unlock Level 2")) From de1b341ceebfb630accf54ec4c7c38fcf33b34de Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 22 Nov 2023 11:17:24 -0500 Subject: [PATCH 13/19] Allow LEVEL 2 panel hunt when it isn't the wincon It is still disabled by default (for generation optimisation), but you can set the level 2 requirement higher than 1 in order to enable it. --- worlds/lingo/options.py | 5 +++-- worlds/lingo/player_logic.py | 6 +++++- worlds/lingo/rules.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 7dc6a1389c0c..c4d091252cc6 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -75,11 +75,12 @@ class Level2Requirement(Range): """The number of panel solves required to unlock LEVEL 2. In the base game, 223 are needed. Note that this count includes ANOTHER TRY. + When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free. """ display_name = "Level 2 Requirement" - range_start = 2 + range_start = 1 range_end = 800 - default = 223 + default = 1 class EarlyColorHallways(Toggle): diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index dad68f8b5ca4..ac18b3dc2979 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -151,7 +151,7 @@ def __init__(self, world: "LingoWorld"): # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" - self.level_2_location = "N/A" + self.level_2_location = "Second Room - LEVEL 2" if victory_condition == VictoryCondition.option_the_end: self.victory_condition = "Orange Tower Seventh Floor - THE END" @@ -170,6 +170,10 @@ def __init__(self, world: "LingoWorld"): self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")]) self.event_loc_to_item[self.level_2_location] = "Victory" + if world.options.level_2_requirement == 1: + raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.") + + if world.options.level_2_requirement > 1: self.create_panel_hunt_events(world) # Instantiate all real locations. diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 2c783fac4552..62900e9e798f 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -76,7 +76,7 @@ def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_l if location.name == player_logic.mastery_location: return lambda state: lingo_can_use_mastery_location(state, world) - if world.options.victory_condition == VictoryCondition.option_level_2\ + if world.options.level_2_requirement > 1\ and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location): return lambda state: lingo_can_use_level_2_location(state, world) From 2de68d3eaf7f0d8ab99f50b7fc2ba1b37e4eb488 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 22 Nov 2023 11:20:07 -0500 Subject: [PATCH 14/19] Better documentation for VictoryCondition option --- worlds/lingo/options.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index c4d091252cc6..f580c77fc582 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -52,7 +52,10 @@ class ShufflePaintings(Toggle): class VictoryCondition(Choice): - """Change the victory condition.""" + """Change the victory condition. + On "the_end", the goal is to solve THE END at the top of the tower. + On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option. + On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option.""" display_name = "Victory Condition" option_the_end = 0 option_the_master = 1 From cea745461b39953db300f769f48a39dc98420c1d Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 13:26:14 -0500 Subject: [PATCH 15/19] Style was changed in merge --- worlds/lingo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index ba22346385bd..8c33e5cce4e8 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -93,7 +93,7 @@ def create_item(self, name: str) -> Item: classification = item.classification if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ - and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.PAINTING_MAPPING + and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping for painting_id in item.painting_ids): # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings # go nowhere, then this item should not be progression. From 2590691c8fefaf379e222acc3c968b3095842b02 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 16:58:53 -0500 Subject: [PATCH 16/19] Got rid of door events This is a significant speedup for vanilla doors, and a minor speedup for simple/complex doors. --- worlds/lingo/player_logic.py | 56 +++++++++++++++--------------------- worlds/lingo/regions.py | 38 +++++++++++++++--------- worlds/lingo/rules.py | 53 ++++++++++++++++++++-------------- 3 files changed, 79 insertions(+), 68 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index ac18b3dc2979..fecb9466046a 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -61,7 +61,7 @@ class LingoPlayerLogic: panel_reqs: Dict[str, Dict[str, AccessRequirements]] door_reqs: Dict[str, Dict[str, AccessRequirements]] - def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel]): + def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"): """ Creates a location. This function determines the access requirements for the location by combining and flattening the requirements for each of the given panels. @@ -72,7 +72,7 @@ def add_location(self, room: str, name: str, code: Optional[int], panels: List[R access_reqs.rooms.add(panel.room) panel_room = room if panel.room is None else panel.room - sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel) + sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel, world) access_reqs.merge(sub_access_reqs) self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs)) @@ -120,32 +120,20 @@ def __init__(self, world: "LingoWorld"): # doors that are event-only. for room_name, room_data in DOORS_BY_ROOM.items(): for door_name, door_data in room_data.items(): - if door_shuffle == ShuffleDoors.option_none: - itemloc_name = f"{room_name} - {door_name} (Opened)" - self.add_location(room_name, itemloc_name, None, door_data.panels) - self.event_loc_to_item[itemloc_name] = itemloc_name - self.set_door_item(room_name, door_name, itemloc_name) - else: - # This line is duplicated from StaticLingoItems - if door_data.skip_item is False and door_data.event is False: - if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: - # Grouped doors are handled differently if shuffle doors is on simple. - self.set_door_item(room_name, door_name, door_data.group) - else: - self.handle_non_grouped_door(room_name, door_data, world) - - if door_data.event: - event_item_name = f"{door_data.item_name} (Opened)" - self.add_location(room_name, door_data.item_name, None, door_data.panels) - self.event_loc_to_item[door_data.item_name] = event_item_name - self.set_door_item(room_name, door_name, event_item_name) + if door_data.skip_item is False and door_data.event is False\ + and door_shuffle != ShuffleDoors.option_none: + if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: + # Grouped doors are handled differently if shuffle doors is on simple. + self.set_door_item(room_name, door_name, door_data.group) + else: + self.handle_non_grouped_door(room_name, door_data, world) # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. for room_name, room_data in PANELS_BY_ROOM.items(): for panel_name, panel_data in room_data.items(): if panel_data.achievement: event_name = f"{room_name} - {panel_name} (Achieved)" - self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)]) + self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)], world) self.event_loc_to_item[event_name] = "Mastery Achievement" # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need @@ -155,19 +143,20 @@ def __init__(self, world: "LingoWorld"): if victory_condition == VictoryCondition.option_the_end: self.victory_condition = "Orange Tower Seventh Floor - THE END" - self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, []) + self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) self.event_loc_to_item["The End (Solved)"] = "Victory" elif victory_condition == VictoryCondition.option_the_master: self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements" - self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, []) + self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, [], world) self.event_loc_to_item[self.mastery_location] = "Victory" elif victory_condition == VictoryCondition.option_level_2: self.victory_condition = "Second Room - LEVEL 2" self.level_2_location = "Second Room - Unlock Level 2" - self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")]) + self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")], + world) self.event_loc_to_item[self.level_2_location] = "Victory" if world.options.level_2_requirement == 1: @@ -188,7 +177,7 @@ def __init__(self, world: "LingoWorld"): if location_classification not in location_data.classification: continue - self.add_location(location_data.room, location_name, location_data.code, location_data.panels) + self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world) self.real_locations.append(location_name) # Instantiate all real items. @@ -329,7 +318,7 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: return True - def calculate_panel_requirements(self, room: str, panel: str): + def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld"): """ Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required @@ -344,9 +333,9 @@ def calculate_panel_requirements(self, room: str, panel: str): for req_door in panel_object.required_doors: door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door] - if door_object.event: + if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: sub_access_reqs = self.calculate_door_requirements( - room if req_door.room is None else req_door.room, req_door.door) + room if req_door.room is None else req_door.room, req_door.door, world) access_reqs.merge(sub_access_reqs) else: access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door)) @@ -359,14 +348,14 @@ def calculate_panel_requirements(self, room: str, panel: str): access_reqs.rooms.add(req_panel.room) sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, - req_panel.panel) + req_panel.panel, world) access_reqs.merge(sub_access_reqs) self.panel_reqs[room][panel] = access_reqs return self.panel_reqs[room][panel] - def calculate_door_requirements(self, room: str, door: str): + def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld"): """ Similar to calculate_panel_requirements, but for event doors. """ @@ -379,7 +368,7 @@ def calculate_door_requirements(self, room: str, door: str): access_reqs.rooms.add(req_panel.room) sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, - req_panel.panel) + req_panel.panel, world) access_reqs.merge(sub_access_reqs) self.door_reqs[room][door] = access_reqs @@ -415,7 +404,8 @@ def create_panel_hunt_events(self, world: "LingoWorld"): event_name = f"{room_name} - {panel_name} (Counted)" self.event_loc_to_item[event_name] = "1 Counting Panels Solved" self.locations_by_room.setdefault(room_name, []).append( - PlayerLocation(event_name, None, self.calculate_panel_requirements(room_name, panel_name))) + PlayerLocation(event_name, None, self.calculate_panel_requirements(room_name, panel_name, + world))) else: if len(panel_data.colors) == 0 or not world.options.shuffle_colors: color = None diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 47a94e88b8ff..c24144a1609e 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -1,11 +1,11 @@ -from typing import Dict, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING -from BaseClasses import ItemClassification, Region +from BaseClasses import Entrance, ItemClassification, Region from .items import LingoItem from .locations import LingoLocation from .player_logic import LingoPlayerLogic from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda -from .static_logic import ALL_ROOMS, PAINTINGS, Room +from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor if TYPE_CHECKING: from . import LingoWorld @@ -31,7 +31,22 @@ def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_ source_region.connect( target_region, "Pilgrimage", - lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic)) + lambda state: lingo_can_use_pilgrimage(state, world, player_logic)) + + +def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, + door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): + connection = Entrance(world.player, description, source_region) + connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic) + + source_region.exits.append(connection) + connection.connect(target_region) + + if door is not None: + effective_room = target_region.name if door.room is None else door.room + if door.door not in player_logic.item_by_door.get(effective_room, {}): + for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: + world.multiworld.register_indirect_condition(regions[region], connection) def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld", @@ -41,11 +56,10 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str target_region = regions[target_painting.room] source_region = regions[source_painting.room] - source_region.connect( - target_region, - f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)", - lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player, - player_logic)) + + entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)" + connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world, + player_logic) def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: @@ -74,10 +88,8 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: else: entrance_name += f" (through {room.name} - {entrance.door.door})" - regions[entrance.room].connect( - regions[room.name], entrance_name, - lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, - player_logic)) + connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, + player_logic) handle_pilgrim_room(regions, world, player_logic) diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 62900e9e798f..f3beeec8ea9a 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -1,23 +1,23 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState -from .options import VictoryCondition -from .player_logic import LingoPlayerLogic, PlayerLocation +from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor if TYPE_CHECKING: from . import LingoWorld -def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int, +def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld", player_logic: LingoPlayerLogic): if door is None: return True - return _lingo_can_open_door(state, room if door.room is None else door.room, door.door, player, player_logic) + effective_room = room if door.room is None else door.room + return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) -def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic): +def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): fake_pilgrimage = [ ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], @@ -28,7 +28,7 @@ def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: ["Outside The Agreeable", "Tenacious Entrance"] ] for entrance in fake_pilgrimage: - if not state.has(player_logic.item_by_door[entrance[0]][entrance[1]], player): + if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic): return False return True @@ -36,40 +36,49 @@ def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): - for req_room in location.access.rooms: + return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) + + +def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): + return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) + + +def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): + return state.has("COUNTING PANELS", world.player, world.options.level_2_requirement.value - 1) + + +def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld", + player_logic: LingoPlayerLogic): + for req_room in access.rooms: if not state.can_reach(req_room, "Region", world.player): return False - for req_door in location.access.doors: - if not _lingo_can_open_door(state, req_door.room, req_door.door, world.player, player_logic): + for req_door in access.doors: + if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic): return False - if len(location.access.colors) > 0 and world.options.shuffle_colors: - for color in location.access.colors: + if len(access.colors) > 0 and world.options.shuffle_colors: + for color in access.colors: if not state.has(color.capitalize(), world.player): return False return True -def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): - return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) - - -def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): - return state.has("COUNTING PANELS", world.player, world.options.level_2_requirement.value - 1) - - -def _lingo_can_open_door(state: CollectionState, room: str, door: str, player: int, player_logic: LingoPlayerLogic): +def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld", + player_logic: LingoPlayerLogic): """ Determines whether a door can be opened """ + if door not in player_logic.item_by_door.get(room, {}): + return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic) + item_name = player_logic.item_by_door[room][door] if item_name in PROGRESSIVE_ITEMS: progression = PROGRESSION_BY_ROOM[room][door] - return state.has(item_name, player, progression.index) + return state.has(item_name, world.player, progression.index) - return state.has(item_name, player) + return state.has(item_name, world.player) def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): From 926d868a518734f05a704b3aac75a2f387ba17ae Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 17:28:45 -0500 Subject: [PATCH 17/19] No reason not to move the check outside --- worlds/lingo/player_logic.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index fecb9466046a..98dd328c8e16 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -116,17 +116,16 @@ def __init__(self, world: "LingoWorld"): raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " "be enough locations for all of the door items.") - # Create an event for every door, representing whether that door has been opened. Also create event items for - # doors that are event-only. - for room_name, room_data in DOORS_BY_ROOM.items(): - for door_name, door_data in room_data.items(): - if door_data.skip_item is False and door_data.event is False\ - and door_shuffle != ShuffleDoors.option_none: - if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: - # Grouped doors are handled differently if shuffle doors is on simple. - self.set_door_item(room_name, door_name, door_data.group) - else: - self.handle_non_grouped_door(room_name, door_data, world) + # Create door items, where needed. + if door_shuffle != ShuffleDoors.option_none: + for room_name, room_data in DOORS_BY_ROOM.items(): + for door_name, door_data in room_data.items(): + if door_data.skip_item is False and door_data.event is False: + if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: + # Grouped doors are handled differently if shuffle doors is on simple. + self.set_door_item(room_name, door_name, door_data.group) + else: + self.handle_non_grouped_door(room_name, door_data, world) # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. for room_name, room_data in PANELS_BY_ROOM.items(): From c44c20d4f0954292601642e1f183b6c607c00111 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 18:38:47 -0500 Subject: [PATCH 18/19] Get rid of counting panel events --- worlds/lingo/__init__.py | 12 ------------ worlds/lingo/player_logic.py | 33 ++++++++++++++++----------------- worlds/lingo/rules.py | 28 ++++++++++++++++++++-------- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 8c33e5cce4e8..da8a246e79c0 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -119,15 +119,3 @@ def fill_slot_data(self): slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping return slot_data - - def collect(self, state, item: Item) -> bool: - if item.name.endswith("Counting Panels Solved"): - state.prog_items[self.player]["COUNTING PANELS"] += int(item.name.rstrip(" Counting Panels Solved")) - return True - return super().collect(state, item) - - def remove(self, state, item: Item) -> bool: - if item.name.endswith("Counting Panels Solved"): - state.prog_items[self.player]["COUNTING PANELS"] -= int(item.name.rstrip(" Counting Panels Solved")) - return True - return super().remove(state, item) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 98dd328c8e16..a0b33d1dbe58 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -1,4 +1,4 @@ -from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING +from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING from .items import ALL_ITEM_TABLE from .locations import ALL_LOCATION_TABLE, LocationClassification @@ -60,6 +60,8 @@ class LingoPlayerLogic: panel_reqs: Dict[str, Dict[str, AccessRequirements]] door_reqs: Dict[str, Dict[str, AccessRequirements]] + mastery_reqs: List[AccessRequirements] + counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]] def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"): """ @@ -104,6 +106,8 @@ def __init__(self, world: "LingoWorld"): self.forced_good_item = "" self.panel_reqs = {} self.door_reqs = {} + self.mastery_reqs = [] + self.counting_panel_reqs = {} door_shuffle = world.options.shuffle_doors color_shuffle = world.options.shuffle_colors @@ -131,9 +135,11 @@ def __init__(self, world: "LingoWorld"): for room_name, room_data in PANELS_BY_ROOM.items(): for panel_name, panel_data in room_data.items(): if panel_data.achievement: - event_name = f"{room_name} - {panel_name} (Achieved)" - self.add_location(room_name, event_name, None, [RoomAndPanel(room_name, panel_name)], world) - self.event_loc_to_item[event_name] = "Mastery Achievement" + access_req = AccessRequirements() + access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) + access_req.rooms.add(room_name) + + self.mastery_reqs.append(access_req) # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. @@ -161,8 +167,8 @@ def __init__(self, world: "LingoWorld"): if world.options.level_2_requirement == 1: raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.") - if world.options.level_2_requirement > 1: - self.create_panel_hunt_events(world) + # Create groups of counting panel access requirements for the LEVEL 2 check. + self.create_panel_hunt_events(world) # Instantiate all real locations. location_classification = LocationClassification.normal @@ -400,11 +406,8 @@ def create_panel_hunt_events(self, world: "LingoWorld"): if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1): - event_name = f"{room_name} - {panel_name} (Counted)" - self.event_loc_to_item[event_name] = "1 Counting Panels Solved" - self.locations_by_room.setdefault(room_name, []).append( - PlayerLocation(event_name, None, self.calculate_panel_requirements(room_name, panel_name, - world))) + self.counting_panel_reqs.setdefault(room_name, []).append( + (self.calculate_panel_requirements(room_name, panel_name, world), 1)) else: if len(panel_data.colors) == 0 or not world.options.shuffle_colors: color = None @@ -415,11 +418,7 @@ def create_panel_hunt_events(self, world: "LingoWorld"): for color, panel_count in unhindered_panels_by_color.items(): access_reqs = AccessRequirements() - if color is None: - event_name = f"{room_name} - {panel_count} White Panels (Counted)" - else: - event_name = f"{room_name} - {panel_count} {color.capitalize()} Panels (Counted)" + if color is not None: access_reqs.colors.add(color) - self.locations_by_room.setdefault(room_name, []).append(PlayerLocation(event_name, None, access_reqs)) - self.event_loc_to_item[event_name] = f"{panel_count} Counting Panels Solved" + self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count)) diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index f3beeec8ea9a..ee9dcc41929f 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -39,12 +39,24 @@ def lingo_can_use_location(state: CollectionState, location: PlayerLocation, wor return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) -def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): - return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) - - -def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): - return state.has("COUNTING PANELS", world.player, world.options.level_2_requirement.value - 1) +def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): + satisfied_count = 0 + for access_req in player_logic.mastery_reqs: + if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + satisfied_count += 1 + return satisfied_count >= world.options.mastery_achievements.value + + +def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): + counted_panels = 0 + state.update_reachable_regions(world.player) + for region in state.reachable_regions[world.player]: + for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []): + if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + counted_panels += panel_count + if counted_panels >= world.options.level_2_requirement.value - 1: + return True + return False def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld", @@ -83,10 +95,10 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): if location.name == player_logic.mastery_location: - return lambda state: lingo_can_use_mastery_location(state, world) + return lambda state: lingo_can_use_mastery_location(state, world, player_logic) if world.options.level_2_requirement > 1\ and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location): - return lambda state: lingo_can_use_level_2_location(state, world) + return lambda state: lingo_can_use_level_2_location(state, world, player_logic) return lambda state: lingo_can_use_location(state, location, world, player_logic) From 9448600e2e937c808c57652f63b7557d3d3fc84f Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 18:40:22 -0500 Subject: [PATCH 19/19] Yknow what? Default LEVEL 2 to 223 again --- worlds/lingo/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index f580c77fc582..fc9ddee0e0e9 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -83,7 +83,7 @@ class Level2Requirement(Range): display_name = "Level 2 Requirement" range_start = 1 range_end = 800 - default = 1 + default = 223 class EarlyColorHallways(Toggle):