hypeorcode
Network offOpen alone (new tab)

Run details

Model
claude-opus-5-5
Route
Claude Code CLI
Reasoning
provider default
Limits
60 min · 128,000 output tokens
Date
21 Oct 2026
Time
4 min 39 s
Size
31 kB
Tokens
6 in, 31,455 out
List price
$0.63 ($4 in / $20 out per M)
Paid
Not billed per run (CLI plan)

Raw files: output · meta.json

Exact prompt

Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice.

It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Save it as index.html in the current directory, then stop. Do not ask questions.

Source

31 kB 718 lines Raw

<!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:#05050c;overflow:hidden;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
  canvas{display:block;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);cursor:none;border-radius:10px;
    box-shadow:0 0 80px rgba(80,120,255,.25),0 0 0 1px rgba(120,150,255,.15)}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
'use strict';
const W = 800, H = 600, PLAY_TOP = 50;
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
let K = 1;
function resize() {
  const dpr = window.devicePixelRatio || 1;
  const s = Math.min(innerWidth / W, innerHeight / H) * 0.97;
  cv.style.width = (W * s) + 'px'; cv.style.height = (H * s) + 'px';
  cv.width = Math.round(W * s * dpr); cv.height = Math.round(H * s * dpr);
  K = s * dpr;
}
addEventListener('resize', resize); resize();

// ---------- helpers ----------
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const rand = (a, b) => a + Math.random() * (b - a);
const easeOutBack = t => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); };
function rr(x, y, w, h, r) {
  r = Math.min(r, w / 2, h / 2);
  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();
}

// ---------- audio (synthesized, no assets) ----------
let AC = null, muted = false, noiseBuf = null;
function audio() {
  if (!AC) {
    try {
      AC = new (window.AudioContext || window.webkitAudioContext)();
      noiseBuf = AC.createBuffer(1, AC.sampleRate * 0.3, AC.sampleRate);
      const d = noiseBuf.getChannelData(0);
      for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
    } catch (e) { AC = null; }
  }
  if (AC && AC.state === 'suspended') AC.resume();
}
function tone(freq, dur = 0.08, type = 'square', vol = 0.06, slide = 0, delay = 0) {
  if (!AC || muted) return;
  const t = AC.currentTime + delay;
  const o = AC.createOscillator(), g = AC.createGain();
  o.type = type; o.frequency.setValueAtTime(freq, t);
  if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq * slide), t + dur);
  g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
  o.connect(g); g.connect(AC.destination); o.start(t); o.stop(t + dur + 0.02);
}
function noise(dur = 0.08, vol = 0.05, freq = 2000) {
  if (!AC || muted) return;
  const t = AC.currentTime, s = AC.createBufferSource(), f = AC.createBiquadFilter(), g = AC.createGain();
  s.buffer = noiseBuf; f.type = 'lowpass'; f.frequency.value = freq;
  g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
  s.connect(f); f.connect(g); g.connect(AC.destination); s.start(t); s.stop(t + dur);
}
const sfx = {
  paddle() { tone(220, 0.1, 'square', 0.05, 1.8); },
  wall() { tone(520, 0.04, 'square', 0.025); },
  crack() { tone(760, 0.06, 'triangle', 0.06, 0.8); noise(0.04, 0.03, 3000); },
  metal() { tone(1400, 0.09, 'square', 0.025, 0.6); tone(2100, 0.05, 'sine', 0.03); },
  brk(c) { const f = 330 * Math.pow(2, Math.min(c, 24) / 12); tone(f, 0.13, 'triangle', 0.08, 1.5); noise(0.07, 0.05, 2500); },
  launch() { tone(300, 0.15, 'sine', 0.08, 2.5); },
  lose() { tone(420, 0.7, 'sawtooth', 0.07, 0.12); noise(0.4, 0.08, 600); },
  power() { [0, 4, 7, 12].forEach((s, i) => tone(523 * Math.pow(2, s / 12), 0.1, 'square', 0.04, 0, i * 0.05)); },
  clear() { [0, 4, 7, 12, 16, 19, 24].forEach((s, i) => tone(392 * Math.pow(2, s / 12), 0.16, 'triangle', 0.07, 0, i * 0.07)); },
  over() { [12, 7, 3, 0, -5].forEach((s, i) => tone(330 * Math.pow(2, s / 12), 0.3, 'triangle', 0.07, 0, i * 0.18)); },
};

