From: Axy Date: Sun, 15 Mar 2026 21:24:04 +0000 (+0100) Subject: Partial avl implementation X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=302603db6c087c4fbf23aae40cbafcd24e8fcdf2;p=axy%2Fft%2Fa-maze-ing.git Partial avl implementation --- diff --git a/__main__.py b/__main__.py index 77a6493..be26415 100644 --- a/__main__.py +++ b/__main__.py @@ -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) diff --git a/amazeing/maze_class/__init__.py b/amazeing/maze_class/__init__.py index b9dcb9b..f7d4856 100644 --- a/amazeing/maze_class/__init__.py +++ b/amazeing/maze_class/__init__.py @@ -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", +] diff --git a/amazeing/maze_class/maze.py b/amazeing/maze_class/maze.py index 58cfd92..032d7ea 100644 --- a/amazeing/maze_class/maze.py +++ b/amazeing/maze_class/maze.py @@ -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()) diff --git a/amazeing/maze_class/maze_walls.py b/amazeing/maze_class/maze_coords.py similarity index 87% rename from amazeing/maze_class/maze_walls.py rename to amazeing/maze_class/maze_coords.py index 3507a8c..0e8528b 100644 --- a/amazeing/maze_class/maze_walls.py +++ b/amazeing/maze_class/maze_coords.py @@ -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 index 0000000..4e09f8c --- /dev/null +++ b/amazeing/maze_class/maze_dirty_tracker.py @@ -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 index 0000000..6589018 --- /dev/null +++ b/amazeing/maze_class/maze_network_tracker.py @@ -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) diff --git a/amazeing/maze_class/maze_pattern.py b/amazeing/maze_class/maze_pattern.py index 04d889e..cda2221 100644 --- a/amazeing/maze_class/maze_pattern.py +++ b/amazeing/maze_class/maze_pattern.py @@ -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) diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index e84af68..a08fad3 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -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: diff --git a/amazeing/maze_make_empty.py b/amazeing/maze_make_empty.py index 7acb5ef..68f9808 100644 --- a/amazeing/maze_make_empty.py +++ b/amazeing/maze_make_empty.py @@ -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) diff --git a/amazeing/maze_make_pacman.py b/amazeing/maze_make_pacman.py index 5014347..0381851 100644 --- a/amazeing/maze_make_pacman.py +++ b/amazeing/maze_make_pacman.py @@ -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 index 0000000..75a6923 --- /dev/null +++ b/amazeing/utils/__init__.py @@ -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 index 0000000..ce53209 --- /dev/null +++ b/amazeing/utils/avl.py @@ -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 index 0000000..5cfaf76 --- /dev/null +++ b/amazeing/utils/bi_map.py @@ -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 diff --git a/example.conf b/example.conf index 0e05908..9d60574 100644 --- a/example.conf +++ b/example.conf @@ -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 index e69de29..0000000