]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Layout, curses, and fixes
authorAxy <gilliardmarthey.axel@gmail.com>
Mon, 2 Mar 2026 17:21:50 +0000 (18:21 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Mon, 2 Mar 2026 17:21:50 +0000 (18:21 +0100)
__main__.py
amazeing/config/config_parser.py
amazeing/config/parser_combinator.py
amazeing/maze_class/maze_walls.py
amazeing/maze_display/TTYdisplay.py
amazeing/maze_display/backend.py
amazeing/maze_display/layout.py [new file with mode: 0644]
amazeing/maze_make_pacman.py

index 6d94cb7b10b2c59308d45887b8987d826c4af0fc..238d6398950a632d4d3487152f2734761098d500 100644 (file)
@@ -14,6 +14,11 @@ from amazeing.maze_class.maze_walls import Cardinal, CellCoord
 from amazeing.maze_display.TTYdisplay import TTYTile
 from amazeing.maze_display.backend import IVec2
 
+# from amazeing.maze_display.layout import example
+
+# example()
+# exit(0)
+
 # random.seed(42)
 
 # print(Config.parse(stdin.read()).__dict__)
@@ -29,28 +34,26 @@ pattern.fill(maze)
 
 walls_const = set(maze.walls_full())
 
-backend = TTYBackend(IVec2(*dims), IVec2(1, 1), IVec2(2, 2))
+backend = TTYBackend(IVec2(*dims), IVec2(2, 1), IVec2(2, 1))
 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLACK)
 black = curses.color_pair(1)
 empty = (" ", black)
 style_empty = backend.add_style(
     TTYTile(
         [
-            [empty, empty, empty],
-            [empty, empty, empty],
-            [empty, empty, empty],
+            [empty, empty, empty, empty],
+            [empty, empty, empty, empty],
         ]
     )
 )
-curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
+curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_WHITE)
 white = curses.color_pair(2)
-full = ("#", white)
+full = (" ", white)
 style_full = backend.add_style(
     TTYTile(
         [
-            [full, full, full],
-            [full, full, full],
-            [full, full, full],
+            [full, full, full, full],
+            [full, full, full, full],
         ]
     )
 )
@@ -76,9 +79,10 @@ def display_maze(maze: Maze) -> None:
 
 
 maze_make_perfect(maze, callback=display_maze)
-backend.event(-1)
-# maze_make_pacman(maze, walls_const, callback=display_maze)
+maze_make_pacman(maze, walls_const, callback=display_maze)
 while False:
     maze_make_perfect(maze, callback=display_maze)
     maze_make_pacman(maze, walls_const, callback=display_maze)
     maze._rebuild()
+while backend.event(-1) is None:
+    backend.present()
index f20027d1615d1ab27ef2523238fd514073b49f74..ab3b9bd18b628de39343eadd3a8c045a4a4d1441 100644 (file)
@@ -1,7 +1,6 @@
 from abc import ABC, abstractmethod
