Back to Blog
TutorialApril 17, 2026

How I Built a Ping Pong Game with AI in 3 Hours

A complete guide to building a classic table tennis game with charge system, spin mechanics, and mobile touch controls using AI tools.

👤
AI Tools Lab Team

How I Built a Ping Pong Game with AI in 3 Hours

A complete journey from concept to playable ping pong game with advanced mechanics like charge system, spin ball, and mobile touch controls.

The Challenge

Build a complete, playable ping pong game with modern mechanics (charge system, spin ball, effects) in just a few hours using AI tools for every stage of development.

Timeline Overview

Time Phase Output
0-1h Concept & Design Game mechanics, core features
1-2h Core Development Game loop, paddle controls, ball physics
2-3h Advanced Features Charge system, spin ball, effects
3-4h Mobile Adaptation Touch controls, responsive design
4-5h Bug Fixes & Polish Two-player touch, restart logic

Phase 1: Concept & Design (1 hour)

Game Mechanics:

  • Top-bottom paddle structure (Player 1 at bottom, Player 2/Computer at top)
  • Ball bounces between paddles with increasing speed
  • First to 5 points wins
  • Single-player mode (vs Computer AI)
  • Two-player mode (local same-screen multiplayer)

Core Features:

  • Charge system (hold to charge, loops when full)
  • Spin ball (add spin by pressing direction keys when hitting)
  • Effects system (particles, trails, screen shake)
  • Mobile gesture controls (push forward to charge, drag to move)

Visual Style:

  • Clean, modern design with gradient backgrounds
  • Particle effects for powerful hits
  • Ball trail during gameplay
  • Paddle bend effect when charging

Tools used: ChatGPT/Claude for brainstorming game mechanics

Phase 2: Core Development (1 hour)

Tech Stack:

  • Vanilla JavaScript (ES6+)
  • HTML5 Canvas
  • CSS3 Responsive Design
  • No external dependencies

Core Systems:

Game Loop

function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

function update() {
  updateBall();
  updatePaddles();
  checkCollisions();
  checkScoring();
  checkWinCondition();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawCourt();
  drawPaddles();
  drawBall();
  drawScores();
  drawChargeBars();
}

Ball Physics

function updateBall() {
  this.ball.x += this.ball.vx;
  this.ball.y += this.ball.vy;

  // Wall bounce (left/right)
  if (this.ball.x <= 0 || this.ball.x >= this.width - this.ball.size) {
    this.ball.vx = -this.ball.vx;
  }

  // Paddle collision
  if (this.checkPaddleCollision(this.player1)) {
    const hitPoint = (this.ball.x - this.player1.x) / this.player1.width;
    const angle = (hitPoint - 0.5) * Math.PI * 0.7;
    const speed = Math.sqrt(this.ball.vx ** 2 + this.ball.vy ** 2);
    
    this.ball.vx = speed * Math.sin(angle);
    this.ball.vy = -speed * Math.cos(angle);
  }
}

Paddle Controls

// Keyboard controls
const keys = {
  a: false,
  d: false,
  ArrowLeft: false,
  ArrowRight: false
};

window.addEventListener('keydown', (e) => {
  if (keys.hasOwnProperty(e.key)) keys[e.key] = true;
});

window.addEventListener('keyup', (e) => {
  if (keys.hasOwnProperty(e.key)) keys[e.key] = false;
});

function updatePaddles() {
  // Player 1 (bottom) - A/D keys
  if (keys.a) this.player1.x -= this.player1.speed;
  if (keys.d) this.player1.x += this.player1.speed;

  // Player 2 (top) - Arrow keys
  if (keys.ArrowLeft) this.player2.x -= this.player2.speed;
  if (keys.ArrowRight) this.player2.x += this.player2.speed;

  // Clamp to court boundaries
  this.player1.x = Math.max(0, Math.min(this.width - this.player1.width, this.player1.x));
  this.player2.x = Math.max(0, Math.min(this.width - this.player2.width, this.player2.x));
}

AI Assistance:

  • Used Cursor/Copilot for collision detection logic
  • AI helped optimize ball physics calculations
  • Quick debugging with AI pair programming

Phase 3: Advanced Features (1 hour)

Charge System

Problem: Want to add more strategy with a charge-powered hit system.

How to Describe to AI:

Please add a charge system:
- Hold Space (bottom) or Numpad 0 (top) to charge
- Charges fully in 1.5 seconds, then loops from 0
- Charge value decreases slowly when released
- Charging increases ball speed and spin when serving
- Display charge bar and power level indicators

AI's Proposed Solution:

Charge Effect Icon
0-50% Normal 💪
50-80% Power
80-100% Heavy 🔥

