<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML Ping Pong</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="score">
<div id="player-score">0</div>
<div id="ai-score">0</div>
</div>
<div id="game-board">
<div class="net"></div>
<div id="ball"></div>
<div id="paddle-left" class="paddle"></div>
<div id="paddle-right" class="paddle"></div>
<div id="start-message" class="message">Click to Start</div>
<div id="win-message" class="message" style="display: none;"></div>
</div>
<script src="script.js"></script>
</body>
</html>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #2c3e50; /* Dark background */
margin: 0;
font-family: 'Arial', sans-serif;
flex-direction: column; /* Stack score above board */
overflow: hidden; /* Prevent scrollbars if board is slightly too big */
}
.score {
display: flex;
justify-content: space-around;
width: 600px; /* Match game board width */
font-size: 2.5em;
color: #ecf0f1; /* Light text color */
margin-bottom: 20px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
#game-board {
width: 600px;
height: 400px;
background: linear-gradient(to bottom, #16a085, #1abc9c); /* Green gradient for table */
border: 5px solid #ecf0f1; /* White border */
position: relative; /* Crucial for positioning elements inside */
cursor: none; /* Hide cursor over game board */
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
overflow: hidden; /* Keep ball and paddles inside visually */
}
.net {
position: absolute;
left: 50%;
top: 0;
width: 4px; /* Net thickness */
height: 100%;
border-left: 4px dashed rgba(255, 255, 255, 0.5); /* Dashed white line */
transform: translateX(-50%); /* Center the net */
}
#ball {
width: 20px;
height: 20px;
background-color: #f1c40f; /* Yellow ball */
border-radius: 50%; /* Make it round */
position: absolute;
/* Start position set by JS */
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
}
.paddle {
width: 15px;
height: 100px;
background-color: #e74c3c; /* Red paddle color */
position: absolute;
border-radius: 5px;
box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.4);
}
#paddle-left {
left: 10px;
/* top position set by JS */
}
#paddle-right {
right: 10px;
background-color: #3498db; /* Blue AI paddle */
/* top position set by JS */
}
.message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2em;
color: rgba(255, 255, 255, 0.8);
background-color: rgba(0, 0, 0, 0.6);
padding: 15px 30px;
border-radius: 10px;
text-align: center;
z-index: 10; /* Ensure message is on top */
}
document.addEventListener('DOMContentLoaded', () => {
const gameBoard = document.getElementById('game-board');
const ball = document.getElementById('ball');
const paddleLeft = document.getElementById('paddle-left');
const paddleRight = document.getElementById('paddle-right');
const playerScoreElem = document.getElementById('player-score');
const aiScoreElem = document.getElementById('ai-score');
const startMessage = document.getElementById('start-message');
const winMessage = document.getElementById('win-message');
let boardRect = gameBoard.getBoundingClientRect();
let ballX, ballY, ballSpeedX, ballSpeedY;
let paddleLeftY = gameBoard.clientHeight / 2 - paddleLeft.offsetHeight / 2;
let paddleRightY = gameBoard.clientHeight / 2 - paddleRight.offsetHeight / 2;
let playerScore = 0;
let aiScore = 0;
const PADDLE_HEIGHT = paddleLeft.offsetHeight;
const BALL_SIZE = ball.offsetWidth;
const PADDLE_SPEED = 6; // AI paddle speed adjustment
const WINNING_SCORE = 5;
let gameRunning = false;
let animationFrameId;
// --- Initialization ---
function resetBall(serveDirection) {
ballX = gameBoard.clientWidth / 2 - BALL_SIZE / 2;
ballY = gameBoard.clientHeight / 2 - BALL_SIZE / 2;
// Randomize initial vertical angle slightly
let angle = (Math.random() * Math.PI / 4) - (Math.PI / 8); // -22.5 to +22.5 degrees
ballSpeedY = 4 * Math.sin(angle); // Slower initial speed
ballSpeedX = 4 * serveDirection * Math.cos(angle); // Serve left or right
}
function updateScore() {
playerScoreElem.textContent = playerScore;
aiScoreElem.textContent = aiScore;
}
function resetGame() {
playerScore = 0;
aiScore = 0;
updateScore();
paddleLeftY = gameBoard.clientHeight / 2 - PADDLE_HEIGHT / 2;
paddleRightY = gameBoard.clientHeight / 2 - PADDLE_HEIGHT / 2;
paddleLeft.style.top = `${paddleLeftY}px`;
paddleRight.style.top = `${paddleRightY}px`;
winMessage.style.display = 'none';
startMessage.style.display = 'block';
cancelAnimationFrame(animationFrameId); // Stop previous loop if any
gameRunning = false;
}
// --- Game Loop ---
function gameLoop() {
if (!gameRunning) return;
// 1. Move Ball
ballX += ballSpeedX;
ballY += ballSpeedY;
// 2. Ball Collision Detection (Walls)
// Top/Bottom Walls
if (ballY <= 0 || ballY >= gameBoard.clientHeight - BALL_SIZE) {
ballSpeedY *= -1;
ballY = Math.max(0, Math.min(ballY, gameBoard.clientHeight - BALL_SIZE)); // Prevent sticking
}
// Score Walls
if (ballX <= 0) {
aiScore++;
updateScore();
checkForWin();
if (!gameRunning) return; // Stop if game ended
resetBall(1); // Serve to AI
} else if (ballX >= gameBoard.clientWidth - BALL_SIZE) {
playerScore++;
updateScore();
checkForWin();
if (!gameRunning) return; // Stop if game ended
resetBall(-1); // Serve to Player
}
// 3. Ball Collision Detection (Paddles)
let paddle = (ballSpeedX < 0) ? paddleLeft : paddleRight;
let paddleY = (ballSpeedX < 0) ? paddleLeftY : paddleRightY;
let paddleX = (ballSpeedX < 0) ? paddle.offsetLeft + paddle.offsetWidth : paddle.offsetLeft;
if (
// Check if ball is aligned horizontally with the correct paddle
(ballSpeedX < 0 && ballX <= paddleX && ballX > paddle.offsetLeft) ||
(ballSpeedX > 0 && ballX + BALL_SIZE >= paddleX && ballX < paddle.offsetLeft + paddle.offsetWidth)
) {
// Check if ball is aligned vertically with the paddle
if (ballY + BALL_SIZE > paddleY && ballY < paddleY + PADDLE_HEIGHT) {
// Collision!
ballSpeedX *= -1;
// Make ball bounce angle depend on where it hits the paddle
let hitPos = (ballY + BALL_SIZE / 2) - (paddleY + PADDLE_HEIGHT / 2);
let normalizedHitPos = hitPos / (PADDLE_HEIGHT / 2); // Range -1 to 1
let bounceAngle = normalizedHitPos * (Math.PI / 3); // Max bounce angle (e.g., 60 degrees)
let currentSpeed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
currentSpeed *= 1.05; // Increase speed slightly on hit
currentSpeed = Math.min(currentSpeed, 12); // Max speed limit
ballSpeedY = currentSpeed * Math.sin(bounceAngle);
// Keep the horizontal direction, adjust magnitude based on angle
ballSpeedX = currentSpeed * Math.cos(bounceAngle) * Math.sign(ballSpeedX);
// Prevent ball getting stuck inside paddle slightly
if (ballSpeedX > 0) {
ballX = paddleLeft.offsetLeft + paddleLeft.offsetWidth;
} else {
ballX = paddleRight.offsetLeft - BALL_SIZE;
}
}
}
// 4. Move AI Paddle (Simple follow)
let targetY = ballY - PADDLE_HEIGHT / 2;
let dy = targetY - paddleRightY;
// Introduce slight delay/inertia
if (Math.abs(dy) > PADDLE_SPEED / 2) {
paddleRightY += Math.sign(dy) * PADDLE_SPEED * 0.6; // AI slightly slower
}
// Clamp AI paddle position
paddleRightY = Math.max(0, Math.min(paddleRightY, gameBoard.clientHeight - PADDLE_HEIGHT));
// 5. Render updates
ball.style.left = `${ballX}px`;
ball.style.top = `${ballY}px`;
paddleRight.style.top = `${paddleRightY}px`;
// Player paddle position updated by mousemove event
// 6. Request next frame
animationFrameId = requestAnimationFrame(gameLoop);
}
// --- Input Handling ---
gameBoard.addEventListener('mousemove', (e) => {
// Calculate mouse position relative to the game board
boardRect = gameBoard.getBoundingClientRect(); // Update in case of scroll/resize
let mouseY = e.clientY - boardRect.top;
// Move player paddle (paddleLeft)
paddleLeftY = mouseY - PADDLE_HEIGHT / 2;
// Clamp paddle position to board boundaries
paddleLeftY = Math.max(0, Math.min(paddleLeftY, gameBoard.clientHeight - PADDLE_HEIGHT));
paddleLeft.style.top = `${paddleLeftY}px`;
});
// --- Game State Control ---
function checkForWin() {
let winner = null;
if (playerScore >= WINNING_SCORE) {
winner = "Player";
} else if (aiScore >= WINNING_SCORE) {
winner = "AI";
}
if (winner) {
gameRunning = false;
winMessage.textContent = `${winner} Wins! Click to Play Again`;
winMessage.style.display = 'block';
startMessage.style.display = 'none';
cancelAnimationFrame(animationFrameId);
}
}
function startGame() {
if (!gameRunning) {
// Check if game just ended
if (playerScore >= WINNING_SCORE || aiScore >= WINNING_SCORE) {
resetGame(); // Reset scores and positions if restarting after win
}
startMessage.style.display = 'none';
winMessage.style.display = 'none';
resetBall(Math.random() > 0.5 ? 1 : -1); // Random serve direction
gameRunning = true;
animationFrameId = requestAnimationFrame(gameLoop);
}
}
// Click to start/restart
gameBoard.addEventListener('click', startGame);
// --- Initial Setup ---
resetGame(); // Set initial positions and score display
// Adjust boardRect on resize
window.addEventListener('resize', () => {
boardRect = gameBoard.getBoundingClientRect();
});
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.