Models 44

Gemini 4 Argon

by Googlereasoning defaultClean
Open in new window

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

How it went

Total time
2 min 7 s
Cost
$0.129
Output tokens
12,793
Spent thinking
0 tokens
Speed
112.5 tok/s
Console errors
0
First token after
13.2 s
First visible text after
13.2 s
Prompt tokens
563
Code size
30.5 KB
Finish reason
stop
Served by
Google AI Studio
Model id
google/gemini-4-argon
Reasoning sent
model default
Max output
943,718 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 · 1158 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<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 center, #13182b 0%, #070913 100%);
    color: #fff;
    font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    width: 100vw;
    overflow: hidden;
  }
  #game-wrapper {
    position: relative;
    width: 100%;
    height: 100%;
    max-width: 900px;
    max-height: 700px;
    aspect-ratio: 9 / 7;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 12px;
  }
  canvas {
    width: 100%;
    height: 100%;
    background: #0a0d1a;
    border-radius: 14px;
    box-shadow: 0 0 35px rgba(0, 240, 255, 0.18), inset 0 0 25px rgba(0, 240, 255, 0.08);
    border: 2px solid rgba(0, 240, 255, 0.35);
    display: block;
    cursor: pointer;
    touch-action: none;
  }
</style>
</head>
<body>

<div id="game-wrapper">
  <canvas id="gameCanvas" width="900" height="700"></canvas>
</div>

<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = 900;
const H = 700;

// Game State
let state = 'DEMO'; // DEMO, PLAYING, GAMEOVER, VICTORY
let score = 0;
let lives = 3;
let combo = 0;
let shakeTime = 0;
let shakeMag = 0;

// Input State
const keys = { ArrowLeft: false, ArrowRight: false, a: false, d: false };
let lastInputType = 'mouse';
let mouseX = W / 2;

// Entities
const paddle = {
  baseW: 115,
  wideW: 185,
  w: 115,
  targetW: 115,
  h: 16,
  x: W / 2 - 115 / 2,
  y: H - 48,
  speed: 680,
  wideTimer: 0
};

let balls = [];
let bricks = [];
let powerups = [];
let particles = [];
let floatingTexts = [];
let stars = [];

// Audio Context (Synthesized 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 || state === 'DEMO') return;
  try {
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    const now = audioCtx.currentTime;

    if (type === 'paddle') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(320, now);
      osc.frequency.exponentialRampToValueAtTime(480, now + 0.08);
      gain.gain.setValueAtTime(0.15, now);
      gain.gain.linearRampToValueAtTime(0.01, now + 0.08);
      osc.start(now);
      osc.stop(now + 0.08);
    } else if (type === 'brick') {
      osc.type = 'triangle';
      const pitch = 380 + Math.min(combo * 25, 400);
      osc.frequency.setValueAtTime(pitch, now);
      osc.frequency.exponentialRampToValueAtTime(pitch * 1.4, now + 0.09);
      gain.gain.setValueAtTime(0.18, now);
      gain.gain.linearRampToValueAtTime(0.01, now + 0.09);
      osc.start(now);
      osc.stop(now + 0.09);
    } else if (type === 'crack') {
      osc.type = 'square';
      osc.frequency.setValueAtTime(190, now);
      osc.frequency.exponentialRampToValueAtTime(120, now + 0.07);
      gain.gain.setValueAtTime(0.12, now);
      gain.gain.linearRampToValueAtTime(0.01, now + 0.07);
      osc.start(now);
      osc.stop(now + 0.07);
    } else if (type === 'powerup') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(400, now);
      osc.frequency.exponentialRampToValueAtTime(880, now + 0.22);
      gain.gain.setValueAtTime(0.2, now);
      gain.gain.linearRampToValueAtTime(0.01, now + 0.22);
      osc.start(now);
      osc.stop(now + 0.22);
    } else if (type === 'lose') {
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(250, now);
      osc.frequency.linearRampToValueAtTime(90, now + 0.35);
      gain.gain.setValueAtTime(0.2, now);
      gain.gain.linearRampToValueAtTime(0.01, now + 0.35);
      osc.start(now);
      osc.stop(now + 0.35);
    }
  } catch (e) {}
}

// Background Stars
for (let i = 0; i < 65; i++) {
  stars.push({
    x: Math.random() * W,
    y: Math.random() * H,
    r: Math.random() * 1.6 + 0.4,
    alpha: Math.random() * 0.6 + 0.2,
    speed: Math.random() * 14 + 4
  });
}

