From: Axy Date: Thu, 19 Feb 2026 11:28:21 +0000 (+0100) Subject: Config and display tweaks X-Git-Url: https://git.uwuaxy.net/sitemap.xml?a=commitdiff_plain;h=2227a85a5ebe60e123afd32754a2dd194d809d74;p=axy%2Fft%2Fa-maze-ing.git Config and display tweaks --- diff --git a/__main__.py b/__main__.py index 1a252ec..e15277d 100644 --- a/__main__.py +++ b/__main__.py @@ -6,10 +6,16 @@ from amazeing import ( maze_make_perfect, ) from time import sleep +from sys import stdin + +from amazeing.config.config_parser import Config +from amazeing.maze_class.maze_walls import Cardinal, CellCoord # random.seed(42) -dims = (50, 15) +# print(Config.parse(stdin.read()).__dict__) + +dims = (25, 25) maze = Maze(dims) @@ -31,8 +37,9 @@ def display_maze(maze: Maze) -> None: sleep(0.05) -# maze_make_perfect(maze, callback=display_maze) -maze_make_perfect(maze) -maze_make_pacman(maze, walls_const, callback=display_maze) -maze._rebuild() -display_maze(maze) +maze_make_perfect(maze, 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() diff --git a/amazeing/config/__init__.py b/amazeing/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py new file mode 100644 index 0000000..ded3946 --- /dev/null +++ b/amazeing/config/config_parser.py @@ -0,0 +1,215 @@ +from abc import ABC, abstractmethod +from collections.abc import Callable, Iterable +from typing import Any, Type, cast +from dataclasses import dataclass +from .parser_combinator import ( + ParseResult, + Parser, + alt, + ascii_digit, + cut, + delimited, + fold, + many_count, + none_of, + one_of, + pair, + parser_complete, + parser_default, + parser_map, + preceeded, + recognize, + seq, + tag, + terminated, + value, +) + + +def parse_bool(s: str) -> ParseResult[bool]: + return alt(value(True, tag("True")), value(False, tag("False")))(s) + + +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]: + return recognize(many_count(one_of(" \t")))(s) + + +def parse_comment(s: str) -> ParseResult[str]: + return recognize(seq(tag("#"), many_count(none_of("\n"))))(s) + + +def parse_coord(s: str) -> ParseResult[tuple[int, int]]: + return pair( + terminated( + parse_int, + delimited(parse_space, tag(","), parse_space), + ), + parse_int, + )(s) + + +def parse_path(s: str) -> ParseResult[str]: + return recognize(many_count(none_of("\n"), min_n=1))(s) + + +class ConfigException(Exception): + pass + + +class ConfigField[T](ABC): + def __init__( + self, name: str, default: Callable[[], T] | None = None + ) -> 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( + "Value " + + self.__name + + " not provided, and no default value exists" + ) + return self.__default() + + def merge(self, vals: list[T]) -> T: + if len(vals) == 0: + return self.default() + if len(vals) == 1: + return vals[0] + raise ConfigException( + "More than one definition of config field " + self.__name + ) + + def name(self) -> str: + return self.__name + + +class IntField(ConfigField[int]): + parse = lambda self, s: parse_int(s) + + +class BoolField(ConfigField[bool]): + parse = lambda self, s: parse_bool(s) + + +class CoordField(ConfigField[tuple[int, int]]): + parse = lambda self, s: parse_coord(s) + + +class PathField(ConfigField[str]): + parse = lambda self, s: 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 DefaultedField[T]( + cls: Type[ConfigField[T]], default: T +) -> Type[ConfigField[T]]: + class Inner(ConfigField[T]): + def __init__( + self, + name: str, + default: Callable[[], T] = lambda: default, + ) -> None: + super().__init__(name, default) + + parse = cls.parse + + return Inner + + +def line_parser( + fields: dict[str, ConfigField[Any]], +) -> Parser[tuple[str, Any] | 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) + ), + ) + for name, field in fields.items() + ), + ) + + +def fields_parser( + fields_raw: dict[str, type[ConfigField[Any]]], +) -> Parser[dict[str, Any]]: + fields = {key: cls(key) for key, cls in fields_raw.items()} + parse_line = terminated(line_parser(fields), cut(tag("\n"))) + + def inner(s: str) -> ParseResult[dict[str, Any]]: + def fold_fn( + acc: dict[str, list[Any]], elem: tuple[str, Any] | None + ) -> dict[str, list[Any]]: + if elem is not None: + acc[elem[0]].append(elem[1]) + return acc + + return parser_map( + lambda res: { + name: fields[name].merge(values) + for name, values in res.items() + }, + fold( + parse_line, + fold_fn, + {name: [] for name in fields.keys()}, + ), + )(s) + + return inner + + +class Config: + width: int + height: int + entry: tuple[int, int] | None + exit: tuple[int, int] | None + output_file: str | None + perfect: bool + seed: int | None + + def __init__(self) -> None: + pass + + @staticmethod + def parse(s: str) -> "Config": + fields = parser_complete( + fields_parser( + { + "WIDTH": IntField, + "HEIGHT": IntField, + "ENTRY": OptionalField(CoordField), + "EXIT": OptionalField(CoordField), + "OUTPUT_FILE": PathField, + "PERFECT": BoolField, + "SEED": OptionalField(IntField), + } + ) + )(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 + + return res diff --git a/amazeing/config/parser_combinator.py b/amazeing/config/parser_combinator.py new file mode 100644 index 0000000..4c9bbea --- /dev/null +++ b/amazeing/config/parser_combinator.py @@ -0,0 +1,185 @@ +from collections.abc import Callable +from typing import Any, cast + + +type ParseResult[T] = tuple[T, str] | None +type Parser[T] = Callable[[str], ParseResult[T]] + + +class ParseError(Exception): + def __init__(self, msg: str, at: str) -> None: + self.msg: str = msg + self.at: str = at + super().__init__(f"{msg}\n\nat: {at[:40]}") + + +def option_map[T, R](f: Callable[[T], R], val: T | None) -> R | None: + return f(val) if val is not None else None + + +def parser_map[T, M](m: Callable[[T], M], p: Parser[T]) -> Parser[M]: + return lambda s: option_map(lambda res: (m(res[0]), res[1]), p(s)) + + +def parser_flatten[T](p: Parser[T | None]) -> Parser[T]: + return lambda s: option_map( + lambda res: cast(tuple[T, str], res) if res[0] is not None else None, + p(s), + ) + + +def parser_default[T](p: Parser[T], default: T) -> Parser[T]: + return alt(p, value(default, null_parser)) + + +def parser_complete[T](p: Parser[T]) -> Parser[T]: + def inner(res: tuple[T, str]) -> ParseResult[T]: + if len(res[1]) != 0: + raise ParseError( + "Complete parser still had leftover characters to process", + res[1], + ) + return res + + return lambda s: option_map(inner, p(s)) + + +def recognize[T](p: Parser[T]) -> Parser[str]: + return lambda s: option_map( + lambda rem: (s[: len(s) - len(rem[1])], rem[1]), + p(s), + ) + + +def cut[T](p: Parser[T]) -> Parser[T]: + def inner(s: str) -> ParseResult[T]: + res: ParseResult[T] = p(s) + if res is None: + raise ParseError("Cut error: parser did not complete", s) + return res + + return inner + + +def tag(tag: str) -> Parser[str]: + return lambda s: ( + (s[: len(tag)], s[len(tag) :]) if s.startswith(tag) else None + ) + + +def char(s: str) -> ParseResult[str]: + return (s[0], s[1:]) if len(s) > 0 else None + + +def null_parser(s: str) -> ParseResult[str]: + return ("", s) + + +def value[T, V](val: V, p: Parser[T]) -> Parser[V]: + return parser_map(lambda _: val, p) + + +def alt[T](*choices: Parser[T]) -> Parser[T]: + return lambda s: next( + filter( + lambda e: e is not None, + map(lambda p: p(s), choices), + ), + None, + ) + + +def fold[T, R]( + p: Parser[T], + f: Callable[[R, T], R], + acc: 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 + count: int = 0 + curr_p: Parser[T] = p + while max_n is None or count < max_n: + next: ParseResult[T] = curr_p(s) + if next is None: + break + if count == 0: + curr_p = preceeded(sep, p) + count += 1 + acc = f(acc, next[0]) + s = next[1] + return (acc, s) if count >= min_n else None + + return inner + + +def many[T]( + p: Parser[T], + min_n: int = 0, + max_n: int | None = None, + sep: Parser[Any] = null_parser, +) -> Parser[list[T]]: + return fold( + parser_map(lambda e: [e], p), list.__add__, [], min_n, max_n, sep + ) + + +def many_count[T]( + p: Parser[T], + min_n: int = 0, + 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) + + +def seq[T](*parsers: Parser[T]) -> Parser[str]: + def inner(s: str) -> ParseResult[None]: + for parser in parsers: + res = parser(s) + if res is None: + return None + s = res[1] + return (None, s) + + return recognize(inner) + + +def pair[T, U](p1: Parser[T], p2: Parser[U]) -> Parser[tuple[T, U]]: + return lambda s: option_map( + lambda res1: parser_map(lambda res2: (res1[0], res2), p2)(res1[1]), + p1(s), + ) + + +def preceeded[_T0, T1](p1: Parser[_T0], p2: Parser[T1]) -> Parser[T1]: + return parser_map(lambda res: res[1], pair(p1, p2)) + + +def terminated[T0, _T1](p1: Parser[T0], p2: Parser[_T1]) -> Parser[T0]: + return parser_map(lambda res: res[0], pair(p1, p2)) + + +def delimited[_T0, T1, _T2]( + p1: Parser[_T0], p2: Parser[T1], p3: Parser[_T2] +) -> Parser[T1]: + return preceeded(p1, terminated(p2, p3)) + + +def one_of(chars: str) -> Parser[str]: + return alt(*map(tag, chars)) + + +def none_of(chars: str) -> Parser[str]: + return lambda s: char(s) if one_of(chars)(s) is None else None + + +def ascii_hexdigit(s: str) -> ParseResult[str]: + return one_of("0123456789abcdefABCDEF")(s) + + +def ascii_digit(s: str) -> ParseResult[str]: + return one_of("0123456789")(s) diff --git a/amazeing/maze_class/maze.py b/amazeing/maze_class/maze.py index 772a454..70b84f8 100644 --- a/amazeing/maze_class/maze.py +++ b/amazeing/maze_class/maze.py @@ -36,7 +36,7 @@ class Maze: for wall in walls: self.fill_wall(wall) - def __get_wall(self, coord: WallCoord) -> MazeWall: + def get_wall(self, coord: WallCoord) -> MazeWall: if coord.orientation == Orientation.HORIZONTAL: return self.horizontal[coord.a][coord.b] return self.vertical[coord.a][coord.b] @@ -45,7 +45,7 @@ class Maze: """ removes the wall, without updating network connectivity """ - wall = self.__get_wall(coord) + wall = self.get_wall(coord) if wall.network_id is not None: self.networks[wall.network_id].remove_wall(coord) wall.network_id = None @@ -72,7 +72,7 @@ class Maze: return True def get_walls_checked(self, ids: list[WallCoord]) -> list[MazeWall]: - return [self.__get_wall(id) for id in ids if self._check_coord(id)] + return [self.get_wall(id) for id in ids if self._check_coord(id)] def get_neighbours(self, id: WallCoord) -> list[MazeWall]: return self.get_walls_checked(id.neighbours()) @@ -85,7 +85,7 @@ class Maze: self.networks[network_id] = network def fill_wall(self, id: WallCoord) -> None: - wall = self.__get_wall(id) + wall = self.get_wall(id) if wall.is_full(): return @@ -107,7 +107,7 @@ class Maze: for to_merge in filter(lambda n: n != dest_id, networks): for curr in self.networks[to_merge].walls: - self.__get_wall(curr).network_id = dest_id + self.get_wall(curr).network_id = dest_id dest.add_wall(curr) del self.networks[to_merge] @@ -128,11 +128,11 @@ class Maze: self.fill_wall(WallCoord(orientation, a, b)) def walls_full(self) -> Iterable[WallCoord]: - return filter(lambda w: self.__get_wall(w).is_full(), self.all_walls()) + return filter(lambda w: self.get_wall(w).is_full(), self.all_walls()) def walls_empty(self) -> Iterable[WallCoord]: return filter( - lambda w: not self.__get_wall(w).is_full(), self.all_walls() + lambda w: not self.get_wall(w).is_full(), self.all_walls() ) def wall_bisects(self, wall: WallCoord) -> bool: @@ -158,7 +158,7 @@ class Maze: if wall.is_full() ] ) - >= (3 if self.__get_wall(wall).is_full() else 2) + >= (3 if self.get_wall(wall).is_full() else 2) ) for cell in wall.neighbour_cells() ) diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index 57d3f47..ccbc766 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -6,6 +6,7 @@ class TTYBackend(Backend): """ Takes the ABC Backend and displays the maze in the terminal. """ + def __init__( self, maze_width: int, maze_height: int, style: str = " " ) -> None: diff --git a/amazeing/maze_display/backend.py b/amazeing/maze_display/backend.py index 7f17bed..56d173b 100644 --- a/amazeing/maze_display/backend.py +++ b/amazeing/maze_display/backend.py @@ -13,6 +13,7 @@ class Backend(ABC): defining how the maze should be drawn. (PixelCoord) """ + @abstractmethod def draw_pixel(self, pos: PixelCoord) -> None: pass diff --git a/example.conf b/example.conf new file mode 100644 index 0000000..4dd6d98 --- /dev/null +++ b/example.conf @@ -0,0 +1,7 @@ +WIDTH=250 +HEIGHT=100 +ENTRY=2,5 +#EXIT=100,100 +OUTPUT_FILE=test +PERFECT=False +SEED=111