From: Axy Date: Sat, 28 Feb 2026 15:33:13 +0000 (+0100) Subject: Scuffed curses frontend X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=31d54d97f2e92701d624cc94d750a2b32133fb87;p=axy%2Fft%2Fa-maze-ing.git Scuffed curses frontend --- diff --git a/__main__.py b/__main__.py index e15277d..6d94cb7 100644 --- a/__main__.py +++ b/__main__.py @@ -1,3 +1,4 @@ +import curses from amazeing import ( Maze, TTYBackend, @@ -6,16 +7,18 @@ from amazeing import ( maze_make_perfect, ) from time import sleep -from sys import stdin +from sys import stderr, stdin from amazeing.config.config_parser import Config from amazeing.maze_class.maze_walls import Cardinal, CellCoord +from amazeing.maze_display.TTYdisplay import TTYTile +from amazeing.maze_display.backend import IVec2 # random.seed(42) # print(Config.parse(stdin.read()).__dict__) -dims = (25, 25) +dims = (15, 15) maze = Maze(dims) @@ -26,18 +29,54 @@ pattern.fill(maze) walls_const = set(maze.walls_full()) +backend = TTYBackend(IVec2(*dims), IVec2(1, 1), IVec2(2, 2)) +curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLACK) +black = curses.color_pair(1) +empty = (" ", black) +style_empty = backend.add_style( + TTYTile( + [ + [empty, empty, empty], + [empty, empty, empty], + [empty, empty, empty], + ] + ) +) +curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE) +white = curses.color_pair(2) +full = ("#", white) +style_full = backend.add_style( + TTYTile( + [ + [full, full, full], + [full, full, full], + [full, full, full], + ] + ) +) + + +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)) + def display_maze(maze: Maze) -> None: - backend = TTYBackend(*dims, style="\x1b[48;5;240m \x1b[0m") - backend.set_style("\x1b[48;5;248m \x1b[0m") + clear_backend() + backend.set_style(style_full) for wall in maze.walls_full(): - for pixel in wall.pixel_coords(): - backend.draw_pixel(pixel) + for pixel in wall.tile_coords(): + backend.draw_tile(pixel) backend.present() - sleep(0.05) + if backend.event(0) is not None: + exit() maze_make_perfect(maze, callback=display_maze) +backend.event(-1) # maze_make_pacman(maze, walls_const, callback=display_maze) while False: maze_make_perfect(maze, callback=display_maze) diff --git a/amazeing/__init__.py b/amazeing/__init__.py index 3c2e118..7b2c0d2 100644 --- a/amazeing/__init__.py +++ b/amazeing/__init__.py @@ -2,7 +2,7 @@ __version__ = "0.0.0" __author__ = "luflores & agilliar" from amazeing.maze_class import WallCoord, Maze, Pattern -from amazeing.maze_display import Backend, PixelCoord, TTYBackend +from amazeing.maze_display import Backend, IVec2, TTYBackend from .maze_make_pacman import maze_make_pacman from .maze_make_perfect import maze_make_perfect @@ -11,7 +11,7 @@ __all__ = [ "Maze", "Pattern", "Backend", - "PixelCoord", + "IVec2", "TTYBackend", "maze_make_pacman", "maze_make_perfect", diff --git a/amazeing/config/config_parser.py b/amazeing/config/config_parser.py index ded3946..f20027d 100644 --- a/amazeing/config/config_parser.py +++ b/amazeing/config/config_parser.py @@ -15,7 +15,6 @@ from .parser_combinator import ( one_of, pair, parser_complete, - parser_default, parser_map, preceeded, recognize, diff --git a/amazeing/maze_class/maze_walls.py b/amazeing/maze_class/maze_walls.py index cfbb966..178b619 100644 --- a/amazeing/maze_class/maze_walls.py +++ b/amazeing/maze_class/maze_walls.py @@ -1,6 +1,7 @@ from enum import Enum, auto +from sys import stderr from typing import Iterable, Optional, cast -from ..maze_display import PixelCoord +from ..maze_display import IVec2 class NetworkID: @@ -113,7 +114,7 @@ class WallCoord: def neighbours(self) -> list["WallCoord"]: return self.a_neighbours() + self.b_neighbours() - def pixel_coords(self) -> Iterable[PixelCoord]: + def tile_coords(self) -> Iterable[IVec2]: a: Iterable[int] = [self.a * 2] b: Iterable[int] = [self.b * 2, self.b * 2 + 1, self.b * 2 + 2] x_iter: Iterable[int] = ( @@ -122,7 +123,7 @@ class WallCoord: y_iter: Iterable[int] = ( a if self.orientation == Orientation.HORIZONTAL else b ) - return (PixelCoord(x, y) for x in x_iter for y in y_iter) + return (IVec2(x, y) for x in x_iter for y in y_iter) def neighbour_cells(self) -> list["CellCoord"]: if self.orientation == Orientation.HORIZONTAL: @@ -173,8 +174,8 @@ class CellCoord: case Cardinal.WEST: return WallCoord(Orientation.VERTICAL, self.__x + 1, self.__y) - def pixel_coords(self) -> Iterable[PixelCoord]: - return [PixelCoord(self.__x * 2 + 1, self.__y * 2 + 1)] + 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]) diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index ccbc766..8a44ea8 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -1,33 +1,104 @@ -from .backend import Backend, PixelCoord -import sys +from sys import stderr +from .backend import Backend, IVec2, BackendEvent, KeyboardInput +import curses -class TTYBackend(Backend): +class TTYTile: + def __init__(self, pixels: list[list[tuple[str, int]]]) -> None: + self.__pixels: list[list[tuple[str, int]]] = pixels + + def blit( + self, src: IVec2, dst: IVec2, size: IVec2, window: curses.window + ) -> None: + for y, line in enumerate(self.__pixels[src.y : src.y + size.y]): + for x, (char, attrs) in enumerate(line[src.x : src.x + size.x]): + window.addch(dst.y + y, dst.x + x, char, attrs) + + +class TTYTileMap: + def __init__(self, wall_dim: IVec2, cell_dim: IVec2) -> None: + self.__wall_dim: IVec2 = wall_dim + self.__cell_dim: IVec2 = cell_dim + self.__tiles: list[TTYTile] = [] + + def add_tile(self, tile: TTYTile) -> int: + res = len(self.__tiles) + self.__tiles.append(tile) + return res + + def dst_coord(self, pos: IVec2) -> IVec2: + return (n := pos // 2) * self.__cell_dim + (pos - n) * self.__wall_dim + + def src_coord(self, pos: IVec2) -> IVec2: + return pos % 2 * self.__wall_dim + + def tile_size(self, pos: IVec2) -> IVec2: + return (pos + 1) % 2 * self.__wall_dim + pos % 2 * self.__cell_dim + + def draw_at(self, pos: IVec2, idx: int, window: curses.window) -> None: + self.__tiles[idx].blit( + self.src_coord(pos), + self.dst_coord(pos), + self.tile_size(pos), + window, + ) + + +class TTYBackend(Backend[int]): """ Takes the ABC Backend and displays the maze in the terminal. """ def __init__( - self, maze_width: int, maze_height: int, style: str = " " + self, maze_dims: IVec2, wall_dim: IVec2, cell_dim: IVec2 ) -> None: super().__init__() - self.width: int = maze_width * 2 + 1 - self.height: int = maze_height * 2 + 1 - self.style: str = style - self.lines: list[list[str]] = [ - [style for _ in range(0, self.width)] - for _ in range(0, self.height) - ] + self.__tilemap: TTYTileMap = TTYTileMap(wall_dim, cell_dim) + self.__style = 0 + + dims = self.__tilemap.dst_coord(maze_dims * 2 + 2) + + self.__screen: curses.window = curses.initscr() + curses.start_color() + curses.noecho() + curses.cbreak() + self.__screen.keypad(True) + + self.__pad: curses.window = curses.newpad(dims.y, dims.x) + self.__dims = maze_dims - def draw_pixel(self, pos: PixelCoord) -> None: - self.lines[pos.y][pos.x] = self.style + def __del__(self): + curses.nocbreak() + self.__screen.keypad(False) + curses.echo() + curses.endwin() - def set_style(self, style: str) -> None: - self.style = style + def add_style(self, style: TTYTile) -> int: + return self.__tilemap.add_tile(style) + + def dims(self) -> IVec2: + return self.__dims + + def draw_tile(self, pos: IVec2) -> None: + self.__tilemap.draw_at(pos, self.__style, self.__pad) + + def set_style(self, style: int) -> None: + self.__style = style def present(self) -> None: - for line in self.lines: - for char in line: - sys.stdout.write(char) - sys.stdout.write("\n") - sys.stdout.flush() + self.__screen.refresh() + self.__pad.refresh( + 0, + 0, + 0, + 0, + min(self.__pad.getmaxyx()[0] - 1, self.__screen.getmaxyx()[0] - 1), + min(self.__pad.getmaxyx()[1] - 1, self.__screen.getmaxyx()[1] - 1), + ) + + def event(self, timeout_ms: int = -1) -> BackendEvent | None: + self.__screen.timeout(timeout_ms) + try: + return KeyboardInput(self.__screen.getkey()) + except curses.error: + return None diff --git a/amazeing/maze_display/__init__.py b/amazeing/maze_display/__init__.py index 972cb85..6ae3ed2 100644 --- a/amazeing/maze_display/__init__.py +++ b/amazeing/maze_display/__init__.py @@ -1,11 +1,11 @@ __version__ = "0.0.0" __author__ = "luflores & agilliar" -from .backend import Backend, PixelCoord +from .backend import Backend, IVec2 from .TTYdisplay import TTYBackend __all__ = [ "Backend", - "PixelCoord", + "IVec2", "TTYBackend", ] diff --git a/amazeing/maze_display/backend.py b/amazeing/maze_display/backend.py index 56d173b..45d346e 100644 --- a/amazeing/maze_display/backend.py +++ b/amazeing/maze_display/backend.py @@ -1,19 +1,83 @@ from abc import ABC, abstractmethod +from collections.abc import Callable +from dataclasses import dataclass -class PixelCoord: +class IVec2: def __init__(self, x: int, y: int) -> None: self.x: int = x self.y: int = y + @staticmethod + def splat(n: int) -> "IVec2": + return IVec2(n, n) -class Backend(ABC): + @staticmethod + def with_op( + op: Callable[[int, int], int], + ) -> Callable[["IVec2", "int | IVec2"], "IVec2"]: + return lambda self, other: IVec2( + op( + self.x, + ( + other + if isinstance(other, IVec2) + else (other := IVec2.splat(other)) + ).x, + ), + op(self.y, other.y), + ) + + def __mul__(self, other: "int | IVec2") -> "IVec2": + return self.with_op(int.__mul__)(self, other) + + def __add__(self, other: "int | IVec2") -> "IVec2": + return self.with_op(int.__add__)(self, other) + + def __sub__(self, other: "int | IVec2") -> "IVec2": + return self.with_op(int.__sub__)(self, other) + + def __floordiv__(self, other: "int | IVec2") -> "IVec2": + return self.with_op(int.__floordiv__)(self, other) + + def __mod__(self, other: "int | IVec2") -> "IVec2": + return self.with_op(int.__mod__)(self, other) + + +@dataclass +class KeyboardInput: + sym: str + + +class CloseRequested: + pass + + +type BackendEvent = KeyboardInput | CloseRequested + + +class Backend[T](ABC): """ ABC for the maze display. defining how the maze should be drawn. - (PixelCoord) """ @abstractmethod - def draw_pixel(self, pos: PixelCoord) -> None: + def dims(self) -> IVec2: + pass + + @abstractmethod + def draw_tile(self, pos: IVec2) -> None: + pass + + @abstractmethod + def set_style(self, style: T) -> None: + pass + + @abstractmethod + def present(self) -> None: + pass + + @abstractmethod + def event(self, timeout_ms: int = -1) -> BackendEvent | None: pass