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
38.1 s
Cost
$0.0048
Output tokens
7,922
Spent thinking
0 tokens
Speed
210.4 tok/s
Console errors
0
First token after
436 ms
First visible text after
436 ms
Prompt tokens
692
Code size
22.1 KB
Finish reason
stop
Served by
Parasail
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"low"}
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 · 623 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>
  :root{
    --bg:#0b0f1a;
    --panel:#141b2d;
    --accent:#4dd0e1;
    --accent2:#ff7ae0;
    --text:#e8f1ff;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    background:radial-gradient(1200px 700px at 50% -10%, #1b2540 0%, var(--bg) 60%);
    color:var(--text);
    font-family:'Segoe UI',system-ui,-apple-system,Roboto,sans-serif;
    display:flex;align-items:center;justify-content:center;
    min-height:100vh;padding:16px;
    overflow:hidden;
  }
  #wrap{
    width:min(960px,100%);
    background:linear-gradient(180deg,#101728,#0a0e18);
    border:1px solid rgba(77,208,225,.25);
    border-radius:16px;
    box-shadow:0 20px 60px rgba(0,0,0,.6), inset 0 0 60px rgba(77,208,225,.05);
    padding:14px 14px 12px;
  }
  header{
    display:flex;align-items:center;justify-content:space-between;
    gap:10px;margin-bottom:10px;flex-wrap:wrap;
  }
  h1{
    font-size:clamp(16px,2.6vw,22px);letter-spacing:3px;text-transform:uppercase;
    background:linear-gradient(90deg,var(--accent),var(--accent2));
    -webkit-background-clip:text;background-clip:text;color:transparent;
    font-weight:700;
  }
  .stats{display:flex;gap:8px;flex-wrap:wrap}
  .stat{
    background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.09);
    border-radius:9px;padding:5px 11px;font-size:13px;min-width:74px;text-align:center;
  }
  .stat b{display:block;font-size:17px;color:var(--accent);font-variant-numeric:tabular-nums}
  .stat span{opacity:.6;font-size:10px;letter-spacing:1.4px;text-transform:uppercase}
  #stage{position:relative;width:100%;}
  canvas{
    display:block;width:100%;height:auto;border-radius:10px;
    background:#070a12;
    touch-action:none;
  }
  #overlay{
    position:absolute;inset:0;border-radius:10px;
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    background:rgba(6,9,16,.82);backdrop-filter:blur(3px);
    text-align:center;padding:18px;gap:12px;
  }
  #overlay.hidden{display:none}
  #overlay h2{font-size:clamp(22px,5vw,40px);letter-spacing:2px;text-transform:uppercase;
    background:linear-gradient(90deg,var(--accent),var(--accent2));
    -webkit-background-clip:text;background-clip:text;color:transparent}
  #overlay p{opacity:.8;font-size:14px;max-width:460px;line-height:1.5}
  #overlay .hint{opacity:.55;font-size:12px;letter-spacing:1px}
  button{
    pointer-events:auto;cursor:pointer;border:0;border-radius:10px;
    padding:11px 26px;font-size:15px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
    color:#04121a;background:linear-gradient(90deg,var(--accent),var(--accent2));
    box-shadow:0 6px 22px rgba(77,208,225,.28);
    transition:transform .12s ease, filter .12s ease;
  }
  button:hover{transform:translateY(-2px);filter:brightness(1.1)}
  footer{margin-top:9px;font-size:11.5px;opacity:.5;text-align:center;letter-spacing:.6px}
  kbd{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.18);border-radius:4px;padding:1px 6px;font-size:11px}
</style>
</head>
<body>
<div id="wrap">
  <header>
    <h1>Neon Breakout</h1>
    <div class="stats">
      <div class="stat"><span>Score</span><b id="score">0</b></div>
      <div class="stat"><span>Lives</span><b id="lives">3</b></div>
      <div class="stat"><span>Bricks</span><b id="left">0</b></div>
      <div class="stat"><span>Level</span><b id="level">1</b></div>
    </div>
  </header>
  <div id="stage">
    <canvas id="cv" width="900" height="600"></canvas>
    <div id="overlay">
      <h2 id="ovTitle">Neon Breakout</h2>
      <p id="ovText">Move the mouse or use <kbd>←</kbd> <kbd>→</kbd> to steer the paddle. Bounce off the paddle's edges for sharp angles. Catch falling power-ups.</p>
      <button id="ovBtn">Start Game</button>
      <div class="hint" id="ovHint">Click or press any key to start</div>
    </div>
  </div>
  <footer>Mouse / <kbd>←</kbd><kbd>→</kbd> to move &nbsp;•&nbsp; <kbd>Space</kbd> to launch &nbsp;•&nbsp; <kbd>P</kbd> pause &nbsp;•&nbsp; Click or press a key to start</footer>
</div>

<script>
(()=>{
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
const W=cv.width, H=cv.height;
const overlay=document.getElementById('overlay');
const ovTitle=document.getElementById('ovTitle'), ovText=document.getElementById('ovText'), ovBtn=document.getElementById('ovBtn'), ovHint=document.getElementById('ovHint');
const elScore=document.getElementById('score'), elLives=document.getElementById('lives'), elLeft=document.getElementById('left');

// ---------- Config ----------
const COLS=10, ROWS=6;
const BRICK_W=78, BRICK_H=26, GAP=6, TOP=70, LEFT_PAD=25;
const PLAY_LEFT=LEFT_PAD, PLAY_RIGHT=LEFT_PAD+COLS*(BRICK_W+GAP);
const PADDLE_BASE_W=110, PADDLE_H=13;
const BRICK_COLORS=['#4dd0e1','#5ee0a8','#ffd166','#ff8f6b','#ff7ae0','#8f9bff'];

// ---------- State ----------
let state='menu'; // menu, serve, play, over, win
let score=0, lives=3, level=1;
let bricks=[], balls=[], particles=[], powerups=[];
let paddle, keys={left:false,right:false}, mouseX=null, mouseActive=false;
let wideTimer=0;
let shake=0, flash=0;
let serveTimer=0;

function makePaddle(){
  return {x:W/2-PADDLE_BASE_W/2, y:H-40, w:PADDLE_BASE_W, h:PADDLE_H, vx:0, glow:0};
}
paddle=makePaddle();

function levelLayout(){
  bricks=[];
  const rows=ROWS;
  for(let r=0;r<rows;r++){
    for(let c=0;c<COLS;c++){
      // leave a couple of gaps for style
      if(r===2 && (c===4||c===5)) continue;
      if(r===4 && c===0) continue;
      const hp = (r>=4 || (r===2&&c>=7) || (r===5&&c>=2&&c<=6)) ? 2 : 1;
      const hasPU = (r===1&&c===2) || (r===3&&c===7) || (r===5&&c===5);
      bricks.push({
        x:PLAY_LEFT+c*(BRICK_W+GAP), y:TOP+r*(BRICK_H+GAP),
        w:BRICK_W, h:BRICK_H, hp, maxhp:hp,
        color:BRICK_COLORS[r%BRICK_COLORS.length],
        hasPU, alive:true, flash:0, seed:Math.random()*100
      });
    }
  }
}
function resetLevel(){
  levelLayout();
  balls=[];particles=[];powerups=[];
  paddle=makePaddle(); wideTimer=0;
  state='serve'; serveTimer=0;
  spawnBall();
  updateStats();
}
function spawnBall(extra){
  const b={x:paddle.x+paddle.w/2, y:paddle.y-9, vx:(Math.random()-.5)*140, vy:-330, r:7.5, trail:[]};
  balls.push(b);
  return b;
}
function updateStats(){
  elScore.textContent=score;
  elLives.textContent=lives;
  elLeft.textContent=bricks.filter(b=>b.alive).length;
  elLeft.parentElement.querySelector('span').textContent='Bricks';
}

// ---------- Overlay ----------
function showOverlay(title,text,btn,hint){
  ovTitle.textContent=title; ovText.innerHTML=text; ovBtn.textContent=btn;
  ovHint.textContent=hint||'Click or press any key to start';
  overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }

function startGame(){
  score=0;lives=3;level=1;resetLevel();
  hideOverlay();
}
function gameOver(){
  state='over';
  showOverlay('Game Over',`Final score: <b>${score}</b><br>You cleared ${level} level(s).`,'Play Again');
}
function victory(){
  state='win';
  showOverlay('Victory!',`All bricks smashed! Score: <b>${score}</b>`,'Play Again');
}

// ---------- Input ----------
function pointerMove(e){
  const r=cv.getBoundingClientRect();
  mouseX=(e.clientX-r.left)*(W/r.width);
  mouseActive=true;
}
window.addEventListener('mousemove',pointerMove);
cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();mouseX=(e.touches[0].clientX-r.left)*(W/r.width);mouseActive=true;},{passive:false});
cv.addEventListener('touchstart',e=>{e.preventDefault();const r=cv.getBoundingClientRect();mouseX=(e.touches[0].clientX-r.left)*(W/r.width);mouseActive=true; if(state==='menu')startGame();},{passive:false});
window.addEventListener('keydown',e=>{
  if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A'){keys.left=true;e.preventDefault();}
  if(e.key==='ArrowRight'||e.key==='d'||e.key==='D'){keys.right=true;e.preventDefault();}
  if(e.code==='Space'){e.preventDefault(); if(state==='serve')launch();}
  if(e.key==='p'||e.key==='P'){ if(state==='play'){state='pause';showOverlay('Paused','Press P or click to resume','Resume');} else if(state==='pause'){state='play';hideOverlay();} }
  if(state==='menu'||state==='over'||state==='win'){ startGame(); }
});
window.addEventListener('keyup',e=>{
  if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A')keys.left=false;
  if(e.key==='ArrowRight'||e.key==='d'||e.key==='D')keys.right=false;
});
ovBtn.addEventListener('click',e=>{e.stopPropagation();startGame();});
overlay.addEventListener('click',()=>{ if(state==='menu'||state==='over'||state==='win')startGame(); else if(state==='pause'){state='play';hideOverlay();} });
window.addEventListener('resize',()=>{});