// ---------- levels ----------
// '.' empty, '1'-'3' hit points, '#' unbreakable
const LEVELS = [
  { name: 'Warm Up', map: [
    '............',
    '111111111111',
    '111111111111',
    '111111111111',
    '111111111111',
    '111111111111']},
  { name: 'Pyramid', map: [
    '.....22.....',
    '....2112....',
    '...211112...',
    '..21111112..',
    '.2111111112.',
    '211111111112',
    '222222222222']},
  { name: 'Checkers', map: [
    '2.2.2.2.2.2.',
    '.2.2.2.2.2.2',
    '3.1.3.1.3.1.',
    '.1.3.1.3.1.3',
    '1.1.1.1.1.1.',
    '............',
    '##.##..##.##']},
  { name: 'Invader', map: [
    '..1......1..',
    '...1....1...',
    '..22222222..',
    '.22.2222.22.',
    '222222222222',
    '2.22222222.2',
    '2.2......2.2',
    '...33..33...']},
  { name: 'Fortress', map: [
    '333333333333',
    '#2222222222#',
    '#2111111112#',
    '#2133333312#',
    '#2111111112#',
    '#2222222222#',
    '####....####']},
  { name: 'Diamond Rush', map: [
    '.....33.....',
    '....3223....',
    '...321123...',
    '..32111123..',
    '.3211111123.',
    '..32111123..',
    '...321123...',
    '....3223....',
    '#....33....#']},
];
const PALETTE = ['#ff4d6d', '#ff8c42', '#ffd23f', '#3ddc97', '#3fa7ff', '#9b5de5', '#f15bb5', '#00e5c4', '#fee440'];
const COLS = 12, GAP = 4, BX0 = 20, BY0 = 92, BH = 22;
const BW = (W - BX0 * 2 - GAP * (COLS - 1)) / COLS;

const POWERS = {
  W: { name: 'WIDE', col: '#4ee6ff', ch: '↔', w: 3 },
  M: { name: 'MULTIBALL', col: '#ffd23f', ch: '●', w: 3 },
  S: { name: 'SLOW', col: '#3ddc97', ch: '◷', w: 2 },
  F: { name: 'FIREBALL', col: '#ff6a2a', ch: '✸', w: 2 },
  L: { name: '+1 LIFE', col: '#ff4d9d', ch: '♥', w: 1 },
};
const POWER_BAG = Object.keys(POWERS).flatMap(k => Array(POWERS[k].w).fill(k));

// ---------- state ----------
let state = 'title', paused = false, time = 0;
let score = 0, shownScore = 0, lives = 3, level = 0, combo = 0, comboPulse = 0;
let best = 0; try { best = +localStorage.getItem('breakout-best') || 0; } catch (e) {}
let shake = 0, hitstop = 0, flashA = 0, flashCol = '#fff', stateT = 0, levelT = 0;
let banner = { t: 99, title: '', sub: '' };
let bricks = [], balls = [], parts = [], pops = [], caps = [];
let wideT = 0, slowT = 0, fireT = 0;
const paddle = { x: W / 2, y: H - 38, w: 110, tw: 110, h: 14, vx: 0, sq: 0 };
const MAXSP = 820;

const stars = Array.from({ length: 90 }, () => ({ x: Math.random() * W, y: Math.random() * H, z: Math.random() }));
const bg = document.createElement('canvas'); bg.width = W; bg.height = H;
{
  const b = bg.getContext('2d');
  const g = b.createLinearGradient(0, 0, 0, H); g.addColorStop(0, '#0d0b24'); g.addColorStop(1, '#060a18');
  b.fillStyle = g; b.fillRect(0, 0, W, H);
  b.strokeStyle = 'rgba(90,120,255,0.05)'; b.lineWidth = 1;
  for (let x = 0; x <= W; x += 40) { b.beginPath(); b.moveTo(x + .5, 0); b.lineTo(x + .5, H); b.stroke(); }
  for (let y = 0; y <= H; y += 40) { b.beginPath(); b.moveTo(0, y + .5); b.lineTo(W, y + .5); b.stroke(); }
  const v = b.createRadialGradient(W / 2, H / 2, H * 0.3, W / 2, H / 2, H * 0.85);
  v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(0,0,0,0.55)');
  b.fillStyle = v; b.fillRect(0, 0, W, H);
}

function loadLevel(n) {
  const L = LEVELS[n];
  bricks = [];
  L.map.forEach((row, r) => [...row].forEach((ch, c) => {
    if (ch === '.') return;
    const unb = ch === '#', hp = unb ? 1 : +ch;
    bricks.push({ x: BX0 + c * (BW + GAP), y: BY0 + r * (BH + GAP), w: BW, h: BH, hp, max: hp, unb,
      col: unb ? '#8a90a6' : PALETTE[r % PALETTE.length], flash: 0, wob: 0, delay: r * 0.05 + c * 0.018, alive: true });
  }));
  caps = []; wideT = slowT = fireT = 0; levelT = 0;
  resetBalls();
  banner = { t: 0, title: 'LEVEL ' + (n + 1), sub: L.name };
}
function newBall() { return { x: paddle.x, y: paddle.y - paddle.h / 2 - 7, vx: 0, vy: 0, r: 7, sp: 0, stuck: true, off: rand(-15, 15), trail: [] }; }
function resetBalls() { balls = [newBall()]; combo = 0; }
const baseSpeed = () => 390 + level * 30;

function startGame() {
  score = 0; shownScore = 0; lives = 3; level = 0; paused = false;
  parts = []; pops = [];
  loadLevel(0); state = 'play';
}

function launch() {
  let any = false;
  for (const b of balls) if (b.stuck) {
    const a = clamp(b.off / (paddle.w / 2) * 0.8 + paddle.vx * 0.0003 + rand(-0.12, 0.12), -0.9, 0.9);
    b.sp = baseSpeed(); b.vx = Math.sin(a) * b.sp; b.vy = -Math.cos(a) * b.sp; b.stuck = false; any = true;
  }
  if (any) { sfx.launch(); paddle.sq = 0.6; burst(paddle.x, paddle.y - 10, 10, '#9ef', 160); }
}

