Models 44

Nemotron 3 Ultra (free)

by NVIDIAReasoningClean
Open in new window

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

How it went

Total time
2 min 24 s
Cost
$0
Output tokens
8,102
Spent thinking
269 tokens
Speed
156.5 tok/s
Console errors
0
First token after
1 min 32 s
First visible text after
1 min 37 s
Prompt tokens
531
Code size
23.1 KB
Finish reason
stop
Served by
Nvidia
Model id
nvidia/nemotron-3-ultra-550b-a55b:free
Reasoning sent
{"effort":"medium"}
Max output
32,000 tokens
Generated
Nov 9, 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 · 799 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: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%);
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-family: 'Segoe UI', system-ui, sans-serif;
      color: #e0e0e0;
      overflow: hidden;
    }
    #gameContainer {
      position: relative;
      border-radius: 12px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 0 2px rgba(255,255,255,0.1);
      background: #0d0d1a;
    }
    canvas { display: block; border-radius: 12px; }
    #ui {
      position: absolute;
      top: 16px;
      left: 16px;
      right: 16px;
      display: flex;
      justify-content: space-between;
      pointer-events: none;
      z-index: 10;
    }
    .stat {
      background: rgba(0,0,0,0.7);
      padding: 8px 16px;
      border-radius: 8px;
      font-weight: 600;
      font-size: 14px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255,255,255,0.1);
    }
    .stat span { color: #00d4ff; margin-left: 6px; }
    #lives span { color: #ff4757; }
    #overlay {
      position: absolute;
      inset: 0;
      border-radius: 12px;
      background: rgba(13, 13, 26, 0.95);
      backdrop-filter: blur(8px);
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 20px;
      z-index: 20;
      opacity: 1;
      transition: opacity 0.3s ease;
    }
    #overlay.hidden { opacity: 0; pointer-events: none; }
    #overlay h1 {
      font-size: 48px;
      font-weight: 700;
      background: linear-gradient(135deg, #00d4ff, #ff6b6b);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: 0 0 30px rgba(0,212,255,0.5);
    }
    #overlay p { font-size: 18px; opacity: 0.8; }
    #overlay button {
      padding: 14px 40px;
      font-size: 16px;
      font-weight: 600;
      border: none;
      border-radius: 50px;
      background: linear-gradient(135deg, #00d4ff, #0099cc);
      color: #0d0d1a;
      cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
      box-shadow: 0 4px 20px rgba(0,212,255,0.4);
    }
    #overlay button:hover { transform: scale(1.05); box-shadow: 0 6px 30px rgba(0,212,255,0.6); }
    #overlay button:active { transform: scale(0.98); }
    .powerup-icon { font-size: 20px; }
    @media (max-width: 600px) {
      #overlay h1 { font-size: 32px; }
      .stat { font-size: 12px; padding: 6px 12px; }
    }
  </style>
</head>
<body>
  <div id="gameContainer">
    <canvas id="canvas"></canvas>
    <div id="ui">
      <div class="stat" id="score">SCORE <span>0</span></div>
      <div class="stat" id="lives">LIVES <span>3</span></div>
    </div>
    <div id="overlay">
      <h1 id="overlayTitle">BREAKOUT</h1>
      <p id="overlayText">Click or press a key to start</p>
      <button id="startBtn">Start Game</button>
    </div>
  </div>

<script>
// Game constants
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
const PADDLE_HEIGHT = 16;
const PADDLE_WIDTH = 120;
const PADDLE_Y = CANVAS_HEIGHT - 40;
const BALL_RADIUS = 10;
const BALL_SPEED = 6;
const BRICK_ROWS = 6;
const BRICK_COLS = 10;
const BRICK_WIDTH = 72;
const BRICK_HEIGHT = 28;
const BRICK_PADDING = 4;
const BRICK_OFFSET_TOP = 80;
const BRICK_OFFSET_LEFT = 40;
const POWERUP_DROP_CHANCE = 0.25;
const POWERUP_SIZE = 24;
const POWERUP_SPEED = 3;
const PARTICLE_COUNT = 12;

