Models 44

Claude Haiku 5.5

by AnthropicReasoningClean
Open in new window

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

How it went

Total time
3 min 21 s
Cost
$0.024
Output tokens
48,053
Spent thinking
35,562 tokens
Speed
240.9 tok/s
Console errors
0
First token after
2.0 s
First visible text after
2 min 47 s
Prompt tokens
793
Code size
20.9 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"medium"}
Max output
128,000 tokens
Generated
Nov 10, 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 · 742 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Breakout</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;min-height:100%}
  body{
    background:radial-gradient(ellipse at top,#1b2140 0%,#07080f 70%) fixed;
    color:#e8ecff;
    font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    min-height:100vh;padding:14px 12px;overflow-x:hidden;
  }
  h1{
    margin:0 0 10px;font-size:clamp(20px,4vw,28px);letter-spacing:.3em;font-weight:800;
    background:linear-gradient(90deg,#ff5f6d,#ffc371,#47e891,#24c6dc);
    -webkit-background-clip:text;background-clip:text;color:transparent;
  }
  #wrap{position:relative;width:min(100%,800px);touch-action:none}
  canvas{
    display:block;width:100%;height:auto;aspect-ratio:4/3;border-radius:12px;
    box-shadow:0 0 0 1px #2a3160,0 20px 60px rgba(0,0,0,.6);
    background:#0b0d1a;cursor:none;
  }
  .overlay{
    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    background:rgba(5,7,16,.72);border-radius:12px;cursor:pointer;padding:10px;
  }
  .overlay.soft{background:rgba(5,7,16,.28)}
  .overlay.hidden{display:none}
  .card{
    text-align:center;padding:22px 26px;border-radius:16px;max-width:88%;
    background:rgba(15,18,36,.78);border:1px solid rgba(255,255,255,.12);
    box-shadow:0 10px 40px rgba(0,0,0,.5);
  }
  #ovTitle{margin:0 0 8px;font-size:clamp(24px,6vw,40px);font-weight:900;letter-spacing:.08em}
  #ovSub{margin:0 0 14px;font-size:clamp(14px,3.5vw,18px);color:#cbd5e1}
  #ovSub.pulse{animation:pulse 1.4s ease-in-out infinite;color:#fde68a;font-weight:600}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
  #ovBtn{
    font:inherit;font-weight:700;font-size:16px;color:#0b0d1a;cursor:pointer;
    padding:10px 26px;border:0;border-radius:999px;margin-bottom:12px;
    background:linear-gradient(90deg,#fde047,#f59e0b);
    box-shadow:0 6px 20px rgba(245,158,11,.35);
  }
  #ovBtn:hover{filter:brightness(1.08)}
  #ovBtn[hidden]{display:none}
  #ovHint{margin:0;font-size:12px;color:#94a3b8}
  .foot{margin-top:10px;font-size:12px;color:#64748b;text-align:center}
</style>
</head>
<body>
<h1>BREAKOUT</h1>
<div id="wrap">
  <canvas id="c" width="800" height="600"></canvas>
  <div id="ov" class="overlay">
    <div class="card">
      <h2 id="ovTitle"></h2>
      <p id="ovSub"></p>
      <button id="ovBtn" hidden>Restart</button>
      <p id="ovHint"></p>
    </div>
  </div>
</div>
<div class="foot">Mouse or ← → to move · P to pause · Catch the glowing capsules</div>

<script>
(function(){
'use strict';

const W=800,H=600,HUDH=44,TAU=Math.PI*2;
const BW=72,BH=24,GAP=6,COLS=10;
const BX0=(W-(COLS*BW+(COLS-1)*GAP))/2;
const BY0=HUDH+28;
const BASE_PW=110,WIDE_PW=170,PH=12;
const ROW_COL=['#ff5f6d','#ff9f43','#feca57','#1dd1a1','#54a0ff','#a29bfe'];
// n = normal, t = tough (2 hits), W = normal + wide-paddle power-up, M = normal + multi-ball power-up
const ROWS=[
  'tttttttttt',
  'nWnnnnnMnn',
  'ntnnnnnnnt',
  'nnntnntnnn',
  'MnnnnntnnW',
  'nnnnnnnnnn'
];

const cv=document.getElementById('c');
const ctx=cv.getContext('2d');
const wrap=document.getElementById('wrap');
const ovEl=document.getElementById('ov');
const ovTitle=document.getElementById('ovTitle');
const ovSub=document.getElementById('ovSub');
const ovBtn=document.getElementById('ovBtn');
const ovHint=document.getElementById('ovHint');
const dpr=Math.min(window.devicePixelRatio||1,2);
cv.width=W*dpr;cv.height=H*dpr;
ctx.setTransform(dpr,0,0,dpr,0,0);

let state='demo',isDemo=true;
let paddle,balls=[],bricks=[],parts=[],texts=[],powerups=[];
let score=0,lives=3,bricksBroken=0,wideTimer=0,serveTimer=0,shakeT=0;
let hiScore=0,T=0,lastT=performance.now();
let mouseX=null,keyL=false,keyR=false;

const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const pcx=()=>paddle.x+paddle.w/2;
const live=()=>state==='playing'||state==='demo';
function speed(){return Math.min(340+bricksBroken*1.2,470);}

function mix(hex,t,p){
  const n=parseInt(hex.slice(1),16);
  const r=(n>>16)&255,g=(n>>8)&255,b=n&255;
  const f=c=>Math.round(c+(t-c)*p);
  return `rgb(${f(r)},${f(g)},${f(b)})`;
}
const lighten=(h,p)=>mix(h,255,p);
const darken=(h,p)=>mix(h,0,p);

function rr(x,y,w,h,r){
  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();
}

/* ---------- Level / bricks ---------- */
function makeCracks(w,h){
  const out=[];
  for(let i=0;i<3;i++){
    let px=w*(0.2+Math.random()*0.6);
    let py=Math.random()<0.5?0:h;
    const dirY=py===0?1:-1;
    const pts=[[px,py]];
    const n=3+Math.floor(Math.random()*2);
    for(let j=0;j<n;j++){
      px=clamp(px+(Math.random()*2-1)*w*0.12,0,w);
      py=clamp(py+dirY*h*0.32,0,h);
      pts.push([px,py]);
    }
    out.push(pts);
  }
  return out;
}

function buildBricks(){
  const list=[];
  ROWS.forEach((row,r)=>{
    [...row].forEach((ch,c)=>{
      if(ch==='.')return;
      const tough=ch==='t';
      list.push({
        x:BX0+c*(BW+GAP),y:BY0+r*(BH+GAP),w:BW,h:BH,row:r,
        tough,hp:tough?2:1,maxHp:tough?2:1,
        pu:(ch==='W'||ch==='M')?ch:null,
        alive:true,cracks:null,flash:0
      });
    });
  });
  return list;
}

/* ---------- World ---------- */
function resetWorld(){
  paddle={x:(W-BASE_PW)/2,y:H-48,w:BASE_PW,targetW:BASE_PW,h:PH};
  bricks=buildBricks();
  balls=[];parts=[];texts=[];powerups=[];
  score=0;lives=3;bricksBroken=0;wideTimer=0;serveTimer=0.9;shakeT=0;
}

function serveBall(){
  const a=Math.random()*0.7-0.35;
  const sp=speed();
  balls.push({x:pcx(),y:paddle.y-8,vx:Math.sin(a)*sp,vy:-Math.cos(a)*sp,r:7,trail:[]});
}

function startDemo(){
  isDemo=true;
  resetWorld();
  state='demo';
  showOverlay({title:'BREAKOUT',sub:'Click or press a key to start',pulse:true,soft:true,hint:'Mouse or ← → to move'});
}

function startGame(){
  isDemo=false;
  resetWorld();
  state='playing';
  ovEl.classList.add('hidden');
}

function pauseGame(){
  if(state!=='playing')return;
  state='paused';
  showOverlay({title:'PAUSED',sub:'Press P or click to resume',hint:''});
}
function resumeGame(){
  if(state!=='paused')return;
  state='playing';
  ovEl.classList.add('hidden');
}

function showOverlay(o){
  ovEl.classList.remove('hidden');
  ovEl.classList.toggle('soft',!!o.soft);
  ovTitle.textContent=o.title;
  ovSub.textContent=o.sub;
  ovSub.classList.toggle('pulse',!!o.pulse);
  ovBtn.hidden=!o.btn;
  if(o.btn)ovBtn.textContent=o.btn;
  ovHint.textContent=o.hint||'';
}

function roundEnd(win){
  if(isDemo){ // demo just loops silently
    resetWorld();
    return;
  }
  state=win?'win':'over';
  hiScore=Math.max(hiScore,score);
  if(win){
    showOverlay({title:'VICTORY!',sub:`All bricks cleared · Score ${score}`,btn:'Play again',hint:'Press Enter to play again'});
  }else{
    showOverlay({title:'GAME OVER',sub:`Final score: ${score}`,btn:'Restart',hint:'Press Enter to restart'});
  }
}

function loseLife(){
  lives--;
  shakeT=0.35;
  wideTimer=0;
  paddle.targetW=BASE_PW;
  if(lives<=0){roundEnd(false);return;}
  serveTimer=1.0;
  floatText(pcx(),paddle.y-20,'LIFE LOST','#f87171');
}

/* ---------- Effects ---------- */
function spawnBurst(x,y,color,n,power){
  power=power||260;
  for(let i=0;i<n;i++){
    const a=Math.random()*TAU;
    const sp=Math.random()*power;
    const life=0.45+Math.random()*0.6;
    parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-60,life,max:life,c:color,s:1.5+Math.random()*2.8});
  }
}
function floatText(x,y,text,color){
  texts.push({x,y,text,color,life:1.1,max:1.1});
}

/* ---------- Physics ---------- */
function normalizeBall(b){
  const sp=speed();
  const m=Math.hypot(b.vx,b.vy)||1;
  b.vx*=sp/m;b.vy*=sp/m;
  const minV=sp*0.28;
  if(Math.abs(b.vy)<minV){
    b.vy=(b.vy<0?-1:1)*minV;
    const vxm=Math.sqrt(Math.max(0,sp*sp-minV*minV));
    b.vx=(b.vx<0?-1:1)*vxm;
  }
}

function collideWalls(b){
  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<HUDH){b.y=HUDH+b.r;b.vy=Math.abs(b.vy);}
}

function collidePaddle(b){
  const p=paddle,r=b.r;
  const cx=clamp(b.x,p.x,p.x+p.w),cy=clamp(b.y,p.y,p.y+p.h);
  const dx=b.x-cx,dy=b.y-cy,d2=dx*dx+dy*dy;
  if(d2>=r*r)return;
  // Top surface: angle depends on hit position
  if(dy<0||(d2<1e-9&&b.y<p.y+p.h/2)){
    b.y=p.y-r;
    if(b.vy>0){
      const off=clamp((b.x-(p.x+p.w/2))/(p.w/2),-1,1);
      const ang=off*1.15;
      const sp=speed();
      b.vx=Math.sin(ang)*sp;
      b.vy=-Math.cos(ang)*sp;
      spawnBurst(b.x,p.y,'#c7d2fe',5,140);
    }
    return;
  }
  // Side or bottom: generic reflection, never passes through
  let nx,ny,pen;
  if(d2<1e-9){nx=0;ny=1;pen=r;}
  else{const d=Math.sqrt(d2);nx=dx/d;ny=dy/d;pen=r-d;}
  b.x+=nx*pen;b.y+=ny*pen;
  if(Math.abs(nx)>Math.abs(ny))b.vx=(nx>0?1:-1)*Math.abs(b.vx);
  else b.vy=(ny>0?1:-1)*Math.abs(b.vy);
  normalizeBall(b);
}

function collideBricks(b){
  for(const k of bricks){
    if(!k.alive)continue;
    const cx=clamp(b.x,k.x,k.x+k.w),cy=clamp(b.y,k.y,k.y+k.h);
    const dx=b.x-cx,dy=b.y-cy,d2=dx*dx+dy*dy;
    if(d2>=b.r*b.r)continue;
    let nx,ny,pen;
    if(d2>1e-9){
      const d=Math.sqrt(d2);nx=dx/d;ny=dy/d;pen=b.r-d;
    }else{
      // centre already inside: push out through the nearest edge
      const l=b.x-k.x,rt=k.x+k.w-b.x,t=b.y-k.y,bt=k.y+k.h-b.y;
      const m=Math.min(l,rt,t,bt);
      if(m===l){nx=-1;ny=0;}
      else if(m===rt){nx=1;ny=0;}
      else if(m===t){nx=0;ny=-1;}
      else{nx=0;ny=1;}
      pen=b.r+m;
    }
    b.x+=nx*pen;b.y+=ny*pen;
    if(Math.abs(nx)>Math.abs(ny))b.vx=(nx>0?1:-1)*Math.abs(b.vx);
    else b.vy=(ny>0?1:-1)*Math.abs(b.vy);
    normalizeBall(b);
    hitBrick(k);
    return;
  }
}

function hitBrick(k){
  k.flash=1;
  const cx=k.x+k.w/2,cy=k.y+k.h/2;
  if(k.hp>1){
    // tough brick, first hit: crack it
    k.hp--;
    if(!k.cracks)k.cracks=makeCracks(k.w,k.h);
    score+=5;
    spawnBurst(cx,cy,'#e2e8f0',8,200);
    return;
  }
  k.hp=0;k.alive=false;bricksBroken++;
  const pts=k.tough?25:10;
  score+=pts;
  spawnBurst(cx,cy,ROW_COL[k.row],k.tough?30:22,340);
  if(k.pu){
    powerups.push({type:k.pu,x:cx,y:cy+6,vy:120,w:62,h:20,done:false});
  }
  if(bricks.every(x=>!x.alive)){
    roundEnd(true);
  }
}

function updateBalls(dt){
  for(const b of balls){
    const sp=Math.hypot(b.vx,b.vy);
    const steps=Math.max(1,Math.ceil(sp*dt/2)); // <=2px per substep: no tunnelling
    const h=dt/steps;
    for(let s=0;s<steps;s++){
      b.x+=b.vx*h;b.y+=b.vy*h;
      collideWalls(b);
      collidePaddle(b);
      collideBricks(b);
      if(!live())return;
    }
    b.trail.push({x:b.x,y:b.y});
    if(b.trail.length>9)b.trail.shift();
  }
}

function collect(p){
  if(p.type==='W'){
    wideTimer=10;
    paddle.targetW=WIDE_PW;
    floatText(p.x,paddle.y-22,'WIDE PADDLE 10s','#67e8f9');
    spawnBurst(p.x,p.y,'#22d3ee',26,260);
  }else{
    if(balls.length===0)serveBall();
    const src=balls[0];
    let i=0;
    while(balls.length<3){
      const a=(i%2?-0.45:0.45);i++;
      const c=Math.cos(a),s=Math.sin(a);
      balls.push({x:src.x,y:src.y,vx:src.vx*c-src.vy*s,vy:src.vx*s+src.vy*c,r:7,trail:[]});
    }
    floatText(p.x,paddle.y-22,'MULTI-BALL ×3','#fde047');
    spawnBurst(p.x,p.y,'#fde047',26,260);
  }
}

function updatePowerups(dt){
  for(const p of powerups){
    p.y+=p.vy*dt;
    if(!p.done&&
       p.y+p.h/2>=paddle.y&&p.y-p.h/2<=paddle.y+paddle.h&&
       Math.abs(p.x-pcx())<paddle.w/2+p.w/2){
      p.done=true;
      collect(p);
    }
  }
  powerups=powerups.filter(p=>!p.done&&p.y<H+30);
}

function predictX(b){
  const t=Math.max(0,(paddle.y-b.y)/b.vy);
  const minX=b.r,maxX=W-b.r,span=maxX-minX;
  let x=b.x+b.vx*t;
  let u=(x-minX)%(2*span);
  if(u<0)u+=2*span;
  if(u>span)u=2*span-u;
  return minX+u;
}

function aiMove(dt){
  let best=null;
  for(const b of balls)if(b.vy>0&&(!best||b.y>best.y))best=b;
  let tgt=null;
  if(powerups.length&&(!best||best.y<paddle.y-220)){
    tgt=powerups[0].x;
  }else if(best){
    tgt=predictX(best);
  }else{
    tgt=W/2;
  }
  if(tgt!==null){
    const d=tgt-pcx();
    paddle.x+=clamp(d,-560*dt,560*dt);
  }
}

function update(dt){
  T+=dt;
  for(const p of parts){
    p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=520*dt;p.vx*=0.985;p.life-=dt;
  }
  parts=parts.filter(p=>p.life>0);
  for(const t of texts){t.y-=28*dt;t.life-=dt;}
  texts=texts.filter(t=>t.life>0);
  for(const k of bricks)if(k.flash>0)k.flash=Math.max(0,k.flash-dt*4);
  shakeT=Math.max(0,shakeT-dt);

  if(!live())return;

  // paddle control
  if(state==='demo'){
    aiMove(dt);
  }else if(keyL||keyR){
    paddle.x+=((keyR?1:0)-(keyL?1:0))*640*dt;
    mouseX=null;
  }else if(mouseX!==null){
    paddle.x=mouseX-paddle.w/2;
  }

  if(wideTimer>0){
    wideTimer-=dt;
    if(wideTimer<=0){wideTimer=0;paddle.targetW=BASE_PW;}
  }
  paddle.w+=(paddle.targetW-paddle.w)*Math.min(1,dt*10);
  paddle.x=clamp(paddle.x,0,W-paddle.w);

  if(balls.length===0){
    serveTimer-=dt;
    if(serveTimer<=0)serveBall();
  }

  const count=balls.length;
  updateBalls(dt);
  if(!live())return;
  updatePowerups(dt);
  balls=balls.filter(b=>b.y<H+30);
  if(count>0&&balls.length===0)loseLife();
}

/* ---------- Drawing ---------- */
function drawBg(){
  const g=ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#121838');g.addColorStop(1,'#05060d');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  ctx.strokeStyle='rgba(120,140,255,0.05)';ctx.lineWidth=1;
  ctx.beginPath();
  for(let x=0;x<=W;x+=40){ctx.moveTo(x+0.5,0);ctx.lineTo(x+0.5,H);}
  for(let y=0;y<=H;y+=40){ctx.moveTo(0,y+0.5);ctx.lineTo(W,y+0.5);}
  ctx.stroke();
  const dg=ctx.createLinearGradient(0,H-60,0,H);
  dg.addColorStop(0,'rgba(255,60,90,0)');dg.addColorStop(1,'rgba(255,60,90,0.18)');
  ctx.fillStyle=dg;ctx.fillRect(0,H-60,W,60);
}

function drawBrick(k){
  if(!k.alive)return;
  const {x,y,w,h}=k,c=ROW_COL[k.row];
  ctx.save();
  rr(x,y,w,h,5);
  ctx.save();
  ctx.clip();
  if(k.tough){
    const g=ctx.createLinearGradient(0,y,0,y+h);
    g.addColorStop(0,'#5b6384');g.addColorStop(1,'#262b45');
    ctx.fillStyle=g;ctx.fillRect(x,y,w,h);
    ctx.fillStyle=c;ctx.fillRect(x,y,w,5);
    if(k.cracks&&k.hp<k.maxHp){
      ctx.strokeStyle='rgba(8,10,22,0.92)';ctx.lineWidth=1.6;
      for(const pl of k.cracks){
        ctx.beginPath();
        pl.forEach((pt,i)=>{const px=x+pt[0],py=y+pt[1];if(i)ctx.lineTo(px,py);else ctx.moveTo(px,py);});
        ctx.stroke();
      }
      ctx.strokeStyle='rgba(255,255,255,0.18)';ctx.lineWidth=0.8;
      for(const pl of k.cracks){
        ctx.beginPath();
        pl.forEach((pt,i)=>{const px=x+pt[0]+0.7,py=y+pt[1]+0.7;if(i)ctx.lineTo(px,py);else ctx.moveTo(px,py);});
        ctx.stroke();
      }
    }
  }else{
    const g=ctx.createLinearGradient(0,y,0,y+h);
    g.addColorStop(0,lighten(c,0.18));g.addColorStop(1,darken(c,0.22));
    ctx.fillStyle=g;ctx.fillRect(x,y,w,h);
  }
  ctx.restore();
  if(k.tough){
    rr(x,y,w,h,5);
    ctx.strokeStyle=c;ctx.lineWidth=2;ctx.stroke();
  }
  rr(x,y,w,h,5);
  ctx.fillStyle='rgba(255,255,255,0.22)';
  ctx.fillRect(x+3,y+2,w-6,2);
  if(k.flash>0){
    ctx.globalAlpha=k.flash*0.7;
    ctx.fillStyle='#ffffff';
    rr(x,y,w,h,5);ctx.fill();
    ctx.globalAlpha=1;
  }
  if(k.pu){
    const pulse=0.5+0.5*Math.sin(T*5+k.x*0.05);
    const cxb=x+w/2,cyb=y+h/2;
    const col=k.pu==='W'?'#67e8f9':'#fde047';
    ctx.shadowColor=col;ctx.shadowBlur=8+pulse*10;
    ctx.fillStyle=col;
    ctx.beginPath();ctx.arc(cxb,cyb,9,0,TAU);ctx.fill();
    ctx.shadowBlur=0;
    ctx.fillStyle='#0f172a';
    ctx.font='bold 10px system-ui,sans-serif';
    ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(k.pu==='W'?'W':'×3',cxb,cyb+0.5);
  }
  ctx.restore();
}

function drawPowerupCapsules(){
  for(const p of powerups){
    const col=p.type==='W'?'#22d3ee':'#fde047';
    ctx.save();
    ctx.shadowColor=col;ctx.shadowBlur=14;
    rr(p.x-p.w/2,p.y-p.h/2,p.w,p.h,p.h/2);
    ctx.fillStyle=col;ctx.fill();
    ctx.restore();
    ctx.fillStyle='#0f172a';
    ctx.font='bold 11px system-ui,sans-serif';
    ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(p.type==='W'?'WIDE':'MULTI',p.x,p.y+0.5);
  }
}

function drawPaddle(){
  const p=paddle,wide=wideTimer>0;
  ctx.save();
  ctx.shadowColor=wide?'#22d3ee':'#818cf8';ctx.shadowBlur=18;
  rr(p.x,p.y,p.w,p.h,6);
  const g=ctx.createLinearGradient(0,p.y,0,p.y+p.h);
  g.addColorStop(0,wide?'#a5f3fc':'#e0e7ff');
  g.addColorStop(1,wide?'#0891b2':'#4f46e5');
  ctx.fillStyle=g;ctx.fill();
  ctx.restore();
}

function drawBalls(){
  for(const b of balls){
    const n=b.trail.length;
    for(let i=0;i<n;i++){
      const t=b.trail[i],a=(i+1)/n;
      ctx.globalAlpha=a*0.3;
      ctx.fillStyle='#a5b4fc';
      ctx.beginPath();ctx.arc(t.x,t.y,b.r*a*0.9,0,TAU);ctx.fill();
    }
    ctx.globalAlpha=1;
    ctx.shadowColor='#ffffff';ctx.shadowBlur=14;
    ctx.fillStyle='#ffffff';
    ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,TAU);ctx.fill();
    ctx.shadowBlur=0;
  }
  if(balls.length===0&&live()){
    ctx.globalAlpha=0.45;
    ctx.fillStyle='#ffffff';
    ctx.beginPath();ctx.arc(pcx(),paddle.y-8,7,0,TAU);ctx.fill();
    ctx.globalAlpha=1;
  }
}

function drawParticles(){
  for(const p of parts){
    ctx.globalAlpha=Math.max(0,p.life/p.max);
    ctx.fillStyle=p.c;
    ctx.fillRect(p.x-p.s/2,p.y-p.s/2,p.s,p.s);
  }
  ctx.globalAlpha=1;
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.font='bold 14px system-ui,sans-serif';
  for(const t of texts){
    ctx.globalAlpha=Math.max(0,t.life/t.max);
    ctx.fillStyle=t.color;
    ctx.fillText(t.text,t.x,t.y);
  }
  ctx.globalAlpha=1;
}

function heart(x,y,s,col){
  ctx.fillStyle=col;
  ctx.beginPath();
  ctx.arc(x-s*0.27,y,s*0.3,0,TAU);
  ctx.arc(x+s*0.27,y,s*0.3,0,TAU);
  ctx.moveTo(x-s*0.56,y+s*0.05);
  ctx.lineTo(x+s*0.56,y+s*0.05);
  ctx.lineTo(x,y+s*0.7);
  ctx.closePath();
  ctx.fill();
}

function drawHUD(){
  ctx.fillStyle='rgba(5,7,16,0.6)';ctx.fillRect(0,0,W,HUDH);
  ctx.fillStyle='rgba(148,163,184,0.25)';ctx.fillRect(0,HUDH-1,W,1);
  ctx.textBaseline='middle';ctx.textAlign='left';
  ctx.font='600 12px system-ui,sans-serif';ctx.fillStyle='#94a3b8';
  ctx.fillText('SCORE',16,HUDH/2);
  ctx.font='bold 19px system-ui,sans-serif';ctx.fillStyle='#ffffff';
  ctx.fillText(String(score).padStart(5,'0'),74,HUDH/2+1);
  ctx.font='600 12px system-ui,sans-serif';ctx.fillStyle='#94a3b8';
  ctx.fillText('BEST',170,HUDH/2);
  ctx.font='bold 19px system-ui,sans-serif';ctx.fillStyle='#ffffff';
  ctx.fillText(String(Math.max(hiScore,score)).padStart(5,'0'),214,HUDH/2+1);

  // centre pill
  ctx.textAlign='center';
  const label=isDemo?'AUTOPLAY DEMO':'LEVEL 1 · BRICK BREAKER';
  ctx.font='bold 12px system-ui,sans-serif';
  const tw=ctx.measureText(label).width+24;
  rr(W/2-tw/2,HUDH/2-11,tw,22,11);
  ctx.fillStyle=isDemo?'rgba(251,146,60,0.25)':'rgba(99,102,241,0.25)';
  ctx.fill();
  ctx.fillStyle=isDemo?'#fdba74':'#c7d2fe';
  ctx.fillText(label,W/2,HUDH/2+1);

  // lives
  for(let i=0;i<3;i++){
    heart(W-22-i*28,HUDH/2+1,14,i<lives?'#f43f5e':'rgba(148,163,184,0.22)');
  }

  // wide timer
  if(wideTimer>0){
    ctx.textAlign='right';
    ctx.font='bold 13px system-ui,sans-serif';
    ctx.fillStyle='#67e8f9';
    ctx.fillText('WIDE '+Math.ceil(wideTimer)+'s',W-112,HUDH/2+1);
  }
}

function draw(){
  ctx.save();
  if(shakeT>0){
    ctx.translate((Math.random()-0.5)*shakeT*18,(Math.random()-0.5)*shakeT*18);
  }
  drawBg();
  for(const k of bricks)drawBrick(k);
  drawPowerupCapsules();
  drawPaddle();
  drawBalls();
  drawParticles();
  ctx.restore();
  drawHUD();
}

/* ---------- Loop ---------- */
function frame(now){
  const dt=Math.min((now-lastT)/1000,1/30);
  lastT=now;
  update(dt);
  draw();
  requestAnimationFrame(frame);
}

/* ---------- Input ---------- */
function toLogical(clientX){
  const r=cv.getBoundingClientRect();
  return (clientX-r.left)/r.width*W;
}

wrap.addEventListener('mousemove',e=>{mouseX=toLogical(e.clientX);});
wrap.addEventListener('mouseleave',()=>{mouseX=null;});
wrap.addEventListener('touchstart',e=>{mouseX=toLogical(e.touches[0].clientX);},{passive:true});
wrap.addEventListener('touchmove',e=>{mouseX=toLogical(e.touches[0].clientX);e.preventDefault();},{passive:false});

ovEl.addEventListener('click',()=>{
  if(state==='demo')startGame();
  else if(state==='paused')resumeGame();
});
ovBtn.addEventListener('click',e=>{
  e.stopPropagation();
  startGame();
});

window.addEventListener('keydown',e=>{
  if(e.key==='ArrowLeft'||e.key==='ArrowRight'||e.key===' '){
    if(e.target!==ovBtn)e.preventDefault();
  }
  if(e.key==='ArrowLeft')keyL=true;
  if(e.key==='ArrowRight')keyR=true;
  if(e.repeat)return;
  if(state==='demo'){startGame();return;}
  if(state==='over'||state==='win'){
    if((e.key==='Enter'||e.key===' ')&&e.target!==ovBtn)startGame();
    return;
  }
  if(e.key==='p'||e.key==='P'||e.key==='Escape'){
    if(state==='playing')pauseGame();
    else if(state==='paused')resumeGame();
  }
});
window.addEventListener('keyup',e=>{
  if(e.key==='ArrowLeft')keyL=false;
  if(e.key==='ArrowRight')keyR=false;
});
document.addEventListener('visibilitychange',()=>{
  if(document.hidden)pauseGame();
});

/* ---------- Start ---------- */
startDemo();
requestAnimationFrame(t=>{lastT=t;frame(t);});
})();
</script>
</body>
</html>
Reasoning trace (35,562 tokens)
I'm putting together a single self-contained HTML breakout game that opens in a self-playing demo mode behind a "Click or press a key to start" prompt. I also want it to run without errors, and I'm planning a responsive canvas with a fixed 800x600 logical size that scales to fit its container while keeping its aspect ratio.