// ---------- effects ----------
function burst(x, y, n, col, sp = 220, opt = {}) {
  for (let i = 0; i < n && parts.length < 900; i++) {
    const a = opt.ang !== undefined ? opt.ang + rand(-opt.spread, opt.spread) : rand(0, Math.PI * 2);
    const v = rand(sp * 0.3, sp);
    parts.push({ x, y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: rand(0.4, 0.9) * (opt.life || 1), max: 0,
      col: Array.isArray(col) ? col[(Math.random() * col.length) | 0] : col, s: rand(2, opt.size || 5),
      rot: rand(0, 6), vr: rand(-12, 12), g: opt.g ?? 500 });
    parts[parts.length - 1].max = parts[parts.length - 1].life;
  }
}
function popup(x, y, txt, col, size = 18) { pops.push({ x, y, txt, col, t: 0, size }); }
function addShake(v) { shake = Math.min(18, shake + v); }

// ---------- game events ----------
function hitBrick(br, b) {
  br.flash = 1; br.wob = 1;
  if (br.unb) { sfx.metal(); burst(b.x, b.y, 5, '#dde', 180, { size: 3 }); addShake(1.5); return; }
  br.hp--;
  if (br.hp <= 0) destroyBrick(br);
  else { sfx.crack(); score += 5; burst(b.x, b.y, 6, br.col, 180, { size: 3 }); addShake(2); }
}
function destroyBrick(br) {
  br.alive = false; combo++; comboPulse = 1;
  const mult = Math.min(8, 1 + Math.floor((combo - 1) / 3));
  const pts = 10 * br.max * mult;
  score += pts;
  const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
  popup(cx, cy, '+' + pts + (mult > 1 ? '  x' + mult : ''), mult > 1 ? '#ffe066' : '#fff', mult > 1 ? 20 : 16);
  burst(cx, cy, 16, [br.col, br.col, '#fff'], 280, { size: 7 });
  sfx.brk(combo);
  addShake(3 + Math.min(combo, 10) * 0.3);
  if (combo % 5 === 0) { hitstop = 0.05; flashA = 0.12; flashCol = '#fff'; }
  if (Math.random() < 0.14) {
    const k = POWER_BAG[(Math.random() * POWER_BAG.length) | 0];
    caps.push({ x: cx, y: cy, k, vy: 140, t: 0 });
  }
  if (!bricks.some(b => b.alive && !b.unb)) levelClear();
}
function levelClear() {
  state = 'clear'; stateT = 0;
  const bonus = 1000 + level * 500 + lives * 100;
  score += bonus;
  banner = { t: 0, title: 'LEVEL CLEAR!', sub: 'Bonus +' + bonus };
  sfx.clear(); flashA = 0.5; flashCol = '#bff'; addShake(10);
  for (const b of balls) burst(b.x, b.y, 30, ['#fff', '#9ef', '#ffd23f'], 400);
  balls = []; caps = [];
  for (let i = 0; i < 6; i++) burst(rand(100, W - 100), rand(120, 300), 25, PALETTE, 350, { size: 6 });
}
function nextLevel() {
  level++;
  if (level >= LEVELS.length) { state = 'win'; stateT = 0; saveBest(); sfx.clear(); }
  else { loadLevel(level); state = 'play'; }
}
function loseLife() {
  lives--; addShake(16); flashA = 0.45; flashCol = '#ff2850'; sfx.lose();
  burst(paddle.x, paddle.y, 40, ['#ff4d6d', '#ff8c42', '#fff'], 380);
  wideT = slowT = fireT = 0; caps = [];
  if (lives <= 0) { state = 'over'; stateT = 0; saveBest(); setTimeout(sfx.over, 400); balls = []; }
  else { resetBalls(); popup(paddle.x, paddle.y - 30, lives === 1 ? 'LAST LIFE!' : 'OUCH!', '#ff6680', 22); }
}
function saveBest() {
  if (score > best) { best = score; try { localStorage.setItem('breakout-best', best); } catch (e) {} }
}
function applyPower(k) {
  const P = POWERS[k];
  sfx.power(); flashA = 0.15; flashCol = P.col;
  popup(paddle.x, paddle.y - 34, P.name, P.col, 22);
  burst(paddle.x, paddle.y, 24, [P.col, '#fff'], 300, { ang: -Math.PI / 2, spread: 1.2 });
  score += 50;
  if (k === 'W') wideT = 14;
  else if (k === 'S') slowT = 10;
  else if (k === 'F') fireT = 8;
  else if (k === 'L') lives = Math.min(7, lives + 1);
  else if (k === 'M') {
    if (balls.some(b => b.stuck)) launch();
    const add = [];
    for (const b of balls.slice(0, 4)) for (const d of [-0.45, 0.45]) {
      const a = Math.atan2(b.vy, b.vx) + d;
      const nb = { ...b, trail: [], vx: Math.cos(a) * b.sp, vy: Math.sin(a) * b.sp };
      fixAngle(nb); add.push(nb);
    }
    balls.push(...add);
  }
}

