Skip to content

Commit 95d0dd9

Browse files
Merge pull request #184 from chandra-011220/maze
implemented maze-solver
2 parents fcc8578 + 833ca9a commit 95d0dd9

File tree

3 files changed

+380
-20
lines changed

3 files changed

+380
-20
lines changed

projects/maze/index.html

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
1-
<!doctype html>
1+
<!doctype html>
22
<html lang="en">
3-
43
<head>
54
<meta charset="utf-8">
6-
<meta name="viewport" content="width=device-width,initial-scale=1">
7-
<title>Maze</title>
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Maze Solver</title>
87
<link rel="stylesheet" href="./styles.css">
98
</head>
10-
119
<body>
1210
<main>
13-
<h1>Maze</h1><canvas id="maze" width="320" height="320"></canvas>
14-
<p class="notes">Contribute: generator, solver, keyboard navigation.</p>
11+
<h1>Maze Solver</h1>
12+
<div class="controls">
13+
<div class="control-group">
14+
<label for="algorithm">Algorithm:</label>
15+
<select id="algorithm">
16+
<option value="bfs">BFS (Breadth-First Search)</option>
17+
<option value="astar">A* Search</option>
18+
</select>
19+
</div>
20+
<div class="control-group">
21+
<label for="mazeSize">Size:</label>
22+
<input type="range" id="mazeSize" min="10" max="50" value="20">
23+
<span id="mazeSizeValue">20x20</span>
24+
</div>
25+
<div class="control-group">
26+
<label for="speed">Speed:</label>
27+
<input type="range" id="speed" min="1" max="100" value="50">
28+
</div>
29+
<div class="button-group">
30+
<button id="generateBtn">New Maze</button>
31+
<button id="solveBtn">Solve</button>
32+
<button id="clearBtn">Clear Path</button>
33+
</div>
34+
</div>
35+
<canvas id="maze" width="560" height="560"></canvas>
36+
<div class="metrics">
37+
<span>Nodes Visited: <strong id="nodes-visited">0</strong></span>
38+
<span>Path Length: <strong id="path-length">0</strong></span>
39+
<span>Time: <strong id="time-taken">0ms</strong></span>
40+
</div>
1541
</main>
1642
<script type="module" src="./main.js"></script>
1743
</body>
18-
1944
</html>

projects/maze/main.js

