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 enum import Enum
from graphics import Window, Point, Line
from line import Point, Line
import errors
@ -21,17 +21,48 @@ class CellWall:
a Cell's wall.
"""
def __init__(self, line: Line, window: Window) -> None:
self.exists = True
self.line = line
self._window = window
def __init__(self, line: Line) -> None:
self._exists = True
self._line = line
self._line_colour = "black"
def draw(self):
fill_colour = self._window.cell_grid_colour
if not self.exists:
fill_colour = self._window.background_colour
def configure(self, build: bool) -> None:
"""
builds or destroys the cell wall.
"""
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:
@ -43,7 +74,6 @@ class Cell:
self,
x1: int, y1: int,
x2: int, y2: int,
window: Window = None,
) -> None:
# Validation
if (x2 < x1) or (y2 < y1):
@ -62,10 +92,10 @@ class Cell:
right_wall = Line(Point(x2, y1), Point(x2, y2))
self._walls: Dict[CellWallLabels, CellWall] = {
CellWallLabels.TOP: CellWall(top_wall, window),
CellWallLabels.BOTTOM: CellWall(bottom_wall, window),
CellWallLabels.LEFT: CellWall(left_wall, window),
CellWallLabels.RIGHT: CellWall(right_wall, window),
CellWallLabels.TOP: CellWall(top_wall),
CellWallLabels.BOTTOM: CellWall(bottom_wall),
CellWallLabels.LEFT: CellWall(left_wall),
CellWallLabels.RIGHT: CellWall(right_wall),
}
# Calculate the cell's central point
@ -73,9 +103,6 @@ class Cell:
centre_y = y1 + ((y2 - y1) / 2)
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] = {
"generator": False,
"solver": False,
@ -92,13 +119,13 @@ class Cell:
configure_walls configures the existence of the Cell's walls.
"""
if top is not None:
self._walls[CellWallLabels.TOP].exists = top
self._walls[CellWallLabels.TOP].configure(top)
if bottom is not None:
self._walls[CellWallLabels.BOTTOM].exists = bottom
self._walls[CellWallLabels.BOTTOM].configure(bottom)
if left is not None:
self._walls[CellWallLabels.LEFT].exists = left
self._walls[CellWallLabels.LEFT].configure(left)
if right is not None:
self._walls[CellWallLabels.RIGHT].exists = right
self._walls[CellWallLabels.RIGHT].configure(right)
def centre(self) -> Point:
"""
@ -114,31 +141,7 @@ class Cell:
raise TypeError(
"The argument does not appear to be a valid cell wall."
)
return self._walls[wall].exists
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)
return self._walls[wall].wall_up()
def was_visited_by(self, visitor: str) -> bool:
"""
@ -157,3 +160,6 @@ class Cell:
if visitor not in ("solver", "generator"):
raise ValueError(f"This is an unknown visitor ({visitor})")
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:
"""
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
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,
class Graphics(Canvas):
def __init__(self, container, background="white", width=800, height=800) -> None:
super().__init__(container)
self.config(
bg=background,
width=width,
height=height,
)
self._canvas.pack()
def redraw(self) -> None:
def _redraw(self) -> None:
"""
redraw redraws all the graphics in the window.
"""
self._root.update_idletasks()
self._root.update()
self.update_idletasks()
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
ensure that the window remains visible on the screen.
draws a line onto the canvas.
"""
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 maze import Maze
from solver import Solver
from app import App
def main():
window = Window(800, 800)
app = App()
app.mainloop()
#window = Window(800, 800)
game = Maze(
x_position=10,
y_position=10,
height=19,
width=19,
cell_height=40,
cell_width=40,
window=window
)
#game = Maze(
# x_position=10,
# y_position=10,
# height=19,
# width=19,
# cell_height=40,
# cell_width=40,
# window=window
#)
solver = Solver(game)
#solver = Solver(game)
if solver.solve(solver.solve_with_bfs_r, True):
print("Maze solved successfully :)")
else:
print("I'm unable to solve the maze :(")
#if solver.solve(solver.solve_with_bfs_r, True):
# print("Maze solved successfully :)")
#else:
# print("I'm unable to solve the maze :(")
window.mainloop()
#window.mainloop()
if __name__ == "__main__":

43
maze.py
View file

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