Models 44

Claude Opus 5.5

by Anthropicreasoning defaultClean
Open in new window

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

How it went

Total time
2 min 8 s
Cost
$0.331
Output tokens
16,411
Spent thinking
2,470 tokens
Speed
131.7 tok/s
Console errors
0
First token after
3.1 s
First visible text after
29.6 s
Prompt tokens
793
Code size
24.9 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-opus-5.5
Reasoning sent
model default
Max output
128,000 tokens
Generated
Nov 12, 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 · 650 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Neon Breakout</title>
<style>
  html,body{margin:0;padding:0;height:100%;background:#05060f;overflow:hidden;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
  body{display:flex;align-items:center;justify-content:center}
  #wrap{position:relative;display:flex;align-items:center;justify-content:center;width:100vw;height:100vh}
  canvas{display:block;border-radius:14px;box-shadow:0 0 0 1px rgba(120,160,255,.15),0 20px 60px rgba(0,0,0,.6),0 0 80px rgba(80,120,255,.12);cursor:none;touch-action:none}
</style>
</head>
<body>
<div id="wrap"><canvas id="c"></canvas></div>
<script>
(() => {
const W = 960, H = 640;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let dpr = 1;

function resize() {
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  const maxW = window.innerWidth * 0.97, maxH = window.innerHeight * 0.97;
  let cw = maxW, ch = cw * H / W;
  if (ch > maxH) { ch = maxH; cw = ch * W / H; }
  canvas.style.width = cw + 'px';
  canvas.style.height = ch + 'px';
  canvas.width = Math.round(W * dpr);
  canvas.height = Math.round(H * dpr);
}
window.addEventListener('resize', resize);
resize();

// ---------- State ----------
let state = 'demo'; // demo, serve, play, over, win
let bricks = [], balls = [], particles = [], powerups = [], floaters = [];
let score = 0, lives = 3, wideTimer = 0, shake = 0, serveTimer = 0, time = 0;
let bricksBroken = 0;
const paddle = { x: W / 2, y: H - 46, w: 120, h: 14, baseW: 120, wideW: 200, targetX: W / 2 };
const keys = { left: false, right: false };
let useMouse = false;
let aiOffset = 0;

const ROW_COLORS = [
  ['#ff4d6d', '#ff8fa3'], ['#ff7b39', '#ffb380'], ['#ffd23f', '#ffe791'],
  ['#5ee37a', '#a8f5b8'], ['#2ed8d8', '#9af1f1'], ['#3d8bff', '#9cc2ff'],
  ['#8f5bff', '#c4a8ff'], ['#e655ff', '#f2a8ff']
];

function rand(a, b) { return a + Math.random() * (b - a); }
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }

function buildLevel() {
  bricks = [];
  const cols = 12, rows = 8, margin = 40, gap = 6, top = 82, bh = 24;
  const bw = (W - margin * 2 - gap * (cols - 1)) / cols;
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const tough = r < 2 || (r === 4 && (c % 3 === 1));
      const br = {
        x: margin + c * (bw + gap), y: top + r * (bh + gap), w: bw, h: bh,
        row: r, hits: tough ? 2 : 1, maxHits: tough ? 2 : 1, alive: true,
        power: null, flash: 0, crack: null
      };
      bricks.push(br);
    }
  }
  // assign power-ups
  const candidates = bricks.filter(b => b.row >= 2);
  const count = 12;
  for (let i = 0; i < count; i++) {
    const idx = Math.floor(Math.random() * candidates.length);
    const b = candidates.splice(idx, 1)[0];
    b.power = i % 2 === 0 ? 'wide' : 'multi';
  }
}

function makeCrack(br) {
  const lines = [];
  const cx = br.w * rand(0.35, 0.65), cy = br.h * rand(0.35, 0.65);
  const n = 4 + Math.floor(Math.random() * 2);
  for (let i = 0; i < n; i++) {
    const ang = (i / n) * Math.PI * 2 + rand(-0.4, 0.4);
    const pts = [[cx, cy]];
    let x = cx, y = cy;
    const segs = 3;
    const len = rand(10, 26);
    for (let s = 0; s < segs; s++) {
      const a = ang + rand(-0.5, 0.5);
      x += Math.cos(a) * len / segs * 1.4;
      y += Math.sin(a) * len / segs * 0.7;
      pts.push([clamp(x, 1, br.w - 1), clamp(y, 1, br.h - 1)]);
    }
    lines.push(pts);
  }
  return lines;
}

