def get_entrance_hint(spoiler, world, checked): if not world.entrance_shuffle: return None entrance_hints = list(filter(lambda hint: hint.name not in checked, getHintGroup('entrance', world))) shuffled_entrance_hints = list(filter(lambda entrance_hint: world.get_entrance(entrance_hint.name).shuffled, entrance_hints)) regions_with_hint = [hint.name for hint in getHintGroup('region', world)] valid_entrance_hints = list(filter(lambda entrance_hint: world.get_entrance(entrance_hint.name).connected_region.name in regions_with_hint, shuffled_entrance_hints)) if not valid_entrance_hints: return None entrance_hint = random.choice(valid_entrance_hints) entrance = world.get_entrance(entrance_hint.name) checked.add(entrance.name) entrance_text = entrance_hint.text if '#' not in entrance_text: entrance_text = '#%s#' % entrance_text connected_region = entrance.connected_region if connected_region.dungeon: region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text else: region_text = getHint(connected_region.name, world.clearer_hints).text if '#' not in region_text: region_text = '#%s#' % region_text return (GossipText('%s %s.' % (entrance_text, region_text), ['Light Blue', 'Green']), None)
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
def buildGanonText(world, messages): # empty now unused messages to make space for ganon lines update_message_by_id(messages, 0x70C8, " ") update_message_by_id(messages, 0x70C9, " ") update_message_by_id(messages, 0x70CA, " ") # lines before battle text = '\x08' ganonLines = getHintGroup('ganonLine', world) random.shuffle(ganonLines) text = get_raw_text(ganonLines.pop().text) update_message_by_id(messages, 0x70CB, text) # light arrow hint or validation chest item text = '\x08' if world.trials == 0: for location in world.get_locations(): if location.item.name == 'Light Arrows': text = get_raw_text( getHint('Light Arrow Location', world.clearer_hints).text) location_hint = location.hint.replace('Ganon\'s Castle', 'my castle') text += get_raw_text(location_hint) text += '!' break else: text = get_raw_text( getHint('Validation Line', world.clearer_hints).text) for location in world.get_locations(): if location.name == 'Ganons Tower Boss Key Chest': text += get_raw_text( getHint(getItemGenericName(location.item), world.clearer_hints).text) update_message_by_id(messages, 0x70CC, text)
def buildGanonText(world, messages): # empty now unused messages to make space for ganon lines update_message_by_id(messages, 0x70C8, " ") update_message_by_id(messages, 0x70C9, " ") update_message_by_id(messages, 0x70CA, " ") # lines before battle ganonLines = getHintGroup('ganonLine', world) random.shuffle(ganonLines) text = get_raw_text(ganonLines.pop().text) update_message_by_id(messages, 0x70CB, text) # light arrow hint or validation chest item text = get_raw_text( getHint('Light Arrow Location', world.clearer_hints).text) if world.distribution.get_starting_item('Light Arrows') > 0: text += "\x05\x42your pocket\x05\x40" else: location = world.light_arrow_location location_hint = get_hint_area(location).replace( 'Ganon\'s Castle', 'my castle') if world.id != location.world.id: text += "\x05\x42Player %d's\x05\x40 %s" % ( location.world.id + 1, get_raw_text(location_hint)) else: text += get_raw_text(location_hint) text += '!' update_message_by_id(messages, 0x70CC, text)
def buildGanonText(world, messages): # empty now unused messages to make space for ganon lines update_message_by_id(messages, 0x70C8, " ") update_message_by_id(messages, 0x70C9, " ") update_message_by_id(messages, 0x70CA, " ") # lines before battle ganonLines = getHintGroup('ganonLine', world) random.shuffle(ganonLines) text = get_raw_text(ganonLines.pop().text) update_message_by_id(messages, 0x70CB, text) # light arrow hint or validation chest item if world.trials == 0: text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text) if world.distribution.get_starting_item('Light Arrows') > 0: text += "\x05\x42your pocket\x05\x40" else: location = world.light_arrow_location location_hint = location.hint.replace('Ganon\'s Castle', 'my castle') if world.id != location.world.id: text += "\x05\x42Player %d's\x05\x40 %s" % (location.world.id +1, get_raw_text(location_hint)) else: text += get_raw_text(location_hint) text += '!' else: text = get_raw_text(getHint('Validation Line', world.clearer_hints).text) for location in world.get_filled_locations(): if location.name == 'Ganons Tower Boss Key Chest': text += get_raw_text(getHint(getItemGenericName(location.item), world.clearer_hints).text) text += '!' break update_message_by_id(messages, 0x70CC, text)
def get_entrance_hint(spoiler, world, checked): if world.entrance_shuffle == 'off': return None entrance_hints = getHintGroup('entrance', world) entrance_hints = list(filter(lambda hint: hint.name not in checked, entrance_hints)) valid_entrance_hints = [entrance_hint for entrance_hint in entrance_hints if world.get_entrance(entrance_hint.name).shuffled] if not valid_entrance_hints: return None entrance_hint = random.choice(valid_entrance_hints) entrance = world.get_entrance(entrance_hint.name) checked.append(entrance.name) entrance_text = entrance_hint.text if '#' not in entrance_text: entrance_text = '#%s#' % entrance_text connected_region = entrance.connected_region if connected_region.dungeon: region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text else: region_text = getHint(connected_region.name, world.clearer_hints).text if '#' not in region_text: region_text = '#%s#' % region_text return (GossipText('%s %s.' % (entrance_text, region_text), ['Light Blue', 'Green']), None)
def get_junk_hint(spoiler, world, checked): hints = getHintGroup('junkHint', world) hints = list(filter(lambda hint: hint.name not in checked, hints)) if not hints: return None hint = random.choice(hints) checked.append(hint.name) return (hint.text, None)
def get_junk_hint(spoiler, world, checked): hints = getHintGroup('junk', world) hints = list(filter(lambda hint: hint.name not in checked, hints)) if not hints: return None hint = random.choice(hints) checked.add(hint.name) return (GossipText(hint.text, prefix=''), None)
def get_good_loc_hint(spoiler, world, checked): locations = getHintGroup('location', world) locations = list(filter(lambda hint: hint.name not in checked, locations)) if not locations: return None hint = random.choice(locations) location = world.get_location(hint.name) checked.append(location.name) return (buildHintString(colorText(getHint(location.name, world.clearer_hints).text, 'Green') + " " + \ colorText(getHint(getItemGenericName(location.item), world.clearer_hints).text, 'Red') + "."), location)
def get_specific_hint(spoiler, world, checked, type): hintGroup = getHintGroup(type, world) hintGroup = list(filter(lambda hint: hint.name not in checked, hintGroup)) if not hintGroup: return None hint = random.choice(hintGroup) location = world.get_location(hint.name) checked.append(location.name) location_text = hint.text if '#' not in location_text: location_text = '#%s#' % location_text item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text return (GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']), location)
def get_good_loc_hint(spoiler, world, checked): locations = getHintGroup('location', world) locations = list(filter(lambda hint: hint.name not in checked, locations)) if not locations: return None hint = random.choice(locations) location = world.get_location(hint.name) checked.append(location.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 return (GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']), location)
def buildGanonText(world, rom): # reorganize text header files to make space for text rom.write_bytes(0xB884B1, [0x03, 0x41, 0xED]) rom.write_bytes(0xB884B9, [0x03, 0x41, 0xEE]) rom.write_bytes(0xB884C1, [0x03, 0x41, 0xEF]) rom.write_bytes(0xB884C9, [0x03, 0x42, 0x99]) # clear space for new text for address in range(0x9611EC, 0x961349): rom.write_byte(address, 0x08) Block_code = [] # lines before battle, 160 characters max ganonLines = getHintGroup('ganonLine') random.shuffle(ganonLines) Block_code = getBytes(ganonLines.pop().text) endText(Block_code) rom.write_bytes(0x9611F1, Block_code) if world.fast_ganon: for location in world.get_locations(): if location.item.name == 'Light Arrows': Block_code = getBytes(getHint('Light Arrow Location').text) Block_code.extend(getBytes(location.hint)) Block_code.extend(getBytes('!')) break endText(Block_code) else: Block_code = getBytes(getHint('Validation Line').text) for location in world.get_locations(): if location.name == 'Ganons Tower Boss Key Chest': Block_code.extend(getBytes((getHint(location.item.name).text))) endText(Block_code) rom.write_bytes(0x96129D, Block_code) return rom
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 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)
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 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 = 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 buildHints(world, rom): stoneAddresses = [ 0x938e4c, 0x938ea7, 0x938f02, 0x938f5d, 0x938fb8, 0x939013, 0x93906e, 0x9390c9, 0x939124, 0x93917f, 0x9391da, 0x939235, 0x939290, 0x9392eb, 0x939346, 0x9393a1, 0x9393fc, 0x939457, 0x9394b2, 0x93950d, 0x939568, 0x9395c3, 0x93961e, 0x939679, 0x9396d4, 0x93972f, 0x93978a, 0x9397e5, 0x939840, 0x93989b, 0x9398f6, 0x939951 ] #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]) #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
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.clearer_hints).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.clearer_hints).text, 'Green') + " " + \ colorText(getHint(getItemGenericName(locationWorld.item), world.clearer_hints).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.clearer_hints).text, 'Green') + " " + \ colorText(getHint(getItemGenericName(locationWorld.item), world.clearer_hints).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.clearer_hints).text, 'Green') + \ " hoards " + colorText(getHint(getItemGenericName(locationWorld.item), world.clearer_hints).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.clearer_hints).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.clearer_hints).text, 'Green') + \ " hoards " + colorText(getHint(getItemGenericName(locationWorld.item), world.clearer_hints).text, 'Red') + ".")) else: add_hint(world, stoneIDs.pop(0), buildHintString(colorText(getHint(getItemGenericName(locationWorld.item), world.clearer_hints).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)