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
1 min 21 s
Cost
$0.0098
Output tokens
19,383
Spent thinking
9,812 tokens
Speed
246.6 tok/s
Console errors
0
First token after
2.0 s
First visible text after
51.1 s
Prompt tokens
793
Code size
17.5 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"low"}
Max output
32,000 tokens
Generated
Nov 10, 2026

What it was asked

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

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

Code run-1.html · 474 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>
  html,body{margin:0;height:100%;background:#070818;color:#eef;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;overflow:hidden}
  body{display:flex;align-items:center;justify-content:center}
  #wrap{position:relative;width:min(100vw,calc((100vh - 0px) * 800 / 600));max-width:800px;user-select:none;-webkit-user-select:none}
  canvas{display:block;width:100%;height:auto;border-radius:12px;background:#0d1030;box-shadow:0 0 60px #2d3cff55, inset 0 0 0 1px #3a4aff66;cursor:none;touch-action:none}
  #ov{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:rgba(6,8,26,.5);border-radius:12px;pointer-events:none;padding:16px;box-sizing:border-box}
  #ov.hide{display:none}
  #ov h1{margin:0;font-size:clamp(30px,7vw,60px);letter-spacing:6px;background:linear-gradient(90deg,#ffd54a,#ff5fa2,#5fd4ff);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 4px 18px #0008)}
  #ov p{margin:14px 0 0;font-size:clamp(14px,2.6vw,20px);opacity:.92;text-shadow:0 2px 8px #000}
  #ov .sub{font-size:clamp(12px,2vw,15px);opacity:.6;margin-top:8px}
  #ov button{pointer-events:auto;margin-top:22px;padding:12px 30px;font-size:18px;font-weight:700;letter-spacing:1px;color:#120d2a;background:linear-gradient(90deg,#ffd54a,#ff8c42);border:0;border-radius:999px;cursor:pointer;box-shadow:0 6px 24px #ff8c4288}
  #ov button:hover{filter:brightness(1.08)}
  #ov button.hide{display:none}
  #hint{position:absolute;left:0;right:0;bottom:-28px;text-align:center;font-size:12px;opacity:.5}
</style>
</head>
<body>
<div id="wrap">
  <canvas id="c"></canvas>
  <div id="ov">
    <h1 id="ovTitle">BREAKOUT</h1>
    <p id="ovMsg">Click or press a key to start</p>
    <p class="sub" id="ovSub">Mouse or ← → to move · Space to launch</p>
    <button id="ovBtn" class="hide">Restart</button>
  </div>
  <div id="hint">Mouse / ← → move · Space or click launches · Catch W = wide paddle, M = multi-ball</div>
</div>
<script>
(() => {
"use strict";
const W = 800, H = 600, R = 7, BASE_W = 110, SPEED = 380, MAXANG = 70 * Math.PI / 180;
const COLS = 10, ROWS = 6, BW = 72, BH = 22, GAP = 6, OX = 13, OY = 86;
const COLORS = ['#ff4d6d', '#ff8c42', '#ffd23f', '#3ddc97', '#4cc9f0', '#9d7bff'];
const POWER_AT = { '0,4': 'W', '1,1': 'M', '2,8': 'W', '3,3': 'M', '4,6': 'W', '5,0': 'M' };
const POWER_COL = { W: '#5fd4ff', M: '#ff5fa2' };

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const ov = document.getElementById('ov');
const ovTitle = document.getElementById('ovTitle');
const ovMsg = document.getElementById('ovMsg');
const ovSub = document.getElementById('ovSub');
const ovBtn = document.getElementById('ovBtn');
const wrap = document.getElementById('wrap');

function setupCanvas() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = W * dpr; canvas.height = H * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
setupCanvas();

const clamp = (v, a, b) => Math.max(a, Math.min(b, v));

let paddle, bricks, balls, caps, parts, score, lives, state, demo, wideT, overT, keyL, keyR, flashT;

function makeBricks() {
  const arr = [];
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
    const tough = r < 2 && c % 3 !== 1;
    arr.push({
      x: OX + c * (BW + GAP), y: OY + r * (BH + GAP), w: BW, h: BH,
      hp: tough ? 2 : 1, tough, color: COLORS[r], pts: tough ? 20 : 10,
      alive: true, cracked: false, flash: 0, power: POWER_AT[r + ',' + c] || null,
      crackSeed: Math.random()
    });
  }
  return arr;
}

function stuckBall() {
  return { x: paddle.cx, y: paddle.y - R, vx: 0, vy: 0, r: R, stuck: true, t: 0, dead: false };
}

function newGame(isDemo) {
  paddle = { cx: W / 2, tx: W / 2, y: H - 36, h: 14, w: BASE_W };
  bricks = makeBricks(); balls = [stuckBall()]; caps = []; parts = [];
  score = 0; lives = 3; wideT = 0; overT = 0; flashT = 0;
  demo = isDemo; state = 'play';
  updateOverlay();
}

function aliveCount() { return bricks.reduce((n, b) => n + (b.alive ? 1 : 0), 0); }

function burst(x, y, color, n, speed) {
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, s = speed * (0.3 + Math.random());
    parts.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 60, life: 0.6 + Math.random() * 0.4, max: 1, color, size: 1.5 + Math.random() * 2.5 });
  }
}

