]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Pattern centering
authorAxy <gilliardmarthey.axel@gmail.com>
Mon, 9 Mar 2026 16:24:50 +0000 (17:24 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Mon, 9 Mar 2026 16:24:50 +0000 (17:24 +0100)
__main__.py
amazeing/config/config_parser.py
amazeing/maze_class/maze_pattern.py
amazeing/maze_class/maze_walls.py
amazeing/maze_display/backend.py
example.conf

index 851ac1a7a480ef8868024131eaf6f857477831f4..0e23b76243acec7bb42200e4d461c57d8ddc4ed4 100644 (file)
@@ -27,7 +27,9 @@ maze = Maze(dims)
 
 maze.outline()
 
-pattern = Pattern(config.maze_pattern).centered_for(dims)
+pattern = Pattern(config.maze_pattern).centered_for(
+    dims, {CellCoord(5, 5), CellCoord(10, 10)}
+)
 pattern.fill(maze)
 
 walls_const = set(maze.walls_full())
@@ -41,7 +43,7 @@ backend.set_style(tilemaps.empty)
 for wall in maze.all_walls():
     for tile in wall.tile_coords():
         backend.draw_tile(tile)
-for cell in CellCoord(*dims.xy()).all_up_to():
+for cell in CellCoord(dims).all_up_to():
     backend.draw_tile(cell.tile_coords())
 
 
@@ -73,7 +75,8 @@ def display_maze(maze: Maze) -> None:
             backend.draw_tile(pixel)
     maze.clear_dirty()
     backend.present()
-    poll_events(2)
+    poll_events(0)
+    # poll_events(0)
 
 
 def poll_events(timeout_ms: int = -1) -> None:
@@ -94,11 +97,11 @@ def poll_events(timeout_ms: int = -1) -> None:
 
 maze_make_perfect(maze, callback=display_maze)
 maze_make_pacman(maze, walls_const, callback=display_maze)
-while True:
+while False:
     maze_make_perfect(maze, callback=display_maze)
-    poll_events(200)
-    maze_make_pacman(maze, walls_const, callback=display_maze)
-    maze_make_empty(maze, walls_const, callback=display_maze)
-    poll_events(200)
+    poll_events(200)
+    maze_make_pacman(maze, walls_const, callback=display_maze)
+    maze_make_empty(maze, walls_const, callback=display_maze)
+    poll_events(200)
     maze._rebuild()
 poll_events()
index 6ab43872b9cbe780eec041b2ad9d52b905c072bd..0b20ed7f1cd76cf9043451cfa1d80dec479b46b1 100644 (file)
@@ -14,6 +14,7 @@ from .parser_combinator import (
     many,
     many_count,
     none_of,
+    null_parser,
     one_of,
     pair,
     parser_complete,
index 5c8df9a1405acef2931940b636ebba2ef286542b..ceaaa219c47f97ced4051e9135fc0445b199c88b 100644 (file)
@@ -1,3 +1,4 @@
+from collections.abc import Iterable
 from amazeing.maze_display.backend import IVec2
 from .maze import Maze
 from .maze_walls import CellCoord
@@ -13,8 +14,11 @@ class Pattern:
         "  # ###",
     ]
 
-    def __init__(self, pat: list[str]) -> None:
-        self.cells: set[CellCoord] = {
+    def __init__(self, pat: list[str] | set[CellCoord]) -> None:
+        if isinstance(pat, set):
+            self.__cells: set[CellCoord] = pat
+            return
+        self.__cells: set[CellCoord] = {
             CellCoord(x, y)
             for y, line in enumerate(pat)
             for x, char in enumerate(line)
@@ -22,31 +26,79 @@ class Pattern:
         }
 
     def offset(self, by: IVec2) -> "Pattern":
-        pattern: Pattern = Pattern([])
-        pattern.cells = {cell.offset(by) for cell in self.cells}
-        return pattern
+        return Pattern({CellCoord(cell + by) for cell in self.__cells})
+
+    def flood_filled(self) -> "Pattern":
+        dims = self.dims()
+        border = {CellCoord(-1, -1)}
+        reachable = set()
+        full = {
+            CellCoord(x, y) for x in range(0, dims.x) for y in range(0, dims.y)
+        }
+
+        def coord_propagate(coord: CellCoord) -> Iterable[CellCoord]:
+            return (
+                cell
+                for cell in coord.neighbours_unchecked()
+                if cell not in self.__cells
+                and cell not in reachable
+                and cell not in border
+                and cell.x >= -1
+                and cell.x <= dims.x
+                and cell.y >= -1
+                and cell.y <= dims.y
+            )
+
+        while len(border) != 0:
+            for e in border:
+                reachable.add(e)
+                border.remove(e)
+                for cell in coord_propagate(e):
+                    reachable.add(cell)
+                    border.add(cell)
+                break
+        return Pattern(full - reachable)
+
+    def add_cell(self, cell: CellCoord) -> None:
+        self.__cells.add(cell)
+
+    def remove_cell(self, cell: CellCoord) -> None:
+        self.__cells.discard(cell)
 
     def dims(self) -> IVec2:
         dim_by: Callable[[Callable[[CellCoord], int]], int] = lambda f: (
-            max(map(lambda c: f(c) + 1, self.cells), default=0)
-            - min(map(f, self.cells), default=0)
+            max(map(lambda c: f(c) + 1, self.__cells), default=0)
+            - min(map(f, self.__cells), default=0)
         )
-        return IVec2(dim_by(CellCoord.x), dim_by(CellCoord.y))
+        return IVec2(dim_by(lambda e: e.x), dim_by(lambda e: e.y))
 
     def normalized(self) -> "Pattern":
         min_by: Callable[[Callable[[CellCoord], int]], int] = lambda f: min(
-            map(f, self.cells), default=0
+            map(f, self.__cells), default=0
         )
-        offset = IVec2(-min_by(CellCoord.x), -min_by(CellCoord.y))
+        offset = IVec2(-min_by(lambda e: e.x), -min_by(lambda e: e.y))
         return self.offset(offset)
 
-    def centered_for(self, canvas: IVec2) -> "Pattern":
+    def mirrored(self) -> "Pattern":
+        return Pattern({CellCoord(IVec2.splat(0) - e) for e in self.__cells})
+
+    def centered_for(
+        self, canvas: IVec2, excluding: set[CellCoord] = set()
+    ) -> "Pattern":
         normalized: Pattern = self.normalized()
+        negative = normalized.flood_filled().mirrored()
         dims = normalized.dims()
-        offset = (canvas - dims) // 2
-        return normalized.offset(offset)
+        slots = set(CellCoord(canvas - dims + 1).all_up_to())
+        for excluded in excluding:
+            slots -= negative.offset(excluded).__cells
+        if len(slots) == 0:
+            return Pattern([])
+        ideal = (canvas - dims) // 2
+        slot = min(slots, key=lambda e: sum(((e := e - ideal) * e).xy()))
+
+        return normalized.offset(slot)
 
     def fill(self, maze: "Maze") -> None:
-        for cell in self.cells:
+        for cell in self.__cells:
             for wall in cell.walls():
                 maze.fill_wall(wall)
index 6283ff30c031c9a4730d71013b745c814f3bbd34..bc0608007216b4a2b85045871ee275a8abee35ed 100644 (file)
@@ -1,5 +1,5 @@
 from enum import Enum, auto
-from typing import Iterable, Optional, cast
+from typing import Iterable, Optional, cast, overload
 from ..maze_display import IVec2
 
 
@@ -136,23 +136,18 @@ class WallCoord:
         ]
 
 
-class CellCoord:
-    def __init__(self, x: int, y: int) -> None:
-        self.__x: int = x
-        self.__y: int = y
+class CellCoord(IVec2):
+    @overload
+    def __init__(self, val: IVec2, /) -> None: ...
 
-    def __members(self) -> tuple[int, int]:
-        return (self.__x, self.__y)
+    @overload
+    def __init__(self, x: int, y: int, /) -> None: ...
 
-    def __eq__(self, value: object, /) -> bool:
-        return (
-            self.__members() == cast(CellCoord, value).__members()
-            if type(self) is type(value)
-            else False
-        )
-
-    def __hash__(self) -> int:
-        return hash(self.__members())
+    def __init__(self, a: IVec2 | int, b: int = 0) -> None:
+        if isinstance(a, int):
+            super().__init__(a, b)
+        else:
+            super().__init__(a.x, a.y)
 
     def walls(self) -> Iterable[WallCoord]:
         return map(
@@ -163,29 +158,26 @@ class CellCoord:
     def get_wall(self, cardinal: Cardinal) -> WallCoord:
         match cardinal:
             case Cardinal.NORTH:
-                return WallCoord(Orientation.HORIZONTAL, self.__y, self.__x)
+                return WallCoord(Orientation.HORIZONTAL, self.y, self.x)
             case Cardinal.SOUTH:
-                return WallCoord(
-                    Orientation.HORIZONTAL, self.__y + 1, self.__x
-                )
+                return WallCoord(Orientation.HORIZONTAL, self.y + 1, self.x)
             case Cardinal.EAST:
-                return WallCoord(Orientation.VERTICAL, self.__x, self.__y)
+                return WallCoord(Orientation.VERTICAL, self.x, self.y)
             case Cardinal.WEST:
-                return WallCoord(Orientation.VERTICAL, self.__x + 1, self.__y)
+                return WallCoord(Orientation.VERTICAL, self.x + 1, self.y)
 
     def tile_coords(self) -> IVec2:
-        return IVec2(self.__x * 2 + 1, self.__y * 2 + 1)
+        return IVec2(self.x * 2 + 1, self.y * 2 + 1)
 
     def offset(self, by: IVec2) -> "CellCoord":
-        return CellCoord(self.__x + by.x, self.__y + by.y)
-
-    def x(self) -> int:
-        return self.__x
-
-    def y(self) -> int:
-        return self.__y
+        return CellCoord(self + by)
 
     def all_up_to(self) -> Iterable["CellCoord"]:
-        for x in range(0, self.__x):
-            for y in range(0, self.__y):
+        for x in range(0, self.x):
+            for y in range(0, self.y):
                 yield CellCoord(x, y)
+
+    def neighbours_unchecked(self) -> Iterable["CellCoord"]:
+        return map(
+            self.offset, [IVec2(-1, 0), IVec2(1, 0), IVec2(0, -1), IVec2(0, 1)]
+        )
index 910058f529cc4fd6462a0b0d81bc8ae28ebb884f..6a8f9c0ba6dad4a5d47ea9e1b59275fe3e052923 100644 (file)
@@ -20,15 +20,15 @@ class IVec2[T = int]:
         return f"{self.x, self.y}"
 
     @staticmethod
-    def with_op(
-        op: Callable[[T, T], T],
-    ) -> Callable[["IVec2[T]", "T | IVec2[T]"], "IVec2[T]"]:
+    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, type(self))
+                    if isinstance(other, IVec2)
                     else (other := type(self).splat(cast(T, other)))
                 ).x,
             ),
