From: Axy Date: Tue, 24 Mar 2026 18:24:07 +0000 (+0100) Subject: AVL Update and contour bvh for dead path culling X-Git-Url: https://git.uwuaxy.net/flexible_layout.mp4?a=commitdiff_plain;h=fd81da3056f6e359e23dd139db2401004d890563;p=axy%2Fft%2Fa-maze-ing.git AVL Update and contour bvh for dead path culling --- diff --git a/Makefile b/Makefile index 55d0384..d3a9b23 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ install: python -m venv .venv venv_bash: .venv - bash --init-file <(echo ". ~/.bashrc; source .venv/bin/activate") + bash --init-file <(echo ". ~/.bashrc; export TERM=xterm-256color; . .venv/bin/activate") run: diff --git a/__main__.py b/__main__.py index 348fd28..a5f7d62 100644 --- a/__main__.py +++ b/__main__.py @@ -8,16 +8,17 @@ import random from amazeing.config.config_parser import Config +from amazeing.utils import CellCoord from amazeing.maze import ( - MazeNetworkTracker, - CellCoord, - MazeDirtyTracker, - MazePacmanTracker, - maze_make_pacman, - maze_make_perfect, + 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 config = Config.parse(open("./example.conf").read()) @@ -28,9 +29,9 @@ dims = IVec2(config.width, config.height) maze = Maze(dims) -dirty_tracker = MazeDirtyTracker(maze) -pacman_tracker = MazePacmanTracker(maze) -network_tracker = MazeNetworkTracker(maze) +dirty_tracker = DirtyTracker(maze) +pacman_tracker = PacmanTracker(maze) +network_tracker = NetworkTracker(maze) backend = TTYBackend(dims, config.tilemap_wall_size, config.tilemap_cell_size) pair_map = extract_pairs(config) @@ -123,18 +124,21 @@ maze.outline() walls_const = set(maze.walls_full()) -maze_make_perfect(maze, network_tracker, callback=display_maze) -maze_make_pacman(maze, walls_const, pacman_tracker, callback=display_maze) +make_perfect(maze, network_tracker, callback=display_maze) +make_pacman(maze, walls_const, pacman_tracker, callback=display_maze) # pathfind() while False: - maze_make_perfect(maze, network_tracker, callback=display_maze) + make_perfect(maze, network_tracker, callback=display_maze) # poll_events(200) - maze_make_pacman(maze, walls_const, callback=display_maze) + make_pacman(maze, walls_const, callback=display_maze) # maze_make_empty(maze, walls_const, callback=display_maze) # poll_events(200) # maze._rebuild() -while True: +while False: poll_events(16) + +backend.uninit() +print(network_tracker.contour_bound((CellCoord(0, 1), Cardinal.EAST))) diff --git a/amazeing/display/tty.py b/amazeing/display/tty.py index d142986..a1459ea 100644 --- a/amazeing/display/tty.py +++ b/amazeing/display/tty.py @@ -201,9 +201,9 @@ class MazeTileMap: def dst_coord_rev(self, pixel: IVec2) -> IVec2: mod = self.__wall_dim + self.__cell_dim - return (pixel // mod) * IVec2.splat(2) + IVec2[int].with_op( - lambda a, b: 0 if a < b else 1 - )(pixel % mod, self.__wall_dim) + return (pixel // mod) * IVec2.splat(2) + (pixel % mod).with_op( + lambda a, b: 0 if a < b else 1, self.__wall_dim + ) def tile_size(self, pos: IVec2) -> IVec2: return (pos + IVec2.splat(1)) % IVec2.splat( @@ -247,21 +247,17 @@ class ScrollablePad: return IVec2(x, y) def clamp(self, dims: IVec2) -> None: - self.__pos = IVec2.with_op(min)( - IVec2.with_op(max)(self.__pos, dims - self.dims()), IVec2.splat(0) + self.__pos = self.__pos.lane_max(dims - self.dims()).lane_min( + IVec2.splat(0) ) def present(self, at: IVec2, into: IVec2, window: curses.window) -> None: if self.constrained: self.clamp(into) - pad_start = IVec2.with_op(max)( - IVec2.splat(0) - self.__pos, IVec2.splat(0) - ) - win_start = IVec2.with_op(max)(self.__pos, IVec2.splat(0)) - draw_dim = IVec2.with_op(min)( - self.dims() - pad_start, into - win_start - ) + pad_start = (IVec2.splat(0) - self.__pos).lane_max(IVec2.splat(0)) + win_start = self.__pos.lane_max(IVec2.splat(0)) + draw_dim = (self.dims() - pad_start).lane_min(into - win_start) if draw_dim.x <= 0 or draw_dim.y <= 0: return draw_start = at + win_start @@ -433,11 +429,13 @@ class TTYBackend: self.__filler_boxes: list[DBox] = [] - def filler_box() -> Box: + def filler_box( + dims: IVec2[BInt] = IVec2(BInt(0, True), BInt(0, True)) + ) -> Box: self.__filler_boxes.append( res := DBox( FBox( - IVec2(BInt(0, True), BInt(0, True)), + dims, lambda at, into: ( None if self.__filler is None @@ -476,7 +474,15 @@ class TTYBackend: self.__style_bimap: BiMap[int, IVec2] = BiMap() self.__bg_init: Callable[[IVec2], int] | None = None + self.__uninit: bool = False + def __del__(self) -> None: + self.uninit() + + def uninit(self) -> None: + if self.__uninit: + return + self.__uninit = True curses.curs_set(1) curses.nocbreak() self.__screen.keypad(False) diff --git a/amazeing/maze/__init__.py b/amazeing/maze/__init__.py index e7ab7b7..32302f2 100644 --- a/amazeing/maze/__init__.py +++ b/amazeing/maze/__init__.py @@ -1,26 +1,21 @@ __author__ = "agilliar & luflores" from .maze import Maze -from .maze_pattern import Pattern -from .maze_coords import Cardinal, Orientation, WallCoord, CellCoord -from .maze_dirty_tracker import MazeDirtyTracker -from .maze_pacman_tracker import MazePacmanTracker -from .maze_network_tracker import MazeNetworkTracker -from .maze_make_empty import maze_make_empty -from .maze_make_pacman import maze_make_pacman -from .maze_make_perfect import maze_make_perfect +from .pattern import Pattern +from .dirty_tracker import DirtyTracker +from .pacman_tracker import PacmanTracker +from .network_tracker import NetworkTracker +from .make_empty import make_empty +from .make_pacman import make_pacman +from .make_perfect import make_perfect __all__ = [ "Maze", "Pattern", - "Cardinal", - "Orientation", - "WallCoord", - "CellCoord", - "MazeDirtyTracker", - "MazePacmanTracker", - "MazeNetworkTracker", - "maze_make_empty", - "maze_make_pacman", - "maze_make_perfect", + "DirtyTracker", + "PacmanTracker", + "NetworkTracker", + "make_empty", + "make_pacman", + "make_perfect", ] diff --git a/amazeing/maze/maze_dirty_tracker.py b/amazeing/maze/dirty_tracker.py similarity index 92% rename from amazeing/maze/maze_dirty_tracker.py rename to amazeing/maze/dirty_tracker.py index 0608799..545b339 100644 --- a/amazeing/maze/maze_dirty_tracker.py +++ b/amazeing/maze/dirty_tracker.py @@ -1,9 +1,9 @@ from collections.abc import Iterable from amazeing.maze import Maze -from amazeing.maze import WallCoord +from amazeing.utils import WallCoord -class MazeDirtyTracker: +class DirtyTracker: def __init__(self, maze: Maze) -> None: self.__maze: Maze = maze self.__dirty: set[WallCoord] = set() diff --git a/amazeing/maze/maze_make_empty.py b/amazeing/maze/make_empty.py similarity index 87% rename from amazeing/maze/maze_make_empty.py rename to amazeing/maze/make_empty.py index aed31e8..8b5bcea 100644 --- a/amazeing/maze/maze_make_empty.py +++ b/amazeing/maze/make_empty.py @@ -1,10 +1,10 @@ from collections.abc import Callable from amazeing.maze import Maze -from amazeing.maze import WallCoord +from amazeing.utils import WallCoord import random -def maze_make_empty( +def make_empty( maze: Maze, walls_const: set[WallCoord], callback: Callable[[Maze], None] = lambda _: None, diff --git a/amazeing/maze/maze_make_pacman.py b/amazeing/maze/make_pacman.py similarity index 85% rename from amazeing/maze/maze_make_pacman.py rename to amazeing/maze/make_pacman.py index 9ee10fd..804aff3 100644 --- a/amazeing/maze/maze_make_pacman.py +++ b/amazeing/maze/make_pacman.py @@ -1,14 +1,15 @@ from typing import Callable -from amazeing.maze import Maze, WallCoord +from amazeing.maze import Maze +from amazeing.utils import WallCoord import random -from amazeing.maze import MazePacmanTracker +from amazeing.maze import PacmanTracker -def maze_make_pacman( +def make_pacman( maze: Maze, walls_const: set[WallCoord], - pacman_tracker: MazePacmanTracker, + pacman_tracker: PacmanTracker, callback: Callable[[Maze], None] = lambda _: None, iterations: int = 10, ) -> None: diff --git a/amazeing/maze/maze_make_perfect.py b/amazeing/maze/make_perfect.py similarity index 77% rename from amazeing/maze/maze_make_perfect.py rename to amazeing/maze/make_perfect.py index 7901108..a891777 100644 --- a/amazeing/maze/maze_make_perfect.py +++ b/amazeing/maze/make_perfect.py @@ -2,12 +2,12 @@ from typing import Callable from amazeing.maze import Maze import random -from amazeing.maze import MazeNetworkTracker +from amazeing.maze import NetworkTracker -def maze_make_perfect( +def make_perfect( maze: Maze, - tracker: MazeNetworkTracker, + tracker: NetworkTracker, callback: Callable[[Maze], None] = lambda _: None, ) -> None: empty = list(maze.walls_empty()) diff --git a/amazeing/maze/maze.py b/amazeing/maze/maze.py index 1e75f00..13f899b 100644 --- a/amazeing/maze/maze.py +++ b/amazeing/maze/maze.py @@ -1,9 +1,9 @@ from typing import Callable, Generator, Iterable -from amazeing.utils import IVec2 -from .maze_coords import ( +from amazeing.utils import ( CellCoord, Orientation, WallCoord, + IVec2, ) type MazeObserver = Callable[[WallCoord], None] diff --git a/amazeing/maze/maze_network_tracker.py b/amazeing/maze/network_tracker.py similarity index 74% rename from amazeing/maze/maze_network_tracker.py rename to amazeing/maze/network_tracker.py index dc5d4f5..427ffbf 100644 --- a/amazeing/maze/maze_network_tracker.py +++ b/amazeing/maze/network_tracker.py @@ -1,11 +1,11 @@ from amazeing.maze import Maze -from amazeing.maze.maze_coords import ( - SplitWall, - WallCoord, +from amazeing.utils.coords import ( split_wall_ccw, split_wall_opposite, ) -from amazeing.utils import AVLTree, AVLLeaf +from amazeing.utils import AVLTree, AVLLeaf, SplitWall, WallCoord +from amazeing.utils.avl import BVHKey +from amazeing.utils.quadtree import Rect class NetworkID: @@ -24,8 +24,8 @@ class DualForest: self, ) -> None: # Trees are left hand chiral - self.__trees: set[AVLTree[SplitWall]] = set() - self.__revmap: dict[SplitWall, AVLLeaf[SplitWall]] = {} + self.__trees: set[AVLTree[BVHKey, SplitWall]] = set() + self.__revmap: dict[SplitWall, AVLLeaf[BVHKey, SplitWall]] = {} def __repr__(self) -> str: return ( @@ -48,10 +48,10 @@ class DualForest: if self.get_wall(wall): return a_wall, b_wall = wall.to_split_wall() - a_tree = AVLTree[SplitWall]() - b_tree = AVLTree[SplitWall]() - self.__revmap[a_wall] = a_tree.append(a_wall) - self.__revmap[b_wall] = b_tree.append(b_wall) + a_tree = AVLTree[BVHKey, SplitWall]() + b_tree = AVLTree[BVHKey, SplitWall]() + self.__revmap[a_wall] = a_tree.append(BVHKey.for_wall(a_wall), a_wall) + self.__revmap[b_wall] = b_tree.append(BVHKey.for_wall(b_wall), b_wall) match (self.find_split(a_wall), self.find_split(b_wall)): case (None, None): @@ -66,7 +66,9 @@ class DualForest: lhs, rhs = b_leaf.split_up() lhs.rjoin(a_tree) lhs.rjoin(b_tree) - self.__revmap[b_split] = lhs.append(b_split) + self.__revmap[b_split] = lhs.append( + BVHKey.for_wall(b_split), b_split + ) lhs.rjoin(rhs) self.__trees.add(lhs) case (a_split, None): @@ -78,7 +80,9 @@ class DualForest: lhs, rhs = a_leaf.split_up() lhs.rjoin(b_tree) lhs.rjoin(a_tree) - self.__revmap[a_split] = lhs.append(a_split) + self.__revmap[a_split] = lhs.append( + BVHKey.for_wall(a_split), a_split + ) lhs.rjoin(rhs) self.__trees.add(lhs) case (a_split, b_split): @@ -92,11 +96,15 @@ class DualForest: self.__trees.remove(a_leaf.root()) lhs, rhs = a_leaf.split_up() lhs.rjoin(b_tree) - self.__revmap[a_split] = rhs.prepend(a_split) + self.__revmap[a_split] = rhs.prepend( + BVHKey.for_wall(a_split), a_split + ) rhs.ljoin(a_tree) rhs.rjoin(lhs) lhs, rhs = b_leaf.split_up() - self.__revmap[b_split] = rhs.prepend(b_split) + self.__revmap[b_split] = rhs.prepend( + BVHKey.for_wall(b_split), b_split + ) self.__trees.add(lhs) self.__trees.add(rhs) else: @@ -104,8 +112,12 @@ class DualForest: self.__trees.remove(b_leaf.root()) a_lhs, a_rhs = a_leaf.split_up() b_lhs, b_rhs = b_leaf.split_up() - self.__revmap[a_split] = a_rhs.prepend(a_split) - self.__revmap[b_split] = b_rhs.prepend(b_split) + self.__revmap[a_split] = a_rhs.prepend( + BVHKey.for_wall(a_split), a_split + ) + self.__revmap[b_split] = b_rhs.prepend( + BVHKey.for_wall(b_split), b_split + ) res = a_lhs res.rjoin(b_tree) res.rjoin(b_rhs) @@ -133,7 +145,7 @@ class DualForest: self.__trees.remove(b_leaf.root()) a_lhs, a_rhs = a_leaf.split_up() b_lhs, b_rhs = b_leaf.split_up() - res = AVLTree[SplitWall]() + res = AVLTree[BVHKey, SplitWall]() res.rjoin(a_lhs) res.rjoin(b_rhs) res.rjoin(b_lhs) @@ -145,6 +157,16 @@ class DualForest: a_wall, b_wall = wall.to_split_wall() return a_wall in self.__revmap and b_wall in self.__revmap + def contour_bound(self, wall: SplitWall) -> Rect | None: + if wall not in self.__revmap: + return None + leaf = self.__revmap[wall] + parent = leaf.root() + print(parent) + if parent.root is None: + raise Exception() + return parent.root.key.rect + def wall_bisects(self, wall: WallCoord) -> bool: a_wall, b_wall = wall.to_split_wall() a_split = self.find_split(a_wall) @@ -158,7 +180,7 @@ class DualForest: return a_leaf.root() is b_leaf.root() -class MazeNetworkTracker: +class NetworkTracker: def __init__(self, maze: Maze) -> None: self.__maze: Maze = maze self.__forest: DualForest = DualForest() @@ -176,5 +198,8 @@ class MazeNetworkTracker: def wall_bisects(self, wall: WallCoord) -> bool: return self.__forest.wall_bisects(wall) + def contour_bound(self, wall: SplitWall) -> Rect | None: + return self.__forest.contour_bound(wall) + def end(self) -> None: self.__maze.observers.discard(self.__observer) diff --git a/amazeing/maze/maze_pacman_tracker.py b/amazeing/maze/pacman_tracker.py similarity index 89% rename from amazeing/maze/maze_pacman_tracker.py rename to amazeing/maze/pacman_tracker.py index b33a3b9..be9bea8 100644 --- a/amazeing/maze/maze_pacman_tracker.py +++ b/amazeing/maze/pacman_tracker.py @@ -1,10 +1,9 @@ from collections.abc import Iterable from amazeing.maze import Maze -from amazeing.maze import WallCoord -from amazeing.utils.randset import Randset +from amazeing.utils import Randset, WallCoord -class MazePacmanTracker: +class PacmanTracker: def __init__(self, maze: Maze) -> None: self.__maze: Maze = maze self.__dirty: Randset[WallCoord] = Randset() diff --git a/amazeing/maze/path.py b/amazeing/maze/path.py new file mode 100644 index 0000000..9bd608c --- /dev/null +++ b/amazeing/maze/path.py @@ -0,0 +1,3 @@ +class Path: + def __init__(self) -> None: + pass diff --git a/amazeing/maze/maze_pattern.py b/amazeing/maze/pattern.py similarity index 97% rename from amazeing/maze/maze_pattern.py rename to amazeing/maze/pattern.py index ee73ac6..5428974 100644 --- a/amazeing/maze/maze_pattern.py +++ b/amazeing/maze/pattern.py @@ -1,7 +1,6 @@ from collections.abc import Iterable, Generator, Callable -from amazeing.utils import IVec2 -from .maze import Maze -from .maze_coords import CellCoord +from amazeing.utils import IVec2, CellCoord +from amazeing.maze import Maze class Pattern: diff --git a/amazeing/utils/__init__.py b/amazeing/utils/__init__.py index 59cd57d..368859c 100644 --- a/amazeing/utils/__init__.py +++ b/amazeing/utils/__init__.py @@ -1,6 +1,28 @@ from .bi_map import BiMap -from .avl import Tree as AVLTree, Leaf as AVLLeaf +from .avl import ( + Tree as AVLTree, + Leaf as AVLLeaf, + NoopKey as AVLNoopKey, + BVHKey, +) from .quadtree import Tree as QuadTree, Rect from .ivec2 import IVec2 +from .coords import Cardinal, Orientation, WallCoord, CellCoord, SplitWall +from .randset import Randset -__all__ = ["BiMap", "AVLTree", "AVLLeaf", "QuadTree", "Rect", "IVec2"] +__all__ = [ + "BiMap", + "AVLTree", + "AVLLeaf", + "QuadTree", + "Rect", + "IVec2", + "AVLNoopKey", + "BVHKey", + "Cardinal", + "Orientation", + "WallCoord", + "CellCoord", + "SplitWall", + "Randset", +] diff --git a/amazeing/utils/avl.py b/amazeing/utils/avl.py index 4c1970f..0a9c320 100644 --- a/amazeing/utils/avl.py +++ b/amazeing/utils/avl.py @@ -1,12 +1,67 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Iterator -from typing import cast +from typing import Self, cast import textwrap +from amazeing.utils.coords import CellCoord, SplitWall +from amazeing.utils.ivec2 import IVec2 +from amazeing.utils.quadtree import Rect -class Tree[T]: + +class Key(ABC): + """ + This class represents a tree key + It is expected to be associative but not commutative + """ + + @abstractmethod + def reconcile(self, rhs: Self) -> Self: ... + + +class NoopKey(Key): + instance: Self | None = None + + def __new__(cls) -> Self: + if cls.instance is None: + cls.instance = super().__new__(cls) + return cls.instance + + def reconcile(self, rhs: Key) -> "NoopKey": + if not isinstance(rhs, NoopKey): + raise Exception() + return self + + def __repr__(self) -> str: + return "None" + + +class BVHKey(Key): + def __init__(self, rect: Rect) -> None: + super().__init__() + self.rect: Rect = rect + + @staticmethod + def for_cell(cell: CellCoord) -> "BVHKey": + return BVHKey((cell, cell + IVec2.splat(1))) + + @staticmethod + def for_wall(wall: SplitWall) -> "BVHKey": + return BVHKey.for_cell(wall[0]) + + def reconcile(self, rhs: Key) -> "BVHKey": + if not isinstance(rhs, BVHKey): + raise Exception() + s1, e1 = self.rect + s2, e2 = rhs.rect + return BVHKey((s1.lane_min(s2), e1.lane_max(e2))) + + def __repr__(self) -> str: + return f"{self.rect}" + + +class Tree[K: Key, V]: def __init__(self) -> None: - self.root: Node[T] | None = None + self.root: Node[K, V] | None = None def __repr__(self) -> str: return f"{self.root}" if self.root is not None else "(empty)" @@ -15,38 +70,38 @@ class Tree[T]: if self.root is not None: self.root.validate() - def __iter__(self) -> Iterator[T]: + def __iter__(self) -> Iterator[V]: if self.root is None: return iter(()) return iter(self.root) - def append(self, value: T) -> "Leaf[T]": + def append(self, key: K, value: V) -> "Leaf[K, V]": if self.root is None: - leaf = Leaf(self, value) + leaf = Leaf(self, key, value) self.root = leaf return leaf if isinstance(self.root, Branch): - return self.root.append(value) + return self.root.append(key, value) self.root = Branch( self, self.root.with_parent, - lambda parent: Leaf(parent, value), + lambda parent: Leaf(parent, key, value), ) - return cast(Leaf[T], self.root.rhs) + return cast(Leaf[K, V], self.root.rhs) - def prepend(self, value: T) -> "Leaf[T]": + def prepend(self, key: K, value: V) -> "Leaf[K, V]": if self.root is None: - leaf = Leaf(self, value) + leaf = Leaf(self, key, value) self.root = leaf return leaf if isinstance(self.root, Branch): - return self.root.prepend(value) + return self.root.prepend(key, value) self.root = Branch( self, - lambda parent: Leaf(parent, value), + lambda parent: Leaf(parent, key, value), self.root.with_parent, ) - return cast(Leaf[T], self.root.lhs) + return cast(Leaf[K, V], self.root.lhs) def height(self) -> int: return 0 if self.root is None else self.root.height @@ -54,16 +109,16 @@ class Tree[T]: def is_empty(self) -> bool: return self.root is None - def replace(self, node: "Node[T]", by: "Node[T]") -> None: + def replace(self, node: "Node[K, V]", by: "Node[K, V]") -> None: if node is not self.root: raise Exception("Replace operation with unknown node") self.root = by by.parent = self - def balance_one_propagate(self) -> None: + def balance_update_propagate(self) -> None: return - def exchange(self, other: "Tree[T]") -> None: + def exchange(self, other: "Tree[K, V]") -> None: a = self.root b = other.root if a is not None: @@ -73,7 +128,7 @@ class Tree[T]: other.root = a self.root = b - def ljoin(self, lhs: "Tree[T]") -> None: + def ljoin(self, lhs: "Tree[K, V]") -> None: if self is lhs: raise Exception("Cannot merge tree with itself") if self.height() >= lhs.height(): @@ -82,7 +137,7 @@ class Tree[T]: lhs.__rjoin(self) self.exchange(lhs) - def rjoin(self, rhs: "Tree[T]") -> None: + def rjoin(self, rhs: "Tree[K, V]") -> None: if self is rhs: raise Exception("Cannot merge tree with itself") if self.height() >= rhs.height(): @@ -91,7 +146,7 @@ class Tree[T]: rhs.__ljoin(self) self.exchange(rhs) - def __ljoin(self, lhs: "Tree[T]") -> None: + def __ljoin(self, lhs: "Tree[K, V]") -> None: if self.root is None: self.exchange(lhs) if self.root is None or lhs.root is None: @@ -102,12 +157,16 @@ class Tree[T]: while isinstance(curr, Branch) and curr.height > insert.height: curr = curr.lhs parent = curr.parent - new = Branch(parent, insert.with_parent, curr.with_parent) + new = Branch( + parent, + insert.with_parent, + curr.with_parent, + ) parent.replace(curr, new) new.update_height() - parent.balance_one_propagate() + parent.balance_update_propagate() - def __rjoin(self, rhs: "Tree[T]") -> None: + def __rjoin(self, rhs: "Tree[K, V]") -> None: if self.root is None: self.exchange(rhs) if self.root is None or rhs.root is None: @@ -118,23 +177,30 @@ class Tree[T]: while isinstance(curr, Branch) and curr.height > insert.height: curr = curr.rhs parent = curr.parent - new = Branch(parent, curr.with_parent, insert.with_parent) + new = Branch( + parent, + curr.with_parent, + insert.with_parent, + ) parent.replace(curr, new) new.update_height() - parent.balance_one_propagate() + parent.balance_update_propagate() + +class Node[K: Key, V](ABC): + __slots__: tuple[str, ...] = ("parent", "height", "key") -class Node[T](ABC): - def __init__(self, parent: "Branch[T] | Tree[T]") -> None: - self.parent: Branch[T] | Tree[T] = parent + def __init__(self, parent: "Branch[K, V] | Tree[K, V]", key: K) -> None: + self.parent: Branch[K, V] | Tree[K, V] = parent + self.key: K = key self.height: int = 1 @abstractmethod - def __iter__(self) -> Iterator[T]: ... + def __iter__(self) -> Iterator[V]: ... def validate(self) -> None: visited = set() - border: list[Node[T]] = [self] + border: list[Node[K, V]] = [self] while len(border): curr = border.pop() if curr in visited: @@ -144,25 +210,25 @@ class Node[T](ABC): border.append(curr.lhs) border.append(curr.rhs) - def with_parent(self, parent: "Branch[T] | Tree[T]") -> "Node[T]": + def with_parent(self, parent: "Branch[K, V] | Tree[K, V]") -> "Node[K, V]": self.parent = parent return self - def root(self) -> Tree[T]: + def root(self) -> Tree[K, V]: if isinstance(self.parent, Tree): return self.parent return self.parent.root() - def split_up(self) -> tuple[Tree[T], Tree[T]]: + def split_up(self) -> tuple[Tree[K, V], Tree[K, V]]: """ makes self.parent empty """ curr = self - lhs = Tree[T]() - rhs = Tree[T]() + lhs = Tree[K, V]() + rhs = Tree[K, V]() while isinstance(curr.parent, Node): curr_parent = curr.parent - extra = Tree[T]() + extra = Tree[K, V]() if curr_parent.lhs is curr: extra.root = curr_parent.rhs.with_parent(extra) rhs.rjoin(extra) @@ -176,19 +242,21 @@ class Node[T](ABC): return (lhs, rhs) -class Branch[T](Node[T]): +class Branch[K: Key, V](Node[K, V]): + __slots__: tuple[str, ...] = ("lhs", "rhs") + def __init__( self, - parent: "Branch[T] | Tree[T]", - lhs: Callable[["Branch[T]"], Node[T]], - rhs: Callable[["Branch[T]"], Node[T]], + parent: "Branch[K, V] | Tree[K, V]", + lhs: Callable[["Branch[K, V]"], Node[K, V]], + rhs: Callable[["Branch[K, V]"], Node[K, V]], ) -> None: - super().__init__(parent) - self.lhs: Node[T] = lhs(self) - self.rhs: Node[T] = rhs(self) + self.lhs: Node[K, V] = lhs(self) + self.rhs: Node[K, V] = rhs(self) + super().__init__(parent, self.lhs.key.reconcile(self.rhs.key)) self.update_height() - def __iter__(self) -> Iterator[T]: + def __iter__(self) -> Iterator[V]: for e in self.lhs: yield e for e in self.rhs: @@ -202,7 +270,7 @@ class Branch[T](Node[T]): + textwrap.indent(str(self.rhs), " ") ) - def replace(self, node: Node[T], by: Node[T]) -> None: + def replace(self, node: Node[K, V], by: Node[K, V]) -> None: if self.lhs is node: self.lhs = by elif self.rhs is node: @@ -211,7 +279,7 @@ class Branch[T](Node[T]): raise Exception("Replace operation with unknown node") by.parent = self - def get_other(self, node: Node[T]) -> Node[T]: + def get_other(self, node: Node[K, V]) -> Node[K, V]: if self.lhs is node: return self.rhs elif self.rhs is node: @@ -220,7 +288,10 @@ class Branch[T](Node[T]): raise Exception("Get other operation with unknown node") def update_height(self) -> None: - self.height = max(self.rhs.height, self.lhs.height) + 1 + self.height = max(self.lhs.height, self.rhs.height) + 1 + + def update_key(self) -> None: + self.key = self.lhs.key.reconcile(self.rhs.key) def get_balance(self) -> int: return self.rhs.height - self.lhs.height @@ -251,6 +322,8 @@ class Branch[T](Node[T]): n.parent = m n.update_height() m.update_height() + n.update_key() + m.update_key() m.parent = parent m.parent.replace(self, m) @@ -280,6 +353,8 @@ class Branch[T](Node[T]): m.parent = n n.update_height() m.update_height() + n.update_key() + m.update_key() n.parent = parent n.parent.replace(self, n) @@ -316,30 +391,30 @@ class Branch[T](Node[T]): self.rotate_ll() n = self.lhs - def append(self, value: T) -> "Leaf[T]": + def append(self, key: K, value: V) -> "Leaf[K, V]": if isinstance(self.rhs, Branch): - return self.rhs.append(value) - new = Branch[T]( + return self.rhs.append(key, value) + new = Branch[K, V]( self, self.rhs.with_parent, - lambda parent: Leaf[T](parent, value), + lambda parent: Leaf[K, V](parent, key, value), ) self.rhs = new - new_leaf = cast(Leaf[T], new.rhs) - self.balance_one_propagate() + new_leaf = cast(Leaf[K, V], new.rhs) + self.balance_update_propagate() return new_leaf - def prepend(self, value: T) -> "Leaf[T]": + def prepend(self, key: K, value: V) -> "Leaf[K, V]": if isinstance(self.lhs, Branch): - return self.lhs.prepend(value) - new = Branch[T]( + return self.lhs.prepend(key, value) + new = Branch[K, V]( self, - lambda parent: Leaf[T](parent, value), + lambda parent: Leaf[K, V](parent, key, value), self.lhs.with_parent, ) self.lhs = new - new_leaf = cast(Leaf[T], new.lhs) - self.balance_one_propagate() + new_leaf = cast(Leaf[K, V], new.lhs) + self.balance_update_propagate() return new_leaf def balance_one(self) -> None: @@ -363,28 +438,33 @@ class Branch[T](Node[T]): else: self.rotate_ll() - def balance_one_propagate(self) -> None: + def balance_update_propagate(self) -> None: init_height = self.height + init_key = self.key self.update_height() + self.update_key() self.balance_one() - if init_height != self.height: - self.parent.balance_one_propagate() + if init_height != self.height or init_key != self.key: + self.parent.balance_update_propagate() + +class Leaf[K: Key, V](Node[K, V]): + __slots__: tuple[str, ...] = ("value",) -class Leaf[T](Node[T]): def __init__( self, - parent: Branch[T] | Tree[T], - value: T, + parent: Branch[K, V] | Tree[K, V], + key: K, + value: V, ) -> None: - super().__init__(parent) - self.value: T = value + super().__init__(parent, key) + self.value: V = value - def __iter__(self) -> Iterator[T]: + def __iter__(self) -> Iterator[V]: yield self.value def __repr__(self) -> str: - return f"leaf: {self.value}" + return f"leaf ({self.key}): {self.value}" def remove(self) -> None: if isinstance(self.parent, Tree): @@ -392,4 +472,4 @@ class Leaf[T](Node[T]): return other = self.parent.get_other(self) self.parent.parent.replace(self.parent, other) - other.parent.balance_one_propagate() + other.parent.balance_update_propagate() diff --git a/amazeing/utils/bi_map.py b/amazeing/utils/bi_map.py index 0a42250..ac461a0 100644 --- a/amazeing/utils/bi_map.py +++ b/amazeing/utils/bi_map.py @@ -1,20 +1,24 @@ -from .avl import Tree as AVLTree, Leaf as AVLLeaf +from amazeing.utils.avl import ( + Tree as AVLTree, + Leaf as AVLLeaf, + NoopKey as AVLNoopKey, +) class BiMap[K, R]: def __init__(self) -> None: - self.__map: dict[K, AVLTree[R]] = {} - self.__revmap: dict[AVLTree[R], K] = {} - self.__leafmap: dict[R, AVLLeaf[R]] = {} + self.__map: dict[K, AVLTree[AVLNoopKey, R]] = {} + self.__revmap: dict[AVLTree[AVLNoopKey, R], K] = {} + self.__leafmap: dict[R, AVLLeaf[AVLNoopKey, R]] = {} def add(self, key: K, revkey: R) -> None: if self.revcontains(revkey): self.revremove(revkey) if not self.contains(key): - tree = AVLTree[R]() + tree = AVLTree[AVLNoopKey, R]() self.__map[key] = tree self.__revmap[tree] = key - self.__leafmap[revkey] = self.__map[key].append(revkey) + self.__leafmap[revkey] = self.__map[key].append(AVLNoopKey(), revkey) def remove(self, key: K) -> None: for revkey in self.__map[key]: @@ -28,7 +32,7 @@ class BiMap[K, R]: if root.height() == 0: self.__map.pop(self.__revmap.pop(root)) - def get(self, key: K) -> AVLTree[R]: + def get(self, key: K) -> AVLTree[AVLNoopKey, R]: return self.__map[key] if self.contains(key) else AVLTree() def revget(self, revkey: R) -> K: @@ -40,7 +44,7 @@ class BiMap[K, R]: if src not in self.__map: return if dst not in self.__map: - tree = AVLTree[R]() + tree = AVLTree[AVLNoopKey, R]() self.__map[dst] = tree self.__revmap[tree] = dst self.__map[dst].rjoin(self.__map.pop(src)) diff --git a/amazeing/maze/maze_coords.py b/amazeing/utils/coords.py similarity index 99% rename from amazeing/maze/maze_coords.py rename to amazeing/utils/coords.py index 07fc6a8..565afee 100644 --- a/amazeing/maze/maze_coords.py +++ b/amazeing/utils/coords.py @@ -1,6 +1,6 @@ from enum import Enum, auto from typing import Iterable, cast, overload -from amazeing.utils import IVec2 +from amazeing.utils.ivec2 import IVec2 class Orientation(Enum): diff --git a/amazeing/utils/ivec2.py b/amazeing/utils/ivec2.py index fc0ee68..2e86a25 100644 --- a/amazeing/utils/ivec2.py +++ b/amazeing/utils/ivec2.py @@ -3,6 +3,8 @@ from typing import Type, cast class IVec2[T = int]: + __slots__: tuple[str, str] = ("x", "y") + def copy(self, inner_copy: Callable[[T], T] = lambda e: e) -> "IVec2[T]": return IVec2(inner_copy(self.x), inner_copy(self.y)) @@ -17,20 +19,12 @@ class IVec2[T = int]: def __repr__(self) -> str: return f"{self.x, self.y}" - @staticmethod def with_op[T2]( - op: Callable[[T, T], T2], - ) -> Callable[["IVec2[T]", "T | IVec2[T]"], "IVec2[T2]"]: - return lambda self, other: IVec2( - op( - self.x, - ( - other - if isinstance(other, IVec2) - else (other := type(self).splat(other)) - ).x, - ), - op(self.y, cast(IVec2[T], other).y), + self, op: Callable[[T, T], T2], other: "IVec2[T]" + ) -> "IVec2[T2]": + return IVec2( + op(self.x, other.x), + op(self.y, other.y), ) def innertype(self) -> Type[T]: @@ -61,6 +55,12 @@ class IVec2[T = int]: def __hash__(self) -> int: return hash((self.x, self.y)) + def lane_min(self, other: "IVec2[T]") -> "IVec2[T]": + return IVec2(min(self.x, other.x), min(self.y, other.y)) # type:ignore + + def lane_max(self, other: "IVec2[T]") -> "IVec2[T]": + return IVec2(max(self.x, other.x), max(self.y, other.y)) # type:ignore + def xy(self) -> tuple[T, T]: return (self.x, self.y) diff --git a/amazeing/utils/quadtree.py b/amazeing/utils/quadtree.py index 9af2829..34d3ca5 100644 --- a/amazeing/utils/quadtree.py +++ b/amazeing/utils/quadtree.py @@ -1,5 +1,5 @@ from collections.abc import Callable, Generator -from .ivec2 import IVec2 +from amazeing.utils.ivec2 import IVec2 from functools import partial from itertools import chain diff --git a/example.conf b/example.conf index a0f7394..dbd2d2c 100644 --- a/example.conf +++ b/example.conf @@ -1,5 +1,5 @@ -WIDTH=100 -HEIGHT=100 +WIDTH=25 +HEIGHT=25 ENTRY=0,0 EXIT=24,24 OUTPUT_FILE=test @@ -10,7 +10,7 @@ TILEMAP_CELL_SIZE=4,2 TILEMAP_FULL="{1000,1000,1000:1000,1000,1000}######" TILEMAP_FULL="{1000,1000,1000:1000,1000,1000}######" TILEMAP_FULL="{1000,1000,1000:1000,1000,1000}######" -TILEMAP_FULL=1"{100,1000,1000:1000,1000,1000}######" +TILEMAP_FULL=1"{100,1000,1000:1000,1000,1000}███{1000,100,1000:1000,1000,1000}███" TILEMAP_FULL=1"{100,1000,1000:1000,1000,1000}######" TILEMAP_FULL=1"{100,1000,1000:1000,1000,1000}######" TILEMAP_PATH="{100,100,1000:100,100,1000} "