pong-game / index.html
JavierNV's picture
Add 1 files
58549a6 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile CyberPong</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Rajdhani:wght@300;500&display=swap');
:root {
--neon-pink: #ff00ff;
--neon-blue: #00ffff;
--neon-purple: #9461fb;
--neon-green: #00ff88;
--dark-bg: #0a0a1a;
--grid-color: rgba(0, 255, 255, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
body {
background-color: var(--dark-bg);
color: white;
font-family: 'Rajdhani', sans-serif;
overflow: hidden;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
body::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.3;
z-index: -1;
}
h1 {
font-family: 'Orbitron', sans-serif;
color: var(--neon-pink);
text-shadow: 0 0 5px var(--neon-pink);
margin-bottom: 10px;
font-size: 1.8rem;
letter-spacing: 2px;
text-align: center;
}
.game-container {
position: relative;
width: 95vw;
height: 60vh;
max-width: 600px;
max-height: 500px;
border: 1px solid var(--neon-blue);
box-shadow: 0 0 10px var(--neon-blue), inset 0 0 10px var(--neon-blue);
overflow: hidden;
background-color: rgba(10, 10, 26, 0.7);
touch-action: none;
}
.paddle {
position: absolute;
width: 12px;
height: 20vw;
max-height: 100px;
min-height: 80px;
background: linear-gradient(to bottom, var(--neon-blue), var(--neon-purple));
border-radius: 3px;
box-shadow: 0 0 10px var(--neon-blue);
}
#player {
left: 10px;
top: 50%;
transform: translateY(-50%);
}
#computer {
right: 10px;
top: 50%;
transform: translateY(-50%);
}
.ball {
position: absolute;
width: 15px;
height: 15px;
border-radius: 50%;
box-shadow: 0 0 10px currentColor;
}
#ball1 {
background-color: var(--neon-pink);
color: var(--neon-pink);
}
#ball2 {
background-color: var(--neon-green);
color: var(--neon-green);
}
#ball3 {
background-color: var(--neon-blue);
color: var(--neon-blue);
}
.score {
display: flex;
justify-content: space-between;
width: 95vw;
max-width: 600px;
margin-bottom: 10px;
font-family: 'Orbitron', sans-serif;
font-size: 1.2rem;
}
.player-score, .cpu-score {
text-align: center;
padding: 8px 15px;
border: 1px solid var(--neon-purple);
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 8px var(--neon-purple);
flex: 1;
margin: 0 5px;
}
.player-score {
color: var(--neon-blue);
text-shadow: 0 0 5px var(--neon-blue);
}
.cpu-score {
color: var(--neon-pink);
text-shadow: 0 0 5px var(--neon-pink);
}
.controls {
margin-top: 10px;
text-align: center;
color: var(--neon-green);
font-size: 0.9rem;
letter-spacing: 0.5px;
padding: 0 10px;
}
.pulse {
animation: pulse 1.5s infinite alternate;
}
@keyframes pulse {
from {
opacity: 0.7;
text-shadow: 0 0 3px currentColor;
}
to {
opacity: 1;
text-shadow: 0 0 10px currentColor;
}
}
.message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
text-align: center;
opacity: 0;
transition: opacity 0.5s;
pointer-events: none;
width: 100%;
padding: 0 20px;
}
.sensor {
position: absolute;
bottom: 0;
width: 100%;
height: 1px;
background-color: transparent;
}
.trail {
position: absolute;
width: 3px;
height: 3px;
border-radius: 50%;
pointer-events: none;
}
.mobile-controls {
display: none;
width: 100%;
position: absolute;
bottom: 20px;
justify-content: center;
gap: 20px;
z-index: 10;
}
.mobile-btn {
width: 60px;
height: 60px;
background: rgba(148, 97, 251, 0.3);
border: 1px solid var(--neon-purple);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: var(--neon-blue);
font-size: 1.5rem;
box-shadow: 0 0 10px var(--neon-purple);
user-select: none;
}
@media (max-width: 768px) and (hover: none) {
.mobile-controls {
display: flex;
}
.controls {
display: none;
}
.game-container {
height: 55vh;
}
}
</style>
</head>
<body>
<h1>CYBER<span class="pulse" style="color: var(--neon-blue)">PONG</span></h1>
<div class="score">
<div class="player-score">PLAYER: <span id="player-score">0</span></div>
<div class="cpu-score">CPU: <span id="cpu-score">0</span></div>
</div>
<div class="game-container">
<div id="player" class="paddle"></div>
<div id="computer" class="paddle"></div>
<div id="ball1" class="ball"></div>
<div id="ball2" class="ball"></div>
<div id="ball3" class="ball"></div>
<div class="message" id="message"></div>
<div class="sensor" id="sensor"></div>
</div>
<div class="controls">
Use <span style="color: var(--neon-blue)">W/S</span> or <span style="color: var(--neon-blue)">↑/↓</span> keys to move | First to <span style="color: var(--neon-pink)">10</span> wins
</div>
<div class="mobile-controls">
<div class="mobile-btn" id="up-btn"></div>
<div class="mobile-btn" id="down-btn"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Game elements
const gameContainer = document.querySelector('.game-container');
const playerPaddle = document.getElementById('player');
const computerPaddle = document.getElementById('computer');
const balls = [document.getElementById('ball1'), document.getElementById('ball2'), document.getElementById('ball3')];
const playerScore = document.getElementById('player-score');
const cpuScore = document.getElementById('cpu-score');
const message = document.getElementById('message');
const sensor = document.getElementById('sensor');
const upBtn = document.getElementById('up-btn');
const downBtn = document.getElementById('down-btn');
// Detect mobile device
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
// Game state
const state = {
playerScore: 0,
computerScore: 0,
playerY: gameContainer.offsetHeight / 2 - playerPaddle.offsetHeight / 2,
computerY: gameContainer.offsetHeight / 2 - computerPaddle.offsetHeight / 2,
balls: [
{
x: gameContainer.offsetWidth / 2,
y: gameContainer.offsetHeight / 2,
dx: isMobile ? 3 : 4,
dy: (isMobile ? 3 : 4) * (Math.random() * 0.6 + 0.7) * (Math.random() > 0.5 ? 1 : -1),
speed: isMobile ? 3 : 4,
active: true,
lastHit: null
},
{
x: gameContainer.offsetWidth / 2 - 30,
y: gameContainer.offsetHeight / 2 + 30,
dx: isMobile ? -3 : -4,
dy: -(isMobile ? 3 : 4) * (Math.random() * 0.6 + 0.7) * (Math.random() > 0.5 ? 1 : -1),
speed: isMobile ? 3 : 4,
active: true,
lastHit: null
},
{
x: gameContainer.offsetWidth / 2 + 30,
y: gameContainer.offsetHeight / 2 - 30,
dx: isMobile ? -2 : -3,
dy: (isMobile ? 2 : 3) * (Math.random() * 0.6 + 0.7) * (Math.random() > 0.5 ? 1 : -1),
speed: isMobile ? 2 : 3,
active: true,
lastHit: null
}
],
gameRunning: false,
keys: {},
trailParticles: [],
mobileTouch: {
touchStartY: 0,
paddleStartY: 0,
isTouching: false
}
};
// Initialize game
function init() {
render();
addEventListeners();
displayMessage("CYBERPONG", 1500, () => {
displayMessage("READY", 800, () => {
displayMessage("GO!", 400, () => {
state.gameRunning = true;
gameLoop();
});
});
});
// Adjust for mobile changes in orientation
window.addEventListener('resize', handleResize);
}
// Handle screen resize (especially important for mobile)
function handleResize() {
if (!state.gameRunning) return;
// Reset paddle positions
state.playerY = gameContainer.offsetHeight / 2 - playerPaddle.offsetHeight / 2;
state.computerY = gameContainer.offsetHeight / 2 - computerPaddle.offsetHeight / 2;
// Reset ball positions
state.balls.forEach(ball => {
ball.x = gameContainer.offsetWidth / 2;
ball.y = gameContainer.offsetHeight / 2;
});
render();
}
// Main game loop
function gameLoop() {
if (!state.gameRunning) return;
movePlayer();
moveComputer();
state.balls.forEach((ball, index) => {
if (ball.active) {
moveBall(index);
checkBallCollision(index);
}
});
updateTrails();
render();
if (state.playerScore >= 10 || state.computerScore >= 10) {
endGame();
return;
}
requestAnimationFrame(gameLoop);
}
// Move player paddle
function movePlayer() {
if (state.keys['ArrowUp'] || state.keys['w'] || state.keys['up']) {
state.playerY = Math.max(0, state.playerY - (isMobile ? 6 : 8));
}
if (state.keys['ArrowDown'] || state.keys['s'] || state.keys['down']) {
state.playerY = Math.min(
gameContainer.offsetHeight - playerPaddle.offsetHeight,
state.playerY + (isMobile ? 6 : 8)
);
}
}
// AI for computer paddle (mobile has slightly less aggressive AI)
function moveComputer() {
const activeBalls = state.balls.filter(ball => ball.active);
if (activeBalls.length === 0) return;
// Find the ball that's most threatening (closest and coming toward computer)
let targetBall = null;
let minDistance = Infinity;
activeBalls.forEach(ball => {
if (ball.dx > 0) { // Ball moving toward computer
const distance = gameContainer.offsetWidth - ball.x;
if (distance < minDistance) {
minDistance = distance;
targetBall = ball;
}
}
});
// If all balls are moving away, track the one that's closest to computer side
if (!targetBall) {
activeBalls.forEach(ball => {
const distance = gameContainer.offsetWidth - ball.x;
if (distance < minDistance) {
minDistance = distance;
targetBall = ball;
}
});
}
if (!targetBall) return;
// Calculate predicted position with some imperfection
const predictedPosition = targetBall.y + (Math.random() * (isMobile ? 30 : 40) - (isMobile ? 15 : 20));
const paddleCenter = state.computerY + computerPaddle.offsetHeight / 2;
if (paddleCenter < predictedPosition - 10) {
state.computerY = Math.min(
gameContainer.offsetHeight - computerPaddle.offsetHeight,
state.computerY + (isMobile ? 3 : 5) * (Math.random() * 0.3 + 0.85)
);
} else if (paddleCenter > predictedPosition + 10) {
state.computerY = Math.max(
0,
state.computerY - (isMobile ? 3 : 5) * (Math.random() * 0.3 + 0.85)
);
}
}
// Move ball
function moveBall(index) {
const ball = state.balls[index];
ball.x += ball.dx;
ball.y += ball.dy;
// Add trail particle (fewer on mobile for performance)
if (Math.random() > (isMobile ? 0.8 : 0.7)) {
state.trailParticles.push({
x: ball.x,
y: ball.y,
color: balls[index].style.backgroundColor,
life: isMobile ? 15 : 20,
size: Math.random() * (isMobile ? 2 : 3) + (isMobile ? 1 : 2)
});
}
}
// Check ball collisions
function checkBallCollision(index) {
const ball = state.balls[index];
const ballElement = balls[index];
// Wall collision
if (ball.y <= 0 || ball.y >= gameContainer.offsetHeight - ballElement.offsetHeight) {
ball.dy = -ball.dy;
// Spark effect on wall hit (fewer on mobile)
for (let i = 0; i < (isMobile ? 3 : 5); i++) {
state.trailParticles.push({
x: ball.x,
y: ball.y,
color: ballElement.style.backgroundColor,
life: isMobile ? 8 : 10,
size: Math.random() * (isMobile ? 2 : 3) + 1,
dx: (Math.random() - 0.5) * (isMobile ? 2 : 3),
dy: (Math.random() - 0.5) * (Math.random() > 0.5 ? 1 : -1)
});
}
return;
}
// Player paddle collision
if (
ball.x <= playerPaddle.offsetWidth + 15 &&
ball.x >= 10 &&
ball.y + ballElement.offsetHeight >= state.playerY &&
ball.y <= state.playerY + playerPaddle.offsetHeight &&
ball.lastHit !== 'player'
) {
ball.dx = -ball.dx * (isMobile ? 1.03 : 1.05);
// Angle the ball based on where it hits the paddle
const hitPosition = (ball.y + ballElement.offsetHeight/2 - state.playerY) / playerPaddle.offsetHeight;
ball.dy = (hitPosition - 0.5) * (isMobile ? 6 : 8);
ball.speed = Math.min(isMobile ? 8 : 10, ball.speed + 0.3);
const speedRatio = ball.speed / (isMobile ? 3 : 4);
ball.dx *= speedRatio;
ball.dy *= speedRatio;
ball.lastHit = 'player';
// Add impact particles (fewer on mobile)
for (let i = 0; i < (isMobile ? 6 : 10); i++) {
state.trailParticles.push({
x: 10 + playerPaddle.offsetWidth,
y: ball.y,
color: ballElement.style.backgroundColor,
life: isMobile ? 10 : 15,
size: Math.random() * (isMobile ? 3 : 4) + (isMobile ? 1 : 2),
dx: (Math.random() - 0.5) * (isMobile ? 1.5 : 2),
dy: (Math.random() - 0.5) * (Math.random() > 0.5 ? 1 : -1)
});
}
return;
}
// Computer paddle collision
if (
ball.x >= gameContainer.offsetWidth - computerPaddle.offsetWidth - 15 &&
ball.x <= gameContainer.offsetWidth - 10 &&
ball.y + ballElement.offsetHeight >= state.computerY &&
ball.y <= state.computerY + computerPaddle.offsetHeight &&
ball.lastHit !== 'computer'
) {
ball.dx = -ball.dx * (isMobile ? 1.03 : 1.05);
// Angle the ball based on where it hits the paddle
const hitPosition = (ball.y + ballElement.offsetHeight/2 - state.computerY) / computerPaddle.offsetHeight;
ball.dy = (hitPosition - 0.5) * (isMobile ? 6 : 8);
ball.speed = Math.min(isMobile ? 8 : 10, ball.speed + 0.3);
const speedRatio = ball.speed / (isMobile ? 3 : 4);
ball.dx *= speedRatio;
ball.dy *= speedRatio;
ball.lastHit = 'computer';
// Add impact particles (fewer on mobile)
for (let i = 0; i < (isMobile ? 6 : 10); i++) {
state.trailParticles.push({
x: gameContainer.offsetWidth - 10 - computerPaddle.offsetWidth,
y: ball.y,
color: ballElement.style.backgroundColor,
life: isMobile ? 10 : 15,
size: Math.random() * (isMobile ? 3 : 4) + (isMobile ? 1 : 2),
dx: (Math.random() - 0.5) * (isMobile ? 1.5 : 2),
dy: (Math.random() - 0.5) * (Math.random() > 0.5 ? 1 : -1)
});
}
return;
}
// Reset last hit when ball is in neutral zone
if (ball.x > gameContainer.offsetWidth * 0.3 && ball.x < gameContainer.offsetWidth * 0.7) {
ball.lastHit = null;
}
// Score points
if (ball.x < 0) {
state.computerScore++;
cpuScore.textContent = state.computerScore;
resetBall(index, 'player');
ballElement.style.display = 'none';
setTimeout(() => {
ballElement.style.display = 'block';
}, 800);
} else if (ball.x > gameContainer.offsetWidth) {
state.playerScore++;
playerScore.textContent = state.playerScore;
resetBall(index, 'computer');
ballElement.style.display = 'none';
setTimeout(() => {
ballElement.style.display = 'block';
}, 800);
}
}
// Reset ball position
function resetBall(index, scorer) {
const ball = state.balls[index];
ball.x = gameContainer.offsetWidth / 2;
ball.y = gameContainer.offsetHeight / 2;
if (scorer === 'player') {
ball.dx = -Math.abs(ball.speed);
} else {
ball.dx = Math.abs(ball.speed);
}
ball.dy = ball.speed * (Math.random() * 0.6 + 0.7) * (Math.random() > 0.5 ? 1 : -1);
ball.active = false;
setTimeout(() => {
ball.active = true;
}, 800);
}
// Update trail particles
function updateTrails() {
for (let i = state.trailParticles.length - 1; i >= 0; i--) {
const particle = state.trailParticles[i];
particle.life--;
if (particle.dx) particle.x += particle.dx;
if (particle.dy) particle.y += particle.dy;
if (particle.life <= 0 ||
particle.x < 0 ||
particle.x > gameContainer.offsetWidth ||
particle.y < 0 ||
particle.y > gameContainer.offsetHeight) {
state.trailParticles.splice(i, 1);
}
}
}
// Render all game elements
function render() {
playerPaddle.style.top = state.playerY + 'px';
computerPaddle.style.top = state.computerY + 'px';
state.balls.forEach((ball, index) => {
if (ball.active) {
balls[index].style.left = ball.x + 'px';
balls[index].style.top = ball.y + 'px';
}
});
// Clear old trails (but limit clearing on mobile for performance)
const oldTrails = document.querySelectorAll('.trail');
if (!isMobile || Date.now() % 3 === 0) {
oldTrails.forEach(trail => trail.remove());
}
// Create new trail elements
state.trailParticles.forEach(particle => {
const trail = document.createElement('div');
trail.className = 'trail';
trail.style.left = particle.x + 'px';
trail.style.top = particle.y + 'px';
trail.style.backgroundColor = particle.color;
trail.style.width = particle.size + 'px';
trail.style.height = particle.size + 'px';
trail.style.opacity = particle.life / (isMobile ? 15 : 20);
gameContainer.appendChild(trail);
});
}
// Display a message
function displayMessage(text, duration, callback) {
message.textContent = text;
message.style.opacity = 1;
// Apply different colors based on message
if (text === 'GO!') {
message.style.color = 'var(--neon-green)';
message.style.textShadow = '0 0 10px var(--neon-green)';
} else if (text.includes('WINS')) {
message.style.color = text.includes('PLAYER') ? 'var(--neon-blue)' : 'var(--neon-pink)';
message.style.textShadow = text.includes('PLAYER') ? '0 0 10px var(--neon-blue)' : '0 0 10px var(--neon-pink)';
} else {
message.style.color = 'white';
message.style.textShadow = '0 0 5px white';
}
setTimeout(() => {
message.style.opacity = 0;
if (callback) setTimeout(callback, 500);
}, duration);
}
// End the game
function endGame() {
state.gameRunning = false;
const winner = state.playerScore >= 10 ? 'PLAYER WINS!' : 'CPU WINS!';
displayMessage(winner, 1500, () => {
// Reset game
state.playerScore = 0;
state.computerScore = 0;
playerScore.textContent = '0';
cpuScore.textContent = '0';
state.balls.forEach((ball, index) => {
resetBall(index, index % 2 === 0 ? 'player' : 'computer');
});
displayMessage("READY", 800, () => {
displayMessage("GO!", 400, () => {
state.gameRunning = true;
gameLoop();
});
});
});
}
// Add event listeners
function addEventListeners() {
// Keyboard controls
document.addEventListener('keydown', (e) => {
state.keys[e.key.toLowerCase()] = true;
});
document.addEventListener('keyup', (e) => {
state.keys[e.key.toLowerCase()] = false;
});
// Mouse/touch controls for player paddle
gameContainer.addEventListener('mousemove', (e) => {
const rect = gameContainer.getBoundingClientRect();
const relativeY = e.clientY - rect.top;
state.playerY = Math.max(
0,
Math.min(
gameContainer.offsetHeight - playerPaddle.offsetHeight,
relativeY - playerPaddle.offsetHeight / 2
)
);
});
// Enhanced touch controls for better mobile experience
gameContainer.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = gameContainer.getBoundingClientRect();
state.mobileTouch.touchStartY = touch.clientY;
state.mobileTouch.paddleStartY = state.playerY;
state.mobileTouch.isTouching = true;
}, { passive: false });
gameContainer.addEventListener('touchmove', (e) => {
if (!state.mobileTouch.isTouching) return;
e.preventDefault();
const touch = e.touches[0];
const rect = gameContainer.getBoundingClientRect();
const deltaY = touch.clientY - state.mobileTouch.touchStartY;
const newY = state.mobileTouch.paddleStartY + deltaY;
state.playerY = Math.max(
0,
Math.min(
gameContainer.offsetHeight - playerPaddle.offsetHeight,
newY
)
);
}, { passive: false });
gameContainer.addEventListener('touchend', () => {
state.mobileTouch.isTouching = false;
}, { passive: false });
// Mobile button controls
upBtn.addEventListener('touchstart', () => {
state.keys['up'] = true;
});
upBtn.addEventListener('touchend', () => {
state.keys['up'] = false;
});
downBtn.addEventListener('touchstart', () => {
state.keys['down'] = true;
});
downBtn.addEventListener('touchend', () => {
state.keys['down'] = false;
});
// Prevent context menu on mobile
document.addEventListener('contextmenu', (e) => {
if (isMobile) e.preventDefault();
});
}
// Start the game
init();
});
</script>
</body>
</html>