879 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			879 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from collections import defaultdict | ||
|  | from collections.abc import Generator | ||
|  | from dataclasses import dataclass | ||
|  | from typing import TYPE_CHECKING, ClassVar, Optional | ||
|  | 
 | ||
|  | from Fill import FillError | ||
|  | from Options import OptionError | ||
|  | 
 | ||
|  | from .. import Macros | ||
|  | from ..Locations import LOCATION_TABLE, TWWFlag, split_location_name_by_zone | ||
|  | 
 | ||
|  | if TYPE_CHECKING: | ||
|  |     from .. import TWWWorld | ||
|  | 
 | ||
|  | 
 | ||
|  | @dataclass(frozen=True) | ||
|  | class ZoneEntrance: | ||
|  |     """
 | ||
|  |     A data class that encapsulates information about a zone entrance. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     entrance_name: str | ||
|  |     island_name: Optional[str] = None | ||
|  |     nested_in: Optional["ZoneExit"] = None | ||
|  | 
 | ||
|  |     @property | ||
|  |     def is_nested(self) -> bool: | ||
|  |         """
 | ||
|  |         Determine if this entrance is nested within another entrance. | ||
|  | 
 | ||
|  |         :return: `True` if the entrance is nested, `False` otherwise. | ||
|  |         """
 | ||
|  |         return self.nested_in is not None | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         """
 | ||
|  |         Provide a string representation of the zone exit. | ||
|  | 
 | ||
|  |         :return: A string representing the zone exit. | ||
|  |         """
 | ||
|  |         return f"ZoneEntrance('{self.entrance_name}')" | ||
|  | 
 | ||
|  |     all: ClassVar[dict[str, "ZoneEntrance"]] = {} | ||
|  | 
 | ||
|  |     def __post_init__(self) -> None: | ||
|  |         ZoneEntrance.all[self.entrance_name] = self | ||
|  | 
 | ||
|  |         # Must be an island entrance XOR must be a nested entrance. | ||
|  |         assert (self.island_name is None) ^ (self.nested_in is None) | ||
|  | 
 | ||
|  | 
 | ||
|  | @dataclass(frozen=True) | ||
|  | class ZoneExit: | ||
|  |     """
 | ||
|  |     A data class that encapsulates information about a zone exit. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     unique_name: str | ||
|  |     zone_name: Optional[str] = None | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         """
 | ||
|  |         Provide a string representation of the zone exit. | ||
|  | 
 | ||
|  |         :return: A string representing the zone exit. | ||
|  |         """
 | ||
|  |         return f"ZoneExit('{self.unique_name}')" | ||
|  | 
 | ||
|  |     all: ClassVar[dict[str, "ZoneExit"]] = {} | ||
|  | 
 | ||
|  |     def __post_init__(self) -> None: | ||
|  |         ZoneExit.all[self.unique_name] = self | ||
|  | 
 | ||
|  | 
 | ||
|  | DUNGEON_ENTRANCES: list[ZoneEntrance] = [ | ||
|  |     ZoneEntrance("Dungeon Entrance on Dragon Roost Island", "Dragon Roost Island"), | ||
|  |     ZoneEntrance("Dungeon Entrance in Forest Haven Sector", "Forest Haven"), | ||
|  |     ZoneEntrance("Dungeon Entrance in Tower of the Gods Sector", "Tower of the Gods Sector"), | ||
|  |     ZoneEntrance("Dungeon Entrance on Headstone Island", "Headstone Island"), | ||
|  |     ZoneEntrance("Dungeon Entrance on Gale Isle", "Gale Isle"), | ||
|  | ] | ||
|  | DUNGEON_EXITS: list[ZoneExit] = [ | ||
|  |     ZoneExit("Dragon Roost Cavern", "Dragon Roost Cavern"), | ||
|  |     ZoneExit("Forbidden Woods", "Forbidden Woods"), | ||
|  |     ZoneExit("Tower of the Gods", "Tower of the Gods"), | ||
|  |     ZoneExit("Earth Temple", "Earth Temple"), | ||
|  |     ZoneExit("Wind Temple", "Wind Temple"), | ||
|  | ] | ||
|  | 
 | ||
