hypeorcode
Network offOpen alone (new tab)

Run details

Model
moonshotai/kimi-k3
Route
Command Code CLI
Reasoning
provider default
Limits
60 min
Date
2 Oct 2026
Time
13 min 13 s
Size
24 kB
Tokens
271,731 in, 21,970 out
List price
$0.33 ($0.239 in / $11.94 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

24 kB 851 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>Breakout</title>
<style>
  html, body {
    margin: 0;
    height: 100%;
    background: #05070f;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  #wrap {
    width: min(96vw, calc(96vh * 4 / 3));
    aspect-ratio: 4 / 3;
    box-shadow: 0 0 60px rgba(84, 160, 255, 0.25), 0 0 0 1px #1c2440;
    border-radius: 10px;
    overflow: hidden;
  }
  canvas {
    display: block;
    width: 100%;
    height: 100%;
    cursor: none;
    touch-action: none;
  }
</style>
</head>
<body>
<div id="wrap"><canvas id="game"></canvas></div>
<script>
'use strict';

const cvs = document.getElementById('game');
const ctx = cvs.getContext('2d');
const W = 800, H = 600;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
cvs.width = W * dpr;
cvs.height = H * dpr;
ctx.scale(dpr, dpr);

const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const rand = (a, b) => a + Math.random() * (b - a);

let AC = null;
let muted = false;
function audio() {
  if (!AC) {
    try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { AC = null; }
  }
  if (AC && AC.state === 'suspended') AC.resume();
  return AC;
}
function tone(f0, f1, dur, type, vol, delay) {
  if (muted) return;
  const ac = audio();
  if (!ac) return;
  const t = ac.currentTime + (delay || 0);
  const o = ac.createOscillator();
  const g = ac.createGain();
  o.type = type;
  o.frequency.setValueAtTime(Math.max(1, f0), t);
  if (f1 !== f0) o.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t + dur);
  g.gain.setValueAtTime(vol, t);
  g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
  o.connect(g).connect(ac.destination);
  o.start(t);
  o.stop(t + dur + 0.03);
}
function noiseBurst(dur, vol) {
  if (muted) return;
  const ac = audio();
  if (!ac) return;
  const n = Math.floor(ac.sampleRate * dur);
  const buf = ac.createBuffer(1, n, ac.sampleRate);
  const d = buf.getChannelData(0);
  for (let i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / n);
  const s = ac.createBufferSource();
  s.buffer = buf;
  const f = ac.createBiquadFilter();
  f.type = 'lowpass';
  f.frequency.value = 1600;
  const g = ac.createGain();
  g.gain.value = vol;
  s.connect(f).connect(g).connect(ac.destination);
  s.start();
}
const sfx = {
  paddle() { tone(210, 140, 0.08, 'square', 0.16); },
  wall()   { tone(320, 290, 0.05, 'square', 0.07); },
  chip(c)  { tone(340 + c * 25, 290, 0.06, 'triangle', 0.13); },
  brk(c)   { noiseBurst(0.12, 0.22); tone(480 + c * 35, 190, 0.12, 'triangle', 0.12); },
  power()  { tone(420, 940, 0.16, 'sine', 0.2); },
  lose()   { tone(280, 60, 0.5, 'sawtooth', 0.22); },
  clear()  { [440, 554, 659, 880].forEach((f, i) => tone(f, f, 0.12, 'sine', 0.18, i * 0.09)); },
  over()   { [300, 240, 180, 120].forEach((f, i) => tone(f, f * 0.8, 0.25, 'sawtooth', 0.16, i * 0.16)); },
  win()    { [523, 659, 784, 1046, 1318].forEach((f, i) => tone(f, f, 0.16, 'triangle', 0.18, i * 0.1)); }
};

