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 test_requirement_as_set_4(): req = RequirementOr([ Requirement.impossible(), _req("A"), Requirement.trivial(), ]) assert req.as_set(None) == RequirementSet([ RequirementList([]), ])
def test_edit_connections(game_editor): # Setup landing_site = game_editor.game.world_list.area_by_area_location( AreaIdentifier("Temple Grounds", "Landing Site")) source = landing_site.node_with_name("Save Station") target = landing_site.node_with_name("Door to Service Access") assert landing_site.connections[source][target] != Requirement.trivial() # Run game_editor.edit_connections(landing_site, source, target, Requirement.trivial()) # Assert assert landing_site.connections[source][target] == Requirement.trivial()
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 requirement_to_leave( self, patches: GamePatches, current_resources: CurrentResources) -> Requirement: if current_resources.get("add_self_as_requirement_to_resources") == 1: return ResourceRequirement(self.event, 1, False) else: return Requirement.trivial()
def requirement_to_leave(self, context: NodeContext) -> Requirement: # FIXME: using non-resource as key in CurrentResources if context.current_resources.get( "add_self_as_requirement_to_resources") == 1: return ResourceRequirement(self.pickup_index, 1, False) else: return Requirement.trivial()
def _lock_connection( self, context: NodeContext) -> typing.Optional[tuple[Node, Requirement]]: requirement = Requirement.trivial() forward_weakness = self.get_front_weakness(context) forward_lock = forward_weakness.lock if forward_lock is not None: requirement = self._get_lock_requirement(forward_weakness) back_weakness = self.get_back_weakness(context) back_lock = None if back_weakness is not None: back_lock = back_weakness.lock if back_lock is not None and back_lock.lock_type == DockLockType.FRONT_BLAST_BACK_BLAST: requirement = RequirementAnd( [requirement, back_lock.requirement]) if forward_lock is None and back_lock is None: return None lock_node = context.node_provider.node_by_identifier( self.get_lock_node_identifier(context)) return lock_node, requirement
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 reach_from_state(cls, game: GameDescription, initial_state: State, ) -> "GeneratorReach": reach = cls(game, initial_state, networkx.DiGraph()) reach._expand_graph([GraphPath(None, initial_state.node, Requirement.trivial())]) return reach
def requirement_to_leave( self, patches: GamePatches, current_resources: CurrentResources) -> Requirement: # FIXME: using non-resource as key in CurrentResources if current_resources.get("add_self_as_requirement_to_resources") == 1: return ResourceRequirement(self.pickup_index, 1, False) else: return Requirement.trivial()
def test_requirement_as_set_3(): req = RequirementOr([ Requirement.impossible(), _req("A"), ]) assert req.as_set == RequirementSet([ RequirementList([_req("A")]), ])
def test_requirement_as_set_2(): req = RequirementAnd([ Requirement.trivial(), _req("A"), ]) assert req.as_set(None) == RequirementSet([ RequirementList([_req("A")]), ])
def create_new_template(self): template_name, did_confirm = QtWidgets.QInputDialog.getText(self, "New Template", "Insert template name:") if not did_confirm or template_name == "": return self.db.requirement_template[template_name] = Requirement.trivial() self.create_template_editor(template_name) self.tab_template_layout.removeWidget(self.create_new_template_button) self.tab_template_layout.addWidget(self.create_new_template_button)
def reach_from_state( cls, game: GameDescription, initial_state: State, ) -> "GeneratorReach": reach = cls(game, initial_state, graph_module.RandovaniaGraph.new()) reach._expand_graph( [GraphPath(None, initial_state.node, Requirement.trivial())]) return reach
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 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 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 connections_from( self, context: NodeContext) -> typing.Iterator[tuple[Node, Requirement]]: target_area_identifier = context.patches.elevator_connection.get( context.node_provider.identifier_for_node(self), self.default_connection, ) if target_area_identifier is None: return yield context.node_provider.default_node_for_area( target_area_identifier), Requirement.trivial()
def read_dock_weakness_database( data: Dict, resource_database: ResourceDatabase, ) -> DockWeaknessDatabase: door_types = read_array( data["door"], lambda item: read_dock_weakness(item, resource_database, DockType.DOOR)) portal_types = read_array( data["portal"], lambda item: read_dock_weakness( item, resource_database, DockType.PORTAL)) return DockWeaknessDatabase(door=door_types, morph_ball=[ DockWeakness(0, "Morph Ball Door", False, Requirement.trivial(), DockType.MORPH_BALL_DOOR) ], other=[ DockWeakness(0, "Other Door", False, Requirement.trivial(), DockType.OTHER) ], portal=portal_types)
def test_apply_edit_connections_change( echoes_game_data, skip_qtbot, ): # Setup window = DataEditorWindow(echoes_game_data, True) skip_qtbot.addWidget(window) game = window.game_description landing_site = game.world_list.area_by_asset_id(1655756413) source = landing_site.node_with_name("Save Station") target = landing_site.node_with_name("Door to Service Access") # Run window.world_selector_box.setCurrentIndex( window.world_selector_box.findText( "Temple Grounds (Sky Temple Grounds)")) window.area_selector_box.setCurrentIndex( window.area_selector_box.findText(landing_site.name)) window._apply_edit_connections(source, target, Requirement.trivial()) # Assert assert landing_site.connections[source][target] == Requirement.trivial()
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 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 _potential_nodes_from(self, node: Node) -> Iterator[Tuple[Node, Requirement, bool]]: extra_requirement = _extra_requirement_for_node(self._game, node) requirement_to_leave = node.requirement_to_leave(self._state.patches, self._state.resources) for target_node, requirement in self._game.world_list.potential_nodes_from(node, self.state.patches): if target_node is None: continue if requirement_to_leave != Requirement.trivial(): requirement = RequirementAnd([requirement, requirement_to_leave]) if extra_requirement is not None: requirement = RequirementAnd([requirement, extra_requirement]) satisfied = requirement.satisfied(self._state.resources, self._state.energy) yield target_node, requirement, satisfied
def satisfiable_actions( self, state: State, victory_condition: Requirement, ) -> Iterator[Tuple[ResourceNode, int]]: interesting_resources = calculate_interesting_resources( self._satisfiable_requirements.union( victory_condition.as_set( state.resource_database).alternatives), state.resources, state.energy, state.resource_database) # print(" > satisfiable actions, with {} interesting resources".format(len(interesting_resources))) for action, energy in self.possible_actions(state): for resource, amount in action.resource_gain_on_collect( state.node_context()): if resource in interesting_resources: yield action, energy break
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 _potential_nodes_from( self, node: Node) -> Iterator[Tuple[Node, RequirementSet]]: extra_requirement = _extra_requirement_for_node( self._game, self.node_context(), node) requirement_to_leave = node.requirement_to_leave( self._state.node_context()) for target_node, requirement in self._game.world_list.potential_nodes_from( node, self.node_context()): if target_node is None: continue if requirement_to_leave != Requirement.trivial(): requirement = RequirementAnd( [requirement, requirement_to_leave]) if extra_requirement is not None: requirement = RequirementAnd([requirement, extra_requirement]) yield target_node, requirement.as_set( self._state.resource_database)
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