// ---------- physics ----------
function fixAngle(b) {
  const min = b.sp * 0.3;
  if (Math.abs(b.vy) < min) {
    b.vy = (b.vy < 0 ? -1 : 1) * min;
    b.vx = (b.vx < 0 ? -1 : 1) * Math.sqrt(b.sp * b.sp - min * min);
  }
}
function moveBall(b, dt) {
  if (b.stuck) {
    b.off = clamp(b.off, -paddle.w / 2 + 8, paddle.w / 2 - 8);
    b.x = paddle.x + b.off; b.y = paddle.y - paddle.h / 2 * (1 - 0.4 * paddle.sq) - b.r; return;
  }
  const dist = b.sp * dt, steps = Math.max(1, Math.ceil(dist / 3)), sdt = dt / steps;
  for (let i = 0; i < steps; i++) {
    if (state !== 'play') return;
    b.x += b.vx * sdt; b.y += b.vy * sdt;
    // walls
    if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx); wallFx(b, 0); }
    else if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx); wallFx(b, Math.PI); }
    if (b.y < PLAY_TOP + b.r) { b.y = PLAY_TOP + b.r; b.vy = Math.abs(b.vy); wallFx(b, Math.PI / 2); }
    // paddle
    const pw = paddle.w, ph = paddle.h, px = paddle.x - pw / 2, py = paddle.y - ph / 2;
    if (b.vy > 0 && b.y < paddle.y) {
      const cx = clamp(b.x, px, px + pw), cy = clamp(b.y, py, py + ph);
      if ((b.x - cx) ** 2 + (b.y - cy) ** 2 <= b.r * b.r) {
        const rel = clamp((b.x - paddle.x) / (pw / 2), -1, 1);
        const a = rel * 1.05;
        b.sp = Math.min(MAXSP, b.sp + 6);
        b.vx = Math.sin(a) * b.sp; b.vy = -Math.cos(a) * b.sp;
        b.y = py - b.r;
        paddle.sq = 1; combo = 0; sfx.paddle();
        burst(b.x, py, 8, '#9ef', 200, { ang: -Math.PI / 2, spread: 1.1, size: 3 });
        addShake(1.5);
      }
    }
    // bricks
    for (const br of bricks) {
      if (!br.alive) continue;
      const cx = clamp(b.x, br.x, br.x + br.w), cy = clamp(b.y, br.y, br.y + br.h);
      let nx = b.x - cx, ny = b.y - cy;
      const d2 = nx * nx + ny * ny;
      if (d2 > b.r * b.r) continue;
      if (fireT > 0 && !br.unb) {
        br.hp = 0; destroyBrick(br);
        if (state !== 'play') return;
        continue;
      }
      if (d2 === 0) { nx = -b.vx; ny = -b.vy; }
      const d = Math.hypot(nx, ny) || 1;
      if (Math.abs(nx) > Math.abs(ny)) b.vx = Math.sign(nx) * Math.abs(b.vx);
      else b.vy = Math.sign(ny) * Math.abs(b.vy);
      if (d2 > 0) { b.x = cx + nx / d * (b.r + 0.5); b.y = cy + ny / d * (b.r + 0.5); }
      fixAngle(b);
      hitBrick(br, b);
      break;
    }
    if (b.y - b.r > H) { b.dead = true; return; }
  }
}
function wallFx(b, ang) {
  sfx.wall(); fixAngle(b);
  burst(b.x, b.y, 5, '#8af', 150, { ang, spread: 1.0, size: 3, g: 0 });
}