I'm defining the game states, with the demo mode running an AI paddle until a click or key press starts a real game, and I'm laying out the brick grid with tougher rows and power-up bricks.

I'm writing a collision helper that treats the ball as a circle and checks it against each brick's rectangle, clamping to the nearest point to see if they overlap. The ball moves in small substeps so it can't pass through a brick, and I'll flip whichever velocity component matches the side that was hit.

Still working out the paddle bounce: the hit offset from the paddle centre sets the outgoing angle, up to about 60 degrees from vertical, while keeping the ball's speed roughly constant.

I'm checking the paddle collision: if the ball is moving down and its bottom edge reaches the paddle's top while overlapping it horizontally, I'll reset its position just above the paddle. Paddle tunneling should be rare since the paddle is thick and the substeps are small.

I'm working out how the ball relaunches after a loss in player mode, with a short auto-serve delay and a random angle. For the multi-ball power-up, I'll top the ball count up to three from the catch point, and the wide paddle will last ten seconds with a HUD timer that refreshes on re-catch.

I'm settling the brick layout now: ten columns and six rows, with the top two rows as tougher two-hit bricks and the rest as normal ones.

Still working out the brick layout: row one is a line of tough bricks, and I'm mapping out where the wide and multi-ball power-up bricks should sit in the next rows.

