diff options
| -rw-r--r-- | __init__.py | 12 | ||||
| -rw-r--r-- | data/LL1.yaml | 2 | ||||
| -rw-r--r-- | options.py | 8 | ||||
| -rw-r--r-- | player_logic.py | 298 | ||||
| -rw-r--r-- | regions.py | 48 | ||||
| -rw-r--r-- | rules.py | 112 | ||||
| -rw-r--r-- | test/TestPanelsanity.py | 19 |
7 files changed, 330 insertions, 169 deletions
| diff --git a/__init__.py b/__init__.py index 3d98ae9..da8a246 100644 --- a/__init__.py +++ b/__init__.py | |||
| @@ -55,14 +55,14 @@ class LingoWorld(World): | |||
| 55 | create_regions(self, self.player_logic) | 55 | create_regions(self, self.player_logic) |
| 56 | 56 | ||
| 57 | def create_items(self): | 57 | def create_items(self): |
| 58 | pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS] | 58 | pool = [self.create_item(name) for name in self.player_logic.real_items] |
| 59 | 59 | ||
| 60 | if self.player_logic.FORCED_GOOD_ITEM != "": | 60 | if self.player_logic.forced_good_item != "": |
| 61 | new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM) | 61 | new_item = self.create_item(self.player_logic.forced_good_item) |
| 62 | location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player) | 62 | location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player) |
| 63 | location_obj.place_locked_item(new_item) | 63 | location_obj.place_locked_item(new_item) |
| 64 | 64 | ||
| 65 | item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool) | 65 | item_difference = len(self.player_logic.real_locations) - len(pool) |
| 66 | if item_difference: | 66 | if item_difference: |
| 67 | trap_percentage = self.options.trap_percentage | 67 | trap_percentage = self.options.trap_percentage |
| 68 | traps = int(item_difference * trap_percentage / 100.0) | 68 | traps = int(item_difference * trap_percentage / 100.0) |
| @@ -93,7 +93,7 @@ class LingoWorld(World): | |||
| 93 | 93 | ||
| 94 | classification = item.classification | 94 | classification = item.classification |
| 95 | if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ | 95 | if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ |
| 96 | and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.PAINTING_MAPPING | 96 | and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping |
| 97 | for painting_id in item.painting_ids): | 97 | for painting_id in item.painting_ids): |
| 98 | # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings | 98 | # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings |
| 99 | # go nowhere, then this item should not be progression. | 99 | # go nowhere, then this item should not be progression. |
| @@ -116,6 +116,6 @@ class LingoWorld(World): | |||
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | if self.options.shuffle_paintings: | 118 | if self.options.shuffle_paintings: |
| 119 | slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING | 119 | slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping |
| 120 | 120 | ||
| 121 | return slot_data | 121 | return slot_data |
| diff --git a/data/LL1.yaml b/data/LL1.yaml index d46403e..8a4f831 100644 --- a/data/LL1.yaml +++ b/data/LL1.yaml | |||
| @@ -379,8 +379,6 @@ | |||
| 379 | tag: forbid | 379 | tag: forbid |
| 380 | non_counting: True | 380 | non_counting: True |
| 381 | check: True | 381 | check: True |
| 382 | required_panel: | ||
| 383 | - panel: ANOTHER TRY | ||
| 384 | doors: | 382 | doors: |
| 385 | Exit Door: | 383 | Exit Door: |
| 386 | id: Entry Room Area Doors/Door_hi_high | 384 | id: Entry Room Area Doors/Door_hi_high |
| diff --git a/options.py b/options.py index 7dc6a13..fc9ddee 100644 --- a/options.py +++ b/options.py | |||
| @@ -52,7 +52,10 @@ class ShufflePaintings(Toggle): | |||
| 52 | 52 | ||
| 53 | 53 | ||
| 54 | class VictoryCondition(Choice): | 54 | class VictoryCondition(Choice): |
| 55 | """Change the victory condition.""" | 55 | """Change the victory condition. |
| 56 | On "the_end", the goal is to solve THE END at the top of the tower. | ||
| 57 | 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. | ||
| 58 | 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.""" | ||
| 56 | display_name = "Victory Condition" | 59 | display_name = "Victory Condition" |
| 57 | option_the_end = 0 | 60 | option_the_end = 0 |
| 58 | option_the_master = 1 | 61 | option_the_master = 1 |
| @@ -75,9 +78,10 @@ class Level2Requirement(Range): | |||
| 75 | """The number of panel solves required to unlock LEVEL 2. | 78 | """The number of panel solves required to unlock LEVEL 2. |
| 76 | In the base game, 223 are needed. | 79 | In the base game, 223 are needed. |
| 77 | Note that this count includes ANOTHER TRY. | 80 | Note that this count includes ANOTHER TRY. |
| 81 | When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free. | ||
| 78 | """ | 82 | """ |
| 79 | display_name = "Level 2 Requirement" | 83 | display_name = "Level 2 Requirement" |
| 80 | range_start = 2 | 84 | range_start = 1 |
| 81 | range_end = 800 | 85 | range_end = 800 |
| 82 | default = 223 | 86 | default = 223 |
| 83 | 87 | ||
| 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)) | ||
| diff --git a/regions.py b/regions.py index e5f947d..c24144a 100644 --- a/regions.py +++ b/regions.py | |||
| @@ -1,11 +1,11 @@ | |||
| 1 | from typing import Dict, TYPE_CHECKING | 1 | from typing import Dict, Optional, TYPE_CHECKING |
| 2 | 2 | ||
| 3 | from BaseClasses import ItemClassification, Region | 3 | from BaseClasses import Entrance, ItemClassification, Region |
| 4 | from .items import LingoItem | 4 | from .items import LingoItem |
| 5 | from .locations import LingoLocation | 5 | from .locations import LingoLocation |
| 6 | from .player_logic import LingoPlayerLogic | 6 | from .player_logic import LingoPlayerLogic |
| 7 | from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda | 7 | from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda |
| 8 | from .static_logic import ALL_ROOMS, PAINTINGS, Room | 8 | from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor |
| 9 | 9 | ||
| 10 | if TYPE_CHECKING: | 10 | if TYPE_CHECKING: |
| 11 | from . import LingoWorld | 11 | from . import LingoWorld |
| @@ -13,12 +13,12 @@ if TYPE_CHECKING: | |||
| 13 | 13 | ||
| 14 | def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: | 14 | def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: |
| 15 | new_region = Region(room.name, world.player, world.multiworld) | 15 | new_region = Region(room.name, world.player, world.multiworld) |
| 16 | for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): | 16 | for location in player_logic.locations_by_room.get(room.name, {}): |
| 17 | new_location = LingoLocation(world.player, location.name, location.code, new_region) | 17 | new_location = LingoLocation(world.player, location.name, location.code, new_region) |
| 18 | new_location.access_rule = make_location_lambda(location, room.name, world, player_logic) | 18 | new_location.access_rule = make_location_lambda(location, world, player_logic) |
| 19 | new_region.locations.append(new_location) | 19 | new_region.locations.append(new_location) |
| 20 | if location.name in player_logic.EVENT_LOC_TO_ITEM: | 20 | if location.name in player_logic.event_loc_to_item: |
| 21 | event_name = player_logic.EVENT_LOC_TO_ITEM[location.name] | 21 | event_name = player_logic.event_loc_to_item[location.name] |
| 22 | event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) | 22 | event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) |
| 23 | new_location.place_locked_item(event_item) | 23 | new_location.place_locked_item(event_item) |
| 24 | 24 | ||
| @@ -31,7 +31,22 @@ def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_ | |||
| 31 | source_region.connect( | 31 | source_region.connect( |
| 32 | target_region, | 32 | target_region, |
| 33 | "Pilgrimage", | 33 | "Pilgrimage", |
| 34 | lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic)) | 34 | lambda state: lingo_can_use_pilgrimage(state, world, player_logic)) |
| 35 | |||
| 36 | |||
| 37 | def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, | ||
| 38 | door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): | ||
| 39 | connection = Entrance(world.player, description, source_region) | ||
| 40 | connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic) | ||
| 41 | |||
| 42 | source_region.exits.append(connection) | ||
| 43 | connection.connect(target_region) | ||
| 44 | |||
| 45 | if door is not None: | ||
| 46 | effective_room = target_region.name if door.room is None else door.room | ||
| 47 | if door.door not in player_logic.item_by_door.get(effective_room, {}): | ||
| 48 | for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: | ||
| 49 | world.multiworld.register_indirect_condition(regions[region], connection) | ||
| 35 | 50 | ||
| 36 | 51 | ||
| 37 | def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld", | 52 | 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 | |||
| 41 | 56 | ||
| 42 | target_region = regions[target_painting.room] | 57 | target_region = regions[target_painting.room] |
| 43 | source_region = regions[source_painting.room] | 58 | source_region = regions[source_painting.room] |
| 44 | source_region.connect( | 59 | |
| 45 | target_region, | 60 | entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)" |
| 46 | f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)", | 61 | connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world, |
| 47 | lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player, | 62 | player_logic) |
| 48 | player_logic)) | ||
| 49 | 63 | ||
| 50 | 64 | ||
| 51 | def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: | 65 | def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: |
| @@ -74,10 +88,8 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: | |||
| 74 | else: | 88 | else: |
| 75 | entrance_name += f" (through {room.name} - {entrance.door.door})" | 89 | entrance_name += f" (through {room.name} - {entrance.door.door})" |
| 76 | 90 | ||
| 77 | regions[entrance.room].connect( | 91 | connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, |
| 78 | regions[room.name], entrance_name, | 92 | player_logic) |
| 79 | lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, | ||
| 80 | player_logic)) | ||
| 81 | 93 | ||
| 82 | handle_pilgrim_room(regions, world, player_logic) | 94 | handle_pilgrim_room(regions, world, player_logic) |
| 83 | 95 | ||
| @@ -85,7 +97,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: | |||
| 85 | regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") | 97 | regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") |
| 86 | 98 | ||
| 87 | if painting_shuffle: | 99 | if painting_shuffle: |
| 88 | for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items(): | 100 | for warp_enter, warp_exit in player_logic.painting_mapping.items(): |
| 89 | connect_painting(regions, warp_enter, warp_exit, world, player_logic) | 101 | connect_painting(regions, warp_enter, warp_exit, world, player_logic) |
| 90 | 102 | ||
| 91 | world.multiworld.regions += regions.values() | 103 | world.multiworld.regions += regions.values() |
| diff --git a/rules.py b/rules.py index d59b8a1..ee9dcc4 100644 --- a/rules.py +++ b/rules.py | |||
| @@ -1,23 +1,23 @@ | |||
| 1 | from typing import TYPE_CHECKING | 1 | from typing import TYPE_CHECKING |
| 2 | 2 | ||
| 3 | from BaseClasses import CollectionState | 3 | from BaseClasses import CollectionState |
| 4 | from .options import VictoryCondition | 4 | from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation |
| 5 | from .player_logic import LingoPlayerLogic, PlayerLocation | 5 | from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor |
| 6 | from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor | ||
| 7 | 6 | ||
| 8 | if TYPE_CHECKING: | 7 | if TYPE_CHECKING: |
| 9 | from . import LingoWorld | 8 | from . import LingoWorld |
| 10 | 9 | ||
| 11 | 10 | ||
| 12 | def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int, | 11 | def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld", |
| 13 | player_logic: LingoPlayerLogic): | 12 | player_logic: LingoPlayerLogic): |
| 14 | if door is None: | 13 | if door is None: |
| 15 | return True | 14 | return True |
| 16 | 15 | ||
| 17 | return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic) | 16 | effective_room = room if door.room is None else door.room |
| 17 | return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) | ||
| 18 | 18 | ||
| 19 | 19 | ||
| 20 | def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic): | 20 | def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): |
| 21 | fake_pilgrimage = [ | 21 | fake_pilgrimage = [ |
| 22 | ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], | 22 | ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], |
| 23 | ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], | 23 | ["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: | |||
| 28 | ["Outside The Agreeable", "Tenacious Entrance"] | 28 | ["Outside The Agreeable", "Tenacious Entrance"] |
| 29 | ] | 29 | ] |
| 30 | for entrance in fake_pilgrimage: | 30 | for entrance in fake_pilgrimage: |
| 31 | if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player): | 31 | if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic): |
| 32 | return False | 32 | return False |
| 33 | 33 | ||
| 34 | return True | 34 | return True |
| 35 | 35 | ||
| 36 | 36 | ||
| 37 | def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld", | 37 | def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", |
| 38 | player_logic: LingoPlayerLogic): | 38 | player_logic: LingoPlayerLogic): |
| 39 | for panel in location.panels: | 39 | return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) |
| 40 | panel_room = room_name if panel.room is None else panel.room | ||
| 41 | if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic): | ||
| 42 | return False | ||
| 43 | |||
| 44 | return True | ||
| 45 | |||
| 46 | 40 | ||
| 47 | def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): | ||
| 48 | return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) | ||
| 49 | |||
| 50 | |||
| 51 | def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int, | ||
| 52 | player_logic: LingoPlayerLogic): | ||
| 53 | """ | ||
| 54 | Determines whether a door can be opened | ||
| 55 | """ | ||
| 56 | item_name = player_logic.ITEM_BY_DOOR[room][door] | ||
| 57 | if item_name in PROGRESSIVE_ITEMS: | ||
| 58 | progression = PROGRESSION_BY_ROOM[room][door] | ||
| 59 | return state.has(item_name, player, progression.index) | ||
| 60 | 41 | ||
| 61 | return state.has(item_name, player) | 42 | def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): |
| 43 | satisfied_count = 0 | ||
| 44 | for access_req in player_logic.mastery_reqs: | ||
| 45 | if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): | ||
| 46 | satisfied_count += 1 | ||
| 47 | return satisfied_count >= world.options.mastery_achievements.value | ||
| 62 | 48 | ||
| 63 | 49 | ||
| 64 | def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld", | 50 | def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): |
| 65 | player_logic: LingoPlayerLogic): | 51 | counted_panels = 0 |
| 66 | """ | 52 | state.update_reachable_regions(world.player) |
| 67 | Determines whether a panel can be solved | 53 | for region in state.reachable_regions[world.player]: |
| 68 | """ | 54 | for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []): |
| 69 | if start_room != room and not state.can_reach(room, "Region", world.player): | 55 | if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): |
| 70 | return False | 56 | counted_panels += panel_count |
| 57 | if counted_panels >= world.options.level_2_requirement.value - 1: | ||
| 58 | return True | ||
| 59 | return False | ||
| 71 | 60 | ||
| 72 | if room == "Second Room" and panel == "ANOTHER TRY" \ | ||
| 73 | and world.options.victory_condition == VictoryCondition.option_level_2 \ | ||
| 74 | and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1): | ||
| 75 | return False | ||
| 76 | 61 | ||
| 77 | panel_object = PANELS_BY_ROOM[room][panel] | 62 | def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld", |
| 78 | for req_room in panel_object.required_rooms: | 63 | player_logic: LingoPlayerLogic): |
| 64 | for req_room in access.rooms: | ||
| 79 | if not state.can_reach(req_room, "Region", world.player): | 65 | if not state.can_reach(req_room, "Region", world.player): |
| 80 | return False | 66 | return False |
| 81 | 67 | ||
| 82 | for req_door in panel_object.required_doors: | 68 | for req_door in access.doors: |
| 83 | if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room, | 69 | if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic): |
| 84 | req_door.door, world.player, player_logic): | ||
| 85 | return False | ||
| 86 | |||
| 87 | for req_panel in panel_object.required_panels: | ||
| 88 | if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room, | ||
| 89 | req_panel.panel, world, player_logic): | ||
| 90 | return False | 70 | return False |
| 91 | 71 | ||
| 92 | if len(panel_object.colors) > 0 and world.options.shuffle_colors: | 72 | if len(access.colors) > 0 and world.options.shuffle_colors: |
| 93 | for color in panel_object.colors: | 73 | for color in access.colors: |
| 94 | if not state.has(color.capitalize(), world.player): | 74 | if not state.has(color.capitalize(), world.player): |
| 95 | return False | 75 | return False |
| 96 | 76 | ||
| 97 | return True | 77 | return True |
| 98 | 78 | ||
| 99 | 79 | ||
| 100 | def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic): | 80 | def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld", |
| 101 | if location.name == player_logic.MASTERY_LOCATION: | 81 | player_logic: LingoPlayerLogic): |
| 102 | return lambda state: lingo_can_use_mastery_location(state, world) | 82 | """ |
| 83 | Determines whether a door can be opened | ||
| 84 | """ | ||
| 85 | if door not in player_logic.item_by_door.get(room, {}): | ||
| 86 | return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic) | ||
| 87 | |||
| 88 | item_name = player_logic.item_by_door[room][door] | ||
| 89 | if item_name in PROGRESSIVE_ITEMS: | ||
| 90 | progression = PROGRESSION_BY_ROOM[room][door] | ||
| 91 | return state.has(item_name, world.player, progression.index) | ||
| 92 | |||
| 93 | return state.has(item_name, world.player) | ||
| 94 | |||
| 95 | |||
| 96 | def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): | ||
| 97 | if location.name == player_logic.mastery_location: | ||
| 98 | return lambda state: lingo_can_use_mastery_location(state, world, player_logic) | ||
| 99 | |||
| 100 | if world.options.level_2_requirement > 1\ | ||
| 101 | and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location): | ||
| 102 | return lambda state: lingo_can_use_level_2_location(state, world, player_logic) | ||
| 103 | 103 | ||
| 104 | return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic) | 104 | return lambda state: lingo_can_use_location(state, location, world, player_logic) |
| diff --git a/test/TestPanelsanity.py b/test/TestPanelsanity.py new file mode 100644 index 0000000..34c1b38 --- /dev/null +++ b/test/TestPanelsanity.py | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | from . import LingoTestBase | ||
| 2 | |||
| 3 | |||
| 4 | class TestPanelHunt(LingoTestBase): | ||
| 5 | options = { | ||
| 6 | "shuffle_doors": "complex", | ||
| 7 | "location_checks": "insanity", | ||
| 8 | "victory_condition": "level_2", | ||
| 9 | "level_2_requirement": "15" | ||
| 10 | } | ||
| 11 | |||
| 12 | def test_another_try(self) -> None: | ||
| 13 | self.collect_by_name("The Traveled - Entrance") # idk why this is needed | ||
| 14 | self.assertFalse(self.can_reach_location("Second Room - ANOTHER TRY")) | ||
| 15 | self.assertFalse(self.can_reach_location("Second Room - Unlock Level 2")) | ||
| 16 | |||
| 17 | self.collect_by_name("Second Room - Exit Door") | ||
| 18 | self.assertTrue(self.can_reach_location("Second Room - ANOTHER TRY")) | ||
| 19 | self.assertTrue(self.can_reach_location("Second Room - Unlock Level 2")) | ||