// Power-up types
const POWERUP_WIDE = 'wide';
const POWERUP_MULTI = 'multi';

// Game state
let canvas, ctx;
let paddle = { x: 0, y: PADDLE_Y, width: PADDLE_WIDTH, height: PADDLE_HEIGHT, targetX: 0 };
let balls = [];
let bricks = [];
let particles = [];
let powerups = [];
let score = 0;
let lives = 3;
let gameState = 'demo'; // demo, playing, gameover, victory
let demoMode = true;
let widePaddleTimer = 0;
let lastTime = 0;
let keys = { left: false, right: false };

// Colors
const COLORS = {
  bg: '#0d0d1a',
  paddle: '#00d4ff',
  paddleGlow: 'rgba(0,212,255,0.5)',
  ball: '#ff6b6b',
  ballGlow: 'rgba(255,107,107,0.6)',
  brickNormal: ['#ff6b6b', '#ff9f43', '#feca57', '#48dbfb', '#1dd1a1', '#5f27cd'],
  brickTough: '#ffffff',
  brickToughCrack: '#cccccc',
  particle: ['#ff6b6b', '#ff9f43', '#feca57', '#48dbfb', '#1dd1a1', '#00d4ff'],
  powerupWide: '#00d4ff',
  powerupMulti: '#ff6b6b',
  text: '#e0e0e0',
  uiBg: 'rgba(0,0,0,0.7)',
  uiBorder: 'rgba(255,255,255,0.1)'
};

// Initialize canvas
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);

function resizeCanvas() {
  const container = document.getElementById('gameContainer');
  const maxWidth = Math.min(CANVAS_WIDTH, window.innerWidth - 32);
  const maxHeight = Math.min(CANVAS_HEIGHT, window.innerHeight - 32);
  const scale = Math.min(maxWidth / CANVAS_WIDTH, maxHeight / CANVAS_HEIGHT);
  canvas.width = CANVAS_WIDTH;
  canvas.height = CANVAS_HEIGHT;
  canvas.style.width = (CANVAS_WIDTH * scale) + 'px';
  canvas.style.height = (CANVAS_HEIGHT * scale) + 'px';
}

// Input handling
canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  const scaleX = CANVAS_WIDTH / rect.width;
  paddle.targetX = (e.clientX - rect.left) * scaleX - paddle.width / 2;
  if (demoMode) exitDemo();
});

window.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowLeft') keys.left = true;
  if (e.key === 'ArrowRight') keys.right = true;
  if (demoMode && (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === ' ' || e.key === 'Enter')) exitDemo();
});
window.addEventListener('keyup', (e) => {
  if (e.key === 'ArrowLeft') keys.left = false;
  if (e.key === 'ArrowRight') keys.right = false;
});

canvas.addEventListener('click', () => {
  if (gameState === 'gameover' || gameState === 'victory') restartGame();
});

document.getElementById('startBtn').addEventListener('click', () => {
  if (demoMode) exitDemo();
  else if (gameState === 'gameover' || gameState === 'victory') restartGame();
});

function exitDemo() {
  demoMode = false;
  gameState = 'playing';
  document.getElementById('overlay').classList.add('hidden');
  spawnBall();
}

function restartGame() {
  score = 0;
  lives = 3;
  widePaddleTimer = 0;
  paddle.width = PADDLE_WIDTH;
  paddle.x = (CANVAS_WIDTH - paddle.width) / 2;
  paddle.targetX = paddle.x;
  balls = [];
  bricks = [];
  particles = [];
  powerups = [];
  createBricks();
  gameState = 'playing';
  document.getElementById('overlay').classList.add('hidden');
  document.getElementById('overlayTitle').textContent = 'BREAKOUT';
  document.getElementById('overlayText').textContent = 'Click or press a key to start';
  document.getElementById('startBtn').textContent = 'Start Game';
  spawnBall();
  updateUI();
}

