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
5 min 3 s
Cost
$0.037
Output tokens
73,440
Spent thinking
57,991 tokens
Speed
244.1 tok/s
Console errors
0
First token after
1.9 s
First visible text after
4 min 20 s
Prompt tokens
793
Code size
28.6 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"high"}
Max output
128,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 · 969 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breakout</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0}
  body{
    min-height:100vh;
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    padding:12px;overflow-x:hidden;
    background:radial-gradient(ellipse at 50% 0%,#1d2450 0%,#0b0d1f 60%,#060712 100%);
    color:#e9edff;
    font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    -webkit-user-select:none;user-select:none;
  }
  .top{width:min(100%,800px);display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;min-height:30px}
  .brand{display:flex;align-items:center;gap:10px}
  .title{font-weight:900;font-size:20px;letter-spacing:.22em;background:linear-gradient(90deg,#ff6b9d,#feca57,#1dd1a1,#54a0ff);-webkit-background-clip:text;background-clip:text;color:transparent}
  .badge{font-size:11px;font-weight:700;letter-spacing:.15em;padding:4px 9px;border-radius:999px;background:rgba(255,107,157,.15);border:1px solid rgba(255,107,157,.5);color:#ff9ec0}
  .hint{font-size:12px;opacity:.6;text-align:right}
  @media (max-width:520px){.hint{display:none}}

  .stage{
    position:relative;
    width:min(100%,800px,calc((100vh - 120px) * 4 / 3));
    aspect-ratio:4/3;
    border-radius:14px;overflow:hidden;
    box-shadow:0 0 0 1px rgba(160,180,255,.12),0 24px 60px rgba(0,0,0,.55);
    touch-action:none;
  }
  canvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none}

  .ov{
    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    padding:16px;transition:opacity .3s ease;background:rgba(5,6,16,.55);
  }
  .ov.soft{background:rgba(5,6,16,.12)}
  .ov.hidden{opacity:0;pointer-events:none}
  .card{
    width:min(100%,420px);text-align:center;
    padding:clamp(16px,4vw,30px);border-radius:16px;
    background:rgba(14,17,40,.78);
    border:1px solid rgba(140,160,255,.25);
    box-shadow:0 16px 50px rgba(0,0,0,.5);
    backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  }
  .card h1,.card h2{margin:0 0 8px;font-weight:900;letter-spacing:.14em;font-size:clamp(22px,6vw,40px)}
  .card p{margin:6px 0;font-size:clamp(13px,3.4vw,16px);opacity:.92}
  .card .small{font-size:12px;opacity:.6}
  .card b{color:#feca57}
  .pulse{animation:pulse 1.4s ease-in-out infinite;color:#7ff7ff !important;font-weight:700}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
  button{
    margin-top:12px;font:inherit;font-weight:800;letter-spacing:.1em;font-size:15px;
    padding:12px 28px;border-radius:999px;border:0;cursor:pointer;color:#0b0d1f;
    background:linear-gradient(90deg,#7ff7ff,#a29bfe);
    box-shadow:0 8px 24px rgba(127,247,255,.3);
  }
  button:hover{filter:brightness(1.12)}
  button:focus-visible{outline:3px solid #fff;outline-offset:3px}
</style>
</head>
<body>
<div class="top">
  <div class="brand">
    <span class="title">BREAKOUT</span>
    <span class="badge" id="badge">DEMO</span>
  </div>
  <div class="hint">Mouse or ← → to move · Space to launch · P to pause</div>
</div>

<div class="stage" id="stage">
  <canvas id="game" width="800" height="600" aria-label="Breakout game"></canvas>

  <div class="ov soft" id="ov-start">
    <div class="card">
      <h1>BREAKOUT</h1>
      <p class="pulse">Click or press a key to start</p>
      <p class="small">Mouse or ← → to move the paddle · Space to launch · P to pause</p>
    </div>
  </div>

  <div class="ov hidden" id="ov-over">
    <div class="card">
      <h2>GAME OVER</h2>
      <p>Final score: <b id="over-score">0</b></p>
      <button id="btn-over" type="button">Restart</button>
      <p class="small">Press Enter or R to restart</p>
    </div>
  </div>

  <div class="ov hidden" id="ov-won">
    <div class="card">
      <h2>VICTORY!</h2>
      <p>All bricks cleared. Score: <b id="won-score">0</b></p>
      <button id="btn-won" type="button">Play again</button>
      <p class="small">Press Enter or R to play again</p>
    </div>
  </div>

  <div class="ov hidden" id="ov-pause">
    <div class="card">
      <h2>PAUSED</h2>
      <p>Press P or Esc (or click) to resume</p>
    </div>
  </div>
</div>

<script>
(() => {
'use strict';

/* ---------- constants ---------- */
const W = 800, H = 600, TOP = 44, R = 7;
const PAD_Y = H - 52, PAD_H = 12, PAD_BASE = 120, PAD_WIDE = 188, PAD_SPEED = 640;
const BALL_BASE = 340, BALL_MAX = 480;
const MAX_ANG = 65 * Math.PI / 180;
const BW = 72, BH = 26, GX = 6, GY = 7;
const BX0 = (W - (10 * BW + 9 * GX)) / 2, BY0 = TOP + 42;
const ROW_COLORS = ['#ff5d73', '#ff9f43', '#feca57', '#1dd1a1', '#54a0ff', '#a29bfe', '#ff6bcb'];
// T = tough (2 hits), N = normal, W = normal + wide-paddle icon, M = normal + multi-ball icon, . = empty
const LAYOUT = [
  'TTTTTTTTTT',
  'NNWNNMNNWN',
  'NTNNNNNNTN',
  'NNMNTTNMNN',
  'NNNNNNNNNN',
  'TNWNNMNNWT',
  'NNNNNNNNNN'
];

/* ---------- canvas setup ---------- */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const DPR = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * DPR;
canvas.height = H * DPR;

const bgCanvas = document.createElement('canvas');
bgCanvas.width = W * DPR;
bgCanvas.height = H * DPR;
const bgCtx = bgCanvas.getContext('2d');

const stage = document.getElementById('stage');
const badge = document.getElementById('badge');
const ovEls = {
  start: document.getElementById('ov-start'),
  over: document.getElementById('ov-over'),
  won: document.getElementById('ov-won'),
  pause: document.getElementById('ov-pause')
};
const overScoreEl = document.getElementById('over-score');
const wonScoreEl = document.getElementById('won-score');

/* ---------- state ---------- */
let started = false;
let state = 'serve';       // 'serve' | 'play' | 'paused' | 'over' | 'won'
let prePause = 'serve';
let serveTimer = 0, endTimer = 0;
let score = 0, lives = 3, bricksBroken = 0, remaining = 0;
let ballSpeed = BALL_BASE;
let shake = 0, time = 0;
let bricks = [], balls = [], powerups = [], particles = [], popups = [];
let wideTimer = 0;
const paddle = { x: (W - PAD_BASE) / 2, w: PAD_BASE, targetW: PAD_BASE, flash: 0 };
const keys = { l: false, r: false };

/* ---------- helpers ---------- */
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
const rnd = (a, b) => a + Math.random() * (b - a);

function shade(hex, f) {
  const n = parseInt(hex.slice(1), 16);
  let r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
  const t = f < 0 ? 0 : 255, p = Math.abs(f);
  r = Math.round(r + (t - r) * p);
  g = Math.round(g + (t - g) * p);
  b = Math.round(b + (t - b) * p);
  return `rgb(${r},${g},${b})`;
}

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

function makeCracks() {
  const out = [];
  const n = 3 + Math.floor(Math.random() * 2);
  for (let i = 0; i < n; i++) {
    let x = BW * rnd(0.35, 0.65), y = BH * rnd(0.3, 0.7), a = rnd(0, Math.PI * 2);
    const pts = [[x, y]];
    for (let k = 0; k < 3; k++) {
      a += rnd(-0.9, 0.9);
      const L = rnd(6, 12);
      x += Math.cos(a) * L;
      y += Math.sin(a) * L;
      pts.push([x, y]);
    }
    out.push(pts);
  }
  return out;
}

function buildLevel() {
  bricks = [];
  for (let r = 0; r < LAYOUT.length; r++) {
    for (let c = 0; c < 10; c++) {
      const ch = LAYOUT[r][c];
      if (ch === '.') continue;
      const tough = ch === 'T';
      const pu = ch === 'W' ? 'wide' : ch === 'M' ? 'multi' : null;
      const hp = tough ? 2 : 1;
      bricks.push({
        x: BX0 + c * (BW + GX),
        y: BY0 + r * (BH + GY),
        w: BW, h: BH,
        row: r, col: c,
        hp, maxHp: hp,
        alive: true,
        pu,
        flash: 0,
        cracks: tough ? makeCracks() : null
      });
    }
  }
  remaining = bricks.length;
}

function makeBall(x, y, vx, vy) {
  return { x, y, vx, vy, prevY: y, trail: [], stuck: false, dead: false };
}

/* ---------- effects ---------- */
function burst(x, y, color, n, power = 1) {
  for (let i = 0; i < n && particles.length < 600; i++) {
    const a = Math.random() * Math.PI * 2;
    const s = (60 + Math.random() * 220) * power;
    const life = 0.5 + Math.random() * 0.5;
    particles.push({
      x, y,
      vx: Math.cos(a) * s,
      vy: Math.sin(a) * s - 60,
      life, max: life,
      s: 1.5 + Math.random() * 2.5,
      color
    });
  }
}

function popup(x, y, text, color) {
  popups.push({ x, y, text, color, life: 1 });
}

function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.life -= dt;
    if (p.life <= 0) { particles.splice(i, 1); continue; }
    p.vy += 620 * dt;
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vx *= 0.985;
  }
}

function updatePopups(dt) {
  for (let i = popups.length - 1; i >= 0; i--) {
    const p = popups[i];
    p.life -= dt * 1.2;
    p.y -= 26 * dt;
    if (p.life <= 0) popups.splice(i, 1);
  }
}

/* ---------- game flow ---------- */
function syncOverlays() {
  const show = !started ? 'start'
    : state === 'over' ? 'over'
    : state === 'won' ? 'won'
    : state === 'paused' ? 'pause' : '';
  for (const k in ovEls) ovEls[k].classList.toggle('hidden', k !== show);
  overScoreEl.textContent = score;
  wonScoreEl.textContent = score;
  badge.style.display = started ? 'none' : '';
}

function placeServe() {
  state = 'serve';
  serveTimer = 0.9;
  const b = makeBall(paddle.x + paddle.w / 2, PAD_Y - R - 0.5, 0, 0);
  b.stuck = true;
  balls = [b];
}

function resetGame() {
  score = 0; lives = 3; bricksBroken = 0; ballSpeed = BALL_BASE; shake = 0;
  buildLevel();
  powerups = []; particles = []; popups = [];
  wideTimer = 0;
  paddle.targetW = PAD_BASE; paddle.w = PAD_BASE;
  paddle.x = (W - PAD_BASE) / 2; paddle.flash = 0;
  placeServe();
  syncOverlays();
}

function startGame() {
  if (started) return;
  started = true;
  resetGame();
}

function launch() {
  if (state !== 'serve' || balls.length === 0) return;
  const b = balls[0];
  b.stuck = false;
  const a = (Math.random() - 0.5) * 0.6;
  b.vx = ballSpeed * Math.sin(a);
  b.vy = -ballSpeed * Math.cos(a);
  state = 'play';
}

function loseLife() {
  lives--;
  wideTimer = 0;
  paddle.targetW = PAD_BASE;
  powerups = [];
  shake = 0.6;
  burst(paddle.x + paddle.w / 2, PAD_Y, '#ff5d73', 26, 1.2);
  if (lives <= 0) {
    state = 'over';
    endTimer = 2.5;
    syncOverlays();
  } else {
    placeServe();
  }
}

function win() {
  const bonus = lives * 250;
  score += bonus;
  state = 'won';
  endTimer = 3.5;
  popup(W / 2, H / 2, 'BONUS +' + bonus, '#feca57');
  const cols = ROW_COLORS;
  for (let i = 0; i < 40; i++) burst(rnd(0, W), rnd(TOP, H * 0.6), cols[i % cols.length], 6, 0.8);
  syncOverlays();
}

function togglePause() {
  if (!started) return;
  if (state === 'paused') state = prePause;
  else if (state === 'play' || state === 'serve') { prePause = state; state = 'paused'; }
  else return;
  keys.l = false; keys.r = false;
  syncOverlays();
}

/* ---------- collisions ---------- */
function paddleCollide(b) {
  const px = paddle.x, pw = paddle.w, py = PAD_Y, ph = PAD_H;

  // Landing on top of the paddle
  if (b.vy > 0 && b.prevY + R <= py + 0.5 && b.y + R >= py && b.x >= px - R && b.x <= px + pw + R) {
    const hit = clamp((b.x - (px + pw / 2)) / (pw / 2), -1, 1);
    const ang = hit * MAX_ANG;
    b.vx = ballSpeed * Math.sin(ang);
    b.vy = -ballSpeed * Math.cos(ang);
    b.y = py - R;
    paddle.flash = 1;
    return;
  }

  // Overlapping the paddle body (from below or from the side): push out, never pass through
  if (b.y + R > py && b.y - R < py + ph) {
    if (b.x > px && b.x < px + pw) {
      b.y = py + ph + R;
      if (b.vy < 0) b.vy = -b.vy;
    } else if (b.x <= px && b.x > px - R) {
      b.x = px - R;
      if (b.vx > 0) b.vx = -b.vx;
    } else if (b.x >= px + pw && b.x < px + pw + R) {
      b.x = px + pw + R;
      if (b.vx < 0) b.vx = -b.vx;
    }
  }
}

function brickHit(br, b) {
  br.flash = 1;
  const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
  br.hp--;
  if (br.hp > 0) {
    // first hit on a tough brick: crack it
    score += 5;
    popup(cx, cy, '+5', '#ffffff');
    burst(cx, cy, '#c9d3e0', 7, 0.7);
    shake = Math.max(shake, 0.08);
  } else {
    br.alive = false;
    remaining--;
    bricksBroken++;
    const pts = br.maxHp > 1 ? 20 : 10;
    score += pts;
    const col = ROW_COLORS[br.row];
    popup(cx, cy - 4, '+' + pts, col);
    burst(cx, cy, br.maxHp > 1 ? '#cfd8e3' : col, 22, 1);
    burst(cx, cy, col, 8, 1.3);
    shake = Math.max(shake, 0.18);
    ballSpeed = Math.min(BALL_MAX, BALL_BASE + bricksBroken * 2.2);
    if (br.pu) powerups.push({ type: br.pu, x: cx, y: cy, vy: 120 });
  }
}

function brickCollide(b, br) {
  const cx = clamp(b.x, br.x, br.x + br.w);
  const cy = clamp(b.y, br.y, br.y + br.h);
  const dx = b.x - cx, dy = b.y - cy;
  const d2 = dx * dx + dy * dy;
  if (d2 >= R * R) return false;
  let nx, ny;
  if (d2 > 1e-6) {
    const d = Math.sqrt(d2);
    nx = dx / d; ny = dy / d;
    b.x = cx + nx * R;
    b.y = cy + ny * R;
  } else {
    // centre already inside the brick: push out through the nearest side
    const l = b.x - br.x, r = br.x + br.w - b.x, t = b.y - br.y, bt = br.y + br.h - b.y;
    const m = Math.min(l, r, t, bt);
    if (m === l) { nx = -1; ny = 0; b.x = br.x - R; }
    else if (m === r) { nx = 1; ny = 0; b.x = br.x + br.w + R; }
    else if (m === t) { nx = 0; ny = -1; b.y = br.y - R; }
    else { nx = 0; ny = 1; b.y = br.y + br.h + R; }
  }
  const dot = b.vx * nx + b.vy * ny;
  if (dot < 0) {
    b.vx -= 2 * dot * nx;
    b.vy -= 2 * dot * ny;
  }
  brickHit(br, b);
  return true;
}

function stepBall(b, sub) {
  b.prevY = b.y;
  b.x += b.vx * sub;
  b.y += b.vy * sub;
  if (b.x - R < 0) { b.x = R; b.vx = Math.abs(b.vx); }
  else if (b.x + R > W) { b.x = W - R; b.vx = -Math.abs(b.vx); }
  if (b.y - R < TOP) { b.y = TOP + R; b.vy = Math.abs(b.vy); }
  paddleCollide(b);
  for (const br of bricks) {
    if (br.alive && brickCollide(b, br)) break;
  }
  if (b.y - R > H) b.dead = true;
}

function updateBalls(dt) {
  for (const b of balls) {
    if (b.stuck) continue;
    const sp = Math.hypot(b.vx, b.vy);
    const steps = Math.max(1, Math.ceil(sp * dt / 2));   // sub-steps of <= 2px: no tunnelling
    const sub = dt / steps;
    for (let i = 0; i < steps && !b.dead; i++) stepBall(b, sub);
  }
  balls = balls.filter(b => !b.dead);
}

/* ---------- power-ups ---------- */
function collect(p) {
  if (p.type === 'wide') {
    wideTimer = 10;
    paddle.targetW = PAD_WIDE;
    popup(paddle.x + paddle.w / 2, PAD_Y - 18, 'WIDE PADDLE 10s', '#7ff7ff');
    burst(p.x, PAD_Y, '#7ff7ff', 18, 1);
  } else {
    const lead = balls.find(b => !b.stuck) || balls[0];
    const want = balls.length < 3 ? 3 - balls.length : Math.min(2, 5 - balls.length);
    const a0 = Math.asin(clamp(lead.vx / ballSpeed, -1, 1));
    for (let k = 0; k < want; k++) {
      const side = k % 2 === 0 ? 1 : -1;
      const off = 0.4 + 0.25 * Math.floor(k / 2);
      const a = clamp(a0 + side * off, -1.25, 1.25);
      balls.push(makeBall(lead.x, lead.y, ballSpeed * Math.sin(a), -ballSpeed * Math.cos(a)));
    }
    popup(paddle.x + paddle.w / 2, PAD_Y - 18, 'MULTI-BALL x3', '#ffd166');
    burst(p.x, PAD_Y, '#ffd166', 18, 1);
  }
}

function updatePowerups(dt) {
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.y += p.vy * dt;
    if (p.y - 9 > H) { powerups.splice(i, 1); continue; }
    if (p.y + 8 >= PAD_Y && p.y - 8 <= PAD_Y + PAD_H &&
        p.x + 16 >= paddle.x && p.x - 16 <= paddle.x + paddle.w) {
      powerups.splice(i, 1);
      collect(p);
    }
  }
}

/* ---------- demo AI ---------- */
function predictX(b) {
  if (b.vy <= 0) return b.x;
  const t = (PAD_Y - R - b.y) / b.vy;
  const x = b.x + b.vx * t;
  const L = W - 2 * R;
  let p = (x - R) % (2 * L);
  if (p < 0) p += 2 * L;
  if (p > L) p = 2 * L - p;
  return R + p;
}

function aiControl(dt) {
  const cx = paddle.x + paddle.w / 2;
  let target = W / 2;
  if (state === 'serve') {
    target = W / 2 + Math.sin(time * 0.9) * 160;
  } else {
    const desc = balls.filter(b => !b.stuck && b.vy > 0);
    if (desc.length) {
      let lead = desc[0];
      for (const b of desc) if (b.y > lead.y) lead = b;
      target = predictX(lead);
    } else if (powerups.length) {
      let p = powerups[0];
      for (const q of powerups) if (q.y > p.y) p = q;
      target = p.x;
    } else {
      const up = balls.filter(b => !b.stuck);
      target = up.length ? up[0].x : W / 2;
    }
  }
  target += Math.sin(time * 2.1) * paddle.w * 0.22;
  const d = target - cx;
  const step = PAD_SPEED * 0.92 * dt;
  paddle.x += clamp(d, -step, step);
}

/* ---------- update ---------- */
function update(dt) {
  time += dt;
  shake = Math.max(0, shake - dt * 2.5);
  paddle.flash = Math.max(0, paddle.flash - dt * 5);
  for (const br of bricks) if (br.flash > 0) br.flash = Math.max(0, br.flash - dt * 6);

  // paddle width animates around its centre
  const cx0 = paddle.x + paddle.w / 2;
  paddle.w += (paddle.targetW - paddle.w) * Math.min(1, dt * 8);
  paddle.x = cx0 - paddle.w / 2;

  if (wideTimer > 0) {
    wideTimer -= dt;
    if (wideTimer <= 0) { wideTimer = 0; paddle.targetW = PAD_BASE; }
  }
  updateParticles(dt);
  updatePopups(dt);

  if (state === 'over' || state === 'won') {
    if (!started) {               // demo mode: loop automatically
      endTimer -= dt;
      if (endTimer <= 0) resetGame();
    }
    return;
  }

  if (!started) aiControl(dt);
  else {
    const dir = (keys.r ? 1 : 0) - (keys.l ? 1 : 0);
    paddle.x += dir * PAD_SPEED * dt;
  }
  paddle.x = clamp(paddle.x, 0, W - paddle.w);

  if (state === 'serve') {
    const b = balls[0];
    b.x = paddle.x + paddle.w / 2;
    b.y = PAD_Y - R - 0.5;
    if (!started) {
      serveTimer -= dt;
      if (serveTimer <= 0) launch();
    }
  } else if (state === 'play') {
    updateBalls(dt);
    if (balls.length === 0) loseLife();
    else {
      updatePowerups(dt);
      if (remaining === 0) win();
    }
  }

  for (const b of balls) {
    if (b.stuck) continue;
    b.trail.push({ x: b.x, y: b.y });
    if (b.trail.length > 8) b.trail.shift();
  }
}

/* ---------- background (pre-rendered) ---------- */
function buildBackground() {
  const g = bgCtx;
  g.setTransform(DPR, 0, 0, DPR, 0, 0);
  g.fillStyle = '#0a0c1d';
  g.fillRect(0, 0, W, H);
  const rg = g.createRadialGradient(W / 2, -100, 50, W / 2, -100, 900);
  rg.addColorStop(0, 'rgba(90,110,255,.28)');
  rg.addColorStop(1, 'rgba(0,0,0,0)');
  g.fillStyle = rg;
  g.fillRect(0, 0, W, H);
  g.strokeStyle = 'rgba(120,140,255,.06)';
  g.lineWidth = 1;
  for (let x = 0; x <= W; x += 40) { g.beginPath(); g.moveTo(x + .5, 0); g.lineTo(x + .5, H); g.stroke(); }
  for (let y = 0; y <= H; y += 40) { g.beginPath(); g.moveTo(0, y + .5); g.lineTo(W, y + .5); g.stroke(); }
  for (let i = 0; i < 80; i++) {
    g.fillStyle = `rgba(255,255,255,${rnd(0.05, 0.35).toFixed(2)})`;
    g.fillRect(rnd(0, W), rnd(0, H), 1.5, 1.5);
  }
  // walls
  g.fillStyle = 'rgba(140,160,255,.28)';
  g.fillRect(0, TOP, 2, H - TOP);
  g.fillRect(W - 2, TOP, 2, H - TOP);
  g.fillRect(0, TOP, W, 2);
  // faint danger glow above the paddle
  const dg = g.createLinearGradient(0, H - 90, 0, H);
  dg.addColorStop(0, 'rgba(255,93,115,0)');
  dg.addColorStop(1, 'rgba(255,93,115,.10)');
  g.fillStyle = dg;
  g.fillRect(0, H - 90, W, 90);
}

/* ---------- drawing ---------- */
function drawIcon(type, cx, cy, s, col) {
  ctx.save();
  ctx.translate(cx, cy);
  ctx.scale(s, s);
  ctx.strokeStyle = col;
  ctx.fillStyle = col;
  ctx.lineWidth = 2;
  if (type === 'wide') {
    ctx.beginPath(); ctx.moveTo(-8, 0); ctx.lineTo(8, 0); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(-12, 0); ctx.lineTo(-6, -4.5); ctx.lineTo(-6, 4.5); ctx.closePath(); ctx.fill();
    ctx.beginPath(); ctx.moveTo(12, 0); ctx.lineTo(6, -4.5); ctx.lineTo(6, 4.5); ctx.closePath(); ctx.fill();
  } else {
    const pts = [[0, -4], [-4, 3], [4, 3]];
    for (const [x, y] of pts) { ctx.beginPath(); ctx.arc(x, y, 2.6, 0, Math.PI * 2); ctx.fill(); }
  }
  ctx.restore();
}

function puColor(type) { return type === 'wide' ? '#7ff7ff' : '#ffd166'; }

function drawBricks() {
  for (const br of bricks) {
    if (!br.alive) continue;
    const x = br.x, y = br.y, w = br.w, h = br.h;
    const tough = br.maxHp > 1;
    const col = ROW_COLORS[br.row];

    // drop shadow
    rr(ctx, x + 1, y + 2.5, w, h, 5);
    ctx.fillStyle = 'rgba(0,0,0,.35)';
    ctx.fill();

    // body
    const g = ctx.createLinearGradient(0, y, 0, y + h);
    if (tough) {
      g.addColorStop(0, '#aab6c4');
      g.addColorStop(0.5, '#7d8a99');
      g.addColorStop(1, '#4d5865');
    } else {
      g.addColorStop(0, shade(col, 0.45));
      g.addColorStop(0.5, col);
      g.addColorStop(1, shade(col, -0.25));
    }
    rr(ctx, x, y, w, h, 5);
    ctx.fillStyle = g;
    ctx.fill();

    // gloss line
    ctx.fillStyle = 'rgba(255,255,255,.35)';
    ctx.fillRect(x + 4, y + 2, w - 8, 2);

    // tough bricks: coloured inner frame and rivets
    if (tough) {
      rr(ctx, x + 2, y + 2, w - 4, h - 4, 4);
      ctx.strokeStyle = col;
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.fillStyle = 'rgba(20,25,35,.55)';
      for (const [dx, dy] of [[6, 6], [w - 6, 6], [6, h - 6], [w - 6, h - 6]]) {
        ctx.beginPath(); ctx.arc(x + dx, y + dy, 1.4, 0, Math.PI * 2); ctx.fill();
      }
    }

    // hit flash
    if (br.flash > 0) {
      rr(ctx, x, y, w, h, 5);
      ctx.fillStyle = `rgba(255,255,255,${(br.flash * 0.7).toFixed(3)})`;
      ctx.fill();
    }

    // visible cracks after the first hit on a tough brick
    if (tough && br.hp < br.maxHp && br.cracks) {
      ctx.save();
      rr(ctx, x, y, w, h, 5);
      ctx.clip();
      ctx.translate(x, y);
      ctx.lineJoin = 'round';
      for (const pts of br.cracks) {
        ctx.beginPath();
        ctx.moveTo(pts[0][0], pts[0][1]);
        for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]);
        ctx.strokeStyle = 'rgba(12,15,24,.92)';
        ctx.lineWidth = 1.6;
        ctx.stroke();
        ctx.strokeStyle = 'rgba(255,255,255,.35)';
        ctx.lineWidth = 0.6;
        ctx.stroke();
      }
      ctx.restore();
    }

    // power-up icon carried by the brick
    if (br.pu) {
      const cx = x + w / 2, cy = y + h / 2;
      const pulse = 1 + Math.sin(time * 6 + br.x * 0.1) * 0.08;
      ctx.save();
      ctx.shadowColor = puColor(br.pu);
      ctx.shadowBlur = 8;
      ctx.beginPath();
      ctx.arc(cx, cy, 8.5 * pulse, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(6,8,22,.78)';
      ctx.fill();
      ctx.lineWidth = 1.2;
      ctx.strokeStyle = puColor(br.pu);
      ctx.stroke();
      ctx.restore();
      drawIcon(br.pu, cx, cy, 0.55, puColor(br.pu));
    }
  }
}

function drawPowerups() {
  for (const p of powerups) {
    const col = puColor(p.type);
    ctx.save();
    ctx.shadowColor = col;
    ctx.shadowBlur = 12;
    rr(ctx, p.x - 17, p.y - 8, 34, 16, 8);
    ctx.fillStyle = col;
    ctx.fill();
    ctx.restore();
    drawIcon(p.type, p.x, p.y, 0.9, '#0b0e22');
  }
}

function drawPaddle() {
  const x = paddle.x, w = paddle.w, y = PAD_Y;
  const wide = paddle.targetW > PAD_BASE;
  ctx.save();
  ctx.shadowColor = wide ? '#7ff7ff' : '#8fa2ff';
  ctx.shadowBlur = 10 + paddle.flash * 14;
  rr(ctx, x, y, w, PAD_H, 6);
  const g = ctx.createLinearGradient(0, y, 0, y + PAD_H);
  g.addColorStop(0, wide ? '#c9ffff' : '#f4f6ff');
  g.addColorStop(1, wide ? '#3fb8d6' : '#6f7fe8');
  ctx.fillStyle = g;
  ctx.fill();
  ctx.restore();
}

function drawBalls() {
  for (const b of balls) {
    if (!b.stuck) {
      const n = b.trail.length;
      for (let i = 0; i < n; i++) {
        const t = b.trail[i];
        const k = (i + 1) / (n + 1);
        ctx.globalAlpha = k * 0.35;
        ctx.beginPath();
        ctx.arc(t.x, t.y, R * k, 0, Math.PI * 2);
        ctx.fillStyle = '#9fe3ff';
        ctx.fill();
      }
      ctx.globalAlpha = 1;
    }
    ctx.save();
    ctx.shadowColor = '#9fe3ff';
    ctx.shadowBlur = 14;
    const g = ctx.createRadialGradient(b.x - 2, b.y - 2, 1, b.x, b.y, R);
    g.addColorStop(0, '#ffffff');
    g.addColorStop(1, '#8fd8ff');
    ctx.beginPath();
    ctx.arc(b.x, b.y, R, 0, Math.PI * 2);
    ctx.fillStyle = g;
    ctx.fill();
    ctx.restore();
  }
}

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

function drawPopups() {
  ctx.font = '700 14px system-ui, sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  for (const p of popups) {
    ctx.globalAlpha = clamp(p.life, 0, 1);
    ctx.fillStyle = p.color;
    ctx.fillText(p.text, p.x, p.y);
  }
  ctx.globalAlpha = 1;
}

function drawHeart(x, y, on) {
  ctx.save();
  ctx.translate(x, y);
  ctx.beginPath();
  ctx.moveTo(0, 5);
  ctx.bezierCurveTo(-9, -1, -6, -9, 0, -4);
  ctx.bezierCurveTo(6, -9, 9, -1, 0, 5);
  ctx.closePath();
  ctx.fillStyle = on ? '#ff5d73' : 'rgba(255,255,255,.14)';
  ctx.fill();
  ctx.restore();
}

function drawHUD() {
  ctx.fillStyle = 'rgba(4,6,16,.85)';
  ctx.fillRect(0, 0, W, TOP);
  ctx.fillStyle = 'rgba(140,160,255,.35)';
  ctx.fillRect(0, TOP - 1, W, 1);

  ctx.textBaseline = 'middle';
  ctx.textAlign = 'left';
  ctx.font = '700 20px ui-monospace, Consolas, Menlo, monospace';
  ctx.fillStyle = '#e9edff';
  ctx.fillText('SCORE ' + String(score).padStart(6, '0'), 14, TOP / 2 + 1);

  ctx.textAlign = 'center';
  ctx.font = '700 14px system-ui, sans-serif';
  const chips = [];
  if (wideTimer > 0) chips.push({ t: 'WIDE ' + Math.ceil(wideTimer) + 's', c: '#7ff7ff' });
  if (balls.length > 1) chips.push({ t: 'BALLS x' + balls.length, c: '#ffd166' });
  if (!chips.length) chips.push({ t: 'BRICKS ' + remaining, c: '#8e99c7' });
  chips.forEach((ch, i) => {
    ctx.fillStyle = ch.c;
    ctx.fillText(ch.t, W / 2 + (i - (chips.length - 1) / 2) * 120, TOP / 2 + 1);
  });

  for (let i = 0; i < 3; i++) drawHeart(W - 30 - i * 28, TOP / 2, i < lives);

  // wide-paddle timer runs along the top wall
  if (wideTimer > 0) {
    ctx.fillStyle = '#7ff7ff';
    ctx.fillRect(0, TOP, W * wideTimer / 10, 3);
  }
}

function drawServeHint() {
  if (!started || state !== 'serve') return;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.font = '700 16px system-ui, sans-serif';
  ctx.fillStyle = `rgba(233,237,255,${0.55 + 0.35 * Math.sin(time * 4)})`;
  ctx.fillText('Click or press Space to launch', W / 2, PAD_Y - 36);
}

function draw() {
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
  ctx.save();
  if (shake > 0) ctx.translate(rnd(-1, 1) * shake * 12, rnd(-1, 1) * shake * 12);
  ctx.drawImage(bgCanvas, 0, 0, W, H);
  drawBricks();
  drawPowerups();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawPopups();
  drawServeHint();
  drawHUD();
  ctx.restore();
}

/* ---------- input ---------- */
function toLogicalX(e) {
  const r = canvas.getBoundingClientRect();
  return (e.clientX - r.left) / r.width * W;
}

stage.addEventListener('pointerdown', e => {
  if (e.target && e.target.closest && e.target.closest('button')) return;
  if (!started) { startGame(); return; }
  if (state === 'serve') launch();
  else if (state === 'paused') togglePause();
});

stage.addEventListener('pointermove', e => {
  if (!started) return;
  if (state === 'play' || state === 'serve') {
    paddle.x = toLogicalX(e) - paddle.w / 2;
  }
});

document.getElementById('btn-over').addEventListener('click', () => resetGame());
document.getElementById('btn-won').addEventListener('click', () => resetGame());

window.addEventListener('keydown', e => {
  const k = e.key;
  if (e.target && e.target.tagName === 'BUTTON' && (k === ' ' || k === 'Enter')) return;
  if (k === 'ArrowLeft' || k === 'ArrowRight' || k === ' ' || k === 'ArrowUp' || k === 'ArrowDown') {
    e.preventDefault();
  }
  if (k === 'ArrowLeft') keys.l = true;
  if (k === 'ArrowRight') keys.r = true;

  if (!started) {
    if (!e.repeat) startGame();
    return;
  }
  if (k === ' ' || k === 'ArrowUp') {
    if (state === 'serve') launch();
  }
  if ((k === 'Enter' || k === ' ') && (state === 'over' || state === 'won')) resetGame();
  if ((k === 'r' || k === 'R') && (state === 'over' || state === 'won')) resetGame();
  if (k === 'p' || k === 'P' || k === 'Escape') togglePause();
});

window.addEventListener('keyup', e => {
  if (e.key === 'ArrowLeft') keys.l = false;
  if (e.key === 'ArrowRight') keys.r = false;
});

window.addEventListener('blur', () => { keys.l = false; keys.r = false; });

/* ---------- loop ---------- */
let last = performance.now();
function frame(now) {
  const dt = Math.min(0.05, Math.max(0, (now - last) / 1000));
  last = now;
  if (state !== 'paused') update(dt);
  draw();
  requestAnimationFrame(frame);
}

buildBackground();
resetGame();      // starts in self-playing demo mode behind the start message
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (57,991 tokens)
I'm working out a single-file Breakout game that follows the output rules: one code block and nothing outside it. It needs a start prompt reading "Click or press a key to start" with a self-playing demo behind it, something visible on first paint, and no console errors. I'm planning a fixed 800x600 logical canvas that scales to fit the container and uses devicePixelRatio for sharp rendering.

I'm now deciding how the game states fit together, covering the demo, playing, the life-lost pause, and the game-over and victory screens. The main constraint is that in demo mode the AI drives the paddle, so a click or key press should start a real game with three lives and a score of zero. I'm also planning the paddle controls, with mouse position setting the paddle's x and the arrow keys moving it, while the bricks stay visible on first paint.

I'm settling the paddle controls: the mouse sets its x position directly, and the arrow keys nudge it each frame, so both can work together. The ball stays stuck to the paddle until a click or Space, with auto-launch after a second in demo mode, moving at a constant speed around 360 px/s. I'm also working out how the paddle angle depends on where the ball hits it, so the ball always heads upward.

I'm working through how the ball collides with bricks, using substeps so it can't tunnel through them at higher speeds. For each brick I'll test whether the ball's circle overlaps its rectangle, and if so reflect the velocity off whichever side it penetrated least.

I'm settling how the ball bounces off bricks: I'll check each brick against the ball's circle, push the ball out along the contact normal, and reflect its velocity. To avoid double-hitting, I'll handle only one brick per substep before moving on.

I'm settling the paddle-hit check: the ball has to be moving down and overlap the paddle's top edge, horizontally including its corners, and then I'll reposition it above the paddle and apply the angle from the hit point. Walls reflect the ball on the left, right, and top, and a ball lost through the bottom gets removed, with a life lost only once no balls remain. After that I'll lay out the brick grid.

I'm laying out the brick grid now: ten columns of 70px bricks with 6px gaps, which fits within the 800px width with a small margin. I'm settling on seven rows of 22px-tall bricks, with a mix of normal and tough bricks, and I'm weighing how to encode each row's pattern.

I'm settling on a brick legend where N is a normal brick, T is tough, and W and M are normal bricks that carry a wide-paddle or multi-ball power-up, with empty cells marked by dots.

I'm now setting out the brick layout as a grid of codes: T for tough bricks, and N, W, and M for normal bricks, with W and M carrying the power-ups. I'm sketching the top row as all tough bricks, then planning the rows below it with a mix of the other codes.

Now I'm laying out the brick grid as rows of codes, with T for tough bricks and N, W, and M for normal bricks carrying power-ups, and I'm fitting a seven-row pattern to it.

Now I'm settling the brick icons: power-up bricks will show a small drawn symbol instead of an emoji, with two arrows for the wide-paddle brick and three dots for multi-ball. The falling capsule can reuse the same shape at the brick's center.

I'm settling the brick styling now: tough bricks get a darker metallic border and crack lines once they've taken a hit, and each row gets its own color. Breaking a brick should spawn a burst of particles that fall with gravity.

I'm working out how the power-ups behave: a falling capsule drops at about 140 px/s and the paddle catches it. The wide-paddle pickup lasts 10 seconds and refreshes if it's already active, and the multi-ball pickup brings the total to three balls by spawning extras from the lead ball.

I'm settling how multi-ball works: the pickup adds two balls, capped at five, and the HUD shows the count. Extra balls only cost a life when the last one is lost, and the wide-paddle effect gets a timer bar in the HUD, with hearts drawn for lives.

I'm settling the HUD and responsive layout: the score, lives, and power-up timers will be drawn on the canvas itself, and the canvas will scale fluidly with CSS, capped at 800px wide and keeping a 4:3 aspect ratio so it fits both phones and a 1280x800 window.

I'm wiring up the canvas sizing with a CSS cap so it stays within the viewport, and converting mouse positions into the game's 800-wide coordinate space. Pointer events will handle both mouse and touch input, and I'm now thinking about how the keyboard controls should work alongside them.

I'm deciding how the start overlay and restart button will work, using DOM elements over the canvas so the text stays crisp and accessible. For the demo, I'll have an AI paddle track the ball's predicted landing point with a bit of lag and error, and when lives run out during the demo I'll quietly reset the level and keep it running.

I'm thinking about the demo AI now: it should track the ball's predicted landing spot with a small error so it doesn't lose too often, and I'm weighing whether it should also chase nearby falling power-ups. I'm also keeping the overlay light so the bricks, ball, paddle, and particles stay visible behind the start prompt.

I'm sorting out the demo AI's targeting: it will predict where the ball lands by unfolding its path across the walls, aim at the lowest descending ball, and add a small wobble so it doesn't play perfectly.

I'm locking in the demo's pacing: the AI paddle is capped at about 520 px/s so it lags a little, and a ball resting on the paddle auto-launches after 0.6 seconds. I'm setting up a game state machine with title, serve, play, over, and won states, starting in demo mode. Once the player starts, the game resets to a fresh level with three lives. In demo mode, losing all lives or clearing the board just resets the level and keeps the demo running, while a real game-over shows the overlay instead.

I'm settling the overlay flow: a start screen reading "Click or press a key to start," and game-over and victory screens with a restart button. Keys like Enter, Space, or R can restart too. I'm also thinking about launch: the human has to click or press Space to release the ball, with a hint shown so the player knows, and I'm weighing whether to add a pause key.

Still writing collision code...

Still writing collision code...

Still writing collision code...

I'm working out how the paddle collision should only trigger when the ball comes down from above, so I'm tracking each ball's previous y and checking that it was above the paddle top before the hit.

Still writing collision code...

I'm settling the multi-ball spawns and the life-loss flow: extra balls launch at about 0.45 rad off vertical, the total is capped at five, and losing the last ball costs a life and resets the wide-paddle timer. Victory comes when no bricks remain, which shows the overlay and spawns celebration particles.

I'm laying out the HUD now: score on the left, hearts for lives on the right, the level label in the middle, and a power-up timer bar. Brick destruction will kick off small particles, a bit of screen shake that fades, and a "+10" popup near the brick. I'm keeping the top strip about 40px tall for the HUD, with the playfield below it and bricks starting lower down.

I'm laying out the brick grid: ten columns of 72px bricks with 6px gaps, seven rows starting at y=80, and the paddle sitting near the bottom with a 7px ball radius. For the bricks, I'm thinking about a metallic look with a colored border, and each brick precomputing a few jittered crack lines so damage shows up as it's hit.

Still writing draw code for the brick power-up icons...

Still writing the falling power-up capsules and the paddle and ball visuals...

Still sorting out the responsive layout: the canvas will scale with its container, but at phone widths the HUD text would get very small, so I'm considering keeping the internal resolution fixed.

I'm setting up the overlay system now, with the start screen showing "BREAKOUT" and the required prompt, and the pause state toggled by P or Escape through a shared overlay helper.

I'm settling how clicks route through the stage: a start overlay or demo mode launches a fresh game, the serve state launches the ball, and clicks on the game-over or victory overlay get ignored so the restart button handles them.

I'm settling the input handling: the button's click stops propagation so the stage doesn't also launch the ball, Space and Enter handle restarts on the end screens, and P toggles pause during play. I'll also keep the arrow and space keys from scrolling the page.

Still writing the game loop logic...

Still writing the paddle movement code...

Still working through the paddle and ball mechanics: the paddle is clamped to the canvas edges, and the demo AI steers toward the ball's target at a capped speed with a bit of jitter. Ball speed scales up with bricks broken, and each ball keeps its own speed, normalized to the current global speed whenever it hits the paddle.

Still writing the ball physics update...

Still writing the paddle collision check...

Still working through the paddle collision: a side hit on the paddle edge only matters if the ball's vertical path crosses the paddle's top, which the x-range check handles, so I'm checking whether anything could slip through the paddle from below.

Still writing the paddle side-collision code...

Still writing the brick destruction code...

Still working on the AI targeting: the demo paddle will go for the lowest descending ball, predicting where it lands and otherwise drifting toward the center.

Now I'm working out the HUD layout: score on the left, lives shown as small hearts on the right, and a power-up timer bar beneath the top strip.

Still writing particle and popup code...

Still writing the draw code for the background, bricks, and balls.

Still writing the HTML and CSS for the Breakout page...

Still writing the page styles: the body, the title bar, and the stage layout.

Still writing the page styles: the stage sizing, the canvas fill, and the rest of the layout rules.

Still writing the stage sizing and layout rules...

Still writing the overlay styles: the centered layout, the hidden state, and the button and heading sizing.

I'm setting up the JavaScript for the game, starting with the canvas size and the high-DPI scaling.

Still writing the brick layout grid...

Still writing the brick layout grid...

Still writing the brick grid setup code...

Still writing the brick grid setup and crack generation...

Still writing the crack-generation helper for the bricks...

Still writing the crack-drawing code for the bricks...

Still writing the state variables and game mode setup...

Still writing the game state setup: the start handler hides the overlay and switches out of demo mode, and the reset routine fills in the score, lives, bricks, and the serve timer.

Still writing the game-over timer logic...

Still writing the game-over and pause handling...

Still writing the lose-life handler...

Still writing the collision code for the bricks...

Still writing the ball step update...

Still writing the paddle collision code...

Still writing the paddle collision logic...

Still writing the top-hit bounce and the paddle side-collision handling...

Still writing the top-hit bounce code...

Still working on the brick collision: the closest point on the brick to the ball's center decides whether they overlap, and the ball's direction is set by which side it struck.

Still writing the brick collision branch for when the ball's center sits inside the brick, pushing it out along the shallowest side.

Still writing the brick collision branch...

Still writing the brick hit effects: each hit either drains a brick's health and throws off a few gray sparks, or destroys it, adding points, a burst of color, and a chance of a power-up.

Still working on the victory condition: once the last brick falls, the game switches to the won state, and I'm adding a bonus of 250 per remaining life to the score.

Still writing the power-up collection code...

Still working out the power-up collection: I'm deciding whether falling power-ups should only be collected during active play, since balls are stuck to the paddle during the serve.

Still writing the ball-spawning code for the multi-ball power-up, working out the starting angles for the new balls from the lead ball's direction.

Still writing the AI targeting code...

Still writing the AI targeting logic...

Still writing the AI jitter so the demo paddle sometimes misses, which keeps the attract mode looking natural.

Still writing the AI predictor, working out where the lead ball will cross the paddle line and folding that x-position back inside the walls.

Still working out the paddle movement: the demo paddle moves at about 90% of its max speed, while the keyboard moves it directly and the mouse sets its center.

Still writing the particle and popup updates, with gravity pulling the sparks down and the popups drifting upward as they fade.

Still writing the draw loop: the background is drawn first from a pre-rendered offscreen canvas, with screen shake applied as a random translate.

Still writing the brick draw loop, picking each brick's color by row and marking the tougher ones.

Still writing the brick draw code: a shadow, a body gradient, a highlight and border, and a flash on hit.

Still writing the brick draw code, with a shadow, a body gradient, and a highlight and border, plus a flash on hit.

Still writing the shade helper for the brick colors, mixing each hex toward white or black by a factor.

Still writing the icon drawing: the wide power-up gets a horizontal bar with arrowheads, and the other type gets three dots.

Still writing the falling power-up capsules, with a rounded body and a gradient fill carrying the icon.

Still writing the ball drawing loop, looping over each ball to paint it.

Still writing the HUD code: the top bar, the score text, the brick count, and the lives shown as hearts.

Still writing the HUD code: the wide-timer and multi-ball labels sit in the top bar's center, and I'm adding a thin gradient line at the top of the playfield.

I'm wiring up the overlay so the start screen stays light enough that the bricks show through, with the message sitting in a bordered card.

Still writing the pointer input: a click on the start screen begins the game, and during the serve a click launches the ball, while clicks on buttons are ignored.

Still writing the keyboard handler: arrow keys and space go to the paddle and launch controls.

Still writing the keyboard handler: the arrow keys and space go to the paddle and launch controls, and P, Escape, or R toggle pause or restart.

Still working out the keyboard controls: I'm making the arrow keys set the paddle direction whether or not the game has started, and clearing them on window blur so nothing sticks.

Still writing the pause toggle and the pause overlay, with a click on the stage resuming the game.

Still writing the frame loop, clamping the timestep so a slow frame can't jump the physics ahead.

Still writing the paddle's per-frame updates: the flash fades, each brick's flash fades, and the paddle width eases toward its target until the wide timer runs out.

Still working out the paddle physics: the demo AI steers toward the ball while the keys move it directly, then clamping keeps it inside the walls.

Still writing the ball movement: each ball gets split into sub-steps sized to its own speed, with its trail updated before it moves.

Still working out the ball update: each ball's sub-steps are sized to its own speed, and a ball removed mid-loop stops the loop early. Once the ball list is processed, an empty list costs a life, and the brick-count win check happens inside the hit effects instead.

Still writing the lose-life logic, resetting the wide timer and the paddle's target width.

Still writing the win logic, adding the bonus of 250 per remaining life to the score.

I'm now settling how the win condition is checked: the brick-hit code just counts down the remaining bricks, and the update loop calls the win routine after the ball pass if the game is still in play. That way a win can't fire in the middle of the ball loop.

I'm now working out what happens when a ball falls off the bottom: it's marked dead, and if no balls remain during play, a life is lost. I'm also starting the multi-ball power-up collection, which spawns extra balls off the lead ball.

Still writing the multi-ball spawn: the extra balls fan out at widening angles around the lead ball's heading, clamped to a safe range.

Still writing the HUD code: the top bar with its gradient edge, then the score, lives shown as hearts, and the timer indicator.

Still working on the HUD: the wide-paddle timer and the multi-ball count go in the center of the top bar, falling back to the brick count when neither effect is active. The demo badge will be a DOM element in the header, and the title will be a gradient "BREAKOUT" with the controls hint on the right.

Still writing the overlay markup: a start card with the "Click or press a key to start" prompt, plus game-over, victory, and pause cards that stay hidden until needed.

Still writing the overlay event handling: the overlays stay on top of the stage, so clicks bubble up to it, and I'm making the stage handler skip button presses so the restart button's own click handles them.

I'm checking whether pressing Space on a focused restart button would fire twice, once from my keydown handler and once from the button's own click. I'm thinking the second restart is harmless since nothing changes between them, but I'm wondering whether calling preventDefault on the Space keydown stops the button from activating at all.

I'm settling the demo behavior now: the start-stage overlay doesn't block the game, so the self-playing paddle runs from the first frame. I'm also keeping the AI active only until the game actually starts, and the serve timer counts down before the demo launches the ball.

Still writing the launch function: the serve launches the lone ball at a slightly randomized angle and switches the state to play.

Still writing the power-up code: each falling power-up moves down with its own velocity, and any that drop off the bottom of the screen get removed.

Still writing the power-up collision check: a falling power-up that touches the paddle gets removed and handed to the collect routine.

Still writing the particle burst: each spark gets a random direction and speed, drifting slightly upward, and the particle count is capped.

Still writing the particle drawing: each spark is drawn as a small square whose opacity fades with its remaining life. Then the popups get drawn in bold text, and I'm preparing a pre-rendered background with a glow, faint grid lines, and stars.

Still writing the initialization and the demo-mode details: the background is pre-rendered once, and the first paint shows bricks and balls already moving because the serve timer launches the ball after about a second.

Still writing the initialization: resetGame runs on load and sets the game state, while the start overlay stays visible for the demo.

Still writing the overlay visibility: the start card shows only while the game hasn't started, and game-over, victory, or pause cards take over once the state calls for them, so I'll handle that in one updateOverlays routine.

Next I'm wiring up the overlay sync: a single routine picks which card to show based on whether the game has started and what state it's in, and updates the score text while it's at it. I'll call it wherever the state changes, like when a life is lost, the level is won, or pause toggles, rather than every frame.

Still writing the overlay sync and the demo badge: the badge shows only while the game hasn't started, and I'll update it alongside the overlays rather than every frame.

Still writing the HTML head: the page setup with a full-height flex body and a stage that stays 4:3 within the window.

Still writing the page styles: the body is a full-height flex column with a dark radial background, and the top bar is capped at 800px wide.

Still writing the stylesheet: the title gets a gradient text fill, and the hint and demo badge are styled as small labels in the top bar.

Still writing the stylesheet: the canvas fills the stage, and I'm working on how it sits inside the frame.

Still writing the overlay styles: each card is centered with a translucent backdrop, and hidden overlays fade out and stop taking clicks.

Still writing the button and link styles, with the font, the margin, and the sizing of the restart and start controls.

Still writing the button and link styles, with the overlay buttons getting their gradient fill, hover brightening, and a visible focus ring.

Still writing the HTML body: a header with the title, a demo badge, and the controls hint, then the stage with the canvas and the start overlay.

Still writing the overlay markup: the game-over card shows the final score with a restart button and a keyboard hint, and I'm adding the rest of the cards after it.

Still writing the JavaScript: I'm setting up the game constants, the brick layout, and the canvas handles.

Still writing the JavaScript setup: I'm wiring up the canvas at the device pixel ratio and initializing the game state variables for lives, score, bricks, balls, and power-ups.

Still writing the game state: the paddle, the ball list, and the brick and power-up arrays are set up, with helper functions for clamping and random ranges.

Still writing the Breakout level layout: the brick grid gets built from the layout rows, and tough bricks get cracks drawn on them.

Still writing the ball setup: a helper builds each ball with its position, velocity, and trail, and the serve places one resting on the paddle.

Still writing the game reset: score, lives, and speed get reset, the level is rebuilt, and the paddle goes back to center before the ball is placed on it.

Still writing the overlay sync: a single routine checks whether the game has started and what state it's in, then shows the matching card and hides the rest.

Still writing the win routine: the bonus is lives times 250, added to the score, and the state flips to won with a short timer before the card takes over.

Still writing the loseLife routine: it takes a life, resets the paddle to base width, clears the power-ups, and triggers a screen shake, then starts a burst of sparks at the paddle.

Still writing the update loop: the timers, paddle width easing, and brick flash fade are all handled each frame.

Still writing the update loop: the shake and paddle flash decay, the paddle eases toward its target width, and the wide-power timer resets width when it runs out.

Still working on the paddle's width easing: I want its center to stay put while the width animates, so each frame I'll shift the left edge by half the width change.

Still writing the paddle and ball loop: the paddle keeps its center while the width eases, then the serve ball follows the paddle and launches after the timer, with the ball update and power-up update running during play.

Still writing the aiControl routine: it tracks the lowest descending ball and steers the paddle toward its predicted landing spot.

Still writing the aiControl routine: I'm adding a small sine-based jitter to the target so the demo paddle doesn't hit the exact same spots every time, and I'm checking that the jitter stays small enough that it won't push the paddle off the edges.

I'm checking the AI's odds: the ball should land within the paddle's reach even with the jitter, so the demo mostly succeeds, though lag or a fast ball can still cause an occasional miss. Still I need to keep the target from pushing past the edges, which the clamp already covers. There's also the question of the jitter itself.

Still writing the update helpers: each ball steps forward in small sub-steps so fast balls don't tunnel through bricks, and any ball flagged dead gets filtered out.

Still writing the stepBall routine: it advances the ball by a sub-step, bounces it off the side and top walls, and runs the paddle check.

Still checking the ball physics: I'm making sure brick collisions keep the ball's speed, and I'm weighing whether a ball bouncing off a brick could end up with a vertical velocity that sends it straight back into the same brick.

Still writing the paddle collision: I'm checking whether the ball is coming down onto the paddle, and if so, I'm setting its angle from where it hit along the paddle.

Still writing the paddle collision: when the ball is overlapping the paddle's body from below or from the side, I'm pushing it out to the nearest free side and flipping the velocity so it moves away.

I'm checking the overlap case for a ball that was already inside the paddle's top edge before the paddle moved up into it, since the center test would wrongly push it below the paddle.

I'm checking the paddle overlap case: a ball can only be inside the paddle band already if it was placed there, like the serve position just above the paddle, so the top-hit check still catches normal descents.

Still working through the brick hit handling in brickCollide; I'm about to write the effects for when a ball breaks a brick.

Still writing brickHit: each hit drops the brick's hit points, and a surviving brick gets a small white popup, a spark burst, and a light shake.

Still writing brickHit: a surviving brick gets a hit-point drop, a popup, and a burst, and a destroyed brick scores, speeds up the ball, and may drop a power-up.

Still writing the updatePowerups routine: each power-up drifts down, gets removed once it leaves the bottom, and is caught if it overlaps the paddle.

Still writing the collect routine: a wide-paddle pickup sets a 10-second timer and targets the wide width, with a popup and a burst of sparks at the capsule.

Still writing the multi-ball spawn in collect: I'm picking a lead ball, adding enough extra balls to reach a cap, and fanning them out at angles off the lead's direction.

Still writing the collect routine: the multi-ball spawn needs a starting angle taken from the lead ball's horizontal speed, so I'm working out how to keep that angle pointing upward even when the lead is descending.

Still writing the multi-ball angle: I'm deriving the starting angle from the lead ball's horizontal speed, so the new balls fan out from where it is heading.

Still writing the particle update: I'm making each particle age, drop it once its life runs out, and apply gravity and drift to the survivors.

Still writing the draw routine: it sets the scale, applies a shake offset when shaking, draws the cached background, then layers bricks, power-ups, paddle, balls, particles, popups, and the HUD on top.

Still writing the background cache: I'm drawing the static backdrop once onto an offscreen canvas at device resolution, so it can be blitted each frame without redrawing the gradients.

Still writing the background: I'm drawing the star field onto the cached backdrop.

Still writing the background walls: I'm drawing thin side walls along the playfield edges on the cached backdrop.

Still writing the brick draw: I'm laying out each live brick's rounded shape, starting with a dark offset shadow underneath it.

Still writing the brick draw: I'm adding a glossy highlight line along the top of each brick's body.

Still writing the brick draw: I'm adding rivets and a corner-dot pattern to the tougher bricks, plus a white flash overlay when one has just been hit.

Still writing the brick draw: I'm adding the icon for power-up bricks, a small pulsing circle with its symbol, and cracks on the tougher bricks as they take damage.

Still writing the brick draw: I'm adding the cracks now, clipping them to each brick's rounded shape.

Still writing the crack lines: each one is stroked as a dark jagged path, clipped to the brick's rounded shape.

Still writing the power-up draw: each capsule gets a glowing rounded body in its type's color, with its icon drawn on top at a smaller scale.

Still writing the paddle draw: I'm setting its glow color and blur from whether it's widened and how hard it's flashing, then building its rounded body with a gradient fill.

Still writing the balls draw: I'm drawing each moving ball's fading trail as a series of shrinking, increasingly opaque circles behind it.

Still writing the ball trail: I'm drawing each fading trail point as a smaller, more opaque circle, then resetting the alpha before drawing the ball itself with a glowing radial gradient.

Still writing the particle draw: each particle is drawn as a small square that fades out as its life runs down.

Still writing the HUD: a dark translucent bar across the top with a thin glowing divider line beneath it, then the score and lives text in a heavy system font.

Still writing the HUD: the score sits on the left in monospace digits, and I'm placing the wide-paddle timer in the center of the bar.

Still writing the HUD timer: I'm laying out a wide-paddle countdown and a multi-ball count in the center of the bar, with a thin bar along the top showing the remaining time.

Still writing the HUD: the timer bar runs along the top of the playfield and shrinks as the wide-paddle time runs out, and the center text lists the active effects.

Still writing the heart icon: a small curved shape filled red when the life is still left and dimmed otherwise.

Still writing the event handlers: a pointer-down on the stage starts the game if it hasn't begun, launches the ball during serve, or toggles pause, while pointer-move tracks the paddle once the game is running.

Still writing the pointer handling: the paddle follows the pointer once the game is running, and I'm checking whether the overlays and touch scrolling need handling too.

Still writing the keyboard handler: I'm checking for the arrow keys and space, and I'm working out whether up-arrow should also count as a launch input.

Still writing the keyboard handler: I'm adding the pause and restart keys, and the sustained arrow-key movement.

Still writing the keyboard handler: I'm wiring up the escape/pause key and clearing the arrow-key flags on keyup and when the window loses focus.

Still deciding how the R key should behave: I'm weighing whether it should restart at any time, since an accidental press would wipe progress, or only from the game-over and win overlays where the hint already appears.

Still writing the togglePause handler: it flips between paused and the state it came from, and clears the arrow-key flags.

Still writing the game loop: the frame function clamps the elapsed time, only updates when not paused, then draws and requests the next frame.

I'm checking that the DOM references are declared at the top, then going through the demo flow: the game starts serving, launches on its own, and cycles through game over or a win back into a fresh reset.

I'm checking the update order for the lose-life and win conditions, and confirming that a win during play brings up the win overlay as well.

Still checking the launch path: the ball's previous position gets set when it's launched, so the top-edge collision test works from the first frame.

Checking the brick boundaries: the bricks start at y=86 and the bottom row ends around y=276, which leaves plenty of room above the paddle at 548. I'm also confirming that a ball moving upward after launch won't trigger the top-edge hit test.

Still writing the brick sizes: I'm bumping the bricks up to 26px tall with 7px gaps so they fill more of the canvas, and checking that the 10-column grid is centered.

Still writing the icon helper: I'm drawing each power-up icon in its color, scaled to fit the capsule.

Still writing the power-up icon: the wide-paddle icon is drawn as a bar with two arrowheads, and the other icons are drawn as a set of triangles.

Still writing the icon backdrop: I'm scaling the wide-paddle icon down so it fits inside the brick's circle, with a dark translucent disc and a colored outline behind it.

I'm checking the badge lookup and the "DEMO" label, and I'm considering whether the demo should also say "AUTO PLAY" on the canvas or whether the badge alone is enough.

Still writing the layout CSS: the stage uses aspect-ratio so its height follows its width, and the canvas fills it absolutely. I'm also checking how the body flex centering and padding will hold up at narrow widths.

Still writing the layout CSS: I'm dropping the height:100% on html and body in favor of a min-height:100vh on body, and checking the stage width against the viewport height so the canvas fits without overflow.

Still writing the overlay markup: the game-over card shows the score with a restart button, the victory card adds a "Play again" button, and the pause card gives the resume keys.

Still checking the paddle collision: I'm making sure a ball that hits the top-left part of the paddle gets a negative horizontal bounce so it heads left.

Checking the paddle-edge collision: a ball grazing the far corner of the paddle still gets a clamped sideways angle, so the bounce direction stays sensible.

Checking the ball's bottom edge against the paddle top: if it's already overlapping from the previous step, the top-hit test won't catch it, so the ball just slips past the paddle's side edge at that corner.

Checking the side-collision branch: when the ball is just left of the paddle, I push it out to the left edge and flip its horizontal speed so it heads away, and the mirror case handles the right side.

Checking the side-collision branch for the left edge: the ball needs to push out and bounce away only when it's moving toward the paddle, and the strict comparison leaves the ball's center exactly on the edge out of that branch.

Still writing the game loop's final pieces: I'm checking the paddle's top-edge collision near the corners, where a ball coming from above or beside the paddle should bounce cleanly and not slip through. Once that's settled I'll wrap up the code, keeping the ctx.save and ctx.restore calls balanced.

Writing out the full HTML page now, with the markup, styles, and game script all in one file.

Still writing the JS: I'm setting up the constants and the brick layout, and I'm about to start the main game script.

Still writing the constants and layout: I'm setting the paddle, ball, and brick dimensions, then the canvas setup with its device-pixel scaling.

Still writing the Breakout code: I'm wiring up the canvas, the background buffer, and the overlay and score elements, plus a restart button that stops its click from bubbling.

Still writing the JS: I'm wiring up the canvas and the demo's AI paddle, with a check that the AI logic only runs in the serve and play states.

Still writing the paddle update: I'm keeping the paddle centered while its width animates toward the target, and placing that step before the AI control runs.

Still writing the update function: I'm advancing the timers and flash effects, then easing the paddle's width toward its target while keeping its center fixed.

Still writing the update function: I'm setting the ball's position at the start of serve, keeping it anchored to the paddle.

Still writing the update function: I'm setting the ball's position at the start of serve, then handling the serve timer and the launch.

Still writing the update loop: I'm checking whether the paddle keeps moving under key control during the demo, and making sure the win and lose transitions in the play state don't run on an empty ball list.

I'm checking how the demo handles keys and the pointer, and confirming the top HUD timer bar draws along the top edge while a wide-paddle effect is active.

Still writing the JS: I'm setting up the state sync so the start, game-over, win, and pause overlays show only for the current state, and the score text updates on each one.

Still writing the JS: I'm finishing the overlay toggling so the start, game-over, won, and pause cards show only in their matching state, and wiring the win confetti burst.

Still writing the drawHUD function: I'm filling the top bar background, then adding the score and lives readout along it.

Still drawing the top bar: I'm adding a centered readout for the ball count and wide-paddle timer, which I'll finish with the lives display on the right.

Still writing the centered HUD chips: I'm picking which status to show, a wide-paddle timer or a multi-ball count, falling back to the remaining brick count when neither applies.

Still writing the HUD: I'm setting the score readout on the left and the lives hearts on the right, with the centered chips showing the wide-paddle timer or ball count.

Still writing the HTML drawing code: I'm finishing the final touches to the game and checking the heart-shaped life icons, which I'll draw with a filled path that changes color depending on whether a life is still left.

Still writing the drawing code: I'm finishing the heart-shaped life icons, filling them in one color when a life remains and a faint outline when it's gone.