diff options
Diffstat (limited to 'player_logic.py')
| -rw-r--r-- | player_logic.py | 298 |
1 files changed, 298 insertions, 0 deletions
| diff --git a/player_logic.py b/player_logic.py new file mode 100644 index 0000000..217ad91 --- /dev/null +++ b/player_logic.py | |||
| @@ -0,0 +1,298 @@ | |||
| 1 | from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING | ||
| 2 | |||
| 3 | from .items import ALL_ITEM_TABLE | ||
| 4 | from .locations import ALL_LOCATION_TABLE, LocationClassification | ||
| 5 | from .options import LocationChecks, ShuffleDoors, VictoryCondition | ||
| 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, \ | ||
| 8 | RoomAndPanel | ||
| 9 | from .testing import LingoTestOptions | ||
| 10 | |||
| 11 | if TYPE_CHECKING: | ||
| 12 | from . import LingoWorld | ||
| 13 | |||
| 14 | |||
| 15 | class PlayerLocation(NamedTuple): | ||
| 16 | name: str | ||
| 17 | code: Optional[int] = None | ||
| 18 | panels: List[RoomAndPanel] = [] | ||
| 19 | |||
| 20 | |||
| 21 | class LingoPlayerLogic: | ||
| 22 | """ | ||
| 23 | Defines logic after a player's options have been applied | ||
| 24 | """ | ||
| 25 | |||
| 26 | ITEM_BY_DOOR: Dict[str, Dict[str, str]] | ||
| 27 | |||
| 28 | LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]] | ||
| 29 | REAL_LOCATIONS: List[str] | ||
| 30 | |||
| 31 | EVENT_LOC_TO_ITEM: Dict[str, str] | ||
| 32 | REAL_ITEMS: List[str] | ||
| 33 | |||
| 34 | VICTORY_CONDITION: str | ||
| 35 | MASTERY_LOCATION: str | ||
| 36 | LEVEL_2_LOCATION: str | ||
| 37 | |||
| 38 | PAINTING_MAPPING: Dict[str, str] | ||
| 39 | |||
| 40 | FORCED_GOOD_ITEM: str | ||
| 41 | |||
| 42 | def add_location(self, room: str, loc: PlayerLocation): | ||
| 43 | self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) | ||
| 44 | |||
| 45 | def set_door_item(self, room: str, door: str, item: str): | ||
| 46 | self.ITEM_BY_DOOR.setdefault(room, {})[door] = item | ||
| 47 | |||
| 48 | 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]: | ||
| 50 | if room_name == "Orange Tower" and not world.options.progressive_orange_tower: | ||
| 51 | self.set_door_item(room_name, door_data.name, door_data.item_name) | ||
| 52 | else: | ||
| 53 | 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) | ||
| 55 | self.REAL_ITEMS.append(progressive_item_name) | ||
| 56 | else: | ||
| 57 | self.set_door_item(room_name, door_data.name, door_data.item_name) | ||
| 58 | |||
| 59 | def __init__(self, world: "LingoWorld"): | ||
| 60 | self.ITEM_BY_DOOR = {} | ||
| 61 | self.LOCATIONS_BY_ROOM = {} | ||
| 62 | self.REAL_LOCATIONS = [] | ||
| 63 | self.EVENT_LOC_TO_ITEM = {} | ||
| 64 | self.REAL_ITEMS = [] | ||
| 65 | self.VICTORY_CONDITION = "" | ||
| 66 | self.MASTERY_LOCATION = "" | ||
| 67 | self.LEVEL_2_LOCATION = "" | ||
| 68 | self.PAINTING_MAPPING = {} | ||
| 69 | self.FORCED_GOOD_ITEM = "" | ||
| 70 | |||
| 71 | door_shuffle = world.options.shuffle_doors | ||
| 72 | color_shuffle = world.options.shuffle_colors | ||
| 73 | painting_shuffle = world.options.shuffle_paintings | ||
| 74 | location_checks = world.options.location_checks | ||
| 75 | victory_condition = world.options.victory_condition | ||
| 76 | early_color_hallways = world.options.early_color_hallways | ||
| 77 | |||
| 78 | if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none: | ||
| 79 | 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.") | ||
| 81 | |||
| 82 | # Create an event for every room that represents being able to reach that room. | ||
| 83 | for room_name in ROOMS.keys(): | ||
| 84 | roomloc_name = f"{room_name} (Reached)" | ||
| 85 | self.add_location(room_name, PlayerLocation(roomloc_name, None, [])) | ||
| 86 | self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name | ||
| 87 | |||
| 88 | # Create an event for every door, representing whether that door has been opened. Also create event items for | ||
| 89 | # doors that are event-only. | ||
| 90 | for room_name, room_data in DOORS_BY_ROOM.items(): | ||
| 91 | for door_name, door_data in room_data.items(): | ||
| 92 | if door_shuffle == ShuffleDoors.option_none: | ||
| 93 | itemloc_name = f"{room_name} - {door_name} (Opened)" | ||
| 94 | self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels)) | ||
| 95 | self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name | ||
| 96 | self.set_door_item(room_name, door_name, itemloc_name) | ||
| 97 | else: | ||
| 98 | # This line is duplicated from StaticLingoItems | ||
| 99 | if door_data.skip_item is False and door_data.event is False: | ||
| 100 | if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: | ||
| 101 | # Grouped doors are handled differently if shuffle doors is on simple. | ||
| 102 | self.set_door_item(room_name, door_name, door_data.group) | ||
| 103 | else: | ||
| 104 | self.handle_non_grouped_door(room_name, door_data, world) | ||
| 105 | |||
| 106 | if door_data.event: | ||
| 107 | self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels)) | ||
| 108 | self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" | ||
| 109 | self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") | ||
| 110 | |||
| 111 | # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also | ||
| 112 | # create events for each counting panel, so that we can determine when LEVEL 2 is accessible. | ||
| 113 | for room_name, room_data in PANELS_BY_ROOM.items(): | ||
| 114 | for panel_name, panel_data in room_data.items(): | ||
| 115 | if panel_data.achievement: | ||
| 116 | event_name = room_name + " - " + panel_name + " (Achieved)" | ||
| 117 | self.add_location(room_name, PlayerLocation(event_name, None, | ||
| 118 | [RoomAndPanel(room_name, panel_name)])) | ||
| 119 | self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" | ||
| 120 | |||
| 121 | if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: | ||
| 122 | event_name = room_name + " - " + panel_name + " (Counted)" | ||
| 123 | self.add_location(room_name, PlayerLocation(event_name, None, | ||
| 124 | [RoomAndPanel(room_name, panel_name)])) | ||
| 125 | self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" | ||
| 126 | |||
| 127 | # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need | ||
| 128 | # to prevent the actual victory condition from becoming a check. | ||
| 129 | self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" | ||
| 130 | self.LEVEL_2_LOCATION = "N/A" | ||
| 131 | |||
| 132 | if victory_condition == VictoryCondition.option_the_end: | ||
| 133 | self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" | ||
| 134 | self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)")) | ||
| 135 | self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" | ||
| 136 | elif victory_condition == VictoryCondition.option_the_master: | ||
| 137 | self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" | ||
| 138 | self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" | ||
| 139 | |||
| 140 | self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, [])) | ||
| 141 | self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" | ||
| 142 | elif victory_condition == VictoryCondition.option_level_2: | ||
| 143 | self.VICTORY_CONDITION = "Second Room - LEVEL 2" | ||
| 144 | self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" | ||
| 145 | |||
| 146 | self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None, | ||
| 147 | [RoomAndPanel("Second Room", "LEVEL 2")])) | ||
| 148 | self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" | ||
| 149 | |||
| 150 | # Instantiate all real locations. | ||
| 151 | location_classification = LocationClassification.normal | ||
| 152 | if location_checks == LocationChecks.option_reduced: | ||
| 153 | location_classification = LocationClassification.reduced | ||
| 154 | elif location_checks == LocationChecks.option_insanity: | ||
| 155 | location_classification = LocationClassification.insanity | ||
| 156 | |||
| 157 | for location_name, location_data in ALL_LOCATION_TABLE.items(): | ||
| 158 | if location_name != self.VICTORY_CONDITION: | ||
| 159 | if location_classification not in location_data.classification: | ||
| 160 | continue | ||
| 161 | |||
| 162 | self.add_location(location_data.room, PlayerLocation(location_name, location_data.code, | ||
| 163 | location_data.panels)) | ||
| 164 | self.REAL_LOCATIONS.append(location_name) | ||
| 165 | |||
| 166 | # Instantiate all real items. | ||
| 167 | for name, item in ALL_ITEM_TABLE.items(): | ||
| 168 | if item.should_include(world): | ||
| 169 | self.REAL_ITEMS.append(name) | ||
| 170 | |||
| 171 | # Create the paintings mapping, if painting shuffle is on. | ||
| 172 | if painting_shuffle: | ||
| 173 | # Shuffle paintings until we get something workable. | ||
| 174 | workable_paintings = False | ||
| 175 | for i in range(0, 20): | ||
| 176 | workable_paintings = self.randomize_paintings(world) | ||
| 177 | if workable_paintings: | ||
| 178 | break | ||
| 179 | |||
| 180 | if not workable_paintings: | ||
| 181 | raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 " | ||
| 182 | "iterations. This is very unlikely to happen on its own, and probably indicates some " | ||
| 183 | "kind of logic error.") | ||
| 184 | |||
| 185 | if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ | ||
| 186 | and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False: | ||
| 187 | # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, | ||
| 188 | # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right | ||
| 189 | # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are | ||
| 190 | # no extra checks in there. We only include the entrance to the Rhyme Room when color shuffle is off and | ||
| 191 | # door shuffle is on simple, because otherwise there are no extra checks in there. | ||
| 192 | good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"] | ||
| 193 | |||
| 194 | if not color_shuffle: | ||
| 195 | good_item_options.append("Pilgrim Room - Sun Painting") | ||
| 196 | |||
| 197 | if door_shuffle == ShuffleDoors.option_simple: | ||
| 198 | good_item_options += ["Welcome Back Doors"] | ||
| 199 | |||
| 200 | if not color_shuffle: | ||
| 201 | good_item_options.append("Rhyme Room Doors") | ||
| 202 | else: | ||
| 203 | good_item_options += ["Welcome Back Area - Shortcut to Starting Room"] | ||
| 204 | |||
| 205 | for painting_obj in PAINTINGS_BY_ROOM["Starting Room"]: | ||
| 206 | if not painting_obj.enter_only or painting_obj.required_door is None: | ||
| 207 | continue | ||
| 208 | |||
| 209 | # If painting shuffle is on, we only want to consider paintings that actually go somewhere. | ||
| 210 | if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys(): | ||
| 211 | continue | ||
| 212 | |||
| 213 | pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] | ||
| 214 | good_item_options.append(pdoor.item_name) | ||
| 215 | |||
| 216 | # Copied from The Witness -- remove any plandoed items from the possible good items set. | ||
| 217 | for v in world.multiworld.plando_items[world.player]: | ||
| 218 | if v.get("from_pool", True): | ||
| 219 | for item_key in {"item", "items"}: | ||
| 220 | if item_key in v: | ||
| 221 | if type(v[item_key]) is str: | ||
| 222 | if v[item_key] in good_item_options: | ||
| 223 | good_item_options.remove(v[item_key]) | ||
| 224 | elif type(v[item_key]) is dict: | ||
| 225 | for item, weight in v[item_key].items(): | ||
| 226 | if weight and item in good_item_options: | ||
| 227 | good_item_options.remove(item) | ||
| 228 | else: | ||
| 229 | # Other type of iterable | ||
| 230 | for item in v[item_key]: | ||
| 231 | if item in good_item_options: | ||
| 232 | good_item_options.remove(item) | ||
| 233 | |||
| 234 | if len(good_item_options) > 0: | ||
| 235 | self.FORCED_GOOD_ITEM = world.random.choice(good_item_options) | ||
| 236 | self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM) | ||
| 237 | self.REAL_LOCATIONS.remove("Second Room - Good Luck") | ||
| 238 | |||
| 239 | def randomize_paintings(self, world: "LingoWorld") -> bool: | ||
| 240 | self.PAINTING_MAPPING.clear() | ||
| 241 | |||
| 242 | door_shuffle = world.options.shuffle_doors | ||
| 243 | |||
| 244 | # Determine the set of exit paintings. All required-exit paintings are included, as are all | ||
| 245 | # required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings. | ||
| 246 | chosen_exits = [] | ||
| 247 | if door_shuffle == ShuffleDoors.option_none: | ||
| 248 | chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items() | ||
| 249 | if painting.required_when_no_doors] | ||
| 250 | chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items() | ||
| 251 | if painting.exit_only and painting.required] | ||
| 252 | exitable = [painting_id for painting_id, painting in PAINTINGS.items() | ||
| 253 | if not painting.enter_only and not painting.disable and not painting.required] | ||
| 254 | chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits)) | ||
| 255 | |||
| 256 | # Determine the set of entrance paintings. | ||
| 257 | enterable = [painting_id for painting_id, painting in PAINTINGS.items() | ||
| 258 | if not painting.exit_only and not painting.disable and painting_id not in chosen_exits] | ||
| 259 | chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES) | ||
| 260 | |||
| 261 | # Create a mapping from entrances to exits. | ||
| 262 | for warp_exit in chosen_exits: | ||
| 263 | warp_enter = world.random.choice(chosen_entrances) | ||
| 264 | |||
| 265 | # Check whether this is a warp from a required painting room to another (or the same) required painting | ||
| 266 | # room. This could cause a cycle that would make certain regions inaccessible. | ||
| 267 | warp_exit_room = PAINTINGS[warp_exit].room | ||
| 268 | warp_enter_room = PAINTINGS[warp_enter].room | ||
| 269 | |||
| 270 | required_painting_rooms = REQUIRED_PAINTING_ROOMS | ||
| 271 | if door_shuffle == ShuffleDoors.option_none: | ||
| 272 | required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS | ||
| 273 | |||
| 274 | if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms: | ||
| 275 | # This shuffling is non-workable. Start over. | ||
| 276 | return False | ||
| 277 | |||
| 278 | chosen_entrances.remove(warp_enter) | ||
| 279 | self.PAINTING_MAPPING[warp_enter] = warp_exit | ||
| 280 | |||
| 281 | for warp_enter in chosen_entrances: | ||
| 282 | warp_exit = world.random.choice(chosen_exits) | ||
| 283 | self.PAINTING_MAPPING[warp_enter] = warp_exit | ||
| 284 | |||
| 285 | # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). | ||
| 286 | # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the | ||
| 287 | # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall | ||
| 288 | # is forced to point to the vanilla exit. | ||
| 289 | if "eye_painting_2" not in self.PAINTING_MAPPING.keys(): | ||
| 290 | self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2" | ||
| 291 | |||
| 292 | # Just for sanity's sake, ensure that all required painting rooms are accessed. | ||
| 293 | for painting_id, painting in PAINTINGS.items(): | ||
| 294 | if painting_id not in self.PAINTING_MAPPING.values() \ | ||
| 295 | and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)): | ||
| 296 | return False | ||
| 297 | |||
| 298 | return True | ||
