def distances_to_node( world_list: WorldList, starting_node: Node, *, ignore_elevators: bool = True, cutoff: Optional[int] = None, patches: Optional[GamePatches] = None, ) -> Dict[Area, int]: """ Compute the shortest distance from a node to all reachable areas. :param world_list: :param starting_node: :param ignore_elevators: :param cutoff: Exclude areas with a length longer that cutoff. :param patches: :return: Dict keyed by area to shortest distance to starting_node. """ import networkx g = networkx.DiGraph() dock_connections = patches.dock_connection if patches is not None else {} elevator_connections: ElevatorConnection = patches.elevator_connection if patches is not None else {} for area in world_list.all_areas: g.add_node(area) for world in world_list.worlds: for area in world.areas: new_areas = set() for node in area.nodes: connection = None if isinstance(node, DockNode): connection = dock_connections.get( world_list.identifier_for_node(node), node.default_connection).area_identifier elif isinstance(node, TeleporterNode) and not ignore_elevators: connection = elevator_connections.get( world_list.identifier_for_node(node), node.default_connection) if connection is not None: new_areas.add(world_list.area_by_area_location(connection)) for next_area in new_areas: g.add_edge(area, next_area) return networkx.single_source_shortest_path_length( g, world_list.nodes_to_area(starting_node), cutoff)
def _get_nodes_by_teleporter_id( world_list: WorldList) -> Dict[NodeIdentifier, TeleporterNode]: return { world_list.identifier_for_node(node): node for node in world_list.all_nodes if isinstance(node, TeleporterNode) and node.editable }
def fill_unassigned_hints(self, patches: GamePatches, world_list: WorldList, rng: Random, scan_asset_initial_pickups: dict[NodeIdentifier, frozenset[PickupIndex]], ) -> GamePatches: new_hints = copy.copy(patches.hints) # Get all LogbookAssets from the WorldList potential_hint_locations: set[NodeIdentifier] = { world_list.identifier_for_node(node) for node in world_list.iterate_nodes() if isinstance(node, LogbookNode) } for logbook in potential_hint_locations: if logbook not in scan_asset_initial_pickups: scan_asset_initial_pickups[logbook] = frozenset() # But remove these that already have hints potential_hint_locations -= patches.hints.keys() # We try our best to not hint the same thing twice hinted_indices: set[PickupIndex] = {hint.target for hint in patches.hints.values() if hint.target is not None} # Get interesting items to place hints for possible_indices: set[PickupIndex] = { index for index, target in patches.pickup_assignment.items() if self.interesting_pickup_to_hint(target.pickup) } possible_indices -= hinted_indices debug.debug_print("fill_unassigned_hints had {} decent indices for {} hint locations".format( len(possible_indices), len(potential_hint_locations))) if debug.debug_level() > 1: print(f"> Num pickups per asset:") for asset, pickups in scan_asset_initial_pickups.items(): print(f"* {asset}: {len(pickups)} pickups") print("> Done.") all_pickup_indices = [ node.pickup_index for node in world_list.iterate_nodes() if isinstance(node, PickupNode) ] rng.shuffle(all_pickup_indices) # If there isn't enough indices, use unhinted non-majors placed by generator if (num_indices_needed := len(potential_hint_locations) - len(possible_indices)) > 0: potential_indices = [ index for index in all_pickup_indices if index not in possible_indices and index not in hinted_indices ] debug.debug_print( f"Had only {len(possible_indices)} hintable indices, but needed {len(potential_hint_locations)}." f" Found {len(potential_indices)} less desirable locations.") possible_indices |= set(potential_indices[:num_indices_needed])
def create_elevator_database(world_list: WorldList, all_teleporters: List[NodeIdentifier], ) -> Tuple[ElevatorHelper, ...]: """ Creates a tuple of Elevator objects, exclude those that belongs to one of the areas provided. :param world_list: :param all_teleporters: Set of teleporters to use :return: """ all_helpers = [ ElevatorHelper(world_list.identifier_for_node(node), node.default_connection) for world, area, node in world_list.all_worlds_areas_nodes if isinstance(node, TeleporterNode) ] return tuple( helper for helper in all_helpers if helper.teleporter in all_teleporters )
def create_patches_hints( all_patches: Dict[int, GamePatches], players_config: PlayersConfiguration, world_list: WorldList, namer: HintNamer, rng: Random, ) -> list: exporter = HintExporter(namer, rng, JOKE_HINTS) hints_for_asset: dict[NodeIdentifier, str] = {} for identifier, hint in all_patches[ players_config.player_index].hints.items(): hints_for_asset[identifier] = exporter.create_message_for_hint( hint, all_patches, players_config, True) return [ create_simple_logbook_hint( logbook_node.string_asset_id, hints_for_asset.get(world_list.identifier_for_node(logbook_node), "Someone forgot to leave a message."), ) for logbook_node in world_list.iterate_nodes() if isinstance(logbook_node, LogbookNode) ]
def fill_unassigned_hints( self, patches: GamePatches, world_list: WorldList, rng: Random, scan_asset_initial_pickups: dict[NodeIdentifier, frozenset[PickupIndex]], ) -> GamePatches: new_hints = copy.copy(patches.hints) # Get all LogbookAssets from the WorldList potential_hint_locations: set[NodeIdentifier] = { world_list.identifier_for_node(node) for node in world_list.all_nodes if isinstance(node, LogbookNode) } for logbook in potential_hint_locations: if logbook not in scan_asset_initial_pickups: scan_asset_initial_pickups[logbook] = frozenset() # But remove these that already have hints potential_hint_locations -= patches.hints.keys() # Get interesting items to place hints for possible_indices = set(patches.pickup_assignment.keys()) possible_indices -= { hint.target for hint in patches.hints.values() if hint.target is not None } possible_indices -= { index for index in possible_indices if not self.interesting_pickup_to_hint( patches.pickup_assignment[index].pickup) } debug.debug_print( "fill_unassigned_hints had {} decent indices for {} hint locations" .format(len(possible_indices), len(potential_hint_locations))) if debug.debug_level() > 1: print(f"> Num pickups per asset:") for asset, pickups in scan_asset_initial_pickups.items(): print(f"* {asset}: {len(pickups)} pickups") print("> Done.") # But if we don't have enough hints, just pick randomly from everything if len(possible_indices) < len(potential_hint_locations): possible_indices = { node.pickup_index for node in world_list.all_nodes if isinstance(node, PickupNode) } # Get an stable order ordered_possible_indices = list(sorted(possible_indices)) ordered_potential_hint_locations = list( sorted(potential_hint_locations)) num_logbooks: dict[PickupIndex, int] = { index: sum(1 for indices in scan_asset_initial_pickups.values() if index in indices) for index in ordered_possible_indices } max_seen = max(num_logbooks.values()) if num_logbooks else 0 pickup_indices_weight: dict[PickupIndex, int] = { index: max_seen - num_logbook for index, num_logbook in num_logbooks.items() } # Ensure all indices are present with at least weight 0 for index in ordered_possible_indices: if index not in pickup_indices_weight: pickup_indices_weight[index] = 0 for logbook in sorted(ordered_potential_hint_locations, key=lambda r: len(scan_asset_initial_pickups[r]), reverse=True): try: new_index = random_lib.select_element_with_weight( pickup_indices_weight, rng) except StopIteration: # If everything has weight 0, then just choose randomly. new_index = random_lib.random_key(pickup_indices_weight, rng) del pickup_indices_weight[new_index] new_hints[logbook] = Hint(HintType.LOCATION, None, new_index) debug.debug_print( f"Added hint at {logbook} for item at {new_index}") return dataclasses.replace(patches, hints=new_hints)