function hitBrick(br, x, y) {
  br.hp--; br.flash = 0.08;
  if (br.hp <= 0) {
    br.alive = false; score += br.pts;
    burst(br.x + br.w / 2, br.y + br.h / 2, br.color, 22, 220);
    if (br.power) caps.push({ x: br.x + br.w / 2, y: br.y + br.h / 2, type: br.power, vy: 130 });
  } else {
    br.cracked = true; score += 5;
    burst(x, y, '#ffffff', 8, 140);
  }
}

function launch() {
  for (const b of balls) if (b.stuck) {
    b.stuck = false;
    const a = (Math.random() - 0.5) * 0.5;
    b.vx = SPEED * Math.sin(a); b.vy = -SPEED * Math.cos(a);
  }
}

function moveBall(b, dt) {
  const steps = Math.max(1, Math.ceil(SPEED * dt / 2));  // <= 2px per substep: no tunneling
  const k = dt / steps;
  let sx = b.vx * k, sy = b.vy * k;
  for (let i = 0; i < steps && !b.dead; i++) {
    const px = b.x, py = b.y;
    b.x += sx; b.y += sy;
    // walls
    if (b.x - R < 0) { b.x = R; b.vx = Math.abs(b.vx); }
    if (b.x + R > W) { b.x = W - R; b.vx = -Math.abs(b.vx); }
    if (b.y - R < 0) { b.y = R; b.vy = Math.abs(b.vy); }
    // paddle (only when descending and crossing paddle top)
    const pl = paddle.cx - paddle.w / 2, pr = pl + paddle.w, pt = paddle.y;
    if (b.vy > 0 && py + R <= pt && b.y + R >= pt && b.x >= pl - R && b.x <= pr + R) {
      const rel = clamp((b.x - paddle.cx) / (paddle.w / 2), -1, 1);
      const a = rel * MAXANG;
      b.vx = SPEED * Math.sin(a); b.vy = -SPEED * Math.cos(a);
      b.y = pt - R;
      burst(b.x, pt, '#9ff', 4, 90);
    }
    // bricks: swept side detection
    for (const br of bricks) {
      if (!br.alive) continue;
      const l = br.x, t = br.y, rt = br.x + br.w, bt = br.y + br.h;
      if (b.x + R > l && b.x - R < rt && b.y + R > t && b.y - R < bt) {
        if (py + R <= t) { b.vy = -Math.abs(b.vy); b.y = py; }
        else if (py - R >= bt) { b.vy = Math.abs(b.vy); b.y = py; }
        else if (px + R <= l) { b.vx = -Math.abs(b.vx); b.x = px; }
        else if (px - R >= rt) { b.vx = Math.abs(b.vx); b.x = px; }
        else { b.vy = -b.vy; b.y = py; }
        hitBrick(br, b.x, b.y);
        break;
      }
    }
    if (b.y - R > H) b.dead = true;
    sx = b.vx * k; sy = b.vy * k;
  }
}

