From: Axy Date: Thu, 5 Feb 2026 01:49:47 +0000 (+0100) Subject: Simple generator done :D X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=27c2725189460133a1af3237af75b3f85e7f3139;p=axy%2Fft%2Fa-maze-ing.git Simple generator done :D --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eeb8a6e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/__pycache__ diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..8848e9e --- /dev/null +++ b/__main__.py @@ -0,0 +1,17 @@ +from amazeing import * +import random + +maze = Maze(10, 10) +maze.outline() +empty = list(maze.walls_empty()) +random.shuffle(empty) +for wall in empty: + if maze.wall_maintains_topology(wall): + maze.fill_wall(wall) + +backend = TTYBackend(10, 10) +backend.set_style("#") +for wall in maze.walls_full(): + for pixel in wall.pixel_coords(): + backend.draw_pixel(pixel) +backend.present() diff --git a/amazeing/__init__.py b/amazeing/__init__.py new file mode 100644 index 0000000..5041990 --- /dev/null +++ b/amazeing/__init__.py @@ -0,0 +1,4 @@ +__version__ = "0.0.0" + +from .display import Backend, PixelCoord, TTYBackend +from .maze import WallID, Maze diff --git a/amazeing/display.py b/amazeing/display.py new file mode 100644 index 0000000..835e1a0 --- /dev/null +++ b/amazeing/display.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod +from typing import Iterable +import sys + + +class PixelCoord: + def __init__(self, x: int, y: int) -> None: + self.x: int = x + self.y: int = y + + +class Backend(ABC): + @abstractmethod + def draw_pixel(self, pos: PixelCoord) -> None: + pass + + +class TTYBackend(Backend): + def __init__(self, maze_width: int, maze_height: int, style: str = " ") -> 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) + ] + + def draw_pixel(self, pos: PixelCoord) -> None: + self.lines[pos.y][pos.x] = self.style + + def set_style(self, style: str): + 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() diff --git a/amazeing/maze.py b/amazeing/maze.py new file mode 100644 index 0000000..0a96aa9 --- /dev/null +++ b/amazeing/maze.py @@ -0,0 +1,184 @@ +from enum import Enum, auto +from typing import Generator, Iterable, Optional, cast + +from amazeing.display import PixelCoord + + +class NetworkID: + __uuid_gen: int = 0 + + def __init__(self) -> None: + self.uuid = NetworkID.__uuid_gen + NetworkID.__uuid_gen += 1 + + +class MazeWall: + def __init__(self, network_id: Optional[NetworkID] = None) -> None: + self.network_id: Optional[NetworkID] = network_id + + def is_full(self) -> bool: + return self.network_id is not None + + +class Orientation(Enum): + HORIZONTAL = auto() + VERTICAL = auto() + + def opposite(self) -> "Orientation": + if self == Orientation.HORIZONTAL: + return Orientation.VERTICAL + return Orientation.HORIZONTAL + + +class WallID: + def __init__(self, orientation: Orientation, a: int, b: int) -> None: + self.orientation: Orientation = orientation + self.a: int = a + self.b: int = b + + def a_neighbours(self) -> list["WallID"]: + return [ + WallID(self.orientation.opposite(), self.b, self.a - 1), + WallID(self.orientation, self.a, self.b - 1), + WallID(self.orientation.opposite(), self.b, self.a), + ] + + def b_neighbours(self) -> list["WallID"]: + return [ + WallID(self.orientation.opposite(), self.b + 1, self.a - 1), + WallID(self.orientation, self.a, self.b + 1), + WallID(self.orientation.opposite(), self.b + 1, self.a), + ] + + def neighbours(self) -> list["WallID"]: + return self.a_neighbours() + self.b_neighbours() + + def pixel_coords(self) -> Iterable[PixelCoord]: + a: Iterable[int] = [self.a * 2] + b: Iterable[int] = [self.b * 2, self.b * 2 + 1, self.b * 2 + 2] + x_iter: Iterable[int] = a if self.orientation == Orientation.HORIZONTAL else b + y_iter: Iterable[int] = a if self.orientation == Orientation.VERTICAL else b + return (PixelCoord(x, y) for x in x_iter for y in y_iter) + + +class WallNetwork: + def __init__(self) -> None: + self.walls: set[WallID] = set() + + def size(self) -> int: + return len(self.walls) + + def add_wall(self, id: WallID) -> None: + self.walls.add(id) + + +class Maze: + def __init__(self, width: int, height: int) -> None: + self.width: int = width + self.height: int = height + # list of lines + self.horizontal: list[list[MazeWall]] = [ + [MazeWall() for _ in range(0, width)] for _ in range(0, height + 1) + ] + # list of lines + self.vertical: list[list[MazeWall]] = [ + [MazeWall() for _ in range(0, height)] for _ in range(0, width + 1) + ] + self.networks: dict[NetworkID, WallNetwork] = {} + + def _get_wall(self, id: WallID) -> MazeWall: + if id.orientation == Orientation.HORIZONTAL: + return self.horizontal[id.a][id.b] + return self.vertical[id.a][id.b] + + def all_walls(self) -> Generator[WallID]: + for orientation, a_count, b_count in [ + (Orientation.HORIZONTAL, self.width + 1, self.height), + (Orientation.VERTICAL, self.height + 1, self.width), + ]: + for a in range(0, a_count): + for b in range(0, b_count): + yield WallID(orientation, a, b) + + def _check_id(self, id: WallID) -> bool: + if id.a < 0 or id.b < 0: + return False + (a_max, b_max) = ( + (self.height, self.width - 1) + if id.orientation == Orientation.HORIZONTAL + else (self.width, self.height - 1) + ) + if id.a > a_max or id.b > b_max: + return False + return True + + def get_walls_checked(self, ids: list[WallID]) -> list[MazeWall]: + return [self._get_wall(id) for id in ids if self._check_id(id)] + + def get_neighbours(self, id: WallID) -> list[MazeWall]: + return self.get_walls_checked(id.neighbours()) + + def _fill_wall_alone(self, id: WallID, wall: MazeWall) -> None: + network_id: NetworkID = NetworkID() + wall.network_id = network_id + network = WallNetwork() + network.add_wall(id) + self.networks[network_id] = network + + def fill_wall(self, id: WallID) -> None: + wall: MazeWall = self._get_wall(id) + + if wall.is_full(): + return + + networks = { + cast(NetworkID, neighbour.network_id) + for neighbour in self.get_neighbours(id) + if neighbour.is_full() + } + + if len(networks) == 0: + return self._fill_wall_alone(id, 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) + + 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 + dest.add_wall(curr) + + del self.networks[to_merge] + + def outline(self) -> None: + if self.width < 1 or self.height < 1: + return + for orientation, a_iter, b_iter in [ + (Orientation.VERTICAL, (0, self.width), range(0, self.height)), + (Orientation.HORIZONTAL, (0, self.height), range(0, self.width)), + ]: + for a in a_iter: + for b in b_iter: + self.fill_wall(WallID(orientation, a, b)) + + def walls_full(self) -> Iterable[WallID]: + return filter(lambda w: self._get_wall(w).is_full(), self.all_walls()) + + def walls_empty(self) -> Iterable[WallID]: + return filter(lambda w: not self._get_wall(w).is_full(), self.all_walls()) + + def wall_maintains_topology(self, wall: WallID) -> bool: + a = { + cast(NetworkID, neighbour.network_id) + for neighbour in self.get_walls_checked(wall.a_neighbours()) + if neighbour.is_full() + } + b = { + cast(NetworkID, neighbour.network_id) + for neighbour in self.get_walls_checked(wall.b_neighbours()) + if neighbour.is_full() + } + return len(a & b) == 0 diff --git a/maze.py b/maze.py deleted file mode 100644 index 9ec3e72..0000000 --- a/maze.py +++ /dev/null @@ -1,134 +0,0 @@ -from enum import Enum -from typing import Optional, cast - - -class NetworkID: - __uuid_gen: int = 0 - - def __init__(self) -> None: - self.uuid = NetworkID.__uuid_gen - NetworkID.__uuid_gen += 1 - - -class MazeWall: - def __init__(self, network_id: Optional[NetworkID] = None) -> None: - self.network_id: Optional[NetworkID] = network_id - - def is_full(self) -> bool: - return self.network_id is not None - - -class Orientation(Enum): - HORIZONTAL = "horizontal" - VERTICAL = "vertical" - - def opposite(self) -> "Orientation": - if self == Orientation.HORIZONTAL: - return Orientation.VERTICAL - return Orientation.HORIZONTAL - - -class WallId: - def __init__(self, orientation: Orientation, a: int, b: int) -> None: - self.orientation: Orientation = orientation - self.a: int = a - self.b: int = b - - def a_neighbours(self) -> list["WallId"]: - return [ - WallId(self.orientation.opposite(), self.b - 1, self.a), - WallId(self.orientation, self.a - 1, self.b), - WallId(self.orientation.opposite(), self.b, self.a), - ] - - def b_neighbours(self) -> list["WallId"]: - return [ - WallId(self.orientation.opposite(), self.b - 1, self.a + 1), - WallId(self.orientation, self.a + 1, self.b), - WallId(self.orientation.opposite(), self.b, self.a + 1), - ] - - def neighbours(self) -> list["WallId"]: - return self.a_neighbours() + self.b_neighbours() - - -class WallNetwork: - def __init__(self) -> None: - self.walls: set[WallId] = set() - - def size(self) -> int: - return len(self.walls) - - def add_wall(self, id: WallId) -> None: - self.walls.add(id) - - -class Maze: - def __init__(self, width: int, height: int) -> None: - self.width: int = width - self.height: int = height - self.horizontal: list[list[MazeWall]] = [ - [MazeWall() for _ in range(0, width)] for _ in range(0, height + 1) - ] - self.vertical: list[list[MazeWall]] = [ - [MazeWall() for _ in range(0, height)] for _ in range(0, width + 1) - ] - self.networks: dict[NetworkID, WallNetwork] = {} - - def _get_wall(self, id: WallId) -> MazeWall: - if id.orientation == Orientation.HORIZONTAL: - return self.horizontal[id.a][id.b] - return self.vertical[id.a][id.b] - - def _check_id(self, id: WallId) -> bool: - if id.a < 0 or id.b < 0: - return False - (a_max, b_max) = ( - (self.height + 1, self.width) - if id.orientation == Orientation.HORIZONTAL - else (self.width + 1, self.height) - ) - if id.a > a_max or id.b > b_max: - return False - return True - - def get_walls_checked(self, ids: list[WallId]) -> list[MazeWall]: - return [self._get_wall(id) for id in ids if self._check_id(id)] - - def get_neighbours(self, id: WallId) -> list[MazeWall]: - return self.get_walls_checked(id.neighbours()) - - def _fill_wall_alone(self, id: WallId, wall: MazeWall) -> None: - network_id: NetworkID = NetworkID() - wall.network_id = network_id - network = WallNetwork() - network.add_wall(id) - self.networks[network_id] = network - - def fill_wall(self, id: WallId) -> None: - wall: MazeWall = self._get_wall(id) - - if wall.is_full(): - return - - networks = { - cast(NetworkID, neighbour.network_id) - for neighbour in self.get_neighbours(id) - if neighbour.is_full() - } - - if networks == {}: - return self._fill_wall_alone(id, 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) - - 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 - dest.add_wall(curr) - - del self.networks[to_merge]