@@ -58,6 +58,9 @@ class IVec2[T = int]:
             return False
         return self.x == value.x and self.y == value.y
 
+    def __hash__(self) -> int:
+        return hash((self.x, self.y))
+
     def xy(self) -> tuple[T, T]:
         return (self.x, self.y)
 
index dc3427d2a05f50f9293875fce471e6cbf89b84b0..04227c4c12ccfd346dc168aa88a29a1e860aec75 100644 (file)
@@ -1,5 +1,5 @@
-WIDTH=25
-HEIGHT=25
+WIDTH=20
+HEIGHT=20
 ENTRY=2,5
 #EXIT=100,100
 OUTPUT_FILE=test
@@ -7,9 +7,9 @@ PERFECT=False
 SEED=111
 TILEMAP_WALL_SIZE=2,1
 TILEMAP_CELL_SIZE=4,2
-TILEMAP_FULL="{1000,100,100:1000,1000,1000}###{1000,1000,1000:1000,1000,1000}###"
-TILEMAP_FULL="{100,1000,100:1000,1000,1000}###{1000,1000,1000:1000,1000,1000}###"
-TILEMAP_FULL="{100,100,1000:1000,1000,1000}###{1000,1000,1000:1000,1000,1000}###"
+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_EMPTY="{0,0,0:0,0,0}      "
 TILEMAP_EMPTY="{0,0,0:0,0,0}      "
 TILEMAP_EMPTY="{0,0,0:0,0,0}      "
