Why AI Bots?

Human play-testing is slow, inconsistent, and doesn't scale across 5 game steps × 3 layouts. An AI bot provides:

Bot Versions

Each bot version gets a number and a changelog entry. Improvements are measured by: average score, clear rate (levels completed), survival time (lives remaining), and bricks cleared %.

VersionGameStrategyStatus
Breakout v1BreakoutReactive — tracks ball X within 10px dead zone, speed 200✅ Live
Breakout v2BreakoutTrajectory prediction with wall-bounce calculationIn progress
Breakout v3BreakoutAlternating center zone — breaks ball cycling corridor on center hits✅ Live
Breakout v4Breakout4-zone cycling — sweeps full field via paddle edge offsets✅ Live (current)
Breakout v5BreakoutBrick-scanning — experimental, outperformed by v4📉 Reverted
Gun Fight v1Gun FightRandom-direction strafe + fire-toward-opponent within 450px range✅ Live

Breakout Bot

v1 — Reactive Tracker

The original bot simply follows the ball's current X position. It has a 10px dead zone to prevent jitter and moves at speed 200. Works well for simple layouts but fails on multi-ball levels (steps 3-5) where it can't track multiple active balls simultaneously.

updateBot(paddle) {
  let diff = this.ball.x - paddle.x;
  if (Math.abs(diff) > 10) {
    paddle.body.setVelocityX(diff > 0 ? 200 : -200);
  } else {
    paddle.body.setVelocityX(0);
  }
  paddle.x = Phaser.Math.Clamp(paddle.x, 40, W - 40);
  if (this.stickyBall) this.releaseSticky();
}

Limitations: No trajectory prediction. Bot reacts to the ball's current position, not where it will land. On fast angles where the ball bounces off a wall, the bot chases the ball's current X instead of moving to the predicted interception point. Results in ~3-5 missed returns per level on average.

v2 — Trajectory Predictor (current)

Improves on v1 by computing where the ball will intersect the paddle's Y level, accounting for wall bounces. This means the bot moves to the interception point rather than chasing the ball.

predictLanding(ball) {
  // Predict where ball will land at paddle Y-level
  let px = ball.x, py = ball.y;
  let vx = ball.body.velocity.x, vy = ball.body.velocity.y;
  if (vy <= 0) return px; // ball moving up — can't predict yet

  // Step forward until ball reaches paddle Y
  while (py < 438) {
    px += vx * 0.016;  // one frame at ~60fps
    py += vy * 0.016;
    // Wall bounce
    if (px < 6 || px > 674) { vx *= -1; px = px < 6 ? 6 : 674; }
  }
  return px;
}

updateBot() {
  let tx = this.predictLanding(this.ball);
  let diff = tx - this.paddle.x;
  this.paddle.body.setVelocityX(Math.abs(diff) > 10 ? (diff > 0 ? 300 : -300) : 0);
  this.paddle.x = Phaser.Math.Clamp(this.paddle.x, 40, W - 40);
  if (this.stickyBall) this.releaseSticky();
}

Improvements over v1: Faster response (speed 300 vs 200), trajectory prediction eliminates chasing, wall-bounce awareness means the bot positions for the ball's actual landing point. Missed returns drop to ~1-2 per level.

v3 — Alternating Center Zone

Fixes a degenerate case exposed by v2's trajectory prediction. When the ball hit the paddle center zone, the 0° angle produced purely vertical motion. The min-vx guard (±60) created a narrow corridor that only hit the center column of bricks — cycling forever without spreading to other bricks.

// v3: center zone alternates direction every hit
var angles = [-65, -35, this._paddleHits % 2 === 0 ? 20 : -20, 35, 65];

Result: Even returns go +20°, odd returns go -20°. The ball naturally spreads across the full width within 3-4 returns.

Benchmark (10 trials × 20s on step-01): avg score 36.7, median 21.5, high 113, bricks 13.3, 100% survival. Median +43% over v2 (15→21.5).

v4 — 4-Zone Cycling (current)

The trajectory predictor lands every return at center, always hitting paddle zone 2 (±20°). This produces a narrow corridor. v4 adds an intentional lateral offset that alternates through all 4 wide paddle edge zones every 4 hits. This sweeps the full 680px brick field.

