Models 44

Qwen3.8 Flash

by QwenReasoningClean
Open in new window

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

How it went

Total time
1 min 45 s
Cost
$0.0039
Output tokens
8,238
Spent thinking
0 tokens
Speed
82.1 tok/s
Console errors
0
First token after
4.8 s
First visible text after
4.8 s
Prompt tokens
535
Code size
21.2 KB
Finish reason
stop
Served by
Alibaba
Model id
qwen/qwen3.8-flash
Reasoning sent
{"effort":"none"}
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 · 547 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>
  *{margin:0;padding:0;box-sizing:border-box;}
  html,body{height:100%;}
  body{
    background:#070b16;
    display:flex;align-items:center;justify-content:center;
    font-family:'Segoe UI',system-ui,-apple-system,sans-serif;
    overflow:hidden;color:#eaf2ff;user-select:none;-webkit-user-select:none;
  }
  #wrap{position:relative;width:min(94vw,calc(92vh*0.75));aspect-ratio:3/4;}
  canvas{display:block;width:100%;height:100%;border-radius:16px;background:#0a1122;
    box-shadow:0 24px 70px rgba(0,0,0,.6),0 0 0 1px rgba(120,160,255,.12),0 0 60px rgba(60,120,255,.12);}
</style>
</head>
<body>
<div id="wrap"><canvas id="game"></canvas></div>
<script>
(function(){
"use strict";

/* ---------- constants ---------- */
const W=600,H=800;
const BRICKS_PER_ROW=10, ROWS=6;
const SIDE_PAD=24, TOP_OFFSET=96, GAP=6;
const BRICK_W=(W-SIDE_PAD*2-GAP*(BRICKS_PER_ROW-1))/BRICKS_PER_ROW; // 50
const BRICK_H=26;
const PADDLE_W=104, PADDLE_H=16, PADDLE_Y=H-58, BALL_R=8, BASE_SPEED=370;
const MAX_BALLS=7;
const COLS=['#ff4d6d','#ff8c42','#ffd23f','#3ddc97','#3ab0ff','#b06bff'];
const POWERUP_COLORS={wide:'#3ab0ff',multi:'#ffd23f'};
const FONT="'Segoe UI',system-ui,sans-serif";

/* ---------- canvas setup ---------- */
const cv=document.getElementById('game');
cv.width=W; cv.height=H;
const ctx=cv.getContext('2d');
function fitCanvas(){
  const dpr=Math.min(window.devicePixelRatio||1,2);
  cv.width=Math.round(W*dpr); cv.height=Math.round(H*dpr);
  ctx.setTransform(dpr,0,0,dpr,0,0);
}
fitCanvas(); window.addEventListener('resize',fitCanvas);

/* ---------- audio (optional, guarded) ---------- */
let AC=null;
function ensureAudio(){ if(!AC){ try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){AC=null;} } if(AC&&AC.state==='suspended'){AC.resume().catch(()=>{});} }
function tone(f,d,type,g,slideTo){
  if(!AC)return;
  try{
    const o=AC.createOscillator(),ga=AC.createGain();
    o.type=type||'square'; o.frequency.value=f;
    if(slideTo)o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo),AC.currentTime+d);
    ga.gain.setValueAtTime(g||0.06,AC.currentTime);
    ga.gain.exponentialRampToValueAtTime(0.0001,AC.currentTime+d);
    o.connect(ga).connect(AC.destination); o.start(); o.stop(AC.currentTime+d);
  }catch(e){}
}
function sfx(n){
  switch(n){
    case 'wall':tone(240,0.05,'square',0.035);break;
    case 'paddle':tone(330,0.07,'square',0.05,430);break;
    case 'brick':tone(560,0.05,'triangle',0.05,760);break;
    case 'crack':tone(190,0.06,'square',0.04);break;
    case 'powerup':tone(520,0.09,'sine',0.06,900);setTimeout(()=>tone(820,0.12,'sine',0.05,1250),70);break;
    case 'life':tone(300,0.2,'sawtooth',0.06,90);break;
    case 'win':[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.18,'triangle',0.06),i*120));break;
    case 'over':[400,300,200,120].forEach((f,i)=>setTimeout(()=>tone(f,0.22,'sawtooth',0.06,f*0.7),i*150));break;
  }
}

/* ---------- state ---------- */
const game={state:'menu',score:0,lives:3,best:0};
let bricks=[],balls=[],particles=[],powerups=[];
const paddle={x:(W-PADDLE_W)/2,w:PADDLE_W,h:PADDLE_H,y:PADDLE_Y,targetX:null,glow:0};
const powerupsActive={wide:0,multi:0};
let shake=0,flash=0;
let demoTimer=0,serveTimer=0,ballSpeedMul=1;
const keys={left:false,right:false};
let lastTime=performance.now();

/* ---------- helpers ---------- */
function clamp(v,a,b){return v<a?a:v>b?b:v;}
function rand(a,b){return a+Math.random()*(b-a);}
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 hexA(hex,a){const n=parseInt(hex.slice(1),16);return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${a})`;}
function mix(h,t){const n=parseInt(h.slice(1),16);const r=(n>>16)&255,g=(n>>8)&255,b=n&255;return `rgb(${Math.round(r+(t-r)*0.5)},${Math.round(g+(t-g)*0.5)},${Math.round(b+(t-b)*0.5)})`;}
function lightCol(c){return mix(c,255);}
function darkCol(c){return mix(c,0);}
function roundRectFill(x,y,w,h,r,fill){rr(x,y,w,h,r);ctx.fillStyle=fill;ctx.fill();}

/* ---------- level ---------- */
function makeBall(cx,cy,vx,vy){return {x:cx,y:cy,vx,vy,r:BALL_R,trail:[],active:true,dead:false,justBounced:false};}
function buildLevel(){
  bricks=[];
  for(let r=0;r<ROWS;r++)for(let c=0;c<BRICKS_PER_ROW;c++){
    const x=SIDE_PAD+c*(BRICK_W+GAP), y=TOP_OFFSET+r*(BRICK_H+GAP);
    let hp=1; if(r===0||r===1)hp=2; else if(Math.random()<0.22)hp=2;
    bricks.push({x,y,w:BRICK_W,h:BRICK_H,color:COLS[r%COLS.length],hp,maxHp:hp,alive:true,powerup:null});
  }
  const pool=[];
  bricks.forEach((b,i)=>{if(b.alive&&b.hp===1)pool.push(i);});
  for(let i=0;i<4&&pool.length;i++){const idx=Math.floor(Math.random()*pool.length);bricks[pool[idx]].powerup=Math.random()<0.5?'wide':'multi';pool.splice(idx,1);}
}
function resetGame(){
  game.score=0; game.lives=3;
  buildLevel();
  particles=[]; powerups=[]; balls=[];
  powerupsActive.wide=0; powerupsActive.multi=0;
  paddle.w=PADDLE_W; paddle.x=(W-paddle.w)/2; paddle.targetX=null;
  serveTimer=0.9; ballSpeedMul=1;
  game.state='playing';
}

/* ---------- serving & spawning ---------- */
function spawnServeBall(){
  const cx=paddle.x+paddle.w/2, cy=paddle.y-BALL_R-2;
  const vx=rand(-140,140), vy=-BASE_SPEED;
  balls=[makeBall(cx,cy,vx,vy)];
}
function spawnPowerup(b){
  powerups.push({x:b.x+b.w/2,y:b.y+b.h/2,vy:135,type:b.powerup,r:15,spin:0});
}
function applyPowerup(p){
  if(p.type==='wide'){powerupsActive.wide=10;sfx('powerup');}
  else{multiball();sfx('powerup');}
}
function multiball(){
  const src=balls.find(b=>b.active&&!b.dead)||null;
  const base=src?{x:src.x,y:src.y}: {x:paddle.x+paddle.w/2,y:paddle.y-BALL_R-2};
  const spd=BASE_SPEED*ballSpeedMul;
  let added=0;
  [-0.6,0.6].forEach(off=>{
    if(balls.length>=MAX_BALLS)return;
    const ang=Math.atan2(base.vy,base.vx)+off;
    balls.push(makeBall(base.x,base.y,Math.cos(ang)*spd,Math.sin(ang)*spd));
    added++;
  });
  if(added===0&&balls.length<MAX_BALLS){
    balls.push(makeBall(base.x,base.y,rand(-160,160),-spd));
  }
}

/* ---------- collisions ---------- */
function hitBrick(b,bx,by,prevX,prevY,ball){
  b.hp--; game.score+=10;
  if(b.hp<=0){
    b.alive=false; game.score+=15; spawnParticles(b,true);
    if(b.powerup)spawnPowerup(b);
    sfx('brick'); flash=0.12;
  }else{
    spawnParticles(b,false); sfx('crack');
  }
  const dx=bx-prevX, dy=by-prevY, len=Math.hypot(dx,dy)||1;
  ball.vx=dx/len*Math.abs(ball.vx); ball.vy=dy/len*Math.abs(ball.vy);
  ball.justBounced=true;
}
function resolveBallBrick(ball,b){
  const overlapX=Math.min(ball.x+ball.r,b.x+b.w)-Math.max(ball.x-ball.r,b.x);
  const overlapY=Math.min(ball.y+ball.r,b.y+b.h)-Math.max(ball.y-ball.r,b.y);
  if(overlapX<=0||overlapY<=0)return false;
  const prevX=ball.x-ball.vx*dt, prevY=ball.y-ball.vy*dt;
  if(overlapX<overlapY){
    if(ball.x<b.x+b.w/2){ball.x=b.x-ball.r;ball.vx=-Math.abs(ball.vx);}
    else{ball.x=b.x+b.w+ball.r;ball.vx=Math.abs(ball.vx);}
    hitBrick(b,ball.x,ball.y,prevX,prevY,ball); return true;
  }else{
    if(ball.y<b.y+b.h/2){ball.y=b.y-ball.r;ball.vy=-Math.abs(ball.vy);}
    else{ball.y=b.y+b.h+ball.r;ball.vy=Math.abs(ball.vy);}
    hitBrick(b,ball.x,ball.y,prevX,prevY,ball); return true;
  }
}
function collidePaddle(ball){
  if(ball.vy<=0)return;
  const p=paddle;
  if(ball.y+ball.r>=p.y && ball.y-ball.r<=p.y+p.h && ball.x>=p.x-ball.r && ball.x<=p.x+p.w+ball.r){
    const rel=clamp((ball.x-(p.x+p.w/2))/(p.w/2),-1,1);
    const ang=rel*(Math.PI*0.42);
    const sp=BASE_SPEED*ballSpeedMul;
    ball.vx=Math.sin(ang)*sp; ball.vy=-Math.cos(ang)*sp;
    ball.y=p.y-ball.r-0.5; ball.justBounced=true;
    p.glow=1; sfx('paddle');
  }
}

/* ---------- particles ---------- */
function spawnParticles(b,big){
  const col=b.color, n=big?18:7;
  for(let i=0;i<n;i++){
    particles.push({
      x:b.x+Math.random()*b.w, y:b.y+Math.random()*b.h,
      vx:rand(-150,150), vy:rand(-220,-40), g:520,
      life:rand(0.4,0.9), maxLife:0, size:rand(2,big?5:4), color:col
    });
    particles[particles.length-1].maxLife=particles[particles.length-1].life;
  }
}
function updateParticles(dt){
  for(const p of particles){p.life-=dt;p.vy+=p.g*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;}
  particles=particles.filter(p=>p.life>0);
}

/* ---------- AI ---------- */
function aiTarget(){
  let best=null,bestT=Infinity;
  for(const b of balls){
    if(!b.active||b.dead||b.vy<=0)continue;
    const t=(paddle.y-BALL_R-b.y)/b.vy; if(t<=0)continue;
    let px=b.x+b.vx*t;
    const span=W-2*BALL_R;
    let m=(px-BALL_R)%span; if(m<0)m+=span;
    const cyc=m/span;
    px=BALL_R+m; if(cyc>0.5)px=W-BALL_R-m;
    const cost=t+Math.abs(px-(paddle.x+paddle.w/2))/1400;
    if(cost<bestT){bestT=cost;best=px;}
  }
  if(best==null)best=paddle.x+paddle.w/2;
  return best;
}

/* ---------- update ---------- */
function update(dt){
  if(game.state!=='playing')return;

  // paddle control
  if(demoMode()){paddle.targetX=aiTarget()-paddle.w/2;}
  else{
    if(keys.left)paddle.targetX=(paddle.targetX==null?paddle.x:paddle.targetX)-640*dt;
    if(keys.right)paddle.targetX=(paddle.targetX==null?paddle.x:paddle.targetX)+640*dt;
    if(paddle.targetX!=null)paddle.targetX=clamp(paddle.targetX,0,W-paddle.w);
  }
  if(paddle.targetX!=null){
    paddle.x+=(paddle.targetX-paddle.x)*Math.min(1,dt*18);
    if(Math.abs(paddle.targetX-paddle.x)<0.4)paddle.x=paddle.targetX;
  }
  paddle.x=clamp(paddle.x,0,W-paddle.w);
  paddle.glow*=Math.pow(0.0008,dt);

  // power-up timers
  if(powerupsActive.wide>0){
    powerupsActive.wide-=dt;
    if(powerupsActive.wide<=0){
      powerupsActive.wide=0;
      const nc=(paddle.x+paddle.w/2); paddle.w=PADDLE_W; paddle.x=clamp(nc-paddle.w/2,0,W-paddle.w);
    }
  }

  // serve / speed
  if(serveTimer>0){serveTimer-=dt;if(serveTimer<=0)spawnServeBall();}
  ballSpeedMul=Math.min(1.7,1+game.score/2600);

  // balls
  for(const ball of balls){
    if(!ball.active||ball.dead)continue;
    ball.trail.unshift({x:ball.x,y:ball.y}); if(ball.trail.length>12)ball.trail.pop();
    const steps=Math.max(1,Math.ceil(Math.max(Math.abs(ball.vx),Math.abs(ball.vy))*dt/6));
    const sdt=dt/steps;
    for(let s=0;s<steps;s++){
      ball.justBounced=false;
      ball.x+=ball.vx*sdt; ball.y+=ball.vy*sdt;
      // walls
      if(ball.x-ball.r<0){ball.x=ball.r;ball.vx=Math.abs(ball.vx);ball.justBounced=true;sfx('wall');}
      if(ball.x+ball.r>W){ball.x=W-ball.r;ball.vx=-Math.abs(ball.vx);ball.justBounced=true;sfx('wall');}
      if(ball.y-ball.r<TOP_OFFSET-40){ball.y=TOP_OFFSET-40+ball.r;ball.vy=Math.abs(ball.vy);ball.justBounced=true;sfx('wall');}
      // bricks
      for(const b of bricks){
        if(!b.alive)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){
          if(resolveBallBrick(ball,b))break;
        }
      }
      // paddle
      collidePaddle(ball);
      // bottom
      if(ball.y-ball.r>H){ball.dead=true;break;}
      // anti-stuck
      if(!ball.justBounced){
        if(ball.vy<-BASE_SPEED*0.32)ball.vy=-BASE_SPEED*0.32;
        if(ball.vy>BASE_SPEED*0.32)ball.vy=BASE_SPEED*0.32;
      }
      const sp=Math.hypot(ball.vx,ball.vy), target=BASE_SPEED*ballSpeedMul;
      if(sp>1){const k=target/sp;ball.vx*=k;ball.vy*=k;}
    }
  }
  balls=balls.filter(b=>!b.dead);

  // powerups falling
  for(const p of powerups){
    p.y+=p.vy*dt; p.spin+=dt*4;
    if(p.y+p.r>=paddle.y && p.y-p.r<=paddle.y+paddle.h && p.x>=paddle.x && p.x<=paddle.x+paddle.w){
      applyPowerup(p); p.collected=true;
    }
  }
  powerups=powerups.filter(p=>!p.collected && p.y<H+30);

  // lose a life
  if(balls.length===0){
    game.lives--; sfx('life'); shake=0.5;
    if(game.lives<=0){game.best=Math.max(game.best,game.score);game.state='over';sfx('over');}
    else{powerupsActive.wide=0;paddle.w=PADDLE_W;paddle.x=(W-paddle.w)/2;paddle.targetX=null;serveTimer=1.1;}
  }

  // win
  if(bricks.every(b=>!b.alive)){game.best=Math.max(game.best,game.score);game.state='win';sfx('win');}

  updateParticles(dt);
  shake=Math.max(0,shake-dt);
  flash=Math.max(0,flash-dt);
}
function demoMode(){return game.state==='playing'&&paddle.targetX==null;}

/* ---------- render ---------- */
function drawBackground(){
  const g=ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#0c1730');g.addColorStop(0.5,'#0a1122');g.addColorStop(1,'#070b16');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  ctx.save();ctx.globalAlpha=0.5;
  const rg=ctx.createRadialGradient(W/2,TOP_OFFSET,20,W/2,TOP_OFFSET,420);
  rg.addColorStop(0,'rgba(70,110,220,0.22)');rg.addColorStop(1,'rgba(70,110,220,0)');
  ctx.fillStyle=rg;ctx.fillRect(0,0,W,H);ctx.restore();
  ctx.save();ctx.strokeStyle='rgba(120,160,255,0.05)';ctx.lineWidth=1;
  for(let x=0;x<=W;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
  ctx.restore();
}
function drawHUD(){
  ctx.textBaseline='alphabetic';
  ctx.font='bold 22px '+FONT;ctx.fillStyle='#fff';ctx.textAlign='left';
  ctx.fillText(String(game.score).padStart(5,'0'),18,40);
  ctx.font='11px '+FONT;ctx.fillStyle='rgba(180,200,230,0.6)';
  ctx.fillText('SCORE',18,54);
  ctx.textAlign='center';ctx.font='bold 13px '+FONT;ctx.fillStyle='rgba(180,200,230,0.5)';
  ctx.fillText('NEON BREAKOUT',W/2,34);
  ctx.textAlign='right';ctx.font='11px '+FONT;ctx.fillStyle='rgba(180,200,230,0.6)';
  ctx.fillText('BEST '+String(game.best).padStart(5,'0'),W-18,40);
  // lives
  ctx.textAlign='left';ctx.font='11px '+FONT;ctx.fillStyle='rgba(180,200,230,0.6)';
  ctx.fillText('LIVES',18,72);
  for(let i=0;i<3;i++){
    const x=64+i*20, y=67, alive=i<game.lives;
    ctx.beginPath();ctx.arc(x,y,7,0,7);
    if(alive){ctx.fillStyle='#3ddc97';ctx.shadowColor='#3ddc97';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;}
    else{ctx.strokeStyle='rgba(120,140,170,0.5)';ctx.lineWidth=1.5;ctx.stroke();}
  }
  // active power-ups
  let px=18,py=H-14;
  if(powerupsActive.wide>0){
    ctx.font='bold 11px '+FONT;ctx.fillStyle=POWERUP_COLORS.wide;ctx.textAlign='left';
    ctx.fillText('◈ WIDE '+Math.ceil(powerupsActive.wide)+'s',px,py);
    px+=ctx.measureText('◈ WIDE '+Math.ceil(powerupsActive.wide)+'s').width+16;
  }
  if(balls.length>1){
    ctx.font='bold 11px '+FONT;ctx.fillStyle=POWERUP_COLORS.multi;ctx.textAlign='left';
    ctx.fillText('✦ ×'+balls.length+' BALLS',px,py);
  }
}
function drawCracks(b){
  const cx=b.x+b.w/2, cy=b.y+b.h/2;
  ctx.strokeStyle='rgba(0,0,0,0.5)';ctx.lineWidth=1.4;
  ctx.beginPath();
  ctx.moveTo(cx-8,cy-9);ctx.lineTo(cx+3,cy-1);ctx.lineTo(cx-4,cy+8);
  ctx.moveTo(cx+3,cy-1);ctx.lineTo(cx+11,cy+3);
  ctx.moveTo(cx+3,cy-1);ctx.lineTo(cx+9,cy-8);
  ctx.stroke();
}
function drawBricks(){
  for(const b of bricks){
    if(!b.alive)continue;
    const tough=b.maxHp===2, cracked=tough&&b.hp===1;
    ctx.save();
    ctx.shadowColor=b.color;ctx.shadowBlur=12;
    const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
    g.addColorStop(0,lightCol(b.color));g.addColorStop(0.5,b.color);g.addColorStop(1,darkCol(b.color));
    rr(b.x,b.y,b.w,b.h,5);ctx.fillStyle=g;ctx.fill();
    ctx.shadowBlur=0;
    ctx.strokeStyle='rgba(255,255,255,0.35)';ctx.lineWidth=1;
    ctx.beginPath();ctx.moveTo(b.x+3,b.y+2);ctx.lineTo(b.x+b.w-3,b.y+2);ctx.stroke();
    ctx.strokeStyle='rgba(0,0,0,0.28)';
    ctx.beginPath();ctx.moveTo(b.x+2,b.y+b.h-2);ctx.lineTo(b.x+b.w-2,b.y+b.h-2);ctx.stroke();
    if(tough){
      ctx.strokeStyle='rgba(0,0,0,0.45)';ctx.lineWidth=2;
      rr(b.x+3,b.y+3,b.w-6,b.h-6,3);ctx.stroke();
      ctx.fillStyle='rgba(255,255,255,0.18)';
      ctx.fillRect(b.x+b.w/2-8,b.y+b.h/2-1,16,2);
      ctx.fillRect(b.x+b.w/2-1,b.y+b.h/2-8,2,16);
    }
    if(cracked)drawCracks(b);
    if(b.powerup){
      ctx.beginPath();ctx.arc(b.x+b.w/2,b.y+b.h/2,7,0,7);
      ctx.fillStyle='rgba(255,255,255,0.16)';ctx.fill();
      ctx.strokeStyle='rgba(255,255,255,0.55)';ctx.lineWidth=1.2;ctx.stroke();
      ctx.fillStyle='#fff';ctx.font='bold 11px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
      ctx.fillText(b.powerup==='wide'?'⇔':'✦',b.x+b.w/2,b.y+b.h/2+0.5);
      ctx.textBaseline='alphabetic';
    }
    ctx.restore();
  }
}
function drawPaddle(){
  const p=paddle, glow=Math.max(p.glow,powerupsActive.wide>0?0.45:0);
  ctx.save();
  ctx.shadowColor=glow>0.05?'#3ab0ff':'#8fd0ff';ctx.shadowBlur=10+glow*26;
  const g=ctx.createLinearGradient(0,p.y,0,p.y+p.h);
  g.addColorStop(0,'#dff3ff');g.addColorStop(0.45,'#7cc4ff');g.addColorStop(1,'#2f7fe0');
  rr(p.x,p.y,p.w,p.h,p.h/2);ctx.fillStyle=g;ctx.fill();
  ctx.shadowBlur=0;
  ctx.fillStyle='rgba(255,255,255,0.5)';
  rr(p.x+6,p.y+2,p.w-12,3,2);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.14)';
  ctx.fillRect(p.x+p.w/2-1,p.y+3,2,p.h-6);
  ctx.restore();
}
function drawBall(b){
  for(let i=b.trail.length-1;i>=0;i--){
    const t=b.trail[i], f=(i+1)/b.trail.length;
    ctx.beginPath();ctx.arc(t.x,t.y,b.r*f*0.9,0,7);
    ctx.fillStyle=`rgba(180,225,255,${0.05*f})`;ctx.fill();
  }
  ctx.save();
  ctx.shadowColor='#bfe6ff';ctx.shadowBlur=16;
  const g=ctx.createRadialGradient(b.x-b.r*0.35,b.y-b.r*0.35,1,b.x,b.y,b.r);
  g.addColorStop(0,'#ffffff');g.addColorStop(0.6,'#cdeeff');g.addColorStop(1,'#7fb8ff');
  ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fillStyle=g;ctx.fill();
  ctx.restore();
}
function drawPowerups(){
  for(const p of powerups){
    const col=POWERUP_COLORS[p.type];
    ctx.save();
    ctx.translate(p.x,p.y);
    ctx.shadowColor=col;ctx.shadowBlur=14;
    ctx.rotate(Math.sin(p.spin)*0.25);
    rr(-p.r,-p.r,p.r*2,p.r*2,6);
    const g=ctx.createLinearGradient(0,-p.r,0,p.r);
    g.addColorStop(0,lightCol(col));g.addColorStop(1,col);
    ctx.fillStyle=g;ctx.fill();
    ctx.shadowBlur=0;
    ctx.strokeStyle='rgba(255,255,255,0.6)';ctx.lineWidth=1.5;ctx.stroke();
    ctx.fillStyle='#fff';ctx.font='bold 16px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(p.type==='wide'?'⇔':'✦',0,1);
    ctx.restore();
  }
}
function drawParticles(){
  for(const p of particles){
    const a=clamp(p.life/p.maxLife,0,1);
    ctx.globalAlpha=a;ctx.fillStyle=p.color;
    ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
  }
  ctx.globalAlpha=1;
}
function overlay(alpha){ctx.fillStyle=`rgba(5,8,18,${alpha})`;ctx.fillRect(0,0,W,H);}
function centerText(txt,size,color,y,weight){
  ctx.font=(weight||'bold')+' '+size+'px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.fillStyle=color;ctx.fillText(txt,W/2,y);
}
function drawMenu(){
  overlay(0.35);
  ctx.save();ctx.shadowColor='#3ab0ff';ctx.shadowBlur=30;
  centerText('NEON BREAKOUT',48,'#eaf5ff',150);ctx.restore();
  centerText('Break every brick • Catch the drops',15,'rgba(190,210,240,0.8)',200);
  const pulse=0.5+0.5*Math.sin(performance.now()/300);
  ctx.globalAlpha=0.55+0.45*pulse;
  centerText('▶  CLICK OR PRESS A KEY TO START',20,'#ffd23f',258);
  ctx.globalAlpha=1;
  centerText('MOUSE or ← → move the paddle   •   SPACE to launch',13,'rgba(170,190,220,0.7)',300);
  centerText('◈ Wide paddle   ✦ Multi-ball',13,'rgba(170,190,220,0.7)',324);
}
function drawEnd(win){
  overlay(0.6);
  ctx.save();ctx.shadowColor=win?'#3ddc97':'#ff4d6d';ctx.shadowBlur=30;
  centerText(win?'VICTORY!':'GAME OVER',46,win?'#c9ffe6':'#ffd0d8',300);ctx.restore();
  centerText('Score  '+String(game.score).padStart(5,'0'),22,'#fff',360);
  centerText('Best  '+String(game.best).padStart(5,'0'),15,'rgba(190,210,240,0.8)',392);
  const pulse=0.5+0.5*Math.sin(performance.now()/300);
  ctx.globalAlpha=0.55+0.45*pulse;
  centerText('▶  CLICK OR PRESS R TO PLAY AGAIN',18,'#ffd23f',452);
  ctx.globalAlpha=1;
}
function render(){
  ctx.clearRect(0,0,W,H);
  ctx.save();
  if(shake>0){const m=shake*10;ctx.translate(rand(-m,m),rand(-m,m));}
  drawBackground();
  drawBricks();
  drawPowerups();
  drawPaddle();
  for(const b of balls)drawBall(b);
  drawParticles();
  ctx.restore();
  drawHUD();
  if(flash>0){ctx.fillStyle=`rgba(255,255,255,${flash*0.5})`;ctx.fillRect(0,0,W,H);}
  if(game.state==='menu')drawMenu();
  else if(game.state==='over')drawEnd(false);
  else if(game.state==='win')drawEnd(true);
}

/* ---------- loop ---------- */
function frame(now){
  let dt=(now-lastTime)/1000; lastTime=now;
  if(dt>0.033)dt=0.033; if(dt<0)dt=0;
  update(dt); render();
  requestAnimationFrame(frame);
}

/* ---------- input ---------- */
function startFromMenu(){ensureAudio();resetGame();}
cv.addEventListener('mousemove',e=>{
  if(game.state!=='playing'||demoMode())return;
  const rect=cv.getBoundingClientRect();
  const mx=(e.clientX-rect.left)*(W/rect.width);
  paddle.targetX=clamp(mx-paddle.w/2,0,W-paddle.w);
});
cv.addEventListener('mousedown',()=>{
  ensureAudio();
  if(game.state==='menu')startFromMenu();
  else if(game.state==='playing'){paddle.targetX=null;if(serveTimer>0)serveTimer=0;}
  else if(game.state==='over'||game.state==='win')resetGame();
});
window.addEventListener('keydown',e=>{
  if(['ArrowLeft','ArrowRight','Space'].includes(e.code))e.preventDefault();
  ensureAudio();
  if(e.code==='ArrowLeft')keys.left=true;
  if(e.code==='ArrowRight')keys.right=true;
  if(e.code==='KeyR'){if(game.state==='over'||game.state==='win')resetGame();}
  if(game.state==='menu'){startFromMenu();}
  else if(game.state==='playing'){
    if(e.code==='Space'&&serveTimer>0){serveTimer=0;}
    if(e.code==='ArrowLeft'||e.code==='ArrowRight')paddle.targetX=null;
  }
});
window.addEventListener('keyup',e=>{
  if(e.code==='ArrowLeft')keys.left=false;
  if(e.code==='ArrowRight')keys.right=false;
});

/* ---------- boot ---------- */
buildLevel();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>