def _area_name(world_list: WorldList, pickup_node: PickupNode, hide_world: bool) -> str: area = world_list.nodes_to_area(pickup_node) if hide_world: return area.name else: return world_list.area_name(area)
def _name_for_location(world_list: WorldList, location: AreaIdentifier) -> str: loc = location.as_tuple if loc in prime1_elevators.RANDOM_PRIME_CUSTOM_NAMES and loc != ( "Frigate Orpheon", "Exterior Docking Hangar"): return prime1_elevators.RANDOM_PRIME_CUSTOM_NAMES[loc] else: return world_list.area_name(world_list.area_by_area_location(location), separator=":")
def test_connections_from_dock_blast_shield(empty_patches): # Setup trivial = Requirement.trivial() req_1 = ResourceRequirement.simple( SimpleResourceInfo(0, "Ev1", "Ev1", ResourceType.EVENT)) req_2 = ResourceRequirement.simple( SimpleResourceInfo(1, "Ev2", "Ev2", ResourceType.EVENT)) dock_type = DockType("Type", "Type", frozendict()) weak_1 = DockWeakness(0, "Weak 1", frozendict(), req_1, None) weak_2 = DockWeakness(1, "Weak 2", frozendict(), trivial, DockLock(DockLockType.FRONT_BLAST_BACK_BLAST, req_2)) node_1_identifier = NodeIdentifier.create("W", "Area 1", "Node 1") node_2_identifier = NodeIdentifier.create("W", "Area 2", "Node 2") node_1 = DockNode(node_1_identifier, 0, False, None, "", ("default", ), {}, dock_type, node_2_identifier, weak_1, None, None) node_1_lock = DockLockNode.create_from_dock(node_1, 1) node_2 = DockNode(node_2_identifier, 2, False, None, "", ("default", ), {}, dock_type, node_1_identifier, weak_2, None, None) node_2_lock = DockLockNode.create_from_dock(node_2, 3) area_1 = Area("Area 1", None, True, [node_1, node_1_lock], {}, {}) area_2 = Area("Area 2", None, True, [node_2, node_2_lock], {}, {}) world = World("W", [area_1, area_2], {}) world_list = WorldList([world]) world_list.ensure_has_node_cache() game_mock = MagicMock() game_mock.world_list = world_list patches = dataclasses.replace(empty_patches, game=game_mock) context = NodeContext( patches=patches, current_resources=ResourceCollection(), database=patches.game.resource_database, node_provider=world_list, ) # Run result_1 = list(node_1.connections_from(context)) result_2 = list(node_2.connections_from(context)) # Assert simple = ResourceRequirement.simple assert result_1 == [ (node_2, RequirementAnd( [req_1, simple(NodeResourceInfo.from_node(node_2, context))])), (node_1_lock, RequirementAnd([trivial, req_2])), ] assert result_2 == [ (node_1, simple(NodeResourceInfo.from_node(node_2, context))), (node_2_lock, req_2), ]
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 add_relative_hint(self, world_list: WorldList, patches: GamePatches, rng: Random, target: PickupIndex, target_precision: HintItemPrecision, relative_type: HintLocationPrecision, precise_distance: bool, precision: Union[HintItemPrecision, HintRelativeAreaName], max_distance: int, ) -> Optional[Hint]: """ Creates a relative hint. :return: Might be None, if no hint could be created. """ target_node = node_search.pickup_index_to_node(world_list, target) target_area = world_list.nodes_to_area(target_node) distances = node_search.distances_to_node(world_list, target_node, patches=patches, cutoff=max_distance) def _major_pickups(area: Area) -> Iterator[PickupIndex]: for index in area.pickup_indices: t = patches.pickup_assignment.get(index) # FIXME: None should be ok, but this must be called after junk has been filled if t is not None: cat = t.pickup.item_category if cat.is_major or (not cat.is_expansion and target_precision == HintItemPrecision.DETAILED): yield index area_choices = { area: 1 / max(distance, 2) for area, distance in distances.items() if (distance > 0 and area.in_dark_aether == target_area.in_dark_aether and (relative_type == HintLocationPrecision.RELATIVE_TO_AREA or _not_empty(_major_pickups(area)))) } if not area_choices: return None area = random_lib.select_element_with_weight(dict(sorted(area_choices.items(), key=lambda a: a[0].name)), rng) distance_offset = None if not precise_distance: distance_offset = max_distance - distances[area] if relative_type == HintLocationPrecision.RELATIVE_TO_AREA: relative = RelativeDataArea(distance_offset, world_list.identifier_for_area(area), precision) elif relative_type == HintLocationPrecision.RELATIVE_TO_INDEX: relative = RelativeDataItem(distance_offset, rng.choice(list(_major_pickups(area))), precision) else: raise ValueError(f"Invalid relative_type: {relative_type}") precision_pair = PrecisionPair(relative_type, target_precision, include_owner=False, relative=relative) return Hint(HintType.LOCATION, precision_pair, target)
def _calculate_dangerous_resources_in_db( wl: WorldList, db: DockWeaknessDatabase, database: ResourceDatabase, ) -> Iterator[ResourceInfo]: for dock_type in db.dock_types: for dock_weakness in db.weaknesses[dock_type].values(): yield from wl.open_requirement_for(dock_weakness).as_set( database).dangerous_resources if dock_weakness.lock is not None: yield from wl.lock_requirement_for(dock_weakness).as_set( database).dangerous_resources
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() if patches is None: def get_elevator_connection_for(n: TeleporterNode): return n.default_connection def get_dock_connection_for(n: DockNode): return n.default_connection else: get_elevator_connection_for = patches.get_elevator_connection_for def get_dock_connection_for(n: DockNode): return patches.get_dock_connection_for(n).identifier 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 = get_dock_connection_for(node).area_identifier elif isinstance(node, TeleporterNode) and not ignore_elevators: connection = get_elevator_connection_for(node) 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 write_world_list(world_list: WorldList) -> list: errors = [] known_indices = {} worlds = [] for world in world_list.worlds: try: worlds.append(write_world(world)) for node in world.all_nodes: if isinstance(node, PickupNode): name = world_list.node_name(node, with_world=True, distinguish_dark_aether=True) if node.pickup_index in known_indices: errors.append(f"{name} has {node.pickup_index}, " f"but it was already used in {known_indices[node.pickup_index]}") else: known_indices[node.pickup_index] = name except ValueError as e: errors.append(str(e)) if errors: raise ValueError("\n\n".join(errors)) return worlds
def _pickup_assignment_to_item_locations( world_list: WorldList, pickup_assignment: PickupAssignment, num_players: int, ) -> Dict[str, Dict[str, str]]: items_locations: DefaultDict[str, Dict[str, str]] = collections.defaultdict(dict) for world, area, node in world_list.all_worlds_areas_nodes: if not node.is_resource_node or not isinstance(node, PickupNode): continue if node.pickup_index in pickup_assignment: target = pickup_assignment[node.pickup_index] if num_players > 1: item_name = f"{target.pickup.name} for Player {target.player + 1}" else: item_name = f"{target.pickup.name}" else: item_name = _ETM_NAME world_name = world.dark_name if area.in_dark_aether else world.name items_locations[world_name][world_list.node_name(node)] = item_name return { world: {area: item for area, item in sorted(items_locations[world].items())} for world in sorted(items_locations.keys()) }
def test_calculate_dangerous_resources(danger_a, danger_b, expected_result): set_a: Requirement = MagicMock() set_b: Requirement = MagicMock() set_a.as_set.return_value.dangerous_resources = danger_a set_b.as_set.return_value.dangerous_resources = danger_b n1 = MagicMock() n1.node_index = 0 n2 = MagicMock() n2.node_index = 1 n3 = MagicMock() n3.node_index = 2 n4 = MagicMock() n4.node_index = 3 area_a = Area("area_a", 0, True, [n1, n2], {n1: {n2: set_a}, n2: {}}, {}) area_b = Area("area_b", 0, True, [n3, n4], {n3: {}, n4: {n3: set_b}}, {}) world = World("W", [area_a, area_b], {}) wl = WorldList([world]) # Run result = game_description._calculate_dangerous_resources_in_areas(wl, None) # Assert assert set(result) == set(expected_result)
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 _area_name_to_area_location(world_list: WorldList, area_name: str) -> AreaIdentifier: world_name, area_name = re.match("([^/]+)/([^/]+)", area_name).group(1, 2) # Filter out dark world names world_name = world_list.world_with_name(world_name).name return AreaIdentifier(world_name, area_name)
def _calculate_dangerous_resources_in_areas( wl: WorldList, database: ResourceDatabase, ) -> Iterator[ResourceInfo]: for area in wl.all_areas: for node in area.nodes: for _, requirement in wl.area_connections_from(node): yield from requirement.as_set(database).dangerous_resources
def _area_identifier_to_json(world_list: WorldList, identifier: AreaIdentifier) -> dict: world = world_list.world_by_area_location(identifier) area = world.area_by_identifier(identifier) return { "world_asset_id": world.extra['asset_id'], "area_asset_id": area.extra['asset_id'], }
def _get_elevator_or_area_name(custom_names_to_use: dict[RandovaniaGame, dict[tuple[str, str], str]], game: RandovaniaGame, world_list: WorldList, area_location: AreaIdentifier, include_world_name: bool) -> str: custom_names_by_game = custom_names_to_use.get(game, {}) area_loc = area_location.as_tuple if area_loc in custom_names_by_game: return custom_names_by_game[area_loc] else: area = world_list.area_by_area_location(area_location) if include_world_name: return world_list.area_name(area) else: return area.name
def _assign_remaining_items( rng: Random, world_list: WorldList, pickup_assignment: PickupAssignment, remaining_items: List[PickupEntry], randomization_mode: RandomizationMode, ) -> PickupAssignment: """ :param rng: :param world_list: :param pickup_assignment: :param remaining_items: :return: """ unassigned_pickup_nodes = list( filter_unassigned_pickup_nodes(world_list.iterate_nodes(), pickup_assignment)) num_etm = len(unassigned_pickup_nodes) - len(remaining_items) if num_etm < 0: raise InvalidConfiguration( "Received {} remaining items, but there's only {} unassigned pickups" .format(len(remaining_items), len(unassigned_pickup_nodes))) # Shuffle the items to add and the spots to choose from rng.shuffle(remaining_items) rng.shuffle(unassigned_pickup_nodes) assignment = {} if randomization_mode is RandomizationMode.MAJOR_MINOR_SPLIT: remaining_majors = [ item for item in remaining_items if not item.is_expansion ] + ([None] * num_etm) unassigned_major_locations = [ pickup_node for pickup_node in unassigned_pickup_nodes if pickup_node.major_location ] for pickup_node, item in zip(unassigned_major_locations, remaining_majors): if item is not None: assignment[pickup_node.pickup_index] = item remaining_items.remove(item) unassigned_pickup_nodes.remove(pickup_node) assignment.update({ pickup_node.pickup_index: item for pickup_node, item in zip(unassigned_pickup_nodes, remaining_items) }) return assignment
def test_basic_search_with_translator_gate(has_translator: bool, echoes_resource_database, echoes_game_patches): # Setup scan_visor = echoes_resource_database.get_item("DarkVisor") nc = functools.partial(NodeIdentifier.create, "Test World", "Test Area A") node_a = GenericNode(nc("Node A"), True, None, "", ("default",), {}) node_b = GenericNode(nc("Node B"), True, None, "", ("default",), {}) node_c = GenericNode(nc("Node C"), True, None, "", ("default",), {}) translator_node = ConfigurableNode(translator_identif := nc("Translator Gate"), True, None, "", ("default",), {}) world_list = WorldList([ World("Test World", [ Area("Test Area A", None, True, [node_a, node_b, node_c, translator_node], { node_a: { node_b: Requirement.trivial(), translator_node: Requirement.trivial(), }, node_b: { node_a: Requirement.trivial(), }, node_c: { translator_node: Requirement.trivial(), }, translator_node: { node_a: Requirement.trivial(), node_c: Requirement.trivial(), }, }, {} ) ], {}) ]) game = GameDescription(RandovaniaGame.METROID_PRIME_ECHOES, DockWeaknessDatabase([], {}, (None, None)), echoes_resource_database, ("default",), Requirement.impossible(), None, {}, None, world_list) patches = echoes_game_patches.assign_node_configuration({ translator_identif: ResourceRequirement(scan_visor, 1, False) }) initial_state = State({scan_visor: 1 if has_translator else 0}, (), 99, node_a, patches, None, StateGameData(echoes_resource_database, game.world_list, 100, 99)) # Run reach = reach_lib.reach_with_all_safe_resources(game, initial_state) # Assert if has_translator: assert set(reach.safe_nodes) == {node_a, node_b, translator_node, node_c} else: assert set(reach.safe_nodes) == {node_a, node_b}
def pretty_print_node_type(node: Node, world_list: WorldList): if isinstance(node, DockNode): try: other = world_list.node_by_identifier(node.default_connection) other_name = world_list.node_name(other) except IndexError as e: other_name = (f"(Area {node.default_connection.area_name}, " f"index {node.default_connection.node_name}) [{e}]") return f"{node.default_dock_weakness.name} to {other_name}" elif isinstance(node, TeleporterNode): other = world_list.area_by_area_location(node.default_connection) return f"Teleporter to {world_list.area_name(other)}" elif isinstance(node, PickupNode): return f"Pickup {node.pickup_index.index}; Major Location? {node.major_location}" elif isinstance(node, EventNode): return f"Event {node.event.long_name}" elif isinstance(node, ConfigurableNode): return f"Configurable Node" elif isinstance(node, LogbookNode): message = "" if node.lore_type == LoreType.REQUIRES_ITEM: message = f" ({node.required_translator.long_name})" return f"Logbook {node.lore_type.long_name}{message} for {node.string_asset_id:x}" elif isinstance(node, PlayerShipNode): unlocked_pretty = list(pretty_print_requirement(node.is_unlocked)) if len(unlocked_pretty) > 1: unlocked_by = "Complex requirement" else: unlocked_by = unlocked_pretty[0][1] return f"Player Ship (Unlocked by {unlocked_by})" return ""
def hide_patches_hints(world_list: WorldList) -> list: """ Creates the string patches entries that changes the Lore scans in the game completely useless text. :return: """ return [ create_simple_logbook_hint(logbook_node.string_asset_id, "Some item was placed somewhere.") for logbook_node in world_list.iterate_nodes() if isinstance(logbook_node, LogbookNode) ]
def test_connections_from_dock_blast_shield(empty_patches): # Setup trivial = Requirement.trivial() req_1 = ResourceRequirement( SimpleResourceInfo("Ev1", "Ev1", ResourceType.EVENT), 1, False) req_2 = ResourceRequirement( SimpleResourceInfo("Ev2", "Ev2", ResourceType.EVENT), 1, False) dock_type = DockType("Type", "Type", frozendict()) weak_1 = DockWeakness("Weak 1", frozendict(), req_1, None) weak_2 = DockWeakness("Weak 2", frozendict(), trivial, DockLock(DockLockType.FRONT_BLAST_BACK_BLAST, req_2)) node_1_identifier = NodeIdentifier.create("W", "Area 1", "Node 1") node_2_identifier = NodeIdentifier.create("W", "Area 2", "Node 2") node_1 = DockNode(node_1_identifier, False, None, "", ("default", ), {}, dock_type, node_2_identifier, weak_1, None, None) node_1_lock = DockLockNode.create_from_dock(node_1) node_2 = DockNode(node_2_identifier, False, None, "", ("default", ), {}, dock_type, node_1_identifier, weak_2, None, None) node_2_lock = DockLockNode.create_from_dock(node_2) area_1 = Area("Area 1", None, True, [node_1, node_1_lock], {}, {}) area_2 = Area("Area 2", None, True, [node_2, node_2_lock], {}, {}) world = World("W", [area_1, area_2], {}) world_list = WorldList([world]) context = NodeContext( patches=empty_patches, current_resources={}, database=None, node_provider=world_list, ) # Run result_1 = list(node_1.connections_from(context)) result_2 = list(node_2.connections_from(context)) # Assert assert result_1 == [ (node_2, RequirementAnd([req_1, ResourceRequirement.simple(node_2_identifier)])), (node_1_lock, RequirementAnd([trivial, req_2])), ] assert result_2 == [ (node_1, ResourceRequirement.simple(node_2_identifier)), (node_2_lock, req_2), ]
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 get_mutable(self) -> "GameDescription": if self.mutable: return self else: result = GameDescription( game=self.game, resource_database=self.resource_database, layers=self.layers, dock_weakness_database=self.dock_weakness_database, world_list=WorldList( [world.duplicate() for world in self.world_list.worlds]), victory_condition=self.victory_condition, starting_location=self.starting_location, initial_states=copy.copy(self.initial_states), minimal_logic=self.minimal_logic, ) result.mutable = True return result
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 _pretty_name_for_elevator( game: RandovaniaGame, world_list: WorldList, original_teleporter_node: TeleporterNode, connection: AreaIdentifier, ) -> str: """ Calculates the name the room that contains this elevator should have :param world_list: :param original_teleporter_node: :param connection: :return: """ if original_teleporter_node.keep_name_when_vanilla: if original_teleporter_node.default_connection == connection: return world_list.nodes_to_area(original_teleporter_node).name return "Transport to {}".format( elevators.get_elevator_or_area_name(game, world_list, connection, False))
def _create_world_list(asset_id: int, pickup_index: PickupIndex): nc = NodeIdentifier.create logbook_node = LogbookNode(nc("World", "Area", "Logbook A"), True, None, "", ("default", ), {}, asset_id, None, None, None, None) pickup_node = PickupNode(nc("World", "Area", "Pickup Node"), True, None, "", ("default", ), {}, pickup_index, True) world_list = WorldList([ World("World", [ Area("Area", 0, True, [logbook_node, pickup_node], {}, {}), Area("Other Area", 0, True, [ PickupNode(nc("World", "Other Area", f"Pickup {i}"), True, None, "", ("default", ), {}, PickupIndex(i), True) for i in range(pickup_index.index) ], {}, {}), ], {}), ]) return logbook_node, pickup_node, world_list
def export_all_indices( patches: GamePatches, useless_target: PickupTarget, world_list: WorldList, rng: Random, model_style: PickupModelStyle, data_source: PickupModelDataSource, exporter: PickupExporter, visual_etm: PickupEntry, ) -> List[ExportedPickupDetails]: """ Creates the patcher data for all pickups in the game :param patches: :param useless_target: :param world_list: :param rng: :param model_style: :param data_source: :param exporter: :param visual_etm: :return: """ pickup_assignment = patches.pickup_assignment pickup_list = list(pickup_assignment.values()) rng.shuffle(pickup_list) indices = sorted(node.pickup_index for node in world_list.iterate_nodes() if isinstance(node, PickupNode)) pickups = [ exporter.export( index, pickup_assignment.get(index, useless_target), _get_visual_model(i, pickup_list, data_source, visual_etm), model_style, ) for i, index in enumerate(indices) ] return pickups
def read_world_list(self, data: List[Dict]) -> WorldList: return WorldList(read_array(data, self.read_world))
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)
def _find_area_with_teleporter(world_list: WorldList, teleporter: NodeIdentifier) -> Area: return world_list.area_by_area_location(teleporter.area_location)
def pickup_index_to_node(world_list: WorldList, index: PickupIndex) -> PickupNode: for node in world_list.iterate_nodes(): if isinstance(node, PickupNode) and node.pickup_index == index: return node raise ValueError(f"PickupNode with {index} not found.")