Dan Anglin
c809292c04
- Add methods to mark cells as visited and to get results of the visits. - renamed MazeDirections to MazeDirection
295 lines
9.4 KiB
Python
295 lines
9.4 KiB
Python
from typing import List, Dict
|
|
from time import sleep
|
|
import random
|
|
from enum import Enum
|
|
from graphics import Window
|
|
from cell import Cell, CellWallLabels
|
|
|
|
|
|
class MazeDirection(Enum):
|
|
"""
|
|
MazeDirection represents the directions you can
|
|
take in the maze.
|
|
"""
|
|
ABOVE = 0
|
|
BELOW = 1
|
|
LEFT = 2
|
|
RIGHT = 3
|
|
|
|
|
|
class MazePosition:
|
|
"""
|
|
MazePosition represents a position on the maze grid.
|
|
"""
|
|
|
|
def __init__(self, i: int, j: int, max_i: int, max_j: int):
|
|
self.i = i
|
|
self.j = j
|
|
self.max_i = max_i
|
|
self.max_j = max_j
|
|
|
|
def __eq__(self, other) -> bool:
|
|
if (self.i == other.i) and (self.j == other.j) and (self.max_i == other.max_i) and (self.max_j == other.max_j):
|
|
return True
|
|
return False
|
|
|
|
def get_adjacent_position(
|
|
self,
|
|
direction: MazeDirection
|
|
) -> 'MazePosition':
|
|
if direction not in MazeDirection:
|
|
raise TypeError(
|
|
"The argument does not appear to be a valid maze direction."
|
|
)
|
|
|
|
if direction is MazeDirection.ABOVE and (self.i-1 >= 0):
|
|
return MazePosition(
|
|
i=self.i-1,
|
|
j=self.j,
|
|
max_i=self.max_i,
|
|
max_j=self.max_j,
|
|
)
|
|
if direction is MazeDirection.BELOW and (self.i+1 <= self.max_i):
|
|
return MazePosition(
|
|
i=self.i+1,
|
|
j=self.j,
|
|
max_i=self.max_i,
|
|
max_j=self.max_j,
|
|
)
|
|
if direction is MazeDirection.LEFT and (self.j-1 >= 0):
|
|
return MazePosition(
|
|
i=self.i,
|
|
j=self.j-1,
|
|
max_i=self.max_i,
|
|
max_j=self.max_j,
|
|
)
|
|
if direction is MazeDirection.RIGHT and (self.j+1 <= self.max_j):
|
|
return MazePosition(
|
|
i=self.i,
|
|
j=self.j+1,
|
|
max_i=self.max_i,
|
|
max_j=self.max_j,
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
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_cell_grid()
|
|
|
|
# Open up the maze's entrance and exit.
|
|
self._open_entrance_and_exit()
|
|
|
|
start_position = MazePosition(
|
|
i=0,
|
|
j=0,
|
|
max_i=self._num_cell_rows-1,
|
|
max_j=self._num_cells_per_row-1,
|
|
)
|
|
|
|
# Generate the maze.
|
|
self._break_walls_r(start_position)
|
|
|
|
def _create_cell_grid(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_cell_grid()
|
|
|
|
def _draw_cell_grid(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._draw_cell(i=i, j=j)
|
|
|
|
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[self._num_cell_rows -
|
|
1][self._num_cells_per_row-1].configure_walls(bottom=False)
|
|
|
|
if self._window:
|
|
self._draw_cell(0, 0)
|
|
self._draw_cell(
|
|
i=self._num_cell_rows-1,
|
|
j=self._num_cells_per_row-1
|
|
)
|
|
|
|
def _break_walls_r(self, current_position: MazePosition) -> None:
|
|
"""
|
|
_break_walls_r generates a random maze by traversing through the
|
|
cells and randomly knocking down the walls to create the maze's paths.
|
|
"""
|
|
|
|
generator = "generator"
|
|
|
|
current_cell = self._cells[current_position.i][current_position.j]
|
|
current_cell.mark_as_visited_by(generator)
|
|
|
|
while True:
|
|
possible_directions: List[MazeDirection] = []
|
|
|
|
for direction in MazeDirection:
|
|
adjacent_position = current_position.get_adjacent_position(
|
|
direction)
|
|
if adjacent_position is None:
|
|
continue
|
|
adjacent_cell = self._cells[adjacent_position.i][adjacent_position.j]
|
|
if adjacent_cell.was_visited_by(generator):
|
|
continue
|
|
possible_directions.append(direction)
|
|
|
|
if len(possible_directions) == 0:
|
|
if self._window:
|
|
self._draw_cell(i=current_position.i, j=current_position.j)
|
|
break
|
|
|
|
chosen_direction = random.choice(possible_directions)
|
|
next_position = current_position.get_adjacent_position(
|
|
chosen_direction)
|
|
next_cell = self._cells[next_position.i][next_position.j]
|
|
|
|
if chosen_direction is MazeDirection.ABOVE:
|
|
current_cell.configure_walls(top=False)
|
|
next_cell.configure_walls(bottom=False)
|
|
elif chosen_direction is MazeDirection.BELOW:
|
|
current_cell.configure_walls(bottom=False)
|
|
next_cell.configure_walls(top=False)
|
|
elif chosen_direction is MazeDirection.LEFT:
|
|
current_cell.configure_walls(left=False)
|
|
next_cell.configure_walls(right=False)
|
|
elif chosen_direction is MazeDirection.RIGHT:
|
|
current_cell.configure_walls(right=False)
|
|
next_cell.configure_walls(left=False)
|
|
|
|
if self._window:
|
|
self._draw_cell(i=current_position.i, j=current_position.j)
|
|
|
|
self._break_walls_r(next_position)
|
|
|
|
def solve(self) -> bool:
|
|
"""
|
|
solve attempts to solve the generated maze.
|
|
"""
|
|
start_position = MazePosition(
|
|
i=0,
|
|
j=0,
|
|
max_i=self._num_cell_rows-1,
|
|
max_j=self._num_cells_per_row-1,
|
|
)
|
|
|
|
end_position = MazePosition(
|
|
i=self._num_cell_rows-1,
|
|
j=self._num_cells_per_row-1,
|
|
max_i=self._num_cell_rows-1,
|
|
max_j=self._num_cells_per_row-1,
|
|
)
|
|
|
|
return self._solve_r(start_position, end_position)
|
|
|
|
def _solve_r(
|
|
self,
|
|
current_position: MazePosition,
|
|
end_position: MazePosition,
|
|
) -> bool:
|
|
solver = "solver"
|
|
|
|
if current_position == end_position:
|
|
return True
|
|
current_cell = self._cells[current_position.i][current_position.j]
|
|
current_cell.mark_as_visited_by(solver)
|
|
|
|
wall_map: Dict[MazeDirection, CellWallLabels] = {
|
|
MazeDirection.ABOVE: CellWallLabels.BOTTOM,
|
|
MazeDirection.BELOW: CellWallLabels.TOP,
|
|
MazeDirection.LEFT: CellWallLabels.RIGHT,
|
|
MazeDirection.RIGHT: CellWallLabels.LEFT,
|
|
}
|
|
|
|
for direction in MazeDirection:
|
|
adjacent_position = current_position.get_adjacent_position(
|
|
direction
|
|
)
|
|
if adjacent_position is None:
|
|
continue
|
|
adjacent_cell = self._cells[adjacent_position.i][adjacent_position.j]
|
|
if adjacent_cell.was_visited_by(solver) or adjacent_cell.wall_exists(wall_map[direction]):
|
|
continue
|
|
self._draw_path(current_cell, adjacent_cell)
|
|
result = self._solve_r(adjacent_position, end_position)
|
|
if result is True:
|
|
return True
|
|
self._draw_path(current_cell, adjacent_cell, undo=True)
|
|
|
|
return False
|
|
|
|
def _draw_cell(self, i: int, j: int) -> None:
|
|
"""
|
|
_draw_cell draws the cells in an animated way.
|
|
"""
|
|
self._cells[i][j].draw()
|
|
self._window.redraw()
|
|
sleep(0.05)
|
|
|
|
def _draw_path(self, current_cell: Cell, next_cell: Cell, undo: bool = False) -> None:
|
|
current_cell.draw_move(next_cell, undo)
|
|
self._window.redraw()
|
|
sleep(0.05)
|