Now I'm laying out the brick grid row by row, with the top rows tough and power-up bricks scattered through the middle rows.

Now I'm setting the brick colours by row and deciding that the tough bricks should look darker with a border, then show crack lines after their first hit.

Still writing the drawing code for the brick visuals and power-up badges...

Now I'm adding the particle effects, with gravity and fading life, and a small burst when a tough brick cracks. For scoring I'm weighing points per destroyed brick, with tough bricks worth more. The demo AI will track the lowest falling ball with a little lag, auto-serve, and restart after a game over or a win, with the start prompt still showing.

I'm settling the rest of the start flow: a demo-mode badge, a click or key press to begin a real game, and a pause on P. The overlays will be DOM elements, with a Restart button on the game over and victory screens and the start prompt showing from the first frame.

I'm wiring up the controls now: the mouse sets the paddle position directly, while the arrow keys nudge it at a fixed speed. In demo mode the AI paddle chases its target at a capped speed with some smoothing.

Now I'm sizing the canvas to 800x600 logical units, with CSS width at 100% and a 4:3 aspect ratio, and scaling the backing store by the device pixel ratio. Mouse coordinates will be converted from the bounding rect into that logical space.

Now I'm sketching the HUD for the top band of the canvas, with the score on the left, lives on the right, and the power-up timer in between, with the bricks starting lower down.

