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>