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
12 changes: 8 additions & 4 deletions projects/maze/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
Expand All @@ -9,7 +9,7 @@
<body>
<main>
<h1>Maze Solver</h1>
<div class="controls">
<div class="controls" role="region" aria-label="Maze controls">
<div class="control-group">
<label for="algorithm">Algorithm:</label>
<select id="algorithm">
Expand All @@ -29,15 +29,19 @@ <h1>Maze Solver</h1>
<div class="button-group">
<button id="generateBtn">New Maze</button>
<button id="solveBtn">Solve</button>
<button id="clearBtn">Clear Path</button>
<button id="drawToggle" aria-pressed="false">Draw to Exit Mode</button>
<button id="clearPath">Clear Path</button>
</div>
<span id="status" aria-live="polite" class="status">Ready</span>
</div>
<canvas id="maze" width="560" height="560"></canvas>
<canvas id="maze" width="560" height="560" tabindex="0"></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>
<p class="notes">Contribute: generator, solver, keyboard navigation.</p>
<p class="instructions">Instructions: Use "Solve" for automatic pathfinding, or toggle "Draw to Exit Mode" to manually draw a path from entrance to exit.</p>
</main>
<script type="module" src="./main.js"></script>
</body>
Expand Down
159 changes: 152 additions & 7 deletions projects/maze/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const canvas = document.getElementById('maze');
const canvas = document.getElementById('maze');
const ctx = canvas.getContext('2d');

// UI Elements
Expand All @@ -8,18 +8,28 @@ 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');
const clearBtn = document.getElementById('clearPath');
const drawToggle = document.getElementById('drawToggle');
const status = document.getElementById('status');

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

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

// Drawing state
let drawMode = false;
let isDrawing = false;
let pathCells = [];

function setStatus(text) { status.textContent = text; }

