Skip to content

Commit

Permalink
OoT Time Optimization (ArchipelagoMW#2401)
Browse files Browse the repository at this point in the history
- Entrance randomizer no longer grows with multiworld
- Improved ER success rate again by prioritizing Temple of Time even more
- Prefill is faster, has slightly reduced failure rate when map/compass are in dungeon but previous items in any_dungeon (which consumed all available locations), no longer removes items from the main itempool; itemlinked prefill items removed to accomodate improvements
- Now triggers only one recache after `generate_basic` instead of one per oot world
- Avoids recaches during `create_regions`
- All ER temp entrances have unique names (so the entrance cache does not break)
  • Loading branch information
espeon65536 authored and FlySniper committed Nov 14, 2023
1 parent fe599a1 commit 71932ee
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 146 deletions.
10 changes: 4 additions & 6 deletions worlds/oot/Entrance.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

from BaseClasses import Entrance
from .Regions import TimeOfDay

class OOTEntrance(Entrance):
game: str = 'Ocarina of Time'
Expand Down Expand Up @@ -29,16 +27,16 @@ def disconnect(self):
self.connected_region = None
return previously_connected

def get_new_target(self):
def get_new_target(self, pool_type):
root = self.multiworld.get_region('Root Exits', self.player)
target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root)
target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root)
target_entrance.connect(self.connected_region)
target_entrance.replaces = self
root.exits.append(target_entrance)
return target_entrance

def assume_reachable(self):
def assume_reachable(self, pool_type):
if self.assumed == None:
self.assumed = self.get_new_target()
self.assumed = self.get_new_target(pool_type)
self.disconnect()
return self.assumed
52 changes: 25 additions & 27 deletions worlds/oot/EntranceShuffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging

from worlds.generic.Rules import set_rule, add_rule
from BaseClasses import CollectionState

from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay
Expand All @@ -25,12 +26,12 @@ def set_all_entrances_data(world, player):
return_entrance.data['index'] = 0x7FFF


def assume_entrance_pool(entrance_pool, ootworld):
def assume_entrance_pool(entrance_pool, ootworld, pool_type):
assumed_pool = []
for entrance in entrance_pool:
assumed_forward = entrance.assume_reachable()
assumed_forward = entrance.assume_reachable(pool_type)
if entrance.reverse != None and not ootworld.decouple_entrances:
assumed_return = entrance.reverse.assume_reachable()
assumed_return = entrance.reverse.assume_reachable(pool_type)
if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)):
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
Expand All @@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld):
return assumed_pool


def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()):
def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()):
one_way_entrances = []
for pool_type in types_to_include:
one_way_entrances += world.get_shufflable_entrances(type=pool_type)
valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances))
if target_region_names:
return [entrance.get_new_target() for entrance in valid_one_way_entrances
return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances
if entrance.connected_region.name in target_region_names]
return [entrance.get_new_target() for entrance in valid_one_way_entrances]
return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances]


# Abbreviations
Expand Down Expand Up @@ -423,14 +424,14 @@ def _add_boss_entrances():
}

interior_entrance_bias = {
'Kakariko Village -> Kak Potion Shop Front': 4,
'Kak Backyard -> Kak Potion Shop Back': 4,
'Kakariko Village -> Kak Impas House': 3,
'Kak Impas Ledge -> Kak Impas House Back': 3,
'Goron City -> GC Shop': 2,
'Zoras Domain -> ZD Shop': 2,
'ToT Entrance -> Temple of Time': 4,
'Kakariko Village -> Kak Potion Shop Front': 3,
'Kak Backyard -> Kak Potion Shop Back': 3,
'Kakariko Village -> Kak Impas House': 2,
'Kak Impas Ledge -> Kak Impas House Back': 2,
'Market Entrance -> Market Guard House': 2,
'ToT Entrance -> Temple of Time': 1,
'Goron City -> GC Shop': 1,
'Zoras Domain -> ZD Shop': 1,
}


