def __init__(self, id, settings): self.id = id self.shuffle = 'vanilla' self.dungeons = [] self.regions = [] self.itempool = [] self._cached_locations = None self._entrance_cache = {} self._region_cache = {} self._location_cache = {} self.required_locations = [] self.shop_prices = {} self.scrub_prices = {} self.maximum_wallets = 0 self.light_arrow_location = None self.triforce_count = 0 self.bingosync_url = None self.parser = Rule_AST_Transformer(self) self.event_items = set() # dump settings directly into world's namespace # this gives the world an attribute for every setting listed in Settings.py self.settings = settings self.__dict__.update(settings.__dict__) self.distribution = settings.distribution.world_dists[id] # rename a few attributes... self.keysanity = self.shuffle_smallkeys in [ 'keysanity', 'remove', 'any_dungeon', 'overworld' ] self.check_beatable_only = not self.all_reachable self.shuffle_special_interior_entrances = self.shuffle_interior_entrances == 'all' self.shuffle_interior_entrances = self.shuffle_interior_entrances in [ 'simple', 'all' ] self.entrance_shuffle = self.shuffle_interior_entrances or self.shuffle_grotto_entrances or self.shuffle_dungeon_entrances or \ self.shuffle_overworld_entrances or self.owl_drops or self.warp_songs or self.spawn_positions self.ensure_tod_access = self.shuffle_interior_entrances or self.shuffle_overworld_entrances or self.spawn_positions self.disable_trade_revert = self.shuffle_interior_entrances or self.shuffle_overworld_entrances if self.open_forest == 'closed' and ( self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions or self.decouple_entrances or (self.mix_entrance_pools != 'off')): self.open_forest = 'closed_deku' self.triforce_goal = self.triforce_goal_per_world * settings.world_count if self.triforce_hunt: # Pin shuffle_ganon_bosskey to 'triforce' when triforce_hunt is enabled # (specifically, for randomize_settings) self.shuffle_ganon_bosskey = 'triforce' # Determine LACS Condition if self.shuffle_ganon_bosskey == 'lacs_medallions': self.lacs_condition = 'medallions' elif self.shuffle_ganon_bosskey == 'lacs_dungeons': self.lacs_condition = 'dungeons' elif self.shuffle_ganon_bosskey == 'lacs_stones': self.lacs_condition = 'stones' elif self.shuffle_ganon_bosskey == 'lacs_tokens': self.lacs_condition = 'tokens' else: self.lacs_condition = 'vanilla' # trials that can be skipped will be decided later self.skipped_trials = { 'Forest': False, 'Fire': False, 'Water': False, 'Spirit': False, 'Shadow': False, 'Light': False } # dungeon forms will be decided later self.dungeon_mq = { 'Deku Tree': False, 'Dodongos Cavern': False, 'Jabu Jabus Belly': False, 'Bottom of the Well': False, 'Ice Cavern': False, 'Gerudo Training Grounds': False, 'Forest Temple': False, 'Fire Temple': False, 'Water Temple': False, 'Spirit Temple': False, 'Shadow Temple': False, 'Ganons Castle': False } self.can_take_damage = True self.resolve_random_settings() if len(settings.hint_dist_user) == 0: for d in HintDistFiles(): dist = read_json(d) if dist['name'] == self.hint_dist: self.hint_dist_user = dist else: self.hint_dist = 'custom' # Validate hint distribution format # Originally built when I was just adding the type distributions # Location/Item Additions and Overrides are not validated hint_dist_valid = False if all(key in self.hint_dist_user['distribution'] for key in hint_dist_keys): hint_dist_valid = True sub_keys = {'order', 'weight', 'fixed', 'copies'} for key in self.hint_dist_user['distribution']: if not all(sub_key in sub_keys for sub_key in self.hint_dist_user['distribution'][key]): hint_dist_valid = False if not hint_dist_valid: raise InvalidFileException( """Hint distributions require all hint types be present in the distro (trial, always, woth, barren, item, song, overworld, dungeon, entrance, sometimes, random, junk, named-item). If a hint type should not be shuffled, set its order to 0. Hint type format is \"type\": { \"order\": 0, \"weight\": 0.0, \"fixed\": 0, \"copies\": 0 }""" ) self.added_hint_types = {} self.item_added_hint_types = {} self.hint_exclusions = set() if self.skip_child_zelda or settings.skip_child_zelda: self.hint_exclusions.add('Song from Impa') self.hint_type_overrides = {} self.item_hint_type_overrides = {} for dist in hint_dist_keys: self.added_hint_types[dist] = [] for loc in self.hint_dist_user['add_locations']: if 'types' in loc: if dist in loc['types']: self.added_hint_types[dist].append(loc['location']) self.item_added_hint_types[dist] = [] for i in self.hint_dist_user['add_items']: if dist in i['types']: self.item_added_hint_types[dist].append(i['item']) self.hint_type_overrides[dist] = [] for loc in self.hint_dist_user['remove_locations']: if dist in loc['types']: self.hint_type_overrides[dist].append(loc['location']) self.item_hint_type_overrides[dist] = [] for i in self.hint_dist_user['remove_items']: if dist in i['types']: self.item_hint_type_overrides[dist].append(i['item']) self.hint_text_overrides = {} for loc in self.hint_dist_user['add_locations']: if 'text' in loc: # Arbitrarily throw an error at 80 characters to prevent overfilling the text box. if len(loc['text']) > 80: raise Exception('Custom hint text too large for %s', loc['location']) self.hint_text_overrides.update({loc['location']: loc['text']}) self.always_hints = [hint.name for hint in getRequiredHints(self)] self.state = State(self) # Allows us to cut down on checking whether some items are required self.max_progressions = { item: value[3].get('progressive', 1) if value[3] else 1 for item, value in item_table.items() } max_tokens = 0 if self.bridge == 'tokens': max_tokens = max(max_tokens, self.bridge_tokens) if self.lacs_condition == 'tokens': max_tokens = max(max_tokens, self.lacs_tokens) tokens = [50, 40, 30, 20, 10] for t in tokens: if f'{t} Gold Skulltula Reward' not in self.disabled_locations: max_tokens = max(max_tokens, t) self.max_progressions['Gold Skulltula Token'] = max_tokens # Additional Ruto's Letter become Bottle, so we may have to collect two. self.max_progressions['Rutos Letter'] = 2
# items never required: # refills, maps, compasses, capacity upgrades, masks (not listed in logic) never_prefix = ['Bombs', 'Arrows', 'Rupee', 'Deku Seeds', 'Map', 'Compass'] never_suffix = ['Capacity'] never = { 'Bunny Hood', 'Recovery Heart', 'Milk', 'Ice Arrows', 'Ice Trap', 'Double Defense', 'Biggoron Sword', } | { item for item, (t, adv, _, special) in item_table.items() if adv is False or any(map(item.startswith, never_prefix)) or any(map(item.endswith, never_suffix)) } # items required at most once, specifically things with multiple possible names # (except bottles) once = { 'Goron Tunic', 'Zora Tunic', } progressive = { item for item, (_, _, _, special) in item_table.items() if special and 'progressive' in special
def __init__(self, id, settings): self.id = id self.shuffle = 'vanilla' self.dungeons = [] self.regions = [] self.itempool = [] self._cached_locations = None self._entrance_cache = {} self._region_cache = {} self._location_cache = {} self.required_locations = [] self.shop_prices = {} self.scrub_prices = {} self.maximum_wallets = 0 self.light_arrow_location = None self.triforce_count = 0 self.parser = Rule_AST_Transformer(self) self.event_items = set() # dump settings directly into world's namespace # this gives the world an attribute for every setting listed in Settings.py self.settings = settings self.__dict__.update(settings.__dict__) self.distribution = settings.distribution.world_dists[id] if self.open_forest == 'closed' and self.entrance_shuffle in ['all-indoors', 'all']: self.open_forest = 'closed_deku' # rename a few attributes... self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove'] self.check_beatable_only = not self.all_reachable self.shuffle_dungeon_entrances = self.entrance_shuffle != 'off' self.shuffle_grotto_entrances = self.entrance_shuffle in ['simple-indoors', 'all-indoors', 'all'] self.shuffle_interior_entrances = self.entrance_shuffle in ['simple-indoors', 'all-indoors', 'all'] self.shuffle_special_indoor_entrances = self.entrance_shuffle in ['all-indoors', 'all'] self.shuffle_overworld_entrances = self.entrance_shuffle == 'all' self.disable_trade_revert = self.shuffle_interior_entrances or self.shuffle_overworld_entrances self.ensure_tod_access = self.shuffle_interior_entrances or self.shuffle_overworld_entrances self.triforce_goal = self.triforce_goal_per_world * settings.world_count if self.triforce_hunt: # Pin shuffle_ganon_bosskey to 'triforce' when triforce_hunt is enabled # (specifically, for randomize_settings) self.shuffle_ganon_bosskey = 'triforce' # Determine LACS Condition if self.shuffle_ganon_bosskey == 'lacs_medallions': self.lacs_condition = 'medallions' elif self.shuffle_ganon_bosskey == 'lacs_dungeons': self.lacs_condition = 'dungeons' elif self.shuffle_ganon_bosskey == 'lacs_stones': self.lacs_condition = 'stones' else: self.lacs_condition = 'vanilla' # trials that can be skipped will be decided later self.skipped_trials = { 'Forest': False, 'Fire': False, 'Water': False, 'Spirit': False, 'Shadow': False, 'Light': False } # dungeon forms will be decided later self.dungeon_mq = { 'Deku Tree': False, 'Dodongos Cavern': False, 'Jabu Jabus Belly': False, 'Bottom of the Well': False, 'Ice Cavern': False, 'Gerudo Training Grounds': False, 'Forest Temple': False, 'Fire Temple': False, 'Water Temple': False, 'Spirit Temple': False, 'Shadow Temple': False, 'Ganons Castle': False } self.can_take_damage = True self.resolve_random_settings() self.always_hints = [hint.name for hint in getRequiredHints(self)] self.state = State(self) # Allows us to cut down on checking whether some items are required self.max_progressions = { item: value[3].get('progressive', 1) if value[3] else 1 for item, value in item_table.items() } max_tokens = 0 if self.bridge == 'tokens': max_tokens = self.bridge_tokens tokens = [50, 40, 30, 20, 10] for t in tokens: if t > max_tokens and f'{t} Gold Skulltula Reward' not in self.disabled_locations: max_tokens = t self.max_progressions['Gold Skulltula Token'] = max_tokens # Additional Ruto's Letter become Bottle, so we may have to collect two. self.max_progressions['Ruto\'s Letter'] = 2
from Settings import Settings, get_preset_files test_dir = os.path.join(os.path.dirname(__file__), 'tests') output_dir = os.path.join(test_dir, 'Output') os.makedirs(output_dir, exist_ok=True) logging.basicConfig(level=logging.INFO, filename=os.path.join(output_dir, 'LAST_TEST_LOG'), filemode='w') # items never required: # refills, maps, compasses, capacity upgrades, masks (not listed in logic) never_prefix = ['Bombs', 'Arrows', 'Rupee', 'Deku Seeds', 'Map', 'Compass'] never_suffix = ['Capacity'] never = { 'Bunny Hood', 'Recovery Heart', 'Milk', 'Ice Arrows', 'Ice Trap', 'Double Defense', 'Biggoron Sword', 'Giants Knife', } | {item for item, (t, adv, _, special) in item_table.items() if adv is False or any(map(item.startswith, never_prefix)) or any(map(item.endswith, never_suffix))} # items required at most once, specifically things with multiple possible names # (except bottles) once = { 'Goron Tunic', 'Zora Tunic', } progressive = { item for item, (_, _, _, special) in item_table.items() if special and 'progressive' in special } bottles = { item for item, (_, _, _, special) in item_table.items()
def distribute_items_restrictive(window, worlds, fill_locations=None): song_locations = [ world.get_location(location) for world in worlds for location in [ 'Song from Composers Grave', 'Song from Impa', 'Song from Malon', 'Song from Saria', 'Song from Ocarina of Time', 'Song from Windmill', 'Sheik in Forest', 'Sheik at Temple', 'Sheik in Crater', 'Sheik in Ice Cavern', 'Sheik in Kakariko', 'Sheik at Colossus' ] ] shop_locations = [ location for world in worlds for location in world.get_unfilled_locations() if location.type == 'Shop' and location.price == None ] # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: fill_locations = [location for world in worlds for location in world.get_unfilled_locations() \ if location not in song_locations and \ location not in shop_locations and \ location.type != 'GossipStone'] world_states = [world.state for world in worlds] window.locationcount = len(fill_locations) + len(song_locations) + len( shop_locations) window.fillcount = 0 # Generate the itempools shopitempool = [ item for world in worlds for item in world.itempool if item.type == 'Shop' ] songitempool = [ item for world in worlds for item in world.itempool if item.type == 'Song' ] itempool = [ item for world in worlds for item in world.itempool if item.type != 'Shop' and item.type != 'Song' ] if worlds[0].shuffle_song_items: itempool.extend(songitempool) fill_locations.extend(song_locations) songitempool = [] song_locations = [] # add unrestricted dungeon items to main item pool itempool.extend([ item for world in worlds for item in world.get_unrestricted_dungeon_items() ]) dungeon_items = [ item for world in worlds for item in world.get_restricted_dungeon_items() ] random.shuffle( itempool ) # randomize item placement order. this ordering can greatly affect the location accessibility bias progitempool = [item for item in itempool if item.advancement] prioitempool = [ item for item in itempool if not item.advancement and item.priority ] restitempool = [ item for item in itempool if not item.advancement and not item.priority ] cloakable_locations = shop_locations + song_locations + fill_locations all_models = shopitempool + dungeon_items + songitempool + itempool worlds[0].settings.distribution.fill( window, worlds, [shop_locations, song_locations, fill_locations], [ shopitempool, dungeon_items, songitempool, progitempool, prioitempool, restitempool ]) itempool = progitempool + prioitempool + restitempool # set ice traps to have the appearance of other random items in the item pool ice_traps = [item for item in itempool if item.name == 'Ice Trap'] # Extend with ice traps manually placed in plandomizer ice_traps.extend( location.item for location in cloakable_locations if (location.name in location_groups['CanSee'] and location.item is not None and location.item.name == 'Ice Trap' and location.item.looks_like_item is None)) junk_items = remove_junk_items.copy() junk_items.remove('Ice Trap') major_items = [ item for (item, data) in item_table.items() if data[0] == 'Item' and data[1] and data[2] is not None ] fake_items = [] if worlds[0].settings.ice_trap_appearance == 'major_only': model_items = [item for item in itempool if item.majoritem] if len( model_items ) == 0: # All major items were somehow removed from the pool (can happen in plando) model_items = ItemFactory(major_items) elif worlds[0].settings.ice_trap_appearance == 'junk_only': model_items = [item for item in itempool if item.name in junk_items] if len(model_items) == 0: # All junk was removed model_items = ItemFactory(junk_items) else: # world[0].settings.ice_trap_appearance == 'anything': model_items = [item for item in itempool if item.name != 'Ice Trap'] if len( model_items ) == 0: # All major items and junk were somehow removed from the pool (can happen in plando) model_items = ItemFactory(major_items) + ItemFactory(junk_items) while len(ice_traps) > len(fake_items): # if there are more ice traps than model items, then double up on model items fake_items.extend(model_items) for random_item in random.sample(fake_items, len(ice_traps)): ice_trap = ice_traps.pop(0) ice_trap.looks_like_item = random_item # Start a search cache here. search = Search([world.state for world in worlds]) # We place all the shop items first. Like songs, they have a more limited # set of locations that they can be placed in, so placing them first will # reduce the odds of creating unbeatable seeds. This also avoids needing # to create item rules for every location for whether they are a shop item # or not. This shouldn't have much affect on item bias. if shop_locations: logger.info('Placing shop items.') fill_ownworld_restrictive(window, worlds, search, shop_locations, shopitempool, itempool + songitempool + dungeon_items, "shop") # Update the shop item access rules for world in worlds: set_shop_rules(world) search.collect_locations() # If there are dungeon items that are restricted to their original dungeon, # we must place them first to make sure that there is always a location to # place them. This could probably be replaced for more intelligent item # placement, but will leave as is for now if dungeon_items: logger.info('Placing dungeon items.') fill_dungeons_restrictive(window, worlds, search, fill_locations, dungeon_items, itempool + songitempool) search.collect_locations() # 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. # Placing songs on their own since they have a relatively high chance # of failing compared to other item type. So this way we only have retry # the song locations only. if not worlds[0].shuffle_song_items: logger.info('Placing song items.') fill_ownworld_restrictive(window, worlds, search, song_locations, songitempool, progitempool, "song") search.collect_locations() fill_locations += [ location for location in song_locations if location.item is None ] # Put one item in every dungeon, needs to be done before other items are # placed to ensure there is a spot available for them if worlds[0].one_item_per_dungeon: logger.info('Placing one major item per dungeon.') fill_dungeon_unique_item(window, worlds, search, fill_locations, progitempool) search.collect_locations() # Place all progression items. This will include keys in keysanity. # Items in this group will check for reachability and will be placed # such that the game is guaranteed beatable. logger.info('Placing progression items.') fill_restrictive(window, worlds, search, fill_locations, progitempool) search.collect_locations() # Place all priority items. # These items are items that only check if the item is allowed to be # placed in the location, not checking reachability. This is important # for things like Ice Traps that can't be found at some locations logger.info('Placing priority items.') fill_restrictive_fast(window, worlds, fill_locations, prioitempool) # Place the rest of the items. # No restrictions at all. Places them completely randomly. Since they # cannot affect the beatability, we don't need to check them logger.info('Placing the rest of the items.') fast_fill(window, fill_locations, restitempool) # Log unplaced item/location warnings for item in progitempool + prioitempool + restitempool: logger.error('Unplaced Items: %s [World %d]' % (item.name, item.world.id)) for location in fill_locations: logger.error('Unfilled Locations: %s [World %d]' % (location.name, location.world.id)) if progitempool + prioitempool + restitempool: raise FillError('Not all items are placed.') if fill_locations: raise FillError('Not all locations have an item.') if not search.can_beat_game(): raise FillError('Cannot beat game!') worlds[0].settings.distribution.cloak(worlds, [cloakable_locations], [all_models]) for world in worlds: for location in world.get_filled_locations(): # Get the maximum amount of wallets required to purchase an advancement item. if world.maximum_wallets < 3 and location.price and location.item.advancement: if location.price > 500: world.maximum_wallets = 3 elif world.maximum_wallets < 2 and location.price > 200: world.maximum_wallets = 2 elif world.maximum_wallets < 1 and location.price > 99: world.maximum_wallets = 1 # Get Light Arrow location for later usage. if location.item and location.item.name == 'Light Arrows': location.item.world.light_arrow_location = location
'Ice Trap', ] item_groups = { 'Junk': remove_junk_items, 'JunkSong': ('Prelude of Light', 'Serenade of Water'), 'AdultTrade': tradeitems, 'Bottle': normal_bottles, 'Spell': ('Dins Fire', 'Farores Wind', 'Nayrus Love'), 'Shield': ('Deku Shield', 'Hylian Shield'), 'Song': songlist, 'NonWarpSong': songlist[0:6], 'WarpSong': songlist[6:], 'HealthUpgrade': ('Heart Container', 'Piece of Heart'), 'ProgressItem': [name for (name, data) in item_table.items() if data[0] == 'Item' and data[1]], 'DungeonReward': dungeon_rewards, 'ForestFireWater': ('Forest Medallion', 'Fire Medallion', 'Water Medallion'), 'FireWater': ('Fire Medallion', 'Water Medallion'), } def get_junk_item(count=1, pool=None, plando_pool=None): if count < 1: raise ValueError("get_junk_item argument 'count' must be greater than 0.") return_pool = [] if pending_junk_pool: pending_count = min(len(pending_junk_pool), count) return_pool = [pending_junk_pool.pop() for _ in range(pending_count)]
remove_junk_items, 'AdultTrade': tradeitems, 'Bottle': normal_bottles, 'Spell': ('Dins Fire', 'Farores Wind', 'Nayrus Love'), 'Shield': ('Deku Shield', 'Hylian Shield'), 'Song': songlist, 'NonWarpSong': songlist[0:6], 'WarpSong': songlist[6:], 'HealthUpgrade': ('Heart Container', 'Piece of Heart'), 'ProgressItem': [ name for (name, data) in item_table.items() if data[0] == 'Item' and data[1] ], 'ForestFireWater': ('Forest Medallion', 'Fire Medallion', 'Water Medallion'), 'FireWater': ('Fire Medallion', 'Water Medallion'), } def get_junk_item(count=1): if count < 1: raise ValueError( "get_junk_item argument 'count' must be greater than 0.") return_pool = [] if pending_junk_pool: