How to Create Randomly Generated Mazes in JavaScript Using Recursive Backtracking

Procedural generation is a fascinating aspect of programming that can add infinite variety to games and applications. One popular algorithm for procedural maze generation is recursive backtracking. In this article, we’ll walk through building a maze generator in JavaScript using this technique and rendering it with HTML5 canvas. 1. Maze Grid Setup We'll represent the maze as a 2D grid of cells. Each cell tracks whether its walls (top, right, bottom, left) exist, and whether it has been visited. const cols = 20; const rows = 20; const grid = []; let current; class Cell { constructor(x, y) { this.x = x; this.y = y; this.walls = [true, true, true, true]; // top, right, bottom, left this.visited = false; } index(x, y) { if (x < 0 || y < 0 || x > cols - 1 || y > rows - 1) return -1; return x + y * cols; } checkNeighbors() { const neighbors = []; const top = grid[this.index(this.x, this.y - 1)]; const right = grid[this.index(this.x + 1, this.y)]; const bottom = grid[this.index(this.x, this.y + 1)]; const left = grid[this.index(this.x - 1, this.y)]; [top, right, bottom, left].forEach(cell => { if (cell && !cell.visited) neighbors.push(cell); }); if (neighbors.length > 0) { const r = Math.floor(Math.random() * neighbors.length); return neighbors[r]; } return undefined; } } 2. Canvas Setup Use an HTML5 canvas to draw the grid and render the maze generation process. const canvas = document.getElementById("maze"); const ctx = canvas.getContext("2d"); const cellSize = 25; canvas.width = cols * cellSize; canvas.height = rows * cellSize; function drawCell(cell) { const x = cell.x * cellSize; const y = cell.y * cellSize; ctx.strokeStyle = "black"; ctx.lineWidth = 2; if (cell.walls[0]) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + cellSize, y); ctx.stroke(); } if (cell.walls[1]) { ctx.beginPath(); ctx.moveTo(x + cellSize, y); ctx.lineTo(x + cellSize, y + cellSize); ctx.stroke(); } if (cell.walls[2]) { ctx.beginPath(); ctx.moveTo(x + cellSize, y + cellSize); ctx.lineTo(x, y + cellSize); ctx.stroke(); } if (cell.walls[3]) { ctx.beginPath(); ctx.moveTo(x, y + cellSize); ctx.lineTo(x, y); ctx.stroke(); } if (cell.visited) { ctx.fillStyle = "#eee"; ctx.fillRect(x, y, cellSize, cellSize); } } 3. Maze Generation Logic Implement recursive backtracking with a stack to navigate through the grid, carving a path and removing walls between cells. function removeWalls(a, b) { const x = a.x - b.x; const y = a.y - b.y; if (x === 1) { a.walls[3] = false; b.walls[1] = false; } else if (x === -1) { a.walls[1] = false; b.walls[3] = false; } if (y === 1) { a.walls[0] = false; b.walls[2] = false; } else if (y === -1) { a.walls[2] = false; b.walls[0] = false; } } for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { grid.push(new Cell(x, y)); } } current = grid[0]; const stack = []; function step() { current.visited = true; const next = current.checkNeighbors(); if (next) { next.visited = true; stack.push(current); removeWalls(current, next); current = next; } else if (stack.length > 0) { current = stack.pop(); } } function draw() { requestAnimationFrame(draw); step(); ctx.clearRect(0, 0, canvas.width, canvas.height); grid.forEach(drawCell); } draw(); 4. Conclusion This implementation provides a simple yet powerful maze generator using JavaScript and the recursive backtracking algorithm. From here, you can experiment with different algorithms (like Prim’s or Kruskal’s), add player movement, or generate larger/dynamic mazes. If you enjoyed this post, consider supporting my work: buymeacoffee.com/hexshift

Apr 20, 2025 - 04:34
 0
How to Create Randomly Generated Mazes in JavaScript Using Recursive Backtracking