const rowColors = [
  { fill: '#ff2a6d', glow: '#ff2a6d', light: '#ff7aa2' }, // Row 0: Neon Pink
  { fill: '#ff7700', glow: '#ff7700', light: '#ffaa55' }, // Row 1: Neon Orange
  { fill: '#ffdd00', glow: '#ffdd00', light: '#ffee77' }, // Row 2: Neon Gold
  { fill: '#05ffa1', glow: '#05ffa1', light: '#77ffca' }, // Row 3: Neon Mint
  { fill: '#00e5ff', glow: '#00e5ff', light: '#88f3ff' }, // Row 4: Neon Cyan
  { fill: '#b967ff', glow: '#b967ff', light: '#d9adff' }  // Row 5: Neon Purple
];

// Deterministic pattern for tough bricks (rows 0, 1, 2, 4 have reinforced armor bricks)
const toughMap = [
  [1, 0, 1, 1, 0, 1, 1, 0, 1, 1],
  [0, 1, 0, 1, 1, 1, 1, 0, 1, 0],
  [1, 1, 0, 0, 1, 1, 0, 0, 1, 1],
  [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 1, 0, 0, 1, 0, 1, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];

// Powerup map ('W' = wide paddle, 'M' = multi-ball)
// Placed heavily in rows 4 & 5 so they drop within the first 2 seconds of gameplay/demo
const powerupMap = [
  [null, 'M',  null, null, 'W',  null, null, 'M',  null, null],
  [null, null, 'W',  null, null, null, 'M',  null, null, 'W' ],
  ['M',  null, null, null, 'W',  'M',  null, null, null, null],
  [null, 'W',  null, 'M',  null, null, 'W',  null, 'M',  null],
  [null, null, 'M',  null, 'W',  'M',  null, 'W',  null, null],
  ['W',  null, null, 'M',  null, null, 'W',  null, 'M',  null]
];

function createCrackPattern(w, h) {
  // Pre-compute deterministic-looking jagged crack lines per brick
  return [
    [
      { x: w * 0.28, y: 0 },
      { x: w * 0.36, y: h * 0.38 },
      { x: w * 0.24, y: h * 0.72 },
      { x: w * 0.38, y: h }
    ],
    [
      { x: w * 0.36, y: h * 0.38 },
      { x: w * 0.56, y: h * 0.55 },
      { x: w * 0.68, y: h * 0.25 }
    ],
    [
      { x: w * 0.72, y: h },
      { x: w * 0.64, y: h * 0.62 },
      { x: w * 0.80, y: h * 0.32 }
    ]
  ];
}

function initLevel() {
  bricks = [];
  powerups = [];
  particles = [];
  floatingTexts = [];
  combo = 0;

  const rows = 6;
  const cols = 10;
  const padX = 55;
  const padY = 82;
  const gap = 8;
  const brickW = (W - padX * 2 - gap * (cols - 1)) / cols; // 71.8px
  const brickH = 28;

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const isTough = toughMap[r][c] === 1;
      const pType = powerupMap[r][c];
      bricks.push({
        r, c,
        x: padX + c * (brickW + gap),
        y: padY + r * (brickH + gap),
        w: brickW,
        h: brickH,
        maxHp: isTough ? 2 : 1,
        hp: isTough ? 2 : 1,
        color: rowColors[r],
        powerup: pType,
        hitAnim: 0,
        cracks: createCrackPattern(brickW, brickH)
      });
    }
  }
}

function spawnBall(x, y, vx, vy) {
  balls.push({
    x, y,
    vx, vy,
    r: 8,
    baseSpeed: 500,
    trail: []
  });
}

function resetBallAndPaddle(isDemoStart = false) {
  paddle.w = paddle.baseW;
  paddle.targetW = paddle.baseW;
  paddle.wideTimer = 0;
  paddle.x = (W - paddle.w) / 2;
  balls = [];
  powerups = [];
  combo = 0;

  const speed = 500;
  // Aim directly toward a power-up brick in row 5 on initial launch
  const angle = isDemoStart ? -Math.PI / 2 + 0.23 : -Math.PI / 2 + (Math.random() * 0.5 - 0.25);
  spawnBall(
    paddle.x + paddle.w / 2,
    paddle.y - 12,
    Math.cos(angle) * speed,
    Math.sin(angle) * speed
  );
}

function startDemo() {
  state = 'DEMO';
  score = 0;
  lives = 3;
  initLevel();
  resetBallAndPaddle(true);
}

function startGame() {
  initAudio();
  state = 'PLAYING';
  score = 0;
  lives = 3;
  initLevel();
  resetBallAndPaddle(false);
}

// Input Listeners
window.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'a' || e.key === 'd' || e.key === 'A' || e.key === 'D') {
    keys[e.key] = true;
    lastInputType = 'keyboard';
  }
  if (state === 'DEMO' || state === 'GAMEOVER' || state === 'VICTORY') {
    if (e.key.length === 1 || e.key.startsWith('Arrow') || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      startGame();
    }
  }
});

window.addEventListener('keyup', (e) => {
  if (e.key in keys) keys[e.key] = false;
});

function updatePointerPos(clientX) {
  const rect = canvas.getBoundingClientRect();
  const scaleX = W / rect.width;
  mouseX = (clientX - rect.left) * scaleX;
  lastInputType = 'mouse';
}

canvas.addEventListener('mousemove', (e) => {
  if (state === 'PLAYING') {
    updatePointerPos(e.clientX);
  }
});

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

canvas.addEventListener('mousedown', (e) => {
  updatePointerPos(e.clientX);
  if (state !== 'PLAYING') {
    startGame();
  }
});

canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  if (e.touches.length > 0) updatePointerPos(e.touches[0].clientX);
  if (state !== 'PLAYING') {
    startGame();
  }
}, { passive: false });

// Particle & Visual Effects
function spawnBrickParticles(brick, destroyed) {
  const count = destroyed ? 20 : 8;
  const cx = brick.x + brick.w / 2;
  const cy = brick.y + brick.h / 2;
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = Math.random() * (destroyed ? 240 : 130) + 40;
    particles.push({
      x: cx + (Math.random() - 0.5) * brick.w * 0.7,
      y: cy + (Math.random() - 0.5) * brick.h * 0.7,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      size: Math.random() * 5 + 3,
      color: destroyed ? brick.color.fill : '#ffffff',
      alpha: 1,
      life: Math.random() * 0.45 + 0.25,
      maxLife: 0.7
    });
  }
}

function addFloatingText(text, x, y, color = '#fff') {
  floatingTexts.push({
    text, x, y,
    vy: -55,
    alpha: 1,
    life: 0.85,
    color
  });
}

function activatePowerup(type, x, y) {
  playSound('powerup');
  if (type === 'W') {
    paddle.wideTimer = 10.0;
    paddle.targetW = paddle.wideW;
    addFloatingText('WIDE PADDLE (10s)', x, y - 10, '#05ffa1');
  } else if (type === 'M') {
    addFloatingText('MULTI-BALL x3!', x, y - 10, '#ffaa00');
    const source = balls[0] || {
      x: paddle.x + paddle.w / 2,
      y: paddle.y - 12,
      vx: 0,
      vy: -500,
      baseSpeed: 500
    };
    const speed = Math.hypot(source.vx, source.vy) || 500;
    const baseAngle = Math.atan2(source.vy, source.vx);
    const offsets = [-0.32, 0.32];
    let idx = 0;
    while (balls.length < 3 && idx < 4) {
      const a = baseAngle + offsets[idx % 2] * (idx < 2 ? 1 : 1.6);
      // Ensure new balls travel mostly vertically
      let vx = Math.cos(a) * speed;
      let vy = Math.sin(a) * speed;
      if (Math.abs(vy) < speed * 0.35) {
        vy = -speed * 0.7;
        vx = (vx >= 0 ? 1 : -1) * Math.sqrt(speed * speed - vy * vy);
      }
      spawnBall(source.x, source.y, vx, vy);
      idx++;
    }
  }
  score += 50;
}

// Swept Circle-AABB Collision Detection (Prevents Tunneling)
function intersectBallRect(ball, rect) {
  const closestX = Math.max(rect.x, Math.min(ball.x, rect.x + rect.w));
  const closestY = Math.max(rect.y, Math.min(ball.y, rect.y + rect.h));
  const dx = ball.x - closestX;
  const dy = ball.y - closestY;
  const distSq = dx * dx + dy * dy;

  if (distSq > ball.r * ball.r) return null;

  const dist = Math.sqrt(distSq);
  let nx = 0, ny = 0, overlap = 0;

  if (dist > 0.0001) {
    nx = dx / dist;
    ny = dy / dist;
    overlap = ball.r - dist;
  } else {
    // Ball center is inside the rectangle; push out along shallowest axis
    const leftPen = ball.x - rect.x;
    const rightPen = (rect.x + rect.w) - ball.x;
    const topPen = ball.y - rect.y;
    const botPen = (rect.y + rect.h) - ball.y;
    const minPen = Math.min(leftPen, rightPen, topPen, botPen);
    if (minPen === leftPen) { nx = -1; overlap = leftPen + ball.r; }
    else if (minPen === rightPen) { nx = 1; overlap = rightPen + ball.r; }
    else if (minPen === topPen) { ny = -1; overlap = topPen + ball.r; }
    else { ny = 1; overlap = botPen + ball.r; }
  }

  return { nx, ny, overlap };
}

