From: Axy Date: Wed, 4 Mar 2026 17:52:38 +0000 (+0100) Subject: Dirtiness tracking and config, not yet used X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=c18542e9face9fe2726f16e69ca2924ee64e1134;p=axy%2Fft%2Fa-maze-ing.git Dirtiness tracking and config, not yet used --- diff --git a/__main__.py b/__main__.py index e5b693d..f71a0d0 100644 --- a/__main__.py +++ b/__main__.py @@ -7,24 +7,19 @@ from amazeing import ( maze_make_pacman, maze_make_perfect, ) -from time import sleep -from sys import stderr, stdin +import random from amazeing.config.config_parser import Config from amazeing.maze_class.maze_walls import Cardinal, CellCoord from amazeing.maze_display.TTYdisplay import Tile from amazeing.maze_display.backend import BackendEvent, CloseRequested, IVec2 -# from amazeing.maze_display.layout import example +config = Config.parse(open("./example.conf").read()) -# example() -# exit(0) +if config.seed is not None: + random.seed(config.seed) -# random.seed(42) - -# print(Config.parse(stdin.read()).__dict__) - -dims = (15, 15) +dims = (config.width, config.height) maze = Maze(dims) @@ -61,19 +56,32 @@ style_full = backend.add_style( def clear_backend() -> None: - dims = backend.dims() * 2 + 1 backend.set_style(style_empty) - for x in range(dims.x): - for y in range(dims.y): - backend.draw_tile(IVec2(x, y)) + for wall in maze.walls_dirty(): + if maze.get_wall(wall).is_full(): + continue + for tile in wall.tile_coords(): + backend.draw_tile(tile) + pass def display_maze(maze: Maze) -> None: clear_backend() backend.set_style(style_full) - for wall in maze.walls_full(): + + rewrites = { + wall for wall in maze.walls_dirty() if maze.get_wall(wall).is_full() + } | { + e + for wall in maze.walls_dirty() + for e in wall.neighbours() + if maze._check_coord(e) and maze.get_wall(e).is_full() + } + + for wall in rewrites: for pixel in wall.tile_coords(): backend.draw_tile(pixel) + maze.clear_dirty() backend.present() poll_events(0) @@ -96,7 +104,7 @@ def poll_events(timeout_ms: int = -1) -> None: maze_make_perfect(maze, callback=display_maze) maze_make_pacman(maze, walls_const, callback=display_maze) -while False: +while True: maze_make_perfect(maze, callback=display_maze) maze_make_pacman(maze, walls_const, callback=display_maze) maze._rebuild() diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py index ab3b9bd..2dc9cc6 100644 --- a/amazeing/config/config_parser.py +++ b/amazeing/config/config_parser.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from collections.abc import Callable +from collections.abc import Callable, Generator from typing import Any, Type from .parser_combinator import ( ParseResult, @@ -9,6 +9,7 @@ from .parser_combinator import ( cut, delimited, fold, + many, many_count, none_of, one_of, @@ -32,7 +33,7 @@ def parse_int(s: str) -> ParseResult[int]: return parser_map(int, recognize(many_count(ascii_digit, min_n=1)))(s) -def parse_space(s: str) -> ParseResult[str]: +def multispace0(s: str) -> ParseResult[str]: return recognize(many_count(one_of(" \t")))(s) @@ -40,11 +41,15 @@ def parse_comment(s: str) -> ParseResult[str]: return recognize(seq(tag("#"), many_count(none_of("\n"))))(s) +def spaced[T](parser: Parser[T]) -> Parser[T]: + return delimited(multispace0, parser, multispace0) + + def parse_coord(s: str) -> ParseResult[tuple[int, int]]: return pair( terminated( parse_int, - delimited(parse_space, tag(","), parse_space), + delimited(multispace0, tag(","), multispace0), ), parse_int, )(s) @@ -54,6 +59,76 @@ def parse_path(s: str) -> ParseResult[str]: return recognize(many_count(none_of("\n"), min_n=1))(s) +def char_range(a: str, b: str) -> str: + res = "" + for c in range(ord(a), ord(b) + 1): + res = res + chr(c) + return res + + +def parse_varname(s: str) -> ParseResult[str]: + varstart = "_" + char_range("a", "z") + char_range("A", "Z") + vartail = varstart + char_range("0", "9") + return recognize(seq(one_of(varstart), many_count(one_of(vartail))))(s) + + +type Color = tuple[int, int, int] | str +type ColorPair = tuple[Color, Color] + +type ColoredLine = list[tuple[ColorPair, str]] + + +def parse_color(s: str) -> ParseResult[Color]: + return alt( + parser_map( + lambda l: (l[0], l[1], l[2]), + many(parse_int, 3, 3, spaced(tag(","))), + ), + parse_varname, + )(s) + + +def parse_color_pair(s: str) -> ParseResult[ColorPair]: + return parser_map( + lambda l: (l[0], l[1]), + many(parse_color, 2, 2, spaced(tag(":"))), + )(s) + + +def parse_colored_line( + s: str, +) -> ParseResult[ColoredLine]: + """ + returns a list of a color pair variable associated with its string + """ + color_prefix = delimited( + tag("{"), cut(spaced(parse_color_pair)), cut(tag("}")) + ) + noncolor_str = fold( + alt( + none_of('\n{\\"'), + preceeded( + tag("\\"), + cut( + alt( + value("{", tag("{")), + value("\\", tag("\\")), + value('"', tag('"')), + ) + ), + ), + ), + lambda a, b: a + b, + "", + ) + + return spaced( + delimited( + tag('"'), many(pair(color_prefix, cut(noncolor_str))), tag('"') + ) + )(s) + + class ConfigException(Exception): pass @@ -71,9 +146,8 @@ class ConfigField[T](ABC): def default(self) -> T: if self.__default is None: raise ConfigException( - "Value " - + self.__name - + " not provided, and no default value exists" + f"Value {self.__name} not provided, " + + "and no default value exists" ) return self.__default() @@ -83,7 +157,7 @@ class ConfigField[T](ABC): if len(vals) == 1: return vals[0] raise ConfigException( - "More than one definition of config field " + self.__name + f"More than one definition of config field {self.__name}" ) def name(self) -> str: @@ -120,7 +194,7 @@ def OptionalField[T](cls: Type[ConfigField[T]]) -> Type[ConfigField[T | None]]: def DefaultedField[T]( cls: Type[ConfigField[T]], default: T ) -> Type[ConfigField[T]]: - class Inner(ConfigField[T]): + class Inner(cls): def __init__( self, name: str, @@ -128,11 +202,51 @@ def DefaultedField[T]( ) -> None: super().__init__(name, default) - parse = cls.parse + return Inner + + +def DefaultedStrField[T]( + cls: Type[ConfigField[T]], default_strs: list[str] +) -> Type[ConfigField[T]]: + class Inner(cls): + def __init__( + self, + name: str, + default: Callable[[], T] | None = None, + ) -> None: + super().__init__(name, default) + + def default(self) -> T: + acc = [] + for s in default_strs: + res = self.parse(s) + if res is None or res[1] != "": + raise ConfigException( + "Failed to construct defaulted field " + self.name() + ) + acc.append(res[0]) + return self.merge(acc) return Inner +def ListParser[T](parser: Parser[T]) -> Type[ConfigField[list[T]]]: + class Inner(ConfigField[list[T]]): + 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 Inner + + +ColoredLineField = ListParser(parse_colored_line) + + def line_parser[T]( fields: dict[str, ConfigField[T]], ) -> Parser[tuple[str, T] | None]: @@ -140,10 +254,10 @@ def line_parser[T]( parser_map(lambda _: None, parse_comment), *( preceeded( - seq(tag(name), parse_space, tag("="), parse_space), + seq(tag(name), multispace0, tag("="), multispace0), parser_map( (lambda name: lambda res: (name, res))(name), - cut(field.parse), + cut(terminated(field.parse, multispace0)), ), ) for name, field in fields.items() @@ -194,6 +308,11 @@ class Config: screensaver: bool visual: bool interactive: bool + tilemap_wall_size: tuple[int, int] + tilemap_cell_size: tuple[int, int] + tilemap_full: list[ColoredLine] + tilemap_empty: list[ColoredLine] + tilemap_background: list[ColoredLine] def __init__(self) -> None: pass @@ -213,6 +332,20 @@ class Config: "SCREENSAVER": DefaultedField(BoolField, False), "VISUAL": DefaultedField(BoolField, False), "INTERACTIVE": DefaultedField(BoolField, False), + "TILEMAP_WALL_SIZE": DefaultedField(CoordField, (2, 1)), + "TILEMAP_CELL_SIZE": DefaultedField(CoordField, (2, 1)), + "TILEMAP_FULL": DefaultedStrField( + ColoredLineField, + ['"{WHITE:WHITE} "', '"{WHITE:WHITE} "'], + ), + "TILEMAP_EMPTY": DefaultedStrField( + ColoredLineField, + ['"{BLACK:BLACK} "', '"{BLACK:BLACK} "'], + ), + "TILEMAP_BACKGROUND": DefaultedStrField( + ColoredLineField, + ['"{BLACK:BLACK} "', '"{BLACK:BLACK} "'], + ), } ) )(s) diff --git a/amazeing/maze_class/maze.py b/amazeing/maze_class/maze.py index 70b84f8..75cebe8 100644 --- a/amazeing/maze_class/maze.py +++ b/amazeing/maze_class/maze.py @@ -12,9 +12,12 @@ class Maze: def __init__(self, dims: tuple[int, int]) -> None: self.__width: int = dims[0] self.__height: int = dims[1] + self.__dirty: set[WallCoord] = set() self._clear() def _clear(self) -> None: + if hasattr(self, "horizontal") and hasattr(self, "vertical"): + self.__dirty ^= {wall for wall in self.walls_full()} # list of lines self.horizontal: list[list[MazeWall]] = [ [MazeWall() for _ in range(0, self.__width)] @@ -48,6 +51,7 @@ class Maze: wall = self.get_wall(coord) if wall.network_id is not None: self.networks[wall.network_id].remove_wall(coord) + self.__dirty ^= {coord} wall.network_id = None def all_walls(self) -> Generator[WallCoord]: @@ -84,26 +88,28 @@ class Maze: network.add_wall(id) self.networks[network_id] = network - def fill_wall(self, id: WallCoord) -> None: - wall = self.get_wall(id) + def fill_wall(self, coord: WallCoord) -> None: + wall = self.get_wall(coord) if wall.is_full(): return + self.__dirty ^= {coord} + networks = { cast(NetworkID, neighbour.network_id) - for neighbour in self.get_neighbours(id) + for neighbour in self.get_neighbours(coord) if neighbour.is_full() } if len(networks) == 0: - return self._fill_wall_alone(id, wall) + return self._fill_wall_alone(coord, wall) dest_id = max(networks, key=lambda n: self.networks[n].size()) dest = self.networks[dest_id] wall.network_id = dest_id - dest.add_wall(id) + dest.add_wall(coord) for to_merge in filter(lambda n: n != dest_id, networks): for curr in self.networks[to_merge].walls: @@ -130,6 +136,9 @@ class Maze: def walls_full(self) -> Iterable[WallCoord]: return filter(lambda w: self.get_wall(w).is_full(), self.all_walls()) + def walls_dirty(self) -> Iterable[WallCoord]: + return self.__dirty + def walls_empty(self) -> Iterable[WallCoord]: return filter( lambda w: not self.get_wall(w).is_full(), self.all_walls() @@ -174,3 +183,6 @@ class Maze: else [] ) return leaf_f(WallCoord.a_neighbours) + leaf_f(WallCoord.b_neighbours) + + def clear_dirty(self) -> None: + self.__dirty = set() diff --git a/example.conf b/example.conf index 4dd6d98..8269123 100644 --- a/example.conf +++ b/example.conf @@ -1,7 +1,19 @@ -WIDTH=250 +WIDTH=100 HEIGHT=100 ENTRY=2,5 #EXIT=100,100 OUTPUT_FILE=test 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_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="{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}# # "