Procedural generation is a fascinating aspect of programming that can add infinite variety to games and applications. One popular algorithm for procedural maze generation is recursive backtracking. In this article, we’ll walk through building a maze generator in JavaScript using this technique and rendering it with HTML5 canvas.

1. Maze Grid Setup

We'll represent the maze as a 2D grid of cells. Each cell tracks whether its walls (top, right, bottom, left) exist, and whether it has been visited.

const cols = 20;
const rows = 20;
const grid = [];
let current;

class Cell {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.walls = [true, true, true, true]; // top, right, bottom, left
    this.visited = false;
  }

  index(x, y) {
    if (x < 0 || y < 0 || x > cols - 1 || y > rows - 1) return -1;
    return x + y * cols;
  }

  checkNeighbors() {
    const neighbors = [];

    const top = grid[this.index(this.x, this.y - 1)];
    const right = grid[this.index(this.x + 1, this.y)];
    const bottom = grid[this.index(this.x, this.y + 1)];
    const left = grid[this.index(this.x - 1, this.y)];

    [top, right, bottom, left].forEach(cell => {
      if (cell && !cell.visited) neighbors.push(cell);
    });

    if (neighbors.length > 0) {
      const r = Math.floor(Math.random() * neighbors.length);
      return neighbors[r];
    }

    return undefined;
  }
}

2. Canvas Setup

Use an HTML5 canvas to draw the grid and render the maze generation process.

const canvas = document.getElementById("maze");
const ctx = canvas.getContext("2d");
const cellSize = 25;

canvas.width = cols * cellSize;
canvas.height = rows * cellSize;

function drawCell(cell) {
  const x = cell.x * cellSize;
  const y = cell.y * cellSize;

  ctx.strokeStyle = "black";
  ctx.lineWidth = 2;

  if (cell.walls[0]) {
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x + cellSize, y);
    ctx.stroke();
  }
  if (cell.walls[1]) {
    ctx.beginPath();
    ctx.moveTo(x + cellSize, y);
    ctx.lineTo(x + cellSize, y + cellSize);
    ctx.stroke();
  }
  if (cell.walls[2]) {
    ctx.beginPath();
    ctx.moveTo(x + cellSize, y + cellSize);
    ctx.lineTo(x, y + cellSize);
    ctx.stroke();
  }
  if (cell.walls[3]) {
    ctx.beginPath();
    ctx.moveTo(x, y + cellSize);
    ctx.lineTo(x, y);
    ctx.stroke();
  }

  if (cell.visited) {
    ctx.fillStyle = "#eee";
    ctx.fillRect(x, y, cellSize, cellSize);
  }
}

3. Maze Generation Logic

Implement recursive backtracking with a stack to navigate through the grid, carving a path and removing walls between cells.

function removeWalls(a, b) {
  const x = a.x - b.x;
  const y = a.y - b.y;

  if (x === 1) {
    a.walls[3] = false;
    b.walls[1] = false;
  } else if (x === -1) {
    a.walls[1] = false;
    b.walls[3] = false;
  }

  if (y === 1) {
    a.walls[0] = false;
    b.walls[2] = false;
  } else if (y === -1) {
    a.walls[2] = false;
    b.walls[0] = false;
  }
}

for (let y = 0; y < rows; y++) {
  for (let x = 0; x < cols; x++) {
    grid.push(new Cell(x, y));
  }
}

current = grid[0];
const stack = [];

function step() {
  current.visited = true;
  const next = current.checkNeighbors();

  if (next) {
    next.visited = true;
    stack.push(current);
    removeWalls(current, next);
    current = next;
  } else if (stack.length > 0) {
    current = stack.pop();
  }
}

function draw() {
  requestAnimationFrame(draw);
  step();

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  grid.forEach(drawCell);
}

draw();

4. Conclusion

This implementation provides a simple yet powerful maze generator using JavaScript and the recursive backtracking algorithm. From here, you can experiment with different algorithms (like Prim’s or Kruskal’s), add player movement, or generate larger/dynamic mazes.

If you enjoyed this post, consider supporting my work: buymeacoffee.com/hexshift