refactor: add the Solver class

Create a separate class to represent the solver of the maze.
Additional search algorithms will be implemented here.
This commit is contained in:
Dan Anglin 2024-02-16 23:22:50 +00:00
parent c809292c04
commit 856391542b
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
4 changed files with 292 additions and 154 deletions

14
main.py
View file

@ -1,5 +1,6 @@
from graphics import Window
from maze import Maze
from solver import Solver
def main():
@ -8,15 +9,16 @@ def main():
game = Maze(
x_position=10,
y_position=10,
num_cell_rows=16,
num_cells_per_row=16,
cell_size_x=40,
cell_size_y=40,
height=16,
width=16,
cell_height=40,
cell_width=40,
window=window
)
solved = game.solve()
if solved:
solver = Solver(game)
if solver.solve():
print("Maze solved successfully :)")
else:
print("I'm unable to solve the maze :(")

291
maze.py
View file

@ -1,4 +1,4 @@
from typing import List, Dict
from typing import List
from time import sleep
import random
from enum import Enum
@ -22,14 +22,14 @@ class MazePosition:
MazePosition represents a position on the maze grid.
"""
def __init__(self, i: int, j: int, max_i: int, max_j: int):
def __init__(self, i: int, j: int, last_i: int, last_j: int):
self.i = i
self.j = j
self.max_i = max_i
self.max_j = max_j
self.last_i = last_i
self.last_j = last_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):
if (self.i == other.i) and (self.j == other.j) and (self.last_i == other.last_i) and (self.last_j == other.last_j):
return True
return False
@ -37,6 +37,12 @@ class MazePosition:
self,
direction: MazeDirection
) -> 'MazePosition':
"""
calculate and return the position on the maze that is directly adjacent
to this maze position in the specified direction. If the adjacent
position is outside the boundaries of the maze then a value of None
is returned.
"""
if direction not in MazeDirection:
raise TypeError(
"The argument does not appear to be a valid maze direction."
@ -46,29 +52,29 @@ class MazePosition:
return MazePosition(
i=self.i-1,
j=self.j,
max_i=self.max_i,
max_j=self.max_j,
last_i=self.last_i,
last_j=self.last_j,
)
if direction is MazeDirection.BELOW and (self.i+1 <= self.max_i):
if direction is MazeDirection.BELOW and (self.i+1 <= self.last_i):
return MazePosition(
i=self.i+1,
j=self.j,
max_i=self.max_i,
max_j=self.max_j,
last_i=self.last_i,
last_j=self.last_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,
last_i=self.last_i,
last_j=self.last_j,
)
if direction is MazeDirection.RIGHT and (self.j+1 <= self.max_j):
if direction is MazeDirection.RIGHT and (self.j+1 <= self.last_j):
return MazePosition(
i=self.i,
j=self.j+1,
max_i=self.max_i,
max_j=self.max_j,
last_i=self.last_i,
last_j=self.last_j,
)
return None
@ -83,27 +89,27 @@ class Maze:
self,
x_position: int,
y_position: int,
num_cell_rows: int,
num_cells_per_row: int,
cell_size_x: int,
cell_size_y: int,
height: int,
width: int,
cell_height: int,
cell_width: 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._height = height
self._width = width
self._cell_height = cell_height
self._cell_width = cell_width
self._window = window
self._generator = "generator"
# 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._cell_grid: List[List[Cell]] = []
self._create_cell_grid()
# Open up the maze's entrance and exit.
@ -112,37 +118,49 @@ class Maze:
start_position = MazePosition(
i=0,
j=0,
max_i=self._num_cell_rows-1,
max_j=self._num_cells_per_row-1,
last_i=self._height-1,
last_j=self._width-1,
)
# Generate the maze.
self._break_walls_r(start_position)
def get_last_i(self) -> int:
"returns the last position of the Maze's outer list."
return self._height-1
def get_last_j(self) -> int:
"returns the last position of the Maze's inner list."
return self._width-1
def _create_cell_grid(self) -> None:
"""
creates all the cells and draws them.
"""
self._cell_grid: List[List[Cell]] = [None for i in range(self._height)]
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):
for i in range(self._height):
cells: List[Cell] = [None for j in range(self._width)]
for j in range(self._width):
cell = Cell(
cursor_x,
cursor_y,
(cursor_x + self._cell_size_x),
(cursor_y + self._cell_size_y),
(cursor_x + self._cell_width),
(cursor_y + self._cell_height),
self._window
)
cells[j] = cell
if j == self._num_cells_per_row - 1:
if j == self._width - 1:
cursor_x = self._x_position
else:
cursor_x += self._cell_size_x
self._cells[i] = cells
cursor_y += self._cell_size_y
cursor_x += self._cell_width
self._cell_grid[i] = cells
cursor_y += self._cell_height
if self._window:
self._draw_cell_grid()
@ -152,8 +170,9 @@ class Maze:
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):
for i in range(self._height):
for j in range(self._width):
self._draw_cell(i=i, j=j)
def _open_entrance_and_exit(self) -> None:
@ -162,15 +181,16 @@ class Maze:
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)
self._cell_grid[0][0].configure_walls(top=False)
self._cell_grid[self._height-1][self._width -
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
i=self._height-1,
j=self._width-1
)
def _break_walls_r(self, current_position: MazePosition) -> None:
@ -179,10 +199,11 @@ class Maze:
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)
self.mark_cell_as_visited(
i=current_position.i,
j=current_position.j,
visitor=self._generator,
)
while True:
possible_directions: List[MazeDirection] = []
@ -192,8 +213,11 @@ class Maze:
direction)
if adjacent_position is None:
continue
adjacent_cell = self._cells[adjacent_position.i][adjacent_position.j]
if adjacent_cell.was_visited_by(generator):
if self.cell_was_visited_by(
i=adjacent_position.i,
j=adjacent_position.j,
visitor=self._generator,
):
continue
possible_directions.append(direction)
@ -203,93 +227,124 @@ class Maze:
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]
next_position = current_position.get_adjacent_position(chosen_direction)
if chosen_direction is MazeDirection.ABOVE:
current_cell.configure_walls(top=False)
next_cell.configure_walls(bottom=False)
self._configure_cell_walls(
i=current_position.i,
j=current_position.j,
top=False,
)
self._configure_cell_walls(
i=next_position.i,
j=next_position.j,
bottom=False,
)
elif chosen_direction is MazeDirection.BELOW:
current_cell.configure_walls(bottom=False)
next_cell.configure_walls(top=False)
self._configure_cell_walls(
i=current_position.i,
j=current_position.j,
bottom=False,
)
self._configure_cell_walls(
i=next_position.i,
j=next_position.j,
top=False,
)
elif chosen_direction is MazeDirection.LEFT:
current_cell.configure_walls(left=False)
next_cell.configure_walls(right=False)
self._configure_cell_walls(
i=current_position.i,
j=current_position.j,
left=False,
)
self._configure_cell_walls(
i=next_position.i,
j=next_position.j,
right=False,
)
elif chosen_direction is MazeDirection.RIGHT:
current_cell.configure_walls(right=False)
next_cell.configure_walls(left=False)
self._configure_cell_walls(
i=current_position.i,
j=current_position.j,
right=False,
)
self._configure_cell_walls(
i=next_position.i,
j=next_position.j,
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.
draws the cells in an animated way.
"""
self._cells[i][j].draw()
self._cell_grid[i][j].draw()
self._window.redraw()
sleep(0.05)
def _draw_path(self, current_cell: Cell, next_cell: Cell, undo: bool = False) -> None:
"""
draws a path between two cells in an animated way.
"""
current_cell.draw_move(next_cell, undo)
self._window.redraw()
sleep(0.05)
def mark_cell_as_visited(self, i: int, j: int, visitor: str) -> None:
"""
marks the cell at the specified position by the specified visitor.
"""
self._cell_grid[i][j].mark_as_visited_by(visitor)
def cell_was_visited_by(self, i: int, j: int, visitor: str) -> bool:
"""
returns True if the cell at the specified position was visited by
the specified visitor.
"""
return self._cell_grid[i][j].was_visited_by(visitor)
def cell_wall_exists(self, i: int, j: int, wall: CellWallLabels) -> bool:
"""
returns true if a specified cell's wall exists.
"""
return self._cell_grid[i][j].wall_exists(wall)
def draw_path_between(self, a: MazePosition, b: MazePosition, undo: bool = False) -> None:
"""
draws a path between position A and position B
"""
cell_a = self._cell_grid[a.i][a.j]
cell_b = self._cell_grid[b.i][b.j]
self._draw_path(cell_a, cell_b, undo)
def _configure_cell_walls(
self,
i: int,
j: int,
top: bool = None,
bottom: bool = None,
left: bool = None,
right: bool = None,
) -> None:
"""
(re)configures the walls of the specified cell.
"""
self._cell_grid[i][j].configure_walls(
top=top,
bottom=bottom,
left=left,
right=right,
)

