]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Partial avl implementation
authorAxy <gilliardmarthey.axel@gmail.com>
Sun, 15 Mar 2026 21:24:04 +0000 (22:24 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Sun, 15 Mar 2026 21:24:04 +0000 (22:24 +0100)
15 files changed:
__main__.py
amazeing/maze_class/__init__.py
amazeing/maze_class/maze.py
amazeing/maze_class/maze_coords.py [moved from amazeing/maze_class/maze_walls.py with 87% similarity]
amazeing/maze_class/maze_dirty_tracker.py [new file with mode: 0644]
amazeing/maze_class/maze_network_tracker.py [new file with mode: 0644]
amazeing/maze_class/maze_pattern.py
amazeing/maze_display/TTYdisplay.py
amazeing/maze_make_empty.py
amazeing/maze_make_pacman.py
amazeing/utils/__init__.py [new file with mode: 0644]
amazeing/utils/avl.py [new file with mode: 0644]
amazeing/utils/bi_map.py [new file with mode: 0644]
example.conf
tmp [deleted file]

index 77a64936143ae6426e52d808f4ae33a89d2e4f4e..be26415a629f3011d1da7ce6cf3eb6ff508dcb81 100644 (file)
@@ -9,10 +9,35 @@ from amazeing import (
 )
 import random
 
+
 from amazeing.config.config_parser import Config
-from amazeing.maze_class.maze_walls import Cardinal, CellCoord
+from amazeing.maze_class.maze_coords import Cardinal, CellCoord
 from amazeing.maze_display.TTYdisplay import TileCycle, TileMaps, extract_pairs
 from amazeing.maze_display.backend import CloseRequested, IVec2
+from amazeing.utils import AVLTree
+
+tree = AVLTree()
+
+keys = {i: tree.append(i) for i in range(25)}
+
+for i in range(1, 5):
+    keys[i].remove()
+for i in range(5, 15, 2):
+    keys[i].remove()
+
+tree2 = AVLTree()
+
+keys2 = {i: tree2.append(i) for i in range(25)}
+
+for i in range(1, 10, 3):
+    keys2[i].remove()
+
+tree.join(tree2)
+
+print(tree)
+
+
+exit(0)
 
 config = Config.parse(open("./example.conf").read())
 
@@ -74,7 +99,7 @@ def display_maze(maze: Maze) -> None:
         e
         for wall in maze.walls_dirty()
         for e in wall.neighbours()
-        if maze._check_coord(e) and maze.get_wall(e).is_full()
+        if maze.check_coord(e) and maze.get_wall(e).is_full()
     }
 
     for wall in rewrites:
@@ -179,7 +204,7 @@ maze_make_pacman(maze, walls_const, callback=display_maze)
 pathfind()
 
 
-while False:
+while True:
     maze_make_perfect(maze, callback=display_maze)
     # poll_events(200)
     maze_make_pacman(maze, walls_const, callback=display_maze)
index b9dcb9b97909f5f0d3e76f132ad39329a22426a2..f7d4856b1753c50eb82448cd635596ac481c00c4 100644 (file)
@@ -2,12 +2,13 @@ __author__ = "agilliar & luflores"
 
 from .maze import Maze
 from .maze_pattern import Pattern
-from .maze_walls import (MazeWall, NetworkID, Orientation,
-                         WallCoord)
+from .maze_coords import Cardinal, Orientation, WallCoord, CellCoord
 
-__all__ = ["Maze",
-           "Pattern",
-           "MazeWall",
-           "NetworkID",
-           "Orientation",
-           "WallCoord",]
+__all__ = [
+    "Maze",
+    "Pattern",
+    "Cardinal",
+    "Orientation",
+    "WallCoord",
+    "CellCoord",
+]
index 58cfd927959f0d9dd4bbcca3b79c0153f6cbcbc7..032d7ea6930c499d251618742ed4ea18fcab3468 100644 (file)
@@ -1,61 +1,44 @@
 from typing import Callable, Generator, Iterable, cast
 
 from amazeing.maze_display.backend import IVec2