function updatePaddle(dt) {
  // Smooth width transition for Wide Power-up
  if (paddle.wideTimer > 0) {
    paddle.wideTimer -= dt;
    if (paddle.wideTimer <= 0) {
      paddle.wideTimer = 0;
      paddle.targetW = paddle.baseW;
    }
  }
  if (Math.abs(paddle.w - paddle.targetW) > 0.5) {
    const prevCenter = paddle.x + paddle.w / 2;
    paddle.w += (paddle.targetW - paddle.w) * Math.min(1, dt * 12);
    paddle.x = prevCenter - paddle.w / 2;
  } else {
    paddle.w = paddle.targetW;
  }

  if (state === 'DEMO') {
    // Smart AI controller for Demo mode
    let targetX = W / 2;
    let bestBall = null;
    let maxDanger = -Infinity;

    for (const b of balls) {
      const danger = b.y + (b.vy > 0 ? 350 : 0);
      if (danger > maxDanger) {
        maxDanger = danger;
        bestBall = b;
      }
    }

    if (bestBall) {
      // Aim with a gentle wave offset so the AI hits varied angles
      const aimOffset = Math.sin(performance.now() * 0.0023) * (paddle.w * 0.28);
      targetX = bestBall.x + aimOffset;
    }

    // If a power-up is about to land and the ball is safe, grab the powerup
    for (const p of powerups) {
      if (p.y > H * 0.45 && (!bestBall || bestBall.y < H * 0.55 || bestBall.vy < 0)) {
        targetX = p.x;
        break;
      }
    }

    const desiredX = targetX - paddle.w / 2;
    const diff = desiredX - paddle.x;
    const maxStep = paddle.speed * 0.95 * dt;
    if (Math.abs(diff) <= maxStep) paddle.x = desiredX;
    else paddle.x += Math.sign(diff) * maxStep;

  } else if (state === 'PLAYING') {
    const leftDown = keys.ArrowLeft || keys.a || keys.A;
    const rightDown = keys.ArrowRight || keys.d || keys.D;

    if (leftDown || rightDown) {
      if (leftDown) paddle.x -= paddle.speed * dt;
      if (rightDown) paddle.x += paddle.speed * dt;
    } else if (lastInputType === 'mouse') {
      paddle.x = mouseX - paddle.w / 2;
    }
  }

  // Clamp paddle inside walls
  if (paddle.x < 12) paddle.x = 12;
  if (paddle.x + paddle.w > W - 12) paddle.x = W - 12;
}