Implementation:

function updateCharge(deltaTime) {
  const chargeRate = 100 / 1.5; // ~66.67% per second

  if (this.player1.isCharging) {
    this.player1.charge += chargeRate * deltaTime;
    if (this.player1.charge >= 100) {
      this.player1.charge = this.player1.charge % 100; // Loop
    }
  } else {
    // Decrease slowly when released
    if (this.player1.charge > 0) {
      this.player1.charge -= chargeRate * deltaTime;
    }
  }
}

function getPowerIcon(charge) {
  if (charge >= 80) return '🔥';
  if (charge >= 50) return '⚡';
  if (charge >= 20) return '💪';
  return '';
}

Spin Ball System

Problem: Want to add more skill to hitting with a spin ball feature.

How to Describe to AI:

Please add spin ball functionality:
- Press A/D (bottom) or ← → (top) when hitting to add spin
- Spin direction: left spin / right spin
- Spin intensity relates to charge value
- Spin decays over time
- Spin affects ball flight trajectory

AI Solution:

// Detect key press when hitting
if (this.checkPaddleCollision(this.player1)) {
  let hitSpin = 0;
  
  if (this.keys.a) {
    hitSpin = -1 * (this.player1.charge / 100); // Left spin
  } else if (this.keys.d) {
    hitSpin = 1 * (this.player1.charge / 100); // Right spin
  }

  this.ball.spin = hitSpin;
  this.ball.vx += this.ball.spin * 5;
}

// Spin decay during flight
this.ball.spin *= 0.99;

Effects System

Problem: Visual effects are not rich enough, need to add more.

How to Describe to AI:

Please add game effects:
1. Hit particle effects - more particles with more power
2. Ball trail effect - only show during gameplay
3. Screen shake when scoring
4. Paddle bend effect - paddle deforms when charging

AI's Implementation:

// Particle system
function createHitParticles(x, y, power) {
  const particleCount = Math.floor(power / 20) + 5;
  
  for (let i = 0; i < particleCount; i++) {
    this.particles.push({
      x: x,
      y: y,
      vx: (Math.random() - 0.5) * 10,
      vy: (Math.random() - 0.5) * 10,
      life: 1,
      decay: 0.02 + Math.random() * 0.03,
      color: `hsl(${Math.random() * 60 + 280}, 100%, 60%)`
    });
  }
}

function updateParticles() {
  this.particles.forEach((p, i) => {
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.5; // Gravity
    p.life -= p.decay;
    
    if (p.life <= 0) this.particles.splice(i, 1);
  });
}

// Ball trail
function drawBallTrail() {
  if (this.ball.trail.length < 2) return;
  
  ctx.beginPath();
  ctx.moveTo(this.ball.trail[0].x, this.ball.trail[0].y);
  
  for (let i = 1; i < this.ball.trail.length; i++) {
    ctx.lineTo(this.ball.trail[i].x, this.ball.trail[i].y);
  }
  
  ctx.strokeStyle = 'rgba(147, 51, 234, 0.5)';
  ctx.lineWidth = this.ball.size * 0.8;
  ctx.stroke();
}

// Screen shake
function applyScreenShake() {
  if (this.shakeIntensity > 0) {
    const dx = (Math.random() - 0.5) * this.shakeIntensity;
    const dy = (Math.random() - 0.5) * this.shakeIntensity;
    ctx.translate(dx, dy);
    this.shakeIntensity *= 0.9;
    
    if (this.shakeIntensity < 0.5) {
      this.shakeIntensity = 0;
    }
  }
}

Phase 4: Mobile Adaptation (1 hour)

Responsive Sizing

Problem: Game screen becomes very small on mobile with black borders.

How to Describe to AI:

The game screen becomes very small on mobile with black borders around it.
The canvas uses a fixed 500x700 size. How can I make it fullscreen on mobile?

AI Solution: Use CSS media queries, mobile uses 100vw/100vh

/* PC default */
#gameCanvas {
  width: 500px;
  height: 700px;
}

/* Mobile */
@media (max-width: 768px) {
  #gameCanvas {
    width: 100vw;
    height: 100vh;
    max-width: 100%;
    max-height: 100%;
  }
}

Touch Controls

Problem: PC keyboard controls don't work on mobile, need touch controls.

How to Describe to AI:

Please add mobile touch controls:
- Drag to control paddle left/right movement
- Push forward gesture to charge (bottom player pushes up, top player pushes down)
- Move back to stop charging
- Support two players touching simultaneously

AI Solution:

// Touch point tracking
this.touchPoints = {
  top: { id: null, startX: null, startY: null, lastX: null },
  bottom: { id: null, startX: null, startY: null, lastX: null }
};