-from .maze_walls import (
-    Cardinal,
+from .maze_coords import (
     CellCoord,
-    MazeWall,
-    NetworkID,
     Orientation,
     WallCoord,
-    WallNetwork,
 )
 
+type MazeObserver = Callable[[WallCoord], None]
+
 
 class Maze:
     def __init__(self, dims: IVec2) -> None:
         self.__dims = dims
-        self.__dirty: set[WallCoord] = set()
-        self._clear()
-
-    def _clear(self) -> None:
-        if hasattr(self, "horizontal") and hasattr(self, "vertical"):
-            self.__dirty ^= {wall for wall in self.walls_full()}
+        self.observers: set[MazeObserver] = set()
         # list of lines
-        self.horizontal: list[list[MazeWall]] = [
-            [MazeWall() for _ in range(0, self.__dims.x)]
+        self.horizontal: list[list[bool]] = [
+            [False for _ in range(0, self.__dims.x)]
             for _ in range(0, self.__dims.y + 1)
         ]
         # list of lines
-        self.vertical: list[list[MazeWall]] = [
-            [MazeWall() for _ in range(0, self.__dims.y)]
+        self.vertical: list[list[bool]] = [
+            [False for _ in range(0, self.__dims.y)]
             for _ in range(0, self.__dims.x + 1)
         ]
-        self.networks: dict[NetworkID, WallNetwork] = {}
 
-    def _rebuild(self) -> None:
-        """
-        rebuilds the maze to recompute proper connectivity values
-        """
-        walls = {wall for wall in self.walls_full()}
-        self._clear()
-        for wall in walls:
-            self.fill_wall(wall)
-
-    def get_wall(self, coord: WallCoord) -> MazeWall:
+    def get_wall(self, coord: WallCoord) -> bool:
         if coord.orientation == Orientation.HORIZONTAL:
             return self.horizontal[coord.a][coord.b]
         return self.vertical[coord.a][coord.b]
 
-    def _remove_wall(self, coord: WallCoord) -> None:
-        """
-        removes the wall, without updating network connectivity
-        """
+    def set_wall(self, coord: WallCoord, value: bool) -> None:
         wall = self.get_wall(coord)
-        if wall.network_id is not None:
-            self.networks[wall.network_id].remove_wall(coord)
-            self.__dirty ^= {coord}
-            wall.network_id = None
+        if wall != value:
+            if coord.orientation == Orientation.HORIZONTAL:
+                self.horizontal[coord.a][coord.b] = value
+            self.vertical[coord.a][coord.b] = value
+
+            for observer in self.observers:
+                observer(coord)
 
     def all_walls(self) -> Generator[WallCoord]:
         for orientation, a_count, b_count in [
@@ -66,7 +49,10 @@ class Maze:
                 for b in range(0, b_count):
                     yield WallCoord(orientation, a, b)
 
-    def _check_coord(self, coord: WallCoord) -> bool:
+    def all_cells(self) -> Iterable[CellCoord]:
+        return CellCoord(self.__dims).all_up_to()
+
+    def check_coord(self, coord: WallCoord) -> bool:
         if coord.a < 0 or coord.b < 0:
             return False
         (a_max, b_max) = (
@@ -78,49 +64,12 @@ class Maze:
             return False
         return True
 
-    def get_walls_checked(self, ids: list[WallCoord]) -> list[MazeWall]:
-        return [self.get_wall(id) for id in ids if self._check_coord(id)]
+    def get_walls_checked(self, ids: list[WallCoord]) -> list[bool]:
+        return [self.get_wall(id) for id in ids if self.check_coord(id)]
 
-    def get_neighbours(self, id: WallCoord) -> list[MazeWall]:
+    def get_neighbours(self, id: WallCoord) -> list[bool]:
         return self.get_walls_checked(id.neighbours())
 
-    def _fill_wall_alone(self, id: WallCoord, wall: MazeWall) -> None:
-        network_id = NetworkID()
-        wall.network_id = network_id
-        network = WallNetwork()
-        network.add_wall(id)
-        self.networks[network_id] = network
-
-    def fill_wall(self, coord: WallCoord) -> None:
-        wall = self.get_wall(coord)
-
-        if wall.is_full():
-            return
-
-        self.__dirty ^= {coord}
-
-        networks = {
-            cast(NetworkID, neighbour.network_id)
-            for neighbour in self.get_neighbours(coord)
-            if neighbour.is_full()
-        }
-
-        if len(networks) == 0:
-            return self._fill_wall_alone(coord, wall)
-
-        dest_id = max(networks, key=lambda n: self.networks[n].size())
-        dest = self.networks[dest_id]
-
-        wall.network_id = dest_id
-        dest.add_wall(coord)
-
-        for to_merge in filter(lambda n: n != dest_id, networks):
-            for curr in self.networks[to_merge].walls:
-                self.get_wall(curr).network_id = dest_id
-                dest.add_wall(curr)
-
-            del self.networks[to_merge]
-
     def outline(self) -> None:
         if self.__dims.x < 1 or self.__dims.y < 1:
             return
@@ -138,98 +87,10 @@ class Maze:
         ]:
             for a in a_iter:
                 for b in b_iter:
-                    self.fill_wall(WallCoord(orientation, a, b))
+                    self.set_wall(WallCoord(orientation, a, b), True)
 
     def walls_full(self) -> Iterable[WallCoord]:
-        return filter(lambda w: self.get_wall(w).is_full(), self.all_walls())
-
-    def walls_dirty(self) -> set[WallCoord]:
-        return self.__dirty
+        return filter(lambda w: self.get_wall(w), self.all_walls())
 
     def walls_empty(self) -> Iterable[WallCoord]:
-        return filter(
-            lambda w: not self.get_wall(w).is_full(), self.all_walls()
-        )
-
-    def wall_bisects(self, wall: WallCoord) -> bool:
-        a = {
-            cast(NetworkID, neighbour.network_id)
-            for neighbour in self.get_walls_checked(wall.a_neighbours())
-            if neighbour.is_full()
-        }
-        b = {
-            cast(NetworkID, neighbour.network_id)
-            for neighbour in self.get_walls_checked(wall.b_neighbours())
-            if neighbour.is_full()
-        }
-        return len(a & b) != 0
-
-    def wall_cuts_cycle(self, wall: WallCoord) -> bool:
-        return any(
-            (
-                len(
-                    [
-                        ()
-                        for wall in self.get_walls_checked(list(cell.walls()))
-                        if wall.is_full()
-                    ]
-                )
-                >= (3 if self.get_wall(wall).is_full() else 2)
-            )
-            for cell in wall.neighbour_cells()
-        )
-
-    def wall_leaf_neighbours(self, coord: WallCoord) -> list[WallCoord]:
-        leaf_f: Callable[
-            [Callable[[WallCoord], list[WallCoord]]], list[WallCoord]
-        ] = lambda f: (
-            list(filter(lambda c: self._check_coord(c), f(coord)))
-            if all(
-                not wall.is_full() for wall in self.get_walls_checked(f(coord))
-            )
-            else []
-        )
-        return leaf_f(WallCoord.a_neighbours) + leaf_f(WallCoord.b_neighbours)
-
-    def clear_dirty(self) -> None:
-        self.__dirty = set()
-
-    def pathfind(
-        self, src: CellCoord, dst: CellCoord
-    ) -> list[Cardinal] | None:
-        class Path:
-            def __init__(self, prev: tuple["Path", Cardinal] | None) -> None:
-                self.prev: tuple["Path", Cardinal] | None = prev
-
-            def to_list(self) -> list[Cardinal]:
-                if self.prev is None:
-                    return []
-                prev, direction = self.prev
-                prev_list = prev.to_list()
-                prev_list.append(direction)
-                return prev_list
-
-            def __add__(self, value: Cardinal) -> "Path":
-                return Path((self, value))
-
-        walls_empty = set(self.walls_empty())
-        visited = set()
-        border = {src: Path(None)}
-        while len(border) != 0:
-            border_next = {}
-            for pos, path in border.items():
-                if pos == dst:
-                    return path.to_list()
-                visited.add(pos)
-                for direction in Cardinal.all():
-                    if pos.get_wall(direction) not in walls_empty:
-                        continue
-                    neighbour = pos.get_neighbour(direction)
-                    if neighbour in visited:
-                        continue
-                    if neighbour in border or neighbour in border_next:
-                        continue
-                    border_next[neighbour] = path + direction
-            border = border_next
-
-        return None
+        return filter(lambda w: not self.get_wall(w), self.all_walls())
similarity index 87%
rename from amazeing/maze_class/maze_walls.py
rename to amazeing/maze_class/maze_coords.py
index 3507a8c90a19853686b44e1553d296fec239c2bb..0e8528bd7c54ecf3a37712dd01b922e840724000 100644 (file)
@@ -1,41 +1,8 @@
-from collections.abc import Generator
 from enum import Enum, auto
-from typing import Iterable, Optional, cast, overload
+from typing import Iterable, cast, overload
 from ..maze_display import IVec2
 
 
-class NetworkID:
-    __uuid_gen: int = 0
-
-    def __init__(self) -> None:
-        self.uuid: int = NetworkID.__uuid_gen
-        NetworkID.__uuid_gen += 1
-
-
-class WallNetwork:
-    def __init__(self) -> None:
-        from .maze_walls import WallCoord
-
-        self.walls: set[WallCoord] = set()
-
-    def size(self) -> int:
-        return len(self.walls)
-
-    def add_wall(self, id: "WallCoord") -> None:
-        self.walls.add(id)
-
-    def remove_wall(self, id: "WallCoord") -> None:
-        self.walls.remove(id)
-
-
-class MazeWall:
-    def __init__(self, network_id: Optional[NetworkID] = None) -> None:
-        self.network_id: Optional[NetworkID] = network_id
-
-    def is_full(self) -> bool:
-        return self.network_id is not None
-
-
 class Orientation(Enum):
     HORIZONTAL = auto()
     VERTICAL = auto()
diff --git a/amazeing/maze_class/maze_dirty_tracker.py b/amazeing/maze_class/maze_dirty_tracker.py
new file mode 100644 (file)
index 0000000..4e09f8c
--- /dev/null
@@ -0,0 +1,27 @@
+from collections.abc import Iterable
+from amazeing.maze_class.maze import Maze
+from amazeing.maze_class.maze_coords import WallCoord
+
+
+class MazeDirtyTracker:
+    def __init__(self, maze: Maze) -> None:
+        self.__maze: Maze = maze
+        self.__dirty: set[WallCoord] = set()
+        maze.observers.add(self.__observer)
+
+    def __del__(self):
+        self.__maze.observers.discard(self.__observer)
+
+    def __observer(self, wall: WallCoord) -> None:
+        self.__dirty ^= {wall}
+
+    def end(self):
+        self.__maze.observers.discard(self.__observer)
+
+    def clear(self) -> set[WallCoord]:
+        res = self.__dirty
+        self.__dirty = set()
+        return res
+
+    def curr_dirty(self) -> Iterable[WallCoord]:
+        return list(self.__dirty)
diff --git a/amazeing/maze_class/maze_network_tracker.py b/amazeing/maze_class/maze_network_tracker.py
new file mode 100644 (file)
index 0000000..6589018
--- /dev/null
@@ -0,0 +1,44 @@
+from amazeing.maze_class.maze import Maze
+from amazeing.maze_class.maze_coords import CellCoord, WallCoord
+from amazeing.utils import BiMap
+from amazeing.utils import AVLTree, AVLLeaf
+
+
+class NetworkID:
+    pass
+
+
+class DualForest[T]:
+    """
+    A forest of trees that contour networks
+    AVL trees are used to represent the contours, such that split and
+    merge operations are of complexity O(log n), each tree is a cycle
+    of each connex graph boundary
+    """
+
+    def __init__(
+        self,
+    ) -> None:
+        self.__trees: set[AVLTree[T]] = set()
+        self.__revmap: dict[T, set[AVLLeaf[T]]] = {}
+
+
+class MazeNetworkTracker:
+    def __init__(self, maze: Maze) -> None:
+        self.__maze: Maze = maze
+        self.__networks_wall: BiMap[NetworkID, WallCoord] = BiMap()
+        self.__networks_cell: BiMap[NetworkID, CellCoord] = BiMap()
+
+        netid = NetworkID()
+        for cell in maze.all_cells():
+            self.__networks_cell.add(netid, cell)
+
+        maze.observers.add(self.__observer)
+        for wall in maze.walls_full():
+            self.__observer(wall)
+
+    def __observer(self, wall: WallCoord) -> None:
+        return
+
+    def end(self):
+        self.__maze.observers.discard(self.__observer)
index 04d889e72a43d40f5c7dfbadd2254f3573af3483..cda222194c41ce494cfe82a19b81cd1ed7ac85ec 100644 (file)
@@ -1,7 +1,7 @@
 from collections.abc import Iterable
 from amazeing.maze_display.backend import IVec2
 from .maze import Maze
-from .maze_walls import CellCoord
+from .maze_coords import CellCoord
 from typing import Callable
 
 
@@ -109,4 +109,4 @@ class Pattern:
     def fill(self, maze: "Maze") -> None:
         for cell in self.__cells:
             for wall in cell.walls():
-                maze.fill_wall(wall)
+                maze.set_wall(wall, True)
index e84af6898af519df2f36f203d7fd5d99715d0bc0..a08fad3d4925d000e52987a0ed06290f7a8003c6 100644 (file)
@@ -1,5 +1,6 @@
 from collections.abc import Callable, Generator, Iterable
-from ..config.config_parser import Color, Config, ColoredLine, ColorPair
+from amazeing.utils import BiMap
+from amazeing.config.config_parser import Color, Config, ColoredLine, ColorPair
 from amazeing.maze_display.layout import (
     BInt,
     Box,
@@ -386,8 +387,7 @@ class TTYBackend(Backend[int]):
 
         self.__filler: None | int = None
 
-        self.__style_mapping: dict[int, set[IVec2]] = {}
-        self.__style_revmapping: dict[IVec2, int] = {}
+        self.__style_bimap: BiMap[int, IVec2] = BiMap()
 
     def __del__(self):
         curses.curs_set(1)
@@ -404,11 +404,7 @@ class TTYBackend(Backend[int]):
             box.mark_dirty()
 
     def get_styled(self, style: int) -> Iterable[IVec2]:
-        return set(
-            self.__style_mapping[style]
-            if style in self.__style_mapping
-            else []
-        )
+        return self.__style_bimap.get(style)
 
     def map_style_cb(self) -> Callable[[int], None]:
         curr: int | None = None
@@ -434,16 +430,7 @@ class TTYBackend(Backend[int]):
 
     def draw_tile(self, pos: IVec2) -> None:
         style = self.__style
-        mapping = self.__style_mapping
-        revmapping = self.__style_revmapping
-
-        if pos in revmapping:
-            mapping[revmapping[pos]].remove(pos)
-        revmapping[pos] = style
-        if style not in mapping:
-            mapping[style] = set()
-        mapping[style].add(pos)
-
+        self.__style_bimap.add(style, pos)
         self.__tilemap.draw_at(pos, style, self.__pad.pad)
 
     def set_style(self, style: int) -> None:
index 7acb5ef574b3dd7db53e066db110340213326238..68f9808331bb8d8d48a031f1472ebf4e85aaf7b4 100644 (file)
@@ -1,6 +1,6 @@
 from collections.abc import Callable
 from amazeing.maze_class.maze import Maze
-from amazeing.maze_class.maze_walls import WallCoord
+from amazeing.maze_class.maze_coords import WallCoord
 import random
 
 
@@ -12,5 +12,5 @@ def maze_make_empty(
     walls = [wall for wall in maze.walls_full() if wall not in walls_const]
     random.shuffle(walls)
     for wall in walls:
-        maze._remove_wall(wall)
+        maze.set_wall(wall)
         callback(maze)
index 501434730e140e71294ae52a7c13522708dcb8c2..038185198ce1641cfe2b69e933f681ebbefcf3d6 100644 (file)
@@ -18,9 +18,9 @@ def maze_make_pacman(
             if not maze.wall_cuts_cycle(wall):
                 continue
             if len(leaf_neighbours) == 0:
-                maze._remove_wall(wall)
+                maze.set_wall(wall)
             else:
-                maze._remove_wall(wall)
+                maze.set_wall(wall)
                 maze.fill_wall(random.choice(leaf_neighbours))
             n += 1
             callback(maze)
diff --git a/amazeing/utils/__init__.py b/amazeing/utils/__init__.py
new file mode 100644 (file)
index 0000000..75a6923
--- /dev/null
@@ -0,0 +1,5 @@
+from .bi_map import BiMap
+from .avl import Tree as AVLTree
+from .avl import Leaf as AVLLeaf
+
+__all__ = ["BiMap", "AVLTree", "AVLLeaf"]
diff --git a/amazeing/utils/avl.py b/amazeing/utils/avl.py
new file mode 100644 (file)
index 0000000..ce53209
--- /dev/null
@@ -0,0 +1,336 @@
+from collections.abc import Callable
+from typing import cast
+import textwrap
+
+
+class Tree[T]:
+    def __init__(self) -> None:
+        self.root: Node[T] | None = None
+
+    def __repr__(self) -> str:
+        return f"{self.root}" if self.root is not None else "(empty)"
+
+    def append(self, value: T) -> "Leaf[T]":
+        if self.root is None:
+            leaf = Leaf(self, value)
+            self.root = leaf
+            return leaf
+        if isinstance(self.root, Branch):
+            return self.root.append(value)
+        self.root = Branch(
+            self,
+            self.root.with_parent,
+            lambda parent: Leaf(parent, value),
+        )
+        return cast(Leaf, self.root.rhs)
+
+    def height(self) -> int:
+        return 0 if self.root is None else self.root.height
+
+    def is_empty(self) -> bool:
+        return self.root is None
+
+    def replace(self, node: "Node[T]", by: "Node[T]") -> 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:
+        return
+
+    def exchange(self, other: "Tree[T]") -> None:
+        a = self.root
+        b = other.root
+        if a is not None:
+            a = a.with_parent(other)
+        if b is not None:
+            b = b.with_parent(self)
+        other.root = a
+        self.root = b
+
+    def join(self, rhs: "Tree[T]") -> None:
+        if self.height() >= rhs.height():
+            self.rjoin(rhs)
+        else:
+            rhs.ljoin(self)
+            self.exchange(rhs)
+
+    def ljoin(self, lhs: "Tree[T]") -> None:
+        if self.root is None:
+            self.exchange(lhs)
+        if self.root is None or lhs.root is None:
+            return
+        curr = self.root
+        insert = lhs.root
+        lhs.root = None
+        while isinstance(curr, Branch) and curr.height > insert.height + 1:
+            curr = curr.lhs
+        parent = curr.parent
+        new = Branch(curr.parent, insert.with_parent, curr.with_parent)
+        parent.replace(curr, new)
+        new.update_height()
+        new.parent.balance_one_propagate()
+
+    def rjoin(self, rhs: "Tree[T]") -> None:
+        if self.root is None:
+            self.exchange(rhs)
+        if self.root is None or rhs.root is None:
+            return
+        curr = self.root
+        insert = rhs.root
+        rhs.root = None
+        while isinstance(curr, Branch) and curr.height > insert.height + 1:
+            curr = curr.lhs
+        parent = curr.parent
+        new = Branch(curr.parent, curr.with_parent, insert.with_parent)
+        parent.replace(curr, new)
+        new.update_height()
+        new.parent.balance_one_propagate()
+
+
+class Node[T]:
+    def __init__(self, parent: "Branch[T] | Tree[T]") -> None:
+        self.parent: Branch[T] | Tree[T] = parent
+        self.height: int = 1
+
+    def with_parent(self, parent: "Branch[T] | Tree[T]") -> "Node[T]":
+        self.parent = parent
+        return self
+
+    def root(self) -> Tree[T]:
+        if isinstance(self.parent, Tree):
+            return self.parent
+        return self.parent.root()
+
+
+class Branch[T](Node[T]):
+    def __init__(
+        self,
+        parent: "Branch[T] | Tree[T]",
+        lhs: Callable[["Branch[T]"], Node[T]],
+        rhs: Callable[["Branch[T]"], Node[T]],
+    ) -> None:
+        super().__init__(parent)
+        self.lhs: Node[T] = lhs(self)
+        self.rhs: Node[T] = rhs(self)
+        self.update_height()
+
+    def __repr__(self) -> str:
+        return (
+            f"lhs ({self.lhs.height}):\n"
+            + textwrap.indent(str(self.lhs), "|   ")
+            + f"\nrhs ({self.rhs.height}):\n"
+            + textwrap.indent(str(self.rhs), "    ")
+        )
+
+    def replace(self, node: Node[T], by: Node[T]) -> None:
+        if self.lhs is node:
+            self.lhs = by
+        elif self.rhs is node:
+            self.rhs = by
+        else:
+            raise Exception("Replace operation with unknown node")
+        by.parent = self
+
+    def get_other(self, node: Node[T]) -> Node[T]:
+        if self.lhs is node:
+            return self.rhs
+        elif self.rhs is node:
+            return self.lhs
+        else:
+            raise Exception("Get other operation with unknown node")
+
+    def update_height(self) -> None:
+        self.height = max(self.rhs.height, self.lhs.height) + 1
+
+    def get_balance(self) -> int:
+        return self.rhs.height - self.lhs.height
+
+    def rotate_rr(self) -> None:
+        # Simple AVL rotate:
+        #
+        #   self     -->     self
+        #  /    \           /    \
+        # a      n         n      c
+        #       / \       / \
+        #      b   c     a   b
+        n = self.rhs
+        if not isinstance(n, Branch):
+            return
+        a = self.lhs
+        b = n.lhs
+        c = n.rhs
+        n.lhs = a
+        n.rhs = b
+        self.rhs = c
+        self.lhs = n
+        a.parent = n
+        b.parent = n
+        c.parent = self
+        n.parent = self
+        n.update_height()
+        self.update_height()
+
+    def rotate_ll(self) -> None:
+        # Simple AVL rotate:
+        #
+        #     self   -->    self
+        #    /    \        /    \
+        #   n      c     a       n
+        #  / \                  / \
+        # a   b                b   c
+        n = self.lhs
+        if not isinstance(n, Branch):
+            return
+        a = n.lhs
+        b = n.rhs
+        c = self.rhs
+        self.lhs = a
+        n.lhs = b
+        n.rhs = c
+        self.rhs = n
+        a.parent = self
+        b.parent = n
+        c.parent = n
+        n.parent = self
+        n.update_height()
+        self.update_height()
+
+    def rotate_rl(self) -> None:
+        # Double AVL rotate:
+        #
+        #   self     -->     self
+        #  /    \           /    \
+        # a      n         n      m
+        #       / \       / \    / \
+        #      m   d     a   b  c   d
+        #     / \
+        #    b   c
+        n = self.lhs
+        if not isinstance(n, Branch):
+            return
+        m = n.lhs
+        if not isinstance(m, Branch):
+            return
+        a = self.lhs
+        b = m.lhs
+        c = m.rhs
+        d = n.rhs
+        n.lhs = a
+        n.rhs = b
+        m.lhs = c
+        m.rhs = d
+        self.lhs = n
+        self.rhs = m
+        a.parent = n
+        b.parent = n
+        c.parent = m
+        d.parent = m
+        n.parent = self
+        m.parent = self
+        n.update_height()
+        m.update_height()
+        self.update_height()
+
+    def rotate_lr(self) -> None:
+        # Double AVL rotate:
+        #
+        #     self   -->     self
+        #    /    \         /    \
+        #   n      d       n      m
+        #  / \            / \    / \
+        # a   m          a   b  c   d
+        #    / \
+        #   b   c
+        n = self.lhs
+        if not isinstance(n, Branch):
+            return
+        m = n.rhs
+        if not isinstance(m, Branch):
+            return
+        a = n.lhs
+        b = m.lhs
+        c = m.rhs
+        d = self.rhs
+        n.lhs = a
+        n.rhs = b
+        m.lhs = c
+        m.rhs = d
+        self.lhs = n
+        self.rhs = m
+        a.parent = n
+        b.parent = n
+        c.parent = m
+        d.parent = m
+        n.parent = self
+        m.parent = self
+        n.update_height()
+        m.update_height()
+        self.update_height()
+
+    def append(self, value: T) -> "Leaf[T]":
+        if self.rhs is None:
+            leaf = Leaf[T](self, value)
+            self.rhs = leaf
+            self.balance_one_propagate()
+            return leaf
+        if isinstance(self.rhs, Branch):
+            return self.rhs.append(value)
+        new = Branch[T](
+            self,
+            self.rhs.with_parent,
+            lambda parent: Leaf[T](parent, value),
+        )
+        self.rhs = new
+        new_leaf = cast(Leaf[T], new.rhs)
+        self.balance_one_propagate()
+        return new_leaf
+
+    def balance_one(self):
+        if abs(self.get_balance()) <= 1:
+            return
+        if self.get_balance() > 0:
+            # right is taller
+            if not isinstance(self.rhs, Branch):
+                raise Exception("Invalid tree state")
+            if self.rhs.get_balance() >= 0:
+                self.rotate_rr()
+            else:
+                self.rotate_rl()
+        else:
+            # left is taller
+            if not isinstance(self.lhs, Branch):
+                raise Exception("Invalid tree state")
+            if self.lhs.get_balance() >= 0:
+                self.rotate_lr()
+            else:
+                self.rotate_ll()
+
+    def balance_one_propagate(self) -> None:
+        init_height = self.height
+        self.update_height()
+        self.balance_one()
+        if init_height != self.height:
+            self.parent.balance_one_propagate()
+
+
+class Leaf[T](Node[T]):
+    def __init__(
+        self,
+        parent: Branch[T] | Tree[T],
+        value: T,
+    ) -> None:
+        super().__init__(parent)
+        self.value: T = value
+
+    def __repr__(self) -> str:
+        return f"leaf: {self.value}"
+
+    def remove(self) -> None:
+        if isinstance(self.parent, Tree):
+            self.parent.root = None
+            return
+        other = self.parent.get_other(self)
+        self.parent.parent.replace(self.parent, other)
+        other.parent.balance_one_propagate()
diff --git a/amazeing/utils/bi_map.py b/amazeing/utils/bi_map.py
new file mode 100644 (file)
index 0000000..5cfaf76
--- /dev/null
@@ -0,0 +1,38 @@
+from collections.abc import Iterable
+
+
+class BiMap[K, R]:
+    def __init__(self) -> None:
+        self.__map: dict[K, set[R]] = {}
+        self.__revmap: dict[R, K] = {}
+
+    def add(self, key: K, revkey: R) -> None:
+        if self.revcontains(revkey):
+            self.revremove(revkey)
+        if not self.contains(key):
+            self.__map[key] = set()
+        self.__revmap[revkey] = key
+        self.__map[key].add(revkey)
+
+    def remove(self, key: K) -> None:
+        for revkey in self.__map[key]:
+            self.__revmap.pop(revkey)
+        self.__map.pop(key)
+
+    def revremove(self, revkey: R) -> None:
+        key = self.__revmap.pop(revkey)
+        self.__map[key].remove(revkey)
+        if len(self.__map[key]) == 0:
+            self.__map.pop(key)
+
+    def get(self, key: K) -> Iterable[R]:
+        return list(self.__map[key] if self.contains(key) else [])
+
+    def revget(self, revkey: R) -> K:
+        return self.__revmap[revkey]
+
+    def contains(self, key: K) -> bool:
+        return key in self.__map
+
+    def revcontains(self, revkey: R) -> bool:
+        return revkey in self.__revmap
index 0e0590841c0d7685142c31c6422434a53d1fb76d..9d60574c1356c70f9bc3472e3cd53911a5c0b2f9 100644 (file)
@@ -1,7 +1,7 @@
-WIDTH=100
-HEIGHT=100
+WIDTH=25
+HEIGHT=25
 ENTRY=0,0
-EXIT=99,99
+EXIT=24,24
 OUTPUT_FILE=test
 PERFECT=False
 SEED=111
diff --git a/tmp b/tmp
deleted file mode 100644 (file)
index e69de29..0000000