function launch(){
  if(state!=='serve')return;
  state='play';
  for(const b of balls){
    if(Math.abs(b.vy)<120){ b.vy = -Math.abs(b.vy)<0? -330:-330; }
    b.vx=(Math.random()-.5)*180;
    if(b.vy>0)b.vy=-Math.abs(b.vy);
    normalize(b);
  }
}

// ---------- Physics helpers ----------
function normalize(b){
  const speed=Math.hypot(b.vx,b.vy)||1;
  const target=380;
  b.vx=b.vx/speed*target; b.vy=b.vy/speed*target;
}
function movePaddle(dt){
  const targetW = wideTimer>0 ? PADDLE_BASE_W*1.55 : PADDLE_BASE_W;
  paddle.w += (targetW-paddle.w)*Math.min(1,dt*10);
  const speed=640;
  let dx=0;
  if(keys.left)dx-=speed;
  if(keys.right)dx+=speed;
  if(mouseActive&&dx===0){
    const target=Math.max(paddle.x,Math.min(W-paddle.w,mouseX-paddle.w/2));
    dx=(target-paddle.x)*Math.min(1,dt*18);
  }
  paddle.x=Math.max(0,Math.min(W-paddle.w,paddle.x+dx*dt));
}

function brickRectHit(b,r){
  return !(b.x+r<b.x || b.x-r>b.x+b.w || b.y+r<b.y || b.y-r>b.y+b.h);
}
function circleRect(cx,cy,r,rx,ry,rw,rh){
  const nx=Math.max(rx,Math.min(cx,rx+rw)), ny=Math.max(ry,Math.min(cy,ry+rh));
  const dx=cx-nx, dy=cy-ny;
  return dx*dx+dy*dy<=r*r;
}

