Models 44

Claude Haiku 5.5

by AnthropicReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
36.7 s
Cost
$0.0056
Output tokens
11,017
Spent thinking
0 tokens
Speed
310.4 tok/s
Console errors
0
First token after
1.2 s
First visible text after
1.2 s
Prompt tokens
792
Code size
21.8 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"none"}
Max output
32,000 tokens
Generated
Nov 10, 2026

What it was asked

The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 653 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
  :root { --bg1:#0b0f2a; --bg2:#1a1046; --accent:#5ff7ff; --text:#e8ecff; }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { height: 100%; background: radial-gradient(circle at 50% 30%, var(--bg2), var(--bg1)); color: var(--text); font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; overflow: hidden; }
  body { display: flex; flex-direction: column; align-items: center; justify-content: center; }
  header { width: min(640px, 96vw); display: flex; justify-content: space-between; align-items: center; padding: 8px 4px; font-size: 1rem; letter-spacing: .05em; }
  header .title { font-weight: 700; color: var(--accent); text-shadow: 0 0 12px rgba(95,247,255,.6); }
  header .hud span { margin-left: 14px; }
  #wrap { position: relative; width: min(640px, 96vw); aspect-ratio: 4 / 5; max-height: 78vh; }
  canvas { width: 100%; height: 100%; display: block; border-radius: 12px; border: 1px solid rgba(95,247,255,.35); box-shadow: 0 0 30px rgba(95,247,255,.15), inset 0 0 40px rgba(0,0,0,.5); background: #070a1c; cursor: none; touch-action: none; }
  #overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; background: rgba(5,7,20,.6); border-radius: 12px; pointer-events: none; text-align: center; padding: 20px; }
  #overlay h2 { font-size: clamp(1.6rem, 4vw, 2.4rem); color: var(--accent); text-shadow: 0 0 14px rgba(95,247,255,.7); }
  #overlay p { opacity: .85; font-size: clamp(.9rem, 2.2vw, 1.05rem); }
  #overlay .hint { animation: pulse 1.4s infinite; color: #fff; }
  #overlay.hidden { display: none; }
  #overlay button { pointer-events: auto; background: linear-gradient(90deg,#5ff7ff,#8a7bff); border: none; color: #061024; font-weight: 700; padding: 10px 26px; border-radius: 999px; font-size: 1rem; cursor: pointer; box-shadow: 0 0 18px rgba(95,247,255,.5); }
  #overlay button:hover { filter: brightness(1.1); }
  @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
  footer { margin-top: 8px; font-size: .8rem; opacity: .6; }
  @media (max-width: 420px) { header { font-size: .85rem; } footer { font-size: .7rem; } }
</style>
</head>
<body>
<header>
  <div class="title">BREAKOUT</div>
  <div class="hud">
    <span>Score: <b id="score">0</b></span>
    <span>Lives: <b id="lives">3</b></span>
  </div>
</header>
<div id="wrap">
  <canvas id="game" width="640" height="800"></canvas>
  <div id="overlay">
    <h2 id="ovTitle">BREAKOUT</h2>
    <p id="ovText">Mouse or ← → to move. Break every brick.</p>
    <p class="hint" id="ovHint">Click or press a key to start</p>
  </div>
</div>
<footer>Mouse / ← → to move · Click or press a key to start</footer>

<script>
(function () {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  const scoreEl = document.getElementById('score');
  const livesEl = document.getElementById('lives');
  const overlay = document.getElementById('overlay');
  const ovTitle = document.getElementById('ovTitle');
  const ovText = document.getElementById('ovText');
  const ovHint = document.getElementById('ovHint');

  // ---- Config ----
  const PADDLE_W = 110, PADDLE_H = 16, PADDLE_Y = H - 50;
  const PADDLE_SPEED = 620;
  const BALL_R = 9;
  const BASE_SPEED = 430;
  const ROWS = 7, COLS = 10;
  const BRICK_W = 56, BRICK_H = 22, BRICK_GAP = 6;
  const BRICK_TOP = 90;
  const BRICK_LEFT = (W - (COLS * (BRICK_W + BRICK_GAP) - BRICK_GAP)) / 2;
  const ROW_COLORS = ['#ff4d6d','#ff8c42','#ffd166','#06d6a0','#4cc9f0','#8a7bff','#f72585'];
  const POWER_TIME = 10;

  // ---- State ----
  let state = 'title'; // title | playing | paused | over | won
  let score = 0, lives = 3;
  let bricks = [], balls = [], particles = [], drops = [];
  let paddle = { x: W / 2 - PADDLE_W / 2, w: PADDLE_W, targetX: null };
  let widePaddleTimer = 0;
  let keys = { left: false, right: false };
  let lastTime = performance.now();
  let shake = 0;
  let demoMode = true; // self-playing demo until user starts
  let demoTime = 0;

  // ---- Brick setup ----
  function buildLevel() {
    bricks = [];
    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        const tough = (r === 0 || (r === 2 && c % 3 === 1) || (r === 4 && c % 4 === 2));
        const powerup = (!tough && (r * 7 + c * 3) % 11 === 0) ? (((r + c) % 2) ? 'wide' : 'multi') : null;
        bricks.push({
          x: BRICK_LEFT + c * (BRICK_W + BRICK_GAP),
          y: BRICK_TOP + r * (BRICK_H + BRICK_GAP),
          w: BRICK_W, h: BRICK_H,
          color: ROW_COLORS[r % ROW_COLORS.length],
          hp: tough ? 2 : 1,
          maxHp: tough ? 2 : 1,
          power: powerup,
          alive: true,
          flash: 0,
          cracks: []
        });
      }
    }
  }

  function resetBall(stuck) {
    balls = [];
    const b = { x: paddle.x + paddle.w / 2, y: PADDLE_Y - BALL_R - 2, vx: 0, vy: 0, stuck: true, trail: [] };
    balls.push(b);
    if (!stuck) launchBall(b);
  }

  function launchBall(b) {
    b.stuck = false;
    const ang = (-Math.PI / 2) + (Math.random() - 0.5) * 0.6;
    b.vx = Math.cos(ang) * BASE_SPEED;
    b.vy = Math.sin(ang) * BASE_SPEED;
  }

  function newGame() {
    score = 0; lives = 3;
    paddle.w = PADDLE_W; widePaddleTimer = 0;
    particles = []; drops = [];
    buildLevel();
    resetBall(false);
    updateHud();
    state = 'playing';
    demoMode = false;
    overlay.classList.add('hidden');
  }

  function startDemoRound() {
    buildLevel();
    score = 0; lives = 3;
    particles = []; drops = [];
    paddle.w = PADDLE_W;
    resetBall(false);
    updateHud();
    state = 'playing';
  }

  function updateHud() {
    scoreEl.textContent = score;
    livesEl.textContent = lives;
  }

  function spawnParticles(x, y, color, n) {
    for (let i = 0; i < n; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = 60 + Math.random() * 220;
      particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: 0.6 + Math.random() * 0.5, max: 1.1, color, size: 2 + Math.random() * 3 });
    }
  }

  // ---- Input ----
  function movePaddleToPointer(clientX) {
    const rect = canvas.getBoundingClientRect();
    const x = ((clientX - rect.left) / rect.width) * W;
    paddle.targetX = x - paddle.w / 2;
  }
  canvas.addEventListener('mousemove', e => {
    if (demoMode) return;
    movePaddleToPointer(e.clientX);
  });
  canvas.addEventListener('touchmove', e => {
    if (demoMode) return;
    e.preventDefault();
    movePaddleToPointer(e.touches[0].clientX);
  }, { passive: false });
  canvas.addEventListener('touchstart', e => { handleStart(); movePaddleToPointer(e.touches[0].clientX); }, { passive: true });
  canvas.addEventListener('click', handleStart);

  function handleStart() {
    if (state === 'title' || state === 'over' || state === 'won') {
      newGame();
    } else if (state === 'playing') {
      const stuck = balls.find(b => b.stuck);
      if (stuck) launchBall(stuck);
    }
  }

  window.addEventListener('keydown', e => {
    if (e.key === 'ArrowLeft') { keys.left = true; e.preventDefault(); }
    if (e.key === 'ArrowRight') { keys.right = true; e.preventDefault(); }
    if (demoMode || state !== 'playing') {
      if (state !== 'playing' || demoMode) {
        if (['ArrowLeft','ArrowRight',' ','Enter'].includes(e.key) || e.key.length === 1) {
          if (state !== 'playing' || demoMode) newGame();
        }
      }
    } else if (e.key === ' ') {
      const stuck = balls.find(b => b.stuck);
      if (stuck) launchBall(stuck);
    }
    if (e.key === 'Enter' && (state === 'over' || state === 'won')) newGame();
  });
  window.addEventListener('keyup', e => {
    if (e.key === 'ArrowLeft') keys.left = false;
    if (e.key === 'ArrowRight') keys.right = false;
  });

  // ---- Logic ----
  function paddleCenter() { return paddle.x + paddle.w / 2; }

  function updatePaddle(dt) {
    if (demoMode) {
      // Simple AI tracking the lowest ball
      const b = balls.filter(b => !b.stuck).sort((a, b) => b.y - a.y)[0] || balls[0];
      if (b) {
        const target = b.x - paddle.w / 2 + Math.sin(demoTime * 2) * 12;
        const diff = target - paddle.x;
        const step = PADDLE_SPEED * dt;
        paddle.x += Math.max(-step, Math.min(step, diff));
      }
    } else {
      if (keys.left) paddle.x -= PADDLE_SPEED * dt;
      if (keys.right) paddle.x += PADDLE_SPEED * dt;
      if (paddle.targetX !== null) {
        paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt * 18);
      }
    }
    paddle.x = Math.max(0, Math.min(W - paddle.w, paddle.x));
  }

  function reflectFromPaddle(b) {
    const hit = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2); // -1..1
    const clamped = Math.max(-1, Math.min(1, hit));
    const maxAngle = 70 * Math.PI / 180;
    const ang = -Math.PI / 2 + clamped * maxAngle;
    const speed = Math.hypot(b.vx, b.vy) * 1.015;
    const s = Math.min(speed, BASE_SPEED * 1.6);
    b.vx = Math.cos(ang) * s;
    b.vy = -Math.abs(Math.sin(ang) * s);
    b.y = PADDLE_Y - BALL_R - 0.5;
  }

  function hitBrick(b, brick) {
    brick.hp--;
    brick.flash = 0.12;
    if (brick.hp > 0) {
      // crack the tough brick
      brick.cracks.push({
        x1: Math.random() * brick.w, y1: 0,
        x2: brick.w * (0.3 + Math.random() * 0.4), y2: brick.h * (0.5 + Math.random() * 0.5)
      });
      spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, '#ffffff', 6);
      score += 5;
    } else {
      brick.alive = false;
      spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, brick.color, 22);
      score += brick.maxHp > 1 ? 25 : 10;
      shake = Math.min(shake + 2, 6);
      if (brick.power) {
        drops.push({ x: brick.x + brick.w / 2, y: brick.y + brick.h / 2, vy: 140, type: brick.power, r: 12 });
      }
    }
    updateHud();
  }

  function collideBricks(b) {
    for (const brick of bricks) {
      if (!brick.alive) continue;
      if (b.x + BALL_R < brick.x || b.x - BALL_R > brick.x + brick.w ||
          b.y + BALL_R < brick.y || b.y - BALL_R > brick.y + brick.h) continue;
      // Determine side of entry using previous position
      const prevX = b.x - b.vx / 60, prevY = b.y - b.vy / 60;
      const fromLeft = prevX + BALL_R <= brick.x;
      const fromRight = prevX - BALL_R >= brick.x + brick.w;
      const fromTop = prevY + BALL_R <= brick.y;
      const fromBottom = prevY - BALL_R >= brick.y + brick.h;
      if (fromLeft || fromRight) {
        b.vx = -b.vx;
        b.x = fromLeft ? brick.x - BALL_R - 0.5 : brick.x + brick.w + BALL_R + 0.5;
      } else if (fromTop || fromBottom) {
        b.vy = -b.vy;
        b.y = fromTop ? brick.y - BALL_R - 0.5 : brick.y + brick.h + BALL_R + 0.5;
      } else {
        b.vy = -b.vy;
      }
      hitBrick(b, brick);
      return;
    }
  }

  function updateBalls(dt) {
    for (const b of balls) {
      if (b.stuck) {
        b.x = paddleCenter();
        b.y = PADDLE_Y - BALL_R - 2;
        continue;
      }
      // sub-step to avoid tunneling
      const speed = Math.hypot(b.vx, b.vy);
      const steps = Math.max(1, Math.ceil(speed * dt / (BALL_R * 0.8)));
      const sdt = dt / steps;
      for (let s = 0; s < steps; s++) {
        b.x += b.vx * sdt;
        b.y += b.vy * sdt;
        // walls
        if (b.x - BALL_R < 0) { b.x = BALL_R; b.vx = Math.abs(b.vx); }
        if (b.x + BALL_R > W) { b.x = W - BALL_R; b.vx = -Math.abs(b.vx); }
        if (b.y - BALL_R < 0) { b.y = BALL_R; b.vy = Math.abs(b.vy); }
        // paddle
        if (b.vy > 0 && b.y + BALL_R >= PADDLE_Y && b.y + BALL_R <= PADDLE_Y + PADDLE_H + 10 &&
            b.x >= paddle.x - BALL_R && b.x <= paddle.x + paddle.w + BALL_R) {
          reflectFromPaddle(b);
        }
        // bricks
        collideBricks(b);
      }
      b.trail.push({ x: b.x, y: b.y });
      if (b.trail.length > 12) b.trail.shift();
    }

    // lost balls
    const before = balls.length;
    balls = balls.filter(b => b.y - BALL_R <= H);
    if (balls.length === 0 && before > 0) {
      if (demoMode) { // demo: just respawn
        resetBall(false);
      } else {
        lives--;
        updateHud();
        shake = 8;
        if (lives <= 0) {
          state = 'over';
          showOverlay('GAME OVER', 'Final score: ' + score, 'Click or press Enter to restart');
        } else {
          resetBall(true);
        }
      }
    }
  }

  function updateDrops(dt) {
    for (const d of drops) {
      d.y += d.vy * dt;
      const inPaddle = d.y + d.r >= PADDLE_Y && d.y - d.r <= PADDLE_Y + PADDLE_H &&
                       d.x >= paddle.x && d.x <= paddle.x + paddle.w;
      if (inPaddle && !d.dead) {
        d.dead = true;
        applyPower(d.type);
        spawnParticles(d.x, d.y, d.type === 'wide' ? '#5ff7ff' : '#ffd166', 16);
      }
    }
    drops = drops.filter(d => !d.dead && d.y < H + 20);
  }

  function applyPower(type) {
    if (type === 'wide') {
      paddle.w = PADDLE_W * 1.6;
      widePaddleTimer = POWER_TIME;
    } else if (type === 'multi') {
      const src = balls.filter(b => !b.stuck);
      const base = src.length ? src : balls;
      const extra = [];
      for (const b of base) {
        for (const ang of [-0.45, 0.45]) {
          const sp = Math.hypot(b.vx, b.vy);
          const a = Math.atan2(b.vy, b.vx) + ang;
          extra.push({ x: b.x, y: b.y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, stuck: false, trail: [] });
        }
        if (extra.length + balls.length >= 3) break;
      }
      // ensure ~3 balls total
      balls = balls.concat(extra.slice(0, 2));
      if (balls.length < 3) {
        const b = base[0];
        if (b && !b.stuck) balls.push({ x: b.x, y: b.y, vx: -b.vx, vy: b.vy, stuck: false, trail: [] });
      }
    }
  }

  function updateTimers(dt) {
    if (widePaddleTimer > 0) {
      widePaddleTimer -= dt;
      if (widePaddleTimer <= 0) {
        widePaddleTimer = 0;
        const cx = paddle.x + paddle.w / 2;
        paddle.w = PADDLE_W;
        paddle.x = cx - paddle.w / 2;
      }
    }
    shake *= Math.pow(0.001, dt);
  }

  function updateParticles(dt) {
    for (const p of particles) {
      p.x += p.vx * dt; p.y += p.vy * dt;
      p.vx *= 0.97; p.vy = p.vy * 0.97 + 260 * dt;
      p.life -= dt;
    }
    particles = particles.filter(p => p.life > 0);
    for (const b of bricks) if (b.flash > 0) b.flash -= dt;
  }

  function checkEnd() {
    if (!demoMode && state === 'playing' && bricks.every(b => !b.alive)) {
      state = 'won';
      showOverlay('VICTORY!', 'You cleared the wall. Score: ' + score, 'Click or press Enter to play again');
    }
  }

  function showOverlay(title, text, hint) {
    ovTitle.textContent = title;
    ovText.textContent = text;
    ovHint.textContent = hint;
    overlay.classList.remove('hidden');
  }

  // ---- Render ----
  function drawBackground() {
    const g = ctx.createLinearGradient(0, 0, 0, H);
    g.addColorStop(0, '#0a0f2e');
    g.addColorStop(1, '#070a1c');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);
    // grid
    ctx.strokeStyle = 'rgba(95,247,255,0.05)';
    ctx.lineWidth = 1;
    for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
    for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
  }

  function roundRect(x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r);
    ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r);
    ctx.arcTo(x, y, x + w, y, r);
    ctx.closePath();
  }

  function drawBricks() {
    for (const b of bricks) {
      if (!b.alive) continue;
      const damaged = b.maxHp > 1 && b.hp < b.maxHp;
      ctx.save();
      ctx.shadowColor = b.color;
      ctx.shadowBlur = 10;
      const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
      grad.addColorStop(0, b.flash > 0 ? '#ffffff' : lighten(b.color, 0.35));
      grad.addColorStop(1, b.flash > 0 ? '#ffffff' : b.color);
      ctx.fillStyle = grad;
      roundRect(b.x, b.y, b.w, b.h, 4);
      ctx.fill();
      ctx.restore();

      // tough brick outline / metallic look
      if (b.maxHp > 1) {
        ctx.strokeStyle = 'rgba(255,255,255,0.75)';
        ctx.lineWidth = 2;
        roundRect(b.x + 1, b.y + 1, b.w - 2, b.h - 2, 4);
        ctx.stroke();
      }
      // cracks
      if (damaged) {
        ctx.strokeStyle = 'rgba(20,10,30,0.85)';
        ctx.lineWidth = 1.6;
        for (const c of b.cracks) {
          ctx.beginPath();
          ctx.moveTo(b.x + c.x1, b.y + c.y1);
          ctx.lineTo(b.x + c.x2, b.y + c.y2);
          ctx.lineTo(b.x + b.w * 0.6, b.y + b.h * 0.2);
          ctx.stroke();
        }
      }
      // power-up marker
      if (b.power) {
        const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
        ctx.fillStyle = b.power === 'wide' ? '#5ff7ff' : '#ffd166';
        ctx.shadowColor = ctx.fillStyle;
        ctx.shadowBlur = 8;
        ctx.beginPath();
        ctx.arc(cx, cy, 4.5, 0, Math.PI * 2);
        ctx.fill();
        ctx.shadowBlur = 0;
        ctx.fillStyle = '#061024';
        ctx.font = 'bold 9px system-ui';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(b.power === 'wide' ? '↔' : '×3', cx, cy + 0.5);
      }
    }
  }

  function lighten(hex, amt) {
    const n = parseInt(hex.slice(1), 16);
    let r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
    r = Math.round(r + (255 - r) * amt);
    g = Math.round(g + (255 - g) * amt);
    b = Math.round(b + (255 - b) * amt);
    return `rgb(${r},${g},${b})`;
  }

  function drawPaddle() {
    ctx.save();
    ctx.shadowColor = '#5ff7ff';
    ctx.shadowBlur = 18;
    const g = ctx.createLinearGradient(paddle.x, 0, paddle.x + paddle.w, 0);
    g.addColorStop(0, '#8a7bff');
    g.addColorStop(0.5, '#5ff7ff');
    g.addColorStop(1, '#8a7bff');
    ctx.fillStyle = g;
    roundRect(paddle.x, PADDLE_Y, paddle.w, PADDLE_H, PADDLE_H / 2);
    ctx.fill();
    ctx.restore();
    if (widePaddleTimer > 0) {
      ctx.fillStyle = '#5ff7ff';
      ctx.fillRect(paddle.x, PADDLE_Y + PADDLE_H + 4, paddle.w * (widePaddleTimer / POWER_TIME), 3);
    }
  }

  function drawBalls() {
    for (const b of balls) {
      for (let i = 0; i < b.trail.length; i++) {
        const t = b.trail[i];
        const a = (i / b.trail.length) * 0.35;
        ctx.fillStyle = `rgba(95,247,255,${a})`;
        ctx.beginPath();
        ctx.arc(t.x, t.y, BALL_R * (i / b.trail.length), 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.save();
      ctx.shadowColor = '#ffffff';
      ctx.shadowBlur = 16;
      const g = ctx.createRadialGradient(b.x - 2, b.y - 2, 1, b.x, b.y, BALL_R);
      g.addColorStop(0, '#ffffff');
      g.addColorStop(1, '#8ff9ff');
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(b.x, b.y, BALL_R, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
  }

  function drawDrops() {
    for (const d of drops) {
      ctx.save();
      const col = d.type === 'wide' ? '#5ff7ff' : '#ffd166';
      ctx.shadowColor = col;
      ctx.shadowBlur = 14;
      ctx.fillStyle = col;
      ctx.beginPath();
      ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
      ctx.fill();
      ctx.shadowBlur = 0;
      ctx.fillStyle = '#061024';
      ctx.font = 'bold 12px system-ui';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(d.type === 'wide' ? '↔' : '×3', d.x, d.y + 1);
      ctx.restore();
    }
  }

  function drawParticles() {
    for (const p of particles) {
      const a = Math.max(0, p.life / p.max);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
    }
    ctx.globalAlpha = 1;
  }

  function drawLivesIcons() {
    for (let i = 0; i < lives; i++) {
      const x = 20 + i * 18, y = H - 18;
      ctx.fillStyle = '#5ff7ff';
      ctx.shadowColor = '#5ff7ff';
      ctx.shadowBlur = 8;
      ctx.beginPath();
      ctx.arc(x, y, 5, 0, Math.PI * 2);
      ctx.fill();
      ctx.shadowBlur = 0;
    }
  }

  function render() {
    ctx.save();
    if (shake > 0.1) ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
    drawBackground();
    drawBricks();
    drawDrops();
    drawParticles();
    drawPaddle();
    drawBalls();
    drawLivesIcons();
    ctx.restore();

    // HUD hint in canvas for demo
    if (demoMode) {
      ctx.fillStyle = 'rgba(232,236,255,0.6)';
      ctx.font = '13px system-ui';
      ctx.textAlign = 'right';
      ctx.textBaseline = 'top';
      ctx.fillText('AUTO-PLAY DEMO', W - 14, 12);
    }
  }

  // ---- Loop ----
  function frame(now) {
    const dt = Math.min(0.033, (now - lastTime) / 1000);
    lastTime = now;
    demoTime += dt;

    if (state === 'title') {
      // show a self-playing demo behind title overlay
      if (demoMode && (balls.length === 0 || bricks.length === 0 || bricks.every(b => !b.alive))) {
        startDemoRound();
        demoMode = true;
        state = 'playing';
      }
      if (!demoMode) demoMode = true;
    }
    if (state === 'playing') {
      updatePaddle(dt);
      updateBalls(dt);
      updateDrops(dt);
      updateTimers(dt);
      updateParticles(dt);
      if (demoMode && bricks.every(b => !b.alive)) {
        // restart demo level after a short pause
        setTimeout(() => { if (demoMode) startDemoRound(); }, 1200);
        bricks.forEach(b => b.alive = true); // keep visuals during pause
        bricks.forEach(b => { b.hp = b.maxHp; b.cracks = []; });
      }
      checkEnd();
    } else {
      updateParticles(dt);
    }

    render();
    requestAnimationFrame(frame);
  }

  // ---- Boot: demo runs immediately behind title overlay ----
  buildLevel();
  resetBall(false);
  state = 'title';
  demoMode = true;
  showOverlay('BREAKOUT', 'Mouse or ← → to move. Break every brick.', 'Click or press a key to start');
  overlay.classList.remove('hidden');
  // Demo keeps playing while the title overlay is shown
  state = 'playing';
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>