Models 44

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
1 min 1 s
Cost
$0.0073
Output tokens
11,987
Spent thinking
0 tokens
Speed
197.8 tok/s
Console errors
0
First token after
769 ms
First visible text after
769 ms
Prompt tokens
692
Code size
33.8 KB
Finish reason
stop
Served by
Fireworks
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"xhigh"}
Max output
64,000 tokens
Generated
Nov 11, 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 · 1056 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}
  html,body{height:100%}
  body{
    background:
      radial-gradient(ellipse 120% 80% at 50% -10%, #1b2a55 0%, #0a0f24 55%, #05070f 100%);
    color:#e9efff;
    font-family:"Segoe UI",system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;
    display:flex;align-items:center;justify-content:center;
    min-height:100vh;overflow:hidden;
    -webkit-user-select:none;user-select:none;
  }
  #wrap{
    position:relative;width:min(96vw,900px);
    display:flex;flex-direction:column;align-items:center;gap:10px;
  }
  header{
    width:100%;display:flex;align-items:center;justify-content:space-between;
    gap:12px;flex-wrap:wrap;padding:0 4px;
  }
  .brand{
    display:flex;align-items:center;gap:10px;
    font-weight:800;letter-spacing:.18em;font-size:clamp(13px,2.2vw,18px);
    background:linear-gradient(90deg,#5ee7ff,#b06cff 60%,#ff7ad9);
    -webkit-background-clip:text;background-clip:text;color:transparent;
    text-shadow:0 0 22px rgba(120,190,255,.25);
  }
  .brand .dot{width:10px;height:10px;border-radius:50%;background:#5ee7ff;box-shadow:0 0 14px #5ee7ff}
  .stats{display:flex;gap:8px;flex-wrap:wrap}
  .stat{
    background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.10);
    border-radius:10px;padding:6px 11px;min-width:74px;text-align:center;
    backdrop-filter:blur(6px);
  }
  .stat .lab{font-size:9px;letter-spacing:.18em;color:#8fa0c8;text-transform:uppercase}
  .stat .val{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.25}
  #stage{
    position:relative;width:100%;
    border-radius:16px;overflow:hidden;
    border:1px solid rgba(120,170,255,.22);
    box-shadow:0 24px 70px rgba(0,0,0,.65),0 0 0 1px rgba(255,255,255,.03) inset,
               0 0 60px rgba(70,120,255,.10);
    background:#060a16;
  }
  canvas{display:block;width:100%;height:auto;touch-action:none}
  footer{
    display:flex;gap:14px;flex-wrap:wrap;justify-content:center;align-items:center;
    font-size:11px;color:#7d8db5;letter-spacing:.06em;
  }
  kbd{
    background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);
    border-bottom-width:2px;border-radius:5px;padding:2px 6px;font-size:10px;
    font-family:inherit;color:#cfe0ff;
  }
  .hint{color:#8b9dc7}
  @media (max-width:520px){
    .stat{min-width:62px;padding:5px 8px}
    .stat .val{font-size:15px}
    footer{font-size:10px;gap:9px}
  }
</style>
</head>
<body>
<div id="wrap">
  <header>
    <div class="brand"><span class="dot"></span>NEON&nbsp;BREAKOUT</div>
    <div class="stats">
      <div class="stat"><div class="lab">Score</div><div class="val" id="score">0</div></div>
      <div class="stat"><div class="lab">Lives</div><div class="val" id="lives">♥♥♥</div></div>
      <div class="stat"><div class="lab">Level</div><div class="val" id="level">1</div></div>
      <div class="stat"><div class="lab">Bricks</div><div class="val" id="left">0</div></div>
      <div class="stat"><div class="lab">Best</div><div class="val" id="best">0</div></div>
    </div>
  </header>
  <div id="stage"><canvas id="cv"></canvas></div>
  <footer>
    <span><kbd>←</kbd><kbd>→</kbd> / mouse to move</span>
    <span class="hint">Space = launch &amp; pause</span>
    <span class="hint">Catch power-ups: wide paddle (10s) or multi-ball</span>
  </footer>
</div>
<script>
(() => {
'use strict';

/* ---------------- Canvas setup ---------------- */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const stage = document.getElementById('stage');

// Logical resolution
const W = 900, H = 620;
let dpr = Math.min(window.devicePixelRatio || 1, 2);

function resize(){
  const rect = stage.getBoundingClientRect();
  const cssW = Math.max(300, Math.floor(rect.width));
  const cssH = Math.round(cssW * H / W);
  stage.style.height = cssH + 'px';
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  cv.width = Math.floor(cssW * dpr);
  cv.height = Math.floor(cssH * dpr);
  cv.style.width = cssW + 'px';
  cv.style.height = cssH + 'px';
  ctx.setTransform(dpr * cssW / W, 0, 0, dpr * cssW / W, 0, 0);
}
window.addEventListener('resize', resize);

const COLS = 13, ROWS = 8;
const TOP = 62;
const SIDE = 26;
const brickAreaW = W - SIDE * 2;
const BW = brickAreaW / COLS;     // brick width (cell)
const BH = 26;                   // brick height
const GAP = 6;

/* ---------------- Game state ---------------- */
const state = {
  screen: 'title', // title | play | over | win
  score: 0,
  lives: 3,
  bricksLeft: 0,
  balls: [],
  powerups: [],
  particles: [],
  shake: 0,
  wideTimer: 0,
  flash: 0,
  t: 0,
  messageTimer: 0,
  message: ''
};
const paddle = {
  x: W/2, y: H - 46, w: 110, h: 14,
  targetX: W/2, speed: 0, glow: 0
};
const BASE_PADDLE_W = 110;

let bricks = [];
let keys = {left:false, right:false};
let mouseActive = false;
let best = 0;
try { best = parseInt(localStorage.getItem('nb_best') || '0', 10) || 0; } catch(e){ best = 0; }
const el = {
  score: document.getElementById('score'),
  lives: document.getElementById('lives'),
  level: document.getElementById('level'),
  left: document.getElementById('left'),
  best: document.getElementById('best')
};

/* ---------------- Level generation ---------------- */
const NORMAL_COLORS = ['#5ee7ff','#63b3ff','#8f7bff','#c56cff','#ff6fd8','#ff7a9c'];
const HARD_COLORS = ['#ffb347','#ff8a3d'];

function makeLevel(){
  bricks = [];
  let count = 0;
  // Layout: 8 rows of 13. Rows 0-1 fully filled, then a decorative pattern.
  for (let r = 0; r < ROWS; r++){
    for (let c = 0; c < COLS; c++){
      let exists = false;
      if (r <= 1) exists = true;
      else {
        // wave pattern in lower rows
        const wave = Math.abs(Math.sin((c + r*1.7) * 0.55));
        exists = wave > 0.34 || (r === 7 && c % 3 === 0);
      }
      if (!exists) continue;

      // tough bricks: some in lower-middle & edges
      let tough = (r >= 3 && ((c + r) % 4 === 0)) || (r === ROWS-1 && c % 2 === 0);
      // keep some tough bricks but ensure >= 20 normal bricks
      let hasPower = false;
      if (!tough && ((c * 7 + r * 3) % 11 === 0) || (tough && (c*3+r) % 17 === 0)){
        hasPower = true;
      }
      const x = SIDE + c * BW + GAP/2;
      const y = TOP + r * (BH + GAP);
      const w = BW - GAP;
      const h = BH;
      bricks.push({
        x, y, w, h,
        hp: tough ? 2 : 1,
        maxHp: tough ? 2 : 1,
        tough,
        hasPower,
        color: tough ? HARD_COLORS[(c+r)%HARD_COLORS.length] : NORMAL_COLORS[(r*2+c)%NORMAL_COLORS.length],
        alive: true,
        crack: 0,
        pulse: Math.random()*Math.PI*2,
        hitFlash: 0
      });
      count++;
    }
  }
  // Guarantee at least 6 rows of bricks exist
  for (let r = 0; r < 6; r++){
    const hasRow = bricks.some(b => b.alive && Math.abs((b.y - TOP)/(BH+GAP) - r) < 0.4);
    if (!hasRow){
      for (let c = 0; c < COLS; c += 2){
        const x = SIDE + c * BW + GAP/2;
        const y = TOP + r * (BH + GAP);
        bricks.push({
          x, y, w: BW - GAP, h: BH, hp:1, maxHp:1, tough:false,
          hasPower:(c+r)%5===0, color:NORMAL_COLORS[r%NORMAL_COLORS.length],
          alive:true, crack:0, pulse:Math.random()*Math.PI*2, hitFlash:0
        });
      }
    }
  }
  state.bricksLeft = bricks.filter(b=>b.alive).length;
}

function refreshHUD(){
  el.score.textContent = state.score;
  el.lives.textContent = state.lives > 0 ? '♥'.repeat(state.lives) : '—';
  el.lives.style.color = state.lives <= 1 ? '#ff6b8a' : '#ff8fb0';
  el.level.textContent = '1';
  el.left.textContent = state.bricksLeft;
  el.best.textContent = Math.max(best, state.score);
}

/* ---------------- Ball ---------------- */
function makeBall(x, y, vx, vy, r){
  return { x, y, vx, vy, r: r||8, speed: 0, trail: [], stuck: true, attached: null };
}

function launch(b){
  b.stuck = false;
  b.attached = null;
  const speed = 430;
  const ang = -Math.PI/2 + (Math.random()*0.9 - 0.45);
  b.vx = Math.cos(ang)*speed;
  b.vy = Math.sin(ang)*speed;
}

function resetBall(){
  state.balls = [makeBall(paddle.x, paddle.y - 22, 0, 0)];
  state.balls[0].stuck = true;
  state.balls[0].attached = paddle;
}

/* ---------------- Input ---------------- */
function pointerX(clientX){
  const rect = cv.getBoundingClientRect();
  return (clientX - rect.left) / rect.width * W;
}
window.addEventListener('pointermove', (e)=>{
  const px = pointerX(e.clientX);
  if (state.screen === 'title'){ startGame(); }
  paddle.targetX = px;
  mouseActive = true;
});
cv.addEventListener('pointerdown', (e)=>{
  const px = pointerX(e.clientX);
  paddle.targetX = px; mouseActive = true;
  if (state.screen === 'title'){ startGame(); return; }
  if (state.screen === 'over' || state.screen === 'win'){ startGame(); return; }
  if (state.screen === 'play'){
    // launch any stuck ball
    for(const b of state.balls) if (b.stuck) launch(b);
  }
});
window.addEventListener('keydown', (e)=>{
  if (e.code === 'ArrowLeft' || e.code === 'ArrowRight'){
    keys[e.code === 'ArrowLeft' ? 'left' : 'right'] = true;
    mouseActive = false;
    e.preventDefault();
    if (state.screen === 'title') startGame();
  }
  if (e.code === 'Space' || e.code === 'Enter'){
    e.preventDefault();
    if (state.screen === 'title' || state.screen === 'over' || state.screen === 'win'){ startGame(); return; }
    if (state.screen === 'play'){
      for(const b of state.balls) if (b.stuck) launch(b);
    }
  }
  if (e.code === 'KeyP' && state.screen === 'play'){
    state.screen = state.screen === 'pause' ? 'play' : 'pause';
  }
});
window.addEventListener('keyup', (e)=>{
  if (e.code === 'ArrowLeft') keys.left = false;
  if (e.code === 'ArrowRight') keys.right = false;
});
window.addEventListener('blur', ()=>{ keys.left=false; keys.right=false; });

/* ---------------- Start / restart ---------------- */
function startGame(){
  state.score = 0;
  state.lives = 3;
  state.wideTimer = 0;
  state.screen = 'play';
  state.powerups.length = 0;
  state.particles.length = 0;
  makeLevel();
  paddle.w = BASE_PADDLE_W;
  paddle.x = paddle.targetX = W/2;
  resetBall();
  refreshHUD();
}
function gameOver(){
  state.screen = 'over';
  if (state.score > best){
    best = state.score;
    try { localStorage.setItem('nb_best', String(best)); } catch(e){}
  }
  refreshHUD();
}
function victory(){
  state.screen = 'win';
  if (state.score > best){
    best = state.score;
    try { localStorage.setItem('nb_best', String(best)); } catch(e){}
  }
  refreshHUD();
}

/* ---------------- Power-ups ---------------- */
const POWERS = [
  { type:'wide',  label:'WIDE',  color:'#6cffb0', icon:'⇔' },
  { type:'multi', label:'MULTI', color:'#ffd166', icon:'⋮' },
  { type:'wide',  label:'WIDE',  color:'#6cffb0', icon:'⇔' }
];
function dropPower(b){
  const p = POWERS[Math.floor(Math.random()*POWERS.length)];
  state.powerups.push({
    x: b.x + b.w/2, y: b.y + b.h/2,
    vy: 150, type: p.type, color: p.color, icon: p.icon,
    label: p.label, life: 12, rot: 0
  });
}
function applyPower(p){
  if (p.type === 'wide'){
    paddle.w = BASE_PADDLE_W * 1.75;
    state.wideTimer = 10;
    state.message = 'WIDE PADDLE!';
    state.messageTimer = 1.6;
  } else {
    const src = state.balls.filter(b=>!b.stuck);
    const base = src.length ? src[0] : state.balls[0];
    for (let i = 0; i < 2; i++){
      const ang = Math.atan2(base.vy, base.vx) + (i===0 ? 0.45 : -0.45);
      const sp = Math.hypot(base.vx, base.vy) || 430;
      state.balls.push(makeBall(base.x, base.y, Math.cos(ang)*sp, Math.sin(ang)*sp, base.r));
    }
    state.message = 'MULTI-BALL!';
    state.messageTimer = 1.6;
  }
}

/* ---------------- Particles ---------------- */
function spawnParticles(x, y, color, n, power){
  for (let i = 0; i < n; i++){
    const a = Math.random()*Math.PI*2;
    const sp = (60 + Math.random()*260) * (power||1);
    state.particles.push({
      x, y, vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - 40,
      life: 0.4 + Math.random()*0.55, max: 0.95,
      size: 1.5 + Math.random()*3.2,
      color, g: 460
    });
  }
}
function spawnShock(x, y, color){
  state.particles.push({ring:true, x, y, r:6, vr:340, life:0.45, max:0.45, color});
}

/* ---------------- Collision helpers ---------------- */
function clampBall(b){
  const speed = Math.hypot(b.vx, b.vy);
  const min = 300, max = 640;
  if (speed > max){ b.vx = b.vx/speed*max; b.vy = b.vy/speed*max; }
  if (speed < min){ b.vx = b.vx/speed*min; b.vy = b.vy/speed*min; }
  // avoid near-horizontal
  const horiz = Math.abs(b.vx) < 0.22*Math.abs(b.vy);
  if (horiz){
    const sign = b.vx >= 0 ? 1 : -1;
    const ang = Math.atan2(b.vy, b.vx);
    let na = ang;
    if (Math.abs(na) < 0.24) na = 0.24*sign;
    if (Math.abs(Math.abs(na) - Math.PI) < 0.24) na = Math.PI - 0.24*sign;
    const sp = Math.hypot(b.vx, b.vy);
    b.vx = Math.cos(na)*sp; b.vy = Math.sin(na)*sp;
  }
}

function rectCircle(rx, ry, rw, rh, cx, cy, cr){
  const nx = Math.max(rx, Math.min(cx, rx+rw));
  const ny = Math.max(ry, Math.min(cy, ry+rh));
  const dx = cx-nx, dy = cy-ny;
  return dx*dx + dy*dy <= cr*cr;
}

function reflectFromRect(b, rect){
  // Determine the closest edge and push out along its normal.
  const overlaps = [
    {d: Math.abs((b.x + b.r) - rect.x), nx: 1, ny: 0},
    {d: Math.abs(rect.x + rect.w - (b.x - b.r)), nx: -1, ny: 0},
    {d: Math.abs((b.y + b.r) - rect.y), nx: 0, ny: -1},
    {d: Math.abs(rect.y + rect.h - (b.y - b.r)), nx: 0, ny: 1}
  ];
  overlaps.sort((a,c)=>a.d-c.d);
  const o = overlaps[0];
  if (o.nx !== 0){ b.vx = Math.abs(b.vx)*o.nx; b.x += o.nx * 2; }
  else { b.vy = Math.abs(b.vy)*o.ny; b.y += o.ny * 2; }
}

function hitBrick(b, br){
  if (!br.alive) return false;
  br.hitFlash = 1;
  if (br.hp > 1){
    br.hp--;
    br.crack = 1;
    spawnParticles(b.x, b.y, '#ffffff', 6, 0.6);
    state.score += 5;
    return true;
  }
  br.alive = false;
  state.bricksLeft--;
  state.score += 10;
  state.shake = Math.min(9, state.shake + 3);
  spawnParticles(br.x + br.w/2, br.y + br.h/2, br.color, 20, 1);
  spawnShock(br.x + br.w/2, br.y + br.h/2, br.color);
  if (br.hasPower) dropPower(br);
  return true;
}

/* ---------------- Update ---------------- */
function update(dt){
  state.t += dt;
  state.shake *= Math.pow(0.0015, dt);
  state.flash = Math.max(0, state.flash - dt*3);
  if (state.messageTimer > 0) state.messageTimer -= dt;

  // particles always animate
  for (let i = state.particles.length - 1; i >= 0; i--){
    const p = state.particles[i];
    p.life -= dt;
    if (p.ring){
      p.r += p.vr*dt;
      if (p.life <= 0) state.particles.splice(i,1);
      continue;
    }
    p.vy += p.g*dt;
    p.x += p.vx*dt;
    p.y += p.vy*dt;
    p.vx *= Math.pow(0.35, dt);
    if (p.life <= 0) state.particles.splice(i,1);
  }

  if (state.screen === 'title'){
    // Idle demo animation: paddle drifts, ball bounces on a mini-preview
    updateParticlesOnly(dt);
    return;
  }
  if (state.screen !== 'play') return;

  // wide paddle timer
  if (state.wideTimer > 0){
    state.wideTimer -= dt;
    if (state.wideTimer <= 0){
      paddle.w = BASE_PADDLE_W;
      state.wideTimer = 0;
      state.message = 'PADDLE NORMALIZED';
      state.messageTimer = 1.2;
    }
  }

  // paddle movement
  const prevX = paddle.x;
  if (keys.left || keys.right){
    const sp = 620;
    if (keys.left) paddle.x -= sp*dt;
    if (keys.right) paddle.x += sp*dt;
    paddle.targetX = paddle.x;
  } else {
    const diff = paddle.targetX - paddle.x;
    paddle.x += diff * Math.min(1, dt*18);
  }
  paddle.x = Math.max(paddle.w/2 + 4, Math.min(W - paddle.w/2 - 4, paddle.x));
  paddle.speed = (paddle.x - prevX)/Math.max(dt, 0.0001);

  // power-ups
  for (let i = state.powerups.length - 1; i >= 0; i--){
    const p = state.powerups[i];
    p.y += p.vy*dt;
    p.rot += dt*2.4;
    p.life -= dt;
    if (p.life <= 0){ state.powerups.splice(i,1); continue; }
    // paddle catch
    if (p.y + 12 >= paddle.y - paddle.h/2 && p.y - 12 <= paddle.y + paddle.h/2 &&
        p.x >= paddle.x - paddle.w/2 - 6 && p.x <= paddle.x + paddle.w/2 + 6){
      applyPower(p);
      spawnParticles(p.x, p.y, p.color, 18, 1);
      spawnShock(p.x, p.y, p.color);
      state.powerups.splice(i,1);
      continue;
    }
    if (p.y > H + 30){ state.powerups.splice(i,1); }
  }

  // balls
  const subSteps = 3;
  const sdt = dt / subSteps;
  for (let bi = 0; bi < state.balls.length; bi++){
    const b = state.balls[bi];
    if (b.stuck){
      b.x = paddle.x;
      b.y = paddle.y - paddle.h/2 - b.r - 2;
      continue;
    }
    for (let s = 0; s < subSteps; s++){
      b.x += b.vx*sdt;
      b.y += b.vy*sdt;

      // walls
      if (b.x - b.r < 0){ b.x = b.r; b.vx = Math.abs(b.vx); }
      if (b.x + b.r > W){ b.x = W - b.r; b.vx = -Math.abs(b.vx); }
      if (b.y - b.r < 0){ b.y = b.r; b.vy = Math.abs(b.vy); }

      // paddle
      if (b.vy > 0 &&
          rectCircle(paddle.x - paddle.w/2, paddle.y - paddle.h/2, paddle.w, paddle.h, b.x, b.y, b.r)){
        // angle based on hit offset
        const rel = (b.x - paddle.x) / (paddle.w/2); // -1..1
        const clamped = Math.max(-1, Math.min(1, rel));
        let ang = -Math.PI/2 + clamped * (Math.PI/3.1); // max ~58deg
        const sp = Math.max(330, Math.min(640, Math.hypot(b.vx, b.vy) + Math.abs(paddle.speed)*0.06));
        b.vx = Math.cos(ang)*sp;
        b.vy = Math.sin(ang)*sp;
        b.y = paddle.y - paddle.h/2 - b.r - 0.5;
        clampBall(b);
        spawnParticles(b.x, paddle.y - paddle.h/2, '#9fd8ff', 4, 0.5);
      }

      // bricks
      for (let k = 0; k < bricks.length; k++){
        const br = bricks[k];
        if (!br.alive) continue;
        if (rectCircle(br.x, br.y, br.w, br.h, b.x, b.y, b.r)){
          reflectFromRect(b, br);
          hitBrick(b, br);
          clampBall(b);
          break;
        }
      }

      // bottom
      if (b.y - b.r > H){
        b.dead = true;
        break;
      }
    }
    // trail
    b.trail.push({x:b.x, y:b.y});
    if (b.trail.length > 12) b.trail.shift();
  }
  // remove dead balls
  for (let i = state.balls.length - 1; i >= 0; i--){
    if (state.balls[i].dead) state.balls.splice(i,1);
  }

  if (state.balls.length === 0){
    state.lives--;
    state.shake = 8;
    state.flash = 1;
    if (state.lives <= 0){
      refreshHUD();
      gameOver();
      return;
    }
    state.wideTimer = 0;
    paddle.w = BASE_PADDLE_W;
    resetBall();
    state.message = 'BALL LOST — ' + state.lives + (state.lives===1?' LIFE LEFT':' LIVES LEFT');
    state.messageTimer = 2;
  }

  if (state.bricksLeft <= 0){
    victory();
  }
  refreshHUD();
}

function updateParticlesOnly(dt){
  for (let i = state.particles.length - 1; i >= 0; i--){
    const p = state.particles[i];
    p.life -= dt;
    if (p.ring){ p.r += p.vr*dt; if (p.life<=0) state.particles.splice(i,1); continue; }
    p.vy += p.g*dt; p.x += p.vx*dt; p.y += p.vy*dt;
    if (p.life <= 0) state.particles.splice(i,1);
  }
}

/* ---------------- Rendering helpers ---------------- */
function roundRect(c, x, y, w, h, r){
  r = Math.min(r, w/2, h/2);
  c.beginPath();
  c.moveTo(x+r, y);
  c.lineTo(x+w-r, y); c.quadraticCurveTo(x+w, y, x+w, y+r);
  c.lineTo(x+w, y+h-r); c.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
  c.lineTo(x+r, y+h); c.quadraticCurveTo(x, y+h, x, y+h-r);
  c.lineTo(x, y+r); c.quadraticCurveTo(x, y, x+r, y);
  c.closePath();
}
function drawGrid(t){
  ctx.save();
  ctx.strokeStyle = 'rgba(120,170,255,0.055)';
  ctx.lineWidth = 1;
  for (let x = 0; x <= W; x += 45){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
  for (let y = 0; y <= H; y += 45){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
  // subtle moving glow bar
  const gy = (t*40) % (H+300) - 150;
  const grad = ctx.createLinearGradient(0, gy-140, 0, gy+140);
  grad.addColorStop(0, 'rgba(90,160,255,0)');
  grad.addColorStop(0.5, 'rgba(90,160,255,0.055)');
  grad.addColorStop(1, 'rgba(90,160,255,0)');
  ctx.fillStyle = grad;
  ctx.fillRect(0, gy-140, W, 280);
  ctx.restore();
}

function drawBricks(t){
  for (const b of bricks){
    if (!b.alive) continue;
    const pulse = 0.5 + 0.5*Math.sin(t*2.2 + b.pulse);
    ctx.save();
    // glow
    ctx.shadowColor = b.color;
    ctx.shadowBlur = b.tough ? 16 : 10;
    // body
    const g = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
    g.addColorStop(0, b.color);
    g.addColorStop(1, shade(b.color, -0.42));
    roundRect(ctx, b.x, b.y, b.w, b.h, 5);
    ctx.fillStyle = g;
    ctx.globalAlpha = b.tough ? 0.95 : 0.9;
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.globalAlpha = 1;
    // top highlight
    ctx.fillStyle = 'rgba(255,255,255,'+(0.18 + pulse*0.14)+')';
    roundRect(ctx, b.x+2, b.y+2, b.w-4, Math.max(3, b.h*0.24), 3);
    ctx.fill();
    // border
    ctx.strokeStyle = b.tough ? 'rgba(255,255,255,0.55)' : 'rgba(255,255,255,0.22)';
    ctx.lineWidth = b.tough ? 1.6 : 1;
    ctx.stroke();

    // tough brick rivets
    if (b.tough){
      ctx.fillStyle = 'rgba(255,255,255,0.75)';
      ctx.beginPath(); ctx.arc(b.x+7, b.y+b.h/2, 2, 0, Math.PI*2); ctx.fill();
      ctx.beginPath(); ctx.arc(b.x+b.w-7, b.y+b.h/2, 2, 0, Math.PI*2); ctx.fill();
    }
    // cracks after first hit
    if (b.crack > 0){
      ctx.strokeStyle = 'rgba(10,12,22,0.85)';
      ctx.lineWidth = 1.6;
      ctx.beginPath();
      const mx = b.x + b.w*0.5, my = b.y + b.h*0.5;
      ctx.moveTo(b.x+4, b.y+3);
      ctx.lineTo(mx-2, my-1);
      ctx.lineTo(b.x+b.w*0.62, b.y+b.h-3);
      ctx.moveTo(mx-2, my-1);
      ctx.lineTo(b.x+b.w-4, b.y+4);
      ctx.moveTo(mx-2, my-1);
      ctx.lineTo(b.x+6, b.y+b.h-3);
      ctx.stroke();
      ctx.strokeStyle = 'rgba(255,255,255,0.35)';
      ctx.lineWidth = 0.8;
      ctx.stroke();
      // damage tint
      ctx.fillStyle = 'rgba(255,120,90,0.10)';
      roundRect(ctx, b.x, b.y, b.w, b.h, 5);
      ctx.fill();
    }
    // hit flash
    if (b.hitFlash > 0){
      ctx.fillStyle = 'rgba(255,255,255,'+(b.hitFlash*0.55)+')';
      roundRect(ctx, b.x, b.y, b.w, b.h, 5);
      ctx.fill();
      b.hitFlash = Math.max(0, b.hitFlash - 0.06);
    }
    // power-up icon
    if (b.hasPower){
      ctx.save();
      ctx.translate(b.x + b.w/2, b.y + b.h/2);
      ctx.rotate(Math.sin(t*1.6 + b.pulse)*0.14);
      ctx.fillStyle = 'rgba(12,16,30,0.55)';
      ctx.beginPath(); ctx.arc(0,0, Math.min(b.w,b.h)*0.32, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle = '#0b1020';
      ctx.font = 'bold ' + Math.round(b.h*0.62) + 'px "Segoe UI Symbol", "Segoe UI", sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('⚡', 0, 1);
      ctx.restore();
    }
    ctx.restore();
  }
}

function shade(hex, amt){
  const n = parseInt(hex.slice(1), 16);
  let r = (n>>16)&255, g = (n>>8)&255, b = n&255;
  const f = amt < 0 ? 1+amt : 1;
  const add = amt > 0 ? 255*amt : 0;
  r = Math.round(r*f + add); g = Math.round(g*f + add); b = Math.round(b*f + add);
  return 'rgb('+Math.max(0,Math.min(255,r))+','+Math.max(0,Math.min(255,g))+','+Math.max(0,Math.min(255,b))+')';
}

function drawPaddle(t){
  const x = paddle.x - paddle.w/2, y = paddle.y - paddle.h/2;
  ctx.save();
  ctx.shadowColor = state.wideTimer > 0 ? '#6cffb0' : '#5ee7ff';
  ctx.shadowBlur = 22;
  const g = ctx.createLinearGradient(x, y, x, y+paddle.h);
  g.addColorStop(0, state.wideTimer > 0 ? '#b6ffd8' : '#d7f4ff');
  g.addColorStop(0.45, state.wideTimer > 0 ? '#6cffb0' : '#5ee7ff');
  g.addColorStop(1, state.wideTimer > 0 ? '#1f9c6a' : '#2a86c9');
  roundRect(ctx, x, y, paddle.w, paddle.h, 7);
  ctx.fillStyle = g; ctx.fill();
  ctx.shadowBlur = 0;
  ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1; ctx.stroke();
  // center marker
  ctx.fillStyle = 'rgba(255,255,255,0.55)';
  ctx.fillRect(paddle.x - 9, y + paddle.h*0.28, 18, 2.5);
  ctx.restore();
}

function drawBalls(t){
  for (const b of state.balls){
    // trail
    for (let i = 0; i < b.trail.length; i++){
      const p = b.trail[i];
      const a = (i/b.trail.length) * 0.42;
      ctx.fillStyle = 'rgba(150,215,255,'+a+')';
      ctx.beginPath(); ctx.arc(p.x, p.y, b.r*(i/b.trail.length)*0.95, 0, Math.PI*2); ctx.fill();
    }
    ctx.save();
    ctx.shadowColor = '#9fe8ff'; ctx.shadowBlur = 20;
    const g = ctx.createRadialGradient(b.x - b.r*0.35, b.y - b.r*0.4, 1, b.x, b.y, b.r);
    g.addColorStop(0, '#ffffff');
    g.addColorStop(0.45, '#bdf0ff');
    g.addColorStop(1, '#3fa9e8');
    ctx.fillStyle = g;
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }
}

function drawPowerups(t){
  for (const p of state.powerups){
    ctx.save();
    ctx.translate(p.x, p.y);
    // capsule
    ctx.rotate(Math.sin(t*3 + p.rot)*0.12);
    ctx.shadowColor = p.color; ctx.shadowBlur = 18;
    const h = 24, w = 24;
    roundRect(ctx, -w/2, -h/2, w, h, 8);
    const g = ctx.createLinearGradient(0,-h/2,0,h/2);
    g.addColorStop(0, '#ffffff');
    g.addColorStop(0.3, p.color);
    g.addColorStop(1, shade(p.color,-0.5));
    ctx.fillStyle = g; ctx.fill();
    ctx.shadowBlur = 0;
    ctx.strokeStyle = 'rgba(255,255,255,0.7)'; ctx.lineWidth = 1.2; ctx.stroke();
    ctx.fillStyle = '#0b1020';
    ctx.font = 'bold 14px "Segoe UI Symbol", "Segoe UI", sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(p.type === 'wide' ? '↔' : '✦', 0, 0.5);
    ctx.restore();
    // label
    ctx.save();
    ctx.globalAlpha = 0.9;
    ctx.fillStyle = p.color;
    ctx.font = 'bold 10px "Segoe UI", sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(p.label, p.x, p.y - 20);
    ctx.restore();
  }
}

function drawParticles(){
  for (const p of state.particles){
    if (p.ring){
      ctx.save();
      ctx.globalAlpha = Math.max(0, p.life/p.max);
      ctx.strokeStyle = p.color; ctx.lineWidth = 3;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.stroke();
      ctx.restore();
      continue;
    }
    const a = Math.max(0, p.life/p.max);
    ctx.save();
    ctx.globalAlpha = a;
    ctx.fillStyle = p.color;
    ctx.shadowColor = p.color; ctx.shadowBlur = 8;
    ctx.beginPath(); ctx.arc(p.x, p.y, p.size*a + 0.4, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }
}

function drawHudOverlay(){
  // top in-canvas status bar
  ctx.save();
  ctx.fillStyle = 'rgba(8,12,26,0.55)';
  ctx.fillRect(0, 0, W, 40);
  ctx.strokeStyle = 'rgba(120,170,255,0.18)';
  ctx.beginPath(); ctx.moveTo(0, 40.5); ctx.lineTo(W, 40.5); ctx.stroke();
  ctx.font = 'bold 13px "Segoe UI", sans-serif';
  ctx.textBaseline = 'middle';
  ctx.fillStyle = '#8fa0c8';
  ctx.textAlign = 'left';
  ctx.fillText('SCORE', 18, 20);
  ctx.fillStyle = '#e9efff';
  ctx.font = 'bold 17px "Segoe UI", sans-serif';
  ctx.fillText(String(state.score), 18, 20);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#8fa0c8'; ctx.font = 'bold 13px "Segoe UI", sans-serif';
  ctx.fillText('LIVES', W/2 - 34, 20);
  ctx.fillStyle = '#ff8fb0'; ctx.font = 'bold 17px "Segoe UI", sans-serif';
  ctx.fillText('♥'.repeat(Math.max(0,state.lives)) || '-', W/2 + 16, 20);
  ctx.textAlign = 'right';
  ctx.fillStyle = '#8fa0c8'; ctx.font = 'bold 13px "Segoe UI", sans-serif';
  ctx.fillText('BRICKS', W - 74, 20);
  ctx.fillStyle = '#e9efff'; ctx.font = 'bold 17px "Segoe UI", sans-serif';
  ctx.fillText(String(state.bricksLeft), W - 18, 20);
  ctx.restore();

  // wide paddle timer bar
  if (state.wideTimer > 0){
    ctx.save();
    const w = 160, x = W/2 - w/2, y = 48;
    ctx.fillStyle = 'rgba(108,255,176,0.18)';
    roundRect(ctx, x, y, w, 7, 4); ctx.fill();
    ctx.fillStyle = '#6cffb0';
    roundRect(ctx, x, y, w*(state.wideTimer/10), 7, 4); ctx.fill();
    ctx.fillStyle = '#b6ffd8';
    ctx.font = 'bold 10px "Segoe UI", sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
    ctx.fillText('WIDE PADDLE ' + state.wideTimer.toFixed(1) + 's', W/2, y - 3);
    ctx.restore();
  }

  // center message
  if (state.messageTimer > 0 && state.screen === 'play'){
    const a = Math.min(1, state.messageTimer/0.5);
    ctx.save();
    ctx.globalAlpha = a;
    ctx.fillStyle = 'rgba(10,16,32,0.55)';
    roundRect(ctx, W/2 - 160, H/2 - 26, 320, 46, 12); ctx.fill();
    ctx.strokeStyle = 'rgba(150,200,255,0.25)'; ctx.lineWidth = 1; ctx.stroke();
    ctx.fillStyle = '#e9f3ff';
    ctx.font = 'bold 20px "Segoe UI", sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(state.message, W/2, H/2 - 2);
    ctx.restore();
  }
}

function drawTitle(t){
  // dim the field
  ctx.save();
  ctx.fillStyle = 'rgba(4,7,16,0.62)';
  ctx.fillRect(0,0,W,H);
  ctx.restore();

  const cx = W/2, cy = H/2 - 30;
  ctx.save();
  ctx.textAlign = 'center';
  // title
  const g = ctx.createLinearGradient(0, cy-70, 0, cy+10);
  g.addColorStop(0, '#7ff0ff');
  g.addColorStop(0.5, '#b06cff');
  g.addColorStop(1, '#ff7ad9');
  ctx.font = '900 62px "Segoe UI", sans-serif';
  ctx.fillStyle = g;
  ctx.shadowColor = 'rgba(140,120,255,0.6)'; ctx.shadowBlur = 26;
  ctx.fillText('NEON BREAKOUT', cx, cy);
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#a8bcdd';
  ctx.font = '15px "Segoe UI", sans-serif';
  ctx.fillText('Clear every brick. Catch the ⚡ power-ups. Don’t lose the ball.', cx, cy + 32);
  ctx.restore();

  // pulsing prompt
  const pulse = 0.55 + 0.45*Math.sin(t*4);
  ctx.save();
  ctx.globalAlpha = pulse;
  ctx.fillStyle = '#eaf4ff';
  ctx.font = 'bold 20px "Segoe UI", sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText('Click or press a key to start', cx, cy + 96);
  ctx.restore();

  // demo hint panel
  ctx.save();
  ctx.globalAlpha = 0.85;
  ctx.fillStyle = 'rgba(12,18,36,0.6)';
  roundRect(ctx, cx-230, cy+128, 460, 46, 12); ctx.fill();
  ctx.strokeStyle = 'rgba(120,170,255,0.18)'; ctx.lineWidth = 1; ctx.stroke();
  ctx.fillStyle = '#9db3d9';
  ctx.font = '13px "Segoe UI", sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText('← →  or mouse to move the paddle    •    Space to launch the ball', cx, cy+151);
  ctx.restore();
}

function drawEndScreen(t, win){
  ctx.save();
  ctx.fillStyle = win ? 'rgba(6,20,14,0.66)' : 'rgba(18,6,14,0.68)';
  ctx.fillRect(0,0,W,H);
  ctx.restore();

  const cx = W/2, cy = H/2 - 20;
  ctx.save();
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  const g = ctx.createLinearGradient(0, cy-60, 0, cy+20);
  if (win){ g.addColorStop(0,'#8dffc4'); g.addColorStop(1,'#4fd1ff'); }
  else { g.addColorStop(0,'#ff9bb5'); g.addColorStop(1,'#c86cff'); }
  ctx.font = '900 58px "Segoe UI", sans-serif';
  ctx.fillStyle = g;
  ctx.shadowColor = win ? 'rgba(90,255,180,0.55)' : 'rgba(255,110,160,0.5)';
  ctx.shadowBlur = 26;
  ctx.fillText(win ? 'VICTORY!' : 'GAME OVER', cx, cy-10);
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#c6d6f2';
  ctx.font = 'bold 20px "Segoe UI", sans-serif';
  ctx.fillText('Final score: ' + state.score, cx, cy + 34);
  ctx.fillStyle = '#8fa0c8';
  ctx.font = '14px "Segoe UI", sans-serif';
  ctx.fillText('Best: ' + Math.max(best, state.score), cx, cy + 60);
  ctx.restore();

  const pulse = 0.55 + 0.45*Math.sin(t*4);
  ctx.save();
  ctx.globalAlpha = pulse;
  ctx.fillStyle = '#eaf4ff';
  ctx.font = 'bold 19px "Segoe UI", sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText('Click or press a key to play again', cx, cy + 108);
  ctx.restore();
}

/* ---------------- Main render ---------------- */
function render(){
  ctx.save();
  // screen shake
  if (state.shake > 0.2){
    const sx = (Math.random()*2-1)*state.shake;
    const sy = (Math.random()*2-1)*state.shake;
    ctx.translate(sx, sy);
  }
  // background
  const bg = ctx.createLinearGradient(0,0,0,H);
  bg.addColorStop(0,'#0a1024');
  bg.addColorStop(0.5,'#070b18');
  bg.addColorStop(1,'#050810');
  ctx.fillStyle = bg;
  ctx.fillRect(-20,-20,W+40,H+40);
  drawGrid(state.t);
  drawBricks(state.t);
  drawPowerups(state.t);
  drawPaddle(state.t);
  drawBalls(state.t);
  drawParticles();
  ctx.restore();

  // walls glow
  ctx.save();
  ctx.strokeStyle = 'rgba(110,170,255,0.35)';
  ctx.lineWidth = 3;
  ctx.beginPath();
  ctx.moveTo(1.5, 40); ctx.lineTo(1.5, H); ctx.moveTo(W-1.5, 40); ctx.lineTo(W-1.5, H);
  ctx.stroke();
  ctx.restore();

  drawHudOverlay();

  if (state.screen === 'title') drawTitle(state.t);
  else if (state.screen === 'over') drawEndScreen(state.t, false);
  else if (state.screen === 'win') drawEndScreen(state.t, true);
  else if (state.screen === 'pause'){
    ctx.save();
    ctx.fillStyle = 'rgba(4,8,18,0.6)'; ctx.fillRect(0,0,W,H);
    ctx.fillStyle = '#eaf4ff'; ctx.font='bold 40px "Segoe UI",sans-serif';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText('PAUSED', W/2, H/2);
    ctx.font='14px "Segoe UI",sans-serif'; ctx.fillStyle='#93a7cc';
    ctx.fillText('Press P or click to resume', W/2, H/2+34);
    ctx.restore();
  }
  // ready prompt when ball is stuck
  if (state.screen === 'play' && state.balls.length && state.balls[0].stuck){
    const pulse = 0.55 + 0.45*Math.sin(state.t*5);
    ctx.save();
    ctx.globalAlpha = pulse;
    ctx.fillStyle = '#dff0ff';
    ctx.font = 'bold 16px "Segoe UI", sans-serif';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText('Click or press SPACE to launch', W/2, H - 96);
    ctx.restore();
  }
}

/* ---------------- Loop ---------------- */
let last = performance.now();
let demoTimer = 0;

function frame(now){
  let dt = (now - last)/1000;
  last = now;
  if (dt > 0.05) dt = 0.05;

  if (state.screen === 'title'){
    // Self-playing demo behind the title: paddle tracks a demo ball bouncing around.
    demoTimer += dt;
    if (demoTimer > 2.4){ demoTimer = 0; }
    if (!state.demoBall) state.demoBall = {x: W/2, y: 120, vx: 190, vy: 230};
    const db = state.demoBall;
    db.x += db.vx*dt; db.y += db.vy*dt;
    if (db.x < 12 || db.x > W-12) db.vx *= -1;
    if (db.y < 60) db.vy = Math.abs(db.vy);
    if (db.y > H-70){ db.vy = -Math.abs(db.vy); db.x = Math.max(40, Math.min(W-40, paddle.x + (Math.random()*2-1)*220)); }
    paddle.targetX = db.x;
    paddle.x += (db.x - paddle.x) * Math.min(1, dt*3.2);
    updateParticlesOnly(dt);
  } else {
    update(dt);
  }
  render();
  requestAnimationFrame(frame);
}

/* ---------------- Init ---------------- */
function init(){
  resize();
  makeLevel();
  resetBall();
  // hide bricks during title? keep them for visual richness
  refreshHUD();
  requestAnimationFrame(frame);
}
init();
})();
</script>
</body>
</html>