// ---------- Update ----------
function update(dt){
  if(state==='menu'||state==='over'||state==='win'||state==='pause')return;
  movePaddle(dt);
  if(wideTimer>0){wideTimer-=dt; if(wideTimer<=0)paddle.glow=0;}

  if(state==='serve'){
    serveTimer+=dt;
    for(const b of balls){ b.x=paddle.x+paddle.w/2+Math.sin(serveTimer*3)*14; b.y=paddle.y-b.r-2; b.trail=[];}
    return;
  }

  // balls
  for(let i=balls.length-1;i>=0;i--){
    const b=balls[i];
    b.trail.push({x:b.x,y:b.y}); if(b.trail.length>10)b.trail.shift();
    // substeps for accuracy
    const steps=4;
    const sdt=dt/steps;
    for(let s=0;s<steps;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); wallFx(b.x,b.y);}
      if(b.x+b.r>W){b.x=W-b.r;b.vx=-Math.abs(b.vx); wallFx(b.x,b.y);}
      if(b.y-b.r<0){b.y=b.r;b.vy=Math.abs(b.vy); wallFx(b.x,b.y);}
      // bricks
      for(const br of bricks){
        if(!br.alive)continue;
        if(circleRect(b.x,b.y,b.r,br.x,br.y,br.w,br.h)){
          hitBrick(br,b);
          break;
        }
      }
      // paddle
      if(b.vy>0 && b.y+b.r>=paddle.y && b.y-b.r<=paddle.y+paddle.h &&
         b.x>=paddle.x-b.r && b.x<=paddle.x+paddle.w+b.r){
        const rel=(b.x-(paddle.x+paddle.w/2))/(paddle.w/2); // -1..1
        const clamped=Math.max(-1,Math.min(1,rel));
        const angle=clamped*(Math.PI/3); // up to 60 deg
        const speed=Math.max(340,Math.hypot(b.vx,b.vy));
        b.vx=Math.sin(angle)*speed*Math.sign(b.vx||1)*0 + Math.sin(angle)*speed*1;
        // ensure direction follows paddle motion slightly
        b.vx = Math.sin(angle)*speed;
        b.vy = -Math.abs(Math.cos(angle))*speed;
        if(Math.abs(b.vy)<90)b.vy=-90;
        b.y=paddle.y-b.r-0.5;
        paddle.glow=1;
        spark(b.x,paddle.y,8,'#4dd0e1',1.2);
      }
    }
    if(b.y-b.r>H+20){
      balls.splice(i,1);
    }
  }

  if(balls.length===0){
    lives--;
    updateStats();
    flash=0.6; shake=8;
    if(lives<=0){gameOver();return;}
    state='serve';serveTimer=0;
    spawnBall();
  }

  // powerups
  for(let i=powerups.length-1;i>=0;i--){
    const p=powerups[i];
    p.y+=p.vy*dt; p.rot+=p.spin*dt; p.x+=p.vx*dt;
    if(p.x<p.r||p.x>W-p.r)p.vx*=-1;
    if(p.y>H+30){powerups.splice(i,1);continue;}
    if(circleRect(p.x,p.y,p.r+4,paddle.x-2,paddle.y-2,paddle.w+4,paddle.h+6)){
      applyPowerup(p);
      powerups.splice(i,1);
    }
  }

  // particles
  for(let i=particles.length-1;i>=0;i--){
    const q=particles[i];
    q.x+=q.vx*dt; q.y+=q.vy*dt; q.vy+=q.g*dt; q.life-=dt;
    q.vx*=0.99;
    if(q.life<=0)particles.splice(i,1);
  }
  if(shake>0)shake=Math.max(0,shake-dt*22);
  if(flash>0)flash=Math.max(0,flash-dt*2);

  // win check
  if(bricks.every(b=>!b.alive)){ victory(); }
}

function wallFx(x,y){ spark(x,y,5,'#8f9bff',0.8); }

