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