Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Makes pymaze an installable package #37

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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__":
Expand Down Expand Up @@ -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
Expand All @@ -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`.

`pycodestyle pymaze/my_file.py`.
5 changes: 2 additions & 3 deletions examples/generate_binary_tree_algorithm.py
Original file line number Diff line number Diff line change
@@ -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__":
Expand Down
11 changes: 5 additions & 6 deletions examples/quick_start.py
Original file line number Diff line number Diff line change
@@ -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__":
Expand All @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions examples/solve_bi_directional.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from __future__ import absolute_import
from src.maze_manager import MazeManager
from pymaze.maze_manager import MazeManager


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion examples/solve_breadth_first_recursive.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import absolute_import
from src.maze_manager import MazeManager
from pymaze.maze_manager import MazeManager


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion examples/solve_depth_first_recursive.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import absolute_import
from src.maze_manager import MazeManager
from pymaze.maze_manager import MazeManager


if __name__ == "__main__":
Expand Down
File renamed without changes.
187 changes: 187 additions & 0 deletions pymaze/algorithm.py
Original file line number Diff line number Diff line change
@@ -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
Loading