Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d4423da
Dungeon Boss Entrance Rando
mattman107 Oct 25, 2025
bc07931
Dungeon and Boss entrances randomized
mattman107 Oct 29, 2025
9c4ed4f
Remove unused option
mattman107 Nov 2, 2025
844dd07
My proposed solution to the Sheik at Colossus problem
mattman107 Nov 3, 2025
43fe2fe
fix spelling
mattman107 Nov 3, 2025
7d4509d
fix spelling. Confirmed decoupled doesn't currently work for dungeon …
mattman107 Nov 4, 2025
9548821
touch coupled again
mattman107 Nov 5, 2025
811e23d
Clean up grouping
mattman107 Nov 6, 2025
1b77741
add indirect conditions
mattman107 Nov 7, 2025
edfe9ca
Merge branch 'oot-soh' of https://github.com/HarbourMasters/Archipela…
mattman107 Nov 8, 2025
8ad5334
Add retries to the our GER randomization.
mattman107 Nov 8, 2025
7fb00c4
allow mixed entrances. It appears as though GER is preventing boss en…
mattman107 Nov 8, 2025
44e7f1f
Fix GER retries and update groupings
mattman107 Nov 15, 2025
64e68e5
Grotto Shuffle.
mattman107 Nov 15, 2025
a8151e9
Add Warp Song and Owl Drop Shuffle
mattman107 Nov 16, 2025
dca4459
Overworld Spawn Shuffle
mattman107 Nov 16, 2025
235bdd1
Interior Entrances
mattman107 Nov 19, 2025
ea9b9ab
Merge branch 'oot-soh' of https://github.com/HarbourMasters/Archipela…
mattman107 Nov 22, 2025
775f9a3
Put in Interior Entrances
mattman107 Nov 25, 2025
0f04384
More interior changes.
mattman107 Nov 25, 2025
e725682
Fix some misc issues with interior entrances.
mattman107 Nov 25, 2025
7cc3a59
Thieves Hideout entrances
mattman107 Nov 26, 2025
bfbb069
Define Overworld Entrances
mattman107 Nov 26, 2025
9b431e6
Finish initial overworld entrances
mattman107 Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions worlds/oot_soh/EntranceShuffle.py

Large diffs are not rendered by default.

369 changes: 365 additions & 4 deletions worlds/oot_soh/Enums.py

Large diffs are not rendered by default.

21 changes: 16 additions & 5 deletions worlds/oot_soh/LogicHelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import TYPE_CHECKING, Callable
from collections import Counter

from BaseClasses import CollectionState, ItemClassification as IC, MultiWorld
from BaseClasses import CollectionState, ItemClassification as IC, MultiWorld, EntranceType
from .Locations import SohLocation
from worlds.generic.Rules import set_rule
from worlds.AutoWorld import LogicMixin
Expand Down Expand Up @@ -47,14 +47,25 @@ def locationRule(bundle): return True


def connect_regions(parent_region: Regions, world: "SohWorld",
child_regions: list[tuple[Regions, Callable[[tuple[CollectionState, Regions, "SohWorld"]], bool]]]) -> None:
child_regions: list[tuple[Regions, Callable[[tuple[CollectionState, Regions, "SohWorld"]], bool], SOHBossEntranceNames | SOHDungeonExitNames | SOHBossWarpEntranceNames | SOHGrottoEntranceNames | SOHGrottoExitNames, int, EntranceType]]) -> None:
for region in child_regions:
regionName = region[0]
entranceName = None
def regionRule(bundle): return True
if len(region) > 1:
regionRule = region[1] # type: ignore # noqa
world.get_region(parent_region).connect(world.get_region(regionName),
rule=rule_wrapper.wrap(parent_region, regionRule, world))
regionRule = region[1]
if len(region) > 2:
entranceName = region[2].value
entrance = world.get_region(parent_region).connect(world.get_region(
regionName), entranceName, rule_wrapper.wrap(parent_region, regionRule, world))

if len(region) > 3:
entrance.randomization_group = region[3]
if len(region) > 4:
entrance.randomization_type = region[4]
else:
#Default to EntranceType.Two_Way
entrance.randomization_type = EntranceType.TWO_WAY


