Models 44

Gemini 3.8 Flash

by GoogleReasoningClean
Open in new window

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

How it went

Total time
59.1 s
Cost
$0.037
Output tokens
9,694
Spent thinking
730 tokens
Speed
170.7 tok/s
Console errors
0
First token after
2.3 s
First visible text after
9.0 s
Prompt tokens
524
Code size
23.4 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
model default
Max output
65,536 tokens
Generated
Nov 12, 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 · 858 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>NEON BREAKOUT</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    body {
      background: radial-gradient(circle at 50% 30%, #15102a, #07050f 85%);
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
      color: #fff;
      overflow: hidden;
    }
    #game-container {
      position: relative;
      width: min(95vw, 840px);
      aspect-ratio: 840 / 620;
      box-shadow: 0 10px 40px rgba(0, 0, 0, 0.7), 0 0 30px rgba(120, 80, 255, 0.2);
      border-radius: 12px;
      overflow: hidden;
      border: 1px solid rgba(255, 255, 255, 0.1);
      background: #090814;
    }
    canvas {
      width: 100%;
      height: 100%;
      display: block;
      cursor: none;
    }
    .hint-bar {
      margin-top: 10px;
      font-size: 13px;
      letter-spacing: 1px;
      color: rgba(255, 255, 255, 0.5);
      text-transform: uppercase;
      text-align: center;
    }
  </style>
</head>
<body>

<div id="game-container">
  <canvas id="canvas" width="840" height="620"></canvas>
</div>
<div class="hint-bar">Mouse or Left/Right Arrow Keys &bull; P to Pause</div>

