From: Axy Date: Tue, 10 Mar 2026 14:09:32 +0000 (+0100) Subject: Cycleable tilemaps X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=bfe322ed5904fe4192cb829e9e947598d8b9e18f;p=axy%2Fft%2Fa-maze-ing.git Cycleable tilemaps --- diff --git a/__main__.py b/__main__.py index cd2d05d..1830248 100644 --- a/__main__.py +++ b/__main__.py @@ -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) diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py index 8332eab..167699e 100644 --- a/amazeing/config/config_parser.py +++ b/amazeing/config/config_parser.py @@ -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: diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index 57c15d0..a79bcdc 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -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() diff --git a/example.conf b/example.conf index c00a50a..242625a 100644 --- a/example.conf +++ b/example.conf @@ -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=" # # "