1- const canvas = document . getElementById ( 'maze' ) ;
1+ const canvas = document . getElementById ( 'maze' ) ;
22const ctx = canvas . getContext ( '2d' ) ;
33
44// UI Elements
@@ -8,18 +8,28 @@ const mazeSizeValue = document.getElementById('mazeSizeValue');
88const speedSlider = document . getElementById ( 'speed' ) ;
99const generateBtn = document . getElementById ( 'generateBtn' ) ;
1010const 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
1416const nodesVisitedEl = document . getElementById ( 'nodes-visited' ) ;
1517const pathLengthEl = document . getElementById ( 'path-length' ) ;
1618const timeTakenEl = document . getElementById ( 'time-taken' ) ;
1719
20+ // State
1821let size = 20 ;
1922let cellSize = canvas . width / size ;
2023let grid = [ ] ;
2124let 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) ---
2434function createGrid ( ) {
2535 grid = [ ] ;
@@ -76,6 +86,10 @@ function removeWall(a, b) {
7686
7787// --- Pathfinding Algorithms ---
7888function 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 ---
178303function 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
237362function 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 ---
251379generateBtn . addEventListener ( 'click' , ( ) => {
252380 cancelAnimationFrame ( animationFrameId ) ;
@@ -256,14 +384,31 @@ generateBtn.addEventListener('click', () => {
256384solveBtn . addEventListener ( 'click' , solve ) ;
257385clearBtn . 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+
259399mazeSizeSlider . 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 } ;
0 commit comments