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
41.9 s
Cost
$0.028
Output tokens
7,333
Spent thinking
0 tokens
Speed
181.4 tok/s
Console errors
0
First token after
1.4 s
First visible text after
1.4 s
Prompt tokens
524
Code size
18.4 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"low"}
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 · 711 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    user-select: none;
  }
  body {
    background: radial-gradient(circle at center, #111428 0%, #060814 100%);
    color: #e0e6ed;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  #wrapper {
    position: relative;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8), 0 0 30px rgba(0, 195, 255, 0.15);
    border-radius: 12px;
    overflow: hidden;
    border: 2px solid rgba(255, 255, 255, 0.1);
  }
  canvas {
    display: block;
    background: #080a16;
  }
  .hint {
    margin-top: 12px;
    font-size: 13px;
    color: rgba(255, 255, 255, 0.5);
    letter-spacing: 0.5px;
  }
</style>
</head>
<body>

<div id="wrapper">
  <canvas id="game" width="800" height="600"></canvas>
</div>
<div class="hint">Use Mouse or Left/Right Arrow Keys &bull; Press 'P' for Pause</div>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

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

// Game State
let isAutoplay = true;
let isGameOver = false;
let isVictory = false;
let isPaused = false;
let score = 0;
let lives = 3;

// Paddle
const PADDLE_BASE_WIDTH = 110;
const PADDLE_HEIGHT = 14;
let paddleWidth = PADDLE_BASE_WIDTH;
let paddleX = (W - paddleWidth) / 2;
let paddleY = H - 36;
let paddleSpeed = 8;
let leftPressed = false;
let rightPressed = false;
let widePaddleTimer = 0;

// Balls
let balls = [];
const BALL_RADIUS = 6.5;
const BASE_BALL_SPEED = 5.8;

// Bricks
const BRICK_ROWS = 7;
const BRICK_COLS = 11;
const BRICK_W = 62;
const BRICK_H = 22;
const BRICK_PADDING_X = 8;
const BRICK_PADDING_Y = 8;
const BRICK_OFFSET_TOP = 55;
const BRICK_OFFSET_LEFT = (W - (BRICK_COLS * (BRICK_W + BRICK_PADDING_X) - BRICK_PADDING_X)) / 2;

let bricks = [];

// Particles & Powerups
let particles = [];
let powerups = [];

// Colors
const ROW_COLORS = [
  { fill: '#ff2a6d', stroke: '#ff7597' }, // Top: Pink
  { fill: '#ff5e00', stroke: '#ffa266' }, // Orange
  { fill: '#ffb703', stroke: '#ffe066' }, // Yellow
  { fill: '#05d550', stroke: '#61f893' }, // Green
  { fill: '#00f0ff', stroke: '#8bf7ff' }, // Cyan
  { fill: '#7928ca', stroke: '#b773ff' }, // Purple
  { fill: '#4361ee', stroke: '#8ba2ff' }  // Blue
];

function initGame() {
  score = 0;
  lives = 3;
  isGameOver = false;
  isVictory = false;
  widePaddleTimer = 0;
  paddleWidth = PADDLE_BASE_WIDTH;
  paddleX = (W - paddleWidth) / 2;
  particles = [];
  powerups = [];

  initBricks();
  resetBalls();
}

function initBricks() {
  bricks = [];
  for (let r = 0; r < BRICK_ROWS; r++) {
    bricks[r] = [];
    for (let c = 0; c < BRICK_COLS; c++) {
      let isTough = (r === 0 || r === 1 || ((r + c) % 4 === 0 && r < 4));
      let hp = isTough ? 2 : 1;

      // Power-up chance
      let powerupType = null;
      if (Math.random() < 0.22) {
        powerupType = Math.random() < 0.5 ? 'wide' : 'multiball';
      }

      bricks[r][c] = {
        x: BRICK_OFFSET_LEFT + c * (BRICK_W + BRICK_PADDING_X),
        y: BRICK_OFFSET_TOP + r * (BRICK_H + BRICK_PADDING_Y),
        w: BRICK_W,
        h: BRICK_H,
        hp: hp,
        maxHp: hp,
        powerup: powerupType,
        color: ROW_COLORS[r]
      };
    }
  }
}

