hypeorcode
Network offOpen alone (new tab)

Run details

Model
google/gemini-3.8-flash
Route
Command Code CLI
Reasoning
provider default
Limits
60 min
Date
2 Oct 2026
Time
86 s
Size
32 kB
Tokens
104,731 in, 13,067 out
List price
$0.14 ($0.825 in / $4.13 out per M)
Paid
Not billed per run (CLI plan)

Raw files: output · meta.json

Exact prompt

Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice.

It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Save it as index.html in the current directory, then stop. Do not ask questions.

Source

32 kB 1,152 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout Deluxe</title>
<style>
  * {
    box-sizing: border-box;
    user-select: none;
    -webkit-user-select: none;
    margin: 0;
    padding: 0;
  }
  body {
    background: #090a12;
    color: #e0e6ed;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    overflow: hidden;
  }
  #game-container {
    position: relative;
    box-shadow: 0 0 50px rgba(0, 150, 255, 0.25), 0 20px 40px rgba(0, 0, 0, 0.8);
    border-radius: 12px;
    overflow: hidden;
    border: 2px solid #1f273d;
    background: #0d111d;
  }
  canvas {
    display: block;
    cursor: none;
  }
  .info-bar {
    margin-top: 14px;
    font-size: 13px;
    color: #7b8ba4;
    letter-spacing: 0.5px;
    display: flex;
    gap: 18px;
  }
  .info-bar span kbd {
    background: #192135;
    border: 1px solid #2d3b59;
    border-radius: 4px;
    padding: 2px 6px;
    color: #a7bce0;
    font-size: 11px;
    font-family: inherit;
  }
</style>
</head>
<body>

<div id="game-container">
  <canvas id="game" width="800" height="600"></canvas>
</div>

<div class="info-bar">
  <span><kbd>Mouse</kbd> or <kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> Move</span>
  <span><kbd>Space</kbd> or <kbd>Click</kbd> Launch</span>
  <span><kbd>P</kbd> Pause</span>
  <span><kbd>M</kbd> Mute</span>
</div>