Still writing the page's head and stylesheet...

Still writing the stylesheet: the wrapper and canvas get a rounded frame and shadow, the canvas cursor is hidden, and the start and game-over overlays are absolutely positioned over it.

Still writing the overlay markup and its CSS: a single overlay container whose content changes for the start, game-over and victory states, with a pulsing "Click or press a key to start" prompt.

Now I'm setting up the game state machine, starting in demo mode with the AI paddle, and sizing the canvas backing store to the device pixel ratio.

I'm setting up the game variables now: a paddle with a base width and target width, empty lists for balls, bricks, particles and power-ups, plus score, lives and the wide-paddle timer. Each ball will carry a position, velocity, radius and trail, starting at a base speed that creeps up as bricks fall. When the ball list runs empty I'll drop a life, and the game ends once none are left.

Still writing the game loop and the serve logic for when the ball is lost, with the ball sitting on the paddle until a short timer runs out.

Still writing the collision code: the ball is clamped to the walls and to the HUD band, and I'm checking whether it's dropping onto the paddle within its horizontal span.

Still working out the paddle bounce: the hit offset sets the outgoing angle, capped near 65 degrees from vertical, and the ball keeps its current speed. I'm also thinking about a small spark and a flash on the paddle at that moment.

Still working out the brick collision: I'm finding the nearest point on the brick to the ball's centre, and if that point is within the radius I'll reflect the velocity off the side it hit, pushing the ball back out of the brick.