function applyPower(type) {
  if (type === 'W') {
    wideT = 10;
  } else {
    const moving = balls.filter(b => !b.stuck);
    const src = moving[0];
    let guard = 0;
    while (balls.length < 3 && guard++ < 5) {
      if (src) {
        const sp = Math.hypot(src.vx, src.vy);
        const a = Math.atan2(src.vx, -src.vy) + (guard % 2 ? 0.45 : -0.45);
        balls.push({ x: src.x, y: src.y, vx: sp * Math.sin(a), vy: -sp * Math.cos(a), r: R, stuck: false, t: 0, dead: false });
      } else {
        const b = stuckBall(); b.stuck = false; b.vx = SPEED * 0.3; b.vy = -SPEED * 0.95;
        balls.push(b);
      }
    }
  }
  burst(paddle.cx, paddle.y, POWER_COL[type], 26, 240);
}

function loseLife() {
  lives--; caps = []; wideT = 0;
  if (lives <= 0) {
    state = 'over'; overT = 2; updateOverlay();
  } else {
    balls = [stuckBall()];
  }
}

function update(dt) {
  for (const p of parts) { p.life -= dt; p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 420 * dt; }
  parts = parts.filter(p => p.life > 0);
  for (const br of bricks) if (br.flash > 0) br.flash -= dt;
  flashT = Math.max(0, flashT - dt);

  if (state === 'over' || state === 'win') {
    if (demo) { overT -= dt; if (overT <= 0) newGame(true); }
    return;
  }
  if (state !== 'play') return;

  // paddle control
  if (demo) {
    const ref = balls.filter(b => !b.stuck).sort((a, b) => b.y - a.y)[0]
              || balls[0];
    const diff = ref.x - paddle.tx;
    paddle.tx += clamp(diff, -480 * dt, 480 * dt);
    for (const b of balls) if (b.stuck) { b.t += dt; if (b.t > 0.8) launch(); }
  } else {
    if (keyL) paddle.tx -= 620 * dt;
    if (keyR) paddle.tx += 620 * dt;
  }
  paddle.tx = clamp(paddle.tx, 0, W);
  wideT = Math.max(0, wideT - dt);
  paddle.w = wideT > 0 ? BASE_W * 1.6 : BASE_W;
  paddle.cx = clamp(paddle.tx, paddle.w / 2, W - paddle.w / 2);

  // capsules
  for (const c of caps) c.y += c.vy * dt;
  caps = caps.filter(c => {
    if (c.y > H + 20) return false;
    if (c.y + 10 >= paddle.y && c.y - 10 <= paddle.y + paddle.h &&
        c.x >= paddle.cx - paddle.w / 2 - 10 && c.x <= paddle.cx + paddle.w / 2 + 10) {
      applyPower(c.type); return false;
    }
    return true;
  });

  // balls
  for (const b of balls) {
    if (b.stuck) { b.x = paddle.cx; b.y = paddle.y - R; }
    else moveBall(b, dt);
  }
  balls = balls.filter(b => !b.dead);
  if (balls.length === 0) { loseLife(); return; }

  if (aliveCount() === 0) { state = 'win'; overT = 3; updateOverlay(); }
}

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

