About this snippet

Pong, rebuilt with a canvas element and a requestAnimationFrame loop. Arrow keys move your paddle and the computer plays the other side.

The AI is deliberately basic. It tracks the ball's vertical position and moves toward it at a fixed speed, which leaves it beatable. Raising that speed value is the quickest way to make the game harder.

Collisions are axis-aligned box checks. Where the ball hits the paddle changes its outgoing angle, so returns are not all flat reflections. The retro look comes from Press Start 2P, with a system font fallback if it fails to load.

Size
9.7 KB
Uses
HTML, CSS, JavaScript
Loads
fonts.googleapis.com
Views
922
<div class="rpp-game-container">
  <h1 class="rpp-title">Retro Ping Pong</h1>
  <div class="rpp-scoreboard">
    <div class="rpp-score">
      <span class="rpp-score-label">Player 1</span>
      <span id="rppPlayer1Score" class="rpp-score-value">0</span>
    </div>
    <div class="rpp-score">
      <span class="rpp-score-label">Player 2 (AI)</span>
      <span id="rppPlayer2Score" class="rpp-score-value">0</span>
    </div>
  </div>
  <canvas id="rppGameCanvas"></canvas>
  <button id="rppStartButton" class="rpp-button">Start Game</button>
  <p id="rppInstructions" class="rpp-instructions">Use W (Up) and S (Down) keys to move your paddle.</p>
  <p id="rppWinMessage" class="rpp-win-message" style="display:none;"></p>
</div>
/* ==== Retro Ping Pong Styles ==== */
body.rpp-active-page {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  font-family: "Press Start 2P", cursive, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; /* Retro font + fallbacks */
  background-color: #1a1a1a; /* Dark background */
  color: #ffffff;
  margin: 0;
  text-align: center;
  padding: 10px;
  box-sizing: border-box;
  image-rendering: pixelated; /* For a sharper retro look if using images/canvas */
}

/* Import Retro Font */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

.rpp-game-container {
  background-color: #0d0d0d;
  padding: 20px;
  border-radius: 10px;
  border: 3px solid #333;
  box-shadow: 0 0 15px rgba(0, 255, 0, 0.3), inset 0 0 10px rgba(0,0,0,0.5);
  display: flex;
  flex-direction: column;
  align-items: center;
}

.rpp-title {
  font-size: 2rem;
  color: #00ff00; /* Classic green */
  margin-top: 0;
  margin-bottom: 15px;
  text-shadow: 2px 2px #008000;
}

.rpp-scoreboard {
  display: flex;
  justify-content: space-around;
  width: 100%;
  max-width: 600px;
  margin-bottom: 15px;
}

.rpp-score {
  flex: 1;
}

.rpp-score-label {
  display: block;
  font-size: 0.8rem;
  color: #cccccc;
  margin-bottom: 5px;
}

.rpp-score-value {
  font-size: 2rem;
  color: #ffffff;
  font-weight: bold;
}

#rppGameCanvas {
  background-color: #000000;
  border: 2px solid #00ff00;
  /* Dimensions will be set by JavaScript */
}

.rpp-button {
  background-color: #009900;
  color: #ffffff;
  border: 2px solid #00ff00;
  padding: 10px 20px;
  font-family: "Press Start 2P", cursive;
  font-size: 1rem;
  cursor: pointer;
  margin-top: 20px;
  text-transform: uppercase;
  transition: background-color 0.2s, box-shadow 0.2s;
  box-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
}

.rpp-button:hover {
  background-color: #00cc00;
  box-shadow: 0 0 10px rgba(0, 255, 0, 0.7);
}

.rpp-button:active {
  background-color: #006600;
  transform: translateY(1px);
}

.rpp-instructions {
  font-size: 0.8rem;
  color: #bbbbbb;
  margin-top: 15px;
}