Still working through the collision response: I'm pushing the ball out by its penetration depth, and for a centre-inside case I'll pick the nearest edge to set the normal and push it out from there.

I'm working through the power-up effects: the wide paddle sets a target width of 170 and a ten-second timer that refreshes if it's already active, while the multi-ball one tops the ball count up to three from the first ball's position. I'm still deciding what the multi-ball should do when there are already three balls.

I'm handling the multi-ball power-up: if the ball list is empty, I'll create a base ball first, then top it up to three. I'm also giving the wide-paddle effect a ten-second timer, and easing the paddle width toward its target while the timer counts down.

I'm handling the timers and lives: when the ball list empties after balls fall past the bottom, I lose a life and reset the wide-paddle effect, then either end the game or start a short serve timer. For the demo AI, I'll aim at the lowest downward-moving ball by predicting where it lands with wall reflections, plus a little random aim error.

I'm deciding how the demo AI should choose its target: it'll track the descending ball and, when there are power-ups falling, head for the nearest one, moving at a capped speed with a bit of randomness so it looks natural.

Now I'm working out how the demo AI picks its target. It follows the lowest ball that's moving down, predicting where it'll land, and if no ball is descending it heads for a falling power-up instead. I'm also mapping the keyboard controls: arrows move the paddle, P pauses, and Enter restarts from game over.

