Skip to content

Commit

Permalink
Merge "Add 'empty dungeons' setting" (OoTRandomizer#1608)
Browse files Browse the repository at this point in the history
  • Loading branch information
cjohnson57 committed Aug 2, 2022
2 parents 907c588 + 06d4007 commit b7a55d7
Show file tree
Hide file tree
Showing 29 changed files with 741 additions and 76 deletions.
34 changes: 33 additions & 1 deletion Fill.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import random
import logging
from Hints import HintArea
from State import State
from Rules import set_shop_rules
from Location import DisableType
Expand Down Expand Up @@ -141,6 +142,33 @@ def distribute_items_restrictive(window, worlds, fill_locations=None):
fill_dungeons_restrictive(window, worlds, search, fill_locations, dungeon_items, itempool + songitempool)
search.collect_locations()


# If some dungeons are supposed to be empty, fill them with useless items.
if worlds[0].settings.empty_dungeons_mode != 'none':
empty_locations = [location for location in fill_locations \
if world.empty_dungeons[HintArea.at(location).dungeon_name].empty]
for location in empty_locations:
fill_locations.remove(location)
location.world.hint_type_overrides['sometimes'].append(location.name)
location.world.hint_type_overrides['random'].append(location.name)

if worlds[0].settings.shuffle_mapcompass in ['any_dungeon', 'overworld', 'keysanity']:
# Non-empty dungeon items are present in restitempool but yet we
# don't want to place them in an empty dungeon
restdungeon, restother = [], []
for item in restitempool:
if item.dungeonitem:
restdungeon.append(item)
else:
restother.append(item)
fast_fill(window, empty_locations, restother)
restitempool = restdungeon + restother
random.shuffle(restitempool)
else:
# We don't have to worry about this if dungeon items stay in their own dungeons
fast_fill(window, empty_locations, restitempool)


# places the songs into the world
# Currently places songs only at song locations. if there's an option
# to allow at other locations then they should be in the main pool.
Expand Down Expand Up @@ -240,8 +268,12 @@ def fill_dungeon_unique_item(window, worlds, search, fill_locations, itempool):
# since the rest are already placed.
major_items = [item for item in itempool if item.majoritem]
minor_items = [item for item in itempool if not item.majoritem]

if worlds[0].settings.empty_dungeons_mode != 'none':
dungeons = [dungeon for world in worlds for dungeon in world.dungeons if not world.empty_dungeons[dungeon.name].empty]
else:
dungeons = [dungeon for world in worlds for dungeon in world.dungeons]

dungeons = [dungeon for world in worlds for dungeon in world.dungeons]
double_dungeons = []
for dungeon in dungeons:
# we will count spirit temple twice so that it gets 2 items to match vanilla
Expand Down
3 changes: 2 additions & 1 deletion ItemPool.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,8 @@ def get_pool_core(world):
world.state.collect(ItemFactory('Small Key (Shadow Temple)'))
world.state.collect(ItemFactory('Small Key (Shadow Temple)'))

if not world.keysanity and not world.dungeon_mq['Fire Temple']:
if (not world.keysanity or (world.empty_dungeons['Fire Temple'].empty and world.settings.shuffle_smallkeys != 'remove'))\
and not world.dungeon_mq['Fire Temple']:
world.state.collect(ItemFactory('Small Key (Fire Temple)'))

if world.settings.shuffle_ganon_bosskey == 'on_lacs':
Expand Down
6 changes: 5 additions & 1 deletion Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,9 @@ def create_playthrough(spoiler):
entrance_spheres = []
remaining_entrances = set(entrance for world in worlds for entrance in world.get_shuffled_entrances())

search.checkpoint()
search.collect_pseudo_starting_items()

while True:
search.checkpoint()
# Not collecting while the generator runs means we only get one sphere at a time
Expand Down Expand Up @@ -752,6 +755,7 @@ def create_playthrough(spoiler):
# Regenerate the spheres as we might not reach places the same way anymore.
search.reset() # search state has no items, okay to reuse sphere 0 cache
collection_spheres = []
collection_spheres.append(list(search.iter_pseudo_starting_locations()))
entrance_spheres = []
remaining_entrances = set(required_entrances)
collected = set()
Expand Down Expand Up @@ -780,7 +784,7 @@ def create_playthrough(spoiler):
logger.info('Collected %d final spheres', len(collection_spheres))

# Then we can finally output our playthrough
spoiler.playthrough = OrderedDict((str(i + 1), {location: location.item for location in sphere}) for i, sphere in enumerate(collection_spheres))
spoiler.playthrough = OrderedDict((str(i), {location: location.item for location in sphere}) for i, sphere in enumerate(collection_spheres))
# Copy our misc. hint items, since we set them in the world copy
for w, sw in zip(worlds, spoiler.worlds):
for hint_type, item_location in w.misc_hint_item_locations.items():
Expand Down
46 changes: 41 additions & 5 deletions Plandomizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from Fill import FillError
from EntranceShuffle import EntranceShuffleError, change_connections, confirm_replacement, validate_world, check_entrances_compatibility
from Hints import gossipLocations, GossipText
from Hints import HintArea, gossipLocations, GossipText
from Item import ItemFactory, ItemInfo, ItemIterator, IsItem
from ItemPool import item_groups, get_junk_item, song_list
from Location import LocationIterator, LocationFactory, IsLocation
Expand All @@ -31,6 +31,7 @@ class InvalidFileException(Exception):
'randomized_settings',
'item_pool',
'dungeons',
'empty_dungeons',
'trials',
'songs',
'entrances',
Expand Down Expand Up @@ -85,6 +86,20 @@ def to_json(self):
return 'mq' if self.mq else 'vanilla'


class EmptyDungeonRecord(SimpleRecord({'empty': None})):
def __init__(self, src_dict='random'):
if src_dict == 'random':
src_dict = {'empty': None}
if src_dict in (True, False):
src_dict = {'empty': src_dict}
super().__init__(src_dict)


def to_json(self):
return self.empty



class GossipRecord(SimpleRecord({'text': None, 'colors': None, 'hinted_locations': None, 'hinted_items': None})):
def to_json(self):
if self.colors is not None:
Expand Down Expand Up @@ -240,6 +255,7 @@ def update(self, src_dict, update_all=False):
update_dict = {
'randomized_settings': {name: record for (name, record) in src_dict.get('randomized_settings', {}).items()},
'dungeons': {name: DungeonRecord(record) for (name, record) in src_dict.get('dungeons', {}).items()},
'empty_dungeons': {name: EmptyDungeonRecord(record) for (name, record) in src_dict.get('empty_dungeons', {}).items()},
'trials': {name: TrialRecord(record) for (name, record) in src_dict.get('trials', {}).items()},
'songs': {name: SongRecord(record) for (name, record) in src_dict.get('songs', {}).items()},
'item_pool': {name: ItemPoolRecord(record) for (name, record) in src_dict.get('item_pool', {}).items()},
Expand Down Expand Up @@ -271,6 +287,7 @@ def to_json(self):
return {
'randomized_settings': self.randomized_settings,
'dungeons': {name: record.to_json() for (name, record) in self.dungeons.items()},
'empty_dungeons': {name: record.to_json() for (name, record) in self.empty_dungeons.items()},
'trials': {name: record.to_json() for (name, record) in self.trials.items()},
'songs': {name: record.to_json() for (name, record) in self.songs.items()},
'item_pool': SortedDict({name: record.to_json() for (name, record) in self.item_pool.items()}),
Expand Down Expand Up @@ -364,15 +381,21 @@ def add_location(self, new_location, new_item):
self.locations[new_location] = LocationRecord(new_item)


def configure_dungeons(self, world, dungeon_pool):
dist_num_mq = 0
def configure_dungeons(self, world, mq_dungeon_pool, empty_dungeon_pool):
dist_num_mq, dist_num_empty = 0, 0
for (name, record) in self.dungeons.items():
if record.mq is not None:
dungeon_pool.remove(name)
mq_dungeon_pool.remove(name)
if record.mq:
dist_num_mq += 1
world.dungeon_mq[name] = True
return dist_num_mq
for (name, record) in self.empty_dungeons.items():
if record.empty is not None:
empty_dungeon_pool.remove(name)
if record.empty:
dist_num_empty += 1
world.empty_dungeons[name].empty = True
return dist_num_mq, dist_num_empty


def configure_trials(self, trial_pool):
Expand Down Expand Up @@ -1003,6 +1026,18 @@ def configure_effective_starting_items(self, worlds, world):
skipped_locations = ['Links Pocket']
if world.settings.skip_child_zelda:
skipped_locations += ['HC Malon Egg', 'HC Zeldas Letter', 'Song from Impa']
if world.settings.empty_dungeons_mode != 'none':
boss_map = world.get_boss_map()
for info in world.empty_dungeons.values():
if info.empty:
skipped_locations.append(boss_map[info.boss_name])
if world.settings.shuffle_song_items == 'dungeon':
for location_name in location_groups['BossHeart']:
location = world.get_location(location_name)
hint_area = HintArea.at(location)
if hint_area.is_dungeon and world.empty_dungeons[hint_area.dungeon_name].empty:
skipped_locations.append(location.name)
world.item_added_hint_types['barren'].append(location.item.name)
for iter_world in worlds:
for location in skipped_locations:
loc = iter_world.get_location(location)
Expand Down Expand Up @@ -1228,6 +1263,7 @@ def update_spoiler(self, spoiler, output_spoiler):
world_dist = self.world_dists[world.id]
world_dist.randomized_settings = {randomized_item: getattr(world.settings, randomized_item) for randomized_item in world.randomized_list}
world_dist.dungeons = {dung: DungeonRecord({ 'mq': world.dungeon_mq[dung] }) for dung in world.dungeon_mq}
world_dist.empty_dungeons = {dung: EmptyDungeonRecord({ 'empty': world.empty_dungeons[dung].empty }) for dung in world.empty_dungeons}
world_dist.trials = {trial: TrialRecord({ 'active': not world.skipped_trials[trial] }) for trial in world.skipped_trials}
if hasattr(world, 'song_notes'):
world_dist.songs = {song: SongRecord({ 'notes': str(world.song_notes[song]) }) for song in world.song_notes}
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ do that.
* New setting `Blue Fire Arrows` gives Ice Arrows the power to melt red ice and mud walls to give them more utility.
* New Misc. Hint `Dampé's Diary` which reveals the location of a hookshot.
* New item pool setting `Ludicrous` makes it so every check will be a major item.
* `Shuffle Dungeon Entrances` has new setting `Dungeons and Ganon` which puts Ganon's Castle into the pool of dungeons which can be shuffled.
* `Shuffle Dungeon Entrances` has new setting `Dungeons and Ganon` which puts Ganon's Castle into the pool of dungeons which can be shuffled.
* New setting `Pre-Completed Dungeons` which allows some dungeons to be filled with junk and their dungeon rewards given as a starting item.

* **Gameplay**
* Shortened the animation for equipping magic arrows.
Expand Down
9 changes: 9 additions & 0 deletions Region.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ def hint(self):


def can_fill(self, item, manual=False):
if not manual and self.world.settings.empty_dungeons_mode != 'none' and item.dungeonitem:
# An empty dungeon can only store its own dungeon items
if self.dungeon and self.dungeon.world.empty_dungeons[self.dungeon.name].empty:
return self.dungeon.is_dungeon_item(item) and item.world.id == self.world.id
# Items from empty dungeons can only be in their own dungeons
for dungeon in item.world.dungeons:
if item.world.empty_dungeons[dungeon.name].empty and dungeon.is_dungeon_item(item):
return False

from Hints import HintArea

is_self_dungeon_restricted = False
Expand Down
34 changes: 31 additions & 3 deletions Search.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import defaultdict
import itertools

from LocationList import location_groups
from Region import TimeOfDay
from State import State

Expand Down Expand Up @@ -285,13 +286,40 @@ def test_category_goals(self, goal_categories, world_filter = None):
return valid_goals


def collect_pseudo_starting_items(self):
def iter_pseudo_starting_locations(self):
for state in self.state_list:
# Skip Child Zelda and Link's Pocket are not technically starting items, so collect them now
if state.world.settings.skip_child_zelda:
self.collect(state.world.get_location('Song from Impa').item)
self.collect(state.world.get_location('Links Pocket').item)
location = state.world.get_location('Song from Impa')
self._cache['visited_locations'].add(location)
yield location

location = state.world.get_location('Links Pocket')
self._cache['visited_locations'].add(location)
yield location

# Rewards from empty dungeons are also given for free
if state.world.settings.empty_dungeons_mode != 'none':
boss_map = state.world.get_boss_map()
for info in state.world.empty_dungeons.values():
if info.empty:
location = state.world.get_location(boss_map[info.boss_name])
self._cache['visited_locations'].add(location)
yield location
# If songs are shuffled inside dungeons, collect them too
if state.world.settings.shuffle_song_items == 'dungeon':
from Hints import HintArea
for location_name in location_groups['BossHeart']:
location = state.world.get_location(location_name)
hint_area = HintArea.at(location)
if hint_area.is_dungeon and state.world.empty_dungeons[hint_area.dungeon_name].empty:
self._cache['visited_locations'].add(location)
yield location


def collect_pseudo_starting_items(self):
for location in self.iter_pseudo_starting_locations():
self.collect(location.item)

# Use the cache in the search to determine region reachability.
# Implicitly requires is_starting_age or Time_Travel.
Expand Down
Loading

0 comments on commit b7a55d7

Please sign in to comment.