pacai.core.search
This file provides the core infrastructure for search problems.
1""" 2This file provides the core infrastructure for search problems. 3""" 4 5import abc 6import random 7import typing 8 9import pacai.core.action 10import pacai.core.board 11import pacai.util.comparable 12 13class SolutionNotFoundError(Exception): 14 """ An error for when a search problem solver cannot find a solution. """ 15 16class SearchNode(abc.ABC, pacai.util.comparable.SimpleComparable): 17 """ 18 A node or "state" in a search problem/tree. 19 A search node represents one possible state of the problem. 20 21 It is common to refer to search nodes as "search states" or "states". 22 To avoid confusion with game states, this project will use "node" when referencing search problems. 23 """ 24 25NodeType = typing.TypeVar('NodeType', bound = SearchNode) # pylint: disable=invalid-name 26 27class SuccessorInfo(typing.Generic[NodeType]): 28 """ 29 A possible search node (and related information) that can be reached from another node in a search problem. 30 """ 31 32 def __init__(self, 33 node: NodeType, 34 action: pacai.core.action.Action, 35 cost: float, 36 **kwargs: typing.Any) -> None: 37 self.node = node 38 """ The search node of this successor. """ 39 40 self.action = action 41 """ The action that was taken at the old node to reach this successor. """ 42 43 self.cost = cost 44 """ The cost of taken the action that lead to this successor. """ 45 46class SearchProblem(abc.ABC, typing.Generic[NodeType]): 47 """ 48 This class outlines the structure of a search problem. 49 Any search problem will need to provide answers to the following questions: 50 1) Where should the search start? 51 2) Is some specific search node a goal (are we done?)? 52 3) What moves are possible from a given search node? 53 54 The answers to these questions are provided by implementing 55 the methods of this class. 56 """ 57 58 def __init__(self, **kwargs: typing.Any) -> None: 59 self.expanded_node_count: int = 0 60 """ 61 The number of search nodes that have been expended. 62 It is important that subclasses accurately keep this count up-to-date. 63 """ 64 65 self.visited_nodes: set[NodeType] = set() 66 """ 67 Keep track of the board positions that have been visited. 68 This can help agents quickly check where they have previously been. 69 """ 70 71 self.position_history: list[pacai.core.board.Position] = [] 72 """ 73 Keep track of the order that positions have been visited. 74 This let's us know exactly how the agent has moved about. 75 """ 76 77 def complete(self, goal_node: NodeType) -> None: 78 """ Notify this search problem that the solver choose this goal node. """ 79 80 @abc.abstractmethod 81 def get_starting_node(self) -> NodeType: 82 """ Get the starting node for the search problem. """ 83 84 @abc.abstractmethod 85 def is_goal_node(self, node: NodeType) -> bool: 86 """ Check if this node is a valid goal node. """ 87 88 @abc.abstractmethod 89 def get_successor_nodes(self, node: NodeType) -> list[SuccessorInfo[NodeType]]: 90 """ 91 Get all the possible successors (successor nodes) to the current node. 92 This action can be though of expanding a search node, 93 or getting the children of a node in the search tree. 94 """ 95 96class SearchSolution(typing.Generic[NodeType]): 97 """ 98 A potential solution to a search problem. 99 This does not have to be an optimal (or even correct) solution, 100 but just what a solver returns. 101 """ 102 103 def __init__(self, 104 actions: list[pacai.core.action.Action], 105 cost: float, 106 goal_node: NodeType | None = None, 107 ) -> None: 108 self.actions: list[pacai.core.action.Action] = actions 109 """ 110 The actions to take for this solution. 111 These actions should guide the agent from its starting location (SearchProblem.get_starting_node()) 112 to the goal (SearchProblem.is_goal_node()). 113 If the agent is just moving, you can think of this as it's "path". 114 """ 115 116 self.cost: float = cost 117 """ The cost of this solution. """ 118 119 self.goal_node: NodeType | None = goal_node 120 """ 121 The node that the search was ended on. 122 May be None in cases where the solver does not use search nodes. 123 """ 124 125 def get_path(self, start_position: pacai.core.board.Position) -> list[pacai.core.board.Position]: 126 """ 127 Given the starting locatrion, get the path this search solution represents. 128 The resulting path will start at the starting position. 129 It is assumed that unknown actions will not result in a move. 130 """ 131 132 path = [start_position] 133 134 for action in self.actions: 135 path.append(path[-1].apply_action(action)) 136 137 return path 138 139@typing.runtime_checkable 140class CostFunction(typing.Protocol): 141 """ 142 A function that computes the cost associated with a specific search node. 143 """ 144 145 def __call__(self, node: SearchNode, **kwargs: typing.Any) -> float: 146 ... 147 148@typing.runtime_checkable 149class SearchHeuristic(typing.Protocol): 150 """ 151 A heuristic function attempts to score a search node in the context of a problem. 152 """ 153 154 def __call__(self, node: SearchNode, problem: SearchProblem, **kwargs: typing.Any) -> float: 155 ... 156 157@typing.runtime_checkable 158class SearchProblemSolver(typing.Protocol): 159 """ 160 A function that solves a search problem and returns a solution. 161 Not all solvers will need to heuristic or RNG, 162 but they will always be provided (even if the heuristic is a null heuristic). 163 If no solution/path can be found, a SolutionNotFoundError exception should be raised. 164 """ 165 166 def __call__(self, 167 problem: SearchProblem, 168 heuristic: SearchHeuristic, 169 rng: random.Random, 170 **kwargs: typing.Any) -> SearchSolution: 171 ...
14class SolutionNotFoundError(Exception): 15 """ An error for when a search problem solver cannot find a solution. """
An error for when a search problem solver cannot find a solution.
17class SearchNode(abc.ABC, pacai.util.comparable.SimpleComparable): 18 """ 19 A node or "state" in a search problem/tree. 20 A search node represents one possible state of the problem. 21 22 It is common to refer to search nodes as "search states" or "states". 23 To avoid confusion with game states, this project will use "node" when referencing search problems. 24 """
A node or "state" in a search problem/tree. A search node represents one possible state of the problem.
It is common to refer to search nodes as "search states" or "states". To avoid confusion with game states, this project will use "node" when referencing search problems.
28class SuccessorInfo(typing.Generic[NodeType]): 29 """ 30 A possible search node (and related information) that can be reached from another node in a search problem. 31 """ 32 33 def __init__(self, 34 node: NodeType, 35 action: pacai.core.action.Action, 36 cost: float, 37 **kwargs: typing.Any) -> None: 38 self.node = node 39 """ The search node of this successor. """ 40 41 self.action = action 42 """ The action that was taken at the old node to reach this successor. """ 43 44 self.cost = cost 45 """ The cost of taken the action that lead to this successor. """
A possible search node (and related information) that can be reached from another node in a search problem.
33 def __init__(self, 34 node: NodeType, 35 action: pacai.core.action.Action, 36 cost: float, 37 **kwargs: typing.Any) -> None: 38 self.node = node 39 """ The search node of this successor. """ 40 41 self.action = action 42 """ The action that was taken at the old node to reach this successor. """ 43 44 self.cost = cost 45 """ The cost of taken the action that lead to this successor. """
47class SearchProblem(abc.ABC, typing.Generic[NodeType]): 48 """ 49 This class outlines the structure of a search problem. 50 Any search problem will need to provide answers to the following questions: 51 1) Where should the search start? 52 2) Is some specific search node a goal (are we done?)? 53 3) What moves are possible from a given search node? 54 55 The answers to these questions are provided by implementing 56 the methods of this class. 57 """ 58 59 def __init__(self, **kwargs: typing.Any) -> None: 60 self.expanded_node_count: int = 0 61 """ 62 The number of search nodes that have been expended. 63 It is important that subclasses accurately keep this count up-to-date. 64 """ 65 66 self.visited_nodes: set[NodeType] = set() 67 """ 68 Keep track of the board positions that have been visited. 69 This can help agents quickly check where they have previously been. 70 """ 71 72 self.position_history: list[pacai.core.board.Position] = [] 73 """ 74 Keep track of the order that positions have been visited. 75 This let's us know exactly how the agent has moved about. 76 """ 77 78 def complete(self, goal_node: NodeType) -> None: 79 """ Notify this search problem that the solver choose this goal node. """ 80 81 @abc.abstractmethod 82 def get_starting_node(self) -> NodeType: 83 """ Get the starting node for the search problem. """ 84 85 @abc.abstractmethod 86 def is_goal_node(self, node: NodeType) -> bool: 87 """ Check if this node is a valid goal node. """ 88 89 @abc.abstractmethod 90 def get_successor_nodes(self, node: NodeType) -> list[SuccessorInfo[NodeType]]: 91 """ 92 Get all the possible successors (successor nodes) to the current node. 93 This action can be though of expanding a search node, 94 or getting the children of a node in the search tree. 95 """
This class outlines the structure of a search problem. Any search problem will need to provide answers to the following questions: 1) Where should the search start? 2) Is some specific search node a goal (are we done?)? 3) What moves are possible from a given search node?
The answers to these questions are provided by implementing the methods of this class.
The number of search nodes that have been expended. It is important that subclasses accurately keep this count up-to-date.
Keep track of the board positions that have been visited. This can help agents quickly check where they have previously been.
Keep track of the order that positions have been visited. This let's us know exactly how the agent has moved about.
78 def complete(self, goal_node: NodeType) -> None: 79 """ Notify this search problem that the solver choose this goal node. """
Notify this search problem that the solver choose this goal node.
81 @abc.abstractmethod 82 def get_starting_node(self) -> NodeType: 83 """ Get the starting node for the search problem. """
Get the starting node for the search problem.
85 @abc.abstractmethod 86 def is_goal_node(self, node: NodeType) -> bool: 87 """ Check if this node is a valid goal node. """
Check if this node is a valid goal node.
89 @abc.abstractmethod 90 def get_successor_nodes(self, node: NodeType) -> list[SuccessorInfo[NodeType]]: 91 """ 92 Get all the possible successors (successor nodes) to the current node. 93 This action can be though of expanding a search node, 94 or getting the children of a node in the search tree. 95 """
Get all the possible successors (successor nodes) to the current node. This action can be though of expanding a search node, or getting the children of a node in the search tree.
97class SearchSolution(typing.Generic[NodeType]): 98 """ 99 A potential solution to a search problem. 100 This does not have to be an optimal (or even correct) solution, 101 but just what a solver returns. 102 """ 103 104 def __init__(self, 105 actions: list[pacai.core.action.Action], 106 cost: float, 107 goal_node: NodeType | None = None, 108 ) -> None: 109 self.actions: list[pacai.core.action.Action] = actions 110 """ 111 The actions to take for this solution. 112 These actions should guide the agent from its starting location (SearchProblem.get_starting_node()) 113 to the goal (SearchProblem.is_goal_node()). 114 If the agent is just moving, you can think of this as it's "path". 115 """ 116 117 self.cost: float = cost 118 """ The cost of this solution. """ 119 120 self.goal_node: NodeType | None = goal_node 121 """ 122 The node that the search was ended on. 123 May be None in cases where the solver does not use search nodes. 124 """ 125 126 def get_path(self, start_position: pacai.core.board.Position) -> list[pacai.core.board.Position]: 127 """ 128 Given the starting locatrion, get the path this search solution represents. 129 The resulting path will start at the starting position. 130 It is assumed that unknown actions will not result in a move. 131 """ 132 133 path = [start_position] 134 135 for action in self.actions: 136 path.append(path[-1].apply_action(action)) 137 138 return path
A potential solution to a search problem. This does not have to be an optimal (or even correct) solution, but just what a solver returns.
104 def __init__(self, 105 actions: list[pacai.core.action.Action], 106 cost: float, 107 goal_node: NodeType | None = None, 108 ) -> None: 109 self.actions: list[pacai.core.action.Action] = actions 110 """ 111 The actions to take for this solution. 112 These actions should guide the agent from its starting location (SearchProblem.get_starting_node()) 113 to the goal (SearchProblem.is_goal_node()). 114 If the agent is just moving, you can think of this as it's "path". 115 """ 116 117 self.cost: float = cost 118 """ The cost of this solution. """ 119 120 self.goal_node: NodeType | None = goal_node 121 """ 122 The node that the search was ended on. 123 May be None in cases where the solver does not use search nodes. 124 """
The actions to take for this solution. These actions should guide the agent from its starting location (SearchProblem.get_starting_node()) to the goal (SearchProblem.is_goal_node()). If the agent is just moving, you can think of this as it's "path".
The node that the search was ended on. May be None in cases where the solver does not use search nodes.
126 def get_path(self, start_position: pacai.core.board.Position) -> list[pacai.core.board.Position]: 127 """ 128 Given the starting locatrion, get the path this search solution represents. 129 The resulting path will start at the starting position. 130 It is assumed that unknown actions will not result in a move. 131 """ 132 133 path = [start_position] 134 135 for action in self.actions: 136 path.append(path[-1].apply_action(action)) 137 138 return path
Given the starting locatrion, get the path this search solution represents. The resulting path will start at the starting position. It is assumed that unknown actions will not result in a move.
140@typing.runtime_checkable 141class CostFunction(typing.Protocol): 142 """ 143 A function that computes the cost associated with a specific search node. 144 """ 145 146 def __call__(self, node: SearchNode, **kwargs: typing.Any) -> float: 147 ...
A function that computes the cost associated with a specific search node.
1953def _no_init_or_replace_init(self, *args, **kwargs): 1954 cls = type(self) 1955 1956 if cls._is_protocol: 1957 raise TypeError('Protocols cannot be instantiated') 1958 1959 # Already using a custom `__init__`. No need to calculate correct 1960 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1961 if cls.__init__ is not _no_init_or_replace_init: 1962 return 1963 1964 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1965 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1966 # searches for a proper new `__init__` in the MRO. The new `__init__` 1967 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1968 # instantiation of the protocol subclass will thus use the new 1969 # `__init__` and no longer call `_no_init_or_replace_init`. 1970 for base in cls.__mro__: 1971 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1972 if init is not _no_init_or_replace_init: 1973 cls.__init__ = init 1974 break 1975 else: 1976 # should not happen 1977 cls.__init__ = object.__init__ 1978 1979 cls.__init__(self, *args, **kwargs)
149@typing.runtime_checkable 150class SearchHeuristic(typing.Protocol): 151 """ 152 A heuristic function attempts to score a search node in the context of a problem. 153 """ 154 155 def __call__(self, node: SearchNode, problem: SearchProblem, **kwargs: typing.Any) -> float: 156 ...
A heuristic function attempts to score a search node in the context of a problem.
1953def _no_init_or_replace_init(self, *args, **kwargs): 1954 cls = type(self) 1955 1956 if cls._is_protocol: 1957 raise TypeError('Protocols cannot be instantiated') 1958 1959 # Already using a custom `__init__`. No need to calculate correct 1960 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1961 if cls.__init__ is not _no_init_or_replace_init: 1962 return 1963 1964 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1965 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1966 # searches for a proper new `__init__` in the MRO. The new `__init__` 1967 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1968 # instantiation of the protocol subclass will thus use the new 1969 # `__init__` and no longer call `_no_init_or_replace_init`. 1970 for base in cls.__mro__: 1971 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1972 if init is not _no_init_or_replace_init: 1973 cls.__init__ = init 1974 break 1975 else: 1976 # should not happen 1977 cls.__init__ = object.__init__ 1978 1979 cls.__init__(self, *args, **kwargs)
158@typing.runtime_checkable 159class SearchProblemSolver(typing.Protocol): 160 """ 161 A function that solves a search problem and returns a solution. 162 Not all solvers will need to heuristic or RNG, 163 but they will always be provided (even if the heuristic is a null heuristic). 164 If no solution/path can be found, a SolutionNotFoundError exception should be raised. 165 """ 166 167 def __call__(self, 168 problem: SearchProblem, 169 heuristic: SearchHeuristic, 170 rng: random.Random, 171 **kwargs: typing.Any) -> SearchSolution: 172 ...
A function that solves a search problem and returns a solution. Not all solvers will need to heuristic or RNG, but they will always be provided (even if the heuristic is a null heuristic). If no solution/path can be found, a SolutionNotFoundError exception should be raised.
1953def _no_init_or_replace_init(self, *args, **kwargs): 1954 cls = type(self) 1955 1956 if cls._is_protocol: 1957 raise TypeError('Protocols cannot be instantiated') 1958 1959 # Already using a custom `__init__`. No need to calculate correct 1960 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1961 if cls.__init__ is not _no_init_or_replace_init: 1962 return 1963 1964 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1965 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1966 # searches for a proper new `__init__` in the MRO. The new `__init__` 1967 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1968 # instantiation of the protocol subclass will thus use the new 1969 # `__init__` and no longer call `_no_init_or_replace_init`. 1970 for base in cls.__mro__: 1971 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1972 if init is not _no_init_or_replace_init: 1973 cls.__init__ = init 1974 break 1975 else: 1976 # should not happen 1977 cls.__init__ = object.__init__ 1978 1979 cls.__init__(self, *args, **kwargs)