Skip to content

Commit 0776a9a

Browse files
Merge pull request #193 from Nischay-loq/path_to_exit
Added path to exit.
2 parents 4297f15 + c406c87 commit 0776a9a

File tree

3 files changed

+201
-15
lines changed

3 files changed

+201
-15
lines changed

projects/maze/index.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!doctype html>
1+
<!doctype html>
22
<html lang="en">
33
<head>
44
<meta charset="utf-8">
@@ -9,7 +9,7 @@
99
<body>
1010
<main>
1111
<h1>Maze Solver</h1>
12-
<div class="controls">
12+
<div class="controls" role="region" aria-label="Maze controls">
1313
<div class="control-group">
1414
<label for="algorithm">Algorithm:</label>
1515
<select id="algorithm">
@@ -29,15 +29,19 @@ <h1>Maze Solver</h1>
2929
<div class="button-group">
3030
<button id="generateBtn">New Maze</button>
3131
<button id="solveBtn">Solve</button>
32-
<button id="clearBtn">Clear Path</button>
32+
<button id="drawToggle" aria-pressed="false">Draw to Exit Mode</button>
33+
<button id="clearPath">Clear Path</button>
3334
</div>
35+
<span id="status" aria-live="polite" class="status">Ready</span>
3436
</div>
35-
<canvas id="maze" width="560" height="560"></canvas>
37+
<canvas id="maze" width="560" height="560" tabindex="0"></canvas>
3638
<div class="metrics">
3739
<span>Nodes Visited: <strong id="nodes-visited">0</strong></span>
3840
<span>Path Length: <strong id="path-length">0</strong></span>
3941
<span>Time: <strong id="time-taken">0ms</strong></span>
4042
</div>
43+
<p class="notes">Contribute: generator, solver, keyboard navigation.</p>
44+
<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>
4145
</main>
4246
<script type="module" src="./main.js"></script>
4347
</body>

projects/maze/main.js

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const canvas = document.getElementById('maze');
1+
const canvas = document.getElementById('maze');
22
const ctx = canvas.getContext('2d');
33

44
// UI Elements
@@ -8,18 +8,28 @@ const mazeSizeValue = document.getElementById('mazeSizeValue');
88
const speedSlider = document.getElementById('speed');
99
const generateBtn = document.getElementById('generateBtn');
1010
const solveBtn = document.getElementById('solveBtn');
11-
const clearBtn = document.getElementById('clearBtn');
11+
const clearBtn = document.getElementById('clearPath');
12+
const drawToggle = document.getElementById('drawToggle');
13+
const status = document.getElementById('status');
1214

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

20+
// State
1821
let size = 20;
1922
let cellSize = canvas.width / size;
2023
let grid = [];
2124
let animationFrameId;
2225

