From 280694a524cf2688945c0c8664885f518df5673c Mon Sep 17 00:00:00 2001 From: Axy Date: Thu, 5 Mar 2026 00:48:44 +0100 Subject: [PATCH] Partial refactor for more IVec2 usage, before possible breakage with config --- __main__.py | 6 +-- amazeing/config/config_parser.py | 27 +++++++----- amazeing/maze_class/maze.py | 35 ++++++++------- amazeing/maze_class/maze_pattern.py | 18 ++++---- amazeing/maze_class/maze_walls.py | 4 +- amazeing/maze_display/TTYdisplay.py | 68 ++++++++++++++++++++++++++++- example.conf | 4 +- 7 files changed, 118 insertions(+), 44 deletions(-) diff --git a/__main__.py b/__main__.py index f71a0d0..1fa2f49 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 Cardinal, CellCoord -from amazeing.maze_display.TTYdisplay import Tile +from amazeing.maze_display.TTYdisplay import Tile, extract_pairs from amazeing.maze_display.backend import BackendEvent, CloseRequested, IVec2 config = Config.parse(open("./example.conf").read()) @@ -19,7 +19,7 @@ config = Config.parse(open("./example.conf").read()) if config.seed is not None: random.seed(config.seed) -dims = (config.width, config.height) +dims = IVec2(config.width, config.height) maze = Maze(dims) @@ -30,7 +30,7 @@ pattern.fill(maze) walls_const = set(maze.walls_full()) -backend = TTYBackend(IVec2(*dims), IVec2(2, 1), IVec2(2, 1)) +backend = TTYBackend(dims, IVec2(2, 1), IVec2(2, 1)) curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLACK) black = curses.color_pair(1) empty = (" ", black) diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py index 2dc9cc6..fa47b28 100644 --- a/amazeing/config/config_parser.py +++ b/amazeing/config/config_parser.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Generator from typing import Any, Type + +from amazeing.maze_display.backend import IVec2 from .parser_combinator import ( ParseResult, Parser, @@ -45,13 +47,16 @@ 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( +def parse_coord(s: str) -> ParseResult[IVec2]: + return parser_map( + lambda e: IVec2(*e), + pair( + terminated( + parse_int, + delimited(multispace0, tag(","), multispace0), + ), parse_int, - delimited(multispace0, tag(","), multispace0), ), - parse_int, )(s) @@ -174,8 +179,8 @@ class BoolField(ConfigField[bool]): return parse_bool(s) -class CoordField(ConfigField[tuple[int, int]]): - def parse(self, s: str) -> ParseResult[tuple[int, int]]: +class CoordField(ConfigField[IVec2]): + def parse(self, s: str) -> ParseResult[IVec2]: return parse_coord(s) @@ -300,16 +305,16 @@ def fields_parser( class Config: width: int height: int - entry: tuple[int, int] | None - exit: tuple[int, int] | None + entry: IVec2 | None + exit: IVec2 | None output_file: str | None perfect: bool seed: int | None screensaver: bool visual: bool interactive: bool - tilemap_wall_size: tuple[int, int] - tilemap_cell_size: tuple[int, int] + tilemap_wall_size: IVec2 + tilemap_cell_size: IVec2 tilemap_full: list[ColoredLine] tilemap_empty: list[ColoredLine] tilemap_background: list[ColoredLine] diff --git a/amazeing/maze_class/maze.py b/amazeing/maze_class/maze.py index 75cebe8..a01e07d 100644 --- a/amazeing/maze_class/maze.py +++ b/amazeing/maze_class/maze.py @@ -1,4 +1,6 @@ from typing import Callable, Generator, Iterable, cast + +from amazeing.maze_display.backend import IVec2 from .maze_walls import ( MazeWall, NetworkID, @@ -9,9 +11,8 @@ from .maze_walls import ( class Maze: - def __init__(self, dims: tuple[int, int]) -> None: - self.__width: int = dims[0] - self.__height: int = dims[1] + def __init__(self, dims: IVec2) -> None: + self.__dims = dims self.__dirty: set[WallCoord] = set() self._clear() @@ -20,13 +21,13 @@ class Maze: self.__dirty ^= {wall for wall in self.walls_full()} # list of lines self.horizontal: list[list[MazeWall]] = [ - [MazeWall() for _ in range(0, self.__width)] - for _ in range(0, self.__height + 1) + [MazeWall() for _ in range(0, self.__dims.x)] + for _ in range(0, self.__dims.y + 1) ] # list of lines self.vertical: list[list[MazeWall]] = [ - [MazeWall() for _ in range(0, self.__height)] - for _ in range(0, self.__width + 1) + [MazeWall() for _ in range(0, self.__dims.y)] + for _ in range(0, self.__dims.x + 1) ] self.networks: dict[NetworkID, WallNetwork] = {} @@ -56,8 +57,8 @@ class Maze: def all_walls(self) -> Generator[WallCoord]: for orientation, a_count, b_count in [ - (Orientation.HORIZONTAL, self.__height + 1, self.__width), - (Orientation.VERTICAL, self.__width + 1, self.__height), + (Orientation.HORIZONTAL, self.__dims.y + 1, self.__dims.x), + (Orientation.VERTICAL, self.__dims.x + 1, self.__dims.y), ]: for a in range(0, a_count): for b in range(0, b_count): @@ -67,9 +68,9 @@ class Maze: if coord.a < 0 or coord.b < 0: return False (a_max, b_max) = ( - (self.__height, self.__width - 1) + (self.__dims.y, self.__dims.x - 1) if coord.orientation == Orientation.HORIZONTAL - else (self.__width, self.__height - 1) + else (self.__dims.x, self.__dims.y - 1) ) if coord.a > a_max or coord.b > b_max: return False @@ -119,14 +120,18 @@ class Maze: del self.networks[to_merge] def outline(self) -> None: - if self.__width < 1 or self.__height < 1: + if self.__dims.x < 1 or self.__dims.y < 1: return for orientation, a_iter, b_iter in [ - (Orientation.VERTICAL, (0, self.__width), range(0, self.__height)), + ( + Orientation.VERTICAL, + (0, self.__dims.x), + range(0, self.__dims.y), + ), ( Orientation.HORIZONTAL, - (0, self.__height), - range(0, self.__width), + (0, self.__dims.y), + range(0, self.__dims.x), ), ]: for a in a_iter: diff --git a/amazeing/maze_class/maze_pattern.py b/amazeing/maze_class/maze_pattern.py index 2e5ed62..5c8df9a 100644 --- a/amazeing/maze_class/maze_pattern.py +++ b/amazeing/maze_class/maze_pattern.py @@ -1,3 +1,4 @@ +from amazeing.maze_display.backend import IVec2 from .maze import Maze from .maze_walls import CellCoord from typing import Callable @@ -20,32 +21,29 @@ class Pattern: if char != " " } - def offset(self, by: tuple[int, int]) -> "Pattern": + def offset(self, by: IVec2) -> "Pattern": pattern: Pattern = Pattern([]) pattern.cells = {cell.offset(by) for cell in self.cells} return pattern - def dims(self) -> tuple[int, int]: + def dims(self) -> IVec2: dim_by: Callable[[Callable[[CellCoord], int]], int] = lambda f: ( max(map(lambda c: f(c) + 1, self.cells), default=0) - min(map(f, self.cells), default=0) ) - return (dim_by(CellCoord.x), dim_by(CellCoord.y)) + return IVec2(dim_by(CellCoord.x), dim_by(CellCoord.y)) def normalized(self) -> "Pattern": min_by: Callable[[Callable[[CellCoord], int]], int] = lambda f: min( map(f, self.cells), default=0 ) - offset: tuple[int, int] = (-min_by(CellCoord.x), -min_by(CellCoord.y)) + offset = IVec2(-min_by(CellCoord.x), -min_by(CellCoord.y)) return self.offset(offset) - def centered_for(self, canvas: tuple[int, int]) -> "Pattern": + def centered_for(self, canvas: IVec2) -> "Pattern": normalized: Pattern = self.normalized() - dims: tuple[int, int] = normalized.dims() - offset: tuple[int, int] = ( - (canvas[0] - dims[0]) // 2, - (canvas[1] - dims[1]) // 2, - ) + dims = normalized.dims() + offset = (canvas - dims) // 2 return normalized.offset(offset) def fill(self, maze: "Maze") -> None: diff --git a/amazeing/maze_class/maze_walls.py b/amazeing/maze_class/maze_walls.py index 5e279c2..c45d4b4 100644 --- a/amazeing/maze_class/maze_walls.py +++ b/amazeing/maze_class/maze_walls.py @@ -176,8 +176,8 @@ class CellCoord: def pixel_coords(self) -> Iterable[IVec2]: return [IVec2(self.__x * 2 + 1, self.__y * 2 + 1)] - def offset(self, by: tuple[int, int]) -> "CellCoord": - return CellCoord(self.__x + by[0], self.__y + by[1]) + def offset(self, by: IVec2) -> "CellCoord": + return CellCoord(self.__x + by.x, self.__y + by.y) def x(self) -> int: return self.__x diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index 0c90d68..64c965d 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -1,4 +1,5 @@ -from collections.abc import Generator +from collections.abc import Generator, Iterable +from ..config.config_parser import Color, Config, ColoredLine, ColorPair from amazeing.maze_display.layout import ( BInt, Box, @@ -13,6 +14,10 @@ from .backend import Backend, IVec2, BackendEvent, KeyboardInput import curses +class BackendException(Exception): + pass + + def pad_write_safe( pad: curses.window, dst: IVec2, char: str, attrs: int ) -> None: @@ -169,6 +174,67 @@ class ScrollablePad: self.move(by * IVec2.splat(-1)) +def extract_pairs( + config: Config, extra_colors: Iterable[ColorPair] = [] +) -> dict[ColorPair, int]: + all_tilemaps = ( + config.tilemap_empty, + config.tilemap_full, + config.tilemap_background, + ) + pairs = { + pair + for tilemap in all_tilemaps + for line in tilemap + for pair, _ in line + } | set(extra_colors) + colors = {color for pair in pairs for color in pair} + var_colors = {color for color in colors if isinstance(color, str)} + value_colors = {color for color in colors if not isinstance(color, str)} + color_lookup = { + "BLACK": curses.COLOR_BLACK, + "BLUE": curses.COLOR_BLUE, + "CYAN": curses.COLOR_CYAN, + "GREEN": curses.COLOR_GREEN, + "MAGENTA": curses.COLOR_MAGENTA, + "RED": curses.COLOR_RED, + "WHITE": curses.COLOR_WHITE, + "YELLOW": curses.COLOR_YELLOW, + } + available_colors = {i for i in range(0, curses.COLORS)} + res_colors: dict[Color, int] = {} + for color in var_colors: + if color not in color_lookup: + raise BackendException("Unknown color " + color + " in config") + res_colors[color] = color_lookup[color] + available_colors -= {color_lookup[color]} + if len(available_colors) < len(value_colors): + raise BackendException( + "Too many value color values in config: " + + f"maximum: {len(available_colors)}, " + + f"got: {len(value_colors)}" + ) + for color, color_number in zip(value_colors, available_colors): + curses.init_color( + color_number, *(min(0, max(1000, channel)) for channel in color) + ) + res_colors[color] = color_number + available_pairs = {i for i in range(1, curses.COLOR_PAIRS)} + if len(available_pairs) < len(pairs): + raise BackendException( + "Too many color pairs in config: " + + f"maximum: {len(available_pairs)}, " + + f"got: {len(pairs)}" + ) + res_pairs = {} + for colors, pair_number in zip(pairs, available_pairs): + fg, bg = colors + curses.init_pair(pair_number, res_colors[fg], res_colors[bg]) + res_pairs[colors] = pair_number + + return res_pairs + + class TTYBackend(Backend[int]): def __init__( self, maze_dims: IVec2, wall_dim: IVec2, cell_dim: IVec2 diff --git a/example.conf b/example.conf index 8269123..a8366b3 100644 --- a/example.conf +++ b/example.conf @@ -1,5 +1,5 @@ -WIDTH=100 -HEIGHT=100 +WIDTH=250 +HEIGHT=250 ENTRY=2,5 #EXIT=100,100 OUTPUT_FILE=test -- 2.53.0