From 89e0fc5b756a86b5ff2e324c9095e79a5a3f19a1 Mon Sep 17 00:00:00 2001 From: eugenep Date: Wed, 14 Apr 2021 00:22:27 +0200 Subject: [PATCH] Implementing graphsearch Co-authored-by: Ladislaus3III Co-authored-by: Sebastian Piotrowski --- goal_map.txt | 8 ++ graphsearch.py | 246 +++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 4 + map.txt | 6 +- problem.py | 142 ++++++++++++++++++++++++++++ utils.py | 3 + 6 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 goal_map.txt create mode 100644 graphsearch.py create mode 100644 problem.py create mode 100644 utils.py diff --git a/goal_map.txt b/goal_map.txt new file mode 100644 index 0000000..7c2b326 --- /dev/null +++ b/goal_map.txt @@ -0,0 +1,8 @@ +....... +###.... +....... +....... +...###. +...#^#. +....... + diff --git a/graphsearch.py b/graphsearch.py new file mode 100644 index 0000000..10e6670 --- /dev/null +++ b/graphsearch.py @@ -0,0 +1,246 @@ +import sys +from collections import deque +from os import path +import queue + +from utils import * +from settings import * + + +class Problem: + """The abstract class for a formal problem. You should subclass + this and implement the methods actions and result, and possibly + __init__, goal_test, and path_cost. Then you will create instances + of your subclass and solve them with the various search functions.""" + + def __init__(self, initial=[], goal=[]): + """The constructor specifies the initial state, and possibly a goal + state, if there is a unique goal. Your subclass's constructor can add + other arguments.""" + self.initial = initial + self.goal = goal + + def actions(self, state): + """Return the actions that can be executed in the given + state. The result would typically be a list, but if there are + many actions, consider yielding them one at a time in an + iterator, rather than building them all at once.""" + + moves = [] + if self.turn_left(self, state): + moves.append('Left') + if self.turn_right(self, state): + moves.append('Right') + if self.move_forward(self, state): + moves.append('Forward') + + print(moves) + return moves + + + def turn_left(self, state): + return True + + def turn_right(self, state): + return True + + def move_forward(self, state): + + a_row = 0 + a_column = 0 + + for row in range(MAP_SIZE): + for column, pos in enumerate(state[row]): + if pos == ">": + a_row = row + a_column = column + + if a_column == MAP_SIZE-1: + return False + elif state[a_row][a_column+1] == '.': + return True + return False + if pos == "<": + a_row = row + a_column = column + + if a_column == 0: + return False + elif state[a_row][a_column-1] == '.': + return True + return False + + if pos == "v": + a_row = row + a_column = column + + if a_row == MAP_SIZE-1: + return False + elif state[a_row+1][a_column] == '.': + return True + return False + if pos == "^": + a_row = row + a_column = column + + if row == 0: + return False + elif state[a_row-1][a_column] == '.': + return True + return False + + def turn_me_or_move(self, state, do_it): + a_row = 0 + a_column = 0 + + for row in range(MAP_SIZE): + for column, pos in enumerate(state[row]): + if pos == ">": + a_row = row + a_column = column + + if(do_it == 'Left'): + state[a_row][a_column] = '^' + if(do_it == 'Right'): + state[a_row][a_column] = 'v' + if(do_it == 'Forward'): + state[a_row][a_column] = '.' + state[a_row][a_column+1] = '>' + + if pos == "<": + a_row = row + a_column = column + if(do_it == 'Left'): + state[a_row][a_column] = 'v' + if(do_it == 'Right'): + state[a_row][a_column] = '^' + if(do_it == 'Forward'): + state[a_row][a_column] = '.' + state[a_row][a_column-1] = '<' + + if pos == "v": + a_row = row + a_column = column + if(do_it == 'Left'): + state[a_row][a_column] = '>' + if(do_it == 'Right'): + state[a_row][a_column] = '<' + if(do_it == 'Forward'): + state[a_row][a_column] = '.' + state[a_row+1][a_column] = 'v' + if pos == "^": + a_row = row + a_column = column + if(do_it == 'Left'): + state[a_row][a_column] = '<' + if(do_it == 'Right'): + state[a_row][a_column] = '>' + if(do_it == 'Forward'): + state[a_row][a_column] = '.' + state[a_row-1][a_column] = '^' + return state + + def result(self, state, action): + """Return the state that results from executing the given + action in the given state. The action must be one of + self.actions(state).""" + new_state = [] + + if action == 'Left': + new_state = self.turn_me_or_move(state, 'Left') + elif action == 'Right': + new_state = self.turn_me_or_move(state, 'Right') + elif action == 'Forward': + new_state = self.turn_me_or_move(state, 'Forward') + + return new_state + + def goal_test(self, state): + """Return True if the state is a goal. The default method compares the + state to self.goal or checks for state in self.goal if it is a + list, as specified in the constructor. Override this method if + checking against a single self.goal is not enough.""" + if isinstance(self.goal, list): + return is_in(state, self.goal) + else: + return state == self.goal + + + + + + +class Node: + """A node in a search tree. Contains a pointer to the parent (the node + that this is a successor of) and to the actual state for this node. Note + that if a state is arrived at by two paths, then there are two nodes with + the same state. Also includes the action that got us to this state, and + the total path_cost (also known as g) to reach the node. Other functions + may add an f and h value; see best_first_graph_search and astar_search for + an explanation of how the f and h values are handled. You will not need to + subclass this class.""" + + def __init__(self, state, parent=None, action=None): + """Create a search tree Node, derived from a parent by an action.""" + self.state = state + self.parent = parent + self.action = action + + def expand(self, problem): + """List the nodes reachable in one step from this node.""" + return [self.child_node(problem, action) + for action in problem.actions(self.state)] + + def child_node(self, problem, action): + next_state = problem.result(self.state, action) + next_node = Node(next_state, self, action) + return next_node + + + +class BFS: + @staticmethod + def breadth_first_graph_search(problem): + """[Figure 3.11] + Note that this function can be implemented in a + single line as below: + return graph_search(problem, FIFOQueue()) + """ + + node = Node(problem.initial) + if problem.goal_test(node.state): + return node + frontier = deque([node]) + explored = set() + while frontier: + node = frontier.popleft() + explored.add(node.state) + for child in node.expand(problem): + if child.state not in explored and child not in frontier: + if problem.goal_test(child.state): + return child + frontier.append(child) + return None + + @staticmethod + def loadMap(map_name=''): + maze = [] + map_folder = path.dirname(__file__) + with open(path.join(map_folder, map_name), 'rt') as f: + for line in f: + maze.append(line) + return maze + + @staticmethod + def run(): + initial_map = Map(BFS.loadMap('map.txt')) + goal_map = Map(BFS.loadMap('goal_map.txt')) + problem = Problem(initial_map, goal_map) + + result = BFS.breadth_first_graph_search(problem) + print(result) + + +class Map: + def __init__(self, maze): + self.maze = maze \ No newline at end of file diff --git a/main.py b/main.py index 87ffe77..a4ea4c5 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ from maze import * from grid import * from settings import * from sprites import * +from graphsearch import * @@ -96,6 +97,9 @@ class Game: self.player.parse_maze_moves() self.i_like_to_move_it() self.wentyl_bezpieczenstwa = 1 + if event.key == pg.K_F3: + BFS.run() + def i_like_to_move_it(self): for i in self.player.moves: diff --git a/map.txt b/map.txt index 4fb8aa7..567a813 100644 --- a/map.txt +++ b/map.txt @@ -1,7 +1,7 @@ -.A..... -###.... +.>..... +#.#.... ....... ....... ...###. -...#2#. +...#.#. ....... diff --git a/problem.py b/problem.py new file mode 100644 index 0000000..db88103 --- /dev/null +++ b/problem.py @@ -0,0 +1,142 @@ +import sys +from collections import deque + +from utils import * + + +class Problem: + """The abstract class for a formal problem. You should subclass + this and implement the methods actions and result, and possibly + __init__, goal_test, and path_cost. Then you will create instances + of your subclass and solve them with the various search functions.""" + + def __init__(self, initial, goal=None): + """The constructor specifies the initial state, and possibly a goal + state, if there is a unique goal. Your subclass's constructor can add + other arguments.""" + self.initial = initial + self.goal = goal + + def actions(self, state): + """Return the actions that can be executed in the given + state. The result would typically be a list, but if there are + many actions, consider yielding them one at a time in an + iterator, rather than building them all at once.""" + raise NotImplementedError + + def result(self, state, action): + """Return the state that results from executing the given + action in the given state. The action must be one of + self.actions(state).""" + raise NotImplementedError + + def goal_test(self, state): + """Return True if the state is a goal. The default method compares the + state to self.goal or checks for state in self.goal if it is a + list, as specified in the constructor. Override this method if + checking against a single self.goal is not enough.""" + if isinstance(self.goal, list): + return is_in(state, self.goal) + else: + return state == self.goal + + def path_cost(self, c, state1, action, state2): + """Return the cost of a solution path that arrives at state2 from + state1 via action, assuming cost c to get up to state1. If the problem + is such that the path doesn't matter, this function will only look at + state2. If the path does matter, it will consider c and maybe state1 + and action. The default method costs 1 for every step in the path.""" + return c + 1 + + def value(self, state): + """For optimization problems, each state has a value. Hill Climbing + and related algorithms try to maximize this value.""" + raise NotImplementedError + + + +class Node: + """A node in a search tree. Contains a pointer to the parent (the node + that this is a successor of) and to the actual state for this node. Note + that if a state is arrived at by two paths, then there are two nodes with + the same state. Also includes the action that got us to this state, and + the total path_cost (also known as g) to reach the node. Other functions + may add an f and h value; see best_first_graph_search and astar_search for + an explanation of how the f and h values are handled. You will not need to + subclass this class.""" + + def __init__(self, state, parent=None, action=None, path_cost=0): + """Create a search tree Node, derived from a parent by an action.""" + self.state = state + self.parent = parent + self.action = action + self.path_cost = path_cost + self.depth = 0 + if parent: + self.depth = parent.depth + 1 + + def __repr__(self): + return "".format(self.state) + + def __lt__(self, node): + return self.state < node.state + + def expand(self, problem): + """List the nodes reachable in one step from this node.""" + return [self.child_node(problem, action) + for action in problem.actions(self.state)] + + def child_node(self, problem, action): + """[Figure 3.10]""" + next_state = problem.result(self.state, action) + next_node = Node(next_state, self, action, problem.path_cost(self.path_cost, self.state, action, next_state)) + return next_node + + def solution(self): + """Return the sequence of actions to go from the root to this node.""" + return [node.action for node in self.path()[1:]] + + def path(self): + """Return a list of nodes forming the path from the root to this node.""" + node, path_back = self, [] + while node: + path_back.append(node) + node = node.parent + return list(reversed(path_back)) + + # We want for a queue of nodes in breadth_first_graph_search or + # astar_search to have no duplicated states, so we treat nodes + # with the same state as equal. [Problem: this may not be what you + # want in other contexts.] + + def __eq__(self, other): + return isinstance(other, Node) and self.state == other.state + + def __hash__(self): + # We use the hash value of the state + # stored in the node instead of the node + # object itself to quickly search a node + # with the same state in a Hash Table + return hash(self.state) + + +def breadth_first_graph_search(problem): + """[Figure 3.11] + Note that this function can be implemented in a + single line as below: + return graph_search(problem, FIFOQueue()) + """ + node = Node(problem.initial) + if problem.goal_test(node.state): + return node + frontier = deque([node]) + explored = set() + while frontier: + node = frontier.popleft() + explored.add(node.state) + for child in node.expand(problem): + if child.state not in explored and child not in frontier: + if problem.goal_test(child.state): + return child + frontier.append(child) + return None diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..d4f1b05 --- /dev/null +++ b/utils.py @@ -0,0 +1,3 @@ +def is_in(elt, seq): + """Similar to (elt in seq), but compares with 'is', not '=='.""" + return any(x is elt for x in seq) \ No newline at end of file