.rpp-win-message {
  font-size: 1.5rem;
  color: #ffff00; /* Yellow for win message */
  margin-top: 15px;
  font-weight: bold;
  text-shadow: 2px 2px #cc0000;
}
document.addEventListener('DOMContentLoaded', function() {
    const canvas = document.getElementById('rppGameCanvas');
    const ctx = canvas.getContext('2d');
    const startButton = document.getElementById('rppStartButton');
    const player1ScoreEl = document.getElementById('rppPlayer1Score');
    const player2ScoreEl = document.getElementById('rppPlayer2Score');
    const winMessageEl = document.getElementById('rppWinMessage');
    const instructionsEl = document.getElementById('rppInstructions');

    // Add class to body for specific page styling
    if (document.querySelector('.rpp-game-container')) {
        document.body.classList.add('rpp-active-page');
    }

    // Game settings
    const PADDLE_HEIGHT = 100;
    const PADDLE_WIDTH = 10;
    const BALL_RADIUS = 7;
    const WINNING_SCORE = 5;
    let gameRunning = false;
    let animationFrameId;

    canvas.width = 600;
    canvas.height = 400;

    // Game objects
    let player1 = { x: 10, y: canvas.height / 2 - PADDLE_HEIGHT / 2, width: PADDLE_WIDTH, height: PADDLE_HEIGHT, score: 0, speed: 8 };
    let player2 = { x: canvas.width - PADDLE_WIDTH - 10, y: canvas.height / 2 - PADDLE_HEIGHT / 2, width: PADDLE_WIDTH, height: PADDLE_HEIGHT, score: 0, speed: 4 }; // AI speed
    let ball = { x: canvas.width / 2, y: canvas.height / 2, radius: BALL_RADIUS, speedX: 5, speedY: 5 };

    // Player controls
    let keysPressed = {};
    document.addEventListener('keydown', (e) => { keysPressed[e.key.toLowerCase()] = true; });
    document.addEventListener('keyup', (e) => { keysPressed[e.key.toLowerCase()] = false; });

    function drawRect(x, y, w, h, color) {
        ctx.fillStyle = color;
        ctx.fillRect(x, y, w, h);
    }

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

    function drawNet() {
        for (let i = 0; i < canvas.height; i += 15) {
            drawRect(canvas.width / 2 - 1, i, 2, 10, '#555');
        }
    }

    function resetBall() {
        ball.x = canvas.width / 2;
        ball.y = canvas.height / 2;
        ball.speedX = (Math.random() > 0.5 ? 1 : -1) * 5; // Random horizontal direction
        ball.speedY = (Math.random() * 6) - 3;      // Random vertical direction, less steep
        if (Math.abs(ball.speedY) < 2) ball.speedY = ball.speedY < 0 ? -2 : 2; // Ensure not too flat
    }

    function updateScores() {
        player1ScoreEl.textContent = player1.score;
        player2ScoreEl.textContent = player2.score;
    }

    function checkWin() {
        if (player1.score >= WINNING_SCORE || player2.score >= WINNING_SCORE) {
            gameRunning = false;
            winMessageEl.textContent = (player1.score >= WINNING_SCORE ? 'Player 1 Wins!' : 'Player 2 Wins!');
            winMessageEl.style.display = 'block';
            startButton.textContent = 'Restart Game';
            startButton.style.display = 'block';
            if (animationFrameId) cancelAnimationFrame(animationFrameId);
        }
    }

    function update() {
        if (!gameRunning) return;

        // Move player 1 paddle
        if (keysPressed['w'] && player1.y > 0) {
            player1.y -= player1.speed;
        }
        if (keysPressed['s'] && player1.y < canvas.height - player1.height) {
            player1.y += player1.speed;
        }

        // AI for player 2 paddle (simple)
        let player2Center = player2.y + player2.height / 2;
        if (player2Center < ball.y - 20 && player2.y < canvas.height - player2.height) {
            player2.y += player2.speed;
        } else if (player2Center > ball.y + 20 && player2.y > 0) {
            player2.y -= player2.speed;
        }
        // Keep AI paddle within bounds
        if (player2.y < 0) player2.y = 0;
        if (player2.y > canvas.height - player2.height) player2.y = canvas.height - player2.height;

        // Move ball
        ball.x += ball.speedX;
        ball.y += ball.speedY;

        // Ball collision with top/bottom walls
        if (ball.y - ball.radius < 0 || ball.y + ball.radius > canvas.height) {
            ball.speedY = -ball.speedY;
        }

        // Ball collision with paddles
        // Player 1 (left)
        if (ball.x - ball.radius < player1.x + player1.width &&
            ball.x + ball.radius > player1.x &&
            ball.y + ball.radius > player1.y &&
            ball.y - ball.radius < player1.y + player1.height) {
            ball.speedX = -ball.speedX;
            // Optional: Change angle based on where it hits paddle
            let deltaY = ball.y - (player1.y + player1.height / 2);
            ball.speedY = deltaY * 0.15; // Max speedY change
            // Increase ball speed slightly on hit
            ball.speedX *= 1.05;
        }

        // Player 2 (right)
        if (ball.x + ball.radius > player2.x &&
            ball.x - ball.radius < player2.x + player2.width &&
            ball.y + ball.radius > player2.y &&
            ball.y - ball.radius < player2.y + player2.height) {
            ball.speedX = -ball.speedX;
            let deltaY = ball.y - (player2.y + player2.height / 2);
            ball.speedY = deltaY * 0.15;
            ball.speedX *= 1.05;
        }

        // Score points
        if (ball.x - ball.radius < 0) { // Player 2 scores
            player2.score++;
            updateScores();
            resetBall();
            checkWin();
        } else if (ball.x + ball.radius > canvas.width) { // Player 1 scores
            player1.score++;
            updateScores();
            resetBall();
            checkWin();
        }
    }

    function render() {
        // Clear canvas
        drawRect(0, 0, canvas.width, canvas.height, '#000000');
        drawNet();
        // Draw paddles
        drawRect(player1.x, player1.y, player1.width, player1.height, '#00ff00');
        drawRect(player2.x, player2.y, player2.width, player2.height, '#00ff00');
        // Draw ball
        drawCircle(ball.x, ball.y, ball.radius, '#ffffff');
    }

    function gameLoop() {
        if (gameRunning) {
            update();
            render();
            animationFrameId = requestAnimationFrame(gameLoop);
        }
    }

    function startGame() {
        player1.score = 0;
        player2.score = 0;
        player1.y = canvas.height / 2 - PADDLE_HEIGHT / 2;
        player2.y = canvas.height / 2 - PADDLE_HEIGHT / 2;
        updateScores();
        resetBall();
        gameRunning = true;
        winMessageEl.style.display = 'none';
        startButton.style.display = 'none';
        instructionsEl.style.display = 'block';
        if (animationFrameId) cancelAnimationFrame(animationFrameId);
        gameLoop();
    }

    startButton.addEventListener('click', startGame);
    
    // Initial render (e.g. to show paddles before game starts)
    render(); 
    updateScores();
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save