function baseSpeed() { return Math.min(430 + bricksBroken * 3.5, 680); }

function newServeBall() {
  balls = [{ x: paddle.x, y: paddle.y - 9, vx: 0, vy: 0, r: 8, stuck: true, trail: [] }];
  serveTimer = 0;
}

function launch(b) {
  const ang = rand(-0.35, 0.35);
  const s = baseSpeed();
  b.vx = Math.sin(ang) * s; b.vy = -Math.cos(ang) * s; b.stuck = false;
}

function resetGame(newState) {
  score = 0; lives = 3; wideTimer = 0; bricksBroken = 0;
  paddle.w = paddle.baseW; paddle.x = W / 2; paddle.targetX = W / 2;
  particles = []; powerups = []; floaters = [];
  buildLevel();
  newServeBall();
  state = newState;
}

// ---------- Input ----------
function toLogical(clientX) {
  const r = canvas.getBoundingClientRect();
  return (clientX - r.left) / r.width * W;
}
canvas.addEventListener('mousemove', e => {
  if (state === 'demo') return;
  useMouse = true; paddle.targetX = toLogical(e.clientX);
});
canvas.addEventListener('touchmove', e => {
  if (state === 'demo') return;
  useMouse = true; paddle.targetX = toLogical(e.touches[0].clientX); e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchstart', e => {
  action();
  if (state !== 'demo') { useMouse = true; paddle.targetX = toLogical(e.touches[0].clientX); }
  e.preventDefault();
}, { passive: false });
canvas.addEventListener('mousedown', e => {
  action();
  if (state !== 'demo') { useMouse = true; paddle.targetX = toLogical(e.clientX); }
});
window.addEventListener('keydown', e => {
  if (e.key === 'ArrowLeft') { keys.left = true; useMouse = false; }
  if (e.key === 'ArrowRight') { keys.right = true; useMouse = false; }
  if (['ArrowLeft', 'ArrowRight', ' ', 'ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault();
  if (e.repeat) return;
  if (state === 'serve' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return;
  action();
});
window.addEventListener('keyup', e => {
  if (e.key === 'ArrowLeft') keys.left = false;
  if (e.key === 'ArrowRight') keys.right = false;
});
function action() {
  if (state === 'demo' || state === 'over' || state === 'win') { resetGame('serve'); return; }
  if (state === 'serve') { balls.forEach(b => { if (b.stuck) launch(b); }); state = 'play'; }
}

// ---------- Effects ----------
function burst(x, y, color, n, spd) {
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, s = rand(40, spd);
    particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 40, life: rand(0.5, 1.1), max: 1.1, color, size: rand(2, 5) });
  }
}
function brickBurst(br) {
  const col = ROW_COLORS[br.row][0];
  for (let i = 0; i < 26; i++) {
    const px = br.x + Math.random() * br.w, py = br.y + Math.random() * br.h;
    const a = Math.atan2(py - (br.y + br.h / 2), px - (br.x + br.w / 2)) + rand(-0.6, 0.6);
    const s = rand(60, 280);
    particles.push({ x: px, y: py, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 60, life: rand(0.5, 1.2), max: 1.2, color: i % 4 === 0 ? '#ffffff' : col, size: rand(2, 5.5), sq: true, rot: Math.random() * 6, vr: rand(-10, 10) });
  }
}
function floatText(x, y, text, color) { floaters.push({ x, y, text, color, life: 1 }); }