Expand All @@ -443,7 +444,8 @@ def shuffle_random_entrances(ootworld):
player = ootworld.player

# Gather locations to keep reachable for validation
all_state = world.get_all_state(use_cache=True)
all_state = ootworld.get_state_with_complete_itempool()
all_state.sweep_for_events(locations=ootworld.get_locations())
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}

# Set entrance data for all entrances
Expand Down Expand Up @@ -523,12 +525,12 @@ def shuffle_random_entrances(ootworld):
for pool_type, entrance_pool in one_way_entrance_pools.items():
if pool_type == 'OwlDrop':
valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
for target in one_way_target_entrance_pools[pool_type]:
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
elif pool_type in {'Spawn', 'WarpSong'}:
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types)
# Ensure that the last entrance doesn't assume the rest of the targets are reachable
for target in one_way_target_entrance_pools[pool_type]:
add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))())
Expand All @@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld):

target_entrance_pools = {}
for pool_type, entrance_pool in entrance_pools.items():
target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld)
target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type)

# Build all_state and none_state
all_state = ootworld.get_state_with_complete_itempool()
none_state = all_state.copy()
for item_tuple in none_state.prog_items:
if item_tuple[1] == player:
none_state.prog_items[item_tuple] = 0
none_state = CollectionState(ootworld.multiworld)

# Plando entrances
if world.plando_connections[player]:
Expand Down Expand Up @@ -628,7 +627,7 @@ def shuffle_random_entrances(ootworld):
logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}')
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
# Game is beatable
new_all_state = world.get_all_state(use_cache=False)
new_all_state = ootworld.get_state_with_complete_itempool()
if not world.has_beaten_game(new_all_state, player):
raise EntranceShuffleError('Cannot beat game')
# Validate world
Expand Down Expand Up @@ -700,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')


def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10):

restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)

Expand Down Expand Up @@ -745,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback


def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances):
world = ootworld.multiworld
player = ootworld.player

# Disconnect all root assumed entrances and save original connections
Expand All @@ -755,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
if entrance.connected_region:
original_connected_regions[entrance] = entrance.disconnect()

all_state = world.get_all_state(use_cache=False)
all_state = ootworld.get_state_with_complete_itempool()

restrictive_entrances = []
soft_entrances = []
Expand Down Expand Up @@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
all_state = all_state_orig.copy()
none_state = none_state_orig.copy()

all_state.sweep_for_events()
none_state.sweep_for_events()
all_state.sweep_for_events(locations=ootworld.get_locations())
none_state.sweep_for_events(locations=ootworld.get_locations())

if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
time_travel_state = none_state.copy()
Expand Down
18 changes: 11 additions & 7 deletions worlds/oot/Rules.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from collections import deque
import logging
import typing

from .Regions import TimeOfDay
from .DungeonList import dungeon_table
from .Hints import HintArea
from .Items import oot_is_item_of_type
from .LocationList import dungeon_song_locations

from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
Expand Down Expand Up @@ -150,11 +154,16 @@ def set_rules(ootworld):
location = world.get_location('Forest Temple MQ First Room Chest', player)
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)

if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items:
if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items:
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
# This is required if map/compass included, or any_dungeon shuffle.
location = world.get_location('Sheik in Ice Cavern', player)
add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song'))
add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song'))

if ootworld.shuffle_child_trade == 'skip_child_zelda':
# Song from Impa must be local
location = world.get_location('Song from Impa', player)
add_item_rule(location, lambda item: item.player == player)

for name in ootworld.always_hints:
add_rule(world.get_location(name, player), guarantee_hint)
Expand All @@ -176,11 +185,6 @@ def required_wallets(price):
return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price))


def limit_to_itemset(location, itemset):
old_rule = location.item_rule
location.item_rule = lambda item: item.name in itemset and old_rule(item)


# This function should be run once after the shop items are placed in the world.
# It should be run before other items are placed in the world so that logic has
# the correct checks for them. This is safe to do since every shop is still
Expand Down
Loading

0 comments on commit 71932ee

Please sign in to comment.