def connections_from(self, node: Node, patches: GamePatches) -> Iterator[Tuple[Node, Requirement]]: """ Queries all nodes from other areas you can go from a given node. Aka, doors and teleporters :param patches: :param node: :return: Generator of pairs Node + Requirement for going to that node """ if isinstance(node, DockNode): # TODO: respect is_blast_shield: if already opened once, no requirement needed. # Includes opening form behind with different criteria try: target_node = self.resolve_dock_node(node, patches) original_area = self.nodes_to_area(node) dock_weakness = patches.dock_weakness.get((original_area.area_asset_id, node.dock_index), node.default_dock_weakness) yield target_node, dock_weakness.requirement except IndexError: # TODO: fix data to not having docks pointing to nothing yield None, Requirement.impossible() if isinstance(node, TeleporterNode): try: yield self.resolve_teleporter_node(node, patches), Requirement.trivial() except IndexError: # TODO: fix data to not have teleporters pointing to areas with invalid default_node_index print("Teleporter is broken!", node) yield None, Requirement.impossible() if isinstance(node, PlayerShipNode): for other_node in self.all_nodes: if isinstance(other_node, PlayerShipNode) and other_node != node: yield other_node, other_node.is_unlocked
def base_dock_name_raw(dock_type: DockType, weakness: DockWeakness, connection: AreaIdentifier) -> str: expected_connector = "to" if weakness.requirement == Requirement.impossible( ) and weakness.name != "Not Determined": expected_connector = "from" return f"{dock_type.long_name} {expected_connector} {connection.area_name}"
def read_area(self, data: Dict) -> Area: nodes = read_array(data["nodes"], self.read_node) nodes_by_name = {node.name: node for node in nodes} connections = {} for i, origin_data in enumerate(data["nodes"]): origin = nodes[i] connections[origin] = {} for target_name, target_requirement in origin_data[ "connections"].items(): try: the_set = read_requirement(target_requirement, self.resource_database) except MissingResource as e: raise MissingResource( f"In area {data['name']}, connection from {origin.name} to {target_name} got error: {e}" ) if the_set != Requirement.impossible(): connections[origin][nodes_by_name[target_name]] = the_set area_name = data["name"] try: return Area(area_name, data["in_dark_aether"], data["asset_id"], data["default_node_index"], data["valid_starting_location"], nodes, connections) except KeyError as e: raise KeyError(f"Missing key `{e}` for area `{area_name}`")
def configurable_node_assignment(self, configuration: DreadConfiguration, game: GameDescription, rng: Random) -> NodeConfigurationAssignment: result = {} rsb = game.resource_database requirement_for_type = { "POWERBEAM": rsb.requirement_template["Shoot Beam"], "BOMB": rsb.requirement_template["Lay Bomb"], "MISSILE": rsb.requirement_template["Shoot Missile"], "SUPERMISSILE": rsb.requirement_template["Shoot Super Missile"], "POWERBOMB": rsb.requirement_template["Lay Power Bomb"], "SCREWATTACK": ResourceRequirement(rsb.get_item("Screw"), 1, False), "WEIGHT": Requirement.impossible(), "SPEEDBOOST": ResourceRequirement(rsb.get_item("Speed"), 1, False), } for node in game.world_list.all_nodes: if not isinstance(node, ConfigurableNode): continue result[game.world_list.identifier_for_node(node)] = RequirementAnd([ requirement_for_type[block_type] for block_type in node.extra["tile_types"] ]).simplify() return result
def test_requirement_as_set_3(): req = RequirementOr([ Requirement.impossible(), _req("A"), ]) assert req.as_set(None) == RequirementSet([ RequirementList([_req("A")]), ])
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 test_requirement_as_set_4(): req = RequirementOr([ Requirement.impossible(), _req("A"), Requirement.trivial(), ]) assert req.as_set == RequirementSet([ RequirementList([]), ])
def replace_connection_with(self, target_node: Node, requirement: Requirement): current_node = self.current_node if requirement == Requirement.impossible(): requirement = None self.editor.edit_connections(self.current_area, current_node, target_node, requirement) self.update_connections() self.area_view_canvas.update()
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 _open_edit_connection(self): from_node = self.current_node target_node = self.current_connection_node assert from_node is not None assert target_node is not None requirement = self.current_area.connections[from_node].get( target_node, Requirement.impossible()) editor = ConnectionsEditor(self, self.resource_database, requirement) result = editor.exec_() if result == QDialog.Accepted: self._apply_edit_connections(from_node, target_node, editor.final_requirement)
async def _open_edit_connection(self): if self._check_for_edit_dialog(): return from_node = self.current_node target_node = self.current_connection_node assert from_node is not None assert target_node is not None requirement = self.current_area.connections[from_node].get( target_node, Requirement.impossible()) editor = ConnectionsEditor(self, self.resource_database, requirement) if await self._execute_edit_dialog(editor): self._apply_edit_connections(from_node, target_node, editor.final_requirement)
def pretty_print_requirement(requirement: Requirement, level: int = 0) -> Iterator[Tuple[int, str]]: if requirement == Requirement.impossible(): yield level, "Impossible" elif requirement == Requirement.trivial(): yield level, "Trivial" elif isinstance(requirement, (RequirementAnd, RequirementOr)): yield from pretty_print_requirement_array(requirement, level) elif isinstance(requirement, ResourceRequirement): yield level, pretty_print_resource_requirement(requirement) elif isinstance(requirement, RequirementTemplate): yield level, requirement.template_name else: raise RuntimeError( f"Unknown requirement type: {type(requirement)} - {requirement}")
def update_connections(self): current_node = self.current_node current_connection_node = self.current_connection_node assert current_node != current_connection_node or current_node is None if self._connections_visualizer is not None: self._connections_visualizer.deleteLater() self._connections_visualizer = None if current_connection_node is None or current_node is None: assert len(self.current_area.nodes) <= 1 or not self.edit_mode return requirement = self.current_area.connections[current_node].get( self.current_connection_node, Requirement.impossible()) self._connections_visualizer = ConnectionsVisualizer( self.other_node_alternatives_contents, self.alternatives_grid_layout, self.resource_database, requirement, False)
def state_for_current_configuration(self) -> Optional[State]: state = self._initial_state.copy() if self._actions: state.node = self._actions[-1] for teleporter, combo in self._elevator_id_to_combo.items(): state.patches.elevator_connection[teleporter] = combo.currentData() for gate, item in self._translator_gate_to_combo.items(): scan_visor = self.game_description.resource_database.get_item( "Scan") requirement: Optional[ LayoutTranslatorRequirement] = item.currentData() if requirement is None: translator_req = Requirement.impossible() else: translator = self.game_description.resource_database.get_item( requirement.item_name) translator_req = ResourceRequirement(translator, 1, False) state.patches.configurable_nodes[gate] = RequirementAnd([ ResourceRequirement(scan_visor, 1, False), translator_req, ]) for pickup, quantity in self._collected_pickups.items(): for _ in range(quantity): add_pickup_to_state(state, pickup) for node in self._collected_nodes: add_resource_gain_to_current_resources( node.resource_gain_on_collect(state.node_context()), state.resources) return state
def final_requirement(self) -> Optional[Requirement]: result = self.build_requirement() if result == Requirement.impossible(): return None return result
def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None: local_pos = QPointF(self.mapFromGlobal(event.globalPos())) local_pos -= self.get_area_canvas_offset() self._next_node_location = self.qt_local_to_game_loc(local_pos) menu = QtWidgets.QMenu(self) if self.state is None: menu.addAction(self._show_all_connections_action) if self.edit_mode: menu.addAction(self._create_node_action) menu.addAction(self._move_node_action) self._move_node_action.setEnabled( self.highlighted_node is not None) if self.highlighted_node is not None: self._move_node_action.setText( f"Move {self.highlighted_node.name} here") # Areas Menu menu.addSeparator() areas_at_mouse = self._other_areas_at_position(local_pos) for area in areas_at_mouse: sub_menu = QtWidgets.QMenu(f"Area: {area.name}", self) sub_menu.addAction("View area").triggered.connect( functools.partial(self.SelectAreaRequest.emit, area)) if self.edit_mode: sub_menu.addAction( "Create dock here to this area").triggered.connect( functools.partial(self.CreateDockRequest.emit, self._next_node_location, area)) menu.addMenu(sub_menu) if not areas_at_mouse: sub_menu = QtGui.QAction("No areas here", self) sub_menu.setEnabled(False) menu.addAction(sub_menu) # Nodes Menu menu.addSeparator() nodes_at_mouse = self._nodes_at_position(local_pos) if self.highlighted_node in nodes_at_mouse: nodes_at_mouse.remove(self.highlighted_node) for node in nodes_at_mouse: if len(nodes_at_mouse) == 1: menu.addAction(node.name).setEnabled(False) sub_menu = menu else: sub_menu = QtWidgets.QMenu(node.name, self) sub_menu.addAction("Highlight this").triggered.connect( functools.partial(self.SelectNodeRequest.emit, node)) view_connections = sub_menu.addAction("View connections to this") view_connections.setEnabled( (self.edit_mode and self.highlighted_node != node) or (node in self.area.connections.get(self.highlighted_node, {}))) view_connections.triggered.connect( functools.partial(self.SelectConnectionsRequest.emit, node)) if self.edit_mode: sub_menu.addSeparator() sub_menu.addAction( "Replace connection with Trivial").triggered.connect( functools.partial( self.ReplaceConnectionsRequest.emit, node, Requirement.trivial(), )) sub_menu.addAction("Remove connection").triggered.connect( functools.partial( self.ReplaceConnectionsRequest.emit, node, Requirement.impossible(), )) if areas_at_mouse: move_menu = QtWidgets.QMenu("Move to...", self) for area in areas_at_mouse: move_menu.addAction(area.name).triggered.connect( functools.partial( self.MoveNodeToAreaRequest.emit, node, area, )) sub_menu.addMenu(move_menu) if sub_menu != menu: menu.addMenu(sub_menu) if not nodes_at_mouse: sub_menu = QtGui.QAction("No other nodes here", self) sub_menu.setEnabled(False) menu.addAction(sub_menu) # Done menu.exec_(event.globalPos())
def test_impossible_requirement_str(): assert str(Requirement.impossible()) == "Impossible"
def test_impossible_requirement_damage(): assert Requirement.impossible().damage({}, None) == MAX_DAMAGE
def test_impossible_requirement_satisfied(): assert not Requirement.impossible().satisfied({}, 99, None)
def test_impossible_requirement_as_set(): assert Requirement.impossible().as_set(None) == RequirementSet.impossible()
def find_invalid_strongly_connected_components( game: GameDescription) -> Iterator[str]: import networkx graph = networkx.DiGraph() for node in game.world_list.all_nodes: if isinstance(node, DockLockNode): continue graph.add_node(node) context = NodeContext( patches=GamePatches( player_index=0, configuration=None, pickup_assignment={}, elevator_connection={}, dock_connection={}, dock_weakness={}, configurable_nodes={}, starting_items={}, starting_location=game.starting_location, hints={}, ), current_resources={}, database=game.resource_database, node_provider=game.world_list, ) for node in game.world_list.all_nodes: if node not in graph: continue for other, req in game.world_list.potential_nodes_from(node, context): if other not in graph: continue if req != Requirement.impossible(): graph.add_edge(node, other) starting_node = game.world_list.resolve_teleporter_connection( game.starting_location) for strong_comp in networkx.strongly_connected_components(graph): components: set[Node] = strong_comp # The starting location determines the default component if starting_node in components: continue if any( node.extra.get("different_strongly_connected_component", False) for node in components): continue if len(components) == 1: node = next(iter(components)) # If the component is a single node which is the default node of it's area, allow it area = game.world_list.nodes_to_area(node) if area.default_node == node.name: continue # We accept nodes that have no paths out or in. if not graph.in_edges(node) and not graph.edges(node): continue names = sorted( game.world_list.node_name(node, with_world=True) for node in strong_comp) yield "Unknown strongly connected component detected containing {} nodes:\n{}".format( len(names), names)