function hitBrick(br,b){
  // determine bounce by overlap side
  const cx=b.x, cy=b.y;
  const overlapL = Math.abs((cx+b.r)-(br.x));
  const overlapR = Math.abs((br.x+br.w)-(cx-b.r));
  const overlapT = Math.abs((cy+b.r)-(br.y));
  const overlapB = Math.abs((br.y+br.h)-(cy-b.r));
  const m=Math.min(overlapL,overlapR,overlapT,overlapB);
  if(m===overlapT){ b.y=br.y-b.r-0.5; b.vy=-Math.abs(b.vy); }
  else if(m===overlapB){ b.y=br.y+br.h+b.r+0.5; b.vy=Math.abs(b.vy); }
  else if(m===overlapL){ b.x=br.x-b.r-0.5; b.vx=-Math.abs(b.vx); }
  else { b.x=br.x+br.w+b.r+0.5; b.vx=Math.abs(b.vx); }

  br.flash=1;
  if(br.hp>1){
    br.hp--;
    score+=5; updateStats();
    spark(b.x,b.y,10,br.color,1.0);
    shake=Math.max(shake,3);
  }else{
    br.alive=false;
    score+=10; updateStats();
    spark(b.x,b.y,22,br.color,1.6);
    shake=Math.max(shake,6);
    if(br.hasPU) dropPowerup(br);
  }
}

function dropPowerup(br){
  const type = Math.random()<0.5 ? 'wide' : 'multi';
  powerups.push({x:br.x+br.w/2, y:br.y+br.h/2, vy:105, vx:(Math.random()-.5)*60, r:12, type, rot:0, spin:2.2});
}

function applyPowerup(p){
  if(p.type==='wide'){
    wideTimer=10; paddle.glow=1;
    score+=50; updateStats();
    floatText(p.x,p.y,'WIDE PADDLE','#ffd166');
  }else{
    for(let i=0;i<2;i++){
      const b=spawnBall();
      b.vx=(Math.random()-.5)*300; b.vy=-Math.abs(300+Math.random()*120);
      normalize(b);
    }
    score+=50; updateStats();
    floatText(p.x,p.y,'MULTI BALL','#ff7ae0');
  }
}

let texts=[];
function floatText(x,y,t,c){texts.push({x,y,t,c,life:1.2});}

function spark(x,y,n,color,scale){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2, sp=(40+Math.random()*220)*scale;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-40,g:340,life:0.4+Math.random()*0.5,max:0.9,size:1.5+Math.random()*3,color});
  }
}

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

function drawBackground(){
  const g=ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#0a0f1d');g.addColorStop(1,'#06080f');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  // grid
  ctx.strokeStyle='rgba(120,160,255,0.055)';ctx.lineWidth=1;
  for(let x=0;x<W;x+=45){ctx.beginPath();ctx.moveTo(x+.5,0);ctx.lineTo(x+.5,H);ctx.stroke();}
  for(let y=0;y<H;y+=45){ctx.beginPath();ctx.moveTo(0,y+.5);ctx.lineTo(W,y+.5);ctx.stroke();}
  // bottom glow
  const g2=ctx.createRadialGradient(W/2,H,20,W/2,H,420);
  g2.addColorStop(0,'rgba(77,208,225,0.12)');g2.addColorStop(1,'rgba(77,208,225,0)');
  ctx.fillStyle=g2;ctx.fillRect(0,H-220,W,220);
}

function drawBrick(br){
  if(!br.alive)return;
  ctx.save();
  const cracked = br.hp< br.maxhp;
  const pad=2;
  roundRect(br.x+pad,br.y+pad,br.w-pad*2,br.h-pad*2,5);
  const grad=ctx.createLinearGradient(br.x,br.y,br.x,br.y+br.h);
  grad.addColorStop(0,br.color);grad.addColorStop(1,shade(br.color,-60));
  ctx.fillStyle=grad;ctx.fill();
  // top highlight
  ctx.fillStyle='rgba(255,255,255,0.28)';
  roundRect(br.x+pad+3,br.y+pad,br.w-pad*2-6,4,2);ctx.fill();
  if(cracked){
    // crack lines
    ctx.strokeStyle='rgba(0,0,0,0.55)';ctx.lineWidth=1.6;
    const s=br.seed;
    ctx.beginPath();
    ctx.moveTo(br.x+br.w*0.3+Math.sin(s)*4, br.y+2);
    ctx.lineTo(br.x+br.w*0.42+Math.sin(s)*8, br.y+br.h*0.45);
    ctx.lineTo(br.x+br.w*0.3+Math.sin(s)*12, br.y+br.h-2);
    ctx.moveTo(br.x+br.w*0.66+Math.cos(s)*6, br.y+2);
    ctx.lineTo(br.x+br.w*0.58+Math.cos(s)*10, br.y+br.h*0.5);
    ctx.lineTo(br.x+br.w*0.7+Math.cos(s)*14, br.y+br.h-2);
    ctx.stroke();
    ctx.fillStyle='rgba(0,0,0,0.18)';roundRect(br.x+pad,br.y+pad,br.w-pad*2,br.h-pad*2,5);ctx.fill();
  }
  if(br.hasPU){
    // power-up icon badge
    const cx=br.x+br.w-13, cy=br.y+br.h/2;
    ctx.beginPath();ctx.arc(cx,cy,7.5,0,Math.PI*2);
    ctx.fillStyle='rgba(10,14,24,0.85)';ctx.fill();
    ctx.strokeStyle='#ffffff';ctx.lineWidth=1.6;ctx.stroke();
    ctx.fillStyle='#ffffff';
    ctx.font='bold 9px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText('★',cx,cy+0.5);
  }
  if(br.flash>0){
    ctx.fillStyle='rgba(255,255,255,'+(br.flash*0.55)+')';
    roundRect(br.x+pad,br.y+pad,br.w-pad*2,br.h-pad*2,5);ctx.fill();
    br.flash=Math.max(0,br.flash-0.06);
  }
  ctx.restore();
}