<script>
(() => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const W = canvas.width;
  const H = canvas.height;

  // Web Audio Synth
  let audioCtx = null;
  function initAudio() {
    if (!audioCtx) {
      const AudioContext = window.AudioContext || window.webkitAudioContext;
      if (AudioContext) audioCtx = new AudioContext();
    }
    if (audioCtx && audioCtx.state === 'suspended') {
      audioCtx.resume();
    }
  }

  function playTone(freq, type, dur, vol = 0.15, slide = 0) {
    if (!audioCtx) return;
    try {
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.type = type;
      osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
      if (slide !== 0) {
        osc.frequency.exponentialRampToValueAtTime(Math.max(10, freq + slide), audioCtx.currentTime + dur);
      }
      gain.gain.setValueAtTime(vol, audioCtx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.start();
      osc.stop(audioCtx.currentTime + dur);
    } catch(e) {}
  }

  const sfx = {
    paddleHit: () => playTone(320, 'sine', 0.1, 0.12, 100),
    brickHit: () => playTone(440, 'triangle', 0.08, 0.15, -80),
    brickBreak: () => playTone(620, 'square', 0.12, 0.15, 200),
    powerUp: () => {
      playTone(523, 'sine', 0.08, 0.15);
      setTimeout(() => playTone(659, 'sine', 0.08, 0.15), 60);
      setTimeout(() => playTone(784, 'sine', 0.15, 0.18), 120);
    },
    loseLife: () => playTone(180, 'sawtooth', 0.35, 0.2, -100),
    win: () => {
      [400, 500, 600, 800].forEach((f, i) => {
        setTimeout(() => playTone(f, 'triangle', 0.2, 0.15), i * 120);
      });
    }
  };

  // Game States
  const STATES = { PLAYING: 0, GAMEOVER: 1, VICTORY: 2 };
  let state = STATES.PLAYING;
  let isDemo = true;
  let paused = false;

  let score = 0;
  let lives = 3;
  let screenShake = 0;

  // Paddle
  const defaultPaddleWidth = 110;
  const paddle = {
    x: W / 2,
    y: H - 38,
    w: defaultPaddleWidth,
    h: 14,
    speed: 9,
    targetX: W / 2,
    wideTimer: 0
  };

  // Balls
  const baseBallSpeed = 6.2;
  let balls = [];

  function createBall(x, y, vx, vy) {
    return {
      x, y,
      r: 6.5,
      vx, vy,
      trail: []
    };
  }

  function resetBall() {
    balls = [createBall(paddle.x, paddle.y - 12, (Math.random() > 0.5 ? 1 : -1) * 3, -baseBallSpeed)];
  }

  // Bricks
  const BRICK_ROWS = 7;
  const BRICK_COLS = 10;
  const BRICK_PAD = 8;
  const BRICK_W = (W - (BRICK_PAD * (BRICK_COLS + 1))) / BRICK_COLS;
  const BRICK_H = 22;
  const BRICK_TOP = 65;

  // Power-up types: 'wide', 'multi'
  let bricks = [];
  let particles = [];
  let powerUps = [];

  const ROW_CONFIG = [
    { hp: 2, color: '#ff2d75', score: 30 }, // Tough row
    { hp: 2, color: '#e040fb', score: 30 }, // Tough row
    { hp: 2, color: '#7c4dff', score: 25 }, // Tough row
    { hp: 1, color: '#00e5ff', score: 15 },
    { hp: 1, color: '#1de9b6', score: 15 },
    { hp: 1, color: '#00e676', score: 10 },
    { hp: 1, color: '#ffea00', score: 10 }
  ];

  function initLevel() {
    bricks = [];
    powerUps = [];
    particles = [];
    // Plan special bricks (approx 8-10 with powerups)
    for (let r = 0; r < BRICK_ROWS; r++) {
      for (let c = 0; c < BRICK_COLS; c++) {
        const x = BRICK_PAD + c * (BRICK_W + BRICK_PAD);
        const y = BRICK_TOP + r * (BRICK_H + BRICK_PAD);
        const cfg = ROW_CONFIG[r];
        
        // Powerup assignment
        let ptype = null;
        const rand = Math.random();
        if (rand < 0.11) ptype = 'wide';
        else if (rand < 0.22) ptype = 'multi';

        bricks.push({
          x, y,
          w: BRICK_W,
          h: BRICK_H,
          maxHp: cfg.hp,
          hp: cfg.hp,
          color: cfg.color,
          score: cfg.score,
          powerUp: ptype,
          shake: 0
        });
      }
    }
  }

  function spawnParticles(x, y, color, count = 14) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = Math.random() * 4.5 + 1.2;
      particles.push({
        x, y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed,
        size: Math.random() * 3.5 + 1.5,
        color,
        alpha: 1,
        life: 1
      });
    }
  }

  // Keyboard & Mouse Controls
  const keys = { left: false, right: false };

  window.addEventListener('keydown', (e) => {
    initAudio();
    if (isDemo && (e.key === ' ' || e.key.startsWith('Arrow') || e.key === 'Enter')) {
      startRealGame();
      return;
    }
    if (state !== STATES.PLAYING && (e.key === ' ' || e.key === 'Enter')) {
      restartGame();
      return;
    }
    if (e.key === 'p' || e.key === 'P') {
      if (state === STATES.PLAYING && !isDemo) paused = !paused;
    }
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = true;
    if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = true;
  });

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

  canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    const mouseX = (e.clientX - rect.left) * (W / rect.width);
    paddle.targetX = Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, mouseX));
    if (isDemo && (e.movementX !== 0 || e.movementY !== 0)) {
      // Don't leave demo solely on slight mouse hover, but allow click
    }
  });

  canvas.addEventListener('mousedown', () => {
    initAudio();
    if (isDemo) {
      startRealGame();
    } else if (state !== STATES.PLAYING) {
      restartGame();
    }
  });

  function startRealGame() {
    isDemo = false;
    score = 0;
    lives = 3;
    state = STATES.PLAYING;
    paddle.w = defaultPaddleWidth;
    paddle.wideTimer = 0;
    initLevel();
    resetBall();
  }

  function restartGame() {
    startRealGame();
  }

  // Multi-ball trigger
  function triggerMultiBall() {
    sfx.powerUp();
    const refBall = balls[0] || createBall(paddle.x, paddle.y - 12, 0, -baseBallSpeed);
    const speed = Math.hypot(refBall.vx, refBall.vy) || baseBallSpeed;
    
    // Create 2 additional balls with divergent angles
    const angles = [-0.45, 0.45];
    angles.forEach(angOffset => {
      const curAng = Math.atan2(refBall.vy, refBall.vx);
      const newAng = curAng + angOffset;
      balls.push(createBall(
        refBall.x,
        refBall.y,
        Math.cos(newAng) * speed,
        Math.sin(newAng) * speed
      ));
    });
  }

  // Wide paddle trigger
  function triggerWidePaddle() {
    sfx.powerUp();
    paddle.wideTimer = 600; // 10 seconds at 60fps
    paddle.w = defaultPaddleWidth * 1.6;
  }

  // Ball vs Box Collision with precise edge reflection
  function checkBallBoxCollision(ball, box) {
    // Find closest point on box
    const closestX = Math.max(box.x, Math.min(ball.x, box.x + box.w));
    const closestY = Math.max(box.y, Math.min(ball.y, box.y + box.h));

    const distX = ball.x - closestX;
    const distY = ball.y - closestY;
    const distanceSq = distX * distX + distY * distY;

    if (distanceSq <= ball.r * ball.r) {
      // Collision detected; determine collision normal
      const overlapLeft = (ball.x + ball.r) - box.x;
      const overlapRight = (box.x + box.w) - (ball.x - ball.r);
      const overlapTop = (ball.y + ball.r) - box.y;
      const overlapBottom = (box.y + box.h) - (ball.y - ball.r);

      const minOverlapX = Math.min(overlapLeft, overlapRight);
      const minOverlapY = Math.min(overlapTop, overlapBottom);

      if (minOverlapX < minOverlapY) {
        ball.vx = (ball.x < box.x + box.w / 2) ? -Math.abs(ball.vx) : Math.abs(ball.vx);
        ball.x = (ball.vx < 0) ? box.x - ball.r : box.x + box.w + ball.r;
      } else {
        ball.vy = (ball.y < box.y + box.h / 2) ? -Math.abs(ball.vy) : Math.abs(ball.vy);
        ball.y = (ball.vy < 0) ? box.y - ball.r : box.y + box.h + ball.r;
      }
      return true;
    }
    return false;
  }

  // Update Game
  function update() {
    if (paused) return;

    if (screenShake > 0) screenShake *= 0.88;
    if (screenShake < 0.2) screenShake = 0;

    // Paddle Power-up timer
    if (paddle.wideTimer > 0) {
      paddle.wideTimer--;
      if (paddle.wideTimer <= 0) {
        paddle.w = defaultPaddleWidth;
      }
    }

    // Paddle AI in Demo mode, else Player controls
    if (isDemo) {
      // Find lowest ball or center
      let targetBall = null;
      let maxY = -1;
      for (const b of balls) {
        if (b.y > maxY) {
          maxY = b.y;
          targetBall = b;
        }
      }
      if (targetBall) {
        // Predictive lead
        const lead = targetBall.vx * 1.5;
        paddle.targetX = targetBall.x + lead;
      } else {
        paddle.targetX = W / 2;
      }
    } else {
      if (keys.left) paddle.targetX -= paddle.speed;
      if (keys.right) paddle.targetX += paddle.speed;
    }

    paddle.targetX = Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, paddle.targetX));
    paddle.x += (paddle.targetX - paddle.x) * 0.35; // smooth chase

    if (state !== STATES.PLAYING) return;

    // Sub-stepping for ultra-reliable collision detection (anti-tunneling)
    const SUBSTEPS = 4;

    for (let step = 0; step < SUBSTEPS; step++) {
      for (let bi = balls.length - 1; bi >= 0; bi--) {
        const b = balls[bi];

        b.x += b.vx / SUBSTEPS;
        b.y += b.vy / SUBSTEPS;

        // Wall collisions
        if (b.x - b.r <= 0) {
          b.x = b.r;
          b.vx = Math.abs(b.vx);
          sfx.paddleHit();
        } else if (b.x + b.r >= W) {
          b.x = W - b.r;
          b.vx = -Math.abs(b.vx);
          sfx.paddleHit();
        }
        if (b.y - b.r <= 0) {
          b.y = b.r;
          b.vy = Math.abs(b.vy);
          sfx.paddleHit();
        }

        // Paddle Collision
        const pBox = { x: paddle.x - paddle.w / 2, y: paddle.y, w: paddle.w, h: paddle.h };
        if (
          b.y + b.r >= pBox.y &&
          b.y - b.r <= pBox.y + pBox.h &&
          b.x >= pBox.x - b.r &&
          b.x <= pBox.x + pBox.w + b.r &&
          b.vy > 0
        ) {
          b.y = pBox.y - b.r;
          // Calculate hit offset: -1 (left edge) to +1 (right edge)
          const hit = (b.x - paddle.x) / (paddle.w / 2);
          const clampedHit = Math.max(-0.92, Math.min(0.92, hit));
          
          // Max bounce angle: 72 degrees
          const maxAngle = (72 * Math.PI) / 180;
          const bounceAngle = clampedHit * maxAngle;

          // Ball speed slightly increases with score
          const currentSpeed = Math.min(9.5, baseBallSpeed + (score / 400) * 0.4);
          b.vx = currentSpeed * Math.sin(bounceAngle);
          b.vy = -currentSpeed * Math.cos(bounceAngle);

          sfx.paddleHit();
          screenShake = 2.5;

          // Splash particles from paddle
          for (let p = 0; p < 4; p++) {
            particles.push({
              x: b.x,
              y: paddle.y,
              vx: (Math.random() - 0.5) * 4,
              vy: -Math.random() * 3 - 1,
              size: Math.random() * 2.5 + 1,
              color: '#00e5ff',
              alpha: 0.8,
              life: 0.8
            });
          }
        }

        // Brick Collisions
        for (let i = bricks.length - 1; i >= 0; i--) {
          const brk = bricks[i];
          if (checkBallBoxCollision(b, brk)) {
            brk.hp--;
            brk.shake = 5;
            screenShake = Math.max(screenShake, 3.5);

            if (brk.hp <= 0) {
              sfx.brickBreak();
              score += brk.score;
              spawnParticles(brk.x + brk.w / 2, brk.y + brk.h / 2, brk.color, 16);

              // Spawn power-up if attached
              if (brk.powerUp) {
                powerUps.push({
                  x: brk.x + brk.w / 2,
                  y: brk.y + brk.h / 2,
                  type: brk.powerUp,
                  vy: 2.2,
                  rot: 0
                });
              }

              bricks.splice(i, 1);

              // Win condition check
              if (bricks.length === 0) {
                state = STATES.VICTORY;
                sfx.win();
              }
            } else {
              sfx.brickHit();
              spawnParticles(b.x, b.y, brk.color, 5);
            }
            break; // One brick collision per substep
          }
        }

        // Floor death check (only on last substep)
        if (step === SUBSTEPS - 1 && b.y - b.r > H) {
          balls.splice(bi, 1);
        }
      }
    }

    // Ball Trails & check death
    balls.forEach(b => {
      b.trail.push({ x: b.x, y: b.y });
      if (b.trail.length > 7) b.trail.shift();
    });

    if (balls.length === 0) {
      lives--;
      sfx.loseLife();
      screenShake = 8;
      if (lives <= 0) {
        state = STATES.GAMEOVER;
      } else {
        resetBall();
        paddle.w = defaultPaddleWidth;
        paddle.wideTimer = 0;
      }
    }

    // Update PowerUps
    for (let i = powerUps.length - 1; i >= 0; i--) {
      const p = powerUps[i];
      p.y += p.vy;
      p.rot += 0.04;

      // Catch by paddle
      if (
        p.y + 12 >= paddle.y &&
        p.y - 12 <= paddle.y + paddle.h &&
        p.x >= paddle.x - paddle.w / 2 - 8 &&
        p.x <= paddle.x + paddle.w / 2 + 8
      ) {
        if (p.type === 'wide') triggerWidePaddle();
        if (p.type === 'multi') triggerMultiBall();
        
        spawnParticles(p.x, p.y, p.type === 'wide' ? '#00e5ff' : '#ffea00', 20);
        powerUps.splice(i, 1);
        continue;
      }

      // Out of bounds
      if (p.y > H + 20) {
        powerUps.splice(i, 1);
      }
    }

    // Update particles
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.08; // gravity
      p.alpha -= 0.024;
      if (p.alpha <= 0) particles.splice(i, 1);
    }

    // Update brick shakes
    bricks.forEach(b => {
      if (b.shake > 0) b.shake *= 0.8;
      if (b.shake < 0.1) b.shake = 0;
    });
  }

  // Draw Functions
  function draw() {
    ctx.save();

    // Screen Shake Offset
    if (screenShake > 0) {
      const sx = (Math.random() - 0.5) * screenShake * 2;
      const sy = (Math.random() - 0.5) * screenShake * 2;
      ctx.translate(sx, sy);
    }

    // Background gradient and subtle grid
    ctx.fillStyle = '#0a0818';
    ctx.fillRect(0, 0, W, H);

    ctx.strokeStyle = 'rgba(255, 255, 255, 0.025)';
    ctx.lineWidth = 1;
    for (let x = 0; x < W; x += 40) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, H);
      ctx.stroke();
    }
    for (let y = 0; y < H; y += 40) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(W, y);
      ctx.stroke();
    }

    // Bricks
    bricks.forEach(b => {
      ctx.save();
      const ox = (Math.random() - 0.5) * b.shake;
      const oy = (Math.random() - 0.5) * b.shake;
      ctx.translate(b.x + ox, b.y + oy);

      // Brick body
      ctx.fillStyle = b.color;
      ctx.shadowColor = b.color;
      ctx.shadowBlur = b.hp === b.maxHp ? 8 : 2;

      // Rounded brick
      ctx.beginPath();
      ctx.roundRect(0, 0, b.w, b.h, 4);
      ctx.fill();

      // Top glossy highlight
      ctx.shadowBlur = 0;
      ctx.fillStyle = 'rgba(255, 255, 255, 0.22)';
      ctx.fillRect(2, 2, b.w - 4, 3);

      // Cracks for 2-HP brick damaged down to 1
      if (b.maxHp === 2 && b.hp === 1) {
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
        ctx.lineWidth = 1.8;
        ctx.beginPath();
        ctx.moveTo(b.w * 0.25, 2);
        ctx.lineTo(b.w * 0.4, b.h * 0.55);
        ctx.lineTo(b.w * 0.35, b.h - 2);
        ctx.moveTo(b.w * 0.4, b.h * 0.55);
        ctx.lineTo(b.w * 0.65, b.h * 0.4);
        ctx.lineTo(b.w * 0.8, b.h - 2);
        ctx.stroke();
      }

      // Power-up indicator badge inside brick
      if (b.powerUp) {
        ctx.fillStyle = b.powerUp === 'wide' ? '#00e5ff' : '#ffea00';
        ctx.shadowColor = ctx.fillStyle;
        ctx.shadowBlur = 6;
        ctx.beginPath();
        if (b.powerUp === 'wide') {
          // Double arrow symbol <->
          const cx = b.w / 2, cy = b.h / 2;
          ctx.arc(cx, cy, 3.5, 0, Math.PI * 2);
          ctx.fill();
        } else {
          // Multi-dot triangle symbol
          const cx = b.w / 2, cy = b.h / 2;
          ctx.arc(cx - 5, cy, 2, 0, Math.PI * 2);
          ctx.arc(cx, cy - 2, 2, 0, Math.PI * 2);
          ctx.arc(cx + 5, cy, 2, 0, Math.PI * 2);
          ctx.fill();
        }
      }

      ctx.restore();
    });

    // Falling Power-ups
    powerUps.forEach(p => {
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot);

      const color = p.type === 'wide' ? '#00e5ff' : '#ffea00';
      ctx.shadowColor = color;
      ctx.shadowBlur = 12;
      ctx.fillStyle = color;

      // Hexagonal capsule
      ctx.beginPath();
      for (let i = 0; i < 6; i++) {
        const ang = (i * Math.PI) / 3;
        const px = Math.cos(ang) * 11;
        const py = Math.sin(ang) * 11;
        if (i === 0) ctx.moveTo(px, py);
        else ctx.lineTo(px, py);
      }
      ctx.closePath();
      ctx.fill();

      // Icon overlay
      ctx.fillStyle = '#0a0818';
      ctx.font = 'bold 11px system-ui';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(p.type === 'wide' ? 'W' : '3x', 0, 1);

      ctx.restore();
    });

    // Paddle
    ctx.save();
    ctx.translate(paddle.x - paddle.w / 2, paddle.y);
    const paddleColor = paddle.wideTimer > 0 ? '#00e5ff' : '#a855f7';
    ctx.shadowColor = paddleColor;
    ctx.shadowBlur = 14;

    // Body
    const padGrad = ctx.createLinearGradient(0, 0, 0, paddle.h);
    padGrad.addColorStop(0, '#ffffff');
    padGrad.addColorStop(0.3, paddleColor);
    padGrad.addColorStop(1, '#3b0764');
    ctx.fillStyle = padGrad;

    ctx.beginPath();
    ctx.roundRect(0, 0, paddle.w, paddle.h, paddle.h / 2);
    ctx.fill();

    // Neon Core Line
    ctx.fillStyle = '#ffffff';
    ctx.shadowBlur = 6;
    ctx.fillRect(8, paddle.h / 2 - 1.5, paddle.w - 16, 3);
    ctx.restore();

    // Balls with neon glowing trails
    balls.forEach(b => {
      // Trail
      for (let i = 0; i < b.trail.length; i++) {
        const t = b.trail[i];
        const alpha = (i + 1) / (b.trail.length * 2.8);
        ctx.fillStyle = `rgba(0, 229, 255, ${alpha})`;
        ctx.beginPath();
        ctx.arc(t.x, t.y, b.r * (i / b.trail.length), 0, Math.PI * 2);
        ctx.fill();
      }

      // Main Ball
      ctx.save();
      ctx.shadowColor = '#00e5ff';
      ctx.shadowBlur = 12;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
      ctx.fill();

      // Glowing outer rim
      ctx.strokeStyle = '#00e5ff';
      ctx.lineWidth = 1.5;
      ctx.stroke();
      ctx.restore();
    });

    // Particles
    particles.forEach(p => {
      ctx.save();
      ctx.globalAlpha = Math.max(0, p.alpha);
      ctx.fillStyle = p.color;
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 6;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    });

    // HUD Header
    ctx.save();
    ctx.font = 'bold 16px "Segoe UI", system-ui, sans-serif';
    ctx.fillStyle = '#ffffff';
    ctx.shadowColor = 'rgba(255, 255, 255, 0.4)';
    ctx.shadowBlur = 8;

    // Score
    ctx.textAlign = 'left';
    ctx.fillText(`SCORE: ${score}`, 25, 34);

    // Wide timer active badge
    if (paddle.wideTimer > 0) {
      ctx.fillStyle = '#00e5ff';
      ctx.fillText(`WIDE: ${(paddle.wideTimer / 60).toFixed(1)}s`, 180, 34);
    }

    // Lives as neon hearts
    ctx.textAlign = 'right';
    ctx.fillStyle = '#ff2d75';
    ctx.shadowColor = '#ff2d75';
    let lifeStr = '';
    for (let i = 0; i < lives; i++) lifeStr += '♥ ';
    ctx.fillText(lifeStr, W - 25, 34);

    ctx.restore();

    // Overlay: Demo Mode Banner
    if (isDemo) {
      ctx.save();
      ctx.fillStyle = 'rgba(9, 8, 20, 0.65)';
      ctx.fillRect(0, H - 110, W, 85);

      ctx.fillStyle = '#00e5ff';
      ctx.shadowColor = '#00e5ff';
      ctx.shadowBlur = 14;
      ctx.font = '900 24px "Segoe UI", system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('CLICK OR PRESS ANY KEY TO PLAY', W / 2, H - 65);

      ctx.font = '500 13px system-ui';
      ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
      ctx.shadowBlur = 0;
      ctx.fillText('AUTO-DEMO ACTIVE &bull; CATCH GLOWING POWER-UPS FOR WIDE PADDLE & MULTI-BALL', W / 2, H - 40);
      ctx.restore();
    }

    // Overlay: Pause
    if (paused) {
      ctx.save();
      ctx.fillStyle = 'rgba(7, 5, 15, 0.75)';
      ctx.fillRect(0, 0, W, H);
      ctx.font = '900 38px "Segoe UI", system-ui';
      ctx.fillStyle = '#ffffff';
      ctx.textAlign = 'center';
      ctx.shadowColor = '#00e5ff';
      ctx.shadowBlur = 20;
      ctx.fillText('PAUSED', W / 2, H / 2);
      ctx.restore();
    }

    // Overlay: Game Over & Victory
    if (state === STATES.GAMEOVER || state === STATES.VICTORY) {
      ctx.save();
      ctx.fillStyle = 'rgba(5, 4, 12, 0.84)';
      ctx.fillRect(0, 0, W, H);

      const isWin = state === STATES.VICTORY;
      const title = isWin ? 'VICTORY!' : 'GAME OVER';
      const color = isWin ? '#00e5ff' : '#ff2d75';

      ctx.font = '900 46px "Segoe UI", system-ui';
      ctx.fillStyle = color;
      ctx.shadowColor = color;
      ctx.shadowBlur = 25;
      ctx.textAlign = 'center';
      ctx.fillText(title, W / 2, H / 2 - 35);

      ctx.font = '600 22px system-ui';
      ctx.fillStyle = '#ffffff';
      ctx.shadowColor = 'rgba(255,255,255,0.4)';
      ctx.shadowBlur = 8;
      ctx.fillText(`FINAL SCORE: ${score}`, W / 2, H / 2 + 15);

      ctx.font = '500 15px system-ui';
      ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
      ctx.shadowBlur = 0;
      ctx.fillText('CLICK OR PRESS SPACE TO RESTART', W / 2, H / 2 + 65);

      ctx.restore();
    }

    ctx.restore();
  }

  // Main Loop
  function loop() {
    update();
    draw();
    requestAnimationFrame(loop);
  }

  // Bootstrap game in Demo Mode
  initLevel();
  resetBall();
  loop();

})();
</script>
</body>
</html>
Reasoning trace (730 tokens)
**Building Game**

I'm focusing on the core mechanics for the Breakout game, aiming for a single HTML file with vanilla JS and canvas. Current efforts are centered on implementing responsive paddle controls, including both mouse and arrow key support, and refining the physics for ball-paddle collision to ensure varied bounce angles based on impact point.

**Developing Brick Logic**

I'm refining the brick structure, incorporating different hit point values and visually indicating damage. The next step is integrating power-up drops upon brick destruction, focusing on the wider paddle and multi-ball mechanics, alongside ensuring robust collision detection to prevent tunneling and adding particle effects for visual flair.

**Refining Game Elements**

I'm working on the visual and auditory polish. This involves implementing distinct brick types with visual damage indicators, adding subtle synth sound effects for key game events, and ensuring the game scales responsively to various resolutions. The demo mode AI is also being tuned for smoother paddle movement.