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
57.5 s
Cost
$0.038
Output tokens
9,929
Spent thinking
532 tokens
Speed
194.5 tok/s
Console errors
0
First token after
6.5 s
First visible text after
6.5 s
Prompt tokens
524
Code size
25.1 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"medium"}
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 · 875 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CyberBreak - Deluxe Brick Breaker</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
    }
    body {
      background: radial-gradient(circle at center, #0b0f19 0%, #030508 100%);
      color: #e2e8f0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      overflow: hidden;
    }
    #game-container {
      position: relative;
      width: 100%;
      max-width: 900px;
      aspect-ratio: 4 / 3;
      max-height: 94vh;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 10px 40px rgba(0, 240, 255, 0.15), 0 0 100px rgba(112, 0, 255, 0.1);
      border-radius: 12px;
      background: #050811;
      border: 1px solid rgba(0, 240, 255, 0.25);
    }
    canvas {
      width: 100%;
      height: 100%;
      display: block;
      border-radius: 12px;
      cursor: none;
    }
    .banner {
      position: absolute;
      bottom: 24px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(10, 15, 30, 0.85);
      border: 1px solid #00f0ff;
      padding: 10px 24px;
      border-radius: 30px;
      font-size: 15px;
      letter-spacing: 2px;
      text-transform: uppercase;
      font-weight: 700;
      color: #00f0ff;
      box-shadow: 0 0 20px rgba(0, 240, 255, 0.4);
      animation: pulse 1.6s infinite ease-in-out;
      pointer-events: none;
      white-space: nowrap;
      z-index: 10;
    }
    @keyframes pulse {
      0%, 100% { transform: translateX(-50%) scale(1); opacity: 0.95; }
      50% { transform: translateX(-50%) scale(1.05); opacity: 0.65; }
    }
    @media (max-width: 600px) {
      .banner { font-size: 12px; padding: 8px 16px; bottom: 12px; }
    }
  </style>
</head>
<body>

<div id="game-container">
  <canvas id="gameCanvas" width="900" height="675"></canvas>
  <div id="startBanner" class="banner">Auto-Demo: Click or Press Any Key to Play</div>
</div>

<script>
  // Web Audio Context for synthesized retro sound effects
  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 playSound(type) {
    if (!audioCtx) return;
    try {
      const t = audioCtx.currentTime;
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.connect(gain);
      gain.connect(audioCtx.destination);

      if (type === 'hitPaddle') {
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(220, t);
        osc.frequency.exponentialRampToValueAtTime(440, t + 0.08);
        gain.gain.setValueAtTime(0.2, t);
        gain.gain.exponentialRampToValueAtTime(0.01, t + 0.08);
        osc.start(t);
        osc.stop(t + 0.09);
      } else if (type === 'hitBrickNormal') {
        osc.type = 'sine';
        osc.frequency.setValueAtTime(520, t);
        osc.frequency.exponentialRampToValueAtTime(880, t + 0.08);
        gain.gain.setValueAtTime(0.25, t);
        gain.gain.exponentialRampToValueAtTime(0.01, t + 0.08);
        osc.start(t);
        osc.stop(t + 0.09);
      } else if (type === 'hitBrickHard') {
        osc.type = 'square';
        osc.frequency.setValueAtTime(180, t);
        osc.frequency.exponentialRampToValueAtTime(120, t + 0.1);
        gain.gain.setValueAtTime(0.25, t);
        gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
        osc.start(t);
        osc.stop(t + 0.11);
      } else if (type === 'powerup') {
        osc.type = 'sine';
        osc.frequency.setValueAtTime(400, t);
        osc.frequency.setValueAtTime(600, t + 0.06);
        osc.frequency.setValueAtTime(900, t + 0.12);
        gain.gain.setValueAtTime(0.25, t);
        gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25);
        osc.start(t);
        osc.stop(t + 0.26);
      } else if (type === 'loseLife') {
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(220, t);
        osc.frequency.exponentialRampToValueAtTime(70, t + 0.25);
        gain.gain.setValueAtTime(0.3, t);
        gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25);
        osc.start(t);
        osc.stop(t + 0.26);
      } else if (type === 'win') {
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(440, t);
        osc.frequency.setValueAtTime(554.37, t + 0.12);
        osc.frequency.setValueAtTime(659.25, t + 0.24);
        osc.frequency.setValueAtTime(880, t + 0.36);
        gain.gain.setValueAtTime(0.3, t);
        gain.gain.exponentialRampToValueAtTime(0.01, t + 0.6);
        osc.start(t);
        osc.stop(t + 0.61);
      }
    } catch (e) {
      // Audio fallback silent
    }
  }

  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  const banner = document.getElementById('startBanner');

  const W = canvas.width;
  const H = canvas.height;

  // Game States
  let isDemo = true;
  let state = 'PLAYING'; // 'PLAYING', 'GAMEOVER', 'VICTORY'
  let score = 0;
  let lives = 3;
  let shakeTime = 0;

  // Controls
  let keys = { left: false, right: false };
  let mouseActive = false;
  let mouseX = W / 2;

  // Paddle
  const PADDLE_DEFAULT_WIDTH = 120;
  const PADDLE_WIDE_WIDTH = 190;
  const PADDLE_HEIGHT = 16;
  const PADDLE_Y = H - 42;

  let paddle = {
    x: W / 2 - PADDLE_DEFAULT_WIDTH / 2,
    y: PADDLE_Y,
    width: PADDLE_DEFAULT_WIDTH,
    height: PADDLE_HEIGHT,
    speed: 680,
    wideTimer: 0
  };

  // Ball configuration
  const BALL_RADIUS = 8;
  const BALL_BASE_SPEED = 460;
  let balls = [];

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

  function resetBalls() {
    balls = [createBall(paddle.x + paddle.width / 2, paddle.y - BALL_RADIUS - 2, 180, -BALL_BASE_SPEED)];
  }

  // Bricks setup: 7 rows x 10 cols
  const BRICK_ROWS = 7;
  const BRICK_COLS = 10;
  const BRICK_GAP = 8;
  const BRICK_OFFSET_TOP = 70;
  const BRICK_OFFSET_LEFT = 45;
  const BRICK_WIDTH = (W - BRICK_OFFSET_LEFT * 2 - (BRICK_COLS - 1) * BRICK_GAP) / BRICK_COLS;
  const BRICK_HEIGHT = 24;

  let bricks = [];

  function initBricks() {
    bricks = [];
    const colorTiers = [
      { fill: '#ff0055', stroke: '#ff5599', points: 40 }, // Row 0
      { fill: '#ff7700', stroke: '#ffaa33', points: 30 }, // Row 1
      { fill: '#ffd200', stroke: '#fff066', points: 25 }, // Row 2
      { fill: '#00f0ff', stroke: '#88f9ff', points: 20 }, // Row 3
      { fill: '#00ff88', stroke: '#88ffc2', points: 15 }, // Row 4
      { fill: '#b800ff', stroke: '#e288ff', points: 10 }, // Row 5
      { fill: '#3b82f6', stroke: '#93c5fd', points: 10 }  // Row 6
    ];

    for (let r = 0; r < BRICK_ROWS; r++) {
      for (let c = 0; c < BRICK_COLS; c++) {
        // Rows 0 and 1 are 2-hit tough bricks; Row 2 also has some alternating tough bricks
        const isTough = (r <= 1) || (r === 2 && (c % 2 === 0));
        const maxHits = isTough ? 2 : 1;

        // Power-ups: Wide paddle ('W') or Multi-ball ('M')
        let power = null;
        if (r === 1 && (c === 2 || c === 7)) power = 'W';
        if (r === 2 && (c === 4 || c === 5)) power = 'M';
        if (r === 3 && (c === 1 || c === 8)) power = 'W';
        if (r === 4 && (c === 3 || c === 6)) power = 'M';
        if (r === 5 && c === 4) power = 'W';

        bricks.push({
          x: BRICK_OFFSET_LEFT + c * (BRICK_WIDTH + BRICK_GAP),
          y: BRICK_OFFSET_TOP + r * (BRICK_HEIGHT + BRICK_GAP),
          w: BRICK_WIDTH,
          h: BRICK_HEIGHT,
          r: r,
          c: c,
          hits: maxHits,
          maxHits: maxHits,
          color: colorTiers[r],
          power: power,
          alive: true
        });
      }
    }
  }

  // Particles
  let particles = [];
  function spawnParticles(x, y, color, count = 16) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = Math.random() * 220 + 40;
      particles.push({
        x: x,
        y: y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed,
        size: Math.random() * 4 + 2,
        life: 1.0,
        decay: Math.random() * 1.5 + 1.2,
        color: color
      });
    }
  }

  // Falling Power-ups
  let powerups = [];
  function spawnPowerup(x, y, type) {
    powerups.push({
      x: x,
      y: y,
      w: 24,
      h: 24,
      vy: 140,
      type: type // 'W' or 'M'
    });
  }

  function startUserGame() {
    initAudio();
    if (isDemo || state !== 'PLAYING') {
      isDemo = false;
      banner.style.display = 'none';
      if (state !== 'PLAYING') {
        score = 0;
        lives = 3;
        paddle.wideTimer = 0;
        paddle.width = PADDLE_DEFAULT_WIDTH;
        initBricks();
        state = 'PLAYING';
      }
      resetBalls();
    }
  }

  // Input Listeners
  window.addEventListener('keydown', (e) => {
    initAudio();
    if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
    if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
    if (e.code === 'Space' || e.code === 'Enter') {
      startUserGame();
    } else if (isDemo) {
      startUserGame();
    }
  });

  window.addEventListener('keyup', (e) => {
    if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
    if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
  });

  canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    mouseX = (e.clientX - rect.left) * scaleX;
    mouseActive = true;
  });

  canvas.addEventListener('mousedown', () => {
    startUserGame();
  });

  // Touch Support
  canvas.addEventListener('touchmove', (e) => {
    e.preventDefault();
    if (!e.touches[0]) return;
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    mouseX = (e.touches[0].clientX - rect.left) * scaleX;
    mouseActive = true;
  }, { passive: false });

  canvas.addEventListener('touchstart', (e) => {
    e.preventDefault();
    startUserGame();
    if (e.touches[0]) {
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      mouseX = (e.touches[0].clientX - rect.left) * scaleX;
      mouseActive = true;
    }
  }, { passive: false });

  // Reset initial game
  initBricks();
  resetBalls();

  // Continuous physics & collision
  let lastTime = performance.now();

  function update(dt) {
    if (shakeTime > 0) shakeTime -= dt;

    // Paddle Wide Timer
    if (paddle.wideTimer > 0) {
      paddle.wideTimer -= dt;
      paddle.width = PADDLE_WIDE_WIDTH;
      if (paddle.wideTimer <= 0) {
        paddle.width = PADDLE_DEFAULT_WIDTH;
      }
    }

    // Move Paddle
    if (isDemo) {
      // Simple Auto AI: follow closest or lowest falling ball
      let targetBall = balls[0];
      for (let b of balls) {
        if (!targetBall || b.y > targetBall.y) targetBall = b;
      }
      if (targetBall) {
        const destX = targetBall.x - paddle.width / 2;
        paddle.x += (destX - paddle.x) * Math.min(1, dt * 14);
      }
    } else {
      if (mouseActive) {
        paddle.x = mouseX - paddle.width / 2;
      }
      if (keys.left) {
        paddle.x -= paddle.speed * dt;
        mouseActive = false;
      }
      if (keys.right) {
        paddle.x += paddle.speed * dt;
        mouseActive = false;
      }
    }

    // Clamp paddle within screen
    if (paddle.x < 10) paddle.x = 10;
    if (paddle.x + paddle.width > W - 10) paddle.x = W - 10 - paddle.width;

    if (state !== 'PLAYING') return;

    // Sub-stepping for ultra-reliable collision (prevent tunneling)
    const SUB_STEPS = 4;
    const subDt = dt / SUB_STEPS;

    for (let step = 0; step < SUB_STEPS; step++) {
      for (let i = balls.length - 1; i >= 0; i--) {
        const ball = balls[i];

        ball.x += ball.vx * subDt;
        ball.y += ball.vy * subDt;

        // Trail record (only record on main steps)
        if (step === 0) {
          ball.trail.push({ x: ball.x, y: ball.y });
          if (ball.trail.length > 8) ball.trail.shift();
        }

        // Left / Right Walls
        if (ball.x - ball.r < 8) {
          ball.x = 8 + ball.r;
          ball.vx = Math.abs(ball.vx);
        } else if (ball.x + ball.r > W - 8) {
          ball.x = W - 8 - ball.r;
          ball.vx = -Math.abs(ball.vx);
        }

        // Top Wall
        if (ball.y - ball.r < 8) {
          ball.y = 8 + ball.r;
          ball.vy = Math.abs(ball.vy);
        }

        // Bottom Wall (Dead ball)
        if (ball.y - ball.r > H + 10) {
          balls.splice(i, 1);
          continue;
        }

        // Paddle Collision (Angle-based bounce)
        // Center = straight up; Edges = sharp angles
        if (ball.vy > 0 &&
            ball.y + ball.r >= paddle.y &&
            ball.y - ball.r <= paddle.y + paddle.height &&
            ball.x + ball.r >= paddle.x &&
            ball.x - ball.r <= paddle.x + paddle.width) {
          
          ball.y = paddle.y - ball.r; // snap to top of paddle
          const hitOffset = (ball.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2);
          const clampedOffset = Math.max(-0.92, Math.min(0.92, hitOffset));

          // Max bounce angle = ~68 degrees
          const maxAngle = Math.PI * 0.38;
          const bounceAngle = clampedOffset * maxAngle;
          const currentSpeed = Math.hypot(ball.vx, ball.vy);
          const newSpeed = Math.min(680, currentSpeed * 1.015); // gentle speed up

          ball.vx = newSpeed * Math.sin(bounceAngle);
          ball.vy = -newSpeed * Math.cos(bounceAngle);

          playSound('hitPaddle');
          spawnParticles(ball.x, paddle.y, '#00f0ff', 6);
        }

        // Brick Collisions
        for (let b of bricks) {
          if (!b.alive) continue;

          // Closest point on brick to ball center
          const nearestX = Math.max(b.x, Math.min(ball.x, b.x + b.w));
          const nearestY = Math.max(b.y, Math.min(ball.y, b.y + b.h));

          const dx = ball.x - nearestX;
          const dy = ball.y - nearestY;
          const distSq = dx * dx + dy * dy;

          if (distSq < ball.r * ball.r) {
            // Collision occurred! Determine bounce direction
            const overlapX = ball.r - Math.abs(dx);
            const overlapY = ball.r - Math.abs(dy);

            if (dx === 0 && dy === 0) {
              // Center inside brick, push out upwards
              ball.vy = -Math.abs(ball.vy);
              ball.y = b.y - ball.r;
            } else if (overlapX < overlapY) {
              ball.vx = dx > 0 ? Math.abs(ball.vx) : -Math.abs(ball.vx);
              ball.x += dx > 0 ? overlapX : -overlapX;
            } else {
              ball.vy = dy > 0 ? Math.abs(ball.vy) : -Math.abs(ball.vy);
              ball.y += dy > 0 ? overlapY : -overlapY;
            }

            // Hit logic
            b.hits--;
            shakeTime = 0.08;

            if (b.hits <= 0) {
              b.alive = false;
              score += b.color.points * b.maxHits;
              playSound('hitBrickNormal');
              spawnParticles(b.x + b.w / 2, b.y + b.h / 2, b.color.fill, 20);

              if (b.power) {
                spawnPowerup(b.x + b.w / 2 - 12, b.y + b.h / 2, b.power);
              }
            } else {
              playSound('hitBrickHard');
              score += 10;
              spawnParticles(nearestX, nearestY, '#ffffff', 8);
            }

            // Check Victory
            const anyAlive = bricks.some(k => k.alive);
            if (!anyAlive) {
              state = 'VICTORY';
              playSound('win');
            }
            break; // Break inner loop after colliding with a brick
          }
        }
      }
    }

    // If all balls are lost
    if (balls.length === 0) {
      lives--;
      playSound('loseLife');
      shakeTime = 0.25;

      if (lives <= 0) {
        state = 'GAMEOVER';
      } else {
        resetBalls();
      }
    }

    // Powerups Update
    for (let i = powerups.length - 1; i >= 0; i--) {
      const p = powerups[i];
      p.y += p.vy * dt;

      // Check paddle catch
      if (p.y + p.h >= paddle.y &&
          p.y <= paddle.y + paddle.height &&
          p.x + p.w >= paddle.x &&
          p.x <= paddle.x + paddle.width) {
        
        playSound('powerup');
        spawnParticles(p.x + p.w / 2, paddle.y, p.type === 'W' ? '#00f0ff' : '#ff00ff', 18);

        if (p.type === 'W') {
          paddle.wideTimer = 10; // 10 seconds of wide paddle
          score += 50;
        } else if (p.type === 'M') {
          score += 50;
          // Spawn 2 more balls with varied directions
          const base = balls[0] || { x: paddle.x + paddle.width / 2, y: paddle.y - 15 };
          balls.push(createBall(base.x, base.y, -260, -BALL_BASE_SPEED));
          balls.push(createBall(base.x, base.y, 260, -BALL_BASE_SPEED));
        }

        powerups.splice(i, 1);
        continue;
      }

      // Fell off screen
      if (p.y > H + 30) {
        powerups.splice(i, 1);
      }
    }

    // Particles Update
    for (let i = particles.length - 1; i >= 0; i--) {
      const pt = particles[i];
      pt.x += pt.vx * dt;
      pt.y += pt.vy * dt;
      pt.life -= pt.decay * dt;
      if (pt.life <= 0) {
        particles.splice(i, 1);
      }
    }
  }

  // Render Loop
  function draw() {
    ctx.save();

    // Screen Shake effect
    if (shakeTime > 0) {
      const mag = shakeTime * 12;
      ctx.translate((Math.random() - 0.5) * mag, (Math.random() - 0.5) * mag);
    }

    // Clear background
    ctx.fillStyle = '#060913';
    ctx.fillRect(0, 0, W, H);

    // Subtle Neon Arena Border
    ctx.lineWidth = 3;
    ctx.strokeStyle = 'rgba(0, 240, 255, 0.4)';
    ctx.strokeRect(6, 6, W - 12, H - 12);

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

    // Draw Bricks
    for (let b of bricks) {
      if (!b.alive) continue;

      ctx.save();
      ctx.shadowBlur = b.maxHits > 1 ? 12 : 6;
      ctx.shadowColor = b.color.fill;

      // Tough brick vs Normal appearance
      if (b.maxHits > 1 && b.hits === 2) {
        // Uncracked Tough Brick: metallic gradient
        const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
        grad.addColorStop(0, '#f1f5f9');
        grad.addColorStop(0.5, b.color.fill);
        grad.addColorStop(1, '#1e293b');
        ctx.fillStyle = grad;
      } else {
        ctx.fillStyle = b.color.fill;
      }

      ctx.beginPath();
      ctx.roundRect(b.x, b.y, b.w, b.h, 4);
      ctx.fill();

      // Border highlight
      ctx.lineWidth = 1.5;
      ctx.strokeStyle = b.color.stroke;
      ctx.stroke();

      // Visible Cracks on 2-hit brick after 1st hit
      if (b.maxHits > 1 && b.hits === 1) {
        ctx.strokeStyle = '#ffffff';
        ctx.lineWidth = 2;
        ctx.beginPath();
        // Jagged crack lines across the brick
        ctx.moveTo(b.x + b.w * 0.2, b.y + 2);
        ctx.lineTo(b.x + b.w * 0.45, b.y + b.h * 0.55);
        ctx.lineTo(b.x + b.w * 0.35, b.y + b.h - 2);
        ctx.moveTo(b.x + b.w * 0.45, b.y + b.h * 0.55);
        ctx.lineTo(b.x + b.w * 0.8, b.y + b.h * 0.7);
        ctx.stroke();
      }

      // Visible Power-up Icon Badge on Brick
      if (b.power) {
        ctx.fillStyle = b.power === 'W' ? '#00f0ff' : '#ff00ea';
        ctx.font = 'bold 12px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(b.power === 'W' ? '⚡W' : '★M', b.x + b.w / 2, b.y + b.h / 2 + 1);
      }

      ctx.restore();
    }

    // Draw Power-ups
    for (let p of powerups) {
      ctx.save();
      ctx.shadowBlur = 10;
      ctx.shadowColor = p.type === 'W' ? '#00f0ff' : '#ff00ff';
      ctx.fillStyle = p.type === 'W' ? '#00f0ff' : '#ff00ff';

      ctx.beginPath();
      ctx.roundRect(p.x, p.y, p.w, p.h, 6);
      ctx.fill();
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 2;
      ctx.stroke();

      ctx.fillStyle = '#000000';
      ctx.font = 'bold 13px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(p.type === 'W' ? 'W' : 'M', p.x + p.w / 2, p.y + p.h / 2);
      ctx.restore();
    }

    // Draw Particles
    for (let pt of particles) {
      ctx.save();
      ctx.globalAlpha = Math.max(0, pt.life);
      ctx.fillStyle = pt.color;
      ctx.shadowBlur = 8;
      ctx.shadowColor = pt.color;
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, pt.size, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    // Draw Paddle
    ctx.save();
    ctx.shadowBlur = 16;
    ctx.shadowColor = paddle.wideTimer > 0 ? '#ff00ff' : '#00f0ff';

    const pGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.height);
    if (paddle.wideTimer > 0) {
      pGrad.addColorStop(0, '#ffffff');
      pGrad.addColorStop(0.5, '#e000ff');
      pGrad.addColorStop(1, '#7000aa');
    } else {
      pGrad.addColorStop(0, '#e0ffff');
      pGrad.addColorStop(0.5, '#00f0ff');
      pGrad.addColorStop(1, '#0055aa');
    }

    ctx.fillStyle = pGrad;
    ctx.beginPath();
    ctx.roundRect(paddle.x, paddle.y, paddle.width, paddle.height, 8);
    ctx.fill();

    // Paddle border & center mark
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
    ctx.fillRect(paddle.x + paddle.width / 2 - 2, paddle.y + 2, 4, paddle.height - 4);
    ctx.restore();

    // Draw Balls & Trails
    for (let ball of balls) {
      // Trail
      for (let t = 0; t < ball.trail.length; t++) {
        const pt = ball.trail[t];
        const alpha = (t + 1) / ball.trail.length * 0.45;
        const rad = ball.r * (0.4 + 0.6 * ((t + 1) / ball.trail.length));
        ctx.beginPath();
        ctx.fillStyle = `rgba(0, 240, 255, ${alpha})`;
        ctx.arc(pt.x, pt.y, rad, 0, Math.PI * 2);
        ctx.fill();
      }

      // Ball Body
      ctx.save();
      ctx.shadowBlur = 14;
      ctx.shadowColor = '#00f0ff';
      const bGrad = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.r);
      bGrad.addColorStop(0, '#ffffff');
      bGrad.addColorStop(0.6, '#a6f9ff');
      bGrad.addColorStop(1, '#00c3ff');
      ctx.fillStyle = bGrad;
      ctx.beginPath();
      ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    // Top HUD
    ctx.save();
    ctx.fillStyle = '#ffffff';
    ctx.font = 'bold 20px -apple-system, sans-serif';
    ctx.textBaseline = 'top';

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

    // Power-up Timer indicator
    if (paddle.wideTimer > 0) {
      ctx.textAlign = 'center';
      ctx.fillStyle = '#ff00ff';
      ctx.font = 'bold 15px -apple-system, sans-serif';
      ctx.fillText(`⚡ WIDE PADDLE: ${paddle.wideTimer.toFixed(1)}s`, W / 2, 24);
    }

    // Lives
    ctx.textAlign = 'right';
    ctx.fillStyle = '#ff3366';
    let heartIcons = '';
    for (let l = 0; l < lives; l++) heartIcons += '♥ ';
    ctx.fillText(`LIVES: ${heartIcons}`, W - 25, 22);
    ctx.restore();

    // Game Over Overlay
    if (state === 'GAMEOVER') {
      ctx.save();
      ctx.fillStyle = 'rgba(5, 8, 16, 0.82)';
      ctx.fillRect(0, 0, W, H);

      ctx.textAlign = 'center';
      ctx.fillStyle = '#ff0055';
      ctx.shadowBlur = 24;
      ctx.shadowColor = '#ff0055';
      ctx.font = '900 52px -apple-system, sans-serif';
      ctx.fillText('GAME OVER', W / 2, H / 2 - 40);

      ctx.shadowBlur = 0;
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 22px -apple-system, sans-serif';
      ctx.fillText(`Final Score: ${score}`, W / 2, H / 2 + 15);

      ctx.fillStyle = '#00f0ff';
      ctx.font = '16px -apple-system, sans-serif';
      ctx.fillText('CLICK OR PRESS ANY KEY TO PLAY AGAIN', W / 2, H / 2 + 65);
      ctx.restore();
    }

    // Victory Overlay
    if (state === 'VICTORY') {
      ctx.save();
      ctx.fillStyle = 'rgba(5, 8, 16, 0.82)';
      ctx.fillRect(0, 0, W, H);

      ctx.textAlign = 'center';
      ctx.fillStyle = '#00ff88';
      ctx.shadowBlur = 24;
      ctx.shadowColor = '#00ff88';
      ctx.font = '900 52px -apple-system, sans-serif';
      ctx.fillText('VICTORY!', W / 2, H / 2 - 40);

      ctx.shadowBlur = 0;
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 22px -apple-system, sans-serif';
      ctx.fillText(`Well Done! Final Score: ${score}`, W / 2, H / 2 + 15);

      ctx.fillStyle = '#00f0ff';
      ctx.font = '16px -apple-system, sans-serif';
      ctx.fillText('CLICK OR PRESS ANY KEY TO RESTART', W / 2, H / 2 + 65);
      ctx.restore();
    }

    ctx.restore();
  }

  // Main Loop
  function loop(now) {
    const dt = Math.min((now - lastTime) / 1000, 0.05);
    lastTime = now;

    update(dt);
    draw();

    requestAnimationFrame(loop);
  }

  requestAnimationFrame(loop);
</script>

</body>
</html>