function drawPaddle(){
  ctx.save();
  const g=ctx.createLinearGradient(paddle.x,0,paddle.x+paddle.w,0);
  if(wideTimer>0){
    g.addColorStop(0,'#ffd166');g.addColorStop(0.5,'#fff0a8');g.addColorStop(1,'#ffb347');
  }else{
    g.addColorStop(0,'#4dd0e1');g.addColorStop(0.5,'#a9f3ff');g.addColorStop(1,'#4dd0e1');
  }
  ctx.shadowColor=wideTimer>0?'rgba(255,209,102,0.9)':'rgba(77,208,225,0.8)';
  ctx.shadowBlur=18+Math.sin(performance.now()/120)*4;
  roundRect(paddle.x,paddle.y,paddle.w,paddle.h,7);
  ctx.fillStyle=g;ctx.fill();
  ctx.shadowBlur=0;
  // center marker
  ctx.fillStyle='rgba(0,0,0,0.28)';
  roundRect(paddle.x+paddle.w/2-8,paddle.y+3,16,3,1.5);ctx.fill();
  ctx.restore();
  if(wideTimer>0){
    ctx.save();
    ctx.font='bold 12px sans-serif';ctx.fillStyle='#ffd166';ctx.textAlign='center';
    ctx.fillText('WIDE '+wideTimer.toFixed(1)+'s',paddle.x+paddle.w/2,paddle.y-10);
    ctx.restore();
  }
}

function drawBall(b){
  ctx.save();
  // trail
  for(let i=0;i<b.trail.length;i++){
    const t=b.trail[i], k=(i+1)/b.trail.length;
    ctx.beginPath();ctx.arc(t.x,t.y,b.r*k*0.75,0,Math.PI*2);
    ctx.fillStyle='rgba(255,255,255,'+(k*0.16)+')';ctx.fill();
  }
  const g=ctx.createRadialGradient(b.x-b.r*0.3,b.y-b.r*0.3,1,b.x,b.y,b.r);
  g.addColorStop(0,'#ffffff');g.addColorStop(0.45,'#bff3ff');g.addColorStop(1,'#4dd0e1');
  ctx.shadowColor='rgba(77,208,225,0.9)';ctx.shadowBlur=14;
  ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
  ctx.restore();
}

function drawPowerup(p){
  ctx.save();
  ctx.translate(p.x,p.y);ctx.rotate(p.rot);
  const color = p.type==='wide' ? '#ffd166' : '#ff7ae0';
  ctx.shadowColor=color;ctx.shadowBlur=14;
  ctx.beginPath();
  for(let i=0;i<6;i++){
    const a=i*Math.PI/3-Math.PI/2;
    const x=Math.cos(a)*p.r, y=Math.sin(a)*p.r;
    if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
  }
  ctx.closePath();
  ctx.fillStyle=color;ctx.fill();
  ctx.shadowBlur=0;
  ctx.fillStyle='#0b0f1a';ctx.font='bold 13px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.fillText(p.type==='wide'?'W':'×3',0,0.5);
  ctx.restore();
}