I'm settling the paddle controls: when a key is held, the paddle moves at a fixed speed and the mouse is ignored, otherwise it follows the mouse position directly.

Now I'm drawing the frame: a dark background with faint grid lines, then the HUD band with score, session high score, heart icons for lives, the level label, and a wide-paddle timer bar when it's active. Bricks get rounded gradient fills with a top highlight, tougher ones show a grey metal finish with a coloured stripe, and damaged ones show crack lines and dim, flashing white on a hit. Power-up bricks carry a small badge in the centre, with a cyan "W" for the wide paddle.

Now I'm drawing the power-up pieces: the falling capsules are cyan for wide and gold for multi-ball, and the paddle gets a glow that changes colour while it's widened. The balls get a glowing gradient with a fading trail, and I'm adding particles and a ghost ball on the paddle during the serve pause. Last, the start, game-over and pause messages will go through a single DOM overlay helper.

Now I'm laying out the overlays: the start card sits in the centre over a translucent background so the demo stays visible behind it, and the game-over and victory screens use a darker backdrop with the score and a restart or play-again button.

Next I'm wiring up the pause overlay, where a click resumes the game, while the demo starts on click and the game-over screen only responds to its button. I also want the demo to restart itself a couple of seconds after it ends.

Next I'm writing the game-state logic: a new-game reset that clears everything, a demo flag, and a start function that hides the overlay. When a demo game ends I'll restart the demo after about two seconds, and when the player loses I'll wait for them to press restart. I'll also show a "DEMO" label in the HUD while the demo runs.

Still writing the game loop logic...

Still working out how the demo AI predicts where the ball will land, reflecting its path off the side walls.

Still working out how the ball's speed should be enforced after each bounce, so it stays at a steady magnitude and doesn't get stuck flying almost horizontally.

Still writing the game code: I'm setting up the canvas and defining the brick layout as rows of tough, power-up and normal bricks.

Still working out the brick layout: I'm setting the bricks to 72 by 24 with a 6-pixel gap, centring the ten columns and placing the rows just below the HUD.

Still writing the brick builder: each brick gets its position, whether it's tough or a power-up carrier, and its hit points.

Still writing the brick drawing code: tough bricks get a darker metal body with stripes, and the rest get a rounded gradient fill.

Still writing the brick colour helper: a shade function that lightens or darkens a hex colour toward white or black.

Still writing the brick drawing: tough bricks get a dark metal body with a coloured top stripe and cracks once damaged, and I'm adding a white flash overlay for hits.

Still writing the brick colour helpers, mixing toward white or black by a fraction, and I'm setting up the thin top highlight on each brick. Power-up bricks will get a small centred badge, with a cyan "W" for wide paddle and an "×3" label for multi-ball.

Still writing the HUD: the score sits on the left, and I'm adding a centred label that reads "AUTOPLAY DEMO" while the demo runs.

Now I'm writing the HUD's right side: three heart icons for lives, greyed out once lost, plus a wide-paddle timer that shows "WIDE" with the seconds left and a bar when that power-up is active. Next I'm drawing the paddle as a rounded gradient with a glow, shifting to a cyan tint while it's widened. Then I'm starting on the overlay markup: a wrapper holding the canvas, and an overlay card with a title and subtitle.

Still writing the overlay markup and the game's main code, with the paddle set up as the first object.

Still writing the game setup: I'm assembling the paddle, balls, bricks, particles and power-ups, then resetting the score, lives and timers for a new game.

Still writing the game setup: I'm rebuilding the bricks and clearing the balls, particles and power-ups, then resetting the score, lives and timers, with a short serve delay before the first ball launches.

