def breadth_first_search(self, graph: MatrixGraph, start: tuple, target: tuple, surface: pygame.Surface, display: pygame.Surface) \ -> list[tuple[int, int]]: """ Return a list of tuples representing the final path if target is found, return an empty list otherwise. This function is an implementation of the breadth_first_search pathfinding algorithm """ queue = [] visited = set() paths = {} # A dictionary that maps new nodes to the previous node queue.extend(graph.get_valid_neighbours(start[0], start[1])) visited.update(graph.get_valid_neighbours(start[0], start[1])) # Update paths with the original visited set for node in graph.get_valid_neighbours(start[0], start[1]): paths[node] = start found = False # Counter to store current iteration counter = 0 # Pygame clock clock = Timer() while queue != [] and not found: # Draw and update the loop iteration counter counter += 1 iteration_counter = f'Nodes Searched: {counter}' self._draw_loop_iterations(iteration_counter, surface) # Pop the current node curr = queue.pop(0) # Visualize step _ = pygame.event.get() # Call event.get to stop program from crashing on clicks curr_x = curr[0] + self._maze_x_offset + 1 curr_y = curr[1] + self._maze_y_offset + 1 pygame.draw.circle(surface, (255, 0, 0), (curr_x, curr_y), 3) display.blit(surface, (0, 0)) pygame.display.flip() if curr == target: found = True for node in graph.get_valid_neighbours(curr[0], curr[1]): if node not in visited: queue.append(node) visited.add(node) # Add the node as a key with the current node as the value paths[node] = curr clock.update_time() self._draw_timer(clock, surface) if found is False: return [] else: return self._find_and_draw_final_path(paths, start, target, surface, display)
def test3(self): mg = MatrixGraph([[7, 8, 9, 2, 1], [2, 4, 7, 0, 3], [1, 2, 3, 2, 8], [2, 9, 6, 5, 8]]) neighbors = mg.get_neighbors((0, 4)) self.assertEqual(len(neighbors), 3) self.assertIn((0, 3), neighbors) self.assertIn((1, 3), neighbors) self.assertIn((1, 4), neighbors)
def test4(self): mg = MatrixGraph([[7, 8, 9, 2, 1], [2, 4, 7, 0, 3], [1, 2, 3, 2, 8], [2, 9, 6, 5, 8]]) neighbors = mg.get_neighbors((3, 1)) self.assertEqual(len(neighbors), 5) self.assertIn((2, 0), neighbors) self.assertIn((2, 1), neighbors) self.assertIn((2, 2), neighbors) self.assertIn((3, 0), neighbors) self.assertIn((3, 2), neighbors)
def test2(self): mg = MatrixGraph([[7, 8, 9], [2, 4, 7], [1, 2, 3]]) neighbors = mg.get_neighbors((1, 1)) self.assertEqual(len(neighbors), 8) self.assertIn((0, 0), neighbors) self.assertIn((0, 1), neighbors) self.assertIn((0, 2), neighbors) self.assertIn((1, 0), neighbors) self.assertIn((1, 2), neighbors) self.assertIn((2, 0), neighbors) self.assertIn((2, 1), neighbors) self.assertIn((2, 2), neighbors)
def depth_first_search_iterative(self, graph: MatrixGraph, start: tuple, target: tuple, surface: pygame.Surface, display: pygame.Surface) \ -> list[tuple[int, int]]: """ Return a list of tuples representing the final path if target is found, return an empty list otherwise. This is an iterative version of depth_first_search, since the recursive version exceeds the maximum recursion depth. """ discovered = set() stack = [start] # Stack is a reversed list for now. Later we can make a stack class if we # need paths = {} # A dictionary that maps new nodes to the previous node found = False # Variables and Surfaces used to display the current iterations counter = 0 # Pygame clock clock = Timer() while stack != [] and not found: # Draw and update the loop iteration counter counter += 1 iteration_counter = f'Nodes Searched: {counter}' self._draw_loop_iterations(iteration_counter, surface) vertex = stack.pop() # Visualize step _ = pygame.event.get() # Call event.get to stop program from crashing on clicks curr_x = vertex[0] + self._maze_x_offset + 1 curr_y = vertex[1] + self._maze_y_offset + 1 pygame.draw.circle(surface, (255, 0, 0), (curr_x, curr_y), 3) display.blit(surface, (0, 0)) pygame.display.flip() if vertex == target: found = True elif vertex not in discovered: discovered.add(vertex) neighbors = graph.get_valid_neighbours(vertex[0], vertex[1]) for neighbor in neighbors: if neighbor not in discovered: # Add the neighbor as a key in the path dictionary with vertex as a parent stack.append(neighbor) paths[neighbor] = vertex clock.update_time() self._draw_timer(clock, surface) if found is False: return [] else: return self._find_and_draw_final_path(paths, start, target, surface, display)
def set_pos(self, graph: MatrixGraph, posx: int, posy: int) -> Union[None, tuple[int, int]]: """ Set the position of the button to be the closest on the path. If the point is too far print an error message """ if self.active: try: return graph.closest_path((posx, posy), 5) except IndexError: print('Select point closer to path') return None else: return None
def a_star(self, graph: MatrixGraph, start: tuple, target: tuple, surface: pygame.Surface, display: pygame.Surface) -> list[tuple[int, int]]: """ The heuristic used is the distance from target to the current node if f(n) = 0 we have reached our node, our promising choice is the min(f(n)) for each neighbour """ open_queue = PriorityQueue() open_queue.put((graph.euclidean_distance(start, target), (start, 0))) closed = {start} paths = {} # A dictionary that maps new nodes to the previous node found = False # Variables and Surfaces used to display the current iterations counter = 0 # Pygame clock clock = Timer() while not open_queue.empty() and not found: # Draw and update iteration counter counter += 1 iteration_counter = f'Nodes Searched: {counter}' self._draw_loop_iterations(iteration_counter, surface) curr = open_queue.get() closed.add(curr[1][0]) _ = pygame.event.get() # Call event.get to stop program from crashing on clicks curr_x = curr[1][0][0] + self._maze_x_offset + 1 curr_y = curr[1][0][1] + self._maze_y_offset + 1 pygame.draw.circle(surface, (255, 0, 0), (curr_x, curr_y), 3) display.blit(surface, (0, 0)) pygame.display.flip() if curr[1][0] == target: found = True neighbours = graph.get_valid_neighbours(curr[1][0][0], curr[1][0][1]) for coord in neighbours: if coord in closed: # If the neighbor has already been computed, do nothing continue if not any(tup[1][0] == coord for tup in open_queue.queue): # If the neighbor is not in the the open queue, add it # Compute the heuristic and add it to open neighbour_f = curr[1][1] + 1 + graph.euclidean_distance(target, coord) open_queue.put((neighbour_f, (coord, curr[1][1] + 1))) # Track the path paths[coord] = curr[1][0] # Update clock clock.update_time() self._draw_timer(clock, surface) if found is False: return [] else: return self._find_and_draw_final_path(paths, start, target, surface, display)
def test5(self): mg = MatrixGraph([[4, 5, 2], [5, 1, 5], [8, 1, 1]]) expected = [[False, False, False], [False, False, False], [True, False, False]] self.assertEqual(expected, mg.mark_plateaus())
def test4(self): mg = MatrixGraph([[1, 9, 1], [3, 5, 4], [2, 5, 1]]) expected = [[False, True, False], [False, False, False], [False, False, False]] self.assertEqual(expected, mg.mark_plateaus())
def test3(self): mg = MatrixGraph([[1, 8, 2, 5], [6, 7, 4, 3], [9, 8, 2, 3], [1, 4, 2, 2]]) expected = [[False, True, False, True], [False, False, False, False], [True, False, False, False], [False, False, False, False]] self.assertEqual(expected, mg.mark_plateaus())
def test2(self): mg = MatrixGraph([[3]]) expected = [[True]] self.assertEqual(expected, mg.mark_plateaus())
def test1(self): mg = MatrixGraph([[7]]) neighbors = mg.get_neighbors((0, 0)) self.assertEqual(len(neighbors), 0)
def test1(self): mg = MatrixGraph([[4, 5, 2], [1, 5, 5], [2, 3, 1]]) expected = [[False, True, False], [False, True, True], [False, False, False]] self.assertEqual(expected, mg.mark_plateaus())
def test8(self): mg = MatrixGraph([[9, 9, 9], [9, 9, 9], [9, 9, 5]]) expected = [[True, True, True], [True, True, True], [True, True, False]] self.assertEqual(expected, mg.mark_plateaus())
def test7(self): mg = MatrixGraph([[5, 5, 5], [5, 5, 5], [5, 5, 9]]) expected = [[False, False, False], [False, False, False], [False, False, True]] self.assertEqual(expected, mg.mark_plateaus())
def initialize_maze( maze_path: str, rectangular: bool = True ) -> tuple[pygame.Surface, pygame.Surface, MatrixGraph, pygame.Surface, int, int, None, None, bool]: """ Initialize the program variables with respect to the maze found at maze_path. Return a tuple containing: (The Pygame Display, The Surface to draw on, The MatrixGraph for the maze, The MatrixGraph Surface layer, The amount of pixels used to center the width, The amount of pixels used to center the height, A None value representing the start of the maze, A None value representing the end of the maze, and a bool representing if we can run the program once) """ # Initialize pygame and the read the maze image pygame.init() maze = maze_path image = cv2.resize(cv2.imread(maze), (1280, 720)) # crop only works for rectangular mazes if rectangular: cropped = crop_image(image) else: cropped = image # Global thresholding using Otsu's binarization # Note: Although unpacking like this results in one of the variables to be unused and makes # PyTA heavily depressed, this is standard OpenCV notation. # For reference, you may check docs.opencv.org/master/d7/d4d/tutorial_py_thresholding.html retVal, thresh = cv2.threshold(cv2.cvtColor(cropped, cv2.COLOR_RGB2GRAY), 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) thinned = np.array(cv2.ximgproc.thinning(thresh)) // 255 # cut borders if not rectangular: temp = np.delete(thinned, [0, thinned.shape[1] - 1], axis=1) temp = np.delete(thinned, [0, thinned.shape[0] - 1], axis=0) graph = MatrixGraph(np.swapaxes(temp, 0, 1)) else: graph = MatrixGraph(np.swapaxes(thinned, 0, 1)) # Create pygame surfaces for the display, and the maze display_surface = pygame.display.set_mode( (1280 + PADDING_X, 720 + GUI_Y_OFFSET + PADDING_Y)) maze_surface = pygame.surfarray.make_surface(np.swapaxes(cropped, 0, 1)) # Center the maze image maze_img_w = maze_surface.get_width() maze_img_h = maze_surface.get_height() surface_w = display_surface.get_width() surface_h = display_surface.get_height() maze_centered_width = ((surface_w - maze_img_w) // 2) maze_centered_height = ((surface_h - maze_img_h) // 2) + 2 * GUI_Y_OFFSET # Draw the maze at the centered location display_surface.blit(maze_surface, (maze_centered_width, maze_centered_height)) # Create the surface to draw the pathing on surface = pygame.Surface( (1280 + PADDING_X, 720 + GUI_Y_OFFSET + PADDING_Y), pygame.SRCALPHA, 32) surface = surface.convert_alpha() display_surface.blit(surface, (0, 0)) # Initialize starting variables start_vertex = None end_vertex = None run_once = True return (display_surface, surface, graph, maze_surface, maze_centered_width, maze_centered_height, start_vertex, end_vertex, run_once)