maze-solver/maze.py
Dan Anglin d1c3ca6658
feat: add maze generation functionality
Main feature:

- Added functionality to randomly generate a maze before the solver
  solves it.

Fixes:

- Add a public method in the Cell class called wall_exists() that
  returns true if a given cell wall exists (false otherwise).

Refactors:

- Added an enum type called CellWallLabel for labelling the four cell
  walls.
- Added a draw() function in the CellWall class to reduce repeated code.
- Move the custom exceptions to errors.py

Tests:

- Add tests for the custom exceptions.

CI:

- Added a workflow for Forgejo Actions.
2024-02-14 22:50:46 +00:00

148 lines
4.9 KiB
Python

from typing import List
from time import sleep
import random
from graphics import Window
from cell import Cell
class Maze:
"""
Maze represents a two-dimensional grid of Cells.
"""
def __init__(
self,
x_position: int,
y_position: int,
num_cell_rows: int,
num_cells_per_row: int,
cell_size_x: int,
cell_size_y: int,
window: Window = None,
seed=None,
) -> None:
self._x_position = x_position
self._y_position = y_position
self._num_cell_rows = num_cell_rows
self._num_cells_per_row = num_cells_per_row
self._cell_size_x = cell_size_x
self._cell_size_y = cell_size_y
self._window = window
# initialise the random number generator
random.seed(seed)
# Create the Maze's cells
self._cells: List[List[Cell]] = [None for i in range(self._num_cell_rows)]
self._create_cells()
self._open_entrance_and_exit()
self._break_walls_r(0, 0)
def _create_cells(self) -> None:
"""
creates all the cells and draws them.
"""
cursor_x = self._x_position
cursor_y = self._y_position
for i in range(self._num_cell_rows):
cells: List[Cell] = [None for j in range(self._num_cells_per_row)]
for j in range(self._num_cells_per_row):
cell = Cell(
cursor_x,
cursor_y,
(cursor_x + self._cell_size_x),
(cursor_y + self._cell_size_y),
self._window
)
cells[j] = cell
if j == self._num_cells_per_row - 1:
cursor_x = self._x_position
else:
cursor_x += self._cell_size_x
self._cells[i] = cells
cursor_y += self._cell_size_y
if self._window:
self._draw_cells()
def _open_entrance_and_exit(self) -> None:
"""
opens the maze's entrance and exit cells by breaking their respective
walls. The entrance is located at the top left and the exit is located
at the bottom right of the maze.
"""
self._cells[0][0].configure_walls(top=False)
self._cells[0][0].draw()
self._cells[self._num_cell_rows-1][self._num_cells_per_row-1].configure_walls(bottom=False)
self._cells[self._num_cell_rows-1][self._num_cells_per_row-1].draw()
def _break_walls_r(self, y: int, x: int) -> None:
current_cell = self._cells[y][x]
current_cell.visited = True
above, below, left, right = "above", "below", "left", "right"
while True:
adjacent_cells = {
above: (y-1, x),
below: (y+1, x),
left: (y, x-1),
right: (y, x+1),
}
to_visit: List[str] = []
for k, value in adjacent_cells.items():
if (value[0] < 0)or (value[1] < 0) or (value[0] > self._num_cell_rows-1) or (value[1] > self._num_cells_per_row-1):
continue
if self._cells[value[0]][value[1]].visited:
continue
to_visit.append(k)
if len(to_visit) == 0:
current_cell.draw()
break
next_direction = random.choice(to_visit)
next_cell = self._cells[adjacent_cells[next_direction][0]][adjacent_cells[next_direction][1]]
if next_direction is above:
current_cell.configure_walls(top=False)
next_cell.configure_walls(bottom=False)
elif next_direction is below:
current_cell.configure_walls(bottom=False)
next_cell.configure_walls(top=False)
elif next_direction is left:
current_cell.configure_walls(left=False)
next_cell.configure_walls(right=False)
elif next_direction is right:
current_cell.configure_walls(right=False)
next_cell.configure_walls(left=False)
current_cell.draw()
next_cell.draw()
self._animate()
self._break_walls_r(
adjacent_cells[next_direction][0],
adjacent_cells[next_direction][1],
)
def _draw_cells(self) -> None:
"""
draws all the cells on the maze with a short pause between each cell
for animation purposes.
"""
for i in range(self._num_cell_rows):
for j in range(self._num_cells_per_row):
self._cells[i][j].draw()
self._animate()
def _animate(self) -> None:
"""
redraws the application and pauses for a short period of time to
provide an animation effect.
"""
self._window.redraw()
sleep(0.05)