// ---------- Game logic ----------
function hitBrick(br, ball) {
  br.hits--;
  br.flash = 1;
  if (br.hits <= 0) {
    br.alive = false;
    bricksBroken++;
    const pts = br.maxHits === 2 ? 50 : 10;
    if (state !== 'demo') score += pts;
    floatText(br.x + br.w / 2, br.y + br.h / 2, '+' + pts, ROW_COLORS[br.row][1]);
    brickBurst(br);
    shake = Math.min(shake + 3, 8);
    if (br.power) {
      powerups.push({ x: br.x + br.w / 2, y: br.y + br.h / 2, vy: 150, type: br.power, t: 0 });
    }
    // speed up slightly
    balls.forEach(b => {
      if (b.stuck) return;
      const s = Math.hypot(b.vx, b.vy), ns = Math.max(s, baseSpeed());
      b.vx *= ns / s; b.vy *= ns / s;
    });
    if (bricks.every(b => !b.alive)) {
      if (state === 'demo') { setTimeout(() => { if (state === 'demo') resetGame('demo'); }, 1200); }
      else { state = 'win'; for (let i = 0; i < 6; i++) setTimeout(() => burst(rand(200, 760), rand(150, 400), ROW_COLORS[i % 8][0], 60, 400), i * 200); }
    }
  } else {
    br.crack = makeCrack(br);
    if (state !== 'demo') score += 5;
    burst(ball.x, ball.y, '#ffffff', 10, 180);
    shake = Math.min(shake + 1.5, 8);
  }
}

function fixAngle(b) {
  const s = Math.hypot(b.vx, b.vy);
  if (Math.abs(b.vy) < s * 0.28) {
    b.vy = (b.vy < 0 ? -1 : 1) * s * 0.28;
    b.vx = Math.sign(b.vx || 1) * Math.sqrt(s * s - b.vy * b.vy);
  }
}

function stepBall(b, dt) {
  b.x += b.vx * dt; b.y += b.vy * dt;
  // walls
  if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); }
  if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); }
  if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); }
  // paddle
  const px = paddle.x - paddle.w / 2, py = paddle.y;
  if (b.vy > 0) {
    const cx = clamp(b.x, px, px + paddle.w), cy = clamp(b.y, py, py + paddle.h);
    const dx = b.x - cx, dy = b.y - cy;
    if (dx * dx + dy * dy <= b.r * b.r && b.y < py + paddle.h) {
      const off = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
      const ang = off * (Math.PI / 180) * 62;
      const s = Math.max(Math.hypot(b.vx, b.vy), baseSpeed());
      b.vx = Math.sin(ang) * s; b.vy = -Math.cos(ang) * s;
      b.y = py - b.r - 0.5;
      burst(b.x, py, '#9cdcff', 6, 120);
      aiOffset = rand(-0.75, 0.75);
      return;
    }
  }
  // bricks: resolve the deepest single collision per substep
  let best = null, bestD = Infinity, bdx = 0, bdy = 0;
  for (const br of bricks) {
    if (!br.alive) continue;
    if (b.x + b.r < br.x || b.x - b.r > br.x + br.w || b.y + b.r < br.y || b.y - b.r > br.y + br.h) continue;
    const cx = clamp(b.x, br.x, br.x + br.w), cy = clamp(b.y, br.y, br.y + br.h);
    const dx = b.x - cx, dy = b.y - cy, d2 = dx * dx + dy * dy;
    if (d2 <= b.r * b.r && d2 < bestD) { best = br; bestD = d2; bdx = dx; bdy = dy; }
  }
  if (best) {
    const br = best;
    if (bdx === 0 && bdy === 0) {
      // center inside (shouldn't happen with substeps) - push out along smallest overlap
      const ol = b.x - br.x, or = br.x + br.w - b.x, ot = b.y - br.y, ob = br.y + br.h - b.y;
      const m = Math.min(ol, or, ot, ob);
      if (m === ol) { b.x = br.x - b.r; b.vx = -Math.abs(b.vx); }
      else if (m === or) { b.x = br.x + br.w + b.r; b.vx = Math.abs(b.vx); }
      else if (m === ot) { b.y = br.y - b.r; b.vy = -Math.abs(b.vy); }
      else { b.y = br.y + br.h + b.r; b.vy = Math.abs(b.vy); }
    } else if (Math.abs(bdx) > Math.abs(bdy)) {
      b.vx = Math.sign(bdx) * Math.abs(b.vx);
      b.x = (bdx > 0 ? br.x + br.w : br.x) + Math.sign(bdx) * b.r;
    } else {
      b.vy = Math.sign(bdy) * Math.abs(b.vy);
      b.y = (bdy > 0 ? br.y + br.h : br.y) + Math.sign(bdy) * b.r;
    }
    fixAngle(b);
    hitBrick(br, b);
  }
}

