def buildBossString(reward, world): text = '' for location in world.get_locations(): if location.item.name == reward: text += '\x08' + get_raw_text(getHint(location.name).text) return text
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(world, messages): stoneIDs = [ 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A, 0x040B, 0x040C, 0x040D, 0x040E, 0x040F, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420 ] #shuffles the stone addresses for randomization, always locations will be placed first and twice random.shuffle(stoneIDs) stoneLocations = { 0x0401: 'Zoras Fountain - Fairy', 0x0402: 'Zoras Fountain - Jabu', 0x0403: 'Lake Hylia - Lab', 0x0404: 'Death Mountain Trail - Biggoron', 0x0405: 'Death Mountain Crater - Bombable Wall', 0x0406: 'Temple of Time - Left', 0x0407: 'Temple of Time - Middle-Left', 0x0408: 'Lake Hylia - South-West Corner', 0x0409: 'Zoras Domain - Mweep', 0x040A: 'Graveyard - Shadow Temple', 0x040B: 'Hyrule Castle - Rock Wall', 0x040C: 'Zoras River - Waterfall', 0x040D: 'Zoras River - Plateau', 0x040E: 'Temple of Time - Middle-Right', 0x040F: 'Lake Hylia - South-East Corner', 0x0410: 'Temple of Time - Right', 0x0411: 'Gerudo Valley - Waterfall', 0x0412: 'Hyrule Castle - Malon', 0x0413: 'Hyrule Castle - Storms Grotto', 0x0414: 'Dodongos Cavern - Bombable Wall', 0x0415: 'Goron City - Maze', 0x0416: 'Sacred Forest Meadow - Maze Lower', 0x0417: 'Sacred Forest Meadow - Maze Upper', 0x0418: 'Generic Grotto', 0x0419: 'Goron City - Medigoron', 0x041A: 'Desert Colossus - Spirit Temple', 0x041B: 'Hyrule Field - Hammer Grotto', 0x041C: 'Sacred Forest Meadow - Saria', 0x041D: 'Lost Woods - Bridge', 0x041E: 'Kokiri Forest - Storms', 0x041F: 'Kokiri Forest - Deku Tree Left', 0x0420: 'Kokiri Forest - Deku Tree Right' } spoilerHintsList.append('-Way of the Hero-') # add required items locations for hints (good hints) requiredSample = [] requiredSample1 = world.spoiler.required_locations #print([(location.parent_region.name, location.item.name) for location in requiredSample]) # loop through the locations we got for "always required locatiosns" # if you find lens, remove it for l in requiredSample1: if l.item.name in ['Master Sword', 'Stone of Agony']: continue else: requiredSample.append(l) #print([(location.parent_region.name, location.item.name) for location in requiredSample]) if len(requiredSample) >= 4: requiredSample = random.sample(requiredSample, 4) #Pick exactly 4 for location in requiredSample: for _ in range(0, 3): #and distribute each 3 times (12 / 32) ID = stoneIDs.pop(0) if location.parent_region.dungeon: update_hint(messages, ID, buildHintString(getHint(location.parent_region.dungeon.name).text + \ " is on the way of the hero.")) spoilerHintsList.append(location.parent_region.dungeon.name + ': ' + location.item.name + \ ' (' + stoneLocations[ID] + ')') #print(location.parent_region.dungeon.name, ': ', location.item.name) else: update_hint( messages, ID, buildHintString(location.parent_region.name + " is on the way of the hero.")) spoilerHintsList.append(location.parent_region.name + ": " + location.item.name + \ ' (' + stoneLocations[ID] + ')') #print(location.parent_region.name, ': ', location.item.name) # Don't repeat hints checkedLocations = [] spoilerHintsList.append('\n-Required Locations-') # Add required location hints alwaysLocations = getHintGroup('alwaysLocation', world) for hint in alwaysLocations: for locationWorld in world.get_locations(): if hint.name == locationWorld.name: checkedLocations.append(hint.name) for _ in range(0, 2): #populate each of these twice (24 / 32) ID = stoneIDs.pop(0) update_hint(messages, ID, getHint(locationWorld.name).text + " " + \ getHint(getItemGenericName(locationWorld.item)).text + ".") spoilerHintsList.append(locationWorld.name + ": " + locationWorld.item.name + \ ' (' + stoneLocations[ID] + ')') ## spoilerHintsList.append('\n-Good Locations-') ## # Add good location hints ## sometimesLocations = getHintGroup('location', world) ## if sometimesLocations: ## # for _ in range(0, random.randint(9,10) - len(alwaysLocations)): ## for _ in range(0, 2): # Exactly 2 of these (26 / ## hint = random.choice(sometimesLocations) ## # Repick if location isn't new ## while hint.name in checkedLocations or hint.name in alwaysLocations: ## hint = random.choice(sometimesLocations) ## ## for locationWorld in world.get_locations(): ## if hint.name == locationWorld.name: ## checkedLocations.append(locationWorld.name) ## update_hint(messages, stoneIDs.pop(0), getHint(locationWorld.name).text + " " + \ ## getHint(getItemGenericName(locationWorld.item)).text + ".") ## spoilerHintsList.append(locationWorld.name + ': ' + locationWorld.item.name) ## spoilerHintsList.append('\n-BadItem Dungeon-') ## # add bad dungeon locations hints ## for dungeon in random.sample(world.dungeons, 2): # Exactly 2 of these (28 / 32) ## # Choose a randome dungeon location that is a non-dungeon item ## locationWorld = random.choice([location for region in dungeon.regions for location in world.get_region(region).locations ## if location.item.type != 'Event' and \ ## not location.name in eventlocations and \ ## not isDungeonItem(location.item) and \ ## (world.tokensanity != 'off' or location.item.name != 'Gold Skulltulla Token') and\ ## location.item.type != 'Song']) ## ## checkedLocations.append(locationWorld.name) ## update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(dungeon.name).text + \ ## " hoards " + getHint(getItemGenericName(locationWorld.item)).text + ".")) ## spoilerHintsList.append(dungeon.name + ': ' + locationWorld.item.name) ## ## spoilerHintsList.append('\n-BadItem Overworld-') ## # add bad overworld locations hints ## # only choose location if it is new and a proper item from the overworld ## overworldlocations = [locationWorld for locationWorld in world.get_locations() ## if not locationWorld.name in checkedLocations and \ ## not locationWorld.name in alwaysLocations and \ ## not locationWorld.name in sometimesLocations and \ ## locationWorld.item.type != 'Event' and \ ## not locationWorld.name in eventlocations and \ ## (world.tokensanity == 'all' or locationWorld.item.name != 'Gold Skulltulla Token') and \ ## not locationWorld.parent_region.dungeon and \ ## not locationWorld.name in checkedLocations] ## overworldSample = overworldlocations ## if len(overworldSample) >= 2: # Only need to check for 2 ## overworldSample = random.sample(overworldlocations, 2) # Exactly 2 of these (30 / 32) ## for locationWorld in overworldSample: ## checkedLocations.append(locationWorld.name) ## update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(getItemGenericName(locationWorld.item)).text + \ ## " can be found at " + locationWorld.parent_region.name + ".")) ## spoilerHintsList.append(locationWorld.parent_region.name + ': ' + locationWorld.item.name) #Populate 4 bad item hints spoilerHintsList.append('\n-Bad Items-') # add good item hints # only choose location if it is new and a good item if world.shuffle_weird_egg: gooditems.append('Weird Egg') baditemlocations = [locationWorld for locationWorld in world.get_locations() if not locationWorld.name in checkedLocations and \ not locationWorld.name in alwaysLocations and \ (world.tokensanity == 'all' or locationWorld.item.name != 'Gold Skulltulla Token') and \ locationWorld.item.type != 'Event' and \ not locationWorld.name in eventlocations and \ not isDungeonItem(locationWorld.item) and \ locationWorld.item.name not in gooditems] baditemSample = random.sample(baditemlocations, 4) # Don't need this check, we'll fill the rest from this pool, usually only 2, can be more in very rare cases # if len(gooditemSample) >= 5: # gooditemSample = random.sample(gooditemlocations, random.randint(3,5)) # for locationWorld in gooditemSample: for locationWorld in baditemSample: #locationWorld = gooditemSample.pop() checkedLocations.append(locationWorld.name) ID = stoneIDs.pop(0) if locationWorld.parent_region.dungeon: update_hint(messages, ID, buildHintString(getHint(locationWorld.parent_region.dungeon.name).text + \ " hoards " + getHint(getItemGenericName(locationWorld.item)).text + ".")) spoilerHintsList.append(locationWorld.parent_region.dungeon.name + ': ' + locationWorld.item.name + \ ' (' + stoneLocations[ID] + ')') else: update_hint(messages, ID, buildHintString(getHint(getItemGenericName(locationWorld.item)).text + \ " can be found at " + locationWorld.parent_region.name + ".")) spoilerHintsList.append(locationWorld.parent_region.name + ': ' + locationWorld.item.name + \ ' (' + stoneLocations[ID] + ')') spoilerHintsList.append('\n-Good Items-') # add good item hints # only choose location if it is new and a good item if world.shuffle_weird_egg: gooditems.append('Weird Egg') gooditemlocations = [locationWorld for locationWorld in world.get_locations() if not locationWorld.name in checkedLocations and \ locationWorld.item.name in gooditems] gooditemSample = gooditemlocations # Don't need this check, we'll fill the rest from this pool, usually only 2, can be more in very rare cases # if len(gooditemSample) >= 5: # gooditemSample = random.sample(gooditemlocations, random.randint(3,5)) # for locationWorld in gooditemSample: random.shuffle(gooditemSample) while stoneIDs: locationWorld = gooditemSample.pop() checkedLocations.append(locationWorld.name) ID = stoneIDs.pop(0) if locationWorld.parent_region.dungeon: update_hint(messages, ID, buildHintString(getHint(locationWorld.parent_region.dungeon.name).text + \ " hoards " + getHint(getItemGenericName(locationWorld.item)).text + ".")) spoilerHintsList.append(locationWorld.parent_region.dungeon.name + ': ' + locationWorld.item.name + \ ' (' + stoneLocations[ID] + ')') else: update_hint(messages, ID, buildHintString(getHint(getItemGenericName(locationWorld.item)).text + \ " can be found at " + locationWorld.parent_region.name + ".")) spoilerHintsList.append(locationWorld.parent_region.name + ': ' + locationWorld.item.name + \ ' (' + stoneLocations[ID] + ')') #spoilerHintsList.append('\n-Junk-\n') f = open("hints.txt", "w") f.write('~~~ NEW HINTS ~~~\n\n') f.write('\n'.join(spoilerHintsList)) f.close()
def buildGossipHints(world): stoneIDs = [ 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A, 0x040B, 0x040C, 0x040D, 0x040E, 0x040F, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420 ] #shuffles the stone addresses for randomization, always locations will be placed first random.shuffle(stoneIDs) # Add trial hints if world.trials < 6 and world.trials > 3: for trial, skipped in world.skipped_trials.items(): if skipped: add_hint( world, stoneIDs.pop(0), buildHintString("the " + colorText(trial + " Trial", 'Yellow') + " was dispelled by Sheik.")) elif world.trials <= 3 and world.trials > 0: for trial, skipped in world.skipped_trials.items(): if not skipped: add_hint( world, stoneIDs.pop(0), buildHintString("the " + colorText(trial + " Trial", 'Pink') + " protects Ganon's Tower.")) # add required items locations for hints (good hints) requiredSample = world.spoiler.required_locations if len(requiredSample) >= 5: requiredSample = random.sample(requiredSample, random.randint(3, 4)) for location in requiredSample: if location.parent_region.dungeon: add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(location.parent_region.dungeon.name, world).text, 'Light Blue') + \ " is on the way of the hero.")) else: add_hint( world, stoneIDs.pop(0), buildHintString( colorText(location.hint, 'Light Blue') + " is on the way of the hero.")) # Don't repeat hints checkedLocations = [] # Add required location hints alwaysLocations = getHintGroup('alwaysLocation', world) for hint in alwaysLocations: for locationWorld in world.get_locations(): if hint.name == locationWorld.name: checkedLocations.append(hint.name) add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(locationWorld.name, world).text, 'Green') + " " + \ colorText(getHint(getItemGenericName(locationWorld.item), world).text, 'Red') + ".")) # Add good location hints sometimesLocations = getHintGroup('location', world) if sometimesLocations: for _ in range(0, random.randint(11, 12) - len(alwaysLocations)): hint = random.choice(sometimesLocations) # Repick if location isn't new while hint.name in checkedLocations or hint.name in alwaysLocations: hint = random.choice(sometimesLocations) for locationWorld in world.get_locations(): if hint.name == locationWorld.name: checkedLocations.append(locationWorld.name) add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(locationWorld.name, world).text, 'Green') + " " + \ colorText(getHint(getItemGenericName(locationWorld.item), world).text, 'Red') + ".")) # add bad dungeon locations hints for dungeon in random.sample(world.dungeons, random.randint(3, 4)): # Choose a randome dungeon location that is a non-dungeon item locationWorld = random.choice([location for region in dungeon.regions for location in region.locations if location.item.type != 'Event' and \ location.item.type != 'Shop' and \ not location.event and \ not isDungeonItem(location.item) and \ (world.tokensanity != 'off' or location.item.name != 'Gold Skulltulla Token') and\ location.item.type != 'Song']) checkedLocations.append(locationWorld.name) add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(dungeon.name, world).text, 'Green') + \ " hoards " + colorText(getHint(getItemGenericName(locationWorld.item), world).text, 'Red') + ".")) # add bad overworld locations hints # only choose location if it is new and a proper item from the overworld overworldlocations = [locationWorld for locationWorld in world.get_locations() if not locationWorld.name in checkedLocations and \ not locationWorld.name in alwaysLocations and \ not locationWorld.name in sometimesLocations and \ locationWorld.item.type != 'Event' and \ locationWorld.item.type != 'Shop' and \ not locationWorld.event and \ (world.tokensanity == 'all' or locationWorld.item.name != 'Gold Skulltulla Token') and \ not locationWorld.parent_region.dungeon] overworldSample = overworldlocations if len(overworldSample) >= 3: # Use this hint type to balance hints given via trials if world.trials == 3: overworldSample = random.sample(overworldlocations, random.randint(1, 2)) elif world.trials in [2, 4]: overworldSample = random.sample(overworldlocations, random.randint(1, 3)) elif world.trials in [1, 5]: overworldSample = random.sample(overworldlocations, random.randint(2, 3)) else: overworldSample = random.sample(overworldlocations, random.randint(2, 4)) for locationWorld in overworldSample: checkedLocations.append(locationWorld.name) add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(getItemGenericName(locationWorld.item), world).text, 'Red') + \ " can be found at " + colorText(locationWorld.hint, 'Green') + ".")) # add good item hints # only choose location if it is new and a good item gooditemlocations = [locationWorld for locationWorld in world.get_locations() if not locationWorld.name in checkedLocations and \ locationWorld.item.advancement and \ locationWorld.item.type != 'Event' and \ locationWorld.item.type != 'Shop' and \ not locationWorld.event and \ locationWorld.item.name != 'Gold Skulltulla Token' and \ not locationWorld.item.key] gooditemSample = gooditemlocations if len(gooditemSample) >= 6: gooditemSample = random.sample(gooditemlocations, random.randint(4, 6)) for locationWorld in gooditemSample: checkedLocations.append(locationWorld.name) if locationWorld.parent_region.dungeon: add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(locationWorld.parent_region.dungeon.name, world).text, 'Green') + \ " hoards " + colorText(getHint(getItemGenericName(locationWorld.item), world).text, 'Red') + ".")) else: add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(getItemGenericName(locationWorld.item), world).text, 'Red') + \ " can be found at " + colorText(locationWorld.hint, 'Green') + ".")) # fill the remaining hints with junk junkHints = getHintGroup('junkHint', world) random.shuffle(junkHints) while stoneIDs: add_hint(world, stoneIDs.pop(0), junkHints.pop().text)
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()) 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, 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) 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 = '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: 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 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(world, messages): stoneIDs = [ 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A, 0x040B, 0x040C, 0x040D, 0x040E, 0x040F, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420 ] #shuffles the stone addresses for randomization, always locations will be placed first and twice random.shuffle(stoneIDs) spoilerHintsList.append('-Way of the Hero-') # add required items locations for hints (good hints) requiredSample = [] requiredSample1 = world.spoiler.required_locations #print([(location.parent_region.name, location.item.name) for location in requiredSample]) # loop through the locations we got for "always required locatiosns" # if you find lens, remove it for l in requiredSample1: if l.item.name in ['Master Sword', 'Stone of Agony']: continue else: requiredSample.append(l) #print([(location.parent_region.name, location.item.name) for location in requiredSample]) if len(requiredSample) >= 4: requiredSample = random.sample(requiredSample, 4) #Pick exactly 4 for location in requiredSample: for _ in range(0, 3): #and distribute each 3 times (12 / 32) if location.parent_region.dungeon: update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(location.parent_region.dungeon.name).text + \ " is on the way of the hero.")) spoilerHintsList.append(location.parent_region.dungeon.name + ': ' + location.item.name) #print(location.parent_region.dungeon.name, ': ', location.item.name) else: update_hint( messages, stoneIDs.pop(0), buildHintString(location.parent_region.name + " is on the way of the hero.")) spoilerHintsList.append(location.parent_region.name + ": " + location.item.name) #print(location.parent_region.name, ': ', location.item.name) # Don't repeat hints checkedLocations = [] spoilerHintsList.append('\n-Required Locations-') # Add required location hints alwaysLocations = getHintGroup('alwaysLocation', world) for hint in alwaysLocations: for locationWorld in world.get_locations(): if hint.name == locationWorld.name: checkedLocations.append(hint.name) for _ in range(0, 2): #populate each of these twice (24 / 32) update_hint(messages, stoneIDs.pop(0), getHint(locationWorld.name).text + " " + \ getHint(getItemGenericName(locationWorld.item)).text + ".") spoilerHintsList.append(locationWorld.name + ": " + locationWorld.item.name) ## spoilerHintsList.append('\n-Good Locations-') ## # Add good location hints ## sometimesLocations = getHintGroup('location', world) ## if sometimesLocations: ## # for _ in range(0, random.randint(9,10) - len(alwaysLocations)): ## for _ in range(0, 2): # Exactly 2 of these (26 / ## hint = random.choice(sometimesLocations) ## # Repick if location isn't new ## while hint.name in checkedLocations or hint.name in alwaysLocations: ## hint = random.choice(sometimesLocations) ## ## for locationWorld in world.get_locations(): ## if hint.name == locationWorld.name: ## checkedLocations.append(locationWorld.name) ## update_hint(messages, stoneIDs.pop(0), getHint(locationWorld.name).text + " " + \ ## getHint(getItemGenericName(locationWorld.item)).text + ".") ## spoilerHintsList.append(locationWorld.name + ': ' + locationWorld.item.name) ## spoilerHintsList.append('\n-BadItem Dungeon-') ## # add bad dungeon locations hints ## for dungeon in random.sample(world.dungeons, 2): # Exactly 2 of these (28 / 32) ## # Choose a randome dungeon location that is a non-dungeon item ## locationWorld = random.choice([location for region in dungeon.regions for location in world.get_region(region).locations ## if location.item.type != 'Event' and \ ## not location.name in eventlocations and \ ## not isDungeonItem(location.item) and \ ## (world.tokensanity != 'off' or location.item.name != 'Gold Skulltulla Token') and\ ## location.item.type != 'Song']) ## ## checkedLocations.append(locationWorld.name) ## update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(dungeon.name).text + \ ## " hoards " + getHint(getItemGenericName(locationWorld.item)).text + ".")) ## spoilerHintsList.append(dungeon.name + ': ' + locationWorld.item.name) ## ## spoilerHintsList.append('\n-BadItem Overworld-') ## # add bad overworld locations hints ## # only choose location if it is new and a proper item from the overworld ## overworldlocations = [locationWorld for locationWorld in world.get_locations() ## if not locationWorld.name in checkedLocations and \ ## not locationWorld.name in alwaysLocations and \ ## not locationWorld.name in sometimesLocations and \ ## locationWorld.item.type != 'Event' and \ ## not locationWorld.name in eventlocations and \ ## (world.tokensanity == 'all' or locationWorld.item.name != 'Gold Skulltulla Token') and \ ## not locationWorld.parent_region.dungeon and \ ## not locationWorld.name in checkedLocations] ## overworldSample = overworldlocations ## if len(overworldSample) >= 2: # Only need to check for 2 ## overworldSample = random.sample(overworldlocations, 2) # Exactly 2 of these (30 / 32) ## for locationWorld in overworldSample: ## checkedLocations.append(locationWorld.name) ## update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(getItemGenericName(locationWorld.item)).text + \ ## " can be found at " + locationWorld.parent_region.name + ".")) ## spoilerHintsList.append(locationWorld.parent_region.name + ': ' + locationWorld.item.name) #Populate 4 bad item hints spoilerHintsList.append('\n-Bad Items-') # add good item hints # only choose location if it is new and a good item if world.shuffle_weird_egg: gooditems.append('Weird Egg') baditemlocations = [locationWorld for locationWorld in world.get_locations() if not locationWorld.name in checkedLocations and \ not locationWorld.name in alwaysLocations and \ (world.tokensanity == 'all' or locationWorld.item.name != 'Gold Skulltulla Token') and \ locationWorld.item.type != 'Event' and \ not locationWorld.name in eventlocations and \ not isDungeonItem(locationWorld.item) and \ locationWorld.item.name not in gooditems] baditemSample = random.sample(baditemlocations, 4) # Don't need this check, we'll fill the rest from this pool, usually only 2, can be more in very rare cases # if len(gooditemSample) >= 5: # gooditemSample = random.sample(gooditemlocations, random.randint(3,5)) # for locationWorld in gooditemSample: for locationWorld in baditemSample: #locationWorld = gooditemSample.pop() checkedLocations.append(locationWorld.name) if locationWorld.parent_region.dungeon: update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(locationWorld.parent_region.dungeon.name).text + \ " hoards " + getHint(getItemGenericName(locationWorld.item)).text + ".")) spoilerHintsList.append(locationWorld.parent_region.dungeon.name + ': ' + locationWorld.item.name) else: update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(getItemGenericName(locationWorld.item)).text + \ " can be found at " + locationWorld.parent_region.name + ".")) spoilerHintsList.append(locationWorld.parent_region.name + ': ' + locationWorld.item.name) spoilerHintsList.append('\n-Good Items-') # add good item hints # only choose location if it is new and a good item if world.shuffle_weird_egg: gooditems.append('Weird Egg') gooditemlocations = [locationWorld for locationWorld in world.get_locations() if not locationWorld.name in checkedLocations and \ locationWorld.item.name in gooditems] gooditemSample = gooditemlocations # Don't need this check, we'll fill the rest from this pool, usually only 2, can be more in very rare cases # if len(gooditemSample) >= 5: # gooditemSample = random.sample(gooditemlocations, random.randint(3,5)) # for locationWorld in gooditemSample: random.shuffle(gooditemSample) while stoneIDs: locationWorld = gooditemSample.pop() checkedLocations.append(locationWorld.name) if locationWorld.parent_region.dungeon: update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(locationWorld.parent_region.dungeon.name).text + \ " hoards " + getHint(getItemGenericName(locationWorld.item)).text + ".")) spoilerHintsList.append(locationWorld.parent_region.dungeon.name + ': ' + locationWorld.item.name) else: update_hint(messages, stoneIDs.pop(0), buildHintString(getHint(getItemGenericName(locationWorld.item)).text + \ " can be found at " + locationWorld.parent_region.name + ".")) spoilerHintsList.append(locationWorld.parent_region.name + ': ' + locationWorld.item.name) #spoilerHintsList.append('\n-Junk-\n') f = open("hints.txt", "w") f.write('~~~ NEW HINTS ~~~\n\n') f.write('\n'.join(spoilerHintsList)) f.close()
def buildGossipHints(world, rom): stoneAddresses = [ 0x938e4c, 0x938EA8, 0x938F04, 0x938F60, 0x938FBC, 0x939018, 0x939074, 0x9390D0, 0x93912C, 0x939188, 0x9391E4, 0x939240, 0x93929C, 0x9392F8, 0x939354, 0x9393B0, 0x93940C, 0x939468, 0x9394C4, 0x939520, 0x93957C, 0x9395D8, 0x939634, 0x939690, 0x9396EC, 0x939748, 0x9397A4, 0x939800, 0x93985C, 0x9398B8, 0x939914, 0x939970 ] #address for gossip stone text boxes, byte limit is 92 alwaysLocations = getHintGroup( 'alwaysLocation' ) #These location will always have a hint somewhere in the world. sometimesSpace = (int( (len(stoneAddresses) - len(alwaysLocations) * 2) / 2)) sometimesLocations = getHintGroup( 'location' ) #A random selection of these locations will be in the hint pool. random.shuffle(sometimesLocations) sometimesLocations = sometimesLocations[0:sometimesSpace] hintList = alwaysLocations hintList.extend(alwaysLocations) hintList.extend(sometimesLocations) locationData = [] for hint in hintList: for locationWorld in world.get_locations(): if hint.name == locationWorld.name: locationData.extend([locationWorld]) #hopefully fixes weird VC error where the last character from a previous text box would sometimes spill over into the next box. for address in range(stoneAddresses[0], 0x9399D8): rom.write_byte(address, 0x08) #shuffles the stone addresses for randomization, always locations will be placed first and twice random.shuffle(stoneAddresses) #loops through shuffled locations and addresses and builds hint. while locationData: currentLoc = locationData.pop(0) Block_code = getBytes((getHint(currentLoc.name).text)) if currentLoc.item.type == 'Map' or currentLoc.item.type == 'Compass' or currentLoc.item.type == 'BossKey' or currentLoc.item.type == 'SmallKey': Block_code.extend(getBytes((getHint(currentLoc.item.type).text))) else: Block_code.extend(getBytes((getHint(currentLoc.item.name).text))) endText(Block_code) if len(Block_code) > 92: print('Too many characters in hint') Block_code = getBytes("I am Error.") Block_code.extend(getBytes(currentLoc.name)) Block_code.extend(getBytes('&')) Block_code.extend(getBytes(currentLoc.item.name)) rom.write_bytes(stoneAddresses.pop(0), Block_code) junkHints = getHintGroup('junkHint') random.shuffle(junkHints) while stoneAddresses: junkHint = junkHints.pop() Block_code = getBytes(junkHint.text) endText(Block_code) rom.write_bytes(stoneAddresses.pop(0), Block_code) return rom