@@ -18,11 +18,19 @@ TILEMAP_BACKGROUND="{1000,1000,1000:0,0,0}##      "
 TILEMAP_BACKGROUND="{1000,1000,1000:0,0,0}######  "
 TILEMAP_BACKGROUND="{1000,1000,1000:0,0,0}    ##  "
 TILEMAP_BACKGROUND="{1000,1000,1000:0,0,0}##  ##  "
-MAZE_PATTERN=" #   # "
-MAZE_PATTERN="  # #  "
-MAZE_PATTERN="   #   "
-MAZE_PATTERN="       "
-MAZE_PATTERN=" #  #  "
-MAZE_PATTERN="       "
+#MAZE_PATTERN=" #   # "
+#MAZE_PATTERN="  # #  "
+#MAZE_PATTERN="   #   "
+#MAZE_PATTERN="       "
+#MAZE_PATTERN=" #  #  "
+#MAZE_PATTERN="       "
+#MAZE_PATTERN="#  #  #"
+#MAZE_PATTERN=" ## ## "
+MAZE_PATTERN="#######"
+MAZE_PATTERN="# # # #"
 MAZE_PATTERN="#  #  #"
-MAZE_PATTERN=" ## ## "
+MAZE_PATTERN="#     #"
+MAZE_PATTERN="## ####"
+MAZE_PATTERN="#     #"
+MAZE_PATTERN="#  #  #"
+MAZE_PATTERN="#######"