function drawBrick(br) {
  if (!br.alive) return;
  const { x, y, w, h } = br;
  ctx.save();
  const g = ctx.createLinearGradient(x, y, x, y + h);
  if (br.tough) {
    g.addColorStop(0, '#c9d3e6'); g.addColorStop(0.5, '#7d8aa6'); g.addColorStop(1, '#3d4762');
  } else {
    g.addColorStop(0, lighten(br.color, 60)); g.addColorStop(0.5, br.color); g.addColorStop(1, darken(br.color, 45));
  }
  roundRect(x, y, w, h, 4);
  ctx.fillStyle = g; ctx.fill();
  ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(0,0,0,.45)'; ctx.stroke();
  if (br.flash > 0) { ctx.fillStyle = 'rgba(255,255,255,.55)'; ctx.fill(); }
  // gloss
  ctx.fillStyle = 'rgba(255,255,255,.18)';
  roundRect(x + 3, y + 2, w - 6, 5, 2.5); ctx.fill();
  if (br.tough && br.hp === 2) {
    // metallic rivets to signal toughness
    ctx.fillStyle = 'rgba(0,0,0,.35)';
    ctx.beginPath(); ctx.arc(x + 6, y + h / 2, 1.8, 0, 7); ctx.arc(x + w - 6, y + h / 2, 1.8, 0, 7); ctx.fill();
  }
  if (br.cracked) {
    // seeded crack pattern, drawn after first hit
    let s = br.crackSeed * 1000;
    const rnd = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
    ctx.strokeStyle = 'rgba(15,15,30,.9)'; ctx.lineWidth = 1.6;
    ctx.beginPath();
    let cx = x + w * (0.3 + rnd() * 0.4), cy = y + h / 2;
    ctx.moveTo(cx, cy);
    for (let i = 0; i < 6; i++) {
      cx += (rnd() - 0.5) * 16; cy = clamp(y + rnd() * h, y + 1, y + h - 1);
      ctx.lineTo(cx, cy);
      if (i % 2) { ctx.moveTo(cx, cy); ctx.lineTo(cx + (rnd() - 0.5) * 14, cy + (rnd() - 0.5) * 10); ctx.moveTo(cx, cy); }
    }
    ctx.stroke();
  }
  if (br.power) {
    const cx = x + w / 2, cy = y + h / 2;
    ctx.beginPath(); ctx.arc(cx, cy, 8.5, 0, Math.PI * 2);
    ctx.fillStyle = '#0b0d22'; ctx.fill();
    ctx.lineWidth = 1.6; ctx.strokeStyle = POWER_COL[br.power]; ctx.stroke();
    ctx.fillStyle = POWER_COL[br.power];
    ctx.font = 'bold 11px system-ui,sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(br.power, cx, cy + 0.5);
  }
  ctx.restore();
}

function lighten(hex, a) { return shade(hex, a); }
function darken(hex, a) { return shade(hex, -a); }
function shade(hex, amt) {
  const n = parseInt(hex.slice(1), 16);
  const f = v => clamp(v + amt, 0, 255);
  return `rgb(${f(n >> 16 & 255)},${f(n >> 8 & 255)},${f(n & 255)})`;
}