// Ball spawning
function spawnBall(x, y, vx, vy) {
  const ball = {
    x: x || paddle.x + paddle.width / 2,
    y: y || PADDLE_Y - BALL_RADIUS - 2,
    radius: BALL_RADIUS,
    vx: vx || (Math.random() - 0.5) * 4,
    vy: vy || -BALL_SPEED,
    trail: []
  };
  balls.push(ball);
  return ball;
}

// Brick creation
function createBricks() {
  bricks = [];
  for (let row = 0; row < BRICK_ROWS; row++) {
    for (let col = 0; col < BRICK_COLS; col++) {
      const x = BRICK_OFFSET_LEFT + col * (BRICK_WIDTH + BRICK_PADDING);
      const y = BRICK_OFFSET_TOP + row * (BRICK_HEIGHT + BRICK_PADDING);
      const isTough = row < 2; // Top 2 rows are tough bricks
      const hasPowerup = !isTough && Math.random() < POWERUP_DROP_CHANCE;
      const powerupType = hasPowerup ? (Math.random() < 0.5 ? POWERUP_WIDE : POWERUP_MULTI) : null;
      
      bricks.push({
        x, y,
        width: BRICK_WIDTH,
        height: BRICK_HEIGHT,
        hits: isTough ? 2 : 1,
        maxHits: isTough ? 2 : 1,
        color: isTough ? COLORS.brickTough : COLORS.brickNormal[row % COLORS.brickNormal.length],
        hasPowerup,
        powerupType,
        destroyed: false,
        glow: 0
      });
    }
  }
}

// Collision detection - circle vs rect
function circleRectCollide(circle, rect) {
  const distX = Math.abs(circle.x - rect.x - rect.width / 2);
  const distY = Math.abs(circle.y - rect.y - rect.height / 2);
  
  if (distX > rect.width / 2 + circle.radius) return null;
  if (distY > rect.height / 2 + circle.radius) return null;
  if (distX <= rect.width / 2) return 'top';
  if (distY <= rect.height / 2) return 'side';
  
  const cornerDist = Math.pow(distX - rect.width / 2, 2) + Math.pow(distY - rect.height / 2, 2);
  return cornerDist <= Math.pow(circle.radius, 2) ? 'corner' : null;
}

// Game update
function update(deltaTime) {
  if (gameState !== 'playing') return;
  
  const dt = deltaTime / 1000;
  
  // Update paddle
  if (demoMode) {
    // Simple AI: follow the first ball
    if (balls.length > 0) {
      const target = balls[0].x - paddle.width / 2;
      paddle.targetX = target;
    }
  } else {
    if (keys.left) paddle.targetX -= 600 * dt;
    if (keys.right) paddle.targetX += 600 * dt;
  }
  
  paddle.targetX = Math.max(0, Math.min(CANVAS_WIDTH - paddle.width, paddle.targetX));
  paddle.x += (paddle.targetX - paddle.x) * Math.min(1, 15 * dt);
  
  // Wide paddle timer
  if (widePaddleTimer > 0) {
    widePaddleTimer -= dt;
    if (widePaddleTimer <= 0) {
      paddle.width = PADDLE_WIDTH;
      widePaddleTimer = 0;
    }
  }
  
  // Update balls
  for (let i = balls.length - 1; i >= 0; i--) {
    const ball = balls[i];
    
    // Store trail
    ball.trail.unshift({ x: ball.x, y: ball.y });
    if (ball.trail.length > 8) ball.trail.pop();
    
    // Move ball
    ball.x += ball.vx * dt * 60;
    ball.y += ball.vy * dt * 60;
    
    // Wall collisions
    if (ball.x - ball.radius < 0) {
      ball.x = ball.radius;
      ball.vx = Math.abs(ball.vx);
    }
    if (ball.x + ball.radius > CANVAS_WIDTH) {
      ball.x = CANVAS_WIDTH - ball.radius;
      ball.vx = -Math.abs(ball.vx);
    }
    if (ball.y - ball.radius < 0) {
      ball.y = ball.radius;
      ball.vy = Math.abs(ball.vy);
    }
    
    // Paddle collision
    if (ball.y + ball.radius >= paddle.y && 
        ball.y - ball.radius <= paddle.y + paddle.height &&
        ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
      
      ball.y = paddle.y - ball.radius;
      
      // Calculate bounce angle based on hit position
      const hitPos = (ball.x - paddle.x) / paddle.width; // 0 to 1
      const angle = (hitPos - 0.5) * Math.PI * 0.75; // -67.5 to 67.5 degrees
      const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
      ball.vx = Math.sin(angle) * speed;
      ball.vy = -Math.cos(angle) * speed;
      
      // Add slight paddle velocity influence
      ball.vx += (paddle.targetX - paddle.x) * 0.1;
    }
    
    // Brick collisions
    for (const brick of bricks) {
      if (brick.destroyed) continue;
      
      const collision = circleRectCollide(ball, brick);
      if (collision) {
        // Determine bounce direction
        const prevX = ball.x - ball.vx * dt * 60;
        const prevY = ball.y - ball.vy * dt * 60;
        const prevCollision = circleRectCollide({x: prevX, y: prevY, radius: ball.radius}, brick);
        
        if (collision === 'top' || (collision === 'corner' && prevCollision === 'side')) {
          ball.vy = -Math.abs(ball.vy);
          ball.y = brick.y - ball.radius;
        } else if (collision === 'side' || (collision === 'corner' && prevCollision === 'top')) {
          ball.vx = ball.x < brick.x + brick.width / 2 ? -Math.abs(ball.vx) : Math.abs(ball.vx);
          ball.x = ball.x < brick.x + brick.width / 2 ? brick.x - ball.radius : brick.x + brick.width + ball.radius;
        }
        
        // Damage brick
        brick.hits--;
        brick.glow = 1;
        
        if (brick.hits <= 0) {
          brick.destroyed = true;
          score += brick.maxHits === 2 ? 20 : 10;
          createParticles(brick.x + brick.width/2, brick.y + brick.height/2, brick.color);
          
          // Drop powerup
          if (brick.hasPowerup && brick.powerupType) {
            powerups.push({
              x: brick.x + brick.width/2,
              y: brick.y + brick.height/2,
              type: brick.powerupType,
              width: POWERUP_SIZE,
              height: POWERUP_SIZE,
              vy: POWERUP_SPEED,
              rotation: 0
            });
          }
        }
        break; // Only hit one brick per frame
      }
    }
    
    // Ball lost
    if (ball.y - ball.radius > CANVAS_HEIGHT) {
      balls.splice(i, 1);
      if (balls.length === 0) {
        loseLife();
      }
    }
  }
  
  // Update powerups
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.y += p.vy * dt * 60;
    p.rotation += dt * 2;
    
    // Check paddle collision
    if (p.y + p.height/2 >= paddle.y &&
        p.y - p.height/2 <= paddle.y + paddle.height &&
        p.x >= paddle.x && p.x <= paddle.x + paddle.width) {
      applyPowerup(p.type);
      powerups.splice(i, 1);
      continue;
    }
    
    if (p.y - p.height/2 > CANVAS_HEIGHT) {
      powerups.splice(i, 1);
    }
  }
  
  // Update particles
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx * dt * 60;
    p.y += p.vy * dt * 60;
    p.vy += 300 * dt; // gravity
    p.life -= dt;
    p.alpha = p.life / p.maxLife;
    if (p.life <= 0) particles.splice(i, 1);
  }
  
  // Update brick glow
  for (const brick of bricks) {
    if (brick.glow > 0) brick.glow = Math.max(0, brick.glow - dt * 5);
  }
  
  // Check victory
  const remainingBricks = bricks.filter(b => !b.destroyed).length;
  if (remainingBricks === 0) {
    victory();
  }
  
  updateUI();
}

