Source code for bruhanimate.demos.grid_demo

"""A public maze fixture: pure BFS snapshots plus a small playback composition.

Run python -m bruhanimate.demos.grid_demo. Space pauses, n/right steps,
+/- changes speed, and q quits. The final path remains visible until quit.
"""

from collections import deque
from dataclasses import dataclass

from bruhcolor import bruhcolored

from bruhanimate import (
    CallbackEffect,
    Playback,
    ResizePolicy,
    Scene,
    draw_grid,
    effect_registry,
    play,
)

MAZE = (
    "#############",
    "#S..#.......#",
    "#.#.#.#####.#",
    "#.#...#...#.#",
    "#.#####.#.#.#",
    "#.......#..E#",
    "#############",
)

Point = tuple[int, int]


[docs] @dataclass(frozen=True) class SearchState: current: Point visited: frozenset[Point] frontier: frozenset[Point] path: tuple[Point, ...] = () complete: bool = False
[docs] def solve(board=MAZE): """Yield immutable BFS snapshots without terminal access or retained history. This small example copies sets into snapshots; large searches should sample or aggregate progress rather than produce a snapshot of every branch. """ if not board or not board[0] or any(len(row) != len(board[0]) for row in board): raise ValueError("Maze must be a non-empty rectangle") starts = [ (x, y) for y, row in enumerate(board) for x, cell in enumerate(row) if cell == "S" ] ends = [ (x, y) for y, row in enumerate(board) for x, cell in enumerate(row) if cell == "E" ] if len(starts) != 1 or len(ends) != 1: raise ValueError("Maze needs exactly one S and one E") queue = deque(starts) parents = {starts[0]: None} visited = set() while queue: current = queue.popleft() visited.add(current) if current == ends[0]: path = [] point = current while point is not None: path.append(point) point = parents[point] yield SearchState( current, frozenset(visited), frozenset(queue), tuple(reversed(path)), True, ) return x, y = current for nx, ny in ((x + 1, y), (x, y + 1), (x - 1, y), (x, y - 1)): if ( 0 <= ny < len(board) and 0 <= nx < len(board[0]) and board[ny][nx] != "#" and (nx, ny) not in parents ): parents[nx, ny] = current queue.append((nx, ny)) yield SearchState(current, frozenset(visited), frozenset(queue))
WALL = bruhcolored("#", color=240).colored VISITED = bruhcolored(".", color=39).colored FRONTIER = bruhcolored("o", color=220).colored PATH = bruhcolored("*", color=46).colored CURSOR = bruhcolored("@", color=231).colored
[docs] def draw_state(state, buffer): def glyph(cell, column, row): if cell == "#": return WALL if (column, row) in state.frontier: return FRONTIER if (column, row) in state.visited: return VISITED return cell if cell in "SE" else None grid = draw_grid( buffer, MAZE, glyph=glyph, spacing=(2, 1), region=(0, 1, buffer.width(), max(0, buffer.height() - 2)), ) grid.path(state.path, PATH) grid.put(*state.current, CURSOR)
[docs] def run(*, frames=None, fps=60, on_resize: ResizePolicy = "resize"): playback = Playback(solve(), draw_state, steps_per_second=10) def status(buffer): state = playback.state mode = ( "Solved" if state and state.complete else "Paused" if playback.paused else "Searching" ) buffer.put_at( 0, 0, f"{mode} | {playback.steps} states | {playback.steps_per_second:g}/s" ) buffer.put_at( 0, buffer.height() - 1, "Space pause | n/right step | +/- speed | q quit" ) def build(buffer): return Scene( effect_registry.create("stars", buffer, " ", preset="color"), playback, CallbackEffect(status), ) return play( effect_factory=build, frames=frames, fps=fps, updates_per_second=30, event_handler=playback.handle_event, on_resize=on_resize, )
if __name__ == "__main__": run()