514 lines
26 KiB
Python
514 lines
26 KiB
Python
from enum import Enum
|
|
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
|
|
|
|
from Options import OptionError
|
|
from .datatypes import Door, DoorType, RoomAndDoor, RoomAndPanel
|
|
from .items import ALL_ITEM_TABLE, ItemType
|
|
from .locations import ALL_LOCATION_TABLE, LocationClassification
|
|
from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition
|
|
from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \
|
|
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, \
|
|
SUNWARP_ENTRANCES, SUNWARP_EXITS
|
|
|
|
if TYPE_CHECKING:
|
|
from . import LingoWorld
|
|
|
|
|
|
class AccessRequirements:
|
|
rooms: Set[str]
|
|
doors: Set[RoomAndDoor]
|
|
colors: Set[str]
|
|
|
|
def __init__(self):
|
|
self.rooms = set()
|
|
self.doors = set()
|
|
self.colors = set()
|
|
|
|
def merge(self, other: "AccessRequirements"):
|
|
self.rooms |= other.rooms
|
|
self.doors |= other.doors
|
|
self.colors |= other.colors
|
|
|
|
def __str__(self):
|
|
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})"
|
|
|
|
|
|
class PlayerLocation(NamedTuple):
|
|
name: str
|
|
code: Optional[int]
|
|
access: AccessRequirements
|
|
|
|
|
|
class ProgressiveItemBehavior(Enum):
|
|
DISABLE = 1
|
|
SPLIT = 2
|
|
PROGRESSIVE = 3
|
|
|
|
|
|
def should_split_progression(progression_name: str, world: "LingoWorld") -> ProgressiveItemBehavior:
|
|
if progression_name == "Progressive Orange Tower":
|
|
if world.options.progressive_orange_tower:
|
|
return ProgressiveItemBehavior.PROGRESSIVE
|
|
else:
|
|
return ProgressiveItemBehavior.SPLIT
|
|
elif progression_name == "Progressive Colorful":
|
|
if world.options.progressive_colorful:
|
|
return ProgressiveItemBehavior.PROGRESSIVE
|
|
else:
|
|
return ProgressiveItemBehavior.SPLIT
|
|
|
|
return ProgressiveItemBehavior.PROGRESSIVE
|
|
|
|
|
|
class LingoPlayerLogic:
|
|
"""
|
|
Defines logic after a player's options have been applied
|
|
"""
|
|
|
|
item_by_door: Dict[str, Dict[str, str]]
|
|
|
|
locations_by_room: Dict[str, List[PlayerLocation]]
|
|
real_locations: List[str]
|
|
|
|
event_loc_to_item: Dict[str, str]
|
|
real_items: List[str]
|
|
|
|
victory_condition: str
|
|
mastery_location: str
|
|
level_2_location: str
|
|
|
|
painting_mapping: Dict[str, str]
|
|
|
|
forced_good_item: str
|
|
|
|
panel_reqs: Dict[str, Dict[str, AccessRequirements]]
|
|
door_reqs: Dict[str, Dict[str, AccessRequirements]]
|
|
mastery_reqs: List[AccessRequirements]
|
|
counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]]
|
|
|
|
sunwarp_mapping: List[int]
|
|
sunwarp_entrances: List[str]
|
|
sunwarp_exits: List[str]
|
|
|
|
def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"):
|
|
"""
|
|
Creates a location. This function determines the access requirements for the location by combining and
|
|
flattening the requirements for each of the given panels.
|
|
"""
|
|
access_reqs = AccessRequirements()
|
|
for panel in panels:
|
|
if panel.room is not None and panel.room != room:
|
|
access_reqs.rooms.add(panel.room)
|
|
|
|
panel_room = room if panel.room is None else panel.room
|
|
sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel, world)
|
|
access_reqs.merge(sub_access_reqs)
|
|
|
|
self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs))
|
|
|
|
def set_door_item(self, room: str, door: str, item: str):
|
|
self.item_by_door.setdefault(room, {})[door] = item
|
|
|
|
def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
|
|
if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
|
|
progression_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
|
|
progression_handling = should_split_progression(progression_name, world)
|
|
|
|
if progression_handling == ProgressiveItemBehavior.SPLIT:
|
|
self.set_door_item(room_name, door_data.name, door_data.item_name)
|
|
self.real_items.append(door_data.item_name)
|
|
elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE:
|
|
progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
|
|
self.set_door_item(room_name, door_data.name, progressive_item_name)
|
|
self.real_items.append(progressive_item_name)
|
|
else:
|
|
self.set_door_item(room_name, door_data.name, door_data.item_name)
|
|
self.real_items.append(door_data.item_name)
|
|
|
|
def __init__(self, world: "LingoWorld"):
|
|
self.item_by_door = {}
|
|
self.locations_by_room = {}
|
|
self.real_locations = []
|
|
self.event_loc_to_item = {}
|
|
self.real_items = []
|
|
self.victory_condition = ""
|
|
self.mastery_location = ""
|
|
self.level_2_location = ""
|
|
self.painting_mapping = {}
|
|
self.forced_good_item = ""
|
|
self.panel_reqs = {}
|
|
self.door_reqs = {}
|
|
self.mastery_reqs = []
|
|
self.counting_panel_reqs = {}
|
|
self.sunwarp_mapping = []
|
|
|
|
door_shuffle = world.options.shuffle_doors
|
|
color_shuffle = world.options.shuffle_colors
|
|
painting_shuffle = world.options.shuffle_paintings
|
|
location_checks = world.options.location_checks
|
|
victory_condition = world.options.victory_condition
|
|
early_color_hallways = world.options.early_color_hallways
|
|
|
|
if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none:
|
|
raise OptionError("You cannot have reduced location checks when door shuffle is on, because there would not"
|
|
" be enough locations for all of the door items.")
|
|
|
|
# Create door items, where needed.
|
|
door_groups: Set[str] = set()
|
|
for room_name, room_data in DOORS_BY_ROOM.items():
|
|
for door_name, door_data in room_data.items():
|
|
if door_data.skip_item is False and door_data.event is False:
|
|
if door_data.type == DoorType.NORMAL and door_shuffle != ShuffleDoors.option_none:
|
|
if door_data.door_group is not None and door_shuffle == ShuffleDoors.option_simple:
|
|
# Grouped doors are handled differently if shuffle doors is on simple.
|
|
self.set_door_item(room_name, door_name, door_data.door_group)
|
|
door_groups.add(door_data.door_group)
|
|
else:
|
|
self.handle_non_grouped_door(room_name, door_data, world)
|
|
elif door_data.type == DoorType.SUNWARP:
|
|
if world.options.sunwarp_access == SunwarpAccess.option_unlock:
|
|
self.set_door_item(room_name, door_name, "Sunwarps")
|
|
door_groups.add("Sunwarps")
|
|
elif world.options.sunwarp_access == SunwarpAccess.option_individual:
|
|
self.set_door_item(room_name, door_name, door_data.item_name)
|
|
self.real_items.append(door_data.item_name)
|
|
elif world.options.sunwarp_access == SunwarpAccess.option_progressive:
|
|
self.set_door_item(room_name, door_name, "Progressive Pilgrimage")
|
|
self.real_items.append("Progressive Pilgrimage")
|
|
elif door_data.type == DoorType.SUN_PAINTING:
|
|
if not world.options.enable_pilgrimage:
|
|
self.set_door_item(room_name, door_name, door_data.item_name)
|
|
self.real_items.append(door_data.item_name)
|
|
|
|
self.real_items += door_groups
|
|
|
|
# Create color items, if needed.
|
|
if color_shuffle:
|
|
self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
|
|
|
|
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
|
|
for room_name, room_data in PANELS_BY_ROOM.items():
|
|
for panel_name, panel_data in room_data.items():
|
|
if panel_data.achievement:
|
|
access_req = AccessRequirements()
|
|
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
|
|
access_req.rooms.add(room_name)
|
|
|
|
self.mastery_reqs.append(access_req)
|
|
|
|
# Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
|
|
# to prevent the actual victory condition from becoming a check.
|
|
self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
|
|
self.level_2_location = "Second Room - LEVEL 2"
|
|
|
|
if victory_condition == VictoryCondition.option_the_end:
|
|
self.victory_condition = "Orange Tower Seventh Floor - THE END"
|
|
self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world)
|
|
self.event_loc_to_item["The End (Solved)"] = "Victory"
|
|
elif victory_condition == VictoryCondition.option_the_master:
|
|
self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
|
|
self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements"
|
|
|
|
self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, [], world)
|
|
self.event_loc_to_item[self.mastery_location] = "Victory"
|
|
elif victory_condition == VictoryCondition.option_level_2:
|
|
self.victory_condition = "Second Room - LEVEL 2"
|
|
self.level_2_location = "Second Room - Unlock Level 2"
|
|
|
|
self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")],
|
|
world)
|
|
self.event_loc_to_item[self.level_2_location] = "Victory"
|
|
|
|
if world.options.level_2_requirement == 1:
|
|
raise OptionError("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.")
|
|
elif victory_condition == VictoryCondition.option_pilgrimage:
|
|
self.victory_condition = "Pilgrim Antechamber - PILGRIM"
|
|
self.add_location("Pilgrim Antechamber", "PILGRIM (Solved)", None,
|
|
[RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
|
|
self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
|
|
|
|
# Create groups of counting panel access requirements for the LEVEL 2 check.
|
|
self.create_panel_hunt_events(world)
|
|
|
|
# Instantiate all real locations.
|
|
location_classification = LocationClassification.normal
|
|
if location_checks == LocationChecks.option_reduced:
|
|
location_classification = LocationClassification.reduced
|
|
elif location_checks == LocationChecks.option_insanity:
|
|
location_classification = LocationClassification.insanity
|
|
|
|
if door_shuffle != ShuffleDoors.option_none and not early_color_hallways:
|
|
location_classification |= LocationClassification.small_sphere_one
|
|
|
|
for location_name, location_data in ALL_LOCATION_TABLE.items():
|
|
if location_name != self.victory_condition:
|
|
if not (location_classification & location_data.classification):
|
|
continue
|
|
|
|
self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world)
|
|
self.real_locations.append(location_name)
|
|
|
|
if world.options.enable_pilgrimage and world.options.sunwarp_access == SunwarpAccess.option_disabled:
|
|
raise OptionError("Sunwarps cannot be disabled when pilgrimage is enabled.")
|
|
|
|
if world.options.shuffle_sunwarps:
|
|
if world.options.sunwarp_access == SunwarpAccess.option_disabled:
|
|
raise OptionError("Sunwarps cannot be shuffled if they are disabled.")
|
|
|
|
self.sunwarp_mapping = list(range(0, 12))
|
|
world.random.shuffle(self.sunwarp_mapping)
|
|
|
|
sunwarp_rooms = SUNWARP_ENTRANCES + SUNWARP_EXITS
|
|
self.sunwarp_entrances = [sunwarp_rooms[i] for i in self.sunwarp_mapping[0:6]]
|
|
self.sunwarp_exits = [sunwarp_rooms[i] for i in self.sunwarp_mapping[6:12]]
|
|
else:
|
|
self.sunwarp_entrances = SUNWARP_ENTRANCES
|
|
self.sunwarp_exits = SUNWARP_EXITS
|
|
|
|
# Create the paintings mapping, if painting shuffle is on.
|
|
if painting_shuffle:
|
|
# Shuffle paintings until we get something workable.
|
|
workable_paintings = False
|
|
for i in range(0, 20):
|
|
workable_paintings = self.randomize_paintings(world)
|
|
if workable_paintings:
|
|
break
|
|
|
|
if not workable_paintings:
|
|
raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 "
|
|
"iterations. This is very unlikely to happen on its own, and probably indicates some "
|
|
"kind of logic error.")
|
|
|
|
if door_shuffle != ShuffleDoors.option_none and location_checks != LocationChecks.option_insanity \
|
|
and not early_color_hallways and world.multiworld.players > 1:
|
|
# Under the combination of door shuffle, normal location checks, and no early color hallways, sphere 1 is
|
|
# only three checks. In a multiplayer situation, this can be frustrating for the player because they are
|
|
# more likely to be stuck in the starting room for a long time. To remedy this, we will force a useful item
|
|
# onto the GOOD LUCK check under these circumstances. The goal is to expand sphere 1 to at least four
|
|
# checks (and likely more than that).
|
|
#
|
|
# Note: A very low LEVEL 2 requirement would naturally expand sphere 1 to four checks, but this is a very
|
|
# uncommon configuration, so we will ignore it and force a good item anyway.
|
|
|
|
# Starting Room - Back Right Door gives access to OPEN and DEAD END.
|
|
# Starting Room - Exit Door gives access to OPEN and TRACE.
|
|
good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
|
|
|
|
if not color_shuffle and not world.options.enable_pilgrimage:
|
|
# HOT CRUST and THIS.
|
|
good_item_options.append("Pilgrim Room - Sun Painting")
|
|
|
|
if not color_shuffle:
|
|
if door_shuffle == ShuffleDoors.option_simple:
|
|
# WELCOME BACK, CLOCKWISE, and DRAWL + RUNS.
|
|
good_item_options.append("Welcome Back Doors")
|
|
else:
|
|
# WELCOME BACK and CLOCKWISE.
|
|
good_item_options.append("Welcome Back Area - Shortcut to Starting Room")
|
|
|
|
if door_shuffle == ShuffleDoors.option_simple:
|
|
# Color hallways access (NOTE: reconsider when sunwarp shuffling exists).
|
|
good_item_options.append("Rhyme Room Doors")
|
|
|
|
# When painting shuffle is off, most Starting Room paintings give color hallways access. The Wondrous's
|
|
# painting does not, but it gives access to SHRINK and WELCOME BACK.
|
|
for painting_obj in PAINTINGS.values():
|
|
if not painting_obj.enter_only or painting_obj.required_door is None\
|
|
or painting_obj.room != "Starting Room":
|
|
continue
|
|
|
|
# If painting shuffle is on, we only want to consider paintings that actually go somewhere.
|
|
#
|
|
# NOTE: This does not guarantee that there will be any checks on the other side.
|
|
if painting_shuffle and painting_obj.id not in self.painting_mapping.keys():
|
|
continue
|
|
|
|
pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
|
|
good_item_options.append(pdoor.item_name)
|
|
|
|
# Copied from The Witness -- remove any plandoed items from the possible good items set.
|
|
for v in world.multiworld.plando_items[world.player]:
|
|
if v.get("from_pool", True):
|
|
for item_key in {"item", "items"}:
|
|
if item_key in v:
|
|
if type(v[item_key]) is str:
|
|
if v[item_key] in good_item_options:
|
|
good_item_options.remove(v[item_key])
|
|
elif type(v[item_key]) is dict:
|
|
for item, weight in v[item_key].items():
|
|
if weight and item in good_item_options:
|
|
good_item_options.remove(item)
|
|
else:
|
|
# Other type of iterable
|
|
for item in v[item_key]:
|
|
if item in good_item_options:
|
|
good_item_options.remove(item)
|
|
|
|
if len(good_item_options) > 0:
|
|
self.forced_good_item = world.random.choice(good_item_options)
|
|
self.real_items.remove(self.forced_good_item)
|
|
self.real_locations.remove("Second Room - Good Luck")
|
|
|
|
def randomize_paintings(self, world: "LingoWorld") -> bool:
|
|
self.painting_mapping.clear()
|
|
|
|
door_shuffle = world.options.shuffle_doors
|
|
|
|
# First, assign mappings to the required-exit paintings. We ensure that req-blocked paintings do not lead to
|
|
# required paintings.
|
|
req_exits = []
|
|
required_painting_rooms = REQUIRED_PAINTING_ROOMS
|
|
if door_shuffle == ShuffleDoors.option_none:
|
|
required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
|
|
req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors]
|
|
req_enterable = [painting_id for painting_id, painting in PAINTINGS.items()
|
|
if not painting.exit_only and not painting.disable and not painting.req_blocked and
|
|
not painting.req_blocked_when_no_doors and painting.room not in required_painting_rooms]
|
|
else:
|
|
req_enterable = [painting_id for painting_id, painting in PAINTINGS.items()
|
|
if not painting.exit_only and not painting.disable and not painting.req_blocked and
|
|
painting.room not in required_painting_rooms]
|
|
req_exits += [painting_id for painting_id, painting in PAINTINGS.items()
|
|
if painting.exit_only and painting.required]
|
|
req_entrances = world.random.sample(req_enterable, len(req_exits))
|
|
|
|
self.painting_mapping = dict(zip(req_entrances, req_exits))
|
|
|
|
# Next, determine the rest of the exit paintings.
|
|
exitable = [painting_id for painting_id, painting in PAINTINGS.items()
|
|
if not painting.enter_only and not painting.disable and painting_id not in req_exits and
|
|
painting_id not in req_entrances]
|
|
nonreq_exits = world.random.sample(exitable, PAINTING_EXITS - len(req_exits))
|
|
chosen_exits = req_exits + nonreq_exits
|
|
|
|
# Determine the rest of the entrance paintings.
|
|
enterable = [painting_id for painting_id, painting in PAINTINGS.items()
|
|
if not painting.exit_only and not painting.disable and painting_id not in chosen_exits and
|
|
painting_id not in req_entrances]
|
|
chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES - len(req_entrances))
|
|
|
|
# Assign one entrance to each non-required exit, to ensure that the total number of exits is achieved.
|
|
for warp_exit in nonreq_exits:
|
|
warp_enter = world.random.choice(chosen_entrances)
|
|
chosen_entrances.remove(warp_enter)
|
|
self.painting_mapping[warp_enter] = warp_exit
|
|
|
|
# Assign each of the remaining entrances to any required or non-required exit.
|
|
for warp_enter in chosen_entrances:
|
|
warp_exit = world.random.choice(chosen_exits)
|
|
self.painting_mapping[warp_enter] = warp_exit
|
|
|
|
# The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves).
|
|
# There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the
|
|
# exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall
|
|
# is forced to point to the vanilla exit.
|
|
if "eye_painting_2" not in self.painting_mapping.keys():
|
|
self.painting_mapping["eye_painting"] = "eye_painting_2"
|
|
|
|
# Just for sanity's sake, ensure that all required painting rooms are accessed.
|
|
for painting_id, painting in PAINTINGS.items():
|
|
if painting_id not in self.painting_mapping.values() \
|
|
and (painting.required or (painting.required_when_no_doors and
|
|
door_shuffle == ShuffleDoors.option_none)):
|
|
return False
|
|
|
|
return True
|
|
|
|
def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld"):
|
|
"""
|
|
Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in
|
|
the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required
|
|
by this panel. Memoization is used so that no panel is evaluated more than once.
|
|
"""
|
|
if panel not in self.panel_reqs.setdefault(room, {}):
|
|
access_reqs = AccessRequirements()
|
|
panel_object = PANELS_BY_ROOM[room][panel]
|
|
|
|
for req_room in panel_object.required_rooms:
|
|
access_reqs.rooms.add(req_room)
|
|
|
|
for req_door in panel_object.required_doors:
|
|
door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door]
|
|
if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none:
|
|
sub_access_reqs = self.calculate_door_requirements(
|
|
room if req_door.room is None else req_door.room, req_door.door, world)
|
|
access_reqs.merge(sub_access_reqs)
|
|
else:
|
|
access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door))
|
|
|
|
for color in panel_object.colors:
|
|
access_reqs.colors.add(color)
|
|
|
|
for req_panel in panel_object.required_panels:
|
|
if req_panel.room is not None and req_panel.room != room:
|
|
access_reqs.rooms.add(req_panel.room)
|
|
|
|
sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room,
|
|
req_panel.panel, world)
|
|
access_reqs.merge(sub_access_reqs)
|
|
|
|
self.panel_reqs[room][panel] = access_reqs
|
|
|
|
return self.panel_reqs[room][panel]
|
|
|
|
def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld"):
|
|
"""
|
|
Similar to calculate_panel_requirements, but for event doors.
|
|
"""
|
|
if door not in self.door_reqs.setdefault(room, {}):
|
|
access_reqs = AccessRequirements()
|
|
door_object = DOORS_BY_ROOM[room][door]
|
|
|
|
for req_panel in door_object.panels:
|
|
panel_room = room if req_panel.room is None else req_panel.room
|
|
access_reqs.rooms.add(panel_room)
|
|
sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world)
|
|
access_reqs.merge(sub_access_reqs)
|
|
|
|
self.door_reqs[room][door] = access_reqs
|
|
|
|
return self.door_reqs[room][door]
|
|
|
|
def create_panel_hunt_events(self, world: "LingoWorld"):
|
|
"""
|
|
Creates the event locations/items used for determining access to the LEVEL 2 panel. Instead of creating an event
|
|
for every single counting panel in the game, we try to coalesce panels with identical access rules into the same
|
|
event. Right now, this means the following:
|
|
|
|
When color shuffle is off, panels in a room with no extra access requirements (room, door, or other panel) are
|
|
all coalesced into one event.
|
|
|
|
When color shuffle is on, single-colored panels (including white) in a room are combined into one event per
|
|
color. Multicolored panels and panels with any extra access requirements are not coalesced, and will each
|
|
receive their own event.
|
|
"""
|
|
for room_name, room_data in PANELS_BY_ROOM.items():
|
|
unhindered_panels_by_color: dict[Optional[str], int] = {}
|
|
|
|
for panel_name, panel_data in room_data.items():
|
|
# We won't count non-counting panels. THE MASTER has special access rules and is handled separately.
|
|
if panel_data.non_counting or panel_name == "THE MASTER":
|
|
continue
|
|
|
|
# We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will
|
|
# only coalesce single-color panels. Chains/stacks/combo puzzles will be separate.
|
|
if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\
|
|
or len(panel_data.required_rooms) > 0\
|
|
or (world.options.shuffle_colors and len(panel_data.colors) > 1):
|
|
self.counting_panel_reqs.setdefault(room_name, []).append(
|
|
(self.calculate_panel_requirements(room_name, panel_name, world), 1))
|
|
else:
|
|
if len(panel_data.colors) == 0 or not world.options.shuffle_colors:
|
|
color = None
|
|
else:
|
|
color = panel_data.colors[0]
|
|
|
|
unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1
|
|
|
|
for color, panel_count in unhindered_panels_by_color.items():
|
|
access_reqs = AccessRequirements()
|
|
if color is not None:
|
|
access_reqs.colors.add(color)
|
|
|
|
self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count))
|