DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

I Built Pong in 60 Lines of JavaScript — Five State Variables and a setInterval

🌐 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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(); }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)