checkpoint: Add side panel

Add a side panel to allow users to interact with the app.
The generate button now generates the maze.
This commit is contained in:
Dan Anglin 2024-02-17 20:06:09 +00:00
parent ed5f0033fa
commit 4c7e50ec39
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
6 changed files with 161 additions and 75 deletions

86
app.py Normal file
View file

@ -0,0 +1,86 @@
from tkinter import ttk, Tk, Canvas, 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
# 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.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.canvas = Canvas(self)
self.canvas.config(
bg=self.background_colour,
width=800,
height=800
)
self.canvas.grid(column=1, row=0)
self.graphics = Graphics(self.canvas)
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.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",
)
solve.pack()
return frame

32
cell.py
View file

@ -1,6 +1,7 @@
from typing import Dict from typing import Dict
from enum import Enum from enum import Enum
from graphics import Window, Point, Line from tkinter import Canvas
from graphics import Graphics, Point, Line
import errors import errors
@ -21,17 +22,17 @@ class CellWall:
a Cell's wall. a Cell's wall.
""" """
def __init__(self, line: Line, window: Window) -> None: def __init__(self, line: Line, graphics: Graphics) -> None:
self.exists = True self.exists = True
self.line = line self.line = line
self._window = window self._graphics = graphics
def draw(self): def draw(self):
fill_colour = self._window.cell_grid_colour fill_colour = "black"
if not self.exists: if not self.exists:
fill_colour = self._window.background_colour fill_colour = "white"
self._window.draw_line(self.line, fill_colour=fill_colour) self._graphics.draw_line(self.line, fill_colour=fill_colour)
class Cell: class Cell:
@ -43,7 +44,7 @@ class Cell:
self, self,
x1: int, y1: int, x1: int, y1: int,
x2: int, y2: int, x2: int, y2: int,
window: Window = None, graphics: Graphics = None,
) -> None: ) -> None:
# Validation # Validation
if (x2 < x1) or (y2 < y1): if (x2 < x1) or (y2 < y1):
@ -62,10 +63,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, graphics),
CellWallLabels.BOTTOM: CellWall(bottom_wall, window), CellWallLabels.BOTTOM: CellWall(bottom_wall, graphics),
CellWallLabels.LEFT: CellWall(left_wall, window), CellWallLabels.LEFT: CellWall(left_wall, graphics),
CellWallLabels.RIGHT: CellWall(right_wall, window), CellWallLabels.RIGHT: CellWall(right_wall, graphics),
} }
# Calculate the cell's central point # Calculate the cell's central point
@ -73,8 +74,7 @@ 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._graphics = graphics
self._window = window
self._visited: Dict[str, bool] = { self._visited: Dict[str, bool] = {
"generator": False, "generator": False,
@ -120,7 +120,7 @@ class Cell:
""" """
draw draws the cell onto the canvas draw draws the cell onto the canvas
""" """
if not self._window: if not self._graphics:
return return
for label in CellWallLabels: for label in CellWallLabels:
@ -131,14 +131,14 @@ class Cell:
draw_move draws a path between the centre of this cell and draw_move draws a path between the centre of this cell and
the centre of the given cell. the centre of the given cell.
""" """
if not self._window: if not self._graphics:
return return
fill_colour = "red" fill_colour = "red"
if undo: if undo:
fill_colour = "grey" fill_colour = "grey"
line = Line(self.centre(), to_cell.centre()) line = Line(self.centre(), to_cell.centre())
self._window.draw_line(line, fill_colour) self._graphics.draw_line(line, fill_colour)
def was_visited_by(self, visitor: str) -> bool: def was_visited_by(self, visitor: str) -> bool:
""" """

View file

@ -1,4 +1,4 @@
from tkinter import Tk, BOTH, Canvas from tkinter import Tk, Canvas
class Point: class Point:
@ -20,16 +20,26 @@ class Line:
self.point_a = point_a self.point_a = point_a
self.point_b = point_b self.point_b = point_b
def draw(self, canvas: Canvas, fill_colour: str) -> None:
class Graphics:
def __init__(self, canvas: Canvas) -> None:
self._canvas = canvas
def draw_line(
self,
line: Line,
fill_colour: str = "black",
width: int = 2
) -> None:
""" """
draw draws a line on a given canvas. draws a line onto the canvas.
""" """
canvas.create_line( self._canvas.create_line(
self.point_a.x, self.point_a.y, line.point_a.x, line.point_a.y,
self.point_b.x, self.point_b.y, line.point_b.x, line.point_b.y,
fill=fill_colour, width=2 fill=fill_colour,
width=width,
) )
canvas.pack(fill=BOTH, expand=1)
class Window: class Window:
@ -66,16 +76,3 @@ class Window:
""" """
self._root.update_idletasks() self._root.update_idletasks()
self._root.update() self._root.update()
def mainloop(self) -> None:
"""
mainloop calls the root widget's mainloop method to
ensure that the window remains visible on the screen.
"""
self._root.mainloop()
def draw_line(self, line: Line, fill_colour: str = "black") -> None:
"""
draw_line draws a line on the canvas.
"""
line.draw(self._canvas, fill_colour)

35
main.py
View file

@ -1,29 +1,32 @@
from graphics import Window from graphics import Window
from maze import Maze from maze import Maze
from solver import Solver from solver import Solver
from app import App
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

@ -2,7 +2,7 @@ from typing import List
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 Graphics
from cell import Cell, CellWallLabels from cell import Cell, CellWallLabels
@ -93,7 +93,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 +102,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 +112,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 +153,7 @@ 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 self._graphics,
) )
cells[j] = cell cells[j] = cell
if j == self._width - 1: if j == self._width - 1:
@ -162,9 +163,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 +184,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 +220,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 +273,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)
@ -285,8 +284,8 @@ class Maze:
""" """
self._cell_grid[i][j].draw() self._cell_grid[i][j].draw()
self._window.redraw() #self._canvas.redraw()
sleep(0.05) #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:
""" """
@ -294,8 +293,8 @@ class Maze:
""" """
current_cell.draw_move(next_cell, undo) current_cell.draw_move(next_cell, undo)
self._window.redraw() #self._canvas.redraw()
sleep(0.05) #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]