// ---------- input ----------
const keys = {};
let mouseX = W / 2, useMouse = true;
function action() {
  audio();
  if (state === 'title' || ((state === 'over' || state === 'win') && stateT > 0.8)) { startGame(); return; }
  if (paused) { paused = false; return; }
  if (state === 'play') launch();
}
addEventListener('keydown', e => {
  const k = e.key;
  if ([' ', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(k)) e.preventDefault();
  if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = true;
  if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = true;
  if (k === ' ' || k === 'ArrowUp' || k === 'Enter' || k === 'w' || k === 'W') { if (!e.repeat) action(); }
  if ((k === 'p' || k === 'P' || k === 'Escape') && (state === 'play' || state === 'clear')) { audio(); paused = !paused; }
  if (k === 'm' || k === 'M') { audio(); muted = !muted; }
});
addEventListener('keyup', e => {
  const k = e.key;
  if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = false;
  if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = false;
});
function toGame(clientX) { const r = cv.getBoundingClientRect(); return (clientX - r.left) / r.width * W; }
addEventListener('mousemove', e => { mouseX = toGame(e.clientX); useMouse = true; });
addEventListener('mousedown', e => { mouseX = toGame(e.clientX); useMouse = true; action(); });
addEventListener('touchstart', e => { e.preventDefault(); mouseX = toGame(e.touches[0].clientX); useMouse = true; action(); }, { passive: false });
addEventListener('touchmove', e => { e.preventDefault(); mouseX = toGame(e.touches[0].clientX); useMouse = true; }, { passive: false });
addEventListener('blur', () => { keys.left = keys.right = false; if (state === 'play') paused = true; });

// ---------- update ----------
function update(dt) {
  time += dt;
  shake *= Math.exp(-dt * 9); if (shake < 0.05) shake = 0;
  flashA *= Math.exp(-dt * 6);
  if (paused) return;
  stateT += dt; banner.t += dt;
  shownScore += (score - shownScore) * Math.min(1, dt * 10);
  if (Math.abs(score - shownScore) < 1) shownScore = score;
  comboPulse *= Math.exp(-dt * 6);

  // stars
  for (const s of stars) { s.y += (8 + s.z * 30) * dt; if (s.y > H) { s.y = 0; s.x = Math.random() * W; } }

  // particles & popups (keep going even in hitstop for feel)
  for (const p of parts) { p.vy += p.g * dt; p.x += p.vx * dt; p.y += p.vy * dt; p.vx *= Math.exp(-dt * 1.5); p.rot += p.vr * dt; p.life -= dt; }
  parts = parts.filter(p => p.life > 0);
  for (const p of pops) p.t += dt;
  pops = pops.filter(p => p.t < 1.1);
  for (const br of bricks) { br.flash *= Math.exp(-dt * 12); br.wob *= Math.exp(-dt * 8); }

  if (state === 'win' && Math.random() < dt * 6) burst(rand(80, W - 80), rand(80, 300), 30, PALETTE, 320, { size: 6 });

  if (hitstop > 0) { hitstop -= dt; return; }

  // paddle
  const prev = paddle.x;
  const dir = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
  if (dir) { useMouse = false; paddle.x += dir * 780 * dt; }
  else if (useMouse) paddle.x += (mouseX - paddle.x) * Math.min(1, dt * 28);
  paddle.tw = wideT > 0 ? 175 : 110;
  paddle.w += (paddle.tw - paddle.w) * Math.min(1, dt * 10);
  paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
  paddle.vx = (paddle.x - prev) / Math.max(dt, 1e-4);
  paddle.sq *= Math.exp(-dt * 10);

  if (state === 'clear') { if (stateT > 2.4) nextLevel(); return; }
  if (state !== 'play') return;

  levelT += dt;
  wideT = Math.max(0, wideT - dt); slowT = Math.max(0, slowT - dt); fireT = Math.max(0, fireT - dt);

  const bdt = dt * (slowT > 0 ? 0.6 : 1);
  for (const b of balls) {
    moveBall(b, bdt);
    if (state !== 'play') return;
    b.trail.push({ x: b.x, y: b.y }); if (b.trail.length > 12) b.trail.shift();
    if (fireT > 0 && !b.stuck && Math.random() < 0.8)
      parts.push({ x: b.x + rand(-3, 3), y: b.y + rand(-3, 3), vx: rand(-30, 30), vy: rand(-60, -10), life: 0.35, max: 0.35,
        col: Math.random() < 0.5 ? '#ffb02a' : '#ff4d1a', s: rand(3, 6), rot: 0, vr: 0, g: -100 });
  }
  balls = balls.filter(b => !b.dead);
  if (!balls.length) { loseLife(); return; }

  // capsules
  for (const c of caps) {
    c.t += dt; c.y += c.vy * dt;
    const pw = paddle.w;
    if (c.y + 9 > paddle.y - paddle.h / 2 && c.y - 9 < paddle.y + paddle.h / 2 && Math.abs(c.x - paddle.x) < pw / 2 + 18) {
      c.dead = true; applyPower(c.k);
    } else if (c.y > H + 20) c.dead = true;
  }
  caps = caps.filter(c => !c.dead);
}

// ---------- render ----------
function render() {
  ctx.setTransform(K, 0, 0, K, 0, 0);
  ctx.drawImage(bg, 0, 0);
  for (const s of stars) {
    ctx.globalAlpha = 0.25 + s.z * 0.6 * (0.7 + 0.3 * Math.sin(time * 3 + s.x));
    ctx.fillStyle = '#cfe0ff'; const sz = 1 + s.z * 1.5; ctx.fillRect(s.x, s.y, sz, sz);
  }
  ctx.globalAlpha = 1;

  ctx.save();
  if (shake > 0) ctx.translate(rand(-shake, shake), rand(-shake, shake));

  drawBricks();
  drawCaps();
  drawParticles();
  if (state !== 'title') { drawPaddle(); drawBalls(); }
  drawPops();
  ctx.restore();

  drawHUD();
  if (flashA > 0.01) { ctx.globalAlpha = flashA; ctx.fillStyle = flashCol; ctx.fillRect(0, 0, W, H); ctx.globalAlpha = 1; }
  drawOverlay();
}

function drawBricks() {
  for (const br of bricks) {
    if (!br.alive) continue;
    const t = state === 'title' ? 1 : clamp((levelT - br.delay) / 0.45, 0, 1);
    if (t <= 0) continue;
    const oy = (1 - easeOutBack(t)) * -220;
    const s = 1 + 0.14 * br.wob;
    ctx.save();
    ctx.globalAlpha = t;
    ctx.translate(br.x + br.w / 2, br.y + br.h / 2 + oy + (state === 'title' ? Math.sin(time * 2 + br.x * 0.02) * 3 : 0));
    ctx.scale(s, s);
    const w = br.w, h = br.h;
    if (br.unb) {
      const g = ctx.createLinearGradient(0, -h / 2, 0, h / 2);
      g.addColorStop(0, '#c9cfe0'); g.addColorStop(0.5, '#7d849a'); g.addColorStop(1, '#4b5063');
      ctx.fillStyle = g; rr(-w / 2, -h / 2, w, h, 4); ctx.fill();
      ctx.fillStyle = 'rgba(30,32,45,.7)';
      for (const dx of [-w / 2 + 6, w / 2 - 6]) { ctx.beginPath(); ctx.arc(dx, 0, 2, 0, 7); ctx.fill(); }
    } else {
      ctx.fillStyle = br.col; rr(-w / 2, -h / 2, w, h, 4); ctx.fill();
      ctx.fillStyle = 'rgba(255,255,255,.28)'; rr(-w / 2 + 2, -h / 2 + 2, w - 4, h * 0.32, 3); ctx.fill();
      ctx.fillStyle = 'rgba(0,0,0,.22)'; ctx.fillRect(-w / 2 + 3, h / 2 - 4, w - 6, 2);
      // toughness rings
      ctx.strokeStyle = 'rgba(255,255,255,.6)'; ctx.lineWidth = 1.5;
      for (let i = 1; i < br.max; i++) { rr(-w / 2 + 3 + i * 3, -h / 2 + 3 + i * 2, w - 6 - i * 6, h - 6 - i * 4, 3); ctx.stroke(); }
      // cracks
      if (br.hp < br.max) {
        ctx.strokeStyle = 'rgba(0,0,0,.55)'; ctx.lineWidth = 1.5; ctx.beginPath();
        ctx.moveTo(-w * 0.3, -h / 2); ctx.lineTo(-w * 0.15, -2); ctx.lineTo(-w * 0.25, 4); ctx.lineTo(-w * 0.1, h / 2);
        if (br.max - br.hp > 1) { ctx.moveTo(w * 0.35, -h / 2); ctx.lineTo(w * 0.2, 0); ctx.lineTo(w * 0.3, h / 2); }
        ctx.stroke();
      }
    }
    if (br.flash > 0.02) { ctx.globalAlpha = br.flash * t; ctx.fillStyle = '#fff'; rr(-w / 2, -h / 2, w, h, 4); ctx.fill(); }
    ctx.restore();
  }
}

function drawPaddle() {
  const pw = paddle.w, ph = paddle.h, sx = 1 + 0.2 * paddle.sq, sy = 1 - 0.45 * paddle.sq;
  ctx.save(); ctx.translate(paddle.x, paddle.y); ctx.scale(sx, sy);
  ctx.shadowColor = fireT > 0 ? '#ff7a2a' : wideT > 0 ? '#4ee6ff' : '#4aa3ff'; ctx.shadowBlur = 22;
  const g = ctx.createLinearGradient(0, -ph / 2, 0, ph / 2);
  g.addColorStop(0, '#e6fbff'); g.addColorStop(0.45, fireT > 0 ? '#ffb347' : '#5fd4ff'); g.addColorStop(1, fireT > 0 ? '#c3401a' : '#2a64d8');
  ctx.fillStyle = g; rr(-pw / 2, -ph / 2, pw, ph, ph / 2); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255,255,255,.7)'; rr(-pw / 2 + 8, -ph / 2 + 2, pw - 16, 3, 2); ctx.fill();
  ctx.restore();
}

