| | <!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> |