Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OoT Time Optimization #2401

Merged
merged 14 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading