diff --git a/README.md b/README.md index b068578..c063f56 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,25 @@ # Maze generator and solver -Python scripts for generating random solvable mazes using the depth-first search and recursive backtracking algorithms. The code also implements a recursive backtracking pathfinding algorithm for solving the generated mazes. Here is an example of a generated maze and its computed solution. +Python scripts for generating random solvable mazes using the depth-first search and recursive backtracking algorithms. The code also implements a recursive backtracking pathfinding algorithm for solving the generated mazes. -Both the generator and solver algorithm uses recursive backtracking and here an example of the latter can be seen. Cells indicated in light orange are part of the backtracking. The algorithms works by moving randomly from a cell to one of its unvisited neighbours. If the search reaches cell which have no unvisited neighbours, the search backtracks until it moves to a cell with unvisited neighbours. The generator algorithm is heavily inspired by the psuedo code provided by [Wikipedia](https://en.wikipedia.org/wiki/Maze_generation_algorithm). The main difference between the generator and solver algorithms are in the fact that, when solving the maze, one has to take into account not being able to move through walls. And thus proper pathfinding needs to be implemented. There's also impmeneted an ehnaced version of the solver algorithm which moves not to a random neighbour, but moves to the neighbour that minimizes the distance sqrt(x^2 + y^2) to the exit cell (final destination). +![maze_solution.png](maze_solution.png) +Both the generator and the solver algorithm use recursive backtracking. Here is an example of how that looks like: + +![backtracking.png](backtracking.png) + + Cells indicated in light orange are part of the backtracking. The algorithm moves randomly from a cell to one of its unvisited neighbors. If the search reaches a cell for which all neighbors are visited, the search backtracks until it moves to a cell with unvisited neighbors. + + The generator algorithm is inspired by the pseudocode provided by [Wikipedia](https://en.wikipedia.org/wiki/Maze_generation_algorithm). The main difference between the generator and solver algorithms is that, when solving the maze, one has to take into account not being able to move through walls, so proper pathfinding needs to be implemented. There is also an enhanced version of the solver algorithm. Instead of jumping to a random neighbor, this version moves to the one that minimizes the L2 distance, sqrt(x^2 + y^2), to the exit cell. ## Quick Use Guide -The first step is to install the dependancies by opening the terminal, navigating to -the MazeGenerator directory, and running -`pip install -r requirements.txt` +The first step is to install the dependencies is by opening the terminal, navigating to the MazeGenerator directory, and running -Next, run the `quick_start` python example under the examples directory. If this ran without any errors, -you should be fine to create your own program. Use the format outlined in quick_start, or use +`pip install -e .` + +Next, run the `quick_start` python example under the examples directory. If this runs without any errors, +you should be fine to create your program. Use the format outlined in quick_start, or use another example as a template. The process for creating and solving a maze follows. @@ -22,15 +29,12 @@ The process for creating and solving a maze follows. 3. Solve the maze 4. Optionally visualize the results - An example of using the library with different options is shown below. - ```python -from __future__ import absolute_import -from src.maze_manager import MazeManager -from src.maze import Maze +from pymaze.maze_manager import MazeManager +from pymaze.maze import Maze if __name__ == "__main__": @@ -76,25 +80,22 @@ if __name__ == "__main__": manager.show_solution(maze.id) ``` - - - ## Developer's Guide ### Source Layout -* /src/ Holds the source code (modules) needed to run MazeGenerator. -* /tests/ Holds the unit tests that test the code in /src/ -* /examples/ Example files that demonstrate how to use the library. +* /pymaze/ Holds the source code (modules) needed to run MazeGenerator. +* /tests/ Holds the unit tests that test the code in /src/ +* /examples/ Example files that demonstrate how to use the library. ### Class Overview + * The`Maze` class. This class provides helper functions to easily manipulate the cells. It can be thought of as being a grid of Cells * The `Cell` class is used to keep track of walls, and is what makes up the list. * The `Visualizer` class is responsible for handling the generation, display, and saving of animations and grid images. It can be interacted with directly, or controlled thought the `MazeManager` class. -* The `Solve` class. All solution methods are derived from this class. +* The `Solve` class. All solution methods are derived from this class. * The `MazeManager` class acts as the glue, bridging the `Visualizer`, `Maze`, and `Solve` classes together. - ### Adding a new Solution Algorithm #### Additional Overhead @@ -110,9 +111,9 @@ Be sure to create a new example using the new generation algorithm. #### Using the linter The style guide employed is pycodestyle. To install pycodestyle, navigate to the main directory and run - + `pip install -r requirements.txt` To check your file run - -`pycodestyle src/my_file.py`. \ No newline at end of file + +`pycodestyle pymaze/my_file.py`. diff --git a/examples/generate_binary_tree_algorithm.py b/examples/generate_binary_tree_algorithm.py index d9f116e..d65b8e9 100644 --- a/examples/generate_binary_tree_algorithm.py +++ b/examples/generate_binary_tree_algorithm.py @@ -1,6 +1,5 @@ -from __future__ import absolute_import -from src.maze_manager import MazeManager -from src.maze import Maze +from pymaze.maze_manager import MazeManager +from pymaze.maze import Maze if __name__ == "__main__": diff --git a/examples/quick_start.py b/examples/quick_start.py index f43bd22..6450315 100644 --- a/examples/quick_start.py +++ b/examples/quick_start.py @@ -1,6 +1,5 @@ -from __future__ import absolute_import -from src.maze_manager import MazeManager -from src.maze import Maze +from pymaze.maze_manager import MazeManager +from pymaze.maze import Maze if __name__ == "__main__": @@ -23,15 +22,15 @@ # by default when creating a maze, depth first search is used. # to generate maze using binary tree method, - maze_binTree = Maze(10, 10, algorithm = "bin_tree") + maze_binTree = Maze(10, 10, algorithm="bin_tree") maze_binTree = manager.add_existing_maze(maze_binTree) # We can disable showing any output from the solver by entering quiet mode # manager.set_quiet_mode(True) # Once we have a maze in the manager, we can tell the manager to solve it with a particular algorithm. - #manager.solve_maze(maze.id, "BreadthFirst") - #manager.solve_maze(maze.id, "BiDirectional") + # manager.solve_maze(maze.id, "BreadthFirst") + # manager.solve_maze(maze.id, "BiDirectional") manager.solve_maze(maze.id, "DepthFirstBacktracker") # If we want to save the maze & maze solution images along with their animations, we need to let the manager know. diff --git a/examples/solve_bi_directional.py b/examples/solve_bi_directional.py index 6566bd8..89528af 100644 --- a/examples/solve_bi_directional.py +++ b/examples/solve_bi_directional.py @@ -1,5 +1,4 @@ -from __future__ import absolute_import -from src.maze_manager import MazeManager +from pymaze.maze_manager import MazeManager if __name__ == "__main__": diff --git a/examples/solve_breadth_first_recursive.py b/examples/solve_breadth_first_recursive.py index d9c6b98..952a21c 100644 --- a/examples/solve_breadth_first_recursive.py +++ b/examples/solve_breadth_first_recursive.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -from src.maze_manager import MazeManager +from pymaze.maze_manager import MazeManager if __name__ == "__main__": diff --git a/examples/solve_depth_first_recursive.py b/examples/solve_depth_first_recursive.py index f28f7e3..c504de6 100644 --- a/examples/solve_depth_first_recursive.py +++ b/examples/solve_depth_first_recursive.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -from src.maze_manager import MazeManager +from pymaze.maze_manager import MazeManager if __name__ == "__main__": diff --git a/src/__init__.py b/pymaze/__init__.py similarity index 100% rename from src/__init__.py rename to pymaze/__init__.py diff --git a/pymaze/algorithm.py b/pymaze/algorithm.py new file mode 100644 index 0000000..e0151dd --- /dev/null +++ b/pymaze/algorithm.py @@ -0,0 +1,187 @@ +import time +import random + +# global variable to store list of all available algorithms +algorithm_list = ["dfs_backtrack", "bin_tree"] + + +def depth_first_recursive_backtracker(maze, start_coor): + k_curr, l_curr = start_coor # Where to start generating + path = [(k_curr, l_curr)] # To track path of solution + maze.grid[k_curr][l_curr].visited = True # Set initial cell to visited + visit_counter = 1 # To count number of visited cells + visited_cells = list() # Stack of visited cells for backtracking + + print("\nGenerating the maze with depth-first search...") + time_start = time.time() + + while visit_counter < maze.grid_size: # While there are unvisited cells + neighbour_indices = maze.find_neighbours( + k_curr, l_curr + ) # Find neighbour indicies + neighbour_indices = maze._validate_neighbours_generate(neighbour_indices) + + if neighbour_indices is not None: # If there are unvisited neighbour cells + visited_cells.append((k_curr, l_curr)) # Add current cell to stack + k_next, l_next = random.choice(neighbour_indices) # Choose random neighbour + maze.grid[k_curr][l_curr].remove_walls( + k_next, l_next + ) # Remove walls between neighbours + maze.grid[k_next][l_next].remove_walls( + k_curr, l_curr + ) # Remove walls between neighbours + maze.grid[k_next][l_next].visited = True # Move to that neighbour + k_curr = k_next + l_curr = l_next + path.append((k_curr, l_curr)) # Add coordinates to part of generation path + visit_counter += 1 + + elif len(visited_cells) > 0: # If there are no unvisited neighbour cells + ( + k_curr, + l_curr, + ) = visited_cells.pop() # Pop previous visited cell (backtracking) + path.append((k_curr, l_curr)) # Add coordinates to part of generation path + + print("Number of moves performed: {}".format(len(path))) + print("Execution time for algorithm: {:.4f}".format(time.time() - time_start)) + + maze.grid[maze.entry_coor[0]][maze.entry_coor[1]].set_as_entry_exit( + "entry", maze.num_rows - 1, maze.num_cols - 1 + ) + maze.grid[maze.exit_coor[0]][maze.exit_coor[1]].set_as_entry_exit( + "exit", maze.num_rows - 1, maze.num_cols - 1 + ) + + for i in range(maze.num_rows): + for j in range(maze.num_cols): + maze.grid[i][ + j + ].visited = False # Set all cells to unvisited before returning grid + + maze.generation_path = path + + +def binary_tree(maze, start_coor): + # store the current time + time_start = time.time() + + # repeat the following for all rows + for i in range(0, maze.num_rows): + + # check if we are in top row + if i == maze.num_rows - 1: + # remove the right wall for this, because we cant remove top wall + for j in range(0, maze.num_cols - 1): + maze.grid[i][j].remove_walls(i, j + 1) + maze.grid[i][j + 1].remove_walls(i, j) + + # go to the next row + break + + # repeat the following for all cells in rows + for j in range(0, maze.num_cols): + + # check if we are in the last column + if j == maze.num_cols - 1: + # remove only the top wall for this cell + maze.grid[i][j].remove_walls(i + 1, j) + maze.grid[i + 1][j].remove_walls(i, j) + continue + + # for all other cells + # randomly choose between 0 and 1. + # if we get 0, remove top wall; otherwise remove right wall + remove_top = random.choice([True, False]) + + # if we chose to remove top wall + if remove_top: + maze.grid[i][j].remove_walls(i + 1, j) + maze.grid[i + 1][j].remove_walls(i, j) + # if we chose top remove right wall + else: + maze.grid[i][j].remove_walls(i, j + 1) + maze.grid[i][j + 1].remove_walls(i, j) + + print("Number of moves performed: {}".format(maze.num_cols * maze.num_rows)) + print("Execution time for algorithm: {:.4f}".format(time.time() - time_start)) + + # choose the entry and exit coordinates + maze.grid[maze.entry_coor[0]][maze.entry_coor[1]].set_as_entry_exit( + "entry", maze.num_rows - 1, maze.num_cols - 1 + ) + maze.grid[maze.exit_coor[0]][maze.exit_coor[1]].set_as_entry_exit( + "exit", maze.num_rows - 1, maze.num_cols - 1 + ) + + # create a path for animating the maze creation using a binary tree + path = list() + # variable for holding number of cells visited until now + visit_counter = 0 + # created list of cell visited uptil now to for backtracking + visited = list() + + # create variables to hold the coords of current cell + # no matter what the user gives as start coords, we choose the + k_curr, l_curr = (maze.num_rows - 1, maze.num_cols - 1) + # add first cell to the path + path.append((k_curr, l_curr)) + + # mark first cell as visited + begin_time = time.time() + + # repeat until all the cells have been visited + while visit_counter < maze.grid_size: # While there are unvisited cells + + # for each cell, we only visit top and right cells. + possible_neighbours = list() + + try: + # take only those cells that are unvisited and accessible + if not maze.grid[k_curr - 1][l_curr].visited and k_curr != 0: + if not maze.grid[k_curr][l_curr].is_walls_between( + maze.grid[k_curr - 1][l_curr] + ): + possible_neighbours.append((k_curr - 1, l_curr)) + except: + print() + + try: + # take only those cells that are unvisited and accessible + if not maze.grid[k_curr][l_curr - 1].visited and l_curr != 0: + if not maze.grid[k_curr][l_curr].is_walls_between( + maze.grid[k_curr][l_curr - 1] + ): + possible_neighbours.append((k_curr, l_curr - 1)) + except: + print() + + # if there are still traversible cell from current cell + if len(possible_neighbours) != 0: + # select to first element to traverse + k_next, l_next = possible_neighbours[0] + # add this cell to the path + path.append(possible_neighbours[0]) + # add this cell to the visited + visited.append((k_curr, l_curr)) + # mark this cell as visited + maze.grid[k_next][l_next].visited = True + + visit_counter += 1 + + # update the current cell coords + k_curr, l_curr = k_next, l_next + + else: + # check if no more cells can be visited + if len(visited) != 0: + k_curr, l_curr = visited.pop() + path.append((k_curr, l_curr)) + else: + break + for row in maze.grid: + for cell in row: + cell.visited = False + + print(f"Generating path for maze took {time.time() - begin_time}s.") + maze.generation_path = path diff --git a/src/cell.py b/pymaze/cell.py similarity index 64% rename from src/cell.py rename to pymaze/cell.py index 534c065..053c471 100644 --- a/src/cell.py +++ b/pymaze/cell.py @@ -1,23 +1,28 @@ - class Cell(object): """Class for representing a cell in a 2D grid. - Attributes: - row (int): The row that this cell belongs to - col (int): The column that this cell belongs to - visited (bool): True if this cell has been visited by an algorithm - active (bool): - is_entry_exit (bool): True when the cell is the beginning or end of the maze - walls (list): - neighbours (list): + Attributes: + row (int): The row that this cell belongs to + col (int): The column that this cell belongs to + visited (bool): True if this cell has been visited by an algorithm + active (bool): + is_entry_exit (bool): True when the cell is the beginning or end of the maze + walls (list): + neighbours (list): """ + def __init__(self, row, col): self.row = row self.col = col self.visited = False self.active = False self.is_entry_exit = None - self.walls = {"top": True, "right": True, "bottom": True, "left": True} + self.walls = { + "top": True, + "right": True, + "bottom": True, + "left": True, + } self.neighbours = list() def is_walls_between(self, neighbour): @@ -32,13 +37,29 @@ def is_walls_between(self, neighbour): False: If there are no walls in between the neighbors and self """ - if self.row - neighbour.row == 1 and self.walls["top"] and neighbour.walls["bottom"]: + if ( + self.row - neighbour.row == 1 + and self.walls["top"] + and neighbour.walls["bottom"] + ): return True - elif self.row - neighbour.row == -1 and self.walls["bottom"] and neighbour.walls["top"]: + elif ( + self.row - neighbour.row == -1 + and self.walls["bottom"] + and neighbour.walls["top"] + ): return True - elif self.col - neighbour.col == 1 and self.walls["left"] and neighbour.walls["right"]: + elif ( + self.col - neighbour.col == 1 + and self.walls["left"] + and neighbour.walls["right"] + ): return True - elif self.col - neighbour.col == -1 and self.walls["right"] and neighbour.walls["left"]: + elif ( + self.col - neighbour.col == -1 + and self.walls["right"] + and neighbour.walls["left"] + ): return True return False @@ -46,13 +67,13 @@ def is_walls_between(self, neighbour): def remove_walls(self, neighbour_row, neighbour_col): """Function that removes walls between neighbour cell given by indices in grid. - Args: - neighbour_row (int): - neighbour_col (int): + Args: + neighbour_row (int): + neighbour_col (int): - Return: - True: If the operation was a success - False: If the operation failed + Return: + True: If the operation was a success + False: If the operation failed """ if self.row - neighbour_row == 1: diff --git a/src/maze.py b/pymaze/maze.py similarity index 68% rename from src/maze.py rename to pymaze/maze.py index fecf919..1a07a8c 100644 --- a/src/maze.py +++ b/pymaze/maze.py @@ -1,9 +1,7 @@ - import random import math -import time -from src.cell import Cell -from src.algorithm import depth_first_recursive_backtracker, binary_tree +from .cell import Cell +from .algorithm import depth_first_recursive_backtracker, binary_tree class Maze(object): @@ -21,21 +19,21 @@ class Maze(object): solution_path : The path that was taken by a solver when solving the maze initial_grid (list): grid (list): A copy of initial_grid (possible this is un-needed) - """ + """ - def __init__(self, num_rows, num_cols, id=0, algorithm = "dfs_backtrack"): + def __init__(self, num_rows, num_cols, id=0, algorithm="dfs_backtrack"): """Creates a gird of Cell objects that are neighbors to each other. - Args: - num_rows (int): The width of the maze, in cells - num_cols (int): The height of the maze in cells - id (id): An unique identifier + Args: + num_rows (int): The width of the maze, in cells + num_cols (int): The height of the maze in cells + id (id): An unique identifier """ self.num_cols = num_cols self.num_rows = num_rows self.id = id - self.grid_size = num_rows*num_cols + self.grid_size = num_rows * num_cols self.entry_coor = self._pick_random_entry_exit(None) self.exit_coor = self._pick_random_entry_exit(self.entry_coor) self.generation_path = [] @@ -81,19 +79,24 @@ def find_neighbours(self, cell_row, cell_col): def check_neighbour(row, col): # Check that a neighbour exists and that it's not visited before. - if row >= 0 and row < self.num_rows and col >= 0 and col < self.num_cols: + if ( + row >= 0 + and row < self.num_rows + and col >= 0 + and col < self.num_cols + ): neighbours.append((row, col)) - check_neighbour(cell_row-1, cell_col) # Top neighbour - check_neighbour(cell_row, cell_col+1) # Right neighbour - check_neighbour(cell_row+1, cell_col) # Bottom neighbour - check_neighbour(cell_row, cell_col-1) # Left neighbour + check_neighbour(cell_row - 1, cell_col) # Top neighbour + check_neighbour(cell_row, cell_col + 1) # Right neighbour + check_neighbour(cell_row + 1, cell_col) # Bottom neighbour + check_neighbour(cell_row, cell_col - 1) # Left neighbour if len(neighbours) > 0: return neighbours else: - return None # None if no unvisited neighbours found + return None # None if no unvisited neighbours found def _validate_neighbours_generate(self, neighbour_indices): """Function that validates whether a neighbour is unvisited or not. When generating @@ -108,14 +111,18 @@ def _validate_neighbours_generate(self, neighbour_indices): """ - neigh_list = [n for n in neighbour_indices if not self.grid[n[0]][n[1]].visited] + neigh_list = [ + n for n in neighbour_indices if not self.grid[n[0]][n[1]].visited + ] if len(neigh_list) > 0: return neigh_list else: return None - def validate_neighbours_solve(self, neighbour_indices, k, l, k_end, l_end, method = "fancy"): + def validate_neighbours_solve( + self, neighbour_indices, k, l, k_end, l_end, method="fancy" + ): """Function that validates whether a neighbour is unvisited or not and discards the neighbours that are inaccessible due to walls between them and the current cell. The function implements two methods for choosing next cell; one is 'brute-force' where one @@ -139,11 +146,14 @@ def validate_neighbours_solve(self, neighbour_indices, k, l, k_end, l_end, metho min_dist_to_target = 100000 for k_n, l_n in neighbour_indices: - if (not self.grid[k_n][l_n].visited - and not self.grid[k][l].is_walls_between(self.grid[k_n][l_n])): - dist_to_target = math.sqrt((k_n - k_end) ** 2 + (l_n - l_end) ** 2) - - if (dist_to_target < min_dist_to_target): + if not self.grid[k_n][l_n].visited and not self.grid[k][ + l + ].is_walls_between(self.grid[k_n][l_n]): + dist_to_target = math.sqrt( + (k_n - k_end) ** 2 + (l_n - l_end) ** 2 + ) + + if dist_to_target < min_dist_to_target: min_dist_to_target = dist_to_target min_neigh = (k_n, l_n) @@ -151,8 +161,14 @@ def validate_neighbours_solve(self, neighbour_indices, k, l, k_end, l_end, metho neigh_list.append(min_neigh) elif method == "brute-force": - neigh_list = [n for n in neighbour_indices if not self.grid[n[0]][n[1]].visited - and not self.grid[k][l].is_walls_between(self.grid[n[0]][n[1]])] + neigh_list = [ + n + for n in neighbour_indices + if not self.grid[n[0]][n[1]].visited + and not self.grid[k][l].is_walls_between( + self.grid[n[0]][n[1]] + ) + ] if len(neigh_list) > 0: return neigh_list @@ -169,27 +185,33 @@ def _pick_random_entry_exit(self, used_entry_exit=None): Return: """ - rng_entry_exit = used_entry_exit # Initialize with used value + rng_entry_exit = used_entry_exit # Initialize with used value # Try until unused location along boundary is found. while rng_entry_exit == used_entry_exit: rng_side = random.randint(0, 3) - if (rng_side == 0): # Top side - rng_entry_exit = (0, random.randint(0, self.num_cols-1)) + if rng_side == 0: # Top side + rng_entry_exit = (0, random.randint(0, self.num_cols - 1)) - elif (rng_side == 2): # Right side - rng_entry_exit = (self.num_rows-1, random.randint(0, self.num_cols-1)) + elif rng_side == 2: # Right side + rng_entry_exit = ( + self.num_rows - 1, + random.randint(0, self.num_cols - 1), + ) - elif (rng_side == 1): # Bottom side - rng_entry_exit = (random.randint(0, self.num_rows-1), self.num_cols-1) + elif rng_side == 1: # Bottom side + rng_entry_exit = ( + random.randint(0, self.num_rows - 1), + self.num_cols - 1, + ) - elif (rng_side == 3): # Left side - rng_entry_exit = (random.randint(0, self.num_rows-1), 0) + elif rng_side == 3: # Left side + rng_entry_exit = (random.randint(0, self.num_rows - 1), 0) - return rng_entry_exit # Return entry/exit that is different from exit/entry + return rng_entry_exit # Return entry/exit that is different from exit/entry - def generate_maze(self, algorithm, start_coor = (0, 0)): + def generate_maze(self, algorithm, start_coor=(0, 0)): """This takes the internal grid object and removes walls between cells using the depth-first recursive backtracker algorithm. diff --git a/src/maze_manager.py b/pymaze/maze_manager.py similarity index 85% rename from src/maze_manager.py rename to pymaze/maze_manager.py index 3630f92..cb1fd11 100644 --- a/src/maze_manager.py +++ b/pymaze/maze_manager.py @@ -1,8 +1,8 @@ -from src.maze import Maze -from src.maze_viz import Visualizer -from src.solver import DepthFirstBacktracker -from src.solver import BiDirectional -from src.solver import BreadthFirst +from .maze import Maze +from .maze_viz import Visualizer +from .solver import DepthFirstBacktracker +from .solver import BiDirectional +from .solver import BreadthFirst class MazeManager(object): @@ -67,7 +67,7 @@ def add_existing_maze(self, maze, override=True): if len(self.mazes) < 1: maze.id = 0 else: - maze.id = self.mazes.__len__()+1 + maze.id = self.mazes.__len__() + 1 else: return False self.mazes.append(maze) @@ -76,12 +76,12 @@ def add_existing_maze(self, maze, override=True): def get_maze(self, id): """Get a maze by its id. - Args: - id (int): The id of the desired maze + Args: + id (int): The id of the desired maze - Return: - Maze: Returns the maze if it was found. - None: If no maze was found + Return: + Maze: Returns the maze if it was found. + None: If no maze was found """ for maze in self.mazes: @@ -99,7 +99,7 @@ def get_maze_count(self): return self.mazes.__len__() def solve_maze(self, maze_id, method, neighbor_method="fancy"): - """ Called to solve a maze by a particular method. The method + """Called to solve a maze by a particular method. The method is specified by a string. The options are 1. DepthFirstBacktracker 2. @@ -118,7 +118,9 @@ def solve_maze(self, maze_id, method, neighbor_method="fancy"): """DEVNOTE: When adding a new solution method, call it from here. Also update the list of names in the documentation above""" if method == "DepthFirstBacktracker": - solver = DepthFirstBacktracker(maze, neighbor_method, self.quiet_mode) + solver = DepthFirstBacktracker( + maze, neighbor_method, self.quiet_mode + ) maze.solution_path = solver.solve() elif method == "BiDirectional": solver = BiDirectional(maze, neighbor_method, self.quiet_mode) @@ -140,7 +142,7 @@ def show_solution(self, id, cell_size=1): vis = Visualizer(self.get_maze(id), cell_size, self.media_name) vis.show_maze_solution() - def show_solution_animation(self, id, cell_size =1): + def show_solution_animation(self, id, cell_size=1): """ Shows the animation of the path that the solver took. @@ -160,7 +162,7 @@ def check_matching_id(self, id): Returns: """ - return next((maze for maze in self.mazes if maze .id == id), None) + return next((maze for maze in self.mazes if maze.id == id), None) def set_filename(self, filename): """ @@ -177,4 +179,4 @@ def set_quiet_mode(self, enabled): Args: enabled (bool): True when quiet mode is on, False when it is off """ - self.quiet_mode=enabled + self.quiet_mode = enabled diff --git a/pymaze/maze_viz.py b/pymaze/maze_viz.py new file mode 100644 index 0000000..e43cf86 --- /dev/null +++ b/pymaze/maze_viz.py @@ -0,0 +1,545 @@ +import matplotlib.pyplot as plt +from matplotlib import animation +import logging + +logging.basicConfig(level=logging.DEBUG) + + +class Visualizer(object): + """Class that handles all aspects of visualization. + + + Attributes: + maze: The maze that will be visualized + cell_size (int): How large the cells will be in the plots + height (int): The height of the maze + width (int): The width of the maze + ax: The axes for the plot + lines: + squares: + media_filename (string): The name of the animations and images + + """ + + def __init__(self, maze, cell_size, media_filename): + self.maze = maze + self.cell_size = cell_size + self.height = maze.num_rows * cell_size + self.width = maze.num_cols * cell_size + self.ax = None + self.lines = dict() + self.squares = dict() + self.media_filename = media_filename + + def set_media_filename(self, filename): + """Sets the filename of the media + Args: + filename (string): The name of the media + """ + self.media_filename = filename + + def show_maze(self): + """Displays a plot of the maze without the solution path""" + + # Create the plot figure and style the axes + fig = self.configure_plot() + + # Plot the walls on the figure + self.plot_walls() + + # Display the plot to the user + plt.show() + + # Handle any potential saving + if self.media_filename: + fig.savefig( + "{}{}.png".format(self.media_filename, "_generation"), + frameon=None, + ) + + def plot_walls(self): + """Plots the walls of a maze. This is used when generating the maze image""" + for i in range(self.maze.num_rows): + for j in range(self.maze.num_cols): + if self.maze.initial_grid[i][j].is_entry_exit == "entry": + self.ax.text( + j * self.cell_size, + i * self.cell_size, + "START", + fontsize=7, + weight="bold", + ) + elif self.maze.initial_grid[i][j].is_entry_exit == "exit": + self.ax.text( + j * self.cell_size, + i * self.cell_size, + "END", + fontsize=7, + weight="bold", + ) + if self.maze.initial_grid[i][j].walls["top"]: + self.ax.plot( + [j * self.cell_size, (j + 1) * self.cell_size], + [i * self.cell_size, i * self.cell_size], + color="k", + ) + if self.maze.initial_grid[i][j].walls["right"]: + self.ax.plot( + [(j + 1) * self.cell_size, (j + 1) * self.cell_size], + [i * self.cell_size, (i + 1) * self.cell_size], + color="k", + ) + if self.maze.initial_grid[i][j].walls["bottom"]: + self.ax.plot( + [(j + 1) * self.cell_size, j * self.cell_size], + [(i + 1) * self.cell_size, (i + 1) * self.cell_size], + color="k", + ) + if self.maze.initial_grid[i][j].walls["left"]: + self.ax.plot( + [j * self.cell_size, j * self.cell_size], + [(i + 1) * self.cell_size, i * self.cell_size], + color="k", + ) + + def configure_plot(self): + """Sets the initial properties of the maze plot. Also creates the plot and axes""" + + # Create the plot figure + fig = plt.figure( + figsize=(7, 7 * self.maze.num_rows / self.maze.num_cols) + ) + + # Create the axes + self.ax = plt.axes() + + # Set an equal aspect ratio + self.ax.set_aspect("equal") + + # Remove the axes from the figure + self.ax.axes.get_xaxis().set_visible(False) + self.ax.axes.get_yaxis().set_visible(False) + + title_box = self.ax.text( + 0, + self.maze.num_rows + self.cell_size + 0.1, + r"{}$\times${}".format(self.maze.num_rows, self.maze.num_cols), + bbox={"facecolor": "gray", "alpha": 0.5, "pad": 4}, + fontname="serif", + fontsize=15, + ) + + return fig + + def show_maze_solution(self): + """Function that plots the solution to the maze. Also adds indication of entry and exit points.""" + + # Create the figure and style the axes + fig = self.configure_plot() + + # Plot the walls onto the figure + self.plot_walls() + + list_of_backtrackers = [ + path_element[0] + for path_element in self.maze.solution_path + if path_element[1] + ] + + # Keeps track of how many circles have been drawn + circle_num = 0 + + self.ax.add_patch( + plt.Circle( + ( + (self.maze.solution_path[0][0][1] + 0.5) * self.cell_size, + (self.maze.solution_path[0][0][0] + 0.5) * self.cell_size, + ), + 0.2 * self.cell_size, + fc=( + 0, + circle_num + / ( + len(self.maze.solution_path) + - 2 * len(list_of_backtrackers) + ), + 0, + ), + alpha=0.4, + ) + ) + + for i in range(1, self.maze.solution_path.__len__()): + if ( + self.maze.solution_path[i][0] not in list_of_backtrackers + and self.maze.solution_path[i - 1][0] + not in list_of_backtrackers + ): + circle_num += 1 + self.ax.add_patch( + plt.Circle( + ( + (self.maze.solution_path[i][0][1] + 0.5) + * self.cell_size, + (self.maze.solution_path[i][0][0] + 0.5) + * self.cell_size, + ), + 0.2 * self.cell_size, + fc=( + 0, + circle_num + / ( + len(self.maze.solution_path) + - 2 * len(list_of_backtrackers) + ), + 0, + ), + alpha=0.4, + ) + ) + + # Display the plot to the user + plt.show() + + # Handle any saving + if self.media_filename: + fig.savefig( + "{}{}.png".format(self.media_filename, "_solution"), + frameon=None, + ) + + def show_generation_animation(self): + """Function that animates the process of generating the a maze where path is a list + of coordinates indicating the path taken to carve out (break down walls) the maze.""" + + # Create the figure and style the axes + fig = self.configure_plot() + + # The square that represents the head of the algorithm + indicator = plt.Rectangle( + ( + self.maze.generation_path[0][0] * self.cell_size, + self.maze.generation_path[0][1] * self.cell_size, + ), + self.cell_size, + self.cell_size, + fc="purple", + alpha=0.6, + ) + + self.ax.add_patch(indicator) + + # Only need to plot right and bottom wall for each cell since walls overlap. + # Also adding squares to animate the path taken to carve out the maze. + color_walls = "k" + for i in range(self.maze.num_rows): + for j in range(self.maze.num_cols): + self.lines["{},{}: right".format(i, j)] = self.ax.plot( + [(j + 1) * self.cell_size, (j + 1) * self.cell_size], + [i * self.cell_size, (i + 1) * self.cell_size], + linewidth=2, + color=color_walls, + )[0] + self.lines["{},{}: bottom".format(i, j)] = self.ax.plot( + [(j + 1) * self.cell_size, j * self.cell_size], + [(i + 1) * self.cell_size, (i + 1) * self.cell_size], + linewidth=2, + color=color_walls, + )[0] + + self.squares["{},{}".format(i, j)] = plt.Rectangle( + (j * self.cell_size, i * self.cell_size), + self.cell_size, + self.cell_size, + fc="red", + alpha=0.4, + ) + self.ax.add_patch(self.squares["{},{}".format(i, j)]) + + # Plotting boundaries of maze. + color_boundary = "k" + self.ax.plot( + [0, self.width], + [self.height, self.height], + linewidth=2, + color=color_boundary, + ) + self.ax.plot( + [self.width, self.width], + [self.height, 0], + linewidth=2, + color=color_boundary, + ) + self.ax.plot( + [self.width, 0], [0, 0], linewidth=2, color=color_boundary + ) + self.ax.plot( + [0, 0], [0, self.height], linewidth=2, color=color_boundary + ) + + def animate(frame): + """Function to supervise animation of all objects.""" + animate_walls(frame) + animate_squares(frame) + animate_indicator(frame) + self.ax.set_title( + "Step: {}".format(frame + 1), fontname="serif", fontsize=19 + ) + return [] + + def animate_walls(frame): + """Function that animates the visibility of the walls between cells.""" + if frame > 0: + self.maze.grid[self.maze.generation_path[frame - 1][0]][ + self.maze.generation_path[frame - 1][1] + ].remove_walls( + self.maze.generation_path[frame][0], + self.maze.generation_path[frame][1], + ) # Wall between curr and neigh + + self.maze.grid[self.maze.generation_path[frame][0]][ + self.maze.generation_path[frame][1] + ].remove_walls( + self.maze.generation_path[frame - 1][0], + self.maze.generation_path[frame - 1][1], + ) # Wall between neigh and curr + + current_cell = self.maze.grid[ + self.maze.generation_path[frame - 1][0] + ][self.maze.generation_path[frame - 1][1]] + next_cell = self.maze.grid[ + self.maze.generation_path[frame][0] + ][self.maze.generation_path[frame][1]] + + """Function to animate walls between cells as the search goes on.""" + for wall_key in [ + "right", + "bottom", + ]: # Only need to draw two of the four walls (overlap) + if current_cell.walls[wall_key] is False: + self.lines[ + "{},{}: {}".format( + current_cell.row, current_cell.col, wall_key + ) + ].set_visible(False) + if next_cell.walls[wall_key] is False: + self.lines[ + "{},{}: {}".format( + next_cell.row, next_cell.col, wall_key + ) + ].set_visible(False) + + def animate_squares(frame): + """Function to animate the searched path of the algorithm.""" + self.squares[ + "{},{}".format( + self.maze.generation_path[frame][0], + self.maze.generation_path[frame][1], + ) + ].set_visible(False) + return [] + + def animate_indicator(frame): + """Function to animate where the current search is happening.""" + indicator.set_xy( + ( + self.maze.generation_path[frame][1] * self.cell_size, + self.maze.generation_path[frame][0] * self.cell_size, + ) + ) + return [] + + logging.debug("Creating generation animation") + anim = animation.FuncAnimation( + fig, + animate, + frames=self.maze.generation_path.__len__(), + interval=100, + blit=True, + repeat=False, + ) + + logging.debug("Finished creating the generation animation") + + # Display the plot to the user + plt.show() + + # Handle any saving + if self.media_filename: + print("Saving generation animation. This may take a minute....") + mpeg_writer = animation.FFMpegWriter( + fps=24, + bitrate=1000, + codec="libx264", + extra_args=["-pix_fmt", "yuv420p"], + ) + anim.save( + "{}{}{}x{}.mp4".format( + self.media_filename, + "_generation_", + self.maze.num_rows, + self.maze.num_cols, + ), + writer=mpeg_writer, + ) + + def add_path(self): + # Adding squares to animate the path taken to solve the maze. Also adding entry/exit text + color_walls = "k" + for i in range(self.maze.num_rows): + for j in range(self.maze.num_cols): + if self.maze.initial_grid[i][j].is_entry_exit == "entry": + self.ax.text( + j * self.cell_size, + i * self.cell_size, + "START", + fontsize=7, + weight="bold", + ) + elif self.maze.initial_grid[i][j].is_entry_exit == "exit": + self.ax.text( + j * self.cell_size, + i * self.cell_size, + "END", + fontsize=7, + weight="bold", + ) + + if self.maze.initial_grid[i][j].walls["top"]: + self.lines["{},{}: top".format(i, j)] = self.ax.plot( + [j * self.cell_size, (j + 1) * self.cell_size], + [i * self.cell_size, i * self.cell_size], + linewidth=2, + color=color_walls, + )[0] + if self.maze.initial_grid[i][j].walls["right"]: + self.lines["{},{}: right".format(i, j)] = self.ax.plot( + [(j + 1) * self.cell_size, (j + 1) * self.cell_size], + [i * self.cell_size, (i + 1) * self.cell_size], + linewidth=2, + color=color_walls, + )[0] + if self.maze.initial_grid[i][j].walls["bottom"]: + self.lines["{},{}: bottom".format(i, j)] = self.ax.plot( + [(j + 1) * self.cell_size, j * self.cell_size], + [(i + 1) * self.cell_size, (i + 1) * self.cell_size], + linewidth=2, + color=color_walls, + )[0] + if self.maze.initial_grid[i][j].walls["left"]: + self.lines["{},{}: left".format(i, j)] = self.ax.plot( + [j * self.cell_size, j * self.cell_size], + [(i + 1) * self.cell_size, i * self.cell_size], + linewidth=2, + color=color_walls, + )[0] + self.squares["{},{}".format(i, j)] = plt.Rectangle( + (j * self.cell_size, i * self.cell_size), + self.cell_size, + self.cell_size, + fc="red", + alpha=0.4, + visible=False, + ) + self.ax.add_patch(self.squares["{},{}".format(i, j)]) + + def animate_maze_solution(self): + """Function that animates the process of generating the a maze where path is a list + of coordinates indicating the path taken to carve out (break down walls) the maze.""" + + # Create the figure and style the axes + fig = self.configure_plot() + + # Adding indicator to see shere current search is happening. + indicator = plt.Rectangle( + ( + self.maze.solution_path[0][0][0] * self.cell_size, + self.maze.solution_path[0][0][1] * self.cell_size, + ), + self.cell_size, + self.cell_size, + fc="purple", + alpha=0.6, + ) + self.ax.add_patch(indicator) + + self.add_path() + + def animate_squares(frame): + """Function to animate the solved path of the algorithm.""" + if frame > 0: + if self.maze.solution_path[frame - 1][ + 1 + ]: # Color backtracking + self.squares[ + "{},{}".format( + self.maze.solution_path[frame - 1][0][0], + self.maze.solution_path[frame - 1][0][1], + ) + ].set_facecolor("orange") + + self.squares[ + "{},{}".format( + self.maze.solution_path[frame - 1][0][0], + self.maze.solution_path[frame - 1][0][1], + ) + ].set_visible(True) + self.squares[ + "{},{}".format( + self.maze.solution_path[frame][0][0], + self.maze.solution_path[frame][0][1], + ) + ].set_visible(False) + return [] + + def animate_indicator(frame): + """Function to animate where the current search is happening.""" + indicator.set_xy( + ( + self.maze.solution_path[frame][0][1] * self.cell_size, + self.maze.solution_path[frame][0][0] * self.cell_size, + ) + ) + return [] + + def animate(frame): + """Function to supervise animation of all objects.""" + animate_squares(frame) + animate_indicator(frame) + self.ax.set_title( + "Step: {}".format(frame + 1), fontname="serif", fontsize=19 + ) + return [] + + logging.debug("Creating solution animation") + anim = animation.FuncAnimation( + fig, + animate, + frames=self.maze.solution_path.__len__(), + interval=100, + blit=True, + repeat=False, + ) + logging.debug("Finished creating solution animation") + + # Display the animation to the user + plt.show() + + # Handle any saving + if self.media_filename: + print("Saving solution animation. This may take a minute....") + mpeg_writer = animation.FFMpegWriter( + fps=24, + bitrate=1000, + codec="libx264", + extra_args=["-pix_fmt", "yuv420p"], + ) + anim.save( + "{}{}{}x{}.mp4".format( + self.media_filename, + "_solution_", + self.maze.num_rows, + self.maze.num_cols, + ), + writer=mpeg_writer, + ) diff --git a/pymaze/solver.py b/pymaze/solver.py new file mode 100644 index 0000000..b6237d3 --- /dev/null +++ b/pymaze/solver.py @@ -0,0 +1,377 @@ +import time +import random +import logging +from .maze import Maze + +logging.basicConfig(level=logging.DEBUG) + + +class Solver(object): + """Base class for solution methods. + Every new solution method should override the solve method. + + Attributes: + maze (list): The maze which is being solved. + neighbor_method: + quiet_mode: When enabled, information is not outputted to the console + + """ + + def __init__(self, maze, quiet_mode, neighbor_method): + logging.debug("Class Solver ctor called") + + self.maze = maze + self.neighbor_method = neighbor_method + self.name = "" + self.quiet_mode = quiet_mode + + def solve(self): + logging.debug("Class: Solver solve called") + raise NotImplementedError + + def get_name(self): + logging.debug("Class Solver get_name called") + raise self.name + + def get_path(self): + logging.debug("Class Solver get_path called") + return self.path + + +class BreadthFirst(Solver): + def __init__(self, maze, quiet_mode=False, neighbor_method="fancy"): + logging.debug("Class BreadthFirst ctor called") + + self.name = "Breadth First Recursive" + super().__init__(maze, neighbor_method, quiet_mode) + + def solve(self): + + """Function that implements the breadth-first algorithm for solving the maze. This means that + for each iteration in the outer loop, the search visits one cell in all possible branches. Then + moves on to the next level of cells in each branch to continue the search.""" + + logging.debug("Class BreadthFirst solve called") + current_level = [ + self.maze.entry_coor + ] # Stack of cells at current level of search + path = list() # To track path of solution cell coordinates + + print("\nSolving the maze with breadth-first search...") + time_start = time.clock() + + while True: # Loop until return statement is encountered + next_level = list() + + while ( + current_level + ): # While still cells left to search on current level + k_curr, l_curr = current_level.pop( + 0 + ) # Search one cell on the current level + self.maze.grid[k_curr][ + l_curr + ].visited = True # Mark current cell as visited + path.append( + ((k_curr, l_curr), False) + ) # Append current cell to total search path + + if ( + k_curr, + l_curr, + ) == self.maze.exit_coor: # Exit if current cell is exit cell + if not self.quiet_mode: + print( + "Number of moves performed: {}".format(len(path)) + ) + print( + "Execution time for algorithm: {:.4f}".format( + time.clock() - time_start + ) + ) + return path + + neighbour_coors = self.maze.find_neighbours( + k_curr, l_curr + ) # Find neighbour indicies + neighbour_coors = self.maze.validate_neighbours_solve( + neighbour_coors, + k_curr, + l_curr, + self.maze.exit_coor[0], + self.maze.exit_coor[1], + self.neighbor_method, + ) + + if neighbour_coors is not None: + for coor in neighbour_coors: + next_level.append( + coor + ) # Add all existing real neighbours to next search level + + for cell in next_level: + current_level.append( + cell + ) # Update current_level list with cells for nex search level + logging.debug("Class BreadthFirst leaving solve") + + +class BiDirectional(Solver): + def __init__(self, maze, quiet_mode=False, neighbor_method="fancy"): + logging.debug("Class BiDirectional ctor called") + + super().__init__(maze, neighbor_method, quiet_mode) + self.name = "Bi Directional" + + def solve(self): + + """Function that implements a bidirectional depth-first recursive backtracker algorithm for + solving the maze, i.e. starting at the entry point and exit points where each search searches + for the other search path. NOTE: THE FUNCTION ENDS IN AN INFINITE LOOP FOR SOME RARE CASES OF + THE INPUT MAZE. WILL BE FIXED IN FUTURE.""" + logging.debug("Class BiDirectional solve called") + + grid = self.maze.grid + ( + k_curr, + l_curr, + ) = self.maze.entry_coor # Where to start the first search + ( + p_curr, + q_curr, + ) = self.maze.exit_coor # Where to start the second search + grid[k_curr][l_curr].visited = True # Set initial cell to visited + grid[p_curr][q_curr].visited = True # Set final cell to visited + backtrack_kl = list() # Stack of visited cells for backtracking + backtrack_pq = list() # Stack of visited cells for backtracking + path_kl = list() # To track path of solution and backtracking cells + path_pq = list() # To track path of solution and backtracking cells + + if not self.quiet_mode: + print( + "\nSolving the maze with bidirectional depth-first search..." + ) + time_start = time.clock() + + while True: # Loop until return statement is encountered + neighbours_kl = self.maze.find_neighbours( + k_curr, l_curr + ) # Find neighbours for first search + real_neighbours_kl = [ + neigh + for neigh in neighbours_kl + if not grid[k_curr][l_curr].is_walls_between( + grid[neigh[0]][neigh[1]] + ) + ] + neighbours_kl = [ + neigh + for neigh in real_neighbours_kl + if not grid[neigh[0]][neigh[1]].visited + ] + + neighbours_pq = self.maze.find_neighbours( + p_curr, q_curr + ) # Find neighbours for second search + real_neighbours_pq = [ + neigh + for neigh in neighbours_pq + if not grid[p_curr][q_curr].is_walls_between( + grid[neigh[0]][neigh[1]] + ) + ] + neighbours_pq = [ + neigh + for neigh in real_neighbours_pq + if not grid[neigh[0]][neigh[1]].visited + ] + + if ( + len(neighbours_kl) > 0 + ): # If there are unvisited neighbour cells + backtrack_kl.append( + (k_curr, l_curr) + ) # Add current cell to stack + path_kl.append( + ((k_curr, l_curr), False) + ) # Add coordinates to part of search path + k_next, l_next = random.choice( + neighbours_kl + ) # Choose random neighbour + grid[k_next][l_next].visited = True # Move to that neighbour + k_curr = k_next + l_curr = l_next + + elif ( + len(backtrack_kl) > 0 + ): # If there are no unvisited neighbour cells + path_kl.append( + ((k_curr, l_curr), True) + ) # Add coordinates to part of search path + ( + k_curr, + l_curr, + ) = ( + backtrack_kl.pop() + ) # Pop previous visited cell (backtracking) + + if ( + len(neighbours_pq) > 0 + ): # If there are unvisited neighbour cells + backtrack_pq.append( + (p_curr, q_curr) + ) # Add current cell to stack + path_pq.append( + ((p_curr, q_curr), False) + ) # Add coordinates to part of search path + p_next, q_next = random.choice( + neighbours_pq + ) # Choose random neighbour + grid[p_next][q_next].visited = True # Move to that neighbour + p_curr = p_next + q_curr = q_next + + elif ( + len(backtrack_pq) > 0 + ): # If there are no unvisited neighbour cells + path_pq.append( + ((p_curr, q_curr), True) + ) # Add coordinates to part of search path + ( + p_curr, + q_curr, + ) = ( + backtrack_pq.pop() + ) # Pop previous visited cell (backtracking) + + # Exit loop and return path if any opf the kl neighbours are in path_pq. + if any( + ( + True + for n_kl in real_neighbours_kl + if (n_kl, False) in path_pq + ) + ): + path_kl.append(((k_curr, l_curr), False)) + path = [ + p_el + for p_tuple in zip(path_kl, path_pq) + for p_el in p_tuple + ] # Zip paths + if not self.quiet_mode: + print("Number of moves performed: {}".format(len(path))) + print( + "Execution time for algorithm: {:.4f}".format( + time.clock() - time_start + ) + ) + logging.debug("Class BiDirectional leaving solve") + return path + + # Exit loop and return path if any opf the pq neighbours are in path_kl. + elif any( + ( + True + for n_pq in real_neighbours_pq + if (n_pq, False) in path_kl + ) + ): + path_pq.append(((p_curr, q_curr), False)) + path = [ + p_el + for p_tuple in zip(path_kl, path_pq) + for p_el in p_tuple + ] # Zip paths + if not self.quiet_mode: + print("Number of moves performed: {}".format(len(path))) + print( + "Execution time for algorithm: {:.4f}".format( + time.clock() - time_start + ) + ) + logging.debug("Class BiDirectional leaving solve") + return path + + +class DepthFirstBacktracker(Solver): + """A solver that implements the depth-first recursive backtracker algorithm.""" + + def __init__(self, maze, quiet_mode=False, neighbor_method="fancy"): + logging.debug("Class DepthFirstBacktracker ctor called") + + super().__init__(maze, neighbor_method, quiet_mode) + self.name = "Depth First Backtracker" + + def solve(self): + logging.debug("Class DepthFirstBacktracker solve called") + k_curr, l_curr = self.maze.entry_coor # Where to start searching + self.maze.grid[k_curr][ + l_curr + ].visited = True # Set initial cell to visited + visited_cells = list() # Stack of visited cells for backtracking + path = list() # To track path of solution and backtracking cells + if not self.quiet_mode: + print("\nSolving the maze with depth-first search...") + + time_start = time.time() + + while ( + k_curr, + l_curr, + ) != self.maze.exit_coor: # While the exit cell has not been encountered + neighbour_indices = self.maze.find_neighbours( + k_curr, l_curr + ) # Find neighbour indices + neighbour_indices = self.maze.validate_neighbours_solve( + neighbour_indices, + k_curr, + l_curr, + self.maze.exit_coor[0], + self.maze.exit_coor[1], + self.neighbor_method, + ) + + if ( + neighbour_indices is not None + ): # If there are unvisited neighbour cells + visited_cells.append( + (k_curr, l_curr) + ) # Add current cell to stack + path.append( + ((k_curr, l_curr), False) + ) # Add coordinates to part of search path + k_next, l_next = random.choice( + neighbour_indices + ) # Choose random neighbour + self.maze.grid[k_next][ + l_next + ].visited = True # Move to that neighbour + k_curr = k_next + l_curr = l_next + + elif ( + len(visited_cells) > 0 + ): # If there are no unvisited neighbour cells + path.append( + ((k_curr, l_curr), True) + ) # Add coordinates to part of search path + ( + k_curr, + l_curr, + ) = ( + visited_cells.pop() + ) # Pop previous visited cell (backtracking) + + path.append( + ((k_curr, l_curr), False) + ) # Append final location to path + if not self.quiet_mode: + print("Number of moves performed: {}".format(len(path))) + print( + "Execution time for algorithm: {:.4f}".format( + time.time() - time_start + ) + ) + + logging.debug("Class DepthFirstBacktracker leaving solve") + return path diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0aae196 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, find_packages + +with open("requirements.txt", "r") as f: + requirements = f.read().splitlines() + +setup( + name="pymaze", + description="A maze generator, solver and visualizer for Python.", + license="MIT", + packages=find_packages(include="pymaze"), + install_requires=requirements, +) diff --git a/src/algorithm.py b/src/algorithm.py deleted file mode 100644 index dc98147..0000000 --- a/src/algorithm.py +++ /dev/null @@ -1,167 +0,0 @@ -import time -import random -import math - -# global variable to store list of all available algorithms -algorithm_list = ["dfs_backtrack", "bin_tree"] - -def depth_first_recursive_backtracker( maze, start_coor ): - k_curr, l_curr = start_coor # Where to start generating - path = [(k_curr, l_curr)] # To track path of solution - maze.grid[k_curr][l_curr].visited = True # Set initial cell to visited - visit_counter = 1 # To count number of visited cells - visited_cells = list() # Stack of visited cells for backtracking - - print("\nGenerating the maze with depth-first search...") - time_start = time.time() - - while visit_counter < maze.grid_size: # While there are unvisited cells - neighbour_indices = maze.find_neighbours(k_curr, l_curr) # Find neighbour indicies - neighbour_indices = maze._validate_neighbours_generate(neighbour_indices) - - if neighbour_indices is not None: # If there are unvisited neighbour cells - visited_cells.append((k_curr, l_curr)) # Add current cell to stack - k_next, l_next = random.choice(neighbour_indices) # Choose random neighbour - maze.grid[k_curr][l_curr].remove_walls(k_next, l_next) # Remove walls between neighbours - maze.grid[k_next][l_next].remove_walls(k_curr, l_curr) # Remove walls between neighbours - maze.grid[k_next][l_next].visited = True # Move to that neighbour - k_curr = k_next - l_curr = l_next - path.append((k_curr, l_curr)) # Add coordinates to part of generation path - visit_counter += 1 - - elif len(visited_cells) > 0: # If there are no unvisited neighbour cells - k_curr, l_curr = visited_cells.pop() # Pop previous visited cell (backtracking) - path.append((k_curr, l_curr)) # Add coordinates to part of generation path - - print("Number of moves performed: {}".format(len(path))) - print("Execution time for algorithm: {:.4f}".format(time.time() - time_start)) - - maze.grid[maze.entry_coor[0]][maze.entry_coor[1]].set_as_entry_exit("entry", - maze.num_rows-1, maze.num_cols-1) - maze.grid[maze.exit_coor[0]][maze.exit_coor[1]].set_as_entry_exit("exit", - maze.num_rows-1, maze.num_cols-1) - - for i in range(maze.num_rows): - for j in range(maze.num_cols): - maze.grid[i][j].visited = False # Set all cells to unvisited before returning grid - - maze.generation_path = path - -def binary_tree( maze, start_coor ): - # store the current time - time_start = time.time() - - # repeat the following for all rows - for i in range(0, maze.num_rows): - - # check if we are in top row - if( i == maze.num_rows - 1 ): - # remove the right wall for this, because we cant remove top wall - for j in range(0, maze.num_cols-1): - maze.grid[i][j].remove_walls(i, j+1) - maze.grid[i][j+1].remove_walls(i, j) - - # go to the next row - break - - # repeat the following for all cells in rows - for j in range(0, maze.num_cols): - - # check if we are in the last column - if( j == maze.num_cols-1 ): - # remove only the top wall for this cell - maze.grid[i][j].remove_walls(i+1, j) - maze.grid[i+1][j].remove_walls(i, j) - continue - - # for all other cells - # randomly choose between 0 and 1. - # if we get 0, remove top wall; otherwise remove right wall - remove_top = random.choice([True,False]) - - # if we chose to remove top wall - if remove_top: - maze.grid[i][j].remove_walls(i+1, j) - maze.grid[i+1][j].remove_walls(i, j) - # if we chose top remove right wall - else: - maze.grid[i][j].remove_walls(i, j+1) - maze.grid[i][j+1].remove_walls(i, j) - - print("Number of moves performed: {}".format(maze.num_cols * maze.num_rows)) - print("Execution time for algorithm: {:.4f}".format(time.time() - time_start)) - - # choose the entry and exit coordinates - maze.grid[maze.entry_coor[0]][maze.entry_coor[1]].set_as_entry_exit("entry", - maze.num_rows-1, maze.num_cols-1) - maze.grid[maze.exit_coor[0]][maze.exit_coor[1]].set_as_entry_exit("exit", - maze.num_rows-1, maze.num_cols-1) - - # create a path for animating the maze creation using a binary tree - path = list() - # variable for holding number of cells visited until now - visit_counter = 0 - # created list of cell visited uptil now to for backtracking - visited = list() - - # create variables to hold the coords of current cell - # no matter what the user gives as start coords, we choose the - k_curr, l_curr = (maze.num_rows-1, maze.num_cols-1) - # add first cell to the path - path.append( (k_curr,l_curr) ) - - # mark first cell as visited - begin_time = time.time() - - # repeat until all the cells have been visited - while visit_counter < maze.grid_size: # While there are unvisited cells - - # for each cell, we only visit top and right cells. - possible_neighbours = list() - - try: - # take only those cells that are unvisited and accessible - if not maze.grid[k_curr-1][l_curr].visited and k_curr != 0: - if not maze.grid[k_curr][l_curr].is_walls_between(maze.grid[k_curr-1][l_curr]): - possible_neighbours.append( (k_curr-1,l_curr)) - except: - print() - - try: - # take only those cells that are unvisited and accessible - if not maze.grid[k_curr][l_curr-1].visited and l_curr != 0: - if not maze.grid[k_curr][l_curr].is_walls_between(maze.grid[k_curr][l_curr-1]): - possible_neighbours.append( (k_curr,l_curr-1)) - except: - print() - - # if there are still traversible cell from current cell - if len( possible_neighbours ) != 0: - # select to first element to traverse - k_next, l_next = possible_neighbours[0] - # add this cell to the path - path.append( possible_neighbours[0] ) - # add this cell to the visited - visited.append( (k_curr,l_curr) ) - # mark this cell as visited - maze.grid[k_next][l_next].visited = True - - visit_counter+= 1 - - # update the current cell coords - k_curr, l_curr = k_next, l_next - - else: - # check if no more cells can be visited - if len( visited ) != 0: - k_curr, l_curr = visited.pop() - path.append( (k_curr,l_curr) ) - else: - break - for row in maze.grid: - for cell in row: - cell.visited = False - - print(f"Generating path for maze took {time.time() - begin_time}s.") - maze.generation_path = path diff --git a/src/maze_viz.py b/src/maze_viz.py deleted file mode 100644 index 17ebe72..0000000 --- a/src/maze_viz.py +++ /dev/null @@ -1,311 +0,0 @@ -import matplotlib.pyplot as plt -from matplotlib import animation -import logging - -logging.basicConfig(level=logging.DEBUG) - - -class Visualizer(object): - """Class that handles all aspects of visualization. - - - Attributes: - maze: The maze that will be visualized - cell_size (int): How large the cells will be in the plots - height (int): The height of the maze - width (int): The width of the maze - ax: The axes for the plot - lines: - squares: - media_filename (string): The name of the animations and images - - """ - def __init__(self, maze, cell_size, media_filename): - self.maze = maze - self.cell_size = cell_size - self.height = maze.num_rows * cell_size - self.width = maze.num_cols * cell_size - self.ax = None - self.lines = dict() - self.squares = dict() - self.media_filename = media_filename - - def set_media_filename(self, filename): - """Sets the filename of the media - Args: - filename (string): The name of the media - """ - self.media_filename = filename - - def show_maze(self): - """Displays a plot of the maze without the solution path""" - - # Create the plot figure and style the axes - fig = self.configure_plot() - - # Plot the walls on the figure - self.plot_walls() - - # Display the plot to the user - plt.show() - - # Handle any potential saving - if self.media_filename: - fig.savefig("{}{}.png".format(self.media_filename, "_generation"), frameon=None) - - def plot_walls(self): - """ Plots the walls of a maze. This is used when generating the maze image""" - for i in range(self.maze.num_rows): - for j in range(self.maze.num_cols): - if self.maze.initial_grid[i][j].is_entry_exit == "entry": - self.ax.text(j*self.cell_size, i*self.cell_size, "START", fontsize=7, weight="bold") - elif self.maze.initial_grid[i][j].is_entry_exit == "exit": - self.ax.text(j*self.cell_size, i*self.cell_size, "END", fontsize=7, weight="bold") - if self.maze.initial_grid[i][j].walls["top"]: - self.ax.plot([j*self.cell_size, (j+1)*self.cell_size], - [i*self.cell_size, i*self.cell_size], color="k") - if self.maze.initial_grid[i][j].walls["right"]: - self.ax.plot([(j+1)*self.cell_size, (j+1)*self.cell_size], - [i*self.cell_size, (i+1)*self.cell_size], color="k") - if self.maze.initial_grid[i][j].walls["bottom"]: - self.ax.plot([(j+1)*self.cell_size, j*self.cell_size], - [(i+1)*self.cell_size, (i+1)*self.cell_size], color="k") - if self.maze.initial_grid[i][j].walls["left"]: - self.ax.plot([j*self.cell_size, j*self.cell_size], - [(i+1)*self.cell_size, i*self.cell_size], color="k") - - def configure_plot(self): - """Sets the initial properties of the maze plot. Also creates the plot and axes""" - - # Create the plot figure - fig = plt.figure(figsize = (7, 7*self.maze.num_rows/self.maze.num_cols)) - - # Create the axes - self.ax = plt.axes() - - # Set an equal aspect ratio - self.ax.set_aspect("equal") - - # Remove the axes from the figure - self.ax.axes.get_xaxis().set_visible(False) - self.ax.axes.get_yaxis().set_visible(False) - - title_box = self.ax.text(0, self.maze.num_rows + self.cell_size + 0.1, - r"{}$\times${}".format(self.maze.num_rows, self.maze.num_cols), - bbox={"facecolor": "gray", "alpha": 0.5, "pad": 4}, fontname="serif", fontsize=15) - - return fig - - def show_maze_solution(self): - """Function that plots the solution to the maze. Also adds indication of entry and exit points.""" - - # Create the figure and style the axes - fig = self.configure_plot() - - # Plot the walls onto the figure - self.plot_walls() - - list_of_backtrackers = [path_element[0] for path_element in self.maze.solution_path if path_element[1]] - - # Keeps track of how many circles have been drawn - circle_num = 0 - - self.ax.add_patch(plt.Circle(((self.maze.solution_path[0][0][1] + 0.5)*self.cell_size, - (self.maze.solution_path[0][0][0] + 0.5)*self.cell_size), 0.2*self.cell_size, - fc=(0, circle_num/(len(self.maze.solution_path) - 2*len(list_of_backtrackers)), - 0), alpha=0.4)) - - for i in range(1, self.maze.solution_path.__len__()): - if self.maze.solution_path[i][0] not in list_of_backtrackers and\ - self.maze.solution_path[i-1][0] not in list_of_backtrackers: - circle_num += 1 - self.ax.add_patch(plt.Circle(((self.maze.solution_path[i][0][1] + 0.5)*self.cell_size, - (self.maze.solution_path[i][0][0] + 0.5)*self.cell_size), 0.2*self.cell_size, - fc = (0, circle_num/(len(self.maze.solution_path) - 2*len(list_of_backtrackers)), 0), alpha = 0.4)) - - # Display the plot to the user - plt.show() - - # Handle any saving - if self.media_filename: - fig.savefig("{}{}.png".format(self.media_filename, "_solution"), frameon=None) - - def show_generation_animation(self): - """Function that animates the process of generating the a maze where path is a list - of coordinates indicating the path taken to carve out (break down walls) the maze.""" - - # Create the figure and style the axes - fig = self.configure_plot() - - # The square that represents the head of the algorithm - indicator = plt.Rectangle((self.maze.generation_path[0][0]*self.cell_size, self.maze.generation_path[0][1]*self.cell_size), - self.cell_size, self.cell_size, fc = "purple", alpha = 0.6) - - self.ax.add_patch(indicator) - - # Only need to plot right and bottom wall for each cell since walls overlap. - # Also adding squares to animate the path taken to carve out the maze. - color_walls = "k" - for i in range(self.maze.num_rows): - for j in range(self.maze.num_cols): - self.lines["{},{}: right".format(i, j)] = self.ax.plot([(j+1)*self.cell_size, (j+1)*self.cell_size], - [i*self.cell_size, (i+1)*self.cell_size], - linewidth = 2, color = color_walls)[0] - self.lines["{},{}: bottom".format(i, j)] = self.ax.plot([(j+1)*self.cell_size, j*self.cell_size], - [(i+1)*self.cell_size, (i+1)*self.cell_size], - linewidth = 2, color = color_walls)[0] - - self.squares["{},{}".format(i, j)] = plt.Rectangle((j*self.cell_size, - i*self.cell_size), self.cell_size, self.cell_size, fc = "red", alpha = 0.4) - self.ax.add_patch(self.squares["{},{}".format(i, j)]) - - # Plotting boundaries of maze. - color_boundary = "k" - self.ax.plot([0, self.width], [self.height,self.height], linewidth = 2, color = color_boundary) - self.ax.plot([self.width, self.width], [self.height, 0], linewidth = 2, color = color_boundary) - self.ax.plot([self.width, 0], [0, 0], linewidth = 2, color = color_boundary) - self.ax.plot([0, 0], [0, self.height], linewidth = 2, color = color_boundary) - - def animate(frame): - """Function to supervise animation of all objects.""" - animate_walls(frame) - animate_squares(frame) - animate_indicator(frame) - self.ax.set_title("Step: {}".format(frame + 1), fontname="serif", fontsize=19) - return [] - - def animate_walls(frame): - """Function that animates the visibility of the walls between cells.""" - if frame > 0: - self.maze.grid[self.maze.generation_path[frame-1][0]][self.maze.generation_path[frame-1][1]].remove_walls( - self.maze.generation_path[frame][0], - self.maze.generation_path[frame][1]) # Wall between curr and neigh - - self.maze.grid[self.maze.generation_path[frame][0]][self.maze.generation_path[frame][1]].remove_walls( - self.maze.generation_path[frame-1][0], - self.maze.generation_path[frame-1][1]) # Wall between neigh and curr - - current_cell = self.maze.grid[self.maze.generation_path[frame-1][0]][self.maze.generation_path[frame-1][1]] - next_cell = self.maze.grid[self.maze.generation_path[frame][0]][self.maze.generation_path[frame][1]] - - """Function to animate walls between cells as the search goes on.""" - for wall_key in ["right", "bottom"]: # Only need to draw two of the four walls (overlap) - if current_cell.walls[wall_key] is False: - self.lines["{},{}: {}".format(current_cell.row, - current_cell.col, wall_key)].set_visible(False) - if next_cell.walls[wall_key] is False: - self.lines["{},{}: {}".format(next_cell.row, - next_cell.col, wall_key)].set_visible(False) - - def animate_squares(frame): - """Function to animate the searched path of the algorithm.""" - self.squares["{},{}".format(self.maze.generation_path[frame][0], - self.maze.generation_path[frame][1])].set_visible(False) - return [] - - def animate_indicator(frame): - """Function to animate where the current search is happening.""" - indicator.set_xy((self.maze.generation_path[frame][1]*self.cell_size, - self.maze.generation_path[frame][0]*self.cell_size)) - return [] - - logging.debug("Creating generation animation") - anim = animation.FuncAnimation(fig, animate, frames=self.maze.generation_path.__len__(), - interval=100, blit=True, repeat=False) - - logging.debug("Finished creating the generation animation") - - # Display the plot to the user - plt.show() - - # Handle any saving - if self.media_filename: - print("Saving generation animation. This may take a minute....") - mpeg_writer = animation.FFMpegWriter(fps=24, bitrate=1000, - codec="libx264", extra_args=["-pix_fmt", "yuv420p"]) - anim.save("{}{}{}x{}.mp4".format(self.media_filename, "_generation_", self.maze.num_rows, - self.maze.num_cols), writer=mpeg_writer) - - def add_path(self): - # Adding squares to animate the path taken to solve the maze. Also adding entry/exit text - color_walls = "k" - for i in range(self.maze.num_rows): - for j in range(self.maze.num_cols): - if self.maze.initial_grid[i][j].is_entry_exit == "entry": - self.ax.text(j*self.cell_size, i*self.cell_size, "START", fontsize = 7, weight = "bold") - elif self.maze.initial_grid[i][j].is_entry_exit == "exit": - self.ax.text(j*self.cell_size, i*self.cell_size, "END", fontsize = 7, weight = "bold") - - if self.maze.initial_grid[i][j].walls["top"]: - self.lines["{},{}: top".format(i, j)] = self.ax.plot([j*self.cell_size, (j+1)*self.cell_size], - [i*self.cell_size, i*self.cell_size], linewidth = 2, color = color_walls)[0] - if self.maze.initial_grid[i][j].walls["right"]: - self.lines["{},{}: right".format(i, j)] = self.ax.plot([(j+1)*self.cell_size, (j+1)*self.cell_size], - [i*self.cell_size, (i+1)*self.cell_size], linewidth = 2, color = color_walls)[0] - if self.maze.initial_grid[i][j].walls["bottom"]: - self.lines["{},{}: bottom".format(i, j)] = self.ax.plot([(j+1)*self.cell_size, j*self.cell_size], - [(i+1)*self.cell_size, (i+1)*self.cell_size], linewidth = 2, color = color_walls)[0] - if self.maze.initial_grid[i][j].walls["left"]: - self.lines["{},{}: left".format(i, j)] = self.ax.plot([j*self.cell_size, j*self.cell_size], - [(i+1)*self.cell_size, i*self.cell_size], linewidth = 2, color = color_walls)[0] - self.squares["{},{}".format(i, j)] = plt.Rectangle((j*self.cell_size, - i*self.cell_size), self.cell_size, self.cell_size, - fc = "red", alpha = 0.4, visible = False) - self.ax.add_patch(self.squares["{},{}".format(i, j)]) - - def animate_maze_solution(self): - """Function that animates the process of generating the a maze where path is a list - of coordinates indicating the path taken to carve out (break down walls) the maze.""" - - # Create the figure and style the axes - fig = self.configure_plot() - - # Adding indicator to see shere current search is happening. - indicator = plt.Rectangle((self.maze.solution_path[0][0][0]*self.cell_size, - self.maze.solution_path[0][0][1]*self.cell_size), self.cell_size, self.cell_size, - fc="purple", alpha=0.6) - self.ax.add_patch(indicator) - - self.add_path() - - def animate_squares(frame): - """Function to animate the solved path of the algorithm.""" - if frame > 0: - if self.maze.solution_path[frame - 1][1]: # Color backtracking - self.squares["{},{}".format(self.maze.solution_path[frame - 1][0][0], - self.maze.solution_path[frame - 1][0][1])].set_facecolor("orange") - - self.squares["{},{}".format(self.maze.solution_path[frame - 1][0][0], - self.maze.solution_path[frame - 1][0][1])].set_visible(True) - self.squares["{},{}".format(self.maze.solution_path[frame][0][0], - self.maze.solution_path[frame][0][1])].set_visible(False) - return [] - - def animate_indicator(frame): - """Function to animate where the current search is happening.""" - indicator.set_xy((self.maze.solution_path[frame][0][1] * self.cell_size, - self.maze.solution_path[frame][0][0] * self.cell_size)) - return [] - - def animate(frame): - """Function to supervise animation of all objects.""" - animate_squares(frame) - animate_indicator(frame) - self.ax.set_title("Step: {}".format(frame + 1), fontname = "serif", fontsize = 19) - return [] - - logging.debug("Creating solution animation") - anim = animation.FuncAnimation(fig, animate, frames=self.maze.solution_path.__len__(), - interval=100, blit=True, repeat=False) - logging.debug("Finished creating solution animation") - - # Display the animation to the user - plt.show() - - # Handle any saving - if self.media_filename: - print("Saving solution animation. This may take a minute....") - mpeg_writer = animation.FFMpegWriter(fps=24, bitrate=1000, - codec="libx264", extra_args=["-pix_fmt", "yuv420p"]) - anim.save("{}{}{}x{}.mp4".format(self.media_filename, "_solution_", self.maze.num_rows, - self.maze.num_cols), writer=mpeg_writer) diff --git a/src/solver.py b/src/solver.py deleted file mode 100644 index 2537422..0000000 --- a/src/solver.py +++ /dev/null @@ -1,218 +0,0 @@ -import time -import random -import logging -from src.maze import Maze - -logging.basicConfig(level=logging.DEBUG) - - -class Solver(object): - """Base class for solution methods. - Every new solution method should override the solve method. - - Attributes: - maze (list): The maze which is being solved. - neighbor_method: - quiet_mode: When enabled, information is not outputted to the console - - """ - - def __init__(self, maze, quiet_mode, neighbor_method): - logging.debug("Class Solver ctor called") - - self.maze = maze - self.neighbor_method = neighbor_method - self.name = "" - self.quiet_mode = quiet_mode - - def solve(self): - logging.debug('Class: Solver solve called') - raise NotImplementedError - - def get_name(self): - logging.debug('Class Solver get_name called') - raise self.name - - def get_path(self): - logging.debug('Class Solver get_path called') - return self.path - - -class BreadthFirst(Solver): - - def __init__(self, maze, quiet_mode=False, neighbor_method="fancy"): - logging.debug('Class BreadthFirst ctor called') - - self.name = "Breadth First Recursive" - super().__init__(maze, neighbor_method, quiet_mode) - - def solve(self): - - """Function that implements the breadth-first algorithm for solving the maze. This means that - for each iteration in the outer loop, the search visits one cell in all possible branches. Then - moves on to the next level of cells in each branch to continue the search.""" - - logging.debug("Class BreadthFirst solve called") - current_level = [self.maze.entry_coor] # Stack of cells at current level of search - path = list() # To track path of solution cell coordinates - - print("\nSolving the maze with breadth-first search...") - time_start = time.clock() - - while True: # Loop until return statement is encountered - next_level = list() - - while current_level: # While still cells left to search on current level - k_curr, l_curr = current_level.pop(0) # Search one cell on the current level - self.maze.grid[k_curr][l_curr].visited = True # Mark current cell as visited - path.append(((k_curr, l_curr), False)) # Append current cell to total search path - - if (k_curr, l_curr) == self.maze.exit_coor: # Exit if current cell is exit cell - if not self.quiet_mode: - print("Number of moves performed: {}".format(len(path))) - print("Execution time for algorithm: {:.4f}".format(time.clock() - time_start)) - return path - - neighbour_coors = self.maze.find_neighbours(k_curr, l_curr) # Find neighbour indicies - neighbour_coors = self.maze.validate_neighbours_solve(neighbour_coors, k_curr, - l_curr, self.maze.exit_coor[0], - self.maze.exit_coor[1], self.neighbor_method) - - if neighbour_coors is not None: - for coor in neighbour_coors: - next_level.append(coor) # Add all existing real neighbours to next search level - - for cell in next_level: - current_level.append(cell) # Update current_level list with cells for nex search level - logging.debug("Class BreadthFirst leaving solve") - - -class BiDirectional(Solver): - - def __init__(self, maze, quiet_mode=False, neighbor_method="fancy"): - logging.debug('Class BiDirectional ctor called') - - super().__init__(maze, neighbor_method, quiet_mode) - self.name = "Bi Directional" - - def solve(self): - - """Function that implements a bidirectional depth-first recursive backtracker algorithm for - solving the maze, i.e. starting at the entry point and exit points where each search searches - for the other search path. NOTE: THE FUNCTION ENDS IN AN INFINITE LOOP FOR SOME RARE CASES OF - THE INPUT MAZE. WILL BE FIXED IN FUTURE.""" - logging.debug("Class BiDirectional solve called") - - grid = self.maze.grid - k_curr, l_curr = self.maze.entry_coor # Where to start the first search - p_curr, q_curr = self.maze.exit_coor # Where to start the second search - grid[k_curr][l_curr].visited = True # Set initial cell to visited - grid[p_curr][q_curr].visited = True # Set final cell to visited - backtrack_kl = list() # Stack of visited cells for backtracking - backtrack_pq = list() # Stack of visited cells for backtracking - path_kl = list() # To track path of solution and backtracking cells - path_pq = list() # To track path of solution and backtracking cells - - if not self.quiet_mode: - print("\nSolving the maze with bidirectional depth-first search...") - time_start = time.clock() - - while True: # Loop until return statement is encountered - neighbours_kl = self.maze.find_neighbours(k_curr, l_curr) # Find neighbours for first search - real_neighbours_kl = [neigh for neigh in neighbours_kl if not grid[k_curr][l_curr].is_walls_between(grid[neigh[0]][neigh[1]])] - neighbours_kl = [neigh for neigh in real_neighbours_kl if not grid[neigh[0]][neigh[1]].visited] - - neighbours_pq = self.maze.find_neighbours(p_curr, q_curr) # Find neighbours for second search - real_neighbours_pq = [neigh for neigh in neighbours_pq if not grid[p_curr][q_curr].is_walls_between(grid[neigh[0]][neigh[1]])] - neighbours_pq = [neigh for neigh in real_neighbours_pq if not grid[neigh[0]][neigh[1]].visited] - - if len(neighbours_kl) > 0: # If there are unvisited neighbour cells - backtrack_kl.append((k_curr, l_curr)) # Add current cell to stack - path_kl.append(((k_curr, l_curr), False)) # Add coordinates to part of search path - k_next, l_next = random.choice(neighbours_kl) # Choose random neighbour - grid[k_next][l_next].visited = True # Move to that neighbour - k_curr = k_next - l_curr = l_next - - elif len(backtrack_kl) > 0: # If there are no unvisited neighbour cells - path_kl.append(((k_curr, l_curr), True)) # Add coordinates to part of search path - k_curr, l_curr = backtrack_kl.pop() # Pop previous visited cell (backtracking) - - if len(neighbours_pq) > 0: # If there are unvisited neighbour cells - backtrack_pq.append((p_curr, q_curr)) # Add current cell to stack - path_pq.append(((p_curr, q_curr), False)) # Add coordinates to part of search path - p_next, q_next = random.choice(neighbours_pq) # Choose random neighbour - grid[p_next][q_next].visited = True # Move to that neighbour - p_curr = p_next - q_curr = q_next - - elif len(backtrack_pq) > 0: # If there are no unvisited neighbour cells - path_pq.append(((p_curr, q_curr), True)) # Add coordinates to part of search path - p_curr, q_curr = backtrack_pq.pop() # Pop previous visited cell (backtracking) - - # Exit loop and return path if any opf the kl neighbours are in path_pq. - if any((True for n_kl in real_neighbours_kl if (n_kl, False) in path_pq)): - path_kl.append(((k_curr, l_curr), False)) - path = [p_el for p_tuple in zip(path_kl, path_pq) for p_el in p_tuple] # Zip paths - if not self.quiet_mode: - print("Number of moves performed: {}".format(len(path))) - print("Execution time for algorithm: {:.4f}".format(time.clock() - time_start)) - logging.debug("Class BiDirectional leaving solve") - return path - - # Exit loop and return path if any opf the pq neighbours are in path_kl. - elif any((True for n_pq in real_neighbours_pq if (n_pq, False) in path_kl)): - path_pq.append(((p_curr, q_curr), False)) - path = [p_el for p_tuple in zip(path_kl, path_pq) for p_el in p_tuple] # Zip paths - if not self.quiet_mode: - print("Number of moves performed: {}".format(len(path))) - print("Execution time for algorithm: {:.4f}".format(time.clock() - time_start)) - logging.debug("Class BiDirectional leaving solve") - return path - - -class DepthFirstBacktracker(Solver): - """A solver that implements the depth-first recursive backtracker algorithm. - """ - - def __init__(self, maze, quiet_mode=False, neighbor_method="fancy"): - logging.debug('Class DepthFirstBacktracker ctor called') - - super().__init__(maze, neighbor_method, quiet_mode) - self.name = "Depth First Backtracker" - - def solve(self): - logging.debug("Class DepthFirstBacktracker solve called") - k_curr, l_curr = self.maze.entry_coor # Where to start searching - self.maze.grid[k_curr][l_curr].visited = True # Set initial cell to visited - visited_cells = list() # Stack of visited cells for backtracking - path = list() # To track path of solution and backtracking cells - if not self.quiet_mode: - print("\nSolving the maze with depth-first search...") - - time_start = time.time() - - while (k_curr, l_curr) != self.maze.exit_coor: # While the exit cell has not been encountered - neighbour_indices = self.maze.find_neighbours(k_curr, l_curr) # Find neighbour indices - neighbour_indices = self.maze.validate_neighbours_solve(neighbour_indices, k_curr, - l_curr, self.maze.exit_coor[0], self.maze.exit_coor[1], self.neighbor_method) - - if neighbour_indices is not None: # If there are unvisited neighbour cells - visited_cells.append((k_curr, l_curr)) # Add current cell to stack - path.append(((k_curr, l_curr), False)) # Add coordinates to part of search path - k_next, l_next = random.choice(neighbour_indices) # Choose random neighbour - self.maze.grid[k_next][l_next].visited = True # Move to that neighbour - k_curr = k_next - l_curr = l_next - - elif len(visited_cells) > 0: # If there are no unvisited neighbour cells - path.append(((k_curr, l_curr), True)) # Add coordinates to part of search path - k_curr, l_curr = visited_cells.pop() # Pop previous visited cell (backtracking) - - path.append(((k_curr, l_curr), False)) # Append final location to path - if not self.quiet_mode: - print("Number of moves performed: {}".format(len(path))) - print("Execution time for algorithm: {:.4f}".format(time.time() - time_start)) - - logging.debug('Class DepthFirstBacktracker leaving solve') - return path diff --git a/tests/algorithm_tests.py b/tests/algorithm_tests.py index 54af70b..184fb0e 100644 --- a/tests/algorithm_tests.py +++ b/tests/algorithm_tests.py @@ -1,14 +1,15 @@ - # test no cell has a wall on all four walls import unittest # import all algorithms present in algorithm.py -from src.algorithm import * -from src.maze import Maze +from pymaze.algorithm import * +from pymaze.maze import Maze + def create_maze(algorithm): - rows, cols = (5,5) - return Maze(rows, cols, algorithm = algorithm) + rows, cols = (5, 5) + return Maze(rows, cols, algorithm=algorithm) + class TestAlgorithm(unittest.TestCase): def test_NonEmptyPath(self): @@ -16,30 +17,30 @@ def test_NonEmptyPath(self): # repeat the following for all algorithms developed in algorithm.py for algorithm in algorithm_list: # generate a maze using this algorithm - maze = create_maze( algorithm ) + maze = create_maze(algorithm) # create message to display when test fails - err_msg = f'Algorithm {algorithm} generated empty path' + err_msg = f"Algorithm {algorithm} generated empty path" # assert path is non empty list - self.assertNotEqual( maze.generation_path, list(), msg = err_msg) + self.assertNotEqual(maze.generation_path, list(), msg=err_msg) def test_MazeHasEntryExit(self): """Test to check that entry and exit cells have been properly marked""" # repeat the following for all algorithms for algorithm in algorithm_list: # generate a maze using the algorithm - maze = create_maze( algorithm ) + maze = create_maze(algorithm) # create message to display when test fails - err_msg = f'Algorithm {algorithm} did not generate entry_exit cells' + err_msg = f"Algorithm {algorithm} did not generate entry_exit cells" # get the cell that has been set as entry point entry_cell = maze.grid[maze.entry_coor[0]][maze.entry_coor[1]] # check that the cell has been marked as an entry cell - self.assertIsNotNone( entry_cell.is_entry_exit, msg = err_msg ) + self.assertIsNotNone(entry_cell.is_entry_exit, msg=err_msg) # get the cell that has been set as exit point exit_cell = maze.grid[maze.exit_coor[0]][maze.exit_coor[1]] # check that the cell has been marked as an exit cell - self.assertIsNotNone( entry_cell.is_entry_exit ,msg = err_msg ) + self.assertIsNotNone(entry_cell.is_entry_exit, msg=err_msg) def test_AllCellsUnvisited(self): """Test to check that after maze generation all cells have been @@ -47,16 +48,16 @@ def test_AllCellsUnvisited(self): # repeat the following for all algorithms for algorithm in algorithm_list: # generate a maze using the algorithm - maze = create_maze( algorithm ) + maze = create_maze(algorithm) # create message to display when test fails - err_msg = f'Algorithm {algorithm} did not unvisit all cells' + err_msg = f"Algorithm {algorithm} did not unvisit all cells" # repeat the following for all rows in maze for row in maze.grid: # repeat the following for all cells in the row for cell in row: # assert that no cell is marked as visited - self.assertFalse( cell.visited, msg = err_msg ) + self.assertFalse(cell.visited, msg=err_msg) def test_NoCellUnvisited(self): """Test to check that all cells have been processed, thus no cell has @@ -64,9 +65,9 @@ def test_NoCellUnvisited(self): # repeat the following for all algorithms for algorithm in algorithm_list: # generate a maze using the algorithm - maze = create_maze( algorithm ) + maze = create_maze(algorithm) # create message to display when test fails - err_msg = f'Algorithm {algorithm} did not generate entry_exit cells' + err_msg = f"Algorithm {algorithm} did not generate entry_exit cells" # variable to store how a cell with walls on all sides is denoted walls_4 = {"top": True, "right": True, "bottom": True, "left": True} @@ -75,4 +76,4 @@ def test_NoCellUnvisited(self): # repeat the following for all cells in the row for cell in row: # check that the cell does not have walls on all four sides - self.assertNotEqual( cell.walls, walls_4, msg = err_msg ) + self.assertNotEqual(cell.walls, walls_4, msg=err_msg) diff --git a/tests/cell_tests.py b/tests/cell_tests.py index fcc30e2..860d7ed 100644 --- a/tests/cell_tests.py +++ b/tests/cell_tests.py @@ -1,6 +1,5 @@ -from __future__ import absolute_import import unittest -from src.cell import Cell +from pymaze.cell import Cell class TestCell(unittest.TestCase): @@ -13,7 +12,9 @@ def test_ctor(self): self.assertEqual(cell.visited, False) self.assertEqual(cell.active, False) self.assertEqual(cell.is_entry_exit, None) - self.assertEqual(cell.walls, {"top": True, "right": True, "bottom": True, "left": True}) + self.assertEqual( + cell.walls, {"top": True, "right": True, "bottom": True, "left": True} + ) self.assertEqual(cell.neighbours, list()) def test_entry_exit(self): @@ -55,7 +56,7 @@ def test_remove_walls(self): """Test the Cell::remove_walls method""" # Remove the cell to the right cell = Cell(0, 0) - cell.remove_walls(0,1) + cell.remove_walls(0, 1) self.assertEqual(cell.walls["right"], False) # Remove the cell to the left @@ -76,21 +77,20 @@ def test_remove_walls(self): def test_is_walls_between(self): """Test the Cell::is_walls_between method - Note that cells are constructed with neighbors on each side. - We'll need to remove some walls to get full coverage. + Note that cells are constructed with neighbors on each side. + We'll need to remove some walls to get full coverage. """ # Create a base cell for which we will be testing whether walls exist - cell = Cell (1, 1) + cell = Cell(1, 1) # Create a cell appearing to the top of this cell - cell_top = Cell(0,1) + cell_top = Cell(0, 1) # Create a cell appearing to the right of this cell - cell_right = Cell(1,2) + cell_right = Cell(1, 2) # Create a cell appearing to the bottom of this cell - cell_bottom = Cell(2,1) + cell_bottom = Cell(2, 1) # Create a cell appearing to the left of this cell - cell_left = Cell(1,0) - + cell_left = Cell(1, 0) # check for walls between all these cells self.assertEqual(cell.is_walls_between(cell_top), True) @@ -99,13 +99,12 @@ def test_is_walls_between(self): self.assertEqual(cell.is_walls_between(cell_left), True) # remove top wall of 'cell' and bottom wall of 'cell_top' - cell.remove_walls(0,1) - cell_top.remove_walls(1,1) + cell.remove_walls(0, 1) + cell_top.remove_walls(1, 1) # check that there are no walls between these cells self.assertEqual(cell.is_walls_between(cell_top), False) - if __name__ == "__main__": unittest.main() diff --git a/tests/maze_manager_tests.py b/tests/maze_manager_tests.py index 7922c87..84ec1ad 100644 --- a/tests/maze_manager_tests.py +++ b/tests/maze_manager_tests.py @@ -1,13 +1,11 @@ -from __future__ import absolute_import import unittest -from src.maze_manager import MazeManager -from src.maze_viz import Visualizer -from src.maze import Maze +from pymaze.maze_manager import MazeManager +from pymaze.maze_viz import Visualizer +from pymaze.maze import Maze class TestMgr(unittest.TestCase): - def test_ctor(self): """Make sure that the constructor values are getting properly set.""" manager = MazeManager() diff --git a/tests/maze_tests.py b/tests/maze_tests.py index 211a422..b7590bd 100644 --- a/tests/maze_tests.py +++ b/tests/maze_tests.py @@ -1,8 +1,8 @@ from __future__ import absolute_import import unittest -from src.maze import Maze -from src.cell import Cell +from pymaze.maze import Maze +from pymaze.cell import Cell def generate_maze(): @@ -23,9 +23,9 @@ def test_ctor(self): self.assertEqual(maze.num_cols, cols) self.assertEqual(maze.num_rows, rows) self.assertEqual(maze.id, 0) - self.assertEqual(maze.grid_size, rows*cols) + self.assertEqual(maze.grid_size, rows * cols) - id=33 + id = 33 maze2 = Maze(rows, cols, id) self.assertEqual(maze2.num_cols, cols) self.assertEqual(maze2.num_rows, rows) @@ -47,4 +47,4 @@ def test_find_neighbors(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/solver_tests.py b/tests/solver_tests.py index 1626acf..a18cfe3 100644 --- a/tests/solver_tests.py +++ b/tests/solver_tests.py @@ -1,8 +1,6 @@ -from __future__ import absolute_import import unittest -from src.solver import Solver - +from pymaze.solver import Solver class TestSolver(unittest.TestCase): @@ -14,4 +12,4 @@ def test_ctor(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main()