function applyPower(p) {
  if (p.type === 'wide') {
    wideTimer = 10;
    floatText(paddle.x, paddle.y - 20, 'WIDE PADDLE!', '#5ef0ff');
  } else {
    const src = balls.find(b => !b.stuck) || balls[0];
    if (!src) return;
    if (src.stuck) launch(src);
    const s = Math.max(Math.hypot(src.vx, src.vy), baseSpeed());
    const baseAng = Math.atan2(src.vx, -src.vy);
    const need = Math.max(2, 3 - balls.length);
    for (let i = 0; i < need; i++) {
      const a = baseAng + (i % 2 === 0 ? 0.45 : -0.45) * (1 + Math.floor(i / 2) * 0.5);
      balls.push({ x: src.x, y: src.y, vx: Math.sin(a) * s, vy: -Math.abs(Math.cos(a) * s), r: 8, stuck: false, trail: [] });
    }
    floatText(paddle.x, paddle.y - 20, 'MULTI-BALL!', '#ffcf5e');
  }
  burst(p.x, p.y, p.type === 'wide' ? '#5ef0ff' : '#ffcf5e', 30, 260);
}

function aiControl(dt) {
  let target = W / 2;
  const desc = balls.filter(b => !b.stuck && b.vy > 0);
  let best = null, bestT = Infinity;
  for (const b of desc) {
    const t = (paddle.y - b.r - b.y) / b.vy;
    if (t < bestT) { bestT = t; best = b; }
  }
  const pu = powerups.filter(p => p.y < paddle.y).sort((a, b) => b.y - a.y)[0];
  if (best) {
    let x = best.x + best.vx * Math.max(bestT, 0);
    const lo = best.r, hi = W - best.r, span = hi - lo;
    let m = (x - lo) % (2 * span); if (m < 0) m += 2 * span;
    x = m > span ? hi - (m - span) : lo + m;
    target = x - aiOffset * paddle.w / 2 * 0.8;
    if (pu && bestT > 0.9) target = pu.x;
  } else if (pu) target = pu.x;
  else if (balls[0]) target = balls[0].x;
  const maxMove = 1100 * dt;
  paddle.x += clamp(target - paddle.x, -maxMove, maxMove);
}

function update(dt) {
  time += dt;
  // paddle width animation
  if (wideTimer > 0) { wideTimer -= dt; if (wideTimer <= 0) wideTimer = 0; }
  const targetW = wideTimer > 0 ? paddle.wideW : paddle.baseW;
  paddle.w += (targetW - paddle.w) * Math.min(1, dt * 10);

  // paddle control
  if (state === 'demo') aiControl(dt);
  else if (state === 'serve' || state === 'play') {
    if (keys.left || keys.right) {
      paddle.x += ((keys.right ? 1 : 0) - (keys.left ? 1 : 0)) * 760 * dt;
      paddle.targetX = paddle.x;
    } else if (useMouse) {
      paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt * 25);
    }
  }
  paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);

  const active = state === 'demo' || state === 'serve' || state === 'play';
  if (active) {
    // demo auto-serve
    if (state === 'demo' && balls.some(b => b.stuck)) {
      serveTimer += dt;
      if (serveTimer > 0.8) balls.forEach(b => { if (b.stuck) launch(b); });
    }
    for (const b of balls) {
      if (b.stuck) { b.x = paddle.x; b.y = paddle.y - b.r - 1; continue; }
      const dist = Math.hypot(b.vx, b.vy) * dt;
      const steps = Math.max(1, Math.ceil(dist / (b.r * 0.4)));
      const sdt = dt / steps;
      for (let i = 0; i < steps; i++) stepBall(b, sdt);
      b.trail.push([b.x, b.y]); if (b.trail.length > 10) b.trail.shift();
    }
    // lost balls
    const before = balls.length;
    balls = balls.filter(b => b.y - b.r <= H);
    if (before && balls.length === 0) {
      shake = 10;
      burst(paddle.x, H - 10, '#ff4d6d', 40, 300);
      if (state === 'demo') { newServeBall(); }
      else {
        lives--;
        wideTimer = 0;
        powerups = [];
        if (lives <= 0) { state = 'over'; }
        else { newServeBall(); state = 'serve'; }
      }
    }
    // power-ups
    for (const p of powerups) {
      p.y += p.vy * dt; p.t += dt;
      const px = paddle.x - paddle.w / 2;
      if (p.y + 12 >= paddle.y && p.y - 12 <= paddle.y + paddle.h && p.x + 22 >= px && p.x - 22 <= px + paddle.w) {
        p.caught = true; applyPower(p);
        if (state !== 'demo') score += 25;
      }
    }
    powerups = powerups.filter(p => !p.caught && p.y < H + 20);
  }

  for (const br of bricks) if (br.flash > 0) br.flash = Math.max(0, br.flash - dt * 4);
  for (const p of particles) {
    p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 520 * dt; p.vx *= 0.99; p.life -= dt;
    if (p.rot !== undefined) p.rot += p.vr * dt;
  }
  particles = particles.filter(p => p.life > 0);
  if (particles.length > 900) particles.splice(0, particles.length - 900);
  for (const f of floaters) { f.y -= 40 * dt; f.life -= dt * 1.2; }
  floaters = floaters.filter(f => f.life > 0);
  shake = Math.max(0, shake - dt * 30);
}

