commit b215893fe8ec2a9b92db249bb8e6ee85b46442ce Author: Darius Rapalis Date: Sat Feb 22 22:49:26 2025 +0200 DaMaze 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()