-from collections.abc import Callable, Iterable
-from typing import Any, Type, cast
-from dataclasses import dataclass
+from collections.abc import Callable
+from typing import Any, Type
 from .parser_combinator import (
     ParseResult,
     Parser,
@@ -68,6 +67,7 @@ class ConfigField[T](ABC):
 
     @abstractmethod
     def parse(self, s: str) -> ParseResult[T]: ...
+
     def default(self) -> T:
         if self.__default is None:
             raise ConfigException(
@@ -91,19 +91,23 @@ class ConfigField[T](ABC):
 
 
 class IntField(ConfigField[int]):
-    parse = lambda self, s: parse_int(s)
+    def parse(self, s: str) -> ParseResult[int]:
+        return parse_int(s)
 
 
 class BoolField(ConfigField[bool]):
-    parse = lambda self, s: parse_bool(s)
+    def parse(self, s: str) -> ParseResult[bool]:
+        return parse_bool(s)
 
 
 class CoordField(ConfigField[tuple[int, int]]):
-    parse = lambda self, s: parse_coord(s)
+    def parse(self, s: str) -> ParseResult[tuple[int, int]]:
+        return parse_coord(s)
 
 
 class PathField(ConfigField[str]):
-    parse = lambda self, s: parse_path(s)
+    def parse(self, s: str) -> ParseResult[str]:
+        return parse_path(s)
 
 
 def OptionalField[T](cls: Type[ConfigField[T]]) -> Type[ConfigField[T | None]]:
@@ -129,19 +133,17 @@ def DefaultedField[T](
     return Inner
 
 
-def line_parser(
-    fields: dict[str, ConfigField[Any]],
-) -> Parser[tuple[str, Any] | None]:
+def line_parser[T](
+    fields: dict[str, ConfigField[T]],
+) -> Parser[tuple[str, T] | None]:
     return alt(
         parser_map(lambda _: None, parse_comment),
         *(
             preceeded(
                 seq(tag(name), parse_space, tag("="), parse_space),
-                # name=name is used to actually capture the value, because
-                # lambdas are by-reference otherwise, including for trivial
-                # value types, much smart very clever :)
                 parser_map(
-                    lambda res, name=name: (name, res), cut(field.parse)
+                    (lambda name: lambda res: (name, res))(name),
+                    cut(field.parse),
                 ),
             )
             for name, field in fields.items()
@@ -150,7 +152,7 @@ def line_parser(
 
 
 def fields_parser(
-    fields_raw: dict[str, type[ConfigField[Any]]],
+    fields_raw: dict[str, type[ConfigField]],
 ) -> Parser[dict[str, Any]]:
     fields = {key: cls(key) for key, cls in fields_raw.items()}
     parse_line = terminated(line_parser(fields), cut(tag("\n")))
@@ -163,11 +165,14 @@ def fields_parser(
                 acc[elem[0]].append(elem[1])
             return acc
 
-        return parser_map(
+        fields_map: Callable[[dict[str, list[Any]]], dict[str, Any]] = (
             lambda res: {
                 name: fields[name].merge(values)
                 for name, values in res.items()
-            },
+            }
+        )
+        return parser_map(
+            fields_map,
             fold(
                 parse_line,
                 fold_fn,
@@ -186,6 +191,9 @@ class Config:
     output_file: str | None
     perfect: bool
     seed: int | None
+    screensaver: bool
+    visual: bool
+    interactive: bool
 
     def __init__(self) -> None:
         pass
@@ -199,16 +207,22 @@ class Config:
                     "HEIGHT": IntField,
                     "ENTRY": OptionalField(CoordField),
                     "EXIT": OptionalField(CoordField),
-                    "OUTPUT_FILE": PathField,
-                    "PERFECT": BoolField,
+                    "OUTPUT_FILE": OptionalField(PathField),
+                    "PERFECT": DefaultedField(BoolField, True),
                     "SEED": OptionalField(IntField),
+                    "SCREENSAVER": DefaultedField(BoolField, False),
+                    "VISUAL": DefaultedField(BoolField, False),
+                    "INTERACTIVE": DefaultedField(BoolField, False),
                 }
             )
         )(s)
         if fields is None:
             raise ConfigException("Failed to parse config")
         res = Config()
-        for key, value in fields[0].items():
-            res.__dict__[key.lower()] = value
+        for key, val in fields[0].items():
+            res.__dict__[key.lower()] = val
+
+        if res.screensaver:
+            res.visual = True
 
         return res
index 4c9bbeacdf61bd53efe63c15e4ef6f85fdcce557..a025da8063b4f59f7e5510a88c0f8a9a8e340901 100644 (file)
@@ -63,7 +63,9 @@ def cut[T](p: Parser[T]) -> Parser[T]:
 
 def tag(tag: str) -> Parser[str]:
     return lambda s: (
-        (s[: len(tag)], s[len(tag) :]) if s.startswith(tag) else None
+        (s[: len(tag)], s[len(tag) :])  # noqa E203
+        if s.startswith(tag)
+        else None
     )
 
 
index 178b619c1daa614b080e015139b83445844a1a2c..5e279c2a199412586d21d80a756102ead98ce913 100644 (file)
@@ -1,5 +1,4 @@
 from enum import Enum, auto
-from sys import stderr
 from typing import Iterable, Optional, cast
 from ..maze_display import IVec2
 
index 8a44ea84b6d5b7915cecd8fc07059e40489b2b4a..0ec946156f4db07f5a2315e248fdc0c43e5ad3f6 100644 (file)
@@ -1,4 +1,13 @@
-from sys import stderr
+from amazeing.maze_display.layout import (
+    BInt,
+    Box,
+    FBox,
+    HBox,
+    VBox,
+    layout_fair,
+    vpad_box,
+    hpad_box,
+)
 from .backend import Backend, IVec2, BackendEvent, KeyboardInput
 import curses
 
@@ -10,8 +19,12 @@ class TTYTile:
     def blit(
         self, src: IVec2, dst: IVec2, size: IVec2, window: curses.window
     ) -> None:
-        for y, line in enumerate(self.__pixels[src.y : src.y + size.y]):
-            for x, (char, attrs) in enumerate(line[src.x : src.x + size.x]):
+        for y, line in enumerate(
+            self.__pixels[src.y : src.y + size.y]  # noqa E203
+        ):
+            for x, (char, attrs) in enumerate(
+                line[src.x : src.x + size.x]  # noqa E203
+            ):
                 window.addch(dst.y + y, dst.x + x, char, attrs)
 
 
@@ -44,6 +57,10 @@ class TTYTileMap:
         )
 
 
+class ScrollablePad:
+    pass
+
+
 class TTYBackend(Backend[int]):
     """
     Takes the ABC Backend and displays the maze in the terminal.
@@ -56,18 +73,35 @@ class TTYBackend(Backend[int]):
         self.__tilemap: TTYTileMap = TTYTileMap(wall_dim, cell_dim)
         self.__style = 0
 
-        dims = self.__tilemap.dst_coord(maze_dims * 2 + 2)
+        dims = self.__tilemap.dst_coord(maze_dims * 2 + 1)
 
         self.__screen: curses.window = curses.initscr()
         curses.start_color()
         curses.noecho()
         curses.cbreak()
+        curses.curs_set(0)
         self.__screen.keypad(True)
 
-        self.__pad: curses.window = curses.newpad(dims.y, dims.x)
+        self.__pad: curses.window = curses.newpad(dims.y + 1, dims.x + 1)
         self.__dims = maze_dims
 
+        maze_box = FBox(
+            IVec2(BInt(dims.x), BInt(dims.y)),
+            lambda at, into: self.__pad.refresh(
+                0, 0, at.y, at.x, at.y + into.y - 1, at.x + into.x - 1
+            ),
+        )
+        self.__layout: Box = VBox.noassoc(
+            layout_fair,
+            [
+                vpad_box(),
+                HBox.noassoc(layout_fair, [hpad_box(), maze_box, hpad_box()]),
+                vpad_box(),
+            ],
+        )
+
     def __del__(self):
+        curses.curs_set(1)
         curses.nocbreak()
         self.__screen.keypad(False)
         curses.echo()
@@ -87,18 +121,16 @@ class TTYBackend(Backend[int]):
 
     def present(self) -> None:
         self.__screen.refresh()
-        self.__pad.refresh(
-            0,
-            0,
-            0,
-            0,
-            min(self.__pad.getmaxyx()[0] - 1, self.__screen.getmaxyx()[0] - 1),
-            min(self.__pad.getmaxyx()[1] - 1, self.__screen.getmaxyx()[1] - 1),
-        )
+        y, x = self.__screen.getmaxyx()
+        self.__layout.laid_out(IVec2(0, 0), IVec2(x, y))
 
     def event(self, timeout_ms: int = -1) -> BackendEvent | None:
         self.__screen.timeout(timeout_ms)
         try:
-            return KeyboardInput(self.__screen.getkey())
+            key = self.__screen.getkey()
+            if key == "KEY_RESIZE":
+                self.__screen.erase()
+                return None
+            return KeyboardInput(key)
         except curses.error:
             return None
index 45d346e9f635ef504d473d66071abb4842a24219..0d053a19001a744b3b5b32da7389a4b8ae04cfd6 100644 (file)
@@ -1,47 +1,51 @@
 from abc import ABC, abstractmethod
 from collections.abc import Callable
 from dataclasses import dataclass
+from typing import Type, cast
 
 
-class IVec2:
-    def __init__(self, x: int, y: int) -> None:
-        self.x: int = x
-        self.y: int = y
+class IVec2[T = int]:
+    def __init__(self, x: T, y: T) -> None:
+        self.x: T = x
+        self.y: T = y
 
     @staticmethod
-    def splat(n: int) -> "IVec2":
+    def splat(n: T) -> "IVec2[T]":
         return IVec2(n, n)
 
     @staticmethod
     def with_op(
-        op: Callable[[int, int], int],
-    ) -> Callable[["IVec2", "int | IVec2"], "IVec2"]:
+        op: Callable[[T, T], T],
+    ) -> Callable[["IVec2[T]", "T | IVec2[T]"], "IVec2[T]"]:
         return lambda self, other: IVec2(
             op(
                 self.x,
                 (
                     other
-                    if isinstance(other, IVec2)
-                    else (other := IVec2.splat(other))
+                    if isinstance(other, type(self))
+                    else (other := type(self).splat(cast(T, other)))
                 ).x,
             ),
-            op(self.y, other.y),
+            op(self.y, cast(IVec2[T], other).y),
         )
 
-    def __mul__(self, other: "int | IVec2") -> "IVec2":
-        return self.with_op(int.__mul__)(self, other)
+    def innertype(self) -> Type:
+        return type(self.x)
 
-    def __add__(self, other: "int | IVec2") -> "IVec2":
-        return self.with_op(int.__add__)(self, other)
+    def __mul__(self, other: "T | IVec2[T]") -> "IVec2[T]":
+        return self.with_op(self.innertype().__mul__)(self, other)
 
-    def __sub__(self, other: "int | IVec2") -> "IVec2":
-        return self.with_op(int.__sub__)(self, other)
+    def __add__(self, other: "T | IVec2[T]") -> "IVec2[T]":
+        return self.with_op(self.innertype().__add__)(self, other)
 
-    def __floordiv__(self, other: "int | IVec2") -> "IVec2":
-        return self.with_op(int.__floordiv__)(self, other)
+    def __sub__(self, other: "T | IVec2[T]") -> "IVec2[T]":
+        return self.with_op(self.innertype().__sub__)(self, other)
 
-    def __mod__(self, other: "int | IVec2") -> "IVec2":
-        return self.with_op(int.__mod__)(self, other)
+    def __floordiv__(self, other: "T| IVec2[T]") -> "IVec2[T]":
+        return self.with_op(self.innertype().__floordiv__)(self, other)
+
+    def __mod__(self, other: "T | IVec2[T]") -> "IVec2[T]":
+        return self.with_op(self.innertype().__mod__)(self, other)
 
 
 @dataclass
diff --git a/amazeing/maze_display/layout.py b/amazeing/maze_display/layout.py
new file mode 100644 (file)
index 0000000..acbb80f
--- /dev/null
@@ -0,0 +1,164 @@
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from .backend import IVec2
+
+
+class BInt:
+    def __init__(self, val: int, has_flex: bool = False) -> None:
+        self.val: int = val
+        self.has_flex: bool = has_flex
+
+
+type BVec2 = IVec2[BInt]
+
+type Layout[T] = Callable[[list[tuple[BInt, T]], int], list[int]]
+
+
+def layout_priority(sizes: list[BInt], available: int) -> list[int]:
+    res = []
+    for size in sizes:
+        size_scaled = min(size.val, available)
+        res.append(size_scaled)
+        available -= size_scaled
+    return res
+
+
+def rdiv(a: int, b: int) -> int:
+    return a // b + (a % b != 0) if a != 0 else 0
+
+
+def layout_fair[T](sizes: list[tuple[BInt, T]], available: int) -> list[int]:
+    res = [0 for _ in sizes]
+    count = len(sizes)
+    for idx, size in sorted(enumerate(sizes), key=lambda e: e[1][0].val):
+        size_scaled = min(size[0].val, rdiv(available, count))
+        res[idx] += size_scaled
+        available -= size_scaled
+        count -= 1
+    count = sum(1 for e in sizes if e[0].has_flex)
+    for idx, size in enumerate(sizes):
+        if not size[0].has_flex:
+            continue
+        size_scaled = rdiv(available, count)
+        res[idx] += size_scaled
+        available -= size_scaled
+        count -= 1
+    return res
+
+
+class Box(ABC):
+    @abstractmethod
+    def dims(self) -> BVec2: ...
+    @abstractmethod
+    def laid_out(self, at: IVec2, into: IVec2) -> None: ...
+
+
+class VBox[T](Box):
+    def __init__(
+        self, layout: Layout, boxes: list[tuple[Box, T]] = []
+    ) -> None:
+        self.boxes: list[tuple[Box, T]] = boxes
+        self.layout: Layout = layout
+
+    @staticmethod
+    def noassoc(layout: Layout, boxes: list[Box]) -> "VBox[None]":
+        return VBox(layout, [(box, None) for box in boxes])
+
+    def dims(self) -> BVec2:
+        dims = [box.dims() for box, _ in self.boxes]
+        return IVec2(
+            BInt(
+                max(map(lambda e: e.x.val, dims)),
+                any(map(lambda e: e.x.has_flex, dims)),
+            ),
+            BInt(
+                sum(map(lambda e: e.y.val, dims)),
+                any(map(lambda e: e.y.has_flex, dims)),
+            ),
+        )
+
+    def laid_out(self, at: IVec2, into: IVec2) -> None:
+        get_width: Callable[[BInt], int] = lambda w: (
+            into.x if w.has_flex and w.val < into.x else min(w.val, into.x)
+        )
+
+        dims = [(box.dims(), assoc) for box, assoc in self.boxes]
+        heights = self.layout([(dim.y, assoc) for dim, assoc in dims], into.y)
+        widths = [(get_width(dim.x), assoc) for dim, assoc in dims]
+
+        for height, (width, _), (box, _) in zip(heights, widths, self.boxes):
+            box.laid_out(at, IVec2(width, height))
+            at.y += height
+
+
+class HBox[T](Box):
+    def __init__(
+        self, layout: Layout, boxes: list[tuple[Box, T]] = []
+    ) -> None:
+        self.boxes: list[tuple[Box, T]] = boxes
+        self.layout: Layout = layout
+
+    @staticmethod
+    def noassoc(layout: Layout, boxes: list[Box]) -> "HBox[None]":
+        return HBox(layout, [(box, None) for box in boxes])
+
+    def dims(self) -> BVec2:
+        dims = [box.dims() for box, _ in self.boxes]
+        return IVec2(
+            BInt(
+                sum(map(lambda e: e.x.val, dims)),
+                any(map(lambda e: e.x.has_flex, dims)),
+            ),
+            BInt(
+                max(map(lambda e: e.y.val, dims)),
+                any(map(lambda e: e.y.has_flex, dims)),
+            ),
+        )
+
+    def laid_out(self, at: IVec2, into: IVec2) -> None:
+        get_height: Callable[[BInt], int] = lambda w: (
+            into.y if w.has_flex and w.val < into.y else min(w.val, into.y)
+        )
+
+        dims = [(box.dims(), assoc) for box, assoc in self.boxes]
+        widths = self.layout([(dim.x, assoc) for dim, assoc in dims], into.x)
+        heights = [(get_height(dim.y), assoc) for dim, assoc in dims]
+
+        for (height, _), width, (box, _) in zip(heights, widths, self.boxes):
+            box.laid_out(at, IVec2(width, height))
+            at.x += width
+
+
+class FBox(Box):
+    def __init__(
+        self, dims: BVec2, cb: Callable[[IVec2, IVec2], None]
+    ) -> None:
+        self.__dims: BVec2 = dims
+        self.__cb: Callable[[IVec2, IVec2], None] = cb
+
+    def dims(self) -> BVec2:
+        return self.__dims
+
+    def laid_out(self, at: IVec2, into: IVec2) -> None:
+        self.__cb(at, into)
+
+
+def vpad_box[T](min_pad: int = 0) -> FBox:
+    return FBox(IVec2(BInt(0), BInt(min_pad, True)), lambda _at, _into: None)
+
+
+def hpad_box(min_pad: int = 0) -> FBox:
+    return FBox(IVec2(BInt(min_pad, True), BInt(0)), lambda _at, _into: None)
+
+
+def print_cb(at: IVec2, into: IVec2) -> None:
+    print(f"at {at.x, at.y}, into {into.x, into.y}")
+
+
+def example() -> None:
+    a = FBox(IVec2(BInt(8, False), BInt(4, True)), print_cb)
+    b = FBox(IVec2(BInt(4, False), BInt(8, False)), print_cb)
+    c = VBox.noassoc(layout_fair, [a, b])
+    c.laid_out(IVec2(0, 0), IVec2(3, 4))
+    c.laid_out(IVec2(0, 0), IVec2(8, 30))
+    c.laid_out(IVec2(0, 0), IVec2(12, 30))
index 622494a45566e2b81a624095501088ebbf56d69f..501434730e140e71294ae52a7c13522708dcb8c2 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Any, Callable
+from typing import Callable
 from amazeing import Maze, WallCoord
 import random