def run_tests(self, access_pool): for location, access, *item_pool in access_pool: items = item_pool[0] all_except = item_pool[1] if len(item_pool) > 1 else None with self.subTest(location=location, access=access, items=items, all_except=all_except): if all_except and len(all_except) > 0: items = self.world.itempool[:] items = [ item for item in items if item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except) ] items.extend(ItemFactory(item_pool[0], 1)) else: items = ItemFactory(items, 1) state = CollectionState(self.world) for item in items: item.advancement = True state.collect(item) self.assertEqual( self.world.get_location(location, 1).can_reach(state), access)
def run_tests(self, access_pool): for exit in self.remove_exits: self.world.get_entrance(exit, 1).connected_region = self.world.get_region('Menu', 1) for location, access, *item_pool in access_pool: items = item_pool[0] all_except = item_pool[1] if len(item_pool) > 1 else None with self.subTest(location=location, access=access, items=items, all_except=all_except): if all_except and len(all_except) > 0: items = self.world.itempool[:] items = [item for item in items if item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)] items.extend(ItemFactory(item_pool[0], 1)) else: items = ItemFactory(items, 1) state = CollectionState(self.world) state.reachable_regions[1].add(self.world.get_region('Menu', 1)) for region_name in self.starting_regions: region = self.world.get_region(region_name, 1) state.reachable_regions[1].add(region) for exit in region.exits: if exit.connected_region is not None: state.blocked_connections[1].add(exit) for item in items: item.advancement = True state.collect(item) self.assertEqual(self.world.get_location(location, 1).can_reach(state), access)
def fill_restrictive(window, worlds, base_state_list, locations, itempool, count=-1): unplaced_items = [] # loop until there are no items or locations while itempool and locations: # if remaining count is 0, return. Negative means unbounded. if count == 0: break # get and item and remove it from the itempool item_to_place = itempool.pop() # generate the max states that include every remaining item # this will allow us to place this item in a reachable location maximum_exploration_state_list = CollectionState.get_states_with_items(base_state_list, itempool + unplaced_items) # perform_access_check checks location reachability perform_access_check = True if worlds[0].check_beatable_only: # if any world can not longer be beatable with the remaining items # then we must check for reachability no matter what. # This way the reachability test is monotonic. If we were to later # stop checking, then we could place an item needed in one world # in an unreachable place in another world perform_access_check = not CollectionState.can_beat_game(maximum_exploration_state_list) # find a location that the item can be places. It must be a valid location # in the world we are placing it (possibly checking for reachability) spot_to_fill = None for location in locations: if location.can_fill(maximum_exploration_state_list[location.world.id], item_to_place, perform_access_check): spot_to_fill = location break # if we failed to find a suitable location if spot_to_fill is None: # if we specify a count, then we only want to place a subset, so a miss might be ok if count > 0: # don't decrement count, we didn't place anything unplaced_items.append(item_to_place) continue else: # we expect all items to be placed raise FillError('Game unbeatable: No more spots to place %s [World %d]' % (item_to_place, item_to_place.world.id)) # Place the item in the world and continue spot_to_fill.world.push_item(spot_to_fill, item_to_place) locations.remove(spot_to_fill) window.fillcount += 1 window.update_progress(5 + ((window.fillcount / window.locationcount) * 30)) # decrement count count -= 1 # assert that the specified number of items were placed if count > 0: raise FillError('Could not place the specified number of item. %d remaining to be placed.' % count) # re-add unplaced items that were skipped itempool.extend(unplaced_items)
def fill_restrictive(worlds, base_state_list, locations, itempool): # loop until there are no items or locations while itempool and locations: # get and item and remove it from the itempool item_to_place = itempool.pop() # generate the max states that include every remaining item # this will allow us to place this item in a reachable location maximum_exploration_state_list = CollectionState.get_states_with_items( base_state_list, itempool) # perform_access_check checks location reachability perform_access_check = True if worlds[0].check_beatable_only: # if any world can not longer be beatable with the remaining items # then we must check for reachability no matter what. # This way the reachability test is monotonic. If we were to later # stop checking, then we could place an item needed in one world # in an unreachable place in another world perform_access_check = not CollectionState.can_beat_game( maximum_exploration_state_list) # find a location that the item can be places. It must be a valid location # in the world we are placing it (possibly checking for reachability) spot_to_fill = None for location in locations: if location.can_fill( maximum_exploration_state_list[location.world.id], item_to_place, perform_access_check): spot_to_fill = location break # if we failed to find a suitable location, then stop placing items if spot_to_fill is None: # Maybe the game can be beaten anyway? if not CollectionState.can_beat_game( maximum_exploration_state_list): raise FillError( 'Game unbeatable: No more spots to place %s [World %d]' % (item_to_place, item_to_place.world.id)) if not worlds[0].check_beatable_only: logging.getLogger('').warning( 'Not all items placed. Game beatable anyway.') break # Place the item in the world and continue spot_to_fill.world.push_item(spot_to_fill, item_to_place) locations.remove(spot_to_fill)
def fill_shops(window, worlds, locations, shoppool, itempool, attempts=15): # List of states with all items all_state_base_list = CollectionState.get_states_with_items( [world.state for world in worlds], itempool) while attempts: attempts -= 1 try: prizepool = list(shoppool) prize_locs = list(locations) random.shuffle(prizepool) random.shuffle(prize_locs) fill_restrictive(window, worlds, all_state_base_list, prize_locs, prizepool) logging.getLogger('').info("Shop items placed") except FillError as e: logging.getLogger('').info( "Failed to place shop items. Will retry %s more times", attempts) for location in locations: location.item = None logging.getLogger('').info('\t%s' % str(e)) continue break else: raise FillError('Unable to place shops')
def fill_songs(window, worlds, locations, songpool, itempool, attempts=15): # get the song locations for each world # look for preplaced items placed_prizes = [loc.item.name for loc in locations if loc.item is not None] unplaced_prizes = [song for song in songpool if song.name not in placed_prizes] empty_song_locations = [loc for loc in locations if loc.item is None] # List of states with all items all_state_base_list = CollectionState.get_states_with_items([world.state for world in worlds], itempool) while attempts: attempts -= 1 try: prizepool = list(unplaced_prizes) prize_locs = list(empty_song_locations) random.shuffle(prizepool) random.shuffle(prize_locs) fill_restrictive(window, worlds, all_state_base_list, prize_locs, prizepool) logging.getLogger('').info("Songs placed") except FillError as e: logging.getLogger('').info("Failed to place songs. Will retry %s more times", attempts) for location in empty_song_locations: location.item = None logging.getLogger('').info('\t%s' % str(e)) continue break else: raise FillError('Unable to place songs')
def sweep_from_pool( base_state: CollectionState, itempool: typing.Sequence[Item] = tuple() ) -> CollectionState: new_state = base_state.copy() for item in itempool: new_state.collect(item, True) new_state.sweep_for_events() return new_state
def fill_dungeon_unique_item(window, worlds, fill_locations, itempool): # We should make sure that we don't count event items, shop items, # token items, or dungeon items as a major item. itempool at this # point should only be able to have tokens of those restrictions # 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] 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 if dungeon.name == 'Spirit Temple': double_dungeons.append(dungeon) dungeons.extend(double_dungeons) random.shuffle(dungeons) random.shuffle(itempool) all_other_item_state = CollectionState.get_states_with_items([world.state for world in worlds], minor_items) all_dungeon_locations = [] # iterate of all the dungeons in a random order, placing the item there for dungeon in dungeons: dungeon_locations = [location for region in dungeon.regions for location in region.locations if location in fill_locations] if dungeon.name == 'Spirit Temple': # spirit temple is weird and includes a couple locations outside of the dungeon dungeon_locations.extend(filter(lambda location: location in fill_locations, [dungeon.world.get_location(location) for location in ['Mirror Shield Chest', 'Silver Gauntlets Chest']])) # cache this list to flag afterwards all_dungeon_locations.extend(dungeon_locations) # place 1 item into the dungeon random.shuffle(dungeon_locations) fill_restrictive(window, worlds, all_other_item_state, dungeon_locations, major_items, 1) # update the location and item pool, removing any placed items and filled locations # the fact that you can remove items from a list you're iterating over is python magic for item in itempool: if item.location != None: fill_locations.remove(item.location) itempool.remove(item) # flag locations to not place further major items. it's important we do it on the # locations instead of the dungeon because some locations are not in the dungeon for location in all_dungeon_locations: location.minor_only = True logging.getLogger('').info("Unique dungeon items placed")
def get_state(self, items): if (self.world, tuple(items)) in self._state_cache: return self._state_cache[self.world, tuple(items)] state = CollectionState(self.world) for item in items: item.classification = ItemClassification.progression state.collect(item) state.sweep_for_events() self._state_cache[self.world, tuple(items)] = state return state
def get_state(self, items): if (self.world, tuple(items)) in self._state_cache: return self._state_cache[self.world, tuple(items)] state = CollectionState(self.world) for item in items: item.advancement = True state.collect(item) state.sweep_for_events() self._state_cache[self.world, tuple(items)] = state return state
def testEmptyStateCanReachSomething(self): for game_name, world_type in AutoWorldRegister.world_types.items(): # Final Fantasy logic is controlled by finalfantasyrandomizer.com if game_name != "Archipelago" and game_name != "Final Fantasy": with self.subTest("Game", game=game_name): world = setup_default_world(world_type) state = CollectionState(world) locations = set() for location in world.get_locations(): if location.can_reach(state): locations.add(location) self.assertGreater( len(locations), 0, msg= "Need to be able to reach at least one location to get started." )
def fill_dungeons_restrictive(window, worlds, shuffled_locations, dungeon_items, itempool): # List of states with all non-key items all_state_base_list = CollectionState.get_states_with_items([world.state for world in worlds], itempool) # shuffle this list to avoid placement bias random.shuffle(dungeon_items) # sort in the order Boss Key, Small Key, Other before placing dungeon items # python sort is stable, so the ordering is still random within groups sort_order = {"BossKey": 3, "SmallKey": 2} dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) # place dungeon items fill_restrictive(window, worlds, all_state_base_list, shuffled_locations, dungeon_items) for world in worlds: world.state.clear_cached_unreachable()
def create_playthrough(world): # create a copy as we will modify it world = copy_world(world) # get locations containing progress items prog_locations = [location for location in world.get_locations() if location.item is not None and location.item.advancement] collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) while sphere_candidates: sphere = [] # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location): sphere.append(location) for location in sphere: sphere_candidates.remove(location) state.collect(location.item) collection_spheres.append(sphere) # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for sphere in reversed(collection_spheres): to_delete = [] for location in sphere: # we remove the item at location and check if game is still beatable old_item = location.item location.item = None state.remove(old_item) world._item_cache = {} # need to invalidate if world.can_beat_game(): to_delete.append(location) else: # still required, got to keep it around location.item = old_item # cull entries in spheres for spoiler walkthrough at end for location in to_delete: sphere.remove(location) # we are now down to just the required progress items in collection_spheres in a minimum number of spheres. As a cleanup, we right trim empty spheres (can happen if we have multiple triforces) collection_spheres = [sphere for sphere in collection_spheres if sphere] # we can finally output our playthrough return 'Playthrough:\n' + ''.join(['%s: {\n%s}\n' % (i + 1, ''.join([' %s: %s\n' % (location, location.item) for location in sphere])) for i, sphere in enumerate(collection_spheres)]) + '\n'
def set_shop_rules(ootworld): found_bombchus = ootworld.parser.parse_rule('found_bombchus') wallet = ootworld.parser.parse_rule('Progressive_Wallet') wallet2 = ootworld.parser.parse_rule('(Progressive_Wallet, 2)') for location in filter( lambda location: location.item and location.item.type == 'Shop', ootworld.get_locations()): # Add wallet requirements if location.item.name in [ 'Buy Arrows (50)', 'Buy Fish', 'Buy Goron Tunic', 'Buy Bombchu (20)', 'Buy Bombs (30)' ]: add_rule(location, wallet) elif location.item.name in ['Buy Zora Tunic', 'Buy Blue Fire']: add_rule(location, wallet2) # Add adult only checks if location.item.name in ['Buy Goron Tunic', 'Buy Zora Tunic']: add_rule(location, ootworld.parser.parse_rule('is_adult', location)) # Add item prerequisite checks if location.item.name in [ 'Buy Blue Fire', 'Buy Blue Potion', 'Buy Bottle Bug', 'Buy Fish', 'Buy Green Potion', 'Buy Poe', 'Buy Red Potion [30]', 'Buy Red Potion [40]', 'Buy Red Potion [50]', 'Buy Fairy\'s Spirit' ]: add_rule( location, lambda state: CollectionState._oot_has_bottle( state, ootworld.player)) if location.item.name in [ 'Buy Bombchu (10)', 'Buy Bombchu (20)', 'Buy Bombchu (5)' ]: add_rule(location, found_bombchus)
def balance_multiworld_progression(world): state = CollectionState(world) checked_locations = [] unchecked_locations = world.get_locations().copy() random.shuffle(unchecked_locations) reachable_locations_count = {} for player in range(1, world.players + 1): reachable_locations_count[player] = 0 def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) return [loc for loc in locations if sphere_state.can_reach(loc)] while True: sphere_locations = get_sphere_locations(state, unchecked_locations) for location in sphere_locations: unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 if checked_locations: threshold = max(reachable_locations_count.values()) - 20 balancing_players = [ player for player, reachables in reachable_locations_count.items() if reachables < threshold ] if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() candidate_items = [] while True: for location in balancing_sphere: if location.event and ( world.keyshuffle[location.item.player] or not location.item.smallkey) and ( world.bigkeyshuffle[location.item.player] or not location.item.bigkey): balancing_state.collect(location.item, True, location) if location.item.player in balancing_players and not location.locked: candidate_items.append(location) balancing_sphere = get_sphere_locations( balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all([ reachables >= threshold for reachables in balancing_reachables.values() ]): break elif not balancing_sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) unlocked_locations = [ l for l in unchecked_locations if l not in balancing_unchecked_locations ] items_to_replace = [] for player in balancing_players: locations_to_test = [ l for l in unlocked_locations if l.player == player ] # only replace items that end up in another player's world items_to_test = [ l for l in candidate_items if l.item.player == player and l.player != player ] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() for location in [ *[ l for l in items_to_replace if l.item.player == player ], *items_to_test ]: reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events( locations=locations_to_test) if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations( reducing_state, locations_to_test) if reachable_locations_count[player] + len( reduced_sphere) < threshold: items_to_replace.append(testing) replaced_items = False replacement_locations = [ l for l in checked_locations if not l.event and not l.locked ] while replacement_locations and items_to_replace: new_location = replacement_locations.pop() old_location = items_to_replace.pop() while not new_location.can_fill( state, old_location.item, False) or (new_location.item and not old_location.can_fill( state, new_location.item, False)): replacement_locations.insert(0, new_location) new_location = replacement_locations.pop() new_location.item, old_location.item = old_location.item, new_location.item new_location.event, old_location.event = True, False state.collect(new_location.item, True, new_location) replaced_items = True if replaced_items: for location in get_sphere_locations( state, [ l for l in unlocked_locations if l.player in balancing_players ]): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 sphere_locations.append(location) for location in sphere_locations: if location.event and ( world.keyshuffle[location.item.player] or not location.item.smallkey) and ( world.bigkeyshuffle[location.item.player] or not location.item.bigkey): state.collect(location.item, True, location) checked_locations.extend(sphere_locations) if world.has_beaten_game(state): break elif not sphere_locations: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' )
def create_playthrough(world): # create a copy as we will modify it old_world = world world = copy_world(world) # get locations containing progress items prog_locations = [ location for location in world.get_filled_locations() if location.item.advancement ] state_cache = [None] collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) logging.debug('Building up collection spheres.') while sphere_candidates: state.sweep_for_events(key_only=True) sphere = set() # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location): sphere.add(location) for location in sphere: sphere_candidates.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) state_cache.append(state.copy()) logging.debug( 'Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations)) if not sphere: logging.debug('The following items could not be reached: %s', [ '%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates ]) if any([ world.accessibility[location.item.player] != 'none' for location in sphere_candidates ]): raise RuntimeError( f'Not all progression items reachable ({sphere_candidates}). ' f'Something went terribly wrong here.') else: old_world.spoiler.unreachables = sphere_candidates.copy() break # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(tuple(enumerate(collection_spheres))): to_delete = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug( 'Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item location.item = None if world.can_beat_game(state_cache[num]): to_delete.add(location) else: # still required, got to keep it around location.item = old_item # cull entries in spheres for spoiler walkthrough at end sphere -= to_delete # second phase, sphere 0 for item in (i for i in world.precollected_items if i.advancement): logging.getLogger('').debug( 'Checking if %s (Player %d) is required to beat the game.', item.name, item.player) world.precollected_items.remove(item) world.state.remove(item) if not world.can_beat_game(): world.push_precollected(item) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others # in the same or later sphere (because the location had 2 ways to access but the item originally # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres required_locations = { item for sphere in collection_spheres for item in sphere } state = CollectionState(world) collection_spheres = [] while required_locations: state.sweep_for_events(key_only=True) sphere = set(filter(state.can_reach, required_locations)) for location in sphere: required_locations.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) logging.getLogger('').debug( 'Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) if not sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) def flist_to_iter(node): while node: value, node = node yield value def get_path(state, region): reversed_path_as_flist = state.path.get(region, (region, None)) string_path_flat = reversed( list(map(str, flist_to_iter(reversed_path_as_flist)))) # Now we combine the flat string list into (region, exit) pairs pathsiter = iter(string_path_flat) pathpairs = zip_longest(pathsiter, pathsiter) return list(pathpairs) old_world.spoiler.paths = dict() for player in range(1, world.players + 1): old_world.spoiler.paths.update({ str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player }) for path in dict(old_world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): if world.mode[player] != 'inverted': old_world.spoiler.paths[str( world.get_region('Big Bomb Shop', player))] = get_path( state, world.get_region('Big Bomb Shop', player)) else: old_world.spoiler.paths[str( world.get_region('Inverted Big Bomb Shop', player))] = get_path( state, world.get_region( 'Inverted Big Bomb Shop', player)) # we can finally output our playthrough old_world.spoiler.playthrough = { "0": sorted([ str(item) for item in world.precollected_items if item.advancement ]) } for i, sphere in enumerate(collection_spheres): old_world.spoiler.playthrough[str(i + 1)] = { str(location): str(location.item) for location in sorted(sphere) }
def balance_multiworld_progression(world): state = CollectionState(world) checked_locations = [] unchecked_locations = world.get_locations().copy() random.shuffle(unchecked_locations) reachable_locations_count = {} for player in range(1, world.players + 1): reachable_locations_count[player] = 0 def get_sphere_locations(sphere_state, locations): if not world.keysanity: sphere_state.sweep_for_events(key_only=True, locations=locations) return [loc for loc in locations if sphere_state.can_reach(loc)] while True: sphere_locations = get_sphere_locations(state, unchecked_locations) for location in sphere_locations: unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 if checked_locations: average_reachable_locations = sum( reachable_locations_count.values()) / world.players threshold = ((average_reachable_locations + max(reachable_locations_count.values())) / 2) * 0.8 #todo: probably needs some tweaking balancing_players = [ player for player, reachables in reachable_locations_count.items() if reachables < threshold ] if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() candidate_items = [] while True: for location in balancing_sphere: if location.event: balancing_state.collect(location.item, True, location) if location.item.player in balancing_players: candidate_items.append(location) balancing_sphere = get_sphere_locations( balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all([ reachables >= threshold for reachables in balancing_reachables.values() ]): break unlocked_locations = [ l for l in unchecked_locations if l not in balancing_unchecked_locations ] items_to_replace = [] for player in balancing_players: locations_to_test = [ l for l in unlocked_locations if l.player == player ] items_to_test = [ l for l in candidate_items if l.item.player == player and l.player != player ] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() for location in [ *[ l for l in items_to_replace if l.item.player == player ], *items_to_test ]: reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events( locations=locations_to_test) if testing.locked: continue if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations( reducing_state, locations_to_test) if reachable_locations_count[player] + len( reduced_sphere) < threshold: items_to_replace.append(testing) replaced_items = False locations_for_replacing = [ l for l in checked_locations if not l.event and not l.locked ] while locations_for_replacing and items_to_replace: new_location = locations_for_replacing.pop() old_location = items_to_replace.pop() new_location.item, old_location.item = old_location.item, new_location.item new_location.event = True old_location.event = False state.collect(new_location.item, True, new_location) replaced_items = True if replaced_items: for location in get_sphere_locations( state, [ l for l in unlocked_locations if l.player in balancing_players ]): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 sphere_locations.append(location) for location in sphere_locations: if location.event: state.collect(location.item, True, location) checked_locations.extend(sphere_locations) if world.has_beaten_game(state): break
def main(settings): start = time.clock() # initialize the world worlds = [] if not settings.world_count: settings.world_count = 1 if settings.world_count < 1: raise Exception('World Count must be at least 1') if settings.player_num > settings.world_count or settings.player_num < 1: raise Exception('Player Num must be between 1 and %d' % settings.world_count) for i in range(0, settings.world_count): worlds.append(World(settings)) logger = logging.getLogger('') random.seed(worlds[0].numeric_seed) logger.info('OoT Randomizer Version %s - Seed: %s\n\n', __version__, worlds[0].seed) for id, world in enumerate(worlds): world.id = id logger.info('Generating World %d.' % id) logger.info('Creating Overworld') create_regions(world) logger.info('Creating Dungeons') create_dungeons(world) logger.info('Linking Entrances') link_entrances(world) logger.info('Calculating Access Rules.') set_rules(world) logger.info('Generating Item Pool.') generate_itempool(world) logger.info('Fill the world.') distribute_items_restrictive(worlds) if settings.create_spoiler: logger.info('Calculating playthrough.') create_playthrough(worlds) CollectionState.update_required_items(worlds) logger.info('Patching ROM.') if settings.world_count > 1: outfilebase = 'OoT_%s_%s_W%dP%d' % ( worlds[0].settings_string, worlds[0].seed, worlds[0].world_count, worlds[0].player_num) else: outfilebase = 'OoT_%s_%s' % (worlds[0].settings_string, worlds[0].seed) output_dir = default_output_path(settings.output_dir) if not settings.suppress_rom: rom = LocalRom(settings) patch_rom(worlds[settings.player_num - 1], rom) rom_path = os.path.join(output_dir, '%s.z64' % outfilebase) rom.write_to_file(rom_path) if settings.compress_rom: logger.info('Compressing ROM.') if platform.system() == 'Windows': subprocess.call([ "Compress\\Compress.exe", rom_path, os.path.join(output_dir, '%s-comp.z64' % outfilebase) ]) elif platform.system() == 'Linux': subprocess.call([ "Compress/Compress", rom_path, os.path.join(output_dir, '%s-comp.z64' % outfilebase) ]) elif platform.system() == 'Darwin': subprocess.call( ["Compress/Compress.out", ('%s.z64' % outfilebase)]) else: logger.info('OS not supported for compression') if settings.create_spoiler: worlds[settings.player_num - 1].spoiler.to_file( os.path.join(output_dir, '%s_Spoiler.txt' % outfilebase)) os.remove('hints.txt') logger.info('Done. Enjoy.') logger.debug('Total Time: %s', time.clock() - start) return worlds[settings.player_num - 1]
def get_sphere_locations( sphere_state: CollectionState, locations: typing.Set[Location]) -> typing.Set[Location]: sphere_state.sweep_for_events(key_only=True, locations=locations) return {loc for loc in locations if sphere_state.can_reach(loc)}
def fill_dungeon_unique_item(window, worlds, fill_locations, itempool, attempts=15): # We should make sure that we don't count event items, shop items, # token items, or dungeon items as a major item. itempool at this # point should only be able to have tokens of those restrictions # since the rest are already placed. major_items = [item for item in itempool if item.type != 'Token'] token_items = [item for item in itempool if item.type == 'Token'] while attempts: attempts -= 1 try: # choose a random set of items and locations dungeon_locations = [] for dungeon in [ dungeon for world in worlds for dungeon in world.dungeons ]: dungeon_locations.append( random.choice([ location for region in dungeon.regions for location in region.locations if location in fill_locations ])) dungeon_items = random.sample(major_items, len(dungeon_locations)) new_dungeon_locations = list(dungeon_locations) new_dungeon_items = list(dungeon_items) non_dungeon_items = [ item for item in major_items if item not in dungeon_items ] all_other_item_state = CollectionState.get_states_with_items( [world.state for world in worlds], token_items + non_dungeon_items) # attempt to place the items into the locations random.shuffle(new_dungeon_locations) random.shuffle(new_dungeon_items) fill_restrictive(window, worlds, all_other_item_state, new_dungeon_locations, new_dungeon_items) if len(new_dungeon_locations) > 0: raise FillError('Not all items were placed successfully') logging.getLogger('').info("Unique dungeon items placed") # remove the placed items from the fill_location and itempool for location in dungeon_locations: fill_locations.remove(location) for item in dungeon_items: itempool.remove(item) except FillError as e: logging.getLogger('').info( "Failed to place unique dungeon items. Will retry %s more times", attempts) for location in dungeon_locations: location.item = None for dungeon in [ dungeon for world in worlds for dungeon in world.dungeons ]: dungeon.major_items = 0 logging.getLogger('').info('\t%s' % str(e)) continue break else: raise FillError('Unable to place unique dungeon items')
def create_playthrough(world): # create a copy as we will modify it old_world = world world = copy_world(world) # in treasure hunt and pedestal goals, ganon is invincible if world.goal in ['pedestal', 'starhunt', 'triforcehunt']: world.get_location('Ganon').item = None # if we only check for beatable, we can do this sanity check first before writing down spheres if world.check_beatable_only and not world.can_beat_game(): raise RuntimeError( 'Cannot beat game. Something went terribly wrong here!') # get locations containing progress items prog_locations = [ location for location in world.get_locations() if location.item is not None and location.item.advancement ] collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) logging.getLogger('').debug('Building up collection spheres.') while sphere_candidates: state.sweep_for_events(key_only=True) sphere = [] # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location): sphere.append(location) for location in sphere: sphere_candidates.remove(location) state.collect(location.item, True) collection_spheres.append(sphere) logging.getLogger('').debug( 'Calculated sphere %i, containing %i of %i progress items.' % (len(collection_spheres), len(sphere), len(prog_locations))) if not sphere: logging.getLogger('').debug( 'The following items could not be reached: %s' % [ '%s at %s' % (location.item.name, location.name) for location in sphere_candidates ]) if not world.check_beatable_only: raise RuntimeError( 'Not all progression items reachable. Something went terribly wrong here.' ) else: break # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for sphere in reversed(collection_spheres): to_delete = [] for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug( 'Checking if %s is required to beat the game.' % location.item.name) old_item = location.item location.item = None state.remove(old_item) world._item_cache = {} # need to invalidate if world.can_beat_game(): to_delete.append(location) else: # still required, got to keep it around location.item = old_item # cull entries in spheres for spoiler walkthrough at end for location in to_delete: sphere.remove(location) # we are now down to just the required progress items in collection_spheres in a minimum number of spheres. As a cleanup, we right trim empty spheres (can happen if we have multiple triforces) collection_spheres = [sphere for sphere in collection_spheres if sphere] # store the required locations for statistical analysis old_world.required_locations = [ location.name for sphere in collection_spheres for location in sphere ] # we can finally output our playthrough return 'Playthrough:\n' + ''.join([ '%s: {\n%s}\n' % (i + 1, ''.join( [' %s: %s\n' % (location, location.item) for location in sphere])) for i, sphere in enumerate(collection_spheres) ]) + '\n'
def fill_dungeons(world): ES = (['Hyrule Castle'], None, [ESSmallKey()], [ESMap()]) EP = (['Eastern Palace'], EPBigKey(), [], [EPMap(), EPCompass()]) DP = (['Desert Palace Main', 'Desert Palace East', 'Desert Palace North'], DPBigKey(), [DPSmallKey()], [DPCompass(), DPMap()]) ToH = ([ 'Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)' ], THBigKey(), [THSmallKey()], [THCompass(), THMap()]) AT = (['Agahnims Tower', 'Agahnim 1'], None, [ATSmallKey(), ATSmallKey()], []) PoD = ([ 'Dark Palace (Entrance)', 'Dark Palace (Center)', 'Dark Palace (Big Key Chest)', 'Dark Palace (Bonk Section)', 'Dark Palace (North)', 'Dark Palace (Maze)', 'Dark Palace (Spike Statue Room)', 'Dark Palace (Final Section)' ], PDBigKey(), [ PDSmallKey(), PDSmallKey(), PDSmallKey(), PDSmallKey(), PDSmallKey(), PDSmallKey() ], [PDCompass(), PDMap()]) TT = (['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], TTBigKey(), [TTSmallKey()], [TTCompass(), TTMap()]) SW = ([ 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Final Section (Entrance)', 'Skull Woods Final Section (Mothula)' ], SWBigKey(), [SWSmallKey(), SWSmallKey()], [SWCompass(), SWMap()]) SP = ([ 'Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)' ], SPBigKey(), [SPSmallKey()], [SPMap(), SPCompass()]) IP = ([ 'Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)' ], IPBigKey(), [IPSmallKey(), IPSmallKey()], [IPMap(), IPCompass()]) MM = ([ 'Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)' ], MMBigKey(), [MMSmallKey(), MMSmallKey(), MMSmallKey()], [MMCompass(), MMMap()]) TR = ([ 'Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Roller Switch Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)' ], TRBigKey(), [TRSmallKey(), TRSmallKey(), TRSmallKey(), TRSmallKey()], [TRMap(), TRCompass()]) GT = ([ 'Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2' ], GTBigKey(), [GTSmallKey(), GTSmallKey(), GTSmallKey(), GTSmallKey()], [GTMap(), GTCompass()]) freebes = [ '[dungeon-A2-1F] Ganons Tower - Map Room', '[dungeon-D1-1F] Dark Palace - Spike Statue Room', '[dungeon-D1-1F] Dark Palace - Big Key Room' ] # this key is in a fixed location (for now) world.push_item( world.get_location( '[dungeon - D3 - B1] Skull Woods - South of Big Chest'), SWSmallKey(), False) for dungeon_regions, big_key, small_keys, dungeon_items in [ TR, ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, GT ]: # this is what we need to fill dungeon_locations = [ location for location in world.get_unfilled_locations() if location.parent_region.name in dungeon_regions ] random.shuffle(dungeon_locations) all_state = CollectionState(world, True) # first place big key if big_key is not None: bk_location = None for location in dungeon_locations: if location.item_rule(big_key): bk_location = location break if bk_location is None: raise RuntimeError('No suitable location for %s' % big_key) world.push_item(bk_location, big_key, False) dungeon_locations.remove(bk_location) all_state._clear_cache() # next place small keys for small_key in small_keys: sk_location = None for location in dungeon_locations: if location.name in freebes or location.can_reach(all_state): sk_location = location break if sk_location is None: raise RuntimeError('No suitable location for %s' % small_key) world.push_item(sk_location, small_key, False) dungeon_locations.remove(sk_location) all_state._clear_cache() # next place dungeon items if world.place_dungeon_items: for dungeon_item in dungeon_items: di_location = dungeon_locations.pop() world.push_item(di_location, dungeon_item, False) world.state._clear_cache()
def main(settings, window=dummy_window()): start = time.clock() logger = logging.getLogger('') # verify that the settings are valid if settings.free_scarecrow: verify_scarecrow_song_str(settings.scarecrow_song, settings.ocarina_songs) # initialize the world worlds = [] if settings.compress_rom == 'None': settings.create_spoiler = True settings.update() if not settings.world_count: settings.world_count = 1 if settings.world_count < 1: raise Exception('World Count must be at least 1') if settings.player_num > settings.world_count or settings.player_num < 1: raise Exception('Player Num must be between 1 and %d' % settings.world_count) for i in range(0, settings.world_count): worlds.append(World(settings)) random.seed(worlds[0].numeric_seed) logger.info('OoT Randomizer Version %s - Seed: %s\n\n', __version__, worlds[0].seed) window.update_status('Creating the Worlds') for id, world in enumerate(worlds): world.id = id logger.info('Generating World %d.' % id) window.update_progress(0 + (((id + 1) / settings.world_count) * 1)) logger.info('Creating Overworld') if world.quest == 'master': for dungeon in world.dungeon_mq: world.dungeon_mq[dungeon] = True elif world.quest == 'mixed': for dungeon in world.dungeon_mq: world.dungeon_mq[dungeon] = random.choice([True, False]) else: for dungeon in world.dungeon_mq: world.dungeon_mq[dungeon] = False create_regions(world) window.update_progress(0 + (((id + 1) / settings.world_count) * 2)) logger.info('Creating Dungeons') create_dungeons(world) window.update_progress(0 + (((id + 1) / settings.world_count) * 3)) logger.info('Linking Entrances') link_entrances(world) if settings.shopsanity != 'off': world.random_shop_prices() window.update_progress(0 + (((id + 1) / settings.world_count) * 4)) logger.info('Calculating Access Rules.') set_rules(world) window.update_progress(0 + (((id + 1) / settings.world_count) * 5)) logger.info('Generating Item Pool.') generate_itempool(world) window.update_status('Placing the Items') logger.info('Fill the world.') distribute_items_restrictive(window, worlds) window.update_progress(35) if settings.create_spoiler: window.update_status('Calculating Spoiler Data') logger.info('Calculating playthrough.') create_playthrough(worlds) window.update_progress(50) if settings.hints != 'none': window.update_status('Calculating Hint Data') CollectionState.update_required_items(worlds) buildGossipHints(worlds[settings.player_num - 1]) window.update_progress(55) logger.info('Patching ROM.') if settings.world_count > 1: outfilebase = 'OoT_%s_%s_W%dP%d' % ( worlds[0].settings_string, worlds[0].seed, worlds[0].world_count, worlds[0].player_num) else: outfilebase = 'OoT_%s_%s' % (worlds[0].settings_string, worlds[0].seed) output_dir = default_output_path(settings.output_dir) if settings.compress_rom != 'None': window.update_status('Patching ROM') rom = LocalRom(settings) patch_rom(worlds[settings.player_num - 1], rom) window.update_progress(65) rom_path = os.path.join(output_dir, '%s.z64' % outfilebase) window.update_status('Saving Uncompressed ROM') rom.write_to_file(rom_path) if settings.compress_rom == 'True': window.update_status('Compressing ROM') logger.info('Compressing ROM.') compressor_path = "" if platform.system() == 'Windows': if 8 * struct.calcsize("P") == 64: compressor_path = "Compress\\Compress.exe" else: compressor_path = "Compress\\Compress32.exe" elif platform.system() == 'Linux': compressor_path = "Compress/Compress" elif platform.system() == 'Darwin': compressor_path = "Compress/Compress.out" else: logger.info('OS not supported for compression') run_process(window, logger, [ compressor_path, rom_path, os.path.join(output_dir, '%s-comp.z64' % outfilebase) ]) os.remove(rom_path) window.update_progress(95) if settings.create_spoiler: window.update_status('Creating Spoiler Log') worlds[settings.player_num - 1].spoiler.to_file( os.path.join(output_dir, '%s_Spoiler.txt' % outfilebase)) window.update_progress(100) window.update_status('Success: Rom patched successfully') logger.info('Done. Enjoy.') logger.debug('Total Time: %s', time.clock() - start) return worlds[settings.player_num - 1]
def create_playthrough(world): # create a copy as we will modify it old_world = world world = copy_world(world) # if we only check for beatable, we can do this sanity check first before writing down spheres if world.check_beatable_only and not world.can_beat_game(): raise RuntimeError( 'Cannot beat game. Something went terribly wrong here!') # get locations containing progress items prog_locations = [ location for location in world.get_filled_locations() if location.item.advancement ] state_cache = [None] collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) logging.getLogger('').debug('Building up collection spheres.') while sphere_candidates: state.sweep_for_events(key_only=True) sphere = [] # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location): sphere.append(location) for location in sphere: sphere_candidates.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) state_cache.append(state.copy()) logging.getLogger('').debug( 'Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations)) if not sphere: logging.getLogger('').debug( 'The following items could not be reached: %s', [ '%s at %s' % (location.item.name, location.name) for location in sphere_candidates ]) if not world.check_beatable_only: raise RuntimeError( 'Not all progression items reachable. Something went terribly wrong here.' ) else: break # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(list(enumerate(collection_spheres))): to_delete = [] for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug( 'Checking if %s is required to beat the game.', location.item.name) old_item = location.item location.item = None state.remove(old_item) if world.can_beat_game(state_cache[num]): to_delete.append(location) else: # still required, got to keep it around location.item = old_item # cull entries in spheres for spoiler walkthrough at end for location in to_delete: sphere.remove(location) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others # in the same or later sphere (because the location had 2 ways to access but the item originally # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres required_locations = [ item for sphere in collection_spheres for item in sphere ] state = CollectionState(world) collection_spheres = [] while required_locations: state.sweep_for_events(key_only=True) sphere = list(filter(state.can_reach, required_locations)) for location in sphere: required_locations.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) logging.getLogger('').debug( 'Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) if not sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) # store the required locations for statistical analysis old_world.required_locations = [ location.name for sphere in collection_spheres for location in sphere ] def flist_to_iter(node): while node: value, node = node yield value def get_path(state, region): reversed_path_as_flist = state.path.get(region, (region, None)) string_path_flat = reversed( list(map(str, flist_to_iter(reversed_path_as_flist)))) # Now we combine the flat string list into (region, exit) pairs pathsiter = iter(string_path_flat) pathpairs = zip_longest(pathsiter, pathsiter) return list(pathpairs) old_world.spoiler.paths = { location.name: get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere } # we can finally output our playthrough old_world.spoiler.playthrough = OrderedDict([ (str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres) ])
def create_playthrough(worlds): if worlds[0].check_beatable_only and not CollectionState.can_beat_game( [world.state for world in worlds]): raise RuntimeError('Uncopied is broken too.') # create a copy as we will modify it old_worlds = worlds worlds = [world.copy() for world in worlds] # if we only check for beatable, we can do this sanity check first before writing down spheres if worlds[0].check_beatable_only and not CollectionState.can_beat_game( [world.state for world in worlds]): raise RuntimeError( 'Cannot beat game. Something went terribly wrong here!') state_list = [CollectionState(world) for world in worlds] # Get all item locations in the worlds collection_spheres = [] item_locations = [ location for state in state_list for location in state.world.get_filled_locations() if location.item.advancement ] # in the first phase, we create the generous spheres. Collecting every item in a sphere will # mean that every item in the next sphere is collectable. Will contain every reachable item logging.getLogger('').debug('Building up collection spheres.') # will loop if there is more items opened up in the previous iteration. Always run once reachable_items_locations = True while reachable_items_locations: # get reachable new items locations reachable_items_locations = [ location for location in item_locations if location.name not in state_list[ location.world.id].collected_locations and state_list[location.world.id].can_reach(location) ] for location in reachable_items_locations: # Mark the location collected in the state world it exists in state_list[location.world.id].collected_locations.append( location.name) # Collect the item for the state world it is for state_list[location.item.world.id].collect(location.item) if reachable_items_locations: collection_spheres.append(reachable_items_locations) # in the second phase, we cull each sphere such that the game is still beatable, reducing each # range of influence to the bare minimum required inside it. Effectively creates a min play for num, sphere in reversed(list(enumerate(collection_spheres))): to_delete = [] for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug( 'Checking if %s is required to beat the game.', location.item.name) old_item = location.item old_state_list = [state.copy() for state in state_list] location.item = None state_list[old_item.world.id].remove(old_item) CollectionState.remove_locations(state_list) if CollectionState.can_beat_game(state_list, False): to_delete.append(location) else: # still required, got to keep it around state_list = old_state_list location.item = old_item # cull entries in spheres for spoiler walkthrough at end for location in to_delete: sphere.remove(location) collection_spheres = [sphere for sphere in collection_spheres if sphere] # we can finally output our playthrough for world in old_worlds: world.spoiler.playthrough = OrderedDict([ (str(i + 1), {location: location.item for location in sphere}) for i, sphere in enumerate(collection_spheres) ])
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None): if not baked_server_options: baked_server_options = get_options()["server_options"] if args.outputpath: os.makedirs(args.outputpath, exist_ok=True) output_path.cached_path = args.outputpath start = time.perf_counter() # initialize the world world = MultiWorld(args.multi) logger = logging.getLogger() world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed)) world.shuffle = args.shuffle.copy() world.logic = args.logic.copy() world.mode = args.mode.copy() world.difficulty = args.difficulty.copy() world.item_functionality = args.item_functionality.copy() world.timer = args.timer.copy() world.goal = args.goal.copy() world.open_pyramid = args.open_pyramid.copy() world.boss_shuffle = args.shufflebosses.copy() world.enemy_health = args.enemy_health.copy() world.enemy_damage = args.enemy_damage.copy() world.beemizer_total_chance = args.beemizer_total_chance.copy() world.beemizer_trap_chance = args.beemizer_trap_chance.copy() world.timer = args.timer.copy() world.countdown_start_time = args.countdown_start_time.copy() world.red_clock_time = args.red_clock_time.copy() world.blue_clock_time = args.blue_clock_time.copy() world.green_clock_time = args.green_clock_time.copy() world.dungeon_counters = args.dungeon_counters.copy() world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy() world.shop_shuffle = args.shop_shuffle.copy() world.shuffle_prizes = args.shuffle_prizes.copy() world.sprite_pool = args.sprite_pool.copy() world.dark_room_logic = args.dark_room_logic.copy() world.plando_items = args.plando_items.copy() world.plando_texts = args.plando_texts.copy() world.plando_connections = args.plando_connections.copy() world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() world.player_name = args.name.copy() world.enemizer = args.enemizercli world.sprite = args.sprite.copy() world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.set_options(args) world.set_item_links() world.state = CollectionState(world) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) logger.info("Found World Types:") longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) numlength = 8 for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): if not cls.hidden: logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} " f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - " f"{max(cls.item_id_to_name):{numlength}}) | " f"{len(cls.location_names):3} " f"Locations (IDs: {min(cls.location_id_to_name):{numlength}} - " f"{max(cls.location_id_to_name):{numlength}})") AutoWorld.call_stage(world, "assert_generate") AutoWorld.call_all(world, "generate_early") logger.info('') for player in world.player_ids: for item_name, count in world.start_inventory[player].value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) for player in world.player_ids: if player in world.get_game_players("A Link to the Past"): # enforce pre-defined local items. if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: world.local_items[player].value.add('Triforce Piece') # Not possible to place pendants/crystals out side of boss prizes yet. world.non_local_items[player].value -= item_name_groups['Pendants'] world.non_local_items[player].value -= item_name_groups['Crystals'] # items can't be both local and non-local, prefer local world.non_local_items[player].value -= world.local_items[player].value logger.info('Creating World.') AutoWorld.call_all(world, "create_regions") logger.info('Creating Items.') AutoWorld.call_all(world, "create_items") logger.info('Calculating Access Rules.') if world.players > 1: for player in world.player_ids: locality_rules(world, player) group_locality_rules(world) else: world.non_local_items[1].value = set() world.local_items[1].value = set() AutoWorld.call_all(world, "set_rules") for player in world.player_ids: exclusion_rules(world, player, world.exclude_locations[player].value) world.priority_locations[player].value -= world.exclude_locations[player].value for location_name in world.priority_locations[player].value: world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY AutoWorld.call_all(world, "generate_basic") # temporary home for item links, should be moved out of Main for group_id, group in world.groups.items(): def find_common_pool(players: Set[int], shared_pool: Set[str]): classifications = collections.defaultdict(int) counters = {player: {name: 0 for name in shared_pool} for player in players} for item in world.itempool: if item.player in counters and item.name in shared_pool: counters[item.player][item.name] += 1 classifications[item.name] |= item.classification for player in players.copy(): if all([counters[player][item] == 0 for item in shared_pool]): players.remove(player) del(counters[player]) if not players: return None, None for item in shared_pool: count = min(counters[player][item] for player in players) if count: for player in players: counters[player][item] = count else: for player in players: del(counters[player][item]) return counters, classifications common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) if not common_item_count: continue new_itempool = [] for item_name, item_count in next(iter(common_item_count.values())).items(): for _ in range(item_count): new_item = group["world"].create_item(item_name) # mangle together all original classification bits new_item.classification |= classifications[item_name] new_itempool.append(new_item) region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world) world.regions.append(region) locations = region.locations = [] for item in world.itempool: count = common_item_count.get(item.player, {}).get(item.name, 0) if count: loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}", None, region) loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ state.has(item_name, group_id_, count_) locations.append(loc) loc.place_locked_item(item) common_item_count[item.player][item.name] -= 1 else: new_itempool.append(item) itemcount = len(world.itempool) world.itempool = new_itempool while itemcount > len(world.itempool): items_to_add = [] for player in group["players"]: if group["replacement_items"][player]: items_to_add.append(AutoWorld.call_single(world, "create_item", player, group["replacement_items"][player])) else: items_to_add.append(AutoWorld.call_single(world, "create_filler", player)) world.random.shuffle(items_to_add) world.itempool.extend(items_to_add[:itemcount - len(world.itempool)]) if any(world.item_links.values()): world._recache() world._all_state = None logger.info("Running Item Plando") for item in world.itempool: item.world = world distribute_planned(world) logger.info('Running Pre Main Fill.') AutoWorld.call_all(world, "pre_fill") logger.info(f'Filling the world with {len(world.itempool)} items.') if world.algorithm == 'flood': flood_items(world) # different algo, biased towards early game progress items elif world.algorithm == 'balanced': distribute_items_restrictive(world) AutoWorld.call_all(world, 'post_fill') if world.players > 1: balance_multiworld_progression(world) logger.info(f'Beginning output...') outfilebase = 'AP_' + world.seed_name output = tempfile.TemporaryDirectory() with output as temp_dir: with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool: check_accessibility_task = pool.submit(world.fulfills_accessibility) output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)] for player in world.player_ids: # skip starting a thread for methods that say "pass". if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__: output_file_futures.append( pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) def get_entrance_to_region(region: Region): for entrance in region.entrances: if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic): return entrance for entrance in region.entrances: # BFS might be better here, trying DFS for now. return get_entrance_to_region(entrance.parent_region) # collect ER hint info er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if world.shuffle[player] != "vanilla" or world.retro_caves[player]} for region in world.regions: if region.player in er_hint_data and region.locations: main_entrance = get_entrance_to_region(region) for location in region.locations: if type(location.address) == int: # skips events and crystals if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: er_hint_data[region.player][location.address] = main_entrance.name checks_in_area = {player: {area: list() for area in ordered_areas} for player in range(1, world.players + 1)} for player in range(1, world.players + 1): checks_in_area[player]["Total"] = 0 for location in world.get_filled_locations(): if type(location.address) is int: main_entrance = get_entrance_to_region(location.parent_region) if location.game != "A Link to the Past": checks_in_area[location.player]["Light World"].append(location.address) elif location.parent_region.dungeon: dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', 'Inverted Ganons Tower': 'Ganons Tower'} \ .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) checks_in_area[location.player][dungeonname].append(location.address) elif location.parent_region.type == RegionType.LightWorld: checks_in_area[location.player]["Light World"].append(location.address) elif location.parent_region.type == RegionType.DarkWorld: checks_in_area[location.player]["Dark World"].append(location.address) elif main_entrance.parent_region.type == RegionType.LightWorld: checks_in_area[location.player]["Light World"].append(location.address) elif main_entrance.parent_region.type == RegionType.DarkWorld: checks_in_area[location.player]["Dark World"].append(location.address) checks_in_area[location.player]["Total"] += 1 oldmancaves = [] takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] for index, take_any in enumerate(takeanyregions): for region in [world.get_region(take_any, player) for player in world.get_game_players("A Link to the Past") if world.retro_caves[player]]: item = world.create_item( region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player) player = region.player location_id = SHOP_ID_START + total_shop_slots + index main_entrance = get_entrance_to_region(region) if main_entrance.parent_region.type == RegionType.LightWorld: checks_in_area[player]["Light World"].append(location_id) else: checks_in_area[player]["Dark World"].append(location_id) checks_in_area[player]["Total"] += 1 er_hint_data[player][location_id] = main_entrance.name oldmancaves.append(((location_id, player), (item.code, player))) FillDisabledShopSlots(world) def write_multidata(): import NetUtils slot_data = {} client_versions = {} games = {} minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions} slot_info = {} names = [[name for player, name in sorted(world.player_name.items())]] for slot in world.player_ids: player_world: AutoWorld.World = world.worlds[slot] minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version) client_versions[slot] = player_world.required_client_version games[slot] = world.game[slot] slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot], world.player_types[slot]) for slot, group in world.groups.items(): games[slot] = world.game[slot] slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot], group_members=sorted(group["players"])) precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int] for player, world_precollected in world.precollected_items.items()} precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))} for slot in world.player_ids: slot_data[slot] = world.worlds[slot].fill_slot_data() def precollect_hint(location): entrance = er_hint_data.get(location.player, {}).get(location.address, "") hint = NetUtils.Hint(location.item.player, location.player, location.address, location.item.code, False, entrance, location.item.flags) precollected_hints[location.player].add(hint) if location.item.player not in world.groups: precollected_hints[location.item.player].add(hint) else: for player in world.groups[location.item.player]["players"]: precollected_hints[player].add(hint) locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids} for location in world.get_filled_locations(): if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ "location.address should then also be None" locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags if location.name in world.start_location_hints[location.player]: precollect_hint(location) elif location.item.name in world.start_hints[location.item.player]: precollect_hint(location) elif any([location.item.name in world.start_hints[player] for player in world.groups.get(location.item.player, {}).get("players", [])]): precollect_hint(location) multidata = { "slot_data": slot_data, "slot_info": slot_info, "names": names, # TODO: remove around 0.2.5 in favor of slot_info "games": games, # TODO: remove around 0.2.5 in favor of slot_info "connect_names": {name: (0, player) for player, name in world.player_name.items()}, "remote_items": {player for player in world.player_ids if world.worlds[player].remote_items}, "remote_start_inventory": {player for player in world.player_ids if world.worlds[player].remote_start_inventory}, "locations": locations_data, "checks_in_area": checks_in_area, "server_options": baked_server_options, "er_hint_data": er_hint_data, "precollected_items": precollected_items, "precollected_hints": precollected_hints, "version": tuple(version_tuple), "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": world.seed_name } AutoWorld.call_all(world, "modify_multidata", multidata) multidata = zlib.compress(pickle.dumps(multidata), 9) with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f: f.write(bytes([3])) # version of format f.write(multidata) multidata_task = pool.submit(write_multidata) if not check_accessibility_task.result(): if not world.can_beat_game(): raise Exception("Game appears as unbeatable. Aborting.") else: logger.warning("Location Accessibility requirements not fulfilled.") # retrieve exceptions via .result() if they occurred. multidata_task.result() for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1): if i % 10 == 0 or i == len(output_file_futures): logger.info(f'Generating output files ({i}/{len(output_file_futures)}).') future.result() if args.spoiler > 1: logger.info('Calculating playthrough.') create_playthrough(world) if args.spoiler: world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) zipfilename = output_path(f"AP_{world.seed_name}.zip") logger.info(f'Creating final archive at {zipfilename}.') with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: for file in os.scandir(temp_dir): zf.write(file.path, arcname=file.name) logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start) return world
def balance_multiworld_progression(world: MultiWorld) -> None: # A system to reduce situations where players have no checks remaining, popularly known as "BK mode." # Overall progression balancing algorithm: # Gather up all locations in a sphere. # Define a threshold value based on the player with the most available locations. # If other players are below the threshold value, swap progression in this sphere into earlier spheres, # which gives more locations available by this sphere. balanceable_players: typing.Dict[int, float] = { player: world.progression_balancing[player] / 100 for player in world.player_ids if world.progression_balancing[player] > 0 } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') else: logging.info( f'Balancing multiworld progression for {len(balanceable_players)} Players.' ) logging.debug(balanceable_players) state: CollectionState = CollectionState(world) checked_locations: typing.Set[Location] = set() unchecked_locations: typing.Set[Location] = set(world.get_locations()) reachable_locations_count: typing.Dict[int, int] = { player: 0 for player in world.player_ids if len(world.get_filled_locations(player)) != 0 } total_locations_count: typing.Counter[int] = Counter( location.player for location in world.get_locations() if not location.locked) balanceable_players = { player: balanceable_players[player] for player in balanceable_players if total_locations_count[player] } sphere_num: int = 1 moved_item_count: int = 0 def get_sphere_locations( sphere_state: CollectionState, locations: typing.Set[Location]) -> typing.Set[Location]: sphere_state.sweep_for_events(key_only=True, locations=locations) return {loc for loc in locations if sphere_state.can_reach(loc)} def item_percentage(player: int, num: int) -> float: return num / total_locations_count[player] while True: # Gather non-locked locations. # This ensures that only shuffled locations get counted for progression balancing, # i.e. the items the players will be checking. sphere_locations = get_sphere_locations(state, unchecked_locations) for location in sphere_locations: unchecked_locations.remove(location) if not location.locked: reachable_locations_count[location.player] += 1 logging.debug(f"Sphere {sphere_num}") logging.debug(f"Reachable locations: {reachable_locations_count}") debug_percentages = { player: round(item_percentage(player, num), 2) for player, num in reachable_locations_count.items() } logging.debug(f"Reachable percentages: {debug_percentages}\n") sphere_num += 1 if checked_locations: max_percentage = max( map( lambda p: item_percentage(p, reachable_locations_count[ p]), reachable_locations_count)) threshold_percentages = { player: max_percentage * balanceable_players[player] for player in balanceable_players } logging.debug(f"Thresholds: {threshold_percentages}") balancing_players = { player for player, reachables in reachable_locations_count.items() if (player in threshold_percentages and item_percentage( player, reachables) < threshold_percentages[player]) } if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() candidate_items: typing.Dict[ int, typing.Set[Location]] = collections.defaultdict(set) while True: # Check locations in the current sphere and gather progression items to swap earlier for location in balancing_sphere: if location.event: balancing_state.collect( location.item, True, location) player = location.item.player # only replace items that end up in another player's world if (not location.locked and not location.item. skip_in_prog_balancing and player in balancing_players and location.player != player and location.progress_type != LocationProgressType.PRIORITY): candidate_items[player].add(location) logging.debug( f"Candidate item: {location.name}, {location.item.name}" ) balancing_sphere = get_sphere_locations( balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) if not location.locked: balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all( item_percentage(player, reachables) >= threshold_percentages[player] for player, reachables in balancing_reachables. items() if player in threshold_percentages): break elif not balancing_sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) # Gather a set of locations which we can swap items into unlocked_locations: typing.Dict[ int, typing.Set[Location]] = collections.defaultdict(set) for l in unchecked_locations: if l not in balancing_unchecked_locations: unlocked_locations[l.player].add(l) items_to_replace: typing.List[Location] = [] for player in balancing_players: locations_to_test = unlocked_locations[player] items_to_test = list(candidate_items[player]) items_to_test.sort() world.random.shuffle(items_to_test) while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() for location in itertools.chain( (l for l in items_to_replace if l.item.player == player), items_to_test): reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events( locations=locations_to_test) if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations( reducing_state, locations_to_test) p = item_percentage( player, reachable_locations_count[player] + len(reduced_sphere)) if p < threshold_percentages[player]: items_to_replace.append(testing) replaced_items = False # sort then shuffle to maintain deterministic behaviour, # while allowing use of set for better algorithm growth behaviour elsewhere replacement_locations = sorted( l for l in checked_locations if not l.event and not l.locked) world.random.shuffle(replacement_locations) items_to_replace.sort() world.random.shuffle(items_to_replace) # Start swapping items. Since we swap into earlier spheres, no need for accessibility checks. while replacement_locations and items_to_replace: old_location = items_to_replace.pop() for new_location in replacement_locations: if new_location.can_fill(state, old_location.item, False) and \ old_location.can_fill(state, new_location.item, False): replacement_locations.remove(new_location) swap_location_item(old_location, new_location) logging.debug( f"Progression balancing moved {new_location.item} to {new_location}, " f"displacing {old_location.item} into {old_location}" ) moved_item_count += 1 state.collect(new_location.item, True, new_location) replaced_items = True break else: logging.warning( f"Could not Progression Balance {old_location.item}" ) if replaced_items: logging.debug( f"Moved {moved_item_count} items so far\n") unlocked = { fresh for player in balancing_players for fresh in unlocked_locations[player] } for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) if not location.locked: reachable_locations_count[location.player] += 1 sphere_locations.add(location) for location in sphere_locations: if location.event: state.collect(location.item, True, location) checked_locations |= sphere_locations if world.has_beaten_game(state): break elif not sphere_locations: logging.warning("Progression Balancing ran out of paths.") break
def balance_multiworld_progression(world): balanceable_players = { player for player in range(1, world.players + 1) if world.progression_balancing[player] } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') else: logging.info( f'Balancing multiworld progression for {len(balanceable_players)} Players.' ) state = CollectionState(world) checked_locations = [] unchecked_locations = world.get_locations().copy() world.random.shuffle(unchecked_locations) reachable_locations_count = {player: 0 for player in world.player_ids} def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) return [loc for loc in locations if sphere_state.can_reach(loc)] while True: sphere_locations = get_sphere_locations(state, unchecked_locations) for location in sphere_locations: unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 if checked_locations: threshold = max(reachable_locations_count.values()) - 20 balancing_players = [ player for player, reachables in reachable_locations_count.items() if reachables < threshold and player in balanceable_players ] if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() candidate_items = collections.defaultdict(list) while True: for location in balancing_sphere: if location.event: balancing_state.collect( location.item, True, location) player = location.item.player # only replace items that end up in another player's world if not location.locked and player in balancing_players and location.player != player: candidate_items[player].append(location) balancing_sphere = get_sphere_locations( balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all( reachables >= threshold for reachables in balancing_reachables.values()): break elif not balancing_sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) unlocked_locations = collections.defaultdict(list) for l in unchecked_locations: if l not in balancing_unchecked_locations: unlocked_locations[l.player].append(l) items_to_replace = [] for player in balancing_players: locations_to_test = unlocked_locations[player] items_to_test = candidate_items[player] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() for location in itertools.chain( (l for l in items_to_replace if l.item.player == player), items_to_test): reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events( locations=locations_to_test) if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations( reducing_state, locations_to_test) if reachable_locations_count[player] + len( reduced_sphere) < threshold: items_to_replace.append(testing) replaced_items = False replacement_locations = [ l for l in checked_locations if not l.event and not l.locked ] while replacement_locations and items_to_replace: new_location = replacement_locations.pop() old_location = items_to_replace.pop() while not new_location.can_fill( state, old_location.item, False) or ( new_location.item and not old_location.can_fill( state, new_location.item, False)): replacement_locations.insert(0, new_location) new_location = replacement_locations.pop() swap_location_item(old_location, new_location) logging.debug( f"Progression balancing moved {new_location.item} to {new_location}, " f"displacing {old_location.item} into {old_location}" ) state.collect(new_location.item, True, new_location) replaced_items = True if replaced_items: unlocked = [ fresh for player in balancing_players for fresh in unlocked_locations[player] ] for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 sphere_locations.append(location) for location in sphere_locations: if location.event: state.collect(location.item, True, location) checked_locations.extend(sphere_locations) if world.has_beaten_game(state): break elif not sphere_locations: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' )
def collect_item(self, state: CollectionState, item: Item, remove=False): item_name = item.name if item_name.startswith('Progressive '): if remove: if 'Sword' in item_name: if state.has('Golden Sword', item.player): return 'Golden Sword' elif state.has('Tempered Sword', item.player): return 'Tempered Sword' elif state.has('Master Sword', item.player): return 'Master Sword' elif state.has('Fighter Sword', item.player): return 'Fighter Sword' else: return None elif 'Glove' in item.name: if state.has('Titans Mitts', item.player): return 'Titans Mitts' elif state.has('Power Glove', item.player): return 'Power Glove' else: return None elif 'Shield' in item_name: if state.has('Mirror Shield', item.player): return 'Mirror Shield' elif state.has('Red Shield', item.player): return 'Red Shield' elif state.has('Blue Shield', item.player): return 'Blue Shield' else: return None elif 'Bow' in item_name: if state.has('Silver Bow', item.player): return 'Silver Bow' elif state.has('Bow', item.player): return 'Bow' else: return None else: if 'Sword' in item_name: if state.has('Golden Sword', item.player): pass elif state.has( 'Tempered Sword', item.player ) and self.world.difficulty_requirements[ item.player].progressive_sword_limit >= 4: return 'Golden Sword' elif state.has( 'Master Sword', item.player ) and self.world.difficulty_requirements[ item.player].progressive_sword_limit >= 3: return 'Tempered Sword' elif state.has( 'Fighter Sword', item.player ) and self.world.difficulty_requirements[ item.player].progressive_sword_limit >= 2: return 'Master Sword' elif self.world.difficulty_requirements[ item.player].progressive_sword_limit >= 1: return 'Fighter Sword' elif 'Glove' in item_name: if state.has('Titans Mitts', item.player): return elif state.has('Power Glove', item.player): return 'Titans Mitts' else: return 'Power Glove' elif 'Shield' in item_name: if state.has('Mirror Shield', item.player): return elif state.has( 'Red Shield', item.player ) and self.world.difficulty_requirements[ item.player].progressive_shield_limit >= 3: return 'Mirror Shield' elif state.has( 'Blue Shield', item.player ) and self.world.difficulty_requirements[ item.player].progressive_shield_limit >= 2: return 'Red Shield' elif self.world.difficulty_requirements[ item.player].progressive_shield_limit >= 1: return 'Blue Shield' elif 'Bow' in item_name: if state.has('Silver Bow', item.player): return elif state.has('Bow', item.player) and ( self.world.difficulty_requirements[ item.player].progressive_bow_limit >= 2 or self.world.logic[item.player] == 'noglitches' or self.world.swordless[item.player] ): # modes where silver bow is always required for ganon return 'Silver Bow' elif self.world.difficulty_requirements[ item.player].progressive_bow_limit >= 1: return 'Bow' elif item.advancement: return item_name