]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Cycleable tilemaps
authorAxy <gilliardmarthey.axel@gmail.com>
Tue, 10 Mar 2026 14:09:32 +0000 (15:09 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Tue, 10 Mar 2026 14:09:32 +0000 (15:09 +0100)
__main__.py
amazeing/config/config_parser.py
amazeing/maze_display/TTYdisplay.py
example.conf

index cd2d05ddb3fae5e5dc4c8a00fe602d2aaef723fc..18302487907176281db49182a7d14a6d28a063f3 100644 (file)
@@ -11,7 +11,7 @@ import random
 
 from amazeing.config.config_parser import Config
 from amazeing.maze_class.maze_walls import CellCoord
-from amazeing.maze_display.TTYdisplay import TileMaps, extract_pairs
+from amazeing.maze_display.TTYdisplay import TileCycle, TileMaps, extract_pairs
 from amazeing.maze_display.backend import CloseRequested, IVec2
 
 config = Config.parse(open("./example.conf").read())
@@ -39,9 +39,9 @@ walls_const = set(maze.walls_full())
 backend = TTYBackend(dims, config.tilemap_wall_size, config.tilemap_cell_size)
 pair_map = extract_pairs(config)
 tilemaps = TileMaps(config, pair_map, backend)
-backend.set_filler(tilemaps.filler)
+filler = TileCycle(tilemaps.filler, backend.set_filler)
 
-backend.set_style(tilemaps.empty)
+backend.set_style(tilemaps.empty[0])
 for wall in maze.all_walls():
     for tile in wall.tile_coords():
         backend.draw_tile(tile)
@@ -50,7 +50,7 @@ for cell in CellCoord(dims).all_up_to():
 
 
 def clear_backend() -> None:
-    backend.set_style(tilemaps.empty)
+    backend.set_style(tilemaps.empty[0])
     for wall in maze.walls_dirty():
         if maze.get_wall(wall).is_full():
             continue
@@ -60,7 +60,7 @@ def clear_backend() -> None:
 
 def display_maze(maze: Maze) -> None:
     clear_backend()
-    backend.set_style(tilemaps.full)
+    backend.set_style(tilemaps.full[0])
 
     rewrites = {
         wall for wall in maze.walls_dirty() if maze.get_wall(wall).is_full()
@@ -93,6 +93,11 @@ def poll_events(timeout_ms: int = -1) -> None:
             return
         if isinstance(event, CloseRequested) or event.sym == "q":
             exit(0)
+        if event.sym == "c":
+            filler.cycle()
+        else:
+            continue
+        backend.present()
 
 
 maze_make_perfect(maze, callback=display_maze)
index 8332eabc576706bf4b6c420909ea9fc005a08c6a..167699e93830dcc6ebef535d3f69bb8a8457e79b 100644 (file)
@@ -14,6 +14,7 @@ from .parser_combinator import (
     many,
     many_count,
     none_of,
+    null_parser,
     one_of,
     pair,
     parser_complete,
@@ -85,6 +86,7 @@ type Color = tuple[int, int, int] | str
 type ColorPair = tuple[Color, Color]
 
 type ColoredLine = list[tuple[ColorPair, str]]
+type Grouped[T] = tuple[int, T]
 
 
 def parse_color(s: str) -> ParseResult[Color]:
@@ -159,28 +161,38 @@ def parse_str_line(s: str) -> ParseResult[str]:
     )(s)
 
 
+def grouped_parser[T](parser: Parser[T]) -> Parser[Grouped[T]]:
+    return pair(alt(spaced(parse_int), value(0, null_parser)), parser)
+
+
 class ConfigException(Exception):
     pass
 
 
-class ConfigField[T](ABC):
+class ConfigField[T, U = T](ABC):
     def __init__(
-        self, name: str, default: Callable[[], T] | None = None
+        self,
+        name: str,
     ) -> None:
         self.__name = name
-        self.__default = default
 
     @abstractmethod
     def parse(self, s: str) -> ParseResult[T]: ...
 
-    def default(self) -> T:
-        if self.__default is None:
-            raise ConfigException(
-                f"Value {self.__name} not provided, "
-                + "and no default value exists"
-            )
-        return self.__default()
+    def default(self) -> U:
+        raise ConfigException(
+            f"Value {self.__name} not provided, "
+            + "and no default value exists"
+        )
+
+    @abstractmethod
+    def merge(self, vals: list[T]) -> U: ...
 
+    def name(self) -> str:
+        return self.__name
+
+
+class SimpleField[T](ConfigField[T, T]):
     def merge(self, vals: list[T]) -> T:
         if len(vals) == 0:
             return self.default()
@@ -190,63 +202,48 @@ class ConfigField[T](ABC):
             f"More than one definition of config field {self.__name}"
         )
 
-    def name(self) -> str:
-        return self.__name
-
 
-class IntField(ConfigField[int]):
+class IntField(SimpleField[int]):
     def parse(self, s: str) -> ParseResult[int]:
         return parse_int(s)
 
 
-class BoolField(ConfigField[bool]):
+class BoolField(SimpleField[bool]):
     def parse(self, s: str) -> ParseResult[bool]:
         return parse_bool(s)
 
 
-class CoordField(ConfigField[IVec2]):
+class CoordField(SimpleField[IVec2]):
     def parse(self, s: str) -> ParseResult[IVec2]:
         return parse_coord(s)
 
 
-class PathField(ConfigField[str]):
+class PathField(SimpleField[str]):
     def parse(self, s: str) -> ParseResult[str]:
         return parse_path(s)
 
 
-def OptionalField[T](cls: Type[ConfigField[T]]) -> Type[ConfigField[T | None]]:
-    class Inner(ConfigField[T | None]):
-        parse = cls.parse
-
-    return DefaultedField(Inner, None)
+def OptionalField[T, U](
+    cls: Type[ConfigField[T, U]],
+) -> Type[ConfigField[T, U | None]]:
+    return DefaultedField(cls, None)
 
 
-def DefaultedField[T](
-    cls: Type[ConfigField[T]], default: T
-) -> Type[ConfigField[T]]:
+def DefaultedField[T, U](
+    cls: Type[ConfigField[T, U]], default: U
+) -> Type[ConfigField[T, U]]:
     class Inner(cls):  # type: ignore
-        def __init__(
-            self,
-            name: str,
-            default: Callable[[], T] = lambda: default,
-        ) -> None:
-            super().__init__(name, default)
+        def default(self) -> U:
+            return default
 
     return Inner
 
 
-def DefaultedStrField[T](
-    cls: Type[ConfigField[T]], default_strs: list[str]
-) -> Type[ConfigField[T]]:
+def DefaultedStrField[T, U](
+    cls: Type[ConfigField[T, U]], default_strs: list[str]
+) -> Type[ConfigField[T, U]]:
     class Inner(cls):  # type: ignore
-        def __init__(
-            self,
-            name: str,
-            default: Callable[[], T] | None = None,
-        ) -> None:
-            super().__init__(name, default)
-
-        def default(self) -> T:
+        def default(self) -> U:
             acc = []
             for s in default_strs:
                 res = self.parse(s)
@@ -260,16 +257,37 @@ def DefaultedStrField[T](
     return Inner
 
 
+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
+        def __init__(self, name: str) -> None:
+            self.__inner = cls(name)
+            super().__init__(name)
+
+        def parse(self, s: str) -> ParseResult[T]:
+            return self.__inner.parse(s)
+
+        def default(self) -> V:
+            return mapping(self.__inner.default())
+
+        def merge(self, vals: list[T]) -> V:
+            return mapping(self.__inner.merge(vals))
+
+    return Inner
+
+
 def ListParser[T](parser: Parser[T]) -> Type[ConfigField[list[T]]]:
     class Inner(ConfigField[list[T]]):
-        def __init__(
-            self, name: str, default: Callable[[], list[T]] | None = lambda: []
-        ) -> None:
-            super().__init__(name, default)
+        def __init__(self, name: str) -> None:
+            super().__init__(name)
 
         def parse(self, s: str) -> ParseResult[list[T]]:
             return parser_map(lambda e: [e], parser)(s)
 
+        def default(self) -> list[T]:
+            return []
+
         def merge(self, vals: list[list[T]]) -> list[T]:
             return (
                 [e for val in vals for e in val]
@@ -280,7 +298,18 @@ def ListParser[T](parser: Parser[T]) -> Type[ConfigField[list[T]]]:
     return Inner
 
 
-ColoredLineField = ListParser(parse_colored_line)
+def map_grouped[T](vals: list[Grouped[T]]) -> list[list[T]]:
+    res: dict[int, list[T]] = {}
+    for group, elem in vals:
+        if group not in res:
+            res[group] = []
+        res[group].append(elem)
+    return list(res.values())
+
+
+ColoredLineField = MappedField(
+    ListParser(grouped_parser(parse_colored_line)), map_grouped
+)
 
 PatternField = ListParser(parse_str_line)
 
@@ -349,10 +378,10 @@ class Config:
     interactive: bool
     tilemap_wall_size: IVec2
     tilemap_cell_size: IVec2
-    tilemap_full: list[ColoredLine]
-    tilemap_empty: list[ColoredLine]
+    tilemap_full: list[list[ColoredLine]]
+    tilemap_empty: list[list[ColoredLine]]
     tilemap_background_size: IVec2
-    tilemap_background: list[ColoredLine]
+    tilemap_background: list[list[ColoredLine]]
     maze_pattern: list[str]
 
     def __init__(self) -> None:
index 57c15d05dbbf60d3c75bfca76d1bb657720826a2..a79bcdcd66543581eaa10c4ad1e360067bd201a2 100644 (file)
@@ -204,11 +204,15 @@ class ScrollablePad:
 def extract_pairs(
     config: Config, extra_colors: Iterable[ColorPair] = []
 ) -> dict[ColorPair, int]:
-    all_tilemaps = (
-        config.tilemap_empty,
-        config.tilemap_full,
-        config.tilemap_background,
-    )
+    all_tilemaps = [
+        e
+        for tilemaps in (
+            config.tilemap_empty,
+            config.tilemap_full,
+            config.tilemap_background,
+        )
+        for e in tilemaps
+    ]
     pairs = {
         pair
         for tilemap in all_tilemaps
@@ -280,19 +284,34 @@ class TileMaps:
                 dim,
             )
 
-        self.empty: int = backend.add_style(
-            new_tilemap(config.tilemap_empty, mazetile_dims)
-        )
-        self.full: int = backend.add_style(
-            new_tilemap(config.tilemap_full, mazetile_dims)
-        )
-        self.filler: int = backend.add_style(
-            new_tilemap(
-                config.tilemap_background, config.tilemap_background_size
+        def add_style(tilemap, size=mazetile_dims):
+            return backend.add_style(new_tilemap(tilemap, size))
+
+        self.empty: list[int] = list(map(add_style, config.tilemap_empty))
+        self.full: list[int] = list(map(add_style, config.tilemap_full))
+        self.filler: list[int] = list(
+            map(
+                lambda e: add_style(e, config.tilemap_background_size),
+                config.tilemap_background,
             )
         )
 
 
+class TileCycle:
+    def __init__[T](
+        self, styles: list[T], cb: Callable[[T], None], i=0
+    ) -> None:
+        self.__styles = styles
+        self.__cb = cb
+        self.__i = i
+        cb(styles[i])
+
+    def cycle(self, by: int = 1):
+        self.__i += by
+        self.__i %= len(self.__styles)
+        self.__cb(self.__styles[self.__i])
+
+
 class TTYBackend(Backend[int]):
     def __init__(
         self, maze_dims: IVec2, wall_dim: IVec2, cell_dim: IVec2
@@ -369,6 +388,8 @@ class TTYBackend(Backend[int]):
         curses.endwin()
 
     def set_filler(self, style: int) -> None:
+        if self.__filler == style:
+            return
         self.__filler = style
         for box in self.__filler_boxes:
             box.mark_dirty()
index c00a50abc40a5b38c6b87509983478c07953f1a1..242625ab6f318ee1abbc9ef44128fdb1aed1bee4 100644 (file)
@@ -18,6 +18,10 @@ 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}##  ##  "
+TILEMAP_BACKGROUND=1"{100,1000,1000:0,0,0}##      "
+TILEMAP_BACKGROUND=1"{100,1000,1000:0,0,0}######  "
+TILEMAP_BACKGROUND=1"{100,1000,1000:0,0,0}    ##  "
+TILEMAP_BACKGROUND=1"{100,1000,1000:0,0,0}##  ##  "
 
 #MAZE_PATTERN=" #   # "
 #MAZE_PATTERN="  # #  "