const COLORS = { r: '#ff5b5b', o: '#ff9f43', y: '#feca57', g: '#1dd1a1', b: '#54a0ff', p: '#c56cf0', s: '#a4b0be' };
const LEVELS = [
  { name: 'WARM UP', rows: [
    'rrrrrrrrrrrr',
    'oooooooooooo',
    'yyyyyyyyyyyy',
    'gggggggggggg',
    'bbbbbbbbbbbb',
    'pppppppppppp'
  ]},
  { name: 'CHECKERBOARD', rows: [
    'r.r.r.r.r.r.',
    '.o.o.o.o.o.o',
    'y.y.y.y.y.y.',
    '.g.g.g.g.g.g',
    'b.b.b.b.b.b.',
    '.p.p.p.p.p.p'
  ]},
  { name: 'PYRAMID', rows: [
    '.....pp.....',
    '....bbbb....',
    '...gggggg...',
    '..yyyyyyyy..',
    '.oooooooooo.',
    'rrrrrrrrrrrr'
  ]},
  { name: 'REINFORCED', rows: [
    'RRRRRRRRRRRR',
    'ROOOOOOOOOOR',
    'RYYYYYYYYYYR',
    '..GGGGGGGG..',
    '..BBBBBBBB..',
    '..PPPPPPPP..'
  ]},
  { name: 'FORTRESS', rows: [
    '#RRRRRRRRRR#',
    '#OOOOOOOOOO#',
    '#YYYYYYYYYY#',
    '....GGGG....',
    '...g....g...',
    '..g......g..'
  ]}
];

const COLS = 12, GAP = 6, SIDE = 24, TOP = 92;
const BW = (W - SIDE * 2 - GAP * (COLS - 1)) / COLS;
const BH = 22;

const state = {
  mode: 'menu',
  score: 0,
  lives: 3,
  level: 0,
  combo: 0,
  spdMul: 1,
  bannerT: 0,
  shake: 0,
  flash: 0,
  paused: false,
  t: 0
};

const paddle = { x: W / 2, y: H - 36, w: 110, baseW: 110, h: 14, vx: 0, expandT: 0, slowT: 0 };
let balls = [];
let bricks = [];
let particles = [];
let popups = [];
let powerups = [];
let stars = [];
for (let i = 0; i < 60; i++) stars.push({ x: rand(0, W), y: rand(0, H), s: rand(0.5, 1.8), ph: rand(0, 6.28), vy: rand(4, 14) });

function speedFor(lv) { return Math.min(380 + lv * 35, 620) * state.spdMul; }

function buildLevel(idx) {
  bricks = [];
  const rows = LEVELS[idx].rows;
  for (let r = 0; r < rows.length; r++) {
    for (let c = 0; c < COLS; c++) {
      const ch = rows[r][c];
      if (ch === '.') continue;
      let key, hp;
      if (ch === '#') { key = 's'; hp = 3; }
      else if (ch >= 'A' && ch <= 'Z') { key = ch.toLowerCase(); hp = 2; }
      else { key = ch; hp = 1; }
      bricks.push({
        x: SIDE + c * (BW + GAP),
        y: TOP + r * (BH + GAP),
        w: BW, h: BH,
        hp, maxHp: hp,
        color: COLORS[key],
        alive: true,
        wob: 0
      });
    }
  }
}

function resetBall() {
  balls = [{ x: paddle.x, y: paddle.y - 10, vx: 0, vy: 0, r: 8, stuck: true, trail: [] }];
}

function launch() {
  let launched = false;
  for (const b of balls) {
    if (!b.stuck) continue;
    const a = rand(-0.35, 0.35);
    const sp = speedFor(state.level);
    b.vx = Math.sin(a) * sp;
    b.vy = -Math.cos(a) * sp;
    b.stuck = false;
    launched = true;
  }
  if (launched) tone(500, 700, 0.08, 'sine', 0.15);
}

function startGame() {
  state.score = 0;
  state.lives = 3;
  state.level = 0;
  state.combo = 0;
  state.spdMul = 1;
  state.mode = 'play';
  state.paused = false;
  particles = [];
  popups = [];
  powerups = [];
  paddle.expandT = 0;
  paddle.slowT = 0;
  paddle.w = paddle.baseW;
  buildLevel(0);
  resetBall();
}