def add_events(parent_region: Regions, world: "SohWorld",
Expand Down
146 changes: 135 additions & 11 deletions worlds/oot_soh/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,117 @@ class ShuffleTycoonWallet(Toggle):
display_name = "Shuffle Tycoon Wallet"


class ShuffleDungeonBossEntrances(Choice):
"""
Shuffle the pool of dungeon boss entrances. This affects the boss rooms of all stone and medallion dungeons
Age Restricted - Shuffle the entrances of child and adult boss rooms separetly.
Full - Shuffle the entrances of all boss rooms together. Child may be expected to defeat Phantom Ganon and/or Bongo Bongo
"""
display_name = "Boss Entrances Shuffle"
option_off = 0
option_age_restricted = 1
option_full = 2
default = 0


class ShuffleOverworldEntrances(Toggle):
"""
Shuffle the pool of Overworld Entrances, which corresponds to almost all loading zones between overworld areas.

Some Entrances are unshuffled to avoid issues:
- Hyrule Castle Courtyard and Garden Entrance
- Both Market and Back Alley Entrances
- Gerudo Valley to Lake Hylia (unless entrances are decoupled)
"""
display_name = "Shuffle Overworld Entrances"


class ShuffleDungeonEntrances(Choice):
"""
Shuffle the pool of dungeon entrances, including Bottom of the Well, Ice Cavern and Gerudo Training Ground.
Shuffling Ganon's Castle can be enabled separately.
Additionally, the entrances of Deku Tree, Fire Temple, Bottom of the Well and Gerudo Training Ground are
opened for both child and adult.
- Deku Tree will be open for adult after Mido has seen child Link with a sword and a shield.
- Bottom of the Well will be open for adult after playing Song of Storms to the Windmill guy as child.
- Gerudo Training Ground will be open for child after adult has paid to open the gate once.
"""
display_name = "Dungeon Entrances Shuffle"
option_off = 0
option_on = 1
option_on_plus_ganon = 2
default = 0


class ShuffleTheivesHideoutEntrances(Toggle):
"""
Shuffle the pool of entrances between Gerudo Fortress & Theives' Hideout
"""
display_name = "Shuffle Theives' Hideout Entrances"


class ShuffleGrottoEntrances(Toggle):
"""
Shuffle the pool of grotto entrances, including all graves, small Fairy fountains and Deku Theatre.
"""
display_name = "Grotto Entrances Shuffle"


class ShuffleOwlDropEntrances(Toggle):
"""
Randomized where Kaepora Gaebora (the Owl) drops you at when you talk to him at Lake Hylia or at the top of Death Mountain Trail.
"""
display_name = " Shuffle Owl Drop Entrances"


class ShuffleWarpSongEntrances(Toggle):
"""
Randomized where each of the 6 warp songs leads to.
"""
display_name = "Shuffle Warp Song Entrances"


class ShuffleInteriorEntrances(Choice):
"""
Shuffle the pool of interior entrances which contains most houses and all Great Fairies
All - An extended version of 'Simple' with some extra places:
- Windmill
- Link's House
- Temple of Time
- Kakariko Potion Shop
"""
display_name = "Shuffle Interior Entrances"
option_off = 0
option_simple = 1
option_all = 2
default = 0


class ShuffleOverworldSpawns(Toggle):
"""
Randomized where you start as Child or Adult when loading a save in the Overworld. This means you may not necessarily spawn inside Link's House or Temple of Time.
This stays consistent after saving and loading the game.
Keep in mind you man need to temporarily disable the "Remember Save Location" time saver to be able to use the spawn positions, especially if they are the only logical way to get to certain areas.
"""
display_name = "Shuffle Overworld Spawns"

#currently doesn't work
class DecoupleEntrances(Toggle):
"""
Decouple entrances when shuffling them. This means that you are no longer guaranteed to end up back where you came from when you go back through an entrance.
This also adds the one way entrance from Gerudo Valley to Lake Hylia in the pool of overworld entrances when they are shuffled.
"""
display_name = "Decouple Entrances"
visibility = Visibility.none


class MixedEntrancePools(Toggle):
"""
Shuffle entrances into a mixed pool instead of separate ones. Has no effect on pools whose entrances aren't shuffled, and "Shuffle Boss Entrances" must be set to "Full" to include them.
"""
display_name = "Mixed Entrances Pools"


@dataclass
class SohOptions(PerGameCommonOptions):
closed_forest: ClosedForest
Expand Down Expand Up @@ -976,6 +1087,17 @@ class SohOptions(PerGameCommonOptions):
shuffle_tycoon_wallet: ShuffleTycoonWallet
tricks_in_logic: TricksInLogic
enable_all_tricks: EnableAllTricks
shuffle_dungeon_entrances: ShuffleDungeonEntrances
shuffle_boss_entrances: ShuffleDungeonBossEntrances
shuffle_overworld_entrances: ShuffleOverworldEntrances
shuffle_theives_hideout_entrances: ShuffleTheivesHideoutEntrances
shuffle_grotto_entrances: ShuffleGrottoEntrances
shuffle_warp_song_entrances: ShuffleWarpSongEntrances
shuffle_owl_drop_entrances: ShuffleOwlDropEntrances
shuffle_overworld_spawns: ShuffleOverworldSpawns
shuffle_interior_entrances: ShuffleInteriorEntrances
decouple_entrances: DecoupleEntrances
mixed_entrances_pools: MixedEntrancePools


soh_option_groups = [
Expand Down Expand Up @@ -1005,17 +1127,19 @@ class SohOptions(PerGameCommonOptions):
TriforceHuntPiecesTotal,
TriforceHuntPiecesRequiredPercentage,
]),
# OptionGroup("Shuffle Entrances", [
# # Dungeon Entrances
# # Boss Entrances
# # Overworld Entrances
# # Interior Entrances
# # Grotto Entrances
# # Owl Drops
# # Warp Songs
# # Overworld Spawns
# # Decouple Entrances
# ]),
OptionGroup("Shuffle Entrances", [
ShuffleDungeonEntrances,
ShuffleDungeonBossEntrances,
ShuffleOverworldEntrances,
ShuffleInteriorEntrances,
ShuffleTheivesHideoutEntrances,
ShuffleGrottoEntrances,
ShuffleWarpSongEntrances,
ShuffleOwlDropEntrances,
ShuffleOverworldSpawns,
DecoupleEntrances,
MixedEntrancePools
]),
OptionGroup("Shuffle Items", [
# Shuffle Songs -- idk if this or the other ones here will be an actual option here, delete if not
ShuffleTokens,
Expand Down
101 changes: 95 additions & 6 deletions worlds/oot_soh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from typing import Any, List, ClassVar

from BaseClasses import CollectionState, Item, Tutorial, ItemClassification
from BaseClasses import CollectionState, Item, Tutorial, ItemClassification, Entrance
from worlds.AutoWorld import WebWorld, World
from .Items import SohItem, item_data_table, item_table, item_name_groups, progressive_items
from .Locations import location_table, location_name_groups, token_amounts
Expand All @@ -19,6 +19,8 @@
from settings import Group, Bool
from Options import OptionError
from .LogicHelpers import wallet_capacities
from .EntranceShuffle import randomize_entrances_soh, on_connect_soh, randomize_soh_one_way_entrances
from .location_access.overworld.graveyard import connect_dampes_grave_windmill

import logging
logger = logging.getLogger("SOH_OOT")
Expand Down Expand Up @@ -65,6 +67,7 @@ class SohWorld(World):
item_name_to_id = item_table
item_name_groups = item_name_groups
location_name_groups = location_name_groups
er_pairings: List[tuple[str, str]] = []

# Universal Tracker stuff, does not do anything in normal gen
glitches_item_name = Items.GLITCHED
Expand Down Expand Up @@ -228,6 +231,91 @@ def set_rules(self) -> None:
# Completion condition.
self.multiworld.completion_condition[self.player] = lambda state: state.has(
Events.GAME_COMPLETED.value, self.player)

def connect_entrances(self):
# Do quircky one ways separate from the rest
randomize_soh_one_way_entrances(self)

entrances_to_shuffle = set()
# Reverse decoupled option for randomize_entrances because it is asking if you wanted coupled
# Update this when it is figured out why decoupled doesn't work with the current groupings
coupled = True #(not self.options.decouple_entrances)

if self.options.shuffle_theives_hideout_entrances:
# Make Temp Entrance to GF_ABOVE_JAIL to appease GER
temp_entrance: Entrance = self.get_region(Regions.ROOT.value).connect(self.get_region(Regions.GF_ABOVE_JAIL.value), "Temp GF_ABOVE_JAIL Entrance", None)

for entranceName in SOHThievesHideoutEntranceNames:
entrances_to_shuffle.add(entranceName)
randomize_entrances_soh(self, entrances_to_shuffle)
entrances_to_shuffle.clear()

# Clean up Temp Entrance
temp_entrance.connected_region.entrances.remove(temp_entrance)
temp_entrance.connected_region = None

# Boss Entrances
if self.options.shuffle_boss_entrances:
for entranceName in SOHBossEntranceNames:
entrances_to_shuffle.add(entranceName)

if self.options.shuffle_boss_entrances == "age_restricted":
randomize_entrances_soh(self, entrances_to_shuffle, ageRestricted=True)
entrances_to_shuffle.clear()

# Overworld Entrances
if self.options.shuffle_overworld_entrances:
for entranceName in SOHOverworldEntranceNames:
if not self.options.decouple_entrances and sum(map(bool, [self.options.shuffle_grotto_entrances, self.options.shuffle_interior_entrances, self.options.shuffle_dungeon_entrances, True if self.options.shuffle_boss_entrances == "full" else False])) >= 1 and entranceName == SOHOverworldEntranceNames.LAKE_HYLIA_RIVER_ENTRANCE:
continue

# ignore our one way exit here
if entranceName == SOHOverworldEntranceNames.LAKE_HYLIA_RIVER_EXIT:
continue

entrances_to_shuffle.add(entranceName)

# Grotto Entrances
if self.options.shuffle_grotto_entrances:
for entranceName in SOHGrottoExitNames:
entrances_to_shuffle.add(entranceName)

for entranceName in SOHGrottoEntranceNames:
entrances_to_shuffle.add(entranceName)

# Interior Entrances
if self.options.shuffle_interior_entrances:
if self.options.shuffle_interior_entrances == "all":
for entranceName in SOHSpecialInteriorExitNames:
entrances_to_shuffle.add(entranceName)

for entranceName in SOHSpecialInteriorEntranceNames:
entrances_to_shuffle.add(entranceName)

for entranceName in SOHInteriorExitNames:
entrances_to_shuffle.add(entranceName)

for entranceName in SOHInteriorEntranceNames:
entrances_to_shuffle.add(entranceName)

# Dungeon Entrances
if self.options.shuffle_dungeon_entrances:
for entranceName in SOHDungeonExitNames:
if entranceName != SOHDungeonExitNames.GANONS_CASTLE_DUNGEON_EXIT or self.options.shuffle_dungeon_entrances == 2:
entrances_to_shuffle.add(entranceName)

for entranceName in SOHDungeonEntranceNames:
if entranceName != SOHDungeonEntranceNames.GANONS_CASTLE_DUNGEON_ENTRANCE or self.options.shuffle_dungeon_entrances == 2:
entrances_to_shuffle.add(entranceName)

if len(entrances_to_shuffle) > 0:
randomize_entrances_soh(
self, entrances_to_shuffle, on_connect_soh, coupled)

# Connect Dampes After randomization so GER doesn't get any funny ideas
connect_dampes_grave_windmill(self)

return super().connect_entrances()

def collect(self, state: CollectionState, item: Item) -> bool:
changed = super().collect(state, item)
Expand Down Expand Up @@ -275,11 +363,11 @@ def remove(self, state: CollectionState, item: Item) -> bool:
return changed

# For debugging purposes
# def generate_output(self, output_directory: str):
# from Utils import visualize_regions
# visualize_regions(self.get_region(self.origin_region_name), f"SOH-Player{self.player}.puml",
# show_entrance_names=True,
# regions_to_highlight=self.multiworld.get_all_state().reachable_regions[self.player])
def generate_output(self, output_directory: str):
from Utils import visualize_regions
visualize_regions(self.get_region(self.origin_region_name), f"SOH-Player{self.player}.puml",
show_entrance_names=True,
regions_to_highlight=self.multiworld.get_all_state().reachable_regions[self.player])

def fill_slot_data(self) -> dict[str, Any]:
return {
Expand Down Expand Up @@ -366,4 +454,5 @@ def fill_slot_data(self) -> dict[str, Any]:
"apworld_version": self.apworld_version,
"enable_all_tricks": self.options.enable_all_tricks.value,
"tricks_in_logic": self.options.tricks_in_logic.value
# Need to figure out how to get the randomized entrances to Ship
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def set_region_rules(world: "SohWorld") -> None:
(Regions.BOTTOM_OF_THE_WELL_PERIMETER, lambda bundle: is_child(
bundle) and can_pass_enemy(bundle, Enemies.BIG_SKULLTULA)),
# [Regions.BOTTOM_OF_THE_WELL_MQ_PERIMETER, lambda bundle: is_child(bundle),
(Regions.KAK_WELL, lambda bundle: True)
(Regions.KAK_WELL, lambda bundle: True, SOHDungeonExitNames.BOTTOM_OF_THE_WELL_DUNGEON_EXIT,
SOHEntranceGroups.DUNGEON_ENTRANCE | SOHEntranceGroups.CHILD_ONLY, EntranceType.TWO_WAY)
])

# Bottom of the Well Perimeter
Expand Down
7 changes: 4 additions & 3 deletions worlds/oot_soh/location_access/dungeons/deku_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def set_region_rules(world: "SohWorld") -> None:
# Connections
connect_regions(Regions.DEKU_TREE_ENTRYWAY, world, [
(Regions.DEKU_TREE_LOBBY, lambda bundle: True),
(Regions.KF_OUTSIDE_DEKU_TREE, lambda bundle: True)
(Regions.KF_OUTSIDE_DEKU_TREE, lambda bundle: True, SOHDungeonExitNames.DEKU_TREE_DUNGEON_EXIT,
SOHEntranceGroups.DUNGEON_ENTRANCE | SOHEntranceGroups.CHILD, EntranceType.TWO_WAY)
])

# Deku Lobby
Expand Down Expand Up @@ -277,7 +278,7 @@ def set_region_rules(world: "SohWorld") -> None:
connect_regions(Regions.DEKU_TREE_OUTSIDE_BOSS_ROOM, world, [
(Regions.DEKU_TREE_BASEMENT_UPPER, lambda bundle: True),
(Regions.DEKU_TREE_BOSS_ENTRYWAY, lambda bundle: (has_item(Items.BRONZE_SCALE, bundle) or can_use(Items.IRON_BOOTS, bundle))
and can_reflect_nuts(bundle))
and can_reflect_nuts(bundle), SOHBossEntranceNames.DEKU_TREE_BOSS_ENTRANCE, SOHEntranceGroups.BOSS_ENTRANCE | SOHEntranceGroups.CHILD, EntranceType.ONE_WAY)
])

# Skipping master quest for now
Expand Down Expand Up @@ -328,5 +329,5 @@ def set_region_rules(world: "SohWorld") -> None:
connect_regions(Regions.DEKU_TREE_BOSS_ROOM, world, [
(Regions.DEKU_TREE_BOSS_EXIT, lambda bundle: True),
(Regions.KF_OUTSIDE_DEKU_TREE, lambda bundle: has_item(
Events.DEKU_TREE_COMPLETED, bundle))
Events.DEKU_TREE_COMPLETED, bundle), SOHBossWarpEntranceNames.DEKU_TREE_BOSS_WARP_ENTRANCE, SOHEntranceGroups.OTHER, EntranceType.ONE_WAY)
])
Loading