From: Axy Date: Tue, 17 Mar 2026 01:48:54 +0000 (+0100) Subject: Performance improvements for dirty tracker using shuffleable set X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=338a47511ca03fb9f68cd1306bd80b16f2681e59;p=axy%2Fft%2Fa-maze-ing.git Performance improvements for dirty tracker using shuffleable set --- diff --git a/.gitignore b/.gitignore index 64d02bc..93e9841 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ **/__pycache__ **/.mypy_cache +out.prof +.venv diff --git a/Makefile b/Makefile index dd76e6f..55d0384 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,11 @@ install: + pip install flake8 mypy flameprof + +.venv: + python -m venv .venv + +venv_bash: .venv + bash --init-file <(echo ". ~/.bashrc; source .venv/bin/activate") run: @@ -9,5 +16,9 @@ lint: mypy . --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs lint-strict: - flake8 . - mypy . --strict + bash -c "flake8 . --extend-exclude .venv; mypy . --strict" + +profile: + python -m cProfile -o out.prof __main__.py + +.PHONY: install venv run clean lint lint-strict profile diff --git a/__main__.py b/__main__.py index 9b35b9c..5619b5e 100644 --- a/__main__.py +++ b/__main__.py @@ -1,3 +1,4 @@ +from sys import stderr import time from amazeing import ( Maze, @@ -5,7 +6,6 @@ from amazeing import ( Pattern, maze_make_pacman, maze_make_perfect, - maze_make_empty, ) import random @@ -27,6 +27,7 @@ dims = IVec2(config.width, config.height) maze = Maze(dims) dirty_tracker = MazeDirtyTracker(maze) +pacman_tracker = MazeDirtyTracker(maze) maze.outline() @@ -92,16 +93,20 @@ def display_maze(maze: Maze) -> None: def poll_events(timeout_ms: int = -1) -> None: start = time.monotonic() - elapsed_ms = lambda: int((time.monotonic() - start) * 1000.0) - timeout = lambda: ( - max(timeout_ms - elapsed_ms(), 0) if timeout_ms != -1 else -1 - ) + + def elapsed_ms() -> int: + return int((time.monotonic() - start) * 1000.0) + + def timeout() -> int: + return max(timeout_ms - elapsed_ms(), 0) if timeout_ms != -1 else -1 + + backend.present() while True: event = backend.event(timeout()) - if event is None: - if timeout_ms == -1: - continue - return + if isinstance(event, bool): + if timeout() == 0 and not event: + return + continue if isinstance(event, CloseRequested) or event.sym == "q": exit(0) if event.sym == "c": @@ -111,7 +116,6 @@ def poll_events(timeout_ms: int = -1) -> None: empty.cycle() else: continue - backend.present() prev_solution: list[Cardinal] = [] @@ -164,7 +168,7 @@ def elipse_manhattan(a: IVec2, b: IVec2, a2: IVec2, b2: IVec2) -> int: # solution = maze.pathfind(CellCoord(config.entry), CellCoord(config.exit)) # if solution is None or prev_solution == solution: # return -# prev_tiles = Cardinal.path_to_tiles(prev_solution, CellCoord(config.entry)) +# prev_tiles = Cardinal.path_to_tiles(prev_solution, CellCoord(config.entry)) # tiles = Cardinal.path_to_tiles(solution, CellCoord(config.entry)) # backend.set_style(empty.curr_style()) # for tile in prev_tiles: @@ -179,7 +183,7 @@ def elipse_manhattan(a: IVec2, b: IVec2, a2: IVec2, b2: IVec2) -> int: network_tracker = MazeNetworkTracker(maze) maze_make_perfect(maze, network_tracker, callback=display_maze) -# maze_make_pacman(maze, walls_const, callback=display_maze) +maze_make_pacman(maze, walls_const, pacman_tracker, callback=display_maze) # pathfind() @@ -191,4 +195,5 @@ while False: # maze_make_empty(maze, walls_const, callback=display_maze) # poll_events(200) # maze._rebuild() -poll_events() +while True: + poll_events(16) diff --git a/amazeing/__init__.py b/amazeing/__init__.py index 2557f93..a68e0eb 100644 --- a/amazeing/__init__.py +++ b/amazeing/__init__.py @@ -2,7 +2,7 @@ __version__ = "0.0.0" __author__ = "luflores & agilliar" from amazeing.maze_class import WallCoord, Maze, Pattern -from amazeing.maze_display import Backend, IVec2, TTYBackend +from amazeing.maze_display import IVec2, TTYBackend from .maze_make_pacman import maze_make_pacman from .maze_make_perfect import maze_make_perfect from .maze_make_empty import maze_make_empty @@ -11,7 +11,6 @@ __all__ = [ "WallCoord", "Maze", "Pattern", - "Backend", "IVec2", "TTYBackend", "maze_make_pacman", diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py index e9b7041..a18e305 100644 --- a/amazeing/config/config_parser.py +++ b/amazeing/config/config_parser.py @@ -252,7 +252,7 @@ def DefaultedStrField[T, U]( "Failed to construct defaulted field " + self.name() ) acc.append(res[0]) - return self.merge(acc) + return self.merge(acc) # type: ignore return Inner @@ -260,7 +260,7 @@ def DefaultedStrField[T, U]( def MappedField[T, U, V]( cls: Type[ConfigField[T, U]], mapping: Callable[[U], V] ) -> Type[ConfigField[T, V]]: - class Inner(ConfigField[T, V]): # type: ignore + class Inner(ConfigField[T, V]): def __init__(self, name: str) -> None: self.__inner = cls(name) super().__init__(name) @@ -334,7 +334,7 @@ def line_parser[T]( def fields_parser( - fields_raw: dict[str, type[ConfigField]], + fields_raw: dict[str, type[ConfigField[Any]]], ) -> Parser[dict[str, Any]]: fields = {key: cls(key) for key, cls in fields_raw.items()} parse_line = terminated(line_parser(fields), cut(tag("\n"))) diff --git a/amazeing/maze_class/maze.py b/amazeing/maze_class/maze.py index 71b7166..4acb454 100644 --- a/amazeing/maze_class/maze.py +++ b/amazeing/maze_class/maze.py @@ -1,5 +1,4 @@ -from typing import Callable, Generator, Iterable, cast - +from typing import Callable, Generator, Iterable from amazeing.maze_display.backend import IVec2 from .maze_coords import ( CellCoord, @@ -83,3 +82,28 @@ class Maze: def walls_empty(self) -> Iterable[WallCoord]: return filter(lambda w: not self.get_wall(w), self.all_walls()) + + def wall_cuts_cycle(self, wall: WallCoord) -> bool: + return any( + ( + len( + [ + () + for wall in self.get_walls_checked(list(cell.walls())) + if wall + ] + ) + >= (3 if self.get_wall(wall) 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 for wall in self.get_walls_checked(f(coord))) + else [] + ) + return leaf_f(WallCoord.a_neighbours) + leaf_f(WallCoord.b_neighbours) diff --git a/amazeing/maze_class/maze_dirty_tracker.py b/amazeing/maze_class/maze_dirty_tracker.py index 4e09f8c..7300320 100644 --- a/amazeing/maze_class/maze_dirty_tracker.py +++ b/amazeing/maze_class/maze_dirty_tracker.py @@ -1,27 +1,31 @@ from collections.abc import Iterable from amazeing.maze_class.maze import Maze from amazeing.maze_class.maze_coords import WallCoord +from amazeing.utils.randset import Randset class MazeDirtyTracker: def __init__(self, maze: Maze) -> None: self.__maze: Maze = maze - self.__dirty: set[WallCoord] = set() + self.__dirty: Randset[WallCoord] = Randset() maze.observers.add(self.__observer) - def __del__(self): + def __repr__(self) -> str: + return f"MazeDirtyTracker({self.__dirty})" + + def __del__(self) -> None: 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]: + def clear(self) -> Randset[WallCoord]: res = self.__dirty - self.__dirty = set() + self.__dirty = Randset() return res def curr_dirty(self) -> Iterable[WallCoord]: - return list(self.__dirty) + return self.__dirty + + def end(self) -> None: + self.__maze.observers.discard(self.__observer) diff --git a/amazeing/maze_class/maze_network_tracker.py b/amazeing/maze_class/maze_network_tracker.py index 14c1e0a..8b0297b 100644 --- a/amazeing/maze_class/maze_network_tracker.py +++ b/amazeing/maze_class/maze_network_tracker.py @@ -176,5 +176,5 @@ class MazeNetworkTracker: def wall_bisects(self, wall: WallCoord) -> bool: return self.__forest.wall_bisects(wall) - def end(self): + def end(self) -> None: self.__maze.observers.discard(self.__observer) diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index a08fad3..5d7b4c9 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -13,7 +13,7 @@ from amazeing.maze_display.layout import ( layout_sort_chunked, layout_split, ) -from .backend import Backend, IVec2, BackendEvent, KeyboardInput +from .backend import IVec2, BackendEvent, KeyboardInput import curses @@ -286,7 +286,9 @@ class TileMaps: dim, ) - def add_style(tilemap, size=mazetile_dims): + def add_style( + tilemap: list[ColoredLine], size: IVec2 = mazetile_dims + ) -> int: return backend.add_style(new_tilemap(tilemap, size)) self.empty: list[int] = list(map(add_style, config.tilemap_empty)) @@ -301,7 +303,9 @@ class TileMaps: class TileCycle[T]: - def __init__(self, styles: list[T], cb: Callable[[T], None], i=0) -> None: + def __init__( + self, styles: list[T], cb: Callable[[T], None], i: int = 0 + ) -> None: if len(styles) == 0: raise BackendException("No styles provided in tilecycle") self.__styles = styles @@ -309,7 +313,7 @@ class TileCycle[T]: self.__i = i cb(styles[i]) - def cycle(self, by: int = 1): + def cycle(self, by: int = 1) -> None: new = (self.__i + by) % len(self.__styles) if new != self.__i: self.__cb(self.__styles[new]) @@ -319,7 +323,7 @@ class TileCycle[T]: return self.__styles[self.__i] -class TTYBackend(Backend[int]): +class TTYBackend: def __init__( self, maze_dims: IVec2, wall_dim: IVec2, cell_dim: IVec2 ) -> None: @@ -389,7 +393,7 @@ class TTYBackend(Backend[int]): self.__style_bimap: BiMap[int, IVec2] = BiMap() - def __del__(self): + def __del__(self) -> None: curses.curs_set(1) curses.nocbreak() self.__screen.keypad(False) @@ -411,7 +415,7 @@ class TTYBackend(Backend[int]): def inner(new: int) -> None: nonlocal curr - if curr == None: + if curr is None: curr = new if curr == new: return @@ -448,12 +452,12 @@ class TTYBackend(Backend[int]): self.__layout.laid_out(IVec2(0, 0), IVec2(x, y)) self.__scratch.overwrite(self.__screen) - def event(self, timeout_ms: int = -1) -> BackendEvent | None: + def event(self, timeout_ms: int = -1) -> BackendEvent | bool: self.__screen.timeout(timeout_ms) try: key = self.__screen.getkey() except curses.error: - return None + return False match key: case "KEY_RESIZE": self.__resize = True @@ -467,5 +471,4 @@ class TTYBackend(Backend[int]): self.__pad.scroll(IVec2(-1, 0)) case _: return KeyboardInput(key) - self.present() - return None + return True diff --git a/amazeing/maze_display/__init__.py b/amazeing/maze_display/__init__.py index 6ae3ed2..0964b48 100644 --- a/amazeing/maze_display/__init__.py +++ b/amazeing/maze_display/__init__.py @@ -1,11 +1,10 @@ __version__ = "0.0.0" __author__ = "luflores & agilliar" -from .backend import Backend, IVec2 +from .backend import IVec2 from .TTYdisplay import TTYBackend __all__ = [ - "Backend", "IVec2", "TTYBackend", ] diff --git a/amazeing/maze_display/backend.py b/amazeing/maze_display/backend.py index 6a8f9c0..77ecf16 100644 --- a/amazeing/maze_display/backend.py +++ b/amazeing/maze_display/backend.py @@ -1,4 +1,3 @@ -from abc import ABC, abstractmethod from collections.abc import Callable from dataclasses import dataclass from typing import Type, cast @@ -29,34 +28,40 @@ class IVec2[T = int]: ( other if isinstance(other, IVec2) - else (other := type(self).splat(cast(T, other))) + else (other := type(self).splat(other)) ).x, ), op(self.y, cast(IVec2[T], other).y), ) - def innertype(self) -> Type: + def innertype(self) -> Type[T]: return type(self.x) def __mul__(self, other: "T | IVec2[T]") -> "IVec2[T]": - return self.with_op(self.innertype().__mul__)(self, other) + return self.with_op(getattr(self.innertype(), "__mul__"))(self, other) def __add__(self, other: "T | IVec2[T]") -> "IVec2[T]": - return self.with_op(self.innertype().__add__)(self, other) + return self.with_op(getattr(self.innertype(), "__add__"))(self, other) def __sub__(self, other: "T | IVec2[T]") -> "IVec2[T]": - return self.with_op(self.innertype().__sub__)(self, other) + return self.with_op(getattr(self.innertype(), "__sub__"))(self, other) def __floordiv__(self, other: "T| IVec2[T]") -> "IVec2[T]": - return self.with_op(self.innertype().__floordiv__)(self, other) + return self.with_op(getattr(self.innertype(), "__floordiv__"))( + self, other + ) def __mod__(self, other: "T | IVec2[T]") -> "IVec2[T]": - return self.with_op(self.innertype().__mod__)(self, other) + return self.with_op(getattr(self.innertype(), "__mod__"))(self, other) def __eq__(self, value: object, /) -> bool: if not isinstance(value, IVec2): return False - return self.x == value.x and self.y == value.y + if self.x != value.x: + return False + if self.y != value.y: + return False + return True def __hash__(self) -> int: return hash((self.x, self.y)) @@ -78,30 +83,3 @@ class CloseRequested: type BackendEvent = KeyboardInput | CloseRequested - - -class Backend[T](ABC): - """ - ABC for the maze display. - defining how the maze should be drawn. - """ - - @abstractmethod - def dims(self) -> IVec2: - pass - - @abstractmethod - def draw_tile(self, pos: IVec2) -> None: - pass - - @abstractmethod - def set_style(self, style: T) -> None: - pass - - @abstractmethod - def present(self) -> None: - pass - - @abstractmethod - def event(self, timeout_ms: int = -1) -> BackendEvent | None: - pass diff --git a/amazeing/maze_display/layout.py b/amazeing/maze_display/layout.py index 2482801..44435a1 100644 --- a/amazeing/maze_display/layout.py +++ b/amazeing/maze_display/layout.py @@ -165,13 +165,13 @@ class Box(ABC): class VBox[T](Box): def __init__( - self, layout: Layout, boxes: list[tuple[Box, T]] = [] + self, layout: Layout[T], boxes: list[tuple[Box, T]] = [] ) -> None: self.boxes: list[tuple[Box, T]] = boxes - self.layout: Layout = layout + self.layout: Layout[T] = layout @staticmethod - def noassoc(layout: Layout, boxes: list[Box]) -> "VBox[None]": + def noassoc(layout: Layout[None], boxes: list[Box]) -> "VBox[None]": return VBox(layout, [(box, None) for box in boxes]) def dims(self) -> BVec2: @@ -199,13 +199,13 @@ class VBox[T](Box): class HBox[T](Box): def __init__( - self, layout: Layout, boxes: list[tuple[Box, T]] = [] + self, layout: Layout[T], boxes: list[tuple[Box, T]] = [] ) -> None: self.boxes: list[tuple[Box, T]] = boxes - self.layout: Layout = layout + self.layout: Layout[T] = layout @staticmethod - def noassoc(layout: Layout, boxes: list[Box]) -> "HBox[None]": + def noassoc(layout: Layout[None], boxes: list[Box]) -> "HBox[None]": return HBox(layout, [(box, None) for box in boxes]) def dims(self) -> BVec2: @@ -265,11 +265,17 @@ class DBox(Box): self.__inner.laid_out(at, into) -def vpad_box(min_pad: int = 0, cb=lambda _at, _into: None) -> FBox: +def vpad_box( + min_pad: int = 0, + cb: Callable[[IVec2, IVec2], None] = lambda _at, _into: None, +) -> FBox: return FBox(IVec2(BInt(0), BInt(min_pad, True)), cb) -def hpad_box(min_pad: int = 0, cb=lambda _at, _into: None) -> FBox: +def hpad_box( + min_pad: int = 0, + cb: Callable[[IVec2, IVec2], None] = lambda _at, _into: None, +) -> FBox: return FBox(IVec2(BInt(min_pad, True), BInt(0)), cb) diff --git a/amazeing/maze_make_pacman.py b/amazeing/maze_make_pacman.py index 0381851..dc4de9d 100644 --- a/amazeing/maze_make_pacman.py +++ b/amazeing/maze_make_pacman.py @@ -1,29 +1,33 @@ +from sys import stderr from typing import Callable from amazeing import Maze, WallCoord import random +from amazeing.maze_class.maze_dirty_tracker import MazeDirtyTracker + def maze_make_pacman( maze: Maze, walls_const: set[WallCoord], + dirty_tracker: MazeDirtyTracker, callback: Callable[[Maze], None] = lambda _: None, iterations: int = 10, ) -> None: for _ in range(0, iterations): - walls = [wall for wall in maze.walls_full() if wall not in walls_const] - random.shuffle(walls) + walls = dirty_tracker.clear() n = 0 for wall in walls: + if not maze.get_wall(wall) or wall in walls_const: + continue leaf_neighbours = maze.wall_leaf_neighbours(wall) if not maze.wall_cuts_cycle(wall): continue if len(leaf_neighbours) == 0: - maze.set_wall(wall) + maze.set_wall(wall, False) else: - maze.set_wall(wall) - maze.fill_wall(random.choice(leaf_neighbours)) + maze.set_wall(wall, False) + maze.set_wall(random.choice(leaf_neighbours), True) n += 1 callback(maze) if n == 0: break - maze._rebuild() diff --git a/amazeing/utils/avl.py b/amazeing/utils/avl.py index 4f061e5..5ce17ff 100644 --- a/amazeing/utils/avl.py +++ b/amazeing/utils/avl.py @@ -26,7 +26,7 @@ class Tree[T]: self.root.with_parent, lambda parent: Leaf(parent, value), ) - return cast(Leaf, self.root.rhs) + return cast(Leaf[T], self.root.rhs) def prepend(self, value: T) -> "Leaf[T]": if self.root is None: @@ -40,7 +40,7 @@ class Tree[T]: lambda parent: Leaf(parent, value), self.root.with_parent, ) - return cast(Leaf, self.root.lhs) + return cast(Leaf[T], self.root.lhs) def height(self) -> int: return 0 if self.root is None else self.root.height @@ -358,7 +358,7 @@ class Branch[T](Node[T]): self.balance_one_propagate() return new_leaf - def balance_one(self): + def balance_one(self) -> None: if abs(self.get_balance()) <= 1: return diff --git a/amazeing/utils/randset.py b/amazeing/utils/randset.py new file mode 100644 index 0000000..2961af2 --- /dev/null +++ b/amazeing/utils/randset.py @@ -0,0 +1,73 @@ +from collections.abc import Iterable, MutableSequence, MutableSet +from typing import cast, overload + + +class Randset[T](MutableSequence[T], MutableSet[T]): + # __getitem__, __setitem__, __delitem__, __len__, and insert(). + # __contains__, __iter__, __len__, + # add(), and discard(). + def __init__(self) -> None: + self.__elems: list[T] = [] + self.__idx_map: dict[T, int] = {} + + def __repr__(self) -> str: + return str(self.__idx_map) + + @overload + def __getitem__(self, pos: int) -> T: ... + @overload + def __getitem__(self, pos: slice) -> "Randset[T]": ... + + def __getitem__(self, pos: int | slice) -> T | "Randset[T]": + if isinstance(pos, int): + return self.__elems[pos] + else: + res = Randset[T]() + res.__elems = self.__elems[pos] + res.__idx_map = {e: i for i, e in enumerate(res.__elems)} + return res + + @overload + def __setitem__(self, pos: int, value: T) -> None: ... + @overload + def __setitem__(self, pos: slice, value: Iterable[T]) -> None: ... + + def __setitem__(self, pos: int | slice, value: T | Iterable[T]) -> None: + if isinstance(pos, int): + del self.__idx_map[self.__elems[pos]] + self.__elems[pos] = cast(T, value) + self.__idx_map[cast(T, value)] = pos + else: + raise NotImplementedError("slice setitem in randset") + + def __len__(self) -> int: + return len(self.__elems) + + @overload + def __delitem__(self, pos: int) -> None: ... + @overload + def __delitem__(self, pos: slice) -> None: ... + def __delitem__(self, pos: int | slice) -> None: + if isinstance(pos, int): + self.discard(self.__elems[pos]) + else: + elems = self.__elems[pos] + for e in elems: + self.discard(e) + + def add(self, value: T) -> None: + if value in self.__idx_map: + return + self.__idx_map[value] = len(self.__elems) + self.__elems.append(value) + + def discard(self, value: T) -> None: + if value not in self.__idx_map: + return + self.__idx_map[self.__elems[-1]] = self.__idx_map[value] + self.__elems[self.__idx_map[value]] = self.__elems[-1] + self.__elems.pop() + del self.__idx_map[value] + + def insert(self, index: int, value: T) -> None: + raise NotImplementedError("slice setitem in randset")