function draw(){
  ctx.save();
  if(shake>0)ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);
  drawBackground();
  // walls
  ctx.fillStyle='rgba(120,170,255,0.18)';
  ctx.fillRect(0,0,W,3);ctx.fillRect(0,0,3,H);ctx.fillRect(W-3,0,3,H);
  for(const br of bricks)drawBrick(br);
  for(const p of powerups)drawPowerup(p);
  for(const b of balls)drawBall(b);
  drawPaddle();
  for(const q of particles){
    ctx.globalAlpha=Math.max(0,q.life/q.max);
    ctx.fillStyle=q.color;
    ctx.fillRect(q.x-q.size/2,q.y-q.size/2,q.size,q.size);
  }
  ctx.globalAlpha=1;
  for(let i=texts.length-1;i>=0;i--){
    const t=texts[i];
    t.y-=40*0.016; t.life-=0.016;
    ctx.save();ctx.globalAlpha=Math.max(0,t.life/1.2);
    ctx.fillStyle=t.c;ctx.font='bold 16px sans-serif';ctx.textAlign='center';
    ctx.fillText(t.t,t.x,t.y);ctx.restore();
    if(t.life<=0)texts.splice(i,1);
  }
  if(flash>0){
    ctx.fillStyle='rgba(255,60,90,'+(flash*0.35)+')';ctx.fillRect(0,0,W,H);
  }
  ctx.restore();
}

// ---------- Demo AI (auto play in menu) ----------
function demoAI(dt){
  // move paddle toward lowest descending ball
  let target=null;
  for(const b of balls){
    if(b.vy>0 && (!target || b.y>target.y)) target=b;
  }
  if(!target) target=balls[0];
  if(target){
    const desired=target.x-paddle.w/2;
    paddle.x += (desired-paddle.x)*Math.min(1,dt*4.5);
  }
  // launch automatically
  if(state==='serve'){ serveTimer+=0.016; if(serveTimer>0.6)launch(); }
}

// ---------- Loop ----------
let last=performance.now(), demo=false;
function frame(now){
  let dt=(now-last)/1000; last=now;
  dt=Math.min(dt,0.033);
  if(demo){
    demoAI(dt);
    update(dt);
  }else{
    update(dt);
  }
  draw();
  requestAnimationFrame(frame);
}

// Start in demo mode so something is visible immediately
resetLevel();
demo=true;
showOverlay('Neon Breakout','Move the mouse or use <kbd>←</kbd><kbd>→</kbd> to steer. Edge hits = sharp angles. Catch the <span style="color:#ffd166">★</span> power-ups.','Start Game');
requestAnimationFrame(frame);

// Stop demo when user starts
const origStart=startGame;
function handleStart(){ demo=false; origStart(); }
ovBtn.addEventListener('click',handleStart);
overlay.addEventListener('click',(e)=>{ if(e.target===ovBtn)return; if(state==='menu'||state==='over'||state==='win')handleStart(); else if(state==='pause'){state='play';hideOverlay();} });

// If user moves mouse or presses keys, exit demo into real game
window.addEventListener('mousemove',()=>{ if(demo){demo=false; if(state==='menu'||state==='over'||state==='win'){origStart();} } });
window.addEventListener('keydown',()=>{ if(demo){demo=false; if(state==='menu'||state==='over'||state==='win'){origStart();} } },true);

// Auto restart demo after game over in demo mode
setInterval(()=>{ if(demo && (state==='over'||state==='win')){ score=0;lives=3;level=1;resetLevel(); hideOverlay(); } },2500);

function shade(hex,amt){
  const n=parseInt(hex.slice(1),16);
  let r=(n>>16)+amt,g=((n>>8)&255)+amt,b=(n&255)+amt;
  r=Math.max(0,Math.min(255,r));g=Math.max(0,Math.min(255,g));b=Math.max(0,Math.min(255,b));
  return '#'+((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1);
}
})();
</script>
</body>
</html>