diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 3d98ae91..da8a246e 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -55,14 +55,14 @@ class LingoWorld(World): 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) @@ -93,7 +93,7 @@ class LingoWorld(World): 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. @@ -116,6 +116,6 @@ class LingoWorld(World): } 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/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index d46403e8..8a4f831f 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -379,8 +379,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/options.py b/worlds/lingo/options.py index 7dc6a138..fc9ddee0 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 @@ -75,9 +78,10 @@ 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 diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index abb975e0..a0b33d1d 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -1,10 +1,10 @@ -from typing import Dict, List, NamedTuple, Optional, 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 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, \ + PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ RoomAndPanel from .testing import LingoTestOptions @@ -12,10 +12,29 @@ 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] = [] + code: Optional[int] + access: AccessRequirements class LingoPlayerLogic: @@ -23,27 +42,45 @@ 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 - def add_location(self, room: str, loc: PlayerLocation): - self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) + 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"): + """ + Creates a location. This function determines the access requirements for the location by combining and + flattening the requirements for each of the given panels. + """ + 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 + 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)) 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]: @@ -52,21 +89,25 @@ class LingoPlayerLogic: 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.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.mastery_reqs = [] + self.counting_panel_reqs = {} door_shuffle = world.options.shuffle_doors color_shuffle = world.options.shuffle_colors @@ -79,17 +120,10 @@ class LingoPlayerLogic: 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_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.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 + # 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. @@ -97,49 +131,44 @@ class LingoPlayerLogic: else: 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.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: - event_name = room_name + " - " + panel_name + " (Achieved)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)])) - 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) - 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.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" + 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. - 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 = "Second Room - LEVEL 2" 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.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" + self.victory_condition = "Orange Tower Seventh Floor - THE END" + 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.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.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" + 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.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.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" + 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: + raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.") + + # 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 @@ -149,18 +178,17 @@ class LingoPlayerLogic: 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, PlayerLocation(location_name, location_data.code, - location_data.panels)) - self.REAL_LOCATIONS.append(location_name) + 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. 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: @@ -201,7 +229,7 @@ class LingoPlayerLogic: 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] @@ -226,12 +254,12 @@ class LingoPlayerLogic: 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 @@ -253,7 +281,7 @@ class LingoPlayerLogic: 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() @@ -272,25 +300,125 @@ class LingoPlayerLogic: 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 return True + + 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 + 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] + + for req_room in panel_object.required_rooms: + access_reqs.rooms.add(req_room) + + 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 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, 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)) + + 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, 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, world: "LingoWorld"): + """ + 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] + + 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, world) + 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"): + """ + 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], 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): + 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 + 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(): + access_reqs = AccessRequirements() + if color is not None: + access_reqs.colors.add(color) + + self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count)) diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index e5f947de..c24144a1 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 @@ -13,12 +13,12 @@ if TYPE_CHECKING: 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.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] + 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) @@ -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) @@ -85,7 +97,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 d59b8a1e..ee9dcc41 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 .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor +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, 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,77 +28,77 @@ 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 -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): - return False - - return True + 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_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_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int, - player_logic: LingoPlayerLogic): - """ - Determines whether a door can be opened - """ - 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, player) +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_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: +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 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): + for req_door in access.doors: + if not _lingo_can_open_door(state, req_door.room, req_door.door, world, 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 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 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) +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) - return lambda state: lingo_can_use_location(state, location, room_name, 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, world.player, progression.index) + + return state.has(item_name, world.player) + + +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, 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, player_logic) + + 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 00000000..34c1b381 --- /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"))