def pattern_dict_items(pattern_dict, itempool=None, exhausted=None): for (key, value) in pattern_dict.items(): if hasattr(value, 'item') and isinstance(value.item, list): if itempool is not None: valid_items = [ item.name for item in itempool if item.name in value.item ] if exhausted is not None: [ valid_items.remove(item) for item in exhausted if item in valid_items ] else: valid_items = value.item if not valid_items and exhausted is None: continue elif not valid_items: value.item = random_choices(value.item)[0] else: value.item = random_choices(valid_items)[0] if exhausted is not None: exhausted.append(value.item) if is_pattern(key): pattern = lambda loc: pattern_matcher(key)(loc.name) for location in LocationIterator(pattern): yield (location.name, value) else: yield (key, value)
def pattern_dict_items(pattern_dict, itempool=None, used_items=None): for (key, value) in pattern_dict.items(): if hasattr(value, 'item') and isinstance(value.item, list): if itempool is not None: valid_items = [] pool_group_items = [] for item in itempool: if item.name in value.item: valid_items.append(item.name) else: for group in item_groups: if '#' + group in value.item: if item.name in item_groups[group]: pool_group_items.append(item.name) if used_items is not None: for used_item in used_items: if used_item in valid_items: valid_items.remove(used_item) else: for group in item_groups: if group == "AdultTrade": # Special handling for AdultTrade item if used_item in item_groups[group]: pool_group_items = [i for i in pool_group_items if i not in item_groups[group]] continue for item in item_groups[group]: if '#' + group in value.item and used_item == item and item in pool_group_items: pool_group_items.remove(item) break valid_items.extend(pool_group_items) else: valid_items = value.item if not valid_items and used_items is None: continue elif not valid_items and used_items is not None: limited_items = ['Weird Egg', '#AdultTrade', '#Bottle'] value.item = [v for v in value.item if (v not in limited_items and v not in item_groups['AdultTrade'] and v not in item_groups['Bottle'])] value.item = random_choices(value.item)[0] else: value.item = random_choices(valid_items)[0] if used_items is not None: used_items.append(value.item) elif used_items is not None: used_items.append(value.item) if is_pattern(key): pattern = lambda loc: pattern_matcher(key)(loc.name) for location in LocationIterator(pattern): yield(location.name, value) else: yield (key, value)
def __init__(self, rand_song=True, piece_size=3, extra_position='none', starting_range=range(0, 5), activation_transform=identity, playback_transform=identity, activation=None): if activation: self.length = len(activation) self.activation = activation self.playback = fast_playback(self.activation) self.break_repeated_notes(0x03) self.format_playback_data() self.increase_duration_to(45) return if rand_song: self.length = random.randint(4, 8) self.activation = random_choices(range(0, 5), k=self.length) self.playback = random_playback(self.activation) else: if extra_position != 'none': piece_size = 3 piece = random_piece(piece_size, starting_range) self.two_piece_playback(piece, extra_position, activation_transform, playback_transform) self.break_repeated_notes() self.format_activation_data() self.format_playback_data()
def get_junk_item(count=1, pool=None, plando_pool=None): if count < 1: raise ValueError( "get_junk_item argument 'count' must be greater than 0.") return_pool = [] if pending_junk_pool: pending_count = min(len(pending_junk_pool), count) return_pool = [pending_junk_pool.pop() for _ in range(pending_count)] count -= pending_count if pool and plando_pool: jw_list = [(junk, weight) for (junk, weight) in junk_pool if junk not in plando_pool or pool.count(junk) < plando_pool[junk].count] try: junk_items, junk_weights = zip(*jw_list) except ValueError: raise RuntimeError( "Not enough junk is available in the item pool to replace removed items." ) else: junk_items, junk_weights = zip(*junk_pool) return_pool.extend( random_choices(junk_items, weights=junk_weights, k=count)) return return_pool
def resolve_random_settings(self): for info in setting_infos: if 'randomize_key' in info.gui_params and self.__dict__[ info.gui_params['randomize_key']]: choices, weights = zip(*info.gui_params['distribution']) self.__dict__[info.name] = random_choices(choices, weights=weights)[0]
def get_barren_hint(spoiler, world, checked): if not hasattr(world, 'get_barren_hint_prev'): world.get_barren_hint_prev = RegionRestriction.NONE areas = list( filter( lambda area: area not in checked and not (world.barren_dungeon >= world.hint_dist_user[ 'dungeons_barren_limit'] and world.empty_areas[area]['dungeon'] ), world.empty_areas.keys())) if not areas: return None # Randomly choose between overworld or dungeon dungeon_areas = list( filter(lambda area: world.empty_areas[area]['dungeon'], areas)) overworld_areas = list( filter(lambda area: not world.empty_areas[area]['dungeon'], areas)) if not dungeon_areas: # no dungeons left, default to overworld world.get_barren_hint_prev = RegionRestriction.OVERWORLD elif not overworld_areas: # no overworld left, default to dungeons world.get_barren_hint_prev = RegionRestriction.DUNGEON else: if world.get_barren_hint_prev == RegionRestriction.NONE: # 50/50 draw on the first hint world.get_barren_hint_prev = random.choices( [RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.5, 0.5])[0] elif world.get_barren_hint_prev == RegionRestriction.DUNGEON: # weights 75% against drawing dungeon again world.get_barren_hint_prev = random.choices( [RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.25, 0.75])[0] elif world.get_barren_hint_prev == RegionRestriction.OVERWORLD: # weights 75% against drawing overworld again world.get_barren_hint_prev = random.choices( [RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.75, 0.25])[0] if world.get_barren_hint_prev == RegionRestriction.DUNGEON: areas = dungeon_areas else: areas = overworld_areas if not areas: return None area_weights = [world.empty_areas[area]['weight'] for area in areas] area = random_choices(areas, weights=area_weights)[0] if world.empty_areas[area]['dungeon']: world.barren_dungeon += 1 checked.add(area) return (GossipText("plundering #%s# is a foolish choice." % area, ['Pink']), None)
def update_seed(self, seed): if seed is None or seed == '': # https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits-in-python self.seed = ''.join(random_choices(string.ascii_uppercase + string.digits, k=10)) else: self.seed = seed self.sanitize_seed() self.numeric_seed = self.get_numeric_seed()
def resolve_random_settings(self): sorted_infos = list(setting_infos) sort_key = lambda info: 0 if info.dependency is None else 1 sorted_infos.sort(key=sort_key) for info in sorted_infos: if not self.check_dependency(info.name, check_random=False): continue if 'randomize_key' in info.gui_params and self.__dict__[ info.gui_params['randomize_key']]: choices, weights = zip(*info.gui_params['distribution']) self.__dict__[info.name] = random_choices(choices, weights=weights)[0]
def get_junk_item(count=1): if count < 1: raise ValueError("get_junk_item argument 'count' must be greater than 0.") return_pool = [] if pending_junk_pool: pending_count = min(len(pending_junk_pool), count) return_pool = [pending_junk_pool.pop() for _ in range(pending_count)] count -= pending_count junk_items, junk_weights = zip(*junk_pool) return_pool.extend(random_choices(junk_items, weights=junk_weights, k=count)) return return_pool
def get_random_song(): rand_song = random_choices([True, False], [1, 9])[0] piece_size = random_choices([3, 4], [5, 2])[0] extra_position = random_choices(['none', 'start', 'middle', 'end'], [12, 1, 1, 1])[0] activation_transform = identity playback_transform = identity weight_damage = 0 should_transpose = random_choices([True, False], [1, 4])[0] starting_range = range(0, 5) if should_transpose: weight_damage = 2 direction = random_choices(['up', 'down'], [1, 1])[0] if direction == 'up': starting_range = range(0, 4) activation_transform = transpose_piece(1) elif direction == 'down': starting_range = range(1, 5) activation_transform = transpose_piece(-1) should_invert = random_choices([True, False], [3 - weight_damage, 6])[0] if should_invert: weight_damage += 1 activation_transform = compose(invert_piece, activation_transform) should_reflect = random_choices([True, False], [5 - weight_damage, 4])[0] if should_reflect: activation_transform = compose(reverse_piece, activation_transform) playback_transform = reverse_piece # print([rand_song, piece_size, extra_position, starting_range, should_transpose, should_invert, should_reflect]) song = Song(rand_song, piece_size, extra_position, starting_range, activation_transform, playback_transform) # rate its difficulty difficulty = 0 difficulty = piece_size * 12 if extra_position != 'none': difficulty += 12 if should_transpose: difficulty += 25 if should_reflect: difficulty += 10 if should_invert: difficulty += 20 if rand_song: difficulty = 11 * len(song.activation) song.difficulty = difficulty return song
def resolve_random_settings(self, cosmetic): sorted_infos = list(setting_infos) sort_key = lambda info: 0 if info.dependency is None else 1 sorted_infos.sort(key=sort_key) for info in sorted_infos: # only randomize cosmetics options or non-cosmetic if cosmetic == info.shared: continue if self.check_dependency(info.name, check_random=True): continue if 'randomize_key' in info.gui_params and self.__dict__[info.gui_params['randomize_key']]: choices, weights = zip(*info.gui_params['distribution']) self.__dict__[info.name] = random_choices(choices, weights=weights)[0]
def __init__(self, settings_dict): self.__dict__.update(settings_dict) for info in setting_infos: if info.name not in self.__dict__: if info.type == bool: if info.gui_params is not None and 'default' in info.gui_params: self.__dict__[info.name] = True if info.gui_params[ 'default'] == 'checked' else False else: self.__dict__[info.name] = False if info.type == str: if 'default' in info.args_params: self.__dict__[info.name] = info.args_params['default'] elif info.gui_params is not None and 'default' in info.gui_params: if 'options' in info.gui_params and isinstance( info.gui_params['options'], dict): self.__dict__[info.name] = info.gui_params[ 'options'][info.gui_params['default']] else: self.__dict__[ info.name] = info.gui_params['default'] else: self.__dict__[info.name] = "" if info.type == int: if 'default' in info.args_params: self.__dict__[info.name] = info.args_params['default'] elif info.gui_params is not None and 'default' in info.gui_params: self.__dict__[info.name] = info.gui_params['default'] else: self.__dict__[info.name] = 1 if info.type == list: if 'default' in info.args_params: self.__dict__[info.name] = list( info.args_params['default']) elif info.gui_params is not None and 'default' in info.gui_params: self.__dict__[info.name] = list( info.gui_params['default']) else: self.__dict__[info.name] = [] self.settings_string = self.get_settings_string() if (self.seed is None): # https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits-in-python self.seed = ''.join( random_choices(string.ascii_uppercase + string.digits, k=10)) self.sanitize_seed() self.numeric_seed = self.get_numeric_seed()
def get_barren_hint(spoiler, world, checked): areas = list(filter(lambda area: area not in checked and \ not (world.barren_dungeon and world.empty_areas[area]['dungeon']), world.empty_areas.keys())) if not areas: return None area_weights = [world.empty_areas[area]['weight'] for area in areas] area = random_choices(areas, weights=area_weights)[0] if world.empty_areas[area]['dungeon']: world.barren_dungeon = True checked.append(area) return (GossipText("plundering #%s# is a foolish choice." % area, ['Pink']), None)
def resolve_random_settings(self, cosmetic, randomize_key=None): sorted_infos = list(setting_infos) sort_key = lambda info: 0 if info.dependency is None else 1 sorted_infos.sort(key=sort_key) randomize_keys_enabled = set() for info in sorted_infos: # only randomize cosmetics options or non-cosmetic if cosmetic == info.shared: continue if self.check_dependency(info.name, check_random=True): continue if 'randomize_key' not in info.gui_params: continue if randomize_key is not None and info.gui_params[ 'randomize_key'] != randomize_key: continue # Make sure the setting is meant to be randomized and not specified in distribution # We that check it's not specified in the distribution so that plando can override randomized settings not_in_dist = '_settings' not in self.distribution.src_dict or info.name not in self.distribution.src_dict[ '_settings'].keys() if self.__dict__[info.gui_params['randomize_key']] and not_in_dist: randomize_keys_enabled.add(info.gui_params['randomize_key']) choices, weights = zip(*info.gui_params['distribution']) self.__dict__[info.name] = random_choices(choices, weights=weights)[0] # Second pass to make sure disabled settings are set properly. # Stupid hack: disable randomize keys, then re-enable. for randomize_keys in randomize_keys_enabled: self.__dict__[randomize_keys] = False for info in sorted_infos: if cosmetic == info.shared: continue dependency = self.get_dependency(info.name, check_random=False) if dependency is None: continue self.__dict__[info.name] = dependency for randomize_keys in randomize_keys_enabled: self.__dict__[randomize_keys] = True
def get_barren_hint(spoiler, world, checked): areas = list(filter(lambda area: area not in checked and \ not (world.barren_dungeon and world.empty_areas[area]['dungeon']), world.empty_areas.keys())) if not areas: return None area_weights = [world.empty_areas[area]['weight'] for area in areas] area = random_choices(areas, weights=area_weights)[0] if world.empty_areas[area]['dungeon']: world.barren_dungeon = True checked.append(area) return ( buildHintString(colorText(area, 'Pink') + " is barren of treasure."), None)
def pool_add_item(self, pool, item_name, count): if item_name == '#Junk': added_items = get_junk_item(count, pool=pool, plando_pool=self.item_pool) elif is_pattern(item_name): add_matcher = lambda item: pattern_matcher(item_name)(item.name) candidates = [ item.name for item in ItemIterator(predicate=add_matcher) if item.name not in self.item_pool or self.item_pool[item.name].count != 0 ] # Only allow items to be candidates if they haven't been set to 0 if len(candidates) == 0: raise RuntimeError("Unknown item, or item set to 0 in the item pool could not be added: " + item_name) added_items = random_choices(candidates, k=count) else: if not IsItem(item_name): raise RuntimeError("Unknown item could not be added: " + item_name) added_items = [item_name] * count for item in added_items: pool.append(item) return added_items
def pool_add_item(self, pool, item_name, count): added_items = [] if item_name == '#Junk': added_items = get_junk_item(count) elif is_pattern(item_name): add_matcher = lambda item: pattern_matcher(item_name)(item.name) candidates = [ item.name for item in ItemIterator(predicate=add_matcher) ] if len(candidates) == 0: raise RuntimeError("Unknown item could not be added: " + item_name) added_items = random_choices(candidates, k=count) else: if not IsItem(item_name): raise RuntimeError("Unknown item could not be added: " + item_name) added_items = [item_name] * count for item in added_items: pool.append(item) return added_items
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 get_junk_item(count=1): junk_items, junk_weights = zip(*junk_pool) return random_choices(junk_items, weights=junk_weights, k=count)
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 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 random_piece(count, allowed=range(0,5)): return random_choices(allowed, k=count)