From b215893fe8ec2a9b92db249bb8e6ee85b46442ce Mon Sep 17 00:00:00 2001 From: Darius Rapalis Date: Sat, 22 Feb 2025 22:49:26 +0200 Subject: [PATCH] DaMaze --- .gitignore | 1 + Cell.py | 40 +++++++++++++ Line.py | 14 +++++ Maze.py | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Point.py | 4 ++ Window.py | 27 +++++++++ main.py | 23 ++++++++ tests.py | 35 +++++++++++ 8 files changed, 310 insertions(+) create mode 100644 .gitignore create mode 100644 Cell.py create mode 100644 Line.py create mode 100644 Maze.py create mode 100644 Point.py create mode 100644 Window.py create mode 100644 main.py create mode 100644 tests.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/Cell.py b/Cell.py new file mode 100644 index 0000000..ccf77b0 --- /dev/null +++ b/Cell.py @@ -0,0 +1,40 @@ +from Line import Line +from Point import Point +from Window import Window +from typing import Self + + +class Cell: + def __init__(self, win:Window=None): + self.has_left_wall:bool = True + self.has_right_wall:bool = True + self.has_top_wall:bool = True + self.has_bottom_wall:bool = True + self.visited:bool = False + self._x1:int|None = None + self._x2:int|None = None + self._y1:int|None = None + self._y2:int|None = None + self._win:Window = win + + def draw(self, x1:int, y1:int, x2:int, y2:int) -> None: + if self._win is None: + return + self._x1 = x1 + self._x2 = x2 + self._y1 = y1 + self._y2 = y2 + self._win.draw_line(Line(Point(x1, y1), Point(x1, y2)), "black" if self.has_left_wall else "white") + self._win.draw_line(Line(Point(x1, y1), Point(x2, y1)), "black" if self.has_top_wall else "white") + self._win.draw_line(Line(Point(x2, y1), Point(x2, y2)), "black" if self.has_right_wall else "white") + self._win.draw_line(Line(Point(x1, y2), Point(x2, y2)), "black" if self.has_bottom_wall else "white") + + def draw_move(self, to_cell:Self, undo:bool=False) -> None: + half_length = abs(self._x2 - self._x1) // 2 + p1 = Point(half_length + self._x1, half_length + self._y1) + + half_length2 = abs(to_cell._x2 - to_cell._x1) // 2 + p2 = Point(half_length2 + to_cell._x1, half_length2 + to_cell._y1) + + line = Line(p1, p2) + self._win.draw_line(line, "gray" if undo else "red") diff --git a/Line.py b/Line.py new file mode 100644 index 0000000..d0e14a9 --- /dev/null +++ b/Line.py @@ -0,0 +1,14 @@ +from Point import Point +from tkinter import Canvas + +class Line: + def __init__( + self, + p1:Point, + p2:Point, + ): + self.p1:Point = p1 + self.p2:Point = p2 + + def draw(self, canvas:Canvas, fill_color:str="black") -> None: + canvas.create_line(self.p1.x, self.p1.y, self.p2.x, self.p2.y, fill=fill_color, width=2) diff --git a/Maze.py b/Maze.py new file mode 100644 index 0000000..aac72e8 --- /dev/null +++ b/Maze.py @@ -0,0 +1,166 @@ +from Cell import Cell +import random +import time +from Window import Window +from typing import List + + +class Maze: + def __init__( + self, + x1:int, + y1:int, + num_rows:int, + num_cols:int, + cell_size_x:int, + cell_size_y:int, + win:Window|None=None, + seed:int|None=None, + ): + self.cells:List[List[Cell]] = [] + self.x1:int = x1 + self.y1:int = y1 + self.num_rows:int = num_rows + self.num_cols:int = num_cols + self.cell_size_x:int = cell_size_x + self.cell_size_y:int = cell_size_y + self.win:Window|None = win + + if seed: + random.seed(seed) + + self.create_cells() + self.break_entrance_and_exit() + self.break_walls_r(0, 0) + self.reset_cells_visited() + + def create_cells(self) -> None: + for i in range(self.num_cols): + self.cells.append([]) + for j in range(self.num_rows): + self.cells[i].append(Cell(self.win)) + + for i in range(self.num_cols): + for j in range(self.num_rows): + self.draw_cell(i, j) + + def draw_cell(self, i, j) -> None: + if self.win is None: + return + x1 = self.x1 + i * self.cell_size_x + y1 = self.y1 + j * self.cell_size_y + x2 = x1 + self.cell_size_x + y2 = y1 + self.cell_size_y + self.cells[i][j].draw(x1, y1, x2, y2) + self.animate() + + def animate(self) -> None: + if self.win is None: + return + self.win.redraw() + time.sleep(0.01) + + def reset_cells_visited(self) -> None: + for i in range(self.num_cols): + for j in range(self.num_rows): + self.cells[i][j].visited = False + + def break_entrance_and_exit(self) -> None: + self.cells[0][0].has_top_wall = False + self.draw_cell(0, 0) + self.cells[self.num_cols - 1][self.num_rows - 1].has_bottom_wall = False + self.draw_cell(self.num_cols - 1, self.num_rows - 1) + + def break_walls_r(self, i:int, j:int) -> None: + self.cells[i][j].visited = True + while True: + last_index = [] + + if i > 0 and not self.cells[i - 1][j].visited: + last_index.append((i - 1, j)) + if i < self.num_cols - 1 and not self.cells[i + 1][j].visited: + last_index.append((i + 1, j)) + if j > 0 and not self.cells[i][j - 1].visited: + last_index.append((i, j - 1)) + if j < self.num_rows - 1 and not self.cells[i][j + 1].visited: + last_index.append((i, j + 1)) + + if len(last_index) == 0: + self.draw_cell(i, j) + return + + direction = random.randrange(len(last_index)) + next_index = last_index[direction] + + if next_index[0] == i + 1: + self.cells[i][j].has_right_wall = False + self.cells[i + 1][j].has_left_wall = False + if next_index[0] == i - 1: + self.cells[i][j].has_left_wall = False + self.cells[i - 1][j].has_right_wall = False + if next_index[1] == j + 1: + self.cells[i][j].has_bottom_wall = False + self.cells[i][j + 1].has_top_wall = False + if next_index[1] == j - 1: + self.cells[i][j].has_top_wall = False + self.cells[i][j - 1].has_bottom_wall = False + + self.break_walls_r(next_index[0], next_index[1]) + + + def solve_r(self, i:int, j:int) -> bool: + self.animate() + + self.cells[i][j].visited = True + + if i == self.num_cols - 1 and j == self.num_rows - 1: + return True + + if ( + i > 0 + and not self.cells[i][j].has_left_wall + and not self.cells[i - 1][j].visited + ): + self.cells[i][j].draw_move(self.cells[i - 1][j]) + if self.solve_r(i - 1, j): + return True + else: + self.cells[i][j].draw_move(self.cells[i - 1][j], True) + + if ( + i < self.num_cols - 1 + and not self.cells[i][j].has_right_wall + and not self.cells[i + 1][j].visited + ): + self.cells[i][j].draw_move(self.cells[i + 1][j]) + if self.solve_r(i + 1, j): + return True + else: + self.cells[i][j].draw_move(self.cells[i + 1][j], True) + + if ( + j > 0 + and not self.cells[i][j].has_top_wall + and not self.cells[i][j - 1].visited + ): + self.cells[i][j].draw_move(self.cells[i][j - 1]) + if self.solve_r(i, j - 1): + return True + else: + self.cells[i][j].draw_move(self.cells[i][j - 1], True) + + if ( + j < self.num_rows - 1 + and not self.cells[i][j].has_bottom_wall + and not self.cells[i][j + 1].visited + ): + self.cells[i][j].draw_move(self.cells[i][j + 1]) + if self.solve_r(i, j + 1): + return True + else: + self.cells[i][j].draw_move(self.cells[i][j + 1], True) + + return False + + def solve(self) -> bool: + return self.solve_r(0, 0) diff --git a/Point.py b/Point.py new file mode 100644 index 0000000..f4bdd93 --- /dev/null +++ b/Point.py @@ -0,0 +1,4 @@ +class Point: + def __init__(self, x:int, y:int) -> None: + self.x = x + self.y = y \ No newline at end of file diff --git a/Window.py b/Window.py new file mode 100644 index 0000000..936a462 --- /dev/null +++ b/Window.py @@ -0,0 +1,27 @@ +from tkinter import Tk, BOTH, Canvas +from Line import Line + +class Window: + def __init__(self, width:int, height:int): + self.root = Tk() + self.root.title("Maze Solver") + self.root.protocol("WM_DELETE_WINDOW", self.close) + self.canvas = Canvas(self.root, bg="white", height=height, width=width) + self.canvas.pack(fill=BOTH, expand=1) + self.running = False + + def redraw(self) -> None: + self.root.update_idletasks() + self.root.update() + + def wait_for_close(self) -> None: + self.running = True + while self.running: + self.redraw() + print("window closed...") + + def draw_line(self, line:Line, fill_color="black") -> None: + line.draw(self.canvas, fill_color) + + def close(self) -> None: + self.running = False \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a52ec3f --- /dev/null +++ b/main.py @@ -0,0 +1,23 @@ +from Window import Window +from Maze import Maze +import sys + + +def main(): + cell_size_x = 50 + cell_size_y = 50 + num_rows = 10 + num_cols = 15 + margin = 20 + screen_x = num_cols * cell_size_x + margin * 2 + screen_y = num_rows * cell_size_y + margin * 2 + + sys.setrecursionlimit(10000) + win = Window(screen_x, screen_y) + + maze = Maze(margin, margin, num_rows, num_cols, int(cell_size_x), int(cell_size_y), win, 10) + maze.solve() + win.wait_for_close() + + +main() diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..7e23eb4 --- /dev/null +++ b/tests.py @@ -0,0 +1,35 @@ +import unittest + +from maze import Maze + + +class Tests(unittest.TestCase): + def test_maze_create_cells(self): + num_cols = 12 + num_rows = 10 + m1 = Maze(0, 0, num_rows, num_cols, 10, 10) + self.assertEqual( + len(m1.cells), + num_cols, + ) + self.assertEqual( + len(m1.cells[0]), + num_rows, + ) + + def test_maze_break_entrance_and_exit(self): + num_cols = 12 + num_rows = 10 + m1 = Maze(0, 0, num_rows, num_cols, 10, 10) + self.assertEqual( + m1.cells[0][0].has_top_wall, + False, + ) + self.assertEqual( + m1.cells[num_cols - 1][num_rows - 1].has_bottom_wall, + False, + ) + + +if __name__ == "__main__": + unittest.main()