def pretty_print_node_type(node: Node, world_list: WorldList): if isinstance(node, DockNode): try: other = world_list.resolve_dock_connection( world_list.nodes_to_world(node), node.default_connection) other_name = world_list.node_name(other) except IndexError as e: other_name = (f"(Asset {node.default_connection.area_asset_id:x}, " f"index {node.default_connection.dock_index}) [{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, TranslatorGateNode): return f"Translator Gate ({node.gate})" elif isinstance(node, LogbookNode): message = "" if node.lore_type == LoreType.LUMINOTH_LORE: message = f" ({node.required_translator.long_name})" return f"Logbook {node.lore_type.long_name}{message} for {node.string_asset_id:x}" return ""
def _pretty_name_for_elevator( world_list: WorldList, original_teleporter_node: TeleporterNode, connection: AreaLocation, ) -> 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 if connection.area_asset_id in _CUSTOM_NAMES_FOR_ELEVATORS: target_area_name = _CUSTOM_NAMES_FOR_ELEVATORS[ connection.area_asset_id] else: world = world_list.world_by_area_location(connection) area = world.area_by_asset_id(connection.area_asset_id) target_area_name = area.name return "Transport to {}".format(target_area_name)
def add_relative_hint(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_category or (cat != ItemCategory.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].area_asset_id)), rng) distance_offset = 0 if not precise_distance: distance_offset = max_distance - distances[area] if relative_type == HintLocationPrecision.RELATIVE_TO_AREA: relative = RelativeDataArea(distance_offset, world_list.area_to_area_location(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 _elevator_area_name(world_list: WorldList, area_location: AreaLocation, include_world_name: bool, ) -> str: if area_location.area_asset_id in _CUSTOM_NAMES_FOR_ELEVATORS: return _CUSTOM_NAMES_FOR_ELEVATORS[area_location.area_asset_id] else: world = world_list.world_by_area_location(area_location) area = world.area_by_asset_id(area_location.area_asset_id) if include_world_name: return world_list.area_name(area, distinguish_dark_aether=True, separator=" - ") else: return area.name
def _node_mapping_to_elevator_connection(world_list: WorldList, elevators: Dict[str, str], ) -> Dict[int, TeleporterConnection]: result = {} for source_name, target_node in elevators.items(): source_node: TeleporterNode = world_list.node_from_name(source_name) target_node = world_list.node_from_name(target_node) result[source_node.teleporter_instance_id] = TeleporterConnection( world_list.nodes_to_world(target_node).world_asset_id, world_list.nodes_to_area(target_node).area_asset_id ) return result
def elevator_area_name( world_list: WorldList, area_location: AreaLocation, include_world_name: bool, ) -> str: if area_location.area_asset_id in _CUSTOM_NAMES_FOR_ELEVATORS: return _CUSTOM_NAMES_FOR_ELEVATORS[area_location.area_asset_id] else: world = world_list.world_by_area_location(area_location) area = world.area_by_asset_id(area_location.area_asset_id) if include_world_name: return world_list.area_name(area) else: return area.name
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}" 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 _pickup_assignment_to_item_locations( world_list: WorldList, pickup_assignment: PickupAssignment, ordered_pickups: List[PickupEntry], ) -> Dict[str, Dict[str, str]]: items_locations = {} for world in sorted(world_list.worlds, key=lambda w: w.name): items_in_world = {} items_locations[world.name] = items_in_world for node in sorted(world.all_nodes, key=lambda w: w.name): if not node.is_resource_node or not isinstance(node, PickupNode): continue if node.pickup_index in pickup_assignment: pickup = pickup_assignment[node.pickup_index] ordered_pickups.append(pickup) item_name = pickup.name else: item_name = "Nothing" items_in_world[world_list.node_name(node)] = item_name return items_locations
def _area_name_to_area_location(world_list: WorldList, area_name: str) -> AreaLocation: world_name, area_name = re.match("([^/]+)/([^/]+)", area_name).group(1, 2) starting_world = world_list.world_with_name(world_name) starting_area = starting_world.area_by_name(area_name) return AreaLocation(starting_world.world_asset_id, starting_area.area_asset_id)
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. """ g = networkx.DiGraph() dock_connections = patches.dock_connection if patches is not None else {} elevator_connections = 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: if isinstance(node, DockNode): connection = dock_connections.get( (area.area_asset_id, node.dock_index), node.default_connection) new_areas.add( world.area_by_asset_id(connection.area_asset_id)) elif isinstance(node, TeleporterNode) and not ignore_elevators: connection = elevator_connections.get( node.teleporter_instance_id, node.default_connection) 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 create_temple_key_hint( all_patches: Dict[int, GamePatches], player_index: int, temple: HintDarkTemple, world_list: WorldList, ) -> str: """ Creates the text for . :param all_patches: :param player_index: :param temple: :param world_list: :return: """ all_world_names = set() _TEMPLE_NAMES = ["Dark Agon Temple", "Dark Torvus Temple", "Hive Temple"] temple_index = [ HintDarkTemple.AGON_WASTES, HintDarkTemple.TORVUS_BOG, HintDarkTemple.SANCTUARY_FORTRESS ].index(temple) keys = echoes_items.DARK_TEMPLE_KEY_ITEMS[temple_index] index_to_node = { node.pickup_index: node for node in world_list.all_nodes if isinstance(node, PickupNode) } for patches in all_patches.values(): for pickup_index, target in patches.pickup_assignment.items(): if target.player != player_index: continue resources = resource_info.convert_resource_gain_to_current_resources( target.pickup.resource_gain({})) for resource, quantity in resources.items(): if quantity < 1 or resource.index not in keys: continue pickup_node = index_to_node[pickup_index] all_world_names.add( world_list.world_name_from_node(pickup_node, True)) temple_name = hint_lib.color_text(hint_lib.TextColor.ITEM, _TEMPLE_NAMES[temple_index]) names_sorted = [ hint_lib.color_text(hint_lib.TextColor.LOCATION, world) for world in sorted(all_world_names) ] if len(names_sorted) == 0: return f"The keys to {temple_name} are nowhere to be found." elif len(names_sorted) == 1: return f"The keys to {temple_name} can all be found in {names_sorted[0]}." else: last = names_sorted.pop() front = ", ".join(names_sorted) return f"The keys to {temple_name} can be found in {front} and {last}."
def test_basic_search_with_translator_gate(has_translator: bool, echoes_resource_database): # Setup scan_visor = echoes_resource_database.get_item(10) node_a = GenericNode("Node A", True, None, 0) node_b = GenericNode("Node B", True, None, 1) node_c = GenericNode("Node C", True, None, 2) translator_node = TranslatorGateNode("Translator Gate", True, None, 3, TranslatorGate(1), scan_visor) world_list = WorldList([ World("Test World", "Test Dark World", 1, [ Area( "Test Area A", False, 10, 0, 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_specific = EchoesGameSpecific(energy_per_tank=100, safe_zone_heal_per_second=1, beam_configurations=(), dangerous_energy_tank=False) game = GameDescription(RandovaniaGame.PRIME2, DockWeaknessDatabase([], [], [], []), echoes_resource_database, game_specific, Requirement.impossible(), None, {}, world_list) patches = game.create_game_patches() patches = patches.assign_gate_assignment({TranslatorGate(1): scan_visor}) initial_state = State({scan_visor: 1 if has_translator else 0}, (), 99, node_a, patches, None, echoes_resource_database, game.world_list) # Run reach = 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 _create_elevators_field(world_list: WorldList, patches: GamePatches) -> list: nodes_by_teleporter_id = { node.teleporter_instance_id: node for node in world_list.all_nodes if isinstance(node, TeleporterNode) } elevators = [{ "origin_location": world_list.node_to_area_location( nodes_by_teleporter_id[instance_id]).as_json, "target_location": connection.as_json, "room_name": "Transport to {}".format( world_list.world_by_area_location(connection).name) } for instance_id, connection in patches.elevator_connection.items()] return elevators
def test_basic_search_with_translator_gate(has_translator: bool, echoes_resource_database): # Setup scan_visor = echoes_resource_database.get_by_type_and_index( ResourceType.ITEM, 10) node_a = GenericNode("Node A", True, 0) node_b = GenericNode("Node B", True, 1) node_c = GenericNode("Node C", True, 2) translator_node = TranslatorGateNode("Translator Gate", True, 3, TranslatorGate(1), scan_visor) world_list = WorldList([ World("Test World", 1, [ Area( "Test Area A", False, 10, 0, [node_a, node_b, node_c, translator_node], { node_a: { node_b: RequirementSet.trivial(), translator_node: RequirementSet.trivial(), }, node_b: { node_a: RequirementSet.trivial(), }, node_c: { translator_node: RequirementSet.trivial(), }, translator_node: { node_a: RequirementSet.trivial(), node_c: RequirementSet.trivial(), }, }) ]) ]) game = GameDescription(0, "", DockWeaknessDatabase([], [], [], []), echoes_resource_database, RequirementSet.impossible(), None, {}, world_list) patches = GamePatches.with_game(game) patches = patches.assign_gate_assignment({TranslatorGate(1): scan_visor}) initial_state = State({scan_visor: 1 if has_translator else 0}, (), 99, node_a, patches, None, echoes_resource_database) # Run reach = 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 _create_world_list(asset_id: int, pickup_index: PickupIndex): logbook_node = LogbookNode("Logbook A", True, 0, asset_id, None, None, None, None) pickup_node = PickupNode("Pickup Node", True, 1, pickup_index, True) world_list = WorldList([ World("World", 5000, [ Area("Area", False, 10000, 0, [logbook_node, pickup_node], {}), ]), ]) return logbook_node, pickup_node, world_list
def _pretty_name_for_elevator(world_list: WorldList, original_teleporter_node: TeleporterNode, connection: AreaLocation, ) -> 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(_elevator_area_name(world_list, connection, False))
def _create_world_list(asset_id: int, pickup_index: PickupIndex): logbook_node = LogbookNode("Logbook A", True, None, 0, asset_id, None, None, None, None) pickup_node = PickupNode("Pickup Node", True, None, 1, pickup_index, True) world_list = WorldList([ World("World", "Dark World", 5000, [ Area("Area", False, 10000, 0, True, [logbook_node, pickup_node], {}), Area("Other Area", False, 20000, 0, True, [ PickupNode(f"Pickup {i}", True, None, 1, PickupIndex(i), True) for i in range(pickup_index.index) ], {}), ]), ]) return logbook_node, pickup_node, world_list
def _pickup_assignment_to_item_locations(world_list: WorldList, pickup_assignment: PickupAssignment, ) -> Dict[str, Dict[str, str]]: items_locations = {} for world in world_list.worlds: items_in_world = {} items_locations[world.name] = items_in_world for area in world.areas: for node in area.nodes: if isinstance(node, PickupNode): if node.pickup_index in pickup_assignment: item_name = pickup_assignment[node.pickup_index].name else: item_name = "Nothing" items_in_world[world_list.node_name(node)] = item_name return items_locations
def read_world_list(self, data: List[Dict]) -> WorldList: return WorldList(read_array(data, self.read_world))
def _find_area_with_teleporter(world_list: WorldList, teleporter: Teleporter) -> Area: return world_list.area_by_area_location(teleporter.area_location)
def _name_for_location(world_list: WorldList, location: AreaLocation) -> str: if location in prime1_elevators.CUSTOM_NAMES: return prime1_elevators.CUSTOM_NAMES[location] else: return world_list.area_name(world_list.area_by_area_location(location), separator=":")