Still working out how the ball gets relaunched after it's lost, and how the game handles running out of lives during play.

Still working out how the game handles losing a life: I'm subtracting one, resetting the wide-paddle timer, and starting a short serve countdown while the state is playing or in demo mode.

Still working out what happens when a demo game ends: I'll reset the world straight away and rely on the short serve delay, so the demo restarts without a separate overlay.

Still writing the collision code: I'm stepping each ball along its path in small increments so it can't tunnel through bricks or paddle.

Still writing the collision code: I'm stepping the balls in small substeps and checking walls, paddle and bricks at each one.

Still writing the wall and paddle collision checks, with the paddle hit only counting when the ball is moving downward.

Still writing the paddle collision: I'm mapping where the ball lands on the paddle to an angle, and I'm working out the new velocity from that.

Still working out the collision logic: I'm checking which brick the ball has hit and bouncing it off the right side.

Still writing the brick collision check, looping through each live brick and finding the closest point on it to the ball's centre.

Still writing the collision code: I'm pushing the ball out of any brick it overlaps, then flipping the velocity along whichever axis the contact normal favours.

Still writing the collision code: I'm normalizing the ball's speed after each bounce and clamping its angle so it never gets too flat.

Still writing the brick hit logic: tougher bricks lose a hit point and get cracks, while the rest break outright and add to the score.

Still writing the brick hit logic: bricks with two hit points drop to one and get cracked, while normal bricks break outright, adding to the score and possibly dropping a power-up, and I'm checking whether that was the last brick to win the game.

Still writing the powerup logic: I'm making the falling power-ups drop each frame, and collecting one when it touches the paddle.

Still writing the power-up pickup: the paddle's width target and the wide timer are set when a wide-paddle badge is caught, the multi-ball badge tops up the ball count, and I'm removing any power-up that falls off the bottom.

Still writing the multi-ball spawn: I'm adding balls up to three, copying the first ball's position and rotating its velocity slightly either side.

Still writing the multi-ball spawn and the paddle's width changes: I'm keeping the wide timer fed and smoothing the paddle toward its target width, then clamping its position. Next I'm wiring mouse and keyboard input to move the paddle, and starting on the demo AI's target tracking.

Still writing the game's update and draw loop: particles fall under gravity and fade out, bricks flash back to normal, and I'm laying out the background, grid and HUD band.

Still writing the heart icon for the lives display, drawing it from a few curved segments.

Still writing the heart icon: I'm building it from two circles and a triangle, sized off the icon's scale.

Still writing the overlay: I'm adding the overlay text for each state, with the demo showing "BREAKOUT" and the start prompt, and the paused and game-over states getting their own titles and sub-lines.

Still writing the keyboard handling: arrow keys and space get their default scrolling blocked, and I'm setting the left and right key flags before a repeated press is ignored. In demo mode, any key starts the game, and I'm working out what the other states do with it.

Still writing the keyboard handling: I'm finishing the key handler so Enter or Space restarts from the game-over and victory screens, while P or Escape pauses and resumes during play.

I'm wiring the mouse input to the wrapper rather than the canvas, since the overlay sits on top and would otherwise swallow the events. Clicks on the overlay start the demo or resume from pause, while clicks on the game-over and victory cards do nothing unless they hit the button.

I'm wiring the touch input so it stops the page from scrolling and drives the paddle the same way the mouse does. I'm keeping the demo start on the click handler, so a touch doesn't accidentally launch the game.

Still writing the game loop: I'm clamping the frame time, updating the particles, and fading out the brick flashes.

Still writing the game loop: I'm setting up the update step so the paddle moves from the keyboard, the mouse, or the demo AI, and clamping it to the field.

Still writing the game loop: I'm finishing the update step so the balls move, the power-ups fall, and any ball that drops off the bottom is removed. If the last ball is lost, that costs a life.

Still working out the AI paddle's control code, starting with its target tracking.

Still working out the AI paddle's target: I'm tracking the lowest ball that's moving downward and predicting where it will cross the paddle, falling back to the centre or a nearby power-up when no ball is heading down.

I'm adding the AI's rule for when no ball is coming down: it stays centred or drifts toward a falling power-up. The player's paddle follows the mouse when there's no key input, and otherwise it holds still. Next I'm handling the life loss, decrementing the lives count and clearing the wide-paddle timer.

Still writing the game-end logic: when a round ends, the demo just resets itself, while a real game moves to the win or game-over state and records the session best.

Still writing the game states: I'm putting the demo start, the real start, and the round-end handling into their functions, with the demo quietly resetting itself after each round.

Still writing the game's overlay and HUD: I'm making sure the start prompt reads exactly "Click or press a key to start", hiding the overlay during play, and adding the score and best score to the canvas. I'm also adding a short screen shake when a life is lost, applied as a translate between a save and restore so the baseline transform is kept.

Still writing the game constants: I'm setting the field's logical size, brick grid dimensions and spacing, and the row colours, and I'm laying out the brick grid so it's centred.

Still writing the helper functions: I'm adding a clamp and a colour-mixing helper that blends a row's base colour toward white or black.

Still writing the brick drawing: I'm drawing each brick as a rounded rectangle, with tough bricks getting a darker gradient.

Still writing the brick drawing: I'm adding a coloured stripe clipped to the rounded shape, plus cracks on damaged tough bricks.

Still writing the brick drawing: for the tough bricks I'm clipping to the rounded shape, filling a dark gradient, and adding the coloured stripe and any cracks, then restoring the context before drawing the border.

Still writing the crack helper: I'm generating three jagged polylines per damaged brick, starting from the top or bottom edge and wandering inward.

Still writing the crack helper: I'm picking a random start point along the top or bottom edge for each of three jagged lines, then wandering inward with small random offsets.

Still writing the particle system: each burst spawns particles with random angles and speeds, a slight upward bias, and a short lifetime that fades their alpha as they age. Next I'm drawing the balls with fading trails and a glow, and starting the paddle drawing.

Still writing the paddle drawing: it gets a glowing gradient fill, cyan when the wide power-up is active and indigo otherwise.

Still writing the HUD: I'm adding a dark translucent bar across the top, with the score and best score on the left and a centre badge that reads as an autoplay demo or the current level.

Still writing the HUD: I'm placing the hearts and the wide-paddle timer on the right, then drawing a power-up badge centred on each brick.

Still writing the power-up code: I'm making the falling capsules wider so their labels fit, and checking for paddle overlap when collecting them.

Still writing the page layout: I'm checking the header and the wrapper on narrow screens, and making sure the overlay card stays within about 80% of the width.

