]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Improved main and moved things around
authorAxy <gilliardmarthey.axel@gmail.com>
Wed, 25 Mar 2026 17:14:17 +0000 (18:14 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Wed, 25 Mar 2026 17:14:17 +0000 (18:14 +0100)
__main__.py
amazeing/display/observer.py [new file with mode: 0644]
amazeing/maze/dirty_tracker.py
amazeing/maze/make_pacman.py
amazeing/maze/make_perfect.py
amazeing/maze/maze.py
amazeing/maze/network_tracker.py
amazeing/maze/path.py
amazeing/utils/quadtree.py

index 145f06bcb47b8dcc6ef653b7e84ad1fb5eee4552..5ab6c0c33adda69b2643f37e0c8fbc16bfc058c3 100644 (file)
-import time
+from amazeing.display.observer import TTYTracker
 from amazeing.maze import (
     Maze,
     Pattern,
-)
-from amazeing.display import TTYBackend
-import random
-
-
-from amazeing.config.config_parser import Config
-from amazeing.maze.path import path_pixels, pathfind_astar
-from amazeing.utils import CellCoord
-from amazeing.maze import (
+    make_empty,
     NetworkTracker,
-    DirtyTracker,
     PacmanTracker,
     make_pacman,
     make_perfect,
 )
-from amazeing.display import TileCycle, TileMaps, extract_pairs
-from amazeing.utils import IVec2
-from amazeing.utils.coords import Cardinal
+from amazeing.config.config_parser import Config
+import random
 
 config = Config.parse(open("./example.conf").read())
 
 if config.seed is not None:
     random.seed(config.seed)
 
-dims = IVec2(config.width, config.height)
+maze = Maze(config)
 
-maze = Maze(dims)
-
-dirty_tracker = DirtyTracker(maze)
 pacman_tracker = PacmanTracker(maze)
 network_tracker = NetworkTracker(maze)
+tty_tracker = TTYTracker(maze, config)
 
-backend = TTYBackend(dims, config.tilemap_wall_size, config.tilemap_cell_size)
-pair_map = extract_pairs(config)
-tilemaps = TileMaps(config, pair_map, backend)
-filler_style = TileCycle(tilemaps.filler, backend.set_filler)
-empty_style = TileCycle(tilemaps.empty, backend.map_style_cb())
-backend.set_bg_init(lambda _: empty_style.curr_style())
-
-full_style = TileCycle(tilemaps.full, backend.map_style_cb())
-
-path_style = TileCycle(tilemaps.path, backend.map_style_cb())
-
-
-def clear_backend() -> None:
-    backend.set_style(empty_style.curr_style())
-    for wall in dirty_tracker.curr_dirty():
-        if maze.get_wall(wall):
-            continue
-        for tile in wall.tile_coords():
-            backend.draw_tile(tile)
-
-
-class Tick:
-    tick: float | None = None
-    prev_path: list[Cardinal] | None = None
+excluded = {maze.entry, maze.exit}
 
-
-def display_path() -> None:
-    if config.entry is None or config.exit is None:
-        return
-    path = pathfind_astar(
-        maze, network_tracker, CellCoord(config.entry), CellCoord(config.exit)
-    )
-    if Tick.prev_path is not None:
-        if Tick.prev_path == path:
-            return
-        backend.set_style(empty_style.curr_style())
-        for tile in path_pixels(CellCoord(config.entry), Tick.prev_path):
-            backend.draw_tile(tile)
-    Tick.prev_path = path
-    if path is not None:
-        backend.set_style(path_style.curr_style())
-        for tile in path_pixels(CellCoord(config.entry), path):
-            backend.draw_tile(tile)
-
-
-def display_maze(maze: Maze) -> None:
-    now = time.monotonic()
-    if Tick.tick is not None and now - Tick.tick < 0.016:
-        return
-    Tick.tick = time.monotonic()
-
-    clear_backend()
-    display_path()
-
-    rewrites = {
-        wall for wall in dirty_tracker.curr_dirty() if maze.get_wall(wall)
-    } | {
-        e
-        for wall in dirty_tracker.curr_dirty()
-        for e in wall.neighbours()
-        if maze.check_coord(e) and maze.get_wall(e)
-    }
-
-    backend.set_style(full_style.curr_style())
-    for wall in rewrites:
-        for pixel in wall.tile_coords():
-            backend.draw_tile(pixel)
-    dirty_tracker.clear()
-    backend.present()
-    poll_events(0)
-
-
-def poll_events(timeout_ms: int = -1) -> None:
-    start = time.monotonic()
-
-    def elapsed_ms() -> int:
-        return int((time.monotonic() - start) * 1000.0)
-
-    def timeout() -> int:
-        return max(timeout_ms - elapsed_ms(), 0) if timeout_ms != -1 else -1
-
-    backend.present()
-    while True:
-        event = backend.event(timeout())
-        if isinstance(event, bool):
-            if timeout() == 0 and not event:
-                return
-            continue
-        if event.sym == "q":
-            exit(0)
-        if event.sym == "c":
-            filler_style.cycle()
-            full_style.cycle()
-            path_style.cycle()
-            empty_style.cycle()
-        else:
-            continue
-
-
-excluded = set()
-if config.entry is not None:
-    excluded.add(CellCoord(config.entry))
-if config.exit is not None:
-    excluded.add(CellCoord(config.exit))
-
-pattern = Pattern(config.maze_pattern).centered_for(dims, excluded)
+pattern = Pattern(config.maze_pattern).centered_for(maze.dims, excluded)
 pattern.fill(maze)
 maze.outline()
 
 walls_const = set(maze.walls_full())
 
-make_perfect(maze, network_tracker, callback=display_maze)
-make_pacman(maze, walls_const, pacman_tracker, callback=display_maze)
+make_perfect(maze, network_tracker)
+make_pacman(maze, walls_const, pacman_tracker)
 
 
-while False:
-    make_perfect(maze, network_tracker, callback=display_maze)
-    # poll_events(200)
-    make_pacman(maze, walls_const, callback=display_maze)
-    # maze_make_empty(maze, walls_const, callback=display_maze)
-    # poll_events(200)
-    # maze._rebuild()
+while True:
+    make_perfect(maze, network_tracker)
+    make_pacman(maze, walls_const, pacman_tracker)
+    make_empty(maze, walls_const)
 
 
 while True:
-    poll_events(16)
+    tty_tracker.display_maze()
 
-backend.uninit()
-print(network_tracker.contour_bound((CellCoord(0, 1), Cardinal.EAST)))
+tty_tracker.backend.uninit()
diff --git a/amazeing/display/observer.py b/amazeing/display/observer.py
new file mode 100644 (file)
index 0000000..55cce1d
--- /dev/null
@@ -0,0 +1,113 @@
+import time
+from amazeing.config.config_parser import Config
+from amazeing.display.tty import TTYBackend, TileCycle, TileMaps, extract_pairs
+from amazeing.maze.dirty_tracker import DirtyTracker
+from amazeing.maze.maze import Maze
+from amazeing.maze.path import path_pixels, pathfind_astar
+from amazeing.utils.coords import Cardinal
+
+
+class TTYTracker:
+    def __init__(self, maze: Maze, config: Config):
+        self.maze = maze
+        self.dirty_tracker = DirtyTracker(maze)
+        self.backend = TTYBackend(
+            maze.dims, config.tilemap_wall_size, config.tilemap_cell_size
+        )
+        self.pair_map = extract_pairs(config)
+        tilemaps = TileMaps(config, self.pair_map, self.backend)
+        self.filler_style = TileCycle(tilemaps.filler, self.backend.set_filler)
+        self.empty_style = TileCycle(
+            tilemaps.empty, self.backend.map_style_cb()
+        )
+        self.full_style = TileCycle(tilemaps.full, self.backend.map_style_cb())
+        self.path_style = TileCycle(tilemaps.path, self.backend.map_style_cb())
+
+        self.backend.set_bg_init(lambda _: self.empty_style.curr_style())
+
+        self.tick: float | None = None
+        self.prev_path: list[Cardinal] | None = None
+
+        maze.observers.add(lambda _: self.display_maze())
+
+    def clear_backend(self) -> None:
+        self.backend.set_style(self.empty_style.curr_style())
+        for wall in self.dirty_tracker.curr_dirty():
+            if self.maze.get_wall(wall):
+                continue
+            for tile in wall.tile_coords():
+                self.backend.draw_tile(tile)
+
+    def path_invalidated(self) -> bool:
+        if self.prev_path is None:
+            return True
+        src = self.maze.entry
+        for card in self.prev_path:
+            if src.get_wall(card) in self.dirty_tracker.curr_dirty():
+                return True
+            src = src.get_neighbour(card)
+        return False
+
+    def display_path(self) -> None:
+        if (
+            all(map(self.maze.get_wall, self.dirty_tracker.curr_dirty()))
+            and not self.path_invalidated()
+        ):
+            return None
+        path = pathfind_astar(self.maze)
+        if self.prev_path is not None:
+            if self.prev_path == path:
+                return
+            self.backend.set_style(self.empty_style.curr_style())
+            for tile in path_pixels(self.maze.entry, self.prev_path):
+                self.backend.draw_tile(tile)
+        self.prev_path = path
+        if path is not None:
+            self.backend.set_style(self.path_style.curr_style())
+            for tile in path_pixels(self.maze.entry, path):
+                self.backend.draw_tile(tile)
+
+    def poll_events(self) -> None:
+        while True:
+            event = self.backend.event(0)
+            if isinstance(event, bool):
+                if not event:
+                    return
+                continue
+            if event.sym == "q":
+                exit(0)
+            if event.sym == "c":
+                self.filler_style.cycle()
+                self.full_style.cycle()
+                self.path_style.cycle()
+                self.empty_style.cycle()
+            else:
+                continue
+
+    def display_maze(self) -> None:
+        now = time.monotonic()
+        if self.tick is not None and now - self.tick < 0.016:
+            return
+        self.tick = time.monotonic()
+
+        self.clear_backend()
+        self.display_path()
+
+        rewrites = {
+            wall
+            for wall in self.dirty_tracker.curr_dirty()
+            if self.maze.get_wall(wall)
+        } | {
+            e
+            for wall in self.dirty_tracker.curr_dirty()
+            for e in wall.neighbours()
+            if self.maze.check_coord(e) and self.maze.get_wall(e)
+        }
+
+        self.backend.set_style(self.full_style.curr_style())
+        for wall in rewrites:
+            for pixel in wall.tile_coords():
+                self.backend.draw_tile(pixel)
+        self.dirty_tracker.clear()
+        self.backend.present()
+        self.poll_events()
index 545b339d2ad2f31a2000b2374196b8ea54361967..7e0b661b8d3ed1021ea6f99ebe35404e185230d6 100644 (file)
@@ -1,4 +1,3 @@
-from collections.abc import Iterable
 from amazeing.maze import Maze
 from amazeing.utils import WallCoord
 
@@ -23,7 +22,7 @@ class DirtyTracker:
         self.__dirty = set()
         return res
 
-    def curr_dirty(self) -> Iterable[WallCoord]:
+    def curr_dirty(self) -> set[WallCoord]:
         return self.__dirty
 
     def end(self) -> None:
index 804aff3c34a49d403b2d073c779ac2f7e28a3af3..e5d9e854465fa907918e52084cdf34d000b7f770 100644 (file)
@@ -1,4 +1,3 @@
-from typing import Callable
 from amazeing.maze import Maze
 from amazeing.utils import WallCoord
 import random
@@ -10,7 +9,6 @@ def make_pacman(
     maze: Maze,
     walls_const: set[WallCoord],
     pacman_tracker: PacmanTracker,
-    callback: Callable[[Maze], None] = lambda _: None,
     iterations: int = 10,
 ) -> None:
     for _ in range(0, iterations):
@@ -31,6 +29,5 @@ def make_pacman(
                 maze.set_wall(wall, False)
                 maze.set_wall(random.choice(leaf_neighbours), True)
             n += 1
-            callback(maze)
         if n == 0:
             break
index a891777c7fd704eb8875fd3a608c29d804d5491d..529ffff79c98fb61979d872e7dcf7a3e2fb12053 100644 (file)
@@ -8,11 +8,9 @@ from amazeing.maze import NetworkTracker
 def make_perfect(
     maze: Maze,
     tracker: NetworkTracker,
-    callback: Callable[[Maze], None] = lambda _: None,
 ) -> None:
     empty = list(maze.walls_empty())
     random.shuffle(empty)
     for wall in empty:
         if not tracker.wall_bisects(wall):
             maze.set_wall(wall, True)
-            callback(maze)
index 3b6919a3a14ff4ec790ebd86818852d6a8477623..d1b6e1c6d024531dc916722bf8686b76774ceecd 100644 (file)
@@ -1,4 +1,5 @@
 from typing import Callable, Generator, Iterable
+from amazeing.config.config_parser import Config
 from amazeing.utils import (
     CellCoord,
     Orientation,
@@ -10,9 +11,19 @@ type MazeObserver = Callable[[WallCoord], None]
 
 
 class Maze:
-    def __init__(self, dims: IVec2) -> None:
-        self.__dims = dims
+    def __init__(self, config: Config) -> None:
+        self.dims = IVec2(config.width, config.height)
         self.observers: set[MazeObserver] = set()
+        self.entry: CellCoord = (
+            CellCoord(0, 0)
+            if config.entry is None
+            else CellCoord(config.entry)
+        )
+        self.exit: CellCoord = (
+            CellCoord(self.dims - IVec2.splat(1))
+            if config.exit is None
+            else CellCoord(config.exit)
+        )
         self.__walls_full: dict[WallCoord, None] = {}
 
     def get_wall(self, coord: WallCoord) -> bool:
@@ -30,26 +41,26 @@ class Maze:
 
     def all_walls(self) -> Generator[WallCoord]:
         for orientation, a_count, b_count in [
-            (Orientation.HORIZONTAL, self.__dims.y + 1, self.__dims.x),
-            (Orientation.VERTICAL, self.__dims.x + 1, self.__dims.y),
+            (Orientation.HORIZONTAL, self.dims.y + 1, self.dims.x),
+            (Orientation.VERTICAL, self.dims.x + 1, self.dims.y),
         ]:
             for a in range(0, a_count):
                 for b in range(0, b_count):
                     yield WallCoord(orientation, a, b)
 
     def all_cells(self) -> Iterable[CellCoord]:
-        return CellCoord(self.__dims).all_up_to()
+        return CellCoord(self.dims).all_up_to()
 
     def check_cell(self, cell: CellCoord) -> bool:
-        return self.__dims.x > cell.x and self.__dims.y > cell.y
+        return self.dims.x > cell.x and self.dims.y > cell.y
 
     def check_coord(self, coord: WallCoord) -> bool:
         if coord.a < 0 or coord.b < 0:
             return False
         (a_max, b_max) = (
-            (self.__dims.y, self.__dims.x - 1)
+            (self.dims.y, self.dims.x - 1)
             if coord.orientation == Orientation.HORIZONTAL
-            else (self.__dims.x, self.__dims.y - 1)
+            else (self.dims.x, self.dims.y - 1)
         )
         if coord.a > a_max or coord.b > b_max:
             return False
@@ -62,18 +73,18 @@ class Maze:
         return self.get_walls_checked(id.neighbours())
 
     def outline(self) -> None:
-        if self.__dims.x < 1 or self.__dims.y < 1:
+        if self.dims.x < 1 or self.dims.y < 1:
             return
         for orientation, a_iter, b_iter in [
             (
                 Orientation.VERTICAL,
-                (0, self.__dims.x),
-                range(0, self.__dims.y),
+                (0, self.dims.x),
+                range(0, self.dims.y),
             ),
             (
                 Orientation.HORIZONTAL,
-                (0, self.__dims.y),
-                range(0, self.__dims.x),
+                (0, self.dims.y),
+                range(0, self.dims.x),
             ),
         ]:
             for a in a_iter:
index f26d6ff714f0de2df36beb6baf7716cabf64b028..0e5108af42ca2951a07799a2102330a4c8ad8607 100644 (file)
@@ -1,6 +1,9 @@
 from amazeing.maze import Maze
 from amazeing.utils.coords import (
+    Cardinal,
+    CellCoord,
     split_wall_ccw,
+    split_wall_cw,
     split_wall_opposite,
 )
 from amazeing.utils import AVLTree, AVLLeaf, SplitWall, WallCoord
@@ -158,7 +161,6 @@ class DualForest:
             return None
         leaf = self.__revmap[wall]
         parent = leaf.root()
-        print(parent)
         if parent.root is None:
             raise Exception()
         return parent.root.key.rect
index ed6a3a896b18a116d4b26d197d9036218a399dea..0835e07f997217260bb3a879a1a28686a14ccd42 100644 (file)
@@ -6,6 +6,8 @@ from amazeing.utils.coords import Cardinal, CellCoord
 from amazeing.utils.ivec2 import IVec2
 import heapq
 
+from amazeing.utils.quadtree import rect_collides
+
 
 def taxicab_distance(a: IVec2, b: IVec2) -> int:
     return sum(a.with_op(lambda lhs, rhs: abs(lhs - rhs), b).xy())
@@ -47,25 +49,23 @@ class AStarStep:
         return res
 
 
-def pathfind_astar(
-    maze: Maze, network_tracker: NetworkTracker, src: CellCoord, dst: CellCoord
-) -> list[Cardinal] | None:
+def pathfind_astar(maze: Maze) -> list[Cardinal] | None:
+    src = maze.entry
+    dst = maze.exit
     queue = [AStarStep(src, None, 0, taxicab_distance(src, dst))]
     heapq.heapify(queue)
-    visited = set()
+    visited = {src}
     while len(queue) > 0:
         curr = heapq.heappop(queue)
-        if curr.dst in visited:
-            continue
         if curr.dst == dst:
             return curr.to_path()
-        visited.add(curr.dst)
         for card in Cardinal.all():
             if maze.get_wall(curr.dst.get_wall(card)):
                 continue
             nxt = curr.append(card, dst)
-            if not nxt.ends_in_bounds(maze):
+            if nxt.dst in visited or not nxt.ends_in_bounds(maze):
                 continue
+            visited.add(nxt.dst)
             heapq.heappush(queue, nxt)
     return None
 
index 34d3ca5ad46f1d119621d45215061b475d7d0f0f..346e0a2bdb1af699a39d9ed5ac0efb8c12134626 100644 (file)
@@ -24,7 +24,7 @@ type MaybeNode = Node | bool
 type Rect = tuple[IVec2, IVec2]
 
 
-def rect_collide(a: Rect, b: Rect) -> bool:
+def rects_overlap(a: Rect, b: Rect) -> bool:
     a_start, a_end = a
     b_start, b_end = b
     return (
@@ -35,6 +35,16 @@ def rect_collide(a: Rect, b: Rect) -> bool:
     )
 
 
+def rect_collides(a: Rect, b: IVec2) -> bool:
+    a_start, a_end = a
+    return (
+        a_end.x > b.x
+        and a_end.y > b.y
+        and b.x >= a_start.x
+        and b.y >= a_start.y
+    )
+
+
 def rect_contains(a: Rect, b: Rect) -> bool:
     a_start, a_end = a
     b_start, b_end = b
@@ -171,7 +181,7 @@ class Tree:
         node_rect = Tree.node_rect(pos, height)
         if rect_contains(rect, node_rect):
             return True
-        if not rect_collide(rect, node_rect):
+        if not rects_overlap(rect, node_rect):
             return False
         starts = Tree.node_starts(pos, height)
         return map4(