def buildWorldGossipHints(spoiler, world, checkedLocations=None): # rebuild hint exclusion list hintExclusions(world, clear_cache=True) world.barren_dungeon = False world.woth_dungeon = 0 search = Search.max_explore([w.state for w in spoiler.worlds]) for stone in gossipLocations.values(): stone.reachable = (search.spot_access( world.get_location(stone.location)) and search.state_list[world.id].guarantee_hint()) if checkedLocations is None: checkedLocations = set() stoneIDs = list(gossipLocations.keys()) world.distribution.configure_gossip(spoiler, stoneIDs) random.shuffle(stoneIDs) hint_dist = hint_dist_sets[world.hint_dist] hint_types, hint_prob = zip(*hint_dist.items()) hint_prob, _ = zip(*hint_prob) # Add required location hints alwaysLocations = getHintGroup('always', world) for hint in alwaysLocations: location = world.get_location(hint.name) checkedLocations.add(hint.name) location_text = getHint(location.name, world.clearer_hints).text if '#' not in location_text: location_text = '#%s#' % location_text item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text add_hint(spoiler, world, stoneIDs, GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) # Add trial hints if world.trials_random and world.trials == 6: add_hint(spoiler, world, stoneIDs, GossipText( "#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True) elif world.trials_random and world.trials == 0: add_hint(spoiler, world, stoneIDs, GossipText( "Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials < 6 and world.trials > 3: for trial, skipped in world.skipped_trials.items(): if skipped: add_hint(spoiler, world, stoneIDs, GossipText( "the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials <= 3 and world.trials > 0: for trial, skipped in world.skipped_trials.items(): if not skipped: add_hint(spoiler, world, stoneIDs, GossipText( "the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True) hint_types = list(hint_types) hint_prob = list(hint_prob) hint_counts = {} if world.hint_dist == "tournament": fixed_hint_types = [] for hint_type in hint_types: fixed_hint_types.extend([hint_type] * int(hint_dist[hint_type][0])) fill_hint_types = ['sometimes', 'random'] current_fill_type = fill_hint_types.pop(0) while stoneIDs: if world.hint_dist == "tournament": if fixed_hint_types: hint_type = fixed_hint_types.pop(0) else: hint_type = current_fill_type else: try: # Weight the probabilities such that hints that are over the expected proportion # will be drawn less, and hints that are under will be drawn more. # This tightens the variance quite a bit. The variance can be adjusted via the power weighted_hint_prob = [] for w1_type, w1_prob in zip(hint_types, hint_prob): p = w1_prob if p != 0: # If the base prob is 0, then it's 0 for w2_type, w2_prob in zip(hint_types, hint_prob): if w2_prob != 0: # If the other prob is 0, then it has no effect # Raising this term to a power greater than 1 will decrease variance # Conversely, a power less than 1 will increase variance p = p * ( ((hint_counts.get(w2_type, 0) / w2_prob) + 1) / ((hint_counts.get(w1_type, 0) / w1_prob) + 1)) weighted_hint_prob.append(p) hint_type = random_choices(hint_types, weights=weighted_hint_prob)[0] except IndexError: raise Exception( 'Not enough valid hints to fill gossip stone locations.') hint = hint_func[hint_type](spoiler, world, checkedLocations) if hint == None: index = hint_types.index(hint_type) hint_prob[index] = 0 if world.hint_dist == "tournament" and hint_type == current_fill_type: logging.getLogger('').info( 'Not enough valid %s hints for tournament distribution.', hint_type) if fill_hint_types: current_fill_type = fill_hint_types.pop(0) logging.getLogger('').info( 'Switching to %s hints to fill remaining gossip stone locations.', current_fill_type) else: raise Exception( 'Not enough valid hints for tournament distribution.') else: gossip_text, location = hint place_ok = add_hint(spoiler, world, stoneIDs, gossip_text, hint_dist[hint_type][1], location) if place_ok: hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1 if not place_ok and world.hint_dist == "tournament": logging.getLogger('').debug('Failed to place %s hint for %s.', hint_type, location.name) fixed_hint_types.insert(0, hint_type)
def buildGossipHints(spoiler, world): # rebuild hint exclusion list hintExclusions(world, clear_cache=True) world.barren_dungeon = False max_states = State.get_states_with_items([w.state for w in spoiler.worlds], []) for stone in gossipLocations.values(): stone.reachable = \ max_states[world.id].can_reach(stone.location, resolution_hint='Location') and \ max_states[world.id].guarantee_hint() checkedLocations = [] stoneIDs = list(gossipLocations.keys()) world.distribution.configure_gossip(spoiler, stoneIDs) random.shuffle(stoneIDs) hint_dist = hint_dist_sets[world.hint_dist] hint_types, hint_prob = zip(*hint_dist.items()) hint_prob, _ = zip(*hint_prob) # Add required location hints alwaysLocations = getHintGroup('always', world) for hint in alwaysLocations: location = world.get_location(hint.name) checkedLocations.append(hint.name) location_text = getHint(location.name, world.clearer_hints).text if '#' not in location_text: location_text = '#%s#' % location_text item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text add_hint(spoiler, world, stoneIDs, GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) # Add trial hints if world.trials_random and world.trials == 6: add_hint(spoiler, world, stoneIDs, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True) elif world.trials_random and world.trials == 0: add_hint(spoiler, world, stoneIDs, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials < 6 and world.trials > 3: for trial,skipped in world.skipped_trials.items(): if skipped: add_hint(spoiler, world, stoneIDs,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials <= 3 and world.trials > 0: for trial,skipped in world.skipped_trials.items(): if not skipped: add_hint(spoiler, world, stoneIDs, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True) hint_types = list(hint_types) hint_prob = list(hint_prob) if world.hint_dist == "tournament": fixed_hint_types = [] for hint_type in hint_types: fixed_hint_types.extend([hint_type] * int(hint_dist[hint_type][0])) while stoneIDs: if world.hint_dist == "tournament": if fixed_hint_types: hint_type = fixed_hint_types.pop(0) else: hint_type = 'random' else: try: hint_type = random_choices(hint_types, weights=hint_prob)[0] except IndexError: raise Exception('Not enough valid hints to fill gossip stone locations.') hint = hint_func[hint_type](spoiler, world, checkedLocations) if hint == None: index = hint_types.index(hint_type) hint_prob[index] = 0 else: gossip_text, location = hint place_ok = add_hint(spoiler, world, stoneIDs, gossip_text, hint_dist[hint_type][1], location) if not place_ok and world.hint_dist == "tournament": fixed_hint_types.insert(0, hint_type)
def update_goal_items(spoiler): worlds = spoiler.worlds # get list of all of the progressive items that can appear in hints # all_locations: all progressive items. have to collect from these # item_locations: only the ones that should appear as "required"/WotH all_locations = [ location for world in worlds for location in world.get_filled_locations() ] # Set to test inclusion against item_locations = { location for location in all_locations if location.item.majoritem and not location.locked and location.item.name != 'Triforce Piece' } # required_locations[category.name][goal.name][world_id] = [...] required_locations = defaultdict( lambda: defaultdict(lambda: defaultdict(list))) priority_locations = {(world.id): {} for world in worlds} # rebuild hint exclusion list for world in worlds: hintExclusions(world, clear_cache=True) # getHintGroup relies on hint exclusion list always_locations = [ location.name for world in worlds for location in getHintGroup('always', world) ] if spoiler.playthrough: # Skip even the checks _maybe_set_light_arrows = lambda _: None else: _maybe_set_light_arrows = maybe_set_light_arrows if worlds[0].enable_goal_hints: # References first world for goal categories only for cat_name, category in worlds[0].locked_goal_categories.items(): for cat_world in worlds: search = Search([world.state for world in worlds]) search.collect_pseudo_starting_items() cat_state = list( filter(lambda s: s.world.id == cat_world.id, search.state_list)) category_locks = lock_category_entrances(category, cat_state) if category.is_beaten(search): unlock_category_entrances(category_locks, cat_state) continue full_search = search.copy() full_search.collect_locations() reachable_goals = {} # Goals are changed for beatable-only accessibility per-world category.update_reachable_goals(search, full_search) reachable_goals = full_search.beatable_goals_fast( {cat_name: category}, cat_world.id) identified_locations = search_goals( {cat_name: category}, reachable_goals, search, priority_locations, all_locations, item_locations, always_locations, _maybe_set_light_arrows) # Multiworld can have all goals for one player's bridge entirely # locked by another player's bridge. Therefore, we can't assume # accurate required location lists by locking every world's # entrance locks simultaneously. We must run a new search on the # same category for each world's entrance locks. for goal_name, world_location_lists in identified_locations[ cat_name].items(): required_locations[cat_name][goal_name][ cat_world.id] = list(identified_locations[cat_name] [goal_name][cat_world.id]) unlock_category_entrances(category_locks, cat_state) search = Search([world.state for world in worlds]) search.collect_pseudo_starting_items() reachable_goals = {} # Force all goals to be unreachable if goal hints are not part of the distro # Saves a minor amount of search time. Most of the benefit is skipping the # locked goal categories. This section still needs to run to generate WOTH. # WOTH isn't a goal, so it still is searched successfully. if worlds[0].enable_goal_hints: full_search = search.copy() full_search.collect_locations() for cat_name, category in worlds[0].unlocked_goal_categories.items(): category.update_reachable_goals(search, full_search) reachable_goals = full_search.beatable_goals_fast( worlds[0].unlocked_goal_categories) identified_locations = search_goals(worlds[0].unlocked_goal_categories, reachable_goals, search, priority_locations, all_locations, item_locations, always_locations, _maybe_set_light_arrows, search_woth=True) required_locations.update(identified_locations) woth_locations = list(required_locations['way of the hero']) del required_locations['way of the hero'] # Update WOTH items woth_locations_dict = {} for world in worlds: woth_locations_dict[world.id] = list( filter(lambda location: location.world.id == world.id, woth_locations)) spoiler.required_locations = woth_locations_dict # Fallback to way of the hero required items list if all goals/goal categories already satisfied. # Do not use if the default woth-like goal was already added for open bridge/open ganon. # If the woth list is also empty, fails gracefully to the next hint type for the distro in either case. # required_locations_dict[goal_world][category.name][goal.name][world.id] = [...] required_locations_dict = defaultdict( lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(list)))) if not required_locations and 'ganon' not in worlds[ 0].goal_categories and worlds[0].hint_dist_user[ 'use_default_goals'] and worlds[0].enable_goal_hints: for world in worlds: locations = [(location, 1, 1, [world.id]) for location in spoiler.required_locations[world.id]] c = GoalCategory('ganon', 30, goal_count=1, minimum_goals=1) g = Goal(world, 'the hero', 'way of the hero', 'White', items=[{ 'name': 'Triforce', 'quantity': 1, 'minimum': 1, 'hintable': True }]) g.required_locations = locations c.add_goal(g) world.goal_categories[c.name] = c # The real protagonist of the story required_locations_dict[world.id]['ganon']['the hero'][ world.id] = [l[0] for l in locations] spoiler.goal_categories[world.id] = {c.name: c.copy()} else: for world in worlds: for cat_name, category in world.goal_categories.items(): if cat_name not in required_locations: continue for goal in category.goals: if goal.name not in required_locations[category.name]: continue # Filter the required locations to only include locations in the goal's world. # The fulfilled goal can be for another world, whose ID is saved previously as # the fourth entry in each location tuple goal_locations = defaultdict(list) for goal_world, locations in required_locations[ category.name][goal.name].items(): for location in locations: # filter isn't strictly necessary for the spoiler dictionary, but this way # we only loop once to do that and grab the required locations for the # current goal if location[0].world.id != world.id: continue # The spoiler shows locations across worlds contributing to this world's goal required_locations_dict[goal_world][category.name][ goal.name][world.id].append(location[0]) goal_locations[location[0]].append(goal_world) goal.required_locations = [ (location, 1, 1, world_ids) for location, world_ids in goal_locations.items() ] # Copy of goal categories for the spoiler log to reference # since the hint algorithm mutates the world copy for world in worlds: spoiler.goal_categories[world.id] = { cat_name: category.copy() for cat_name, category in world.goal_categories.items() } spoiler.goal_locations = required_locations_dict
def buildWorldGossipHints(spoiler, world, checkedLocations=None): # rebuild hint exclusion list hintExclusions(world, clear_cache=True) world.barren_dungeon = 0 world.woth_dungeon = 0 search = Search.max_explore([w.state for w in spoiler.worlds]) for stone in gossipLocations.values(): stone.reachable = ( search.spot_access(world.get_location(stone.location)) and search.state_list[world.id].guarantee_hint()) if checkedLocations is None: checkedLocations = set() stoneIDs = list(gossipLocations.keys()) world.distribution.configure_gossip(spoiler, stoneIDs) if 'disabled' in world.hint_dist_user: for stone_name in world.hint_dist_user['disabled']: try: stone_id = gossipLocations_reversemap[stone_name] except KeyError: raise ValueError(f'Gossip stone location "{stone_name}" is not valid') stoneIDs.remove(stone_id) (gossip_text, _) = get_junk_hint(spoiler, world, checkedLocations) spoiler.hints[world.id][stone_id] = gossip_text stoneGroups = [] if 'groups' in world.hint_dist_user: for group_names in world.hint_dist_user['groups']: group = [] for stone_name in group_names: try: stone_id = gossipLocations_reversemap[stone_name] except KeyError: raise ValueError(f'Gossip stone location "{stone_name}" is not valid') stoneIDs.remove(stone_id) group.append(stone_id) stoneGroups.append(group) # put the remaining locations into singleton groups stoneGroups.extend([[id] for id in stoneIDs]) random.shuffle(stoneGroups) # Create list of items for which we want hints. If Bingosync URL is supplied, include items specific to that bingo. # If not (or if the URL is invalid), use generic bingo hints if world.hint_dist == "bingo": bingoDefaults = read_json(data_path('Bingo/generic_bingo_hints.json')) if world.bingosync_url is not None and world.bingosync_url.startswith("https://bingosync.com/"): # Verify that user actually entered a bingosync URL logger = logging.getLogger('') logger.info("Got Bingosync URL. Building board-specific goals.") world.item_hints = buildBingoHintList(world.bingosync_url) else: world.item_hints = bingoDefaults['settings']['item_hints'] if world.tokensanity in ("overworld", "all") and "Suns Song" not in world.item_hints: world.item_hints.append("Suns Song") if world.shopsanity != "off" and "Progressive Wallet" not in world.item_hints: world.item_hints.append("Progressive Wallet") # Load hint distro from distribution file or pre-defined settings # # 'fixed' key is used to mimic the tournament distribution, creating a list of fixed hint types to fill # Once the fixed hint type list is exhausted, weighted random choices are taken like all non-tournament sets # This diverges from the tournament distribution where leftover stones are filled with sometimes hints (or random if no sometimes locations remain to be hinted) sorted_dist = {} type_count = 1 hint_dist = OrderedDict({}) fixed_hint_types = [] max_order = 0 for hint_type in world.hint_dist_user['distribution']: if world.hint_dist_user['distribution'][hint_type]['order'] > 0: hint_order = int(world.hint_dist_user['distribution'][hint_type]['order']) sorted_dist[hint_order] = hint_type if max_order < hint_order: max_order = hint_order type_count = type_count + 1 if (type_count - 1) < max_order: raise Exception("There are gaps in the custom hint orders. Please revise your plando file to remove them.") for i in range(1, type_count): hint_type = sorted_dist[i] if world.hint_dist_user['distribution'][hint_type]['copies'] > 0: fixed_num = world.hint_dist_user['distribution'][hint_type]['fixed'] hint_weight = world.hint_dist_user['distribution'][hint_type]['weight'] else: logging.getLogger('').warning("Hint copies is zero for type %s. Assuming this hint type should be disabled.", hint_type) fixed_num = 0 hint_weight = 0 hint_dist[hint_type] = (hint_weight, world.hint_dist_user['distribution'][hint_type]['copies']) hint_dist.move_to_end(hint_type) fixed_hint_types.extend([hint_type] * int(fixed_num)) hint_types, hint_prob = zip(*hint_dist.items()) hint_prob, _ = zip(*hint_prob) # Add required location hints, only if hint copies > 0 if hint_dist['always'][1] > 0: alwaysLocations = getHintGroup('always', world) for hint in alwaysLocations: location = world.get_location(hint.name) checkedLocations.add(hint.name) if location.item.name in bingoBottlesForHints and world.hint_dist == 'bingo': always_item = 'Bottle' else: always_item = location.item.name if always_item in world.item_hints: world.item_hints.remove(always_item) if location.name in world.hint_text_overrides: location_text = world.hint_text_overrides[location.name] else: location_text = getHint(location.name, world.clearer_hints).text if '#' not in location_text: location_text = '#%s#' % location_text item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text add_hint(spoiler, world, stoneGroups, GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) logging.getLogger('').debug('Placed always hint for %s.', location.name) # Add trial hints, only if hint copies > 0 if hint_dist['trial'][1] > 0: if world.trials_random and world.trials == 6: add_hint(spoiler, world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True) elif world.trials_random and world.trials == 0: add_hint(spoiler, world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials < 6 and world.trials > 3: for trial,skipped in world.skipped_trials.items(): if skipped: add_hint(spoiler, world, stoneGroups,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials <= 3 and world.trials > 0: for trial,skipped in world.skipped_trials.items(): if not skipped: add_hint(spoiler, world, stoneGroups, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True) # Add user-specified hinted item locations if using a built-in hint distribution # Raise error if hint copies is zero if len(world.item_hints) > 0 and world.hint_dist_user['named_items_required']: if hint_dist['named-item'][1] == 0: raise Exception('User-provided item hints were requested, but copies per named-item hint is zero') else: for i in range(0, len(world.item_hints)): hint = get_specific_item_hint(spoiler, world, checkedLocations) if hint == None: raise Exception('No valid hints for user-provided item') else: gossip_text, location = hint place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, hint_dist['named-item'][1], location) if not place_ok: raise Exception('Not enough gossip stones for user-provided item hints') hint_types = list(hint_types) hint_prob = list(hint_prob) hint_counts = {} custom_fixed = True while stoneGroups: if fixed_hint_types: hint_type = fixed_hint_types.pop(0) copies = hint_dist[hint_type][1] if copies > len(stoneGroups): # Quiet to avoid leaking information. logging.getLogger('').debug(f'Not enough gossip stone locations ({len(stoneGroups)} groups) for fixed hint type {hint_type} with {copies} copies, proceeding with available stones.') copies = len(stoneGroups) else: custom_fixed = False # Make sure there are enough stones left for each hint type num_types = len(hint_types) hint_types = list(filter(lambda htype: hint_dist[htype][1] <= len(stoneGroups), hint_types)) new_num_types = len(hint_types) if new_num_types == 0: raise Exception('Not enough gossip stone locations for remaining weighted hint types.') elif new_num_types < num_types: hint_prob = [] for htype in hint_types: hint_prob.append(hint_dist[htype][0]) try: # Weight the probabilities such that hints that are over the expected proportion # will be drawn less, and hints that are under will be drawn more. # This tightens the variance quite a bit. The variance can be adjusted via the power weighted_hint_prob = [] for w1_type, w1_prob in zip(hint_types, hint_prob): p = w1_prob if p != 0: # If the base prob is 0, then it's 0 for w2_type, w2_prob in zip(hint_types, hint_prob): if w2_prob != 0: # If the other prob is 0, then it has no effect # Raising this term to a power greater than 1 will decrease variance # Conversely, a power less than 1 will increase variance p = p * (((hint_counts.get(w2_type, 0) / w2_prob) + 1) / ((hint_counts.get(w1_type, 0) / w1_prob) + 1)) weighted_hint_prob.append(p) hint_type = random_choices(hint_types, weights=weighted_hint_prob)[0] copies = hint_dist[hint_type][1] except IndexError: raise Exception('Not enough valid hints to fill gossip stone locations.') hint = hint_func[hint_type](spoiler, world, checkedLocations) if hint == None: index = hint_types.index(hint_type) hint_prob[index] = 0 # Zero out the probability in the base distribution in case the probability list is modified # to fit hint types in remaining gossip stones hint_dist[hint_type] = (0.0, copies) else: gossip_text, location = hint place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, copies, location) if place_ok: hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1 if location is None: logging.getLogger('').debug('Placed %s hint.', hint_type) else: logging.getLogger('').debug('Placed %s hint for %s.', hint_type, location.name) if not place_ok and custom_fixed: logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, location.name) fixed_hint_types.insert(0, hint_type)
def buildGossipHints(spoiler, world): # rebuild hint exclusion list hintExclusions(world, clear_cache=True) world.barren_dungeon = False max_states = State.get_states_with_items([w.state for w in spoiler.worlds], []) for id, stone in gossipLocations.items(): stone.reachable = \ max_states[world.id].can_reach(stone.location, resolution_hint='Location') and \ max_states[world.id].guarantee_hint() checkedLocations = [] stoneIDs = list(gossipLocations.keys()) random.shuffle(stoneIDs) hint_dist = hint_dist_sets[world.hint_dist] hint_types, hint_prob = zip(*hint_dist.items()) hint_prob, hint_count = zip(*hint_prob) # Add required location hints alwaysLocations = getHintGroup('alwaysLocation', world) for hint in alwaysLocations: location = world.get_location(hint.name) checkedLocations.append(hint.name) add_hint(spoiler, world, stoneIDs, buildHintString(colorText(getHint(location.name, world.clearer_hints).text, 'Green') + " " + \ colorText(getHint(getItemGenericName(location.item), world.clearer_hints).text, 'Red') + "."), hint_dist['always'][1], location, force_reachable=True) # Add trial hints if world.trials_random and world.trials == 6: add_hint(spoiler, world, stoneIDs, buildHintString( colorText("Ganon's Tower", 'Pink') + " is protected by a powerful barrier."), hint_dist['trial'][1], force_reachable=True) elif world.trials_random and world.trials == 0: add_hint(spoiler, world, stoneIDs, buildHintString("Sheik dispelled the barrier around " + colorText("Ganon's Tower", 'Yellow')), hint_dist['trial'][1], force_reachable=True) elif world.trials < 6 and world.trials > 3: for trial, skipped in world.skipped_trials.items(): if skipped: add_hint( spoiler, world, stoneIDs, buildHintString("the " + colorText(trial + " Trial", 'Yellow') + " was dispelled by Sheik."), hint_dist['trial'][1], force_reachable=True) elif world.trials <= 3 and world.trials > 0: for trial, skipped in world.skipped_trials.items(): if not skipped: add_hint(spoiler, world, stoneIDs, buildHintString("the " + colorText(trial + " Trial", 'Pink') + " protects Ganon's Tower."), hint_dist['trial'][1], force_reachable=True) hint_types = list(hint_types) hint_prob = list(hint_prob) if world.hint_dist == "tournament": fixed_hint_types = [] for hint_type in hint_types: fixed_hint_types.extend([hint_type] * int(hint_dist[hint_type][0])) while stoneIDs: if world.hint_dist == "tournament": if fixed_hint_types: hint_type = fixed_hint_types.pop(0) else: hint_type = 'loc' else: try: [hint_type] = random_choices(hint_types, weights=hint_prob) except IndexError: raise Exception( 'Not enough valid hints to fill gossip stone locations.') hint = hint_func[hint_type](spoiler, world, checkedLocations) if hint == None: index = hint_types.index(hint_type) hint_prob[index] = 0 else: text, location = hint place_ok = add_hint(spoiler, world, stoneIDs, text, hint_dist[hint_type][1], location) if not place_ok and world.hint_dist == "tournament": fixed_hint_types.insert(0, hint_type)