
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");