Models 44

DeepSeek V4.1 Flash 1017

by DeepSeekReasoningClean
Open in new window

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

How it went

Total time
39.8 s
Cost
$0.0064
Output tokens
12,779
Spent thinking
0 tokens
Speed
325.3 tok/s
Console errors
0
First token after
531 ms
First visible text after
531 ms
Prompt tokens
534
Code size
35.7 KB
Finish reason
stop
Served by
Wafer
Model id
deepseek/deepseek-v4.1-flash-1017
Reasoning sent
{"effort":"max"}
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 · 1224 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    height: 100%;
    background: #0a0e1a;
    overflow: hidden;
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    -webkit-font-smoothing: antialiased;
  }
  #wrap {
    position: relative;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background:
      radial-gradient(circle at 50% 0%, #16203a 0%, #0a0e1a 55%, #05070d 100%);
  }
  canvas {
    display: block;
    border-radius: 14px;
    box-shadow:
      0 0 0 1px rgba(120,180,255,.18),
      0 0 60px rgba(60,120,255,.22),
      0 24px 70px rgba(0,0,0,.75);
    touch-action: none;
    max-width: 100%;
    max-height: 100%;
  }
  /* click-to-start overlay */
  #startOverlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
    pointer-events: none;
    opacity: 1;
    transition: opacity .35s ease;
    z-index: 5;
  }
  #startOverlay.hide { opacity: 0; }
  .start-card {
    padding: 26px 44px;
    border-radius: 18px;
    background: rgba(8,13,26,.78);
    border: 1px solid rgba(110,170,255,.35);
    box-shadow: 0 0 50px rgba(70,130,255,.35), inset 0 0 40px rgba(70,130,255,.08);
    backdrop-filter: blur(6px);
    text-align: center;
  }
  .start-card h1 {
    font-size: clamp(22px, 4.2vw, 40px);
    letter-spacing: .16em;
    text-transform: uppercase;
    font-weight: 800;
    background: linear-gradient(180deg, #eaf3ff, #7fb6ff 55%, #3f7de0);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    filter: drop-shadow(0 2px 12px rgba(90,150,255,.55));
  }
  .start-card p {
    margin-top: 12px;
    font-size: clamp(12px, 1.6vw, 15px);
    color: #9fb6d6;
    letter-spacing: .06em;
  }
  .pulse { animation: pulse 1.6s ease-in-out infinite; }
  @keyframes pulse { 0%,100% { opacity: .55 } 50% { opacity: 1 } }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game"></canvas>
  <div id="startOverlay">
    <div class="start-card">
      <h1>Breakout</h1>
      <p class="pulse">Click or press a key to start</p>
      <p style="margin-top:6px;font-size:12px;opacity:.55">Mouse or ← → to move · Catch power-ups</p>
    </div>
  </div>
</div>

<script>
(function () {
  'use strict';

  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const startOverlay = document.getElementById('startOverlay');

  // ---------------------------------------------------------------- constants
  const W = 960;
  const H = 640;
  const HUD_H = 54;

  const COLS = 11;
  const ROWS = 7;
  const BRICK_TOP = HUD_H + 46;
  const BRICK_GAP = 6;
  const BRICK_H = 24;
  const FIELD_LEFT = 26;
  const FIELD_RIGHT = W - 26;
  const BRICK_W = (FIELD_RIGHT - FIELD_LEFT - BRICK_GAP * (COLS - 1)) / COLS;

  const PADDLE_Y = H - 46;
  const PADDLE_H = 14;
  const PADDLE_W_BASE = 112;
  const PADDLE_W_WIDE = 176;

  const BALL_R = 8;
  const BALL_SPEED = 430;
  const MAX_BOUNCE = 62 * Math.PI / 180; // sharp angle at paddle edges

  const COLORS = [
    { base: '#ff4d6d', light: '#ff8fa3', dark: '#b31f3c' },
    { base: '#ff9f43', light: '#ffc98a', dark: '#b96a13' },
    { base: '#ffd93d', light: '#fff08a', dark: '#b89a10' },
    { base: '#4ade80', light: '#9bf0ba', dark: '#1e8f4a' },
    { base: '#38bdf8', light: '#93dcff', dark: '#1072a8' },
    { base: '#a78bfa', light: '#cdbcff', dark: '#6544c9' },
    { base: '#f472b6', light: '#ffb0d4', dark: '#b02f76' }
  ];

  // ---------------------------------------------------------------- state
  let state = 'demo';        // demo | playing | gameover | victory
  let started = false;
  let demoAI = true;

  let score = 0;
  let lives = 3;
  let level = 1;

  let paddle = {
    x: W / 2,
    w: PADDLE_W_BASE,
    targetW: PADDLE_W_BASE,
    vx: 0
  };
  let keyLeft = false, keyRight = false;
  let mouseX = null;

  let balls = [];
  let bricks = [];
  let particles = [];
  let powerups = [];
  let floaters = [];

  let wideTimer = 0;             // seconds remaining on wide paddle
  let shake = 0;
  let flash = 0;
  let time = 0;
  let launchCooldown = 0;
  let banner = null;             // {text, t}

  // ---------------------------------------------------------------- helpers
  const rand = (a, b) => a + Math.random() * (b - a);
  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;

  function roundRect(c, x, y, w, h, r) {
    r = Math.min(r, w / 2, h / 2);
    c.beginPath();
    c.moveTo(x + r, y);
    c.arcTo(x + w, y, x + w, y + h, r);
    c.arcTo(x + w, y + h, x, y + h, r);
    c.arcTo(x, y + h, x, y, r);
    c.arcTo(x, y, x + w, y, r);
    c.closePath();
  }

  // ---------------------------------------------------------------- level
  // types: 1 = normal, 2 = tough (2 hits), 3 = tough + powerup, 4 = normal + powerup
  const LEVEL_MAP = [
    [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
    [1, 1, 3, 1, 1, 1, 1, 1, 4, 1, 1],
    [1, 2, 1, 1, 3, 2, 2, 4, 1, 1, 2],
    [2, 1, 1, 1, 2, 1, 1, 2, 1, 3, 1],
    [1, 1, 4, 1, 1, 2, 1, 1, 1, 1, 1],
    [1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1],
    [1, 1, 1, 3, 1, 1, 4, 1, 1, 1, 1]
  ];

  function buildLevel() {
    bricks = [];
    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        const t = LEVEL_MAP[r][c];
        if (!t) continue;
        const tough = (t === 2 || t === 3);
        bricks.push({
          x: FIELD_LEFT + c * (BRICK_W + BRICK_GAP),
          y: BRICK_TOP + r * (BRICK_H + BRICK_GAP),
          w: BRICK_W,
          h: BRICK_H,
          color: COLORS[r % COLORS.length],
          hp: tough ? 2 : 1,
          maxHp: tough ? 2 : 1,
          power: (t === 3 || t === 4) ? (Math.random() < 0.5 ? 'wide' : 'multi') : null,
          alive: true,
          flashT: 0,
          shakeT: 0,
          hitT: 0
        });
      }
    }
  }

  // ---------------------------------------------------------------- balls
  function makeBall(x, y, angle, speed) {
    return {
      x, y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      r: BALL_R,
      trail: [],
      stuck: false
    };
  }

  function resetBallOnPaddle() {
    balls = [{
      x: paddle.x,
      y: PADDLE_Y - BALL_R - 2,
      vx: 0,
      vy: 0,
      r: BALL_R,
      trail: [],
      stuck: true
    }];
    launchCooldown = 0.15;
  }

  function launchBalls() {
    let launched = false;
    for (const b of balls) {
      if (b.stuck) {
        const a = -Math.PI / 2 + rand(-0.32, 0.32);
        b.vx = Math.cos(a) * BALL_SPEED;
        b.vy = Math.sin(a) * BALL_SPEED;
        b.stuck = false;
        launched = true;
      }
    }
    if (launched) launchCooldown = 0.12;
  }

  // ---------------------------------------------------------------- game flow
  function resetGame() {
    score = 0;
    lives = 3;
    level = 1;
    wideTimer = 0;
    paddle.w = PADDLE_W_BASE;
    paddle.targetW = PADDLE_W_BASE;
    paddle.x = W / 2;
    particles = [];
    powerups = [];
    floaters = [];
    buildLevel();
    resetBallOnPaddle();
    shake = 0;
    flash = 0;
    banner = null;
    state = 'playing';
  }

  function startGame() {
    if (started) return;
    started = true;
    demoAI = false;
    state = 'playing';
    startOverlay.classList.add('hide');
    resetGame();
    // auto-launch shortly so the very first moment is lively
    setTimeout(() => { if (state === 'playing') launchBalls(); }, 550);
  }

  // ---------------------------------------------------------------- input
  function onMove(clientX) {
    const rect = canvas.getBoundingClientRect();
    const sx = (clientX - rect.left) / rect.width * W;
    mouseX = sx;
    if (!demoAI) paddle.x = clamp(sx, paddle.w / 2, W - paddle.w / 2);
  }

  canvas.addEventListener('mousemove', e => onMove(e.clientX));
  canvas.addEventListener('touchmove', e => {
    if (e.touches.length) { onMove(e.touches[0].clientX); e.preventDefault(); }
  }, { passive: false });

  canvas.addEventListener('pointerdown', e => {
    if (!started) { startGame(); return; }
    launchBalls();
  });

  window.addEventListener('mousedown', e => {
    if (!started) { startGame(); return; }
  });

  window.addEventListener('keydown', e => {
    if (!started) { startGame(); }
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') { keyLeft = true; e.preventDefault(); }
    if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { keyRight = true; e.preventDefault(); }
    if (e.key === ' ' || e.key === 'Spacebar' || e.key === 'Enter') {
      e.preventDefault();
      if (state === 'gameover' || state === 'victory') { resetGame(); }
      else launchBalls();
    }
    if ((e.key === 'r' || e.key === 'R') && (state === 'gameover' || state === 'victory')) {
      resetGame();
    }
  });

  window.addEventListener('keyup', e => {
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keyLeft = false;
    if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keyRight = false;
  });

  // ---------------------------------------------------------------- particles
  function spawnParticles(x, y, color, n) {
    for (let i = 0; i < n; i++) {
      const a = rand(0, Math.PI * 2);
      const sp = rand(50, 330);
      particles.push({
        x, y,
        vx: Math.cos(a) * sp,
        vy: Math.sin(a) * sp - 40,
        life: rand(0.35, 0.85),
        maxLife: 0.85,
        size: rand(1.6, 4.2),
        color,
        rot: rand(0, 6.28),
        rotV: rand(-10, 10)
      });
    }
  }

  function spawnFloater(x, y, text, color) {
    floaters.push({ x, y, text, color, t: 0, life: 0.9 });
  }

  // ---------------------------------------------------------------- collision
  // circle vs AABB, resolve minimal penetration and reflect
  function circleRectResolve(ball, rx, ry, rw, rh) {
    const cx = clamp(ball.x, rx, rx + rw);
    const cy = clamp(ball.y, ry, ry + rh);
    const dx = ball.x - cx;
    const dy = ball.y - cy;
    const d2 = dx * dx + dy * dy;
    if (d2 > ball.r * ball.r) return null;

    let nx, ny, pen;
    if (d2 > 0.0001) {
      const d = Math.sqrt(d2);
      nx = dx / d; ny = dy / d;
      pen = ball.r - d;
    } else {
      // ball centre inside rect: push out along smallest axis
      const left = ball.x - rx, right = rx + rw - ball.x;
      const top = ball.y - ry, bottom = ry + rh - ball.y;
      const m = Math.min(left, right, top, bottom);
      if (m === left) { nx = -1; ny = 0; pen = left + ball.r; }
      else if (m === right) { nx = 1; ny = 0; pen = right + ball.r; }
      else if (m === top) { nx = 0; ny = -1; pen = top + ball.r; }
      else { nx = 0; ny = 1; pen = bottom + ball.r; }
    }
    return { nx, ny, pen };
  }

  function reflect(ball, nx, ny, pen) {
    ball.x += nx * (pen + 0.01);
    ball.y += ny * (pen + 0.01);
    const dot = ball.vx * nx + ball.vy * ny;
    if (dot < 0) {
      ball.vx -= 2 * dot * nx;
      ball.vy -= 2 * dot * ny;
    }
  }

  // substepping so the ball can never tunnel through bricks/paddle
  function moveBall(ball, dt) {
    const speed = Math.hypot(ball.vx, ball.vy);
    const dist = speed * dt;
    const steps = Math.max(1, Math.ceil(dist / (ball.r * 0.6)));
    const sdt = dt / steps;

    for (let s = 0; s < steps; s++) {
      ball.x += ball.vx * sdt;
      ball.y += ball.vy * sdt;

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

      // paddle
      const px = paddle.x - paddle.w / 2;
      const py = PADDLE_Y;
      const hit = circleRectResolve(ball, px, py, paddle.w, PADDLE_H);
      if (hit && ball.vy > 0) {
        ball.y = py - ball.r - 0.5;
        // reflection angle depends on hit position
        const rel = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
        const ang = -Math.PI / 2 + rel * MAX_BOUNCE;
        const sp = clamp(Math.hypot(ball.vx, ball.vy) * 1.008, BALL_SPEED, BALL_SPEED * 1.55);
        ball.vx = Math.cos(ang) * sp;
        ball.vy = Math.sin(ang) * sp;
        // slight influence from paddle motion
        ball.vx += paddle.vx * 0.06;
        spawnParticles(ball.x, py, '#7fd0ff', 7);
      }

      // bricks
      for (const br of bricks) {
        if (!br.alive) continue;
        const h = circleRectResolve(ball, br.x, br.y, br.w, br.h);
        if (h) {
          reflect(ball, h.nx, h.ny, h.pen);
          hitBrick(br, ball);
          break;
        }
      }

      // safety: keep inside field
      if (ball.y > H + 60) return false;
    }
    return true;
  }

  function hitBrick(br, ball) {
    br.hp--;
    br.flashT = 0.16;
    br.shakeT = 0.18;
    if (br.hp <= 0) {
      br.alive = false;
      score += br.maxHp === 2 ? 150 : 100;
      spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color.base, 22);
      spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color.light, 10);
      if (br.power) {
        powerups.push({
          x: br.x + br.w / 2,
          y: br.y + br.h / 2,
          vy: 120,
          kind: br.power,
          t: 0,
          r: 15
        });
      }
      shake = Math.min(shake + 2.5, 9);
      checkVictory();
    } else {
      spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color.light, 8);
      score += 25;
    }
  }

  function checkVictory() {
    if (bricks.every(b => !b.alive) && state === 'playing') {
      state = 'victory';
      flash = 0.6;
      mouseX = mouseX;
    }
  }

  // ---------------------------------------------------------------- power-ups
  function collectPowerup(p) {
    if (p.kind === 'wide') {
      wideTimer = 10;
      paddle.targetW = PADDLE_W_WIDE;
      spawnFloater(paddle.x, PADDLE_Y - 26, 'WIDE PADDLE', '#7dffb0');
      banner = { text: 'WIDE PADDLE', t: 1.4, color: '#7dffb0' };
    } else {
      spawnFloater(paddle.x, PADDLE_Y - 26, 'MULTI-BALL', '#8fd0ff');
      banner = { text: 'MULTI-BALL', t: 1.4, color: '#8fd0ff' };
      const src = balls.find(b => !b.stuck) || balls[0];
      const bx = src ? src.x : paddle.x;
      const by = src ? src.y : PADDLE_Y - 30;
      const baseSp = BALL_SPEED;
      for (let i = 0; i < 2; i++) {
        const a = -Math.PI / 2 + (i === 0 ? -0.72 : 0.72) + rand(-0.12, 0.12);
        balls.push(makeBall(bx, by, a, baseSp));
      }
    }
    score += 75;
    flash = Math.max(flash, 0.18);
  }

  // ---------------------------------------------------------------- update
  function update(dt) {
    time += dt;
    if (shake > 0) shake = Math.max(0, shake - dt * 26);
    if (flash > 0) flash = Math.max(0, flash - dt * 2.2);
    if (banner) { banner.t -= dt; if (banner.t <= 0) banner = null; }

    for (const br of bricks) {
      if (br.flashT > 0) br.flashT -= dt;
      if (br.shakeT > 0) br.shakeT -= dt;
    }

    // paddle target width
    if (wideTimer > 0) {
      wideTimer -= dt;
      if (wideTimer <= 0) {
        wideTimer = 0;
        paddle.targetW = PADDLE_W_BASE;
      }
    }
    paddle.w += (paddle.targetW - paddle.w) * Math.min(1, dt * 10);
    paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);

    // paddle movement
    const prevX = paddle.x;
    const keySpeed = 620;
    if (state === 'playing' || state === 'demo') {
      if (keyLeft && !keyRight) paddle.x -= keySpeed * dt;
      else if (keyRight && !keyLeft) paddle.x += keySpeed * dt;
    }

    // demo AI
    if (demoAI && started === false) {
      const active = balls.filter(b => !b.stuck);
      let target = W / 2;
      if (active.length) {
        // aim at the lowest ball, predicting near paddle line
        let best = active[0];
        for (const b of active) if (b.y > best.y) best = b;
        const lead = best.x + best.vx * 0.16;
        target = lead;
        // avoid edges
        target = clamp(target, paddle.w / 2 + 12, W - paddle.w / 2 - 12);
      } else {
        target = paddle.x + Math.sin(time * 0.6) * 20;
      }
      const diff = target - paddle.x;
      paddle.x += clamp(diff, -640 * dt, 640 * dt);
    }

    paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
    paddle.vx = (paddle.x - prevX) / Math.max(dt, 0.0001);

    // balls
    if (launchCooldown > 0) launchCooldown -= dt;

    for (let i = balls.length - 1; i >= 0; i--) {
      const b = balls[i];
      if (b.stuck) {
        b.x = paddle.x;
        b.y = PADDLE_Y - b.r - 2;
        if (state === 'demo' && launchCooldown <= 0) launchBalls();
        continue;
      }
      const alive = moveBall(b, dt);

      // trail
      b.trail.push({ x: b.x, y: b.y });
      if (b.trail.length > 9) b.trail.shift();

      if (!alive) {
        spawnParticles(b.x, H - 8, '#5b7fb5', 14);
        balls.splice(i, 1);
      }
    }

    if (balls.length === 0 && state === 'playing') {
      lives--;
      if (lives <= 0) {
        state = 'gameover';
        shake = 8;
      } else {
        wideTimer = 0;
        paddle.targetW = PADDLE_W_BASE;
        resetBallOnPaddle();
        banner = { text: 'BALL LOST', t: 1.1, color: '#ff8fa3' };
      }
    }

    // powerups
    for (let i = powerups.length - 1; i >= 0; i--) {
      const p = powerups[i];
      p.t += dt;
      p.y += p.vy * dt;
      p.vy = Math.min(p.vy + 40 * dt, 240);
      if (p.y - p.r > H) { powerups.splice(i, 1); continue; }
      // catch with paddle
      if (p.y + p.r > PADDLE_Y && p.y - p.r < PADDLE_Y + PADDLE_H &&
          p.x > paddle.x - paddle.w / 2 - p.r && p.x < paddle.x + paddle.w / 2 + p.r) {
        collectPowerup(p);
        spawnParticles(p.x, p.y, p.kind === 'wide' ? '#7dffb0' : '#8fd0ff', 18);
        powerups.splice(i, 1);
        continue;
      }
    }

    // particles
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life -= dt;
      if (p.life <= 0) { particles.splice(i, 1); continue; }
      p.vy += 620 * dt;
      p.vx *= (1 - 1.1 * dt);
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.rot += p.rotV * dt;
    }

    // floaters
    for (let i = floaters.length - 1; i >= 0; i--) {
      const f = floaters[i];
      f.t += dt;
      f.y -= 42 * dt;
      if (f.t > f.life) floaters.splice(i, 1);
    }
  }

  // ---------------------------------------------------------------- drawing
  function drawBackground() {
    // deep gradient
    const g = ctx.createLinearGradient(0, 0, 0, H);
    g.addColorStop(0, '#111a2e');
    g.addColorStop(0.55, '#0c1221');
    g.addColorStop(1, '#070a13');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);

    // top glow
    const rg = ctx.createRadialGradient(W / 2, HUD_H + 40, 10, W / 2, HUD_H + 40, 520);
    rg.addColorStop(0, 'rgba(70,130,255,.10)');
    rg.addColorStop(1, 'rgba(70,130,255,0)');
    ctx.fillStyle = rg;
    ctx.fillRect(0, 0, W, H);

    // subtle grid
    ctx.strokeStyle = 'rgba(90,140,220,.055)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    for (let x = 0; x <= W; x += 48) { ctx.moveTo(x + 0.5, HUD_H); ctx.lineTo(x + 0.5, H); }
    for (let y = HUD_H; y <= H; y += 48) { ctx.moveTo(0, y + 0.5); ctx.lineTo(W, y + 0.5); }
    ctx.stroke();

    // side rails
    ctx.fillStyle = 'rgba(120,180,255,.07)';
    ctx.fillRect(0, HUD_H, 6, H - HUD_H);
    ctx.fillRect(W - 6, HUD_H, 6, H - HUD_H);
  }

  function drawHUD() {
    ctx.save();
    // bar
    const g = ctx.createLinearGradient(0, 0, 0, HUD_H);
    g.addColorStop(0, 'rgba(20,32,58,.95)');
    g.addColorStop(1, 'rgba(12,19,34,.95)');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, HUD_H);
    ctx.fillStyle = 'rgba(110,170,255,.22)';
    ctx.fillRect(0, HUD_H - 1, W, 1);

    // score
    ctx.textBaseline = 'middle';
    ctx.textAlign = 'left';
    ctx.font = '600 13px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = 'rgba(150,180,220,.7)';
    ctx.fillText('SCORE', 26, HUD_H / 2 - 11);
    ctx.font = '800 25px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = '#eaf3ff';
    ctx.fillText(String(score).padStart(5, '0'), 26, HUD_H / 2 + 9);

    // lives
    ctx.textAlign = 'right';
    ctx.font = '600 13px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = 'rgba(150,180,220,.7)';
    ctx.fillText('LIVES', W - 26, HUD_H / 2 - 11);
    for (let i = 0; i < 3; i++) {
      const bx = W - 26 - (2 - i) * 26;
      const by = HUD_H / 2 + 10;
      ctx.beginPath();
      ctx.arc(bx, by, 6.5, 0, Math.PI * 2);
      if (i < lives) {
        const bg = ctx.createRadialGradient(bx - 2, by - 2, 1, bx, by, 7);
        bg.addColorStop(0, '#cfe6ff');
        bg.addColorStop(1, '#4a9bff');
        ctx.fillStyle = bg;
        ctx.fill();
        ctx.shadowColor = '#4a9bff';
        ctx.shadowBlur = 10;
        ctx.fill();
        ctx.shadowBlur = 0;
      } else {
        ctx.strokeStyle = 'rgba(120,160,210,.28)';
        ctx.lineWidth = 1.4;
        ctx.stroke();
      }
    }

    // level pill
    ctx.textAlign = 'center';
    ctx.font = '700 12px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = 'rgba(150,180,220,.55)';
    ctx.fillText('LEVEL ' + level, W / 2, HUD_H / 2);

    // powerup timer bar (center, under level text)
    if (wideTimer > 0) {
      const bw = 132, bh = 5;
      const bx = W / 2 - bw / 2, by = HUD_H / 2 + 12;
      ctx.fillStyle = 'rgba(125,255,176,.16)';
      roundRect(ctx, bx, by, bw, bh, 3);
      ctx.fill();
      ctx.fillStyle = '#7dffb0';
      roundRect(ctx, bx, by, bw * (wideTimer / 10), bh, 3);
      ctx.fill();
      ctx.shadowColor = '#7dffb0';
      ctx.shadowBlur = 8;
      ctx.fill();
      ctx.shadowBlur = 0;
    }
    ctx.restore();
  }

  function drawBrick(br) {
    let ox = 0, oy = 0;
    if (br.shakeT > 0) {
      const k = br.shakeT / 0.18;
      ox = rand(-2.6, 2.6) * k;
      oy = rand(-1.6, 1.6) * k;
    }
    const x = br.x + ox, y = br.y + oy, w = br.w, h = br.h;
    const col = br.color;
    const cracked = br.hp < br.maxHp;

    ctx.save();

    // body
    const g = ctx.createLinearGradient(x, y, x, y + h);
    if (cracked) {
      g.addColorStop(0, 'rgba(70,88,120,.95)');
      g.addColorStop(1, 'rgba(38,50,74,.95)');
    } else {
      g.addColorStop(0, col.light);
      g.addColorStop(0.42, col.base);
      g.addColorStop(1, col.dark);
    }
    ctx.fillStyle = g;
    roundRect(ctx, x, y, w, h, 5);
    ctx.fill();

    // top highlight
    if (!cracked) {
      ctx.fillStyle = 'rgba(255,255,255,.28)';
      roundRect(ctx, x + 2, y + 2, w - 4, h * 0.32, 3);
      ctx.fill();
    }

    // border
    ctx.strokeStyle = cracked ? 'rgba(150,180,220,.35)' : 'rgba(0,0,0,.28)';
    ctx.lineWidth = 1;
    roundRect(ctx, x + 0.5, y + 0.5, w - 1, h - 1, 5);
    ctx.stroke();

    // cracks
    if (cracked) {
      ctx.strokeStyle = 'rgba(200,220,255,.75)';
      ctx.lineWidth = 1.3;
      ctx.beginPath();
      const cx = x + w * 0.32, cy = y + h * 0.18;
      ctx.moveTo(cx, cy);
      ctx.lineTo(cx + 8, cy + 6);
      ctx.lineTo(cx + 3, cy + 13);
      ctx.lineTo(cx + 12, cy + h - 3);
      ctx.stroke();
      ctx.beginPath();
      const cx2 = x + w * 0.68, cy2 = y + h * 0.92;
      ctx.moveTo(cx2, cy2);
      ctx.lineTo(cx2 - 7, cy2 - 7);
      ctx.lineTo(cx2 - 1, cy2 - 14);
      ctx.stroke();
      ctx.beginPath();
      const cx3 = x + w * 0.52, cy3 = y + h * 0.1;
      ctx.moveTo(cx3, cy3);
      ctx.lineTo(cx3 + 5, cy3 + 9);
      ctx.stroke();
    }

    // power-up icon badge
    if (br.power) {
      const cx = x + w / 2, cy = y + h / 2;
      const rr = 9;
      const isWide = br.power === 'wide';
      const ic = isWide ? '#7dffb0' : '#8fd0ff';
      ctx.globalAlpha = cracked ? 0.85 : 1;
      ctx.beginPath();
      ctx.arc(cx, cy, rr, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(8,14,26,.72)';
      ctx.fill();
      ctx.strokeStyle = ic;
      ctx.lineWidth = 1.4;
      ctx.stroke();
      ctx.shadowColor = ic;
      ctx.shadowBlur = 10;
      ctx.stroke();
      ctx.shadowBlur = 0;

      ctx.fillStyle = ic;
      ctx.strokeStyle = ic;
      if (isWide) {
        // two arrows pointing outward
        ctx.lineWidth = 1.8;
        roundRect(ctx, cx - 4.5, cy - 1.6, 9, 3.2, 1.4);
        ctx.fill();
        ctx.beginPath();
        ctx.moveTo(cx - 4.5, cy - 4.6); ctx.lineTo(cx - 7.4, cy); ctx.lineTo(cx - 4.5, cy + 4.6);
        ctx.closePath(); ctx.fill();
        ctx.beginPath();
        ctx.moveTo(cx + 4.5, cy - 4.6); ctx.lineTo(cx + 7.4, cy); ctx.lineTo(cx + 4.5, cy + 4.6);
        ctx.closePath(); ctx.fill();
      } else {
        // three dots = multi ball
        for (let i = 0; i < 3; i++) {
          const a = -Math.PI / 2 + (i - 1) * 0.9;
          ctx.beginPath();
          ctx.arc(cx + Math.cos(a) * 4.2, cy + Math.sin(a) * 4.2 + 1.4, 2.1, 0, Math.PI * 2);
          ctx.fill();
        }
        ctx.beginPath();
        ctx.arc(cx, cy - 4.2, 2.4, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;
    }

    // hit flash
    if (br.flashT > 0) {
      ctx.globalAlpha = clamp(br.flashT / 0.16, 0, 1) * 0.85;
      ctx.fillStyle = '#ffffff';
      roundRect(ctx, x, y, w, h, 5);
      ctx.fill();
      ctx.globalAlpha = 1;
    }

    ctx.restore();
  }

  function drawPaddle() {
    const x = paddle.x - paddle.w / 2;
    const y = PADDLE_Y;
    const w = paddle.w, h = PADDLE_H;

    ctx.save();
    // glow
    ctx.shadowColor = wideTimer > 0 ? '#7dffb0' : '#4a9bff';
    ctx.shadowBlur = 22;
    const g = ctx.createLinearGradient(x, y, x, y + h);
    if (wideTimer > 0) {
      g.addColorStop(0, '#d6ffe8');
      g.addColorStop(0.5, '#7dffb0');
      g.addColorStop(1, '#22a866');
    } else {
      g.addColorStop(0, '#e6f3ff');
      g.addColorStop(0.5, '#78c2ff');
      g.addColorStop(1, '#1f66c9');
    }
    ctx.fillStyle = g;
    roundRect(ctx, x, y, w, h, h / 2);
    ctx.fill();
    ctx.shadowBlur = 0;

    // inner shine
    ctx.fillStyle = 'rgba(255,255,255,.5)';
    roundRect(ctx, x + 6, y + 3, w - 12, 3, 1.5);
    ctx.fill();

    // end caps
    ctx.fillStyle = 'rgba(255,255,255,.35)';
    ctx.beginPath(); ctx.arc(x + h / 2, y + h / 2, 3, 0, Math.PI * 2); ctx.fill();
    ctx.beginPath(); ctx.arc(x + w - h / 2, y + h / 2, 3, 0, Math.PI * 2); ctx.fill();
    ctx.restore();
  }

  function drawBall(b) {
    // trail
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i];
      const k = i / b.trail.length;
      ctx.globalAlpha = k * 0.32;
      ctx.fillStyle = '#7fd0ff';
      ctx.beginPath();
      ctx.arc(t.x, t.y, b.r * (0.35 + k * 0.6), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.save();
    ctx.shadowColor = 'rgba(140,210,255,.95)';
    ctx.shadowBlur = 20;
    const g = ctx.createRadialGradient(b.x - b.r * 0.35, b.y - b.r * 0.4, b.r * 0.1, b.x, b.y, b.r);
    g.addColorStop(0, '#ffffff');
    g.addColorStop(0.5, '#dcefff');
    g.addColorStop(1, '#6fc0ff');
    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function drawPowerups() {
    for (const p of powerups) {
      const isWide = p.kind === 'wide';
      const ic = isWide ? '#7dffb0' : '#8fd0ff';
      const bob = Math.sin(p.t * 6) * 2;
      ctx.save();
      ctx.translate(p.x, p.y + bob);

      ctx.shadowColor = ic;
      ctx.shadowBlur = 18;

      // capsule
      const g = ctx.createLinearGradient(0, -p.r, 0, p.r);
      g.addColorStop(0, 'rgba(20,34,58,.95)');
      g.addColorStop(1, 'rgba(10,18,32,.95)');
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(0, 0, p.r, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = ic;
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.shadowBlur = 0;

      ctx.fillStyle = ic;
      if (isWide) {
        roundRect(ctx, -4.5, -1.6, 9, 3.2, 1.4); ctx.fill();
        ctx.beginPath(); ctx.moveTo(-4.8, -5); ctx.lineTo(-8, 0); ctx.lineTo(-4.8, 5); ctx.closePath(); ctx.fill();
        ctx.beginPath(); ctx.moveTo(4.8, -5); ctx.lineTo(8, 0); ctx.lineTo(4.8, 5); ctx.closePath(); ctx.fill();
      } else {
        ctx.beginPath(); ctx.arc(0, -4.6, 2.6, 0, Math.PI * 2); ctx.fill();
        for (let i = 0; i < 2; i++) {
          const a = Math.PI / 2 + (i === 0 ? -0.8 : 0.8);
          ctx.beginPath();
          ctx.arc(Math.cos(a) * 4.6, Math.sin(a) * 3.4 + 1.5, 2.3, 0, Math.PI * 2);
          ctx.fill();
        }
      }
      ctx.restore();
    }
  }

  function drawParticles() {
    for (const p of particles) {
      const a = clamp(p.life / p.maxLife, 0, 1);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot);
      ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.75);
      ctx.restore();
    }
    ctx.globalAlpha = 1;
  }

  function drawFloaters() {
    for (const f of floaters) {
      const a = 1 - f.t / f.life;
      ctx.globalAlpha = clamp(a, 0, 1);
      ctx.font = '800 16px "Segoe UI", system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillStyle = f.color;
      ctx.shadowColor = f.color;
      ctx.shadowBlur = 12;
      ctx.fillText(f.text, f.x, f.y);
      ctx.shadowBlur = 0;
      ctx.globalAlpha = 1;
    }
  }

  function drawBanner() {
    if (!banner) return;
    const k = clamp(banner.t / 1.4, 0, 1);
    const a = k > 0.75 ? (1 - k) / 0.25 : Math.min(1, k / 0.3);
    ctx.save();
    ctx.globalAlpha = clamp(a, 0, 1);
    ctx.font = '800 20px "Segoe UI", system-ui, sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = banner.color;
    ctx.shadowColor = banner.color;
    ctx.shadowBlur = 16;
    ctx.fillText(banner.text, W / 2, PADDLE_Y - 78);
    ctx.restore();
  }

  function drawEndScreen(victory) {
    ctx.save();
    ctx.fillStyle = 'rgba(5,8,16,.78)';
    ctx.fillRect(0, 0, W, H);

    const cx = W / 2, cy = H / 2;
    const isV = victory;

    // panel
    const pw = 460, ph = 288;
    ctx.save();
    ctx.shadowColor = isV ? 'rgba(125,255,176,.4)' : 'rgba(255,90,120,.35)';
    ctx.shadowBlur = 50;
    const g = ctx.createLinearGradient(0, cy - ph / 2, 0, cy + ph / 2);
    g.addColorStop(0, '#152136');
    g.addColorStop(1, '#0a1120');
    ctx.fillStyle = g;
    roundRect(ctx, cx - pw / 2, cy - ph / 2, pw, ph, 20);
    ctx.fill();
    ctx.restore();
    ctx.strokeStyle = isV ? 'rgba(125,255,176,.55)' : 'rgba(255,120,150,.5)';
    ctx.lineWidth = 1.6;
    roundRect(ctx, cx - pw / 2, cy - ph / 2, pw, ph, 20);
    ctx.stroke();

    // title
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.font = '800 42px "Segoe UI", system-ui, sans-serif';
    const tg = ctx.createLinearGradient(0, cy - 100, 0, cy - 46);
    if (isV) {
      tg.addColorStop(0, '#e8fff2');
      tg.addColorStop(0.55, '#7dffb0');
      tg.addColorStop(1, '#25b06c');
    } else {
      tg.addColorStop(0, '#ffe9ee');
      tg.addColorStop(0.55, '#ff6b8a');
      tg.addColorStop(1, '#b31f3c');
    }
    ctx.fillStyle = tg;
    ctx.shadowColor = isV ? 'rgba(125,255,176,.6)' : 'rgba(255,90,120,.6)';
    ctx.shadowBlur = 26;
    ctx.fillText(isV ? 'VICTORY' : 'GAME OVER', cx, cy - 74);
    ctx.shadowBlur = 0;

    ctx.font = '600 14px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = 'rgba(160,190,225,.75)';
    ctx.fillText(isV ? 'All bricks destroyed — nice work.' : 'You ran out of lives.', cx, cy - 26);

    // score box
    ctx.font = '600 13px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = 'rgba(150,180,220,.6)';
    ctx.fillText('FINAL SCORE', cx, cy + 18);
    ctx.font = '800 40px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = '#eaf3ff';
    ctx.fillText(String(score), cx, cy + 56);

    // restart button
    const bw = 250, bh = 50;
    const bx = cx - bw / 2, by = cy + 88;
    const bg = ctx.createLinearGradient(bx, by, bx, by + bh);
    if (isV) {
      bg.addColorStop(0, '#8dffbe');
      bg.addColorStop(1, '#26a86a');
    } else {
      bg.addColorStop(0, '#7fb6ff');
      bg.addColorStop(1, '#2a63c4');
    }
    ctx.save();
    ctx.shadowColor = isV ? 'rgba(125,255,176,.55)' : 'rgba(90,150,255,.55)';
    ctx.shadowBlur = 24;
    ctx.fillStyle = bg;
    roundRect(ctx, bx, by, bw, bh, 25);
    ctx.fill();
    ctx.restore();
    ctx.strokeStyle = 'rgba(255,255,255,.35)';
    ctx.lineWidth = 1;
    roundRect(ctx, bx, by, bw, bh, 25);
    ctx.stroke();
    ctx.font = '800 16px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = '#08111f';
    ctx.fillText('RESTART  \u21B5', cx, by + bh / 2 + 1);

    ctx.font = '500 12px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = 'rgba(150,180,220,.55)';
    ctx.fillText('Click / press Space or R', cx, by + bh + 22);

    // store button rect for hit test
    endButton = { x: bx, y: by, w: bw, h: bh };
    ctx.restore();
  }

  let endButton = null;

  // ---------------------------------------------------------------- loop
  let lastT = performance.now();
  function frame(now) {
    let dt = (now - lastT) / 1000;
    lastT = now;
    dt = Math.min(dt, 1 / 30);

    update(dt);

    // draw
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.clearRect(0, 0, W, H);

    const sh = shake > 0 ? shake : 0;
    if (sh > 0.05) {
      ctx.translate(rand(-sh, sh) * 0.7, rand(-sh, sh) * 0.7);
    }

    drawBackground();
    drawHUD();
    for (const br of bricks) if (br.alive) drawBrick(br);
    drawPowerups();
    drawParticles();
    drawPaddle();
    for (const b of balls) drawBall(b);
    drawFloaters();
    drawBanner();

    if (flash > 0) {
      ctx.globalAlpha = flash * 0.35;
      ctx.fillStyle = '#aad4ff';
      ctx.fillRect(0, 0, W, H);
      ctx.globalAlpha = 1;
    }

    if (state === 'gameover') drawEndScreen(false);
    else if (state === 'victory') drawEndScreen(true);

    // demo hint on canvas
    if (!started) {
      // keep the demo lively: auto launch
      if (state === 'demo' || !started) {
        // handled in update via launchCooldown
      }
    }

    requestAnimationFrame(frame);
  }

  // demo ball must be launched
  function demoLaunch() {
    if (!started) {
      for (const b of balls) {
        if (b.stuck) {
          const a = -Math.PI / 2 + rand(-0.32, 0.32);
          b.vx = Math.cos(a) * BALL_SPEED;
          b.vy = Math.sin(a) * BALL_SPEED;
          b.stuck = false;
        }
      }
    }
  }

  // ---------------------------------------------------------------- boot
  function resize() {
    const pad = 0;
    const availW = window.innerWidth - pad * 2;
    const availH = window.innerHeight - pad * 2;
    const scale = Math.min(availW / W, availH / H, 1.55);
    canvas.style.width = (W * scale) + 'px';
    canvas.style.height = (H * scale) + 'px';
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = W * dpr;
    canvas.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);

  // init
  resize();
  buildLevel();
  resetBallOnPaddle();
  paddle.x = W / 2;

  // demo mode: auto launch and restart when balls die or level clears
  setInterval(() => {
    if (started) return;
    if (state === 'playing' || state === 'demo') {
      if (state === 'demo') { /* noop */ }
      if (balls.every(b => b.stuck)) demoLaunch();
    }
  }, 400);

  // demo safety: if balls run out before start, respawn; if bricks cleared, rebuild
  const demoTick = setInterval(() => {
    if (started) { clearInterval(demoTick); return; }
    if (balls.length === 0) {
      lives = 3;
      resetBallOnPaddle();
    }
    if (bricks.every(b => !b.alive)) {
      buildLevel();
      powerups = [];
    }
  }, 600);

  // click on end-screen restart button
  canvas.addEventListener('click', e => {
    if (state === 'gameover' || state === 'victory') {
      const rect = canvas.getBoundingClientRect();
      const x = (e.clientX - rect.left) / rect.width * W;
      const y = (e.clientY - rect.top) / rect.height * H;
      if (!endButton || (x > endButton.x && x < endButton.x + endButton.w &&
          y > endButton.y && y < endButton.y + endButton.h)) {
        resetGame();
      } else {
        resetGame();
      }
    }
  });

  // demo AI needs a non-stuck ball; make sure the very first paint is active
  setTimeout(() => { if (!started) demoLaunch(); }, 500);

  requestAnimationFrame(frame);
})();
</script>
</body>
</html>