function nextLevel() {
  state.level++;
  state.spdMul = 1;
  state.combo = 0;
  paddle.expandT = 0;
  paddle.slowT = 0;
  powerups = [];
  buildLevel(state.level);
  resetBall();
  state.mode = 'play';
}

function rr(x, y, w, h, r) {
  r = Math.min(r, w / 2, 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 spawnBreak(br) {
  for (let i = 0; i < 14; i++) {
    particles.push({
      x: br.x + rand(0, br.w), y: br.y + rand(0, br.h),
      vx: rand(-220, 220), vy: rand(-260, 60),
      life: rand(0.4, 0.85), maxLife: 0.85,
      size: rand(2, 5), color: br.color, rot: rand(0, 6.28), vr: rand(-10, 10)
    });
  }
}
function spawnChips(br) {
  for (let i = 0; i < 5; i++) {
    particles.push({
      x: br.x + rand(0, br.w), y: br.y + rand(0, br.h),
      vx: rand(-140, 140), vy: rand(-160, 40),
      life: rand(0.25, 0.5), maxLife: 0.5,
      size: rand(1.5, 3.5), color: br.color, rot: rand(0, 6.28), vr: rand(-8, 8)
    });
  }
}
function spawnSparks(x, y) {
  for (let i = 0; i < 8; i++) {
    particles.push({
      x, y, vx: rand(-200, 200), vy: rand(-240, -40),
      life: rand(0.2, 0.4), maxLife: 0.4,
      size: rand(1, 2.5), color: '#ffffff', rot: 0, vr: 0
    });
  }
}

function comboMult() { return Math.min(Math.max(1, state.combo), 10); }

function maybeDrop(br) {
  if (Math.random() > 0.16) return;
  const roll = Math.random();
  const type = roll < 0.4 ? 'E' : roll < 0.7 ? 'M' : 'S';
  powerups.push({ x: br.x + br.w / 2, y: br.y + br.h / 2, vy: 130, type });
}

function hitBrick(br) {
  br.hp--;
  br.wob = 1;
  state.shake = Math.min(state.shake + 2.5, 9);
  if (br.hp <= 0) {
    br.alive = false;
    state.combo++;
    const pts = 10 * br.maxHp * comboMult();
    state.score += pts;
    popups.push({ x: br.x + br.w / 2, y: br.y, text: '+' + pts, life: 1, color: br.color, size: 15 });
    if (state.combo >= 3) {
      popups.push({ x: br.x + br.w / 2, y: br.y + 22, text: 'COMBO x' + comboMult(), life: 0.9, color: '#ffffff', size: 12 });
    }
    spawnBreak(br);
    sfx.brk(state.combo);
    maybeDrop(br);
    state.spdMul = Math.min(state.spdMul + 0.008, 1.4);
    rescaleBalls();
    if (bricks.every(b => !b.alive)) onLevelClear();
  } else {
    sfx.chip(state.combo);
    spawnChips(br);
  }
}

function rescaleBalls() {
  const sp = speedFor(state.level);
  for (const b of balls) {
    if (b.stuck) continue;
    const cur = Math.hypot(b.vx, b.vy) || 1;
    b.vx = b.vx / cur * sp;
    b.vy = b.vy / cur * sp;
  }
}

function onLevelClear() {
  const bonus = 100 * (state.level + 1);
  state.score += bonus;
  popups.push({ x: W / 2, y: H / 2 - 60, text: 'BONUS +' + bonus, life: 1.6, color: '#feca57', size: 24 });
  if (state.level >= LEVELS.length - 1) {
    state.mode = 'win';
    sfx.win();
  } else {
    state.mode = 'clear';
    state.bannerT = 2.0;
    sfx.clear();
  }
}

function applyPowerup(p) {
  sfx.power();
  if (p.type === 'E') {
    paddle.expandT = 12;
    popups.push({ x: paddle.x, y: paddle.y - 24, text: 'EXPAND!', life: 1, color: '#1dd1a1', size: 16 });
  } else if (p.type === 'S') {
    paddle.slowT = 9;
    popups.push({ x: paddle.x, y: paddle.y - 24, text: 'SLOW-MO!', life: 1, color: '#feca57', size: 16 });
  } else if (p.type === 'M') {
    const src = balls.find(b => !b.stuck);
    if (src) {
      for (const off of [-0.45, 0.45]) {
        if (balls.length >= 4) break;
        const sp = Math.hypot(src.vx, src.vy);
        const ang = Math.atan2(src.vy, src.vx) + off;
        balls.push({ x: src.x, y: src.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp, r: 8, stuck: false, trail: [] });
      }
      popups.push({ x: paddle.x, y: paddle.y - 24, text: 'MULTIBALL!', life: 1, color: '#54a0ff', size: 16 });
    }
  }
}

function loseLife() {
  state.lives--;
  state.combo = 0;
  state.flash = 1;
  state.shake = 14;
  paddle.expandT = 0;
  paddle.slowT = 0;
  powerups = [];
  sfx.lose();
  if (state.lives <= 0) {
    state.mode = 'over';
    sfx.over();
  } else {
    resetBall();
  }
}

function collideBallBricks(b) {
  for (const br of bricks) {
    if (!br.alive) continue;
    const cx = clamp(b.x, br.x, br.x + br.w);
    const cy = clamp(b.y, br.y, br.y + br.h);
    const dx = b.x - cx, dy = b.y - cy;
    const d2 = dx * dx + dy * dy;
    if (d2 > b.r * b.r) continue;
    let nx, ny;
    const d = Math.sqrt(d2);
    if (d > 0.0001) { nx = dx / d; ny = dy / d; }
    else {
      const ox = Math.min(b.x - br.x, br.x + br.w - b.x);
      const oy = Math.min(b.y - br.y, br.y + br.h - b.y);
      if (ox < oy) { nx = (b.x < br.x + br.w / 2) ? -1 : 1; ny = 0; }
      else { nx = 0; ny = (b.y < br.y + br.h / 2) ? -1 : 1; }
    }
    const dot = b.vx * nx + b.vy * ny;
    if (dot < 0) {
      b.vx -= 2 * dot * nx;
      b.vy -= 2 * dot * ny;
    }
    b.x += nx * (b.r - d + 0.5);
    b.y += ny * (b.r - d + 0.5);
    hitBrick(br);
    return;
  }
}

const keys = {};
let mouseX = null;

addEventListener('keydown', e => {
  if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' '].includes(e.key)) e.preventDefault();
  const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
  keys[k] = true;
  if (e.repeat) return;
  if (k === 'm') { muted = !muted; return; }
  if (k === 'p' && state.mode === 'play') { state.paused = !state.paused; return; }
  if (k === ' ' || k === 'ArrowUp' || k === 'Enter') userAction();
});
addEventListener('keyup', e => {
  const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
  keys[k] = false;
});
cvs.addEventListener('mousemove', e => {
  const r = cvs.getBoundingClientRect();
  mouseX = (e.clientX - r.left) / r.width * W;
});
cvs.addEventListener('mousedown', e => { e.preventDefault(); userAction(); });
cvs.addEventListener('touchstart', e => { e.preventDefault(); userAction(); }, { passive: false });
cvs.addEventListener('touchmove', e => {
  e.preventDefault();
  const r = cvs.getBoundingClientRect();
  mouseX = (e.touches[0].clientX - r.left) / r.width * W;
}, { passive: false });
document.addEventListener('visibilitychange', () => {
  if (document.hidden && state.mode === 'play') state.paused = true;
});