<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const W = canvas.width;
  const H = canvas.height;

  // Web Audio Context for synthesized sound fx
  let audioCtx = null;
  let muted = false;

  function initAudio() {
    if (!audioCtx) {
      audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    if (audioCtx.state === 'suspended') {
      audioCtx.resume();
    }
  }

  function playSound(type) {
    if (muted || !audioCtx) return;
    try {
      const now = audioCtx.currentTime;
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.connect(gain);
      gain.connect(audioCtx.destination);

      if (type === 'hit-brick') {
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(360 + Math.random() * 80, now);
        osc.frequency.exponentialRampToValueAtTime(140, now + 0.08);
        gain.gain.setValueAtTime(0.25, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
        osc.start(now);
        osc.stop(now + 0.08);
      } else if (type === 'hit-paddle') {
        osc.type = 'sine';
        osc.frequency.setValueAtTime(220, now);
        osc.frequency.exponentialRampToValueAtTime(440, now + 0.09);
        gain.gain.setValueAtTime(0.3, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.09);
        osc.start(now);
        osc.stop(now + 0.09);
      } else if (type === 'hit-wall') {
        osc.type = 'sine';
        osc.frequency.setValueAtTime(180, now);
        osc.frequency.exponentialRampToValueAtTime(90, now + 0.05);
        gain.gain.setValueAtTime(0.15, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
        osc.start(now);
        osc.stop(now + 0.05);
      } else if (type === 'powerup') {
        osc.type = 'sine';
        osc.frequency.setValueAtTime(320, now);
        osc.frequency.exponentialRampToValueAtTime(880, now + 0.2);
        gain.gain.setValueAtTime(0.25, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
        osc.start(now);
        osc.stop(now + 0.2);
      } else if (type === 'lose-life') {
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(260, now);
        osc.frequency.exponentialRampToValueAtTime(60, now + 0.35);
        gain.gain.setValueAtTime(0.35, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
        osc.start(now);
        osc.stop(now + 0.35);
      } else if (type === 'level-clear') {
        const chord = [440, 554.37, 659.25, 880];
        chord.forEach((freq, idx) => {
          const o = audioCtx.createOscillator();
          const g = audioCtx.createGain();
          o.connect(g);
          g.connect(audioCtx.destination);
          o.type = 'triangle';
          o.frequency.setValueAtTime(freq, now + idx * 0.07);
          g.gain.setValueAtTime(0, now);
          g.gain.setValueAtTime(0.2, now + idx * 0.07);
          g.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.07 + 0.35);
          o.start(now + idx * 0.07);
          o.stop(now + idx * 0.07 + 0.35);
        });
      } else if (type === 'game-over') {
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(200, now);
        osc.frequency.linearRampToValueAtTime(50, now + 0.6);
        gain.gain.setValueAtTime(0.4, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
        osc.start(now);
        osc.stop(now + 0.6);
      } else if (type === 'laser') {
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(900, now);
        osc.frequency.exponentialRampToValueAtTime(200, now + 0.12);
        gain.gain.setValueAtTime(0.2, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
        osc.start(now);
        osc.stop(now + 0.12);
      }
    } catch (e) {
      // Ignore audio errors
    }
  }

  // Level layouts (0: empty, 1: red, 2: orange, 3: yellow, 4: green, 5: cyan, 6: purple, 9: hardened)
  const LEVELS = [
    // Level 1: Classic Rainbow warmup
    {
      name: "The Gateway",
      rows: [
        "111111111111",
        "222222222222",
        "333333333333",
        "444444444444",
        "555555555555"
      ]
    },
    // Level 2: Space Invader / Castle shape
    {
      name: "Fortress",
      rows: [
        "001111111100",
        "019222222910",
        "190333333091",
        "114400004411",
        "005555555500",
        "000966669000"
      ]
    },
    // Level 3: Checkerboard / Diamonds
    {
      name: "Prism Maze",
      rows: [
        "101010101010",
        "020202020202",
        "309030309030",
        "040404040404",
        "505050505050",
        "060906609060",
        "101010101010"
      ]
    },
    // Level 4: The Citadel with armored blocks
    {
      name: "Titan's Keep",
      rows: [
        "999999999999",
        "111001100111",
        "229229922922",
        "333333333333",
        "044094490440",
        "555555555555",
        "006660066600"
      ]
    },
    // Level 5: Heart of the Arcade
    {
      name: "Core Meltdown",
      rows: [
        "011000000110",
        "122109901221",
        "233296692332",
        "344396693443",
        "044009900440",
        "005500005500",
        "000660066000",
        "000099990000"
      ]
    }
  ];

  const COLOR_PALETTE = {
    1: { top: '#ff3366', bot: '#b3003b', glow: '#ff3366', points: 100 }, // Red
    2: { top: '#ff9933', bot: '#cc5500', glow: '#ff9933', points: 80 },  // Orange
    3: { top: '#ffea00', bot: '#c7a700', glow: '#ffea00', points: 60 },  // Yellow
    4: { top: '#00e676', bot: '#00963f', glow: '#00e676', points: 40 },  // Green
    5: { top: '#00d4ff', bot: '#007fa3', glow: '#00d4ff', points: 30 },  // Cyan
    6: { top: '#b347ff', bot: '#6c00b8', glow: '#b347ff', points: 50 },  // Purple
    9: { top: '#8a99ad', bot: '#424f63', glow: '#9bb1cc', points: 150, hp: 2 } // Steel / Armored
  };

  const POWERUP_TYPES = [
    { type: 'expand', label: 'WIDE', color: '#00e676', icon: '↔' },
    { type: 'multiball', label: 'MULTI', color: '#00d4ff', icon: '••' },
    { type: 'laser', label: 'LASER', color: '#ff3366', icon: '▲' },
    { type: 'fireball', label: 'FIRE', color: '#ff9933', icon: '🔥' },
    { type: 'life', label: '+LIFE', color: '#ffea00', icon: '♥' }
  ];

  // Game state
  let state = 'title'; // 'title', 'playing', 'paused', 'levelclear', 'gameover', 'victory'
  let score = 0;
  let highScore = parseInt(localStorage.getItem('breakout_high') || '0', 10);
  let lives = 3;
  let currentLevelIdx = 0;
  let combo = 0;
  let comboTimer = 0;
  let screenShake = 0;
  let messageTimer = 0;
  let levelBannerText = '';

  // Entities
  let paddle = {
    x: W / 2 - 55,
    y: H - 42,
    w: 110,
    baseW: 110,
    targetW: 110,
    h: 14,
    speed: 9,
    dx: 0,
    laserTimer: 0
  };

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

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

  function resetPaddle() {
    paddle.w = paddle.baseW;
    paddle.targetW = paddle.baseW;
    paddle.x = W / 2 - paddle.w / 2;
    paddle.laserTimer = 0;
  }

  function spawnBall(stickToPaddle = true) {
    const ballSpeed = 6.5 + currentLevelIdx * 0.4;
    return {
      x: paddle.x + paddle.w / 2,
      y: paddle.y - 10,
      vx: (Math.random() > 0.5 ? 1 : -1) * (ballSpeed * 0.6),
      vy: -ballSpeed * 0.8,
      speed: ballSpeed,
      radius: 7,
      stuck: stickToPaddle,
      fireball: 0,
      tail: []
    };
  }

  function loadLevel(idx) {
    currentLevelIdx = idx % LEVELS.length;
    const lvl = LEVELS[currentLevelIdx];
    bricks = [];
    powerups = [];
    lasers = [];
    particles = [];

    const numCols = 12;
    const padding = 8;
    const offsetTop = 75;
    const offsetLeft = 40;
    const availableW = W - offsetLeft * 2;
    const brickW = (availableW - padding * (numCols - 1)) / numCols;
    const brickH = 22;

    lvl.rows.forEach((row, r) => {
      for (let c = 0; c < row.length; c++) {
        const char = row[c];
        if (char !== '0') {
          const type = parseInt(char, 10);
          const meta = COLOR_PALETTE[type];
          bricks.push({
            x: offsetLeft + c * (brickW + padding),
            y: offsetTop + r * (brickH + padding),
            w: brickW,
            h: brickH,
            type: type,
            color: meta,
            hp: meta.hp || 1,
            maxHp: meta.hp || 1,
            scale: 1,
            wobble: 0
          });
        }
      }
    });

    resetPaddle();
    balls = [spawnBall(true)];
    levelBannerText = `Level ${currentLevelIdx + 1}: ${lvl.name}`;
    messageTimer = 100;
  }

  function addScore(pts, x, y) {
    const multiplier = 1 + Math.floor(combo / 3) * 0.25;
    const finalPts = Math.round(pts * multiplier);
    score += finalPts;
    if (score > highScore) {
      highScore = score;
      localStorage.setItem('breakout_high', highScore.toString());
    }

    combo++;
    comboTimer = 110;

    floatingTexts.push({
      text: `+${finalPts}` + (multiplier > 1 ? ` (x${multiplier.toFixed(2)})` : ''),
      x: x || W / 2,
      y: y || H / 2,
      vy: -1.4,
      alpha: 1,
      color: multiplier > 1 ? '#ffea00' : '#ffffff',
      size: multiplier > 1 ? 16 : 14
    });
  }

  function createExplosion(x, y, color, count = 16, speed = 4) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const spd = (0.5 + Math.random()) * speed;
      particles.push({
        x: x,
        y: y,
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd,
        size: 2 + Math.random() * 4,
        alpha: 1,
        color: color,
        life: 25 + Math.random() * 20
      });
    }
  }

  function tryDropPowerup(x, y) {
    // 22% chance of dropping powerup
    if (Math.random() < 0.22) {
      const p = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
      powerups.push({
        x: x,
        y: y,
        w: 32,
        h: 18,
        vy: 2.2,
        type: p.type,
        label: p.label,
        color: p.color,
        icon: p.icon,
        angle: 0
      });
    }
  }

  function applyPowerup(p) {
    playSound('powerup');
    screenShake = Math.max(screenShake, 6);
    floatingTexts.push({
      text: p.label + '!',
      x: paddle.x + paddle.w / 2,
      y: paddle.y - 20,
      vy: -2,
      alpha: 1,
      color: p.color,
      size: 20
    });

    if (p.type === 'expand') {
      paddle.targetW = Math.min(220, paddle.w + 45);
    } else if (p.type === 'multiball') {
      const currentCount = balls.length;
      for (let i = 0; i < currentCount; i++) {
        const b = balls[i];
        if (!b.stuck) {
          for (let k = -1; k <= 1; k += 2) {
            balls.push({
              x: b.x,
              y: b.y,
              vx: b.vx * Math.cos(k * 0.4) - b.vy * Math.sin(k * 0.4),
              vy: b.vx * Math.sin(k * 0.4) + b.vy * Math.cos(k * 0.4),
              speed: b.speed,
              radius: b.radius,
              stuck: false,
              fireball: b.fireball,
              tail: []
            });
          }
        }
      }
    } else if (p.type === 'laser') {
      paddle.laserTimer = 400; // ~6.5 seconds of firing
    } else if (p.type === 'fireball') {
      balls.forEach(b => {
        b.fireball = 400;
      });
    } else if (p.type === 'life') {
      lives++;
      createExplosion(paddle.x + paddle.w / 2, paddle.y, '#ffea00', 25, 5);
    }
  }

  function launchBall() {
    initAudio();
    let launchedAny = false;
    balls.forEach(b => {
      if (b.stuck) {
        b.stuck = false;
        // Direction based on paddle center offset
        const offset = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
        b.vx = offset * b.speed * 0.8;
        b.vy = -Math.sqrt(Math.max(16, b.speed * b.speed - b.vx * b.vx));
        launchedAny = true;
      }
    });
    if (launchedAny) {
      playSound('hit-paddle');
    }
  }

  function startGame() {
    initAudio();
    score = 0;
    lives = 3;
    combo = 0;
    comboTimer = 0;
    currentLevelIdx = 0;
    loadLevel(0);
    state = 'playing';
  }

  // Input listeners
  window.addEventListener('keydown', (e) => {
    initAudio();
    if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
    if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
    if (e.code === 'Space') {
      keys.space = true;
      if (state === 'title' || state === 'gameover' || state === 'victory') {
        startGame();
      } else if (state === 'playing') {
        launchBall();
      } else if (state === 'levelclear') {
        loadLevel(currentLevelIdx + 1);
        state = 'playing';
      }
    }
    if (e.code === 'KeyP') {
      if (state === 'playing') state = 'paused';
      else if (state === 'paused') state = 'playing';
    }
    if (e.code === 'KeyM') {
      muted = !muted;
    }
  });

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

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

  canvas.addEventListener('mousedown', (e) => {
    initAudio();
    if (state === 'title' || state === 'gameover' || state === 'victory') {
      startGame();
    } else if (state === 'playing') {
      launchBall();
    } else if (state === 'levelclear') {
      loadLevel(currentLevelIdx + 1);
      state = 'playing';
    }
  });

  // Main game update loop
  let lastTime = performance.now();

  function update() {
    // Screen shake decay
    if (screenShake > 0) {
      screenShake *= 0.88;
      if (screenShake < 0.2) screenShake = 0;
    }

    if (state !== 'playing') {
      // Update particles even during victory / clear
      updateParticles();
      return;
    }

    // Paddle width lerp
    paddle.w += (paddle.targetW - paddle.w) * 0.15;

    // Paddle movement
    if (mouseActive) {
      const prevX = paddle.x;
      paddle.x = mouseTargetX - paddle.w / 2;
      paddle.dx = paddle.x - prevX;
      mouseActive = false; // until next mousemove
    } else {
      if (keys.left) {
        paddle.dx = -paddle.speed;
        paddle.x += paddle.dx;
      } else if (keys.right) {
        paddle.dx = paddle.speed;
        paddle.x += paddle.dx;
      } else {
        paddle.dx *= 0.7;
      }
    }

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

    // Laser weapon firing
    if (paddle.laserTimer > 0) {
      paddle.laserTimer--;
      if (paddle.laserTimer % 18 === 0) {
        playSound('laser');
        lasers.push({ x: paddle.x + 8, y: paddle.y - 4, vy: -12, w: 4, h: 14 });
        lasers.push({ x: paddle.x + paddle.w - 12, y: paddle.y - 4, vy: -12, w: 4, h: 14 });
      }
    }

    // Lasers update
    for (let i = lasers.length - 1; i >= 0; i--) {
      const l = lasers[i];
      l.y += l.vy;
      let hit = false;
      for (let j = bricks.length - 1; j >= 0; j--) {
        const b = bricks[j];
        if (l.x + l.w > b.x && l.x < b.x + b.w && l.y < b.y + b.h && l.y + l.h > b.y) {
          hit = true;
          b.hp--;
          b.scale = 1.15;
          createExplosion(l.x, l.y, '#ff3366', 8, 3);
          playSound('hit-brick');
          if (b.hp <= 0) {
            addScore(b.color.points, b.x + b.w / 2, b.y + b.h / 2);
            createExplosion(b.x + b.w / 2, b.y + b.h / 2, b.color.top, 18, 4);
            tryDropPowerup(b.x + b.w / 2, b.y + b.h / 2);
            bricks.splice(j, 1);
          }
          break;
        }
      }
      if (hit || l.y < 0) {
        lasers.splice(i, 1);
      }
    }

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

      // Check paddle collision
      if (
        p.x + p.w / 2 > paddle.x &&
        p.x - p.w / 2 < paddle.x + paddle.w &&
        p.y + p.h / 2 > paddle.y &&
        p.y - p.h / 2 < paddle.y + paddle.h
      ) {
        applyPowerup(p);
        powerups.splice(i, 1);
        continue;
      }

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

    // Combo timer
    if (comboTimer > 0) {
      comboTimer--;
      if (comboTimer === 0) combo = 0;
    }

    // Message banner timer
    if (messageTimer > 0) messageTimer--;

    // Update balls
    let allLost = true;
    for (let i = balls.length - 1; i >= 0; i--) {
      const b = balls[i];

      if (b.fireball > 0) b.fireball--;

      if (b.stuck) {
        b.x = paddle.x + paddle.w / 2;
        b.y = paddle.y - b.radius - 2;
        allLost = false;
        continue;
      }

      // Trail recording
      b.tail.unshift({ x: b.x, y: b.y, alpha: 0.6, fireball: b.fireball > 0 });
      if (b.tail.length > 8) b.tail.pop();

      // Ball movement
      b.x += b.vx;
      b.y += b.vy;

      // Wall collisions
      if (b.x - b.radius < 12) {
        b.x = 12 + b.radius;
        b.vx = Math.abs(b.vx);
        playSound('hit-wall');
        createExplosion(12, b.y, '#5577aa', 5, 2);
      } else if (b.x + b.radius > W - 12) {
        b.x = W - 12 - b.radius;
        b.vx = -Math.abs(b.vx);
        playSound('hit-wall');
        createExplosion(W - 12, b.y, '#5577aa', 5, 2);
      }
      if (b.y - b.radius < 12) {
        b.y = 12 + b.radius;
        b.vy = Math.abs(b.vy);
        playSound('hit-wall');
        createExplosion(b.x, 12, '#5577aa', 5, 2);
      }

      // Paddle collision
      if (
        b.y + b.radius >= paddle.y &&
        b.y - b.radius <= paddle.y + paddle.h &&
        b.x + b.radius >= paddle.x &&
        b.x - b.radius <= paddle.x + paddle.w &&
        b.vy > 0
      ) {
        playSound('hit-paddle');
        screenShake = Math.max(screenShake, 3);

        // Angle influence based on impact point on paddle
        const hitPoint = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
        const maxAngle = Math.PI * 0.42; // ~75 deg
        const angle = hitPoint * maxAngle;
        const currentSpeed = Math.min(12, b.speed * 1.02); // tiny acceleration
        b.speed = currentSpeed;
        b.vx = currentSpeed * Math.sin(angle) + paddle.dx * 0.15;
        b.vy = -currentSpeed * Math.cos(angle);
        b.y = paddle.y - b.radius;

        createExplosion(b.x, paddle.y, '#00d4ff', 10, 3);
      }

      // Brick collisions
      for (let j = bricks.length - 1; j >= 0; j--) {
        const brk = bricks[j];
        if (
          b.x + b.radius > brk.x &&
          b.x - b.radius < brk.x + brk.w &&
          b.y + b.radius > brk.y &&
          b.y - b.radius < brk.y + brk.h
        ) {
          playSound('hit-brick');
          screenShake = Math.max(screenShake, b.fireball > 0 ? 8 : 4);

          if (b.fireball === 0) {
            // Determine collision edge
            const prevX = b.x - b.vx;
            const prevY = b.y - b.vy;
            let hitFromSide = false;

            if (prevX + b.radius <= brk.x || prevX - b.radius >= brk.x + brk.w) {
              b.vx = -b.vx;
              hitFromSide = true;
            }
            if (prevY + b.radius <= brk.y || prevY - b.radius >= brk.y + brk.h) {
              b.vy = -b.vy;
            } else if (!hitFromSide) {
              b.vy = -b.vy;
            }
          }

          brk.hp -= (b.fireball > 0 ? 2 : 1);
          brk.scale = 1.25;

          if (brk.hp <= 0) {
            addScore(brk.color.points, brk.x + brk.w / 2, brk.y + brk.h / 2);
            createExplosion(brk.x + brk.w / 2, brk.y + brk.h / 2, brk.color.top, 22, 5);
            tryDropPowerup(brk.x + brk.w / 2, brk.y + brk.h / 2);
            bricks.splice(j, 1);
          } else {
            createExplosion(b.x, b.y, brk.color.top, 8, 3);
          }
          break; // collide with one brick per ball frame
        }
      }

      // Ball below bottom
      if (b.y > H + 20) {
        balls.splice(i, 1);
      } else {
        allLost = false;
      }
    }

    // Check level clear
    if (bricks.length === 0) {
      playSound('level-clear');
      if (currentLevelIdx + 1 < LEVELS.length) {
        state = 'levelclear';
      } else {
        state = 'victory';
      }
      return;
    }

    // Check lost life
    if (allLost) {
      lives--;
      playSound('lose-life');
      screenShake = 12;
      combo = 0;
      if (lives <= 0) {
        playSound('game-over');
        state = 'gameover';
      } else {
        resetPaddle();
        balls = [spawnBall(true)];
      }
    }

    updateParticles();
  }

  function updateParticles() {
    // Particle physics
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vx *= 0.96;
      p.vy *= 0.96;
      p.life--;
      p.alpha = Math.max(0, p.life / 40);
      if (p.life <= 0) particles.splice(i, 1);
    }

    // Floating texts
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
      const t = floatingTexts[i];
      t.y += t.vy;
      t.alpha -= 0.02;
      if (t.alpha <= 0) floatingTexts.splice(i, 1);
    }

    // Brick scale animations
    bricks.forEach(b => {
      if (b.scale > 1) {
        b.scale += (1 - b.scale) * 0.2;
      }
    });
  }

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

    // Screen shake transform
    if (screenShake > 0) {
      const sx = (Math.random() - 0.5) * screenShake * 2;
      const sy = (Math.random() - 0.5) * screenShake * 2;
      ctx.translate(sx, sy);
    }

    // Background gradient & subtle grid
    const bgGrad = ctx.createRadialGradient(W / 2, H / 2, 80, W / 2, H / 2, 500);
    bgGrad.addColorStop(0, '#101626');
    bgGrad.addColorStop(1, '#080a11');
    ctx.fillStyle = bgGrad;
    ctx.fillRect(0, 0, W, H);

    // Subtle arena boundaries
    ctx.strokeStyle = '#1e2942';
    ctx.lineWidth = 4;
    ctx.strokeRect(10, 10, W - 20, H - 20);

    // Border neon glow
    ctx.strokeStyle = 'rgba(0, 170, 255, 0.15)';
    ctx.lineWidth = 1;
    ctx.strokeRect(12, 12, W - 24, H - 24);

    // Draw Bricks
    bricks.forEach(b => {
      ctx.save();
      const cx = b.x + b.w / 2;
      const cy = b.y + b.h / 2;
      ctx.translate(cx, cy);
      ctx.scale(b.scale, b.scale);

      const halfW = b.w / 2;
      const halfH = b.h / 2;

      // Glow for bricks
      ctx.shadowColor = b.color.glow;
      ctx.shadowBlur = b.scale > 1.05 ? 14 : 4;

      const grad = ctx.createLinearGradient(-halfW, -halfH, -halfW, halfH);
      grad.addColorStop(0, b.color.top);
      grad.addColorStop(1, b.color.bot);
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.roundRect(-halfW, -halfH, b.w, b.h, 4);
      ctx.fill();

      // Top highlight
      ctx.shadowBlur = 0;
      ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
      ctx.beginPath();
      ctx.roundRect(-halfW + 2, -halfH + 2, b.w - 4, (b.h - 4) * 0.35, 2);
      ctx.fill();

      // Armored crack indicator
      if (b.type === 9 && b.hp === 1) {
        ctx.strokeStyle = '#ffffff';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(-halfW + 6, -halfH + 4);
        ctx.lineTo(0, 0);
        ctx.lineTo(halfW - 8, halfH - 4);
        ctx.stroke();
      }

      ctx.restore();
    });

    // Draw Lasers
    lasers.forEach(l => {
      ctx.save();
      ctx.fillStyle = '#ff3366';
      ctx.shadowColor = '#ff3366';
      ctx.shadowBlur = 10;
      ctx.fillRect(l.x, l.y, l.w, l.h);
      ctx.restore();
    });

    // Draw Powerups
    powerups.forEach(p => {
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 10;

      ctx.fillStyle = '#101524';
      ctx.strokeStyle = p.color;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.roundRect(-p.w / 2, -p.h / 2, p.w, p.h, 6);
      ctx.fill();
      ctx.stroke();

      ctx.shadowBlur = 0;
      ctx.fillStyle = p.color;
      ctx.font = 'bold 10px monospace';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(p.label, 0, 1);
      ctx.restore();
    });

    // Draw Ball Trails & Balls
    balls.forEach(b => {
      // Trail
      for (let i = 0; i < b.tail.length; i++) {
        const pt = b.tail[i];
        ctx.beginPath();
        ctx.arc(pt.x, pt.y, b.radius * (1 - i / b.tail.length * 0.6), 0, Math.PI * 2);
        ctx.fillStyle = pt.fireball
          ? `rgba(255, 120, 20, ${0.45 * (1 - i / b.tail.length)})`
          : `rgba(0, 212, 255, ${0.35 * (1 - i / b.tail.length)})`;
        ctx.fill();
      }

      // Ball body
      ctx.save();
      const isFire = b.fireball > 0;
      ctx.shadowColor = isFire ? '#ff6600' : '#00d4ff';
      ctx.shadowBlur = isFire ? 18 : 12;

      const ballGrad = ctx.createRadialGradient(
        b.x - b.radius * 0.3,
        b.y - b.radius * 0.3,
        b.radius * 0.1,
        b.x,
        b.y,
        b.radius
      );
      if (isFire) {
        ballGrad.addColorStop(0, '#ffffff');
        ballGrad.addColorStop(0.4, '#ffdd33');
        ballGrad.addColorStop(1, '#ff3300');
      } else {
        ballGrad.addColorStop(0, '#ffffff');
        ballGrad.addColorStop(0.3, '#7fe9ff');
        ballGrad.addColorStop(1, '#0099cc');
      }

      ctx.fillStyle = ballGrad;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    });

    // Draw Paddle
    ctx.save();
    ctx.shadowColor = paddle.laserTimer > 0 ? '#ff3366' : '#00e5ff';
    ctx.shadowBlur = 14;

    const padGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.h);
    if (paddle.laserTimer > 0) {
      padGrad.addColorStop(0, '#ff6688');
      padGrad.addColorStop(0.5, '#cc0033');
      padGrad.addColorStop(1, '#66001a');
    } else {
      padGrad.addColorStop(0, '#66ffff');
      padGrad.addColorStop(0.5, '#0099dd');
      padGrad.addColorStop(1, '#004488');
    }

    ctx.fillStyle = padGrad;
    ctx.beginPath();
    ctx.roundRect(paddle.x, paddle.y, paddle.w, paddle.h, 7);
    ctx.fill();

    // Paddle gloss top
    ctx.shadowBlur = 0;
    ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
    ctx.beginPath();
    ctx.roundRect(paddle.x + 3, paddle.y + 2, paddle.w - 6, 3, 2);
    ctx.fill();

    // Laser cannons indicator
    if (paddle.laserTimer > 0) {
      ctx.fillStyle = '#ffea00';
      ctx.fillRect(paddle.x + 6, paddle.y - 4, 4, 6);
      ctx.fillRect(paddle.x + paddle.w - 10, paddle.y - 4, 4, 6);
    }
    ctx.restore();

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

    // Draw Floating Text
    floatingTexts.forEach(t => {
      ctx.save();
      ctx.globalAlpha = Math.max(0, t.alpha);
      ctx.fillStyle = t.color;
      ctx.font = `bold ${t.size}px monospace`;
      ctx.textAlign = 'center';
      ctx.shadowColor = t.color;
      ctx.shadowBlur = 8;
      ctx.fillText(t.text, t.x, t.y);
      ctx.restore();
    });

    // Top HUD
    ctx.save();
    ctx.font = '600 15px -apple-system, BlinkMacSystemFont, sans-serif';
    ctx.fillStyle = '#94a3b8';

    // Score
    ctx.fillText('SCORE', 24, 32);
    ctx.fillStyle = '#ffffff';
    ctx.font = 'bold 20px monospace';
    ctx.fillText(score.toString().padStart(6, '0'), 24, 54);

    // High score
    ctx.font = '600 13px -apple-system, BlinkMacSystemFont, sans-serif';
    ctx.fillStyle = '#64748b';
    ctx.fillText(`HIGH: ${highScore}`, 130, 52);

    // Level
    ctx.font = '600 15px -apple-system, BlinkMacSystemFont, sans-serif';
    ctx.fillStyle = '#94a3b8';
    ctx.textAlign = 'center';
    ctx.fillText('LEVEL', W / 2, 32);
    ctx.fillStyle = '#00e5ff';
    ctx.font = 'bold 18px monospace';
    ctx.fillText(`${currentLevelIdx + 1} / ${LEVELS.length}`, W / 2, 54);

    // Lives
    ctx.textAlign = 'right';
    ctx.font = '600 15px -apple-system, BlinkMacSystemFont, sans-serif';
    ctx.fillStyle = '#94a3b8';
    ctx.fillText('LIVES', W - 24, 32);
    ctx.fillStyle = '#ff3366';
    let heartStr = '';
    for (let l = 0; l < Math.min(lives, 5); l++) heartStr += '♥ ';
    if (lives > 5) heartStr += `+${lives - 5}`;
    ctx.font = 'bold 18px sans-serif';
    ctx.fillText(heartStr, W - 24, 54);

    // Combo indicator
    if (combo > 2) {
      ctx.textAlign = 'center';
      ctx.font = 'bold 14px monospace';
      ctx.fillStyle = '#ffea00';
      ctx.fillText(`COMBO x${combo}!`, W / 2, 74);
    }

    ctx.restore();

    // Banner message during game
    if (state === 'playing' && messageTimer > 0) {
      ctx.save();
      const alpha = Math.min(1, messageTimer / 30);
      ctx.globalAlpha = alpha;
      ctx.fillStyle = '#00d4ff';
      ctx.font = 'bold 22px monospace';
      ctx.textAlign = 'center';
      ctx.shadowColor = '#00d4ff';
      ctx.shadowBlur = 12;
      ctx.fillText(levelBannerText, W / 2, H / 2 - 20);
      ctx.restore();
    }

    // Overlays for states
    if (state === 'title') {
      renderOverlay('BREAKOUT DELUXE', 'Press SPACE or Click to Play', '#00d4ff');
    } else if (state === 'paused') {
      renderOverlay('PAUSED', 'Press P to Resume', '#ffea00');
    } else if (state === 'levelclear') {
      renderOverlay('LEVEL COMPLETED!', 'Press SPACE or Click for Next Level', '#00e676');
    } else if (state === 'gameover') {
      renderOverlay('GAME OVER', `Final Score: ${score} • Press SPACE to Restart`, '#ff3366');
    } else if (state === 'victory') {
      renderOverlay('VICTORY!', `You Beat All Levels! Score: ${score} • SPACE to Play Again`, '#ffea00');
    }

    ctx.restore();
  }

  function renderOverlay(title, subtitle, color) {
    ctx.save();
    ctx.fillStyle = 'rgba(8, 10, 18, 0.82)';
    ctx.fillRect(0, 0, W, H);

    ctx.shadowColor = color;
    ctx.shadowBlur = 25;
    ctx.fillStyle = color;
    ctx.font = '900 44px -apple-system, BlinkMacSystemFont, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(title, W / 2, H / 2 - 15);

    ctx.shadowBlur = 0;
    ctx.fillStyle = '#e2e8f0';
    ctx.font = '500 16px -apple-system, BlinkMacSystemFont, sans-serif';
    ctx.fillText(subtitle, W / 2, H / 2 + 35);
    ctx.restore();
  }

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

  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Compare with

Gemini 3.8 Flash: other attempts

  • Gemini 3.8 Flash, Watch: Queued · #11 of 15 paid · not before 24 Oct
  • Gemini 3.8 Flash, Logo: Queued · #11 of 15 paid · not before 24 Oct
  • Gemini 3.8 Flash, About: Queued · #11 of 15 paid · not before 24 Oct