Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Neon Pong AI Challenge</title> | |
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Press+Start+2P&display=swap" rel="stylesheet"> | |
<style> | |
:root { | |
--neon-blue: #08f; | |
--neon-pink: #f0f; | |
--neon-purple: #90f; | |
--bg-dark: #111; | |
--bg-darker: #070710; | |
--text: #fff; | |
--glow: 0 0 10px; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: 'Orbitron', sans-serif; | |
background: var(--bg-darker); | |
color: var(--text); | |
min-height: 100vh; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
overflow: hidden; | |
position: relative; | |
} | |
body::before { | |
content: ""; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: radial-gradient(circle at center, transparent 0%, var(--bg-dark) 80%); | |
pointer-events: none; | |
z-index: -2; | |
} | |
.game-title { | |
font-family: 'Press Start 2P', cursive; | |
font-size: 2.5rem; | |
margin-bottom: 20px; | |
text-align: center; | |
color: var(--neon-blue); | |
text-shadow: var(--glow) var(--neon-blue), 0 0 20px var(--neon-blue); | |
animation: pulse 2s infinite alternate; | |
} | |
@keyframes pulse { | |
from { | |
opacity: 1; | |
} | |
to { | |
opacity: 0.7; | |
} | |
} | |
.game-container { | |
position: relative; | |
width: 800px; | |
height: 500px; | |
margin: 20px auto; | |
border: 3px solid var(--neon-purple); | |
box-shadow: var(--glow) var(--neon-purple), inset var(--glow) var(--neon-purple); | |
border-radius: 10px; | |
overflow: hidden; | |
background: var(--bg-dark); | |
} | |
#gameCanvas { | |
display: block; | |
width: 100%; | |
height: 100%; | |
} | |
.center-line { | |
position: absolute; | |
top: 0; | |
left: 50%; | |
width: 2px; | |
height: 100%; | |
background: linear-gradient(transparent 0%, var(--neon-purple) 50%, transparent 100%); | |
opacity: 0.5; | |
} | |
.score-container { | |
display: flex; | |
justify-content: space-between; | |
width: 800px; | |
margin-bottom: 20px; | |
} | |
.score-box { | |
background: rgba(0, 0, 0, 0.5); | |
border: 2px solid var(--neon-blue); | |
border-radius: 10px; | |
padding: 15px 30px; | |
text-align: center; | |
box-shadow: var(--glow) var(--neon-blue); | |
} | |
.score-title { | |
font-size: 1rem; | |
margin-bottom: 5px; | |
color: var(--neon-blue); | |
} | |
.score { | |
font-size: 2rem; | |
font-family: 'Press Start 2P', cursive; | |
} | |
.controls { | |
margin-top: 20px; | |
display: flex; | |
gap: 15px; | |
} | |
.btn { | |
background: transparent; | |
color: var(--text); | |
border: 2px solid var(--neon-pink); | |
padding: 10px 20px; | |
font-family: 'Orbitron', sans-serif; | |
font-size: 1rem; | |
border-radius: 5px; | |
cursor: pointer; | |
transition: all 0.3s; | |
box-shadow: 0 0 5px var(--neon-pink); | |
} | |
.btn:hover { | |
background: var(--neon-pink); | |
color: var(--bg-dark); | |
box-shadow: 0 0 15px var(--neon-pink); | |
transform: translateY(-2px); | |
} | |
.btn:active { | |
transform: translateY(0); | |
} | |
.btn-start { | |
border-color: var(--neon-blue); | |
box-shadow: 0 0 5px var(--neon-blue); | |
} | |
.btn-start:hover { | |
background: var(--neon-blue); | |
color: var(--bg-dark); | |
box-shadow: 0 0 15px var(--neon-blue); | |
} | |
.settings { | |
margin-top: 30px; | |
width: 800px; | |
background: rgba(0, 0, 0, 0.3); | |
border: 1px solid var(--neon-purple); | |
border-radius: 10px; | |
padding: 15px; | |
} | |
.settings-title { | |
color: var(--neon-purple); | |
margin-bottom: 10px; | |
font-size: 1.2rem; | |
text-shadow: 0 0 5px var(--neon-purple); | |
} | |
.slider-container { | |
margin-bottom: 10px; | |
} | |
.slider-label { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 5px; | |
} | |
.slider { | |
width: 100%; | |
-webkit-appearance: none; | |
height: 8px; | |
background: rgba(255, 255, 255, 0.1); | |
outline: none; | |
border-radius: 4px; | |
} | |
.slider::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
appearance: none; | |
width: 18px; | |
height: 18px; | |
border-radius: 50%; | |
background: var(--neon-blue); | |
cursor: pointer; | |
box-shadow: 0 0 5px var(--neon-blue); | |
} | |
.match-history { | |
margin-top: 30px; | |
width: 800px; | |
background: rgba(0, 0, 0, 0.3); | |
border: 1px solid var(--neon-pink); | |
border-radius: 10px; | |
padding: 15px; | |
max-height: 200px; | |
overflow-y: auto; | |
} | |
.history-title { | |
color: var(--neon-pink); | |
margin-bottom: 10px; | |
font-size: 1.2rem; | |
text-shadow: 0 0 5px var(--neon-pink); | |
} | |
.history-item { | |
padding: 8px 0; | |
border-bottom: 1px dashed var(--neon-pink); | |
opacity: 0; | |
animation: fadeIn 0.5s forwards; | |
} | |
@keyframes fadeIn { | |
to { | |
opacity: 1; | |
} | |
} | |
.particle { | |
position: absolute; | |
border-radius: 50%; | |
pointer-events: none; | |
z-index: -1; | |
} | |
.mobile-controls { | |
display: none; | |
width: 100%; | |
padding: 20px; | |
justify-content: space-between; | |
} | |
.mobile-btn { | |
width: 100px; | |
height: 60px; | |
background: rgba(144, 0, 255, 0.2); | |
border: 2px solid var(--neon-purple); | |
color: white; | |
font-family: 'Orbitron', sans-serif; | |
border-radius: 10px; | |
box-shadow: 0 0 10px var(--neon-purple); | |
} | |
@media (max-width: 850px) { | |
.game-container, .score-container, .settings, .match-history { | |
width: 95%; | |
max-width: 400px; | |
} | |
.game-title { | |
font-size: 1.5rem; | |
} | |
.mobile-controls { | |
display: flex; | |
} | |
} | |
@media (max-width: 500px) { | |
.score-box { | |
padding: 10px 15px; | |
} | |
.score-title { | |
font-size: 0.8rem; | |
} | |
.score { | |
font-size: 1.5rem; | |
} | |
} | |
.game-message { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background: rgba(0, 0, 0, 0.8); | |
color: var(--neon-blue); | |
padding: 20px 40px; | |
border-radius: 10px; | |
font-size: 1.5rem; | |
text-align: center; | |
opacity: 0; | |
pointer-events: none; | |
transition: opacity 0.3s; | |
border: 2px solid var(--neon-blue); | |
box-shadow: 0 0 20px var(--neon-blue); | |
z-index: 10; | |
} | |
.game-message.show { | |
opacity: 1; | |
animation: pulse 1.5s infinite; | |
} | |
</style> | |
</head> | |
<body> | |
<h1 class="game-title">NEON PONG AI CHALLENGE</h1> | |
<div class="score-container"> | |
<div class="score-box"> | |
<div class="score-title">PLAYER</div> | |
<div class="score" id="playerScore">0</div> | |
</div> | |
<div class="score-box"> | |
<div class="score-title">AI</div> | |
<div class="score" id="aiScore">0</div> | |
</div> | |
</div> | |
<div class="game-container"> | |
<div class="center-line"></div> | |
<canvas id="gameCanvas"></canvas> | |
<div class="game-message" id="gameMessage">GAME OVER<br><span style="font-size:1rem">Click Start to Play Again</span></div> | |
</div> | |
<div class="mobile-controls"> | |
<button class="mobile-btn" id="mobileUp">↑</button> | |
<button class="mobile-btn" id="mobileDown">↓</button> | |
</div> | |
<div class="controls"> | |
<button class="btn btn-start" id="startBtn">START</button> | |
<button class="btn" id="pauseBtn">PAUSE</button> | |
<button class="btn" id="resetBtn">RESET</button> | |
</div> | |
<div class="settings"> | |
<h3 class="settings-title">GAME SETTINGS</h3> | |
<div class="slider-container"> | |
<div class="slider-label"> | |
<span>Ball Speed</span> | |
<span id="ballSpeedValue">5</span> | |
</div> | |
<input type="range" min="1" max="10" value="5" class="slider" id="ballSpeedSlider"> | |
</div> | |
<div class="slider-container"> | |
<div class="slider-label"> | |
<span>AI Difficulty</span> | |
<span id="aiDifficultyValue">5</span> | |
</div> | |
<input type="range" min="1" max="10" value="5" class="slider" id="aiDifficultySlider"> | |
</div> | |
</div> | |
<div class="match-history"> | |
<h3 class="history-title">MATCH HISTORY</h3> | |
<div id="historyList"></div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Canvas setup | |
const canvas = document.getElementById('gameCanvas'); | |
const ctx = canvas.getContext('2d'); | |
canvas.width = canvas.offsetWidth; | |
canvas.height = canvas.offsetHeight; | |
// Game elements | |
const paddleWidth = 15; | |
const paddleHeight = 100; | |
const ballSize = 12; | |
const borderOffset = 20; | |
const maxAngle = Math.PI / 3; // 60 degrees max bounce angle | |
// Game state | |
const state = { | |
playerScore: 0, | |
aiScore: 0, | |
gameRunning: false, | |
gamePaused: false, | |
winner: null, | |
history: [], | |
ballSpeed: 5, | |
aiDifficulty: 5 | |
}; | |
// Game objects | |
const player = { | |
x: borderOffset, | |
y: canvas.height / 2 - paddleHeight / 2, | |
width: paddleWidth, | |
height: paddleHeight, | |
speed: 8, | |
color: '#08f', | |
movingUp: false, | |
movingDown: false | |
}; | |
const ai = { | |
x: canvas.width - borderOffset - paddleWidth, | |
y: canvas.height / 2 - paddleHeight / 2, | |
width: paddleWidth, | |
height: paddleHeight, | |
speed: 6, | |
color: '#f0f', | |
reactionDelay: 150 // ms | |
}; | |
const ball = { | |
x: canvas.width / 2, | |
y: canvas.height / 2, | |
size: ballSize, | |
speedX: 0, | |
speedY: 0, | |
color: '#fff', | |
maxSpeed: 15 | |
}; | |
// DOM elements | |
const playerScoreDisplay = document.getElementById('playerScore'); | |
const aiScoreDisplay = document.getElementById('aiScore'); | |
const startBtn = document.getElementById('startBtn'); | |
const pauseBtn = document.getElementById('pauseBtn'); | |
const resetBtn = document.getElementById('resetBtn'); | |
const ballSpeedSlider = document.getElementById('ballSpeedSlider'); | |
const ballSpeedValue = document.getElementById('ballSpeedValue'); | |
const aiDifficultySlider = document.getElementById('aiDifficultySlider'); | |
const aiDifficultyValue = document.getElementById('aiDifficultyValue'); | |
const historyList = document.getElementById('historyList'); | |
const gameMessage = document.getElementById('gameMessage'); | |
const mobileUpBtn = document.getElementById('mobileUp'); | |
const mobileDownBtn = document.getElementById('mobileDown'); | |
// Event listeners | |
startBtn.addEventListener('click', startGame); | |
pauseBtn.addEventListener('click', togglePause); | |
resetBtn.addEventListener('click', resetGame); | |
ballSpeedSlider.addEventListener('input', updateBallSpeed); | |
aiDifficultySlider.addEventListener('input', updateAiDifficulty); | |
// Mobile controls | |
mobileUpBtn.addEventListener('touchstart', () => player.movingUp = true); | |
mobileUpBtn.addEventListener('touchend', () => player.movingUp = false); | |
mobileUpBtn.addEventListener('mousedown', () => player.movingUp = true); | |
mobileUpBtn.addEventListener('mouseup', () => player.movingUp = false); | |
mobileDownBtn.addEventListener('touchstart', () => player.movingDown = true); | |
mobileDownBtn.addEventListener('touchend', () => player.movingDown = false); | |
mobileDownBtn.addEventListener('mousedown', () => player.movingDown = true); | |
mobileDownBtn.addEventListener('mouseup', () => player.movingDown = false); | |
// Keyboard controls | |
document.addEventListener('keydown', (e) => { | |
if (e.key === 'ArrowUp' || e.key === 'w') player.movingUp = true; | |
if (e.key === 'ArrowDown' || e.key === 's') player.movingDown = true; | |
if (e.key === ' ') togglePause(); | |
}); | |
document.addEventListener('keyup', (e) => { | |
if (e.key === 'ArrowUp' || e.key === 'w') player.movingUp = false; | |
if (e.key === 'ArrowDown' || e.key === 's') player.movingDown = false; | |
}); | |
// Window resize | |
window.addEventListener('resize', () => { | |
canvas.width = canvas.offsetWidth; | |
canvas.height = canvas.offsetHeight; | |
// Re-center paddles and ball | |
player.y = canvas.height / 2 - paddleHeight / 2; | |
ai.y = canvas.height / 2 - paddleHeight / 2; | |
ball.x = canvas.width / 2; | |
ball.y = canvas.height / 2; | |
}); | |
// Game functions | |
function startGame() { | |
if (state.gameRunning) return; | |
resetPositions(); | |
ball.speedX = state.ballSpeed * (Math.random() > 0.5 ? 1 : -1); | |
ball.speedY = (Math.random() * 2 - 1) * state.ballSpeed; | |
state.gameRunning = true; | |
state.gamePaused = false; | |
state.winner = null; | |
gameMessage.classList.remove('show'); | |
animate(); | |
} | |
function resetGame() { | |
state.playerScore = 0; | |
state.aiScore = 0; | |
state.gameRunning = false; | |
state.gamePaused = false; | |
state.winner = null; | |
updateScoreboard(); | |
resetPositions(); | |
gameMessage.classList.remove('show'); | |
// Clear the animation frame if it's running | |
cancelAnimationFrame(animationId); | |
} | |
function togglePause() { | |
if (!state.gameRunning) return; | |
state.gamePaused = !state.gamePaused; | |
if (!state.gamePaused) { | |
animate(); | |
} | |
} | |
function resetPositions() { | |
player.y = canvas.height / 2 - paddleHeight / 2; | |
ai.y = canvas.height / 2 - paddleHeight / 2; | |
ball.x = canvas.width / 2; | |
ball.y = canvas.height / 2; | |
ball.speedX = 0; | |
ball.speedY = 0; | |
} | |
function updateBallSpeed() { | |
state.ballSpeed = parseInt(ballSpeedSlider.value); | |
ballSpeedValue.textContent = state.ballSpeed; | |
} | |
function updateAiDifficulty() { | |
state.aiDifficulty = parseInt(aiDifficultySlider.value); | |
aiDifficultyValue.textContent = state.aiDifficulty; | |
// Update AI speed based on difficulty (1-10 map to 3-10 speed) | |
ai.speed = 3 + (state.aiDifficulty / 10) * 7; | |
// Update reaction delay (faster AI reacts quicker) | |
ai.reactionDelay = 200 - (state.aiDifficulty * 15); | |
} | |
function checkCollision() { | |
// Ball collision with walls | |
if (ball.y <= 0 || ball.y >= canvas.height - ball.size) { | |
ball.speedY = -ball.speedY; | |
createParticles(ball.x, ball.y, 5, ball.color); | |
} | |
// Ball collision with player paddle | |
if ( | |
ball.x <= player.x + player.width && | |
ball.x >= player.x && | |
ball.y + ball.size >= player.y && | |
ball.y <= player.y + player.height | |
) { | |
// Calculate bounce angle based on where ball hits paddle | |
const hitPosition = (ball.y - player.y) / player.height; | |
const bounceAngle = maxAngle * (hitPosition - 0.5) * 2; | |
ball.speedX = Math.cos(bounceAngle) * state.ballSpeed; | |
ball.speedY = Math.sin(bounceAngle) * state.ballSpeed; | |
// Make the ball faster after each hit (but cap at max speed) | |
const speed = Math.sqrt(ball.speedX * ball.speedX + ball.speedY * ball.speedY); | |
if (speed < ball.maxSpeed) { | |
ball.speedX *= 1.05; | |
ball.speedY *= 1.05; | |
} | |
createParticles(ball.x, ball.y, 10, player.color); | |
} | |
// Ball collision with AI paddle | |
if ( | |
ball.x + ball.size >= ai.x && | |
ball.x <= ai.x + ai.width && | |
ball.y + ball.size >= ai.y && | |
ball.y <= ai.y + ai.height | |
) { | |
// Calculate bounce angle based on where ball hits paddle | |
const hitPosition = (ball.y - ai.y) / ai.height; | |
const bounceAngle = maxAngle * (hitPosition - 0.5) * 2; | |
ball.speedX = -Math.cos(bounceAngle) * state.ballSpeed; | |
ball.speedY = Math.sin(bounceAngle) * state.ballSpeed; | |
// Make the ball faster after each hit (but cap at max speed) | |
const speed = Math.sqrt(ball.speedX * ball.speedX + ball.speedY * ball.speedY); | |
if (speed < ball.maxSpeed) { | |
ball.speedX *= 1.05; | |
ball.speedY *= 1.05; | |
} | |
createParticles(ball.x, ball.y, 10, ai.color); | |
} | |
// Score points | |
if (ball.x <= 0) { | |
// AI scores | |
state.aiScore++; | |
updateScoreboard(); | |
scorePoint(false); | |
} else if (ball.x >= canvas.width) { | |
// Player scores | |
state.playerScore++; | |
updateScoreboard(); | |
scorePoint(true); | |
} | |
} | |
function scorePoint(isPlayer) { | |
resetPositions(); | |
state.gameRunning = false; | |
// Add to history | |
const now = new Date(); | |
const timeString = now.toLocaleTimeString(); | |
const dateString = now.toLocaleDateString(); | |
const newEntry = { | |
timestamp: `${dateString} ${timeString}`, | |
playerScore: state.playerScore, | |
aiScore: state.aiScore, | |
winner: state.playerScore > state.aiScore ? 'Player' : 'AI' | |
}; | |
state.history.unshift(newEntry); | |
updateHistoryList(); | |
// Check for game over (first to 5 points) | |
if (state.playerScore >= 5 || state.aiScore >= 5) { | |
state.winner = state.playerScore > state.aiScore ? 'Player' : 'AI'; | |
gameMessage.textContent = state.playerScore > state.aiScore | |
? 'YOU WIN!' | |
: 'AI WINS!'; | |
gameMessage.textContent += '\n<span style="font-size:1rem">Click Start to Play Again</span>'; | |
gameMessage.classList.add('show'); | |
} else { | |
// Small delay before serving again | |
setTimeout(() => { | |
startGame(); | |
}, 1000); | |
} | |
} | |
function updateScoreboard() { | |
playerScoreDisplay.textContent = state.playerScore; | |
aiScoreDisplay.textContent = state.aiScore; | |
} | |
function updateHistoryList() { | |
historyList.innerHTML = ''; | |
state.history.slice(0, 10).forEach((match, index) => { | |
const item = document.createElement('div'); | |
item.className = 'history-item'; | |
item.style.animationDelay = `${index * 0.1}s`; | |
item.innerHTML = ` | |
<strong>${match.timestamp}</strong> - | |
Player: ${match.playerScore} | AI: ${match.aiScore} - | |
<span style="color: ${match.winner === 'Player' ? '#08f' : '#f0f'}">${match.winner} won</span> | |
`; | |
historyList.appendChild(item); | |
}); | |
} | |
let lastAiUpdate = 0; | |
function updateAI(currentTime) { | |
// Only update AI every reactionDelay ms for more "human-like" behavior | |
if (currentTime - lastAiUpdate < ai.reactionDelay) return; | |
lastAiUpdate = currentTime; | |
// AI prediction with error based on difficulty | |
const predictY = ball.y + (ball.speedY * (ai.x - ball.x) / ball.speedX); | |
const errorRange = 100 - (state.aiDifficulty * 8); | |
const error = (Math.random() - 0.5) * errorRange; | |
const targetY = predictY + error; | |
// Move AI paddle towards predicted position | |
const paddleCenter = ai.y + ai.height / 2; | |
if (paddleCenter < targetY - ai.height / 4) { | |
ai.y += ai.speed; | |
} else if (paddleCenter > targetY + ai.height / 4) { | |
ai.y -= ai.speed; | |
} | |
// Keep AI paddle within bounds | |
ai.y = Math.max(0, Math.min(canvas.height - ai.height, ai.y)); | |
} | |
function updatePlayer() { | |
if (player.movingUp) { | |
player.y -= player.speed; | |
} else if (player.movingDown) { | |
player.y += player.speed; | |
} | |
// Keep player paddle within bounds | |
player.y = Math.max(0, Math.min(canvas.height - player.height, player.y)); | |
} | |
function updateBall() { | |
ball.x += ball.speedX; | |
ball.y += ball.speedY; | |
} | |
const particles = []; | |
function createParticles(x, y, count, color) { | |
for (let i = 0; i < count; i++) { | |
particles.push({ | |
x, | |
y, | |
size: Math.random() * 4 + 2, | |
speedX: (Math.random() - 0.5) * 6, | |
speedY: (Math.random() - 0.5) * 6, | |
color, | |
life: 30 + Math.random() * 20, | |
alpha: 1 | |
}); | |
} | |
} | |
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.alpha = p.life / 50; | |
if (p.life <= 0) { | |
particles.splice(i, 1); | |
} | |
} | |
} | |
function draw() { | |
// Clear canvas | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw background grid | |
ctx.strokeStyle = 'rgba(144, 0, 255, 0.05)'; | |
ctx.lineWidth = 1; | |
// Vertical grid | |
for (let x = borderOffset; x < canvas.width; x += 40) { | |
ctx.beginPath(); | |
ctx.moveTo(x, 0); | |
ctx.lineTo(x, canvas.height); | |
ctx.stroke(); | |
} | |
// Horizontal grid | |
for (let y = 0; y < canvas.height; y += 40) { | |
ctx.beginPath(); | |
ctx.moveTo(0, y); | |
ctx.lineTo(canvas.width, y); | |
ctx.stroke(); | |
} | |
// Draw paddles with glow effect | |
// Player paddle | |
ctx.shadowColor = player.color; | |
ctx.shadowBlur = 15; | |
ctx.fillStyle = player.color; | |
ctx.fillRect(player.x, player.y, player.width, player.height); | |
// AI paddle | |
ctx.shadowColor = ai.color; | |
ctx.fillStyle = ai.color; | |
ctx.fillRect(ai.x, ai.y, ai.width, ai.height); | |
// Reset shadow | |
ctx.shadowBlur = 0; | |
// Draw ball with glow effect | |
ctx.shadowColor = ball.color; | |
ctx.shadowBlur = 15; | |
ctx.fillStyle = ball.color; | |
ctx.beginPath(); | |
ctx.arc(ball.x + ball.size/2, ball.y + ball.size/2, ball.size/2, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.shadowBlur = 0; | |
// Draw trail behind ball | |
ctx.strokeStyle = `rgba(255, 255, 255, 0.3)`; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.moveTo(ball.x + ball.size/2, ball.y + ball.size/2); | |
ctx.lineTo( | |
ball.x + ball.size/2 - ball.speedX * 2, | |
ball.y + ball.size/2 - ball.speedY * 2 | |
); | |
ctx.stroke(); | |
// Draw particles | |
particles.forEach(p => { | |
ctx.fillStyle = `${p.color.replace(')', `, ${p.alpha})`).replace('rgb', 'rgba')}`; | |
ctx.beginPath(); | |
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); | |
ctx.fill(); | |
}); | |
} | |
let animationId; | |
function animate(timestamp = 0) { | |
if (state.gamePaused || !state.gameRunning) return; | |
// Update game state | |
updatePlayer(); | |
updateAI(timestamp); | |
updateBall(); | |
updateParticles(); | |
checkCollision(); | |
// Draw everything | |
draw(); | |
animationId = requestAnimationFrame(animate); | |
} | |
// Initialize controls | |
updateBallSpeed(); | |
updateAiDifficulty(); | |
}); | |
</script> | |
</body> | |
</html> | |