function userAction() {
  audio();
  if (state.mode === 'menu') startGame();
  else if (state.mode === 'over' || state.mode === 'win') { state.mode = 'menu'; }
  else if (state.mode === 'play' && !state.paused) launch();
}

function update(dt) {
  state.t += dt;
  state.shake = Math.max(0, state.shake - 30 * dt);
  state.flash = Math.max(0, state.flash - 2.2 * dt);

  for (const s of stars) {
    s.y += s.vy * dt;
    if (s.y > H + 4) { s.y = -4; s.x = rand(0, W); }
  }

  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.vy += 900 * dt;
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.rot += p.vr * dt;
    p.life -= dt;
    if (p.life <= 0 || p.y > H + 20) particles.splice(i, 1);
  }
  for (let i = popups.length - 1; i >= 0; i--) {
    const p = popups[i];
    p.y -= 42 * dt;
    p.life -= dt;
    if (p.life <= 0) popups.splice(i, 1);
  }
  for (const br of bricks) br.wob = Math.max(0, br.wob - 6 * dt);

  if (state.mode === 'clear') {
    state.bannerT -= dt;
    if (state.bannerT <= 0) nextLevel();
    return;
  }
  if (state.mode !== 'play' || state.paused) return;

  paddle.expandT = Math.max(0, paddle.expandT - dt);
  paddle.slowT = Math.max(0, paddle.slowT - dt);
  const targetW = paddle.expandT > 0 ? paddle.baseW * 1.45 : paddle.baseW;
  paddle.w += (targetW - paddle.w) * Math.min(1, dt * 10);

  const oldX = paddle.x;
  const kb = ((keys['ArrowRight'] || keys['d']) ? 1 : 0) - ((keys['ArrowLeft'] || keys['a']) ? 1 : 0);
  if (kb !== 0) {
    paddle.x += kb * 660 * dt;
    mouseX = paddle.x;
  } else if (mouseX != null) {
    paddle.x += (mouseX - paddle.x) * Math.min(1, dt * 22);
  }
  paddle.x = clamp(paddle.x, paddle.w / 2 + 6, W - paddle.w / 2 - 6);
  paddle.vx = dt > 0 ? (paddle.x - oldX) / dt : 0;

  const slowScale = paddle.slowT > 0 ? 0.72 : 1;

  for (const b of balls) {
    if (b.stuck) {
      b.x = paddle.x;
      b.y = paddle.y - b.r - 2;
      continue;
    }
    b.trail.push({ x: b.x, y: b.y });
    if (b.trail.length > 10) b.trail.shift();

    b.x += b.vx * slowScale * dt;
    b.y += b.vy * slowScale * dt;

    if (b.x < b.r + 4) { b.x = b.r + 4; b.vx = Math.abs(b.vx); sfx.wall(); }
    if (b.x > W - b.r - 4) { b.x = W - b.r - 4; b.vx = -Math.abs(b.vx); sfx.wall(); }
    if (b.y < b.r + 4) { b.y = b.r + 4; b.vy = Math.abs(b.vy); sfx.wall(); }

    if (b.vy > 0 &&
        b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h &&
        Math.abs(b.x - paddle.x) <= paddle.w / 2 + b.r) {
      const t = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
      const ang = t * Math.PI / 3;
      const sp = Math.hypot(b.vx, b.vy);
      b.vx = Math.sin(ang) * sp + paddle.vx * 0.12;
      b.vy = -Math.cos(ang) * sp;
      const nsp = Math.hypot(b.vx, b.vy) || 1;
      b.vx = b.vx / nsp * sp;
      b.vy = b.vy / nsp * sp;
      if (b.vy > -sp * 0.3) {
        b.vy = -sp * 0.3;
        b.vx = (b.vx < 0 ? -1 : 1) * Math.sqrt(Math.max(0, sp * sp - b.vy * b.vy));
      }
      b.y = paddle.y - b.r - 0.5;
      state.combo = 0;
      sfx.paddle();
      spawnSparks(b.x, paddle.y);
    }

    collideBallBricks(b);

    const sp = Math.hypot(b.vx, b.vy);
    if (sp > 1 && Math.abs(b.vy) < sp * 0.14) {
      b.vy = (b.vy < 0 ? -1 : 1) * sp * 0.14;
      b.vx = (b.vx < 0 ? -1 : 1) * Math.sqrt(Math.max(0, sp * sp - b.vy * b.vy));
    }
  }

  for (let i = balls.length - 1; i >= 0; i--) {
    if (balls[i].y - balls[i].r > H + 30) balls.splice(i, 1);
  }
  if (balls.length === 0) loseLife();

  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.y += p.vy * dt;
    if (p.y > H + 20) { powerups.splice(i, 1); continue; }
    if (p.y + 10 >= paddle.y && p.y - 10 <= paddle.y + paddle.h &&
        Math.abs(p.x - paddle.x) <= paddle.w / 2 + 12) {
      applyPowerup(p);
      powerups.splice(i, 1);
    }
  }
}

