snake / index.html
pijou's picture
Add 1 files
0ce141e verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rainbow Neon Snake</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
body {
font-family: 'Press Start 2P', cursive;
background: linear-gradient(135deg, #0f172a, #1e293b);
overflow: hidden;
touch-action: none;
}
#gameCanvas {
box-shadow: 0 0 30px rgba(99, 102, 241, 0.8),
0 0 60px rgba(139, 92, 246, 0.6),
0 0 90px rgba(217, 70, 239, 0.4);
border-radius: 8px;
border: 2px solid #6366f1;
}
.rainbow-text {
background: linear-gradient(90deg,
#ef4444, #f97316, #eab308, #22c55e,
#3b82f6, #6366f1, #8b5cf6, #d946ef);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.neon-button {
transition: all 0.3s;
box-shadow: 0 0 15px rgba(99, 102, 241, 0.7);
position: relative;
overflow: hidden;
}
.neon-button:hover {
box-shadow: 0 0 25px rgba(139, 92, 246, 0.8);
transform: translateY(-2px);
}
.neon-button::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
to bottom right,
rgba(255, 255, 255, 0.3) 0%,
rgba(255, 255, 255, 0) 60%
);
transform: rotate(30deg);
transition: all 0.3s;
}
.neon-button:hover::before {
left: 100%;
}
.pulse {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(0.98); opacity: 0.9; }
50% { transform: scale(1.02); opacity: 1; }
100% { transform: scale(0.98); opacity: 0.9; }
}
.glow {
animation: glow 2s infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 15px rgba(99, 102, 241, 0.7),
0 0 30px rgba(139, 92, 246, 0.5);
}
to {
box-shadow: 0 0 25px rgba(99, 102, 241, 0.9),
0 0 50px rgba(139, 92, 246, 0.7),
0 0 75px rgba(217, 70, 239, 0.5);
}
}
.particle {
position: absolute;
border-radius: 50%;
pointer-events: none;
z-index: 10;
}
.controls {
touch-action: none;
}
.control-btn {
width: 70px;
height: 70px;
background: rgba(30, 41, 59, 0.7);
border: 2px solid;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 28px;
user-select: none;
touch-action: none;
transition: all 0.2s;
position: relative;
overflow: hidden;
}
.control-btn::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
to bottom right,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 60%
);
transform: rotate(30deg);
}
.control-btn:active {
transform: scale(0.95);
}
#upBtn {
border-color: #22c55e;
box-shadow: 0 0 15px rgba(34, 197, 94, 0.5);
}
#downBtn {
border-color: #3b82f6;
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
}
#leftBtn {
border-color: #f97316;
box-shadow: 0 0 15px rgba(249, 115, 22, 0.5);
}
#rightBtn {
border-color: #d946ef;
box-shadow: 0 0 15px rgba(217, 70, 239, 0.5);
}
.score-box {
background: rgba(30, 41, 59, 0.7);
border: 2px solid #6366f1;
border-radius: 10px;
box-shadow: 0 0 15px rgba(99, 102, 241, 0.5);
padding: 10px 20px;
position: relative;
overflow: hidden;
}
.score-box::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg,
#ef4444, #f97316, #eab308, #22c55e,
#3b82f6, #6366f1, #8b5cf6, #d946ef);
}
.trail {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
pointer-events: none;
z-index: 5;
}
.power-up {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: #eab308;
box-shadow: 0 0 10px #eab308, 0 0 20px rgba(234, 179, 8, 0.7);
animation: float 3s ease-in-out infinite;
z-index: 5;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.power-up::after {
content: '';
position: absolute;
top: -5px;
left: -5px;
right: -5px;
bottom: -5px;
border: 2px solid #eab308;
border-radius: 50%;
animation: pulse-ring 2s ease-out infinite;
opacity: 0;
}
@keyframes pulse-ring {
0% { transform: scale(0.8); opacity: 0.8; }
100% { transform: scale(1.5); opacity: 0; }
}
</style>
</head>
<body class="min-h-screen flex flex-col items-center justify-center p-4">
<div class="text-center mb-8">
<h1 class="text-4xl md:text-5xl font-bold rainbow-text mb-4">RAINBOW SNAKE</h1>
<div class="flex justify-center gap-8 mb-6">
<div class="score-box">
<p class="text-sm text-blue-300">SCORE</p>
<p id="score" class="text-2xl text-white">0</p>
</div>
<div class="score-box">
<p class="text-sm text-blue-300">HIGH SCORE</p>
<p id="highScore" class="text-2xl text-white">0</p>
</div>
</div>
</div>
<div class="relative">
<canvas id="gameCanvas" width="400" height="400" class="bg-gray-900"></canvas>
<div id="gameOver" class="hidden absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 rounded">
<h2 class="text-red-500 text-3xl mb-6 rainbow-text">GAME OVER!</h2>
<p class="text-blue-300 mb-4">Your score: <span id="finalScore" class="text-white">0</span></p>
<p class="text-blue-300 mb-8">High score: <span id="finalHighScore" class="text-white">0</span></p>
<button id="restartButton" class="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-full neon-button font-bold glow">
<i class="fas fa-redo mr-2"></i> PLAY AGAIN
</button>
</div>
<div id="startScreen" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 rounded">
<h2 class="rainbow-text text-3xl mb-6 pulse">RAINBOW SNAKE</h2>
<p class="text-blue-300 mb-8 text-center px-4">Eat the food to grow longer! Collect power-ups for bonuses!</p>
<button id="startButton" class="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-full neon-button font-bold glow">
<i class="fas fa-play mr-2"></i> START GAME
</button>
</div>
</div>
<div class="mt-6 grid grid-cols-3 gap-4 controls" id="mobileControls">
<div></div>
<button class="control-btn" id="upBtn"><i class="fas fa-arrow-up"></i></button>
<div></div>
<button class="control-btn" id="leftBtn"><i class="fas fa-arrow-left"></i></button>
<div class="flex items-center justify-center">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-white">
<i class="fas fa-gamepad"></i>
</div>
</div>
<button class="control-btn" id="rightBtn"><i class="fas fa-arrow-right"></i></button>
<div></div>
<button class="control-btn" id="downBtn"><i class="fas fa-arrow-down"></i></button>
<div></div>
</div>
<div class="mt-4 text-blue-300 text-sm text-center">
<p>Controls: Arrow Keys, WASD, or Touch Buttons</p>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const highScoreElement = document.getElementById('highScore');
const finalScoreElement = document.getElementById('finalScore');
const finalHighScoreElement = document.getElementById('finalHighScore');
const gameOverScreen = document.getElementById('gameOver');
const startScreen = document.getElementById('startScreen');
const startButton = document.getElementById('startButton');
const restartButton = document.getElementById('restartButton');
const mobileControls = document.getElementById('mobileControls');
// Game settings
const gridSize = 20;
const tileCount = canvas.width / gridSize;
let speed = 7;
// Game state
let snake = [{x: 10, y: 10}];
let food = {x: 5, y: 5};
let powerUps = [];
let direction = {x: 0, y: 0};
let nextDirection = {x: 0, y: 0};
let score = 0;
let highScore = localStorage.getItem('snakeHighScore') || 0;
let gameRunning = false;
let gameLoop;
let particles = [];
let trails = [];
let lastRenderTime = 0;
let powerUpTimer = 0;
let invincible = false;
let rainbowMode = false;
let rainbowTimer = 0;
highScoreElement.textContent = highScore;
// Color palette
const colors = [
'#ef4444', // red
'#f97316', // orange
'#eab308', // yellow
'#22c55e', // green
'#3b82f6', // blue
'#6366f1', // indigo
'#8b5cf6', // violet
'#d946ef' // pink
];
// Visual effects
function createParticles(x, y, color, count = 15) {
for (let i = 0; i < count; i++) {
const size = Math.random() * 4 + 2;
particles.push({
x: x,
y: y,
size: size,
color: color || colors[Math.floor(Math.random() * colors.length)],
speedX: Math.random() * 8 - 4,
speedY: Math.random() * 8 - 4,
life: 30 + Math.random() * 20,
shrink: size * 0.05
});
}
}
function createTrail(x, y) {
if (Math.random() > 0.3) return; // Limit trail density
trails.push({
x: x * gridSize + gridSize / 2,
y: y * gridSize + gridSize / 2,
size: Math.random() * 10 + 5,
opacity: 0.7,
fade: 0.02
});
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.speedX;
p.y += p.speedY;
p.life--;
p.size -= p.shrink;
if (p.life <= 0 || p.size <= 0) {
particles.splice(i, 1);
}
}
for (let i = trails.length - 1; i >= 0; i--) {
const t = trails[i];
t.opacity -= t.fade;
if (t.opacity <= 0) {
trails.splice(i, 1);
}
}
}
function drawParticles() {
// Draw trails first (under everything)
trails.forEach(t => {
ctx.globalAlpha = t.opacity;
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.beginPath();
ctx.arc(t.x, t.y, t.size, 0, Math.PI * 2);
ctx.fill();
});
// Then draw particles
particles.forEach(p => {
ctx.globalAlpha = p.life / 50;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
}
// Draw functions
function drawSnake() {
snake.forEach((segment, index) => {
// Create gradient based on position or rainbow mode
let gradient;
const x = segment.x * gridSize;
const y = segment.y * gridSize;
if (rainbowMode) {
// Rainbow effect for all segments
gradient = ctx.createLinearGradient(x, y, x + gridSize, y + gridSize);
gradient.addColorStop(0, colors[index % colors.length]);
gradient.addColorStop(1, colors[(index + 4) % colors.length]);
} else {
// Head is a different color
if (index === 0) {
gradient = ctx.createRadialGradient(
x + gridSize / 2,
y + gridSize / 2,
0,
x + gridSize / 2,
y + gridSize / 2,
gridSize
);
gradient.addColorStop(0, '#3b82f6');
gradient.addColorStop(1, 'rgba(59, 130, 246, 0.2)');
} else {
// Body segments get progressively darker
const intensity = 200 - Math.min(190, index * 5);
gradient = `rgb(59, 130, ${intensity})`;
}
}
ctx.fillStyle = gradient;
// Draw rounded rectangle for snake segments
const radius = gridSize / 4;
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + gridSize - radius, y);
ctx.quadraticCurveTo(x + gridSize, y, x + gridSize, y + radius);
ctx.lineTo(x + gridSize, y + gridSize - radius);
ctx.quadraticCurveTo(x + gridSize, y + gridSize, x + gridSize - radius, y + gridSize);
ctx.lineTo(x + radius, y + gridSize);
ctx.quadraticCurveTo(x, y + gridSize, x, y + gridSize - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
// Add glow effect to head
if (index === 0) {
ctx.shadowColor = rainbowMode ? colors[Math.floor(Math.random() * colors.length)] : '#3b82f6';
ctx.shadowBlur = 15;
ctx.fill();
ctx.shadowBlur = 0;
}
// Create trail effect
if (gameRunning && index === 0) {
createTrail(segment.x, segment.y);
}
});
}
function drawFood() {
const x = food.x * gridSize + gridSize / 2;
const y = food.y * gridSize + gridSize / 2;
const radius = gridSize / 2;
// Outer glow
ctx.shadowColor = '#ef4444';
ctx.shadowBlur = 15;
// Main food circle with gradient
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, '#ef4444');
gradient.addColorStop(1, '#7f1d1d');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
// Inner highlight
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.beginPath();
ctx.arc(x - radius/3, y - radius/3, radius/4, 0, Math.PI * 2);
ctx.fill();
// Spikes
const spikeCount = 8;
for (let i = 0; i < spikeCount; i++) {
const angle = (i / spikeCount) * Math.PI * 2;
const spikeLength = radius * 0.6;
const spikeWidth = radius * 0.2;
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.moveTo(
x + Math.cos(angle) * radius,
y + Math.sin(angle) * radius
);
ctx.lineTo(
x + Math.cos(angle) * (radius + spikeLength),
y + Math.sin(angle) * (radius + spikeLength)
);
ctx.lineTo(
x + Math.cos(angle + 0.2) * (radius + spikeWidth),
y + Math.sin(angle + 0.2) * (radius + spikeWidth)
);
ctx.closePath();
ctx.fill();
}
// Reset shadow
ctx.shadowBlur = 0;
}
function drawPowerUps() {
powerUps.forEach(pu => {
const x = pu.x * gridSize + gridSize / 2;
const y = pu.y * gridSize + gridSize / 2;
const radius = gridSize / 2;
// Glowing effect
ctx.shadowColor = '#eab308';
ctx.shadowBlur = 15;
// Main circle
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, '#eab308');
gradient.addColorStop(1, '#713f12');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
// Star shape
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.beginPath();
drawStar(x, y, 5, radius * 0.5, radius * 0.2);
ctx.fill();
// Reset shadow
ctx.shadowBlur = 0;
});
}
function drawStar(cx, cy, spikes, outerRadius, innerRadius) {
let rot = Math.PI / 2 * 3;
let x = cx;
let y = cy;
let step = Math.PI / spikes;
ctx.beginPath();
ctx.moveTo(cx, cy - outerRadius);
for (let i = 0; i < spikes; i++) {
x = cx + Math.cos(rot) * outerRadius;
y = cy + Math.sin(rot) * outerRadius;
ctx.lineTo(x, y);
rot += step;
x = cx + Math.cos(rot) * innerRadius;
y = cy + Math.sin(rot) * innerRadius;
ctx.lineTo(x, y);
rot += step;
}
ctx.lineTo(cx, cy - outerRadius);
ctx.closePath();
}
function drawGrid() {
ctx.strokeStyle = 'rgba(30, 41, 59, 0.5)';
ctx.lineWidth = 0.5;
for (let i = 0; i < tileCount; i++) {
// Vertical lines
ctx.beginPath();
ctx.moveTo(i * gridSize, 0);
ctx.lineTo(i * gridSize, canvas.height);
ctx.stroke();
// Horizontal lines
ctx.beginPath();
ctx.moveTo(0, i * gridSize);
ctx.lineTo(canvas.width, i * gridSize);
ctx.stroke();
}
}
function drawBackground() {
// Gradient background
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#0f172a');
gradient.addColorStop(1, '#1e293b');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Add subtle star effect
if (Math.random() < 0.02) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = Math.random() * 2 + 1;
const opacity = Math.random() * 0.5 + 0.3;
ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
}
// Game logic
function update() {
// Update direction from the next direction buffer
direction = {...nextDirection};
// Move snake
const head = {x: snake[0].x + direction.x, y: snake[0].y + direction.y};
// Check wall collision (unless invincible)
if (!invincible && (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount)) {
createParticles(
head.x * gridSize + gridSize / 2,
head.y * gridSize + gridSize / 2,
'#ef4444',
30
);
gameOver();
return;
}
// Wrap around if invincible
if (invincible) {
if (head.x < 0) head.x = tileCount - 1;
if (head.x >= tileCount) head.x = 0;
if (head.y < 0) head.y = tileCount - 1;
if (head.y >= tileCount) head.y = 0;
}
// Check self collision (unless invincible)
if (!invincible) {
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
createParticles(
head.x * gridSize + gridSize / 2,
head.y * gridSize + gridSize / 2,
'#ef4444',
30
);
gameOver();
return;
}
}
}
// Add new head
snake.unshift(head);
// Check food collision
if (head.x === food.x && head.y === food.y) {
// Create food particles
createParticles(
food.x * gridSize + gridSize / 2,
food.y * gridSize + gridSize / 2,
null, // Random color
20
);
// Increase score
score += 10;
scoreElement.textContent = score;
// Generate new food (not on snake or power-ups)
generateFood();
// Increase speed slightly every 50 points
if (score % 50 === 0 && speed < 15) {
speed += 0.5;
}
// Chance to spawn power-up
if (Math.random() < 0.3 && powerUps.length < 2) {
generatePowerUp();
}
} else {
// Remove tail if no food eaten
snake.pop();
}
// Check power-up collision
for (let i = powerUps.length - 1; i >= 0; i--) {
const pu = powerUps[i];
if (head.x === pu.x && head.y === pu.y) {
// Apply power-up effect
applyPowerUp(pu.type);
// Remove power-up
powerUps.splice(i, 1);
// Create particles
createParticles(
pu.x * gridSize + gridSize / 2,
pu.y * gridSize + gridSize / 2,
'#eab308',
25
);
}
}
// Update power-up timer
if (powerUpTimer > 0) {
powerUpTimer--;
if (powerUpTimer === 0) {
invincible = false;
rainbowMode = false;
}
}
// Update rainbow timer
if (rainbowTimer > 0) {
rainbowTimer--;
if (rainbowTimer === 0) {
rainbowMode = false;
}
}
}
function applyPowerUp(type) {
switch(type) {
case 'invincible':
invincible = true;
powerUpTimer = 300; // 5 seconds at 60fps
break;
case 'rainbow':
rainbowMode = true;
rainbowTimer = 450; // 7.5 seconds at 60fps
break;
case 'speed':
speed = Math.max(5, speed - 2); // Slow down
powerUpTimer = 300;
break;
}
// Add score bonus
score += 50;
scoreElement.textContent = score;
}
function generateFood() {
let validPosition = false;
while (!validPosition) {
food = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
};
// Check if food is on snake or power-ups
validPosition = true;
for (let segment of snake) {
if (segment.x === food.x && segment.y === food.y) {
validPosition = false;
break;
}
}
if (validPosition) {
for (let pu of powerUps) {
if (pu.x === food.x && pu.y === food.y) {
validPosition = false;
break;
}
}
}
}
}
function generatePowerUp() {
let validPosition = false;
const types = ['invincible', 'rainbow', 'speed'];
const type = types[Math.floor(Math.random() * types.length)];
while (!validPosition) {
const pu = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount),
type: type
};
// Check if power-up is on snake or food
validPosition = true;
for (let segment of snake) {
if (segment.x === pu.x && segment.y === pu.y) {
validPosition = false;
break;
}
}
if (validPosition && (food.x === pu.x && food.y === pu.y)) {
validPosition = false;
}
if (validPosition) {
for (let existingPu of powerUps) {
if (existingPu.x === pu.x && existingPu.y === pu.y) {
validPosition = false;
break;
}
}
}
if (validPosition) {
powerUps.push(pu);
break;
}
}
}
function gameOver() {
cancelAnimationFrame(gameLoop);
gameRunning = false;
// Update high score
if (score > highScore) {
highScore = score;
localStorage.setItem('snakeHighScore', highScore);
highScoreElement.textContent = highScore;
}
finalScoreElement.textContent = score;
finalHighScoreElement.textContent = highScore;
gameOverScreen.classList.remove('hidden');
}
function resetGame() {
snake = [{x: 10, y: 10}];
direction = {x: 0, y: 0};
nextDirection = {x: 0, y: 0};
score = 0;
scoreElement.textContent = score;
speed = 7;
powerUps = [];
particles = [];
trails = [];
invincible = false;
rainbowMode = false;
powerUpTimer = 0;
rainbowTimer = 0;
generateFood();
gameOverScreen.classList.add('hidden');
}
function gameLoop(timestamp) {
if (!gameRunning) return;
const secondsSinceLastRender = (timestamp - lastRenderTime) / 1000;
if (secondsSinceLastRender < 1 / speed) {
requestAnimationFrame(gameLoop);
return;
}
lastRenderTime = timestamp;
drawBackground();
drawGrid();
updateParticles();
drawParticles();
drawPowerUps();
drawFood();
drawSnake();
update();
requestAnimationFrame(gameLoop);
}
function startGame() {
resetGame();
startScreen.classList.add('hidden');
gameRunning = true;
lastRenderTime = 0;
requestAnimationFrame(gameLoop);
}
// Event listeners
document.addEventListener('keydown', (e) => {
if (!gameRunning && (e.key === ' ' || e.key === 'Enter')) {
startGame();
return;
}
// Prevent reverse direction
switch (e.key) {
case 'ArrowUp':
case 'w':
case 'W':
if (direction.y === 0) nextDirection = {x: 0, y: -1};
break;
case 'ArrowDown':
case 's':
case 'S':
if (direction.y === 0) nextDirection = {x: 0, y: 1};
break;
case 'ArrowLeft':
case 'a':
case 'A':
if (direction.x === 0) nextDirection = {x: -1, y: 0};
break;
case 'ArrowRight':
case 'd':
case 'D':
if (direction.x === 0) nextDirection = {x: 1, y: 0};
break;
}
});
// Mobile control buttons
document.getElementById('upBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
if (direction.y === 0) nextDirection = {x: 0, y: -1};
});
document.getElementById('downBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
if (direction.y === 0) nextDirection = {x: 0, y: 1};
});
document.getElementById('leftBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
if (direction.x === 0) nextDirection = {x: -1, y: 0};
});
document.getElementById('rightBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
if (direction.x === 0) nextDirection = {x: 1, y: 0};
});
// Mouse controls for desktop (click to change direction)
canvas.addEventListener('click', (e) => {
if (!gameRunning) return;
const rect = canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const headX = snake[0].x * gridSize + gridSize / 2;
const headY = snake[0].y * gridSize + gridSize / 2;
const diffX = clickX - headX;
const diffY = clickY - headY;
// Determine direction based on click position relative to head
if (Math.abs(diffX) > Math.abs(diffY)) {
// Horizontal movement
if (diffX > 0 && direction.x === 0) {
nextDirection = {x: 1, y: 0}; // Right
} else if (diffX < 0 && direction.x === 0) {
nextDirection = {x: -1, y: 0}; // Left
}
} else {
// Vertical movement
if (diffY > 0 && direction.y === 0) {
nextDirection = {x: 0, y: 1}; // Down
} else if (diffY < 0 && direction.y === 0) {
nextDirection = {x: 0, y: -1}; // Up
}
}
});
// Touch controls for mobile (swipe)
let touchStartX = 0;
let touchStartY = 0;
canvas.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
if (!touchStartX || !touchStartY || !gameRunning) return;
const touchEndX = e.touches[0].clientX;
const touchEndY = e.touches[0].clientY;
const diffX = touchStartX - touchEndX;
const diffY = touchStartY - touchEndY;
// Determine swipe direction
if (Math.abs(diffX) > Math.abs(diffY)) {
// Horizontal swipe
if (diffX > 30 && direction.x === 0) {
nextDirection = {x: -1, y: 0}; // Left swipe
touchStartX = 0;
touchStartY = 0;
} else if (diffX < -30 && direction.x === 0) {
nextDirection = {x: 1, y: 0}; // Right swipe
touchStartX = 0;
touchStartY = 0;
}
} else {
// Vertical swipe
if (diffY > 30 && direction.y === 0) {
nextDirection = {x: 0, y: -1}; // Up swipe
touchStartX = 0;
touchStartY = 0;
} else if (diffY < -30 && direction.y === 0) {
nextDirection = {x: 0, y: 1}; // Down swipe
touchStartX = 0;
touchStartY = 0;
}
}
e.preventDefault();
}, { passive: false });
// Button event listeners
startButton.addEventListener('click', startGame);
restartButton.addEventListener('click', startGame);
// Keyboard controls for buttons (accessibility)
startButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
startGame();
}
});
restartButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
startGame();
}
});
// Initial draw
drawBackground();
drawGrid();
drawFood();
drawSnake();
// Show mobile controls only on touch devices
if ('ontouchstart' in window || navigator.maxTouchPoints) {
mobileControls.classList.remove('hidden');
} else {
mobileControls.classList.add('hidden');
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=pijou/snake" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>