This commit is contained in:
2025-02-22 22:49:26 +02:00
commit b215893fe8
8 changed files with 310 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

40
Cell.py Normal file
View File

@@ -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")

14
Line.py Normal file
View File

@@ -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)

166
Maze.py Normal file
View File

@@ -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)

4
Point.py Normal file
View File

@@ -0,0 +1,4 @@
class Point:
def __init__(self, x:int, y:int) -> None:
self.x = x
self.y = y

27
Window.py Normal file
View File

@@ -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

23
main.py Normal file
View File

@@ -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()

35
tests.py Normal file
View File

@@ -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()