26+
// Drawing state
27+
let drawMode = false;
28+
let isDrawing = false;
29+
let pathCells = [];
30+
31+
function setStatus(text) { status.textContent = text; }
32+
2333
// --- Maze Generation (Recursive Backtracker) ---
2434
function createGrid() {
2535
grid = [];
@@ -76,6 +86,10 @@ function removeWall(a, b) {
7686

7787
// --- Pathfinding Algorithms ---
7888
function solve() {
89+
if (drawMode) {
90+
setStatus('Disable draw mode to use automatic solver');
91+
return;
92+
}
7993
cancelAnimationFrame(animationFrameId);
8094
clearPath();
8195
const startTime = performance.now();
@@ -173,6 +187,117 @@ function reconstructPath(parentMap, end) {
173187
return path;
174188
}
175189

190+
// --- Manual Drawing Functions ---
191+
function toggleDrawMode(on) {
192+
drawMode = typeof on === 'boolean' ? on : !drawMode;
193+
drawToggle.setAttribute('aria-pressed', String(drawMode));
194+
drawToggle.classList.toggle('active', drawMode);
195+
setStatus(drawMode ? 'Draw mode: ON — draw a path' : 'Draw mode: OFF');
196+
if (drawMode) {
197+
clearPath();
198+
}
199+
}
200+
201+
function cellFromEvent(e) {
202+
const rect = canvas.getBoundingClientRect();
203+
const px = (e.clientX - rect.left) * (canvas.width / rect.width);
204+
const py = (e.clientY - rect.top) * (canvas.height / rect.height);
205+
const cx = Math.floor(px / cellSize);
206+
const cy = Math.floor(py / cellSize);
207+
if (cx < 0 || cy < 0 || cx >= size || cy >= size) return null;
208+
return grid[cy][cx];
209+
}
210+
211+
function cellsAreNeighbors(a, b) {
212+
const dx = b.x - a.x, dy = b.y - a.y;
213+
if (dx === 1 && dy === 0) return ['right', 'left'];
214+
if (dx === -1 && dy === 0) return ['left', 'right'];
215+
if (dx === 0 && dy === 1) return ['bottom', 'top'];
216+
if (dx === 0 && dy === -1) return ['top', 'bottom'];
217+
return null;
218+
}
219+
220+
function pointerDown(e) {
221+
if (!drawMode) return;
222+
isDrawing = true;
223+
canvas.setPointerCapture(e.pointerId);
224+
const cell = cellFromEvent(e);
225+
if (cell) {
226+
pathCells = [cell];
227+
render();
228+
}
229+
}
230+
231+
function pointerMove(e) {
232+
if (!isDrawing) return;
233+
const cell = cellFromEvent(e);
234+
if (!cell) return;
235+
const last = pathCells[pathCells.length - 1];
236+
if (!last || (last.x === cell.x && last.y === cell.y)) return;
237+
238+
// Check if move is valid (adjacent and no wall between)
239+
const neigh = cellsAreNeighbors(last, cell);
240+
if (!neigh) return; // not adjacent, skip
241+
const [fromSide, toSide] = neigh;
242+
if (last.walls[fromSide] || cell.walls[toSide]) {
243+
// Wall blocking, don't add this cell
244+
return;
245+
}
246+
247+
pathCells.push(cell);
248+
render();
249+
}
250+
251+
function pointerUp(e) {
252+
if (isDrawing) {
253+
isDrawing = false;
254+
tryValidatePath();
255+
}
256+
try { canvas.releasePointerCapture(e.pointerId); } catch (err) {}
257+
}
258+
259+
function tryValidatePath() {
260+
if (!pathCells.length) { setStatus('No path drawn'); return; }
261+
const start = grid[0][0];
262+
const exit = grid[size - 1][size - 1];
263+
const first = pathCells[0];
264+
const last = pathCells[pathCells.length - 1];
265+
if (first.x !== start.x || first.y !== start.y) { setStatus('Path must start at the entrance'); return; }
266+
if (last.x !== exit.x || last.y !== exit.y) { setStatus('Path must end at the exit'); return; }
267+
268+
// ensure each step moves to neighbor and there's no wall between
269+
for (let i = 0; i < pathCells.length - 1; i++) {
270+
const a = pathCells[i], b = pathCells[i + 1];
271+
const neigh = cellsAreNeighbors(a, b);
272+
if (!neigh) { setStatus('Invalid path: must travel between adjacent cells'); return; }
273+
const [fromSide, toSide] = neigh;
274+
if (a.walls[fromSide] || b.walls[toSide]) { setStatus('Invalid path: crosses a wall'); return; }
275+
}
276+
setStatus('Success! Path reaches the exit without crossing walls.');
277+
}
278+
279+
function render() {
280+
drawMaze();
281+
// overlay path
282+
if (pathCells.length && drawMode) {
283+
ctx.save();
284+
ctx.lineJoin = 'round';
285+
ctx.lineCap = 'round';
286+
ctx.strokeStyle = 'rgba(52,144,220,0.95)';
287+
ctx.shadowColor = 'rgba(52,144,220,0.7)';
288+
ctx.shadowBlur = 8;
289+
ctx.lineWidth = Math.max(4, cellSize * 0.45);
290+
ctx.beginPath();
291+
for (let i = 0; i < pathCells.length; i++) {
292+
const p = pathCells[i];
293+
const cx = p.x * cellSize + cellSize / 2;
294+
const cy = p.y * cellSize + cellSize / 2;
295+
if (i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
296+
}
297+
ctx.stroke();
298+
ctx.restore();
299+
}
300+
}
176301

177302
// --- Drawing & Animation ---
178303
function drawCell(cell, color) {
@@ -226,27 +351,30 @@ function drawPath(path) {
226351
i++;
227352
animationFrameId = setTimeout(animate, 20);
228353
} else {
229-
// Redraw start and end over the path
354+
// Redraw start and end over the path
230355
drawCell(grid[0][0], '#6ee7b7');
231-
drawCell(grid[size-1][size-1], '#f472b6');
356+
drawCell(grid[size - 1][size - 1], '#f472b6');
232357
}
233358
}
234359
animate();
235360
}
236361

237362
function clearPath() {
238363
cancelAnimationFrame(animationFrameId);
364+
pathCells = [];
239365
grid.forEach(row => row.forEach(cell => {
240366
cell.visited = false;
241-
delete cell.g; delete cell.h; delete cell.f;
367+
delete cell.g;
368+
delete cell.h;
369+
delete cell.f;
242370
}));
243371
nodesVisitedEl.textContent = 0;
244372
pathLengthEl.textContent = 0;
245373
timeTakenEl.textContent = '0ms';
374+
setStatus(drawMode ? 'Draw mode: ON — path cleared' : 'Path cleared');
246375
drawMaze();
247376
}
248377

249-
250378
// --- Event Listeners ---
251379
generateBtn.addEventListener('click', () => {
252380
cancelAnimationFrame(animationFrameId);
@@ -256,14 +384,31 @@ generateBtn.addEventListener('click', () => {
256384
solveBtn.addEventListener('click', solve);
257385
clearBtn.addEventListener('click', clearPath);
258386

387+
drawToggle.addEventListener('click', () => toggleDrawMode());
388+
drawToggle.addEventListener('keydown', (e) => {
389+
if (e.key === ' ' || e.key === 'Enter') {
390+
e.preventDefault();
391+
toggleDrawMode();
392+
}
393+
});
394+
395+
canvas.addEventListener('pointerdown', pointerDown);
396+
canvas.addEventListener('pointermove', pointerMove);
397+
window.addEventListener('pointerup', pointerUp);
398+
259399
mazeSizeSlider.addEventListener('input', (e) => {
260400
size = parseInt(e.target.value);
261401
mazeSizeValue.textContent = `${size}x${size}`;
262402
cellSize = canvas.width / size;
263403
cancelAnimationFrame(animationFrameId);
404+
pathCells = [];
264405
generateMaze();
265406
clearPath();
266407
});
267408

268409
// --- Initial Load ---
269-
generateMaze();
410+
generateMaze();
411+
setStatus('Ready');
412+
413+
// Expose for debugging
414+
window._maze = { grid, render, clearPath, toggleDrawMode };

projects/maze/styles.css

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
body {
1+
body {
22
font-family: system-ui, -apple-system, sans-serif;
33
background: #0f0f12;
44
color: #eef1f8;
@@ -29,8 +29,10 @@ canvas {
2929
border-radius: .5rem;
3030
width: 100%;
3131
height: auto;
32+
touch-action: none;
3233
}
3334

35+
/* === Controls === */
3436
.controls {
3537
background: #17171c;
3638
border: 1px solid #262631;
@@ -43,7 +45,8 @@ canvas {
4345
align-items: center;
4446
}
4547

46-
.control-group, .button-group {
48+
.control-group,
49+
.button-group {
4750
display: flex;
4851
align-items: center;
4952
gap: 0.5rem;
@@ -54,7 +57,24 @@ label {
5457
color: #a6adbb;
5558
}
5659

57-
select, button {
60+
.controls button {
61+
background: #23232a;
62+
color: #eef1f8;
63+
border: 1px solid #37373f;
64+
padding: .4rem .6rem;
65+
border-radius: .35rem;
66+
cursor: pointer;
67+
font-family: inherit;
68+
}
69+
70+
.controls button[aria-pressed="true"] {
71+
background: linear-gradient(90deg, #2b6cb0, #2b9cf0);
72+
color: #fff;
73+
border-color: #1f6fb5;
74+
}
75+
76+
select,
77+
button {
5878
background: #262631;
5979
color: #eef1f8;
6080
border: 1px solid #3a3a4a;
@@ -80,6 +100,23 @@ input[type="range"] {
80100
cursor: pointer;
81101
}
82102

103+
/* === Extra UI Elements === */
104+
.status {
105+
margin-left: .5rem;
106+
color: #9fb4c8;
107+
font-size: .95rem;
108+
}
109+
110+
.instructions {
111+
color: #9aa3b3;
112+
font-size: .9rem;
113+
margin-top: .5rem;
114+
}
115+
116+
.notes {
117+
color: #a6adbb;
118+
}
119+
83120
.metrics {
84121
color: #a6adbb;
85122
font-size: .9rem;
@@ -92,4 +129,4 @@ input[type="range"] {
92129

93130
.metrics strong {
94131
color: #eef1f8;
95-
}
132+
}

0 commit comments

Comments
 (0)