| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Lux Snake Game</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| #game-board { |
| background-color: #0f172a; |
| background-image: |
| linear-gradient(#1e293b 1px, transparent 1px), |
| linear-gradient(90deg, #1e293b 1px, transparent 1px); |
| background-size: 20px 20px; |
| border: 4px solid #1e40af; |
| box-shadow: 0 0 40px rgba(30, 64, 175, 0.8); |
| border-radius: 12px; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| #game-board::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: radial-gradient(circle at center, transparent 0%, rgba(30, 64, 175, 0.1) 70%); |
| pointer-events: none; |
| } |
| |
| .snake { |
| background: linear-gradient(145deg, #4ade80, #22c55e); |
| border: 1px solid #166534; |
| border-radius: 4px; |
| box-shadow: 0 0 5px rgba(74, 222, 128, 0.7); |
| } |
| |
| .snake-head { |
| background: linear-gradient(145deg, #22c55e, #166534); |
| border: 1px solid #14532d; |
| border-radius: 4px; |
| box-shadow: 0 0 12px rgba(34, 197, 94, 0.9); |
| filter: drop-shadow(0 0 5px rgba(34, 197, 94, 0.6)); |
| position: relative; |
| z-index: 2; |
| } |
| |
| .food { |
| background: radial-gradient(circle at 30% 30%, #ef4444, #b91c1c); |
| border: 1px solid #991b1b; |
| border-radius: 50%; |
| box-shadow: 0 0 15px rgba(239, 68, 68, 0.9); |
| animation: pulse 0.8s infinite alternate; |
| filter: drop-shadow(0 0 8px rgba(239, 68, 68, 0.8)); |
| } |
| |
| .banana { |
| background: linear-gradient(to bottom, #facc15, #eab308); |
| border: 1px solid #ca8a04; |
| border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; |
| box-shadow: 0 0 15px rgba(234, 179, 8, 0.9); |
| filter: drop-shadow(0 0 8px rgba(250, 204, 21, 0.8)); |
| position: relative; |
| } |
| |
| .banana::before { |
| content: ''; |
| position: absolute; |
| top: -5px; |
| left: 10px; |
| width: 8px; |
| height: 8px; |
| background: #4d7c0f; |
| border-radius: 50%; |
| } |
| |
| @keyframes pulse { |
| from { transform: scale(0.9); } |
| to { transform: scale(1.1); } |
| } |
| |
| .game-over { |
| animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; |
| background: linear-gradient(135deg, rgba(15, 23, 42, 0.9), rgba(2, 6, 23, 0.95)); |
| backdrop-filter: blur(4px); |
| } |
| |
| @keyframes shake { |
| 10%, 90% { transform: translate3d(-1px, 0, 0); } |
| 20%, 80% { transform: translate3d(2px, 0, 0); } |
| 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } |
| 40%, 60% { transform: translate3d(4px, 0, 0); } |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col items-center justify-center p-4"> |
| <div class="text-center mb-6"> |
| <h1 class="text-4xl font-bold bg-gradient-to-r from-green-400 via-green-300 to-green-400 bg-clip-text text-transparent mb-2 drop-shadow-lg"> |
| 🐍 Classic Snake Game |
| </h1> |
| <p class="text-gray-400">Use arrow keys or swipe to control the snake</p> |
| </div> |
| |
| <div class="relative"> |
| <div class="flex justify-between items-center mb-4"> |
| <div class="text-xl font-semibold"> |
| Score: <span id="score" class="text-green-400">0</span> |
| </div> |
| <div class="text-xl font-semibold"> |
| High Score: <span id="high-score" class="text-yellow-400">0</span> |
| </div> |
| </div> |
| |
| <canvas id="game-board" class="rounded-lg" width="400" height="400"></canvas> |
| |
| <div id="game-over" class="hidden absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center rounded-lg"> |
| <h2 class="text-3xl font-bold text-red-500 mb-4">Game Over!</h2> |
| <p class="text-xl mb-6">Your score: <span id="final-score" class="text-green-400">0</span></p> |
| <button id="restart-btn" class="px-6 py-2 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-500 hover:to-green-600 rounded-lg font-semibold transition-all transform hover:scale-105 shadow-lg hover:shadow-green-500/20"> |
| Play Again |
| </button> |
| </div> |
| |
| <div id="start-screen" class="absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center rounded-lg"> |
| <h2 class="text-3xl font-bold text-green-400 mb-4">Snake Game</h2> |
| <p class="text-center text-gray-300 mb-8 max-w-md px-4"> |
| Eat the red food to grow. Don't hit the walls or yourself! |
| </p> |
| <button id="start-btn" class="px-6 py-2 bg-green-600 hover:bg-green-700 rounded-lg font-semibold transition-colors"> |
| Start Game |
| </button> |
| </div> |
| </div> |
| |
| <div class="mt-8 grid grid-cols-3 gap-2 max-w-xs touch-screen-controls hidden"> |
| <div></div> |
| <button class="p-4 bg-gray-800 rounded-lg text-center" data-direction="up">↑</button> |
| <div></div> |
| <button class="p-4 bg-gray-800 rounded-lg text-center" data-direction="left">←</button> |
| <button class="p-4 bg-gray-800 rounded-lg text-center" data-direction="down">↓</button> |
| <button class="p-4 bg-gray-800 rounded-lg text-center" data-direction="right">→</button> |
| </div> |
| |
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| const canvas = document.getElementById('game-board'); |
| const ctx = canvas.getContext('2d'); |
| const scoreDisplay = document.getElementById('score'); |
| const highScoreDisplay = document.getElementById('high-score'); |
| const finalScoreDisplay = document.getElementById('final-score'); |
| const gameOverScreen = document.getElementById('game-over'); |
| const startScreen = document.getElementById('start-screen'); |
| const startBtn = document.getElementById('start-btn'); |
| const restartBtn = document.getElementById('restart-btn'); |
| const touchControls = document.querySelector('.touch-screen-controls'); |
| |
| |
| if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { |
| touchControls.classList.remove('hidden'); |
| } |
| |
| |
| const gridSize = 20; |
| const tileCount = canvas.width / gridSize; |
| let speed = 7; |
| |
| |
| let snake = []; |
| let foods = []; |
| let maxFoods = 1; |
| let direction = 'right'; |
| let nextDirection = 'right'; |
| let score = 0; |
| let highScore = localStorage.getItem('snakeHighScore') || 0; |
| let gameRunning = false; |
| let gameLoop; |
| let touchStartX = 0; |
| let touchStartY = 0; |
| |
| highScoreDisplay.textContent = highScore; |
| |
| |
| function initGame() { |
| snake = [ |
| {x: 5, y: 10}, |
| {x: 4, y: 10}, |
| {x: 3, y: 10} |
| ]; |
| |
| direction = 'right'; |
| nextDirection = 'right'; |
| score = 0; |
| scoreDisplay.textContent = score; |
| generateFood(); |
| gameRunning = true; |
| |
| if (gameLoop) clearInterval(gameLoop); |
| gameLoop = setInterval(gameStep, 1000 / speed); |
| } |
| |
| |
| function gameStep() { |
| direction = nextDirection; |
| |
| |
| const head = {x: snake[0].x, y: snake[0].y}; |
| |
| switch(direction) { |
| case 'up': |
| head.y--; |
| break; |
| case 'down': |
| head.y++; |
| break; |
| case 'left': |
| head.x--; |
| break; |
| case 'right': |
| head.x++; |
| break; |
| } |
| |
| |
| if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) { |
| gameOver(); |
| return; |
| } |
| |
| |
| for (let i = 0; i < snake.length; i++) { |
| if (head.x === snake[i].x && head.y === snake[i].y) { |
| gameOver(); |
| return; |
| } |
| } |
| |
| |
| snake.unshift(head); |
| |
| |
| let ateFood = false; |
| for (let i = 0; i < foods.length; i++) { |
| if (head.x === foods[i].x && head.y === foods[i].y) { |
| if (foods[i].type === 'banana') { |
| |
| if (snake.length > 3) { |
| snake = snake.slice(0, Math.max(3, snake.length - 2)); |
| } |
| score += 2; |
| } else { |
| score++; |
| } |
| scoreDisplay.textContent = score; |
| foods.splice(i, 1); |
| ateFood = true; |
| break; |
| } |
| } |
| |
| if (ateFood) { |
| |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; |
| ctx.beginPath(); |
| ctx.arc( |
| head.x * gridSize + gridSize / 2, |
| head.y * gridSize + gridSize / 2, |
| gridSize * 1.5, |
| 0, |
| Math.PI * 2 |
| ); |
| ctx.fill(); |
| |
| |
| if (score % 5 === 0) { |
| speed += 0.5; |
| clearInterval(gameLoop); |
| gameLoop = setInterval(gameStep, 1000 / speed); |
| } |
| |
| generateFood(); |
| } else { |
| |
| snake.pop(); |
| } |
| |
| |
| draw(); |
| } |
| |
| |
| function draw() { |
| |
| ctx.fillStyle = '#1a2e05'; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
| |
| |
| ctx.strokeStyle = '#2a3a15'; |
| ctx.lineWidth = 0.5; |
| |
| for (let i = 0; i < tileCount; i++) { |
| ctx.beginPath(); |
| ctx.moveTo(i * gridSize, 0); |
| ctx.lineTo(i * gridSize, canvas.height); |
| ctx.stroke(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(0, i * gridSize); |
| ctx.lineTo(canvas.width, i * gridSize); |
| ctx.stroke(); |
| } |
| |
| |
| snake.forEach((segment, index) => { |
| if (index === 0) { |
| ctx.fillStyle = '#22c55e'; |
| ctx.strokeStyle = '#166534'; |
| ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize); |
| ctx.strokeRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize); |
| |
| |
| ctx.fillStyle = 'white'; |
| if (direction === 'right' || direction === 'left') { |
| ctx.fillRect(segment.x * gridSize + (direction === 'right' ? 12 : 2), segment.y * gridSize + 4, 4, 4); |
| ctx.fillRect(segment.x * gridSize + (direction === 'right' ? 12 : 2), segment.y * gridSize + 12, 4, 4); |
| } else { |
| ctx.fillRect(segment.x * gridSize + 4, segment.y * gridSize + (direction === 'down' ? 12 : 2), 4, 4); |
| ctx.fillRect(segment.x * gridSize + 12, segment.y * gridSize + (direction === 'down' ? 12 : 2), 4, 4); |
| } |
| } else { |
| ctx.fillStyle = '#4ade80'; |
| ctx.strokeStyle = '#22c55e'; |
| ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize); |
| ctx.strokeRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize); |
| } |
| }); |
| |
| |
| foods.forEach(food => { |
| if (food.type === 'banana') { |
| ctx.fillStyle = '#facc15'; |
| ctx.strokeStyle = '#eab308'; |
| ctx.beginPath(); |
| ctx.ellipse( |
| food.x * gridSize + gridSize / 2, |
| food.y * gridSize + gridSize / 2, |
| gridSize / 2 - 2, |
| gridSize / 2, |
| Math.PI / 4, |
| 0, |
| Math.PI * 2 |
| ); |
| ctx.fill(); |
| ctx.stroke(); |
| } else { |
| ctx.fillStyle = '#ef4444'; |
| ctx.strokeStyle = '#dc2626'; |
| ctx.beginPath(); |
| ctx.arc( |
| food.x * gridSize + gridSize / 2, |
| food.y * gridSize + gridSize / 2, |
| gridSize / 2 - 1, |
| 0, |
| Math.PI * 2 |
| ); |
| ctx.fill(); |
| ctx.stroke(); |
| } |
| }); |
| } |
| |
| |
| function generateFood() { |
| |
| foods = foods.filter(food => { |
| for (let i = 0; i < snake.length; i++) { |
| if (food.x === snake[i].x && food.y === snake[i].y) { |
| return false; |
| } |
| } |
| return true; |
| }); |
| |
| |
| while (foods.length < maxFoods) { |
| let newFood; |
| let validPosition = false; |
| |
| while (!validPosition) { |
| newFood = { |
| x: Math.floor(Math.random() * tileCount), |
| y: Math.floor(Math.random() * tileCount) |
| }; |
| |
| |
| validPosition = true; |
| |
| |
| for (let i = 0; i < snake.length; i++) { |
| if (newFood.x === snake[i].x && newFood.y === snake[i].y) { |
| validPosition = false; |
| break; |
| } |
| } |
| |
| |
| if (validPosition) { |
| for (let i = 0; i < foods.length; i++) { |
| if (newFood.x === foods[i].x && newFood.y === foods[i].y) { |
| validPosition = false; |
| break; |
| } |
| } |
| } |
| } |
| |
| |
| newFood.type = Math.random() < 0.2 ? 'banana' : 'food'; |
| foods.push(newFood); |
| } |
| |
| |
| if (score > 0 && score % 5 === 0) { |
| maxFoods = Math.min(5, Math.floor(score / 5) + 1); |
| } |
| } |
| |
| |
| function gameOver() { |
| clearInterval(gameLoop); |
| gameRunning = false; |
| |
| |
| if (score > highScore) { |
| highScore = score; |
| localStorage.setItem('snakeHighScore', highScore); |
| highScoreDisplay.textContent = highScore; |
| } |
| |
| finalScoreDisplay.textContent = score; |
| gameOverScreen.classList.remove('hidden'); |
| gameOverScreen.classList.add('game-over'); |
| |
| |
| setTimeout(() => { |
| gameOverScreen.classList.remove('game-over'); |
| }, 500); |
| } |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (!gameRunning) return; |
| |
| switch(e.key) { |
| case 'ArrowUp': |
| if (direction !== 'down') nextDirection = 'up'; |
| break; |
| case 'ArrowDown': |
| if (direction !== 'up') nextDirection = 'down'; |
| break; |
| case 'ArrowLeft': |
| if (direction !== 'right') nextDirection = 'left'; |
| break; |
| case 'ArrowRight': |
| if (direction !== 'left') nextDirection = 'right'; |
| break; |
| } |
| }); |
| |
| |
| document.addEventListener('touchstart', (e) => { |
| touchStartX = e.touches[0].clientX; |
| touchStartY = e.touches[0].clientY; |
| }); |
| |
| document.addEventListener('touchmove', (e) => { |
| if (!gameRunning) return; |
| e.preventDefault(); |
| |
| const touchEndX = e.touches[0].clientX; |
| const touchEndY = e.touches[0].clientY; |
| |
| const dx = touchEndX - touchStartX; |
| const dy = touchEndY - touchStartY; |
| |
| |
| if (Math.abs(dx) > Math.abs(dy)) { |
| |
| if (dx > 0 && direction !== 'left') { |
| nextDirection = 'right'; |
| } else if (dx < 0 && direction !== 'right') { |
| nextDirection = 'left'; |
| } |
| } else { |
| |
| if (dy > 0 && direction !== 'up') { |
| nextDirection = 'down'; |
| } else if (dy < 0 && direction !== 'down') { |
| nextDirection = 'up'; |
| } |
| } |
| }); |
| |
| |
| document.querySelectorAll('.touch-screen-controls button').forEach(button => { |
| button.addEventListener('click', () => { |
| if (!gameRunning) return; |
| |
| const dir = button.getAttribute('data-direction'); |
| |
| if ( |
| (dir === 'up' && direction !== 'down') || |
| (dir === 'down' && direction !== 'up') || |
| (dir === 'left' && direction !== 'right') || |
| (dir === 'right' && direction !== 'left') |
| ) { |
| nextDirection = dir; |
| } |
| }); |
| }); |
| |
| |
| startBtn.addEventListener('click', () => { |
| startScreen.classList.add('hidden'); |
| initGame(); |
| draw(); |
| }); |
| |
| |
| restartBtn.addEventListener('click', () => { |
| gameOverScreen.classList.add('hidden'); |
| initGame(); |
| draw(); |
| }); |
| |
| |
| draw(); |
| }); |
| </script> |
| </html> |