// v4: cycle through 4 wide zones — skip center
var zoneOffsets = [32, 16, -16, -32];
var zoneIdx = (this._paddleHits || 0) % 4;
tx += zoneOffsets[zoneIdx];

Benchmark (5 trials × 90s on step-05): avg score 53.6, median 64, high 68, bricks 16/50, 100% survival. 3.3x improvement over no-offset baseline. The bot cannot clear all 50 bricks under 5 min because brick collisions scatter the ball chaotically — about 1 brick per 6s regardless of zone targeting.

v5 — Brick-Scanning (experimental)

Attempted. Scanned remaining bricks per column, aimed at the densest cluster. Failed — the zone-to-column mapping is too approximate. Brick collisions and wall bounces make the trajectory too chaotic for a predetermined "best zone." The v4 blind cycle outperforms it.

Lesson: Rule-based brick targeting in Breakout is a hard AI problem. Brick collisions produce chaotic trajectory changes. To clear levels, you need reinforcement learning (DQN Atari style) or a full brick-collision trajectory simulator.

v4 — Multi-ball + Priority Target (future)

Planned. For steps 3-5 (power-ups, multi-ball), the bot must handle multiple active balls simultaneously. Strategy: track the most dangerous ball (closest to falling below the paddle), predict its landing, and catch it. Less urgent balls are ignored.

Why the Bot Can't Clear Levels

Every bot version achieves 100% survival — they never lose a life. But none clear a single level (50 bricks) within a reasonable benchmark window. This isn't a bug — it's a physics constraint. Here's why.

Brick Collisions Are Chaotic

When the ball hits a brick, it bounces unpredictably. The bounce angle depends on: which edge of the brick it hit, the ball's trajectory at impact, and which adjacent bricks are already destroyed. Each brick hit scatters the ball slightly — and 50 bricks produce 50 trajectory changes. A hand-coded predictor can't account for this complexity because the state space (ball trajectory × remaining brick layout) is enormous.

Time Constraint

MetricStep-01 (v3)Step-05 (v4)
Bricks / sec0.670.18
Bricks / 90s13.316
Time to clear 50 bricks~75s~280s
Level 1 clear rate~50% in 90s~0% in 90s

Step-01 clears faster because it uses continuous angle mapping (diff * 1.8) — every paddle hit produces a unique trajectory. Step-05 uses 5 discrete zones. The trajectory predictor lands every return at center, which always hits zone 2. Even with the v4 offset cycling, the ball only visits 4 angles before repeating. Step-01's ball visits a new angle every time.

The Speed Tunneling Problem

Attempting a 10x speed multiplier (?speed=10) would let the bot play 10 minutes of game time in 1 real minute — enough to clear levels. But at 10x physics speed, the ball moves 48px per frame while bricks are only 20px thick. The ball tunnels through bricks without collision detection firing. Even Phaser's maxSteps sub-stepping can't fix this — the delta per sub-step is still too large relative to brick thickness.

Rule-Based Targeting Is Fragile

The v5 experiment (brick-scanning) confirmed that a hand-coded zone-to-column mapping can't survive brick collisions. The ball's actual trajectory after hitting one brick is unpredictable — so a predetermined "best zone" doesn't reliably reach the intended column. The blind-cycling v4 outperformed smart targeting because it spreads evenly without depending on a fragile trajectory model.

The Real Solution: Reinforcement Learning

Breakout is a solved problem in RL. DQN (Deep Q-Network, 2013) achieved superhuman performance with 10M frames of training. A trained RL agent learns the chaotic brick-physics dynamics implicitly — it doesn't need a zone-to-column mapping because it's trained on millions of actual trajectories. The hand-coded bot hits the ceiling at ~32% brick coverage per 90s. An RL agent can clear 100% of bricks in the same time.

Building an RL training loop for this game is a separate project (state encoding, reward shaping, hyperparameter tuning, ~10M frame training run). The hand-coded bot is infrastructure for game testing — perfect survival is useful for that role even without level clearance.

Gun Fight Bot

v1 — Random Strafe + Fire

The bot moves left-right randomly within boundary zones, fires when the opponent is within 450px. Used for both ?bot (bot vs bot) and ?ai (human vs AI) modes. The AI variant adds Y-axis tracking of the player.