Lines changed: 269 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,269 @@
1-
const c = document.getElementById('maze'); const ctx = c.getContext('2d');
2-
// TODO: implement maze generation and basic player movement
3-
ctx.fillStyle = '#17171c'; ctx.fillRect(0, 0, c.width, c.height);
4-
ctx.fillStyle = '#6ee7b7'; ctx.fillRect(8, 8, 24, 24);
1+
const canvas = document.getElementById('maze');
2+
const ctx = canvas.getContext('2d');
3+
4+
// UI Elements
5+
const algorithmSelect = document.getElementById('algorithm');
6+
const mazeSizeSlider = document.getElementById('mazeSize');
7+
const mazeSizeValue = document.getElementById('mazeSizeValue');
8+
const speedSlider = document.getElementById('speed');
9+
const generateBtn = document.getElementById('generateBtn');
10+
const solveBtn = document.getElementById('solveBtn');
11+
const clearBtn = document.getElementById('clearBtn');
12+
13+
// Metrics
14+
const nodesVisitedEl = document.getElementById('nodes-visited');
15+
const pathLengthEl = document.getElementById('path-length');
16+
const timeTakenEl = document.getElementById('time-taken');
17+
18+
let size = 20;
19+
let cellSize = canvas.width / size;
20+
let grid = [];
21+
let animationFrameId;
22+
23+
// --- Maze Generation (Recursive Backtracker) ---
24+
function createGrid() {
25+
grid = [];
26+
for (let y = 0; y < size; y++) {
27+
let row = [];
28+
for (let x = 0; x < size; x++) {
29+
row.push({ x, y, walls: { top: true, right: true, bottom: true, left: true }, visited: false });
30+
}
31+
grid.push(row);
32+
}
33+
}
34+
35+
function generateMaze() {
36+
createGrid();
37+
let stack = [];
38+
let current = grid[0][0];
39+
current.visited = true;
40+
stack.push(current);
41+
42+
while (stack.length > 0) {
43+
current = stack.pop();
44+
let neighbors = getUnvisitedNeighbors(current.x, current.y);
45+
46+
if (neighbors.length > 0) {
47+
stack.push(current);
48+
let neighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
49+
removeWall(current, neighbor);
50+
neighbor.visited = true;
51+
stack.push(neighbor);
52+
}
53+
}
54+
// Reset visited for solver
55+
grid.forEach(row => row.forEach(cell => cell.visited = false));
56+
drawMaze();
57+
}
58+
59+
function getUnvisitedNeighbors(x, y) {
60+
const neighbors = [];
61+
if (y > 0 && !grid[y - 1][x].visited) neighbors.push(grid[y - 1][x]); // Top
62+
if (x < size - 1 && !grid[y][x + 1].visited) neighbors.push(grid[y][x + 1]); // Right
63+
if (y < size - 1 && !grid[y + 1][x].visited) neighbors.push(grid[y + 1][x]); // Bottom
64+
if (x > 0 && !grid[y][x - 1].visited) neighbors.push(grid[y][x - 1]); // Left
65+
return neighbors;
66+
}
67+
68+
function removeWall(a, b) {
69+
let x = a.x - b.x;
70+
if (x === 1) { a.walls.left = false; b.walls.right = false; }
71+
else if (x === -1) { a.walls.right = false; b.walls.left = false; }
72+
let y = a.y - b.y;
73+
if (y === 1) { a.walls.top = false; b.walls.bottom = false; }
74+
else if (y === -1) { a.walls.bottom = false; b.walls.top = false; }
75+
}
76+
77+
// --- Pathfinding Algorithms ---
78+
function solve() {
79+
cancelAnimationFrame(animationFrameId);
80+
clearPath();
81+
const startTime = performance.now();
82+
const algorithm = algorithmSelect.value === 'bfs' ? bfs : astar;
83+
const { visitedOrder, path } = algorithm();
84+
const endTime = performance.now();
85+
86+
timeTakenEl.textContent = `${Math.round(endTime - startTime)}ms`;
87+
animateSolution(visitedOrder, path);
88+
}
89+
90+
function bfs() {
91+
const start = grid[0][0];
92+
const end = grid[size - 1][size - 1];
93+
let queue = [start];
94+
start.visited = true;
95+
let visitedOrder = [start];
96+
let parentMap = new Map();
97+
98+
while (queue.length > 0) {
99+
const current = queue.shift();
100+
if (current === end) break;
101+
102+
getValidNeighbors(current).forEach(neighbor => {
103+
if (!neighbor.visited) {
104+
neighbor.visited = true;
105+
parentMap.set(neighbor, current);
106+
queue.push(neighbor);
107+
visitedOrder.push(neighbor);
108+
}
109+
});
110+
}
111+
return { visitedOrder, path: reconstructPath(parentMap, end) };
112+
}
113+
114+
function astar() {
115+
const start = grid[0][0];
116+
const end = grid[size - 1][size - 1];
117+
let openSet = [start];
118+
start.g = 0;
119+
start.h = heuristic(start, end);
120+
start.f = start.h;
121+
122+
let visitedOrder = [];
123+
let parentMap = new Map();
124+
125+
while (openSet.length > 0) {
126+
openSet.sort((a, b) => a.f - b.f);
127+
const current = openSet.shift();
128+
129+
visitedOrder.push(current);
130+
current.visited = true;
131+
132+
if (current === end) break;
133+
134+
getValidNeighbors(current).forEach(neighbor => {
135+
if (neighbor.visited) return;
136+
137+
const tentativeG = current.g + 1;
138+
if (tentativeG < (neighbor.g || Infinity)) {
139+
parentMap.set(neighbor, current);
140+
neighbor.g = tentativeG;
141+
neighbor.h = heuristic(neighbor, end);
142+
neighbor.f = neighbor.g + neighbor.h;
143+
if (!openSet.includes(neighbor)) {
144+
openSet.push(neighbor);
145+
}
146+
}
147+
});
148+
}
149+
return { visitedOrder, path: reconstructPath(parentMap, end) };
150+
}
151+
152+
function getValidNeighbors(cell) {
153+
const neighbors = [];
154+
const { x, y } = cell;
155+
if (!cell.walls.top && y > 0) neighbors.push(grid[y - 1][x]);
156+
if (!cell.walls.right && x < size - 1) neighbors.push(grid[y][x + 1]);
157+
if (!cell.walls.bottom && y < size - 1) neighbors.push(grid[y + 1][x]);
158+
if (!cell.walls.left && x > 0) neighbors.push(grid[y][x - 1]);
159+
return neighbors;
160+
}
161+
162+
function heuristic(a, b) { // Manhattan distance
163+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
164+
}
165+
166+
function reconstructPath(parentMap, end) {
167+
let path = [end];
168+
let current = end;
169+
while (parentMap.has(current)) {
170+
current = parentMap.get(current);
171+
path.unshift(current);
172+
}
173+
return path;
174+
}
175+
176+
177+
// --- Drawing & Animation ---
178+
function drawCell(cell, color) {
179+
ctx.fillStyle = color;
180+
ctx.fillRect(cell.x * cellSize + 1, cell.y * cellSize + 1, cellSize - 2, cellSize - 2);
181+
}
182+
183+
function drawMaze() {
184+
ctx.fillStyle = '#17171c';
185+
ctx.fillRect(0, 0, canvas.width, canvas.height);
186+
ctx.strokeStyle = '#3a3a4a';
187+
ctx.lineWidth = 2;
188+
189+
for (let y = 0; y < size; y++) {
190+
for (let x = 0; x < size; x++) {
191+
let cell = grid[y][x];
192+
if (cell.walls.top) { ctx.beginPath(); ctx.moveTo(x * cellSize, y * cellSize); ctx.lineTo((x + 1) * cellSize, y * cellSize); ctx.stroke(); }
193+
if (cell.walls.right) { ctx.beginPath(); ctx.moveTo((x + 1) * cellSize, y * cellSize); ctx.lineTo((x + 1) * cellSize, (y + 1) * cellSize); ctx.stroke(); }
194+
if (cell.walls.bottom) { ctx.beginPath(); ctx.moveTo((x + 1) * cellSize, (y + 1) * cellSize); ctx.lineTo(x * cellSize, (y + 1) * cellSize); ctx.stroke(); }
195+
if (cell.walls.left) { ctx.beginPath(); ctx.moveTo(x * cellSize, (y + 1) * cellSize); ctx.lineTo(x * cellSize, y * cellSize); ctx.stroke(); }
196+
}
197+
}
198+
// Draw start and end points
199+
drawCell(grid[0][0], '#6ee7b7'); // Start
200+
drawCell(grid[size - 1][size - 1], '#f472b6'); // End
201+
}
202+
203+
function animateSolution(visitedOrder, path) {
204+
let i = 0;
205+
const speed = 101 - speedSlider.value;
206+
207+
function animate() {
208+
if (i < visitedOrder.length) {
209+
drawCell(visitedOrder[i], '#3b82f6'); // Visited color
210+
nodesVisitedEl.textContent = i + 1;
211+
i++;
212+
animationFrameId = setTimeout(animate, speed / 5);
213+
} else {
214+
drawPath(path);
215+
}
216+
}
217+
animate();
218+
}
219+
220+
function drawPath(path) {
221+
let i = 0;
222+
function animate() {
223+
if (i < path.length) {
224+
drawCell(path[i], '#eab308'); // Path color
225+
pathLengthEl.textContent = i + 1;
226+
i++;
227+
animationFrameId = setTimeout(animate, 20);
228+
} else {
229+
// Redraw start and end over the path
230+
drawCell(grid[0][0], '#6ee7b7');
231+
drawCell(grid[size-1][size-1], '#f472b6');
232+
}
233+
}
234+
animate();
235+
}
236+
237+
function clearPath() {
238+
cancelAnimationFrame(animationFrameId);
239+
grid.forEach(row => row.forEach(cell => {
240+
cell.visited = false;
241+
delete cell.g; delete cell.h; delete cell.f;
242+
}));
243+
nodesVisitedEl.textContent = 0;
244+
pathLengthEl.textContent = 0;
245+
timeTakenEl.textContent = '0ms';
246+
drawMaze();
247+
}
248+
249+
250+
// --- Event Listeners ---
251+
generateBtn.addEventListener('click', () => {
252+
cancelAnimationFrame(animationFrameId);
253+
generateMaze();
254+
clearPath();
255+
});
256+
solveBtn.addEventListener('click', solve);
257+
clearBtn.addEventListener('click', clearPath);
258+
259+
mazeSizeSlider.addEventListener('input', (e) => {
260+
size = parseInt(e.target.value);
261+
mazeSizeValue.textContent = `${size}x${size}`;
262+
cellSize = canvas.width / size;
263+
cancelAnimationFrame(animationFrameId);
264+
generateMaze();
265+
clearPath();
266+
});
267+
268+
// --- Initial Load ---
269+
generateMaze();

0 commit comments

Comments
 (0)