function loseLife() {
  lives--;
  if (lives <= 0) {
    gameOver();
  } else {
    // Reset ball after short delay
    setTimeout(() => {
      if (gameState === 'playing') spawnBall();
    }, 1000);
  }
}

function gameOver() {
  gameState = 'gameover';
  demoMode = false;
  document.getElementById('overlayTitle').textContent = 'GAME OVER';
  document.getElementById('overlayText').textContent = `Final Score: ${score}`;
  document.getElementById('startBtn').textContent = 'Play Again';
  document.getElementById('overlay').classList.remove('hidden');
}

function victory() {
  gameState = 'victory';
  demoMode = false;
  document.getElementById('overlayTitle').textContent = 'VICTORY!';
  document.getElementById('overlayText').textContent = `Final Score: ${score} | Lives: ${lives}`;
  document.getElementById('startBtn').textContent = 'Play Again';
  document.getElementById('overlay').classList.remove('hidden');
}

function applyPowerup(type) {
  if (type === POWERUP_WIDE) {
    paddle.width = PADDLE_WIDTH * 1.6;
    widePaddleTimer = 10;
  } else if (type === POWERUP_MULTI) {
    // Create 2 additional balls
    for (let i = 0; i < 2; i++) {
      const ball = balls[0];
      if (ball) {
        spawnBall(ball.x, ball.y, 
          (Math.random() - 0.5) * 8, 
          -BALL_SPEED * (0.8 + Math.random() * 0.4));
      }
    }
  }
}

function createParticles(x, y, color) {
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    const angle = (Math.PI * 2 * i) / PARTICLE_COUNT + Math.random() * 0.5;
    const speed = 100 + Math.random() * 200;
    particles.push({
      x, y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed - 100,
      radius: 3 + Math.random() * 3,
      color: color,
      life: 0.5 + Math.random() * 0.5,
      maxLife: 1,
      alpha: 1
    });
  }
}

function updateUI() {
  document.getElementById('score').querySelector('span').textContent = score;
  document.getElementById('lives').querySelector('span').textContent = lives;
}

