index.html (18.4 KB)


  1 <!DOCTYPE html>
  2 <html>
  3 <head>
  4 	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  5 	<meta charset="UTF-8">
  6 	<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍎</text></svg>">
  7 	<title>SNAKE</title>
  8 	<style>
  9 		* {
 10 			box-sizing: border-box;
 11 			user-select: none;
 12 		}
 13 		
 14 		body {
 15 			background-color: white;
 16 			font-family: Arial, sans-serif;
 17 			text-align: center;
 18 			margin: 0;
 19 			padding: 20px;
 20 			overflow-x: hidden;
 21 		}
 22 		
 23 		h1 {
 24 			margin: 20px 0;
 25 		}
 26 		
 27 		#gameBoard {
 28 			display: inline-flex;
 29 			flex-direction: column;
 30 			border: 2px solid #333;
 31 			background-color: lightgrey;
 32 			line-height: 1;
 33 			font-family: monospace;
 34 			overflow: hidden;
 35 		}
 36 		
 37 		.row {
 38 			display: flex;
 39 			margin: 0;
 40 			padding: 0;
 41 			gap: 0;
 42 		}
 43 		
 44 		.cell {
 45 			display: flex;
 46 			align-items: center;
 47 			justify-content: center;
 48 			flex-shrink: 0;
 49 			margin: 0;
 50 			padding: 0;
 51 		}
 52 		
 53 		#score, #highScore {
 54 			font-size: 20px;
 55 			margin: 10px 0;
 56 		}
 57 		
 58 		#gameOver, #startHint {
 59 			font-size: 24px;
 60 			color: red;
 61 			margin: 20px 0;
 62 		}
 63 		
 64 		#startHint {
 65 			color: #333;
 66 		}
 67 		
 68 		#mobileControls {
 69 			display: none;
 70 			margin: 20px 0;
 71 		}
 72 		
 73 		.control-pad {
 74 			display: inline-flex;
 75 			flex-direction: column;
 76 			gap: 0;
 77 		}
 78 		
 79 		.control-row {
 80 			display: flex;
 81 			gap: 0;
 82 		}
 83 		
 84 		.control-cell {
 85 			width: 48px;
 86 			height: 48px;
 87 			display: flex;
 88 			align-items: center;
 89 			justify-content: center;
 90 		}
 91 		
 92 		.control-btn {
 93 			width: 100%;
 94 			height: 100%;
 95 			font-size: 48px;
 96 			cursor: pointer;
 97 			user-select: none;
 98 			touch-action: manipulation;
 99 			display: flex;
100 			align-items: center;
101 			justify-content: center;
102 			border: none;
103 			background: none;
104 			position: relative;
105 		}
106 		
107 		.control-btn:active {
108 			opacity: 0.6;
109 		}
110 		
111 		.control-btn.hidden {
112 			visibility: hidden;
113 		}
114 		
115 		.control-btn.show {
116 			visibility: visible;
117 		}
118 		
119 		#centerBtn {
120 			z-index: 10;
121 		}
122 		
123 		#mobileGameOver, #mobileStartHint {
124 			font-size: 20px;
125 			color: red;
126 			margin-top: 20px;
127 			display: none;
128 		}
129 		
130 		#mobileStartHint {
131 			color: #333;
132 		}
133 		
134 		@media (max-width: 400px) {
135 			body {
136 				padding: 10px;
137 			}
138 			
139 			h1 {
140 				font-size: 24px;
141 				margin: 10px 0;
142 			}
143 			
144 			#score, #highScore {
145 				font-size: 16px;
146 			}
147 		}
148 		
149 		@media (max-width: 360px) {
150 			body {
151 				padding: 5px;
152 			}
153 			
154 			h1 {
155 				font-size: 20px;
156 			}
157 			
158 			#score, #highScore {
159 				font-size: 14px;
160 			}
161 		}
162 		
163 		@media (pointer: none), (pointer: coarse) {
164 			#mobileControls {
165 				display: block;
166 			}
167 			
168 			#gameOver {
169 				display: none !important;
170 			}
171 			
172 			#startHint {
173 				display: none !important;
174 			}
175 		}
176 		
177 		@media (prefers-color-scheme: dark) {
178 			body {
179 				background-color: #1a1a1a;
180 				color: #ffffff;
181 			}
182 			
183 			#gameBoard {
184 				border-color: #666;
185 				background-color: #2d2d2d;
186 			}
187 			
188 			#startHint, #mobileStartHint {
189 				color: #ccc;
190 			}
191 		}
192 	</style>
193 </head>
194 <body>
195 	<h1>SNAKE</h1>
196 	<div id="gameBoard"></div>
197 	<div id="score">Score: 0</div>
198 	<div id="highScore">High Score: 0</div>
199 	<div id="gameOver"></div>
200 	<div id="startHint">Press Space to start!</div>
201 	
202 	<div id="mobileControls">
203 		<div class="control-pad">
204 			<div class="control-row">
205 				<div class="control-cell"><div class="control-btn hidden"></div></div>
206 				<div class="control-cell"><button class="control-btn" id="upBtn">🔼</button></div>
207 				<div class="control-cell"><div class="control-btn hidden"></div></div>
208 			</div>
209 			<div class="control-row">
210 				<div class="control-cell"><button class="control-btn" id="leftBtn">◀️</button></div>
211 				<div class="control-cell"><button class="control-btn show" id="centerBtn">🟢</button></div>
212 				<div class="control-cell"><button class="control-btn" id="rightBtn">▶️</button></div>
213 			</div>
214 			<div class="control-row">
215 				<div class="control-cell"><div class="control-btn hidden"></div></div>
216 				<div class="control-cell"><button class="control-btn" id="downBtn">🔽</button></div>
217 				<div class="control-cell"><div class="control-btn hidden"></div></div>
218 			</div>
219 		</div>
220 		<div id="mobileGameOver"></div>
221 		<div id="mobileStartHint">Press 🟢 to start!</div>
222 	</div>
223 
224 	<script>
225 		const BOARD_SIZE = 20;
226 		const EMPTY_LIGHT = '⬜';
227 		const EMPTY_DARK = '';
228 		const SNAKE = '🟩';
229 		const APPLE = '🟥';
230 		
231 		function getEmptySymbol() {
232 			return window.matchMedia('(prefers-color-scheme: dark)').matches ? EMPTY_DARK : EMPTY_LIGHT;
233 		}
234 		
235 		let board = [];
236 		let snake = [{x: 10, y: 10}];
237 		let direction = {x: 0, y: -1};
238 		let inputBuffer = [];
239 		const MAX_INPUT_BUFFER_SIZE = 3;
240 		let apple = {x: 5, y: 5};
241 		let score = 0;
242 		let highScore = 0;
243 		let gameRunning = false;
244 		let gameStarted = false;
245 		let gameSpeed = 200;
246 		let gameInterval;
247 		
248 		function initBoard() {
249 			for (let y = 0; y < BOARD_SIZE; y++) {
250 				board[y] = [];
251 				for (let x = 0; x < BOARD_SIZE; x++) {
252 					board[y][x] = getEmptySymbol();
253 				}
254 			}
255 		}
256 		
257 		function placeApple() {
258 			let newApple;
259 			do {
260 				newApple = {
261 					x: Math.floor(Math.random() * BOARD_SIZE),
262 					y: Math.floor(Math.random() * BOARD_SIZE)
263 				};
264 			} while (snake.some(segment => segment.x === newApple.x && segment.y === newApple.y));
265 			
266 			apple = newApple;
267 		}
268 		
269 		function updateBoard() {
270 			// Clear board
271 			const emptySymbol = getEmptySymbol();
272 			for (let y = 0; y < BOARD_SIZE; y++) {
273 				for (let x = 0; x < BOARD_SIZE; x++) {
274 					board[y][x] = emptySymbol;
275 				}
276 			}
277 			
278 			// Place snake
279 			snake.forEach(segment => {
280 				if (segment.x >= 0 && segment.x < BOARD_SIZE && segment.y >= 0 && segment.y < BOARD_SIZE) {
281 					board[segment.y][segment.x] = SNAKE;
282 				}
283 			});
284 			
285 			// Place apple
286 			board[apple.y][apple.x] = APPLE;
287 		}
288 		
289 		function calculateCellSize() {
290 			const padding = window.innerWidth <= 360 ? 10 : (window.innerWidth <= 400 ? 20 : 40);
291 			const borderWidth = 4; // 2px border on each side
292 			const availableWidth = window.innerWidth - padding - borderWidth;
293 			const maxPossibleSize = Math.floor(availableWidth / BOARD_SIZE);
294 			
295 			// Use smaller, tighter cell sizes
296 			let cellSize;
297 			if (window.innerWidth <= 360) {
298 				cellSize = Math.min(maxPossibleSize, 12);
299 			} else if (window.innerWidth <= 400) {
300 				cellSize = Math.min(maxPossibleSize, 14);
301 			} else {
302 				cellSize = Math.min(maxPossibleSize, 16);
303 			}
304 			
305 			return Math.max(cellSize, 8); // Minimum cell size
306 		}
307 		
308 		function updateBoardSize() {
309 			const cellSize = calculateCellSize();
310 			const fontSize = Math.max(Math.floor(cellSize * 0.9), 8);
311 			
312 			const gameBoard = document.getElementById('gameBoard');
313 			gameBoard.style.width = 'auto';
314 			gameBoard.style.display = 'inline-flex';
315 			gameBoard.style.flexDirection = 'column';
316 			
317 			const cells = document.querySelectorAll('.cell');
318 			cells.forEach(cell => {
319 				cell.style.width = cellSize + 'px';
320 				cell.style.height = cellSize + 'px';
321 				cell.style.fontSize = fontSize + 'px';
322 			});
323 		}
324 		
325 		function renderBoard() {
326 			const gameBoard = document.getElementById('gameBoard');
327 			gameBoard.innerHTML = '';
328 			
329 			for (let y = 0; y < BOARD_SIZE; y++) {
330 				const row = document.createElement('div');
331 				row.className = 'row';
332 				for (let x = 0; x < BOARD_SIZE; x++) {
333 					const cell = document.createElement('div');
334 					cell.className = 'cell';
335 					cell.textContent = board[y][x];
336 					row.appendChild(cell);
337 				}
338 				gameBoard.appendChild(row);
339 			}
340 			
341 			updateBoardSize();
342 		}
343 		
344 		function moveSnake() {
345 			if (!gameRunning) return;
346 			
347 			// Process next direction from input buffer if available
348 			if (inputBuffer.length > 0) {
349 				direction = inputBuffer.shift();
350 			}
351 			
352 			const head = {
353 				x: snake[0].x + direction.x,
354 				y: snake[0].y + direction.y
355 			};
356 			
357 			// Check wall collision
358 			if (head.x < 0 || head.x >= BOARD_SIZE || head.y < 0 || head.y >= BOARD_SIZE) {
359 				gameOver();
360 				return;
361 			}
362 			
363 			// Check self collision
364 			if (snake.some(segment => segment.x === head.x && segment.y === head.y)) {
365 				gameOver();
366 				return;
367 			}
368 			
369 			snake.unshift(head);
370 			
371 			// Check apple collision
372 			if (head.x === apple.x && head.y === apple.y) {
373 				score += 10;
374 				document.getElementById('score').textContent = `Score: ${score}`;
375 				updateHighScore();
376 				
377 				// Increase speed
378 				gameSpeed = Math.max(50, 200 - Math.floor(score / 1));
379 				clearInterval(gameInterval);
380 				gameInterval = setInterval(gameLoop, gameSpeed);
381 				
382 				placeApple();
383 			} else {
384 				snake.pop();
385 			}
386 		}
387 		
388 		function gameOver() {
389 			gameRunning = false;
390 			
391 			if (gamepadConnected) {
392 				document.getElementById('gameOver').innerHTML = 'Game Over!<br>Press START button to restart';
393 			} else {
394 				document.getElementById('gameOver').innerHTML = 'Game Over!<br>Press Space to restart';
395 			}
396 			
397 			// Show restart button and mobile message
398 			const centerBtn = document.getElementById('centerBtn');
399 			const mobileGameOver = document.getElementById('mobileGameOver');
400 			if (centerBtn) {
401 				centerBtn.textContent = '🟢';
402 				centerBtn.classList.add('show');
403 				centerBtn.classList.remove('hidden');
404 			}
405 			if (mobileGameOver) {
406 				if (gamepadConnected) {
407 					mobileGameOver.innerHTML = 'Game Over!<br>Press START button to restart';
408 				} else {
409 					mobileGameOver.innerHTML = 'Game Over!<br>Press 🟢 to restart';
410 				}
411 				mobileGameOver.style.display = 'block';
412 			}
413 			
414 			clearInterval(gameInterval);
415 		}
416 		
417 		function startGame() {
418 			// Reset game state first
419 			snake = [{x: 10, y: 10}];
420 			direction = {x: 0, y: -1};
421 			inputBuffer = [];
422 			score = 0;
423 			gameSpeed = 200;
424 			document.getElementById('score').textContent = `Score: ${score}`;
425 			
426 			// Only place new apple if restarting (not first time)
427 			if (score > 0 || gameStarted) {
428 				placeApple();
429 			}
430 
431 			gameRunning = true;
432 			gameStarted = true;
433 			
434 			// Hide all hint messages
435 			document.getElementById('startHint').style.display = 'none';
436 			document.getElementById('mobileStartHint').style.display = 'none';
437 			document.getElementById('gameOver').textContent = '';
438 			
439 			const mobileGameOver = document.getElementById('mobileGameOver');
440 			if (mobileGameOver) {
441 				mobileGameOver.style.display = 'none';
442 			}
443 			
444 			// Hide center button
445 			const centerBtn = document.getElementById('centerBtn');
446 			if (centerBtn) {
447 				centerBtn.classList.add('hidden');
448 				centerBtn.classList.remove('show');
449 			}
450 			
451 			updateBoard();
452 			renderBoard();
453 			
454 			// Start game loop
455 			clearInterval(gameInterval);
456 			gameInterval = setInterval(gameLoop, gameSpeed);
457 		}
458 		
459 		function loadHighScore() {
460 			const saved = localStorage.getItem('snakeHighScore');
461 			highScore = saved ? parseInt(saved) : 0;
462 			document.getElementById('highScore').textContent = `High Score: ${highScore}`;
463 		}
464 		
465 		function saveHighScore() {
466 			localStorage.setItem('snakeHighScore', highScore.toString());
467 		}
468 		
469 		function updateHighScore() {
470 			if (score > highScore) {
471 				highScore = score;
472 				document.getElementById('highScore').textContent = `High Score: ${highScore}`;
473 				saveHighScore();
474 			}
475 		}
476 		
477 		function gameLoop() {
478 			moveSnake();
479 			updateBoard();
480 			renderBoard();
481 		}
482 		
483 		function setDirection(newDirection) {
484 			if (!gameRunning) return;
485 			
486 			// Don't add if buffer is full
487 			if (inputBuffer.length >= MAX_INPUT_BUFFER_SIZE) return;
488 			
489 			// Get the most recent direction (either from buffer or current direction)
490 			const lastDirection = inputBuffer.length > 0 ? inputBuffer[inputBuffer.length - 1] : direction;
491 			
492 			// Allow any direction when snake is only 1 segment long
493 			if (snake.length === 1) {
494 				inputBuffer.push(newDirection);
495 				return;
496 			}
497 			
498 			// Prevent reversing into self by checking the last direction
499 			if (newDirection.x !== 0 && lastDirection.x !== -newDirection.x) {
500 				inputBuffer.push(newDirection);
501 			} else if (newDirection.y !== 0 && lastDirection.y !== -newDirection.y) {
502 				inputBuffer.push(newDirection);
503 			}
504 		}
505 		
506 		// Track key state to prevent hold repeats
507 		let keysPressed = {};
508 		
509 		document.addEventListener('keydown', (event) => {
510 			if (event.code === 'Space') {
511 				if (!gameRunning) {
512 					startGame();
513 				}
514 				return;
515 			}
516 			
517 			if (!gameRunning) return;
518 			
519 			// Only process if key wasn't already pressed
520 			if (keysPressed[event.code]) return;
521 			keysPressed[event.code] = true;
522 			
523 			switch(event.code) {
524 				case 'ArrowUp':
525 					setDirection({x: 0, y: -1});
526 					break;
527 				case 'ArrowDown':
528 					setDirection({x: 0, y: 1});
529 					break;
530 				case 'ArrowLeft':
531 					setDirection({x: -1, y: 0});
532 					break;
533 				case 'ArrowRight':
534 					setDirection({x: 1, y: 0});
535 					break;
536 			}
537 		});
538 		
539 		document.addEventListener('keyup', (event) => {
540 			keysPressed[event.code] = false;
541 		});
542 		
543 		// Mobile controls - Track which button was last triggered to prevent hold repeats
544 		let lastTriggeredButton = null;
545 		
546 		function handleTouchMove(e) {
547 			e.preventDefault();
548 			const touch = e.touches[0];
549 			const element = document.elementFromPoint(touch.clientX, touch.clientY);
550 			const button = element?.closest('.control-btn');
551 			
552 			if (button && button !== lastTriggeredButton) {
553 				lastTriggeredButton = button;
554 				
555 				if (gameRunning) {
556 					switch(button.id) {
557 						case 'upBtn':
558 							setDirection({x: 0, y: -1});
559 							break;
560 						case 'downBtn':
561 							setDirection({x: 0, y: 1});
562 							break;
563 						case 'leftBtn':
564 							setDirection({x: -1, y: 0});
565 							break;
566 						case 'rightBtn':
567 							setDirection({x: 1, y: 0});
568 							break;
569 					}
570 				} else if (button.id === 'centerBtn') {
571 					startGame();
572 				}
573 			}
574 		}
575 		
576 		function handleTouchEnd(e) {
577 			e.preventDefault();
578 			lastTriggeredButton = null;
579 		}
580 		
581 		// Add touch event listeners for sliding behavior
582 		document.getElementById('upBtn').addEventListener('touchstart', (e) => {
583 			e.preventDefault();
584 			lastTriggeredButton = e.target;
585 			if (gameRunning) setDirection({x: 0, y: -1});
586 		});
587 		document.getElementById('downBtn').addEventListener('touchstart', (e) => {
588 			e.preventDefault();
589 			lastTriggeredButton = e.target;
590 			if (gameRunning) setDirection({x: 0, y: 1});
591 		});
592 		document.getElementById('leftBtn').addEventListener('touchstart', (e) => {
593 			e.preventDefault();
594 			lastTriggeredButton = e.target;
595 			if (gameRunning) setDirection({x: -1, y: 0});
596 		});
597 		document.getElementById('rightBtn').addEventListener('touchstart', (e) => {
598 			e.preventDefault();
599 			lastTriggeredButton = e.target;
600 			if (gameRunning) setDirection({x: 1, y: 0});
601 		});
602 		document.getElementById('centerBtn').addEventListener('touchstart', (e) => {
603 			e.preventDefault();
604 			lastTriggeredButton = e.target;
605 			if (!gameRunning) startGame();
606 		});
607 		
608 		// Add global touch move and end listeners for sliding
609 		document.addEventListener('touchmove', handleTouchMove);
610 		document.addEventListener('touchend', handleTouchEnd);
611 		
612 		// Gamepad support
613 		let gamepadConnected = false;
614 		let lastGamepadState = {};
615 		
616 		function handleGamepadConnected(event) {
617 			gamepadConnected = true;
618 			console.log('Gamepad connected:', event.gamepad.id);
619 			updateStartHints();
620 		}
621 		
622 		function handleGamepadDisconnected(event) {
623 			gamepadConnected = false;
624 			updateStartHints();
625 		}
626 		
627 		function updateStartHints() {
628 			if (gamepadConnected) {
629 				document.getElementById('startHint').textContent = 'Press START button to start!';
630 				document.getElementById('mobileStartHint').textContent = 'Press START button to start!';
631 			} else {
632 				document.getElementById('startHint').textContent = 'Press Space to start!';
633 				document.getElementById('mobileStartHint').textContent = 'Press 🟢 to start!';
634 			}
635 		}
636 		
637 		function checkGamepadAvailability() {
638 			// Check if Gamepad API is available
639 			if (!navigator.getGamepads) {
640 				return false;
641 			}
642 			
643 			// Check for connected gamepads (some browsers need button press first)
644 			const gamepads = navigator.getGamepads();
645 			for (let i = 0; i < gamepads.length; i++) {
646 				if (gamepads[i]) {
647 					if (!gamepadConnected) {
648 						gamepadConnected = true;
649 						updateStartHints();
650 					}
651 					return true;
652 				}
653 			}
654 			
655 			return false;
656 		}
657 		
658 		function updateGamepad() {
659 			// Always check for gamepads (Firefox sometimes needs this)
660 			if (!checkGamepadAvailability()) return;
661 			
662 			const gamepads = navigator.getGamepads();
663 			let gamepad = null;
664 			
665 			// Find first connected gamepad
666 			for (let i = 0; i < gamepads.length; i++) {
667 				if (gamepads[i]) {
668 					gamepad = gamepads[i];
669 					break;
670 				}
671 			}
672 			
673 			if (!gamepad) return;
674 			
675 			// Check both D-pad buttons AND left analog stick for broader compatibility
676 			const currentState = {
677 				// D-pad buttons (standard mapping)
678 				up: gamepad.buttons[12]?.pressed || gamepad.axes[1] < -0.5 || false,
679 				down: gamepad.buttons[13]?.pressed || gamepad.axes[1] > 0.5 || false,
680 				left: gamepad.buttons[14]?.pressed || gamepad.axes[0] < -0.5 || false,
681 				right: gamepad.buttons[15]?.pressed || gamepad.axes[0] > 0.5 || false,
682 				// Accept face buttons and Start (+ on Switch Pro controller) as "START button"
683 				action: gamepad.buttons[0]?.pressed || gamepad.buttons[1]?.pressed || gamepad.buttons[2]?.pressed || gamepad.buttons[3]?.pressed || gamepad.buttons[9]?.pressed || false
684 			};
685 			
686 			// Check for newly pressed buttons (edge detection)
687 			if (currentState.up && !lastGamepadState.up) {
688 				if (gameRunning) {
689 					setDirection({x: 0, y: -1});
690 				}
691 			}
692 			if (currentState.down && !lastGamepadState.down) {
693 				if (gameRunning) {
694 					setDirection({x: 0, y: 1});
695 				}
696 			}
697 			if (currentState.left && !lastGamepadState.left) {
698 				if (gameRunning) {
699 					setDirection({x: -1, y: 0});
700 				}
701 			}
702 			if (currentState.right && !lastGamepadState.right) {
703 				if (gameRunning) {
704 					setDirection({x: 1, y: 0});
705 				}
706 			}
707 			if (currentState.action && !lastGamepadState.action) {
708 				if (!gameRunning) {
709 					startGame();
710 				}
711 			}
712 			
713 			lastGamepadState = currentState;
714 		}
715 		
716 		// Add gamepad event listeners
717 		window.addEventListener('gamepadconnected', handleGamepadConnected);
718 		window.addEventListener('gamepaddisconnected', handleGamepadDisconnected);
719 		
720 		// Poll gamepad state regularly
721 		setInterval(updateGamepad, 10); // Poll 100 times a second
722 		
723 		// Initial check
724 		setTimeout(checkGamepadAvailability, 1000);
725 		
726 		
727 		// Handle window resize and orientation changes
728 		window.addEventListener('resize', () => {
729 			updateBoardSize();
730 		});
731 		
732 		// Initialize game
733 		loadHighScore();
734 		initBoard();
735 		updateBoard();
736 		renderBoard();
737 		
738 		// Show initial start hints and button
739 		document.getElementById('startHint').style.display = 'block';
740 		document.getElementById('mobileStartHint').style.display = 'block';
741 		const centerBtn = document.getElementById('centerBtn');
742 		centerBtn.classList.add('show');
743 		centerBtn.classList.remove('hidden');
744 		centerBtn.textContent = '🟢';
745 	</script>
746 </body>
747 </html>