neon-pong-ai / index.html
victor's picture
victor HF Staff
Add index.html
5898d93 verified
<!DOCTYPE html>
<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>