]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Config and display tweaks master
authorAxy <gilliardmarthey.axel@gmail.com>
Thu, 19 Feb 2026 11:28:21 +0000 (12:28 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Thu, 19 Feb 2026 11:28:21 +0000 (12:28 +0100)
__main__.py
amazeing/config/__init__.py [new file with mode: 0644]
amazeing/config/config_parser.py [new file with mode: 0644]
amazeing/config/parser_combinator.py [new file with mode: 0644]
amazeing/maze_class/maze.py
amazeing/maze_display/TTYdisplay.py
amazeing/maze_display/backend.py
example.conf [new file with mode: 0644]

index 1a252ec93ae9abb385ee8251db46906fa1b03f63..e15277dc5aa79532b25e0893795f24c16099cfed 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py
new file mode 100644 (file)
index 0000000..ded3946
--- /dev/null
@@ -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 (file)
index 0000000..4c9bbea
--- /dev/null
@@ -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)
index 772a454f478c044637fc1ea1dbafdc07d1cbb217..70b84f89e51d88bd36b07ee0dc104a80ab145f4b 100644 (file)
@@ -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()
         )
index 57d3f47defd85bc5e8783ba66e61a6e467f1ed95..ccbc7669a1349dd50a3b08d795c74032a37f19e7 100644 (file)
@@ -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:
index 7f17bed94936a20a4f74e0134d77e41b945b9924..56d173bc42b70d534aaeb14b679a0bed314568de 100644 (file)
@@ -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 (file)
index 0000000..4dd6d98
--- /dev/null
@@ -0,0 +1,7 @@
+WIDTH=250
+HEIGHT=100
+ENTRY=2,5
+#EXIT=100,100
+OUTPUT_FILE=test
+PERFECT=False
+SEED=111