|
<!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'); |
|
|
|
|
|
const gridSize = 20; |
|
const tileCount = canvas.width / gridSize; |
|
let speed = 7; |
|
|
|
|
|
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; |
|
|
|
|
|
const colors = [ |
|
'#ef4444', |
|
'#f97316', |
|
'#eab308', |
|
'#22c55e', |
|
'#3b82f6', |
|
'#6366f1', |
|
'#8b5cf6', |
|
'#d946ef' |
|
]; |
|
|
|
|
|
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; |
|
|
|
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() { |
|
|
|
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(); |
|
}); |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
function drawSnake() { |
|
snake.forEach((segment, index) => { |
|
|
|
let gradient; |
|
const x = segment.x * gridSize; |
|
const y = segment.y * gridSize; |
|
|
|
if (rainbowMode) { |
|
|
|
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 { |
|
|
|
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 { |
|
|
|
const intensity = 200 - Math.min(190, index * 5); |
|
gradient = `rgb(59, 130, ${intensity})`; |
|
} |
|
} |
|
|
|
ctx.fillStyle = gradient; |
|
|
|
|
|
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(); |
|
|
|
|
|
if (index === 0) { |
|
ctx.shadowColor = rainbowMode ? colors[Math.floor(Math.random() * colors.length)] : '#3b82f6'; |
|
ctx.shadowBlur = 15; |
|
ctx.fill(); |
|
ctx.shadowBlur = 0; |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
ctx.shadowColor = '#ef4444'; |
|
ctx.shadowBlur = 15; |
|
|
|
|
|
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(); |
|
|
|
|
|
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(); |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
ctx.shadowColor = '#eab308'; |
|
ctx.shadowBlur = 15; |
|
|
|
|
|
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(); |
|
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; |
|
ctx.beginPath(); |
|
drawStar(x, y, 5, radius * 0.5, radius * 0.2); |
|
ctx.fill(); |
|
|
|
|
|
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++) { |
|
|
|
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(); |
|
} |
|
} |
|
|
|
function drawBackground() { |
|
|
|
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); |
|
|
|
|
|
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(); |
|
} |
|
} |
|
|
|
|
|
function update() { |
|
|
|
direction = {...nextDirection}; |
|
|
|
|
|
const head = {x: snake[0].x + direction.x, y: snake[0].y + direction.y}; |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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; |
|
} |
|
} |
|
} |
|
|
|
|
|
snake.unshift(head); |
|
|
|
|
|
if (head.x === food.x && head.y === food.y) { |
|
|
|
createParticles( |
|
food.x * gridSize + gridSize / 2, |
|
food.y * gridSize + gridSize / 2, |
|
null, |
|
20 |
|
); |
|
|
|
|
|
score += 10; |
|
scoreElement.textContent = score; |
|
|
|
|
|
generateFood(); |
|
|
|
|
|
if (score % 50 === 0 && speed < 15) { |
|
speed += 0.5; |
|
} |
|
|
|
|
|
if (Math.random() < 0.3 && powerUps.length < 2) { |
|
generatePowerUp(); |
|
} |
|
} else { |
|
|
|
snake.pop(); |
|
} |
|
|
|
|
|
for (let i = powerUps.length - 1; i >= 0; i--) { |
|
const pu = powerUps[i]; |
|
if (head.x === pu.x && head.y === pu.y) { |
|
|
|
applyPowerUp(pu.type); |
|
|
|
|
|
powerUps.splice(i, 1); |
|
|
|
|
|
createParticles( |
|
pu.x * gridSize + gridSize / 2, |
|
pu.y * gridSize + gridSize / 2, |
|
'#eab308', |
|
25 |
|
); |
|
} |
|
} |
|
|
|
|
|
if (powerUpTimer > 0) { |
|
powerUpTimer--; |
|
if (powerUpTimer === 0) { |
|
invincible = false; |
|
rainbowMode = false; |
|
} |
|
} |
|
|
|
|
|
if (rainbowTimer > 0) { |
|
rainbowTimer--; |
|
if (rainbowTimer === 0) { |
|
rainbowMode = false; |
|
} |
|
} |
|
} |
|
|
|
function applyPowerUp(type) { |
|
switch(type) { |
|
case 'invincible': |
|
invincible = true; |
|
powerUpTimer = 300; |
|
break; |
|
case 'rainbow': |
|
rainbowMode = true; |
|
rainbowTimer = 450; |
|
break; |
|
case 'speed': |
|
speed = Math.max(5, speed - 2); |
|
powerUpTimer = 300; |
|
break; |
|
} |
|
|
|
|
|
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) |
|
}; |
|
|
|
|
|
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 |
|
}; |
|
|
|
|
|
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; |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
if (!gameRunning && (e.key === ' ' || e.key === 'Enter')) { |
|
startGame(); |
|
return; |
|
} |
|
|
|
|
|
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; |
|
} |
|
}); |
|
|
|
|
|
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}; |
|
}); |
|
|
|
|
|
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; |
|
|
|
|
|
if (Math.abs(diffX) > Math.abs(diffY)) { |
|
|
|
if (diffX > 0 && direction.x === 0) { |
|
nextDirection = {x: 1, y: 0}; |
|
} else if (diffX < 0 && direction.x === 0) { |
|
nextDirection = {x: -1, y: 0}; |
|
} |
|
} else { |
|
|
|
if (diffY > 0 && direction.y === 0) { |
|
nextDirection = {x: 0, y: 1}; |
|
} else if (diffY < 0 && direction.y === 0) { |
|
nextDirection = {x: 0, y: -1}; |
|
} |
|
} |
|
}); |
|
|
|
|
|
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; |
|
|
|
|
|
if (Math.abs(diffX) > Math.abs(diffY)) { |
|
|
|
if (diffX > 30 && direction.x === 0) { |
|
nextDirection = {x: -1, y: 0}; |
|
touchStartX = 0; |
|
touchStartY = 0; |
|
} else if (diffX < -30 && direction.x === 0) { |
|
nextDirection = {x: 1, y: 0}; |
|
touchStartX = 0; |
|
touchStartY = 0; |
|
} |
|
} else { |
|
|
|
if (diffY > 30 && direction.y === 0) { |
|
nextDirection = {x: 0, y: -1}; |
|
touchStartX = 0; |
|
touchStartY = 0; |
|
} else if (diffY < -30 && direction.y === 0) { |
|
nextDirection = {x: 0, y: 1}; |
|
touchStartX = 0; |
|
touchStartY = 0; |
|
} |
|
} |
|
|
|
e.preventDefault(); |
|
}, { passive: false }); |
|
|
|
|
|
startButton.addEventListener('click', startGame); |
|
restartButton.addEventListener('click', startGame); |
|
|
|
|
|
startButton.addEventListener('keydown', (e) => { |
|
if (e.key === 'Enter' || e.key === ' ') { |
|
startGame(); |
|
} |
|
}); |
|
|
|
restartButton.addEventListener('keydown', (e) => { |
|
if (e.key === 'Enter' || e.key === ' ') { |
|
startGame(); |
|
} |
|
}); |
|
|
|
|
|
drawBackground(); |
|
drawGrid(); |
|
drawFood(); |
|
drawSnake(); |
|
|
|
|
|
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> |