Compare commits

...

3 commits

Author SHA1 Message Date
9bb23b6c6d
checkpoint: Graphics now draws paths
Move the path drawing functionality to the Graphics class. The Cell
class no longer needs to reference an instance of the Graphics class.
2024-02-18 02:21:09 +00:00
429990d12e
checkpoint: trying to move all drawing functions to Graphics 2024-02-17 21:25:03 +00:00
4c7e50ec39
checkpoint: Add side panel
Add a side panel to allow users to interact with the app.
The generate button now generates the maze.
2024-02-17 20:06:09 +00:00
7 changed files with 252 additions and 158 deletions

90
app.py Normal file
View file

@ -0,0 +1,90 @@
from tkinter import ttk, Tk, StringVar, BooleanVar
from maze import Maze
from solver import Solver
from graphics import Graphics
class App(Tk):
def __init__(self):
super().__init__()
self.title("Maze Solver")
# Position the window to the centre of the screen
height = 800
width = 1000
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
centre_x = int(screen_width/2 - width/2)
centre_y = int(screen_height/2 - height/2)
self.geometry(f"{width}x{height}+{centre_x}+{centre_y}")
# Styling
self.style = ttk.Style()
self.style.theme_use("clam")
self.background_colour = "white"
self.cell_grid_colour = "black"
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
self.graphics = Graphics(self)
self.graphics.grid(column=1, row=0)
self.maze = Maze(
x_position=10,
y_position=10,
height=19,
width=19,
cell_height=40,
cell_width=40,
graphics=self.graphics,
)
self.solver = Solver(self.maze)
self.algorithm_map = {
"Breadth-First Search": self.solver.solve_with_dfs_r,
"Depth-First Search": self.solver.solve_with_bfs_r,
}
self.side_panel = self._create_side_panel()
self.side_panel.grid(column=0, row=0)
def _create_side_panel(self):
frame = ttk.Frame(self)
label = ttk.Label(frame)
label.config(text="Maze Solver", font=(None, 20))
label.pack()
generate = ttk.Button(
frame,
text="Generate maze",
command=self.maze.generate,
)
generate.pack()
algorithm = StringVar()
combobox = ttk.Combobox(frame, textvariable=algorithm)
algorithm_label = ttk.Label(frame, text="Searching algorithm:")
algorithm_label.pack()
combobox["values"] = ("Breadth-First Search", "Depth-First Search")
combobox["state"] = "readonly"
combobox.pack()
randomness = BooleanVar()
enable_randomness = ttk.Checkbutton(frame)
enable_randomness.config(
text="Enable Randomness",
variable=randomness,
onvalue=True,
offvalue=False,
)
enable_randomness.pack()
solve = ttk.Button(
frame,
text="Solve the maze",
command=lambda: self.solver.solve(
solve_method=self.algorithm_map[algorithm.get()],
enable_random_direction=randomness.get(),
),
)
solve.pack()
return frame

100
cell.py
View file

