Pico
Prompt
Create a maze generator and pathfinding visualizer. Randomly generate a maze and visualize A* algorithm solving it step by step. Use canvas and animations. Make it visually appealing.
A system prompt was added to support web rendering
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Maze Generator & A* Visualizer</title> <style> /* ───────────── BASIC LAYOUT ───────────── */ html,body{ height:100%; margin:0; display:flex; flex-direction:column; font-family:Arial, Helvetica, sans-serif; background:#202020; color:#eee; user-select:none; } #controls{ background:#2d2d2d; padding:8px 10px; display:flex; gap:10px; align-items:center; } button,select{ background:#444; color:#fff; border:none; padding:7px 14px; font-size:14px; border-radius:4px; cursor:pointer; } button:hover,select:hover{ background:#555; } canvas{ flex:1; background:#111; display:block; } </style> </head> <body> <div id="controls"> <button id="btnGenerate">Generate Maze</button> <button id="btnSolve">Solve with A*</button> <label style="margin-left:auto"> Size <select id="sizeSelect"> <option value="20">20 × 20</option> <option value="30" selected>30 × 30</option> <option value="40">40 × 40</option> <option value="50">50 × 50</option> </select> </label> </div> <canvas id="canvas"></canvas> <script> /* Maze generator (recursive-backtracker) and A* step-by-step visualizer ───────────────────────────────────────────────────────────────────── Pure vanilla JS – no external libs required */ (() => { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); /* ───────────── PARAMETERS ───────────── */ let rows = 30, cols = 30; // will be updated by <select> let cellSize = 20; // computed on resize let grid = []; // 2-D array of Cell let openSet = []; // A* frontier let closedSet = new Set(); // A* explored let path = []; // final back-traced path let solving = false; // animation flag let start, end; // Cell references /* ───────────── CELL CLASS ───────────── */ class Cell{ constructor(r,c){ this.r = r; this.c = c; this.walls = [true,true,true,true]; // top, right, bottom, left this.visited = false; // for maze generation // A* cost fields this.f = 0; this.g = 0; this.h = 0; this.prev = null; } neighbors(){ const n=[]; if(this.r>0) n.push(grid[this.r-1][this.c]); // top if(this.c<cols-1) n.push(grid[this.r][this.c+1]); // right if(this.r<rows-1) n.push(grid[this.r+1][this.c]); // bottom if(this.c>0) n.push(grid[this.r][this.c-1]); // left return n; } unvisitedNeighbor(){ const un = this.neighbors().filter(v=>!v.visited); return un.length ? un[Math.random()*un.length|0] : null; } } /* ─────────── UTILITY FUNCTIONS ─────────── */ const removeWalls = (a,b) => { const x = a.c - b.c, y = a.r - b.r; if (x === 1) { a.walls[3]=b.walls[1]=false; } if (x === -1) { a.walls[1]=b.walls[3]=false; } if (y === 1) { a.walls[0]=b.walls[2]=false; } if (y === -1) { a.walls[2]=b.walls[0]=false; } }; const heuristic = (a,b) => Math.abs(a.r-b.r) + Math.abs(a.c-b.c); // Manhattan /* ─────────── MAZE GENERATION ─────────── */ function generateMaze(){ grid = Array.from({length:rows}, (_,r)=> Array.from({length:cols}, (_,c)=>new Cell(r,c)) ); let current = grid[0][0]; current.visited = true; const stack = []; while(true){ const next = current.unvisitedNeighbor(); if(next){ next.visited = true; stack.push(current); removeWalls(current,next); current = next; }else if(stack.length){ current = stack.pop(); }else break; } // cleanup grid.flat().forEach(c=>c.visited=false); start = grid[0][0]; end = grid[rows-1][cols-1]; } /* ─────────── A* INITIALIZATION ─────────── */ function initAStar(){ openSet = [start]; closedSet.clear(); path.length = 0; grid.flat().forEach(c=>{ c.f=c.g=c.h=0; c.prev=null; }); solving = true; } /* ─────────── A* SINGLE STEP ─────────── */ function stepAStar(){ if(!openSet.length){ solving=false; return; } // get node with smallest f let best = 0; for(let i=1;i<openSet.length;i++) if(openSet[i].f < openSet[best].f) best = i; const current = openSet.splice(best,1)[0]; if(current === end){ // finished! path.length = 0; let t = current; while(t){ path.push(t); t = t.prev; } solving = false; return; } closedSet.add(current); current.neighbors().forEach(nei=>{ // Is there a wall between? let dir; if(nei.r===current.r-1) dir=0; else if(nei.c===current.c+1) dir=1; else if(nei.r===current.r+1) dir=2; else if(nei.c===current.c-1) dir=3; if(current.walls[dir]) return; // blocked if(closedSet.has(nei)) return; // already processed const tentativeG = current.g + 1; let better = false; if(!openSet.includes(nei)){ openSet.push(nei); better = true; } else if(tentativeG < nei.g){ better = true; } if(better){ nei.prev = current; nei.g = tentativeG; nei.h = heuristic(nei,end); nei.f = nei.g + nei.h; } }); } /* ─────────── DRAW EVERYTHING ─────────── */ function draw(){ ctx.clearRect(0,0,canvas.width,canvas.height); // 1. walls ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; grid.forEach(row=>{ row.forEach(cell=>{ const x = cell.c * cellSize; const y = cell.r * cellSize; ctx.beginPath(); if(cell.walls[0]){ ctx.moveTo(x,y); ctx.lineTo(x+cellSize,y); } if(cell.walls[1]){ ctx.moveTo(x+cellSize,y); ctx.lineTo(x+cellSize,y+cellSize);} if(cell.walls[2]){ ctx.moveTo(x+cellSize,y+cellSize); ctx.lineTo(x,y+cellSize);} if(cell.walls[3]){ ctx.moveTo(x,y+cellSize); ctx.lineTo(x,y); } ctx.stroke(); }); }); // 2. closedSet (explored) closedSet.forEach(cell=>{ ctx.fillStyle = 'rgba(70,130,180,0.4)'; ctx.fillRect(cell.c*cellSize+2, cell.r*cellSize+2, cellSize-4, cellSize-4); }); // 3. openSet (frontier) openSet.forEach(cell=>{ ctx.fillStyle = 'rgba(50,220,100,0.5)'; ctx.fillRect(cell.c*cellSize+2, cell.r*cellSize+2, cellSize-4, cellSize-4); }); // 4. final path path.forEach(cell=>{ ctx.fillStyle = 'rgba(255,215,0,0.8)'; ctx.fillRect(cell.c*cellSize+3, cell.r*cellSize+3, cellSize-6, cellSize-6); }); // 5. start / end ctx.fillStyle='lime'; ctx.fillRect(start.c*cellSize+4, start.r*cellSize+4, cellSize-8, cellSize-8); ctx.fillStyle='red'; ctx.fillRect(end.c*cellSize+4, end.r*cellSize+4, cellSize-8, cellSize-8); } /* ─────────── ANIMATION LOOP ─────────── */ function animate(){ if(solving){ // multiple logical steps per frame for speed for(let i=0;i<4 && solving;i++) stepAStar(); } draw(); requestAnimationFrame(animate); } /* ─────────── RESIZE HANDLER ─────────── */ function resizeCanvas(){ canvas.width = window.innerWidth; canvas.height = window.innerHeight - document.getElementById('controls').offsetHeight; cellSize = Math.min(Math.floor(canvas.width/cols), Math.floor(canvas.height/rows)); draw(); } window.addEventListener('resize', resizeCanvas); /* ─────────── UI CONTROLS ─────────── */ document.getElementById('btnGenerate').onclick = () => { solving = false; path.length = 0; rows = cols = +document.getElementById('sizeSelect').value; generateMaze(); resizeCanvas(); }; document.getElementById('btnSolve').onclick = () => { if(!solving) initAStar(); }; document.getElementById('sizeSelect').onchange = () => document.getElementById('btnGenerate').click(); /* ─────────── INITIALIZE ─────────── */ generateMaze(); resizeCanvas(); animate(); })(); </script> </body> </html> ```
Here's a complete HTML file with a maze generator and A* pathfinding visualizer using canvas and animations. I've included all necessary code and styles in a single block for easy rendering, using Tailwind CSS for styling and no external libraries beyond CDN links. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Maze Generator & A* Pathfinding Visualizer</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"></script> <style> canvas { border: 1px solid #4B5563; background: #1F2937; } .container { display: flex; flex-direction: column; align-items: center; background: linear-gradient(to bottom, #1F2937, #111827); min-height: 100vh; padding: 20px; } .controls { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; max-width: 800px; } button { background: #3B82F6; color: white; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 600; transition: all 0.3s ease; } button:hover { background: #2563EB; transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); } button:disabled { background: #4B5563; cursor: not-allowed; transform: none; box-shadow: none; } h1, p { color: #F3F4F6; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .legend { display: flex; gap: 15px; color: #F3F4F6; font-weight: 500; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); justify-content: center; flex-wrap: wrap; max-width: 800px; } .color-box { width: 20px; height: 20px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } </style> </head> <body class="m-0 p-0"> <div class="container"> <h1 class="text-3xl font-bold mb-4">Maze Generator & A* Pathfinding Visualizer</h1> <p class="text-lg mb-6 text-center">Generate a random maze and watch the A* algorithm find the shortest path from start to end.</p> <canvas id="mazeCanvas" width="800" height="600"></canvas> <div class="legend mt-4"> <div class="flex items-center gap-2"><div class="color-box bg-green-500"></div> Start</div> <div class="flex items-center gap-2"><div class="color-box bg-red-500"></div> End</div> <div class="flex items-center gap-2"><div class="color-box bg-blue-400"></div> Path</div> <div class="flex items-center gap-2"><div class="color-box bg-yellow-400"></div> Open Set</div> <div class="flex items-center gap-2"><div class="color-box bg-purple-400"></div> Closed Set</div> <div class="flex items-center gap-2"><div class="color-box bg-gray-800"></div> Walls</div> </div> <div class="controls mt-6"> <button id="generateBtn">Generate Maze</button> <button id="solveBtn">Solve with A*</button> <button id="stepBtn">Step</button> <button id="resetBtn">Reset</button> </div> </div> <script> const canvas = document.getElementById('mazeCanvas'); const ctx = canvas.getContext('2d'); const rows = 30; const cols = 40; const cellSize = canvas.width / cols; let maze = []; let openSet = []; let closedSet = []; let start = null; let end = null; let path = []; let isSolving = false; let animationFrameId = null; class Cell { constructor(row, col) { this.row = row; this.col = col; this.wall = false; this.g = 0; this.h = 0; this.f = 0; this.previous = null; } } function initializeMaze() { maze = Array.from({ length: rows }, (_, r) => Array.from({ length: cols }, (_, c) => new Cell(r, c)) ); start = maze[1][1]; end = maze[rows - 2][cols - 2]; openSet = []; closedSet = []; path = []; isSolving = false; if (animationFrameId) cancelAnimationFrame(animationFrameId); } function generateMaze() { initializeMaze(); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { maze[r][c].wall = Math.random() < 0.3; } } start.wall = false; end.wall = false; drawMaze(); } function drawMaze() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const cell = maze[r][c]; ctx.fillStyle = cell.wall ? '#1F2937' : '#E5E7EB'; ctx.fillRect(c * cellSize, r * cellSize, cellSize, cellSize); ctx.strokeStyle = '#4B5563'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(c * cellSize, r * cellSize); ctx.lineTo((c + 1) * cellSize, r * cellSize); ctx.lineTo((c + 1) * cellSize, (r + 1) * cellSize); ctx.lineTo(c * cellSize, (r + 1) * cellSize); ctx.lineTo(c * cellSize, r * cellSize); ctx.stroke(); } } closedSet.forEach(cell => { ctx.fillStyle = '#A78BFA'; ctx.fillRect(cell.col * cellSize + 2, cell.row * cellSize + 2, cellSize - 4, cellSize - 4); }); openSet.forEach(cell => { ctx.fillStyle = '#FACC15'; ctx.fillRect(cell.col * cellSize + 2, cell.row * cellSize + 2, cellSize - 4, cellSize - 4); }); path.forEach(cell => { ctx.fillStyle = '#60A5FA'; ctx.fillRect(cell.col * cellSize + 2, cell.row * cellSize + 2, cellSize - 4, cellSize - 4); }); ctx.fillStyle = '#10B981'; ctx.fillRect(start.col * cellSize + 2, start.row * cellSize + 2, cellSize - 4, cellSize - 4); ctx.fillStyle = '#EF4444'; ctx.fillRect(end.col * cellSize + 2, end.row * cellSize + 2, cellSize - 4, cellSize - 4); } function heuristic(cell) { return Math.abs(cell.row - end.row) + Math.abs(cell.col - end.col); } function getNeighbors(cell) { const neighbors = []; const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; for (let [dr, dc] of directions) { const nr = cell.row + dr; const nc = cell.col + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !maze[nr][nc].wall) { neighbors.push(maze[nr][nc]); } } return neighbors; } function solveStep() { if (openSet.length === 0) { isSolving = false; alert('No path found!'); return false; } let current = openSet.reduce((min, cell) => cell.f < min.f ? cell : min); if (current === end) { path = []; let temp = current; while (temp) { path.unshift(temp); temp = temp.previous; } drawMaze(); isSolving = false; return false; } openSet = openSet.filter(cell => cell !== current); closedSet.push(current); const neighbors = getNeighbors(current); neighbors.forEach(neighbor => { if (!closedSet.includes(neighbor)) { const tentativeG = current.g + 1; if (!openSet.includes(neighbor)) { openSet.push(neighbor); } else if (tentativeG >= neighbor.g) { return; } neighbor.g = tentativeG; neighbor.h = heuristic(neighbor); neighbor.f = neighbor.g + neighbor.h; neighbor.previous = current; } }); drawMaze(); return true; } function solveAStar() { if (isSolving) return; initializeMaze(); openSet.push(start); start.g = 0; start.h = heuristic(start); start.f = start.h; isSolving = true; function animate() { if (isSolving && solveStep()) { animationFrameId = requestAnimationFrame(animate); } else { animationFrameId = null; } } animationFrameId = requestAnimationFrame(animate); } document.getElementById('generateBtn').addEventListener('click', generateMaze); document.getElementById('solveBtn').addEventListener('click', solveAStar); document.getElementById('stepBtn').addEventListener('click', () => { if (!isSolving) { initializeMaze(); openSet.push(start); start.g = 0; start.h = heuristic(start); start.f = start.h; isSolving = true; } solveStep(); }); document.getElementById('resetBtn').addEventListener('click', () => { initializeMaze(); drawMaze(); }); generateMaze(); </script> </body> </html> ``` This code creates a visually appealing maze generator and A* pathfinding visualizer. It uses a canvas to draw a grid-based maze with randomly generated walls, a green start point, and a red end point. The A* algorithm is visualized step-by-step, showing the open set (yellow), closed set (purple), and final path (blue). Gradient backgrounds, shadows, and smooth animations enhance the visual appeal. Tailwind CSS (via CDN) styles the UI components, and the controls allow users to generate new mazes, solve automatically, step through the solution, or reset the visualization.
Of course! Here is a complete, self-contained maze generator and A* pathfinding visualizer. The application first animates the creation of a random maze using a Depth-First Search algorithm. Once generated, you can trigger the A* algorithm, which will visually search for the optimal path from the top-left to the bottom-right corner, highlighting the nodes it explores. Finally, it draws the shortest path found. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Maze Generator & A* Pathfinder</title> <style> @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap'); body { font-family: 'Poppins', sans-serif; background-color: #121212; color: #e0e0e0; display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 0; padding: 1rem; min-height: 100vh; text-align: center; overflow: hidden; } .container { display: flex; flex-direction: column; align-items: center; gap: 1.5rem; } .controls { display: flex; flex-direction: column; align-items: center; gap: 1rem; } h1 { margin: 0; color: #ffffff; font-weight: 600; font-size: 2.5rem; } p#status { color: #bb86fc; font-size: 1.1rem; height: 20px; margin: 0; transition: color 0.3s ease; } .button-group { display: flex; gap: 1rem; } button { background-color: #03dac6; color: #121212; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; transition: background-color 0.3s ease, transform 0.1s ease; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); } button:hover:not(:disabled) { background-color: #018786; color: #ffffff; } button:active:not(:disabled) { transform: scale(0.98); } button:disabled { background-color: #444; color: #888; cursor: not-allowed; box-shadow: none; } canvas { background-color: #1e1e1e; border-radius: 8px; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); } </style> </head> <body> <div class="container"> <div class="controls"> <h1>Maze & A* Pathfinder</h1> <p id="status">Click 'Generate New Maze' to begin.</p> <div class="button-group"> <button id="generateBtn">Generate New Maze</button> <button id="solveBtn" disabled>Solve Maze</button> </div> </div> <canvas id="mazeCanvas"></canvas> </div> <script> const canvas = document.getElementById('mazeCanvas'); const ctx = canvas.getContext('2d'); const generateBtn = document.getElementById('generateBtn'); const solveBtn = document.getElementById('solveBtn'); const statusEl = document.getElementById('status'); // --- Configuration --- const cellSize = 20; const wallThickness = 2; const animationSpeed = 2; // Lower is faster. 1 is max speed. const mazeColor = '#e0e0e0'; const generatorHighlightColor = '#bb86fc'; const openSetColor = 'rgba(3, 218, 198, 0.25)'; const closedSetColor = 'rgba(207, 102, 121, 0.2)'; const pathColor = '#ffd700'; const startColor = '#03dac6'; const endColor = '#cf6679'; let cols, rows; let grid = []; let generatorStack = []; let currentGeneratorCell; let openSet = []; let closedSet = []; let startNode, endNode; let finalPath = []; let state = 'idle'; // 'generating', 'solving', 'done' let frameCount = 0; let animationFrameId; // --- Cell Class --- class Cell { constructor(i, j) { this.i = i; // Column this.j = j; // Row this.walls = { top: true, right: true, bottom: true, left: true }; this.visited = false; // A* properties this.g = Infinity; this.h = 0; this.f = Infinity; this.parent = null; } getGeneratorNeighbors() { const neighbors = []; const top = grid[this.i]?.[this.j - 1]; const right = grid[this.i + 1]?.[this.j]; const bottom = grid[this.i]?.[this.j + 1]; const left = grid[this.i - 1]?.[this.j]; if (top && !top.visited) neighbors.push(top); if (right && !right.visited) neighbors.push(right); if (bottom && !bottom.visited) neighbors.push(bottom); if (left && !left.visited) neighbors.push(left); if (neighbors.length > 0) { return neighbors[Math.floor(Math.random() * neighbors.length)]; } return undefined; } getSolverNeighbors() { const neighbors = []; const top = grid[this.i]?.[this.j - 1]; const right = grid[this.i + 1]?.[this.j]; const bottom = grid[this.i]?.[this.j + 1]; const left = grid[this.i - 1]?.[this.j]; if (top && !this.walls.top) neighbors.push(top); if (right && !this.walls.right) neighbors.push(right); if (bottom && !this.walls.bottom) neighbors.push(bottom); if (left && !this.walls.left) neighbors.push(left); return neighbors; } highlight(color) { const x = this.i * cellSize + wallThickness / 2; const y = this.j * cellSize + wallThickness / 2; ctx.fillStyle = color; ctx.fillRect(x, y, cellSize - wallThickness, cellSize - wallThickness); } draw() { const x = this.i * cellSize; const y = this.j * cellSize; ctx.strokeStyle = mazeColor; ctx.lineWidth = wallThickness; ctx.lineCap = 'round'; const path = new Path2D(); if (this.walls.top) path.moveTo(x, y); path.lineTo(x + cellSize, y); if (this.walls.right) path.moveTo(x + cellSize, y); path.lineTo(x + cellSize, y + cellSize); if (this.walls.bottom) path.moveTo(x + cellSize, y + cellSize); path.lineTo(x, y + cellSize); if (this.walls.left) path.moveTo(x, y + cellSize); path.lineTo(x, y); ctx.stroke(path); } } // --- Setup and Control --- function setup() { const containerWidth = Math.min(window.innerWidth * 0.9, 800); cols = Math.floor(containerWidth / cellSize); rows = Math.floor(window.innerHeight * 0.6 / cellSize); canvas.width = cols * cellSize; canvas.height = rows * cellSize; grid = Array(cols).fill(null).map(() => Array(rows).fill(null)); for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { grid[i][j] = new Cell(i, j); } } currentGeneratorCell = grid[0][0]; currentGeneratorCell.visited = true; generatorStack = [currentGeneratorCell]; } function removeWalls(a, b) { const x = a.i - b.i; if (x === 1) { a.walls.left = false; b.walls.right = false; } else if (x === -1) { a.walls.right = false; b.walls.left = false; } const y = a.j - b.j; if (y === 1) { a.walls.top = false; b.walls.bottom = false; } else if (y === -1) { a.walls.bottom = false; b.walls.top = false; } } function heuristic(a, b) { // Manhattan distance return Math.abs(a.i - b.i) + Math.abs(a.j - b.j); } function reconstructPath(current) { let path = []; let temp = current; path.push(temp); while (temp.parent) { path.push(temp.parent); temp = temp.parent; } return path.reverse(); } function updateStatus(text, color = '#bb86fc') { statusEl.textContent = text; statusEl.style.color = color; } // --- Animation Loops --- function animate() { if (state === 'generating') animateGenerator(); else if (state === 'solving') animateSolver(); else if (state === 'drawingPath') animateFinalPath(); } function animateGenerator() { animationFrameId = requestAnimationFrame(animateGenerator); if (++frameCount % animationSpeed !== 0) return; if (generatorStack.length > 0) { currentGeneratorCell = generatorStack.pop(); const next = currentGeneratorCell.getGeneratorNeighbors(); if (next) { generatorStack.push(currentGeneratorCell); next.visited = true; removeWalls(currentGeneratorCell, next); generatorStack.push(next); } } else { state = 'idle'; updateStatus("Maze generated. Ready to solve!", '#03dac6'); generateBtn.disabled = false; solveBtn.disabled = false; cancelAnimationFrame(animationFrameId); // Final draw drawGrid(); grid[0][0].highlight(startColor); grid[cols - 1][rows - 1].highlight(endColor); return; } drawGrid(); currentGeneratorCell.highlight(generatorHighlightColor); } function animateSolver() { animationFrameId = requestAnimationFrame(animateSolver); if (++frameCount % animationSpeed !== 0) return; drawGrid(); drawSets(); if (openSet.length > 0) { let winnerIndex = 0; for (let i = 0; i < openSet.length; i++) { if (openSet[i].f < openSet[winnerIndex].f) { winnerIndex = i; } } let current = openSet[winnerIndex]; if (current === endNode) { finalPath = reconstructPath(current); state = 'drawingPath'; updateStatus('Path found! Visualizing...', '#ffd700'); cancelAnimationFrame(animationFrameId); frameCount = 0; animateFinalPath(); return; } openSet.splice(winnerIndex, 1); closedSet.push(current); const neighbors = current.getSolverNeighbors(); for (const neighbor of neighbors) { if (closedSet.includes(neighbor)) continue; let tempG = current.g + 1; let newPath = false; if (openSet.includes(neighbor)) { if (tempG < neighbor.g) { neighbor.g = tempG; newPath = true; } } else { neighbor.g = tempG; newPath = true; openSet.push(neighbor); } if (newPath) { neighbor.h = heuristic(neighbor, endNode); neighbor.f = neighbor.g + neighbor.h; neighbor.parent = current; } } } else { // No solution state = 'done'; updateStatus('No solution found.', '#cf6679'); generateBtn.disabled = false; solveBtn.disabled = true; cancelAnimationFrame(animationFrameId); return; } } let pathIndex = 0; function animateFinalPath() { if (pathIndex < finalPath.length) { animationFrameId = requestAnimationFrame(animateFinalPath); if (++frameCount % animationSpeed !== 0) return; drawPathSegment(); pathIndex++; } else { state = 'done'; updateStatus('Solved!', '#03dac6'); generateBtn.disabled = false; solveBtn.disabled = true; cancelAnimationFrame(animationFrameId); } } // --- Drawing Functions --- function drawGrid() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { grid[i][j].draw(); } } } function drawSets() { for (const cell of closedSet) { cell.highlight(closedSetColor); } for (const cell of openSet) { cell.highlight(openSetColor); } } function drawPathSegment() { ctx.lineWidth = cellSize / 3; ctx.strokeStyle = pathColor; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; const path = new Path2D(); const startX = finalPath[0].i * cellSize + cellSize / 2; const startY = finalPath[0].j * cellSize + cellSize / 2; path.moveTo(startX, startY); for (let i = 1; i <= pathIndex && i < finalPath.length; i++) { const x = finalPath[i].i * cellSize + cellSize / 2; const y = finalPath[i].j * cellSize + cellSize / 2; path.lineTo(x, y); } ctx.stroke(path); // Redraw start and end points on top startNode.highlight(startColor); endNode.highlight(endColor); } // --- Event Listeners --- generateBtn.addEventListener('click', () => { if (state === 'generating' || state === 'solving') return; cancelAnimationFrame(animationFrameId); state = 'generating'; updateStatus('Generating maze...'); generateBtn.disabled = true; solveBtn.disabled = true; setup(); frameCount = 0; animateGenerator(); }); solveBtn.addEventListener('click', () => { if (state !== 'idle') return; state = 'solving'; updateStatus('Solving with A* algorithm...'); generateBtn.disabled = true; solveBtn.disabled = true; // a-star setup openSet = []; closedSet = []; finalPath = []; pathIndex = 0; startNode = grid[0][0]; endNode = grid[cols - 1][rows - 1]; startNode.g = 0; startNode.f = heuristic(startNode, endNode); openSet.push(startNode); frameCount = 0; animateSolver(); }); // Initial setup on load window.addEventListener('load', () => { setup(); drawGrid(); }); window.addEventListener('resize', () => { // A simple redraw on resize. Could be smarter. if(state === 'idle' || state === 'done') { setup(); drawGrid(); updateStatus("Canvas resized. Generate a new maze."); solveBtn.disabled = true; } }); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Maze Generator & A* Visualizer</title> <style> body { background: #232946; margin: 0; padding: 0; display: flex; flex-direction: column; min-height: 100vh; align-items: center; justify-content: center; font-family: "Segoe UI", sans-serif; color: #fff; } h1 { letter-spacing: 2px; margin-bottom: 8px; margin-top: 24px; } .controls { padding: 16px; display: flex; gap: 16px; align-items: center; justify-content: center; } button, select { background: #394a6d; color: #eebbc3; border: none; border-radius: 5px; padding: 8px 18px; font-size: 1rem; cursor: pointer; transition: background 0.2s; } button:hover, select:hover { background: #eebbc3; color: #232946; } .canvas-container { box-shadow: 0 6px 32px rgba(0,0,0,0.40), 0 0 2px #232946; border-radius: 14px; overflow: hidden; background: #d4d8f0; margin-bottom: 32px; } @media (max-width: 800px) { .canvas-container { width: 96vw !important; height: 96vw !important; max-width: 95vw; max-height: 95vw; } } </style> </head> <body> <h1>Maze Generator & A* Pathfinding Visualizer</h1> <div class="controls"> <button id="generate">Generate Maze</button> <button id="solve">Solve (A*)</button> <button id="reset">Clear Path</button> <label> Grid: <select id="size"> <option value="15">15 x 15</option> <option value="20" selected>20 x 20</option> <option value="25">25 x 25</option> <option value="30">30 x 30</option> </select> </label> </div> <div class="canvas-container" id="canvas-container" style="width:600px;height:600px;"> <canvas id="maze" width="600" height="600"></canvas> </div> <script> // ---------------------- Settings --------------------- const COLORS = { background: "#d4d8f0", wall: "#232946", cell: "#eebbc3", frontier: "#94a1b2", visited: "#f3dfc1", path: "#3f72af", start: "#06d6a0", goal: "#ef476f", text: "#232946" }; // ---------------------- Utility ---------------------- function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function shuffle(arr) { for(let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } function lerp(a, b, t) { return a + (b - a) * t; } // --------------------- Maze Classes --------------------- class Cell { constructor(x, y, n) { this.x = x; this.y = y; this.n = n; this.walls = [true, true, true, true]; // top,right,bottom,left this.visited = false; } neighbors(grid) { const dirs = [ [0, -1], [1, 0], [0, 1], [-1, 0] ]; const neigh = []; for(let d=0; d<4; d++) { const nx = this.x + dirs[d][0]; const ny = this.y + dirs[d][1]; if(nx >=0 && nx < this.n && ny >=0 && ny < this.n) { neigh.push({dir:d, cell: grid[nx][ny]}); } } return neigh; } } class Maze { constructor(n) { this.n = n; this.grid = []; for(let x=0; x<n; x++) { this.grid[x] = []; for(let y=0; y<n; y++) { this.grid[x][y] = new Cell(x, y, n); } } this.start = this.grid[0][0]; this.goal = this.grid[n-1][n-1]; } async generate(draw, delay=12) { // Reset cells for (let x=0; x<this.n; x++) for (let y=0; y<this.n; y++) this.grid[x][y].visited = false; const stack = []; let current = this.start; current.visited = true; stack.push(current); while (stack.length > 0) { // Slight animate if(draw) draw(); await sleep(delay); current = stack[stack.length - 1]; const unvisited = shuffle(current.neighbors(this.grid).filter( n => !n.cell.visited )); if (unvisited.length > 0) { const {dir, cell: next} = unvisited[0]; current.walls[dir] = false; next.walls[(dir + 2) % 4] = false; next.visited = true; stack.push(next); } else { stack.pop(); } } // Mark all cells as unvisited for pathfinding. for (let x=0; x<this.n; x++) for (let y=0; y<this.n; y++) this.grid[x][y].visited = false; } } // --------------------- Drawing --------------------- class MazeRenderer { constructor(maze, ctx, w, h) { this.maze = maze; this.ctx = ctx; this.w = w; this.h = h; this.n = maze.n; this.cellSize = Math.min(w, h) / this.n; } drawMaze() { const {ctx, n, cellSize} = this; ctx.fillStyle = COLORS.background; ctx.fillRect(0,0, this.w, this.h); ctx.lineWidth = 2.4; ctx.strokeStyle = COLORS.wall; ctx.save(); for(let x=0; x<n; x++) { for(let y=0; y<n; y++) { const cell = this.maze.grid[x][y]; const px = x * cellSize; const py = y * cellSize; // cell background ctx.fillStyle = COLORS.cell; ctx.fillRect(px, py, cellSize, cellSize); // walls ctx.beginPath(); if(cell.walls[0]) { // top ctx.moveTo(px, py); ctx.lineTo(px+cellSize, py); } if(cell.walls[1]) { // right ctx.moveTo(px+cellSize, py); ctx.lineTo(px+cellSize, py+cellSize); } if(cell.walls[2]) { // bottom ctx.moveTo(px+cellSize, py+cellSize); ctx.lineTo(px, py+cellSize); } if(cell.walls[3]) { // left ctx.moveTo(px, py+cellSize); ctx.lineTo(px, py); } ctx.stroke(); } } ctx.restore(); // Draw start and goal this.drawCell(this.maze.start, COLORS.start); this.drawCell(this.maze.goal, COLORS.goal); } drawCell(cell, color, alpha=1.0, border=false) { const {cellSize, ctx} = this; ctx.save(); ctx.globalAlpha = alpha; ctx.fillStyle = color; ctx.beginPath(); ctx.roundRect( cell.x * cellSize + 1, cell.y * cellSize + 1, cellSize - 2, cellSize - 2, lerp(2, 5, 1 - alpha) ); ctx.fill(); if (border) { ctx.lineWidth = 2; ctx.strokeStyle = "#232946"; ctx.stroke(); } ctx.restore(); } drawCells(cells, color, alpha=1.0, border=false) { for (const c of cells) this.drawCell(c, color, alpha, border); } drawPath(path, thick=1) { const {cellSize, ctx} = this; ctx.save(); ctx.strokeStyle = COLORS.path; ctx.lineWidth = 5 * thick; ctx.globalAlpha = 0.8; ctx.beginPath(); for(let i=0; i<path.length; i++) { const c = path[i]; const cx = c.x * cellSize + cellSize/2; const cy = c.y * cellSize + cellSize/2; if(i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy); } ctx.stroke(); ctx.restore(); // draw head and tail if(path.length) { this.drawCell(path[0], COLORS.start, 0.45, true); this.drawCell(path[path.length-1], COLORS.goal, 0.73, true); } } } // ------------------- Pathfinding (A*) ----------------- async function astar(maze, render, delay=6) { const n = maze.n; for (let x=0; x<n; x++) for (let y=0; y<n; y++) maze.grid[x][y].visited=false; let open = []; let closed = new Set(); let cameFrom = {}; let gScore = {}; let fScore = {}; function key(c) { return `${c.x},${c.y}`; } const start = maze.start, goal = maze.goal; gScore[key(start)] = 0; fScore[key(start)] = heuristic(start, goal); open.push(start); let visitedOrder=[], frontierOrder=[]; while(open.length > 0) { // Pick node with least fscore open.sort((a, b) => fScore[key(a)] - fScore[key(b)]); let current = open[0]; // Animate: show open/closed/frontier frontierOrder = open.map(c => c); visitedOrder.push(current); if(render) render(visitedOrder, frontierOrder, []); await sleep(delay); if(current === goal) { // reconstruct path let path = [current]; let ck = key(current); while (cameFrom[ck]) { path.push(cameFrom[ck]); ck = key(cameFrom[ck]); } path.reverse(); // Animate full path for(let i=0; i<path.length; i++) { if(render) render(visitedOrder, frontierOrder, path.slice(0,i+1)); await sleep(14); } return path; } open.shift(); closed.add(key(current)); current.visited = true; for(const {dir, cell:neighbor} of current.neighbors(maze.grid)) { // wall in between? if(current.walls[dir]) continue; if(closed.has(key(neighbor))) continue; const tentative_g = (gScore[key(current)] ?? Infinity) + 1; if (!open.includes(neighbor)) open.push(neighbor); if(tentative_g < (gScore[key(neighbor)] ?? Infinity)) { cameFrom[key(neighbor)] = current; gScore[key(neighbor)] = tentative_g; fScore[key(neighbor)] = tentative_g + heuristic(neighbor, goal); } } } return []; } // ----------------- Main Logic & Interaction ----------------- let n = 20, // initial grid maze = new Maze(n), canvas = document.getElementById('maze'), ctx = canvas.getContext('2d'), renderer = new MazeRenderer(maze, ctx, canvas.width, canvas.height); function resizeCanvas(n) { // Find suitable canvas size let container = document.getElementById('canvas-container'); let size = Math.min(600, window.innerWidth * 0.95, window.innerHeight*0.68); container.style.width = `${size}px`; container.style.height = `${size}px`; canvas.width = canvas.height = size; renderer = new MazeRenderer(maze, ctx, size, size); } async function generateMaze(animated=true) { maze = new Maze(n); renderer = new MazeRenderer(maze, ctx, canvas.width, canvas.height); await maze.generate(() => renderer.drawMaze(), animated? 9: 0); renderer.drawMaze(); } function clearPath() { renderer.drawMaze(); } async function solveMaze() { await astar(maze, (visited, frontier, path) => { renderer.drawMaze(); renderer.drawCells(visited, COLORS.visited, 0.58); renderer.drawCells(frontier, COLORS.frontier, 0.80, true); if(path.length) renderer.drawPath(path, 1.0); }); } // Initial draw renderer.drawMaze(); // Responsive resize window.addEventListener('resize', () => { resizeCanvas(n); renderer.drawMaze(); }); resizeCanvas(n); // Controls document.getElementById('generate').onclick = async function() { await generateMaze(); } document.getElementById('solve').onclick = async function() { await solveMaze(); } document.getElementById('reset').onclick = function() { clearPath(); } document.getElementById('size').onchange = async function(e) { n = Number(this.value); resizeCanvas(n); await generateMaze(false); } // Generate first maze (async ()=>{ await generateMaze(); })(); </script> </body> </html> ```
```html <canvas id="mazeCanvas" width="800" height="800"></canvas> <script> class Cell { constructor(row, col) { this.row = row; this.col = col; this.walls = { top: true, right: true, bottom: true, left: true }; this.visited = false; } } function generateGrid(rows, cols) { const grid = []; for (let i = 0; i < rows; i++) { grid[i] = []; for (let j = 0; j < cols; j++) { grid[i][j] = new Cell(i, j); } } return grid; } function generateMaze(grid) { const rows = grid.length; const cols = grid[0].length; const stack = []; const startCell = grid[0][0]; startCell.visited = true; stack.push(startCell); while (stack.length > 0) { const current = stack[stack.length - 1]; const directions = [ { dx: -1, dy: 0, currentWall: 'top', neighborWall: 'bottom' }, { dx: 0, dy: 1, currentWall: 'right', neighborWall: 'left' }, { dx: 1, dy: 0, currentWall: 'bottom', neighborWall: 'top' }, { dx: 0, dy: -1, currentWall: 'left', neighborWall: 'right' }, ]; const unvisitedNeighbors = directions .map(dir => { const newRow = current.row + dir.dx; const newCol = current.col + dir.dy; if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) { const neighbor = grid[newRow][newCol]; if (!neighbor.visited) { return { ...dir, neighbor: neighbor }; } } return null; }) .filter(item => item); if (unvisitedNeighbors.length === 0) { stack.pop(); } else { const chosen = unvisitedNeighbors[Math.floor(Math.random() * unvisitedNeighbors.length)]; current.walls[chosen.currentWall] = false; chosen.neighbor.walls[chosen.neighborWall] = false; chosen.neighbor.visited = true; stack.push(chosen.neighbor); } } } function drawMaze(grid, ctx, size) { ctx.clearRect(0, 0, 800, 800); const rows = grid.length; const cols = grid[0].length; ctx.beginPath(); for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) { const cell = grid[i][j]; const x = cell.row * size; const y = cell.col * size; if (cell.walls.top) { ctx.moveTo(x, y); ctx.lineTo(x + size, y); } if (cell.walls.right) { ctx.moveTo(x + size, y); ctx.lineTo(x + size, y + size); } if (cell.walls.bottom) { ctx.moveTo(x, y + size); ctx.lineTo(x + size, y + size); } if (cell.walls.left) { ctx.moveTo(x, y); ctx.lineTo(x, y + size); } } } ctx.stroke(); ctx.fillStyle = 'green'; ctx.beginPath(); ctx.arc(size / 2, size / 2, size / 6, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = 'red'; ctx.beginPath(); const endX = (rows - 1) * size + size / 2; const endY = (cols - 1) * size + size / 2; ctx.arc(endX, endY, size / 6, 0, 2 * Math.PI); ctx.fill(); } function heuristic(a, b) { return Math.abs(a.row - b.row) + Math.abs(a.col - b.col); } function visualizePath(current, cameFrom, ctx, size) { ctx.strokeStyle = 'lime'; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.beginPath(); const endX = current.row * size + size / 2; const endY = current.col * size + size / 2; ctx.moveTo(endX, endY); while (cameFrom.has(current)) { current = cameFrom.get(current); const curX = current.row * size + size / 2; const curY = current.col * size + size / 2; ctx.lineTo(curX, curY); } ctx.stroke(); } async function animateAStar(grid, ctx, size, startRow, startCol, endRow, endCol) { const start = grid[startRow][startCol]; const end = grid[endRow][endCol]; const openList = []; const closedList = new Set(); start.g = 0; start.f = heuristic(start, end); openList.push(start); const cameFrom = new Map(); const directions = [ { dx: -1, dy: 0, wallCheck: cell => cell.walls.top }, { dx: 0, dy: 1, wallCheck: cell => cell.walls.right }, { dx: 1, dy: 0, wallCheck: cell => cell.walls.bottom }, { dx: 0, dy: -1, wallCheck: cell => cell.walls.left }, ]; while (openList.length > 0) { openList.sort((a, b) => a.f - b.f); const current = openList.shift(); closedList.add(current); drawAStarStep(grid, current, end, closedList, openList, ctx, size); if (current === end) { visualizePath(current, cameFrom, ctx, size); return; } for (const dir of directions) { const newRow = current.row + dir.dx; const newCol = current.col + dir.dy; if (newRow >= 0 && newRow < grid.length && newCol >= 0 && newCol < grid[0].length) { const neighbor = grid[newRow][newCol]; if (dir.wallCheck(current)) continue; if (closedList.has(neighbor)) continue; const tentativeG = current.g + 1; if (!neighbor.g || tentativeG < neighbor.g) { cameFrom.set(neighbor, current); neighbor.g = tentativeG; neighbor.f = tentativeG + heuristic(neighbor, end); if (!openList.includes(neighbor)) { openList.push(neighbor); } } } } await new Promise(resolve => setTimeout(resolve, 50)); } } function drawAStarStep(grid, current, end, closedList, openList, ctx, size) { drawMaze(grid, ctx, size); const rows = grid.length; const cols = grid[0].length; for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) { const cell = grid[i][j]; const x = cell.row * size; const y = cell.col * size; let color = null; if (cell === end) continue; if (closedList.has(cell)) { color = 'orange'; } else if (openList.includes(cell)) { color = 'yellow'; } if (color) { ctx.fillStyle = color; ctx.beginPath(); const padding = 4; ctx.roundRect(x + padding, y + padding, size - 2 * padding, size - 2 * padding, 4); ctx.fill(); } } } ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; const x = current.row * size; const y = current.col * size; const padding = 4; ctx.beginPath(); ctx.roundRect(x + padding, y + padding, size - 2 * padding, size - 2 * padding, 4); ctx.fill(); } CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius) { this.beginPath(); this.moveTo(x + radius, y); this.lineTo(x + width - radius, y); this.arcTo(x + width, y, x + width, y + radius, radius); this.lineTo(x + width, y + height - radius); this.arcTo(x + width, y + height, x + width - radius, y + height, radius); this.lineTo(x + radius, y + height); this.arcTo(x, y + height, x, y + height - radius, radius); this.lineTo(x, y + radius); this.arcTo(x, y, x + radius, y, radius); this.closePath(); }; async function main() { const CANVAS_SIZE = 800; const CELL_SIZE = 40; const ROWS = CANVAS_SIZE / CELL_SIZE; const COLS = ROWS; const canvas = document.getElementById('mazeCanvas'); canvas.width = CANVAS_SIZE; canvas.height = CANVAS_SIZE; const ctx = canvas.getContext('2d'); ctx.strokeStyle = '#000'; ctx.lineWidth = 2; const grid = generateGrid(ROWS, COLS); generateMaze(grid); drawMaze(grid, ctx, CELL_SIZE); await animateAStar(grid, ctx, CELL_SIZE, 0, 0, ROWS - 1, COLS - 1); } main(); </script> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Maze Generator & Pathfinder</title> <style> body { margin: 0; padding: 20px; background: #1a1a2e; color: white; font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; } canvas { border: 2px solid #4cc9f0; margin-top: 20px; box-shadow: 0 0 20px rgba(76, 201, 240, 0.5); } .controls { margin-bottom: 20px; display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; } button { padding: 10px 20px; background: #4cc9f0; border: none; border-radius: 5px; color: #1a1a2e; font-weight: bold; cursor: pointer; transition: all 0.3s; } button:hover { background: #f72585; transform: scale(1.05); box-shadow: 0 0 10px rgba(247, 37, 133, 0.7); } select { padding: 10px; border-radius: 5px; background: #16213e; color: white; border: 1px solid #4cc9f0; } </style> </head> <body> <h1>Maze Generator & Pathfinder</h1> <div class="controls"> <button id="generateBtn">Generate Maze</button> <button id="solveBtn">Solve with A*</button> <select id="sizeSelect"> <option value="20">20x20</option> <option value="30" selected>30x30</option> <option value="40">40x40</option> <option value="50">50x50</option> </select> </div> <canvas id="mazeCanvas"></canvas> <script> const canvas = document.getElementById('mazeCanvas'); const ctx = canvas.getContext('2d'); const generateBtn = document.getElementById('generateBtn'); const solveBtn = document.getElementById('solveBtn'); const sizeSelect = document.getElementById('sizeSelect'); let size = 30; let cellSize = 15; let cols, rows; let grid = []; let path = []; let openSet = [], closedSet = []; let start, end; let animationId; let solving = false; // Initialize the maze function init() { size = parseInt(sizeSelect.value); cols = size; rows = size; canvas.width = cols * cellSize; canvas.height = rows * cellSize; // Create grid grid = []; for (let i = 0; i < cols; i++) { grid[i] = []; for (let j = 0; j < rows; j++) { grid[i][j] = new Cell(i, j); } } // Setup start and end points start = grid[1][1]; end = grid[cols - 2][rows - 2]; start.wall = false; end.wall = false; generateMaze(); draw(); } // Cell constructor function Cell(i, j) { this.i = i; this.j = j; this.f = 0; this.g = 0; this.h = 0; this.wall = Math.random() < 0.25; this.neighbors = []; this.previous = undefined; this.visited = false; this.addNeighbors = function(grid) { if (i > 0) this.neighbors.push(grid[i-1][j]); if (i < cols - 1) this.neighbors.push(grid[i+1][j]); if (j > 0) this.neighbors.push(grid[i][j-1]); if (j < rows - 1) this.neighbors.push(grid[i][j+1]); }; } // Generates maze using depth-first search function generateMaze() { // Reset all walls except borders for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { grid[i][j].wall = (i === 0 || j === 0 || i === cols - 1 || j === rows - 1); grid[i][j].visited = false; } } let stack = []; let current = grid[1][1]; current.visited = true; function step() { let next = current.checkNeighbors(); if (next) { next.visited = true; stack.push(current); // Remove wall between current and next if (next.i > current.i) { grid[current.i+1][current.j].wall = false; } else if (next.i < current.i) { grid[current.i-1][current.j].wall = false; } else if (next.j > current.j) { grid[current.i][current.j+1].wall = false; } else if (next.j < current.j) { grid[current.i][current.j-1].wall = false; } current = next; step(); } else if (stack.length > 0) { current = stack.pop(); step(); } } Cell.prototype.checkNeighbors = function() { let neighbors = []; const {i, j} = this; if (i > 1 && !grid[i-2][j].visited) neighbors.push(grid[i-2][j]); if (i < cols-2 && !grid[i+2][j].visited) neighbors.push(grid[i+2][j]); if (j > 1 && !grid[i][j-2].visited) neighbors.push(grid[i][j-2]); if (j < rows-2 && !grid[i][j+2].visited) neighbors.push(grid[i][j+2]); if (neighbors.length > 0) { return neighbors[Math.floor(Math.random() * neighbors.length)]; } else { return undefined; } }; step(); // Ensure start and end are not walls start.wall = false; end.wall = false; // Clear any walls blocking the start and end grid[1][0].wall = false; grid[cols-2][rows-1].wall = false; } // Draw the maze function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grid for (let i = 0; i < cols; i++) { for (let j = ʽ0; j < rows; j++) { const cell = grid[i][j]; ctx.fillStyle = cell.wall ? '#16213e' : '#1a1a2e'; ctx.fillRect(i * cellSize, j * cellSize, cellSize, cellSize); if (cell.wall) { ctx.strokeStyle = '#4cc9f0'; ctx.lineWidth = 1; ctx.strokeRect(i * cellSize, j * cellSize, cellSize, cellSize); } } } // Draw pathfinding elements if solving if (solving) { // Draw explored nodes for (let cell of openSet) { ctx.fillStyle = 'rgba(0, 185, 255, 0.5)'; ctx.fillRect(cell.i * cellSize, cell.j * cellSize, cellSize, cellSize); } for (let cell of closedSet) { ctx.fillStyle = 'rgba(247, 37, 133, 0.3)'; ctx.fillRect(cell.i * cellSize, cell.j * cellSize, cellSize, cellSize); } // Draw path ctx.beginPath(); ctx.moveTo(start.i * cellSize + cellSize/2, start.j * cellSize + cellSize/2); for (let cell of path) { ctx.lineTo(cell.i * cellSize + cellSize/2, cell.j * cellSize + cellSize/2); } ctx.strokeStyle = '#f72585'; ctx.lineWidth = 3; ctx.stroke(); // Draw start and end points ctx.fillStyle = '#4cc9f0'; ctx.beginPath(); ctx.arc(start.i * cellSize + cellSize/2, start.j * cellSize + cellSize/2, cellSize/3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#f72585'; ctx.beginPath(); ctx.arc(end.i * cellSize + cellSize/2, end.j * cellSize + cellSize/2, cellSize/3, 0, Math.PI * 2); ctx.fill(); } } // A* algorithm function solve() { openSet = [start]; closedSet = []; path = []; solving = true; for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { grid[i][j].addNeighbors(grid); } } function step() { if (openSet.length > 0) { // Find the node with lowest f score let winner = 0; for (let i = 1; i < openSet.length; i++) { if (openSet[i].f < openSet[winner].f) { winner = i; } } let current = openSet[winner]; // Did we reach the end? if (current === end) { // Find the path path = []; let temp = current; path.push(temp); while (temp.previous) { path.push(temp.previous); temp = temp.previous; } drawing = false; draw(); solving = false; return; } // Move current from open to closed set openSet.splice(winner, 1); closedSet.push(current); // Check all neighbors let neighbors = current.neighbors.filter(n => !n.wall && !closedSet.includes(n)); for (let neighbor of neighbors) { let tempG = current.g + 1; if (openSet.includes(neighbor)) { if (tempG < neighbor.g) { neighbor.g = tempG; } } else { neighbor.g = tempG; openSet.push(neighbor); } neighbor.h = heuristic(neighbor, end); neighbor.f = neighbor.g + neighbor.h; neighbor.previous = current; } draw(); animationId = requestAnimationFrame(step); } else { // No solution solving = false; console.log('No solution!'); return; } } step(); } // Heuristic function (Manhattan distance) function heuristic(a, b) { return Math.abs(a.i - b.i) + Math.abs(a.j - b.j); } // Event listeners generateBtn.addEventListener('click', () => { if (animationId) cancelAnimationFrame(animationId); init(); }); solveBtn.addEventListener('click', () => { if (!solving && animationId) cancelAnimationFrame(animationId); if (!solving) solve(); }); sizeSelect.addEventListener('change', init); // Initialize init(); </script> </body> </html> ```
Here’s a self‐contained HTML file that generates a random maze with recursive backtracking, draws it on a canvas, then visualizes the A* algorithm step by step solving it. Just save it as an `.html` and open in your browser. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Maze Generator & A* Visualizer</title> <style> body { margin: 0; background: #222; display: flex; align-items: center; justify-content: center; height: 100vh; color: #fff; flex-direction: column; font-family: sans-serif; } canvas { border: 2px solid #555; background: #fff; } button { margin-top: 10px; padding: 8px 16px; font-size: 14px; cursor: pointer; } </style> </head> <body> <canvas id="canvas" width="600" height="600"></canvas> <button id="regen">Regenerate & Solve</button> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const regenBtn = document.getElementById('regen'); const COLS = 40, ROWS = 40; const cw = canvas.width / COLS; const ch = canvas.height / ROWS; let grid = []; let openSet = [], closedSet = [], path = []; let start, end; let solving = false; class Cell { constructor(r, c) { this.r = r; this.c = c; this.walls = [true, true, true, true]; // top, right, bottom, left this.visited = false; // for A* this.g = 0; this.h = 0; this.f = 0; this.prev = null; } show(fillColor) { const x = this.c * cw, y = this.r * ch; if (fillColor) { ctx.fillStyle = fillColor; ctx.fillRect(x, y, cw, ch); } ctx.strokeStyle = '#000'; ctx.lineWidth = 2; // draw walls if (this.walls[0]) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + cw, y); ctx.stroke(); } if (this.walls[1]) { ctx.beginPath(); ctx.moveTo(x + cw, y); ctx.lineTo(x + cw, y + ch); ctx.stroke(); } if (this.walls[2]) { ctx.beginPath(); ctx.moveTo(x + cw, y + ch); ctx.lineTo(x, y + ch); ctx.stroke(); } if (this.walls[3]) { ctx.beginPath(); ctx.moveTo(x, y + ch); ctx.lineTo(x, y); ctx.stroke(); } } neighborsForMaze() { const list = []; const dirs = [ [this.r-1, this.c, 0, 2], [this.r, this.c+1, 1, 3], [this.r+1, this.c, 2, 0], [this.r, this.c-1, 3, 1] ]; for (let [nr, nc, w, ow] of dirs) { if (nr>=0 && nr<ROWS && nc>=0 && nc<COLS) { const neighbor = grid[nr*COLS + nc]; if (!neighbor.visited) list.push([neighbor, w, ow]); } } return list; } neighborsForPath() { const list = []; const dirs = [ [this.r-1, this.c, 0], [this.r, this.c+1, 1], [this.r+1, this.c, 2], [this.r, this.c-1, 3] ]; for (let [nr, nc, w] of dirs) { if (nr>=0 && nr<ROWS && nc>=0 && nc<COLS) { if (!this.walls[w]) { list.push(grid[nr*COLS + nc]); } } } return list; } } function removeWalls(a, b, w, ow) { a.walls[w] = false; b.walls[ow] = false; } function generateMaze() { grid = []; for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { grid.push(new Cell(r, c)); } } const stack = []; let current = grid[0]; current.visited = true; do { const neigh = current.neighborsForMaze(); if (neigh.length > 0) { const [next, w, ow] = neigh[Math.floor(Math.random() * neigh.length)]; removeWalls(current, next, w, ow); stack.push(current); current = next; current.visited = true; } else { current = stack.pop(); } } while (stack.length > 0); } function heuristic(a, b) { return Math.abs(a.r - b.r) + Math.abs(a.c - b.c); } function setupAStar() { openSet = []; closedSet = []; path = []; for (let cell of grid) { cell.g = 0; cell.h = 0; cell.f = 0; cell.prev = null; } start = grid[0]; end = grid[grid.length - 1]; openSet.push(start); solving = true; requestAnimationFrame(step); } function step() { if (!solving) return; if (openSet.length > 0) { // find lowest f let winner = 0; for (let i = 1; i < openSet.length; i++) { if (openSet[i].f < openSet[winner].f) winner = i; } const current = openSet[winner]; if (current === end) { // done let temp = current; path = []; while (temp) { path.push(temp); temp = temp.prev; } solving = false; draw(); return; } openSet.splice(winner, 1); closedSet.push(current); for (let neighbor of current.neighborsForPath()) { if (!closedSet.includes(neighbor)) { const tentativeG = current.g + 1; let newPath = false; if (openSet.includes(neighbor)) { if (tentativeG < neighbor.g) { neighbor.g = tentativeG; newPath = true; } } else { neighbor.g = tentativeG; newPath = true; openSet.push(neighbor); } if (newPath) { neighbor.h = heuristic(neighbor, end); neighbor.f = neighbor.g + neighbor.h; neighbor.prev = current; } } } } else { // no solution solving = false; draw(); return; } draw(); requestAnimationFrame(step); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // draw maze for (let cell of grid) { cell.show(); } // closedSet for (let cell of closedSet) cell.show('rgba(255, 0, 0, 0.3)'); // openSet for (let cell of openSet) cell.show('rgba(0, 255, 0, 0.3)'); // path if (path.length > 0) { for (let cell of path) cell.show('rgba(255, 255, 0, 0.5)'); } // start & end start.show('rgba(0,0,255,0.6)'); end.show('rgba(0,0,255,0.6)'); } function regenerate() { generateMaze(); draw(); setupAStar(); } regenBtn.addEventListener('click', regenerate); // initial run regenerate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Maze Generator and Pathfinding Visualizer</title> <style> body { margin: 0; background-color: #f0f0f0; } #canvas { border: 1px solid black; } </style> </head> <body> <canvas id="canvas" width="800" height="600"></canvas> <script> // Get the canvas element const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Define the maze dimensions const mazeWidth = 40; const mazeHeight = 30; const cellSize = Math.min(canvas.width / mazeWidth, canvas.height / mazeHeight); // Define the colors const wallColor = 'black'; const pathColor = 'white'; const startColor = 'green'; const endColor = 'red'; const visitedColor = 'blue'; const pathFoundColor = 'yellow'; // Generate a random maze function generateMaze(width, height) { const maze = Array(height).fill().map(() => Array(width).fill(1)); const stack = [[0, 0]]; maze[0][0] = 0; while (stack.length > 0) { const [x, y] = stack[stack.length - 1]; const directions = [[0, -1], [0, 1], [-1, 0], [1, 0]]; const validDirections = directions.filter(([dx, dy]) => { const nx = x + 2 * dx; const ny = y + 2 * dy; return nx >= 0 && nx < width && ny >= 0 && ny < height && maze[ny][nx] === 1; }); if (validDirections.length > 0) { const [dx, dy] = validDirections[Math.floor(Math.random() * validDirections.length)]; const nx = x + 2 * dx; const ny = y + 2 * dy; maze[y + dy][x + dx] = 0; maze[ny][nx] = 0; stack.push([nx, ny]); } else { stack.pop(); } } return maze; } // A* algorithm function aStar(maze, start, end) { const openList = [start]; const closedList = []; const cameFrom = {}; const gScore = { [start]: 0 }; const fScore = { [start]: heuristic(start, end) }; while (openList.length > 0) { const current = openList.reduce((min, node) => fScore[node] < fScore[min] ? node : min, openList[0]); if (current === end) { return reconstructPath(cameFrom, current); } openList.splice(openList.indexOf(current), 1); closedList.push(current); const neighbors = getNeighbors(maze, current); for (const neighbor of neighbors) { if (closedList.includes(neighbor)) continue; const tentativeGScore = gScore[current] + 1; if (!openList.includes(neighbor)) { openList.push(neighbor); } else if (tentativeGScore >= gScore[neighbor]) { continue; } cameFrom[neighbor] = current; gScore[neighbor] = tentativeGScore; fScore[neighbor] = tentativeGScore + heuristic(neighbor, end); } // Visualize the step visualizeStep(maze, current, openList, closedList, cameFrom, end); await new Promise(resolve => setTimeout(resolve, 50)); } return null; } // Heuristic function (Manhattan distance) function heuristic(a, b) { return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]); } // Get the neighbors of a cell function getNeighbors(maze, [x, y]) { const neighbors = []; const directions = [[0, -1], [0, 1], [-1, 0], [1, 0]]; for (const [dx, dy] of directions) { const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < maze[0].length && ny >= 0 && ny < maze.length && maze[ny][nx] === 0) { neighbors.push([nx, ny]); } } return neighbors; } // Reconstruct the path function reconstructPath(cameFrom, current) { const path = [current]; while (cameFrom[current]) { current = cameFrom[current]; path.unshift(current); } return path; } // Visualize the maze and the pathfinding process function visualizeMaze(maze, start, end) { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < maze.length; y++) { for (let x = 0; x < maze[y].length; x++) { if (maze[y][x] === 1) { ctx.fillStyle = wallColor; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } } } ctx.fillStyle = startColor; ctx.fillRect(start[0] * cellSize, start[1] * cellSize, cellSize, cellSize); ctx.fillStyle = endColor; ctx.fillRect(end[0] * cellSize, end[1] * cellSize, cellSize, cellSize); } // Visualize a step of the A* algorithm function visualizeStep(maze, current, openList, closedList, cameFrom, end) { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < maze.length; y++) { for (let x = 0; x < maze[y].length; x++) { if (maze[y][x] === 1) { ctx.fillStyle = wallColor; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } } } for (const node of openList) { ctx.fillStyle = 'rgba(0, 0, 255, 0.2)'; ctx.fillRect(node[0] * cellSize, node[1] * cellSize, cellSize, cellSize); } for (const node of closedList) { ctx.fillStyle = 'rgba(0, 255, 0, 0.2)'; ctx.fillRect(node[0] * cellSize, node[1] * cellSize, cellSize, cellSize); } ctx.fillStyle = visitedColor; ctx.fillRect(current[0] * cellSize, current[1] * cellSize, cellSize, cellSize); if (cameFrom[current]) { const path = reconstructPath(cameFrom, current); ctx.strokeStyle = pathFoundColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(path[0][0] * cellSize + cellSize / 2, path[0][1] * cellSize + cellSize / 2); for (const node of path.slice(1)) { ctx.lineTo(node[0] * cellSize + cellSize / 2, node[1] * cellSize + cellSize / 2); } ctx.stroke(); } ctx.fillStyle = startColor; ctx.fillRect([0, 0][0] * cellSize, [0, 0][1] * cellSize, cellSize, cellSize); ctx.fillStyle = endColor; ctx.fillRect(end[0] * cellSize, end[1] * cellSize, cellSize, cellSize); } // Main function async function main() { const maze = generateMaze(mazeWidth, mazeHeight); const start = [0, 0]; const end = [mazeWidth - 1, mazeHeight - 1]; visualizeMaze(maze, start, end); await new Promise(resolve => setTimeout(resolve, 1000)); const path = await aStar(maze, start, end); if (path) { console.log('Path found:', path); } else { console.log('No path found'); } } main(); </script> </body> </html> ``` This code generates a random maze and visualizes the A* algorithm solving it step by step. The `generateMaze` function generates a random maze using a depth-first search algorithm. The `aStar` function implements the A* algorithm and visualizes each step using the `visualizeStep` function. The `main` function generates a maze, visualizes it, and then runs the A* algorithm to find a path from the start to the end. The path is visualized in yellow.
# Maze Generator & Pathfinding Visualizer I'll create a canvas-based application featuring a maze generator with A* pathfinding visualization using beautiful colors and animations. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Maze Generator & Pathfinder</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: radial-gradient(circle at center, #2c3e50, #1a1a2e); display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; color: #ecf0f1; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow: hidden; } .header { text-align: center; padding: 20px 0; } h1 { font-size: 2.8rem; margin-bottom: 8px; background: linear-gradient(to right, #3498db, #2ecc71); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .subtitle { color: #bdc3c7; font-size: 1.2rem; margin-bottom: 20px; } .container { display: flex; flex-direction: column; align-items: center; background: rgba(30, 30, 46, 0.8); border-radius: 20px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); padding: 25px; width: 90%; max-width: 800px; } .canvas-container { position: relative; width: 100%; height: 500px; margin: 10px 0; border-radius: 15px; overflow: hidden; box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.8); } canvas { background: #1e1e2e; } .controls { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 15px; width: 100%; } button { padding: 12px 25px; font-size: 1rem; border: none; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; font-weight: 600; background: linear-gradient(to bottom, #3498db, #2980b9); color: white; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } button:hover { transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); } button:active { transform: translateY(1px); } #solveBtn { background: linear-gradient(to bottom, #2ecc71, #27ae60); } #resetBtn { background: linear-gradient(to bottom, #e74c3c, #c0392b); } .legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; margin-top: 15px; padding: 12px; } .legend-item { display: flex; align-items: center; gap: 6px; } .color-box { width: 20px; height: 20px; border-radius: 4px; } .instructions { text-align: center; padding: 10px; max-width: 600px; margin-top: 10px; opacity: 0.9; } @keyframes pulse { 0%, 100% {opacity: 0.7} 50% {opacity: 1} } #progressText { margin: 10px 0; font-size: 1.1rem; font-weight: 500; height: 30px; animation: pulse 2s infinite; } </style> </head> <body> <div class="header"> <h1>Maze Generator & Pathfinder</h1> <div class="subtitle">Watch the A* Pathfinding Algorithm Solve Mazes in Real Time</div> </div> <div class="container"> <div id="progressText">Generating maze...</div> <div class="canvas-container"> <canvas id="mazeCanvas"></canvas> </div> <div class="controls"> <button id="generateBtn">Generate New Maze</button> <button id="solveBtn">Solve with A*</button> <button id="resetBtn">Reset Solution</button> </div> <div class="legend"> <div class="legend-item"> <div class="color-box" style="background: #3498db;"></div> <span>Path (Open Set)</span> </div> <div class="legend-item"> <div class="color-box" style="background: #e74c3c;"></div> <span>Path (Closed Set)</span> </div> <div class="legend-item"> <div class="color-box" style="background: #f39c12;"></div> <span>Final Path</span> </div> <div class="legend-item"> <div class="color-box" style="background: #9b59b6;"></div> <span>Start</span> </div> <div class="legend-item"> <div class="color-box" style="background: #2ecc71;"></div> <span>End</span> </div> </div> </div> <div class="instructions"> <p>New mazes are automatically generated on page load.<br> Click "Solve with A*" to visualize the pathfinding process.</p> </div> <script> document.addEventListener('DOMContentLoaded', () => { const canvas = document.getElementById('mazeCanvas'); const ctx = canvas.getContext('2ed'); const generateBtn = document.getElementById('generateBtn'); const solveBtn = document.getElementById('solveBtn'); const resetBtn = document.getElementById('resetBtn'); const progressText = document.getElementById('progressText'); // Set canvas dimensions const cellSize = 20; const cellPadding = 1; const cols = Math.floor((window.innerWidth * 0.8) / cellSize); const rows = Math.floor(500 / cellSize); const width = cols * cellSize; const height = rows * cellSize; canvas.width = width; canvas.height = height; let grid = []; let start = {x: 1, y: 1}; let end = {x: cols - 2, y: rows - 2}; let solving = false; let animationId; // Cell class for grid positions class Cell { constructor(x, y) { this.x = x; this.y = y; this.wall = false; this.visited = false; this.path = false; this.inOpenSet = false; this.inClosedSet = false; } } // Initialize grid function initGrid() { grid = []; for (let y = 0; y < rows; y++) { const row = []; for (let x = 0; x < cols; x++) { const cell = new Cell(x, y); row.push(cell); } grid.push(row); } return grid; } // Generate maze using Depth-First Search algorithm function generateMaze() { solving = false; cancelAnimationFrame(animationId); progressText.textContent = "Generating maze..."; initGrid(); // Set borders as walls for (let x = 0; x < cols; x++) { grid[0][x].wall = true; grid[rows-1][x].wall = true; } for (let y = 0; y < rows; y++) { grid[y][0].wall = true; grid[y][cols-1].wall = true; } // DFS maze generation const stack = []; const startX = Math.floor(Math.random() * (cols - 2)) + 1; const startY = Math.floor(Math.random() * (rows - 2)) + 1; stack.push(grid[startY][startX]); grid[startY][startX].visited = true; grid[startY][startX].wall = false; // DFS algorithm const directions = [ [0, -2], [1, 0], [0, 2], [-1, 0] // Up, Right, Down, Left ]; function step() { if (stack.length === 0) { // Maze generation complete // Ensure start and end are not walls grid[start.y][start.x].wall = false; grid[end.y][end.x].wall = false; drawMaze(); progressText.textContent = "Maze generated! Click to solve."; return; } const current = stack[stack.length - 1]; const neighbors = []; // Find unvisited neighbors (2 cells away) for (const [dx, dy] of directions) { const nx = current.x + dx; const ny = current.y + dy; if (nx > 0 && nx < cols-1 && ny > 0 && ny < rows-1 && !grid[ny][nx].visited) { neighbors.push({dx, dy, cell: grid[ny][nx]}); } } if (neighbors.length > 0) { const randDir = neighbors[Math.floor(Math.random() * neighbors.length)]; const next = randDir.cell; // Remove wall between current and next const wallX = current.x + randDir.dx / 2; const wallY = current.y + randDir.dy / 2; grid[wallY][wallX].wall = false; grid[wallY][wallX].visited = true; next.visited = true; next.wall = false; stack.push(next); drawMaze(); // Continue with next step animationId = requestAnimationFrame(step); } else { stack.pop(); step(); } } step(); } // Draw the maze function drawMaze() { ctx.clearRect(0, 0, width, height); for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const cell = grid[y][x]; if (cell.inOpenSet) { // Open set cells (frontier) ctx.fillStyle = '#3498db'; ctx.fillRect( x * cellSize, y * cellSize, cellSize, cellSize ); } else if (cell.inClosedSet) { // Closed set cells (visited) ctx.fillStyle = '#e74c3c'; ctx.fillRect( x * cellSize, y * cellSize, cellSize, cellSize ); } else if (cell.path) { // Final path ctx.fillStyle = '#f39c12'; ctx.fillRect( x * cellSize, y * cellSize, cellSize, cellSize ); } else if (cell.wall) { // Walls ctx.fillStyle = '#34495e'; ctx.fillRect( x * cellSize, y * cellSize, cellSize, cellSize ); } else { // Path ctx.fillStyle = '#2c3e50'; ctx.fillRect( x * cellSize, y * cellSize, cellSize, cellSize ); } } } // Draw start and end points ctx.fillStyle = '#9b59b6'; ctx.beginPath(); ctx.arc( start.x * cellSize + cellSize / 2, start.y * cellSize + cellSize / 2, cellSize / 2 - 2, 0, Math.PI * 2 ); ctx.fill(); ctx.fillStyle = '#2ecc71'; ctx.beginPath(); ctx.arc( end.x * cellSize + cellSize / 2, end.y * cellSize + cellSize / 2, cellSize / 2 - 2, 0, Math.PI * 2 ); ctx.fill(); // Draw grid lines ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'; ctx.lineWidth = 1; for (let x = 0; x <= width; x += cellSize) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } for (let y = 0; y <= height; y += cellSize) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } } // Pathfinding using A* algorithm function solveMaze() { solving = true; cancelAnimationFrame(animationId); progressText.textContent = "Solving with A* algorithm..."; // Reset A* values for each cell for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const cell = grid[y][x]; cell.f = 0; // Total cost cell.g = 0; // Cost from start cell.h = 0; // Heuristic to end cell.parent = null; cell.inOpenSet = false; cell.inClosedSet = false; cell.path = false; } } // Initialize the open and closed sets const openSet = []; const closedSet = []; // Add start to open set grid[start.y][start.x].g = 0; grid[start.y][start.x].h = heuristic(start, end); grid[start.y][start.x].f = grid[start.y][start.x].g + grid[start.y][start.x].h; grid[start.y][start.x].inOpenSet = true; openSet.push(grid[start.y][start.x]); let pathFound = false; function step() { if (openSet.length === 0) { progressText.textContent = "No path found!"; solving = false; return; } // Find node with lowest f score let lowestIdx = 0; for (let i = 0; i < openSet.length; i++) { if (openSet[i].f < openSet[lowestIdx].f) { lowestIdx = i; } } const current = openSet[lowestIdx]; // Check if we've reached the end if (current.x === end.x && current.y === end.y) { pathFound = true; // Reconstruct and draw path let temp = current; temp.path = true; while (temp.parent) { temp = temp.parent; temp.path = true; } drawMaze(); progressText.textContent = "Path found! Total steps: " + current.g; solving = false; return; } // Move current from open to closed set openSet.splice(lowestIdx, 1); current.inOpenSet = false; current.inClosedSet = true; closedSet.push(current); // Explore neighbors const neighbors = [ {x: 0, y: -1}, // Up {x: 1, y: 0}, // Right {x: 0, y: 1}, // Down {x: -1, y: 0} // Left ]; for (const n of neighbors) { const nx = current.x + n.x; const ny = current.y + n.y; // Skip if out of bounds if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue; const neighbor = grid[ny][nx]; // Skip if wall or in closed set if (neighbor.wall || neighbor.inClosedSet) continue; // Calculate tentative g score const tentativeG = current.g + 1; // If this is a new node or we found a better path if (!neighbor.inOpenSet || tentativeG < neighbor.g) { neighbor.parent = current; neighbor.g = tentativeG; neighbor.h = heuristic(neighbor, end); neighbor.f = neighbor.g + neighbor.h; if (!neighbor.inOpenSet) { neighbor.inOpenSet = true; openSet.push(neighbor); } } } drawMaze(); // Continue with next step if (!pathFound) { animationId = requestAnimationFrame(step); } } step(); } // Reset A* solution function resetSolution() { solving = false; cancelAnimationFrame(animationId); for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const cell = grid[y][x]; cell.inOpenSet = false; cell.inClosedSet = false; cell.path = false; } } drawMaze(); progressText.textContent = "Solution was reset"; } // Heuristic function (Manhattan distance) function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } // Event listeners generateBtn.addEventListener('click', generateMaze); solveBtn.addEventListener('click', solveMaze); resetBtn.addEventListener('click', resetSolution); // Initial maze generation generateMaze(); }); </script> </body> </html> ``` This implementation features: 1. **Visually Appealing Design** - Gradient background and modern color scheme - Animated elements and transitions - Smooth animations for solving - Clean, modern interface with shadows and rounded corners 2. **Maze Generation** - Implements Depth-First Search (DFS) algorithm - Creates complex and diverse mazes - Visualizes the generation process in real-time 3. **Pathfinding Visualization** - A* algorithm implementation with Manhattan distance heuristic - Visual distinction between: - Open set (frontier nodes - blue) - Closed set (visited nodes - red) - Final path (yellow) - Start (purple) and end (green) points 4. **User Controls** - Generate new maze button - Solve with A* button - Reset solution button - Clear legend showing pathfinding visualization 5. **Responsiveness** - Adapts to different screen sizes - Clear visual hierarchy When you run the application: - A maze is automatically generated on page load - Click "Solve with A*" to visualize the pathfinding algorithm - Generate new mazes to see different configurations The visualization clearly shows both the maze generation process and the step-by-step solving logic of the A* algorithm, making it both educational and visually engaging.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Maze Generator & A* Pathfinder</title> <style> body { margin: 0; padding: 20px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; flex-direction: column; align-items: center; min-height: 100vh; } h1 { margin-bottom: 20px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); } #controls { margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; } button { padding: 10px 20px; border: none; border-radius: 5px; background: #4CAF50; color: white; cursor: pointer; font-size: 16px; transition: all 0.3s ease; box-shadow: 0 4px 8px rgba(0,0,0,0.2); } button:hover { background: #45a049; transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.3); } button:disabled { background: #cccccc; cursor: not-allowed; transform: none; } #solveBtn { background: #2196F3; } #solveBtn:hover { background: #1976D2; } #clearBtn { background: #f44336; } #clearBtn:hover { background: #d32f2f; } canvas { border: 3px solid white; border-radius: 10px; box-shadow: 0 8px 16px rgba(0,0,0,0.3); background: white; } #info { margin-top: 15px; text-align: center; font-size: 14px; opacity: 0.8; } </style> </head> <body> <h1>🌟 Maze Generator & A* Pathfinder 🌟</h1> <div id="controls"> <button id="generateBtn">Generate New Maze</button> <button id="solveBtn">Solve with A*</button> <button id="clearBtn">Clear Path</button> </div> <canvas id="mazeCanvas" width="800" height="600"></canvas> <div id="info"> 🟢 Start | 🔴 End | 🟡 Exploring | 🟠 Visited | 🟣 Path </div> <script> class MazeGenerator { constructor(width, height, cellSize) { this.width = width; this.height = height; this.cellSize = cellSize; this.cols = Math.floor(width / cellSize); this.rows = Math.floor(height / cellSize); this.grid = []; this.canvas = document.getElementById('mazeCanvas'); this.ctx = this.canvas.getContext('2d'); this.animationId = null; this.isGenerating = false; this.isSolving = false; this.start = { x: 1, y: 1 }; this.end = { x: this.cols - 2, y: this.rows - 2 }; this.initGrid(); this.setupEventListeners(); this.generateMaze(); } initGrid() { this.grid = []; for (let y = 0; y < this.rows; y++) { this.grid[y] = []; for (let x = 0; x < this.cols; x++) { this.grid[y][x] = { x: x, y: y, wall: true, visited: false, exploring: false, inPath: false }; } } } setupEventListeners() { document.getElementById('generateBtn').addEventListener('click', () => { if (!this.isGenerating && !this.isSolving) { this.generateMaze(); } }); document.getElementById('solveBtn').addEventListener('click', () => { if (!this.isGenerating && !this.isSolving) { this.solveWithAStar(); } }); document.getElementById('clearBtn').addEventListener('click', () => { if (!this.isGenerating && !this.isSolving) { this.clearPath(); } }); } generateMaze() { this.isGenerating = true; this.updateButtons(); this.initGrid(); // Start with all walls const stack = []; const startCell = this.grid[1][1]; startCell.wall = false; startCell.visited = true; stack.push(startCell); const animateGeneration = () => { for (let i = 0; i < 3; i++) { // Generate multiple cells per frame if (stack.length === 0) { this.isGenerating = false; this.updateButtons(); this.draw(); return; } const current = stack[stack.length - 1]; const neighbors = this.getUnvisitedNeighbors(current.x, current.y); if (neighbors.length > 0) { const next = neighbors[Math.floor(Math.random() * neighbors.length)]; // Remove wall between current and next const wallX = current.x + (next.x - current.x) / 2; const wallY = current.y + (next.y - current.y) / 2; this.grid[wallY][wallX].wall = false; next.wall = false; next.visited = true; stack.push(next); } else { stack.pop(); } } this.draw(); this.animationId = requestAnimationFrame(animateGeneration); }; animateGeneration(); } getUnvisitedNeighbors(x, y) { const neighbors = []; const directions = [ { x: 0, y: -2 }, // Up { x: 2, y: 0 }, // Right { x: 0, y: 2 }, // Down { x: -2, y: 0 } // Left ]; for (const dir of directions) { const newX = x + dir.x; const newY = y + dir.y; if (newX >= 1 && newX < this.cols - 1 && newY >= 1 && newY < this.rows - 1 && !this.grid[newY][newX].visited) { neighbors.push(this.grid[newY][newX]); } } return neighbors; } heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } getNeighbors(node) { const neighbors = []; const directions = [ { x: 0, y: -1 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 } ]; for (const dir of directions) { const newX = node.x + dir.x; const newY = node.y + dir.y; if (newX >= 0 && newX < this.cols && newY >= 0 && newY < this.rows && !this.grid[newY][newX].wall) { neighbors.push(this.grid[newY][newX]); } } return neighbors; } solveWithAStar() { this.isSolving = true; this.updateButtons(); this.clearPath(); const openSet = []; const closedSet = new Set(); const cameFrom = new Map(); const gScore = new Map(); const fScore = new Map(); const startNode = this.grid[this.start.y][this.start.x]; const endNode = this.grid[this.end.y][this.end.x]; openSet.push(startNode); gScore.set(startNode, 0); fScore.set(startNode, this.heuristic(startNode, endNode)); let stepCount = 0; const animateSolving = () => { for (let i = 0; i < 2; i++) { // Process multiple nodes per frame if (openSet.length === 0) { this.isSolving = false; this.updateButtons(); return; } // Find node with lowest fScore let current = openSet[0]; let currentIndex = 0; for (let i = 1; i < openSet.length; i++) { if (fScore.get(openSet[i]) < fScore.get(current)) { current = openSet[i]; currentIndex = i; } } openSet.splice(currentIndex, 1); closedSet.add(current); if (current !== startNode && current !== endNode) { current.exploring = false; current.visited = true; } if (current === endNode) { // Reconstruct path const path = []; let pathNode = current; while (pathNode) { path.unshift(pathNode); pathNode = cameFrom.get(pathNode); } this.animatePath(path); return; } const neighbors = this.getNeighbors(current); for (const neighbor of neighbors) { if (closedSet.has(neighbor)) continue; const tentativeGScore = gScore.get(current) + 1; if (!openSet.includes(neighbor)) { openSet.push(neighbor); if (neighbor !== startNode && neighbor !== endNode) { neighbor.exploring = true; } } else if (tentativeGScore >= gScore.get(neighbor)) { continue; } cameFrom.set(neighbor, current); gScore.set(neighbor, tentativeGScore); fScore.set(neighbor, tentativeGScore + this.heuristic(neighbor, endNode)); } } this.draw(); this.animationId = requestAnimationFrame(animateSolving); }; animateSolving(); } animatePath(path) { let pathIndex = 0; const animatePathStep = () => { if (pathIndex >= path.length) { this.isSolving = false; this.updateButtons(); return; } const node = path[pathIndex]; if (node !== this.grid[this.start.y][this.start.x] && node !== this.grid[this.end.y][this.end.x]) { node.inPath = true; node.visited = false; node.exploring = false; } pathIndex++; this.draw(); setTimeout(() => { this.animationId = requestAnimationFrame(animatePathStep); }, 50); }; animatePathStep(); } clearPath() { for (let y = 0; y < this.rows; y++) { for (let x = 0; x < this.cols; x++) { this.grid[y][x].visited = false; this.grid[y][x].exploring = false; this.grid[y][x].inPath = false; } } this.draw(); } updateButtons() { const generateBtn = document.getElementById('generateBtn'); const solveBtn = document.getElementById('solveBtn'); const clearBtn = document.getElementById('clearBtn'); const isActive = this.isGenerating || this.isSolving; generateBtn.disabled = isActive; solveBtn.disabled = isActive; clearBtn.disabled = isActive; } draw() { this.ctx.clearRect(0, 0, this.width, this.height); // Draw grid for (let y = 0; y < this.rows; y++) { for (let x = 0; x < this.cols; x++) { const cell = this.grid[y][x]; const pixelX = x * this.cellSize; const pixelY = y * this.cellSize; if (cell.wall) { this.ctx.fillStyle = '#2c3e50'; } else if (cell.inPath) { this.ctx.fillStyle = '#9b59b6'; } else if (cell.exploring) { this.ctx.fillStyle = '#f1c40f'; } else if (cell.visited) { this.ctx.fillStyle = '#e67e22'; } else { this.ctx.fillStyle = '#ecf0f1'; } this.ctx.fillRect(pixelX, pixelY, this.cellSize, this.cellSize); } } // Draw start point this.ctx.fillStyle = '#27ae60'; this.ctx.beginPath(); this.ctx.arc( this.start.x * this.cellSize + this.cellSize / 2, this.start.y * this.cellSize + this.cellSize / 2, this.cellSize * 0.3, 0, 2 * Math.PI ); this.ctx.fill(); // Draw end point this.ctx.fillStyle = '#e74c3c'; this.ctx.beginPath(); this.ctx.arc( this.end.x * this.cellSize + this.cellSize / 2, this.end.y * this.cellSize + this.cellSize / 2, this.cellSize * 0.3, 0, 2 * Math.PI ); this.ctx.fill(); } } // Initialize the maze generator when the page loads window.addEventListener('load', () => { new MazeGenerator(800, 600, 20); }); </script> </body> </html> ``` This creates a beautiful and interactive maze generator with A* pathfinding visualization. Here's what it includes: **Features:** - 🎨 **Beautiful gradient background** and modern UI styling - 🌊 **Smooth animations** for both maze generation and pathfinding - 🎯 **Visual feedback** with different colors for each state: - Green circle: Start point - Red circle: End point - Yellow cells: Currently exploring (A* open set) - Orange cells: Already visited (A* closed set) - Purple cells: Final optimal path - Dark blue: Walls - Light gray: Open passages **Controls:** - **Generate New Maze**: Creates a random maze using recursive backtracking - **Solve with A***: Visualizes the A* algorithm step-by-step - **Clear Path**: Removes the pathfinding visualization while keeping the maze **Technical Details:** - Uses **recursive backtracking** for maze generation - Implements **A* pathfinding algorithm** with Manhattan distance heuristic - **Animated visualization** shows the algorithm's progress in real-time - **Responsive design** with hover effects and disabled states during animations - **Optimized rendering** processes multiple steps per frame for smooth performance The visualization clearly shows how A* explores the maze intelligently, expanding nodes with the lowest estimated total cost first, making it much more efficient than breadth-first search while still guaranteeing the shortest path!
# Maze Generator and A* Pathfinding Visualizer Here's a complete maze generator and A* pathfinding visualizer. The visualization shows maze generation and then the A* algorithm finding the optimal path from start to finish. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Maze Generator & A* Pathfinding Visualizer</title> <style> body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #1a1a2e; font-family: 'Arial', sans-serif; overflow: hidden; } canvas { background-color: #16213e; border-radius: 8px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); } .controls { margin-top: 20px; display: flex; gap: 10px; } button { padding: 10px 20px; background-color: #4361ee; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.3s ease; } button:hover { background-color: #3a56d4; transform: translateY(-2px); } h1 { color: #fff; margin-bottom: 20px; text-align: center; font-size: 28px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } #status { color: #e2e2e2; margin-top: 15px; font-size: 18px; min-height: 24px; } </style> </head> <body> <h1>Maze Generator & A* Pathfinding</h1> <canvas id="mazeCanvas"></canvas> <div id="status"></div> <div class="controls"> <button id="newMazeBtn">Generate New Maze</button> <button id="solveBtn" disabled>Solve with A*</button> </div> <script> const canvas = document.getElementById('mazeCanvas'); const ctx = canvas.getContext('2d'); const statusEl = document.getElementById('status'); const newMazeBtn = document.getElementById('newMazeBtn'); const solveBtn = document.getElementById('solveBtn'); // Responsive sizing const cellSize = 20; const rows = 25; const cols = 35; const wallThickness = 3; canvas.width = cols * cellSize; canvas.height = rows * cellSize; // Colors const colors = { background: '#16213e', wall: '#0d1321', visitedCell: '#4361ee', currentCell: '#ff9f1c', startCell: '#2ec4b6', endCell: '#e71d36', pathCell: '#f8f3d4', openSet: 'rgba(142, 202, 230, 0.7)', closedSet: 'rgba(33, 158, 188, 0.7)' }; // Cell class class Cell { constructor(row, col) { this.row = row; this.col = col; this.walls = { top: true, right: true, bottom: true, left: true }; this.visited = false; this.f = 0; // Total cost this.g = 0; // Distance from start this.h = 0; // Heuristic (estimated distance to goal) this.neighbors = []; this.previous = undefined; this.isStart = false; this.isEnd = false; this.isPath = false; } draw() { const x = this.col * cellSize; const y = this.row * cellSize; // Cell background ctx.fillStyle = colors.background; if (this.visited && !this.isStart && !this.isEnd && !this.isPath) { ctx.fillStyle = colors.visitedCell; ctx.globalAlpha = 0.2; } if (this.isStart) ctx.fillStyle = colors.startCell; if (this.isEnd) ctx.fillStyle = colors.endCell; if (this.inOpenSet) ctx.fillStyle = colors.openSet; if (this.inClosedSet) ctx.fillStyle = colors.closedSet; if (this.isPath) ctx.fillStyle = colors.pathCell; ctx.fillRect(x, y, cellSize, cellSize); ctx.globalAlpha = 1; // Cell walls ctx.strokeStyle = colors.wall; ctx.lineWidth = wallThickness; ctx.beginPath(); if (this.walls.top) { ctx.moveTo(x, y); ctx.lineTo(x + cellSize, y); } if (this.walls.right) { ctx.moveTo(x + cellSize, y); ctx.lineTo(x + cellSize, y + cellSize); } if (this.walls.bottom) { ctx.moveTo(x + cellSize, y + cellSize); ctx.lineTo(x, y + cellSize); } if (this.walls.left) { ctx.moveTo(x, y + cellSize); ctx.lineTo(x, y); } ctx.stroke(); } checkNeighbors(grid) { const neighbors = []; const top = this.row > 0 ? grid[this.row - 1][this.col] : undefined; const right = this.col < cols - 1 ? grid[this.row][this.col + 1] : undefined; const bottom = this.row < rows - 1 ? grid[this.row + 1][this.col] : undefined; const left = this.col > 0 ? grid[this.row][this.col - 1] : undefined; if (top && !top.visited) neighbors.push(top); if (right && !right.visited) neighbors.push(right); if (bottom && !bottom.visited) neighbors.push(bottom); if (left && !left.visited) neighbors.push(left); if (neighbors.length > 0) { const randomIndex = Math.floor(Math.random() * neighbors.length); return neighbors[randomIndex]; } return undefined; } getPathNeighbors(grid) { this.neighbors = []; // Top neighbor (if no wall between them) if (this.row > 0 && !this.walls.top) { this.neighbors.push(grid[this.row - 1][this.col]); } // Right neighbor if (this.col < cols - 1 && !this.walls.right) { this.neighbors.push(grid[this.row][this.col + 1]); } // Bottom neighbor if (this.row < rows - 1 && !this.walls.bottom) { this.neighbors.push(grid[this.row + 1][this.col]); } // Left neighbor if (this.col > 0 && !this.walls.left) { this.neighbors.push(grid[this.row][this.col - 1]); } } removeWalls(next) { const x = this.col - next.col; const y = this.row - next.row; if (x === 1) { this.walls.left = false; next.walls.right = false; } else if (x === -1) { this.walls.right = false; next.walls.left = false; } if (y === 1) { this.walls.top = false; next.walls.bottom = false; } else if (y === -1) { this.walls.bottom = false; next.walls.top = false; } } } // Generate grid let grid = []; let startCell, endCell; let current; let stack = []; let mazeGenerated = false; let openSet = []; let closedSet = []; let path = []; let solving = false; function initMaze() { // Reset variables grid = []; stack = []; openSet = []; closedSet = []; path = []; mazeGenerated = false; solving = false; solveBtn.disabled = true; statusEl.textContent = "Generating maze..."; // Create grid of cells for (let r = 0; r < rows; r++) { const row = []; for (let c = 0; c < cols; c++) { row.push(new Cell(r, c)); } grid.push(row); } // Set start as top-left corner startCell = grid[0][0]; startCell.isStart = true; startCell.visited = true; // Set end as bottom-right corner endCell = grid[rows - 1][cols - 1]; endCell.isEnd = true; // Start maze generation with depth-first search current = startCell; stack.push(current); // Start animation generateMazeStep(); } function generateMazeStep() { if (!mazeGenerated) { // Draw the maze drawMaze(); // Get the next cell current.visited = true; const next = current.checkNeighbors(grid); if (next) { next.visited = true; stack.push(current); current.removeWalls(next); current = next; } else if (stack.length > 0) { current = stack.pop(); } else { // Maze generation complete mazeGenerated = true; solveBtn.disabled = false; statusEl.textContent = "Maze generated! Click 'Solve with A*' to find the path."; return; } // Continue animation requestAnimationFrame(generateMazeStep); } } function drawMaze() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw all cells for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { grid[r][c].draw(); } } // Highlight current cell during generation if (!mazeGenerated) { const x = current.col * cellSize; const y = current.row * cellSize; ctx.fillStyle = colors.currentCell; ctx.fillRect(x, y, cellSize, cellSize); } } // A* pathfinding algorithm function solveMaze() { if (!mazeGenerated || solving) return; solving = true; statusEl.textContent = "Solving with A* algorithm..."; // Reset path finding variables for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { grid[r][c].f = 0; grid[r][c].g = 0; grid[r][c].h = 0; grid[r][c].previous = undefined; grid[r][c].getPathNeighbors(grid); grid[r][c].inOpenSet = false; grid[r][c].inClosedSet = false; grid[r][c].isPath = false; } } openSet = [startCell]; closedSet = []; path = []; // Start A* animation solveStep(); } function solveStep() { if (openSet.length > 0) { // Algorithm continues // Find the node with the lowest f score let lowestIndex = 0; for (let i = 0; i < openSet.length; i++) { if (openSet[i].f < openSet[lowestIndex].f) { lowestIndex = i; } } let current = openSet[lowestIndex]; // Mark current node's status openSet.forEach(cell => cell.inOpenSet = true); closedSet.forEach(cell => cell.inClosedSet = true); // Found the end if (current === endCell) { // Create the path path = []; let temp = current; path.push(temp); while (temp.previous) { path.push(temp.previous); temp = temp.previous; } // Mark path path.forEach(cell => { if (!cell.isStart && !cell.isEnd) { cell.isPath = true; } }); drawMaze(); statusEl.textContent = "Path found! Length: " + (path.length - 1) + " steps"; solving = false; return; } // Move current from openSet to closedSet openSet.splice(lowestIndex, 1); closedSet.push(current); current.inOpenSet = false; current.inClosedSet = true; // Check all neighbors for (let i = 0; i < current.neighbors.length; i++) { const neighbor = current.neighbors[i]; // Skip if already evaluated if (closedSet.includes(neighbor)) continue; // Calculate g score (distance from start) const tempG = current.g + 1; let newPath = false; if (openSet.includes(neighbor)) { // This path to neighbor is better than previous if (tempG < neighbor.g) { neighbor.g = tempG; newPath = true; } } else { // Discover a new node neighbor.g = tempG; newPath = true; openSet.push(neighbor); neighbor.inOpenSet = true; } if (newPath) { // Calculate h (heuristic) - Manhattan distance neighbor.h = manhattanDistance(neighbor, endCell); neighbor.f = neighbor.g + neighbor.h; neighbor.previous = current; } } // Create temporary path for visualization let temp = current; path = []; path.push(temp); while (temp.previous) { path.push(temp.previous); temp = temp.previous; } // Draw current state drawMaze(); // Continue animation requestAnimationFrame(solveStep); } else { // No solution statusEl.textContent = "No path found!"; solving = false; } } // Manhattan distance heuristic function manhattanDistance(a, b) { return Math.abs(a.col - b.col) + Math.abs(a.row - b.row); } // Event listeners newMazeBtn.addEventListener('click', () => { initMaze(); }); solveBtn.addEventListener('click', () => { solveMaze(); }); // Initialize window.onload = initMaze; </script> </body> </html> ``` This visualization features: 1. **Maze Generation** using a depth-first search algorithm with backtracking 2. **A* Pathfinding** with step-by-step visualization 3. **Visual Elements**: - Blue cells show the visited areas during maze generation - Orange highlights the current cell during generation - Teal marks the start point - Red marks the destination - Light blue shows cells in the open set (being considered) - Darker blue shows cells in the closed set (already evaluated) - Yellow shows the final optimal path The interface includes controls to generate a new maze or solve the current one with the A* algorithm. The status display provides information about the current process and results.
``` <html> <head> <title>Maze Generator and A* Visualizer</title> <style> body { margin: 0; display: flex; flex-direction: column; align-items: center; background-color: #f0f0f0; } canvas { border: 1px solid black; margin-top: 20px; } button { margin-top: 20px; padding: 10px 20px; font-size: 16px; cursor: pointer; } </style> </head> <body> <h1>Maze Generator and A* Pathfinding Visualizer</h1> <button onclick="calculatePath()">Generate and Solve Maze</button> <canvas id="mazeCanvas"></canvas> <script> const canvas = document.getElementById('mazeCanvas'); const ctx = canvas.getContext('2d'); const rows = 20; const cols = 20; const cellSize = 20; canvas.width = cols * cellSize; canvas.height = rows * cellSize; let grid = Array(rows).fill().map(() => Array(cols).fill(1)); // 1 for wall, 0 for path function generateMaze() { function carve(x, y) { grid[y][x] = 0; // Path const directions = [[0,1], [1,0], [0,-1], [-1,0]]; // Right, Down, Left, Up directions.sort(() => Math.random() - 0.5); // Shuffle for (let [dx, dy] of directions) { let nx = x + dx * 2; let ny = y + dy * 2; if (nx >= 0 && nx < cols && ny >= 0 && ny < rows && grid[ny][nx] === 1) { grid[y + dy][x + dx] = 0; // Remove wall carve(nx, ny); } } } carve(1, 1); // Start from (1,1) grid[1][1] = 0; // Ensure start is path grid[rows-2][cols-2] = 0; // Ensure end is path } function drawGrid() { for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { ctx.fillStyle = grid[y][x] === 1 ? 'black' : 'white'; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); ctx.strokeStyle = 'gray'; ctx.strokeRect(x * cellSize, y * cellSize, cellSize, cellSize); } } // Draw start and end ctx.fillStyle = 'green'; ctx.fillRect(1 * cellSize, 1 * cellSize, cellSize, cellSize); ctx.fillStyle = 'red'; ctx.fillRect((cols-2) * cellSize, (rows-2) * cellSize, cellSize, cellSize); } function aStar(grid, start, end) { const openSet = [start]; const cameFrom = {}; const gScore = Array(rows).fill().map(() => Array(cols).fill(Infinity)); gScore[start.y][start.x] = 0; const fScore = Array(rows).fill().map(() => Array(cols).fill(Infinity)); fScore[start.y][start.x] = heuristic(start, end); const visited = []; // For visualization while (openSet.length > 0) { openSet.sort((a, b) => fScore[a.y][a.x] - fScore[b.y][b.x]); const current = openSet.shift(); visited.push(current); // Add to visited if (current.x === end.x && current.y === end.y) { const path = reconstructPath(cameFrom, current); return { path, visited }; } const neighbors = getNeighbors(grid, current); for (let neighbor of neighbors) { const tentative_gScore = gScore[current.y][current.x] + 1; if (tentative_gScore < gScore[neighbor.y][neighbor.x]) { cameFrom[`${neighbor.y}-${neighbor.x}`] = current; gScore[neighbor.y][neighbor.x] = tentative_gScore; fScore[neighbor.y][neighbor.x] = gScore[neighbor.y][neighbor.x] + heuristic(neighbor, end); if (!openSet.some(cell => cell.x === neighbor.x && cell.y === neighbor.y)) { openSet.push(neighbor); } } } } return { path: null, visited }; } function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); // Manhattan distance } function getNeighbors(grid, cell) { const neighbors = []; const directions = [[0,1], [1,0], [0,-1], [-1,0]]; for (let [dx, dy] of directions) { let nx = cell.x + dx; let ny = cell.y + dy; if (nx >= 0 && nx < cols && ny >= 0 && ny < rows && grid[ny][nx] === 0) { neighbors.push({x: nx, y: ny}); } } return neighbors; } function reconstructPath(cameFrom, current) { const path = [current]; while (cameFrom.hasOwnProperty(`${current.y}-${current.x}`)) { current = cameFrom[`${current.y}-${current.x}`]; path.unshift(current); } return path; } async function animateVisited(visited) { for (let cell of visited) { ctx.fillStyle = 'blue'; // Explored color ctx.fillRect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize); await sleep(50); } } async function animatePath(path) { for (let cell of path) { if (cell.x !== path[path.length-1].x || cell.y !== path[path.length-1].y) { // Skip end for now ctx.fillStyle = 'yellow'; // Path color ctx.fillRect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize); await sleep(50); } } // Highlight end finally ctx.fillStyle = 'red'; ctx.fillRect(path[path.length-1].x * cellSize, path[path.length-1].y * cellSize, cellSize, cellSize); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function calculatePath() { generateMaze(); drawGrid(); const start = {x: 1, y: 1}; const end = {x: cols-2, y: rows-2}; const { path, visited } = aStar(grid, start, end); if (path) { animateVisited(visited).then(() => animatePath(path)); } } </script> </body> </html> ```