Repository: jostbr/MazeGenerator Branch: master Commit: ac41b42df45b Files: 25 Total size: 75.7 KB Directory structure: gitextract_q7eta_e0/ ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── examples/ │ ├── generate_binary_tree_algorithm.py │ ├── quick_start.py │ ├── solve_bi_directional.py │ ├── solve_breadth_first_recursive.py │ └── solve_depth_first_recursive.py ├── install_linter ├── requirements.txt ├── src/ │ ├── __init__.py │ ├── algorithm.py │ ├── cell.py │ ├── maze.py │ ├── maze_manager.py │ ├── maze_viz.py │ └── solver.py └── tests/ ├── __init__.py ├── algorithm_tests.py ├── cell_tests.py ├── maze_manager_tests.py ├── maze_tests.py ├── maze_viz_tests.py └── solver_tests.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.png *.jpg *.mp4 .idea *.pyc __pycache__ ================================================ FILE: .travis.yml ================================================ language: python python: - "3.5" - "3.6" before_install: - "chmod +x tests/cell_tests.py" - "chmod +x tests/maze_tests.py" - "chmod +x tests/maze_manager_tests.py" - "chmod +x tests/maze_viz_tests.py" - "chmod +x tests/solver_tests.py" install: - "pip install -r requirements.txt" script: - "python -m unittest tests/cell_tests.py" - "python -m unittest tests/maze_tests.py" - "python -m unittest tests/maze_manager_tests.py" - "python -m unittest tests/maze_viz_tests.py" - "python -m unittest tests/solver_tests.py" ================================================ FILE: LICENCE ================================================ MIT License Copyright (c) 2021 Jostein Brændshøi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 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. 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). ## 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` 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 another example as a template. The process for creating and solving a maze follows. 1. Create a maze manager 2. Add a maze to the manager 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 if __name__ == "__main__": # The easiest way to use the library is through the Manager class. It acts as the glue between # The visualization, solver, and maze classes. Mazes inside the manager have unique ids that we use # to specify particular mazes. manager = MazeManager() # We can add mazes to the manager two different ways. # The first way, we specify the maze dimensions. The maze that is created gets returned back to you. maze = manager.add_maze(10, 10) # The second way is by creating a maze, and then adding it to the manager. Doing this will require you to add # from src.maze import Maze # to your imports. Because the ids need to be unique, the manager will ensure this happens. It may change the # id of the maze that was passed in, so we assign it to the return value to make sure we're using the updated maze. maze2 = Maze(10, 10) maze2 = manager.add_existing_maze(maze2) # 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, "DepthFirstBacktracker") # If we want to save the maze & maze solution images along with their animations, we need to let the manager know. manager.set_filename("myFileName") # To see the unsolved maze, call manager.show_maze(maze.id) # You can also set the size of the cell by passing show_maze's second argument. The default is 1. # manager.show_maze(maze.id, 2) # To show an animation of how the maze was generated, use the following line manager.show_generation_animation(maze.id) # You can also see an animation of how the solver went about finding the end manager.show_solution_animation(maze.id) # Finally, you can show an image of the maze with the solution path overlaid. All of these display # functions will save the figure if MazeManager::set_filename has been set. 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. ### 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 `MazeManager` class acts as the glue, bridging the `Visualizer`, `Maze`, and `Solve` classes together. ### Adding a new Solution Algorithm #### Additional Overhead Be sure to add it to add the method to `quick_start.py`. Also create a new example file using the method. ### Adding a new Maze Generation Algorithm #### Additional Overhead 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`. ================================================ FILE: examples/generate_binary_tree_algorithm.py ================================================ from __future__ import absolute_import from src.maze_manager import MazeManager from src.maze import Maze if __name__ == "__main__": # create a maze manager to handle all operations manager = MazeManager() # now create a maze using the binary tree method maze_using_btree = Maze(10, 10, algorithm="bin_tree") # add this maze to the maze manager maze_using_btree = manager.add_existing_maze(maze_using_btree) # show the maze manager.show_maze(maze_using_btree.id) # show how the maze was generated manager.show_generation_animation(maze_using_btree.id) ================================================ FILE: examples/quick_start.py ================================================ from __future__ import absolute_import from src.maze_manager import MazeManager from src.maze import Maze if __name__ == "__main__": # The easiest way to use the library is through the Manager class. It acts as the glue between # The visualization, solver, and maze classes. Mazes inside the manager have unique ids that we use # to specify particular mazes. manager = MazeManager() # We can add mazes to the manager two different ways. # The first way, we specify the maze dimensions. The maze that is created gets returned back to you. maze = manager.add_maze(10, 10) # The second way is by creating a maze, and then adding it to the manager. Doing this will require you to add # from src.maze import Maze # to your imports. Because the ids need to be unique, the manager will ensure this happens. It may change the # id of the maze that was passed in, so we assign it to the return value to make sure we're using the updated maze. maze2 = Maze(10, 10) maze2 = manager.add_existing_maze(maze2) # 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 = 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, "DepthFirstBacktracker") # If we want to save the maze & maze solution images along with their animations, we need to let the manager know. manager.set_filename("myFileName") # To see the unsolved maze, call manager.show_maze(maze.id) # You can also set the size of the cell by passing show_maze's second argument. The default is 1. # manager.show_maze(maze.id, 2) # To show an animation of how the maze was generated, use the following line manager.show_generation_animation(maze.id) # You can also see an animation of how the solver went about finding the end manager.show_solution_animation(maze.id) # Finally, you can show an image of the maze with the solution path overlaid. All of these display # functions will save the figure if MazeManager::set_filename has been set. manager.show_solution(maze.id) ================================================ FILE: examples/solve_bi_directional.py ================================================ from __future__ import absolute_import from src.maze_manager import MazeManager if __name__ == "__main__": # Create the manager manager = MazeManager() # Add a 10x10 maze to the manager maze = manager.add_maze(10, 10) # Solve the maze using the Bi Directional algorithm manager.solve_maze(maze.id, "BiDirectional", "fancy") # Display the maze manager.show_maze(maze.id) # Show how the maze was generated manager.show_generation_animation(maze.id) # Show how the maze was solved manager.show_solution_animation(maze.id) # Display the maze with the solution overlaid manager.show_solution(maze.id) ================================================ FILE: examples/solve_breadth_first_recursive.py ================================================ from __future__ import absolute_import from src.maze_manager import MazeManager if __name__ == "__main__": # Create the manager manager = MazeManager() # Add a 10x10 maze to the manager maze = manager.add_maze(10, 10) # Solve the maze using the Depth First Backtracker algorithm manager.solve_maze(maze.id, "BreadthFirst") # Display the maze manager.show_maze(maze.id) # Show how the maze was generated manager.show_generation_animation(maze.id) # Show how the maze was solved manager.show_solution_animation(maze.id) # Display the maze with the solution overlaid manager.show_solution(maze.id) ================================================ FILE: examples/solve_depth_first_recursive.py ================================================ from __future__ import absolute_import from src.maze_manager import MazeManager if __name__ == "__main__": # Create the manager manager = MazeManager() # Add a 10x10 maze to the manager maze = manager.add_maze(10, 10) # Solve the maze using the Breadth First algorithm manager.solve_maze(maze.id, "DepthFirstBacktracker") # Display the maze manager.show_maze(maze.id) # Show how the maze was generated manager.show_generation_animation(maze.id) # Show how the maze was solved manager.show_solution_animation(maze.id) # Display the maze with the solution overlaid manager.show_solution(maze.id) ================================================ FILE: install_linter ================================================ pycodestyle ================================================ FILE: requirements.txt ================================================ matplotlib==2.0.0 ================================================ FILE: src/__init__.py ================================================ ================================================ FILE: src/algorithm.py ================================================ 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 ================================================ FILE: src/cell.py ================================================ 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): """ 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.neighbours = list() def is_walls_between(self, neighbour): """Function that checks if there are walls between self and a neighbour cell. Returns true if there are walls between. Otherwise returns False. Args: neighbour The cell to check between Return: True: If there are walls in between self and neighbor 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"]: return True 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"]: return True elif self.col - neighbour.col == -1 and self.walls["right"] and neighbour.walls["left"]: return True return False 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): Return: True: If the operation was a success False: If the operation failed """ if self.row - neighbour_row == 1: self.walls["top"] = False return True, "" elif self.row - neighbour_row == -1: self.walls["bottom"] = False return True, "" elif self.col - neighbour_col == 1: self.walls["left"] = False return True, "" elif self.col - neighbour_col == -1: self.walls["right"] = False return True, "" return False def set_as_entry_exit(self, entry_exit, row_limit, col_limit): """Function that sets the cell as an entry/exit cell by disabling the outer boundary wall. First, we check if the entrance/exit is on the top row. Next, we check if it should be on the bottom row. Finally, we check if it is on the left wall or the bottom row. Args: entry_exit: True to set this cell as an exit/entry. False to remove it as one row_limit: col_limit: """ if self.row == 0: self.walls["top"] = False elif self.row == row_limit: self.walls["bottom"] = False elif self.col == 0: self.walls["left"] = False elif self.col == col_limit: self.walls["right"] = False self.is_entry_exit = entry_exit ================================================ FILE: src/maze.py ================================================ import random import math import time from src.cell import Cell from src.algorithm import depth_first_recursive_backtracker, binary_tree class Maze(object): """Class representing a maze; a 2D grid of Cell objects. Contains functions for generating randomly generating the maze as well as for solving the maze. Attributes: num_cols (int): The height of the maze, in Cells num_rows (int): The width of the maze, in Cells id (int): A unique identifier for the maze grid_size (int): The area of the maze, also the total number of Cells in the maze entry_coor Entry location cell of maze exit_coor Exit location cell of maze generation_path : The path that was taken when generating the maze 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"): """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 """ self.num_cols = num_cols self.num_rows = num_rows self.id = id 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 = [] self.solution_path = None self.initial_grid = self.generate_grid() self.grid = self.initial_grid self.generate_maze(algorithm, (0, 0)) def generate_grid(self): """Function that creates a 2D grid of Cell objects. This can be thought of as a maze without any paths carved out Return: A list with Cell objects at each position """ # Create an empty list grid = list() # Place a Cell object at each location in the grid for i in range(self.num_rows): grid.append(list()) for j in range(self.num_cols): grid[i].append(Cell(i, j)) return grid def find_neighbours(self, cell_row, cell_col): """Finds all existing and unvisited neighbours of a cell in the grid. Return a list of tuples containing indices for the unvisited neighbours. Args: cell_row (int): cell_col (int): Return: None: If there are no unvisited neighbors list: A list of neighbors that have not been visited """ neighbours = list() 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: 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 if len(neighbours) > 0: return neighbours else: 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 the maze, we only want to move to move to unvisited cells (unless we are backtracking). Args: neighbour_indices: Return: True: If the neighbor has been visited False: If the neighbor has not been 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"): """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 of the neighbours are chosen randomly. The other is 'fancy' where the next cell is chosen based on which neighbour that gives the shortest distance to the final destination. Args: neighbour_indices k l k_end l_end method Return: """ if method == "fancy": neigh_list = list() 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): min_dist_to_target = dist_to_target min_neigh = (k_n, l_n) if "min_neigh" in locals(): 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]])] if len(neigh_list) > 0: return neigh_list else: return None def _pick_random_entry_exit(self, used_entry_exit=None): """Function that picks random coordinates along the maze boundary to represent either the entry or exit point of the maze. Makes sure they are not at the same place. Args: used_entry_exit Return: """ 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)) 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 == 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 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. Args: start_coor: The starting point for the algorithm """ if algorithm == "dfs_backtrack": depth_first_recursive_backtracker(self, start_coor) elif algorithm == "bin_tree": binary_tree(self, start_coor) ================================================ FILE: src/maze_manager.py ================================================ 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 class MazeManager(object): """A manager that abstracts the interaction with the library's components. The graphs, animations, maze creation, and solutions are all handled through the manager. Attributes: mazes (list): It is possible to have more than one maze. They are stored inside this variable. media_name (string): The filename for animations and images quiet_mode (bool): When true, information is not shown on the console """ def __init__(self): self.mazes = [] self.media_name = "" self.quiet_mode = False def add_maze(self, row, col, id=0): """Add a maze to the manager. We give the maze an index of the total number of mazes in the manager. As long as we don't add functionality to delete mazes from the manager, the ids will always be unique. Note that the id will always be greater than 0 because we add 1 to the length of self.mazes, which is set after the id assignment Args: row (int): The height of the maze col (int): The width of the maze id (int): The optional unique id of the maze. Returns Maze: The newly created maze """ if id is not 0: self.mazes.append(Maze(row, col, id)) else: if len(self.mazes) < 1: self.mazes.append(Maze(row, col, 0)) else: self.mazes.append(Maze(row, col, len(self.mazes) + 1)) return self.mazes[-1] def add_existing_maze(self, maze, override=True): """Add an already existing maze to the manager. Note that it is assumed that the maze already has an id. If the id already exists, the function will fail. To assign a new, unique id to the maze, set the overwrite flag to true. Args: maze: The maze that will be added to the manager override (bool): A flag that you can set to bypass checking the id Returns: True: If the maze was added to the manager False: If the maze could not be added to the manager """ # Check if there is a maze with the same id. If there is a conflict, return False if self.check_matching_id(maze.id) is None: if override: if len(self.mazes) < 1: maze.id = 0 else: maze.id = self.mazes.__len__()+1 else: return False self.mazes.append(maze) return maze def get_maze(self, id): """Get a maze by its id. Args: id (int): The id of the desired maze Return: Maze: Returns the maze if it was found. None: If no maze was found """ for maze in self.mazes: if maze.id == id: return maze print("Unable to locate maze") return None def get_mazes(self): """Get all of the mazes that the manager is holding""" return self.mazes def get_maze_count(self): """Gets the number of mazes that the manager is holding""" 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 is specified by a string. The options are 1. DepthFirstBacktracker 2. 3. Args: maze_id (int): The id of the maze that will be solved method (string): The name of the method (see above) neighbor_method: """ maze = self.get_maze(maze_id) if maze is None: print("Unable to locate maze. Exiting solver.") return None """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) maze.solution_path = solver.solve() elif method == "BiDirectional": solver = BiDirectional(maze, neighbor_method, self.quiet_mode) maze.solution_path = solver.solve() elif method == "BreadthFirst": solver = BreadthFirst(maze, neighbor_method, self.quiet_mode) maze.solution_path = solver.solve() def show_maze(self, id, cell_size=1): """Just show the generation animation and maze""" vis = Visualizer(self.get_maze(id), cell_size, self.media_name) vis.show_maze() def show_generation_animation(self, id, cell_size=1): vis = Visualizer(self.get_maze(id), cell_size, self.media_name) vis.show_generation_animation() 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): """ Shows the animation of the path that the solver took. Args: id (int): The id of the maze whose solution will be shown cell_size (int): """ vis = Visualizer(self.get_maze(id), cell_size, self.media_name) vis.animate_maze_solution() def check_matching_id(self, id): """Check if the id already belongs to an existing maze Args: id (int): The id to be checked Returns: """ return next((maze for maze in self.mazes if maze .id == id), None) def set_filename(self, filename): """ Sets the filename for saving animations and images Args: filename (string): The name of the file without an extension """ self.media_name = filename def set_quiet_mode(self, enabled): """ Enables/Disables the quiet mode Args: enabled (bool): True when quiet mode is on, False when it is off """ self.quiet_mode=enabled ================================================ FILE: src/maze_viz.py ================================================ 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) ================================================ FILE: src/solver.py ================================================ 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 ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/algorithm_tests.py ================================================ # 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 def create_maze(algorithm): rows, cols = (5,5) return Maze(rows, cols, algorithm = algorithm) class TestAlgorithm(unittest.TestCase): def test_NonEmptyPath(self): """Test to check that generation path for a maze is not an empty list""" # 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 ) # create message to display when test fails err_msg = f'Algorithm {algorithm} generated empty path' # assert path is non empty list 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 ) # create message to display when test fails 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 ) # 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 ) def test_AllCellsUnvisited(self): """Test to check that after maze generation all cells have been marked as unvisited.""" # repeat the following for all algorithms for algorithm in algorithm_list: # generate a maze using the algorithm maze = create_maze( algorithm ) # create message to display when test fails 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 ) def test_NoCellUnvisited(self): """Test to check that all cells have been processed, thus no cell has walls on all four sides""" # repeat the following for all algorithms for algorithm in algorithm_list: # generate a maze using the algorithm maze = create_maze( algorithm ) # create message to display when test fails 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} # 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: # check that the cell does not have walls on all four sides self.assertNotEqual( cell.walls, walls_4, msg = err_msg ) ================================================ FILE: tests/cell_tests.py ================================================ from __future__ import absolute_import import unittest from src.cell import Cell class TestCell(unittest.TestCase): def test_ctor(self): """Make sure that the constructor values are getting properly set.""" cell = Cell(2, 2) self.assertEqual(cell.row, 2) self.assertEqual(cell.col, 2) 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.neighbours, list()) def test_entry_exit(self): """Test the Cell::entry_exit method""" # Check if the entrance/exit is on the top row. cell = Cell(0, 1) cell.set_as_entry_exit(True, 3, 3) self.assertEqual(cell.is_entry_exit, True) self.assertEqual(cell.walls["top"], False) cell.set_as_entry_exit(False, 1, 0) self.assertEqual(cell.is_entry_exit, False) self.assertEqual(cell.walls["top"], False) # Check if the entrance/exit is on the bottom row. cell = Cell(1, 0) cell.set_as_entry_exit(True, 1, 0) self.assertEqual(cell.walls["bottom"], False) self.assertEqual(cell.is_entry_exit, True) # Check if the entrance/exit is on the left wall. cell = Cell(2, 0) cell.set_as_entry_exit(True, 3, 1) self.assertEqual(cell.walls["left"], False) cell.set_as_entry_exit(True, 1, 1) # Check if the entrance/exit is on the right side wall. cell = Cell(3, 2) cell.set_as_entry_exit(True, 2, 2) self.assertEqual(cell.walls["right"], False) # Check if we can make the exit on the right wall in a corner cell = Cell(2, 2) cell.set_as_entry_exit(True, 2, 2) self.assertEqual(cell.walls["right"], True) 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) self.assertEqual(cell.walls["right"], False) # Remove the cell to the left cell = Cell(0, 1) cell.remove_walls(0, 0) self.assertEqual(cell.walls["left"], False) # Remove the cell above cell = Cell(1, 1) cell.remove_walls(0, 1) self.assertEqual(cell.walls["top"], False) # Remove the cell below cell = Cell(1, 1) cell.remove_walls(2, 1) self.assertEqual(cell.walls["bottom"], False) 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. """ # Create a base cell for which we will be testing whether walls exist cell = Cell (1, 1) # Create a cell appearing to the top of this cell cell_top = Cell(0,1) # Create a cell appearing to the right of this cell cell_right = Cell(1,2) # Create a cell appearing to the bottom of this cell cell_bottom = Cell(2,1) # Create a cell appearing to the left of this cell cell_left = Cell(1,0) # check for walls between all these cells self.assertEqual(cell.is_walls_between(cell_top), True) self.assertEqual(cell.is_walls_between(cell_right), True) self.assertEqual(cell.is_walls_between(cell_bottom), True) 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) # check that there are no walls between these cells self.assertEqual(cell.is_walls_between(cell_top), False) if __name__ == "__main__": unittest.main() ================================================ FILE: tests/maze_manager_tests.py ================================================ from __future__ import absolute_import import unittest from src.maze_manager import MazeManager from src.maze_viz import Visualizer from src.maze import Maze class TestMgr(unittest.TestCase): def test_ctor(self): """Make sure that the constructor values are getting properly set.""" manager = MazeManager() self.assertEqual(manager.get_maze_count(), 0) self.assertEqual(manager.get_mazes(), []) self.assertEqual(manager.quiet_mode, False) def test_add_new(self): """Test adding mazes by passing maze specs into add_maze""" manager = MazeManager() maze1 = manager.add_maze(6, 6) self.assertEqual(maze1.id, 0) self.assertEqual(manager.get_mazes().__len__(), 1) self.assertEqual(manager.get_maze_count(), 1) maze2 = manager.add_maze(3, 3, 1) self.assertEqual(maze2.id, 1) self.assertEqual(manager.get_mazes().__len__(), 2) self.assertEqual(manager.get_maze_count(), 2) def test_add_existing(self): """Test adding mazes by passing already existing Maze objects in""" manager = MazeManager() maze1 = Maze(2, 2) self.assertEqual(maze1.id, 0) manager.add_existing_maze(maze1) self.assertEqual(manager.get_mazes().__len__(), 1) self.assertEqual(manager.get_maze_count(), 1) self.assertIsNotNone(manager.get_maze(maze1.id)) self.assertEqual(manager.get_maze(maze1.id).id, maze1.id) maze2 = Maze(3, 3, 1) self.assertEqual(maze2.id, 1) manager.add_existing_maze(maze2) self.assertEqual(manager.get_mazes().__len__(), 2) self.assertEqual(manager.get_maze_count(), 2) self.assertIsNotNone(manager.get_maze(maze2.id)) self.assertEqual(manager.get_maze(maze2.id).id, maze2.id) def test_get_maze(self): """Test the get_maze function""" manager = MazeManager() self.assertEqual(manager.get_maze(0), None) self.assertEqual(manager.get_mazes(), []) maze1 = manager.add_maze(6, 6) self.assertEqual(maze1.id, 0) def test_get_mazes(self): """Tests that get_mazes is returning all mazes""" manager = MazeManager() self.assertEqual(manager.get_maze(0), None) self.assertEqual(manager.get_mazes(), []) manager.add_maze(6, 6) manager.add_maze(6, 6) mazes = manager.get_mazes() self.assertAlmostEqual(mazes.__len__(), 2) def test_get_maze_count(self): """Tests the get_maze_number function""" manager = MazeManager() self.assertEqual(manager.get_maze_count(), 0) maze1 = Maze(2, 2) manager.add_existing_maze(maze1) self.assertEqual(manager.get_maze_count(), 1) def test_check_matching_id(self): """Check that check_matching_id is functioning properly""" manager = MazeManager() manager.add_maze(8, 8, 1) manager.add_maze(8, 8, 1) result = [manager.check_matching_id(1)] self.assertEqual(len(result), 1) def test_set_filename(self): """Tests that the filename is getting set""" manager = MazeManager() filename = "myFile" manager.set_filename(filename) self.assertEqual(filename, manager.media_name) def test_set_quiet_mode(self): manager = MazeManager() self.assertEqual(manager.quiet_mode, False) manager.set_quiet_mode(True) self.assertEqual(manager.quiet_mode, True) if __name__ == "__main__": unittest.main() ================================================ FILE: tests/maze_tests.py ================================================ from __future__ import absolute_import import unittest from src.maze import Maze from src.cell import Cell def generate_maze(): # Used to generate a 5x5 maze for testing, Feel free to modify as needed cols = 5 rows = 5 return Maze(rows, cols) class TestMaze(unittest.TestCase): def test_ctor(self): """Make sure that the constructor values are getting properly set.""" cols = 5 rows = 5 maze = Maze(rows, cols) self.assertEqual(maze.num_cols, cols) self.assertEqual(maze.num_rows, rows) self.assertEqual(maze.id, 0) self.assertEqual(maze.grid_size, rows*cols) id=33 maze2 = Maze(rows, cols, id) self.assertEqual(maze2.num_cols, cols) self.assertEqual(maze2.num_rows, rows) self.assertEqual(maze2.id, id) self.assertEqual(maze2.grid_size, rows * cols) def test_generate_grid(self): maze = generate_maze() grid = maze.generate_grid() self.assertEqual(len(grid), maze.num_cols) self.assertGreater(len(grid), 2) self.assertEqual(len(grid[0]), maze.num_rows) def test_find_neighbors(self): maze = Maze(2, 2) neighbors = maze.find_neighbours(0, 1) self.assertIsNotNone(neighbors) if __name__ == "__main__": unittest.main() ================================================ FILE: tests/maze_viz_tests.py ================================================ ================================================ FILE: tests/solver_tests.py ================================================ from __future__ import absolute_import import unittest from src.solver import Solver class TestSolver(unittest.TestCase): def test_ctor(self): solver = Solver("", "", False) self.assertEqual(solver.name, "") self.assertEqual(solver.quiet_mode, False) if __name__ == "__main__": unittest.main()