// --- Maze Generation (Recursive Backtracker) ---
function createGrid() {
grid = [];
Expand Down Expand Up @@ -76,6 +86,10 @@ function removeWall(a, b) {

// --- Pathfinding Algorithms ---
function solve() {
if (drawMode) {
setStatus('Disable draw mode to use automatic solver');
return;
}
cancelAnimationFrame(animationFrameId);
clearPath();
const startTime = performance.now();
Expand Down Expand Up @@ -173,6 +187,117 @@ function reconstructPath(parentMap, end) {
return path;
}

// --- Manual Drawing Functions ---
function toggleDrawMode(on) {
drawMode = typeof on === 'boolean' ? on : !drawMode;
drawToggle.setAttribute('aria-pressed', String(drawMode));
drawToggle.classList.toggle('active', drawMode);
setStatus(drawMode ? 'Draw mode: ON — draw a path' : 'Draw mode: OFF');
if (drawMode) {
clearPath();
}
}

function cellFromEvent(e) {
const rect = canvas.getBoundingClientRect();
const px = (e.clientX - rect.left) * (canvas.width / rect.width);
const py = (e.clientY - rect.top) * (canvas.height / rect.height);
const cx = Math.floor(px / cellSize);
const cy = Math.floor(py / cellSize);
if (cx < 0 || cy < 0 || cx >= size || cy >= size) return null;
return grid[cy][cx];
}

function cellsAreNeighbors(a, b) {
const dx = b.x - a.x, dy = b.y - a.y;
if (dx === 1 && dy === 0) return ['right', 'left'];
if (dx === -1 && dy === 0) return ['left', 'right'];
if (dx === 0 && dy === 1) return ['bottom', 'top'];
if (dx === 0 && dy === -1) return ['top', 'bottom'];
return null;
}

function pointerDown(e) {
if (!drawMode) return;
isDrawing = true;
canvas.setPointerCapture(e.pointerId);
const cell = cellFromEvent(e);
if (cell) {
pathCells = [cell];
render();
}
}

function pointerMove(e) {
if (!isDrawing) return;
const cell = cellFromEvent(e);
if (!cell) return;
const last = pathCells[pathCells.length - 1];
if (!last || (last.x === cell.x && last.y === cell.y)) return;

// Check if move is valid (adjacent and no wall between)
const neigh = cellsAreNeighbors(last, cell);
if (!neigh) return; // not adjacent, skip
const [fromSide, toSide] = neigh;
if (last.walls[fromSide] || cell.walls[toSide]) {
// Wall blocking, don't add this cell
return;
}

pathCells.push(cell);
render();
}

function pointerUp(e) {
if (isDrawing) {
isDrawing = false;
tryValidatePath();
}
try { canvas.releasePointerCapture(e.pointerId); } catch (err) {}
}

function tryValidatePath() {
if (!pathCells.length) { setStatus('No path drawn'); return; }
const start = grid[0][0];
const exit = grid[size - 1][size - 1];
const first = pathCells[0];
const last = pathCells[pathCells.length - 1];
if (first.x !== start.x || first.y !== start.y) { setStatus('Path must start at the entrance'); return; }
if (last.x !== exit.x || last.y !== exit.y) { setStatus('Path must end at the exit'); return; }

// ensure each step moves to neighbor and there's no wall between
for (let i = 0; i < pathCells.length - 1; i++) {
const a = pathCells[i], b = pathCells[i + 1];
const neigh = cellsAreNeighbors(a, b);
if (!neigh) { setStatus('Invalid path: must travel between adjacent cells'); return; }
const [fromSide, toSide] = neigh;
if (a.walls[fromSide] || b.walls[toSide]) { setStatus('Invalid path: crosses a wall'); return; }
}
setStatus('Success! Path reaches the exit without crossing walls.');
}

function render() {
drawMaze();
// overlay path
if (pathCells.length && drawMode) {
ctx.save();
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(52,144,220,0.95)';
ctx.shadowColor = 'rgba(52,144,220,0.7)';
ctx.shadowBlur = 8;
ctx.lineWidth = Math.max(4, cellSize * 0.45);
ctx.beginPath();
for (let i = 0; i < pathCells.length; i++) {
const p = pathCells[i];
const cx = p.x * cellSize + cellSize / 2;
const cy = p.y * cellSize + cellSize / 2;
if (i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
}
ctx.stroke();
ctx.restore();
}
}

// --- Drawing & Animation ---
function drawCell(cell, color) {
Expand Down Expand Up @@ -226,27 +351,30 @@ function drawPath(path) {
i++;
animationFrameId = setTimeout(animate, 20);
} else {
// Redraw start and end over the path
// Redraw start and end over the path
drawCell(grid[0][0], '#6ee7b7');
drawCell(grid[size-1][size-1], '#f472b6');
drawCell(grid[size - 1][size - 1], '#f472b6');
}
}
animate();
}

function clearPath() {
cancelAnimationFrame(animationFrameId);
pathCells = [];
grid.forEach(row => row.forEach(cell => {
cell.visited = false;
delete cell.g; delete cell.h; delete cell.f;
delete cell.g;
delete cell.h;
delete cell.f;
}));
nodesVisitedEl.textContent = 0;
pathLengthEl.textContent = 0;
timeTakenEl.textContent = '0ms';
setStatus(drawMode ? 'Draw mode: ON — path cleared' : 'Path cleared');
drawMaze();
}


// --- Event Listeners ---
generateBtn.addEventListener('click', () => {
cancelAnimationFrame(animationFrameId);
Expand All @@ -256,14 +384,31 @@ generateBtn.addEventListener('click', () => {
solveBtn.addEventListener('click', solve);
clearBtn.addEventListener('click', clearPath);

drawToggle.addEventListener('click', () => toggleDrawMode());
drawToggle.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleDrawMode();
}
});

canvas.addEventListener('pointerdown', pointerDown);
canvas.addEventListener('pointermove', pointerMove);
window.addEventListener('pointerup', pointerUp);

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

// --- Initial Load ---
generateMaze();
generateMaze();
setStatus('Ready');

// Expose for debugging
window._maze = { grid, render, clearPath, toggleDrawMode };
45 changes: 41 additions & 4 deletions projects/maze/styles.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
body {
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f0f12;
color: #eef1f8;
Expand Down Expand Up @@ -29,8 +29,10 @@ canvas {
border-radius: .5rem;
width: 100%;
height: auto;
touch-action: none;
}

/* === Controls === */
.controls {
background: #17171c;
border: 1px solid #262631;
Expand All @@ -43,7 +45,8 @@ canvas {
align-items: center;
}

.control-group, .button-group {
.control-group,
.button-group {
display: flex;
align-items: center;
gap: 0.5rem;
Expand All @@ -54,7 +57,24 @@ label {
color: #a6adbb;
}

select, button {
.controls button {
background: #23232a;
color: #eef1f8;
border: 1px solid #37373f;
padding: .4rem .6rem;
border-radius: .35rem;
cursor: pointer;
font-family: inherit;
}

.controls button[aria-pressed="true"] {
background: linear-gradient(90deg, #2b6cb0, #2b9cf0);
color: #fff;
border-color: #1f6fb5;
}

select,
button {
background: #262631;
color: #eef1f8;
border: 1px solid #3a3a4a;
Expand All @@ -80,6 +100,23 @@ input[type="range"] {
cursor: pointer;
}

/* === Extra UI Elements === */
.status {
margin-left: .5rem;
color: #9fb4c8;
font-size: .95rem;
}

.instructions {
color: #9aa3b3;
font-size: .9rem;
margin-top: .5rem;
}

.notes {
color: #a6adbb;
}

.metrics {
color: #a6adbb;
font-size: .9rem;
Expand All @@ -92,4 +129,4 @@ input[type="range"] {

.metrics strong {
color: #eef1f8;
}
}