From d019c566e433f45edf1e0f1714bdc71d063942b5 Mon Sep 17 00:00:00 2001 From: Axy Date: Wed, 25 Mar 2026 18:14:17 +0100 Subject: [PATCH] Improved main and moved things around --- __main__.py | 151 ++++--------------------------- amazeing/display/observer.py | 113 +++++++++++++++++++++++ amazeing/maze/dirty_tracker.py | 3 +- amazeing/maze/make_pacman.py | 3 - amazeing/maze/make_perfect.py | 2 - amazeing/maze/maze.py | 37 +++++--- amazeing/maze/network_tracker.py | 4 +- amazeing/maze/path.py | 16 ++-- amazeing/utils/quadtree.py | 14 ++- 9 files changed, 177 insertions(+), 166 deletions(-) create mode 100644 amazeing/display/observer.py diff --git a/__main__.py b/__main__.py index 145f06b..5ab6c0c 100644 --- a/__main__.py +++ b/__main__.py @@ -1,165 +1,46 @@ -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 index 0000000..55cce1d --- /dev/null +++ b/amazeing/display/observer.py @@ -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() diff --git a/amazeing/maze/dirty_tracker.py b/amazeing/maze/dirty_tracker.py index 545b339..7e0b661 100644 --- a/amazeing/maze/dirty_tracker.py +++ b/amazeing/maze/dirty_tracker.py @@ -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: diff --git a/amazeing/maze/make_pacman.py b/amazeing/maze/make_pacman.py index 804aff3..e5d9e85 100644 --- a/amazeing/maze/make_pacman.py +++ b/amazeing/maze/make_pacman.py @@ -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 diff --git a/amazeing/maze/make_perfect.py b/amazeing/maze/make_perfect.py index a891777..529ffff 100644 --- a/amazeing/maze/make_perfect.py +++ b/amazeing/maze/make_perfect.py @@ -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) diff --git a/amazeing/maze/maze.py b/amazeing/maze/maze.py index 3b6919a..d1b6e1c 100644 --- a/amazeing/maze/maze.py +++ b/amazeing/maze/maze.py @@ -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: diff --git a/amazeing/maze/network_tracker.py b/amazeing/maze/network_tracker.py index f26d6ff..0e5108a 100644 --- a/amazeing/maze/network_tracker.py +++ b/amazeing/maze/network_tracker.py @@ -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 diff --git a/amazeing/maze/path.py b/amazeing/maze/path.py index ed6a3a8..0835e07 100644 --- a/amazeing/maze/path.py +++ b/amazeing/maze/path.py @@ -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 diff --git a/amazeing/utils/quadtree.py b/amazeing/utils/quadtree.py index 34d3ca5..346e0a2 100644 --- a/amazeing/utils/quadtree.py +++ b/amazeing/utils/quadtree.py @@ -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( -- 2.53.0