|  | MINIBOSS_ENTRANCES: list[ZoneEntrance] = [ | ||
|  |     ZoneEntrance("Miniboss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]), | ||
|  |     ZoneEntrance("Miniboss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]), | ||
|  |     ZoneEntrance("Miniboss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]), | ||
|  |     ZoneEntrance("Miniboss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]), | ||
|  |     ZoneEntrance("Miniboss Entrance in Hyrule Castle", "Tower of the Gods Sector"), | ||
|  | ] | ||
|  | MINIBOSS_EXITS: list[ZoneExit] = [ | ||
|  |     ZoneExit("Forbidden Woods Miniboss Arena"), | ||
|  |     ZoneExit("Tower of the Gods Miniboss Arena"), | ||
|  |     ZoneExit("Earth Temple Miniboss Arena"), | ||
|  |     ZoneExit("Wind Temple Miniboss Arena"), | ||
|  |     ZoneExit("Master Sword Chamber"), | ||
|  | ] | ||
|  | 
 | ||
|  | BOSS_ENTRANCES: list[ZoneEntrance] = [ | ||
|  |     ZoneEntrance("Boss Entrance in Dragon Roost Cavern", nested_in=ZoneExit.all["Dragon Roost Cavern"]), | ||
|  |     ZoneEntrance("Boss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]), | ||
|  |     ZoneEntrance("Boss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]), | ||
|  |     ZoneEntrance("Boss Entrance in Forsaken Fortress", "Forsaken Fortress Sector"), | ||
|  |     ZoneEntrance("Boss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]), | ||
|  |     ZoneEntrance("Boss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]), | ||
|  | ] | ||
|  | BOSS_EXITS: list[ZoneExit] = [ | ||
|  |     ZoneExit("Gohma Boss Arena"), | ||
|  |     ZoneExit("Kalle Demos Boss Arena"), | ||
|  |     ZoneExit("Gohdan Boss Arena"), | ||
|  |     ZoneExit("Helmaroc King Boss Arena"), | ||
|  |     ZoneExit("Jalhalla Boss Arena"), | ||
|  |     ZoneExit("Molgera Boss Arena"), | ||
|  | ] | ||
|  | 
 | ||
|  | SECRET_CAVE_ENTRANCES: list[ZoneEntrance] = [ | ||
|  |     ZoneEntrance("Secret Cave Entrance on Outset Island", "Outset Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Fire Mountain", "Fire Mountain"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Private Oasis", "Private Oasis"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Angular Isles", "Angular Isles"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Boating Course", "Boating Course"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Overlook Island", "Overlook Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Bomb Island", "Bomb Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Shark Island", "Shark Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island"), | ||
|  |     ZoneEntrance("Secret Cave Entrance on Star Island", "Star Island"), | ||
|  | ] | ||
|  | SECRET_CAVE_EXITS: list[ZoneExit] = [ | ||
|  |     ZoneExit("Savage Labyrinth", zone_name="Outset Island"), | ||
|  |     ZoneExit("Dragon Roost Island Secret Cave", zone_name="Dragon Roost Island"), | ||
|  |     ZoneExit("Fire Mountain Secret Cave", zone_name="Fire Mountain"), | ||
|  |     ZoneExit("Ice Ring Isle Secret Cave", zone_name="Ice Ring Isle"), | ||
|  |     ZoneExit("Cabana Labyrinth", zone_name="Private Oasis"), | ||
|  |     ZoneExit("Needle Rock Isle Secret Cave", zone_name="Needle Rock Isle"), | ||
|  |     ZoneExit("Angular Isles Secret Cave", zone_name="Angular Isles"), | ||
|  |     ZoneExit("Boating Course Secret Cave", zone_name="Boating Course"), | ||
|  |     ZoneExit("Stone Watcher Island Secret Cave", zone_name="Stone Watcher Island"), | ||
|  |     ZoneExit("Overlook Island Secret Cave", zone_name="Overlook Island"), | ||
|  |     ZoneExit("Bird's Peak Rock Secret Cave", zone_name="Bird's Peak Rock"), | ||
|  |     ZoneExit("Pawprint Isle Chuchu Cave", zone_name="Pawprint Isle"), | ||
|  |     ZoneExit("Pawprint Isle Wizzrobe Cave"), | ||
|  |     ZoneExit("Diamond Steppe Island Warp Maze Cave", zone_name="Diamond Steppe Island"), | ||
|  |     ZoneExit("Bomb Island Secret Cave", zone_name="Bomb Island"), | ||
|  |     ZoneExit("Rock Spire Isle Secret Cave", zone_name="Rock Spire Isle"), | ||
|  |     ZoneExit("Shark Island Secret Cave", zone_name="Shark Island"), | ||
|  |     ZoneExit("Cliff Plateau Isles Secret Cave", zone_name="Cliff Plateau Isles"), | ||
|  |     ZoneExit("Horseshoe Island Secret Cave", zone_name="Horseshoe Island"), | ||
|  |     ZoneExit("Star Island Secret Cave", zone_name="Star Island"), | ||
|  | ] | ||
|  | 
 | ||
|  | SECRET_CAVE_INNER_ENTRANCES: list[ZoneEntrance] = [ | ||
|  |     ZoneEntrance("Inner Entrance in Ice Ring Isle Secret Cave", nested_in=ZoneExit.all["Ice Ring Isle Secret Cave"]), | ||
|  |     ZoneEntrance( | ||
|  |         "Inner Entrance in Cliff Plateau Isles Secret Cave", nested_in=ZoneExit.all["Cliff Plateau Isles Secret Cave"] | ||
|  |     ), | ||
|  | ] | ||
|  | SECRET_CAVE_INNER_EXITS: list[ZoneExit] = [ | ||
|  |     ZoneExit("Ice Ring Isle Inner Cave"), | ||
|  |     ZoneExit("Cliff Plateau Isles Inner Cave"), | ||
|  | ] | ||
|  | 
 | ||
|  | FAIRY_FOUNTAIN_ENTRANCES: list[ZoneEntrance] = [ | ||
|  |     ZoneEntrance("Fairy Fountain Entrance on Outset Island", "Outset Island"), | ||
|  |     ZoneEntrance("Fairy Fountain Entrance on Thorned Fairy Island", "Thorned Fairy Island"), | ||
|  |     ZoneEntrance("Fairy Fountain Entrance on Eastern Fairy Island", "Eastern Fairy Island"), | ||
|  |     ZoneEntrance("Fairy Fountain Entrance on Western Fairy Island", "Western Fairy Island"), | ||
|  |     ZoneEntrance("Fairy Fountain Entrance on Southern Fairy Island", "Southern Fairy Island"), | ||
|  |     ZoneEntrance("Fairy Fountain Entrance on Northern Fairy Island", "Northern Fairy Island"), | ||
|  | ] | ||
|  | FAIRY_FOUNTAIN_EXITS: list[ZoneExit] = [ | ||
|  |     ZoneExit("Outset Fairy Fountain"), | ||
|  |     ZoneExit("Thorned Fairy Fountain", zone_name="Thorned Fairy Island"), | ||
|  |     ZoneExit("Eastern Fairy Fountain", zone_name="Eastern Fairy Island"), | ||
|  |     ZoneExit("Western Fairy Fountain", zone_name="Western Fairy Island"), | ||
|  |     ZoneExit("Southern Fairy Fountain", zone_name="Southern Fairy Island"), | ||
|  |     ZoneExit("Northern Fairy Fountain", zone_name="Northern Fairy Island"), | ||
|  | ] | ||
|  | 
 | ||
|  | DUNGEON_INNER_EXITS: list[ZoneExit] = ( | ||
|  |     MINIBOSS_EXITS | ||
|  |     + BOSS_EXITS | ||
|  | ) | ||
|  | 
 | ||
|  | ALL_ENTRANCES: list[ZoneEntrance] = ( | ||
|  |     DUNGEON_ENTRANCES | ||
|  |     + MINIBOSS_ENTRANCES | ||
|  |     + BOSS_ENTRANCES | ||
|  |     + SECRET_CAVE_ENTRANCES | ||
|  |     + SECRET_CAVE_INNER_ENTRANCES | ||
|  |     + FAIRY_FOUNTAIN_ENTRANCES | ||
|  | ) | ||
|  | ALL_EXITS: list[ZoneExit] = ( | ||
|  |     DUNGEON_EXITS | ||
|  |     + MINIBOSS_EXITS | ||
|  |     + BOSS_EXITS | ||
|  |     + SECRET_CAVE_EXITS | ||
|  |     + SECRET_CAVE_INNER_EXITS | ||
|  |     + FAIRY_FOUNTAIN_EXITS | ||
|  | ) | ||
|  | 
 | ||
|  | ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES: list[TWWFlag] = [ | ||
|  |     TWWFlag.DUNGEON, | ||
|  |     TWWFlag.PZL_CVE, | ||
|  |     TWWFlag.CBT_CVE, | ||
|  |     TWWFlag.SAVAGE, | ||
|  |     TWWFlag.GRT_FRY, | ||
|  | ] | ||
|  | ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES: dict[str, ZoneExit] = { | ||
|  |   "Forbidden Woods - Mothula Miniboss Room":           ZoneExit.all["Forbidden Woods Miniboss Arena"], | ||
|  |   "Tower of the Gods - Darknut Miniboss Room":         ZoneExit.all["Tower of the Gods Miniboss Arena"], | ||
|  |   "Earth Temple - Stalfos Miniboss Room":              ZoneExit.all["Earth Temple Miniboss Arena"], | ||
|  |   "Wind Temple - Wizzrobe Miniboss Room":              ZoneExit.all["Wind Temple Miniboss Arena"], | ||
|  |   "Hyrule - Master Sword Chamber":                     ZoneExit.all["Master Sword Chamber"], | ||
|  | 
 | ||
|  |   "Dragon Roost Cavern - Gohma Heart Container":       ZoneExit.all["Gohma Boss Arena"], | ||
|  |   "Forbidden Woods - Kalle Demos Heart Container":     ZoneExit.all["Kalle Demos Boss Arena"], | ||
|  |   "Tower of the Gods - Gohdan Heart Container":        ZoneExit.all["Gohdan Boss Arena"], | ||
|  |   "Forsaken Fortress - Helmaroc King Heart Container": ZoneExit.all["Helmaroc King Boss Arena"], | ||
|  |   "Earth Temple - Jalhalla Heart Container":           ZoneExit.all["Jalhalla Boss Arena"], | ||
|  |   "Wind Temple - Molgera Heart Container":             ZoneExit.all["Molgera Boss Arena"], | ||
|  | 
 | ||
|  |   "Pawprint Isle - Wizzrobe Cave":                     ZoneExit.all["Pawprint Isle Wizzrobe Cave"], | ||
|  | 
 | ||
|  |   "Ice Ring Isle - Inner Cave - Chest":                ZoneExit.all["Ice Ring Isle Inner Cave"], | ||
|  |   "Cliff Plateau Isles - Highest Isle":                ZoneExit.all["Cliff Plateau Isles Inner Cave"], | ||
|  | 
 | ||
|  |   "Outset Island - Great Fairy":                       ZoneExit.all["Outset Fairy Fountain"], | ||
|  | } | ||
|  | 
 | ||
|  | MINIBOSS_EXIT_TO_DUNGEON: dict[str, str] = { | ||
|  |     "Forbidden Woods Miniboss Arena":   "Forbidden Woods", | ||
|  |     "Tower of the Gods Miniboss Arena": "Tower of the Gods", | ||
|  |     "Earth Temple Miniboss Arena":      "Earth Temple", | ||
|  |     "Wind Temple Miniboss Arena":       "Wind Temple", | ||
|  | } | ||
|  | 
 | ||
|  | BOSS_EXIT_TO_DUNGEON: dict[str, str] = { | ||
|  |     "Gohma Boss Arena":         "Dragon Roost Cavern", | ||
|  |     "Kalle Demos Boss Arena":   "Forbidden Woods", | ||
|  |     "Gohdan Boss Arena":        "Tower of the Gods", | ||
|  |     "Helmaroc King Boss Arena": "Forsaken Fortress", | ||
|  |     "Jalhalla Boss Arena":      "Earth Temple", | ||
|  |     "Molgera Boss Arena":       "Wind Temple", | ||
|  | } | ||
|  | 
 | ||
|  | VANILLA_ENTRANCES_TO_EXITS: dict[str, str] = { | ||
|  |     "Dungeon Entrance on Dragon Roost Island": "Dragon Roost Cavern", | ||
|  |     "Dungeon Entrance in Forest Haven Sector": "Forbidden Woods", | ||
|  |     "Dungeon Entrance in Tower of the Gods Sector": "Tower of the Gods", | ||
|  |     "Dungeon Entrance on Headstone Island": "Earth Temple", | ||
|  |     "Dungeon Entrance on Gale Isle": "Wind Temple", | ||
|  | 
 | ||
|  |     "Miniboss Entrance in Forbidden Woods": "Forbidden Woods Miniboss Arena", | ||
|  |     "Miniboss Entrance in Tower of the Gods": "Tower of the Gods Miniboss Arena", | ||
|  |     "Miniboss Entrance in Earth Temple": "Earth Temple Miniboss Arena", | ||
|  |     "Miniboss Entrance in Wind Temple": "Wind Temple Miniboss Arena", | ||
|  |     "Miniboss Entrance in Hyrule Castle": "Master Sword Chamber", | ||
|  | 
 | ||
|  |     "Boss Entrance in Dragon Roost Cavern": "Gohma Boss Arena", | ||
|  |     "Boss Entrance in Forbidden Woods": "Kalle Demos Boss Arena", | ||
|  |     "Boss Entrance in Tower of the Gods": "Gohdan Boss Arena", | ||
|  |     "Boss Entrance in Forsaken Fortress": "Helmaroc King Boss Arena", | ||
|  |     "Boss Entrance in Earth Temple": "Jalhalla Boss Arena", | ||
|  |     "Boss Entrance in Wind Temple": "Molgera Boss Arena", | ||
|  | 
 | ||
|  |     "Secret Cave Entrance on Outset Island": "Savage Labyrinth", | ||
|  |     "Secret Cave Entrance on Dragon Roost Island": "Dragon Roost Island Secret Cave", | ||
|  |     "Secret Cave Entrance on Fire Mountain": "Fire Mountain Secret Cave", | ||
|  |     "Secret Cave Entrance on Ice Ring Isle": "Ice Ring Isle Secret Cave", | ||
|  |     "Secret Cave Entrance on Private Oasis": "Cabana Labyrinth", | ||
|  |     "Secret Cave Entrance on Needle Rock Isle": "Needle Rock Isle Secret Cave", | ||
|  |     "Secret Cave Entrance on Angular Isles": "Angular Isles Secret Cave", | ||
|  |     "Secret Cave Entrance on Boating Course": "Boating Course Secret Cave", | ||
|  |     "Secret Cave Entrance on Stone Watcher Island": "Stone Watcher Island Secret Cave", | ||
|  |     "Secret Cave Entrance on Overlook Island": "Overlook Island Secret Cave", | ||
|  |     "Secret Cave Entrance on Bird's Peak Rock": "Bird's Peak Rock Secret Cave", | ||
|  |     "Secret Cave Entrance on Pawprint Isle": "Pawprint Isle Chuchu Cave", | ||
|  |     "Secret Cave Entrance on Pawprint Isle Side Isle": "Pawprint Isle Wizzrobe Cave", | ||
|  |     "Secret Cave Entrance on Diamond Steppe Island": "Diamond Steppe Island Warp Maze Cave", | ||
|  |     "Secret Cave Entrance on Bomb Island": "Bomb Island Secret Cave", | ||
|  |     "Secret Cave Entrance on Rock Spire Isle": "Rock Spire Isle Secret Cave", | ||
|  |     "Secret Cave Entrance on Shark Island": "Shark Island Secret Cave", | ||
|  |     "Secret Cave Entrance on Cliff Plateau Isles": "Cliff Plateau Isles Secret Cave", | ||
|  |     "Secret Cave Entrance on Horseshoe Island": "Horseshoe Island Secret Cave", | ||
|  |     "Secret Cave Entrance on Star Island": "Star Island Secret Cave", | ||
|  | 
 | ||
|  |     "Inner Entrance in Ice Ring Isle Secret Cave": "Ice Ring Isle Inner Cave", | ||
|  |     "Inner Entrance in Cliff Plateau Isles Secret Cave": "Cliff Plateau Isles Inner Cave", | ||
|  | 
 | ||
|  |     "Fairy Fountain Entrance on Outset Island": "Outset Fairy Fountain", | ||
|  |     "Fairy Fountain Entrance on Thorned Fairy Island": "Thorned Fairy Fountain", | ||
|  |     "Fairy Fountain Entrance on Eastern Fairy Island": "Eastern Fairy Fountain", | ||
|  |     "Fairy Fountain Entrance on Western Fairy Island": "Western Fairy Fountain", | ||
|  |     "Fairy Fountain Entrance on Southern Fairy Island": "Southern Fairy Fountain", | ||
|  |     "Fairy Fountain Entrance on Northern Fairy Island": "Northern Fairy Fountain", | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | class EntranceRandomizer: | ||
|  |     """
 | ||
|  |     This class handles the logic for The Wind Waker entrance randomizer. | ||
|  | 
 | ||
|  |     We reference the logic from the base randomizer with some modifications to suit it for Archipelago. | ||
|  |     Reference: https://github.com/LagoLunatic/wwrando/blob/master/randomizers/entrances.py | ||
|  | 
 | ||
|  |     :param world: The Wind Waker game world. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     def __init__(self, world: "TWWWorld"): | ||
|  |         self.world = world | ||
|  |         self.multiworld = world.multiworld | ||
|  |         self.player = world.player | ||
|  | 
 | ||
|  |         self.item_location_to_containing_zone_exit: dict[str, ZoneExit] = {} | ||
|  |         self.zone_exit_to_logically_dependent_item_locations: dict[ZoneExit, list[str]] = defaultdict(list) | ||
|  |         self.register_mappings_between_item_locations_and_zone_exits() | ||
|  | 
 | ||
|  |         self.done_entrances_to_exits: dict[ZoneEntrance, ZoneExit] = {} | ||
|  |         self.done_exits_to_entrances: dict[ZoneExit, ZoneEntrance] = {} | ||
|  | 
 | ||
|  |         for entrance_name, exit_name in VANILLA_ENTRANCES_TO_EXITS.items(): | ||
|  |             zone_entrance = ZoneEntrance.all[entrance_name] | ||
|  |             zone_exit = ZoneExit.all[exit_name] | ||
|  |             self.done_entrances_to_exits[zone_entrance] = zone_exit | ||
|  |             self.done_exits_to_entrances[zone_exit] = zone_entrance | ||
|  | 
 | ||
|  |         self.banned_exits: list[ZoneExit] = [] | ||
|  |         self.islands_with_a_banned_dungeon: set[str] = set() | ||
|  | 
 | ||
|  |     def randomize_entrances(self) -> None: | ||
|  |         """
 | ||
|  |         Randomize entrances for The Wind Waker. | ||
|  |         """
 | ||
|  |         self.init_banned_exits() | ||
|  | 
 | ||
|  |         for relevant_entrances, relevant_exits in self.get_all_entrance_sets_to_be_randomized(): | ||
|  |             self.randomize_one_set_of_entrances(relevant_entrances, relevant_exits) | ||
|  | 
 | ||
|  |         self.finalize_all_randomized_sets_of_entrances() | ||
|  | 
 | ||
|  |     def init_banned_exits(self) -> None: | ||
|  |         """
 | ||
|  |         Initialize the list of banned exits for the randomizer. | ||
|  | 
 | ||
|  |         Dungeon exits in banned dungeons should be prohibited from being randomized. | ||
|  |         Additionally, if dungeon entrances are not randomized, we can now note which island holds these banned dungeons. | ||
|  |         """
 | ||
|  |         options = self.world.options | ||
|  | 
 | ||
|  |         if options.required_bosses: | ||
|  |             for zone_exit in BOSS_EXITS: | ||
|  |                 assert zone_exit.unique_name.endswith(" Boss Arena") | ||
|  |                 boss_name = zone_exit.unique_name.removesuffix(" Boss Arena") | ||
|  |                 if boss_name in self.world.boss_reqs.banned_bosses: | ||
|  |                     self.banned_exits.append(zone_exit) | ||
|  |             for zone_exit in DUNGEON_EXITS: | ||
|  |                 dungeon_name = zone_exit.unique_name | ||
|  |                 if dungeon_name in self.world.boss_reqs.banned_dungeons: | ||
|  |                     self.banned_exits.append(zone_exit) | ||
|  |             for zone_exit in MINIBOSS_EXITS: | ||
|  |                 if zone_exit == ZoneExit.all["Master Sword Chamber"]: | ||
|  |                     # Hyrule cannot be chosen as a banned dungeon. | ||
|  |                     continue | ||
|  |                 assert zone_exit.unique_name.endswith(" Miniboss Arena") | ||
|  |                 dungeon_name = zone_exit.unique_name.removesuffix(" Miniboss Arena") | ||
|  |                 if dungeon_name in self.world.boss_reqs.banned_dungeons: | ||
|  |                     self.banned_exits.append(zone_exit) | ||
|  | 
 | ||
|  |         if not options.randomize_dungeon_entrances: | ||
|  |             # If dungeon entrances are not randomized, `islands_with_a_banned_dungeon` can be initialized early since | ||
|  |             # it's preset and won't be updated later since we won't randomize the dungeon entrances. | ||
|  |             for en in DUNGEON_ENTRANCES: | ||
|  |                 if self.done_entrances_to_exits[en].unique_name in self.world.boss_reqs.banned_dungeons: | ||
|  |                     assert en.island_name is not None | ||
|  |                     self.islands_with_a_banned_dungeon.add(en.island_name) | ||
|  | 
 | ||
|  |     def randomize_one_set_of_entrances( | ||
|  |         self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit] | ||
|  |     ) -> None: | ||
|  |         """
 | ||
|  |         Randomize a single set of entrances and their corresponding exits. | ||
|  | 
 | ||
|  |         :param relevant_entrances: A list of entrances to be randomized. | ||
|  |         :param relevant_exits: A list of exits corresponding to the entrances. | ||
|  |         """
 | ||
|  |         # Keep miniboss and boss entrances vanilla in non-required bosses' dungeons. | ||
|  |         for zone_entrance in relevant_entrances.copy(): | ||
|  |             zone_exit = self.done_entrances_to_exits[zone_entrance] | ||
|  |             if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS: | ||
|  |                 relevant_entrances.remove(zone_entrance) | ||
|  |             else: | ||
|  |                 del self.done_entrances_to_exits[zone_entrance] | ||
|  |         for zone_exit in relevant_exits.copy(): | ||
|  |             if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS: | ||
|  |                 relevant_exits.remove(zone_exit) | ||
|  |             else: | ||
|  |                 del self.done_exits_to_entrances[zone_exit] | ||
|  | 
 | ||
|  |         self.multiworld.random.shuffle(relevant_entrances) | ||
|  | 
 | ||
|  |         # We calculate which exits are terminal (the end of a nested chain) per set instead of for all entrances. | ||
|  |         # This is so that, for example, Ice Ring Isle counts as terminal when its inner cave is not being randomized. | ||
|  |         non_terminal_exits = [] | ||
|  |         for en in relevant_entrances: | ||
|  |             if en.nested_in is not None and en.nested_in not in non_terminal_exits: | ||
|  |                 non_terminal_exits.append(en.nested_in) | ||
|  |         terminal_exits = {ex for ex in relevant_exits if ex not in non_terminal_exits} | ||
|  | 
 | ||
|  |         remaining_entrances = relevant_entrances.copy() | ||
|  |         remaining_exits = relevant_exits.copy() | ||
|  | 
 | ||
|  |         nonprogress_entrances, nonprogress_exits = self.split_nonprogress_entrances_and_exits( | ||
|  |             remaining_entrances, remaining_exits | ||
|  |         ) | ||
|  |         if nonprogress_entrances: | ||
|  |             for en in nonprogress_entrances: | ||
|  |                 remaining_entrances.remove(en) | ||
|  |             for ex in nonprogress_exits: | ||
|  |                 remaining_exits.remove(ex) | ||
|  |             self.randomize_one_set_of_exits(nonprogress_entrances, nonprogress_exits, terminal_exits) | ||
|  | 
 | ||
|  |         self.randomize_one_set_of_exits(remaining_entrances, remaining_exits, terminal_exits) | ||
|  | 
 | ||
|  |     def check_if_one_exit_is_progress(self, zone_exit: ZoneExit) -> bool: | ||
|  |         """
 | ||
|  |         Determine if the zone exit leads to progress locations in the world. | ||
|  | 
 | ||
|  |         :param zone_exit: The zone exit to check. | ||
|  |         :return: Whether the zone exit leads to progress locations. | ||
|  |         """
 | ||
|  |         locs_for_exit = self.zone_exit_to_logically_dependent_item_locations[zone_exit] | ||
|  |         assert locs_for_exit, f"Could not find any item locations corresponding to zone exit: {zone_exit.unique_name}" | ||
|  | 
 | ||
|  |         # Banned required bosses mode dungeons still technically count as progress locations, so filter them out | ||
|  |         # separately first. | ||
|  |         nonbanned_locs = [loc for loc in locs_for_exit if loc not in self.world.boss_reqs.banned_locations] | ||
|  |         progress_locs = [loc for loc in nonbanned_locs if loc not in self.world.nonprogress_locations] | ||
|  |         return bool(progress_locs) | ||
|  | 
 | ||
|  |     def split_nonprogress_entrances_and_exits( | ||
|  |         self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit] | ||
|  |     ) -> tuple[list[ZoneEntrance], list[ZoneExit]]: | ||
|  |         """
 | ||
|  |         Splits the entrance and exit lists into two pairs: ones that should be considered nonprogress on this seed (will | ||
|  |         never lead to any progress items) and ones that should be regarded as potentially required. | ||
|  | 
 | ||
|  |         This is so we can effectively randomize these two pairs separately without convoluted logic to ensure they don't | ||
|  |         connect. | ||
|  | 
 | ||
|  |         :param relevant_entrances: A list of entrances. | ||
|  |         :param relevant_exits: A list of exits corresponding to the entrances. | ||
|  |         :raises FillError: If the number of randomizable entrances does not equal the number of randomizable exits. | ||
|  |         """
 | ||
|  |         nonprogress_exits = [ex for ex in relevant_exits if not self.check_if_one_exit_is_progress(ex)] | ||
|  |         nonprogress_entrances = [ | ||
|  |             en | ||
|  |             for en in relevant_entrances | ||
|  |             if en.nested_in is not None | ||
|  |             and ( | ||
|  |                 (en.nested_in in nonprogress_exits) | ||
|  |                 # The area this entrance is nested in is not randomized, but we still need to determine whether it's | ||
|  |                 # progression. | ||
|  |                 or (en.nested_in not in relevant_exits and not self.check_if_one_exit_is_progress(en.nested_in)) | ||
|  |             ) | ||
|  |         ] | ||
|  | 
 | ||
|  |         # At this point, `nonprogress_entrances` includes only the inner entrances nested inside the main exits, not any | ||
|  |         # island entrances on the sea. So, we need to select `N` random island entrances to allow all of the nonprogress | ||
|  |         # exits to be accessible, where `N` is the difference between the number of entrances and exits we currently | ||
|  |         # have. | ||
|  |         possible_island_entrances = [en for en in relevant_entrances if en.island_name is not None] | ||
|  | 
 | ||
|  |         # We need special logic to handle Forsaken Fortress, as it is the only island entrance inside a dungeon. | ||
|  |         ff_boss_entrance = ZoneEntrance.all["Boss Entrance in Forsaken Fortress"] | ||
|  |         if ff_boss_entrance in possible_island_entrances: | ||
|  |             if self.world.options.progression_dungeons: | ||
|  |                 if "Forsaken Fortress" in self.world.boss_reqs.banned_dungeons: | ||
|  |                     ff_progress = False | ||
|  |                 else: | ||
|  |                     ff_progress = True | ||
|  |             else: | ||
|  |                 ff_progress = False | ||
|  | 
 | ||
|  |             if ff_progress: | ||
|  |                 # If it's progress, don't allow it to be randomly chosen to lead to nonprogress exits. | ||
|  |                 possible_island_entrances.remove(ff_boss_entrance) | ||
|  |             else: | ||
|  |                 # If it's not progress, manually mark it as such, and don't allow it to be chosen randomly. | ||
|  |                 nonprogress_entrances.append(ff_boss_entrance) | ||
|  |                 possible_island_entrances.remove(ff_boss_entrance) | ||
|  | 
 | ||
|  |         num_island_entrances_needed = len(nonprogress_exits) - len(nonprogress_entrances) | ||
|  |         if num_island_entrances_needed > len(possible_island_entrances): | ||
|  |             raise FillError("Not enough island entrances left to split entrances.") | ||
|  | 
 | ||
|  |         for _ in range(num_island_entrances_needed): | ||
|  |             # Note: `relevant_entrances` is already shuffled, so we can just take the first result from | ||
|  |             # `possible_island_entrances`—it's the same as picking one randomly. | ||
|  |             nonprogress_island_entrance = possible_island_entrances.pop(0) | ||
|  |             nonprogress_entrances.append(nonprogress_island_entrance) | ||
|  | 
 | ||
|  |         assert len(nonprogress_entrances) == len(nonprogress_exits) | ||
|  | 
 | ||
|  |         return nonprogress_entrances, nonprogress_exits | ||
|  | 
 | ||
|  |     def randomize_one_set_of_exits( | ||
|  |         self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit], terminal_exits: set[ZoneExit] | ||
|  |     ) -> None: | ||
|  |         """
 | ||
|  |         Randomize a single set of entrances and their corresponding exits. | ||
|  | 
 | ||
|  |         :param relevant_entrances: A list of entrances to be randomized. | ||
|  |         :param relevant_exits: A list of exits corresponding to the entrances. | ||
|  |         :param terminal_exits: A set of exits which do not contain any entrances. | ||
|  |         :raises FillError: If there are no valid exits to assign to an entrance. | ||
|  |         """
 | ||
|  |         options = self.world.options | ||
|  | 
 | ||
|  |         remaining_entrances = relevant_entrances.copy() | ||
|  |         remaining_exits = relevant_exits.copy() | ||
|  | 
 | ||
|  |         doing_banned = False | ||
|  |         if any(ex in self.banned_exits for ex in relevant_exits): | ||
|  |             doing_banned = True | ||
|  | 
 | ||
|  |         if options.required_bosses and not doing_banned: | ||
|  |             # Prioritize entrances that share an island with an entrance randomized to lead into a | ||
|  |             # required-bosses-mode-banned dungeon. (e.g., DRI, Pawprint, Outset, TotG sector.) | ||
|  |             # This is because we need to prevent these islands from having a required boss or anything that could lead | ||
|  |             # to a required boss. If we don't do this first, we can get backed into a corner where there is no other | ||
|  |             # option left. | ||
|  |             entrances_not_on_unique_islands = [] | ||
|  |             for zone_entrance in relevant_entrances: | ||
|  |                 if zone_entrance.is_nested: | ||
|  |                     continue | ||
|  |                 if zone_entrance.island_name in self.islands_with_a_banned_dungeon: | ||
|  |                     # This island was already used on a previous call to `randomize_one_set_of_exits`. | ||
|  |                     entrances_not_on_unique_islands.append(zone_entrance) | ||
|  |                     continue | ||
|  |             for zone_entrance in entrances_not_on_unique_islands: | ||
|  |                 remaining_entrances.remove(zone_entrance) | ||
|  |             remaining_entrances = entrances_not_on_unique_islands + remaining_entrances | ||
|  | 
 | ||
|  |         while remaining_entrances: | ||
|  |             # Filter out boss entrances that aren't yet accessible from the sea. | ||
|  |             # We don't want to connect these to anything yet or we risk creating an infinite loop. | ||
|  |             possible_remaining_entrances = [ | ||
|  |                 en for en in remaining_entrances if self.get_outermost_entrance_for_entrance(en) is not None | ||
|  |             ] | ||
|  |             zone_entrance = possible_remaining_entrances.pop(0) | ||
|  |             remaining_entrances.remove(zone_entrance) | ||
|  | 
 | ||
|  |             possible_remaining_exits = remaining_exits.copy() | ||
|  | 
 | ||
|  |             if len(possible_remaining_entrances) == 0 and len(remaining_entrances) > 0: | ||
|  |                 # If this is the last entrance we have left to attach exits to, we can't place a terminal exit here. | ||
|  |                 # Terminal exits do not create another entrance, so one would leave us with no possible way to continue | ||
|  |                 # placing the remaining exits on future loops. | ||
|  |                 possible_remaining_exits = [ex for ex in possible_remaining_exits if ex not in terminal_exits] | ||
|  | 
 | ||
|  |             if options.required_bosses and zone_entrance.island_name is not None and not doing_banned: | ||
|  |                 # Prevent required bosses (and non-terminal exits, which could lead to required bosses) from appearing | ||
|  |                 # on islands where we already placed a banned boss or dungeon. | ||
|  |                 # This can happen with DRI and Pawprint, as these islands have two entrances. This would be bad because | ||
|  |                 # the required bosses mode's dungeon markers only tell you what island the required dungeons are on, not | ||
|  |                 # which of the two entrances to enter. | ||
|  |                 # So, if a banned dungeon is placed on DRI's main entrance, we will have to fill DRI's pit entrance with | ||
|  |                 # either a miniboss or one of the caves that does not have a nested entrance inside. We allow multiple | ||
|  |                 # banned and required dungeons on a single island. | ||
|  |                 if zone_entrance.island_name in self.islands_with_a_banned_dungeon: | ||
|  |                     possible_remaining_exits = [ | ||
|  |                         ex | ||
|  |                         for ex in possible_remaining_exits | ||
|  |                         if ex in terminal_exits and ex not in (DUNGEON_EXITS + BOSS_EXITS) | ||
|  |                     ] | ||
|  | 
 | ||
|  |             if not possible_remaining_exits: | ||
|  |                 raise FillError(f"No valid exits to place for entrance: {zone_entrance.entrance_name}") | ||
|  | 
 | ||
|  |             zone_exit = self.multiworld.random.choice(possible_remaining_exits) | ||
|  |             remaining_exits.remove(zone_exit) | ||
|  | 
 | ||
|  |             self.done_entrances_to_exits[zone_entrance] = zone_exit | ||
|  |             self.done_exits_to_entrances[zone_exit] = zone_entrance | ||
|  | 
 | ||
|  |             if zone_exit in self.banned_exits: | ||
|  |                 # Keep track of which islands have a required bosses mode banned dungeon to avoid marker overlap. | ||
|  |                 if zone_exit in DUNGEON_EXITS + BOSS_EXITS: | ||
|  |                     # We only keep track of dungeon exits and boss exits, not miniboss exits. | ||
|  |                     # Banned miniboss exits can share an island with required dungeons/bosses. | ||
|  |                     outer_entrance = self.get_outermost_entrance_for_entrance(zone_entrance) | ||
|  | 
 | ||
|  |                     # Because we filter above so that we always assign entrances from the sea inwards, we can assume | ||
|  |                     # that when we assign an entrance, it has a path back to the sea. | ||
|  |                     # If we're assigning a non-terminal entrance, any nested entrances will get assigned after this one, | ||
|  |                     # and we'll run through this code again (so we can reason based on `zone_exit` only instead of | ||
|  |                     # having to recurse through the nested exits to find banned dungeons/bosses). | ||
|  |                     assert outer_entrance and outer_entrance.island_name is not None | ||
|  |                     self.islands_with_a_banned_dungeon.add(outer_entrance.island_name) | ||
|  | 
 | ||
|  |     def finalize_all_randomized_sets_of_entrances(self) -> None: | ||
|  |         """
 | ||
|  |         Finalize all randomized entrance sets. | ||
|  | 
 | ||
|  |         For all entrance-exit pairs, this function adds a connection with the appropriate access rule to the world. | ||
|  |         """
 | ||
|  | 
 | ||
|  |         def get_access_rule(entrance: ZoneEntrance) -> str: | ||
|  |             snake_case_region = entrance.entrance_name.lower().replace("'", "").replace(" ", "_") | ||
|  |             return getattr(Macros, f"can_access_{snake_case_region}") | ||
|  | 
 | ||
|  |         # Connect each entrance-exit pair in the multiworld with the access rule for the entrance. | ||
|  |         # The Great Sea is the parent_region for many entrances, so get it in advance. | ||
|  |         great_sea_region = self.world.get_region("The Great Sea") | ||
|  |         for zone_entrance, zone_exit in self.done_entrances_to_exits.items(): | ||
|  |             # Get the parent region of the entrance. | ||
|  |             if zone_entrance.island_name is not None: | ||
|  |                 # Entrances with an `island_name` are found in The Great Sea. | ||
|  |                 parent_region = great_sea_region | ||
|  |             else: | ||
|  |                 # All other entrances must be nested within some other region. | ||
|  |                 parent_region = self.world.get_region(zone_entrance.nested_in.unique_name) | ||
|  |             exit_region_name = zone_exit.unique_name | ||
|  |             exit_region = self.world.get_region(exit_region_name) | ||
|  |             parent_region.connect( | ||
|  |                 exit_region, | ||
|  |                 # The default name uses the "parent_region -> connecting_region", but the parent_region would not be | ||
|  |                 # useful for spoiler paths or debugging, so use the entrance name at the start. | ||
|  |                 f"{zone_entrance.entrance_name} -> {exit_region_name}", | ||
|  |                 rule=lambda state, rule=get_access_rule(zone_entrance): rule(state, self.player), | ||
|  |             ) | ||
|  | 
 | ||
|  |         if __debug__ and self.world.options.required_bosses: | ||
|  |             # Ensure we didn't accidentally place a banned boss and a required boss on the same island. | ||
|  |             banned_island_names = set( | ||
|  |                 self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.banned_bosses | ||
|  |             ) | ||
|  |             required_island_names = set( | ||
|  |                 self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.required_bosses | ||
|  |             ) | ||
|  |             assert not banned_island_names & required_island_names | ||
|  | 
 | ||
|  |     def register_mappings_between_item_locations_and_zone_exits(self) -> None: | ||
|  |         """
 | ||
|  |         Map item locations to their corresponding zone exits. | ||
|  |         """
 | ||
|  |         for loc_name in list(LOCATION_TABLE.keys()): | ||
|  |             zone_exit = self.get_zone_exit_for_item_location(loc_name) | ||
|  |             if zone_exit is not None: | ||
|  |                 self.item_location_to_containing_zone_exit[loc_name] = zone_exit | ||
|  |                 self.zone_exit_to_logically_dependent_item_locations[zone_exit].append(loc_name) | ||
|  | 
 | ||
|  |             if loc_name == "The Great Sea - Withered Trees": | ||
|  |                 # This location isn't inside a zone exit, but it does logically require the player to be able to reach | ||
|  |                 # a different item location inside one. | ||
|  |                 sub_zone_exit = self.get_zone_exit_for_item_location("Cliff Plateau Isles - Highest Isle") | ||
|  |                 if sub_zone_exit is not None: | ||
|  |                     self.zone_exit_to_logically_dependent_item_locations[sub_zone_exit].append(loc_name) | ||
|  | 
 | ||
|  |     def get_all_entrance_sets_to_be_randomized( | ||
|  |         self, | ||
|  |     ) -> Generator[tuple[list[ZoneEntrance], list[ZoneExit]], None, None]: | ||
|  |         """
 | ||
|  |         Retrieve all entrance-exit pairs that need to be randomized. | ||
|  | 
 | ||
|  |         :raises OptionError: If an invalid randomization option is set in the world's options. | ||
|  |         :return: A generator that yields sets of entrances and exits to be randomized. | ||
|  |         """
 | ||
|  |         options = self.world.options | ||
|  | 
 | ||
|  |         dungeons = bool(options.randomize_dungeon_entrances) | ||
|  |         minibosses = bool(options.randomize_miniboss_entrances) | ||
|  |         bosses = bool(options.randomize_boss_entrances) | ||
|  |         secret_caves = bool(options.randomize_secret_cave_entrances) | ||
|  |         inner_caves = bool(options.randomize_secret_cave_inner_entrances) | ||
|  |         fountains = bool(options.randomize_fairy_fountain_entrances) | ||
|  | 
 | ||
|  |         mix_entrances = options.mix_entrances | ||
|  |         if mix_entrances == "separate_pools": | ||
|  |             if dungeons: | ||
|  |                 yield self.get_one_entrance_set(dungeons=dungeons) | ||
|  |             if minibosses: | ||
|  |                 yield self.get_one_entrance_set(minibosses=minibosses) | ||
|  |             if bosses: | ||
|  |                 yield self.get_one_entrance_set(bosses=bosses) | ||
|  |             if secret_caves: | ||
|  |                 yield self.get_one_entrance_set(caves=secret_caves) | ||
|  |             if inner_caves: | ||
|  |                 yield self.get_one_entrance_set(inner_caves=inner_caves) | ||
|  |             if fountains: | ||
|  |                 yield self.get_one_entrance_set(fountains=fountains) | ||
|  |         elif mix_entrances == "mix_pools": | ||
|  |             yield self.get_one_entrance_set( | ||
|  |                 dungeons=dungeons, | ||
|  |                 minibosses=minibosses, | ||
|  |                 bosses=bosses, | ||
|  |                 caves=secret_caves, | ||
|  |                 inner_caves=inner_caves, | ||
|  |                 fountains=fountains, | ||
|  |             ) | ||
|  |         else: | ||
|  |             raise OptionError(f"Invalid entrance randomization option: {mix_entrances}") | ||
|  | 
 | ||
|  |     def get_one_entrance_set( | ||
|  |         self, | ||
|  |         *, | ||
|  |         dungeons: bool = False, | ||
|  |         caves: bool = False, | ||
|  |         minibosses: bool = False, | ||
|  |         bosses: bool = False, | ||
|  |         inner_caves: bool = False, | ||
|  |         fountains: bool = False, | ||
|  |     ) -> tuple[list[ZoneEntrance], list[ZoneExit]]: | ||
|  |         """
 | ||
|  |         Retrieve a single set of entrance-exit pairs that need to be randomized. | ||
|  | 
 | ||
|  |         :param dungeons: Whether to include dungeon entrances and exits. Defaults to `False`. | ||
|  |         :param caves: Whether to include secret cave entrances and exits. Defaults to `False`. | ||
|  |         :param minibosses: Whether to include miniboss entrances and exits. Defaults to `False`. | ||
|  |         :param bosses: Whether to include boss entrances and exits. Defaults to `False`. | ||
|  |         :param inner_caves: Whether to include inner cave entrances and exits. Defaults to `False`. | ||
|  |         :param fountains: Whether to include fairy fountain entrances and exits. Defaults to `False`. | ||
|  |         :return: A tuple of lists of entrances and exits that should be randomized together. | ||
|  |         """
 | ||
|  |         relevant_entrances: list[ZoneEntrance] = [] | ||
|  |         relevant_exits: list[ZoneExit] = [] | ||
|  |         if dungeons: | ||
|  |             relevant_entrances += DUNGEON_ENTRANCES | ||
|  |             relevant_exits += DUNGEON_EXITS | ||
|  |         if minibosses: | ||
|  |             relevant_entrances += MINIBOSS_ENTRANCES | ||
|  |             relevant_exits += MINIBOSS_EXITS | ||
|  |         if bosses: | ||
|  |             relevant_entrances += BOSS_ENTRANCES | ||
|  |             relevant_exits += BOSS_EXITS | ||
|  |         if caves: | ||
|  |             relevant_entrances += SECRET_CAVE_ENTRANCES | ||
|  |             relevant_exits += SECRET_CAVE_EXITS | ||
|  |         if inner_caves: | ||
|  |             relevant_entrances += SECRET_CAVE_INNER_ENTRANCES | ||
|  |             relevant_exits += SECRET_CAVE_INNER_EXITS | ||
|  |         if fountains: | ||
|  |             relevant_entrances += FAIRY_FOUNTAIN_ENTRANCES | ||
|  |             relevant_exits += FAIRY_FOUNTAIN_EXITS | ||
|  |         return relevant_entrances, relevant_exits | ||
|  | 
 | ||
|  |     def get_outermost_entrance_for_exit(self, zone_exit: ZoneExit) -> Optional[ZoneEntrance]: | ||
|  |         """
 | ||
|  |         Unrecurses nested dungeons to determine a given exit's outermost (island) entrance. | ||
|  | 
 | ||
|  |         :param zone_exit: The given exit. | ||
|  |         :return: The outermost (island) entrance for the exit, or `None` if entrances have yet to be randomized. | ||
|  |         """
 | ||
|  |         zone_entrance = self.done_exits_to_entrances[zone_exit] | ||
|  |         return self.get_outermost_entrance_for_entrance(zone_entrance) | ||
|  | 
 | ||
|  |     def get_outermost_entrance_for_entrance(self, zone_entrance: ZoneEntrance) -> Optional[ZoneEntrance]: | ||
|  |         """
 | ||
|  |         Unrecurses nested dungeons to determine a given entrance's outermost (island) entrance. | ||
|  | 
 | ||
|  |         :param zone_exit: The given entrance. | ||
|  |         :return: The outermost (island) entrance for the entrance, or `None` if entrances have yet to be randomized. | ||
|  |         """
 | ||
|  |         seen_entrances = self.get_all_entrances_on_path_to_entrance(zone_entrance) | ||
|  |         if seen_entrances is None: | ||
|  |             # Undecided. | ||
|  |             return None | ||
|  |         outermost_entrance = seen_entrances[-1] | ||
|  |         return outermost_entrance | ||
|  | 
 | ||
|  |     def get_all_entrances_on_path_to_entrance(self, zone_entrance: ZoneEntrance) -> Optional[list[ZoneEntrance]]: | ||
|  |         """
 | ||
|  |         Unrecurses nested dungeons to build a list of all entrances leading to a given entrance. | ||
|  | 
 | ||
|  |         :param zone_exit: The given entrance. | ||
|  |         :return: A list of entrances leading to the given entrance, or `None` if entrances have yet to be randomized. | ||
|  |         """
 | ||
|  |         seen_entrances: list[ZoneEntrance] = [] | ||
|  |         while zone_entrance.is_nested: | ||
|  |             if zone_entrance in seen_entrances: | ||
|  |                 path_str = ", ".join([e.entrance_name for e in seen_entrances]) | ||
|  |                 raise FillError(f"Entrances are in an infinite loop: {path_str}") | ||
|  |             seen_entrances.append(zone_entrance) | ||
|  |             if zone_entrance.nested_in not in self.done_exits_to_entrances: | ||
|  |                 # Undecided. | ||
|  |                 return None | ||
|  |             zone_entrance = self.done_exits_to_entrances[zone_entrance.nested_in] | ||
|  |         seen_entrances.append(zone_entrance) | ||
|  |         return seen_entrances | ||
|  | 
 | ||
|  |     def is_item_location_behind_randomizable_entrance(self, location_name: str) -> bool: | ||
|  |         """
 | ||
|  |         Determine if the location is behind a randomizable entrance. | ||
|  | 
 | ||
|  |         :param location_name: The location to check. | ||
|  |         :return: `True` if the location is behind a randomizable entrance, `False` otherwise. | ||
|  |         """
 | ||
|  |         loc_zone_name, _ = split_location_name_by_zone(location_name) | ||
|  |         if loc_zone_name in ["Ganon's Tower", "Mailbox"]: | ||
|  |             # Ganon's Tower and the handful of Mailbox locations that depend on beating dungeon bosses are considered | ||
|  |             # "Dungeon" location types by the logic, but the entrance randomizer does not need to consider them. | ||
|  |             # Although the mail locations are technically locked behind dungeons, we can still ignore them here because | ||
|  |             # if all of the locations in the dungeon itself are nonprogress, then any mail depending on that dungeon | ||
|  |             # should also be enforced as nonprogress by other parts of the code. | ||
|  |             return False | ||
|  | 
 | ||
|  |         types = LOCATION_TABLE[location_name].flags | ||
|  |         is_boss = TWWFlag.BOSS in types | ||
|  |         if loc_zone_name == "Forsaken Fortress" and not is_boss: | ||
|  |             # Special case. FF is a dungeon that is not randomized, except for the boss arena. | ||
|  |             return False | ||
|  | 
 | ||
|  |         is_big_octo = TWWFlag.BG_OCTO in types | ||
|  |         if is_big_octo: | ||
|  |             # The Big Octo Great Fairy is the only Great Fairy location that is not also a Fairy Fountain. | ||
|  |             return False | ||
|  | 
 | ||
|  |         # In the general case, we check if the location has a type corresponding to exits that can be randomized. | ||
|  |         if any(t in types for t in ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES): | ||
|  |             return True | ||
|  | 
 | ||
|  |         return False | ||
|  | 
 | ||
|  |     def get_zone_exit_for_item_location(self, location_name: str) -> Optional[ZoneExit]: | ||
|  |         """
 | ||
|  |         Retrieve the zone exit for a given location. | ||
|  | 
 | ||
|  |         :param location_name: The name of the location. | ||
|  |         :raises Exception: If a location exit override should be used instead. | ||
|  |         :return: The zone exit for the location or `None` if the location is not behind a randomizable entrance. | ||
|  |         """
 | ||
|  |         if not self.is_item_location_behind_randomizable_entrance(location_name): | ||
|  |             return None | ||
|  | 
 | ||
|  |         zone_exit = ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES.get(location_name, None) | ||
|  |         if zone_exit is not None: | ||
|  |             return zone_exit | ||
|  | 
 | ||
|  |         loc_zone_name, _ = split_location_name_by_zone(location_name) | ||
|  |         possible_exits = [ex for ex in ZoneExit.all.values() if ex.zone_name == loc_zone_name] | ||
|  |         if len(possible_exits) == 0: | ||
|  |             return None | ||
|  |         elif len(possible_exits) == 1: | ||
|  |             return possible_exits[0] | ||
|  |         else: | ||
|  |             raise Exception( | ||
|  |                 f"Multiple zone exits share the same zone name: {loc_zone_name!r}. " | ||
|  |                 "Use a location exit override instead." | ||
|  |             ) | ||
|  | 
 | ||
|  |     def get_entrance_zone_for_boss(self, boss_name: str) -> str: | ||
|  |         """
 | ||
|  |         Retrieve the entrance zone for a given boss. | ||
|  | 
 | ||
|  |         :param boss_name: The name of the boss. | ||
|  |         :return: The name of the island on which the boss is located. | ||
|  |         """
 | ||
|  |         boss_arena_name = f"{boss_name} Boss Arena" | ||
|  |         zone_exit = ZoneExit.all[boss_arena_name] | ||
|  |         outermost_entrance = self.get_outermost_entrance_for_exit(zone_exit) | ||
|  |         assert outermost_entrance is not None and outermost_entrance.island_name is not None | ||
|  |         return outermost_entrance.island_name |