// ---------- Rendering ----------
function rrect(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 drawIcon(type, x, y, s) {
  ctx.save();
  ctx.translate(x, y);
  if (type === 'wide') {
    ctx.strokeStyle = '#ffffff'; ctx.fillStyle = '#ffffff'; ctx.lineWidth = 2 * s; ctx.lineCap = 'round';
    ctx.beginPath(); ctx.moveTo(-6 * s, 0); ctx.lineTo(6 * s, 0); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(-9 * s, 0); ctx.lineTo(-4.5 * s, -4 * s); ctx.lineTo(-4.5 * s, 4 * s); ctx.closePath(); ctx.fill();
    ctx.beginPath(); ctx.moveTo(9 * s, 0); ctx.lineTo(4.5 * s, -4 * s); ctx.lineTo(4.5 * s, 4 * s); ctx.closePath(); ctx.fill();
  } else {
    ctx.fillStyle = '#ffffff';
    [[-6, 2], [0, -3], [6, 2]].forEach(([dx, dy]) => { ctx.beginPath(); ctx.arc(dx * s, dy * s, 2.6 * s, 0, Math.PI * 2); ctx.fill(); });
  }
  ctx.restore();
}

function drawBackground() {
  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, '#0b0f2a'); g.addColorStop(1, '#130824');
  ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
  ctx.strokeStyle = 'rgba(110,140,255,0.06)'; 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(); }
  const rg = ctx.createRadialGradient(W / 2, H * 0.3, 50, W / 2, H * 0.3, 600);
  rg.addColorStop(0, 'rgba(90,110,255,0.10)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
  ctx.fillStyle = rg; ctx.fillRect(0, 0, W, H);
}

