refactor: add MazeDirections and MazePositions

- Created a new enum type called MazeDirections to represent the
  directions you can make in the maze.
- Created a class called MazePosition to represent the position on the
  maze grid. It has the functionality to calculate and return adjacent
  positions.
- Used the above two additions to refactor the _break_walls_r method and
  make it a bit easier to read.
This commit is contained in:
Dan Anglin 2024-02-15 14:58:51 +00:00
parent a8ef152847
commit 32696f311d
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
2 changed files with 178 additions and 45 deletions

141
maze.py
View file

@ -1,10 +1,79 @@
from typing import List from typing import List
from time import sleep from time import sleep
import random import random
from enum import Enum
from graphics import Window from graphics import Window
from cell import Cell from cell import Cell
class MazeDirections(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: MazeDirections
) -> 'MazePosition':
if direction not in MazeDirections:
raise TypeError(
"The argument does not appear to be a valid MazeDirection"
)
if direction is MazeDirections.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 MazeDirections.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 MazeDirections.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 MazeDirections.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: class Maze:
""" """
Maze represents a two-dimensional grid of Cells. Maze represents a two-dimensional grid of Cells.
@ -75,9 +144,9 @@ class Maze:
draws all the cells on the maze with a short pause between each cell draws all the cells on the maze with a short pause between each cell
for animation purposes. for animation purposes.
""" """
for y in range(self._num_cell_rows): for i in range(self._num_cell_rows):
for x in range(self._num_cells_per_row): for j in range(self._num_cells_per_row):
self._draw_cell(y=y, x=x) self._draw_cell(i=i, j=j)
def _open_entrance_and_exit(self) -> None: def _open_entrance_and_exit(self) -> None:
""" """
@ -92,72 +161,70 @@ class Maze:
if self._window: if self._window:
self._draw_cell(0, 0) self._draw_cell(0, 0)
self._draw_cell( self._draw_cell(
y=self._num_cell_rows-1, i=self._num_cell_rows-1,
x=self._num_cells_per_row-1 j=self._num_cells_per_row-1
) )
def _break_walls_r(self, y: int, x: int) -> None: def _break_walls_r(self, i: int, j: int) -> None:
""" """
_break_walls_r generates a random maze by traversing through the _break_walls_r generates a random maze by traversing through the
cells and randomly knocking down the walls to create the maze's paths. cells and randomly knocking down the walls to create the maze's paths.
""" """
current_cell = self._cells[y][x] current_position = MazePosition(
i=i,
j=j,
max_i=self._num_cell_rows-1,
max_j=self._num_cells_per_row-1
)
current_cell = self._cells[i][j]
current_cell.visited_by_maze_generator = True current_cell.visited_by_maze_generator = True
above, below, left, right = "above", "below", "left", "right"
while True: while True:
adjacent_cells = { possible_directions: List[MazeDirections] = []
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(): for direction in MazeDirections:
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): adjacent_position = current_position.get_adjacent_position(
direction)
if adjacent_position is None:
continue continue
if self._cells[value[0]][value[1]].visited_by_maze_generator: adjacent_cell = self._cells[adjacent_position.i][adjacent_position.j]
if adjacent_cell.visited_by_maze_generator:
continue continue
possible_directions.append(direction)
to_visit.append(k) if len(possible_directions) == 0:
if len(to_visit) == 0:
if self._window: if self._window:
self._draw_cell(y=y, x=x) self._draw_cell(i=i, j=j)
break break
next_direction = random.choice(to_visit) chosen_direction = random.choice(possible_directions)
next_cell = self._cells[adjacent_cells[next_direction] next_position = current_position.get_adjacent_position(
[0]][adjacent_cells[next_direction][1]] chosen_direction)
next_cell = self._cells[next_position.i][next_position.j]
if next_direction is above: if chosen_direction is MazeDirections.ABOVE:
current_cell.configure_walls(top=False) current_cell.configure_walls(top=False)
next_cell.configure_walls(bottom=False) next_cell.configure_walls(bottom=False)
elif next_direction is below: elif chosen_direction is MazeDirections.BELOW:
current_cell.configure_walls(bottom=False) current_cell.configure_walls(bottom=False)
next_cell.configure_walls(top=False) next_cell.configure_walls(top=False)
elif next_direction is left: elif chosen_direction is MazeDirections.LEFT:
current_cell.configure_walls(left=False) current_cell.configure_walls(left=False)
next_cell.configure_walls(right=False) next_cell.configure_walls(right=False)
elif next_direction is right: elif chosen_direction is MazeDirections.RIGHT:
current_cell.configure_walls(right=False) current_cell.configure_walls(right=False)
next_cell.configure_walls(left=False) next_cell.configure_walls(left=False)
current_cell.draw()
if self._window: if self._window:
self._draw_cell(y=y, x=x) self._draw_cell(i=i, j=j)
self._break_walls_r( self._break_walls_r(i=next_position.i, j=next_position.j)
y=adjacent_cells[next_direction][0],
x=adjacent_cells[next_direction][1],
)
def _draw_cell(self, y: int, x: 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.
""" """
self._cells[y][x].draw() self._cells[i][j].draw()
self._window.redraw() self._window.redraw()
sleep(0.05) sleep(0.05)

View file

@ -1,6 +1,6 @@
import unittest import unittest
from cell import Cell, CellWallLabel from cell import Cell, CellWallLabel
from maze import Maze import maze
import errors import errors
@ -25,7 +25,7 @@ class Tests(unittest.TestCase):
] ]
for case in cases: for case in cases:
maze = Maze( m = maze.Maze(
0, 0,
0, 0,
case["number_of_cell_rows"], case["number_of_cell_rows"],
@ -37,11 +37,11 @@ class Tests(unittest.TestCase):
True, True,
) )
self.assertEqual( self.assertEqual(
len(maze._cells), len(m._cells),
case["number_of_cell_rows"], case["number_of_cell_rows"],
) )
self.assertEqual( self.assertEqual(
len(maze._cells[0]), len(m._cells[0]),
case["number_of_cells_per_row"], case["number_of_cells_per_row"],
) )
@ -52,7 +52,7 @@ class Tests(unittest.TestCase):
""" """
number_of_cell_rows = 5 number_of_cell_rows = 5
number_of_cells_per_row = 20 number_of_cells_per_row = 20
maze = Maze( m = maze.Maze(
0, 0,
0, 0,
number_of_cell_rows, number_of_cell_rows,
@ -63,9 +63,9 @@ class Tests(unittest.TestCase):
None, None,
True, True,
) )
self.assertFalse(maze._cells[0][0].wall_exists(CellWallLabel.TOP)) self.assertFalse(m._cells[0][0].wall_exists(CellWallLabel.TOP))
self.assertFalse( self.assertFalse(
maze._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(CellWallLabel.BOTTOM)
) )
@ -90,7 +90,7 @@ class Tests(unittest.TestCase):
def test_cell_too_small_exception(self): def test_cell_too_small_exception(self):
""" """
test_cell_too_small_exception tests the excpetion for when an attempt test_cell_too_small_exception tests the exception for when an attempt
is made to create a Cell that's too small. is made to create a Cell that's too small.
""" """
cases = [ cases = [
@ -107,6 +107,72 @@ class Tests(unittest.TestCase):
y2=case["y2"] y2=case["y2"]
) )
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),
"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),
"expected": False,
}
]
for case in cases:
result = case["m1"] == case["m2"]
self.assertEqual(result, case["expected"])
def test_maze_position_adjacent_positition(self):
cases = [
{
"position": maze.MazePosition(i=3, j=4, max_i=10, max_j=10),
"direction": maze.MazeDirections.ABOVE,
"expected": maze.MazePosition(i=2, j=4, max_i=10, max_j=10),
},
{
"position": maze.MazePosition(i=9, j=4, max_i=10, max_j=10),
"direction": maze.MazeDirections.BELOW,
"expected": maze.MazePosition(i=10, j=4, max_i=10, max_j=10),
},
{
"position": maze.MazePosition(i=1, j=1, max_i=10, max_j=10),
"direction": maze.MazeDirections.LEFT,
"expected": maze.MazePosition(i=1, j=0, max_i=10, max_j=10),
},
{
"position": maze.MazePosition(i=3, j=9, max_i=10, max_j=10),
"direction": maze.MazeDirections.RIGHT,
"expected": maze.MazePosition(i=3, j=10, max_i=10, max_j=10),
},
{
"position": maze.MazePosition(i=0, j=4, max_i=10, max_j=10),
"direction": maze.MazeDirections.ABOVE,
"expected": None,
},
{
"position": maze.MazePosition(i=10, j=4, max_i=10, max_j=10),
"direction": maze.MazeDirections.BELOW,
"expected": None,
},
{
"position": maze.MazePosition(i=1, j=0, max_i=10, max_j=10),
"direction": maze.MazeDirections.LEFT,
"expected": None,
},
{
"position": maze.MazePosition(i=3, j=10, max_i=10, max_j=10),
"direction": maze.MazeDirections.RIGHT,
"expected": None,
},
]
for case in cases:
result = case["position"].get_adjacent_position(case["direction"])
self.assertEqual(result, case["expected"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()