function drawBalls() {
  const fire = fireT > 0;
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const p = b.trail[i], k = i / b.trail.length;
      ctx.globalAlpha = k * 0.35; ctx.fillStyle = fire ? '#ff8a2a' : '#7fd8ff';
      ctx.beginPath(); ctx.arc(p.x, p.y, b.r * (0.4 + 0.6 * k), 0, 7); ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.save(); ctx.translate(b.x, b.y);
    if (!b.stuck) {
      const st = 1 + Math.min(0.35, b.sp / 2400);
      ctx.rotate(Math.atan2(b.vy, b.vx)); ctx.scale(st, 1 / st);
    }
    ctx.shadowColor = fire ? '#ff6a00' : '#9ef'; ctx.shadowBlur = 18;
    ctx.fillStyle = fire ? '#ffe2a8' : '#ffffff';
    ctx.beginPath(); ctx.arc(0, 0, b.r, 0, 7); ctx.fill();
    ctx.restore();
  }
}

function drawCaps() {
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  for (const c of caps) {
    const P = POWERS[c.k];
    ctx.save(); ctx.translate(c.x, c.y); ctx.rotate(Math.sin(c.t * 6) * 0.15);
    const s = 1 + Math.sin(c.t * 10) * 0.06; ctx.scale(s, s);
    ctx.shadowColor = P.col; ctx.shadowBlur = 16; ctx.fillStyle = P.col;
    rr(-20, -9, 40, 18, 9); ctx.fill(); ctx.shadowBlur = 0;
    ctx.fillStyle = 'rgba(255,255,255,.35)'; rr(-17, -7, 34, 5, 3); ctx.fill();
    ctx.fillStyle = '#10121e'; ctx.font = 'bold 13px system-ui,sans-serif'; ctx.fillText(P.ch, 0, 1);
    ctx.restore();
  }
}

