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');
        };