81
solver.py Normal file
View file

@ -0,0 +1,81 @@
from typing import Dict
from maze import Maze, MazeDirection, MazePosition
from cell import CellWallLabels
class Solver:
def __init__(self, game: Maze):
self._game = game
self._solver = "solver"
# This is a dictionary mapping the direction to the maze to the
# wall of the adjacent cell. It is used to identify the wall that could
# potentially block the solver's path.
# For example if the solver wants to move to the right, it's path
# could be blocked by the adjacent cell's left wall.
self._wall_map: Dict[MazeDirection, CellWallLabels] = {
MazeDirection.ABOVE: CellWallLabels.BOTTOM,
MazeDirection.BELOW: CellWallLabels.TOP,
MazeDirection.LEFT: CellWallLabels.RIGHT,
MazeDirection.RIGHT: CellWallLabels.LEFT,
}
def solve(self) -> bool:
"""
solve attempts to solve the generated maze.
"""
start_position = MazePosition(
i=0,
j=0,
last_i=self._game.get_last_i(),
last_j=self._game.get_last_j(),
)
end_position = MazePosition(
i=self._game.get_last_i(),
j=self._game.get_last_j(),
last_i=self._game.get_last_i(),
last_j=self._game.get_last_j(),
)
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
self._game.mark_cell_as_visited(
i=current_position.i,
j=current_position.j,
visitor=self._solver,
)
for direction in MazeDirection:
adjacent_position = current_position.get_adjacent_position(
direction
)
if adjacent_position is None:
continue
if self._game.cell_was_visited_by(
i=adjacent_position.i,
j=adjacent_position.j,
visitor=self._solver,
) or self._game.cell_wall_exists(
i=adjacent_position.i,
j=adjacent_position.j,
wall=self._wall_map[direction],
):
continue
self._game.draw_path_between(current_position, adjacent_position)
result = self._solve_r(adjacent_position, end_position)
if result is True:
return True
self._game.draw_path_between(current_position, adjacent_position, undo=True)
return False