function drawParticles() {
  for (const p of parts) {
    ctx.globalAlpha = clamp(p.life / p.max, 0, 1); ctx.fillStyle = p.col;
    ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillRect(-p.s / 2, -p.s / 2, p.s, p.s); ctx.restore();
  }
  ctx.globalAlpha = 1;
}

function drawPops() {
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  for (const p of pops) {
    const k = p.t / 1.1, s = p.t < 0.12 ? 0.5 + p.t / 0.12 * 0.8 : 1.3 - Math.min(0.3, (p.t - 0.12) * 2);
    ctx.globalAlpha = 1 - k * k;
    ctx.save(); ctx.translate(p.x, p.y - p.t * 50); ctx.scale(s, s);
    ctx.font = `800 ${p.size}px system-ui,sans-serif`;
    ctx.lineWidth = 4; ctx.strokeStyle = 'rgba(0,0,0,.6)'; ctx.strokeText(p.txt, 0, 0);
    ctx.fillStyle = p.col; ctx.fillText(p.txt, 0, 0);
    ctx.restore();
  }
  ctx.globalAlpha = 1;
}

function drawHUD() {
  ctx.fillStyle = 'rgba(10,12,30,.75)'; ctx.fillRect(0, 0, W, PLAY_TOP);
  const g = ctx.createLinearGradient(0, 0, W, 0);
  g.addColorStop(0, 'rgba(255,77,109,.6)'); g.addColorStop(0.5, 'rgba(63,167,255,.8)'); g.addColorStop(1, 'rgba(155,93,229,.6)');
  ctx.fillStyle = g; ctx.fillRect(0, PLAY_TOP - 2, W, 2);

  ctx.textBaseline = 'alphabetic';
  ctx.textAlign = 'left';
  ctx.font = '700 11px system-ui,sans-serif'; ctx.fillStyle = '#7f8bb5';
  ctx.fillText('SCORE', 18, 18); ctx.fillText('BEST', 170, 18);
  ctx.font = '800 22px ui-monospace,Menlo,Consolas,monospace'; ctx.fillStyle = '#fff';
  ctx.fillText(String(Math.round(shownScore)).padStart(6, '0'), 18, 41);
  ctx.fillStyle = '#ffd23f'; ctx.fillText(String(Math.max(best, score)).padStart(6, '0'), 170, 41);

  ctx.textAlign = 'center';
  if (state !== 'title') {
    ctx.font = '700 11px system-ui,sans-serif'; ctx.fillStyle = '#7f8bb5';
    ctx.fillText('LEVEL ' + Math.min(level + 1, LEVELS.length) + ' / ' + LEVELS.length, W / 2, 18);
    ctx.font = '800 18px system-ui,sans-serif'; ctx.fillStyle = '#fff';
    ctx.fillText(LEVELS[Math.min(level, LEVELS.length - 1)].name, W / 2, 40);
  }

  // combo
  if (combo >= 4 && state === 'play') {
    const mult = Math.min(8, 1 + Math.floor((combo - 1) / 3));
    ctx.save(); ctx.translate(560, 30); const s = 1 + comboPulse * 0.35; ctx.scale(s, s); ctx.rotate(-0.05);
    ctx.font = '900 18px system-ui,sans-serif'; ctx.fillStyle = `hsl(${(time * 300) % 360},100%,65%)`;
    ctx.fillText(combo + ' COMBO' + (mult > 1 ? ' x' + mult : ''), 0, 0); ctx.restore();
  }

  // lives
  ctx.textAlign = 'right'; ctx.font = '700 11px system-ui,sans-serif'; ctx.fillStyle = '#7f8bb5';
  ctx.fillText('LIVES', W - 18, 18);
  for (let i = 0; i < lives; i++) {
    ctx.fillStyle = '#5fd4ff'; rr(W - 42 - i * 30, 28, 24, 8, 4); ctx.fill();
  }

  // active powerups
  const act = [['WIDE', wideT, 14, POWERS.W.col], ['SLOW', slowT, 10, POWERS.S.col], ['FIRE', fireT, 8, POWERS.F.col]].filter(a => a[1] > 0);
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = '800 10px system-ui,sans-serif';
  act.forEach((a, i) => {
    const x = 16 + i * 74, y = H - 14;
    ctx.fillStyle = 'rgba(255,255,255,.08)'; rr(x, y - 7, 66, 14, 7); ctx.fill();
    ctx.fillStyle = a[3]; rr(x, y - 7, 66 * a[1] / a[2], 14, 7); ctx.fill();
    ctx.fillStyle = '#0b0d1a'; ctx.fillText(a[0], x + 33, y + 1);
  });
  ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(160,175,220,.5)'; ctx.font = '600 10px system-ui,sans-serif';
  ctx.fillText('[P] pause   [M] sound ' + (muted ? 'off' : 'on'), W - 12, H - 12);
}