function drawBricks() {
  for (const br of bricks) {
    if (!br.alive) continue;
    const [c1, c2] = ROW_COLORS[br.row];
    ctx.save();
    ctx.shadowColor = c1; ctx.shadowBlur = 12;
    const g = ctx.createLinearGradient(0, br.y, 0, br.y + br.h);
    if (br.maxHits === 2) {
      g.addColorStop(0, br.hits === 2 ? '#e8ecf8' : '#b9bfd0');
      g.addColorStop(0.5, br.hits === 2 ? '#a6aec6' : '#7f879e');
      g.addColorStop(1, br.hits === 2 ? '#6c7592' : '#4f5670');
      ctx.shadowColor = '#c8d4ff';
    } else {
      g.addColorStop(0, c2); g.addColorStop(1, c1);
    }
    ctx.fillStyle = g;
    rrect(br.x, br.y, br.w, br.h, 5); ctx.fill();
    ctx.restore();
    if (br.maxHits === 2) {
      ctx.strokeStyle = c1; ctx.lineWidth = 2.5;
      rrect(br.x + 1.5, br.y + 1.5, br.w - 3, br.h - 3, 4); ctx.stroke();
      // rivets
      ctx.fillStyle = 'rgba(40,45,70,0.6)';
      [[5, 5], [br.w - 5, 5], [5, br.h - 5], [br.w - 5, br.h - 5]].forEach(([dx, dy]) => { ctx.beginPath(); ctx.arc(br.x + dx, br.y + dy, 1.6, 0, Math.PI * 2); ctx.fill(); });
    }
    // highlight
    ctx.fillStyle = 'rgba(255,255,255,0.25)';
    rrect(br.x + 3, br.y + 2, br.w - 6, br.h * 0.32, 3); ctx.fill();
    // crack
    if (br.crack) {
      ctx.save(); ctx.translate(br.x, br.y);
      ctx.strokeStyle = 'rgba(20,20,35,0.9)'; ctx.lineWidth = 1.8; ctx.lineJoin = 'round';
      for (const ln of br.crack) { ctx.beginPath(); ln.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.stroke(); }
      ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 0.8;
      for (const ln of br.crack) { ctx.beginPath(); ln.forEach(([x, y], i) => i ? ctx.lineTo(x + 0.8, y + 0.8) : ctx.moveTo(x + 0.8, y + 0.8)); ctx.stroke(); }
      ctx.restore();
    }
    // power icon
    if (br.power) {
      const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
      const pulse = 0.85 + Math.sin(time * 5 + br.x) * 0.15;
      ctx.save();
      ctx.shadowColor = br.power === 'wide' ? '#5ef0ff' : '#ffcf5e'; ctx.shadowBlur = 10 * pulse;
      ctx.fillStyle = br.power === 'wide' ? 'rgba(0,120,170,0.85)' : 'rgba(170,100,0,0.85)';
      rrect(cx - 15, cy - 8, 30, 16, 8); ctx.fill();
      ctx.restore();
      ctx.strokeStyle = 'rgba(255,255,255,0.8)'; ctx.lineWidth = 1;
      rrect(cx - 15, cy - 8, 30, 16, 8); ctx.stroke();
      drawIcon(br.power, cx, cy, 0.9);
    }
    if (br.flash > 0) {
      ctx.fillStyle = `rgba(255,255,255,${br.flash * 0.7})`;
      rrect(br.x, br.y, br.w, br.h, 5); ctx.fill();
    }
  }
}

function drawPaddle() {
  const x = paddle.x - paddle.w / 2, y = paddle.y;
  ctx.save();
  const wide = wideTimer > 0;
  ctx.shadowColor = wide ? '#5ef0ff' : '#7aa8ff'; ctx.shadowBlur = 22;
  const g = ctx.createLinearGradient(0, y, 0, y + paddle.h);
  g.addColorStop(0, wide ? '#c8fbff' : '#e2ebff'); g.addColorStop(1, wide ? '#1fb8d8' : '#4a6cff');
  ctx.fillStyle = g;
  rrect(x, y, paddle.w, paddle.h, 7); ctx.fill();
  ctx.restore();
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  rrect(x + 6, y + 2, paddle.w - 12, 3, 2); ctx.fill();
  if (wide) {
    const frac = wideTimer / 10;
    ctx.fillStyle = 'rgba(94,240,255,0.25)';
    rrect(x, y + paddle.h + 6, paddle.w, 4, 2); ctx.fill();
    ctx.fillStyle = '#5ef0ff';
    rrect(x, y + paddle.h + 6, paddle.w * frac, 4, 2); ctx.fill();
  }
}

function drawBalls() {
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const [tx, ty] = b.trail[i], a = i / b.trail.length;
      ctx.fillStyle = `rgba(140,200,255,${a * 0.35})`;
      ctx.beginPath(); ctx.arc(tx, ty, b.r * a, 0, Math.PI * 2); ctx.fill();
    }
    ctx.save();
    ctx.shadowColor = '#bfe4ff'; ctx.shadowBlur = 18;
    const g = ctx.createRadialGradient(b.x - 3, b.y - 3, 1, b.x, b.y, b.r);
    g.addColorStop(0, '#ffffff'); g.addColorStop(1, '#8fc8ff');
    ctx.fillStyle = g;
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();
    ctx.restore();
  }
}

