From a74cf7d4bd451074a5ab78372a9c5080fc33f7b9 Mon Sep 17 00:00:00 2001 From: Axy Date: Thu, 5 Mar 2026 22:13:18 +0100 Subject: [PATCH] Background, fixes, python is dumb and the lib too --- __main__.py | 3 +- amazeing/config/config_parser.py | 45 ++++++- amazeing/config/parser_combinator.py | 13 ++- amazeing/maze_display/TTYdisplay.py | 92 ++++++++++++--- amazeing/maze_display/backend.py | 6 + amazeing/maze_display/layout.py | 169 ++++++++++++++++++++++----- example.conf | 19 ++- 7 files changed, 282 insertions(+), 65 deletions(-) diff --git a/__main__.py b/__main__.py index f7e494f..21eed84 100644 --- a/__main__.py +++ b/__main__.py @@ -26,7 +26,7 @@ maze = Maze(dims) maze.outline() -pattern = Pattern(Pattern.FT_PATTERN).centered_for(dims) +pattern = Pattern(config.maze_pattern).centered_for(dims) pattern.fill(maze) walls_const = set(maze.walls_full()) @@ -34,6 +34,7 @@ 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) backend.set_style(tilemaps.empty) for wall in maze.all_walls(): diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py index 4a4f37f..6ab4387 100644 --- a/amazeing/config/config_parser.py +++ b/amazeing/config/config_parser.py @@ -106,6 +106,7 @@ def parse_colored_line( """ returns a list of a color pair variable associated with its string """ + color_prefix = delimited( tag("{"), cut(spaced(parse_color_pair)), cut(tag("}")) ) @@ -124,7 +125,7 @@ def parse_colored_line( ), ), lambda a, b: a + b, - "", + lambda: "", ) return spaced( @@ -134,6 +135,20 @@ def parse_colored_line( )(s) +def parse_str_line(s: str) -> ParseResult[str]: + return spaced( + delimited( + tag('"'), + recognize( + many_count( + none_of('"\n'), + ) + ), + tag('"'), + ) + )(s) + + class ConfigException(Exception): pass @@ -237,20 +252,28 @@ def DefaultedStrField[T]( 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 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 l in vals for e in l] + return ( + [e for l in vals for e in l] + if len(vals) > 0 + else self.default() + ) return Inner ColoredLineField = ListParser(parse_colored_line) +PatternField = ListParser(parse_str_line) + def line_parser[T]( fields: dict[str, ConfigField[T]], @@ -295,7 +318,7 @@ def fields_parser( fold( parse_line, fold_fn, - {name: [] for name in fields.keys()}, + lambda: {name: [] for name in fields.keys()}, ), )(s) @@ -317,13 +340,17 @@ class Config: tilemap_cell_size: IVec2 tilemap_full: list[ColoredLine] tilemap_empty: list[ColoredLine] + tilemap_background_size: IVec2 tilemap_background: list[ColoredLine] + maze_pattern: list[str] def __init__(self) -> None: pass @staticmethod def parse(s: str) -> "Config": + from amazeing.maze_class import maze_pattern + fields = parser_complete( fields_parser( { @@ -351,10 +378,16 @@ class Config: ColoredLineField, ['"{BLACK:BLACK} "', '"{BLACK:BLACK} "'], ), + "TILEMAP_BACKGROUND_SIZE": DefaultedField( + CoordField, IVec2(4, 2) + ), "TILEMAP_BACKGROUND": DefaultedStrField( ColoredLineField, ['"{BLACK:BLACK} "', '"{BLACK:BLACK} "'], ), + "MAZE_PATTERN": DefaultedField( + PatternField, maze_pattern.Pattern.FT_PATTERN + ), } ) )(s) diff --git a/amazeing/config/parser_combinator.py b/amazeing/config/parser_combinator.py index a025da8..45569b2 100644 --- a/amazeing/config/parser_combinator.py +++ b/amazeing/config/parser_combinator.py @@ -94,14 +94,14 @@ def alt[T](*choices: Parser[T]) -> Parser[T]: def fold[T, R]( p: Parser[T], f: Callable[[R, T], R], - acc: R, + acc_init: Callable[[], R], min_n: int = 0, max_n: int | None = None, sep: Parser[Any] = null_parser, ) -> Parser[R]: # no clean way to do this with lambdas i could figure out :< def inner(s: str) -> ParseResult[R]: - nonlocal acc + acc = acc_init() count: int = 0 curr_p: Parser[T] = p while max_n is None or count < max_n: @@ -125,7 +125,12 @@ def many[T]( sep: Parser[Any] = null_parser, ) -> Parser[list[T]]: return fold( - parser_map(lambda e: [e], p), list.__add__, [], min_n, max_n, sep + parser_map(lambda e: [e], p), + list.__add__, + lambda: [], + min_n, + max_n, + sep, ) @@ -135,7 +140,7 @@ def many_count[T]( max_n: int | None = None, sep: Parser[Any] = null_parser, ) -> Parser[int]: - return fold(value(1, p), int.__add__, 0, min_n, max_n, sep) + return fold(value(1, p), int.__add__, lambda: 0, min_n, max_n, sep) def seq[T](*parsers: Parser[T]) -> Parser[str]: diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index 88354db..d51607b 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -1,4 +1,5 @@ -from collections.abc import Generator, Iterable +from collections.abc import Callable, Generator, Iterable +from sys import stderr from ..config.config_parser import Color, Config, ColoredLine, ColorPair from amazeing.maze_display.layout import ( BInt, @@ -7,6 +8,10 @@ from amazeing.maze_display.layout import ( HBox, VBox, layout_fair, + layout_priority, + layout_sort_chunked, + layout_sort_shuffled, + layout_split, vpad_box, hpad_box, ) @@ -56,6 +61,7 @@ class Tile: ) -> None: if size.x <= 0 or size.y <= 0: return + print(src, dst, size, window.getmaxyx(), file=stderr) self.__pad.overwrite( window, *src.yx(), *dst.yx(), *(dst + size - IVec2.splat(1)).yx() ) @@ -87,7 +93,9 @@ class Tile: for y_size, y_offset in size_offset_iter(src.y, size.y, dims.y): sub_size = IVec2(x_size, y_size) offset = IVec2(x_offset, y_offset) - self.blit(src + offset, dst + offset, sub_size, window) + self.blit( + (src + offset) % dims, dst + offset, sub_size, window + ) class SubTile(Tile): @@ -129,14 +137,24 @@ class MazeTileMap: def tile_size(self, pos: IVec2) -> IVec2: return (pos + 1) % 2 * self.__wall_dim + pos % 2 * self.__cell_dim - def draw_at(self, pos: IVec2, idx: int, window: curses.window) -> None: + def draw_at(self, at: IVec2, idx: int, window: curses.window) -> None: self.__tiles[idx].blit( - self.src_coord(pos), - self.dst_coord(pos), - self.tile_size(pos), + self.src_coord(at), + self.dst_coord(at), + self.tile_size(at), window, ) + def draw_at_wrapping( + self, + start: IVec2, + at: IVec2, + into: IVec2, + idx: int, + window: curses.window, + ) -> None: + self.__tiles[idx].blit_wrapping(start, at, into, window) + class ScrollablePad: def __init__( @@ -158,7 +176,7 @@ class ScrollablePad: IVec2.with_op(max)(self.__pos, dims - self.dims()), IVec2.splat(0) ) - def refresh(self, at: IVec2, into: IVec2) -> None: + def present(self, at: IVec2, into: IVec2, window: curses.window) -> None: if self.constrained: self.clamp(into) @@ -173,7 +191,8 @@ class ScrollablePad: return draw_start = at + win_start draw_end = draw_start + draw_dim - IVec2.splat(1) - self.pad.refresh( + self.pad.overwrite( + window, *pad_start.yx(), *draw_start.yx(), *draw_end.yx(), @@ -256,17 +275,26 @@ class TileMaps: ) -> None: mazetile_dims = config.tilemap_wall_size + config.tilemap_cell_size - def new_tilemap(lines: list[ColoredLine]) -> Tile: + def new_tilemap(lines: list[ColoredLine], dim: IVec2) -> Tile: return Tile( [ [(s, pair_map[color_pair]) for color_pair, s in line] for line in lines ], - mazetile_dims, + dim, ) - self.empty: int = backend.add_style(new_tilemap(config.tilemap_empty)) - self.full: int = backend.add_style(new_tilemap(config.tilemap_full)) + 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 + ) + ) class TTYBackend(Backend[int]): @@ -286,24 +314,47 @@ class TTYBackend(Backend[int]): curses.curs_set(0) self.__screen.keypad(True) + self.__scratch: curses.window = curses.newpad(1, 1) self.__pad: ScrollablePad = ScrollablePad(dims) self.__dims = maze_dims maze_box = FBox( IVec2(BInt(dims.x), BInt(dims.y)), - self.__pad.refresh, + lambda at, into: self.__pad.present(at, into, self.__scratch), + ) + filler_box = FBox( + IVec2(BInt(0, True), BInt(0, True)), + lambda at, into: ( + None + if self.__filler is None + else self.__tilemap.draw_at_wrapping( + at, at, into, self.__filler, self.__scratch + ) + ), ) - self.__layout: Box = VBox.noassoc( - layout_fair, + f: Callable[[int], int] = lambda e: e + layout = layout_split( + layout_fair, layout_sort_chunked(layout_fair, layout_priority, f) + ) + self.__layout: Box = VBox( + layout, [ - vpad_box(), - HBox.noassoc(layout_fair, [hpad_box(), maze_box, hpad_box()]), - vpad_box(), + (filler_box, 0), + ( + HBox( + layout, + [(filler_box, 0), (maze_box, 1), (filler_box, 0)], + ), + 1, + ), + (filler_box, 0), ], ) self.__resize: bool = False + self.__filler: None | int = None + def __del__(self): curses.curs_set(1) curses.nocbreak() @@ -311,6 +362,9 @@ class TTYBackend(Backend[int]): curses.echo() curses.endwin() + def set_filler(self, style: int) -> None: + self.__filler = style + def add_style(self, style: Tile) -> int: return self.__tilemap.add_tile(style) @@ -329,7 +383,9 @@ class TTYBackend(Backend[int]): self.__screen.erase() self.__screen.refresh() y, x = self.__screen.getmaxyx() + self.__scratch.resize(y, x) self.__layout.laid_out(IVec2(0, 0), IVec2(x, y)) + self.__scratch.overwrite(self.__screen) def event(self, timeout_ms: int = -1) -> BackendEvent | None: self.__screen.timeout(timeout_ms) diff --git a/amazeing/maze_display/backend.py b/amazeing/maze_display/backend.py index e4497f1..910058f 100644 --- a/amazeing/maze_display/backend.py +++ b/amazeing/maze_display/backend.py @@ -5,6 +5,9 @@ from typing import Type, cast class IVec2[T = int]: + def copy(self, inner_copy: Callable[[T], T] = lambda e: e) -> "IVec2[T]": + return IVec2(inner_copy(self.x), inner_copy(self.y)) + def __init__(self, x: T, y: T) -> None: self.x: T = x self.y: T = y @@ -13,6 +16,9 @@ class IVec2[T = int]: def splat(n: T) -> "IVec2[T]": return IVec2(n, n) + def __repr__(self) -> str: + return f"{self.x, self.y}" + @staticmethod def with_op( op: Callable[[T, T], T], diff --git a/amazeing/maze_display/layout.py b/amazeing/maze_display/layout.py index 9412db1..16fc870 100644 --- a/amazeing/maze_display/layout.py +++ b/amazeing/maze_display/layout.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from collections.abc import Callable +from sys import stderr from .backend import IVec2 @@ -8,18 +9,43 @@ class BInt: self.val: int = val self.has_flex: bool = has_flex + def __repr__(self) -> str: + return f"{self.val}" + (" flexible" if self.has_flex else "") + + @staticmethod + def vector_sum(elems: list["BInt"]) -> "BInt": + res = BInt( + sum(map(lambda e: e.val, elems)), + any(map(lambda e: e.has_flex, elems)), + ) + return res + + @staticmethod + def vector_max(elems: list["BInt"]) -> "BInt": + res = BInt( + max(map(lambda e: e.val, elems), default=0), + any(map(lambda e: e.has_flex, elems)), + ) + return res + 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]: +def layout_priority[T]( + sizes: list[tuple[BInt, T]], available: int +) -> list[int]: res = [] - for size in sizes: + for size, _ in sizes: size_scaled = min(size.val, available) res.append(size_scaled) available -= size_scaled + for i, (size, _) in enumerate(sizes): + if size.has_flex: + res[i] += available + break return res @@ -30,14 +56,14 @@ def rdiv(a: int, b: int) -> int: 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)) + for idx, (size, _) in sorted(enumerate(sizes), key=lambda e: e[1][0].val): + size_scaled = min(size.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: + count = sum(1 for (e, _) in sizes if e.has_flex) + for idx, (size, _) in enumerate(sizes): + if not size.has_flex: continue size_scaled = rdiv(available, count) res[idx] += size_scaled @@ -46,6 +72,91 @@ def layout_fair[T](sizes: list[tuple[BInt, T]], available: int) -> list[int]: return res +def layout_split[T](sized: Layout[T], flexed: Layout[T]) -> Layout[T]: + def inner(sizes: list[tuple[BInt, T]], available: int) -> list[int]: + flexes = [(BInt(0, e[0].has_flex), e[1]) for e in sizes] + sizes = [(BInt(e[0].val), e[1]) for e in sizes] + res_sizes = sized(sizes, available) + res_flexes = flexed(flexes, available - sum(res_sizes)) + return [a + b for a, b in zip(res_sizes, res_flexes)] + + return inner + + +def layout_sort_shuffled[T]( + init: Layout[T], extract: Callable[[T], int] +) -> Layout[T]: + def inner(sizes: list[tuple[BInt, T]], available: int) -> list[int]: + mapping = [(i, extract(assoc)) for i, (_, assoc) in enumerate(sizes)] + mapping.sort(key=lambda e: e[1]) + sizes = [e for e in sizes] + sizes.sort(key=lambda e: extract(e[1])) + res_init = init(sizes, available) + res = [0 for _ in res_init] + for src, (dst, _) in enumerate(mapping): + res[dst] = res_init[src] + return res + + return inner + + +def layout_mapped[T, U](init: Layout[T], f: Callable[[U], T]) -> Layout[U]: + return lambda sizes, available: init( + list(map(lambda e: (e[0], f(e[1])), sizes)), available + ) + + +def layout_sort_chunked[T]( + per_chunk: Layout[T], + chunk_layout: Layout[list[tuple[BInt, T]]], + extract: Callable[[T], int], +) -> Layout[T]: + def layout_chunk_seq( + sizes: list[tuple[BInt, T]], available: int + ) -> list[int]: + chunks: list[tuple[BInt, list[tuple[BInt, T]]]] = [] + i = 0 + curr_chunk = None + + def try_add_curr() -> None: + nonlocal curr_chunk + if curr_chunk is None: + return + chunk = curr_chunk[0] + chunks.append( + ( + BInt.vector_sum([e[0] for e in chunk]), + chunk, + ) + ) + curr_chunk = None + + while i < len(sizes): + val = sizes[i] + _, assoc = val + extracted = extract(assoc) + if curr_chunk is None: + curr_chunk = ([val], extracted) + else: + if extracted == curr_chunk[1]: + curr_chunk[0].append(val) + else: + try_add_curr() + continue + i += 1 + try_add_curr() + + chunk_sizes = chunk_layout(chunks, available) + res = [ + size + for (_, chunk), chunk_layout_size in zip(chunks, chunk_sizes) + for size in per_chunk(chunk, chunk_layout_size) + ] + return res + + return layout_sort_shuffled(layout_chunk_seq, extract) + + class Box(ABC): @abstractmethod def dims(self) -> BVec2: ... @@ -67,14 +178,8 @@ class VBox[T](Box): 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)), - ), + BInt.vector_max([e.x for e in dims]), + BInt.vector_sum([e.y for e in dims]), ) def laid_out(self, at: IVec2, into: IVec2) -> None: @@ -87,7 +192,9 @@ class VBox[T](Box): 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)) + # that copy cost me half an hour, thank you pass by reference + # rust would have prevented that :D + box.laid_out(at.copy(), IVec2(width, height)) at.y += height @@ -105,14 +212,8 @@ class HBox[T](Box): 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)), - ), + BInt.vector_sum([e.x for e in dims]), + BInt.vector_max([e.y for e in dims]), ) def laid_out(self, at: IVec2, into: IVec2) -> None: @@ -146,12 +247,12 @@ class FBox(Box): 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 vpad_box(min_pad: int = 0, cb=lambda _at, _into: None) -> FBox: + return FBox(IVec2(BInt(0), BInt(min_pad, True)), cb) -def hpad_box(min_pad: int = 0) -> FBox: - return FBox(IVec2(BInt(min_pad, True), BInt(0)), lambda _at, _into: None) +def hpad_box(min_pad: int = 0, cb=lambda _at, _into: None) -> FBox: + return FBox(IVec2(BInt(min_pad, True), BInt(0)), cb) def print_cb(at: IVec2, into: IVec2) -> None: @@ -159,9 +260,15 @@ def print_cb(at: IVec2, into: IVec2) -> None: 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]) + a = FBox(IVec2(BInt(8, False), BInt(4, False)), print_cb) + c = HBox.noassoc( + layout_fair, + [ + hpad_box(), + VBox.noassoc(layout_fair, [vpad_box(), a, vpad_box()]), + hpad_box(), + ], + ) 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)) diff --git a/example.conf b/example.conf index 50b57d8..8a41563 100644 --- a/example.conf +++ b/example.conf @@ -1,5 +1,5 @@ -WIDTH=25 -HEIGHT=25 +WIDTH=50 +HEIGHT=50 ENTRY=2,5 #EXIT=100,100 OUTPUT_FILE=test @@ -7,13 +7,22 @@ PERFECT=False SEED=111 TILEMAP_WALL_SIZE=2,1 TILEMAP_CELL_SIZE=4,2 -TILEMAP_FULL="{100,100,100:1000,1000,1000}######" -TILEMAP_FULL="{100,100,100:1000,1000,1000}######" -TILEMAP_FULL="{100,100,100:1000,1000,1000}######" +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_EMPTY="{0,0,0:0,0,0} " TILEMAP_EMPTY="{0,0,0:0,0,0} " TILEMAP_EMPTY="{0,0,0:0,0,0} " +TILEMAP_BACKGROUND_SIZE=4,4 TILEMAP_BACKGROUND="{100,100,100:0,0,0}# " TILEMAP_BACKGROUND="{100,100,100:0,0,0}### " TILEMAP_BACKGROUND="{100,100,100:0,0,0} # " TILEMAP_BACKGROUND="{100,100,100:0,0,0}# # " +MAZE_PATTERN=" # # " +MAZE_PATTERN=" # # " +MAZE_PATTERN=" # " +MAZE_PATTERN=" " +MAZE_PATTERN=" # # " +MAZE_PATTERN=" " +MAZE_PATTERN="# # #" +MAZE_PATTERN=" ## ## " -- 2.53.0