function drawBackground() {
  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, '#0b0e1a');
  g.addColorStop(1, '#141b38');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, W, H);
  for (const s of stars) {
    ctx.globalAlpha = 0.12 + 0.12 * Math.sin(state.t * 2 + s.ph);
    ctx.fillStyle = '#8fb2ff';
    ctx.fillRect(s.x, s.y, s.s, s.s);
  }
  ctx.globalAlpha = 1;
}

function drawBricks() {
  for (const br of bricks) {
    if (!br.alive) continue;
    const wob = br.wob > 0 ? Math.sin(br.wob * 30) * br.wob * 2 : 0;
    const x = br.x + wob, y = br.y;
    const g = ctx.createLinearGradient(0, y, 0, y + br.h);
    g.addColorStop(0, br.color);
    g.addColorStop(1, shade(br.color, -0.35));
    ctx.fillStyle = g;
    rr(x, y, br.w, br.h, 5);
    ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,0.28)';
    rr(x + 3, y + 2.5, br.w - 6, 4, 2);
    ctx.fill();
    if (br.maxHp > 1) {
      const dmg = (br.maxHp - br.hp) / br.maxHp;
      ctx.fillStyle = 'rgba(0,0,0,' + (0.12 + dmg * 0.4).toFixed(2) + ')';
      rr(x, y, br.w, br.h, 5);
      ctx.fill();
      ctx.strokeStyle = 'rgba(255,255,255,0.35)';
      ctx.lineWidth = 1.5;
      rr(x + 3.5, y + 3.5, br.w - 7, br.h - 7, 3);
      ctx.stroke();
      if (br.hp < br.maxHp) {
        ctx.strokeStyle = 'rgba(0,0,0,0.5)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(x + br.w * 0.3, y + 3);
        ctx.lineTo(x + br.w * 0.45, y + br.h - 4);
        ctx.moveTo(x + br.w * 0.65, y + 4);
        ctx.lineTo(x + br.w * 0.55, y + br.h - 3);
        ctx.stroke();
      }
    }
  }
}