function resetBalls() {
  balls = [{
    x: paddleX + paddleWidth / 2,
    y: paddleY - BALL_RADIUS - 2,
    vx: (Math.random() > 0.5 ? 1 : -1) * (BASE_BALL_SPEED * 0.5),
    vy: -BASE_BALL_SPEED,
    radius: BALL_RADIUS,
    trail: []
  }];
}

function spawnPowerup(x, y, type) {
  powerups.push({
    x: x,
    y: y,
    w: 24,
    h: 24,
    vy: 2.2,
    type: type,
    pulse: 0
  });
}

function spawnBrickParticles(x, y, w, h, color) {
  const count = 14;
  for (let i = 0; i < count; i++) {
    particles.push({
      x: x + Math.random() * w,
      y: y + Math.random() * h,
      vx: (Math.random() - 0.5) * 6,
      vy: (Math.random() - 0.5) * 6,
      size: Math.random() * 3 + 2,
      color: color,
      life: 1.0,
      decay: Math.random() * 0.03 + 0.02
    });
  }
}

// Input Handlers
window.addEventListener('keydown', (e) => {
  if (isAutoplay && (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === ' ' || e.key === 'Enter')) {
    isAutoplay = false;
    initGame();
    return;
  }
  if ((isGameOver || isVictory) && (e.key === ' ' || e.key === 'Enter')) {
    isAutoplay = false;
    initGame();
    return;
  }
  if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') leftPressed = true;
  if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') rightPressed = true;
  if (e.key === 'p' || e.key === 'P') isPaused = !isPaused;
});

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

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  const mouseX = e.clientX - rect.left;
  if (!isAutoplay) {
    paddleX = mouseX - paddleWidth / 2;
    clampPaddle();
  }
});

canvas.addEventListener('mousedown', () => {
  if (isAutoplay || isGameOver || isVictory) {
    isAutoplay = false;
    initGame();
  }
});

function clampPaddle() {
  if (paddleX < 8) paddleX = 8;
  if (paddleX + paddleWidth > W - 8) paddleX = W - 8 - paddleWidth;
}

// Continuous collision detection between ball and rectangle
function checkBallRectCollision(ball, rect) {
  // Find closest point on rect to ball center
  let closestX = Math.max(rect.x, Math.min(ball.x, rect.x + rect.w));
  let closestY = Math.max(rect.y, Math.min(ball.y, rect.y + rect.h));

  let distX = ball.x - closestX;
  let distY = ball.y - closestY;
  let distSquared = distX * distX + distY * distY;

  if (distSquared <= ball.radius * ball.radius) {
    // Overlapping: determine normal
    let penX = (rect.w / 2 + ball.radius) - Math.abs(ball.x - (rect.x + rect.w / 2));
    let penY = (rect.h / 2 + ball.radius) - Math.abs(ball.y - (rect.y + rect.h / 2));

    if (penX < penY) {
      return { hit: true, normalX: ball.x > rect.x + rect.w / 2 ? 1 : -1, normalY: 0 };
    } else {
      return { hit: true, normalX: 0, normalY: ball.y > rect.y + rect.h / 2 ? 1 : -1 };
    }
  }
  return { hit: false };
}

