Models 44

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
54.2 s
Cost
$0.0077
Output tokens
12,693
Spent thinking
3,180 tokens
Speed
237.8 tok/s
Console errors
0
First token after
853 ms
First visible text after
24.4 s
Prompt tokens
679
Code size
27.3 KB
Finish reason
stop
Served by
DigitalOcean
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"medium"}
Max output
64,000 tokens
Generated
Nov 11, 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 · 828 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Break — Brick Breaker</title>
<style>
  * { box-sizing: border-box; }
  html, body {
    margin: 0;
    width: 100%;
    min-height: 100%;
    overflow-x: hidden;
    background:
      radial-gradient(circle at 18% 12%, rgba(89, 60, 255, .20), transparent 32%),
      radial-gradient(circle at 86% 80%, rgba(0, 218, 255, .14), transparent 34%),
      #070914;
    color: #eef2ff;
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  }
  body {
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 18px;
  }
  .app {
    width: min(1020px, 100%);
  }
  header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 12px;
    padding: 0 4px;
  }
  .brand {
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .mark {
    width: 40px;
    height: 40px;
    border-radius: 12px;
    display: grid;
    place-items: center;
    background: linear-gradient(145deg, #22e6ff, #7057ff);
    box-shadow: 0 0 26px rgba(72, 143, 255, .48), inset 0 1px rgba(255,255,255,.55);
    color: white;
    font-weight: 900;
    font-size: 21px;
    transform: rotate(-5deg);
  }
  h1 {
    margin: 0;
    font-size: clamp(19px, 3vw, 28px);
    line-height: 1;
    letter-spacing: .12em;
    font-weight: 900;
    background: linear-gradient(90deg, #fff, #9cecff 48%, #bca8ff);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
  .sub {
    margin-top: 5px;
    color: #7e89aa;
    font-size: 11px;
    letter-spacing: .16em;
    text-transform: uppercase;
  }
  .hint {
    color: #8d97b6;
    font-size: 12px;
    text-align: right;
    line-height: 1.6;
  }
  .hint b { color: #dce5ff; font-weight: 650; }
  .game-shell {
    position: relative;
    border-radius: 20px;
    padding: 7px;
    background: linear-gradient(145deg, rgba(97,112,255,.38), rgba(14,18,38,.92) 34%, rgba(0,218,255,.22));
    box-shadow: 0 28px 80px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.05) inset;
  }
  .game-shell::before {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: 20px;
    pointer-events: none;
    background: linear-gradient(110deg, rgba(42,225,255,.6), transparent 30%, transparent 72%, rgba(157,94,255,.65));
    filter: blur(1px);
    z-index: -1;
    opacity: .45;
  }
  canvas {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 960 / 620;
    border-radius: 14px;
    background: #090c1a;
    cursor: none;
    touch-action: none;
  }
  footer {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 11px 8px 0;
    color: #67708e;
    font-size: 11px;
    letter-spacing: .06em;
  }
  .legend { display: flex; gap: 14px; flex-wrap: wrap; }
  .legend span::before {
    content: "";
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    margin-right: 6px;
    background: #7b6cff;
    box-shadow: 0 0 8px currentColor;
  }
  .legend .cyan::before { background: #24e5ff; }
  .legend .pink::before { background: #ff5ca8; }
  @media (max-width: 600px) {
    body { padding: 9px; align-items: flex-start; }
    .hint { display: none; }
    .game-shell { padding: 4px; border-radius: 14px; }
    canvas { border-radius: 10px; }
    footer { font-size: 9px; }
    footer > span:last-child { display: none; }
  }
</style>
</head>
<body>
<div class="app">
  <header>
    <div class="brand">
      <div class="mark">N</div>
      <div>
        <h1>NEON BREAK</h1>
        <div class="sub">Paddle / Power / Persist</div>
      </div>
    </div>
    <div class="hint"><b>MOUSE</b> or <b>← →</b> to move &nbsp; · &nbsp; <b>R</b> restart</div>
  </header>
  <div class="game-shell">
    <canvas id="game" width="960" height="620" aria-label="Neon Break brick breaker game"></canvas>
  </div>
  <footer>
    <div class="legend">
      <span class="cyan">Energy brick</span>
      <span class="pink">Power-up</span>
      <span>Armored brick — 2 hits</span>
    </div>
    <span>CLICK OR PRESS A KEY TO TAKE CONTROL</span>
  </footer>
</div>
<script>
(() => {
  "use strict";

  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const W = 960, H = 620, FIELD_TOP = 100;
  const BASE_PADDLE_W = 116;
  const paddle = { x: W / 2 - BASE_PADDLE_W / 2, y: 568, w: BASE_PADDLE_W, h: 15 };
  const keys = { left: false, right: false };

  let state = "demo";
  let userActive = false;
  let score = 0, lives = 3, level = 1;
  let bricks = [], balls = [], particles = [], powerups = [], floaters = [];
  let gameTime = 0, wideUntil = 0, shake = 0, mouseX = W / 2;
  let last = performance.now();
  let pointerDown = false;

  const palettes = [
    ["#18d7ff", "#0878d8"], ["#6176ff", "#5933c9"],
    ["#b45cff", "#7024b9"], ["#ff55ad", "#c62678"],
    ["#ff9763", "#d94c46"], ["#45e6ac", "#128c78"],
    ["#35b9ff", "#2460c4"], ["#9271ff", "#5d35c4"]
  ];

  function roundedRect(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();
  }

  function makeBricks() {
    bricks = [];
    const cols = 10, rows = 8;
    const margin = 42, gap = 9;
    const bw = (W - margin * 2 - gap * (cols - 1)) / cols;
    const top = 137, bh = 25, vgap = 8;
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        const tough = (r >= 2 && r <= 4 && (c + r) % 3 !== 0) || ((r + c) % 11 === 0);
        let power = null;
        if (r === 1 && c === 2) power = "wide";
        if (r === 3 && c === 6) power = "multi";
        if (r === 0 && c === 8) power = "multi";
        if (r === 5 && c === 4) power = "wide";
        if (r === 6 && c === 8) power = "multi";
        bricks.push({
          x: margin + c * (bw + gap), y: top + r * (bh + vgap),
          w: bw, h: bh, row: r, col: c,
          hp: tough ? 2 : 1, maxHp: tough ? 2 : 1,
          tough, power, flash: 0, seed: r * 17 + c * 7
        });
      }
    }
  }

  function makeBall(x, y, angle, speed = 5) {
    return {
      x, y, vx: Math.sin(angle) * speed, vy: -Math.cos(angle) * speed,
      r: 7.2, speed, trail: [], alive: true
    };
  }

  function resetGame(demo = true) {
    score = 0; lives = 3; level = 1; gameTime = 0; wideUntil = 0;
    particles = []; powerups = []; floaters = []; shake = 0;
    paddle.w = BASE_PADDLE_W;
    paddle.x = W / 2 - paddle.w / 2;
    mouseX = W / 2;
    makeBricks();
    balls = [makeBall(W / 2, 490, 0, demo ? 4.7 : 5.1)];
    state = demo ? "demo" : "playing";
    userActive = !demo;
  }

  function beginPlay() {
    if (state === "demo") {
      state = "playing";
      userActive = true;
      flashMessage("PLAYER MODE", "#7ef9ff");
    }
  }

  function flashMessage(text, color) {
    floaters.push({ x: W / 2, y: 102, text, color, life: 1.4, max: 1.4, size: 14, vy: -0.25 });
  }

  function setPaddleWidth(w) {
    const center = paddle.x + paddle.w / 2;
    paddle.w = w;
    paddle.x = center - paddle.w / 2;
  }

  function spawnParticles(x, y, color, count = 15) {
    for (let i = 0; i < count; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = 1 + Math.random() * 4.2;
      particles.push({
        x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 1,
        life: .35 + Math.random() * .55, max: .9,
        size: 1.5 + Math.random() * 3.5, color
      });
    }
  }

  function damageBrick(brick, ball) {
    brick.hp--;
    brick.flash = .13;
    if (brick.hp > 0) {
      score += 10;
      spawnParticles(ball.x, ball.y, "#c6d2ff", 6);
    } else {
      score += brick.tough ? 100 : 50;
      const color = brick.tough ? "#b8c8ff" : palettes[brick.row][0];
      spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, color, brick.tough ? 20 : 14);
      floaters.push({
        x: brick.x + brick.w / 2, y: brick.y + brick.h / 2,
        text: "+" + (brick.tough ? 100 : 50), color: "#ffffff",
        life: .75, max: .75, size: 13, vy: -1.8
      });
      if (brick.power) {
        powerups.push({
          x: brick.x + brick.w / 2, y: brick.y + brick.h / 2,
          vx: (Math.random() - .5) * .7, vy: 1.65,
          type: brick.power, r: 15, life: 8
        });
      }
    }
  }

  function reflectBallFromBrick(ball, brick) {
    const left = ball.x - (brick.x - ball.r);
    const right = (brick.x + brick.w + ball.r) - ball.x;
    const top = ball.y - (brick.y - ball.r);
    const bottom = (brick.y + brick.h + ball.r) - ball.y;
    const m = Math.min(left, right, top, bottom);
    if (m === top) {
      ball.y = brick.y - ball.r - .1; ball.vy = -Math.abs(ball.vy);
    } else if (m === bottom) {
      ball.y = brick.y + brick.h + ball.r + .1; ball.vy = Math.abs(ball.vy);
    } else if (m === left) {
      ball.x = brick.x - ball.r - .1; ball.vx = -Math.abs(ball.vx);
    } else {
      ball.x = brick.x + brick.w + ball.r + .1; ball.vx = Math.abs(ball.vx);
    }
  }

  function hitBrick(ball, brick) {
    reflectBallFromBrick(ball, brick);
    damageBrick(brick, ball);
    shake = Math.max(shake, brick.tough ? 2.2 : 1.3);
  }

  function updateBall(ball, dt) {
    const dist = Math.hypot(ball.vx, ball.vy) * dt;
    const steps = Math.max(1, Math.ceil(dist / 3.2));
    const sdt = dt / steps;
    for (let s = 0; s < steps && ball.alive; s++) {
      const oldY = ball.y;
      ball.x += ball.vx * sdt;
      ball.y += ball.vy * sdt;

      if (ball.x - ball.r < 8) { ball.x = 8 + ball.r; ball.vx = Math.abs(ball.vx); }
      if (ball.x + ball.r > W - 8) { ball.x = W - 8 - ball.r; ball.vx = -Math.abs(ball.vx); }
      if (ball.y - ball.r < FIELD_TOP) { ball.y = FIELD_TOP + ball.r; ball.vy = Math.abs(ball.vy); }

      let hit = null;
      for (const b of bricks) {
        if (b.hp <= 0) continue;
        if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + b.w &&
            ball.y + ball.r > b.y && ball.y - ball.r < b.y + b.h) {
          hit = b; break;
        }
      }
      if (hit) hitBrick(ball, hit);

      if (ball.vy > 0 && oldY + ball.r <= paddle.y + 18 &&
          ball.y + ball.r >= paddle.y &&
          ball.x + ball.r > paddle.x && ball.x - ball.r < paddle.x + paddle.w) {
        const offset = Math.max(-1, Math.min(1, (ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2)));
        const angle = offset * 1.12;
        const speed = Math.max(5.0, Math.hypot(ball.vx, ball.vy) + .018);
        ball.vx = Math.sin(angle) * speed;
        ball.vy = -Math.abs(Math.cos(angle)) * speed;
        ball.y = paddle.y - ball.r - .2;
        spawnParticles(ball.x, paddle.y, "#61edff", 4);
      }

      if (ball.y - ball.r > H + 30) ball.alive = false;
    }
    ball.trail.push({ x: ball.x, y: ball.y });
    if (ball.trail.length > 9) ball.trail.shift();
  }

  function updatePowerups(dt) {
    for (const p of powerups) {
      p.y += p.vy * dt;
      p.x += p.vx * dt;
      p.life -= dt;
      const caught = p.y + p.r >= paddle.y && p.y - p.r <= paddle.y + paddle.h &&
                    p.x + p.r >= paddle.x && p.x - p.r <= paddle.x + paddle.w;
      if (caught) {
        p.life = 0;
        if (p.type === "wide") {
          wideUntil = gameTime + 10;
          setPaddleWidth(BASE_PADDLE_W + 64);
          flashMessage("WIDE PADDLE · 10s", "#55f5ff");
        } else {
          while (balls.length < 3) {
            const offsets = [-.48, .48];
            const offset = offsets[balls.length - 1] || 0;
            const b = makeBall(paddle.x + paddle.w / 2, paddle.y - 25, offset, 5.15);
            balls.push(b);
          }
          flashMessage("MULTI-BALL · 3 BALLS", "#c08cff");
        }
        spawnParticles(p.x, p.y, p.type === "wide" ? "#48efff" : "#bd83ff", 18);
        score += 25;
      }
    }
    powerups = powerups.filter(p => p.life > 0 && p.y < H + 30);
  }

  function updateParticles(dt) {
    for (const p of particles) {
      p.x += p.vx * dt; p.y += p.vy * dt;
      p.vy += 8 * dt; p.vx *= .992;
      p.life -= dt;
    }
    particles = particles.filter(p => p.life > 0);
    for (const f of floaters) {
      f.y += f.vy * dt; f.life -= dt;
    }
    floaters = floaters.filter(f => f.life > 0);
  }

  function update(dt) {
    gameTime += dt;
    if (gameTime > wideUntil && paddle.w !== BASE_PADDLE_W) {
      setPaddleWidth(BASE_PADDLE_W);
      flashMessage("PADDLE NORMALIZED", "#9aa6c9");
    }

    const targetW = gameTime < wideUntil ? BASE_PADDLE_W + 64 : BASE_PADDLE_W;
    if (Math.abs(paddle.w - targetW) > .5) {
      const center = paddle.x + paddle.w / 2;
      paddle.w += (targetW - paddle.w) * Math.min(1, dt * 12);
      paddle.x = center - paddle.w / 2;
    }

    if (state === "demo") {
      let target = W / 2;
      let bestY = -Infinity;
      for (const b of balls) {
        if (b.vy > 0 && b.y > bestY) { bestY = b.y; target = b.x; }
      }
      if (bestY === -Infinity && balls[0]) target = balls[0].x;
      const desired = target - paddle.w / 2;
      paddle.x += (desired - paddle.x) * Math.min(1, dt * 6.4);
    } else if (state === "playing") {
      if (keys.left || keys.right) {
        const dir = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
        paddle.x += dir * 620 * dt;
        mouseX = paddle.x + paddle.w / 2;
      } else if (userActive) {
        const desired = mouseX - paddle.w / 2;
        paddle.x += (desired - paddle.x) * Math.min(1, dt * 18);
      }
    }
    paddle.x = Math.max(8, Math.min(W - 8 - paddle.w, paddle.x));

    for (const b of bricks) b.flash = Math.max(0, b.flash - dt);
    for (const b of balls) if (b.alive) updateBall(b, dt);
    balls = balls.filter(b => b.alive);
    updatePowerups(dt);
    updateParticles(dt);
    shake = Math.max(0, shake - dt * 12);

    if (state === "playing" || state === "demo") {
      if (balls.length === 0) {
        lives--;
        shake = 8;
        if (lives <= 0) {
          state = "gameover";
        } else {
          balls = [makeBall(paddle.x + paddle.w / 2, paddle.y - 35, (Math.random() - .5) * .35, 5)];
          flashMessage(lives + " " + (lives === 1 ? "LIFE" : "LIVES") + " LEFT", "#ff7ca8");
        }
      }
      if (bricks.every(b => b.hp <= 0)) state = "victory";
    }
  }

  function drawBackground() {
    const g = ctx.createLinearGradient(0, 0, 0, H);
    g.addColorStop(0, "#11152b");
    g.addColorStop(.5, "#090d1c");
    g.addColorStop(1, "#070914");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);

    ctx.save();
    ctx.globalAlpha = .11;
    ctx.strokeStyle = "#8aa2ff";
    ctx.lineWidth = 1;
    for (let x = 0; x <= W; x += 40) {
      ctx.beginPath(); ctx.moveTo(x, FIELD_TOP); ctx.lineTo(x, H); ctx.stroke();
    }
    for (let y = FIELD_TOP; y <= H; y += 40) {
      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
    }
    ctx.restore();

    const glow = ctx.createRadialGradient(W / 2, 250, 20, W / 2, 250, 470);
    glow.addColorStop(0, "rgba(91,73,255,.055)");
    glow.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = glow;
    ctx.fillRect(0, FIELD_TOP, W, H - FIELD_TOP);

    ctx.fillStyle = "rgba(90,117,255,.22)";
    ctx.fillRect(0, FIELD_TOP - 2, W, 2);
    ctx.fillStyle = "rgba(45,229,255,.08)";
    ctx.fillRect(0, FIELD_TOP, W, 1);
  }

  function drawBrick(b) {
    if (b.hp <= 0) return;
    const pal = palettes[b.row];
    const x = b.x, y = b.y, w = b.w, h = b.h;
    ctx.save();
    ctx.shadowColor = b.tough ? "rgba(154,171,255,.45)" : pal[0] + "88";
    ctx.shadowBlur = b.tough ? 11 : 8;
    roundedRect(x, y, w, h, 5);
    if (b.tough) {
      const g = ctx.createLinearGradient(x, y, x, y + h);
      g.addColorStop(0, "#707b9e"); g.addColorStop(.45, "#39435f"); g.addColorStop(1, "#222a43");
      ctx.fillStyle = g; ctx.fill();
      ctx.strokeStyle = "#a9b7dd"; ctx.lineWidth = 1.4; ctx.stroke();
      ctx.fillStyle = "rgba(255,255,255,.17)";
      roundedRect(x + 3, y + 2, w - 6, 3, 2); ctx.fill();
      ctx.strokeStyle = "rgba(15,20,38,.65)";
      ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(x + 13, y + 6); ctx.lineTo(x + 19, y + 12); ctx.lineTo(x + 16, y + 19); ctx.stroke();
    } else {
      const g = ctx.createLinearGradient(x, y, x, y + h);
      g.addColorStop(0, pal[0]); g.addColorStop(1, pal[1]);
      ctx.fillStyle = g; ctx.fill();
      ctx.strokeStyle = "rgba(255,255,255,.35)"; ctx.lineWidth = 1; ctx.stroke();
      ctx.fillStyle = "rgba(255,255,255,.25)";
      roundedRect(x + 4, y + 2.5, w - 8, 3, 2); ctx.fill();
    }
    if (b.flash > 0) {
      ctx.globalAlpha = b.flash / .13 * .8;
      ctx.fillStyle = "#ffffff";
      roundedRect(x, y, w, h, 5); ctx.fill();
      ctx.globalAlpha = 1;
    }
    if (b.tough && b.hp === 1) {
      ctx.strokeStyle = "rgba(8,12,25,.9)";
      ctx.lineWidth = 1.6;
      ctx.beginPath();
      ctx.moveTo(x + w * .28, y);
      ctx.lineTo(x + w * .48, y + h * .38);
      ctx.lineTo(x + w * .37, y + h * .52);
      ctx.lineTo(x + w * .63, y + h);
      ctx.moveTo(x + w * .48, y + h * .38);
      ctx.lineTo(x + w * .75, y + h * .28);
      ctx.stroke();
      ctx.strokeStyle = "rgba(255,255,255,.35)";
      ctx.lineWidth = .7; ctx.stroke();
    }
    if (b.power) drawPowerIcon(b.power, x + w - 13, y + h / 2, 7, true);
    ctx.restore();
  }

  function drawPowerIcon(type, x, y, r, small = false) {
    ctx.save();
    ctx.translate(x, y);
    const color = type === "wide" ? "#67f4ff" : "#d39aff";
    ctx.shadowColor = color; ctx.shadowBlur = small ? 8 : 15;
    ctx.fillStyle = "rgba(8,12,28,.86)";
    ctx.strokeStyle = color; ctx.lineWidth = small ? 1.2 : 2;
    ctx.beginPath(); ctx.arc(0, 0, r + (small ? 2 : 4), 0, Math.PI * 2); ctx.fill(); ctx.stroke();
    if (type === "wide") {
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.moveTo(1, -r * .65); ctx.lineTo(-r * .42, 1); ctx.lineTo(-.5, 1);
      ctx.lineTo(-1, r * .65); ctx.lineTo(r * .48, -r * .12); ctx.lineTo(.3, -r * .12);
      ctx.closePath(); ctx.fill();
    } else {
      ctx.fillStyle = color;
      for (let i = -1; i <= 1; i++) {
        ctx.beginPath(); ctx.arc(i * r * .46, 0, r * .24, 0, Math.PI * 2); ctx.fill();
      }
    }
    ctx.restore();
  }

  function drawHUD() {
    ctx.save();
    ctx.fillStyle = "#0b0f20";
    ctx.fillRect(0, 0, W, FIELD_TOP);
    const g = ctx.createLinearGradient(0, 0, W, 0);
    g.addColorStop(0, "rgba(65,96,255,.11)");
    g.addColorStop(.5, "rgba(45,229,255,.03)");
    g.addColorStop(1, "rgba(174,83,255,.1)");
    ctx.fillStyle = g; ctx.fillRect(0, 0, W, FIELD_TOP);

    ctx.font = "700 10px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#687392";
    ctx.textAlign = "left";
    ctx.fillText("SCORE", 31, 26);
    ctx.font = "800 27px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#f4f7ff";
    ctx.fillText(String(score).padStart(5, "0"), 29, 58);

    ctx.textAlign = "center";
    ctx.font = "700 10px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#687392";
    ctx.fillText("ROUND", W / 2, 25);
    ctx.font = "800 22px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#7deaff";
    ctx.fillText(String(level).padStart(2, "0"), W / 2, 52);

    ctx.textAlign = "right";
    ctx.font = "700 10px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#687392";
    ctx.fillText("LIVES", W - 31, 25);
    for (let i = 0; i < 3; i++) {
      const hx = W - 43 - i * 28;
      drawHeart(hx, 48, 10, i < lives);
    }

    const pw = gameTime < wideUntil ? wideUntil - gameTime : 0;
    if (pw > 0) {
      roundedRect(W / 2 - 79, 66, 158, 22, 11);
      ctx.fillStyle = "rgba(42,225,255,.10)"; ctx.fill();
      ctx.strokeStyle = "rgba(75,234,255,.42)"; ctx.lineWidth = 1; ctx.stroke();
      ctx.textAlign = "center"; ctx.font = "700 10px Inter, system-ui, sans-serif";
      ctx.fillStyle = "#72f4ff";
      ctx.fillText("WIDE  " + pw.toFixed(1) + "s", W / 2, 80);
    } else if (balls.length > 1) {
      roundedRect(W / 2 - 60, 66, 120, 22, 11);
      ctx.fillStyle = "rgba(193,125,255,.10)"; ctx.fill();
      ctx.strokeStyle = "rgba(193,125,255,.4)"; ctx.stroke();
      ctx.textAlign = "center"; ctx.font = "700 10px Inter, system-ui, sans-serif";
      ctx.fillStyle = "#dcaaff";
      ctx.fillText("MULTI-BALL", W / 2, 80);
    }
    ctx.restore();
  }

  function drawHeart(x, y, r, filled) {
    ctx.save();
    ctx.translate(x, y);
    ctx.beginPath();
    ctx.moveTo(0, r * .75);
    ctx.bezierCurveTo(-r * 1.4, -r * .25, -r * .65, -r * 1.25, 0, -r * .5);
    ctx.bezierCurveTo(r * .65, -r * 1.25, r * 1.4, -r * .25, 0, r * .75);
    ctx.closePath();
    if (filled) {
      ctx.fillStyle = "#ff5c93"; ctx.shadowColor = "#ff4f91"; ctx.shadowBlur = 11; ctx.fill();
    } else {
      ctx.strokeStyle = "#40495f"; ctx.lineWidth = 1.5; ctx.stroke();
    }
    ctx.restore();
  }

  function drawPaddle() {
    ctx.save();
    const g = ctx.createLinearGradient(0, paddle.y, 0, paddle.y + paddle.h);
    g.addColorStop(0, "#c8fbff"); g.addColorStop(.35, "#50e8ff"); g.addColorStop(1, "#5570ff");
    ctx.shadowColor = gameTime < wideUntil ? "#42eaff" : "#697cff";
    ctx.shadowBlur = 20;
    roundedRect(paddle.x, paddle.y, paddle.w, paddle.h, 7);
    ctx.fillStyle = g; ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = "rgba(255,255,255,.7)";
    roundedRect(paddle.x + 7, paddle.y + 2, paddle.w - 14, 3, 2); ctx.fill();
    ctx.strokeStyle = "rgba(255,255,255,.6)"; ctx.lineWidth = 1;
    roundedRect(paddle.x, paddle.y, paddle.w, paddle.h, 7); ctx.stroke();
    ctx.restore();
  }

  function drawBall(b) {
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i];
      const a = (i / b.trail.length) * .22;
      ctx.fillStyle = "rgba(80,225,255," + a + ")";
      ctx.beginPath(); ctx.arc(t.x, t.y, b.r * (i / b.trail.length) * .9, 0, Math.PI * 2); ctx.fill();
    }
    ctx.save();
    ctx.shadowColor = "#60eeff"; ctx.shadowBlur = 18;
    const g = ctx.createRadialGradient(b.x - 2, b.y - 3, 1, b.x, b.y, b.r);
    g.addColorStop(0, "#ffffff"); g.addColorStop(.35, "#b8fbff"); g.addColorStop(1, "#38bfff");
    ctx.fillStyle = g;
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();
    ctx.restore();
  }

  function drawPowerup(p) {
    const color = p.type === "wide" ? "#55eeff" : "#ce8dff";
    ctx.save();
    ctx.translate(p.x, p.y);
    ctx.shadowColor = color; ctx.shadowBlur = 18;
    ctx.fillStyle = "rgba(9,13,31,.94)";
    ctx.strokeStyle = color; ctx.lineWidth = 2;
    roundedRect(-17, -17, 34, 34, 10); ctx.fill(); ctx.stroke();
    drawPowerIcon(p.type, 0, 0, 7);
    ctx.restore();
  }

  function drawFloaters() {
    ctx.save();
    ctx.textAlign = "center";
    for (const f of floaters) {
      const alpha = Math.min(1, f.life / (f.max * .5));
      ctx.globalAlpha = alpha;
      ctx.font = "800 " + f.size + "px Inter, system-ui, sans-serif";
      ctx.fillStyle = f.color;
      ctx.shadowColor = f.color; ctx.shadowBlur = 10;
      ctx.fillText(f.text, f.x, f.y);
    }
    ctx.restore();
  }

  function drawDemoBanner() {
    ctx.save();
    const pulse = .78 + Math.sin(gameTime * 3) * .12;
    roundedRect(W / 2 - 183, FIELD_TOP + 7, 366, 30, 15);
    ctx.fillStyle = "rgba(11,16,37,.82)"; ctx.fill();
    ctx.strokeStyle = "rgba(97,226,255,.34)"; ctx.lineWidth = 1; ctx.stroke();
    ctx.textAlign = "center";
    ctx.globalAlpha = pulse;
    ctx.font = "800 11px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#8defff";
    ctx.fillText("AI DEMO  ·  CLICK OR PRESS A KEY TO START", W / 2, FIELD_TOP + 26);
    ctx.restore();
  }

  function drawOverlay() {
    const win = state === "victory";
    ctx.save();
    ctx.fillStyle = "rgba(4,7,18,.77)";
    ctx.fillRect(0, 0, W, H);
    ctx.textAlign = "center";
    ctx.shadowColor = win ? "#62eaff" : "#ff5e98";
    ctx.shadowBlur = 28;
    ctx.font = "900 50px Inter, system-ui, sans-serif";
    ctx.fillStyle = win ? "#7df2ff" : "#ff75aa";
    ctx.fillText(win ? "LEVEL CLEARED!" : "GAME OVER", W / 2, 235);
    ctx.shadowBlur = 0;
    ctx.font = "500 16px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#aeb8d4";
    ctx.fillText(win ? "Every brick shattered. Outstanding work." : "The neon wall got the better of you.", W / 2, 275);
    ctx.font = "800 19px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#ffffff";
    ctx.fillText("SCORE  " + String(score).padStart(5, "0"), W / 2, 313);

    const bw = 190, bh = 48, bx = W / 2 - bw / 2, by = 340;
    ctx.shadowColor = win ? "#39dcff" : "#865dff"; ctx.shadowBlur = 20;
    const g = ctx.createLinearGradient(bx, by, bx + bw, by + bh);
    g.addColorStop(0, win ? "#1ebfd9" : "#6550e8"); g.addColorStop(1, win ? "#5979ff" : "#a44ddd");
    roundedRect(bx, by, bw, bh, 10); ctx.fillStyle = g; ctx.fill();
    ctx.shadowBlur = 0;
    ctx.strokeStyle = "rgba(255,255,255,.42)"; ctx.lineWidth = 1; ctx.stroke();
    ctx.font = "800 14px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#ffffff";
    ctx.fillText("RESTART", W / 2, by + 30);
    ctx.font = "500 12px Inter, system-ui, sans-serif";
    ctx.fillStyle = "#717d9d";
    ctx.fillText("Click the button or press R", W / 2, by + 73);
    ctx.restore();
  }

  function draw() {
    ctx.save();
    if (shake > 0) ctx.translate((Math.random() - .5) * shake, (Math.random() - .5) * shake);
    drawBackground();
    drawHUD();
    for (const b of bricks) drawBrick(b);
    for (const p of powerups) drawPowerup(p);
    drawPaddle();
    for (const b of balls) drawBall(b);

    ctx.save();
    for (const p of particles) {
      const alpha = Math.max(0, p.life / p.max);
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.shadowColor = p.color; ctx.shadowBlur = 8;
      ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
    }
    ctx.restore();

    drawFloaters();
    if (state === "demo") drawDemoBanner();
    if (state === "gameover" || state === "victory") drawOverlay();
    ctx.restore();
  }

  function loop(now) {
    const dt = Math.min(.033, (now - last) / 1000);
    last = now;
    update(dt);
    draw();
    requestAnimationFrame(loop);
  }

  canvas.addEventListener("pointermove", e => {
    const rect = canvas.getBoundingClientRect();
    mouseX = (e.clientX - rect.left) * (W / rect.width);
    if (pointerDown && state === "playing") {
      const desired = mouseX - paddle.w / 2;
      paddle.x = Math.max(8, Math.min(W - 8 - paddle.w, desired));
    }
  });
  canvas.addEventListener("pointerdown", e => {
    e.preventDefault();
    pointerDown = true;
    canvas.setPointerCapture?.(e.pointerId);
    const rect = canvas.getBoundingClientRect();
    mouseX = (e.clientX - rect.left) * (W / rect.width);
    if (state === "gameover" || state === "victory") {
      const x = mouseX, y = (e.clientY - rect.top) * (H / rect.height);
      if (y >= 340 && y <= 388 && x >= W / 2 - 95 && x <= W / 2 + 95) {
        resetGame(false);
      }
    } else {
      beginPlay();
    }
  });
  canvas.addEventListener("pointerup", () => pointerDown = false);
  canvas.addEventListener("pointercancel", () => pointerDown = false);

  window.addEventListener("keydown", e => {
    if (["ArrowLeft", "ArrowRight", "Space"].includes(e.code)) e.preventDefault();
    if (e.code === "ArrowLeft") keys.left = true;
    if (e.code === "ArrowRight") keys.right = true;
    if (e.code === "KeyR" && (state === "gameover" || state === "victory")) {
      resetGame(false);
      return;
    }
    if (state === "demo") beginPlay();
  });
  window.addEventListener("keyup", e => {
    if (e.code === "ArrowLeft") keys.left = false;
    if (e.code === "ArrowRight") keys.right = false;
  });
  window.addEventListener("blur", () => { keys.left = keys.right = false; });

  resetGame(true);
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (3,180 tokens)
We need produce single HTML complete. Need polished. Plan canvas 1280x800 responsive. Brick rows 8 maybe. Aesthetic neon arcade. Gameplay AI demo mode: paddle controlled by AI until user clicks/keys. Message click or press key to start, demo auto plays behind. Need no console errors. Include lives score, powerups, multi-ball, wider 10 sec, tougher cracks, particles, paddle angles, collision robust, screens.

Implement HTML with canvas fills stage maybe width 960 height 620 inside layout with side panel? Requirement canvas game. Build polished: page background dark gradient, header title, canvas 960x620, HUD integrated canvas or DOM. Better canvas 1000x640 with side DOM panel? For screenshot appeal, whole thing centered with game shell. Canvas 960x600, top HUD drawn on canvas. Right info panel maybe external DOM adds polished but canvas only game. Let's do layout: header with logo "NEON BREAK" and subtitle, canvas card 980x620, HUD drawn canvas. CSS responsive scale via width 100%, aspect ratio.

Gameplay details:
canvas logical W=960 H=620. Playfield from y=70 to 600. Bricks 10 cols x 8 rows, x start 40, width 82 gap 8, row height 24 gap 8. Rows: normal + tough, some power icons (e.g. lightning widen, dots multi). Icons drawn text ⚡ and ⋯? Use vector drawing for bolt and triple-circle.

Ball radius 8, paddle w 120 h 14, y 570. Balls array. speeds ~5.2.

Collision: continuous-ish step substeps to prevent tunneling: move balls in small steps based on speed, detect brick collisions per step using circle-vs-AABB, reflect based on overlap axis. paddle collision circle-AABB, if vy>0 and ball bottom >= paddle top and previous bottom <= paddle top+something. Use position correction. Bounce angle based offset x: angle = offset * 1.05 (max ~70deg) plus possible vertical random. vx = sin(angle)*speed; vy=-cos(angle)*speed. Note angle sign so left hits go left.

Brick collisions: for each ball each substep, test each brick. Choose nearest? Iterate bricks and resolve once per substep. On collision reflect based on penetration (min overlap) and set brick.hp--. If hp still >0 crack. Else break: score normal 50, tough 100, particles, maybe power drop if brick.power.

Powerup drops from brick at spawn x,y velocity 1.5 falls; catch with paddle. Types widen (10s) multi (3 balls => total 3? "multi-ball (3 balls)" means sets to 3 balls) Implement power.multi: when caught, ensure total balls =3 adding at paddle offsets. Wider: paddle base 120 -> 175 for 10 sec, show timer.

Lives: lose ball when all balls gone. 3 lives. Demo AI: paddle target ball with some drift, and auto catch; in demo mode ball count maybe 1. Demo banner "AI DEMO • Click or press a key to play". Any key/click starts user mode. Mouse move only after click? We can listen mousemove on canvas and pointer. If user hasn't started, moving should maybe start? requirement "Click or press a key to start". implement click starts. Keyboard arrow starts and controls. pointermove starts? only click.

AI must be able to clear level for victory screenshot? Demo plays; maybe lose eventually. Self-playing. Ensure balls not trapped. Powerups caught by AI mostly.

Draw polished: background with subtle grid, glow trails, particles, bricks rounded with gradients, HP cracks drawn lines. HUD panel top with SCORE, LIVES, LEVEL, and power status. Since play area bricks starts y=120 to ~340, paddle bottom 570. Top 0-100 HUD.

Canvas dims 960x620. Brick grid 10 cols: margin 42, gap 9, bw = (960-84-81)/10=79.5. 8 rows: top 130, bh 26, gap 9 => ends 130+8*35-9=401. Good.

Draw text and shapes. Font system, no external.

Game states: demo, playing, gameover, victory. On gameover/victory overlay in canvas with restart button hit area. Also a DOM restart button? canvas clickable restart region. Provide "Restart" button drawn and clickable. Also maybe HTML overlay not needed. Canvas click check state and restart region. Keyboard R restarts.

Lives display hearts drawn.

Screen overlays.

Audio? skip.

Pause? P key maybe.

First paint: demo mode visible with message. Demo AI playing from initial. Great.

Responsive: at 375 width canvas scaled, HUD text scale with canvas logical fixed; okay. Shell width min(980, 96vw). Header small. Touch controls? add pointer move on canvas, click to start. Provide touch move maybe pointerdown start and pointermove while down.

Arrow keys prevent default.

Collision robust: substepping number = ceil(speed*2) i.e speed 6 * dt? Use dt seconds clamped .033; move per frame with substeps count ceil(distance/4). Each substep move by dx=vx*dt/substeps. Brick collision each substep. For circle-AABB: closest point distance. If collision resolve with previous position? Use overlap axis: nearest point, if dist < r. determine normal using dx from nearest. If center inside rect, use min penetration edges. Reflect vx/vy accordingly. To avoid sticking, push out.

Simpler reliable axis method: for each brick, check circle bbox and resolve with axis of minimum overlap between ball and expanded rect: overlapX = (brick.x+ w/2+ r) - abs(bx - cx)? Compute penetration left = bx+r - brick.x, right = brick.x+w-(bx-r), top=by+r-brick.y, bottom=brick.y+h-(by-r). pick min -> reflect and push. This works approximate. Multiple bricks corner maybe one hit per substep. Fine.

Paddle: expand AABB and use similar. Only if vy>0.

Wall collisions left/right/top of play area (y=104). ball below canvas remove.

Demo AI: target lowest descending ball; target x + oscillation? If demo paddle follows perfectly, fine. Occasionally balls multi. Demo can finish quickly — screenshot after 3 sec still bricks. Victory maybe later. Demo shouldn't auto win too fast maybe speed 4.4. Keep.

Power icons visible on bricks: draw bolt / triple dots. Ensure tough bricks distinct with metal gradient and border. Cracks after first hit.

Particles with gravity, glow.

Ball trails: store positions per ball? Each ball object trail array updated each frame.

Score animations maybe floating text.

Powerup falling: draw rounded capsule with icon and label maybe.

Combo? not necessary.

HUD: score left, center level "01", right lives hearts. Top title bar in page already.

CSS polished with gradient, borders, glows, badges.

Let's write code carefully.

Ball cloning multi: balls array objects; add two with angle offsets.

State variables: let state='demo', score=0, lives=3, bricks=[], balls=[], particles=[], powerups=[], floaters=[], paddle, wideUntil=0, shake=0, demo=true? state.

resetGame(startDemo=true): initialize bricks pattern. hp: tough if (r+c)% pattern; tough rows 2-3 etc. power assigned to some normal and tough? requirement some bricks visibly carry icon. Assign power type based on c+r mod.

Initial ball placed above paddle center moving up.

Bricks pattern 8 rows:
rows 0,1 normal mostly; rows 2-4 mix tough; rows 5-7 normal. use tough = (r>=2&&r<=4) || (r+c)%7===0. At least 6 rows yes 8.
Power: (r*10+c) with condition e.g c===2&&r===1 widen; c===6&&r===3 multi; c===4&&r===5 widen; c===8&&r===6 multi; also maybe row0 c7 multi.

Bricks count 80.

Paddle width base 112.

Draw bricks: normal palette by row cyan/purple/pink/blue. Gradient with top highlight. tough silver/violet with segmented? Use fill dark, stroke.

Crack draw: deterministic based on brick seed: lines from center.

Hit animation: brick.flash decays.

When brick destroyed, particles 12.

Score floater.

Powerup drop chance is deterministic brick.power.

Lives: lose one when all balls gone. in demo maybe lives decrease and gameover eventually. Reset? If demo loses, state gameover; then screenshot overlay. Better demo should keep lives replenished? Demo mode can set lives=3 always? On losing all balls in demo, reset ball and lose a life? could go gameover. AI decent. Fine. But maybe demonstration lasts long enough.

User start: clicking resets? Requirement behind message. On click start from demo: continue same state but paddle becomes user controlled, show "PLAYER MODE". Keep score. Key starts. We'll set userActive true and banner disappears.

Arrow keys: paddle moves with velocity key. Mouse: map pointer x to canvas logical coordinate using rect.

We need avoid pointermove changing paddle in demo maybe just after userActive. Actually click sets userActive and pointer.

Keyboard events: arrow key when userActive controls; any key (except maybe) start. Also when keydown arrow start user mode. Space start.

Overlay start banner during demo top area around y=90? There's HUD top 0-90. Banner at playfield top y=112 translucent with "AI DEMO · CLICK OR PRESS A KEY TO PLAY". Keep visible.

Playfield top = 100. Bricks at 132.

Draw particles.

Screen overlays on state gameover/victory with dark translucent and panel.

Restart region coordinates x=W/2-90 y=360 w=180 h=46. canvas clickable. Also text "Press R".

Victory when all bricks destroyed.

Potential issue ball can hit brick repeatedly due to overlap. After reflect push out. Mark brick.flash.

Collision handling per substep: loop bricks and on first collision? Reflect & break; then break out brick loop to avoid multiple. But corner could need multiple. one enough.

Ball removal if y-r>H+20.

Powerup catch: check falling item circle vs paddle rect.

Wide timer based performance.now? Use elapsed sim time `gameTime`. wideUntil=gameTime+10.

Power status drawn on canvas top maybe small pill.

Lives hearts canvas.

Let's ensure no horizontal scrollbar: body overflow hidden? canvas width 100%. shell. Use overflow-x hidden.

Draw rounded rect helper.

Canvas scaling: set canvas.width=960, height=620; style width:100%; aspect-ratio. devicePixelRatio? fixed 960 enough; could set internal to 960*dpr but coords. Keep 960x620 and CSS. Crisp enough.

HTML content includes header title with badge and controls hints.

Implement.

Need exactly one fenced code block, no text.

Let's produce ~700 lines maybe. Keep reasonable.

Details draw paddle gradient, glow.

Ball speed increases slightly per brick? each ball speed base 4.6 + score factor. For user maybe 5.0. Keep speed on hit slightly.

Demo AI paddle target: find ball with vy>0 and lowest y; else first. target x = ball.x + sin(gameTime)*10. ease. Demo collects powerups by target.

Potential demo getting multi-ball and victory too quickly? 80 bricks at 5 speed could take a while. Fine.

Key handling gameover R restart demo? Restart as player maybe. resetGame(false). Clicking restart sets userActive true.

Game state separate 'demo'|'playing'|'gameover'|'victory'. userActive bool.

Click canvas: if gameover/victory and in button -> resetGame(true user). else if state demo -> state playing userActive=true.

Pointerdown also.

Mouse movement after click: pointermove sets mouseX even if user active. Actually if pointer moved without click but demo continues. In playing userActive true, pointermove changes target.

Keyboard arrows: keys.left/right set, movement in update.

Paddle x update: keyboard overrides mouse? combine: if keys then direct speed else if userActive target mouse.

Ball spawn after life loss centered on paddle, vy negative.

If state demo and lives lost, still.

Power multi sets balls to 3 total: while balls.length<3 add new at paddle. Ensure if caught with 1 ball, add 2. Existing ball maybe direction unchanged. New balls angle -0.5, 0.5 upward.

Wider sets wideUntil = gameTime + 10; paddle width calculated dynamic. Need paddle.w updated each frame before collisions. Base x centering? store paddle.cx? Better paddle.x and update width keeping center. function setWidth(w) keep center.

Paddle target x = mouseX - w/2.

Collisions with paddle: ball bottom crosses. Use expanded rect method might reflect even side. Restrict if by + r >= paddle.y and by - r <= paddle.y+ph and ball.vy>0 and previous? We'll store oldY before substep. if ball.vy>0 && oldY+r <= paddle.y+16 && newY+r>=paddle.y and x overlap. Then compute offset = clamp((bx-(px+pw/2))/(pw/2),-1,1), angle=offset*1.15, speed = hypot, vx=sin(angle)*speed, vy=-abs(cos(angle))*speed. Push by = paddle.y - r - 0.1. This is robust. Side hits? paddle wide, ball falling; fine.

Brick collision with expanded rect: expanded rect bx from brick.x-r to brick.x+w+r; if ball center inside expanded rect. min pen top/bottom/left/right. This may trigger near corners with rectangle corner, acceptable. Determine penLeft = (brick.x - r) - bx? Let's define distances:
penLeft = bx - (brick.x - r) if positive (center left of expanded left)
penRight = (brick.x+w+r) - bx
penTop = by - (brick.y-r)
penBottom = (brick.y+h+r) - by
All positive if inside expanded rect. choose min.
If min top: by = brick.y-r; vy=-abs(vy). bottom: by=brick.y+h+r; vy=abs(vy). left/right reflect x.
Because ball center usually outside brick but within r.

Only hit if ball moving toward? Avoid repeated: after reflect outward, next substep won't overlap. Could still hit adjacent.

Brick hp decrement. For tough 2 hits, after first remains crack and hp=1. Score maybe +10 for damage, full points on break.

Victory check after update.

Particle cap.

Drawing particles circles with alpha.

Draw background: gradient, grid lines, side decorative pillars maybe.

Let's code.