Skip to content

feat: Create basic Ping Pong game #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ping Pong Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Ping Pong</h1>

<div id="score">
<div id="player-score">Player: 0</div>
<div id="opponent-score">Opponent: 0</div>
</div>

<canvas id="gameCanvas" width="800" height="600"></canvas>

<div id="instructions">
<h2>Instructions</h2>
<p>Use your mouse to move your paddle (the left one) up and down.<br>
The first player to reach 5 points wins!<br>
If the game ends, refresh the page to play again.</p>
</div>

<script src="script.js" defer></script>
</body>
</html>
242 changes: 242 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// 1. Canvas and Context
const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

// 2. Game Elements - Properties
// Paddles
const paddleWidth = 10;
const paddleHeight = 100;

const player = {
x: 0,
y: canvasHeight / 2 - paddleHeight / 2,
width: paddleWidth,
height: paddleHeight,
color: 'blue',
dy: 0 // vertical speed
};

const opponent = {
x: canvasWidth - paddleWidth,
y: canvasHeight / 2 - paddleHeight / 2,
width: paddleWidth,
height: paddleHeight,
color: 'red',
dy: 0 // vertical speed
};

// Ball
const ballRadius = 7;
const ball = {
x: canvasWidth / 2,
y: canvasHeight / 2,
radius: ballRadius,
speedX: 5, // initial horizontal speed
speedY: 5, // initial vertical speed
color: 'white'
};

// Score
let playerScore = 0;
let opponentScore = 0;
const WINNING_SCORE = 5;
let gameOver = false;

// Net (Optional, for visual)
const net = {
x: canvasWidth / 2 - 1, // center of the canvas, adjusted for width
y: 0,
width: 2,
height: 10, // height of each segment
color: 'grey'
};

// 3. Drawing Functions
function drawRect(x, y, width, height, color) {
context.fillStyle = color;
context.fillRect(x, y, width, height);
}

function drawCircle(x, y, radius, color) {
context.fillStyle = color;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2, false);
context.closePath();
context.fill();
}

function drawNet() {
for (let i = 0; i <= canvasHeight; i += 15) { // Draw dashed line
drawRect(net.x, net.y + i, net.width, net.height, net.color);
}
}

function drawText(text, x, y, color, font = '16px Arial') {
context.fillStyle = color;
context.font = font;
context.fillText(text, x, y);
}

// 4. Initial Rendering Function
function renderInitialState() {
// Clear the canvas (though gameLoop will also do this)
context.clearRect(0, 0, canvasWidth, canvasHeight); // Clear the entire canvas

// Set a background color for the canvas (optional, if not set in CSS or if CSS is overridden)
drawRect(0, 0, canvasWidth, canvasHeight, 'black');


// Draw elements
drawNet();
drawRect(player.x, player.y, player.width, player.height, player.color);
drawRect(opponent.x, opponent.y, opponent.width, opponent.height, opponent.color);
drawCircle(ball.x, ball.y, ball.radius, ball.color);

// Example of drawing scores (will be updated dynamically later)
// drawText('Player: 0', 50, 30, 'white');
// drawText('Opponent: 0', canvasWidth - 150, 30, 'white');
}


// 5. Game Loop Structure
function gameLoop() {
// Clear the canvas
// context.clearRect(0, 0, canvasWidth, canvasHeight); // Already done in renderInitialState for now
// and will be the first step in actual game updates
drawRect(0, 0, canvasWidth, canvasHeight, 'black'); // Draw background

// Draw game elements
drawNet();
drawRect(player.x, player.y, player.width, player.height, player.color);
drawRect(opponent.x, opponent.y, opponent.width, opponent.height, opponent.color);
drawCircle(ball.x, ball.y, ball.radius, ball.color);

// (Movement, collision detection, score updates will go here in later steps)

// Call gameLoop again for the next frame
requestAnimationFrame(gameLoop);
}

// 6. Player Paddle Movement
canvas.addEventListener('mousemove', movePlayerPaddle);

function movePlayerPaddle(event) {
let rect = canvas.getBoundingClientRect();
player.y = event.clientY - rect.top - player.height / 2;

// Prevent paddle from going off-screen
if (player.y < 0) {
player.y = 0;
} else if (player.y + player.height > canvasHeight) {
player.y = canvasHeight - player.height;
}
}