function shade(hex, amt) {
  const n = parseInt(hex.slice(1), 16);
  const r = clamp(((n >> 16) & 255) * (1 + amt), 0, 255);
  const g = clamp(((n >> 8) & 255) * (1 + amt), 0, 255);
  const b = clamp((n & 255) * (1 + amt), 0, 255);
  return 'rgb(' + (r | 0) + ',' + (g | 0) + ',' + (b | 0) + ')';
}

function drawPaddle() {
  const { x, y, w, h } = paddle;
  ctx.save();
  ctx.shadowColor = paddle.expandT > 0 ? '#1dd1a1' : '#54a0ff';
  ctx.shadowBlur = 14;
  const g = ctx.createLinearGradient(0, y, 0, y + h);
  g.addColorStop(0, '#eaf2ff');
  g.addColorStop(0.5, '#9db8e8');
  g.addColorStop(1, '#5f7fc0');
  ctx.fillStyle = g;
  rr(x - w / 2, y, w, h, 7);
  ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  rr(x - w / 2 + 6, y + 2, w - 12, 3.5, 2);
  ctx.fill();
  ctx.restore();
}

function drawBalls() {
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const tp = b.trail[i];
      const a = (i + 1) / b.trail.length;
      ctx.globalAlpha = a * 0.22;
      ctx.fillStyle = '#8fd0ff';
      ctx.beginPath();
      ctx.arc(tp.x, tp.y, b.r * a, 0, 6.283);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.save();
    ctx.translate(b.x, b.y);
    if (!b.stuck) {
      const ang = Math.atan2(b.vy, b.vx);
      ctx.rotate(ang);
      ctx.scale(1.12, 0.88);
      ctx.rotate(-ang);
    }
    ctx.shadowColor = '#9fdcff';
    ctx.shadowBlur = 16;
    const g = ctx.createRadialGradient(-2.5, -2.5, 1, 0, 0, b.r);
    g.addColorStop(0, '#ffffff');
    g.addColorStop(1, '#7ec8ff');
    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.arc(0, 0, b.r, 0, 6.283);
    ctx.fill();
    ctx.restore();
  }
}