function update() {
  if (isPaused) return;

  // Powerup wide paddle timer
  if (widePaddleTimer > 0) {
    widePaddleTimer--;
    paddleWidth = PADDLE_BASE_WIDTH * 1.45;
    if (widePaddleTimer <= 0) {
      paddleWidth = PADDLE_BASE_WIDTH;
    }
  }

  // Autoplay AI or Keyboard input
  if (isAutoplay) {
    if (balls.length > 0) {
      // Track lowest or closest ball
      let targetBall = balls.reduce((prev, curr) => (curr.y > prev.y ? curr : prev), balls[0]);
      let targetX = targetBall.x - paddleWidth / 2;
      paddleX += (targetX - paddleX) * 0.16;
    }
  } else {
    if (leftPressed) paddleX -= paddleSpeed;
    if (rightPressed) paddleX += paddleSpeed;
  }
  clampPaddle();

  // Sub-stepping for robust collision preventing tunneling
  const STEPS = 3;
  for (let s = 0; s < STEPS; s++) {
    for (let i = balls.length - 1; i >= 0; i--) {
      const b = balls[i];
      b.x += b.vx / STEPS;
      b.y += b.vy / STEPS;

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

      // Bottom boundary
      if (b.y - b.radius > H) {
        balls.splice(i, 1);
        continue;
      }

      // Paddle collision
      const paddleRect = { x: paddleX, y: paddleY, w: paddleWidth, h: PADDLE_HEIGHT };
      let pCol = checkBallRectCollision(b, paddleRect);
      if (pCol.hit && b.vy > 0) {
        b.y = paddleY - b.radius; // snap to top of paddle
        
        // Calculate bounce angle: [-1 to 1] from center
        let hitOffset = (b.x - (paddleX + paddleWidth / 2)) / (paddleWidth / 2);
        hitOffset = Math.max(-0.95, Math.min(0.95, hitOffset));

        let maxAngle = (72 * Math.PI) / 180; // 72 degrees max
        let angle = hitOffset * maxAngle;
        let speed = Math.hypot(b.vx, b.vy);
        speed = Math.min(speed * 1.015, BASE_BALL_SPEED * 1.5); // Slight acceleration

        b.vx = speed * Math.sin(angle);
        b.vy = -Math.abs(speed * Math.cos(angle));

        // Paddle impact particles
        for (let p = 0; p < 5; p++) {
          particles.push({
            x: b.x,
            y: paddleY,
            vx: (Math.random() - 0.5) * 4,
            vy: -Math.random() * 3 - 1,
            size: 2,
            color: '#00f0ff',
            life: 0.8,
            decay: 0.05
          });
        }
      }

      // Brick collisions
      let hitAnyBrick = false;
      for (let r = 0; r < BRICK_ROWS && !hitAnyBrick; r++) {
        for (let c = 0; c < BRICK_COLS && !hitAnyBrick; c++) {
          let brick = bricks[r][c];
          if (!brick || brick.hp <= 0) continue;

          let col = checkBallRectCollision(b, brick);
          if (col.hit) {
            hitAnyBrick = true;
            brick.hp--;
            score += 10;

            // Reflect velocity based on surface normal
            if (col.normalX !== 0) b.vx = -b.vx;
            if (col.normalY !== 0) b.vy = -b.vy;

            spawnBrickParticles(brick.x, brick.y, brick.w, brick.h, brick.color.fill);

            // Brick destroyed
            if (brick.hp <= 0) {
              score += 25;
              if (brick.powerup) {
                spawnPowerup(brick.x + brick.w / 2, brick.y + brick.h / 2, brick.powerup);
              }
            }
          }
        }
      }
    }
  }

  // Record ball trail
  balls.forEach(b => {
    b.trail.push({ x: b.x, y: b.y });
    if (b.trail.length > 6) b.trail.shift();
  });

  // Check death & lives
  if (balls.length === 0) {
    lives--;
    if (lives <= 0) {
      isGameOver = true;
    } else {
      resetBalls();
    }
  }

  // Check victory
  let activeBricks = 0;
  for (let r = 0; r < BRICK_ROWS; r++) {
    for (let c = 0; c < BRICK_COLS; c++) {
      if (bricks[r][c].hp > 0) activeBricks++;
    }
  }
  if (activeBricks === 0) {
    isVictory = true;
  }

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

  // Update powerups
  for (let i = powerups.length - 1; i >= 0; i--) {
    let pw = powerups[i];
    pw.y += pw.vy;
    pw.pulse += 0.08;

    // Check collision with paddle
    if (
      pw.y + pw.h / 2 >= paddleY &&
      pw.y - pw.h / 2 <= paddleY + PADDLE_HEIGHT &&
      pw.x >= paddleX - 4 &&
      pw.x <= paddleX + paddleWidth + 4
    ) {
      // Apply powerup
      if (pw.type === 'wide') {
        widePaddleTimer = 60 * 10; // 10 seconds at 60fps
      } else if (pw.type === 'multiball') {
        let currentCount = balls.length;
        for (let j = 0; j < Math.min(2, 6 - currentCount); j++) {
          let ref = balls[0] || { x: paddleX + paddleWidth / 2, y: paddleY - 10, vx: 3, vy: -5 };
          balls.push({
            x: ref.x,
            y: ref.y,
            vx: (Math.random() - 0.5) * 8,
            vy: -Math.abs(ref.vy || BASE_BALL_SPEED),
            radius: BALL_RADIUS,
            trail: []
          });
        }
      }
      // Sparkle effect
      for (let k = 0; k < 12; k++) {
        particles.push({
          x: pw.x,
          y: paddleY,
          vx: (Math.random() - 0.5) * 5,
          vy: -Math.random() * 4 - 1,
          size: 3,
          color: pw.type === 'wide' ? '#00f0ff' : '#ff2a6d',
          life: 1.0,
          decay: 0.04
        });
      }
      powerups.splice(i, 1);
      continue;
    }

    if (pw.y > H + 30) {
      powerups.splice(i, 1);
    }
  }
}

