
Created 2025-09-11
/**
 * ============================================================================
 * =                               Maze                                       =
 * ============================================================================
 *
 * A maze generator and pathfinder visualization using ASCII box-drawing
 * characters. This example demonstrates complex grid-based rendering and
 * animation techniques in Recho.
 *
 * The implementation uses a recursive backtracking algorithm to generate
 * random mazes, then employs breadth-first search to find the shortest path
 * from entrance to exit. The visualization shows:
 *
 * - Maze walls rendered with Unicode box-drawing characters
 * - Animated pathfinding with a filled circle (●) moving along the solution
 * - Automatic maze regeneration when the path animation completes
 *
 * The rendering system manually constructs a character grid using proper
 * junction detection to ensure walls connect seamlessly. Each maze cell
 * tracks which walls are present, allowing for precise wall removal during
 * generation.
 *
 * Adjust the `width` and `height` parameters to create different maze sizes!
 */
//➜ ──┬───────┬─────┬───────────────────┐
//➜   │       │     │                   │
//➜ │ │ ──┬─┐ │ ┌── ├─────┐ ┌────── ┌── │
//➜ │ │   │ │   │   │     │ │       │   │
//➜ │ │ │ │ └───┤ ┌─┤ ──┐ └─┘ ┌─────┤ ──┤
//➜ │ │ │ │     │ │ │   │     │     │   │
//➜ │ └─┘ │ │ ┌─┘ │ │ │ ├───┬─┘ ┌───┴── │
//➜ │     │ │ │   │   │ │   │   │       │
//➜ ├─────┤ │ │ ┌─┘ ┌─┤ │ │ │ │ │ ──┬───┤
//➜ │     │ │ │ │   │ │  ●│ │ │ │   │   │
//➜ │ ┌───┘ │ │ │ ──┘ ├───┘ │ │ └─┐ ├── │
//➜ │ │     │   │     │     │ │   │ │   │
//➜ │ │ ──┬─┴───┴───┐ │ ────┴─┤ ──┤ │ ──┤
//➜ │ │   │         │ │       │   │ │   │
//➜ │ └─┐ └─┐ ──┬─┐ └─┼────── │ │ │ └── │
//➜ │   │   │   │ │   │       │ │ │     │
//➜ │ ──┴─┐ ├── │ └─┐ │ ┌───┬─┴─┘ └─┬── │
//➜ │     │ │   │   │   │   │       │   │
//➜ │ ──┐ │ │ ┌─┘ ──┴───┘ ──┘ │ ────┘ ──┘
//➜ │   │     │               │
//➜ └───┴─────┴───────────────┴──────────
{
  frame;
  if (!maze.walk()) maze.regenerate();
  echo(maze.render());
}
const width = 18;
const height = 10;
const maze = new Maze(width, height);
const frame = recho.interval(100);
const cell = () => ({top: true, right: true, bottom: true, left: true});
class Maze {
  constructor(width, height) {
    this.width = width;
    this.height = height;
    this.initialize();
    this.generate();
  }
  initialize() {
    this.grid = _.times(this.height, () => _.times(this.width, cell));
    this.visited = _.times(this.height, () => _.times(this.width, _.constant(false)));
    this.path = null;
    this.step = 0;
  }
  regenerate() {
    this.initialize();
    this.generate();
  }
  generate() {
    this.carvePassage(0, 0);
    this.grid[0][0].left = false;
    this.grid[this.height - 1][this.width - 1].right = false;
  }
  walk() {
    if (this.path === null) {
      this.path = this.findPath();
      this.step = 0;
      return true;
    } else if (this.step + 1 < this.path.length) {
      this.step += 1;
      return true;
    } else {
      return false;
    }
  }
  carvePassage(x, y) {
    this.visited[y][x] = true;
    for (const neighbor of _.shuffle(this.getUnvisitedNeighbors(x, y))) {
      if (!this.visited[neighbor.y][neighbor.x]) {
        // Remove walls between current cell and neighbor
        this.removeWall(x, y, neighbor.x, neighbor.y);
        // Recursively visit neighbor
        this.carvePassage(neighbor.x, neighbor.y);
      }
    }
  }
  getUnvisitedNeighbors(x, y) {
    const neighbors = [];
    if (y > 0 && !this.visited[y - 1][x]) neighbors.push({x: x, y: y - 1}); // up
    if (x < this.width - 1 && !this.visited[y][x + 1]) neighbors.push({x: x + 1, y: y}); // right
    if (y < this.height - 1 && !this.visited[y + 1][x]) neighbors.push({x: x, y: y + 1}); // down
    if (x > 0 && !this.visited[y][x - 1]) neighbors.push({x: x - 1, y: y}); // left
    return neighbors;
  }
  removeWall(x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    if (dx === 1) {
      // Moving right
      this.grid[y1][x1].right = false;
      this.grid[y2][x2].left = false;
    } else if (dx === -1) {
      // Moving left
      this.grid[y1][x1].left = false;
      this.grid[y2][x2].right = false;
    } else if (dy === 1) {
      // Moving down
      this.grid[y1][x1].bottom = false;
      this.grid[y2][x2].top = false;
    } else if (dy === -1) {
      // Moving up
      this.grid[y1][x1].top = false;
      this.grid[y2][x2].bottom = false;
    }
  }
  findPath() {
    const queue = [[0, 0, [{x: 0, y: 0}]]];
    const visited = new Set([`${0},${0}`]);
    while (queue.length > 0) {
      const [x, y, path] = queue.shift();
      if (x === this.width - 1 && y === this.height - 1) return path;
      for (const {dx, dy, direction} of directions) {
        const nx = x + dx;
        const ny = y + dy;
        const key = `${nx},${ny}`;
        if (
          nx >= 0 &&
          nx < this.width &&
          ny >= 0 &&
          ny < this.height &&
          !visited.has(key) &&
          !this.grid[y][x][direction]
        ) {
          visited.add(key);
          queue.push([nx, ny, [...path, {x: nx, y: ny}]]);
        }
      }
    }
    return null;
  }
  render() {
    const pathSet = new Set();
    if (this.path !== null && this.step < this.path.length) {
      pathSet.add(`${this.path[this.step].x},${this.path[this.step].y}`);
    }
    // Create output grid (2x dimensions + 1 for walls)
    const outputHeight = this.height * 2 + 1;
    const outputWidth = this.width * 2 + 1;
    const output = _.times(outputHeight, () => _.times(outputWidth, _.constant(chars.space)));
    for (let y = 0; y < this.height; y++) {
      for (let x = 0; x < this.width; x++) {
        const cell = this.grid[y][x];
        const ox = x * 2;
        const oy = y * 2;
        if (cell.top) output[oy][ox + 1] = chars.horizontal;
        if (cell.bottom) output[oy + 2][ox + 1] = chars.horizontal;
        if (cell.left) output[oy + 1][ox] = chars.vertical;
        if (cell.right) output[oy + 1][ox + 2] = chars.vertical;
        if (pathSet.has(`${x},${y}`)) output[oy + 1][ox + 1] = chars.ghost;
      }
    }
    // Draw corners and junctions
    for (let y = 0; y < outputHeight; y += 2) {
      for (let x = 0; x < outputWidth; x += 2) {
        const top = y > 0 && output[y - 1][x] === chars.vertical;
        const bottom = y < outputHeight - 1 && output[y + 1][x] === chars.vertical;
        const left = x > 0 && output[y][x - 1] === chars.horizontal;
        const right = x < outputWidth - 1 && output[y][x + 1] === chars.horizontal;
        const connections = (top ? 1 : 0) + (right ? 1 : 0) + (bottom ? 1 : 0) + (left ? 1 : 0);
        if (connections === 0) {
          output[y][x] = chars.space;
        } else if (connections === 1) {
          if (top || bottom) output[y][x] = chars.vertical;
          else output[y][x] = chars.horizontal;
        } else if (connections === 2) {
          if (top && bottom) output[y][x] = chars.vertical;
          else if (left && right) output[y][x] = chars.horizontal;
          else if (top && right) output[y][x] = chars.bottomLeft;
          else if (top && left) output[y][x] = chars.bottomRight;
          else if (bottom && right) output[y][x] = chars.topLeft;
          else if (bottom && left) output[y][x] = chars.topRight;
        } else if (connections === 3) {
          if (!top) output[y][x] = chars.teeDown;
          else if (!right) output[y][x] = chars.teeLeft;
          else if (!bottom) output[y][x] = chars.teeUp;
          else if (!left) output[y][x] = chars.teeRight;
        } else {
          output[y][x] = chars.cross;
        }
      }
    }
    output[1][0] = chars.startMarker;
    output[outputHeight - 2][outputWidth - 1] = chars.endMarker;
    return output.map((row) => row.join("")).join("\n");
  }
}
const directions = [
  {dx: 0, dy: -1, direction: "top"},
  {dx: 1, dy: 0, direction: "right"},
  {dx: 0, dy: 1, direction: "bottom"},
  {dx: -1, dy: 0, direction: "left"},
];
const chars = {
  horizontal: "─",
  vertical: "│",
  topLeft: "┌",
  topRight: "┐",
  bottomLeft: "└",
  bottomRight: "┘",
  teeUp: "┴",
  teeDown: "┬",
  teeLeft: "┤",
  teeRight: "├",
  cross: "┼",
  space: " ",
  ghost: "●",
  startMarker: " ",
  endMarker: " ",
};
const _ = recho.require("lodash");