Games Io Github [hot] -
button:hover background: #2d4055; transform: scale(0.97); color: white; border-color: #5f8bb3;
<script> (function() // ---------- SOLID FEATURE: Persistent Best Score (localStorage) ---------- // ---------- Classic Snake with smooth, reliable mechanics ---------- const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); games io github
.score-card background: #03060cbb; backdrop-filter: blur(8px); padding: 8px 22px; border-radius: 60px; border-left: 4px solid #3bc9ff; font-weight: 700; letter-spacing: 1px; button:hover background: #2d4055; transform: scale(0
<div class="controls"> <button id="restartButton">🔄 NEW GAME</button> <button id="resetBestBtn">✨ RESET BEST</button> </div> <div class="controls" style="margin-top: 8px;"> <span style="font-size: 12px; color:#8aaec0;">⬆️ ⬇️ ⬅️ ➡️ | WASD | Swipe (mobile)</span> </div> </div> </div> '#d9ffb0' : '#b0e57c'; ctx
// Snake state let snake = []; // array of x, y let direction = 'RIGHT'; // current moving direction let nextDirection = 'RIGHT'; let food = x: 12, y: 12 ; let score = 0; let gameLoop = null; let gameActive = true; // UI elements const currentScoreSpan = document.getElementById('currentScore'); const bestScoreSpan = document.getElementById('bestScore'); const gameStatusSpan = document.getElementById('gameStatusText'); // ---------- PERSISTENT BEST SCORE (solid feature) ---------- let bestScore = 0; const STORAGE_KEY = 'snakeio_best_score'; function loadBestScore() const stored = localStorage.getItem(STORAGE_KEY); if(stored !== null && !isNaN(parseInt(stored))) bestScore = parseInt(stored); else bestScore = 0; bestScoreSpan.innerText = bestScore; function updateBestScoreUI() bestScoreSpan.innerText = bestScore; function tryUpdateBestScore(currentScore) if(currentScore > bestScore) bestScore = currentScore; localStorage.setItem(STORAGE_KEY, bestScore); updateBestScoreUI(); // extra visual feedback: little flash effect on best card const bestCard = document.querySelector('.best-card'); if(bestCard) bestCard.style.transition = '0.1s'; bestCard.style.transform = 'scale(1.02)'; bestCard.style.borderRightColor = '#ffd966'; setTimeout(() => bestCard.style.transform = ''; bestCard.style.borderRightColor = '#ffb347'; , 200); return true; return false; // Reset best score manually (solid feature reset option) function resetBestScore() bestScore = 0; localStorage.setItem(STORAGE_KEY, 0); updateBestScoreUI(); // show subtle message on status gameStatusSpan.innerText = '✨ BEST RESET ✨'; setTimeout(() => if(gameActive) gameStatusSpan.innerText = '▶️ PLAYING'; else gameStatusSpan.innerText = '💀 GAME OVER'; , 1200); // ----- Game Helpers ----- function initGame() // classic start: 3 segments snake = [ x: 10, y: 10 , x: 9, y: 10 , x: 8, y: 10 ]; direction = 'RIGHT'; nextDirection = 'RIGHT'; score = 0; gameActive = true; currentScoreSpan.innerText = score; gameStatusSpan.innerText = '▶️ PLAYING'; generateValidFood(); clearGameLoop(); startLoop(); draw(); // immediate render function clearGameLoop() if(gameLoop) clearInterval(gameLoop); gameLoop = null; function startLoop() if(gameLoop) clearInterval(gameLoop); // classic 120ms per frame -> feels responsive but not too fast gameLoop = setInterval(updateGame, 130); function generateValidFood() const totalCells = GRID_SIZE * GRID_SIZE; if(snake.length >= totalCells) // You win! but we'll treat as game win (special case) // In classic snake, win condition triggers game over with victory. gameWin(); return; // Fast method: collect free positions const snakeSet = new Set(snake.map(seg => `$seg.x,$seg.y`)); const freePositions = []; for(let i = 0; i < GRID_SIZE; i++) for(let j = 0; j < GRID_SIZE; j++) if(!snakeSet.has(`$i,$j`)) freePositions.push( x: i, y: j ); if(freePositions.length === 0) gameWin(); return; const randIndex = Math.floor(Math.random() * freePositions.length); food = freePositions[randIndex]; function gameWin() if(!gameActive) return; gameActive = false; clearGameLoop(); gameStatusSpan.innerText = '🏆 YOU WIN! 🏆'; tryUpdateBestScore(score); draw(); // final draw shows win message on canvas function gameOver() if(!gameActive) return; gameActive = false; clearGameLoop(); gameStatusSpan.innerText = '💀 GAME OVER 💀'; tryUpdateBestScore(score); draw(); // draw game over overlay function updateGame() newHead.y >= GRID_SIZE) gameOver(); draw(); return; // self collision (skip if head just ate? careful: but newHead is already in snake[0]) // check if head position collides with any other segment (index > 0) const headPos = snake[0]; for(let i = 1; i < snake.length; i++) if(snake[i].x === headPos.x && snake[i].y === headPos.y) gameOver(); draw(); return; // extra win condition: if snake length equals total cells, trigger win if(snake.length === GRID_SIZE * GRID_SIZE) gameWin(); draw(); return; // all good, redraw draw(); // ----- Drawing with modern neon/io style ----- function draw() if(!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); // draw subtle grid (io style) ctx.strokeStyle = '#1e3347'; ctx.lineWidth = 0.8; for(let i = 0; i <= GRID_SIZE; i++) ctx.beginPath(); ctx.moveTo(i * CELL_SIZE, 0); ctx.lineTo(i * CELL_SIZE, canvas.height); ctx.stroke(); ctx.moveTo(0, i * CELL_SIZE); ctx.lineTo(canvas.width, i * CELL_SIZE); ctx.stroke(); // draw food (glowing berry) ctx.shadowBlur = 12; ctx.shadowColor = '#ff5e7e'; ctx.fillStyle = '#ff3860'; ctx.beginPath(); ctx.arc(food.x * CELL_SIZE + CELL_SIZE/2, food.y * CELL_SIZE + CELL_SIZE/2, CELL_SIZE * 0.4, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ffb3c6'; ctx.beginPath(); ctx.arc(food.x * CELL_SIZE + CELL_SIZE/2 - 2, food.y * CELL_SIZE + CELL_SIZE/2 - 2, 3, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; // draw snake (gradient segments + shiny eyes) for(let i = 0; i < snake.length; i++) const seg = snake[i]; const x = seg.x * CELL_SIZE; const y = seg.y * CELL_SIZE; const isHead = (i === 0); const gradient = ctx.createLinearGradient(x, y, x+CELL_SIZE, y+CELL_SIZE); if(isHead) gradient.addColorStop(0, '#3acb6b'); gradient.addColorStop(1, '#1f9e4a'); else gradient.addColorStop(0, '#2ab15b'); gradient.addColorStop(1, '#18853c'); ctx.fillStyle = gradient; ctx.shadowBlur = isHead ? 6 : 2; ctx.shadowColor = '#29ff8b'; ctx.fillRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2); // rounded corners effect ctx.fillStyle = isHead ? '#d9ffb0' : '#b0e57c'; ctx.beginPath(); ctx.arc(x + CELL_SIZE - 6, y + 6, 3, 0, Math.PI*2); ctx.fill(); if(isHead) // eyes ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(x + CELL_SIZE * 0.7, y + CELL_SIZE * 0.35, 4, 0, Math.PI*2); ctx.arc(x + CELL_SIZE * 0.3, y + CELL_SIZE * 0.35, 4, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.arc(x + CELL_SIZE * 0.72, y + CELL_SIZE * 0.33, 2, 0, Math.PI*2); ctx.arc(x + CELL_SIZE * 0.32, y + CELL_SIZE * 0.33, 2, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; // if game inactive: overlay message if(!gameActive) ctx.fillStyle = 'rgba(0,0,0,0.75)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = `bold $Math.floor(CELL_SIZE * 1.6)px 'Segoe UI', monospace`; ctx.fillStyle = '#f0fcff'; ctx.shadowBlur = 0; ctx.textAlign = 'center'; let msg = gameStatusSpan.innerText.includes('WIN') ? '🌟 PERFECT WIN! 🌟' : (gameStatusSpan.innerText.includes('GAME OVER') ? '💀 GAME OVER 💀' : 'STOPPED'); if(gameStatusSpan.innerText.includes('YOU WIN')) msg = '🏆 VICTORY! 🏆'; ctx.fillText(msg, canvas.width/2, canvas.height/2); ctx.font = `16px monospace`; ctx.fillStyle = '#bbd4ff'; ctx.fillText('click NEW GAME', canvas.width/2, canvas.height/2 + 45); ctx.textAlign = 'left'; else // draw subtle "io" effect ctx.font = `bold 16px 'Courier New'`; ctx.fillStyle = '#6ea8fe30'; ctx.fillText('⚡ SNAKE.IO', 12, 32); // ----- Direction & Controls (keyboard + swipe)----- function changeDirection(newDir) if(!gameActive) return; const opposite = 'UP': 'DOWN', 'DOWN': 'UP', 'LEFT': 'RIGHT', 'RIGHT': 'LEFT' ; // prevent 180-degree turn if(opposite[newDir] !== direction) nextDirection = newDir; function handleKey(e) // restart function function restartGame() clearGameLoop(); initGame(); // ----- Mobile Swipe Support (solid feature: full mobile friendly) ----- let touchStart = null; function handleTouchStart(e) e.preventDefault(); const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; touchStart = x: touch.clientX - rect.left, y: touch.clientY - rect.top ; function handleTouchEnd(e) if(!touchStart) return; e.preventDefault(); const rect = canvas.getBoundingClientRect(); const endX = e.changedTouches[0].clientX - rect.left; const endY = e.changedTouches[0].clientY - rect.top; const dx = endX - touchStart.x; const dy = endY - touchStart.y; if(Math.abs(dx) < 15 && Math.abs(dy) < 15) return; if(Math.abs(dx) > Math.abs(dy)) if(dx > 0) changeDirection('RIGHT'); else changeDirection('LEFT'); else if(dy > 0) changeDirection('DOWN'); else changeDirection('UP'); touchStart = null; // manual canvas touch cancel function attachMobileEvents() canvas.addEventListener('touchstart', handleTouchStart, passive: false); canvas.addEventListener('touchend', handleTouchEnd, passive: false); canvas.addEventListener('touchcancel', (e) => touchStart = null; ); // ----- Event binding ----- window.addEventListener('load', () => loadBestScore(); initGame(); attachMobileEvents(); window.addEventListener('keydown', (e) => e.key === 'w' ); document.getElementById('restartButton').addEventListener('click', () => restartGame(); ); document.getElementById('resetBestBtn').addEventListener('click', () => resetBestScore(); if(!gameActive) // just visual sync, but also if game active, best reset is just ui update draw(); ); ); // avoid page scroll on arrow keys fully window.addEventListener('keydown', function(e) ); // extra: ensure canvas focus not needed )(); </script> </body> </html>
.best-label font-size: 0.85rem; text-transform: uppercase; color: #ffcd94;