// Collision Detection function
function collision(b, p) { // b for ball, p for paddle
return b.x + b.radius > p.x && // ball right edge vs paddle left edge
b.x - b.radius < p.x + p.width && // ball left edge vs paddle right edge
b.y + b.radius > p.y && // ball bottom edge vs paddle top edge
b.y - b.radius < p.y + p.height; // ball top edge vs paddle bottom edge
}

// Reset Ball function
function resetBall() {
ball.x = canvasWidth / 2;
ball.y = canvasHeight / 2;
ball.speedX = -ball.speedX; // Change direction to the player who conceded
ball.speedY = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 3 + 2); // Random Y speed and direction
}

// Update function (called in gameLoop)
function update() {
if (gameOver) return;

// Ball Movement
ball.x += ball.speedX;
ball.y += ball.speedY;

// Ball Collision - Top and Bottom Walls
if (ball.y - ball.radius < 0 || ball.y + ball.radius > canvasHeight) {
ball.speedY = -ball.speedY;
}

// Opponent Paddle (Simple AI)
opponent.y += (ball.y - (opponent.y + opponent.height / 2)) * 0.09; // Slightly adjusted AI speed

// Prevent opponent paddle from going off-screen
if (opponent.y < 0) {
opponent.y = 0;
} else if (opponent.y + opponent.height > canvasHeight) {
opponent.y = canvasHeight - opponent.height;
}

// Ball Collision - Paddles
let targetPaddle = ball.x < canvasWidth / 2 ? player : opponent;
if (collision(ball, targetPaddle)) {
ball.speedX = -ball.speedX;

// Optional: Adjust Y speed based on hit location (simple version)
let deltaY = ball.y - (targetPaddle.y + targetPaddle.height / 2);
ball.speedY = deltaY * 0.25; // The further from center, the more angle

// Optional: Increase ball speed
// ball.speedX *= 1.05;
// ball.speedY *= 1.05;
}

// Scoring System
if (ball.x - ball.radius < 0) { // Opponent scores
opponentScore++;
resetBall();
} else if (ball.x + ball.radius > canvasWidth) { // Player scores
playerScore++;
resetBall();
}

// Check for Game Over
if (playerScore === WINNING_SCORE || opponentScore === WINNING_SCORE) {
gameOver = true;
}
}

// Draw function (called in gameLoop)
function drawGameElements() {
// Clear the canvas
drawRect(0, 0, canvasWidth, canvasHeight, 'black'); // Draw background

// Draw game elements
drawNet();
drawRect(player.x, player.y, player.width, player.height, player.color);
drawRect(opponent.x, opponent.y, opponent.width, opponent.height, opponent.color);
drawCircle(ball.x, ball.y, ball.radius, ball.color);

// Display Scores
drawText(playerScore.toString(), canvasWidth / 4, canvasHeight / 5, 'white', '30px Arial');
drawText(opponentScore.toString(), 3 * canvasWidth / 4, canvasHeight / 5, 'white', '30px Arial');

// Display Game Over Message
if (gameOver) {
let message = playerScore === WINNING_SCORE ? "Player Wins!" : "Opponent Wins!";
drawText(message, canvasWidth / 2 - 100, canvasHeight / 2, 'yellow', '40px Arial');
drawText("Refresh to Play Again", canvasWidth / 2 - 120, canvasHeight / 2 + 40, 'white', '20px Arial');
}
}

// 5. Game Loop Structure
function gameLoop() {
update(); // Update game state
drawGameElements(); // Draw the game

// Call gameLoop again for the next frame if game is not over
if (!gameOver) {
requestAnimationFrame(gameLoop);
}
}

// 7. Initial Call & Ball Serve
resetBall(); // Serve the ball initially
ball.speedX = 5; // Ensure initial serve goes towards player or opponent consistently
requestAnimationFrame(gameLoop); // Start the game loop
53 changes: 53 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
body {
background-color: #f0f0f0; /* Light grey background */
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 20px;
}

h1 {
color: #333;
}

#gameCanvas {
border: 1px solid black;
display: block;
margin: 20px auto; /* Centering the canvas */
background-color: #fff; /* White background for the canvas */
}

#score {
display: flex;
justify-content: space-around;
width: 800px; /* Same width as canvas for alignment */
margin: 10px auto;
font-size: 1.2em;
}

#player-score, #opponent-score {
color: #333;
padding: 10px;
background-color: #e7e7e7;
border-radius: 5px;
}

#instructions {
width: 800px;
margin: 20px auto;
padding: 15px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
text-align: left;
}

#instructions h2 {
text-align: center;
color: #333;
}

#instructions p {
color: #555;
line-height: 1.6;
}