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
1 min 8 s
Cost
$0.0061
Output tokens
7,638
Spent thinking
0 tokens
Speed
126.3 tok/s
Console errors
0
First token after
8.0 s
First visible text after
8.0 s
Prompt tokens
509
Code size
22.1 KB
Finish reason
stop
Served by
OpenInference
Model id
deepseek/deepseek-v4.1-flash-1017
Reasoning sent
{"effort":"none"}
Max output
64,000 tokens
Generated
Nov 10, 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 · 760 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; }
  body {
    background: #0a0a14;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    font-family: 'Segoe UI', Arial, sans-serif;
    overflow: hidden;
  }
  #game {
    display: block;
    border-radius: 8px;
    box-shadow: 0 0 40px rgba(80, 120, 255, 0.25);
    max-width: 100vw;
    max-height: 100vh;
    touch-action: none;
  }
  #overlay {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(2px);
    z-index: 10;
    cursor: pointer;
    transition: opacity 0.3s;
  }
  #overlay.hidden { opacity: 0; pointer-events: none; }
  #overlay .msg {
    color: #e8eefc;
    font-size: clamp(18px, 3vw, 30px);
    font-weight: 700;
    letter-spacing: 1px;
    text-align: center;
    padding: 20px 32px;
    background: rgba(20, 30, 60, 0.7);
    border: 1px solid rgba(120, 160, 255, 0.4);
    border-radius: 12px;
    box-shadow: 0 0 30px rgba(80, 120, 255, 0.3);
    animation: pulse 2s ease-in-out infinite;
  }
  @keyframes pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.03); }
  }