function handleTouchStart(e) {
  e.preventDefault();
  
  for (let i = 0; i < e.touches.length; i++) {
    const touch = e.touches[i];
    const rect = canvas.getBoundingClientRect();
    const touchX = (touch.clientX - rect.left) * (canvas.width / rect.width);
    const touchY = (touch.clientY - rect.top) * (canvas.height / rect.height);
    
    // Assign touch to zone
    if (touchY < this.height / 2 && this.touchPoints.top.id === null) {
      this.touchPoints.top.id = touch.identifier;
      this.touchPoints.top.startX = touchX;
      this.touchPoints.top.lastX = touchX;
      this.touchPoints.top.startY = touchY;
    } else if (touchY >= this.height / 2 && this.touchPoints.bottom.id === null) {
      this.touchPoints.bottom.id = touch.identifier;
      this.touchPoints.bottom.startX = touchX;
      this.touchPoints.bottom.lastX = touchX;
      this.touchPoints.bottom.startY = touchY;
    }
  }
}

function handleTouchMove(e) {
  e.preventDefault();
  
  for (let i = 0; i < e.touches.length; i++) {
    const touch = e.touches[i];
    const rect = canvas.getBoundingClientRect();
    const touchX = (touch.clientX - rect.left) * (canvas.width / rect.width);
    const touchY = (touch.clientY - rect.top) * (canvas.height / rect.height);
    
    // Check if it's a registered top touch point
    if (this.touchPoints.top.id === touch.identifier) {
      const deltaX = touchX - this.touchPoints.top.lastX;
      this.player2.x += deltaX;
      this.touchPoints.top.lastX = touchX;
      
      // Check for charge gesture (push forward)
      const pushDistance = this.touchPoints.top.startY - touchY;
      if (pushDistance > 50) {
        this.player2.isCharging = true;
      }
    }
    
    // Check if it's a registered bottom touch point
    if (this.touchPoints.bottom.id === touch.identifier) {
      const deltaX = touchX - this.touchPoints.bottom.lastX;
      this.player1.x += deltaX;
      this.touchPoints.bottom.lastX = touchX;
      
      // Check for charge gesture (push forward)
      const pushDistance = touchY - this.touchPoints.bottom.startY;
      if (pushDistance > 50) {
        this.player1.isCharging = true;
      }
    }
  }
}

function handleTouchEnd(e) {
  e.preventDefault();
  
  // Reset charge on touch end
  for (let i = 0; i < e.changedTouches.length; i++) {
    const touch = e.changedTouches[i];
    
    if (this.touchPoints.top.id === touch.identifier) {
      this.touchPoints.top.id = null;
      this.player2.isCharging = false;
    }
    
    if (this.touchPoints.bottom.id === touch.identifier) {
      this.touchPoints.bottom.id = null;
      this.player1.isCharging = false;
    }
  }
}

Phase 5: Bug Fixes & Polish (1 hour)

Two-Player Touch Control Bug Fix

Problem: In mobile two-player mode, when one finger drags, the other finger's drag doesn't work.

How to Describe to AI:

In two-player mode, when I touch both top and bottom areas
with two fingers simultaneously, only one paddle moves,
the other doesn't respond. Here's the touch control code, please analyze.

AI Analysis:

  • Issue 1: Using single activeTouchZone variable, later touch overwrites previous
  • Issue 2: Using touchY < this.height / 2 for real-time zone detection, fails when crossing center line
  • Issue 3: Using if-else structure, only one branch can execute

AI's Improvement Suggestions:

1. Use touchPoints object to record touch info for each zone separately
2. Match registered touch points via touch.identifier
3. Use two independent if statements to ensure both touch points are processed
4. Use e.changedTouches in handleTouchEnd to precisely identify ended touch points

Fixed Code:

// Check if it's a registered top touch point
if (this.touchPoints.top.id === touch.identifier) {
  // Control player 2
  this.player2.x += deltaXFromLast;
}

// Check if it's a registered bottom touch point  
if (this.touchPoints.bottom.id === touch.identifier) {
  // Control player 1
  this.player1.x += deltaXFromLast;
}

Game Over Restart Bug Fix

Problem: Clicking "Click to Restart" after game over has no effect.

How to Describe to AI:

After game over, "Click to Restart" text appears on canvas,
but clicking has no reaction. Here's the game over related code.

AI Analysis:

  • Issue 1: Text drawn in drawGameOver is just canvas image, not a real button
  • Issue 2: showGameOver function's overlay tries to append to .game-ui element, which doesn't exist
  • Issue 3: createCanvas() clears container.innerHTML, so .game-ui doesn't exist

