hypeorcode
Network offOpen alone (new tab)

Run details

Model
anthropic/claude-haiku-5.5
Route
OpenRouter API, one user message
Reasoning
provider default, 22,871 reasoning tokens
Limits
60 min · 128,000 output tokens
Date
21 Oct 2026
Time
2 min 21 s
Size
23 kB
Tokens
118 in, 35,658 out
List price
$0.018 ($0.1 in / $0.5 out per M)
Paid
$0.018

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). Reply with the file in a single ```html code block.

Source

23 kB 735 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Breakout Deluxe</title>
<style>
  html, body {
    margin: 0; height: 100%;
    background: #05061a;
    color: #eee;
    font-family: monospace;
    overflow: hidden;
  }
  body { display: flex; align-items: center; justify-content: center; }
  #wrap {
    position: relative;
    width: min(94vw, calc((100vh - 48px) * 4 / 3));
    aspect-ratio: 4 / 3;
  }
  canvas {
    width: 100%; height: 100%; display: block;
    border-radius: 10px;
    box-shadow: 0 0 40px #4a2cff66, 0 0 0 2px #2a2f6e;
    cursor: none;
    touch-action: none;
  }
  #help {
    position: absolute; left: 0; right: 0; bottom: -30px;
    text-align: center; font-size: 13px; opacity: .55;
  }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game"></canvas>
  <div id="help">Mouse / touch or ← → / A D to move · Click / Space to launch · P / Esc to pause</div>
</div>

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

// ---------- Setup ----------
const W = 800, H = 600;
const cv = document.getElementById("game");
const ctx = cv.getContext("2d");
cv.width = W; cv.height = H;

const COLS = 12, BW = 60, BH = 22, GAP = 6;
const GX = (W - (COLS * (BW + GAP) - GAP)) / 2;
const GY = 86;
const PY = H - 44, PH = 14, PSPEED = 760;
const BASE_SPEED = 380;

const TYPES = {
  "1": { hp: 1, pts: 10 },
  "2": { hp: 2, pts: 25 },
  "3": { hp: 3, pts: 60 },
  "X": { hp: Infinity, pts: 0 }   // indestructible steel
};

const LEVELS = [
  { name: "Warm-Up", rows: [
    "111111111111",
    "111111111111",
    "222222222222",
    "222222222222",
    "111111111111"
  ]},
  { name: "Diamonds", rows: [
    "....33......",
    "...2112.....",
    "..213312....",
    "..213312....",
    "...2112.....",
    "....33......"
  ]},
  { name: "Checkers", rows: [
    "1.1.1.1.1.1.",
    ".2.2.2.2.2.2",
    "1.1.1.1.1.1.",
    ".2.2.2.2.2.2",
    "1X1X1X1X1X1X"
  ]},
  { name: "Fortress", rows: [
    "XXXXXXXXXXXX",
    "X2222222222X",
    "X2111111112X",
    "X2113333112X",
    "X2113333112X",
    "X2111111112X",
    "X2222222222X",
    "XXXXX..XXXXX"
  ]},
  { name: "Invaders", rows: [
    "..2......2..",
    "...222222...",
    "..22X22X22..",
    ".22222222222",
    "2.2.2.2.2.2.",
    "1.1.1.1.1.1."
  ]}
];

const PCOL = { W: "#41f5a0", M: "#ff5fd2", S: "#ffd447", L: "#ff4d6d" };
const PNAME = { W: "WIDE", M: "MULTI", S: "SLOW", L: "1UP" };

const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const rand = (a, b) => a + Math.random() * (b - a);

// ---------- State ----------
const game = {
  state: "menu", level: 0, score: 0, lives: 3, combo: 0,
  shake: 0, hitstop: 0, flash: 0, stateT: 0, high: 0
};
const paddle = { x: W / 2, target: W / 2, w: 104, squash: 0 };
let balls = [], bricks = [], powers = [], parts = [], texts = [];
let wideT = 0, slowT = 0;
const keys = {};
const stars = Array.from({ length: 80 }, () => ({ x: rand(0, W), y: rand(0, H), z: rand(0.2, 1) }));

try { game.high = +localStorage.getItem("breakout-deluxe-high") || 0; } catch (e) {}

// ---------- Audio (WebAudio, synthesized, no files) ----------
let actx = null;
function initAudio() {
  if (!actx) {
    try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
  }
  if (actx.state === "suspended") actx.resume();
}
function tone(f, d = 0.08, type = "square", v = 0.05, slide = 1) {
  if (!actx) return;
  const t = actx.currentTime;
  const o = actx.createOscillator(), g = actx.createGain();
  o.type = type;
  o.frequency.setValueAtTime(f, t);
  if (slide !== 1) o.frequency.exponentialRampToValueAtTime(f * slide, t + d);
  g.gain.setValueAtTime(v, t);
  g.gain.exponentialRampToValueAtTime(0.0008, t + d);
  o.connect(g); g.connect(actx.destination);
  o.start(t); o.stop(t + d + 0.03);
}
function noise(d = 0.12, v = 0.05) {
  if (!actx) return;
  const n = Math.floor(actx.sampleRate * d);
  const buf = actx.createBuffer(1, n, actx.sampleRate);
  const data = buf.getChannelData(0);
  for (let i = 0; i < n; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / n);
  const s = actx.createBufferSource(), g = actx.createGain();
  g.gain.value = v;
  s.buffer = buf; s.connect(g); g.connect(actx.destination); s.start();
}
const sfxPaddle = () => tone(220, 0.07, "triangle", 0.09, 1.4);
const sfxWall = () => tone(170, 0.04, "sine", 0.04);
const sfxHit = (combo) => tone(440 * Math.pow(2, Math.min(combo, 24) / 12), 0.09, "square", 0.04);
const sfxBreak = (combo) => { tone(440 * Math.pow(2, Math.min(combo, 24) / 12), 0.14, "triangle", 0.09, 1.8); noise(0.1, 0.05); };
const sfxSteel = () => { tone(120, 0.08, "sawtooth", 0.05); noise(0.05, 0.03); };
const sfxLoss = () => tone(340, 0.6, "sawtooth", 0.08, 0.2);
const sfxPower = () => [523, 659, 784].forEach((f, i) => setTimeout(() => tone(f, 0.1, "triangle", 0.08), i * 70));
const sfxClear = () => [523, 659, 784, 1046].forEach((f, i) => setTimeout(() => tone(f, 0.18, "square", 0.06), i * 110));

// ---------- Helpers ----------
function makeBall(attached, x, y, vx = 0, vy = 0) {
  return { x, y, vx, vy, r: 7, attached, trail: [], dead: false };
}
function ballSpeed() {
  return (BASE_SPEED + game.level * 22) * (slowT > 0 ? 0.65 : 1);
}
function mult() { return Math.min(5, 1 + Math.floor(game.combo / 5)); }

function burst(x, y, color, n, speed = 260) {
  for (let i = 0; i < n; i++) {
    const a = rand(0, Math.PI * 2), s = rand(speed * 0.2, speed), life = rand(0.4, 0.9);
    parts.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life, max: life,
                 color, size: rand(2, 5), g: 300 });
  }
}
function popup(x, y, text, color) {
  texts.push({ x, y, text, life: 1, color });
}

function colorFor(ch, r) {
  if (ch === "1") return `hsl(${200 + r * 18}, 85%, 58%)`;
  if (ch === "2") return `hsl(${265 + r * 12}, 70%, 58%)`;
  if (ch === "3") return "#ffd54a";
  return "#525c70";
}

function loadLevel(i) {
  const L = LEVELS[i];
  bricks = [];
  L.rows.forEach((row, r) => {
    for (let c = 0; c < COLS; c++) {
      const ch = row[c];
      if (!ch || ch === ".") continue;
      const def = TYPES[ch];
      bricks.push({
        x: GX + c * (BW + GAP), y: GY + r * (BH + GAP), w: BW, h: BH,
        t: ch, hp: def.hp, max: def.hp, pts: def.pts,
        color: colorFor(ch, r), flash: 0, alive: true
      });
    }
  });
  balls = [makeBall(true, paddle.x, PY - 8)];
  powers = [];
  wideT = slowT = 0;
  game.combo = 0;
}

function startGame() {
  game.level = 0; game.score = 0; game.lives = 3; game.combo = 0;
  paddle.w = 104;
  loadLevel(0);
  game.state = "play"; game.stateT = 0;
}

function saveHigh() {
  if (game.score > game.high) {
    game.high = game.score;
    try { localStorage.setItem("breakout-deluxe-high", String(game.high)); } catch (e) {}
  }
}

function togglePause() {
  if (game.state === "play") { game.state = "pause"; }
  else if (game.state === "pause") { game.state = "play"; }
}

function launchBalls() {
  let any = false;
  for (const b of balls) {
    if (!b.attached) continue;
    b.attached = false;
    const a = rand(-0.35, 0.35), sp = ballSpeed();
    b.vx = Math.sin(a) * sp;
    b.vy = -Math.cos(a) * sp;
    any = true;
  }
  if (any) tone(523, 0.06, "square", 0.05);
}

function action() {
  switch (game.state) {
    case "menu": startGame(); break;
    case "play": if (balls.some(b => b.attached)) launchBalls(); break;
    case "pause": togglePause(); break;
    case "over": case "win": if (game.stateT > 0.8) startGame(); break;
  }
}

function loseLife() {
  game.lives--;
  game.combo = 0;
  game.shake = 14; game.flash = 1;
  sfxLoss();
  burst(paddle.x, PY, "#ff5577", 30, 320);
  if (game.lives <= 0) {
    game.state = "over"; game.stateT = 0; saveHigh();
  } else {
    balls = [makeBall(true, paddle.x, PY - 8)];
    powers = []; wideT = slowT = 0;
  }
}

function spawnClones() {
  const moving = balls.filter(b => !b.attached && !b.dead);
  for (const b of moving) {
    for (const ang of [-0.5, 0.5]) {
      if (balls.length >= 8) return;
      const c = Math.cos(ang), s = Math.sin(ang);
      balls.push(makeBall(false, b.x, b.y, b.vx * c - b.vy * s, b.vx * s + b.vy * c));
    }
  }
}

function applyPower(type) {
  const col = PCOL[type];
  if (type === "W") { wideT = 12; popup(paddle.x, PY - 30, "WIDE!", col); }
  if (type === "M") { spawnClones(); popup(paddle.x, PY - 30, "MULTI!", col); }
  if (type === "S") { slowT = 8; popup(paddle.x, PY - 30, "SLOW", col); }
  if (type === "L") { game.lives = Math.min(9, game.lives + 1); popup(paddle.x, PY - 30, "1UP!", col); }
  sfxPower();
  burst(paddle.x, PY - 10, col, 16, 200);
}

// ---------- Collision & brick hits ----------
function hitBrick(br, x, y) {
  if (br.t === "X") {
    sfxSteel();
    burst(x, y, "#c0c8d8", 6, 180);
    game.shake = Math.max(game.shake, 2);
    br.flash = 0.2;
    return;
  }
  br.hp--;
  br.flash = 1;
  game.combo++;
  if (br.hp > 0) {
    sfxHit(game.combo);
    burst(x, y, br.color, 6, 200);
    game.shake = Math.max(game.shake, 3);
    return;
  }
  br.alive = false;
  const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
  const pts = br.pts * mult();
  game.score += pts;
  burst(cx, cy, br.color, 26, 340);
  popup(cx, cy, `+${pts}`, "#fff");
  game.shake = Math.max(game.shake, 6);
  game.hitstop = 0.035;
  sfxBreak(game.combo);
  if (Math.random() < 0.15) {
    const types = ["W", "W", "M", "S", "L"];
    powers.push({ x: cx, y: cy, vy: 150, t: 0, type: types[Math.floor(Math.random() * types.length)] });
  }
  if (!bricks.some(b => b.alive && b.t !== "X")) {
    const bonus = 500 + game.lives * 200;
    game.score += bonus;
    game.state = "clear"; game.stateT = 0;
    popup(W / 2, H / 2 - 40, `+${bonus} BONUS`, "#ffd54a");
    sfxClear();
    saveHigh();
  }
}

function stepBall(b, h) {
  b.x += b.vx * h; b.y += b.vy * h;

  // walls
  if (b.x - b.r < 0)  { b.x = b.r;     b.vx = Math.abs(b.vx);  sfxWall(); }
  if (b.x + b.r > W)  { b.x = W - b.r; b.vx = -Math.abs(b.vx); sfxWall(); }
  if (b.y - b.r < 0)  { b.y = b.r;     b.vy = Math.abs(b.vy);  sfxWall(); }

  // paddle
  const pw = paddle.w / 2;
  if (b.vy > 0 && b.y + b.r >= PY && b.y - b.r <= PY + PH &&
      b.x > paddle.x - pw - b.r && b.x < paddle.x + pw + b.r) {
    b.y = PY - b.r;
    const t = clamp((b.x - paddle.x) / pw, -1, 1);
    const ang = t * 1.05;            // up to ~60 degrees off vertical
    const sp = ballSpeed();
    b.vx = sp * Math.sin(ang);
    b.vy = -sp * Math.cos(ang);
    game.combo = game.combo; // combo persists while rallying
    paddle.squash = 1;
    sfxPaddle();
    burst(b.x, PY, "#bff6ff", 4, 120);
    return;
  }

  // bricks
  for (const br of bricks) {
    if (!br.alive) continue;
    const cx = clamp(b.x, br.x, br.x + br.w);
    const cy = clamp(b.y, br.y, br.y + br.h);
    const dx = b.x - cx, dy = b.y - cy;
    if (dx * dx + dy * dy >= b.r * b.r) continue;

    const penL = b.x + b.r - br.x;
    const penR = br.x + br.w - (b.x - b.r);
    const penT = b.y + b.r - br.y;
    const penB = br.y + br.h - (b.y - b.r);
    const m = Math.min(penL, penR, penT, penB);
    if (m === penL)      { b.vx = -Math.abs(b.vx); b.x = br.x - b.r; }
    else if (m === penR) { b.vx =  Math.abs(b.vx); b.x = br.x + br.w + b.r; }
    else if (m === penT) { b.vy = -Math.abs(b.vy); b.y = br.y - b.r; }
    else                 { b.vy =  Math.abs(b.vy); b.y = br.y + br.h + b.r; }
    hitBrick(br, cx, cy);
    return;
  }
}

// ---------- Update ----------
function update(dt) {
  game.stateT += dt;
  game.shake = Math.max(0, game.shake - dt * 30);
  game.flash = Math.max(0, game.flash - dt * 2.5);
  paddle.squash = Math.max(0, paddle.squash - dt * 6);

  for (const s of stars) {
    s.y += dt * (15 + s.z * 60);
    if (s.y > H) { s.y = 0; s.x = rand(0, W); }
  }
  for (const br of bricks) br.flash = Math.max(0, br.flash - dt * 6);
  for (const p of parts) { p.life -= dt; p.vy += p.g * dt; p.x += p.vx * dt; p.y += p.vy * dt; }
  parts = parts.filter(p => p.life > 0);
  for (const t of texts) { t.life -= dt; t.y -= dt * 40; }
  texts = texts.filter(t => t.life > 0);

  // paddle control (keyboard target + smoothed mouse target)
  const dir = ((keys.ArrowLeft || keys.KeyA) ? -1 : 0) + ((keys.ArrowRight || keys.KeyD) ? 1 : 0);
  if (dir) paddle.target += dir * PSPEED * dt;
  paddle.target = clamp(paddle.target, 0, W);
  paddle.x += (paddle.target - paddle.x) * (1 - Math.exp(-dt * 22));
  paddle.w += ((wideT > 0 ? 170 : 104) - paddle.w) * Math.min(1, dt * 10);
  paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);

  if (game.state === "play") {
    let pdt = dt;
    if (game.hitstop > 0) { game.hitstop -= dt; pdt = 0; }
    wideT = Math.max(0, wideT - dt);
    slowT = Math.max(0, slowT - dt);

    for (const b of balls) {
      if (b.attached) { b.x = paddle.x; b.y = PY - b.r - 1; b.trail.length = 0; continue; }
      b.trail.push({ x: b.x, y: b.y });
      if (b.trail.length > 12) b.trail.shift();

      // keep speed near target and avoid near-horizontal stalls
      const sp = ballSpeed();
      const m = Math.hypot(b.vx, b.vy) || 1;
      const nm = m + (sp - m) * Math.min(1, dt * 2);
      b.vx *= nm / m; b.vy *= nm / m;
      const minVy = sp * 0.25;
      if (Math.abs(b.vy) < minVy) {
        b.vy = (b.vy < 0 ? -1 : 1) * minVy;
        b.vx = Math.sign(b.vx || 1) * Math.sqrt(Math.max(0, sp * sp - minVy * minVy));
      }

      const dist = Math.hypot(b.vx, b.vy) * pdt;
      const n = Math.max(1, Math.ceil(dist / 3));
      for (let i = 0; i < n && game.state === "play" && !b.dead; i++) stepBall(b, pdt / n);
      if (b.y - b.r > H) b.dead = true;
    }
    balls = balls.filter(b => !b.dead);
    if (balls.length === 0 && game.state === "play") loseLife();

    for (const p of powers) {
      p.t += dt;
      p.y += p.vy * pdt;
      if (p.y > PY + 30) p.dead = true;
      else if (p.y + 12 >= PY && p.y - 12 <= PY + PH && Math.abs(p.x - paddle.x) < paddle.w / 2 + 12) {
        p.dead = true;
        applyPower(p.type);
      }
    }
    powers = powers.filter(p => !p.dead);
  } else {
    for (const b of balls) if (b.attached) { b.x = paddle.x; b.y = PY - b.r - 1; }
    for (const p of powers) p.t += dt;
  }

  if (game.state === "clear" && game.stateT > 2.8) {
    if (game.level + 1 < LEVELS.length) {
      game.level++;
      loadLevel(game.level);
      game.state = "play"; game.stateT = 0;
    } else {
      game.state = "win"; game.stateT = 0;
    }
  }
}

// ---------- Drawing ----------
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(b) {
  if (!b.alive) return;
  const { x, y, w, h } = b;
  ctx.save();
  if (b.t === "3") { ctx.shadowColor = "#ffd54a"; ctx.shadowBlur = 14; }
  ctx.fillStyle = b.t === "X" ? "#525c70" : b.color;
  ctx.fillRect(x, y, w, h);
  ctx.shadowBlur = 0;
  ctx.fillStyle = "rgba(255,255,255,0.35)"; ctx.fillRect(x, y, w, 3);
  ctx.fillStyle = "rgba(0,0,0,0.35)"; ctx.fillRect(x, y + h - 4, w, 4);
  if (b.t === "X") {
    ctx.save();
    ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip();
    ctx.strokeStyle = "rgba(0,0,0,0.35)"; ctx.lineWidth = 2;
    for (let i = -h; i < w; i += 8) {
      ctx.beginPath(); ctx.moveTo(x + i, y + h); ctx.lineTo(x + i + h, y); ctx.stroke();
    }
    ctx.restore();
  } else if (b.hp < b.max) {
    ctx.strokeStyle = "rgba(0,0,0,0.55)"; ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(x + w * 0.3, y); ctx.lineTo(x + w * 0.45, y + h * 0.6); ctx.lineTo(x + w * 0.4, y + h);
    ctx.stroke();
  }
  if (b.flash > 0) {
    ctx.fillStyle = `rgba(255,255,255,${b.flash * 0.8})`;
    ctx.fillRect(x, y, w, h);
  }
  ctx.restore();
}

function drawPower(p) {
  ctx.save();
  ctx.translate(p.x, p.y);
  ctx.rotate(Math.sin(p.t * 4) * 0.2);
  ctx.shadowColor = PCOL[p.type]; ctx.shadowBlur = 14;
  ctx.fillStyle = PCOL[p.type];
  ctx.beginPath(); ctx.arc(0, 0, 12, 0, Math.PI * 2); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = "#06081a";
  ctx.font = "bold 13px monospace";
  ctx.textAlign = "center"; ctx.textBaseline = "middle";
  ctx.fillText(p.type, 0, 1);
  ctx.restore();
}

function drawPaddle() {
  const w = paddle.w, h = PH + paddle.squash * 4;
  const x = paddle.x - w / 2, y = PY - (h - PH) / 2;
  ctx.save();
  ctx.shadowColor = "#4de1ff"; ctx.shadowBlur = 18;
  const g = ctx.createLinearGradient(0, y, 0, y + h);
  g.addColorStop(0, "#d8fbff"); g.addColorStop(1, "#2a7fff");
  ctx.fillStyle = g;
  roundRect(x, y, w, h, 7); ctx.fill();
  ctx.restore();
}

function drawBall(b) {
  const n = b.trail.length;
  for (let i = 0; i < n; i++) {
    const t = b.trail[i];
    ctx.fillStyle = `rgba(180,240,255,${((i + 1) / n) * 0.3})`;
    ctx.beginPath(); ctx.arc(t.x, t.y, b.r * (i + 1) / n, 0, Math.PI * 2); ctx.fill();
  }
  ctx.save();
  ctx.shadowColor = "#8ff"; ctx.shadowBlur = 16;
  ctx.fillStyle = "#fff";
  ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.restore();
}

function drawHUD() {
  ctx.save();
  ctx.fillStyle = "rgba(0,0,0,0.4)";
  ctx.fillRect(0, 0, W, 62);
  ctx.textBaseline = "middle";

  ctx.textAlign = "left";
  ctx.font = "bold 20px monospace"; ctx.fillStyle = "#fff";
  ctx.fillText("SCORE " + String(game.score).padStart(6, "0"), 14, 22);
  ctx.font = "13px monospace"; ctx.fillStyle = "#9fb0ff";
  ctx.fillText("HI " + String(game.high).padStart(6, "0"), 14, 46);

  ctx.textAlign = "center";
  ctx.font = "bold 18px monospace"; ctx.fillStyle = "#fff";
  ctx.fillText(`LEVEL ${game.level + 1}  ${LEVELS[game.level].name.toUpperCase()}`, W / 2, 22);
  if (game.combo >= 2) {
    ctx.fillStyle = `hsl(${(game.stateT * 300) % 360},100%,70%)`;
    ctx.font = "bold 14px monospace";
    ctx.fillText(`COMBO ${game.combo}  x${mult()}`, W / 2, 46);
  }

  // lives as tiny balls
  ctx.fillStyle = "#fff"; ctx.shadowColor = "#8ff"; ctx.shadowBlur = 8;
  for (let i = 0; i < game.lives; i++) {
    ctx.beginPath(); ctx.arc(W - 22 - i * 20, 22, 6, 0, Math.PI * 2); ctx.fill();
  }
  ctx.shadowBlur = 0;

  // powerup timers
  const parts2 = [];
  if (wideT > 0) parts2.push({ t: `WIDE ${wideT.toFixed(1)}`, c: PCOL.W });
  if (slowT > 0) parts2.push({ t: `SLOW ${slowT.toFixed(1)}`, c: PCOL.S });
  ctx.textAlign = "right"; ctx.font = "13px monospace";
  let xr = W - 14;
  for (const p of parts2.reverse()) {
    ctx.fillStyle = p.c;
    ctx.fillText(p.t, xr, 46);
    xr -= ctx.measureText(p.t).width + 16;
  }
  ctx.restore();
}

function bigText(text, y, size, color) {
  ctx.font = `bold ${size}px monospace`;
  ctx.fillStyle = color;
  ctx.textAlign = "center"; ctx.textBaseline = "middle";
  ctx.fillText(text, W / 2, y);
}

function drawOverlay() {
  const st = game.state;
  if (st === "menu") {
    ctx.fillStyle = "rgba(5,6,20,0.7)"; ctx.fillRect(0, 0, W, H);
    const hue = (game.stateT * 60) % 360;
    bigText("BREAKOUT", H / 2 - 130, 64, `hsl(${hue},100%,65%)`);
    bigText("D E L U X E", H / 2 - 80, 22, "#9fb0ff");
    ctx.font = "16px monospace"; ctx.fillStyle = "#ddd";
    ctx.textAlign = "center";
    const lines = [
      "Mouse / touch or ← → / A D : move paddle",
      "Click / Space / Enter : launch ball",
      "P or Esc : pause",
      "",
      "Yellow = 3 hits · Purple = 2 hits · Grey = unbreakable",
      "Catch the falling capsules: W wide · M multi · S slow · L 1UP"
    ];
    lines.forEach((l, i) => ctx.fillText(l, W / 2, H / 2 - 20 + i * 24));
    if (Math.sin(game.stateT * 4) > -0.3) bigText("PRESS ENTER, SPACE OR CLICK", H / 2 + 180, 18, "#fff");
    if (game.high > 0) bigText(`HIGH SCORE ${game.high}`, H / 2 + 215, 16, "#ffd54a");
  } else if (st === "pause") {
    ctx.fillStyle = "rgba(5,6,20,0.55)"; ctx.fillRect(0, 0, W, H);
    bigText("PAUSED", H / 2, 56, "#fff");
    bigText("P / Esc / Click to resume", H / 2 + 50, 18, "#9fb0ff");
  } else if (st === "clear") {
    ctx.fillStyle = "rgba(5,6,20,0.35)"; ctx.fillRect(0, 0, W, H);
    const s = 1 + Math.max(0, 0.3 - game.stateT) * 2;
    ctx.save(); ctx.translate(W / 2, H / 2); ctx.scale(s, s); ctx.translate(-W / 2, -H / 2);
    bigText("LEVEL CLEAR!", H / 2, 52, "#7dffb0");
    ctx.restore();
  } else if (st === "over") {
    ctx.fillStyle = "rgba(20,0,10,0.7)"; ctx.fillRect(0, 0, W, H);
    bigText("GAME OVER", H / 2 - 40, 60, "#ff5577");
    bigText(`SCORE ${game.score}`, H / 2 + 20, 26, "#fff");
    bigText(`HIGH ${game.high}`, H / 2 + 56, 18, "#ffd54a");
    if (game.stateT > 0.8) bigText("PRESS ENTER / SPACE / CLICK TO RETRY", H / 2 + 110, 16, "#ddd");
  } else if (st === "win") {
    ctx.fillStyle = "rgba(5,6,20,0.7)"; ctx.fillRect(0, 0, W, H);
    bigText("YOU WIN!", H / 2 - 40, 64, `hsl(${(game.stateT * 120) % 360},100%,65%)`);
    bigText(`FINAL SCORE ${game.score}`, H / 2 + 20, 26, "#fff");
    bigText(`HIGH ${game.high}`, H / 2 + 56, 18, "#ffd54a");
    if (game.stateT > 0.8) bigText("PRESS ENTER / SPACE / CLICK TO PLAY AGAIN", H / 2 + 110, 16, "#ddd");
  }

  if (st === "play" && balls.some(b => b.attached)) {
    if (Math.sin(game.stateT * 5) > -0.2)
      bigText("CLICK / SPACE TO LAUNCH", PY - 50, 16, "#cfe");
  }
}

function draw() {
  ctx.save();
  if (game.shake > 0) ctx.translate(rand(-game.shake, game.shake), rand(-game.shake, game.shake));

  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, "#0c0f2e");
  g.addColorStop(1, "#1d0b2e");
  ctx.fillStyle = g;
  ctx.fillRect(-20, -20, W + 40, H + 40);

  for (const s of stars) {
    ctx.globalAlpha = 0.25 + s.z * 0.75;
    ctx.fillStyle = "#fff";
    ctx.fillRect(s.x, s.y, s.z * 2, s.z * 2);
  }
  ctx.globalAlpha = 1;

  bricks.forEach(drawBrick);
  powers.forEach(drawPower);
  drawPaddle();
  balls.forEach(drawBall);

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

  ctx.textAlign = "center"; ctx.textBaseline = "middle";
  ctx.font = "bold 16px monospace";
  for (const t of texts) {
    ctx.globalAlpha = Math.min(1, t.life * 1.5);
    ctx.fillStyle = t.color;
    ctx.fillText(t.text, t.x, t.y);
  }
  ctx.globalAlpha = 1;
  ctx.restore();

  drawHUD();

  if (game.flash > 0) {
    ctx.fillStyle = `rgba(255,40,80,${game.flash * 0.35})`;
    ctx.fillRect(0, 0, W, H);
  }
  if (game.lives === 1 && game.state === "play") {
    const a = 0.12 + 0.08 * Math.sin(game.stateT * 6);
    ctx.strokeStyle = `rgba(255,50,80,${a})`;
    ctx.lineWidth = 10;
    ctx.strokeRect(5, 5, W - 10, H - 10);
  }

  drawOverlay();
}

// ---------- Input ----------
const CODES = ["Space", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
window.addEventListener("keydown", e => {
  if (CODES.includes(e.code)) e.preventDefault();
  initAudio();
  keys[e.code] = true;
  if (e.repeat) return;
  if (e.code === "Enter" || e.code === "Space") action();
  else if (e.code === "KeyP" || e.code === "Escape") togglePause();
});
window.addEventListener("keyup", e => { keys[e.code] = false; });
window.addEventListener("blur", () => {
  for (const k in keys) keys[k] = false;
  if (game.state === "play") game.state = "pause";
});

function toCanvasX(e) {
  const r = cv.getBoundingClientRect();
  return (e.clientX - r.left) * W / r.width;
}
cv.addEventListener("pointermove", e => { paddle.target = toCanvasX(e); });
cv.addEventListener("pointerdown", e => {
  initAudio();
  paddle.target = toCanvasX(e);
  action();
});

// ---------- Loop ----------
loadLevel(0);
let last = performance.now();
function frame(now) {
  const dt = Math.min(0.05, (now - last) / 1000);
  last = now;
  update(dt);
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Compare with

Claude Haiku 5.5: other attempts