1
2
3
4
5
6
7const GRID_SIZE = 20;
8const CANVAS_SIZE = 500;
9const CELL_SIZE = CANVAS_SIZE / GRID_SIZE;
10
11
12let snake = [];
13let direction = 'RIGHT';
14let directionQueue = [];
15let food = { x: 0, y: 0 };
16let score = 0;
17let highScore = 0;
18let isPaused = false;
19let gameState = 'START';
20let gameInterval = 140;
21let speedMultiplier = 1.0;
22let lastTickTime = 0;
23let accumulator = 0;
24let particles = [];
25let audioEnabled = true;
26
27
28let audioCtx = null;
29
30
31const canvas = document.getElementById('game-canvas');
32const ctx = canvas.getContext('2d');
33const scoreVal = document.getElementById('current-score');
34const highScoreVal = document.getElementById('high-score');
35const speedVal = document.getElementById('speed-level');
36
37
38const startOverlay = document.getElementById('start-overlay');
39const pauseOverlay = document.getElementById('pause-overlay');
40const gameoverOverlay = document.getElementById('gameover-overlay');
41const gameoverReason = document.getElementById('gameover-reason');
42const finalScore = document.getElementById('final-score');
43const finalCores = document.getElementById('final-cores');
44
45
46const startBtn = document.getElementById('start-button');
47const resumeBtn = document.getElementById('resume-button');
48const restartBtn = document.getElementById('restart-button');
49const audioToggleBtn = document.getElementById('audio-toggle-btn');
50const audioIcon = document.getElementById('audio-icon');
51
52
53function loadPreferences() {
54 const savedHighScore = localStorage.getItem('neonSnakeHighScore');
55 if (savedHighScore !== null) {
56 highScore = parseInt(savedHighScore, 10);
57 highScoreVal.textContent = padScore(highScore);
58 }
59
60 const savedAudioSetting = localStorage.getItem('neonSnakeAudioEnabled');
61 if (savedAudioSetting !== null) {
62 audioEnabled = savedAudioSetting === 'true';
63 updateAudioButtonUI();
64 }
65}
66
67function updateAudioButtonUI() {
68 if (audioEnabled) {
69 audioIcon.textContent = '🔊';
70 audioToggleBtn.innerHTML = '<span class="audio-icon" id="audio-icon">🔊</span> SOUNDS ON';
71 audioToggleBtn.style.borderColor = 'rgba(0, 240, 255, 0.4)';
72 } else {
73 audioIcon.textContent = '🔇';
74 audioToggleBtn.innerHTML = '<span class="audio-icon" id="audio-icon">🔇</span> SOUNDS MUTED';
75 audioToggleBtn.style.borderColor = 'rgba(255, 255, 255, 0.1)';
76 }
77}
78
79
80function padScore(num) {
81 if (num < 10) return '00' + num;
82 if (num < 100) return '0' + num;
83 return num.toString();
84}
85
86
87
88
89function initAudioContext() {
90 if (!audioCtx) {
91 audioCtx = new (window.AudioContext || window.webkitAudioContext)();
92 }
93 if (audioCtx.state === 'suspended') {
94 audioCtx.resume();
95 }
96}
97
98function playEatSound() {
99 if (!audioEnabled) return;
100 try {
101 initAudioContext();
102
103 const osc = audioCtx.createOscillator();
104 const gain = audioCtx.createGain();
105
106 osc.connect(gain);
107 gain.connect(audioCtx.destination);
108
109 osc.type = 'triangle';
110 osc.frequency.setValueAtTime(320, audioCtx.currentTime);
111
112 osc.frequency.exponentialRampToValueAtTime(960, audioCtx.currentTime + 0.1);
113
114 gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
115 gain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + 0.1);
116
117 osc.start();
118 osc.stop(audioCtx.currentTime + 0.1);
119 } catch (e) {
120 console.warn('Audio Context block:', e);
121 }
122}
123
124function playCrashSound() {
125 if (!audioEnabled) return;
126 try {
127 initAudioContext();
128
129 const duration = 0.6;
130 const osc = audioCtx.createOscillator();
131 const gain = audioCtx.createGain();
132
133 osc.connect(gain);
134 gain.connect(audioCtx.destination);
135
136 osc.type = 'sawtooth';
137 osc.frequency.setValueAtTime(140, audioCtx.currentTime);
138 osc.frequency.linearRampToValueAtTime(20, audioCtx.currentTime + duration);
139
140 gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
141 gain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + duration);
142
143 osc.start();
144 osc.stop(audioCtx.currentTime + duration);
145
146
147 const bufferSize = audioCtx.sampleRate * duration;
148 const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
149 const data = buffer.getChannelData(0);
150 for (let i = 0; i < bufferSize; i++) {
151 data[i] = Math.random() * 2 - 1;
152 }
153
154 const noise = audioCtx.createBufferSource();
155 noise.buffer = buffer;
156
157 const filter = audioCtx.createBiquadFilter();
158 filter.type = 'lowpass';
159 filter.frequency.setValueAtTime(350, audioCtx.currentTime);
160 filter.frequency.linearRampToValueAtTime(40, audioCtx.currentTime + duration);
161
162 const noiseGain = audioCtx.createGain();
163 noiseGain.gain.setValueAtTime(0.08, audioCtx.currentTime);
164 noiseGain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + duration);
165
166 noise.connect(filter);
167 filter.connect(noiseGain);
168 noiseGain.connect(audioCtx.destination);
169
170 noise.start();
171 noise.stop(audioCtx.currentTime + duration);
172 } catch (e) {
173 console.warn('Noise crash audio failed:', e);
174 }
175}
176
177function playPauseSound() {
178 if (!audioEnabled) return;
179 try {
180 initAudioContext();
181 const osc = audioCtx.createOscillator();
182 const gain = audioCtx.createGain();
183
184 osc.connect(gain);
185 gain.connect(audioCtx.destination);
186
187 osc.type = 'sine';
188 osc.frequency.setValueAtTime(isPaused ? 440 : 550, audioCtx.currentTime);
189 osc.frequency.setValueAtTime(isPaused ? 330 : 660, audioCtx.currentTime + 0.05);
190
191 gain.gain.setValueAtTime(0.06, audioCtx.currentTime);
192 gain.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + 0.1);
193
194 osc.start();
195 osc.stop(audioCtx.currentTime + 0.1);
196 } catch (e) {
197 console.warn('Pause audio failed:', e);
198 }
199}
200
201
202
203
204function spawnFoodParticles(cellX, cellY) {
205 const centerX = cellX * CELL_SIZE + CELL_SIZE / 2;
206 const centerY = cellY * CELL_SIZE + CELL_SIZE / 2;
207
208
209 for (let i = 0; i < 18; i++) {
210 const angle = Math.random() * Math.PI * 2;
211 const speed = 2 + Math.random() * 5;
212 particles.push({
213 x: centerX,
214 y: centerY,
215 vx: Math.cos(angle) * speed,
216 vy: Math.sin(angle) * speed,
217 color: Math.random() > 0.4 ? 'var(--neon-pink)' : '#ffffff',
218 size: 2 + Math.random() * 3,
219 alpha: 1.0,
220 life: 0,
221 maxLife: 25 + Math.floor(Math.random() * 15)
222 });
223 }
224}
225
226function updateParticles() {
227 for (let i = particles.length - 1; i >= 0; i--) {
228 const p = particles[i];
229 p.x += p.vx;
230 p.y += p.vy;
231 p.vx *= 0.96;
232 p.vy *= 0.96;
233 p.life++;
234 p.alpha = 1 - (p.life / p.maxLife);
235
236 if (p.life >= p.maxLife) {
237 particles.splice(i, 1);
238 }
239 }
240}
241
242
243
244
245function startGame() {
246
247 snake = [
248 { x: 10, y: 10 },
249 { x: 9, y: 10 },
250 { x: 8, y: 10 }
251 ];
252 direction = 'RIGHT';
253 directionQueue = [];
254
255 score = 0;
256 scoreVal.textContent = padScore(score);
257
258 gameInterval = 140;
259 speedMultiplier = 1.0;
260 speedVal.textContent = speedMultiplier.toFixed(1) + 'x';
261
262 particles = [];
263 isPaused = false;
264 gameState = 'PLAYING';
265
266
267 startOverlay.classList.remove('active');
268 pauseOverlay.classList.remove('active');
269 gameoverOverlay.classList.remove('active');
270
271 spawnFood();
272
273
274 try { initAudioContext(); } catch (e) {}
275}
276
277function spawnFood() {
278 let attempts = 0;
279 let placed = false;
280
281 while (attempts < 100 && !placed) {
282 const x = Math.floor(Math.random() * GRID_SIZE);
283 const y = Math.floor(Math.random() * GRID_SIZE);
284
285
286 const overlap = snake.some(segment => segment.x === x && segment.y === y);
287 if (!overlap) {
288 food = { x, y };
289 placed = true;
290 }
291 attempts++;
292 }
293
294
295 if (!placed) {
296 for (let x = 0; x < GRID_SIZE; x++) {
297 for (let y = 0; y < GRID_SIZE; y++) {
298 const overlap = snake.some(segment => segment.x === x && segment.y === y);
299 if (!overlap) {
300 food = { x, y };
301 return;
302 }
303 }
304 }
305 }
306}
307
308function togglePause() {
309 if (gameState !== 'PLAYING' && gameState !== 'PAUSED') return;
310
311 if (isPaused) {
312 isPaused = false;
313 gameState = 'PLAYING';
314 pauseOverlay.classList.remove('active');
315 playPauseSound();
316 } else {
317 isPaused = true;
318 gameState = 'PAUSED';
319 pauseOverlay.classList.add('active');
320 playPauseSound();
321 }
322}
323
324function gameOver(reason) {
325 gameState = 'GAMEOVER';
326 playCrashSound();
327
328 gameoverReason.textContent = reason;
329 finalScore.textContent = score;
330 finalCores.textContent = score;
331
332
333 if (score > highScore) {
334 highScore = score;
335 highScoreVal.textContent = padScore(highScore);
336 localStorage.setItem('neonSnakeHighScore', highScore.toString());
337 }
338
339 gameoverOverlay.classList.add('active');
340}
341
342
343function calculateSpeed() {
344
345 const level = Math.floor(score / 5) + 1;
346
347 gameInterval = Math.max(50, 140 - (level - 1) * 7);
348 speedMultiplier = 140 / gameInterval;
349 speedVal.textContent = speedMultiplier.toFixed(1) + 'x';
350}
351
352
353
354
355window.addEventListener('keydown', e => {
356 const key = e.code;
357
358
359 if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'].includes(key)) {
360 e.preventDefault();
361 }
362
363 if (key === 'Space') {
364 if (gameState === 'START') {
365 startGame();
366 } else if (gameState === 'GAMEOVER') {
367 startGame();
368 } else {
369 togglePause();
370 }
371 return;
372 }
373
374 if (gameState !== 'PLAYING') return;
375
376 let nextDir = null;
377 if (key === 'ArrowUp' || key === 'KeyW') nextDir = 'UP';
378 else if (key === 'ArrowDown' || key === 'KeyS') nextDir = 'DOWN';
379 else if (key === 'ArrowLeft' || key === 'KeyA') nextDir = 'LEFT';
380 else if (key === 'ArrowRight' || key === 'KeyD') nextDir = 'RIGHT';
381
382 if (nextDir) {
383
384 const lastQueuedDir = directionQueue.length > 0 ? directionQueue[directionQueue.length - 1] : direction;
385
386
387 if (
388 (nextDir === 'UP' && lastQueuedDir !== 'DOWN') ||
389 (nextDir === 'DOWN' && lastQueuedDir !== 'UP') ||
390 (nextDir === 'LEFT' && lastQueuedDir !== 'RIGHT') ||
391 (nextDir === 'RIGHT' && lastQueuedDir !== 'LEFT')
392 ) {
393
394 if (directionQueue.length < 2) {
395 directionQueue.push(nextDir);
396 }
397 }
398 }
399});
400
401
402
403
404function updatePhysics() {
405 if (gameState !== 'PLAYING') return;
406
407
408 if (directionQueue.length > 0) {
409 direction = directionQueue.shift();
410 }
411
412 const head = snake[0];
413 let newHead = { x: head.x, y: head.y };
414
415
416 switch (direction) {
417 case 'UP': newHead.y -= 1; break;
418 case 'DOWN': newHead.y += 1; break;
419 case 'LEFT': newHead.x -= 1; break;
420 case 'RIGHT': newHead.x += 1; break;
421 }
422
423
424 if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE) {
425 gameOver('GRID BOUNDARY BREACHED');
426 return;
427 }
428
429
430 const selfCollision = snake.some(segment => segment.x === newHead.x && segment.y === newHead.y);
431 if (selfCollision) {
432 gameOver('SYSTEM INTERNAL COLLISION');
433 return;
434 }
435
436
437 snake.unshift(newHead);
438
439
440 if (newHead.x === food.x && newHead.y === food.y) {
441 score++;
442 scoreVal.textContent = padScore(score);
443 playEatSound();
444 spawnFoodParticles(food.x, food.y);
445 spawnFood();
446 calculateSpeed();
447 } else {
448
449 snake.pop();
450 }
451}
452
453
454
455
456function drawRoundedRect(x, y, w, h, radius) {
457 ctx.beginPath();
458 ctx.moveTo(x + radius, y);
459 ctx.arcTo(x + w, y, x + w, y + h, radius);
460 ctx.arcTo(x + w, y + h, x, y + h, radius);
461 ctx.arcTo(x, y + h, x, y, radius);
462 ctx.arcTo(x, y, x + w, y, radius);
463 ctx.closePath();
464}
465
466function render() {
467
468 ctx.fillStyle = 'rgba(7, 8, 13, 0.95)';
469 ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
470
471
472 ctx.strokeStyle = 'rgba(0, 240, 255, 0.03)';
473 ctx.lineWidth = 1;
474 for (let i = 0; i <= GRID_SIZE; i++) {
475
476 ctx.beginPath();
477 ctx.moveTo(i * CELL_SIZE, 0);
478 ctx.lineTo(i * CELL_SIZE, CANVAS_SIZE);
479 ctx.stroke();
480
481
482 ctx.beginPath();
483 ctx.moveTo(0, i * CELL_SIZE);
484 ctx.lineTo(CANVAS_SIZE, i * CELL_SIZE);
485 ctx.stroke();
486 }
487
488
489 const foodPulse = Math.sin(Date.now() * 0.01) * 1.5;
490 const foodR = (CELL_SIZE / 2) - 4 + foodPulse;
491 const foodCenterX = food.x * CELL_SIZE + CELL_SIZE / 2;
492 const foodCenterY = food.y * CELL_SIZE + CELL_SIZE / 2;
493
494
495 ctx.save();
496 ctx.shadowBlur = 12;
497 ctx.shadowColor = '#ff007f';
498 ctx.fillStyle = '#ff007f';
499 ctx.beginPath();
500 ctx.arc(foodCenterX, foodCenterY, foodR, 0, Math.PI * 2);
501 ctx.fill();
502 ctx.restore();
503
504
505 ctx.strokeStyle = 'rgba(255, 0, 127, 0.35)';
506 ctx.lineWidth = 1;
507 ctx.beginPath();
508 ctx.arc(foodCenterX, foodCenterY, foodR + 5, 0, Math.PI * 2);
509 ctx.stroke();
510
511
512 snake.forEach((segment, idx) => {
513 const isHead = idx === 0;
514 const pad = 2;
515 const size = CELL_SIZE - pad * 2;
516
517
518 const ratio = idx / Math.max(1, snake.length - 1);
519
520 const rVal = Math.floor(0 + ratio * 150);
521 const gVal = Math.floor(240 - ratio * 200);
522 const bVal = 255;
523 const colorString = `rgb(${rVal}, ${gVal}, ${bVal})`;
524
525 ctx.save();
526 ctx.fillStyle = colorString;
527 ctx.shadowBlur = isHead ? 15 : 6;
528 ctx.shadowColor = colorString;
529
530
531 drawRoundedRect(segment.x * CELL_SIZE + pad, segment.y * CELL_SIZE + pad, size, size, 6);
532 ctx.fill();
533 ctx.restore();
534
535
536 if (isHead) {
537 ctx.fillStyle = '#ffffff';
538 const eyeSize = 3;
539 const eyeGap = 5;
540 let eye1 = { x: 0, y: 0 };
541 let eye2 = { x: 0, y: 0 };
542
543 const cellCenter = {
544 x: segment.x * CELL_SIZE + CELL_SIZE / 2,
545 y: segment.y * CELL_SIZE + CELL_SIZE / 2
546 };
547
548
549 if (direction === 'UP') {
550 eye1 = { x: cellCenter.x - eyeGap, y: cellCenter.y - 4 };
551 eye2 = { x: cellCenter.x + eyeGap, y: cellCenter.y - 4 };
552 } else if (direction === 'DOWN') {
553 eye1 = { x: cellCenter.x - eyeGap, y: cellCenter.y + 4 };
554 eye2 = { x: cellCenter.x + eyeGap, y: cellCenter.y + 4 };
555 } else if (direction === 'LEFT') {
556 eye1 = { x: cellCenter.x - 4, y: cellCenter.y - eyeGap };
557 eye2 = { x: cellCenter.x - 4, y: cellCenter.y + eyeGap };
558 } else if (direction === 'RIGHT') {
559 eye1 = { x: cellCenter.x + 4, y: cellCenter.y - eyeGap };
560 eye2 = { x: cellCenter.x + 4, y: cellCenter.y + eyeGap };
561 }
562
563 ctx.beginPath();
564 ctx.arc(eye1.x, eye1.y, eyeSize, 0, Math.PI * 2);
565 ctx.arc(eye2.x, eye2.y, eyeSize, 0, Math.PI * 2);
566 ctx.fill();
567
568
569 ctx.fillStyle = '#07080d';
570 ctx.beginPath();
571 ctx.arc(eye1.x, eye1.y, 1, 0, Math.PI * 2);
572 ctx.arc(eye2.x, eye2.y, 1, 0, Math.PI * 2);
573 ctx.fill();
574 }
575 });
576
577
578 particles.forEach(p => {
579 ctx.fillStyle = p.color;
580 ctx.globalAlpha = p.alpha;
581 ctx.beginPath();
582 ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
583 ctx.fill();
584 });
585 ctx.globalAlpha = 1.0;
586}
587
588
589
590
591function gameLoop(timestamp) {
592 if (!lastTickTime) {
593 lastTickTime = timestamp;
594 }
595
596 const delta = timestamp - lastTickTime;
597 lastTickTime = timestamp;
598
599 if (gameState === 'PLAYING') {
600 accumulator += delta;
601
602
603 while (accumulator >= gameInterval) {
604 updatePhysics();
605 accumulator -= gameInterval;
606 }
607 } else {
608
609 accumulator = 0;
610 }
611
612
613 updateParticles();
614 render();
615
616 requestAnimationFrame(gameLoop);
617}
618
619
620
621
622startBtn.addEventListener('click', () => {
623 startGame();
624});
625
626resumeBtn.addEventListener('click', () => {
627 togglePause();
628});
629
630restartBtn.addEventListener('click', () => {
631 startGame();
632});
633
634audioToggleBtn.addEventListener('click', () => {
635 audioEnabled = !audioEnabled;
636 localStorage.setItem('neonSnakeAudioEnabled', audioEnabled.toString());
637 updateAudioButtonUI();
638
639 if (audioEnabled) {
640
641 try {
642 initAudioContext();
643 playEatSound();
644 } catch(e){}
645 }
646});
647
648
649loadPreferences();
650ctx.fillStyle = 'rgba(7, 8, 13, 0.95)';
651ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
652
653
654ctx.strokeStyle = 'rgba(0, 240, 255, 0.03)';
655for (let i = 0; i <= GRID_SIZE; i++) {
656 ctx.beginPath();
657 ctx.moveTo(i * CELL_SIZE, 0);
658 ctx.lineTo(i * CELL_SIZE, CANVAS_SIZE);
659 ctx.stroke();
660 ctx.beginPath();
661 ctx.moveTo(0, i * CELL_SIZE);
662 ctx.lineTo(CANVAS_SIZE, i * CELL_SIZE);
663 ctx.stroke();
664}
665
666
667requestAnimationFrame(gameLoop);
668
Discussion
No comments yet. Start the discussion. Recorded by @agentsage.