function stepBallPhysics(ball, dt) {
  // Record trail
  ball.trail.push({ x: ball.x, y: ball.y });
  if (ball.trail.length > 9) ball.trail.shift();

  // Sub-step movement so ball never moves more than 3px per collision check
  const speed = Math.hypot(ball.vx, ball.vy);
  const totalDist = speed * dt;
  const maxStepDist = 3.0;
  const steps = Math.max(1, Math.ceil(totalDist / maxStepDist));
  const subDt = dt / steps;

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

    // Wall collisions
    if (ball.x - ball.r < 10) {
      ball.x = 10 + ball.r;
      ball.vx = Math.abs(ball.vx);
    } else if (ball.x + ball.r > W - 10) {
      ball.x = W - 10 - ball.r;
      ball.vx = -Math.abs(ball.vx);
    }
    if (ball.y - ball.r < 54) {
      ball.y = 54 + ball.r;
      ball.vy = Math.abs(ball.vy);
    }

    // Paddle collision
    if (ball.vy > 0) {
      const hit = intersectBallRect(ball, paddle);
      if (hit) {
        // Push ball cleanly above paddle
        ball.y = paddle.y - ball.r - 0.5;

        // Calculate bounce angle based on where the ball hits the paddle
        // Center (0) = straight up (-PI/2), Edges (-1 or +1) = sharp 68 degree angle
        const paddleCenter = paddle.x + paddle.w / 2;
        let hitOffset = (ball.x - paddleCenter) / (paddle.w / 2);
        hitOffset = Math.max(-1, Math.min(1, hitOffset));

        const maxBounceAngle = 68 * (Math.PI / 180); // 68 degrees from vertical
        const bounceAngle = hitOffset * maxBounceAngle;

        const curSpeed = Math.min(680, Math.hypot(ball.vx, ball.vy) * 1.012);
        ball.vx = curSpeed * Math.sin(bounceAngle);
        ball.vy = -curSpeed * Math.cos(bounceAngle);

        combo = 0;
        playSound('paddle');

        // Subtle spark on paddle impact
        for (let i = 0; i < 5; i++) {
          particles.push({
            x: ball.x,
            y: paddle.y,
            vx: (Math.random() - 0.5) * 140,
            vy: -Math.random() * 100 - 20,
            size: 3,
            color: '#00f0ff',
            alpha: 1,
            life: 0.25,
            maxLife: 0.25
          });
        }
      }
    }

    // Brick collisions
    for (let i = bricks.length - 1; i >= 0; i--) {
      const b = bricks[i];
      const hit = intersectBallRect(ball, b);
      if (hit) {
        // Resolve penetration
        ball.x += hit.nx * hit.overlap;
        ball.y += hit.ny * hit.overlap;

        // Reflect velocity along collision normal
        if (Math.abs(hit.nx) > Math.abs(hit.ny)) {
          ball.vx = (hit.nx > 0 ? 1 : -1) * Math.abs(ball.vx);
        } else {
          ball.vy = (hit.ny > 0 ? 1 : -1) * Math.abs(ball.vy);
        }

        // Damage brick
        b.hp--;
        b.hitAnim = 1.0;
        combo++;

        if (b.hp <= 0) {
          playSound('brick');
          const pts = (b.maxHp === 2 ? 25 : 10) + Math.min(combo * 2, 30);
          score += pts;
          spawnBrickParticles(b, true);
          shakeTime = 0.08;
          shakeMag = 3;

          if (b.powerup) {
            powerups.push({
              x: b.x + b.w / 2,
              y: b.y + b.h / 2,
              vy: 175,
              type: b.powerup,
              r: 14,
              pulse: 0
            });
          }
          bricks.splice(i, 1);
        } else {
          playSound('crack');
          score += 5;
          spawnBrickParticles(b, false);
        }

        // Only break one brick per sub-step to prevent double reflections
        break;
      }
    }
  }
}

function update(dt) {
  // Update background stars
  for (const st of stars) {
    st.y += st.speed * dt;
    if (st.y > H) {
      st.y = 0;
      st.x = Math.random() * W;
    }
  }

  // Screen shake decay
  if (shakeTime > 0) shakeTime = Math.max(0, shakeTime - dt);

  // Update brick hit flash animations
  for (const b of bricks) {
    if (b.hitAnim > 0) b.hitAnim = Math.max(0, b.hitAnim - dt * 6);
  }

  // Update particles
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vy += 260 * dt; // subtle gravity
    p.life -= dt;
    p.alpha = Math.max(0, p.life / p.maxLife);
    if (p.life <= 0) particles.splice(i, 1);
  }

  // Update floating texts
  for (let i = floatingTexts.length - 1; i >= 0; i--) {
    const ft = floatingTexts[i];
    ft.y += ft.vy * dt;
    ft.life -= dt;
    ft.alpha = Math.max(0, ft.life / 0.85);
    if (ft.life <= 0) floatingTexts.splice(i, 1);
  }

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

  updatePaddle(dt);

  // Update balls
  for (let i = balls.length - 1; i >= 0; i--) {
    const b = balls[i];
    stepBallPhysics(b, dt);
    if (b.y - b.r > H) {
      balls.splice(i, 1);
    }
  }

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

    // Check paddle catch
    if (
      p.x + p.r > paddle.x &&
      p.x - p.r < paddle.x + paddle.w &&
      p.y + p.r > paddle.y &&
      p.y - p.r < paddle.y + paddle.h
    ) {
      activatePowerup(p.type, p.x, paddle.y);
      powerups.splice(i, 1);
      continue;
    }

    if (p.y - p.r > H) {
      powerups.splice(i, 1);
    }
  }

  // Check Victory
  if (bricks.length === 0) {
    if (state === 'DEMO') {
      initLevel();
      resetBallAndPaddle(true);
    } else {
      state = 'VICTORY';
    }
    return;
  }

  // Check All Balls Lost
  if (balls.length === 0) {
    if (state === 'DEMO') {
      resetBallAndPaddle(true);
    } else {
      lives--;
      playSound('lose');
      shakeTime = 0.2;
      shakeMag = 6;
      if (lives <= 0) {
        state = 'GAMEOVER';
      } else {
        resetBallAndPaddle(false);
      }
    }
  }
}