AI Solution:

// Fix 1: Append overlay directly to container
container.style.position = 'relative';
container.appendChild(overlay);

// Fix 2: Simplify restartGame logic
function restartGame() {
  const overlay = container.querySelector('.game-over');
  if (overlay) overlay.remove();

  if (game) {
    game.isGameOver = false;
    game.isRunning = false;
    game.reset();
    game.start();
  }
}

Testing Issues and AI Solutions

Issue 1: Mobile Screen Shrinking

Symptom: Game screen much smaller than phone screen with black borders

Question to AI:

The game screen becomes very small on mobile with black borders around it.
How do I make it display fullscreen on mobile?

AI Solution: Use CSS media queries, mobile uses 100vw/100vh


Issue 2: Two-Player Touch Control Failure

Symptom: In two-player mode, only one paddle moves when both fingers touch simultaneously

Question to AI:

In two-player mode, when two fingers touch top and bottom areas simultaneously,
only one paddle follows, the other doesn't respond.
Here's the touch control code.

AI Solution: Use touch.identifier to track each touch point, record touch info for each zone separately


Issue 3: Game Over Restart Not Working

Symptom: Clicking restart button after game over has no effect

Question to AI:

After game over, "Click to Restart" appears but clicking has no reaction.
Here's the showGameOver and restartGame code.

AI Solution: Append overlay directly to container, simplify restart logic


Issue 4: Charge Value Reset

Symptom: Charge value disappears immediately when released, should decrease slowly

Question to AI:

After charging and releasing, the charge value resets to zero directly,
it should decrease slowly. How to fix?

AI Solution: Only set isCharging = false when released, let updateCharge() decrease the value slowly


Issue 5: Touch Teleport

Symptom: Paddle teleports to touch position on touch, should be drag control

Question to AI:

When touching, the paddle teleports directly to the touch position,
it should be relative drag control. How to fix?

AI Solution: Use deltaXFromLast to calculate relative movement distance

Final Results

Development Stats:

  • Total time: ~3-4 hours
  • Lines of code: ~1,200
  • Features implemented: Charge system, spin ball, particles, trails, screen shake
  • Mobile support: Touch controls, responsive design
  • Test pass rate: 100%

Game Stats:

  • File size: ~150 KB (minified)
  • Performance: 60 FPS
  • Browser support: Chrome, Firefox, Safari, Edge
  • Mobile: Full touch control support

Code Structure

ping-pong/
├── index.html           # Main page (start screen, styles)
├── js/
│   ├── index.js         # Game logic (~1200 lines)
│   ├── game-base.js     # Game base class (shared)
│   └── utils.js         # Utility functions (shared)
├── AI 开发教程.md        # Development tutorial
└── DEVELOPMENT.md       # This development log

Key Learnings

What Worked Well

  1. Clear problem descriptions - Specific issues get better AI solutions
  2. Iterative development - Build core first, then add features
  3. Touch identifier tracking - Multi-touch requires careful ID management
  4. Charge loop system - Creates engaging gameplay mechanic

What Could Be Better

  1. More game modes - Could add tournament mode, practice mode
  2. Online multiplayer - Would be great with WebSocket
  3. More spin types - Topspin, backspin, sidespin variations
  4. AI difficulty levels - Easy, medium, hard computer opponents

Tips for Collaborating with AI

1. Describe Problems Clearly

  • ❌ Vague: "There's a problem with the game"
  • ✅ Specific: "In two-player mode, only one paddle moves when both fingers touch simultaneously"

2. Provide Context

  • Explain project structure (e.g., inherited base class)
  • Attach relevant code snippets
  • Mention solutions already attempted

3. Iterate Step by Step

Don't ask AI to complete everything at once. Instead:

  1. Implement core functionality first (basic ping pong)
  2. Test and discover issues
  3. Describe problems to AI
  4. Apply solutions
  5. Continue testing next feature

4. Validate AI Code

AI-generated code needs:

  • Testing on real devices (especially mobile)
  • Checking edge cases (e.g., multi-touch simultaneously)
  • Verifying performance (particle count, animation smoothness)

5. Keep Iteration Records

Document each AI conversation and solution for:

  • Tracing problem roots
  • Understanding modification reasons
  • Future maintenance reference

Play the Demo

The game is now live:

Next Steps

Planning to add:

  • Online multiplayer mode
  • Tournament mode with brackets
  • More spin types (topspin, backspin)
  • AI difficulty levels
  • Replay system
  • High score leaderboard

Want to build your own game? Check out our AI Coding Tools collection.

Play the demo: Ping Pong

Read about our other games: Tank Battle in 24 Hours, Space Shooter in 24 Hours

Share this article