diff --git a/projects/maze/index.html b/projects/maze/index.html
index 75b35c2..1ef9e18 100644
--- a/projects/maze/index.html
+++ b/projects/maze/index.html
@@ -1,19 +1,44 @@
-
+
-
-
- Maze
+
+ Maze Solver
-
- Maze
- Contribute: generator, solver, keyboard navigation.
+ Maze Solver
+
+
+
+
+
+
+
+
+ 20x20
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nodes Visited: 0
+ Path Length: 0
+ Time: 0ms
+
-
\ No newline at end of file
diff --git a/projects/maze/main.js b/projects/maze/main.js
index 8a1f5ad..db0c568 100644
--- a/projects/maze/main.js
+++ b/projects/maze/main.js
@@ -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();
\ No newline at end of file
diff --git a/projects/maze/styles.css b/projects/maze/styles.css
index a3ef54d..80810e7 100644
--- a/projects/maze/styles.css
+++ b/projects/maze/styles.css
@@ -1,25 +1,95 @@
-body {
- font-family: system-ui;
+ body {
+ font-family: system-ui, -apple-system, sans-serif;
background: #0f0f12;
color: #eef1f8;
margin: 0;
- padding: 2rem;
+ padding: 1rem;
display: grid;
- place-items: center
+ place-items: center;
+ min-height: 100vh;
}
main {
max-width: 560px;
- width: 100%
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+h1 {
+ text-align: center;
+ margin: 0;
+ color: #6ee7b7;
}
canvas {
background: #17171c;
border: 1px solid #262631;
- border-radius: .5rem
+ border-radius: .5rem;
+ width: 100%;
+ height: auto;
+}
+
+.controls {
+ background: #17171c;
+ border: 1px solid #262631;
+ border-radius: .5rem;
+ padding: 1rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ justify-content: space-between;
+ align-items: center;
}
-.notes {
+.control-group, .button-group {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+label {
+ font-size: 0.9rem;
color: #a6adbb;
- font-size: .9rem
+}
+
+select, button {
+ background: #262631;
+ color: #eef1f8;
+ border: 1px solid #3a3a4a;
+ border-radius: 0.25rem;
+ padding: 0.4rem 0.6rem;
+ font-family: inherit;
+ cursor: pointer;
+}
+
+button {
+ background: #6ee7b7;
+ color: #0f0f12;
+ font-weight: bold;
+ border: none;
+ transition: background 0.2s;
+}
+
+button:hover {
+ background: #86efc8;
+}
+
+input[type="range"] {
+ cursor: pointer;
+}
+
+.metrics {
+ color: #a6adbb;
+ font-size: .9rem;
+ display: flex;
+ justify-content: space-around;
+ background: #17171c;
+ padding: 0.75rem;
+ border-radius: 0.5rem;
+}
+
+.metrics strong {
+ color: #eef1f8;
}
\ No newline at end of file