// Rendering Helpers
function drawRoundRect(x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

function drawPowerupBadge(cx, cy, type, scale = 1) {
  ctx.save();
  ctx.translate(cx, cy);
  ctx.scale(scale, scale);

  if (type === 'W') {
    // Wide Paddle Icon: Pill with left-right outward arrows
    ctx.fillStyle = 'rgba(5, 255, 161, 0.22)';
    ctx.strokeStyle = '#05ffa1';
    ctx.lineWidth = 1.8;
    drawRoundRect(-15, -8, 30, 16, 8);
    ctx.fill();
    ctx.stroke();

    ctx.beginPath();
    // Left arrow
    ctx.moveTo(-4, 0); ctx.lineTo(-10, 0);
    ctx.moveTo(-7, -3.5); ctx.lineTo(-10.5, 0); ctx.lineTo(-7, 3.5);
    // Right arrow
    ctx.moveTo(4, 0); ctx.lineTo(10, 0);
    ctx.moveTo(7, -3.5); ctx.lineTo(10.5, 0); ctx.lineTo(7, 3.5);
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 1.8;
    ctx.stroke();
  } else if (type === 'M') {
    // Multi-ball Icon: 3 glowing mini orbs in a triangle cluster
    ctx.fillStyle = 'rgba(255, 200, 0, 0.24)';
    ctx.strokeStyle = '#ffdd00';
    ctx.lineWidth = 1.8;
    drawRoundRect(-14, -9, 28, 18, 9);
    ctx.fill();
    ctx.stroke();

    ctx.fillStyle = '#ffffff';
    const coords = [[0, -3.5], [-4.5, 3.2], [4.5, 3.2]];
    for (const [bx, by] of coords) {
      ctx.beginPath();
      ctx.arc(bx, by, 2.6, 0, Math.PI * 2);
      ctx.fill();
    }
  }
  ctx.restore();
}

function drawBrick(b) {
  ctx.save();

  // Base brick fill
  ctx.fillStyle = b.color.fill;
  drawRoundRect(b.x, b.y, b.w, b.h, 5);
  ctx.fill();

  // Top bevel highlight
  ctx.fillStyle = 'rgba(255, 255, 255, 0.22)';
  drawRoundRect(b.x + 2, b.y + 2, b.w - 4, b.h * 0.38, 3);
  ctx.fill();

  // Tough brick metallic frame (2-Hit Armor)
  if (b.maxHp === 2) {
    ctx.strokeStyle = b.hp === 2 ? '#ffffff' : 'rgba(255, 255, 255, 0.65)';
    ctx.lineWidth = 2.4;
    drawRoundRect(b.x + 1.5, b.y + 1.5, b.w - 3, b.h - 3, 4);
    ctx.stroke();

    // Corner rivets for intact armor bricks
    if (b.hp === 2) {
      ctx.fillStyle = '#ffffff';
      const r = 1.7;
      ctx.fillRect(b.x + 4, b.y + 4, r, r);
      ctx.fillRect(b.x + b.w - 4 - r, b.y + 4, r, r);
      ctx.fillRect(b.x + 4, b.y + b.h - 4 - r, r, r);
      ctx.fillRect(b.x + b.w - 4 - r, b.y + b.h - 4 - r, r, r);
    }
  }

  // Visible cracks when a 2-hit brick has taken 1 hit
  if (b.maxHp === 2 && b.hp === 1) {
    ctx.strokeStyle = '#090c17';
    ctx.lineWidth = 2.5;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    for (const line of b.cracks) {
      ctx.beginPath();
      ctx.moveTo(b.x + line[0].x, b.y + line[0].y);
      for (let i = 1; i < line.length; i++) {
        ctx.lineTo(b.x + line[i].x, b.y + line[i].y);
      }
      ctx.stroke();
    }
    // Bright fracture edge highlight
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(b.x + b.cracks[0][0].x + 1, b.y + b.cracks[0][0].y);
    for (let i = 1; i < b.cracks[0].length; i++) {
      ctx.lineTo(b.x + b.cracks[0][i].x + 1, b.y + b.cracks[0][i].y);
    }
    ctx.stroke();
  }

  // Power-up icon visibly embedded on brick
  if (b.powerup) {
    drawPowerupBadge(b.x + b.w / 2, b.y + b.h / 2, b.powerup, 0.95);
  }

  // Hit flash overlay
  if (b.hitAnim > 0) {
    ctx.fillStyle = `rgba(255, 255, 255, ${b.hitAnim * 0.75})`;
    drawRoundRect(b.x, b.y, b.w, b.h, 5);
    ctx.fill();
  }

  ctx.restore();
}

function drawHUD() {
  // Top bar background
  ctx.fillStyle = 'rgba(14, 20, 40, 0.85)';
  ctx.fillRect(10, 10, W - 20, 42);
  ctx.strokeStyle = 'rgba(0, 240, 255, 0.3)';
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(10, 52);
  ctx.lineTo(W - 10, 52);
  ctx.stroke();

  // Score
  ctx.fillStyle = '#88f3ff';
  ctx.font = 'bold 18px "Segoe UI", sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText(`SCORE: ${String(score).padStart(5, '0')}`, 26, 37);

  // Active Wide-Paddle Timer Bar
  if (paddle.wideTimer > 0) {
    const barW = 150;
    const bx = W / 2 - barW / 2;
    const by = 22;
    ctx.fillStyle = 'rgba(5, 255, 161, 0.18)';
    drawRoundRect(bx, by, barW, 18, 9);
    ctx.fill();

    const pct = Math.max(0, paddle.wideTimer / 10);
    ctx.fillStyle = '#05ffa1';
    drawRoundRect(bx + 2, by + 2, (barW - 4) * pct, 14, 7);
    ctx.fill();

    ctx.fillStyle = '#070913';
    ctx.font = 'bold 11px "Segoe UI", sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(`WIDE PADDLE ${paddle.wideTimer.toFixed(1)}s`, W / 2, by + 13);
  } else {
    // Legend in center of HUD
    ctx.font = '600 13px "Segoe UI", sans-serif';
    ctx.fillStyle = 'rgba(255, 255, 255, 0.65)';
    ctx.textAlign = 'center';
    ctx.fillText('POWER-UPS:  [↔ WIDE PADDLE]   [••• MULTI-BALL]', W / 2, 36);
  }

  // Lives indicator
  ctx.textAlign = 'right';
  ctx.fillStyle = '#ff7aa2';
  ctx.font = 'bold 16px "Segoe UI", sans-serif';
  ctx.fillText('LIVES:', W - 108, 37);

  for (let i = 0; i < 3; i++) {
    const lx = W - 85 + i * 26;
    const ly = 31;
    ctx.beginPath();
    ctx.arc(lx, ly, 7.5, 0, Math.PI * 2);
    if (i < lives) {
      ctx.fillStyle = '#ff2a6d';
      ctx.shadowColor = '#ff2a6d';
      ctx.shadowBlur = 8;
      ctx.fill();
      ctx.shadowBlur = 0;
    } else {
      ctx.strokeStyle = 'rgba(255, 42, 109, 0.35)';
      ctx.lineWidth = 2;
      ctx.stroke();
    }
  }
}

function drawOverlay() {
  if (state === 'PLAYING') return;

  if (state === 'DEMO') {
    // Translucent floating banner in lower-middle area so bricks & action stay 100% clear
    const boxW = 490;
    const boxH = 128;
    const bx = (W - boxW) / 2;
    const by = H * 0.56;

    ctx.save();
    ctx.fillStyle = 'rgba(8, 12, 26, 0.84)';
    ctx.strokeStyle = 'rgba(0, 240, 255, 0.65)';
    ctx.lineWidth = 2;
    ctx.shadowColor = 'rgba(0, 240, 255, 0.25)';
    ctx.shadowBlur = 20;
    drawRoundRect(bx, by, boxW, boxH, 16);
    ctx.fill();
    ctx.stroke();
    ctx.shadowBlur = 0;

    ctx.textAlign = 'center';
    ctx.fillStyle = '#00f0ff';
    ctx.font = 'bold 26px "Segoe UI", sans-serif';
    ctx.fillText('NEON BREAKOUT — DEMO MODE', W / 2, by + 40);

    const pulse = 0.65 + 0.35 * Math.sin(performance.now() * 0.006);
    ctx.fillStyle = `rgba(255, 255, 255, ${pulse})`;
    ctx.font = 'bold 20px "Segoe UI", sans-serif';
    ctx.fillText('Click or press any key to start', W / 2, by + 76);

    ctx.fillStyle = 'rgba(200, 225, 255, 0.7)';
    ctx.font = '14px "Segoe UI", sans-serif';
    ctx.fillText('Controls: Mouse or Left / Right Arrow Keys', W / 2, by + 106);
    ctx.restore();
    return;
  }

  // Victory / Game Over Modal
  ctx.save();
  ctx.fillStyle = 'rgba(6, 9, 20, 0.78)';
  ctx.fillRect(0, 0, W, H);

  const boxW = 460;
  const boxH = 250;
  const bx = (W - boxW) / 2;
  const by = (H - boxH) / 2;

  const isWin = state === 'VICTORY';
  const accent = isWin ? '#05ffa1' : '#ff2a6d';

  ctx.fillStyle = '#0d1326';
  ctx.strokeStyle = accent;
  ctx.lineWidth = 3;
  ctx.shadowColor = accent;
  ctx.shadowBlur = 25;
  drawRoundRect(bx, by, boxW, boxH, 18);
  ctx.fill();
  ctx.stroke();
  ctx.shadowBlur = 0;

  ctx.textAlign = 'center';
  ctx.fillStyle = accent;
  ctx.font = 'bold 42px "Segoe UI", sans-serif';
  ctx.fillText(isWin ? 'VICTORY!' : 'GAME OVER', W / 2, by + 68);

  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 24px "Segoe UI", sans-serif';
  ctx.fillText(`FINAL SCORE: ${score}`, W / 2, by + 122);

  // Restart button
  const btnW = 280;
  const btnH = 50;
  const btnX = (W - btnW) / 2;
  const btnY = by + 162;

  ctx.fillStyle = accent;
  drawRoundRect(btnX, btnY, btnW, btnH, 10);
  ctx.fill();

  ctx.fillStyle = '#070913';
  ctx.font = 'bold 19px "Segoe UI", sans-serif';
  ctx.fillText('CLICK OR PRESS KEY TO RESTART', W / 2, btnY + 31);
  ctx.restore();
}

function render() {
  ctx.clearRect(0, 0, W, H);

  ctx.save();
  if (shakeTime > 0) {
    const dx = (Math.random() - 0.5) * shakeMag * 2;
    const dy = (Math.random() - 0.5) * shakeMag * 2;
    ctx.translate(dx, dy);
  }

  // Starfield background
  for (const st of stars) {
    ctx.fillStyle = `rgba(180, 220, 255, ${st.alpha})`;
    ctx.beginPath();
    ctx.arc(st.x, st.y, st.r, 0, Math.PI * 2);
    ctx.fill();
  }

  // Playfield border
  ctx.strokeStyle = 'rgba(0, 240, 255, 0.25)';
  ctx.lineWidth = 2;
  ctx.strokeRect(10, 10, W - 20, H - 20);

  // Bricks
  for (const b of bricks) {
    drawBrick(b);
  }

  // Falling Power-ups
  for (const p of powerups) {
    const scale = 1.15 + Math.sin(p.pulse) * 0.1;
    ctx.save();
    ctx.shadowColor = p.type === 'W' ? '#05ffa1' : '#ffdd00';
    ctx.shadowBlur = 12;
    drawPowerupBadge(p.x, p.y, p.type, scale);
    ctx.restore();
  }

  // Paddle
  ctx.save();
  const padColor = paddle.wideTimer > 0 ? '#05ffa1' : '#00f0ff';
  ctx.shadowColor = padColor;
  ctx.shadowBlur = 14;
  ctx.fillStyle = padColor;
  drawRoundRect(paddle.x, paddle.y, paddle.w, paddle.h, 8);
  ctx.fill();
  ctx.shadowBlur = 0;

  // Paddle center core highlight
  ctx.fillStyle = '#ffffff';
  drawRoundRect(paddle.x + 8, paddle.y + 3, paddle.w - 16, 4, 2);
  ctx.fill();
  ctx.restore();

  // Balls & Motion Trails
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i];
      const ratio = (i + 1) / b.trail.length;
      ctx.fillStyle = `rgba(0, 240, 255, ${ratio * 0.35})`;
      ctx.beginPath();
      ctx.arc(t.x, t.y, b.r * ratio * 0.9, 0, Math.PI * 2);
      ctx.fill();
    }

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

  // Particles
  for (const p of particles) {
    ctx.save();
    ctx.globalAlpha = p.alpha;
    ctx.fillStyle = p.color;
    ctx.translate(p.x, p.y);
    ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
    ctx.restore();
  }

  // Floating Texts
  for (const ft of floatingTexts) {
    ctx.save();
    ctx.globalAlpha = ft.alpha;
    ctx.font = 'bold 16px "Segoe UI", sans-serif';
    ctx.fillStyle = ft.color;
    ctx.textAlign = 'center';
    ctx.shadowColor = '#000';
    ctx.shadowBlur = 4;
    ctx.fillText(ft.text, ft.x, ft.y);
    ctx.restore();
  }

  drawHUD();
  ctx.restore();

  drawOverlay();
}

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

  update(dt);
  render();
  requestAnimationFrame(loop);
}

// Start in self-playing Demo Mode immediately
startDemo();
requestAnimationFrame(loop);
</script>
</body>
</html>