def simplify(self, keep_comments: bool = False) -> Requirement: new_items = expand_items(self.items, RequirementAnd, Requirement.trivial(), keep_comments) if Requirement.impossible() in new_items and mergeable_array( self, keep_comments): return Requirement.impossible() if len(new_items) == 1 and mergeable_array(self, keep_comments): return new_items[0] return RequirementAnd(new_items, comment=self.comment)
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()) if self._collection_for_filtering is not None: requirement = requirement.patch_requirements( self._collection_for_filtering, 1.0, self.game_description.resource_database, ) self._connections_visualizer = ConnectionsVisualizer( self.other_node_alternatives_contents, self.alternatives_grid_layout, self.resource_database, requirement, False)
def configurable_node_assignment( self, configuration: DreadConfiguration, game: GameDescription, rng: Random) -> Iterator[NodeConfigurationAssociation]: 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.simple(rsb.get_item("Screw")), "WEIGHT": Requirement.impossible(), "SPEEDBOOST": ResourceRequirement.simple(rsb.get_item("Speed")), } for node in game.world_list.iterate_nodes(): if not isinstance(node, ConfigurableNode): continue result.append((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 find_invalid_strongly_connected_components(game: GameDescription) -> Iterator[str]: import networkx graph = networkx.DiGraph() for node in game.world_list.iterate_nodes(): if isinstance(node, DockLockNode): continue graph.add_node(node) context = NodeContext( patches=GamePatches.create_from_game(game, 0, None), current_resources=ResourceCollection.with_database(game.resource_database), database=game.resource_database, node_provider=game.world_list, ) for node in game.world_list.iterate_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)
def patch_requirements(self, static_resources: ResourceCollection, damage_multiplier: float, database: ResourceDatabase) -> Requirement: if static_resources.is_resource_set(self.resource): if self.satisfied(static_resources, 0, database): return Requirement.trivial() else: return Requirement.impossible() else: return self.multiply_amount(damage_multiplier)
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_requirement_as_set_3(database): def _req(name: str): id_req = ResourceRequirement.simple(database.get_item(name)) return id_req req = RequirementOr([ Requirement.impossible(), _req("A"), ]) assert req.as_set(database) == RequirementSet([ RequirementList([_req("A")]), ])
def simplify(self, keep_comments: bool = False) -> Requirement: new_items = expand_items(self.items, RequirementOr, Requirement.impossible(), keep_comments) if Requirement.trivial() in new_items and mergeable_array( self, keep_comments): return Requirement.trivial() num_and_requirements = 0 common_requirements = None for item in new_items: if isinstance(item, RequirementAnd) and mergeable_array( item, keep_comments): num_and_requirements += 1 if common_requirements is None: common_requirements = item.items else: common_requirements = [ common for common in common_requirements if common in item.items ] # Only extract the common requirements if there's more than 1 requirement if num_and_requirements >= 2 and common_requirements: simplified_items = [] common_new_or = [] for item in new_items: if isinstance(item, RequirementAnd) and mergeable_array( item, keep_comments): assert set(common_requirements) <= set(item.items) simplified_condition = [ it for it in item.items if it not in common_requirements ] if simplified_condition: common_new_or.append( RequirementAnd(simplified_condition ) if len(simplified_condition) > 1 else simplified_condition[0]) else: simplified_items.append(item) common_requirements.append(RequirementOr(common_new_or)) simplified_items.append(RequirementAnd(common_requirements)) final_items = simplified_items else: final_items = new_items if len(final_items) == 1 and mergeable_array(self, keep_comments): return final_items[0] return RequirementOr(final_items, comment=self.comment)
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.editor.edit_connections(self.current_area, from_node, target_node, editor.final_requirement) self.update_connections() self.area_view_canvas.update()
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, RequirementArrayBase): 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 state_for_current_configuration(self) -> Optional[State]: state = self._initial_state.copy() if self._actions: state.node = self._actions[-1] state.patches = state.patches.assign_elevators( (state.world_list.get_teleporter_node(teleporter), combo.currentData()) for teleporter, combo in self._elevator_id_to_combo.items()) 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.simple(translator) state.patches.configurable_nodes[gate] = RequirementAnd([ ResourceRequirement.simple(scan_visor), 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: state.resources.add_resource_gain( node.resource_gain_on_collect(state.node_context())) return state
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 test_impossible_requirement_as_set(): assert Requirement.impossible().as_set(None) == RequirementSet.impossible()
def test_impossible_requirement_satisfied(): assert not Requirement.impossible().satisfied(_empty_col(), 99, None)
def test_impossible_requirement_damage(): assert Requirement.impossible().damage(_empty_col(), None) == MAX_DAMAGE
def test_impossible_requirement_str(): assert str(Requirement.impossible()) == "Impossible"
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 final_requirement(self) -> Optional[Requirement]: result = self.build_requirement() if result == Requirement.impossible(): return None return result
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"), 0, True, None, "", ("default", ), {}) node_b = GenericNode(nc("Node B"), 1, True, None, "", ("default", ), {}) node_c = GenericNode(nc("Node C"), 2, True, None, "", ("default", ), {}) translator_node = ConfigurableNode( translator_identif := nc("Translator Gate"), 3, 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.simple(scan_visor)), ]) initial_state = State( ResourceCollection.from_dict(echoes_resource_database, {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}