🌐 Live demo (LOOK · UNDERSTAND · BUILD): https://dev48v.infy.uk/game/day2-pong.html
Day 2 of my GameFromZero series — 50 playable mini-games in 50 days, all in the browser, all one HTML file.
Today: Pong. The simplest arcade game ever made and the one that teaches the game loop pattern that powers every game after it.
What you're going to learn
The game-loop pattern. Every action game in history is the same shape:
every frame (16ms ≈ 60 FPS):
step() — update state (positions, velocities, collisions, scores)
draw() — repaint the canvas from current state
That's it. Pong is the cleanest possible illustration of this loop.
The state — five numbers
let ly, ry; // left paddle Y, right paddle Y
let bx, by; // ball X, ball Y
let vx, vy; // ball velocity X, ball velocity Y
let scoreL, scoreR;
That is the entire universe. No classes, no objects, no engine. Five primitive numbers describe the whole game.
The physics — 8 lines
// 1. Move ball
bx += vx;
by += vy;
// 2. Bounce off top/bottom walls
if (by < BR || by > H - BR) vy = -vy;
// 3. Bounce off left paddle
if (bx < PW + BR && by > ly && by < ly + PH) {
vx = Math.abs(vx) * 1.05; // reverse + speed up 5%
vy += ((by - (ly + PH/2)) / PH) * 3; // angle from hit position
}
// 4. Score when ball passes a goal
if (bx < 0) { scoreR++; reset(); }
if (bx > W) { scoreL++; reset(); }
Each line is a single rule of the universe. The speed-up on every paddle hit (* 1.05) is what makes Pong intense — long rallies get faster until somebody misses.
The deflection trick vy += ((by - paddleMid) / PH) * 3 is the entire secret behind paddle games: hit position changes the angle. Hit the top of the paddle, ball goes up. Hit the bottom, ball goes down. Lets a skilled player steer.
Input — flag the keys
const keys = {};
addEventListener('keydown', e => { keys[e.key.toLowerCase()] = true; });
addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
// Every tick:
if (keys.w) ly -= 6;
if (keys.s) ly += 6;
DON'T move the paddle inside the keydown handler — that gives you choppy keyboard-repeat-rate movement. Always poll the keys map in your step() so movement is locked to the frame rate.
CPU opponent in 4 lines
const target = by - PH / 2; // where the paddle CENTER should be
if (ry + 5 < target) ry += 4; // below target → move down
else if (ry - 5 > target) ry -= 4; // above → move up
ry = clamp(ry, 0, H - PH);
4 is the difficulty knob. Lower = easier CPU. The +5 / -5 deadband prevents jittery paddle motion when the ball passes through the center.
The whole file (~60 lines)
<!DOCTYPE html>
<html><body style="background:#0f172a;color:#e2e8f0;text-align:center;font-family:system-ui;">
<h1>🏓 Pong</h1>
<div><span id="scoreL">0</span> · <span id="scoreR">0</span></div>
<canvas id="c" width="600" height="400" style="background:#020617;border:2px solid #334155;"></canvas>
<p>W / S to move · Space to restart</p>
<script>
const c=document.getElementById('c'), ctx=c.getContext('2d');
const SL=document.getElementById('scoreL'), SR=document.getElementById('scoreR');
const PH=80, PW=10, BR=8;
let ly,ry,bx,by,vx,vy,scoreL,scoreR;
const keys={};
function reset(){ ly=ry=c.height/2-PH/2; bx=c.width/2; by=c.height/2;
vx=(Math.random()>.5?1:-1)*4; vy=(Math.random()*4)-2; }
function newGame(){ scoreL=scoreR=0; SL.textContent=0; SR.textContent=0; reset(); }
function step(){
if(keys.w) ly-=6; if(keys.s) ly+=6;
ly=Math.max(0,Math.min(c.height-PH,ly));
const t=by-PH/2;
if(ry+5<t) ry+=4; else if(ry-5>t) ry-=4;
ry=Math.max(0,Math.min(c.height-PH,ry));
bx+=vx; by+=vy;
if(by<BR||by>c.height-BR) vy=-vy;
if(bx<PW+BR && by>ly && by<ly+PH){ vx=Math.abs(vx)*1.05; vy+=((by-(ly+PH/2))/PH)*3; }
if(bx>c.width-PW-BR && by>ry && by<ry+PH){ vx=-Math.abs(vx)*1.05; vy+=((by-(ry+PH/2))/PH)*3; }
if(bx<0){ scoreR++; SR.textContent=scoreR; reset(); }
if(bx>c.width){ scoreL++; SL.textContent=scoreL; reset(); }
}
function draw(){
ctx.fillStyle='#020617'; ctx.fillRect(0,0,c.width,c.height);
ctx.fillStyle='#fff';
for(let i=0;i<c.height;i+=20) ctx.fillRect(c.width/2-1,i,2,10);
ctx.fillStyle='#22c55e'; ctx.fillRect(0,ly,PW,PH);
ctx.fillStyle='#f43f5e'; ctx.fillRect(c.width-PW,ry,PW,PH);
ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(bx,by,BR,0,Math.PI*2); ctx.fill();
}
addEventListener('keydown',e=>{ keys[e.key.toLowerCase()]=true; if(e.key===' ')newGame(); });
addEventListener('keyup', e=>{ keys[e.key.toLowerCase()]=false; });
newGame();
setInterval(()=>{ step(); draw(); }, 16);
</script>
</body></html>
Save as index.html, double-click. Done.
What this unlocks
Same skeleton → many games:
| Game | What changes |
|---|---|
| Pong | what you just built |
| Breakout | one paddle, replace right side with a brick wall |
| Air Hockey | two paddles, add side walls, no scoring zones at the goals |
| Brick Out | same as Breakout but bricks in a grid |
| Snake | swap continuous velocity for discrete grid steps (Day 1) |
State + step + draw + setInterval is the only game-engine skeleton you need for arcade games. Engines like Phaser / Pixi add features, but the core loop is identical.
Try it now
Three tabs in one page:
https://dev48v.infy.uk/game/day2-pong.html
- LOOK — play it
- UNDERSTAND — 9 click-through steps with diagrams + WHY for each physics rule
- BUILD — copy the HTML, save, open
What's next in GameFromZero
Day 3: Tetris. Block rotation, gravity, line clearing. Same loop, more state.
Series: 50 playable browser games · zero install.
🌐 All games: https://dev48v.infy.uk/gamefromzero.php
Top comments (0)