Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions projects/maze/index.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
<!doctype html>
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Maze</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maze Solver</title>
<link rel="stylesheet" href="./styles.css">
</head>

<body>
<main>
<h1>Maze</h1><canvas id="maze" width="320" height="320"></canvas>
<p class="notes">Contribute: generator, solver, keyboard navigation.</p>
<h1>Maze Solver</h1>
<div class="controls">
<div class="control-group">
<label for="algorithm">Algorithm:</label>
<select id="algorithm">
<option value="bfs">BFS (Breadth-First Search)</option>
<option value="astar">A* Search</option>
</select>
</div>
<div class="control-group">
<label for="mazeSize">Size:</label>
<input type="range" id="mazeSize" min="10" max="50" value="20">
<span id="mazeSizeValue">20x20</span>
</div>
<div class="control-group">
<label for="speed">Speed:</label>
<input type="range" id="speed" min="1" max="100" value="50">
</div>
<div class="button-group">
<button id="generateBtn">New Maze</button>
<button id="solveBtn">Solve</button>
<button id="clearBtn">Clear Path</button>
</div>
</div>
<canvas id="maze" width="560" height="560"></canvas>
<div class="metrics">
<span>Nodes Visited: <strong id="nodes-visited">0</strong></span>
<span>Path Length: <strong id="path-length">0</strong></span>
<span>Time: <strong id="time-taken">0ms</strong></span>
</div>
</main>
<script type="module" src="./main.js"></script>
</body>

</html>
273 changes: 269 additions & 4 deletions projects/maze/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,269 @@
const c = document.getElementById('maze'); const ctx = c.getContext('2d');
// TODO: implement maze generation and basic player movement
ctx.fillStyle = '#17171c'; ctx.fillRect(0, 0, c.width, c.height);
ctx.fillStyle = '#6ee7b7'; ctx.fillRect(8, 8, 24, 24);
const canvas = document.getElementById('maze');
const ctx = canvas.getContext('2d');

// UI Elements
const algorithmSelect = document.getElementById('algorithm');
const mazeSizeSlider = document.getElementById('mazeSize');
const mazeSizeValue = document.getElementById('mazeSizeValue');
const speedSlider = document.getElementById('speed');
const generateBtn = document.getElementById('generateBtn');
const solveBtn = document.getElementById('solveBtn');
const clearBtn = document.getElementById('clearBtn');

// Metrics
const nodesVisitedEl = document.getElementById('nodes-visited');
const pathLengthEl = document.getElementById('path-length');
const timeTakenEl = document.getElementById('time-taken');

let size = 20;
let cellSize = canvas.width / size;
let grid = [];
let animationFrameId;

// --- Maze Generation (Recursive Backtracker) ---
function createGrid() {
grid = [];
for (let y = 0; y < size; y++) {
let row = [];
for (let x = 0; x < size; x++) {
row.push({ x, y, walls: { top: true, right: true, bottom: true, left: true }, visited: false });
}
grid.push(row);
}
}

function generateMaze() {
createGrid();
let stack = [];
let current = grid[0][0];
current.visited = true;
stack.push(current);

while (stack.length > 0) {
current = stack.pop();
let neighbors = getUnvisitedNeighbors(current.x, current.y);

if (neighbors.length > 0) {
stack.push(current);
let neighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
removeWall(current, neighbor);
neighbor.visited = true;
stack.push(neighbor);
}
}
// Reset visited for solver
grid.forEach(row => row.forEach(cell => cell.visited = false));
drawMaze();
}

function getUnvisitedNeighbors(x, y) {
const neighbors = [];
if (y > 0 && !grid[y - 1][x].visited) neighbors.push(grid[y - 1][x]); // Top
if (x < size - 1 && !grid[y][x + 1].visited) neighbors.push(grid[y][x + 1]); // Right
if (y < size - 1 && !grid[y + 1][x].visited) neighbors.push(grid[y + 1][x]); // Bottom
if (x > 0 && !grid[y][x - 1].visited) neighbors.push(grid[y][x - 1]); // Left
return neighbors;
}

function removeWall(a, b) {
let x = a.x - b.x;
if (x === 1) { a.walls.left = false; b.walls.right = false; }
else if (x === -1) { a.walls.right = false; b.walls.left = false; }
let y = a.y - b.y;
if (y === 1) { a.walls.top = false; b.walls.bottom = false; }
else if (y === -1) { a.walls.bottom = false; b.walls.top = false; }
}

// --- Pathfinding Algorithms ---
function solve() {
cancelAnimationFrame(animationFrameId);
clearPath();
const startTime = performance.now();
const algorithm = algorithmSelect.value === 'bfs' ? bfs : astar;
const { visitedOrder, path } = algorithm();
const endTime = performance.now();

timeTakenEl.textContent = `${Math.round(endTime - startTime)}ms`;
animateSolution(visitedOrder, path);
}