function drawParticles() {
  for (const p of particles) {
    ctx.globalAlpha = clamp(p.life / p.maxLife, 0, 1);
    ctx.fillStyle = p.color;
    ctx.save();
    ctx.translate(p.x, p.y);
    ctx.rotate(p.rot);
    ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
    ctx.restore();
  }
  ctx.globalAlpha = 1;
}

function drawPopups() {
  ctx.textAlign = 'center';
  for (const p of popups) {
    ctx.globalAlpha = clamp(p.life, 0, 1);
    ctx.fillStyle = p.color;
    ctx.font = 'bold ' + p.size + 'px system-ui, sans-serif';
    ctx.fillText(p.text, p.x, p.y);
  }
  ctx.globalAlpha = 1;
}

function drawPowerups() {
  for (const p of powerups) {
    const col = p.type === 'E' ? '#1dd1a1' : p.type === 'M' ? '#54a0ff' : '#feca57';
    ctx.save();
    ctx.shadowColor = col;
    ctx.shadowBlur = 12;
    ctx.fillStyle = '#0d1226';
    rr(p.x - 13, p.y - 10, 26, 20, 5);
    ctx.fill();
    ctx.strokeStyle = col;
    ctx.lineWidth = 2;
    rr(p.x - 13, p.y - 10, 26, 20, 5);
    ctx.stroke();
    ctx.shadowBlur = 0;
    ctx.fillStyle = col;
    ctx.font = 'bold 13px system-ui, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(p.type, p.x, p.y + 4.5);
    ctx.restore();
  }
}

function drawHUD() {
  ctx.fillStyle = 'rgba(6,9,20,0.72)';
  ctx.fillRect(0, 0, W, 44);
  ctx.fillStyle = 'rgba(120,150,220,0.35)';
  ctx.fillRect(0, 44, W, 1);
  ctx.textAlign = 'left';
  ctx.fillStyle = '#dbe6ff';
  ctx.font = 'bold 18px system-ui, sans-serif';
  ctx.fillText('SCORE ' + state.score, 18, 28);
  if (state.combo >= 2) {
    ctx.fillStyle = '#feca57';
    ctx.font = 'bold 14px system-ui, sans-serif';
    ctx.fillText('COMBO x' + comboMult(), 18, 66);
  }
  ctx.textAlign = 'center';
  for (let i = 0; i < 3; i++) {
    ctx.fillStyle = i < state.lives ? '#ff5b7a' : 'rgba(255,91,122,0.18)';
    ctx.font = '20px serif';
    ctx.fillText('♥', W / 2 - 24 + i * 24, 28);
  }
  ctx.textAlign = 'right';
  ctx.fillStyle = '#dbe6ff';
  ctx.font = 'bold 18px system-ui, sans-serif';
  ctx.fillText('LEVEL ' + (state.level + 1) + '/' + LEVELS.length, W - 18, 28);
  if (muted) {
    ctx.fillStyle = 'rgba(219,230,255,0.5)';
    ctx.font = '12px system-ui, sans-serif';
    ctx.fillText('MUTED (M)', W - 18, 66);
  }
  let chipX = W - 18;
  ctx.font = 'bold 12px system-ui, sans-serif';
  if (paddle.expandT > 0) {
    ctx.fillStyle = '#1dd1a1';
    const txt = 'EXPAND ' + Math.ceil(paddle.expandT) + 's';
    ctx.fillText(txt, chipX, 66);
    chipX -= ctx.measureText(txt).width + 16;
  }
  if (paddle.slowT > 0) {
    ctx.fillStyle = '#feca57';
    ctx.fillText('SLOW ' + Math.ceil(paddle.slowT) + 's', chipX, 66);
  }
}