function drawPowerups() {
  for (const p of powerups) {
    const col = p.type === 'wide' ? '#5ef0ff' : '#ffcf5e';
    const dark = p.type === 'wide' ? '#0a7fa8' : '#b0680a';
    ctx.save();
    ctx.translate(p.x, p.y);
    ctx.scale(1, 0.9 + Math.sin(p.t * 8) * 0.1);
    ctx.shadowColor = col; ctx.shadowBlur = 20;
    const g = ctx.createLinearGradient(0, -11, 0, 11);
    g.addColorStop(0, col); g.addColorStop(1, dark);
    ctx.fillStyle = g;
    rrect(-22, -11, 44, 22, 11); ctx.fill();
    ctx.restore();
    ctx.strokeStyle = 'rgba(255,255,255,0.9)'; ctx.lineWidth = 1.5;
    rrect(p.x - 22, p.y - 11, 44, 22, 11); ctx.stroke();
    drawIcon(p.type, p.x, p.y, 1.2);
  }
}

function drawParticles() {
  for (const p of particles) {
    const a = clamp(p.life / p.max, 0, 1);
    ctx.globalAlpha = a;
    ctx.fillStyle = p.color;
    if (p.sq) {
      ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot);
      ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size); ctx.restore();
    } else {
      ctx.beginPath(); ctx.arc(p.x, p.y, p.size * a, 0, Math.PI * 2); ctx.fill();
    }
  }
  ctx.globalAlpha = 1;
  ctx.textAlign = 'center'; ctx.font = 'bold 16px system-ui,sans-serif';
  for (const f of floaters) {
    ctx.globalAlpha = clamp(f.life, 0, 1);
    ctx.fillStyle = f.color; ctx.fillText(f.text, f.x, f.y);
  }
  ctx.globalAlpha = 1;
}

function drawHUD() {
  ctx.fillStyle = 'rgba(10,12,30,0.55)';
  ctx.fillRect(0, 0, W, 54);
  ctx.strokeStyle = 'rgba(120,150,255,0.25)'; ctx.beginPath(); ctx.moveTo(0, 54); ctx.lineTo(W, 54); ctx.stroke();
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'left'; ctx.fillStyle = '#8ea2d8'; ctx.font = '600 13px system-ui,sans-serif';
  ctx.fillText('SCORE', 24, 27);
  ctx.fillStyle = '#ffffff'; ctx.font = 'bold 24px system-ui,sans-serif';
  ctx.fillText(String(score).padStart(5, '0'), 82, 28);
  ctx.textAlign = 'center'; ctx.font = 'bold 20px system-ui,sans-serif';
  const tg = ctx.createLinearGradient(W / 2 - 90, 0, W / 2 + 90, 0);
  tg.addColorStop(0, '#ff6b9a'); tg.addColorStop(0.5, '#ffd23f'); tg.addColorStop(1, '#5ef0ff');
  ctx.fillStyle = tg; ctx.fillText('NEON BREAKOUT', W / 2, 28);
  ctx.textAlign = 'right'; ctx.fillStyle = '#8ea2d8'; ctx.font = '600 13px system-ui,sans-serif';
  ctx.fillText('LIVES', W - 104, 27);
  for (let i = 0; i < 3; i++) {
    ctx.save();
    const on = i < lives;
    ctx.shadowColor = on ? '#ff4d6d' : 'transparent'; ctx.shadowBlur = on ? 10 : 0;
    ctx.fillStyle = on ? '#ff6b8a' : 'rgba(255,255,255,0.12)';
    ctx.beginPath(); ctx.arc(W - 84 + i * 26, 27, 8, 0, Math.PI * 2); ctx.fill();
    ctx.restore();
  }
  ctx.textBaseline = 'alphabetic';
}

