def fill(self, window, worlds, location_pools, item_pools): search = Search.max_explore([world.state for world in worlds], itertools.chain.from_iterable(item_pools)) if not search.can_beat_game(False): raise FillError('Item pool does not contain items required to beat game!') for world_dist in self.world_dists: world_dist.fill(window, worlds, location_pools, item_pools)
def can_reach_stone(worlds, stone_location, location): if location == None: return True old_item = location.item location.item = None search = Search.max_explore([world.state for world in worlds]) location.item = old_item return (search.spot_access(stone_location) and search.state_list[location.world.id].guarantee_hint())
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 fill(self, window, worlds, location_pools, item_pools): world = worlds[self.id] locations = {} if self.locations: locations = { loc: self.locations[loc] for loc in random.sample(self.locations.keys(), len(self.locations)) } for starting_item in self.starting_items: for _ in range(self.starting_items[starting_item].count): try: if starting_item in item_groups['DungeonReward']: continue item = None if starting_item in item_groups['Bottle']: item = self.pool_replace_item(item_pools, "#Bottle", self.id, "#Junk", worlds) elif starting_item in item_groups['AdultTrade']: item = self.pool_replace_item(item_pools, "#AdultTrade", self.id, "#Junk", worlds) elif IsItem(starting_item): try: item = self.pool_replace_item( item_pools, starting_item, self.id, "#Junk", worlds) except KeyError: pass # If a normal item exceeds the item pool count, continue. except KeyError: raise RuntimeError( 'Started with too many "%s" in world %d, and not enough "%s" are available in the item pool to be removed.' % (starting_item, self.id + 1, starting_item)) if starting_item in item_groups['Song']: self.song_as_items = True # Update item_pool if item is not None: if item not in self.item_pool: self.item_pool[item.name] = ItemPoolRecord({ 'type': 'set', 'count': 1 }) else: self.item_pool[item.name].count += 1 item_pools[5].append(ItemFactory(item.name, world)) for (location_name, record) in pattern_dict_items(locations, world.itempool, []): if record.item is None: continue player_id = self.id if record.player is None else record.player - 1 location_matcher = lambda loc: loc.world.id == world.id and loc.name == location_name location = pull_first_element(location_pools, location_matcher) if location is None: try: location = LocationFactory(location_name) except KeyError: raise RuntimeError('Unknown location in world %d: %s' % (world.id + 1, location_name)) if location.type == 'Boss': continue elif location.name in world.disabled_locations: continue else: raise RuntimeError( 'Location already filled in world %d: %s' % (self.id + 1, location_name)) if record.item in item_groups['DungeonReward']: raise RuntimeError( 'Cannot place dungeon reward %s in world %d in location %s.' % (record.item, self.id + 1, location_name)) if record.item == '#Junk' and location.type == 'Song' and not world.shuffle_song_items: record.item = '#JunkSong' ignore_pools = None is_invert = pattern_matcher(record.item)('!') if is_invert and location.type != 'Song' and not world.shuffle_song_items: ignore_pools = [2] if is_invert and location.type == 'Song' and not world.shuffle_song_items: ignore_pools = [i for i in range(len(item_pools)) if i != 2] try: item = self.pool_remove_item(item_pools, record.item, 1, world_id=player_id, ignore_pools=ignore_pools)[0] except KeyError: if location.type == 'Shop' and "Buy" in record.item: try: self.pool_remove_item([item_pools[0]], "Buy *", 1, world_id=player_id) item = ItemFactory([record.item], world=world)[0] except KeyError: raise RuntimeError( 'Too many shop buy items were added to world %d, and not enough shop buy items are available in the item pool to be removed.' % (self.id + 1)) elif record.item in item_groups['Bottle']: try: item = self.pool_replace_item(item_pools, "#Bottle", player_id, record.item, worlds) except KeyError: raise RuntimeError( 'Too many bottles were added to world %d, and not enough bottles are available in the item pool to be removed.' % (self.id + 1)) elif record.item in item_groups['AdultTrade']: try: item = self.pool_replace_item(item_pools, "#AdultTrade", player_id, record.item, worlds) except KeyError: raise RuntimeError( 'Too many adult trade items were added to world %d, and not enough adult trade items are available in the item pool to be removed.' % (self.id + 1)) else: try: item = self.pool_replace_item(item_pools, "#Junk", player_id, record.item, worlds) except KeyError: raise RuntimeError( 'Too many items were added to world %d, and not enough junk is available to be removed.' % (self.id + 1)) # Update item_pool if item.name not in self.item_pool: self.item_pool[item.name] = ItemPoolRecord({ 'type': 'set', 'count': 1 }) else: self.item_pool[item.name].count += 1 except IndexError: raise RuntimeError( 'Unknown item %s being placed on location %s in world %d.' % (record.item, location, self.id + 1)) if record.price is not None and item.type != 'Shop': location.price = record.price world.shop_prices[location.name] = record.price if location.type == 'Song' and item.type != 'Song': self.song_as_items = True location.world.push_item(location, item, True) if item.advancement: search = Search.max_explore( [world.state for world in worlds], itertools.chain.from_iterable(item_pools)) if not search.can_beat_game(False): raise FillError( '%s in world %d is not reachable without %s in world %d!' % (location.name, self.id + 1, item.name, player_id + 1)) window.fillcount += 1 window.update_progress(5 + ( (window.fillcount / window.locationcount) * 30))
def fill(self, window, worlds, location_pools, item_pools): """Fills the world with restrictions defined in a plandomizer JSON file. :param window: :param worlds: A list of the world objects that define the rules of each game world. :param location_pools: A list containing all of the location pools. 0: Shop Locations 1: Song Locations 2: Fill locations :param item_pools: A list containing all of the item pools. 0: Shop Items 1: Dungeon Items 2: Songs 3: Progression Items 4: Priority Items 5: The rest of the Item pool """ world = worlds[self.id] locations = {} if self.locations: locations = {loc: self.locations[loc] for loc in random.sample(self.locations.keys(), len(self.locations))} used_items = [] for (location_name, record) in pattern_dict_items(locations, world.itempool, used_items): if record.item is None: continue player_id = self.id if record.player is None else record.player - 1 location_matcher = lambda loc: loc.world.id == world.id and loc.name.lower() == location_name.lower() location = pull_first_element(location_pools, location_matcher) if location is None: try: location = LocationFactory(location_name) except KeyError: raise RuntimeError('Unknown location in world %d: %s' % (world.id + 1, location_name)) if location.type == 'Boss': continue elif location.name in world.disabled_locations: continue else: raise RuntimeError('Location already filled in world %d: %s' % (self.id + 1, location_name)) if record.item in item_groups['DungeonReward']: raise RuntimeError('Cannot place dungeon reward %s in world %d in location %s.' % (record.item, self.id + 1, location_name)) if record.item == '#Junk' and location.type == 'Song' and not world.shuffle_song_items: record.item = '#JunkSong' ignore_pools = None is_invert = pattern_matcher(record.item)('!') if is_invert and location.type != 'Song' and not world.shuffle_song_items: ignore_pools = [2] if is_invert and location.type == 'Song' and not world.shuffle_song_items: ignore_pools = [i for i in range(len(item_pools)) if i != 2] try: if record.item == "#Bottle": try: item = self.pool_replace_item(item_pools, "#Bottle", player_id, record.item, worlds) # Update item_pool if item.name not in self.item_pool: self.item_pool[item.name] = ItemPoolRecord() else: self.item_pool[item.name].count += 1 except KeyError: raise RuntimeError( 'Too many bottles were added to world %d, and not enough bottles are available in the item pool to be removed.' % ( self.id + 1)) elif record.item == "#AdultTrade": try: item = self.pool_replace_item(item_pools, "#AdultTrade", player_id, record.item, worlds) # Update item_pool if item.name not in self.item_pool: self.item_pool[item.name] = ItemPoolRecord() else: self.item_pool[item.name].count += 1 except KeyError: raise RuntimeError( 'Too many adult trade items were added to world %d, and not enough adult trade items are available in the item pool to be removed.' % ( self.id + 1)) else: item = self.pool_remove_item(item_pools, record.item, 1, world_id=player_id, ignore_pools=ignore_pools)[0] except KeyError: if location.type == 'Shop' and "Buy" in record.item: try: self.pool_remove_item([item_pools[0]], "Buy *", 1, world_id=player_id) item = ItemFactory([record.item], world=world)[0] except KeyError: raise RuntimeError('Too many shop buy items were added to world %d, and not enough shop buy items are available in the item pool to be removed.' % (self.id + 1)) elif record.item in item_groups['Bottle']: try: item = self.pool_replace_item(item_pools, "#Bottle", player_id, record.item, worlds) except KeyError: raise RuntimeError('Too many bottles were added to world %d, and not enough bottles are available in the item pool to be removed.' % (self.id + 1)) elif record.item in item_groups['AdultTrade']: try: item = self.pool_replace_item(item_pools, "#AdultTrade", player_id, record.item, worlds) except KeyError: raise RuntimeError('Too many adult trade items were added to world %d, and not enough adult trade items are available in the item pool to be removed.' % (self.id + 1)) elif record.item == "Weird Egg": # If Letter has not been shown to guard before obtaining a second weird egg a softlock can occur # if there are important items at deku theater or an important location locked behind the gate # or if Keaton Mask gets overwritten before giving it to the guard. try: item = self.pool_replace_item(item_pools, "Weird Egg", player_id, record.item, worlds) except KeyError: raise RuntimeError('Weird Egg already placed in World %d.' % (self.id + 1)) else: try: item = self.pool_replace_item(item_pools, "#Junk", player_id, record.item, worlds) except KeyError: raise RuntimeError('Too many items were added to world %d, and not enough junk is available to be removed.' % (self.id + 1)) # Update item_pool if item.name not in self.item_pool: self.item_pool[item.name] = ItemPoolRecord() else: self.item_pool[item.name].count += 1 except IndexError: raise RuntimeError('Unknown item %s being placed on location %s in world %d.' % (record.item, location, self.id + 1)) if record.price is not None and item.type != 'Shop': location.price = record.price world.shop_prices[location.name] = record.price if location.type == 'Song' and item.type != 'Song': self.song_as_items = True location.world.push_item(location, item, True) if item.advancement: search = Search.max_explore([world.state for world in worlds], itertools.chain.from_iterable(item_pools)) if not search.can_beat_game(False): raise FillError('%s in world %d is not reachable without %s in world %d!' % (location.name, self.id + 1, item.name, player_id + 1)) window.fillcount += 1 window.update_progress(5 + ((window.fillcount / window.locationcount) * 30))
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)