Still writing the overlay styling: I'm adding the full-screen overlay with its dim variant and hidden state, plus a centred card with a border and shadow. The title is sized with clamp so it fits on narrow screens.

Still writing the overlay styles: the overlay's subtitle gets a pulsing animation in gold, and the card and buttons are being styled. Next I'm making a click anywhere on the start overlay begin the game, or resume it if it's paused.

Still writing the input handling: I'm making Enter in the over and win states start a new game, but skipping it when a button has focus so the click doesn't start the game twice.

Wiring up the keyboard handling next: Enter in the over state will start a new game unless the focus is on the button, so a click or keypress doesn't start it twice. I'm keeping the arrow-key preventDefault only, and I'm adding a pause when the tab is hidden during play.

Still writing the game's helper code: I'm adding the overlay controls, with a function to show the overlay with a title, subtitle, button, and hint, and another to hide it.

Still writing the input handling: I'm tracking the mouse position across the canvas wrapper, and planning to clear it when the pointer leaves. Touch moves will update the paddle too, with the default scroll prevented.

Still writing the draw loop: I'm saving the context, applying a random shake offset while a life-loss shake is active, and then drawing the background, bricks, power-ups, paddle, balls, and particles in order.

Still writing the draw routine: I'm adding a faint grid and a red glow along the bottom edge, then drawing the HUD last so it sits over everything.

Still writing the ball drawing: each ball gets a fading trail of translucent circles, then a bright glowing core. A serving ball is drawn at half opacity.

Still writing the update loop: I'm ticking the timer, decaying the particles and brick flashes, and reducing the screen shake. Then I'm handing paddle movement to the AI in demo mode or to the keys otherwise, and only moving the simulation when the game is playing or in demo.

Still writing the update loop: I'm handling the paddle's movement with the keys or mouse, easing its width toward the target after a wide power-up runs out, and clamping it to the canvas.

Still writing the update loop: I'm finishing the round-end check, then advancing the power-ups and dropping balls that have fallen off the bottom. If the last ball is lost, that costs a life.

Still writing the update loop: I'm writing the ball update that moves each ball in small sub-steps, checking walls, the paddle, and bricks at every step.

Still writing the update loop: I'm checking whether the ball has left the play area, and then working through how it bounces off walls, the paddle, and bricks at each sub-step.

Still checking the paddle collision: a ball only bounces off when it's falling and overlaps the paddle's top edge, so anything that slips past the side just keeps falling.

Still writing the paddle collision: I'm switching it to a circle-against-rectangle test so side hits also count. A ball that overlaps the paddle from above gets the angled bounce, and one hitting from the side or below just reflects away.

Still writing the paddle collision: I'm switching to a circle-against-rectangle test, and a ball whose centre sits above the top edge gets the angled bounce if it's falling. Anything hitting from the side or below is pushed clear, and the top case is pushed out to the paddle's surface.

Still writing the paddle collision: if the ball's centre sits above the paddle's top edge, I'm pushing it out to the surface and only angling the bounce when it's falling. Side and bottom hits just push it clear and reflect the matching velocity component.

Still writing the paddle collision: if the ball's centre is inside the paddle, I'm treating it as a top hit and pushing it up to the surface before deciding how it bounces.

Still writing the paddle collision: I'm treating a ball whose centre is above the paddle's top edge as a top hit, and the angled bounce applies to any such hit, so corners angle the bounce too.

Still writing the paddle collision: I'm treating a ball whose centre is above the paddle's top edge as a top hit, pushing it out to the surface, and only angling the bounce when it's falling.

Still writing the paddle collision: for a side or bottom hit, I'm pushing the ball out along the contact normal and reflecting whichever velocity component matches that side.

Still writing the paddle collision: for a side or bottom hit, I'm pushing the ball out along the contact normal, then reflecting whichever velocity component matches that side and normalizing the speed.

Still checking the paddle's bounce math: the angle at the edges works out to a steep but acceptable vertical speed, so the minimum-angle floor doesn't clash with it.

Still writing the powerup update: each power-up falls, and when it touches the paddle it's collected once and removed, along with any that drop past the bottom.

Still writing the powerup collection: a wide power-up sets the paddle's target width and starts its timer, while any other one tops the ball count up to three by spawning angled copies of the lead ball.

Still writing the AI paddle: I'm picking the nearest descending ball to track.

Still writing the AI paddle: I'm choosing the lowest ball that's heading down, and if none is, I'm aiming for a power-up or the centre of the screen.

Still working out the AI paddle: if a power-up is falling, I'm weighing whether the paddle can get there before the nearest ball needs it, and going for the power-up if so.

I'm deciding how the AI paddle picks its target: if a power-up is falling and the nearest descending ball is still more than about 250px above the paddle, it'll chase the power-up, otherwise it tracks the ball. Once the ball has bounced and started moving up, there's no descending ball left, so the paddle goes for the power-up. I also want to make sure the paddle's centre lines up with the power-up's x position so it actually gets caught.

Still writing the HUD: I'm drawing the score, lives, and the active power-up timers along the top of the canvas.

Still writing the HUD: I'm drawing a translucent bar across the top, then labelling the score in small grey text ahead of the bold number.

Still writing the HUD: I'm laying out the score, best score, and a centred mode label, with the score and best values in bold white beside their grey captions.

Now I'm placing the hearts on the right edge of the HUD, then fitting the wide-paddle timer beside them so the two don't overlap.

Still writing the HUD: I'm settling the font sizes, bumping them up a bit so the text stays readable on small screens, and I'm drawing the brick labels with the same approach. I'll also make sure the rounded rectangles use my own helper, since roundRect isn't a safe call across browsers.

Still writing the reset: I'm rebuilding the paddle, bricks and ball list, and zeroing the score, lives, power-up timers and particles.

Still writing the reset: I'm making sure the demo restarts cleanly after a game ends, and that the key flags clear on keyup.

Still writing the draw loop: I'm finishing the particle and floating-text rendering, and I'm adding the brick crack lines as a set of small polylines.

Still writing the brick rendering: I'm drawing the crack lines on damaged tough bricks, then adding a pulsing glow badge on any brick that hides a power-up.

Still writing the brick badge: I'm drawing a small pulsing circle with a "W" or "×3" label on any brick that hides a power-up.

Still writing the powerup drawing: each falling capsule gets a glow in its colour, with a short "WIDE" or "MULTI" label centred on it.

Now I'm finishing the overlay and the title, and I'm switching the best score to a `hiScore` variable so it doesn't clash with another name.