function overlay(title, sub, color, extra) {
  ctx.fillStyle = 'rgba(5,6,18,0.62)';
  ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center';
  ctx.save();
  ctx.shadowColor = color; ctx.shadowBlur = 30;
  ctx.fillStyle = color; ctx.font = 'bold 64px system-ui,sans-serif';
  ctx.fillText(title, W / 2, H / 2 - 30);
  ctx.restore();
  if (extra) { ctx.fillStyle = '#d0dbff'; ctx.font = '600 22px system-ui,sans-serif'; ctx.fillText(extra, W / 2, H / 2 + 14); }
  const a = 0.6 + Math.sin(time * 4) * 0.4;
  ctx.globalAlpha = a;
  ctx.fillStyle = '#ffffff'; ctx.font = 'bold 22px system-ui,sans-serif';
  ctx.fillText(sub, W / 2, H / 2 + 60);
  ctx.globalAlpha = 1;
}

function draw() {
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.save();
  if (shake > 0) ctx.translate(rand(-shake, shake) * 0.5, rand(-shake, shake) * 0.5);
  drawBackground();
  drawBricks();
  drawPowerups();
  drawPaddle();
  drawBalls();
  drawParticles();
  ctx.restore();
  drawHUD();

  if (state === 'demo') {
    ctx.fillStyle = 'rgba(5,6,18,0.35)';
    ctx.fillRect(0, H / 2 + 20, W, 120);
    ctx.textAlign = 'center';
    ctx.fillStyle = '#9fb3ff'; ctx.font = '600 14px system-ui,sans-serif';
    ctx.fillText('DEMO MODE', W / 2, H / 2 + 48);
    ctx.globalAlpha = 0.65 + Math.sin(time * 4) * 0.35;
    ctx.fillStyle = '#ffffff'; ctx.font = 'bold 30px system-ui,sans-serif';
    ctx.fillText('Click or press a key to start', W / 2, H / 2 + 86);
    ctx.globalAlpha = 1;
    ctx.fillStyle = '#8ea2d8'; ctx.font = '14px system-ui,sans-serif';
    ctx.fillText('Mouse or ← → to move  •  Catch power-ups: ↔ wide paddle, ••• multi-ball', W / 2, H / 2 + 118);
  } else if (state === 'serve') {
    ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '600 18px system-ui,sans-serif';
    ctx.fillText('Click or press Space to launch', W / 2, H / 2 + 120);
  } else if (state === 'over') {
    overlay('GAME OVER', 'Click or press any key to restart', '#ff4d6d', 'Final score: ' + score);
  } else if (state === 'win') {
    overlay('VICTORY!', 'Click or press any key to play again', '#5ee37a', 'All bricks cleared — score: ' + score);
  }
}

// ---------- Loop ----------
let last = performance.now();
function loop(now) {
  let dt = (now - last) / 1000; last = now;
  if (dt > 1 / 30) dt = 1 / 30;
  update(dt);
  draw();
  requestAnimationFrame(loop);
}
resetGame('demo');
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (2,470 tokens)
I'm planning a Breakout game that stays concise but still robust. The canvas will use a logical 800x600 size scaled to fit the viewport, and an AI-driven demo will move the paddle to track the ball until the player clicks or presses a key to start. I also want to substep the ball's movement so it can't tunnel through bricks.

I'm working through the brick layout and power-up rules next: tough bricks that take two hits, icon-marked power-up bricks, and falling pickups that widen the paddle or add balls. I also need the demo AI to track the lowest ball and handle its own resets, plus the paddle bounce angle math and the launch and scaling details.

I'm getting into the collision details now: each substep checks walls, the paddle (only when moving down), and bricks, resolving against the closer axis and handling the rare case where the center lands inside a brick. I'm also settling the speed and substep count, and how a brick hit reduces its hits, awards points, and triggers particles or cracks.

I'm laying out the HUD and the overlay states, with the demo text and the restart prompt on game over or victory. I'm also looking at how keys start the demo and how the canvas sits responsively in its wrapper.

Still writing the game setup code...

Still working on the Breakout game code. I'm settling how multi-ball works, adding two extra balls at ±25° off the first ball so there are three in play, and planning how the demo AI picks its target and moves the paddle.

Still writing the game code...

Still writing the collision code...

Now I'm handling the input side: a click starts the action handler, arrow keys set the paddle moving, and space launches the ball during serve. Mouse movement maps to a logical paddle target, and keys take over direct control until the mouse moves again.

Still writing the input handling...