function render() {
  ctx.clearRect(0, 0, W, H);
  // background
  const bg = ctx.createLinearGradient(0, 0, 0, H);
  bg.addColorStop(0, '#11163f'); bg.addColorStop(1, '#070a1f');
  ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
  ctx.strokeStyle = 'rgba(120,140,255,.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(); }

  // HUD
  ctx.textBaseline = 'middle';
  ctx.font = 'bold 20px system-ui,sans-serif'; ctx.textAlign = 'left';
  ctx.fillStyle = '#ffd54a'; ctx.fillText('SCORE ' + String(score).padStart(5, '0'), 16, 30);
  ctx.textAlign = 'right'; ctx.fillStyle = '#eef';
  ctx.fillText('LIVES', W - 130, 30);
  for (let i = 0; i < 3; i++) {
    ctx.beginPath(); ctx.arc(W - 100 + i * 26, 30, 8, 0, Math.PI * 2);
    ctx.fillStyle = i < lives ? '#ff5fa2' : 'rgba(255,255,255,.15)'; ctx.fill();
  }
  ctx.textAlign = 'center'; ctx.font = 'bold 13px system-ui,sans-serif';
  const multi = balls.length > 1;
  let eff = [];
  if (wideT > 0) eff.push(`WIDE ${wideT.toFixed(0)}s`);
  if (multi) eff.push(`BALLS ×${balls.length}`);
  if (eff.length) { ctx.fillStyle = '#5fd4ff'; ctx.fillText(eff.join('   '), W / 2, 30); }
  ctx.strokeStyle = 'rgba(255,255,255,.12)';
  ctx.beginPath(); ctx.moveTo(0, 50); ctx.lineTo(W, 50); ctx.stroke();

  for (const br of bricks) drawBrick(br);

  // capsules
  for (const c of caps) {
    ctx.save();
    ctx.shadowColor = POWER_COL[c.type]; ctx.shadowBlur = 14;
    ctx.beginPath(); ctx.arc(c.x, c.y, 11, 0, Math.PI * 2);
    ctx.fillStyle = POWER_COL[c.type]; ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#0b0d22'; ctx.font = 'bold 13px system-ui,sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(c.type, c.x, c.y + 0.5);
    ctx.restore();
  }

  // paddle
  ctx.save();
  const px = paddle.cx - paddle.w / 2;
  ctx.shadowColor = wideT > 0 ? '#5fd4ff' : '#8a7bff'; ctx.shadowBlur = 18;
  const pg = ctx.createLinearGradient(0, paddle.y, 0, paddle.y + paddle.h);
  pg.addColorStop(0, '#e8f0ff'); pg.addColorStop(1, '#6f7fd8');
  roundRect(px, paddle.y, paddle.w, paddle.h, 7);
  ctx.fillStyle = pg; ctx.fill();
  ctx.restore();

  // balls
  for (const b of balls) {
    ctx.save();
    ctx.shadowColor = '#fff'; ctx.shadowBlur = 12;
    const g = ctx.createRadialGradient(b.x - 2, b.y - 2, 1, b.x, b.y, R);
    g.addColorStop(0, '#ffffff'); g.addColorStop(1, '#ffd54a');
    ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, Math.PI * 2);
    ctx.fillStyle = g; ctx.fill();
    ctx.restore();
  }

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

  if (demo && state === 'play') {
    ctx.textAlign = 'left'; ctx.font = 'bold 12px system-ui,sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,.45)';
    ctx.fillText('DEMO · AI PLAYING', 16, H - 14);
  }
}

function shadeLerp() {}

function updateOverlay() {
  if (state === 'play' && !demo) { ov.classList.add('hide'); return; }
  ov.classList.remove('hide');
  if (state === 'play' && demo) {
    ovTitle.textContent = 'BREAKOUT';
    ovMsg.textContent = 'Click or press a key to start';
    ovSub.textContent = 'Mouse or ← → to move · Space to launch';
    ovBtn.classList.add('hide');
  } else if (state === 'over') {
    ovTitle.textContent = 'GAME OVER';
    ovMsg.textContent = 'Final score: ' + score;
    ovSub.textContent = demo ? '' : 'Press Enter or click Restart';
    ovBtn.textContent = 'Restart';
    ovBtn.classList.toggle('hide', demo);
  } else if (state === 'win') {
    ovTitle.textContent = 'VICTORY!';
    ovMsg.textContent = 'All bricks cleared · Score: ' + score;
    ovSub.textContent = demo ? '' : 'Press Enter or click to play again';
    ovBtn.textContent = 'Play again';
    ovBtn.classList.toggle('hide', demo);
  }
}

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

function onAction() {
  if (demo || state === 'over' || state === 'win') newGame(false);
  else launch();
}

canvas.addEventListener('mousemove', e => {
  if (demo || state !== 'play') return;
  paddle.tx = toCanvasX(e);
});
wrap.addEventListener('click', onAction);
ovBtn.addEventListener('click', e => { e.stopPropagation(); newGame(false); });

canvas.addEventListener('touchmove', e => {
  if (demo || state !== 'play') return;
  e.preventDefault();
  const t = e.touches[0];
  const r = canvas.getBoundingClientRect();
  paddle.tx = (t.clientX - r.left) * W / r.width;
}, { passive: false });
canvas.addEventListener('touchstart', e => {
  e.preventDefault();
  if (!demo && state === 'play') {
    const t = e.touches[0];
    const r = canvas.getBoundingClientRect();
    paddle.tx = (t.clientX - r.left) * W / r.width;
  }
  onAction();
}, { passive: false });