</style>
</head>
<body>
<canvas id="game" width="1280" height="800"></canvas>
<div id="overlay"><div class="msg">Click or press a key to start</div></div>
<script>
(function(){
  'use strict';
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');

  const W = 1280, H = 800;
  // Scale to fit viewport
  function fit(){
    const s = Math.min(window.innerWidth / W, window.innerHeight / H);
    canvas.style.width = (W*s)+'px';
    canvas.style.height = (H*s)+'px';
  }
  window.addEventListener('resize', fit);
  fit();

  // Game constants
  const COLS = 12;
  const ROWS = 7;
  const BRICK_TOP = 90;
  const BRICK_GAP = 4;
  const BRICK_H = 28;
  const BRICK_W = (W - 40) / COLS - BRICK_GAP;
  const PADDLE_Y = H - 60;
  const PADDLE_H = 18;
  const PADDLE_W = 130;
  const PADDLE_WIDE = 200;
  const BALL_R = 9;
  const PADDLE_SPEED = 700;
  const START_SPEED = 380;

  const state = {
    mode: 'menu', // menu | playing | gameover | win
    paddle: { x: W/2 - PADDLE_W/2, w: PADDLE_W, targetW: PADDLE_W, wideTimer: 0 },
    balls: [],
    bricks: [],
    powerups: [],
    particles: [],
    score: 0,
    lives: 3,
    time: 0,
    mouseX: W/2,
    keys: {},
    demo: false,
    demoAI: { target: W/2, timer: 0 },
    lastT: 0,
    shake: 0,
  };

  function resetBricks(){
    state.bricks = [];
    for (let r = 0; r < ROWS; r++){
      for (let c = 0; c < COLS; c++){
        const x = 20 + c * (BRICK_W + BRICK_GAP);
        const y = BRICK_TOP + r * (BRICK_H + BRICK_GAP);
        // tougher bricks in some rows
        const tough = (r >= 2 && r <= 4) && ((r + c) % 3 === 0) || (r === 1 && c % 4 === 0);
        const power = tough ? (c === 0 || c === 5) : ((c === 3 || c === 8) && r === 2);
        const hue = 200 + r * 22;
        state.bricks.push({
          x, y, w: BRICK_W, h: BRICK_H,
          hp: tough ? 2 : 1,
          maxHp: tough ? 2 : 1,
          alive: true,
          power: power,
          hue: hue,
          hitFlash: 0,
        });
      }
    }
  }

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

  function resetGame(){
    state.paddle = { x: W/2 - PADDLE_W/2, w: PADDLE_W, targetW: PADDLE_W, wideTimer: 0 };
    state.paddle.x = W/2 - state.paddle.w/2;
    state.balls = [newBall(W/2, PADDLE_Y - BALL_R - 2, START_SPEED*0.5, -START_SPEED)];
    state.powerups = [];
    state.particles = [];
    state.score = 0;
    state.lives = 3;
    state.time = 0;
    state.shake = 0;
    resetBricks();
  }

  function startGame(){
    state.mode = 'playing';
    state.demo = false;
    resetGame();
    overlay.classList.add('hidden');
  }

  function startDemo(){
    state.mode = 'playing';
    state.demo = true;
    resetGame();
    overlay.classList.remove('hidden');
  }

  function gameOver(){
    state.mode = 'gameover';
  }
  function win(){
    state.mode = 'win';
  }

  // Input
  function handleKey(e){
    if (e.type === 'keydown' && state.mode === 'menu'){
      startGame();
    }
    if (state.mode === 'playing' && !state.demo){
      if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') e.preventDefault();
      state.keys[e.key] = true;
    }
    if (state.mode === 'gameover' || state.mode === 'win'){
      if (e.type === 'keydown' && (e.key === ' ' || e.key === 'Enter')) {
        startGame();
      }
    }
  }
  window.addEventListener('keydown', handleKey);
  window.addEventListener('keyup', e => { state.keys[e.key] = false; });

  function updateMouse(e){
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width * W;
    state.mouseX = x;
  }
  canvas.addEventListener('mousemove', updateMouse);
  canvas.addEventListener('touchmove', e => {
    e.preventDefault();
    const t = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    state.mouseX = (t.clientX - rect.left) / rect.width * W;
  }, { passive: false });

  canvas.addEventListener('mousedown', () => {
    if (state.mode === 'menu') startGame();
    else if (state.mode === 'gameover' || state.mode === 'win') startGame();
  });
  overlay.addEventListener('mousedown', e => {
    e.preventDefault();
    if (state.mode === 'menu') startGame();
    else if (state.mode === 'gameover' || state.mode === 'win') startGame();
  });
  window.addEventListener('keydown', e => {
    if (state.mode === 'menu') startGame();
  });

  // ---- Collision helpers ----
  function circleRectCollide(cx, cy, r, rx, ry, rw, rh){
    const nx = Math.max(rx, Math.min(cx, rx + rw));
    const ny = Math.max(ry, Math.min(cy, ry + rh));
    const dx = cx - nx, dy = cy - ny;
    const d2 = dx*dx + dy*dy;
    if (d2 > r*r) return null;
    const d = Math.sqrt(d2) || 0.0001;
    return { nx: dx/d, ny: dy/d, dist: d, nx0: nx, ny0: ny };
  }

  function spawnParticles(x, y, hue, count){
    for (let i = 0; i < count; i++){
      const a = Math.random() * Math.PI * 2;
      const sp = 60 + Math.random() * 220;
      state.particles.push({
        x, y, vx: Math.cos(a)*sp, vy: Math.sin(a)*sp,
        life: 0.5 + Math.random()*0.5, maxLife: 0.5 + Math.random()*0.5,
        size: 2 + Math.random()*4, hue,
      });
    }
  }

  function addPowerup(x, y, type){
    state.powerups.push({ x, y, w: 30, h: 22, vy: 130, type, t: 0 });
  }

  function updatePaddle(dt){
    const p = state.paddle;
    // target width
    if (p.wideTimer > 0){
      p.wideTimer -= dt;
      p.targetW = PADDLE_WIDE;
    } else {
      p.targetW = PADDLE_W;
    }
    // smooth width
    const center = p.x + p.w/2;
    p.w += (p.targetW - p.w) * Math.min(1, dt*10);
    p.x = center - p.w/2;

    if (state.demo){
      // AI follows predicted ball
      const ai = state.demoAI;
      ai.timer -= dt;
      if (ai.timer <= 0){
        ai.timer = 0.15;
        // find ball with lowest y (closest to paddle)
        let best = null;
        for (const b of state.balls){
          if (b.vy > 0 && (!best || b.y > best.y)) best = b;
        }
        if (best){
          // predict x at paddle y
          const ty = PADDLE_Y - BALL_R;
          let dy = ty - best.y;
          if (dy > 0){
            let t = dy / best.vy;
            let px = best.x + best.vx * t;
            // bounce off walls
            const span = W - 2*BALL_R;
            px = ((px - BALL_R) % (2*span) + 2*span) % (2*span) + BALL_R;
            if (px > W - BALL_R) px = W - BALL_R - (px - (W - BALL_R));
            ai.target = px + (Math.random()-0.5)*40;
          }
        } else {
          ai.target = W/2;
        }
      }
      const target = ai.target;
      const diff = target - (p.x + p.w/2);
      if (Math.abs(diff) > 4){
        p.x += Math.sign(diff) * Math.min(Math.abs(diff), PADDLE_SPEED * dt);
      }
    } else {
      let move = 0;
      if (state.keys['ArrowLeft']) move -= 1;
      if (state.keys['ArrowRight']) move += 1;
      if (move !== 0){
        p.x += move * PADDLE_SPEED * dt;
      } else {
        // mouse follow
        const targetCenter = state.mouseX;
        const diff = targetCenter - (p.x + p.w/2);
        if (Math.abs(diff) > 2){
          p.x += Math.sign(diff) * Math.min(Math.abs(diff), PADDLE_SPEED * dt * 1.2);
        }
      }
    }
    // clamp
    if (p.x < 0) p.x = 0;
    if (p.x + p.w > W) p.x = W - p.w;
  }

  function updateBalls(dt){
    const p = state.paddle;
    const toRemove = [];
    for (let i = 0; i < state.balls.length; i++){
      const b = state.balls[i];
      // substeps to prevent tunneling
      const speed = Math.hypot(b.vx, b.vy);
      const steps = Math.max(1, Math.ceil(speed * dt / (BALL_R * 0.8)));
      const sdt = dt / steps;
      for (let s = 0; s < steps; s++){
        b.x += b.vx * sdt;
        b.y += b.vy * sdt;

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

        // paddle collision
        const pRect = { x: p.x, y: PADDLE_Y, w: p.w, h: PADDLE_H };
        if (b.vy > 0){
          const hit = circleRectCollide(b.x, b.y, b.r, pRect.x, pRect.y, pRect.w, pRect.h);
          if (hit){
            // reposition
            const overlap = b.r - hit.dist;
            b.x += hit.nx * overlap;
            b.y += hit.ny * overlap;
            // relative x position
            const rel = (b.x - (p.x + p.w/2)) / (p.w/2);
            const clamped = Math.max(-1, Math.min(1, rel));
            // angle: -60 to +60 deg
            const maxAngle = Math.PI/3;
            const angle = clamped * maxAngle;
            const sp = Math.max(speed, START_SPEED);
            // ensure upward
            b.vx = Math.sin(angle) * sp;
            b.vy = -Math.abs(Math.cos(angle)) * sp;
            b.y = pRect.y - b.r - 0.5;
            state.shake = Math.max(state.shake, 3);
          }
        }

        // bricks
        for (const br of state.bricks){
          if (!br.alive) continue;
          const hit = circleRectCollide(b.x, b.y, b.r, br.x, br.y, br.w, br.h);
          if (hit){
            // reposition
            const overlap = b.r - hit.dist;
            b.x += hit.nx * overlap;
            b.y += hit.ny * overlap;
            // reflect
            if (Math.abs(hit.nx) > Math.abs(hit.ny)){
              b.vx = -b.vx * 1.0;
            } else {
              b.vy = -b.vy;
            }
            br.hp--;
            br.hitFlash = 1;
            if (br.hp <= 0){
              br.alive = false;
              state.score += 10;
              spawnParticles(br.x + br.w/2, br.y + br.h/2, br.hue, 14);
              if (br.power) addPowerup(br.x + br.w/2, br.y + br.h/2, br.power === 'w' ? 'wide' : 'multi');
              state.shake = Math.max(state.shake, 5);
            } else {
              state.score += 2;
              spawnParticles(br.x + br.w/2, br.y + br.h/2, br.hue, 5);
              state.shake = Math.max(state.shake, 2);
            }
            break;
          }
        }

        // bottom
        if (b.y - b.r > H){
          toRemove.push(i);
          break;
        }
      }
    }
    for (let i = toRemove.length - 1; i >= 0; i--){
      state.balls.splice(toRemove[i], 1);
    }
    if (state.balls.length === 0 && state.mode === 'playing'){
      state.lives--;
      if (state.lives <= 0){
        gameOver();
      } else {
        state.balls.push(newBall(p.x + p.w/2, PADDLE_Y - BALL_R - 2, START_SPEED*0.5, -START_SPEED));
      }
    }
    // check win
    if (state.mode === 'playing' && state.bricks.every(b => !b.alive)){
      win();
    }
  }

  function updatePowerups(dt){
    for (let i = state.powerups.length - 1; i >= 0; i--){
      const pu = state.powerups[i];
      pu.y += pu.vy * dt;
      pu.t += dt;
      const p = state.paddle;
      if (pu.y + pu.h > PADDLE_Y && pu.y < PADDLE_Y + PADDLE_H && pu.x + pu.w > p.x && pu.x < p.x + p.w){
        if (pu.type === 'wide'){
          p.wideTimer = 10;
        } else {
          // multi
          const center = p.x + p.w/2;
          const src = state.balls[0];
          if (src){
            for (let k = 0; k < 2; k++){
              const a = (k === 0 ? -0.5 : 0.5) + (Math.random()-0.5)*0.2;
              const sp = Math.hypot(src.vx, src.vy);
              state.balls.push(newBall(center, PADDLE_Y - BALL_R - 2, Math.sin(a)*sp, -Math.cos(a)*sp));
            }
          }
        }
        spawnParticles(pu.x + pu.w/2, pu.y + pu.h/2, 60, 12);
        state.powerups.splice(i, 1);
      } else if (pu.y > H + 20){
        state.powerups.splice(i, 1);
      }
    }
  }

  function updateParticles(dt){
    for (let i = state.particles.length - 1; i >= 0; i--){
      const p = state.particles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vy += 220 * dt;
      p.life -= dt;
      if (p.life <= 0) state.particles.splice(i, 1);
    }
  }

  // ---- Draw ----
  function drawBackground(){
    // subtle gradient
    const g = ctx.createRadialGradient(W/2, H/2, 50, W/2, H/2, W*0.8);
    g.addColorStop(0, '#141a2e');
    g.addColorStop(1, '#070a14');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);

    // grid lines
    ctx.strokeStyle = 'rgba(80,120,255,0.05)';
    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();
    }
  }

  function drawBricks(){
    for (const b of state.bricks){
      if (!b.alive) continue;
      const r = 6;
      // base
      ctx.beginPath();
      ctx.roundRect(b.x, b.y, b.w, b.h, r);
      const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
      if (b.hp > 1){
        grad.addColorStop(0, `hsl(${b.hue}, 70%, 55%)`);
        grad.addColorStop(1, `hsl(${b.hue}, 70%, 38%)`);
      } else {
        grad.addColorStop(0, `hsl(${b.hue}, 60%, 45%)`);
        grad.addColorStop(1, `hsl(${b.hue}, 60%, 30%)`);
      }
      ctx.fillStyle = grad;
      ctx.fill();
      // highlight
      ctx.strokeStyle = `hsla(${b.hue}, 90%, 80%, 0.5)`;
      ctx.lineWidth = 1.5;
      ctx.stroke();

      // crack lines if damaged
      if (b.hp < b.maxHp){
        ctx.strokeStyle = 'rgba(10,10,20,0.75)';
        ctx.lineWidth = 2;
        ctx.beginPath();
        const cx = b.x + b.w/2, cy = b.y + b.h/2;
        ctx.moveTo(cx - 8, cy - 4);
        ctx.lineTo(cx - 2, cy + 2);
        ctx.lineTo(cx + 3, cy - 6);
        ctx.lineTo(cx + 9, cy + 4);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(cx - 2, cy + 2);
        ctx.lineTo(cx - 6, cy + 9);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(cx + 3, cy - 6);
        ctx.lineTo(cx + 2, cy - 11);
        ctx.stroke();
      }

      // power icon
      if (b.power){
        const cx = b.x + b.w/2, cy = b.y + b.h/2;
        ctx.save();
        ctx.translate(cx, cy);
        if (b.power === 'wide'){
          // arrows
          ctx.fillStyle = 'rgba(255,255,255,0.9)';
          ctx.beginPath();
          ctx.moveTo(-10, 0); ctx.lineTo(-4, -4); ctx.lineTo(-4, 4); ctx.closePath(); ctx.fill();
          ctx.beginPath();
          ctx.moveTo(10, 0); ctx.lineTo(4, -4); ctx.lineTo(4, 4); ctx.closePath(); ctx.fill();
          ctx.fillRect(-4, -2, 8, 4);
        } else {
          // three dots / ball
          ctx.fillStyle = 'rgba(255,255,255,0.9)';
          ctx.beginPath(); ctx.arc(-6, -2, 3, 0, Math.PI*2); ctx.fill();
          ctx.beginPath(); ctx.arc(6, -2, 3, 0, Math.PI*2); ctx.fill();
          ctx.beginPath(); ctx.arc(0, 4, 3, 0, Math.PI*2); ctx.fill();
        }
        ctx.restore();
      }

      // flash
      if (b.hitFlash > 0){
        ctx.fillStyle = `rgba(255,255,255,${b.hitFlash*0.5})`;
        ctx.beginPath();
        ctx.roundRect(b.x, b.y, b.w, b.h, r);
        ctx.fill();
      }
    }
  }

  function drawPaddle(){
    const p = state.paddle;
    const y = PADDLE_Y;
    const grad = ctx.createLinearGradient(p.x, y, p.x, y + PADDLE_H);
    grad.addColorStop(0, '#5a8cff');
    grad.addColorStop(0.5, '#2a5fff');
    grad.addColorStop(1, '#1a3aa0');
    ctx.beginPath();
    ctx.roundRect(p.x, y, p.w, PADDLE_H, 9);
    ctx.fillStyle = grad;
    ctx.fill();
    ctx.strokeStyle = 'rgba(180,210,255,0.8)';
    ctx.lineWidth = 1.5;
    ctx.stroke();
    // glow
    ctx.shadowColor = 'rgba(80,140,255,0.8)';
    ctx.shadowBlur = 18;
    ctx.beginPath();
    ctx.roundRect(p.x, y, p.w, PADDLE_H, 9);
    ctx.stroke();
    ctx.shadowBlur = 0;
  }

  function drawBalls(){
    for (const b of state.balls){
      // trail
      ctx.save();
      for (let i = 0; i < b.trail.length; i++){
        const t = b.trail[i];
        const a = (i / b.trail.length) * 0.4;
        ctx.fillStyle = `rgba(255,255,255,${a})`;
        ctx.beginPath();
        ctx.arc(t.x, t.y, b.r * (0.4 + 0.5 * i/b.trail.length), 0, Math.PI*2);
        ctx.fill();
      }
      ctx.restore();
      // glow
      ctx.shadowColor = 'rgba(200,230,255,0.9)';
      ctx.shadowBlur = 16;
      ctx.fillStyle = '#f0f8ff';
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r, 0, Math.PI*2);
      ctx.fill();
      ctx.shadowBlur = 0;
      // inner highlight
      ctx.fillStyle = 'rgba(255,255,255,0.9)';
      ctx.beginPath();
      ctx.arc(b.x - b.r*0.3, b.y - b.r*0.3, b.r*0.4, 0, Math.PI*2);
      ctx.fill();
    }
  }

  function drawPowerups(){
    for (const pu of state.powerups){
      const pulse = 1 + Math.sin(pu.t*8)*0.08;
      ctx.save();
      ctx.translate(pu.x + pu.w/2, pu.y + pu.h/2);
      ctx.scale(pulse, pulse);
      ctx.rotate(Math.sin(pu.t*3)*0.1);
      // glow
      ctx.shadowColor = pu.type === 'wide' ? 'rgba(80,255,160,0.9)' : 'rgba(255,120,80,0.9)';
      ctx.shadowBlur = 14;
      // capsule
      const g = ctx.createLinearGradient(0, -pu.h/2, 0, pu.h/2);
      if (pu.type === 'wide'){
        g.addColorStop(0, '#7dffb0');
        g.addColorStop(1, '#1a9e5a');
      } else {
        g.addColorStop(0, '#ff9a7d');
        g.addColorStop(1, '#c02a1a');
      }
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.roundRect(-pu.w/2, -pu.h/2, pu.w, pu.h, pu.h/2);
      ctx.fill();
      ctx.shadowBlur = 0;
      // icon
      ctx.fillStyle = 'rgba(255,255,255,0.95)';
      if (pu.type === 'wide'){
        ctx.beginPath();
        ctx.moveTo(-10, 0); ctx.lineTo(-4, -4); ctx.lineTo(-4, 4); ctx.closePath(); ctx.fill();
        ctx.beginPath();
        ctx.moveTo(10, 0); ctx.lineTo(4, -4); ctx.lineTo(4, 4); ctx.closePath(); ctx.fill();
        ctx.fillRect(-4, -2, 8, 4);
      } else {
        ctx.beginPath(); ctx.arc(-6, -2, 2.5, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(6, -2, 2.5, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(0, 4, 2.5, 0, Math.PI*2); ctx.fill();
      }
      ctx.restore();
    }
  }

  function drawParticles(){
    for (const p of state.particles){
      const a = Math.max(0, p.life / p.maxLife);
      ctx.fillStyle = `hsla(${p.hue}, 90%, 65%, ${a})`;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * a, 0, Math.PI*2);
      ctx.fill();
    }
  }

  function drawHUD(){
    // score
    ctx.save();
    ctx.font = 'bold 22px Arial';
    ctx.textAlign = 'left';
    ctx.fillStyle = '#e8eefc';
    ctx.fillText('SCORE ' + state.score, 24, 40);
    // lives
    ctx.textAlign = 'right';
    for (let i = 0; i < state.lives; i++){
      const cx = W - 32 - i*30;
      const cy = 32;
      ctx.fillStyle = '#5a8cff';
      ctx.beginPath();
      ctx.roundRect(cx - 12, cy - 7, 24, 14, 7);
      ctx.fill();
      ctx.strokeStyle = 'rgba(180,210,255,0.7)';
      ctx.lineWidth = 1.5;
      ctx.stroke();
    }
    ctx.fillStyle = '#e8eefc';
    ctx.font = 'bold 18px Arial';
    ctx.fillText('LIVES', W - 32 - state.lives*30 - 20, 40);
    // wide timer
    if (state.paddle.wideTimer > 0){
      ctx.textAlign = 'left';
      ctx.fillStyle = '#7dffb0';
      ctx.font = 'bold 16px Arial';
      ctx.fillText('WIDE PADDLE ' + state.paddle.wideTimer.toFixed(1) + 's', 24, 64);
    }
    ctx.restore();
  }

  function drawEndScreen(title, subtitle, color){
    ctx.save();
    ctx.fillStyle = 'rgba(0,0,0,0.55)';
    ctx.fillRect(0, 0, W, H);
    ctx.textAlign = 'center';
    ctx.fillStyle = color;
    ctx.font = 'bold 64px Arial';
    ctx.shadowColor = color;
    ctx.shadowBlur = 30;
    ctx.fillText(title, W/2, H/2 - 40);
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#e8eefc';
    ctx.font = 'bold 28px Arial';
    ctx.fillText(subtitle, W/2, H/2 + 30);
    ctx.font = '20px Arial';
    ctx.fillStyle = 'rgba(200,220,255,0.8)';
    ctx.fillText('Click or press Space to restart', W/2, H/2 + 90);
    ctx.restore();
  }

  // ---- Main loop ----
  function update(dt){
    if (state.mode === 'playing'){
      updatePaddle(dt);
      updateBalls(dt);
      updatePowerups(dt);
      updateParticles(dt);
      // ball trail
      for (const b of state.balls){
        b.trail.push({ x: b.x, y: b.y });
        if (b.trail.length > 12) b.trail.shift();
      }
      // hit flash decay
      for (const br of state.bricks){
        if (br.hitFlash > 0) br.hitFlash = Math.max(0, br.hitFlash - dt*4);
      }
      // shake decay
      if (state.shake > 0) state.shake = Math.max(0, state.shake - dt*20);
    } else {
      updateParticles(dt);
      if (state.shake > 0) state.shake = Math.max(0, state.shake - dt*20);
    }
  }

  function render(){
    ctx.save();
    if (state.shake > 0.5){
      const s = state.shake;
      ctx.translate((Math.random()-0.5)*s, (Math.random()-0.5)*s);
    }
    drawBackground();
    drawBricks();
    drawPowerups();
    drawPaddle();
    drawBalls();
    drawParticles();
    ctx.restore();
    drawHUD();

    if (state.mode === 'gameover'){
      drawEndScreen('GAME OVER', 'Score: ' + state.score, '#ff5a6a');
    } else if (state.mode === 'win'){
      drawEndScreen('VICTORY!', 'Score: ' + state.score, '#7dffb0');
    }
  }

  function loop(t){
    const dt = Math.min(0.033, (t - state.lastT) / 1000 || 0);
    state.lastT = t;
    update(dt);
    render();
    requestAnimationFrame(loop);
  }

  // Start in demo mode
  startDemo();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>