@ -1,6 +1,6 @@
from typing import Dict from typing import Dict
from enum import Enum from enum import Enum
from graphics import Window, Point, Line from line import Point, Line
import errors import errors
@ -21,17 +21,48 @@ class CellWall:
a Cell's wall. a Cell's wall.
""" """
def __init__(self, line: Line, window: Window) -> None: def __init__(self, line: Line) -> None:
self.exists = True self._exists = True
self.line = line self._line = line
self._window = window self._line_colour = "black"
def draw(self): def configure(self, build: bool) -> None:
fill_colour = self._window.cell_grid_colour """
if not self.exists: builds or destroys the cell wall.
fill_colour = self._window.background_colour """
if build:
self._build_wall()
else:
self._destroy_wall()
self._window.draw_line(self.line, fill_colour=fill_colour) def _build_wall(self) -> None:
"""
builds the cell wall
"""
self._exists = True
self._line_colour = "black"
def _destroy_wall(self) -> None:
"""
destroys the cell wall
"""
self._exists = False
self._line_colour = "white"
def wall_up(self) -> bool:
"""
returns true if the cell wall is up.
"""
return self._exists
def get_line(self) -> Line:
return self._line
def get_line_colour(self) -> str:
"""
returns the line colour of the wall.
"""
return self._line_colour
class Cell: class Cell:
@ -43,7 +74,6 @@ class Cell:
self, self,
x1: int, y1: int, x1: int, y1: int,
x2: int, y2: int, x2: int, y2: int,
window: Window = None,
) -> None: ) -> None:
# Validation # Validation
if (x2 < x1) or (y2 < y1): if (x2 < x1) or (y2 < y1):
@ -62,10 +92,10 @@ class Cell:
right_wall = Line(Point(x2, y1), Point(x2, y2)) right_wall = Line(Point(x2, y1), Point(x2, y2))
self._walls: Dict[CellWallLabels, CellWall] = { self._walls: Dict[CellWallLabels, CellWall] = {
CellWallLabels.TOP: CellWall(top_wall, window), CellWallLabels.TOP: CellWall(top_wall),
CellWallLabels.BOTTOM: CellWall(bottom_wall, window), CellWallLabels.BOTTOM: CellWall(bottom_wall),
CellWallLabels.LEFT: CellWall(left_wall, window), CellWallLabels.LEFT: CellWall(left_wall),
CellWallLabels.RIGHT: CellWall(right_wall, window), CellWallLabels.RIGHT: CellWall(right_wall),
} }
# Calculate the cell's central point # Calculate the cell's central point
@ -73,9 +103,6 @@ class Cell:
centre_y = y1 + ((y2 - y1) / 2) centre_y = y1 + ((y2 - y1) / 2)
self._centre = Point(centre_x, centre_y) self._centre = Point(centre_x, centre_y)
# A reference to the root Window class for drawing purposes.
self._window = window
self._visited: Dict[str, bool] = { self._visited: Dict[str, bool] = {
"generator": False, "generator": False,
"solver": False, "solver": False,
@ -92,13 +119,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[CellWallLabels.TOP].exists = top self._walls[CellWallLabels.TOP].configure(top)
if bottom is not None: if bottom is not None:
self._walls[CellWallLabels.BOTTOM].exists = bottom self._walls[CellWallLabels.BOTTOM].configure(bottom)
if left is not None: if left is not None:
self._walls[CellWallLabels.LEFT].exists = left self._walls[CellWallLabels.LEFT].configure(left)
if right is not None: if right is not None:
self._walls[CellWallLabels.RIGHT].exists = right self._walls[CellWallLabels.RIGHT].configure(right)
def centre(self) -> Point: def centre(self) -> Point:
""" """
@ -114,31 +141,7 @@ class Cell:
raise TypeError( raise TypeError(
"The argument does not appear to be a valid cell wall." "The argument does not appear to be a valid cell wall."
) )
return self._walls[wall].exists return self._walls[wall].wall_up()
def draw(self) -> None:
"""
draw draws the cell onto the canvas
"""
if not self._window:
return
for label in CellWallLabels:
self._walls[label].draw()
def draw_move(self, to_cell: 'Cell', undo: bool = False) -> None:
"""
draw_move draws a path between the centre of this cell and
the centre of the given cell.
"""
if not self._window:
return
fill_colour = "red"
if undo:
fill_colour = "grey"
line = Line(self.centre(), to_cell.centre())
self._window.draw_line(line, fill_colour)
def was_visited_by(self, visitor: str) -> bool: def was_visited_by(self, visitor: str) -> bool:
""" """
@ -157,3 +160,6 @@ class Cell:
if visitor not in ("solver", "generator"): if visitor not in ("solver", "generator"):
raise ValueError(f"This is an unknown visitor ({visitor})") raise ValueError(f"This is an unknown visitor ({visitor})")
self._visited[visitor] = True self._visited[visitor] = True
def get_walls(self) -> Dict[CellWallLabels, CellWall]:
return self._walls

View file

@ -1,81 +1,67 @@
from tkinter import Tk, BOTH, Canvas from typing import Dict
from time import sleep
from tkinter import Canvas
from line import Line, Point
from cell import CellWallLabels, CellWall
class Point: class Graphics(Canvas):
""" def __init__(self, container, background="white", width=800, height=800) -> None:
Point represents the position of a point. super().__init__(container)
""" self.config(
bg=background,
def __init__(self, x: int, y: int):
self.x = x
self.y = y
class Line:
"""
Line represents a graphical line.
"""
def __init__(self, point_a: Point, point_b: Point) -> None:
self.point_a = point_a
self.point_b = point_b
def draw(self, canvas: Canvas, fill_colour: str) -> None:
"""
draw draws a line on a given canvas.
"""
canvas.create_line(
self.point_a.x, self.point_a.y,
self.point_b.x, self.point_b.y,
fill=fill_colour, width=2
)
canvas.pack(fill=BOTH, expand=1)
class Window:
"""
Window is a Graphical window.
"""
def __init__(self, width: int, height: int) -> None:
self._root = Tk()
self._root.title("Maze Solver")
# Position the window to the centre of the screen
screen_width = self._root.winfo_screenwidth()
screen_height = self._root.winfo_screenheight()
centre_x = int(screen_width/2 - width/2)
centre_y = int(screen_height/2 - height/2)
self._root.geometry(f"{width}x{height}+{centre_x}+{centre_y}")
# Styling
self.background_colour = "white"
self.cell_grid_colour = "black"
self._canvas = Canvas(self._root)
self._canvas.config(
bg=self.background_colour,
height=height,
width=width, width=width,
height=height,
) )
self._canvas.pack()
def redraw(self) -> None: def _redraw(self) -> None:
""" """
redraw redraws all the graphics in the window. redraw redraws all the graphics in the window.
""" """
self._root.update_idletasks() self.update_idletasks()
self._root.update() self.update()
sleep(0.05)
def mainloop(self) -> None: def _draw_line(
self,
line: Line,
fill_colour: str = "black",
width: int = 2
) -> None:
""" """
mainloop calls the root widget's mainloop method to draws a line onto the canvas.
ensure that the window remains visible on the screen.
""" """
self._root.mainloop() self.create_line(
line.point_a.x, line.point_a.y,
line.point_b.x, line.point_b.y,
fill=fill_colour,
width=width,
)
def draw_line(self, line: Line, fill_colour: str = "black") -> None: def draw_cell_walls(self, walls: Dict[CellWallLabels, CellWall]) -> None:
""" """
draw_line draws a line on the canvas. draws the walls of a cell onto the canvas.
""" """
line.draw(self._canvas, fill_colour) for label in CellWallLabels:
self._draw_line(
line=walls[label].get_line(),
fill_colour=walls[label].get_line_colour(),
)
self._redraw()
def draw_path(
self,
from_cell_centre: Point,
to_cell_centre: Point,
undo: bool = False
) -> None:
"""
draws a path between the centre of this cell and
the centre of the given cell.
"""
line = Line(from_cell_centre, to_cell_centre)
fill_colour = "red"
if undo:
fill_colour = "grey"
self._draw_line(line, fill_colour)
self._redraw()

18
line.py Normal file
View file

@ -0,0 +1,18 @@
class Point:
"""
Point represents the position of a point.
"""
def __init__(self, x: int, y: int):
self.x = x
self.y = y
class Line:
"""
Line represents a graphical line.
"""
def __init__(self, point_a: Point, point_b: Point) -> None:
self.point_a = point_a
self.point_b = point_b

38
main.py
View file

@ -1,29 +1,29 @@
from graphics import Window from app import App
from maze import Maze
from solver import Solver
def main(): def main():
window = Window(800, 800) app = App()
app.mainloop()
#window = Window(800, 800)
game = Maze( #game = Maze(
x_position=10, # x_position=10,
y_position=10, # y_position=10,
height=19, # height=19,
width=19, # width=19,
cell_height=40, # cell_height=40,
cell_width=40, # cell_width=40,
window=window # window=window
) #)
solver = Solver(game) #solver = Solver(game)
if solver.solve(solver.solve_with_bfs_r, True): #if solver.solve(solver.solve_with_bfs_r, True):
print("Maze solved successfully :)") # print("Maze solved successfully :)")
else: #else:
print("I'm unable to solve the maze :(") # print("I'm unable to solve the maze :(")
window.mainloop() #window.mainloop()
if __name__ == "__main__": if __name__ == "__main__":

43
maze.py
View file

@ -1,8 +1,7 @@
from typing import List from typing import List
from time import sleep
import random import random
from enum import Enum from enum import Enum
from graphics import Window from graphics import Graphics
from cell import Cell, CellWallLabels from cell import Cell, CellWallLabels
@ -93,7 +92,7 @@ class Maze:
width: int, width: int,
cell_height: int, cell_height: int,
cell_width: int, cell_width: int,
window: Window = None, graphics: Graphics = None,
seed=None, seed=None,
) -> None: ) -> None:
self._x_position = x_position self._x_position = x_position
@ -102,7 +101,7 @@ class Maze:
self._width = width self._width = width
self._cell_height = cell_height self._cell_height = cell_height
self._cell_width = cell_width self._cell_width = cell_width
self._window = window self._graphics = graphics
self._generator = "generator" self._generator = "generator"
# initialise the random number generator # initialise the random number generator
@ -112,18 +111,19 @@ class Maze:
self._cell_grid: List[List[Cell]] = [] self._cell_grid: List[List[Cell]] = []
self._create_cell_grid() self._create_cell_grid()
# Open up the maze's entrance and exit. def generate(self):
self._open_entrance_and_exit() """
randomly generates a new maze.
"""
start_position = MazePosition( self._draw_cell_grid()
self._open_entrance_and_exit()
self._break_walls_r(MazePosition(
i=0, i=0,
j=0, j=0,
last_i=self._height-1, last_i=self._height-1,
last_j=self._width-1, last_j=self._width-1,
) ))
# Generate the maze.
self._break_walls_r(start_position)
def get_last_i(self) -> int: def get_last_i(self) -> int:
"returns the last position of the Maze's outer list." "returns the last position of the Maze's outer list."
@ -152,7 +152,6 @@ class Maze:
cursor_y, cursor_y,
(cursor_x + self._cell_width), (cursor_x + self._cell_width),
(cursor_y + self._cell_height), (cursor_y + self._cell_height),
self._window
) )
cells[j] = cell cells[j] = cell
if j == self._width - 1: if j == self._width - 1:
@ -162,9 +161,6 @@ class Maze:
self._cell_grid[i] = cells self._cell_grid[i] = cells
cursor_y += self._cell_height cursor_y += self._cell_height
if self._window:
self._draw_cell_grid()
def _draw_cell_grid(self) -> None: def _draw_cell_grid(self) -> None:
""" """
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
@ -186,7 +182,7 @@ class Maze:
self._cell_grid[self._height-1][self._width - self._cell_grid[self._height-1][self._width -
1].configure_walls(bottom=False) 1].configure_walls(bottom=False)
if self._window: if self._graphics:
self._draw_cell(0, 0) self._draw_cell(0, 0)
self._draw_cell( self._draw_cell(
i=self._height-1, i=self._height-1,
@ -222,12 +218,13 @@ class Maze:
possible_directions.append(direction) possible_directions.append(direction)
if len(possible_directions) == 0: if len(possible_directions) == 0:
if self._window: if self._graphics:
self._draw_cell(i=current_position.i, j=current_position.j) self._draw_cell(i=current_position.i, j=current_position.j)
break break
chosen_direction = random.choice(possible_directions) chosen_direction = random.choice(possible_directions)
next_position = current_position.get_adjacent_position(chosen_direction) next_position = current_position.get_adjacent_position(
chosen_direction)
if chosen_direction is MazeDirection.ABOVE: if chosen_direction is MazeDirection.ABOVE:
self._configure_cell_walls( self._configure_cell_walls(
@ -274,7 +271,7 @@ class Maze:
left=False, left=False,
) )
if self._window: if self._graphics:
self._draw_cell(i=current_position.i, j=current_position.j) self._draw_cell(i=current_position.i, j=current_position.j)
self._break_walls_r(next_position) self._break_walls_r(next_position)
@ -284,18 +281,14 @@ class Maze:
draws the cells in an animated way. draws the cells in an animated way.
""" """
self._cell_grid[i][j].draw() self._graphics.draw_cell_walls(self._cell_grid[i][j].get_walls())
self._window.redraw()
sleep(0.05)
def _draw_path(self, current_cell: Cell, next_cell: Cell, undo: bool = False) -> None: def _draw_path(self, current_cell: Cell, next_cell: Cell, undo: bool = False) -> None:
""" """
draws a path between two cells in an animated way. draws a path between two cells in an animated way.
""" """
current_cell.draw_move(next_cell, undo) self._graphics.draw_path(current_cell.centre(), next_cell.centre(), undo)
self._window.redraw()
sleep(0.05)
def mark_cell_as_visited(self, i: int, j: int, visitor: str) -> None: def mark_cell_as_visited(self, i: int, j: int, visitor: str) -> None:
""" """

View file

@ -65,6 +65,7 @@ class Tests(unittest.TestCase):
None, None,
None, None,
) )
m.generate()
self.assertFalse(m._cell_grid[0][0].wall_exists(CellWallLabels.TOP)) self.assertFalse(m._cell_grid[0][0].wall_exists(CellWallLabels.TOP))
self.assertFalse( self.assertFalse(
m._cell_grid[number_of_cell_rows - 1] m._cell_grid[number_of_cell_rows - 1]