Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
		
			
				
	
	
		
			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
 |