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
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