]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
AVL Update and contour bvh for dead path culling
authorAxy <gilliardmarthey.axel@gmail.com>
Tue, 24 Mar 2026 18:24:07 +0000 (19:24 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Tue, 24 Mar 2026 18:24:07 +0000 (19:24 +0100)
20 files changed:
Makefile
__main__.py
amazeing/display/tty.py
amazeing/maze/__init__.py
amazeing/maze/dirty_tracker.py [moved from amazeing/maze/maze_dirty_tracker.py with 92% similarity]
amazeing/maze/make_empty.py [moved from amazeing/maze/maze_make_empty.py with 87% similarity]
amazeing/maze/make_pacman.py [moved from amazeing/maze/maze_make_pacman.py with 85% similarity]
amazeing/maze/make_perfect.py [moved from amazeing/maze/maze_make_perfect.py with 77% similarity]
amazeing/maze/maze.py
amazeing/maze/network_tracker.py [moved from amazeing/maze/maze_network_tracker.py with 74% similarity]
amazeing/maze/pacman_tracker.py [moved from amazeing/maze/maze_pacman_tracker.py with 89% similarity]
amazeing/maze/path.py [new file with mode: 0644]
amazeing/maze/pattern.py [moved from amazeing/maze/maze_pattern.py with 97% similarity]
amazeing/utils/__init__.py
amazeing/utils/avl.py
amazeing/utils/bi_map.py
amazeing/utils/coords.py [moved from amazeing/maze/maze_coords.py with 99% similarity]
amazeing/utils/ivec2.py
amazeing/utils/quadtree.py
example.conf

index 55d0384d0425dda34e12caa415b05fdbb7c204db..d3a9b23cf3194ad4876bb67f98d2780eeeb6f409 100644 (file)
--- 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:
 
index 348fd2821bf86d4d6aab63805f98d6305d680b82..a5f7d628bf9d8d208c9d58360659cd24dd7d9ce2 100644 (file)
@@ -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)))
index d1429863b8cec3b76e1856a40afb20fd1cf69ed3..a1459eaa7653602afcf279dcecf5f83571ca71c5 100644 (file)
@@ -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)
index e7ab7b7710080188d0a091aaa19b6a73f7a870b0..32302f2ca6254eda601041026372305dd39bde49 100644 (file)
@@ -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",
 ]
similarity index 92%
rename from amazeing/maze/maze_dirty_tracker.py
rename to amazeing/maze/dirty_tracker.py
index 060879929a68635da9fceb3989914007d0f22e1a..545b339d2ad2f31a2000b2374196b8ea54361967 100644 (file)
@@ -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()
similarity index 87%
rename from amazeing/maze/maze_make_empty.py
rename to amazeing/maze/make_empty.py
index aed31e8e0ce3810d58f7bec5e0434784a2d7484d..8b5bceaec7c13114d0d3e8b8828c39fad4ee6c96 100644 (file)
@@ -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,
similarity index 85%
rename from amazeing/maze/maze_make_pacman.py
rename to amazeing/maze/make_pacman.py
index 9ee10fdc7b76e760e171508c545ac0cdfda5510e..804aff3c34a49d403b2d073c779ac2f7e28a3af3 100644 (file)
@@ -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:
similarity index 77%
rename from amazeing/maze/maze_make_perfect.py
rename to amazeing/maze/make_perfect.py
index 790110846bb52c8c5e7fb16e60f3e7cdeb09947c..a891777c7fd704eb8875fd3a608c29d804d5491d 100644 (file)
@@ -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())
index 1e75f008a61baa991c17d70b172e8a5c29b47723..13f899be6e96a0603c25f6e3c5e9abc1896ce2de 100644 (file)
@@ -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]
similarity index 74%
rename from amazeing/maze/maze_network_tracker.py
rename to amazeing/maze/network_tracker.py
index dc5d4f5c0a70da02a90f0748a8e0cf8803cae93c..427ffbf0839192e2a7799d0b1ea97076cd21a1d9 100644 (file)
@@ -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)
similarity index 89%
rename from amazeing/maze/maze_pacman_tracker.py
rename to amazeing/maze/pacman_tracker.py
index b33a3b9d6ee41807be2a196aeadb165efca96199..be9bea854175c4e57e25438c5829a7cf9fcae73b 100644 (file)
@@ -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 (file)
index 0000000..9bd608c
--- /dev/null
@@ -0,0 +1,3 @@
+class Path:
+    def __init__(self) -> None:
+        pass
similarity index 97%
rename from amazeing/maze/maze_pattern.py
rename to amazeing/maze/pattern.py
index ee73ac6d4ee8bfc352bd8e3828edcd7cb4cdef36..542897433e8388f0faefdebbf031ada2cdae3c9a 100644 (file)
@@ -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:
index 59cd57d43658121cf8aa2beaf3c08ef8b6270456..368859c07ca7533b913596987f475b0a23b84371 100644 (file)
@@ -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",
+]
index 4c1970fcbd17a52db096124ae11cb998761340bc..0a9c320504eb3316d4173351553fcfffd465789f 100644 (file)
@@ -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()
index 0a422507cb87891420e3c2ee3591c7c7958f2375..ac461a078c95a5db15fe1449330a585c600c5a8c 100644 (file)
@@ -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))
similarity index 99%
rename from amazeing/maze/maze_coords.py
rename to amazeing/utils/coords.py
index 07fc6a84295dab03c2a73ec58d37a7e75d6a8c13..565afee9d407b99736e734c5edc8abea1d993f5c 100644 (file)
@@ -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):
index fc0ee6823b6740d3024ce0852990314e6adf10c6..2e86a259b7ef18dd810d2177b4be842d28d5b6da 100644 (file)
@@ -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)
 
index 9af28293a2899fc5b88486737ed5b1f3dacacbb0..34d3ca5ad46f1d119621d45215061b475d7d0f0f 100644 (file)
@@ -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
 
index a0f7394f4dc7d8a57d798b5ba7fde6903db9fc7e..dbd2d2cf7a8361496215e3d840a3bc3789829d15 100644 (file)
@@ -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}      "