diff options
Diffstat (limited to 'player_logic.py')
| -rw-r--r-- | player_logic.py | 298 |
1 files changed, 213 insertions, 85 deletions
| diff --git a/player_logic.py b/player_logic.py index abb975e..a0b33d1 100644 --- a/player_logic.py +++ b/player_logic.py | |||
| @@ -1,10 +1,10 @@ | |||
| 1 | from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING | 1 | from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING |
| 2 | 2 | ||
| 3 | from .items import ALL_ITEM_TABLE | 3 | from .items import ALL_ITEM_TABLE |
| 4 | from .locations import ALL_LOCATION_TABLE, LocationClassification | 4 | from .locations import ALL_LOCATION_TABLE, LocationClassification |
| 5 | from .options import LocationChecks, ShuffleDoors, VictoryCondition | 5 | from .options import LocationChecks, ShuffleDoors, VictoryCondition |
| 6 | from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ | 6 | from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ |
| 7 | PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \ | 7 | PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ |
| 8 | RoomAndPanel | 8 | RoomAndPanel |
| 9 | from .testing import LingoTestOptions | 9 | from .testing import LingoTestOptions |
| 10 | 10 | ||
| @@ -12,10 +12,29 @@ if TYPE_CHECKING: | |||
| 12 | from . import LingoWorld | 12 | from . import LingoWorld |
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | class AccessRequirements: | ||
| 16 | rooms: Set[str] | ||
| 17 | doors: Set[RoomAndDoor] | ||
| 18 | colors: Set[str] | ||
| 19 | |||
| 20 | def __init__(self): | ||
| 21 | self.rooms = set() | ||
| 22 | self.doors = set() | ||
| 23 | self.colors = set() | ||
| 24 | |||
| 25 | def merge(self, other: "AccessRequirements"): | ||
| 26 | self.rooms |= other.rooms | ||
| 27 | self.doors |= other.doors | ||
| 28 | self.colors |= other.colors | ||
| 29 | |||
| 30 | def __str__(self): | ||
| 31 | return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})" | ||
| 32 | |||
| 33 | |||
| 15 | class PlayerLocation(NamedTuple): | 34 | class PlayerLocation(NamedTuple): |
| 16 | name: str | 35 | name: str |
| 17 | code: Optional[int] = None | 36 | code: Optional[int] |
| 18 | panels: List[RoomAndPanel] = [] | 37 | access: AccessRequirements |
| 19 | 38 | ||
| 20 | 39 | ||
| 21 | class LingoPlayerLogic: | 40 | class LingoPlayerLogic: |
| @@ -23,27 +42,45 @@ class LingoPlayerLogic: | |||
| 23 | Defines logic after a player's options have been applied | 42 | Defines logic after a player's options have been applied |
| 24 | """ | 43 | """ |
| 25 | 44 | ||
| 26 | ITEM_BY_DOOR: Dict[str, Dict[str, str]] | 45 | item_by_door: Dict[str, Dict[str, str]] |
| 27 | 46 | ||
| 28 | LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]] | 47 | locations_by_room: Dict[str, List[PlayerLocation]] |
| 29 | REAL_LOCATIONS: List[str] | 48 | real_locations: List[str] |
| 30 | 49 | ||
| 31 | EVENT_LOC_TO_ITEM: Dict[str, str] | 50 | event_loc_to_item: Dict[str, str] |
| 32 | REAL_ITEMS: List[str] | 51 | real_items: List[str] |
| 33 | 52 | ||
| 34 | VICTORY_CONDITION: str | 53 | victory_condition: str |
| 35 | MASTERY_LOCATION: str | 54 | mastery_location: str |
| 36 | LEVEL_2_LOCATION: str | 55 | level_2_location: str |
| 37 | 56 | ||
| 38 | PAINTING_MAPPING: Dict[str, str] | 57 | painting_mapping: Dict[str, str] |
| 39 | 58 | ||
| 40 | FORCED_GOOD_ITEM: str | 59 | forced_good_item: str |
| 41 | 60 | ||
| 42 | def add_location(self, room: str, loc: PlayerLocation): | 61 | panel_reqs: Dict[str, Dict[str, AccessRequirements]] |
| 43 | self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) | 62 | door_reqs: Dict[str, Dict[str, AccessRequirements]] |
| 63 | mastery_reqs: List[AccessRequirements] | ||
| 64 | counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]] | ||
| 65 | |||
| 66 | def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"): | ||
| 67 | """ | ||
| 68 | Creates a location. This function determines the access requirements for the location by combining and | ||
| 69 | flattening the requirements for each of the given panels. | ||
| 70 | """ | ||
| 71 | access_reqs = AccessRequirements() | ||
| 72 | for panel in panels: | ||
| 73 | if panel.room is not None and panel.room != room: | ||
| 74 | access_reqs.rooms.add(panel.room) | ||
| 75 | |||
| 76 | panel_room = room if panel.room is None else panel.room | ||
| 77 | sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel, world) | ||
| 78 | access_reqs.merge(sub_access_reqs) | ||
| 79 | |||
| 80 | self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs)) | ||
| 44 | 81 | ||
| 45 | def set_door_item(self, room: str, door: str, item: str): | 82 | def set_door_item(self, room: str, door: str, item: str): |
| 46 | self.ITEM_BY_DOOR.setdefault(room, {})[door] = item | 83 | self.item_by_door.setdefault(room, {})[door] = item |
| 47 | 84 | ||
| 48 | def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): | 85 | def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): |
| 49 | if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: | 86 | if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: |
| @@ -52,21 +89,25 @@ class LingoPlayerLogic: | |||
| 52 | else: | 89 | else: |
| 53 | progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name | 90 | progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name |
| 54 | self.set_door_item(room_name, door_data.name, progressive_item_name) | 91 | self.set_door_item(room_name, door_data.name, progressive_item_name) |
| 55 | self.REAL_ITEMS.append(progressive_item_name) | 92 | self.real_items.append(progressive_item_name) |
| 56 | else: | 93 | else: |
| 57 | self.set_door_item(room_name, door_data.name, door_data.item_name) | 94 | self.set_door_item(room_name, door_data.name, door_data.item_name) |
| 58 | 95 | ||
| 59 | def __init__(self, world: "LingoWorld"): | 96 | def __init__(self, world: "LingoWorld"): |
| 60 | self.ITEM_BY_DOOR = {} | 97 | self.item_by_door = {} |
| 61 | self.LOCATIONS_BY_ROOM = {} | 98 | self.locations_by_room = {} |
| 62 | self.REAL_LOCATIONS = [] | 99 | self.real_locations = [] |
| 63 | self.EVENT_LOC_TO_ITEM = {} | 100 | self.event_loc_to_item = {} |
| 64 | self.REAL_ITEMS = [] | 101 | self.real_items = [] |
| 65 | self.VICTORY_CONDITION = "" | 102 | self.victory_condition = "" |
| 66 | self.MASTERY_LOCATION = "" | 103 | self.mastery_location = "" |
| 67 | self.LEVEL_2_LOCATION = "" | 104 | self.level_2_location = "" |
| 68 | self.PAINTING_MAPPING = {} | 105 | self.painting_mapping = {} |
| 69 | self.FORCED_GOOD_ITEM = "" | 106 | self.forced_good_item = "" |
| 107 | self.panel_reqs = {} | ||
| 108 | self.door_reqs = {} | ||
| 109 | self.mastery_reqs = [] | ||
| 110 | self.counting_panel_reqs = {} | ||
| 70 | 111 | ||
| 71 | door_shuffle = world.options.shuffle_doors | 112 | door_shuffle = world.options.shuffle_doors |
| 72 | color_shuffle = world.options.shuffle_colors | 113 | color_shuffle = world.options.shuffle_colors |
| @@ -79,17 +120,10 @@ class LingoPlayerLogic: | |||
| 79 | raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " | 120 | raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " |
| 80 | "be enough locations for all of the door items.") | 121 | "be enough locations for all of the door items.") |
| 81 | 122 | ||
| 82 | # Create an event for every door, representing whether that door has been opened. Also create event items for | 123 | # Create door items, where needed. |
| 83 | # doors that are event-only. | 124 | if door_shuffle != ShuffleDoors.option_none: |
| 84 | for room_name, room_data in DOORS_BY_ROOM.items(): | 125 | for room_name, room_data in DOORS_BY_ROOM.items(): |
| 85 | for door_name, door_data in room_data.items(): | 126 | for door_name, door_data in room_data.items(): |
| 86 | if door_shuffle == ShuffleDoors.option_none: | ||
| 87 | itemloc_name = f"{room_name} - {door_name} (Opened)" | ||
| 88 | self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels)) | ||
| 89 | self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name | ||
| 90 | self.set_door_item(room_name, door_name, itemloc_name) | ||
| 91 | else: | ||
| 92 | # This line is duplicated from StaticLingoItems | ||
| 93 | if door_data.skip_item is False and door_data.event is False: | 127 | if door_data.skip_item is False and door_data.event is False: |
| 94 | if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: | 128 | if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: |
| 95 | # Grouped doors are handled differently if shuffle doors is on simple. | 129 | # Grouped doors are handled differently if shuffle doors is on simple. |
| @@ -97,49 +131,44 @@ class LingoPlayerLogic: | |||
| 97 | else: | 131 | else: |
| 98 | self.handle_non_grouped_door(room_name, door_data, world) | 132 | self.handle_non_grouped_door(room_name, door_data, world) |
| 99 | 133 | ||
| 100 | if door_data.event: | 134 | # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. |
| 101 | self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels)) | ||
| 102 | self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" | ||
| 103 | self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") | ||
| 104 | |||
| 105 | # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also | ||
| 106 | # create events for each counting panel, so that we can determine when LEVEL 2 is accessible. | ||
| 107 | for room_name, room_data in PANELS_BY_ROOM.items(): | 135 | for room_name, room_data in PANELS_BY_ROOM.items(): |
| 108 | for panel_name, panel_data in room_data.items(): | 136 | for panel_name, panel_data in room_data.items(): |
| 109 | if panel_data.achievement: | 137 | if panel_data.achievement: |
| 110 | event_name = room_name + " - " + panel_name + " (Achieved)" | 138 | access_req = AccessRequirements() |
| 111 | self.add_location(room_name, PlayerLocation(event_name, None, | 139 | access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) |
| 112 | [RoomAndPanel(room_name, panel_name)])) | 140 | access_req.rooms.add(room_name) |
| 113 | self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" | ||
| 114 | 141 | ||
| 115 | if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: | 142 | self.mastery_reqs.append(access_req) |
| 116 | event_name = room_name + " - " + panel_name + " (Counted)" | ||
| 117 | self.add_location(room_name, PlayerLocation(event_name, None, | ||
| 118 | [RoomAndPanel(room_name, panel_name)])) | ||
| 119 | self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" | ||
| 120 | 143 | ||
| 121 | # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need | 144 | # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need |
| 122 | # to prevent the actual victory condition from becoming a check. | 145 | # to prevent the actual victory condition from becoming a check. |
| 123 | self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" | 146 | self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" |
| 124 | self.LEVEL_2_LOCATION = "N/A" | 147 | self.level_2_location = "Second Room - LEVEL 2" |
| 125 | 148 | ||
| 126 | if victory_condition == VictoryCondition.option_the_end: | 149 | if victory_condition == VictoryCondition.option_the_end: |
| 127 | self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" | 150 | self.victory_condition = "Orange Tower Seventh Floor - THE END" |
| 128 | self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)")) | 151 | self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) |
| 129 | self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" | 152 | self.event_loc_to_item["The End (Solved)"] = "Victory" |
| 130 | elif victory_condition == VictoryCondition.option_the_master: | 153 | elif victory_condition == VictoryCondition.option_the_master: |
| 131 | self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" | 154 | self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" |
| 132 | self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" | 155 | self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements" |
| 133 | 156 | ||
| 134 | self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, [])) | 157 | self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, [], world) |
| 135 | self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" | 158 | self.event_loc_to_item[self.mastery_location] = "Victory" |
| 136 | elif victory_condition == VictoryCondition.option_level_2: | 159 | elif victory_condition == VictoryCondition.option_level_2: |
| 137 | self.VICTORY_CONDITION = "Second Room - LEVEL 2" | 160 | self.victory_condition = "Second Room - LEVEL 2" |
| 138 | self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" | 161 | self.level_2_location = "Second Room - Unlock Level 2" |
| 162 | |||
| 163 | self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")], | ||
| 164 | world) | ||
| 165 | self.event_loc_to_item[self.level_2_location] = "Victory" | ||
| 166 | |||
| 167 | if world.options.level_2_requirement == 1: | ||
| 168 | raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.") | ||
| 139 | 169 | ||
| 140 | self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None, | 170 | # Create groups of counting panel access requirements for the LEVEL 2 check. |
| 141 | [RoomAndPanel("Second Room", "LEVEL 2")])) | 171 | self.create_panel_hunt_events(world) |
| 142 | self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" | ||
| 143 | 172 | ||
| 144 | # Instantiate all real locations. | 173 | # Instantiate all real locations. |
| 145 | location_classification = LocationClassification.normal | 174 | location_classification = LocationClassification.normal |
| @@ -149,18 +178,17 @@ class LingoPlayerLogic: | |||
| 149 | location_classification = LocationClassification.insanity | 178 | location_classification = LocationClassification.insanity |
| 150 | 179 | ||
| 151 | for location_name, location_data in ALL_LOCATION_TABLE.items(): | 180 | for location_name, location_data in ALL_LOCATION_TABLE.items(): |
| 152 | if location_name != self.VICTORY_CONDITION: | 181 | if location_name != self.victory_condition: |
| 153 | if location_classification not in location_data.classification: | 182 | if location_classification not in location_data.classification: |
| 154 | continue | 183 | continue |
| 155 | 184 | ||
| 156 | self.add_location(location_data.room, PlayerLocation(location_name, location_data.code, | 185 | self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world) |
| 157 | location_data.panels)) | 186 | self.real_locations.append(location_name) |
| 158 | self.REAL_LOCATIONS.append(location_name) | ||
| 159 | 187 | ||
| 160 | # Instantiate all real items. | 188 | # Instantiate all real items. |
| 161 | for name, item in ALL_ITEM_TABLE.items(): | 189 | for name, item in ALL_ITEM_TABLE.items(): |
| 162 | if item.should_include(world): | 190 | if item.should_include(world): |
| 163 | self.REAL_ITEMS.append(name) | 191 | self.real_items.append(name) |
| 164 | 192 | ||
| 165 | # Create the paintings mapping, if painting shuffle is on. | 193 | # Create the paintings mapping, if painting shuffle is on. |
| 166 | if painting_shuffle: | 194 | if painting_shuffle: |
| @@ -201,7 +229,7 @@ class LingoPlayerLogic: | |||
| 201 | continue | 229 | continue |
| 202 | 230 | ||
| 203 | # If painting shuffle is on, we only want to consider paintings that actually go somewhere. | 231 | # If painting shuffle is on, we only want to consider paintings that actually go somewhere. |
| 204 | if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys(): | 232 | if painting_shuffle and painting_obj.id not in self.painting_mapping.keys(): |
| 205 | continue | 233 | continue |
| 206 | 234 | ||
| 207 | pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] | 235 | pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] |
| @@ -226,12 +254,12 @@ class LingoPlayerLogic: | |||
| 226 | good_item_options.remove(item) | 254 | good_item_options.remove(item) |
| 227 | 255 | ||
| 228 | if len(good_item_options) > 0: | 256 | if len(good_item_options) > 0: |
| 229 | self.FORCED_GOOD_ITEM = world.random.choice(good_item_options) | 257 | self.forced_good_item = world.random.choice(good_item_options) |
| 230 | self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM) | 258 | self.real_items.remove(self.forced_good_item) |
| 231 | self.REAL_LOCATIONS.remove("Second Room - Good Luck") | 259 | self.real_locations.remove("Second Room - Good Luck") |
| 232 | 260 | ||
| 233 | def randomize_paintings(self, world: "LingoWorld") -> bool: | 261 | def randomize_paintings(self, world: "LingoWorld") -> bool: |
| 234 | self.PAINTING_MAPPING.clear() | 262 | self.painting_mapping.clear() |
| 235 | 263 | ||
| 236 | door_shuffle = world.options.shuffle_doors | 264 | door_shuffle = world.options.shuffle_doors |
| 237 | 265 | ||
| @@ -253,7 +281,7 @@ class LingoPlayerLogic: | |||
| 253 | if painting.exit_only and painting.required] | 281 | if painting.exit_only and painting.required] |
| 254 | req_entrances = world.random.sample(req_enterable, len(req_exits)) | 282 | req_entrances = world.random.sample(req_enterable, len(req_exits)) |
| 255 | 283 | ||
| 256 | self.PAINTING_MAPPING = dict(zip(req_entrances, req_exits)) | 284 | self.painting_mapping = dict(zip(req_entrances, req_exits)) |
| 257 | 285 | ||
| 258 | # Next, determine the rest of the exit paintings. | 286 | # Next, determine the rest of the exit paintings. |
| 259 | exitable = [painting_id for painting_id, painting in PAINTINGS.items() | 287 | exitable = [painting_id for painting_id, painting in PAINTINGS.items() |
| @@ -272,25 +300,125 @@ class LingoPlayerLogic: | |||
| 272 | for warp_exit in nonreq_exits: | 300 | for warp_exit in nonreq_exits: |
| 273 | warp_enter = world.random.choice(chosen_entrances) | 301 | warp_enter = world.random.choice(chosen_entrances) |
| 274 | chosen_entrances.remove(warp_enter) | 302 | chosen_entrances.remove(warp_enter) |
| 275 | self.PAINTING_MAPPING[warp_enter] = warp_exit | 303 | self.painting_mapping[warp_enter] = warp_exit |
| 276 | 304 | ||
| 277 | # Assign each of the remaining entrances to any required or non-required exit. | 305 | # Assign each of the remaining entrances to any required or non-required exit. |
| 278 | for warp_enter in chosen_entrances: | 306 | for warp_enter in chosen_entrances: |
| 279 | warp_exit = world.random.choice(chosen_exits) | 307 | warp_exit = world.random.choice(chosen_exits) |
| 280 | self.PAINTING_MAPPING[warp_enter] = warp_exit | 308 | self.painting_mapping[warp_enter] = warp_exit |
| 281 | 309 | ||
| 282 | # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). | 310 | # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). |
| 283 | # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the | 311 | # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the |
| 284 | # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall | 312 | # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall |
| 285 | # is forced to point to the vanilla exit. | 313 | # is forced to point to the vanilla exit. |
| 286 | if "eye_painting_2" not in self.PAINTING_MAPPING.keys(): | 314 | if "eye_painting_2" not in self.painting_mapping.keys(): |
| 287 | self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2" | 315 | self.painting_mapping["eye_painting"] = "eye_painting_2" |
| 288 | 316 | ||
| 289 | # Just for sanity's sake, ensure that all required painting rooms are accessed. | 317 | # Just for sanity's sake, ensure that all required painting rooms are accessed. |
| 290 | for painting_id, painting in PAINTINGS.items(): | 318 | for painting_id, painting in PAINTINGS.items(): |
| 291 | if painting_id not in self.PAINTING_MAPPING.values() \ | 319 | if painting_id not in self.painting_mapping.values() \ |
| 292 | and (painting.required or (painting.required_when_no_doors and | 320 | and (painting.required or (painting.required_when_no_doors and |
| 293 | door_shuffle == ShuffleDoors.option_none)): | 321 | door_shuffle == ShuffleDoors.option_none)): |
| 294 | return False | 322 | return False |
| 295 | 323 | ||
| 296 | return True | 324 | return True |
| 325 | |||
| 326 | def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld"): | ||
| 327 | """ | ||
| 328 | Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in | ||
| 329 | the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required | ||
| 330 | by this panel. Memoization is used so that no panel is evaluated more than once. | ||
| 331 | """ | ||
| 332 | if panel not in self.panel_reqs.setdefault(room, {}): | ||
| 333 | access_reqs = AccessRequirements() | ||
| 334 | panel_object = PANELS_BY_ROOM[room][panel] | ||
| 335 | |||
| 336 | for req_room in panel_object.required_rooms: | ||
| 337 | access_reqs.rooms.add(req_room) | ||
| 338 | |||
| 339 | for req_door in panel_object.required_doors: | ||
| 340 | door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door] | ||
| 341 | if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: | ||
| 342 | sub_access_reqs = self.calculate_door_requirements( | ||
| 343 | room if req_door.room is None else req_door.room, req_door.door, world) | ||
| 344 | access_reqs.merge(sub_access_reqs) | ||
| 345 | else: | ||
| 346 | access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door)) | ||
| 347 | |||
| 348 | for color in panel_object.colors: | ||
| 349 | access_reqs.colors.add(color) | ||
| 350 | |||
| 351 | for req_panel in panel_object.required_panels: | ||
| 352 | if req_panel.room is not None and req_panel.room != room: | ||
| 353 | access_reqs.rooms.add(req_panel.room) | ||
| 354 | |||
| 355 | sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, | ||
| 356 | req_panel.panel, world) | ||
| 357 | access_reqs.merge(sub_access_reqs) | ||
| 358 | |||
| 359 | self.panel_reqs[room][panel] = access_reqs | ||
| 360 | |||
| 361 | return self.panel_reqs[room][panel] | ||
| 362 | |||
| 363 | def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld"): | ||
| 364 | """ | ||
| 365 | Similar to calculate_panel_requirements, but for event doors. | ||
| 366 | """ | ||
| 367 | if door not in self.door_reqs.setdefault(room, {}): | ||
| 368 | access_reqs = AccessRequirements() | ||
| 369 | door_object = DOORS_BY_ROOM[room][door] | ||
| 370 | |||
| 371 | for req_panel in door_object.panels: | ||
| 372 | if req_panel.room is not None and req_panel.room != room: | ||
| 373 | access_reqs.rooms.add(req_panel.room) | ||
| 374 | |||
| 375 | sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, | ||
| 376 | req_panel.panel, world) | ||
| 377 | access_reqs.merge(sub_access_reqs) | ||
| 378 | |||
| 379 | self.door_reqs[room][door] = access_reqs | ||
| 380 | |||
| 381 | return self.door_reqs[room][door] | ||
| 382 | |||
| 383 | def create_panel_hunt_events(self, world: "LingoWorld"): | ||
| 384 | """ | ||
| 385 | Creates the event locations/items used for determining access to the LEVEL 2 panel. Instead of creating an event | ||
| 386 | for every single counting panel in the game, we try to coalesce panels with identical access rules into the same | ||
| 387 | event. Right now, this means the following: | ||
| 388 | |||
| 389 | When color shuffle is off, panels in a room with no extra access requirements (room, door, or other panel) are | ||
| 390 | all coalesced into one event. | ||
| 391 | |||
| 392 | When color shuffle is on, single-colored panels (including white) in a room are combined into one event per | ||
| 393 | color. Multicolored panels and panels with any extra access requirements are not coalesced, and will each | ||
| 394 | receive their own event. | ||
| 395 | """ | ||
| 396 | for room_name, room_data in PANELS_BY_ROOM.items(): | ||
| 397 | unhindered_panels_by_color: dict[Optional[str], int] = {} | ||
| 398 | |||
| 399 | for panel_name, panel_data in room_data.items(): | ||
| 400 | # We won't count non-counting panels. | ||
| 401 | if panel_data.non_counting: | ||
| 402 | continue | ||
| 403 | |||
| 404 | # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will | ||
| 405 | # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. | ||
| 406 | if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ | ||
| 407 | or len(panel_data.required_rooms) > 0\ | ||
| 408 | or (world.options.shuffle_colors and len(panel_data.colors) > 1): | ||
| 409 | self.counting_panel_reqs.setdefault(room_name, []).append( | ||
| 410 | (self.calculate_panel_requirements(room_name, panel_name, world), 1)) | ||
| 411 | else: | ||
| 412 | if len(panel_data.colors) == 0 or not world.options.shuffle_colors: | ||
| 413 | color = None | ||
| 414 | else: | ||
| 415 | color = panel_data.colors[0] | ||
| 416 | |||
| 417 | unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1 | ||
| 418 | |||
| 419 | for color, panel_count in unhindered_panels_by_color.items(): | ||
| 420 | access_reqs = AccessRequirements() | ||
| 421 | if color is not None: | ||
| 422 | access_reqs.colors.add(color) | ||
| 423 | |||
| 424 | self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count)) | ||