function bfs() {
const start = grid[0][0];
const end = grid[size - 1][size - 1];
let queue = [start];
start.visited = true;
let visitedOrder = [start];
let parentMap = new Map();

while (queue.length > 0) {
const current = queue.shift();
if (current === end) break;

getValidNeighbors(current).forEach(neighbor => {
if (!neighbor.visited) {
neighbor.visited = true;
parentMap.set(neighbor, current);
queue.push(neighbor);
visitedOrder.push(neighbor);
}
});
}
return { visitedOrder, path: reconstructPath(parentMap, end) };
}

function astar() {
const start = grid[0][0];
const end = grid[size - 1][size - 1];
let openSet = [start];
start.g = 0;
start.h = heuristic(start, end);
start.f = start.h;

let visitedOrder = [];
let parentMap = new Map();

while (openSet.length > 0) {
openSet.sort((a, b) => a.f - b.f);
const current = openSet.shift();

visitedOrder.push(current);
current.visited = true;

if (current === end) break;

getValidNeighbors(current).forEach(neighbor => {
if (neighbor.visited) return;

const tentativeG = current.g + 1;
if (tentativeG < (neighbor.g || Infinity)) {
parentMap.set(neighbor, current);
neighbor.g = tentativeG;
neighbor.h = heuristic(neighbor, end);
neighbor.f = neighbor.g + neighbor.h;
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
}
}
});
}
return { visitedOrder, path: reconstructPath(parentMap, end) };
}

function getValidNeighbors(cell) {
const neighbors = [];
const { x, y } = cell;
if (!cell.walls.top && y > 0) neighbors.push(grid[y - 1][x]);
if (!cell.walls.right && x < size - 1) neighbors.push(grid[y][x + 1]);
if (!cell.walls.bottom && y < size - 1) neighbors.push(grid[y + 1][x]);
if (!cell.walls.left && x > 0) neighbors.push(grid[y][x - 1]);
return neighbors;
}

function heuristic(a, b) { // Manhattan distance
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}

function reconstructPath(parentMap, end) {
let path = [end];
let current = end;
while (parentMap.has(current)) {
current = parentMap.get(current);
path.unshift(current);
}
return path;
}


// --- Drawing & Animation ---
function drawCell(cell, color) {
ctx.fillStyle = color;
ctx.fillRect(cell.x * cellSize + 1, cell.y * cellSize + 1, cellSize - 2, cellSize - 2);
}

function drawMaze() {
ctx.fillStyle = '#17171c';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#3a3a4a';
ctx.lineWidth = 2;

for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
let cell = grid[y][x];
if (cell.walls.top) { ctx.beginPath(); ctx.moveTo(x * cellSize, y * cellSize); ctx.lineTo((x + 1) * cellSize, y * cellSize); ctx.stroke(); }
if (cell.walls.right) { ctx.beginPath(); ctx.moveTo((x + 1) * cellSize, y * cellSize); ctx.lineTo((x + 1) * cellSize, (y + 1) * cellSize); ctx.stroke(); }
if (cell.walls.bottom) { ctx.beginPath(); ctx.moveTo((x + 1) * cellSize, (y + 1) * cellSize); ctx.lineTo(x * cellSize, (y + 1) * cellSize); ctx.stroke(); }
if (cell.walls.left) { ctx.beginPath(); ctx.moveTo(x * cellSize, (y + 1) * cellSize); ctx.lineTo(x * cellSize, y * cellSize); ctx.stroke(); }
}
}
// Draw start and end points
drawCell(grid[0][0], '#6ee7b7'); // Start
drawCell(grid[size - 1][size - 1], '#f472b6'); // End
}

function animateSolution(visitedOrder, path) {
let i = 0;
const speed = 101 - speedSlider.value;

function animate() {
if (i < visitedOrder.length) {
drawCell(visitedOrder[i], '#3b82f6'); // Visited color
nodesVisitedEl.textContent = i + 1;
i++;
animationFrameId = setTimeout(animate, speed / 5);
} else {
drawPath(path);
}
}
animate();
}

function drawPath(path) {
let i = 0;
function animate() {
if (i < path.length) {
drawCell(path[i], '#eab308'); // Path color
pathLengthEl.textContent = i + 1;
i++;
animationFrameId = setTimeout(animate, 20);
} else {
// Redraw start and end over the path
drawCell(grid[0][0], '#6ee7b7');
drawCell(grid[size-1][size-1], '#f472b6');
}
}
animate();
}

function clearPath() {
cancelAnimationFrame(animationFrameId);
grid.forEach(row => row.forEach(cell => {
cell.visited = false;
delete cell.g; delete cell.h; delete cell.f;
}));
nodesVisitedEl.textContent = 0;
pathLengthEl.textContent = 0;
timeTakenEl.textContent = '0ms';
drawMaze();
}


// --- Event Listeners ---
generateBtn.addEventListener('click', () => {
cancelAnimationFrame(animationFrameId);
generateMaze();
clearPath();
});
solveBtn.addEventListener('click', solve);
clearBtn.addEventListener('click', clearPath);

mazeSizeSlider.addEventListener('input', (e) => {
size = parseInt(e.target.value);
mazeSizeValue.textContent = `${size}x${size}`;
cellSize = canvas.width / size;
cancelAnimationFrame(animationFrameId);
generateMaze();
clearPath();
});

// --- Initial Load ---
generateMaze();
Loading
Loading