Skip to content

Commit fd390ac

Browse files
committed
Added path to exit.
1 parent 45cf6cd commit fd390ac

File tree

3 files changed

+277
-5
lines changed

3 files changed

+277
-5
lines changed

projects/maze/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010

1111
<body>
1212
<main>
13-
<h1>Maze</h1><canvas id="maze" width="320" height="320"></canvas>
13+
<h1>Maze</h1>
14+
<div class="controls" role="region" aria-label="Maze controls">
15+
<button id="drawToggle" aria-pressed="false">Draw to Exit Mode</button>
16+
<button id="clearPath">Clear Path</button>
17+
<span id="status" aria-live="polite" class="status">Ready</span>
18+
</div>
19+
<canvas id="maze" width="320" height="320" tabindex="0"></canvas>
1420
<p class="notes">Contribute: generator, solver, keyboard navigation.</p>
21+
<p class="instructions">Instructions: Toggle "Draw to Exit Mode", then click/touch and drag across the maze to draw a path from the entrance (top-left) to the exit (bottom-right). Use Clear Path to reset.</p>
1522
</main>
1623
<script type="module" src="./main.js"></script>
1724
</body>

projects/maze/main.js

Lines changed: 233 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,233 @@
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+
// Maze configuration
5+
const cols = 16;
6+
const rows = 16;
7+
const cellSize = Math.floor(canvas.width / cols);
8+
9+
// grid: each cell has walls: top, right, bottom, left
10+
class Cell {
11+
constructor(x, y) {
12+
this.x = x; this.y = y;
13+
this.walls = { top: true, right: true, bottom: true, left: true };
14+
this.visited = false;
15+
}
16+
}
17+
18+
let grid = [];
19+
for (let y = 0; y < rows; y++) {
20+
const row = [];
21+
for (let x = 0; x < cols; x++) row.push(new Cell(x, y));
22+
grid.push(row);
23+
}
24+
25+
function index(x, y) {
26+
if (x < 0 || y < 0 || x >= cols || y >= rows) return null;
27+
return grid[y][x];
28+
}
29+
30+
// Recursive backtracker maze generator
31+
function generateMaze() {
32+
const stack = [];
33+
const start = grid[0][0];
34+
start.visited = true;
35+
stack.push(start);
36+
37+
while (stack.length) {
38+
const current = stack[stack.length - 1];
39+
const { x, y } = current;
40+
const neighbors = [];
41+
const dirs = [ [0,-1,'top','bottom'], [1,0,'right','left'], [0,1,'bottom','top'], [-1,0,'left','right'] ];
42+
for (const [dx,dy,wall,opp] of dirs) {
43+
const n = index(x+dx, y+dy);
44+
if (n && !n.visited) neighbors.push({cell:n,wall,opp});
45+
}
46+
if (neighbors.length) {
47+
const pick = neighbors[Math.floor(Math.random()*neighbors.length)];
48+
// remove wall between
49+
current.walls[pick.wall] = false;
50+
pick.cell.walls[pick.opp] = false;
51+
pick.cell.visited = true;
52+
stack.push(pick.cell);
53+
} else {
54+
stack.pop();
55+
}
56+
}
57+
// open entrance and exit
58+
grid[0][0].walls.left = false;
59+
grid[rows-1][cols-1].walls.right = false;
60+
}
61+
62+
function drawMaze() {
63+
ctx.clearRect(0,0,canvas.width,canvas.height);
64+
ctx.fillStyle = '#17171c';
65+
ctx.fillRect(0,0,canvas.width,canvas.height);
66+
67+
ctx.strokeStyle = '#9aa3b3';
68+
ctx.lineWidth = 2;
69+
for (let y=0;y<rows;y++){
70+
for (let x=0;x<cols;x++){
71+
const cell = grid[y][x];
72+
const sx = x * cellSize;
73+
const sy = y * cellSize;
74+
// draw walls
75+
if (cell.walls.top) {
76+
ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx+cellSize, sy); ctx.stroke();
77+
}
78+
if (cell.walls.right) {
79+
ctx.beginPath(); ctx.moveTo(sx+cellSize, sy); ctx.lineTo(sx+cellSize, sy+cellSize); ctx.stroke();
80+
}
81+
if (cell.walls.bottom) {
82+
ctx.beginPath(); ctx.moveTo(sx, sy+cellSize); ctx.lineTo(sx+cellSize, sy+cellSize); ctx.stroke();
83+
}
84+
if (cell.walls.left) {
85+
ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx, sy+cellSize); ctx.stroke();
86+
}
87+
}
88+
}
89+
90+
// mark entrance and exit
91+
ctx.fillStyle = '#6ee7b7';
92+
ctx.fillRect(1,1,cellSize-2,cellSize-2);
93+
ctx.fillStyle = '#f87171';
94+
ctx.fillRect((cols-1)*cellSize+1,(rows-1)*cellSize+1,cellSize-2,cellSize-2);
95+
}
96+
97+
// Drawing state
98+
let drawMode = false;
99+
let isDrawing = false;
100+
let pathCells = [];
101+
102+
const drawToggle = document.getElementById('drawToggle');
103+
const clearBtn = document.getElementById('clearPath');
104+
const status = document.getElementById('status');
105+
106+
function setStatus(text) { status.textContent = text; }
107+
108+
function toggleDrawMode(on) {
109+
drawMode = typeof on === 'boolean' ? on : !drawMode;
110+
drawToggle.setAttribute('aria-pressed', String(drawMode));
111+
drawToggle.classList.toggle('active', drawMode);
112+
setStatus(drawMode ? 'Draw mode: ON — draw a path' : 'Draw mode: OFF');
113+
}
114+
115+
function clearPath() {
116+
pathCells = [];
117+
setStatus(drawMode ? 'Draw mode: ON — draw cleared' : 'Path cleared');
118+
render();
119+
}
120+
121+
function cellFromEvent(e) {
122+
const rect = canvas.getBoundingClientRect();
123+
const px = (e.clientX - rect.left) * (canvas.width / rect.width);
124+
const py = (e.clientY - rect.top) * (canvas.height / rect.height);
125+
const cx = Math.floor(px / cellSize);
126+
const cy = Math.floor(py / cellSize);
127+
if (cx < 0 || cy < 0 || cx >= cols || cy >= rows) return null;
128+
return grid[cy][cx];
129+
}
130+
131+
function cellsAreNeighbors(a,b) {
132+
const dx = b.x - a.x, dy = b.y - a.y;
133+
if (dx === 1 && dy === 0) return ['right','left'];
134+
if (dx === -1 && dy === 0) return ['left','right'];
135+
if (dx === 0 && dy === 1) return ['bottom','top'];
136+
if (dx === 0 && dy === -1) return ['top','bottom'];
137+
return null;
138+
}
139+
140+
function pointerDown(e) {
141+
if (!drawMode) return;
142+
isDrawing = true;
143+
canvas.setPointerCapture(e.pointerId);
144+
const cell = cellFromEvent(e);
145+
if (cell) {
146+
pathCells = [cell];
147+
render();
148+
}
149+
}
150+
151+
function pointerMove(e) {
152+
if (!isDrawing) return;
153+
const cell = cellFromEvent(e);
154+
if (!cell) return;
155+
const last = pathCells[pathCells.length-1];
156+
if (!last || (last.x === cell.x && last.y === cell.y)) return;
157+
158+
// Check if move is valid (adjacent and no wall between)
159+
const neigh = cellsAreNeighbors(last, cell);
160+
if (!neigh) return; // not adjacent, skip
161+
const [fromSide, toSide] = neigh;
162+
if (last.walls[fromSide] || cell.walls[toSide]) {
163+
// Wall blocking, don't add this cell
164+
return;
165+
}
166+
167+
pathCells.push(cell);
168+
render();
169+
}function pointerUp(e) {
170+
if (isDrawing) {
171+
isDrawing = false;
172+
tryValidatePath();
173+
}
174+
try { canvas.releasePointerCapture(e.pointerId); } catch (err) {}
175+
}
176+
177+
function tryValidatePath() {
178+
if (!pathCells.length) { setStatus('No path drawn'); return; }
179+
const start = grid[0][0];
180+
const exit = grid[rows-1][cols-1];
181+
const first = pathCells[0];
182+
const last = pathCells[pathCells.length-1];
183+
if (first.x !== start.x || first.y !== start.y) { setStatus('Path must start at the entrance'); return; }
184+
if (last.x !== exit.x || last.y !== exit.y) { setStatus('Path must end at the exit'); return; }
185+
186+
// ensure each step moves to neighbor and there's no wall between
187+
for (let i=0;i<pathCells.length-1;i++){
188+
const a = pathCells[i], b = pathCells[i+1];
189+
const neigh = cellsAreNeighbors(a,b);
190+
if (!neigh) { setStatus('Invalid path: must travel between adjacent cells'); return; }
191+
const [fromSide, toSide] = neigh;
192+
if (a.walls[fromSide] || b.walls[toSide]) { setStatus('Invalid path: crosses a wall'); return; }
193+
}
194+
setStatus('Success! Path reaches the exit without crossing walls.');
195+
}
196+
197+
function render() {
198+
drawMaze();
199+
// overlay path
200+
if (pathCells.length) {
201+
ctx.save();
202+
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
203+
ctx.strokeStyle = 'rgba(52,144,220,0.95)';
204+
ctx.shadowColor = 'rgba(52,144,220,0.7)';
205+
ctx.shadowBlur = 8;
206+
ctx.lineWidth = Math.max(4, cellSize * 0.45);
207+
ctx.beginPath();
208+
for (let i=0;i<pathCells.length;i++){
209+
const p = pathCells[i];
210+
const cx = p.x * cellSize + cellSize/2;
211+
const cy = p.y * cellSize + cellSize/2;
212+
if (i===0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
213+
}
214+
ctx.stroke();
215+
ctx.restore();
216+
}
217+
}
218+
219+
// wire up controls and pointer events
220+
drawToggle.addEventListener('click', ()=> toggleDrawMode());
221+
drawToggle.addEventListener('keydown', (e)=>{ if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); toggleDrawMode(); }});
222+
clearBtn.addEventListener('click', clearPath);
223+
224+
canvas.addEventListener('pointerdown', pointerDown);
225+
canvas.addEventListener('pointermove', pointerMove);
226+
window.addEventListener('pointerup', pointerUp);
227+
228+
// initialize
229+
generateMaze();
230+
drawMaze();
231+
232+
// expose for debugging
233+
window._maze = { grid, render, clearPath, toggleDrawMode };

projects/maze/styles.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,42 @@ canvas {
1919
border-radius: .5rem
2020
}
2121

22+
.controls {
23+
display: flex;
24+
gap: .5rem;
25+
margin-bottom: .5rem;
26+
align-items: center;
27+
}
28+
29+
.controls button {
30+
background: #23232a;
31+
color: #eef1f8;
32+
border: 1px solid #37373f;
33+
padding: .4rem .6rem;
34+
border-radius: .35rem;
35+
cursor: pointer;
36+
}
37+
38+
.controls button[aria-pressed="true"] {
39+
background: linear-gradient(90deg,#2b6cb0,#2b9cf0);
40+
color: #fff;
41+
border-color: #1f6fb5;
42+
}
43+
44+
.status {
45+
margin-left: .5rem;
46+
color: #9fb4c8;
47+
font-size: .95rem;
48+
}
49+
50+
.instructions {
51+
color: #9aa3b3;
52+
font-size: .9rem;
53+
margin-top: .5rem;
54+
}
55+
56+
canvas { touch-action: none; }
57+
2258
.notes {
2359
color: #a6adbb;
2460
font-size: .9rem

0 commit comments

Comments
 (0)