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__)
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],
]
)
)
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()
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,
@abstractmethod
def parse(self, s: str) -> ParseResult[T]: ...
+
def default(self) -> T:
if self.__default is None:
raise ConfigException(
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]]:
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()
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")))
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,
output_file: str | None
perfect: bool
seed: int | None
+ screensaver: bool
+ visual: bool
+ interactive: bool
def __init__(self) -> None:
pass
"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
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
)
from enum import Enum, auto
-from sys import stderr
from typing import Iterable, Optional, cast
from ..maze_display import IVec2
-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
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)
)
+class ScrollablePad:
+ pass
+
+
class TTYBackend(Backend[int]):
"""
Takes the ABC Backend and displays the maze in the terminal.
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()
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
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
--- /dev/null
+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))
-from typing import Any, Callable
+from typing import Callable
from amazeing import Maze, WallCoord
import random