View file

@ -15,16 +15,16 @@ class Tests(unittest.TestCase):
"""
cases = [
{
"number_of_cell_rows": 6,
"number_of_cells_per_row": 9,
"height": 6,
"width": 9,
},
{
"number_of_cell_rows": 3,
"number_of_cells_per_row": 12,
"height": 3,
"width": 12,
},
{
"number_of_cell_rows": 4,
"number_of_cells_per_row": 4,
"height": 4,
"width": 4,
},
]
@ -32,20 +32,20 @@ class Tests(unittest.TestCase):
m = maze.Maze(
0,
0,
case["number_of_cell_rows"],
case["number_of_cells_per_row"],
case["height"],
case["width"],
2,
2,
None,
None,
)
self.assertEqual(
len(m._cells),
case["number_of_cell_rows"],
len(m._cell_grid),
case["height"],
)
self.assertEqual(
len(m._cells[0]),
case["number_of_cells_per_row"],
len(m._cell_grid[0]),
case["width"],
)
def test_break_entrance_and_exit(self):
@ -65,9 +65,9 @@ class Tests(unittest.TestCase):
None,
None,
)
self.assertFalse(m._cells[0][0].wall_exists(CellWallLabels.TOP))
self.assertFalse(m._cell_grid[0][0].wall_exists(CellWallLabels.TOP))
self.assertFalse(
m._cells[number_of_cell_rows - 1]
m._cell_grid[number_of_cell_rows - 1]
[number_of_cells_per_row - 1].wall_exists(CellWallLabels.BOTTOM)
)
@ -112,13 +112,13 @@ class Tests(unittest.TestCase):
def test_maze_position_equality(self):
cases = [
{
"m1": maze.MazePosition(i=1, j=3, max_i=10, max_j=100),
"m2": maze.MazePosition(i=1, j=3, max_i=10, max_j=100),
"m1": maze.MazePosition(i=1, j=3, last_i=10, last_j=100),
"m2": maze.MazePosition(i=1, j=3, last_i=10, last_j=100),
"expected": True,
},
{
"m1": maze.MazePosition(i=1, j=3, max_i=10, max_j=100),
"m2": maze.MazePosition(i=100, j=30, max_i=200, max_j=100),
"m1": maze.MazePosition(i=1, j=3, last_i=10, last_j=100),
"m2": maze.MazePosition(i=100, j=30, last_i=200, last_j=100),
"expected": False,
}
]
@ -130,42 +130,42 @@ class Tests(unittest.TestCase):
def test_maze_position_adjacent_positition(self):
cases = [
{
"position": maze.MazePosition(i=3, j=4, max_i=10, max_j=10),
"position": maze.MazePosition(i=3, j=4, last_i=10, last_j=10),
"direction": maze.MazeDirection.ABOVE,
"expected": maze.MazePosition(i=2, j=4, max_i=10, max_j=10),
"expected": maze.MazePosition(i=2, j=4, last_i=10, last_j=10),
},
{
"position": maze.MazePosition(i=9, j=4, max_i=10, max_j=10),
"position": maze.MazePosition(i=9, j=4, last_i=10, last_j=10),
"direction": maze.MazeDirection.BELOW,
"expected": maze.MazePosition(i=10, j=4, max_i=10, max_j=10),
"expected": maze.MazePosition(i=10, j=4, last_i=10, last_j=10),
},
{
"position": maze.MazePosition(i=1, j=1, max_i=10, max_j=10),
"position": maze.MazePosition(i=1, j=1, last_i=10, last_j=10),
"direction": maze.MazeDirection.LEFT,
"expected": maze.MazePosition(i=1, j=0, max_i=10, max_j=10),
"expected": maze.MazePosition(i=1, j=0, last_i=10, last_j=10),
},
{
"position": maze.MazePosition(i=3, j=9, max_i=10, max_j=10),
"position": maze.MazePosition(i=3, j=9, last_i=10, last_j=10),
"direction": maze.MazeDirection.RIGHT,
"expected": maze.MazePosition(i=3, j=10, max_i=10, max_j=10),
"expected": maze.MazePosition(i=3, j=10, last_i=10, last_j=10),
},
{
"position": maze.MazePosition(i=0, j=4, max_i=10, max_j=10),
"position": maze.MazePosition(i=0, j=4, last_i=10, last_j=10),
"direction": maze.MazeDirection.ABOVE,
"expected": None,
},
{
"position": maze.MazePosition(i=10, j=4, max_i=10, max_j=10),
"position": maze.MazePosition(i=10, j=4, last_i=10, last_j=10),
"direction": maze.MazeDirection.BELOW,
"expected": None,
},
{
"position": maze.MazePosition(i=1, j=0, max_i=10, max_j=10),
"position": maze.MazePosition(i=1, j=0, last_i=10, last_j=10),
"direction": maze.MazeDirection.LEFT,
"expected": None,
},
{
"position": maze.MazePosition(i=3, j=10, max_i=10, max_j=10),
"position": maze.MazePosition(i=3, j=10, last_i=10, last_j=10),
"direction": maze.MazeDirection.RIGHT,
"expected": None,
},