function bigText(txt, y, size, cols) {
  ctx.font = `900 ${size}px system-ui,sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  const chars = [...txt], widths = chars.map(c => ctx.measureText(c).width), tot = widths.reduce((a, b) => a + b, 0);
  let x = W / 2 - tot / 2;
  chars.forEach((c, i) => {
    const cx = x + widths[i] / 2, yy = y + Math.sin(time * 4 + i * 0.6) * size * 0.08;
    ctx.save(); ctx.translate(cx, yy); ctx.rotate(Math.sin(time * 3 + i) * 0.05);
    ctx.shadowColor = cols[i % cols.length]; ctx.shadowBlur = 25;
    ctx.fillStyle = cols[i % cols.length]; ctx.fillText(c, 0, 0);
    ctx.shadowBlur = 0; ctx.fillStyle = 'rgba(255,255,255,.35)'; ctx.fillText(c, 0, -2);
    ctx.restore();
    x += widths[i];
  });
}
function centerText(txt, y, font, col, alpha = 1) {
  ctx.globalAlpha = alpha; ctx.font = font; ctx.fillStyle = col; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(txt, W / 2, y); ctx.globalAlpha = 1;
}
function dim(a) { ctx.fillStyle = `rgba(5,6,16,${a})`; ctx.fillRect(0, PLAY_TOP, W, H - PLAY_TOP); }

function drawOverlay() {
  const blink = 0.55 + 0.45 * Math.sin(time * 5);
  if (state === 'title') {
    dim(0.55);
    bigText('BREAKOUT', 230, 92, PALETTE);
    centerText('Smash every brick. Catch the capsules. Chain combos.', 310, '500 18px system-ui,sans-serif', '#c8d2f5');
    centerText('Mouse / touch  or  ← → / A D  to move     ·     Click / Space to launch', 350, '600 14px system-ui,sans-serif', '#8f9cc8');
    centerText('CLICK OR PRESS SPACE TO START', 430, '800 22px system-ui,sans-serif', '#fff', blink);
    const ks = Object.values(POWERS);
    ks.forEach((P, i) => {
      const x = W / 2 - (ks.length - 1) * 70 + i * 140, y = 500;
      ctx.fillStyle = P.col; rr(x - 20, y - 9, 40, 18, 9); ctx.fill();
      ctx.fillStyle = '#10121e'; ctx.font = 'bold 13px system-ui,sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(P.ch, x, y + 1);
      ctx.fillStyle = '#aab4dc'; ctx.font = '700 11px system-ui,sans-serif'; ctx.fillText(P.name, x, y + 24);
    });
    return;
  }
  if (state === 'over') {
    dim(Math.min(0.7, stateT));
    bigText('GAME OVER', 250, 80, ['#ff4d6d', '#ff8c42']);
    centerText('Score ' + score + (score >= best && score > 0 ? '   —   NEW BEST!' : '   ·   Best ' + best), 330, '700 22px system-ui,sans-serif', '#fff');
    if (stateT > 0.8) centerText('Click or press Space to play again', 400, '700 18px system-ui,sans-serif', '#c8d2f5', blink);
    return;
  }
  if (state === 'win') {
    dim(Math.min(0.55, stateT));
    bigText('YOU WIN!', 240, 90, PALETTE);
    centerText('All ' + LEVELS.length + ' levels cleared  ·  Final score ' + score, 330, '700 22px system-ui,sans-serif', '#fff');
    if (score >= best) centerText('NEW BEST!', 370, '900 20px system-ui,sans-serif', '#ffd23f', blink);
    if (stateT > 0.8) centerText('Click or press Space to play again', 430, '700 18px system-ui,sans-serif', '#c8d2f5', blink);
    return;
  }
  // banner (level start / clear)
  if (banner.t < 2.4) {
    const t = banner.t, a = t < 0.25 ? t / 0.25 : t > 1.9 ? Math.max(0, (2.4 - t) / 0.5) : 1;
    const s = t < 0.25 ? 0.6 + 0.4 * easeOutBack(t / 0.25) : 1;
    ctx.save(); ctx.globalAlpha = a; ctx.translate(W / 2, 330); ctx.scale(s, s);
    ctx.font = '900 54px system-ui,sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.shadowColor = '#3fa7ff'; ctx.shadowBlur = 30; ctx.fillStyle = '#fff'; ctx.fillText(banner.title, 0, 0);
    ctx.shadowBlur = 0; ctx.font = '700 22px system-ui,sans-serif'; ctx.fillStyle = '#ffd23f'; ctx.fillText(banner.sub, 0, 44);
    ctx.restore();
  }
  if (state === 'play' && balls.some(b => b.stuck) && !paused && banner.t > 1.2)
    centerText('Click / Space to launch', H - 90, '700 16px system-ui,sans-serif', '#c8d2f5', blink * 0.8);
  if (paused) {
    dim(0.6);
    bigText('PAUSED', 270, 70, ['#3fa7ff', '#9b5de5']);
    centerText('Click or press P to resume', 340, '700 18px system-ui,sans-serif', '#c8d2f5', blink);
  }
}

// ---------- boot ----------
loadLevel(0); state = 'title';
let last = performance.now();
function frame(now) {
  const dt = Math.min(0.033, (now - last) / 1000); last = now;
  update(dt); render();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Compare with

Claude Opus 5.5: other attempts