You Might Also Like
Pong vs Computer — Free HTML CSS JS Snippet
Pong vs Computer · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Pong vs Computer — Angle-Reflection Ball Physics & Rate-Limited AI Paddle Tracking

Pong is the original video game archetype, and its two mechanics — realistic ball bounce physics and a computer opponent that feels fair rather than either trivial or unbeatable — are exactly what separate a convincing Pong clone from a flat, boring one. This snippet implements both properly: the ball's bounce angle genuinely depends on where it strikes each paddle, and the computer paddle tracks the ball with a deliberately imperfect, rate-limited speed so a human player can actually win.
Why a perfectly-tracking AI paddle is not fun
A naive Pong AI simply sets cpuY = ball.y every frame, producing a paddle that never misses — mathematically unbeatable and immediately obvious as artificial. This snippet's update() function instead computes diff = ball.y - cpuCenter (how far the CPU paddle's centre is from the ball) and applies only a fraction of that distance each frame: cpuStep = clamp(diff * 0.09, -CPU_MAX_SPEED, CPU_MAX_SPEED). The 0.09 proportional factor means the paddle always chases the ball but never snaps to it instantly, and CPU_MAX_SPEED = 4.2 caps how many pixels it can move in a single frame even when the ball is far away. The result is an opponent that plays a genuinely strong game on straightforward shots but can be beaten with sharp angle changes and fast cross-court hits that outrun its top speed — exactly the behaviour of a satisfying, beatable AI.
Angle-reflection bounce physics, not a flat mirror bounce
Real Pong's signature feel comes from the paddle acting like a curved surface rather than a flat wall: hitting the ball near the paddle's edge sends it off at a steep angle, while hitting it dead centre sends it nearly straight back. bounceOffPaddle(paddleY) computes relativeHit = (ball.y - paddleY) / PADDLE_H, a value from 0 (top of paddle) to 1 (bottom of paddle), then maps it to an angle: angle = (clampedRel - 0.5) * (Math.PI / 3), giving a range of plus or minus 60 degrees from horizontal. The ball's new velocity is then recomputed from that angle and a speed that increases slightly on every hit — speed = Math.min(9, Math.hypot(ball.vx, ball.vy) * 1.06) — capped at 9 so rallies gradually intensify without becoming physically uncontrollable. This is real angle-reflection physics driven by contact position, not a simple vx *= -1 mirror bounce.
Dual control scheme and canvas coordinate mapping
The player's paddle responds to both mouse movement and Arrow Up/Down keys. Mouse control reads e.clientY, subtracts the canvas's bounding-rect offset, and multiplies by H / rect.height to correctly map the mouse's screen-pixel position to the canvas's internal coordinate space — this scale correction matters because the canvas element is styled at width: 100% in CSS while its internal drawing buffer stays fixed at 480x320, so without the scale factor the paddle would track incorrectly on any screen where the canvas is rendered larger or smaller than its native resolution. Arrow key control simply nudges playerY by a fixed 6px per frame while the key is held, tracked through a keys object updated on keydown/keyup.
Wall bounces, scoring, and match state
The ball reflects off the top and bottom walls with a simple vy *= -1, since walls (unlike paddles) are flat and don't need angle variation. Paddle collision is detected with an axis-aligned range check on both x position and y overlap with the paddle's height before applying the angle-reflection bounce. When the ball passes fully off either the left or right edge, the corresponding score increments, resetPositions() re-centres both paddles and calls serveBall() to launch a new ball toward a random side, and the whole match resolves once either score reaches WIN_SCORE = 7, at which point endMatch() stops the requestAnimationFrame loop and shows a win/loss overlay with a "Play Again" button.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the proportional cpuStep calculation and CPU_MAX_SPEED cap combine to produce an AI that is competitive but beatable — understanding that tuning is the key to extending the difficulty in either direction. It's also a strong candidate for AI-assisted additions: ask the assistant to add a difficulty selector that adjusts CPU_MAX_SPEED and the tracking factor together, to add a local two-player mode by replacing the CPU logic with a second keyboard control scheme, or to add a subtle particle or screen-shake effect on paddle hits for extra game feel. You could also ask it to review the AABB paddle collision ranges to confirm the ball can never tunnel through a paddle at high speed (a classic bug in naive collision code), and to suggest a fix such as continuous collision detection if it finds a gap. Treat the code as a physics sandbox to question and improve, not a finished black box.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a Pong game against a computer opponent using the HTML5 Canvas API in plain HTML, CSS, and JavaScript — no frameworks, no build tooling.
Requirements:
- Two paddles on a canvas: the human player's paddle controlled by mouse movement over the canvas (correctly scaled from screen pixels to canvas coordinates) and by Arrow Up/Down keys, restricted to vertical movement only and clamped within the canvas bounds; the computer's paddle on the opposite side.
- A computer-controlled paddle that tracks the ball's vertical position using a deliberately imperfect, rate-limited movement speed (for example a proportional step toward the ball's position capped at a maximum pixels-per-frame value) rather than snapping instantly to the ball — a perfectly tracking paddle must be avoidable since it would be unbeatable and not fun.
- A ball that moves continuously via a requestAnimationFrame loop, bounces off the top and bottom walls with simple vertical reflection, and bounces off either paddle with real angle-reflection physics where the rebound angle depends on exactly where along the paddle's height the ball made contact (centre hits return nearly straight, edge hits return at a steep angle).
- Ball speed that increases slightly with each paddle hit (capped at a reasonable maximum) so rallies build tension over time rather than staying at a flat constant speed.
- A visible score for both the player and the computer that increments when the ball fully passes the opposing side, immediately followed by re-centring both paddles and serving a new ball toward a random side with a slight randomized angle.
- A "first to 7 points wins" round-end state that stops the game loop, clearly displays who won and the final score, and offers a restart control that resets both scores and paddle positions and starts a fresh match.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Start the matchClick "Start Game" to hide the overlay and begin the requestAnimationFrame loop. The ball serves from centre toward a random side with a slight random vertical angle so every rally starts differently.
- 2Control your paddleMove your mouse over the canvas to directly position your paddle at the corresponding height, or hold Arrow Up/Down to nudge it 6px per frame — both input methods work simultaneously and are clamped to stay within the canvas bounds.
- 3Watch the CPU track imperfectlyThe computer paddle chases the ball's Y position using a proportional step capped at CPU_MAX_SPEED (4.2px/frame), so fast or sharply-angled shots can outrun its tracking speed — this is intentional and is what makes the AI beatable rather than a perfect wall.
- 4Aim your returns using paddle positionbounceOffPaddle() computes the ball's new angle from exactly where it struck your paddle: hitting near the top or bottom edge sends the ball off at a steep angle (up to 60 degrees), while a centre hit returns it nearly straight across.
- 5Play to 7 pointsEach time the ball passes fully off either side, the scoring player's counter increments and the ball re-serves from centre. The first side to reach WIN_SCORE (7) ends the match with a win/loss overlay showing the final score.
- 6ReplayClick "Play Again" after a match ends to call resetMatch(), which zeroes both scores, re-centres both paddles, serves a fresh ball, and restarts the game loop from a clean state.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The CPU paddle moves toward the ball's Y position using a proportional step (roughly 9% of the remaining distance per frame) capped at a maximum speed of 4.2 pixels per frame. On a fast or sharply-angled shot, the ball can cross the court faster than the paddle can close that distance, causing a miss. This is intentional — a paddle that snaps instantly to the ball's position every frame would be mathematically unbeatable and not enjoyable to play against.
bounceOffPaddle() calculates relativeHit as the fraction of the paddle's height where contact occurred (0 at the top, 1 at the bottom), then maps that to an angle between -60 and +60 degrees using (relativeHit - 0.5) * (Math.PI / 3). A centre hit produces a near-horizontal return; a hit near either edge sends the ball off at a steep angle, exactly like the physical spin-and-angle behaviour of the original arcade game.
Yes — every time the ball hits either paddle, its speed is multiplied by 1.06 via Math.hypot(ball.vx, ball.vy) * 1.06, capped at a maximum of 9 to keep the game controllable. This means long rallies become progressively faster and more tense, while short rallies stay at a moderate, learnable pace.
The canvas element is styled with width: 100% in CSS so it resizes responsively, but its internal drawing buffer stays fixed at 480x320 pixels. Without correcting for this, a mouse position read directly from e.clientY would be wrong on any screen where the canvas renders at a different size than its native resolution. Multiplying by H / rect.height converts the mouse's on-screen pixel position into the canvas's internal coordinate space accurately at any display size.
Adjust CPU_MAX_SPEED (its top pixel-per-frame movement speed) and the 0.09 proportional tracking factor inside update(). Raising either value makes the CPU track the ball more aggressively and win more often; lowering them makes it slower to react and easier to beat. Setting CPU_MAX_SPEED very high while keeping the tracking factor at 1 effectively recreates the unbeatable "perfect paddle" this snippet deliberately avoids.