Compare commits

..

No commits in common. "9bb23b6c6d5dc41cf126de93671514a7b83e3c3a" and "ed5f0033fa01888a9bd29d316e99787ae93119c3" have entirely different histories.

7 changed files with 159 additions and 253 deletions

90
app.py
View file

@ -1,90 +0,0 @@
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 line import Point, Line from graphics import Window, Point, Line
import errors import errors
@ -21,48 +21,17 @@ class CellWall:
a Cell's wall. a Cell's wall.
""" """
def __init__(self, line: Line) -> None: def __init__(self, line: Line, window: Window) -> None:
self._exists = True self.exists = True
self._line = line self.line = line
self._line_colour = "black" self._window = window
def configure(self, build: bool) -> None: def draw(self):
""" fill_colour = self._window.cell_grid_colour
builds or destroys the cell wall. if not self.exists:
""" fill_colour = self._window.background_colour
if build:
self._build_wall()
else:
self._destroy_wall()
def _build_wall(self) -> None: self._window.draw_line(self.line, fill_colour=fill_colour)
"""
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:
@ -74,6 +43,7 @@ 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):
@ -92,10 +62,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), CellWallLabels.TOP: CellWall(top_wall, window),
CellWallLabels.BOTTOM: CellWall(bottom_wall), CellWallLabels.BOTTOM: CellWall(bottom_wall, window),
CellWallLabels.LEFT: CellWall(left_wall), CellWallLabels.LEFT: CellWall(left_wall, window),
CellWallLabels.RIGHT: CellWall(right_wall), CellWallLabels.RIGHT: CellWall(right_wall, window),
} }
# Calculate the cell's central point # Calculate the cell's central point
@ -103,6 +73,9 @@ 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,
@ -119,13 +92,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].configure(top) self._walls[CellWallLabels.TOP].exists = top
if bottom is not None: if bottom is not None:
self._walls[CellWallLabels.BOTTOM].configure(bottom) self._walls[CellWallLabels.BOTTOM].exists = bottom
if left is not None: if left is not None:
self._walls[CellWallLabels.LEFT].configure(left) self._walls[CellWallLabels.LEFT].exists = left
if right is not None: if right is not None:
self._walls[CellWallLabels.RIGHT].configure(right) self._walls[CellWallLabels.RIGHT].exists = right
def centre(self) -> Point: def centre(self) -> Point:
""" """
@ -141,7 +114,31 @@ 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].wall_up() 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)
def was_visited_by(self, visitor: str) -> bool: def was_visited_by(self, visitor: str) -> bool:
""" """
@ -160,6 +157,3 @@ 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,67 +1,81 @@
from typing import Dict from tkinter import Tk, BOTH, Canvas
from time import sleep
from tkinter import Canvas
from line import Line, Point
from cell import CellWallLabels, CellWall
class Graphics(Canvas): class Point:
def __init__(self, container, background="white", width=800, height=800) -> None: """
super().__init__(container) Point represents the position of a point.
self.config( """
bg=background,
width=width, def __init__(self, x: int, y: int):
height=height, 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)
def _redraw(self) -> None:
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,
)
self._canvas.pack()
def redraw(self) -> None:
""" """
redraw redraws all the graphics in the window. redraw redraws all the graphics in the window.
""" """
self.update_idletasks() self._root.update_idletasks()
self.update() self._root.update()
sleep(0.05)
def _draw_line( def mainloop(self) -> None:
self,
line: Line,
fill_colour: str = "black",
width: int = 2
) -> None:
""" """
draws a line onto the canvas. mainloop calls the root widget's mainloop method to
ensure that the window remains visible on the screen.
""" """
self.create_line( self._root.mainloop()
line.point_a.x, line.point_a.y,
line.point_b.x, line.point_b.y,
fill=fill_colour,
width=width,
)
def draw_cell_walls(self, walls: Dict[CellWallLabels, CellWall]) -> None: def draw_line(self, line: Line, fill_colour: str = "black") -> None:
""" """
draws the walls of a cell onto the canvas. draw_line draws a line on the canvas.
""" """
for label in CellWallLabels: line.draw(self._canvas, fill_colour)
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
View file

@ -1,18 +0,0 @@
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 app import App from graphics import Window
from maze import Maze
from solver import Solver
def main(): def main():
app = App() window = Window(800, 800)
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,7 +1,8 @@
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 Graphics from graphics import Window
from cell import Cell, CellWallLabels from cell import Cell, CellWallLabels
@ -92,7 +93,7 @@ class Maze:
width: int, width: int,
cell_height: int, cell_height: int,
cell_width: int, cell_width: int,
graphics: Graphics = None, window: Window = None,
seed=None, seed=None,
) -> None: ) -> None:
self._x_position = x_position self._x_position = x_position
@ -101,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._graphics = graphics self._window = window
self._generator = "generator" self._generator = "generator"
# initialise the random number generator # initialise the random number generator
@ -111,19 +112,18 @@ class Maze:
self._cell_grid: List[List[Cell]] = [] self._cell_grid: List[List[Cell]] = []
self._create_cell_grid() self._create_cell_grid()
def generate(self): # Open up the maze's entrance and exit.
"""
randomly generates a new maze.
"""
self._draw_cell_grid()
self._open_entrance_and_exit() self._open_entrance_and_exit()
self._break_walls_r(MazePosition(
start_position = 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,6 +152,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
) )
cells[j] = cell cells[j] = cell
if j == self._width - 1: if j == self._width - 1:
@ -161,6 +162,9 @@ 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
@ -182,7 +186,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._graphics: if self._window:
self._draw_cell(0, 0) self._draw_cell(0, 0)
self._draw_cell( self._draw_cell(
i=self._height-1, i=self._height-1,
@ -218,13 +222,12 @@ class Maze:
possible_directions.append(direction) possible_directions.append(direction)
if len(possible_directions) == 0: if len(possible_directions) == 0:
if self._graphics: if self._window:
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( next_position = current_position.get_adjacent_position(chosen_direction)
chosen_direction)
if chosen_direction is MazeDirection.ABOVE: if chosen_direction is MazeDirection.ABOVE:
self._configure_cell_walls( self._configure_cell_walls(
@ -271,7 +274,7 @@ class Maze:
left=False, left=False,
) )
if self._graphics: if self._window:
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)
@ -281,14 +284,18 @@ class Maze:
draws the cells in an animated way. draws the cells in an animated way.
""" """
self._graphics.draw_cell_walls(self._cell_grid[i][j].get_walls()) 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: 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.
""" """
self._graphics.draw_path(current_cell.centre(), next_cell.centre(), undo) 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: def mark_cell_as_visited(self, i: int, j: int, visitor: str) -> None:
""" """

View file

@ -65,7 +65,6 @@ 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]