feat: add the algorithm to solve the maze

This commit is contained in:
Dan Anglin 2024-02-15 19:13:31 +00:00
parent 5be7f5041a
commit 5c48f47a0a
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
4 changed files with 90 additions and 26 deletions

28
cell.py
View file

@ -4,7 +4,7 @@ from graphics import Window, Point, Line
import errors import errors
class CellWallLabel(Enum): class CellWallLabels(Enum):
""" """
CellWallLabel is used to label a CellWall CellWallLabel is used to label a CellWall
""" """
@ -61,11 +61,11 @@ class Cell:
left_wall = Line(Point(x1, y1), Point(x1, y2)) left_wall = Line(Point(x1, y1), Point(x1, y2))
right_wall = Line(Point(x2, y1), Point(x2, y2)) right_wall = Line(Point(x2, y1), Point(x2, y2))
self._walls: Dict[CellWallLabel, CellWall] = { self._walls: Dict[CellWallLabels, CellWall] = {
CellWallLabel.TOP: CellWall(top_wall, window), CellWallLabels.TOP: CellWall(top_wall, window),
CellWallLabel.BOTTOM: CellWall(bottom_wall, window), CellWallLabels.BOTTOM: CellWall(bottom_wall, window),
CellWallLabel.LEFT: CellWall(left_wall, window), CellWallLabels.LEFT: CellWall(left_wall, window),
CellWallLabel.RIGHT: CellWall(right_wall, window), CellWallLabels.RIGHT: CellWall(right_wall, window),
} }
# Calculate the cell's central point # Calculate the cell's central point
@ -90,13 +90,13 @@ class Cell:
configure_walls configures the existence of the Cell's walls. configure_walls configures the existence of the Cell's walls.
""" """
if top is not None: if top is not None:
self._walls[CellWallLabel.TOP].exists = top self._walls[CellWallLabels.TOP].exists = top
if bottom is not None: if bottom is not None:
self._walls[CellWallLabel.BOTTOM].exists = bottom self._walls[CellWallLabels.BOTTOM].exists = bottom
if left is not None: if left is not None:
self._walls[CellWallLabel.LEFT].exists = left self._walls[CellWallLabels.LEFT].exists = left
if right is not None: if right is not None:
self._walls[CellWallLabel.RIGHT].exists = right self._walls[CellWallLabels.RIGHT].exists = right
def centre(self) -> Point: def centre(self) -> Point:
""" """
@ -104,10 +104,14 @@ class Cell:
""" """
return self._centre return self._centre
def wall_exists(self, wall: CellWallLabel) -> bool: def wall_exists(self, wall: CellWallLabels) -> bool:
""" """
returns True if a given cell wall exists, or false otherwise. returns True if a given cell wall exists, or false otherwise.
""" """
if wall not in CellWallLabels:
raise TypeError(
"The argument does not appear to be a valid cell wall."
)
return self._walls[wall].exists return self._walls[wall].exists
def draw(self) -> None: def draw(self) -> None:
@ -117,7 +121,7 @@ class Cell:
if not self._window: if not self._window:
return return
for label in CellWallLabel: for label in CellWallLabels:
self._walls[label].draw() self._walls[label].draw()
def draw_move(self, to_cell: 'Cell', undo: bool = False) -> None: def draw_move(self, to_cell: 'Cell', undo: bool = False) -> None:

View file

@ -5,7 +5,7 @@ from maze import Maze
def main(): def main():
window = Window(800, 800) window = Window(800, 800)
_ = Maze( game = Maze(
x_position=10, x_position=10,
y_position=10, y_position=10,
num_cell_rows=30, num_cell_rows=30,
@ -15,6 +15,12 @@ def main():
window=window window=window
) )
solved = game.solve()
if solved:
print("Maze solved successfully :)")
else:
print("I'm unable to solve the maze :(")
window.mainloop() window.mainloop()

74
maze.py
View file

@ -1,9 +1,9 @@
from typing import List from typing import List, Dict
from time import sleep from time import sleep
import random import random
from enum import Enum from enum import Enum
from graphics import Window from graphics import Window
from cell import Cell from cell import Cell, CellWallLabels
class MazeDirections(Enum): class MazeDirections(Enum):
@ -39,7 +39,7 @@ class MazePosition:
) -> 'MazePosition': ) -> 'MazePosition':
if direction not in MazeDirections: if direction not in MazeDirections:
raise TypeError( raise TypeError(
"The argument does not appear to be a valid MazeDirection" "The argument does not appear to be a valid maze direction."
) )
if direction is MazeDirections.ABOVE and (self.i-1 >= 0): if direction is MazeDirections.ABOVE and (self.i-1 >= 0):
@ -105,6 +105,8 @@ class Maze:
self._cells: List[List[Cell]] = [ self._cells: List[List[Cell]] = [
None for i in range(self._num_cell_rows)] None for i in range(self._num_cell_rows)]
self._create_cell_grid() self._create_cell_grid()
# Open up the maze's entrance and exit.
self._open_entrance_and_exit() self._open_entrance_and_exit()
start_position = MazePosition( start_position = MazePosition(
@ -114,13 +116,7 @@ class Maze:
max_j=self._num_cells_per_row-1, max_j=self._num_cells_per_row-1,
) )
end_position = MazePosition( # Generate the maze.
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,
)
self._break_walls_r(start_position) self._break_walls_r(start_position)
def _create_cell_grid(self) -> None: def _create_cell_grid(self) -> None:
@ -227,6 +223,59 @@ class Maze:
self._break_walls_r(next_position) 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:
if current_position == end_position:
return True
current_cell = self._cells[current_position.i][current_position.j]
current_cell.visited_by_maze_solver = True
wall_map: Dict[MazeDirections, CellWallLabels] = {
MazeDirections.ABOVE: CellWallLabels.BOTTOM,
MazeDirections.BELOW: CellWallLabels.TOP,
MazeDirections.LEFT: CellWallLabels.RIGHT,
MazeDirections.RIGHT: CellWallLabels.LEFT,
}
for direction in MazeDirections:
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.visited_by_maze_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: def _draw_cell(self, i: int, j: int) -> None:
""" """
_draw_cell draws the cells in an animated way. _draw_cell draws the cells in an animated way.
@ -234,3 +283,8 @@ class Maze:
self._cells[i][j].draw() self._cells[i][j].draw()
self._window.redraw() self._window.redraw()
sleep(0.05) 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)

View file

@ -1,5 +1,5 @@
import unittest import unittest
from cell import Cell, CellWallLabel from cell import Cell, CellWallLabels
import maze import maze
import errors import errors
@ -65,10 +65,10 @@ class Tests(unittest.TestCase):
None, None,
None, None,
) )
self.assertFalse(m._cells[0][0].wall_exists(CellWallLabel.TOP)) self.assertFalse(m._cells[0][0].wall_exists(CellWallLabels.TOP))
self.assertFalse( self.assertFalse(
m._cells[number_of_cell_rows - 1] m._cells[number_of_cell_rows - 1]
[number_of_cells_per_row - 1].wall_exists(CellWallLabel.BOTTOM) [number_of_cells_per_row - 1].wall_exists(CellWallLabels.BOTTOM)
) )
def test_invalid_cell_exception(self): def test_invalid_cell_exception(self):