botAI() {
  // P1: strafe in left zone
  this.p1.body.setVelocityX((d > 0 ? 1 : -1) * SPD * 0.5);
  if (this.p1.x < 40 || this.p1.x > 300) this.p1.body.setVelocityX(...);
  if (Math.abs(d) < 450 && Math.random() < 0.2) this.fireBullet(...);

  // P2: strafe in right zone
  this.p2.body.setVelocityX((d > 0 ? 1 : -1) * SPD * 0.5);
  if (this.p2.x > 640 || this.p2.x < 380) this.p2.body.setVelocityX(...);
  if (Math.abs(d) < 450 && Math.random() < 0.2) this.fireBullet(...);
}

Limitations: Random strafe direction — no strategic positioning. Fire rate is random (20% per 160ms tick ≈ 1.25 bullets/sec average). No bullet-dodge logic. Future versions could add: strafe toward opponent center, aim ahead of opponent movement, dodge incoming bullets.

How Bots Are Scored

Each bot run produces a standard set of metrics for comparison:

MetricBreakoutGun Fight
PrimaryScore, Clear rateWin rate (bot vs bot)
SecondaryBricks cleared %, Lives remainingAvg bullets fired, Hit accuracy
Run count10 runs per version25 matches per version
Bot vsLevel layoutsSame-version opponent

Benchmark Results

GameVersionStrategyAvgMinHighMedianMax ScoreBricksLevelsLivesTrialsStale
breakoutv2Trajectory Prediction32.781311517012.70310✅ No
breakoutv1Reactive Chasing (speed 200)19.51102101709.502.710✅ No
breakoutv3 (step-01)Alternating Center Zone36.71011321.517013.30310✅ No
breakoutv3 (step-05)Lateral Offset + Per-Hit Alternation53.622686417016035✅ No
breakoutv5Brick-Scanning + Trajectory Prediction7.802401702.6035✅ No
gun-fightv1Random Strafe + FireNot yet benchmarked

Benchmarks run for 20s per trial on step-01 layout. Scores vary significantly per run — more trials needed for reliable comparison. Run bash scripts/benchmark-bot.sh to add new results.

Reading Max Score as a Diagnostic

Max score is a bug detector. In Breakout, every brick destroyed adds to score. If a bot finishes a trial with all 3 lives but a low max score, it means the bot survived but wasn't scoring — a stale state or logic bug. A healthy bot with 3 lives should push its ceiling on every run.

Current check: v2 survives at 100% (3/3 lives every trial) but its max was 54 (actual highScore: 131). v3's alternating center zone fix raised median 43% (15→21.5) with highScore 113 and no stale states. The corridor is broken — the remaining ceiling gap is now normal variance, not a stuck state.

For future multi-level steps: a bot with all lives at timeout that hasn't cleared every level indicates a layout navigation bug.

Running Bot Benchmarks

Benchmarks are automated via the Playwright test suite. To run a bot scoring session manually:

# Breakout bot mode (loads and plays automatically)
open https://aigamingdev.com/games/breakout/breakout-step-01.html?bot=true

# Gun Fight bot mode (bot vs bot)
open https://aigamingdev.com/games/gun-fight/gun-fight-01.html?bot

# Gun Fight AI mode (human vs AI)
open https://aigamingdev.com/games/gun-fight/gun-fight-01.html?ai

Bot Improvement Log

DateGameVersionChange
2026-06-30Breakoutv1Initial reactive tracker: follows ball X, 10px dead zone, speed 200
2026-06-30Breakoutv2Trajectory prediction with wall-bounce calculation, speed 300
2026-07-01Breakoutv3Alternating center zone: ±20° on center hits to break ball cycling corridor. Applied to all breakout steps. v2 exposed degenerate 0° bounce physics.
2026-07-01Breakoutv44-zone cycling: lateral offset sweeps through paddle edge zones (0,1,3,4) for full field coverage. 3.3x improvement over no-offset baseline. Applied to step-05.
2026-07-01Breakoutv5 (exp)Brick-scanning: attempted to aim at remaining brick clusters. Failed — zone-to-column mapping too approximate for chaotic brick physics. Reverted to v4.
2026-06-30Gun Fightv1Initial random strafe + fire with 450px range

See Also