// Rendering
function render() {
  // Clear
  ctx.fillStyle = COLORS.bg;
  ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  
  // Draw subtle grid background
  ctx.strokeStyle = 'rgba(255,255,255,0.02)';
  ctx.lineWidth = 1;
  for (let x = 0; x < CANVAS_WIDTH; x += 40) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, CANVAS_HEIGHT);
    ctx.stroke();
  }
  for (let y = 0; y < CANVAS_HEIGHT; y += 40) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(CANVAS_WIDTH, y);
    ctx.stroke();
  }
  
  // Draw bricks
  for (const brick of bricks) {
    if (brick.destroyed) continue;
    
    const isTough = brick.maxHits === 2;
    const glowIntensity = brick.glow;
    
    // Brick shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.fillRect(brick.x + 2, brick.y + 2, brick.width, brick.height);
    
    // Brick body
    let brickColor = brick.color;
    if (isTough && brick.hits === 1) {
      // Cracked tough brick
      brickColor = COLORS.brickToughCrack;
    }
    
    const gradient = ctx.createLinearGradient(brick.x, brick.y, brick.x, brick.y + brick.height);
    gradient.addColorStop(0, lightenColor(brickColor, 30));
    gradient.addColorStop(1, brickColor);
    ctx.fillStyle = gradient;
    roundRect(ctx, brick.x, brick.y, brick.width, brick.height, 4);
    ctx.fill();
    
    // Highlight
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    roundRect(ctx, brick.x, brick.y, brick.width, 4, 4);
    ctx.fill();
    
    // Glow effect on hit
    if (glowIntensity > 0) {
      ctx.strokeStyle = `rgba(255,255,255,${glowIntensity * 0.8})`;
      ctx.lineWidth = 3;
      roundRect(ctx, brick.x, brick.y, brick.width, brick.height, 4);
      ctx.stroke();
    }
    
    // Crack effect for tough bricks
    if (isTough && brick.hits === 1) {
      ctx.strokeStyle = '#666';
      ctx.lineWidth = 2;
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(brick.x + 8, brick.y + 8);
      ctx.lineTo(brick.x + brick.width - 8, brick.y + brick.height - 8);
      ctx.moveTo(brick.x + brick.width - 8, brick.y + 8);
      ctx.lineTo(brick.x + 8, brick.y + brick.height - 8);
      ctx.stroke();
    }
    
    // Powerup indicator
    if (brick.hasPowerup && !brick.destroyed) {
      const pulse = 0.7 + 0.3 * Math.sin(Date.now() / 200);
      ctx.save();
      ctx.translate(brick.x + brick.width/2, brick.y + brick.height/2);
      ctx.scale(pulse, pulse);
      ctx.translate(-(brick.x + brick.width/2), -(brick.y + brick.height/2));
      
      if (brick.powerupType === POWERUP_WIDE) {
        // Wide paddle icon
        ctx.fillStyle = COLORS.powerupWide;
        roundRect(ctx, brick.x + brick.width/2 - 16, brick.y + brick.height/2 - 5, 32, 10, 3);
        ctx.fill();
      } else {
        // Multi-ball icon
        ctx.fillStyle = COLORS.powerupMulti;
        ctx.beginPath();
        ctx.arc(brick.x + brick.width/2, brick.y + brick.height/2, 8, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = COLORS.bg;
        ctx.beginPath();
        ctx.arc(brick.x + brick.width/2 - 3, brick.y + brick.height/2 - 2, 2, 0, Math.PI * 2);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(brick.x + brick.width/2 + 3, brick.y + brick.height/2 + 2, 2, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.restore();
    }
  }
  
  // Draw particles
  for (const p of particles) {
    ctx.globalAlpha = p.alpha;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius * p.alpha, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
  
  // Draw powerups
  for (const p of powerups) {
    ctx.save();
    ctx.translate(p.x, p.y);
    ctx.rotate(p.rotation);
    
    // Glow
    const glowColor = p.type === POWERUP_WIDE ? COLORS.powerupWide : COLORS.powerupMulti;
    ctx.shadowColor = glowColor;
    ctx.shadowBlur = 15;
    
    if (p.type === POWERUP_WIDE) {
      // Wide paddle powerup
      const grad = ctx.createLinearGradient(-12, -12, 12, 12);
      grad.addColorStop(0, lightenColor(glowColor, 40));
      grad.addColorStop(1, glowColor);
      ctx.fillStyle = grad;
      roundRect(ctx, -12, -12, 24, 24, 6);
      ctx.fill();
      
      // Icon
      ctx.fillStyle = COLORS.bg;
      roundRect(ctx, -10, -4, 20, 8, 2);
      ctx.fill();
    } else {
      // Multi-ball powerup
      const grad = ctx.createRadialGradient(-4, -4, 0, 0, 0, 12);
      grad.addColorStop(0, lightenColor(glowColor, 40));
      grad.addColorStop(1, glowColor);
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.arc(0, 0, 12, 0, Math.PI * 2);
      ctx.fill();
      
      // Small balls
      ctx.fillStyle = COLORS.bg;
      ctx.beginPath();
      ctx.arc(-4, -3, 3, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(4, 3, 3, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }
  
  // Draw balls with trails
  for (const ball of balls) {
    // Trail
    for (let i = ball.trail.length - 1; i >= 0; i--) {
      const t = ball.trail[i];
      const alpha = (i + 1) / ball.trail.length * 0.3;
      const radius = ball.radius * (i + 1) / ball.trail.length;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = COLORS.ball;
      ctx.beginPath();
      ctx.arc(t.x, t.y, radius, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    
    // Ball glow
    ctx.shadowColor = COLORS.ballGlow;
    ctx.shadowBlur = 20;
    
    // Ball body
    const grad = ctx.createRadialGradient(
      ball.x - 3, ball.y - 3, 0,
      ball.x, ball.y, ball.radius
    );
    grad.addColorStop(0, '#ffaaaa');
    grad.addColorStop(1, COLORS.ball);
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
  }
  
  // Draw paddle
  // Paddle glow
  ctx.shadowColor = COLORS.paddleGlow;
  ctx.shadowBlur = 20;
  
  const paddleGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.height);
  paddleGrad.addColorStop(0, '#40e0ff');
  paddleGrad.addColorStop(1, COLORS.paddle);
  ctx.fillStyle = paddleGrad;
  roundRect(ctx, paddle.x, paddle.y, paddle.width, paddle.height, 8);
  ctx.fill();
  ctx.shadowBlur = 0;
  
  // Paddle highlight
  ctx.fillStyle = 'rgba(255,255,255,0.3)';
  roundRect(ctx, paddle.x, paddle.y, paddle.width, 4, 8);
  ctx.fill();
  
  // Wide paddle indicator
  if (widePaddleTimer > 0) {
    ctx.strokeStyle = `rgba(0,212,255,${0.5 + 0.5 * Math.sin(Date.now() / 100)})`;
    ctx.lineWidth = 2;
    roundRect(ctx, paddle.x - 2, paddle.y - 2, paddle.width + 4, paddle.height + 4, 10);
    ctx.stroke();
  }
}

// Helper functions
function roundRect(ctx, x, y, w, h, r) {
  if (w < 2 * r) r = w / 2;
  if (h < 2 * r) r = h / 2;
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

function lightenColor(color, percent) {
  const num = parseInt(color.replace('#', ''), 16);
  const amt = Math.round(2.55 * percent);
  const R = Math.min(255, (num >> 16) + amt);
  const G = Math.min(255, ((num >> 8) & 0x00FF) + amt);
  const B = Math.min(255, (num & 0x0000FF) + amt);
  return '#' + (0x1000000 + (R << 16) + (G << 8) + B).toString(16).slice(1);
}

// Game loop
function gameLoop(time) {
  const deltaTime = time - lastTime;
  lastTime = time;
  
  if (demoMode) {
    // In demo mode, run AI paddle
    if (balls.length === 0) spawnBall();
  }
  
  update(deltaTime);
  render();
  
  requestAnimationFrame(gameLoop);
}

// Initialize
createBricks();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
Reasoning trace (269 tokens)
The user wants a polished Breakout game in a single HTML file. Let me plan this carefully.

Key requirements:
1. Canvas-based game
2. Paddle controlled by mouse AND arrow keys
3. Ball bounce angle depends on hit position on paddle
4. 6+ rows of bricks, mix of normal (1 hit) and tough (2 hits, cracks on first hit)
5. Some bricks have power-up icons, drop when broken
6. Power-ups: wider paddle (10s) and multi-ball (3 balls)
7. Ball never passes through bricks/paddle (proper collision detection)
8. Particles on brick break
9. 3 lives, score display
10. Game over and victory screens with restart
11. Self-playing demo mode behind "Click or press a key to start"

Let me structure this:
- HTML structure with canvas
- CSS for styling
- JavaScript game engine with:
  - Game states: demo, playing, gameover, victory
  - Entities: paddle, ball(s), bricks, particles, power-ups
  - Collision detection with proper physics
  - Demo AI for paddle
  - Input handling

I'll make it visually polished with nice colors, gradients, and effects.

Let me write the complete code.