snake: add gamepad support

Hunter
Friday, August 1st, 2025 at 11:54 PM

pages/snake/index.html+126-2

pages/snake/index.html
386  		
387  		function gameOver() {
388  			gameRunning = false;
    -			document.getElementById('gameOver').innerHTML = 'Game Over!<br>Press Space to restart';
389 +			
390 +			if (gamepadConnected) {
391 +				document.getElementById('gameOver').innerHTML = 'Game Over!<br>Press START button to restart';
392 +			} else {
393 +				document.getElementById('gameOver').innerHTML = 'Game Over!<br>Press Space to restart';
394 +			}
395  			
396  			// Show restart button and mobile message
397  			const centerBtn = document.getElementById('centerBtn');

402  				centerBtn.classList.remove('hidden');
403  			}
404  			if (mobileGameOver) {
    -				mobileGameOver.innerHTML = 'Game Over!<br>Press 🟢 to restart';
405 +				if (gamepadConnected) {
406 +					mobileGameOver.innerHTML = 'Game Over!<br>Press START button to restart';
407 +				} else {
408 +					mobileGameOver.innerHTML = 'Game Over!<br>Press 🟢 to restart';
409 +				}
410  				mobileGameOver.style.display = 'block';
411  			}
412  			

608  		document.addEventListener('touchmove', handleTouchMove);
609  		document.addEventListener('touchend', handleTouchEnd);
610  		
611 +		// Gamepad support
612 +		let gamepadConnected = false;
613 +		let lastGamepadState = {};
614 +		
615 +		function handleGamepadConnected(event) {
616 +			gamepadConnected = true;
617 +			console.log('Gamepad connected:', event.gamepad.id);
618 +			updateStartHints();
619 +		}
620 +		
621 +		function handleGamepadDisconnected(event) {
622 +			gamepadConnected = false;
623 +			updateStartHints();
624 +		}
625 +		
626 +		function updateStartHints() {
627 +			if (gamepadConnected) {
628 +				document.getElementById('startHint').textContent = 'Press START button to start!';
629 +				document.getElementById('mobileStartHint').textContent = 'Press START button to start!';
630 +			} else {
631 +				document.getElementById('startHint').textContent = 'Press Space to start!';
632 +				document.getElementById('mobileStartHint').textContent = 'Press 🟢 to start!';
633 +			}
634 +		}
635 +		
636 +		function checkGamepadAvailability() {
637 +			// Check if Gamepad API is available
638 +			if (!navigator.getGamepads) {
639 +				return false;
640 +			}
641 +			
642 +			// Check for connected gamepads (some browsers need button press first)
643 +			const gamepads = navigator.getGamepads();
644 +			for (let i = 0; i < gamepads.length; i++) {
645 +				if (gamepads[i]) {
646 +					if (!gamepadConnected) {
647 +						gamepadConnected = true;
648 +						updateStartHints();
649 +					}
650 +					return true;
651 +				}
652 +			}
653 +			
654 +			return false;
655 +		}
656 +		
657 +		function updateGamepad() {
658 +			// Always check for gamepads (Firefox sometimes needs this)
659 +			if (!checkGamepadAvailability()) return;
660 +			
661 +			const gamepads = navigator.getGamepads();
662 +			let gamepad = null;
663 +			
664 +			// Find first connected gamepad
665 +			for (let i = 0; i < gamepads.length; i++) {
666 +				if (gamepads[i]) {
667 +					gamepad = gamepads[i];
668 +					break;
669 +				}
670 +			}
671 +			
672 +			if (!gamepad) return;
673 +			
674 +			// Check both D-pad buttons AND left analog stick for broader compatibility
675 +			const currentState = {
676 +				// D-pad buttons (standard mapping)
677 +				up: gamepad.buttons[12]?.pressed || gamepad.axes[1] < -0.5 || false,
678 +				down: gamepad.buttons[13]?.pressed || gamepad.axes[1] > 0.5 || false,
679 +				left: gamepad.buttons[14]?.pressed || gamepad.axes[0] < -0.5 || false,
680 +				right: gamepad.buttons[15]?.pressed || gamepad.axes[0] > 0.5 || false,
681 +				// Accept face buttons and Start (+ on Switch Pro controller) as "START button"
682 +				action: gamepad.buttons[0]?.pressed || gamepad.buttons[1]?.pressed || gamepad.buttons[2]?.pressed || gamepad.buttons[3]?.pressed || gamepad.buttons[9]?.pressed || false
683 +			};
684 +			
685 +			// Check for newly pressed buttons (edge detection)
686 +			if (currentState.up && !lastGamepadState.up) {
687 +				if (gameRunning) {
688 +					setDirection({x: 0, y: -1});
689 +				}
690 +			}
691 +			if (currentState.down && !lastGamepadState.down) {
692 +				if (gameRunning) {
693 +					setDirection({x: 0, y: 1});
694 +				}
695 +			}
696 +			if (currentState.left && !lastGamepadState.left) {
697 +				if (gameRunning) {
698 +					setDirection({x: -1, y: 0});
699 +				}
700 +			}
701 +			if (currentState.right && !lastGamepadState.right) {
702 +				if (gameRunning) {
703 +					setDirection({x: 1, y: 0});
704 +				}
705 +			}
706 +			if (currentState.action && !lastGamepadState.action) {
707 +				if (!gameRunning) {
708 +					startGame();
709 +				}
710 +			}
711 +			
712 +			lastGamepadState = currentState;
713 +		}
714 +		
715 +		// Add gamepad event listeners
716 +		window.addEventListener('gamepadconnected', handleGamepadConnected);
717 +		window.addEventListener('gamepaddisconnected', handleGamepadDisconnected);
718 +		
719 +		// Poll gamepad state regularly
720 +		setInterval(updateGamepad, 10); // Poll 100 times a second
721 +		
722 +		// Initial check
723 +		setTimeout(checkGamepadAvailability, 1000);
724 +		
725 +		
726  		// Handle window resize and orientation changes
727  		window.addEventListener('resize', () => {
728  			updateBoardSize();