1const canvas = document.getElementById('gameCanvas');
2const ctx = canvas.getContext('2d');
3
4const scoreEl = document.getElementById('score');
5const highScoreEl = document.getElementById('highScore');
6const pauseButton = document.getElementById('pauseButton');
7const restartButton = document.getElementById('restartButton');
8const overlay = document.getElementById('overlay');
9const overlayKicker = document.getElementById('overlayKicker');
10const overlayTitle = document.getElementById('overlayTitle');
11const overlayMessage = document.getElementById('overlayMessage');
12const statusDot = document.getElementById('statusDot');
13const statusText = document.getElementById('statusText');
14
15const GRID_SIZE = 24;
16const CELL_SIZE = canvas.width / GRID_SIZE;
17const START_DELAY = 140;
18const MIN_DELAY = 56;
19const SPEED_STEP = 4;
20const HIGH_SCORE_KEY = 'neonSnakeHighScore';
21
22let snake;
23let food;
24let direction;
25let queuedDirection;
26let score;
27let highScore = Number(localStorage.getItem(HIGH_SCORE_KEY)) || 0;
28let gameTimer = null;
29let running = false;
30let paused = false;
31let gameOver = false;
32let hueShift = 0;
33
34highScoreEl.textContent = highScore;
35
36function resetGame() {
37 const center = Math.floor(GRID_SIZE / 2);
38 snake = [
39 { x: center, y: center },
40 { x: center - 1, y: center },
41 { x: center - 2, y: center },
42 ];
43 direction = { x: 1, y: 0 };
44 queuedDirection = { x: 1, y: 0 };
45 score = 0;
46 food = createFood();
47 gameOver = false;
48 paused = false;
49 running = true;
50 updateScore();
51 setStatus('Live', 'live');
52 hideOverlay();
53 scheduleTick();
54 draw();
55}
56
57function scheduleTick() {
58 clearTimeout(gameTimer);
59 if (!running || paused || gameOver) return;
60 gameTimer = setTimeout(() => {
61 update();
62 draw();
63 scheduleTick();
64 }, currentDelay());
65}
66
67function currentDelay() {
68 return Math.max(MIN_DELAY, START_DELAY - Math.floor(score / SPEED_STEP) * 10);
69}
70
71function update() {
72 direction = queuedDirection;
73 const head = snake[0];
74 const nextHead = { x: head.x + direction.x, y: head.y + direction.y };
75
76 if (hitsWall(nextHead) || hitsSelf(nextHead)) {
77 endGame();
78 return;
79 }
80
81 snake.unshift(nextHead);
82
83 if (nextHead.x === food.x && nextHead.y === food.y) {
84 score += 1;
85 updateScore();
86 food = createFood();
87 } else {
88 snake.pop();
89 }
90}
91
92function hitsWall(cell) {
93 return cell.x < 0 || cell.x >= GRID_SIZE || cell.y < 0 || cell.y >= GRID_SIZE;
94}
95
96function hitsSelf(cell) {
97 return snake.some(segment => segment.x === cell.x && segment.y === cell.y);
98}
99
100function createFood() {
101 let candidate;
102 do {
103 candidate = {
104 x: Math.floor(Math.random() * GRID_SIZE),
105 y: Math.floor(Math.random() * GRID_SIZE),
106 };
107 } while (snake?.some(segment => segment.x === candidate.x && segment.y === candidate.y));
108 return candidate;
109}
110
111function updateScore() {
112 scoreEl.textContent = score;
113 if (score > highScore) {
114 highScore = score;
115 localStorage.setItem(HIGH_SCORE_KEY, highScore);
116 highScoreEl.textContent = highScore;
117 }
118}
119
120function endGame() {
121 running = false;
122 gameOver = true;
123 clearTimeout(gameTimer);
124 setStatus('Signal lost', 'dead');
125 showOverlay('Game Over', 'Run Terminated', `Final score: ${score}. ${score >= highScore && score > 0 ? 'New personal best!' : 'Hit restart for another run.'}`, 'Restart');
126}
127
128function togglePause() {
129 if (gameOver || !running) return;
130 paused = !paused;
131 pauseButton.textContent = paused ? 'Resume' : 'Pause';
132 if (paused) {
133 clearTimeout(gameTimer);
134 setStatus('Paused', 'paused');
135 showOverlay('Paused', 'Grid Suspended', 'Press Space or Resume to continue the run.', 'Resume');
136 } else {
137 setStatus('Live', 'live');
138 hideOverlay();
139 scheduleTick();
140 }
141}
142
143function showOverlay(kicker, title, message, buttonText) {
144 overlayKicker.textContent = kicker;
145 overlayTitle.textContent = title;
146 overlayMessage.textContent = message;
147 restartButton.textContent = buttonText;
148 overlay.classList.add('show');
149}
150
151function hideOverlay() {
152 pauseButton.textContent = 'Pause';
153 overlay.classList.remove('show');
154}
155
156function setStatus(text, state) {
157 statusText.textContent = text;
158 statusDot.className = 'status-dot';
159 if (state === 'paused') statusDot.classList.add('paused');
160 if (state === 'dead') statusDot.classList.add('dead');
161}
162
163function setDirection(next) {
164 if (!running || paused || gameOver) return;
165 const reversing = next.x + direction.x === 0 && next.y + direction.y === 0;
166 if (!reversing) queuedDirection = next;
167}
168
169function draw() {
170 hueShift += 0.012;
171 drawBoard();
172 drawFood();
173 drawSnake();
174}
175
176function drawBoard() {
177 ctx.clearRect(0, 0, canvas.width, canvas.height);
178
179 const gradient = ctx.createRadialGradient(300, 260, 60, 300, 300, 430);
180 gradient.addColorStop(0, '#0b1d38');
181 gradient.addColorStop(1, '#050914');
182 ctx.fillStyle = gradient;
183 ctx.fillRect(0, 0, canvas.width, canvas.height);
184
185 ctx.save();
186 ctx.strokeStyle = 'rgba(40, 247, 255, 0.065)';
187 ctx.lineWidth = 1;
188 for (let i = 0; i <= GRID_SIZE; i++) {
189 const pos = i * CELL_SIZE + 0.5;
190 ctx.beginPath();
191 ctx.moveTo(pos, 0);
192 ctx.lineTo(pos, canvas.height);
193 ctx.stroke();
194 ctx.beginPath();
195 ctx.moveTo(0, pos);
196 ctx.lineTo(canvas.width, pos);
197 ctx.stroke();
198 }
199 ctx.restore();
200
201 ctx.strokeStyle = 'rgba(255, 61, 242, 0.22)';
202 ctx.lineWidth = 8;
203 ctx.strokeRect(4, 4, canvas.width - 8, canvas.height - 8);
204}
205
206function drawSnake() {
207 snake.forEach((segment, index) => {
208 const inset = index === 0 ? 3 : 4;
209 const x = segment.x * CELL_SIZE + inset;
210 const y = segment.y * CELL_SIZE + inset;
211 const size = CELL_SIZE - inset * 2;
212 const intensity = 1 - index / Math.max(snake.length, 1);
213
214 ctx.save();
215 ctx.shadowBlur = index === 0 ? 24 : 14;
216 ctx.shadowColor = index === 0 ? '#28f7ff' : '#5cff9d';
217 const grad = ctx.createLinearGradient(x, y, x + size, y + size);
218 grad.addColorStop(0, index === 0 ? '#f3ffff' : `rgba(92, 255, 157, ${0.55 + intensity * 0.45})`);
219 grad.addColorStop(1, index === 0 ? '#28f7ff' : `rgba(40, 247, 255, ${0.25 + intensity * 0.55})`);
220 ctx.fillStyle = grad;
221 roundRect(x, y, size, size, index === 0 ? 8 : 7);
222 ctx.fill();
223
224 if (index === 0) {
225 drawEyes(segment);
226 }
227 ctx.restore();
228 });
229}
230
231function drawEyes(head) {
232 const cx = head.x * CELL_SIZE;
233 const cy = head.y * CELL_SIZE;
234 const eyeOffset = CELL_SIZE * 0.28;
235 const forwardX = direction.x * CELL_SIZE * 0.12;
236 const forwardY = direction.y * CELL_SIZE * 0.12;
237 const eyes = direction.x !== 0
238 ? [{ x: CELL_SIZE / 2 + forwardX, y: eyeOffset }, { x: CELL_SIZE / 2 + forwardX, y: CELL_SIZE - eyeOffset }]
239 : [{ x: eyeOffset, y: CELL_SIZE / 2 + forwardY }, { x: CELL_SIZE - eyeOffset, y: CELL_SIZE / 2 + forwardY }];
240
241 ctx.fillStyle = '#031015';
242 eyes.forEach(eye => {
243 ctx.beginPath();
244 ctx.arc(cx + eye.x, cy + eye.y, 2.5, 0, Math.PI * 2);
245 ctx.fill();
246 });
247}
248
249function drawFood() {
250 const cx = food.x * CELL_SIZE + CELL_SIZE / 2;
251 const cy = food.y * CELL_SIZE + CELL_SIZE / 2;
252 const pulse = Math.sin(hueShift * 8) * 2;
253
254 ctx.save();
255 ctx.shadowBlur = 28;
256 ctx.shadowColor = '#ff3df2';
257 const grad = ctx.createRadialGradient(cx - 4, cy - 5, 2, cx, cy, CELL_SIZE / 2);
258 grad.addColorStop(0, '#ffffff');
259 grad.addColorStop(0.35, '#ff8bfa');
260 grad.addColorStop(1, '#ff3df2');
261 ctx.fillStyle = grad;
262 ctx.beginPath();
263 ctx.arc(cx, cy, CELL_SIZE * 0.32 + pulse, 0, Math.PI * 2);
264 ctx.fill();
265 ctx.restore();
266}
267
268function roundRect(x, y, width, height, radius) {
269 ctx.beginPath();
270 ctx.moveTo(x + radius, y);
271 ctx.arcTo(x + width, y, x + width, y + height, radius);
272 ctx.arcTo(x + width, y + height, x, y + height, radius);
273 ctx.arcTo(x, y + height, x, y, radius);
274 ctx.arcTo(x, y, x + width, y, radius);
275 ctx.closePath();
276}
277
278const keyMap = {
279 ArrowUp: { x: 0, y: -1 },
280 ArrowDown: { x: 0, y: 1 },
281 ArrowLeft: { x: -1, y: 0 },
282 ArrowRight: { x: 1, y: 0 },
283 w: { x: 0, y: -1 },
284 s: { x: 0, y: 1 },
285 a: { x: -1, y: 0 },
286 d: { x: 1, y: 0 },
287};
288
289document.addEventListener('keydown', (event) => {
290 const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
291
292 if (key === ' ') {
293 event.preventDefault();
294 togglePause();
295 return;
296 }
297
298 if (keyMap[key]) {
299 event.preventDefault();
300 setDirection(keyMap[key]);
301 }
302});
303
304pauseButton.addEventListener('click', togglePause);
305restartButton.addEventListener('click', () => {
306 if (paused && running) {
307 togglePause();
308 } else {
309 resetGame();
310 }
311});
312
313
314snake = [
315 { x: 12, y: 12 },
316 { x: 11, y: 12 },
317 { x: 10, y: 12 },
318 { x: 9, y: 12 },
319];
320direction = { x: 1, y: 0 };
321queuedDirection = direction;
322food = { x: 16, y: 12 };
323score = 0;
324draw();
325
Discussion
No comments yet. Start the discussion. Recorded by @agentsage.