• Updated 30 Oct, 21:11 UTC
  • Next run 31 Oct, 05:17 UTCNext 31 Oct 05:17
  • Last 7 days: 4 new models · 475 runs7 d: 4 models · 475 runs
Playable Breakout

InklingThinking Machines

Network offOpen alone (new tab)

Run details

Model
thinkingmachines/inkling
Route
OpenRouter API, one user message
Served by
Together
Effort
high, OpenRouter's default for this model
Limits
60 min · 471,859 output tokens (the model's limit)
Date
27 Oct 2026
Time
2 min 40 s
Size
20 kB
Tokens
92 in, 16,853 out (10,086 of them thinking)
List price
$0.068 at today's price ($1 in / $4.05 out per M)
Paid
$0.068

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). Reply with the file in a single ```html code block.

Source

20 kB 664 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Neon Breakout — 5 Levels</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-user-select: none; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #03030a; font-family: system-ui, -apple-system, Segoe UI, sans-serif; }
  canvas { display: block; width: 100%; height: 100vh; object-fit: contain; background: #020208; box-shadow: inset 0 0 120px #000; cursor: crosshair; }
  #overlay {
    position: absolute; inset: 0; pointer-events: none;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
  }
  .hud {
    position: absolute; top: 0; left: 0; width: 100%; padding: 14px 24px;
    display: flex; justify-content: space-between; align-items: center;
    font-family: system-ui, sans-serif; font-weight: 800; letter-spacing: 2px;
    color: #e0f8ff; text-shadow: 0 0 8px #0ff, 0 0 20px #0ff8;
    pointer-events: none; z-index: 10;
  }
  .hud .item { font-size: 18px; text-transform: uppercase; opacity: 0.9; }
  #centerText {
    text-align: center; color: #fff; text-shadow: 0 0 30px #0ff, 0 0 60px #f0f;
    pointer-events: none; transition: opacity 0.2s;
  }
  #centerText h1 { font-size: 64px; letter-spacing: 8px; margin-bottom: 12px; }
  #centerText p { font-size: 22px; opacity: 0.9; font-weight: 300; letter-spacing: 3px; }
  #centerText .small { margin-top: 28px; font-size: 16px; color: #aaa; text-shadow: none; }
</style>
</head>
<body>
  <canvas id="c"></canvas>
  <div class="hud">
    <div class="item">Score <span id="s">0</span></div>
    <div class="item">Level <span id="l">1</span> / 5</div>
    <div class="item">Lives <span id="live">3</span></div>
  </div>
  <div id="overlay"><div id="centerText"></div></div>

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

// Logical resolution (scales to window via CSS object-fit, mouse mapped below)
const W = 800;
const H = 600;
canvas.width = W;
canvas.height = H;

// Audio context (lazy init)
let audioCtx = null;
function initAudio() {
  if (!audioCtx) {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    if (audioCtx.state === 'suspended') audioCtx.resume();
  }
}
function playSound(type) {
  if (!audioCtx) return;
  try {
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    const now = audioCtx.currentTime;
    if (type === 'brick') {
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(200 + Math.random() * 600, now);
      gain.gain.setValueAtTime(0.12, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
      osc.start(now); osc.stop(now + 0.25);
    } else if (type === 'paddle') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(600 + Math.random() * 400, now);
      gain.gain.setValueAtTime(0.1, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
      osc.start(now); osc.stop(now + 0.15);
    } else if (type === 'death') {
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(250, now);
      osc.frequency.exponentialRampToValueAtTime(60, now + 0.35);
      gain.gain.setValueAtTime(0.12, now);
      gain.gain.linearRampToValueAtTime(0.01, now + 0.35);
      osc.start(now); osc.stop(now + 0.35);
    } else if (type === 'level') {
      osc.type = 'square';
      osc.frequency.setValueAtTime(500, now);
      osc.frequency.linearRampToValueAtTime(1000, now + 0.15);
      osc.frequency.linearRampToValueAtTime(800, now + 0.3);
      gain.gain.setValueAtTime(0.12, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
      osc.start(now); osc.stop(now + 0.4);
    } else {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(900, now);
      gain.gain.setValueAtTime(0.08, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
      osc.start(now); osc.stop(now + 0.1);
    }
  } catch (e) {}
}

// Mapping mouse/touch to logical coords
function getMousePos(e) {
  const r = canvas.getBoundingClientRect();
  const x = ((e.clientX - r.left) / r.width) * W;
  const y = ((e.clientY - r.top) / r.height) * H;
  return { x, y };
}

// Game state
let state = 'MENU'; // MENU, READY, PLAYING, LEVEL_CLEAR, GAME_OVER, WON
let level = 1;
let score = 0;
let lives = 3;
let combo = 0;
let comboTimer = 0;
let shake = 0;

const ball = { x: W/2, y: 520, r: 9, dx: 0, dy: 0, stuck: true, trail: [] };
const paddle = { x: W/2 - 60, y: 565, w: 130, h: 14 };

let bricks = [];
const particles = [];
const floatTexts = [];
const stars = [];

for (let i = 0; i < 120; i++) {
  stars.push({
    x: Math.random() * W,
    y: Math.random() * H,
    s: 0.5 + Math.random() * 1.5,
    sp: 0.2 + Math.random() * 0.6,
    b: 0.3 + Math.random() * 0.7
  });
}

function buildLevel(n) {
  bricks = [];
  const bw = 72;
  const bh = 22;
  const gap = 8;
  const totalW = 10 * bw + 9 * gap;
  const startX = (W - totalW) / 2;
  const startY = 60;

  if (n === 1) {
    for (let r = 0; r < 6; r++) {
      for (let c = 0; c < 10; c++) {
        bricks.push({ x: startX + c*(bw+gap), y: startY + r*(bh+gap), w: bw, h: bh, color: `hsl(${40 + r*35}, 100%, 60%)`, active: true, hits: 1 });
      }
    }
  } else if (n === 2) {
    for (let r = 0; r < 6; r++) {
      for (let c = 0; c < 10; c++) {
        if ((r + c) % 2 === 0) {
          bricks.push({ x: startX + c*(bw+gap), y: startY + r*(bh+gap), w: bw, h: bh, color: `hsl(${200 + (r+c)*20}, 100%, 60%)`, active: true, hits: 1 });
        }
      }
    }
  } else if (n === 3) {
    // Diamond pattern
    const cols = [
      [4,5], [3,4,5,6], [2,3,4,5,6,7],
      [1,2,3,4,5,6,7,8], [2,3,4,5,6,7],
      [3,4,5,6], [4,5]
    ];
    for (let r = 0; r < cols.length; r++) {
      for (const c of cols[r]) {
        bricks.push({ x: startX + c*(bw+gap), y: startY + r*(bh+gap), w: bw, h: bh, color: `hsl(${50 + r*30}, 100%, 58%)`, active: true, hits: 1 });
      }
    }
  } else if (n === 4) {
    // Fortress with center hole
    for (let r = 0; r < 7; r++) {
      for (let c = 0; c < 10; c++) {
        let skip = false;
        if (r === 2 && c >= 4 && c <= 5) skip = true;
        if (r === 3 && c >= 3 && c <= 6) skip = true;
        if (r === 4 && c >= 4 && c <= 5) skip = true;
        if (!skip) {
          bricks.push({ x: startX + c*(bw+gap), y: startY + r*(bh+gap), w: bw, h: bh, color: `hsl(${280 + r*25}, 100%, 55%)`, active: true, hits: 1 });
        }
      }
    }
  } else if (n === 5) {
    for (let r = 0; r < 9; r++) {
      for (let c = 0; c < 10; c++) {
        const hard = (r === 4 && c === 4) || (r === 4 && c === 5);
        bricks.push({ x: startX + c*(bw+gap), y: startY + r*(bh+gap), w: bw, h: bh, color: hard ? '#ff0' : `hsl(${r*25}, 100%, 58%)`, active: true, hits: hard ? 2 : 1 });
      }
    }
  }
}

function spawnParticles(b) {
  const cx = b.x + b.w/2;
  const cy = b.y + b.h/2;
  for (let i = 0; i < 14; i++) {
    const ang = (Math.PI * 2 * i) / 14 + Math.random() * 0.5;
    const speed = 2 + Math.random() * 4;
    particles.push({
      x: cx, y: cy,
      vx: Math.cos(ang) * speed,
      vy: Math.sin(ang) * speed,
      life: 30 + Math.random() * 20,
      color: b.color,
      size: 2 + Math.random() * 3
    });
  }
}

function spawnText(x, y, text, big = false) {
  floatTexts.push({
    x, y, text, life: 70, dy: -1.2 - Math.random(), size: big ? 26 : 18
  });
}

function resetGame() {
  score = 0;
  lives = 3;
  combo = 0;
  comboTimer = 0;
  level = 1;
  shake = 0;
  particles.length = 0;
  floatTexts.length = 0;
  ball.stuck = true;
  ball.x = W/2;
  ball.y = 520;
  ball.dx = 0;
  ball.dy = 0;
  ball.trail = [];
  buildLevel(level);
  document.getElementById('s').textContent = score;
  document.getElementById('live').textContent = lives;
  document.getElementById('l').textContent = level;
}

function update() {
  // Background stars
  for (const s of stars) {
    s.y += s.sp;
    if (s.y > H + 10) { s.y = -10; s.x = Math.random() * W; }
  }

  // Paddle control (keyboard continuous)
  if (state === 'READY' || state === 'PLAYING') {
    const spd = 9;
    if (keys['ArrowLeft'] || keys['a'] || keys['A']) paddle.x -= spd;
    if (keys['ArrowRight'] || keys['d'] || keys['D']) paddle.x += spd;
    paddle.x = Math.max(12, Math.min(W - paddle.w - 12, paddle.x));
  }

  if (state === 'PLAYING') {
    // Ball trail
    ball.trail.push({ x: ball.x, y: ball.y });
    if (ball.trail.length > 20) ball.trail.shift();

    if (ball.stuck) {
      ball.x = paddle.x + paddle.w/2;
      ball.y = paddle.y - ball.r - 2;
    } else {
      ball.x += ball.dx;
      ball.y += ball.dy;

      // Wall bounces
      if (ball.x - ball.r < 0) { ball.dx = Math.abs(ball.dx) + (Math.random()*0.5); ball.x = ball.r + 1; playSound('paddle'); shake = 3; }
      if (ball.x + ball.r > W) { ball.dx = -Math.abs(ball.dx) - (Math.random()*0.5); ball.x = W - ball.r - 1; playSound('paddle'); shake = 3; }
      if (ball.y - ball.r < 0) { ball.dy = Math.abs(ball.dy) + (Math.random()*0.5); ball.y = ball.r + 1; playSound('paddle'); shake = 3; }

      // Bottom death
      if (ball.y + ball.r > H + 20) {
        lives--;
        combo = 0;
        comboTimer = 0;
        playSound('death');
        shake = 12;
        spawnText(W/2, H/2 - 40, 'MISS!', true);
        document.getElementById('live').textContent = lives;
        if (lives <= 0) {
          state = 'GAME_OVER';
        } else {
          ball.stuck = true;
          ball.dx = 0;
          ball.dy = 0;
          ball.x = W/2;
          ball.y = 520;
        }
        return;
      }

      // Paddle bounce
      if (ball.y + ball.r > paddle.y && ball.y - ball.r < paddle.y + paddle.h &&
          ball.x + ball.r > paddle.x && ball.x - ball.r < paddle.x + paddle.w) {
        // Calculate angle based on hit point
        const hitPoint = (ball.x - (paddle.x + paddle.w/2)) / (paddle.w/2);
        let angle = hitPoint * (Math.PI/3); // up to 60° from vertical
        const speed = Math.min(Math.hypot(ball.dx, ball.dy) + 0.3, 9.5);
        ball.dx = speed * Math.sin(angle);
        ball.dy = -speed * Math.cos(angle);
        playSound('paddle');
        shake = 4;
        ball.stuck = false;
      }

      // Brick collisions
      for (let i = bricks.length - 1; i >= 0; i--) {
        const b = bricks[i];
        if (!b.active) continue;
        if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + b.w &&
            ball.y + ball.r > b.y && ball.y - ball.r < b.y + b.h) {
          // Hit
          b.hits--;
          if (b.hits <= 0) {
            b.active = false;
            score += 10 * (combo + 1);
            combo++;
            comboTimer = 90; // ~1.5s at 60fps
            spawnParticles(b);
            spawnText(b.x + b.w/2, b.y + b.h/2, '+' + (10*(combo+1)));
            if (combo > 1) spawnText(b.x + b.w/2, b.y + b.h/2 - 20, combo + 'x COMBO!', true);
            playSound('brick');
            shake = 6;
          } else {
            // Glancing hit (hard brick)
            playSound('brick');
            spawnText(b.x + b.w/2, b.y, 'CRACK!', false);
            shake = 3;
          }

          // Resolve overlap side for realistic bounce
          const overlapX = (ball.x + ball.r) - b.x;
          const overlapY = (ball.y + ball.r) - b.y;
          const overlapRight = (b.x + b.w) - (ball.x - ball.r);
          const overlapBottom = (b.y + b.h) - (ball.y - ball.r);
          const minOverlap = Math.min(overlapX, overlapRight, overlapY, overlapBottom);

          if (minOverlap === overlapX || minOverlap === overlapRight) {
            ball.dx *= -1;
          } else {
            ball.dy *= -1;
          }

          // Add slight chaos for juice
          ball.dx += (Math.random() - 0.5) * 1.5;
          ball.dy += (Math.random() - 0.5) * 1.5;

          // Prevent near-horizontal or near-stationary
          if (Math.abs(ball.dy) < 1.5) ball.dy = ball.dy > 0 ? 2 : -2;
          let sp = Math.hypot(ball.dx, ball.dy);
          if (sp > 10) { ball.dx = (ball.dx / sp) * 9; ball.dy = (ball.dy / sp) * 9; }
          if (sp < 4) { ball.dx = (ball.dx / sp) * 4; ball.dy = (ball.dy / sp) * 4; }

          document.getElementById('s').textContent = score;
          break;
        }
      }

      // Check win condition
      const remaining = bricks.filter(b => b.active).length;
      if (remaining === 0) {
        state = 'LEVEL_CLEAR';
        combo = 0;
        comboTimer = 0;
        playSound('level');
        document.getElementById('l').textContent = level;
        shake = 0;
        return;
      }
    }

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

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

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

  // Shake decay
  if (shake > 0) shake *= 0.88;
  if (shake < 0.5) shake = 0;
}

function draw() {
  // Background
  const grad = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, 800);
  grad.addColorStop(0, '#0d0d22');
  grad.addColorStop(0.6, '#080818');
  grad.addColorStop(1, '#020208');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);

  // Stars
  for (const s of stars) {
    ctx.fillStyle = `rgba(255,255,255,${s.b * 0.6})`;
    ctx.fillRect(s.x, s.y, s.s, s.s);
  }

  // Shake offset
  let sx = 0, sy = 0;
  if (shake > 0) {
    sx = (Math.random() - 0.5) * shake * 2.5;
    sy = (Math.random() - 0.5) * shake * 2.5;
  }

  ctx.save();
  ctx.translate(sx, sy);

  // Levels / ready message
  if (state === 'READY') {
    ctx.fillStyle = 'rgba(255,255,255,0.08)';
    ctx.font = 'bold 22px system-ui, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('CLICK OR PRESS SPACE TO LAUNCH', W/2, H/2 + 60);
  }

  // Bricks
  for (const b of bricks) {
    if (!b.active) continue;
    ctx.save();
    ctx.shadowColor = b.color;
    ctx.shadowBlur = 12;
    ctx.fillStyle = b.color;
    // Rounded rect with inner highlight
    roundRect(ctx, b.x, b.y, b.w, b.h, 4);
    ctx.fill();
    ctx.restore();

    // Inner bevel
    ctx.save();
    ctx.strokeStyle = 'rgba(255,255,255,0.25)';
    ctx.lineWidth = 1.5;
    roundRect(ctx, b.x + 1, b.y + 1, b.w - 2, b.h - 2, 2);
    ctx.stroke();
    ctx.restore();
  }

  // Paddle
  ctx.save();
  ctx.shadowColor = '#00f0ff';
  ctx.shadowBlur = 18;
  ctx.fillStyle = '#00f0ff';
  roundRect(ctx, paddle.x, paddle.y, paddle.w, paddle.h, 7);
  ctx.fill();
  // Inner shine
  ctx.fillStyle = 'rgba(255,255,255,0.35)';
  roundRect(ctx, paddle.x + 3, paddle.y + 3, paddle.w - 6, 5, 4);
  ctx.fill();
  ctx.restore();

  // Ball trail
  for (let i = 0; i < ball.trail.length; i++) {
    const t = ball.trail[i];
    const alpha = (i + 1) / ball.trail.length * 0.35;
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.shadowColor = '#00f0ff';
    ctx.shadowBlur = 8;
    ctx.fillStyle = '#00f0ff';
    ctx.beginPath();
    ctx.arc(t.x, t.y, ball.r * 0.6, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  // Ball
  ctx.save();
  ctx.shadowColor = '#ffffff';
  ctx.shadowBlur = 20;
  ctx.fillStyle = '#ffffff';
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
  ctx.fill();
  ctx.restore();

  // Particles
  for (const p of particles) {
    ctx.save();
    ctx.globalAlpha = Math.max(0, p.life / 40);
    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();
  }

  // Floating texts
  for (const t of floatTexts) {
    ctx.save();
    ctx.globalAlpha = Math.max(0, t.life / 70);
    ctx.font = `bold ${t.size}px system-ui, sans-serif`;
    ctx.textAlign = 'center';
    ctx.fillStyle = t.size > 20 ? '#ffee00' : '#ffffff';
    ctx.shadowColor = t.size > 20 ? '#ffee00' : '#ffffff';
    ctx.shadowBlur = 10;
    ctx.fillText(t.text, t.x, t.y);
    ctx.restore();
  }

  // HUD overlay inside canvas for consistency
  ctx.save();
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#e8f8ff';
  ctx.font = 'bold 14px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('SCORE ' + score, 20, 30);
  ctx.textAlign = 'center';
  ctx.fillText('LEVEL ' + level + ' / 5', W/2, 30);
  ctx.textAlign = 'right';
  ctx.fillText('LIVES ' + lives, W - 20, 30);
  ctx.restore();

  // Center messages
  if (state === 'MENU') {
    drawMenu('NEON BREAKOUT', 'Mouse / Arrow Keys / Space • Click to Play', '5 Levels • Score • Juice');
  } else if (state === 'GAME_OVER') {
    drawMenu('GAME OVER', 'Score: ' + score, 'Click or Space to Restart');
  } else if (state === 'WON') {
    drawMenu('YOU WIN!', 'Score: ' + score, 'Thanks for playing! Click to restart');
  } else if (state === 'LEVEL_CLEAR') {
    drawMenu('LEVEL CLEAR!', 'Score: ' + score, 'Click or Space for Level ' + (level + 1));
  }

  ctx.restore();
}

function roundRect(cx, x, y, w, h, r) {
  cx.beginPath();
  cx.moveTo(x + r, y);
  cx.lineTo(x + w - r, y);
  cx.quadraticCurveTo(x + w, y, x + w, y + r);
  cx.lineTo(x + w, y + h - r);
  cx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  cx.lineTo(x + r, y + h);
  cx.quadraticCurveTo(x, y + h, x, y + h - r);
  cx.lineTo(x, y + r);
  cx.quadraticCurveTo(x, y, x + r, y);
  cx.closePath();
}

function drawMenu(title, sub, small) {
  ctx.save();
  ctx.fillStyle = 'rgba(0,0,0,0.55)';
  roundRect(ctx, W/2 - 280, 140, 560, 300, 16);
  ctx.fill();

  ctx.textAlign = 'center';
  ctx.shadowColor = '#00f0ff';
  ctx.shadowBlur = 25;
  ctx.fillStyle = '#00f0ff';
  ctx.font = 'bold 54px system-ui, sans-serif';
  ctx.fillText(title, W/2, 240);

  ctx.shadowBlur = 8;
  ctx.fillStyle = '#ffffff';
  ctx.font = '20px system-ui, sans-serif';
  ctx.fillText(sub, W/2, 290);

  ctx.fillStyle = '#aaa';
  ctx.font = '14px system-ui, sans-serif';
  if (small) ctx.fillText(small, W/2, 330);
  ctx.restore();
}

// Interaction handlers
const keys = {};
window.addEventListener('keydown', (e) => {
  keys[e.key] = true;
  if (e.key === ' ' || e.key === 'Enter') {
    e.preventDefault();
    handleAction();
  }
});
window.addEventListener('keyup', (e) => keys[e.key] = false);

canvas.addEventListener('mousemove', (e) => {
  const pos = getMousePos(e);
  paddle.x = pos.x - paddle.w/2;
  paddle.x = Math.max(12, Math.min(W - paddle.w - 12, paddle.x));
  if (ball.stuck && (state === 'READY' || state === 'PLAYING')) {
    ball.x = paddle.x + paddle.w/2;
    ball.y = paddle.y - ball.r - 2;
  }
});

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  if (e.touches.length) {
    const pos = getMousePos(e.touches[0]);
    paddle.x = pos.x - paddle.w/2;
    paddle.x = Math.max(12, Math.min(W - paddle.w - 12, paddle.x));
    if (ball.stuck && (state === 'READY' || state === 'PLAYING')) {
      ball.x = paddle.x + paddle.w/2;
      ball.y = paddle.y - ball.r - 2;
    }
  }
}, { passive: false });

canvas.addEventListener('click', () => handleAction());

function handleAction() {
  initAudio();
  if (state === 'MENU') {
    resetGame();
    state = 'READY';
  } else if (state === 'READY') {
    state = 'PLAYING';
    ball.stuck = false;
    ball.dx = 4.5 + (Math.random() * 2 - 1) * 2;
    ball.dy = -7.5 - Math.random() * 1.5;
    if (Math.random() > 0.5) ball.dx *= -1;
  } else if (state === 'LEVEL_CLEAR') {
    level++;
    if (level > 5) {
      state = 'WON';
    } else {
      buildLevel(level);
      ball.stuck = true;
      ball.dx = 0; ball.dy = 0;
      ball.x = W/2;
      ball.y = 520;
      document.getElementById('l').textContent = level;
      state = 'READY';
    }
  } else if (state === 'GAME_OVER' || state === 'WON') {
    resetGame();
    state = 'READY';
  }
}

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

resetGame();
loop();
</script>
</body>
</html>

Compare with

Inkling: other attempts