window.addEventListener('keydown', e => {
  const k = e.key;
  if (k === 'ArrowLeft' || k === 'a' || k === 'A') { keyL = true; e.preventDefault(); }
  if (k === 'ArrowRight' || k === 'd' || k === 'D') { keyR = true; e.preventDefault(); }
  if (k === ' ' || k === 'ArrowUp' || k === 'Enter') { e.preventDefault(); if (!e.repeat) onAction(); }
  else if (!e.repeat && !['ArrowLeft','ArrowRight','a','A','d','D'].includes(k)) onAction();
  else if (!e.repeat && (k === 'ArrowLeft' || k === 'ArrowRight' || k === 'a' || k === 'd' || k === 'A' || k === 'D')) onAction();
});
window.addEventListener('keyup', e => {
  const k = e.key;
  if (k === 'ArrowLeft' || k === 'a' || k === 'A') keyL = false;
  if (k === 'ArrowRight' || k === 'd' || k === 'D') keyR = false;
});

// ---------- loop ----------
let last = performance.now();
function frame(now) {
  const dt = Math.min((now - last) / 1000, 0.03);
  last = now;
  update(dt);
  render();
  requestAnimationFrame(frame);
}

newGame(true);
requestAnimationFrame(t => { last = t; frame(t); });
})();
</script>
</body>
</html>
Reasoning trace (9,812 tokens)
I'm working out the full set of requirements for a single HTML file: a paddle controlled by mouse and arrow keys, angled bounces, a level of at least six rows with tougher two-hit bricks that crack after the first hit, and power-ups for a wider paddle and multi-ball. I also need swept collision so the ball can't tunnel through bricks, plus particles, three lives, scoring, victory and game-over screens with restart, and an AI demo mode that starts from a click or key press.

I'm planning the collision handling now: sub-stepping the ball so it can't tunnel through bricks, reflecting off whichever side of a brick it penetrates least, and angling paddle bounces based on where the ball hits.

Hitting bricks from the side versus top or bottom comes down to where the ball was on the previous step, so I'll check that rather than comparing offsets. Bricks get two hits, the first one cracking them. I also need power-up bricks that drop a falling capsule, the wide paddle and multi-ball effects, and the ball-loss handling. Last, the demo AI runs behind a start message and hands control to the player on click or keypress.

I'm laying out the layout and the rest of the game: a fixed 800x600 canvas that scales down to fit, a HUD with score and lives, and overlays for the title, victory, and game-over screens. I'm also settling the controls and flow: the paddle follows the mouse or arrow keys, the ball waits on the paddle until a click, space, or up arrow, and demo mode has the AI launch it after a short delay and hand over to the player on a click or key press.

I'm settling the brick grid next: ten columns with two rows of tough bricks, a fixed ball speed, and three lives to start.

I'm sketching the paddle and ball state, then the update loop with a clamped timestep. In demo mode the paddle follows the lowest moving ball, while in play it follows the mouse or the arrow keys through a shared target position.

I'm setting up the ball state and launch angle, then stepping the ball in small increments so it can't pass through bricks. Each step checks the walls, paddle, and bricks, and a brick hit reflects the ball based on whether it came from above or below versus the side.

I'm settling how the ball bounces off a brick: if it came in vertically it reverses its y-direction and stays at its previous y, otherwise it reverses x and stays at its previous x, so it can't get stuck inside the brick. Each substep handles at most one brick, and a hit cracks a tough brick or destroys it, awarding points, particles, and possibly a power-up capsule, with a win once none are left. Then I'm checking the paddle hit, which only counts when the ball is moving down and crosses the paddle's top edge within its horizontal span.

Still writing the power-up logic...

I'm adding the remaining balls until there are three in play, and deciding a lost life resets the ball to the paddle and clears the falling capsules. For the demo, I'm planning to restart it two seconds after it ends, with unlimited lives.

Still working on the rendering: a gradient background, rounded bricks with highlights and crack lines on the tough ones, and a letter icon on the power-up bricks. Particles go on top, and the HUD shows score, lives, and any active effect timer.