function overlay(alpha) {
  ctx.fillStyle = 'rgba(4,6,14,' + alpha + ')';
  ctx.fillRect(0, 0, W, H);
}
function bigText(txt, y, size, color) {
  ctx.textAlign = 'center';
  ctx.fillStyle = color;
  ctx.font = 'bold ' + size + 'px system-ui, sans-serif';
  ctx.fillText(txt, W / 2, y);
}
function smallText(txt, y, color) {
  ctx.textAlign = 'center';
  ctx.fillStyle = color || 'rgba(219,230,255,0.75)';
  ctx.font = '16px system-ui, sans-serif';
  ctx.fillText(txt, W / 2, y);
}

function draw() {
  ctx.save();
  if (state.shake > 0) ctx.translate(rand(-state.shake, state.shake), rand(-state.shake, state.shake));
  drawBackground();
  drawBricks();
  drawPowerups();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawPopups();
  ctx.restore();

  drawHUD();

  if (state.flash > 0) {
    ctx.fillStyle = 'rgba(255,60,70,' + (state.flash * 0.32).toFixed(3) + ')';
    ctx.fillRect(0, 0, W, H);
  }

  if (state.mode === 'menu') {
    overlay(0.72);
    bigText('BREAKOUT', H / 2 - 70, 64, '#8fd0ff');
    smallText('Mouse or ← → / A D to move the paddle', H / 2 - 10);
    smallText('Click or SPACE to launch · P pause · M mute', H / 2 + 18);
    smallText('5 levels · combos · power-ups (E expand · M multiball · S slow)', H / 2 + 46, 'rgba(254,202,87,0.85)');
    if (Math.sin(state.t * 4) > -0.2) smallText('CLICK OR PRESS SPACE TO START', H / 2 + 100, '#ffffff');
  } else if (state.mode === 'play' && !state.paused && balls.some(b => b.stuck)) {
    if (Math.sin(state.t * 4) > -0.2) smallText('SPACE / CLICK TO LAUNCH', H - 90, 'rgba(255,255,255,0.85)');
  } else if (state.mode === 'clear') {
    overlay(0.55);
    bigText('LEVEL CLEARED!', H / 2 - 30, 48, '#1dd1a1');
    smallText('Next: LEVEL ' + (state.level + 2) + ' — ' + LEVELS[state.level + 1].name, H / 2 + 16);
  } else if (state.mode === 'over') {
    overlay(0.78);
    bigText('GAME OVER', H / 2 - 40, 56, '#ff5b5b');
    smallText('Final score: ' + state.score, H / 2 + 8);
    if (Math.sin(state.t * 4) > -0.2) smallText('CLICK TO CONTINUE', H / 2 + 60, '#ffffff');
  } else if (state.mode === 'win') {
    overlay(0.78);
    bigText('YOU WIN!', H / 2 - 40, 56, '#feca57');
    smallText('All ' + LEVELS.length + ' levels cleared · Final score: ' + state.score, H / 2 + 8);
    if (Math.sin(state.t * 4) > -0.2) smallText('CLICK TO CONTINUE', H / 2 + 60, '#ffffff');
  }

  if (state.paused && state.mode === 'play') {
    overlay(0.6);
    bigText('PAUSED', H / 2, 48, '#dbe6ff');
    smallText('Press P to resume', H / 2 + 36);
  }
}

buildLevel(0);
resetBall();

let last = performance.now();
function frame(now) {
  requestAnimationFrame(frame);
  const dt = Math.min((now - last) / 1000, 0.033);
  last = now;
  update(dt);
  draw();
}
requestAnimationFrame(frame);
</script>
</body>
</html>

Compare with

Kimi K3: other attempts

  • Kimi K3, Watch: Queued · #15 of 15 paid · not before 25 Oct
  • Kimi K3, Logo: Queued · #15 of 15 paid · not before 25 Oct
  • Kimi K3, About: Queued · #15 of 15 paid · not before 25 Oct