Experimental
MAZE GENERATOR
Generate perfect mazes using advanced algorithms. Visualize the generation process and export in multiple formats.
Size: 20x20
Cells: 400
Status: Ready
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const canvas = document.getElementById('mazeCanvas');
const ctx = canvas.getContext('2d');
const generateBtn = document.getElementById('generateBtn');
const stopBtn = document.getElementById('stopBtn');
const btnText = document.getElementById('btnText');
let maze = null;
let isGenerating = false;
let animationId = null;
class Maze {
constructor(width, height, cellSize, wallWeight) {
this.width = width;
this.height = height;
this.cellSize = cellSize;
this.wallWeight = wallWeight;
this.grid = Array.from({ length: height }, () =>
Array.from({ length: width }, () => ({
visited: false,
walls: { top: true, right: true, bottom: true, left: true }
}))
);
}
drawCell(x, y, colorWall, colorPath) {
const px = x * this.cellSize;
const py = y * this.cellSize;
const cell = this.grid[y][x];
ctx.fillStyle = colorPath;
ctx.fillRect(px, py, this.cellSize, this.cellSize);
ctx.strokeStyle = colorWall;
ctx.lineWidth = this.wallWeight;
ctx.lineCap = 'square';
if (cell.walls.top) {
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px + this.cellSize, py);
ctx.stroke();
}
if (cell.walls.right) {
ctx.beginPath();
ctx.moveTo(px + this.cellSize, py);
ctx.lineTo(px + this.cellSize, py + this.cellSize);
ctx.stroke();
}
if (cell.walls.bottom) {
ctx.beginPath();
ctx.moveTo(px + this.cellSize, py + this.cellSize);
ctx.lineTo(px, py + this.cellSize);
ctx.stroke();
}
if (cell.walls.left) {
ctx.beginPath();
ctx.moveTo(px, py + this.cellSize);
ctx.lineTo(px, py);
ctx.stroke();
}
}
draw(colorWall, colorPath) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
this.drawCell(x, y, colorWall, colorPath);
}
}
}
}
async function generateDFS(maze, speed) {
const stack = [];
let current = { x: Math.floor(Math.random() * maze.width), y: Math.floor(Math.random() * maze.height) };
maze.grid[current.y][current.x].visited = true;
const wallColor = document.getElementById('wallColor').value;
const pathColor = document.getElementById('pathColor').value;
while (isGenerating) {
const neighbors = getUnvisitedNeighbors(maze, current);
if (neighbors.length > 0) {
const next = neighbors[Math.floor(Math.random() * neighbors.length)];
removeWalls(maze, current, next);
stack.push(current);
// Highlight current
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(current.x * maze.cellSize, current.y * maze.cellSize, maze.cellSize, maze.cellSize);
current = next;
maze.grid[current.y][current.x].visited = true;
if (speed < 100) {
maze.drawCell(current.x, current.y, wallColor, pathColor);
// Redraw neighbors too to fix shared walls
stack[stack.length-1] && maze.drawCell(stack[stack.length-1].x, stack[stack.length-1].y, wallColor, pathColor);
await new Promise(r => setTimeout(r, 101 - speed));
}
} else if (stack.length > 0) {
current = stack.pop();
if (speed < 100) {
maze.drawCell(current.x, current.y, wallColor, pathColor);
await new Promise(r => setTimeout(r, Math.max(1, (101 - speed)/4)));
}
} else {
break;
}
}
maze.draw(wallColor, pathColor);
}
async function generatePrims(maze, speed) {
const wallColor = document.getElementById('wallColor').value;
const pathColor = document.getElementById('pathColor').value;
const walls = [];
const startX = Math.floor(Math.random() * maze.width);
const startY = Math.floor(Math.random() * maze.height);
maze.grid[startY][startX].visited = true;
addNeighborsToWalls(maze, startX, startY, walls);
while (isGenerating && walls.length > 0) {
const index = Math.floor(Math.random() * walls.length);
const { from, to } = walls.splice(index, 1)[0];
if (!maze.grid[to.y][to.x].visited) {
maze.grid[to.y][to.x].visited = true;
removeWalls(maze, from, to);
addNeighborsToWalls(maze, to.x, to.y, walls);
if (speed < 100) {
maze.drawCell(from.x, from.y, wallColor, pathColor);
maze.drawCell(to.x, to.y, wallColor, pathColor);
await new Promise(r => setTimeout(r, 101 - speed));
}
}
}
maze.draw(wallColor, pathColor);
}
function addNeighborsToWalls(maze, x, y, walls) {
const directions = [
{ nx: x, ny: y - 1, dir: 'top' },
{ nx: x + 1, ny: y, dir: 'right' },
{ nx: x, ny: y + 1, dir: 'bottom' },
{ nx: x - 1, ny: y, dir: 'left' }
];
directions.forEach(({ nx, ny }) => {
if (nx >= 0 && nx < maze.width && ny >= 0 && ny < maze.height && !maze.grid[ny][nx].visited) {
walls.push({ from: { x, y }, to: { x: nx, y: ny } });
}
});
}
function getUnvisitedNeighbors(maze, current) {
const { x, y } = current;
const neighbors = [];
if (y > 0 && !maze.grid[y - 1][x].visited) neighbors.push({ x, y: y - 1 });
if (x < maze.width - 1 && !maze.grid[y][x + 1].visited) neighbors.push({ x: x + 1, y });
if (y < maze.height - 1 && !maze.grid[y + 1][x].visited) neighbors.push({ x, y: y + 1 });
if (x > 0 && !maze.grid[y][x - 1].visited) neighbors.push({ x: x - 1, y });
return neighbors;
}
function removeWalls(maze, a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
if (dx === 1) {
maze.grid[a.y][a.x].walls.left = false;
maze.grid[b.y][b.x].walls.right = false;
} else if (dx === -1) {
maze.grid[a.y][a.x].walls.right = false;
maze.grid[b.y][b.x].walls.left = false;
}
if (dy === 1) {
maze.grid[a.y][a.x].walls.top = false;
maze.grid[b.y][b.x].walls.bottom = false;
} else if (dy === -1) {
maze.grid[a.y][a.x].walls.bottom = false;
maze.grid[b.y][b.x].walls.top = false;
}
}
async function generateHuntAndKill(maze, speed) {
const wallColor = document.getElementById('wallColor').value;
const pathColor = document.getElementById('pathColor').value;
let current = { x: Math.floor(Math.random() * maze.width), y: Math.floor(Math.random() * maze.height) };
maze.grid[current.y][current.x].visited = true;
while (isGenerating) {
const neighbors = getUnvisitedNeighbors(maze, current);
if (neighbors.length > 0) {
const next = neighbors[Math.floor(Math.random() * neighbors.length)];
removeWalls(maze, current, next);
current = next;
maze.grid[current.y][current.x].visited = true;
if (speed < 100) {
maze.drawCell(current.x, current.y, wallColor, pathColor);
await new Promise(r => setTimeout(r, 101 - speed));
}
} else {
// Hunt
let found = false;
for (let y = 0; y < maze.height; y++) {
for (let x = 0; x < maze.width; x++) {
if (!maze.grid[y][x].visited) {
const visitedNeighbors = getVisitedNeighbors(maze, { x, y });
if (visitedNeighbors.length > 0) {
const neighbor = visitedNeighbors[Math.floor(Math.random() * visitedNeighbors.length)];
removeWalls(maze, { x, y }, neighbor);
maze.grid[y][x].visited = true;
current = { x, y };
found = true;
if (speed < 100) {
maze.drawCell(x, y, wallColor, pathColor);
await new Promise(r => setTimeout(r, 101 - speed));
}
break;
}
}
}
if (found) break;
}
if (!found) break;
}
}
maze.draw(wallColor, pathColor);
}
async function generateSidewinder(maze, speed) {
const wallColor = document.getElementById('wallColor').value;
const pathColor = document.getElementById('pathColor').value;
for (let y = 0; y < maze.height; y++) {
let run = [];
for (let x = 0; x < maze.width; x++) {
run.push({ x, y });
maze.grid[y][x].visited = true;
const atEasternBoundary = (x === maze.width - 1);
const atNorthernBoundary = (y === 0);
const shouldCloseOut = atEasternBoundary || (!atNorthernBoundary && Math.random() > 0.5);
if (shouldCloseOut) {
const member = run[Math.floor(Math.random() * run.length)];
if (!atNorthernBoundary) {
removeWalls(maze, member, { x: member.x, y: member.y - 1 });
}
run = [];
} else {
removeWalls(maze, { x, y }, { x: x + 1, y });
}
if (speed < 100) {
maze.drawCell(x, y, wallColor, pathColor);
if (x > 0) maze.drawCell(x - 1, y, wallColor, pathColor);
if (y > 0) maze.drawCell(x, y - 1, wallColor, pathColor);
await new Promise(r => setTimeout(r, 101 - speed));
}
}
}
maze.draw(wallColor, pathColor);
}
function getVisitedNeighbors(maze, current) {
const { x, y } = current;
const neighbors = [];
if (y > 0 && maze.grid[y - 1][x].visited) neighbors.push({ x, y: y - 1 });
if (x < maze.width - 1 && maze.grid[y][x + 1].visited) neighbors.push({ x: x + 1, y });
if (y < maze.height - 1 && maze.grid[y + 1][x].visited) neighbors.push({ x, y: y + 1 });
if (x > 0 && maze.grid[y][x - 1].visited) neighbors.push({ x: x - 1, y });
return neighbors;
}
async function startGeneration() {
if (isGenerating) return;
const w = parseInt(document.getElementById('mazeWidth').value);
const h = parseInt(document.getElementById('mazeHeight').value);
const speed = parseInt(document.getElementById('speed').value);
const wallWeight = parseInt(document.getElementById('wallWeight').value);
const algo = document.getElementById('algorithm').value;
const wallColor = document.getElementById('wallColor').value;
const pathColor = document.getElementById('pathColor').value;
// Constrain canvas size
const maxW = 800;
const cellSize = Math.max(4, Math.floor(maxW / Math.max(w, h)));
canvas.width = w * cellSize;
canvas.height = h * cellSize;
maze = new Maze(w, h, cellSize, wallWeight);
document.getElementById('statSize').innerHTML = `Size: <b>${w}x${h}</b>`;
document.getElementById('statCells').innerHTML = `Cells: <b>${w * h}</b>`;
document.getElementById('statStatus').innerHTML = `Status: <b>Generating...</b>`;
document.getElementById('asciiOutput').style.display = 'none';
isGenerating = true;
generateBtn.disabled = true;
btnText.innerText = "Generating...";
stopBtn.style.display = 'block';
if (algo === 'dfs') await generateDFS(maze, speed);
else if (algo === 'prim') await generatePrims(maze, speed);
else if (algo === 'hunt') await generateHuntAndKill(maze, speed);
else if (algo === 'sidewinder') await generateSidewinder(maze, speed);
else {
await generateDFS(maze, speed);
}
finishGeneration();
}
function finishGeneration() {
isGenerating = false;
generateBtn.disabled = false;
btnText.innerText = "Generate Maze";
stopBtn.style.display = 'none';
document.getElementById('statStatus').innerHTML = `Status: <b>Complete</b>`;
}
stopBtn.onclick = () => {
isGenerating = false;
finishGeneration();
};
generateBtn.onclick = startGeneration;
// Export Functions
function exportImage() {
if (!maze) return;
const link = document.createElement('a');
link.download = `maze_${maze.width}x${maze.height}.png`;
link.href = canvas.toDataURL();
link.click();
}
function exportSVG() {
if (!maze) return;
const wallColor = document.getElementById('wallColor').value;
const pathColor = document.getElementById('pathColor').value;
const w = maze.width * maze.cellSize;
const h = maze.height * maze.cellSize;
let svg = `<svg width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`;
svg += `<rect width="100%" height="100%" fill="${pathColor}"/>`;
svg += `<g stroke="${wallColor}" stroke-width="${maze.wallWeight}" stroke-linecap="square">`;
for (let y = 0; y < maze.height; y++) {
for (let x = 0; x < maze.width; x++) {
const cell = maze.grid[y][x];
const px = x * maze.cellSize;
const py = y * maze.cellSize;
const cs = maze.cellSize;
if (cell.walls.top) svg += `<line x1="${px}" y1="${py}" x2="${px+cs}" y2="${py}"/>`;
if (cell.walls.right) svg += `<line x1="${px+cs}" y1="${py}" x2="${px+cs}" y2="${py+cs}"/>`;
if (cell.walls.bottom) svg += `<line x1="${px+cs}" y1="${py+cs}" x2="${px}" y2="${py+cs}"/>`;
if (cell.walls.left) svg += `<line x1="${px}" y1="${py+cs}" x2="${px}" y2="${py}"/>`;
}
}
svg += `</g></svg>`;
const blob = new Blob([svg], {type: 'image/svg+xml'});
const link = document.createElement('a');
link.download = `maze_${maze.width}x${maze.height}.svg`;
link.href = URL.createObjectURL(blob);
link.click();
}
function exportJSON() {
if (!maze) return;
const data = {
width: maze.width,
height: maze.height,
grid: maze.grid.map(row => row.map(c => ({ w: c.walls })))
};
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const link = document.createElement('a');
link.download = `maze_${maze.width}x${maze.height}.json`;
link.href = URL.createObjectURL(blob);
link.click();
}
function exportASCII() {
if (!maze) return;
let output = "";
for (let y = 0; y < maze.height; y++) {
// Top walls
let row1 = "";
let row2 = "";
for (let x = 0; x < maze.width; x++) {
row1 += maze.grid[y][x].walls.top ? "+---" : "+ ";
row2 += maze.grid[y][x].walls.left ? "| " : " ";
}
output += row1 + "+\n" + row2 + "|\n";
}
// Bottom line
let lastRow = "";
for (let x = 0; x < maze.width; x++) {
lastRow += "+---";
}
output += lastRow + "+";
const el = document.getElementById('asciiOutput');
el.innerText = output;
el.style.display = 'block';
el.scrollIntoView({ behavior: 'smooth' });
}
function openInSolver() {
if (!maze) return;
const data = {
width: maze.width,
height: maze.height,
grid: maze.grid.map(row => row.map(c => ({ w: c.walls })))
};
localStorage.setItem('toolpad_maze_data', JSON.stringify(data));
window.open('/maze-solver/', '_blank');
}
function openIn3D() {
if (!maze) return;
const data = {
width: maze.width,
height: maze.height,
grid: maze.grid.map(row => row.map(c => ({ w: c.walls })))
};
localStorage.setItem('toolpad_maze_data', JSON.stringify(data));
window.open('/3d-maze-player/', '_blank');
}
// Initialize
window.onload = () => {
const w = 20, h = 20;
const cellSize = 20;
canvas.width = w * cellSize;
canvas.height = h * cellSize;
maze = new Maze(w, h, cellSize, 2);
maze.draw('#ff2200', '#0a0a0a');
};