lux-snake / index.html
luxopes's picture
Update index.html
3c88a37 verified
<!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');
// Check if mobile device
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
touchControls.classList.remove('hidden');
}
// Game settings
const gridSize = 20;
const tileCount = canvas.width / gridSize;
let speed = 7; // Initial speed
// Game variables
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;
// Initialize game
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);
}
// Game step
function gameStep() {
direction = nextDirection;
// Move snake
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;
}
// Check collision with walls
if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) {
gameOver();
return;
}
// Check collision with self
for (let i = 0; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
gameOver();
return;
}
}
// Add new head
snake.unshift(head);
// Check if snake ate any food
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') {
// Shorten snake by 2 segments (but not below minimum length of 3)
if (snake.length > 3) {
snake = snake.slice(0, Math.max(3, snake.length - 2));
}
score += 2; // Bonus points for risky banana
} else {
score++;
}
scoreDisplay.textContent = score;
foods.splice(i, 1);
ateFood = true;
break;
}
}
if (ateFood) {
// Add eating effect
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();
// Increase speed slightly every 5 points
if (score % 5 === 0) {
speed += 0.5;
clearInterval(gameLoop);
gameLoop = setInterval(gameStep, 1000 / speed);
}
generateFood();
} else {
// Remove tail if no food eaten
snake.pop();
}
// Draw everything
draw();
}
// Draw game
function draw() {
// Clear canvas
ctx.fillStyle = '#1a2e05';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw grid lines
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();
}
// Draw snake
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);
// Draw eyes on head
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);
}
});
// Draw foods
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();
}
});
}
// Generate food
function generateFood() {
// Remove any food that might be on snake (just in case)
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;
});
// Add new foods until we reach maxFoods
while (foods.length < maxFoods) {
let newFood;
let validPosition = false;
while (!validPosition) {
newFood = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
};
// Check if food is on snake or other foods
validPosition = true;
// Check snake
for (let i = 0; i < snake.length; i++) {
if (newFood.x === snake[i].x && newFood.y === snake[i].y) {
validPosition = false;
break;
}
}
// Check other foods
if (validPosition) {
for (let i = 0; i < foods.length; i++) {
if (newFood.x === foods[i].x && newFood.y === foods[i].y) {
validPosition = false;
break;
}
}
}
}
// 20% chance to spawn banana instead of regular food
newFood.type = Math.random() < 0.2 ? 'banana' : 'food';
foods.push(newFood);
}
// Increase max foods every 5 points
if (score > 0 && score % 5 === 0) {
maxFoods = Math.min(5, Math.floor(score / 5) + 1);
}
}
// Game over
function gameOver() {
clearInterval(gameLoop);
gameRunning = false;
// Update high score
if (score > highScore) {
highScore = score;
localStorage.setItem('snakeHighScore', highScore);
highScoreDisplay.textContent = highScore;
}
finalScoreDisplay.textContent = score;
gameOverScreen.classList.remove('hidden');
gameOverScreen.classList.add('game-over');
// Remove shake animation after it's done
setTimeout(() => {
gameOverScreen.classList.remove('game-over');
}, 500);
}
// Event listeners
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;
}
});
// Touch controls for mobile
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;
// Determine swipe direction
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal swipe
if (dx > 0 && direction !== 'left') {
nextDirection = 'right';
} else if (dx < 0 && direction !== 'right') {
nextDirection = 'left';
}
} else {
// Vertical swipe
if (dy > 0 && direction !== 'up') {
nextDirection = 'down';
} else if (dy < 0 && direction !== 'down') {
nextDirection = 'up';
}
}
});
// Button controls for mobile
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;
}
});
});
// Start game button
startBtn.addEventListener('click', () => {
startScreen.classList.add('hidden');
initGame();
draw();
});
// Restart game button
restartBtn.addEventListener('click', () => {
gameOverScreen.classList.add('hidden');
initGame();
draw();
});
// Initial draw (empty board)
draw();
});
</script>
</html>