I'm laying out the overlay for the title and game-over states: a centered translucent panel with the title and message, which I'll keep light so the bricks stay visible behind it. Clicks during play will be handled separately from the start and restart actions.

Still writing the game code, keeping it concise. Next I'm laying out the brick grid: six rows and ten columns, with the top two rows as tough bricks and the rest as regular ones in varying colors. A few bricks get fixed power-up markers, W for wide paddle and M for multi-ball.

Still writing the game code...

Still writing the page styles: a dark canvas wrapper that scales to fit, and a translucent overlay panel with a gradient title.

Still writing the overlay styles and the click handling: any click or key on the wrapper will start the game from the title or demo state, launch the ball during play, or restart after a game over.

Still writing the game code, setting up the state reset and the demo flag, with the overlay driven by the current mode.

Still writing the game loop, with the update step handling particles and the power-up timers first, and skipping the ball and brick logic unless the game is actively in play.

Still writing the game loop: particles, power-up timers, and paddle control first, with the AI tracking the lowest ball in demo mode and auto-launching after a short wait. Next I'll sort out the paddle width and capsule movement, then move each ball and clean up dead ones, losing a life if none are left or winning once the bricks are gone.

Still writing the movement code: I'm moving the ball in small substeps so it can't skip past a brick, bouncing off the side walls and top, then checking the paddle and bricks at each step.

Still writing the collision code: the ball bounces off the paddle only when it's moving down and crosses the paddle's top edge within its horizontal span. The bounce angle depends on where it hits the paddle, which I'm computing as a value clamped between -1 and 1.

Still writing the collision code: once the ball has been moved, I'm checking each brick, and if it overlaps one, the bounce direction depends on whether it came in from above or below.

Still writing the brick-hit logic: a hit reduces the brick's hit points, and a tough brick that survives gets marked cracked with a small bonus.

Still working on the brick setup: tough rows get two hit points and a metallic border, while the rest take a color per row.

Still writing the game code, setting the brick layout: tough bricks in the top rows get marked by column, and a few fixed spots carry the wide-paddle and multi-ball markers.

Still writing the overlay and mouse handling for the paddle, with the demo ignoring mouse movement and any click or key starting the game.

Still writing the input handling: arrow keys set the paddle flags, and any key or click starts the game from the title or demo, relaunches the stuck ball during play, or restarts after a game over or victory.

I'm setting up the demo's auto-launch so a stuck ball goes after a short wait, and planning for losing a life to reset the ball and clear the capsules. When the last life is gone, the game ends and the demo restarts after a short pause, which the win state will also do.

Still writing the game loop: I'm setting the canvas to scale for high-DPI screens, timing each frame with a clamped delta, and having the paddle AI follow the lowest moving ball while the arrow keys nudge the center left or right.

Still writing the game code, defining the paddle, ball, and brick state along with the 10-by-6 brick grid setup.

Still writing the brick grid setup: I'm generating the 10-by-6 layout with tough bricks in the top two rows and a fixed set of power-up markers.

Still writing the game code, setting the brick styling: tough bricks get a darker gradient with bevel stripes, and normal bricks get a simple gradient in their row color.

Still writing the game code: drawing the paddle as a rounded gradient bar that glows cyan when it's widened, the balls as white radial gradients, and the capsules as small colored circles with their letter.

Still writing the input handling: the keydown handler will treat Enter and Space as actions too, and arrow keys will set the paddle flags, preventing scroll.

Still writing the ball movement: I'm stepping the ball in small increments so it can't skip past a brick, with each step's position saved before it moves.

Still writing the ball movement: I'm stepping through the substeps, bouncing off the side walls and top, and then checking for a paddle hit where the bounce angle depends on the contact point.

Still writing the ball physics: when the ball hits the paddle, I'm reflecting its velocity based on the contact point and recomputing the per-substep movement.

Still writing the ball movement code: I'm working out how the ball bounces off bricks, flipping its vertical direction if it came in from above or below and its horizontal direction if it came in from the side.

Still wrapping up the ball movement: after a bounce I'm resetting the velocity on the axis that was hit, and if no balls are left the life is lost, with any extra balls launched at a random angle.