// Drawing routines
function draw() {
  ctx.clearRect(0, 0, W, H);

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

  // Draw Bricks
  for (let r = 0; r < BRICK_ROWS; r++) {
    for (let c = 0; c < BRICK_COLS; c++) {
      let b = bricks[r][c];
      if (b.hp <= 0) continue;

      ctx.save();
      ctx.shadowBlur = 8;
      ctx.shadowColor = b.color.fill;

      // Body
      let grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
      if (b.hp === 2) {
        grad.addColorStop(0, '#ffffff');
        grad.addColorStop(0.3, b.color.fill);
        grad.addColorStop(1, '#0c1024');
      } else {
        grad.addColorStop(0, b.color.fill);
        grad.addColorStop(1, '#0c1024');
      }

      ctx.fillStyle = grad;
      ctx.strokeStyle = b.color.stroke;
      ctx.lineWidth = 1.5;

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

      // Crack texture if tough brick is damaged (hp === 1 and maxHp === 2)
      if (b.maxHp === 2 && b.hp === 1) {
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(b.x + b.w * 0.25, 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 * 0.8);
        ctx.lineTo(b.x + b.w * 0.65, 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.75, b.y + b.h * 0.4);
        ctx.stroke();
      }

      // Power-up marker icon
      if (b.powerup) {
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 11px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        let icon = b.powerup === 'wide' ? '⟷' : '✦';
        ctx.fillText(icon, b.x + b.w / 2, b.y + b.h / 2);
      }

      ctx.restore();
    }
  }

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

  // Draw Powerups
  powerups.forEach(pw => {
    ctx.save();
    ctx.translate(pw.x, pw.y);
    let scale = 1 + Math.sin(pw.pulse) * 0.12;
    ctx.scale(scale, scale);

    let col = pw.type === 'wide' ? '#00f0ff' : '#ff2a6d';
    ctx.shadowBlur = 12;
    ctx.shadowColor = col;
    ctx.fillStyle = '#0f1428';
    ctx.strokeStyle = col;
    ctx.lineWidth = 2;

    roundRect(ctx, -pw.w / 2, -pw.h / 2, pw.w, pw.h, 6, true, true);

    ctx.fillStyle = col;
    ctx.font = 'bold 12px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(pw.type === 'wide' ? 'W' : '3×', 0, 1);

    ctx.restore();
  });

  // Draw Paddle
  ctx.save();
  let padGrad = ctx.createLinearGradient(paddleX, paddleY, paddleX, paddleY + PADDLE_HEIGHT);
  if (widePaddleTimer > 0) {
    padGrad.addColorStop(0, '#8bf7ff');
    padGrad.addColorStop(1, '#0084ff');
    ctx.shadowColor = '#00f0ff';
  } else {
    padGrad.addColorStop(0, '#ffffff');
    padGrad.addColorStop(1, '#05d550');
    ctx.shadowColor = '#05d550';
  }
  ctx.shadowBlur = 14;
  ctx.fillStyle = padGrad;
  roundRect(ctx, paddleX, paddleY, paddleWidth, PADDLE_HEIGHT, 7, true, false);

  // Paddle inner accent line
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(paddleX + 8, paddleY + 3);
  ctx.lineTo(paddleX + paddleWidth - 8, paddleY + 3);
  ctx.stroke();
  ctx.restore();

  // Draw Balls
  balls.forEach(b => {
    // Trail
    b.trail.forEach((pt, idx) => {
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, b.radius * (idx / b.trail.length) * 0.8, 0, Math.PI * 2);
      ctx.fillStyle = `rgba(0, 240, 255, ${idx * 0.08})`;
      ctx.fill();
    });

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

  // HUD
  ctx.save();
  ctx.font = 'bold 15px sans-serif';
  ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';

  // Score
  ctx.textAlign = 'left';
  ctx.fillText(`SCORE  ${score}`, 24, 28);

  // Wide Paddle Indicator
  if (widePaddleTimer > 0) {
    let secs = (widePaddleTimer / 60).toFixed(1);
    ctx.fillStyle = '#00f0ff';
    ctx.textAlign = 'center';
    ctx.fillText(`WIDE PADDLE: ${secs}s`, W / 2, 28);
  }

  // Lives
  ctx.textAlign = 'right';
  ctx.fillStyle = '#ff2a6d';
  let hearts = '';
  for (let l = 0; l < lives; l++) hearts += '♥ ';
  ctx.fillText(hearts.trim(), W - 24, 28);
  ctx.restore();

  // Overlay Messages
  if (isAutoplay) {
    drawOverlayBanner('NEON BREAKOUT', 'CLICK OR PRESS ANY KEY TO PLAY', '#00f0ff');
  } else if (isGameOver) {
    drawOverlayBanner('GAME OVER', 'PRESS SPACE OR CLICK TO RETRY', '#ff2a6d');
  } else if (isVictory) {
    drawOverlayBanner('VICTORY!', 'CONGRATULATIONS! CLICK TO PLAY AGAIN', '#05d550');
  } else if (isPaused) {
    drawOverlayBanner('PAUSED', 'PRESS P TO RESUME', '#ffb703');
  }
}

function drawOverlayBanner(title, subtitle, color) {
  ctx.save();
  ctx.fillStyle = 'rgba(6, 8, 20, 0.72)';
  ctx.fillRect(0, 0, W, H);

  ctx.shadowColor = color;
  ctx.shadowBlur = 20;
  ctx.fillStyle = '#ffffff';
  ctx.font = '900 42px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(title, W / 2, H / 2 - 12);

  ctx.shadowBlur = 8;
  ctx.fillStyle = color;
  ctx.font = 'bold 14px sans-serif';
  ctx.letterSpacing = '1px';
  ctx.fillText(subtitle, W / 2, H / 2 + 30);
  ctx.restore();
}

function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.lineTo(x + width - radius, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
  ctx.lineTo(x + width, y + height - radius);
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  ctx.lineTo(x + radius, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
  ctx.lineTo(x, y + radius);
  ctx.quadraticCurveTo(x, y, x + radius, y);
  ctx.closePath();
  if (fill) ctx.fill();
  if (stroke) ctx.stroke();
}

function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

// Initial setup
initGame();
loop();
</script>
</body>
</html>