Models 44

GPT-6.1 Terra

by OpenAIReasoningClean
Open in new window

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

How it went

Total time
1 min 21 s
Cost
$0.037
Output tokens
7,220
Spent thinking
0 tokens
Speed
90.1 tok/s
Console errors
0
First token after
1.0 s
First visible text after
1.0 s
Prompt tokens
508
Code size
21.1 KB
Finish reason
stop
Served by
OpenAI
Model id
openai/gpt-6.1-terra
Reasoning sent
{"effort":"none"}
Max output
128,000 tokens
Generated
Nov 12, 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 · 400 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{color-scheme:dark;--bg:#090b17;--muted:#858ba8;--white:#f4f5ff;--cyan:#70f3e1}
  *{box-sizing:border-box}
  html,body{margin:0;min-height:100%;background:var(--bg)}
  body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--white);overflow-x:hidden}
  body:before{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(ellipse at 50% 30%,#25234b 0%,transparent 50%),radial-gradient(ellipse at 0% 90%,#102c38 0%,transparent 35%)}
  .app{position:relative;max-width:1120px;margin:auto;padding:26px 28px 20px}
  header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:20px}
  .brand{display:flex;align-items:center;gap:12px}
  .logo{width:39px;height:39px;border:1px solid #7270a0;border-radius:11px;display:grid;place-items:center;background:linear-gradient(145deg,#343355,#191b31);box-shadow:0 4px 20px #0005}
  .logo svg{width:23px;height:23px}
  .brand-text{font-size:14px;font-weight:800;letter-spacing:.17em;line-height:1.2}
  .brand-text small{display:block;color:#898ea7;font-size:9px;letter-spacing:.2em;margin-top:4px}
  .header-right{display:flex;align-items:center;gap:18px;color:#9298b4;font-size:11px;font-weight:700;letter-spacing:.12em}
  .live{display:flex;align-items:center;gap:7px;color:#a8f4df}
  .live:before{content:"";width:6px;height:6px;border-radius:50%;background:#71eecb;box-shadow:0 0 10px #71eecb}
  .shell{border:1px solid #363750;background:linear-gradient(145deg,#181a2c,#101321);border-radius:17px;padding:13px;box-shadow:0 24px 80px #0008,0 1px 0 #ffffff0a inset}
  .topbar{height:60px;display:flex;align-items:center;justify-content:space-between;padding:0 15px 12px;gap:12px}
  .game-title{display:flex;align-items:center;gap:10px;min-width:0}
  .game-title .icon{width:34px;height:34px;border-radius:9px;background:#2b2b4a;display:grid;place-items:center;color:#c6b4ff;font-size:17px}
  .game-title strong{display:block;font-size:13px;letter-spacing:.02em}
  .game-title span{display:block;font-size:10px;color:#8e93ad;margin-top:3px}
  .stats{display:flex;align-items:center;gap:30px}
  .stat label{display:block;color:#777e9d;font-size:9px;font-weight:800;letter-spacing:.13em;margin-bottom:4px}
  .stat b{font-size:15px;font-weight:750;letter-spacing:.08em}
  .hearts{color:#ff8ba8;letter-spacing:3px!important}
  .canvas-wrap{position:relative;border-radius:10px;overflow:hidden;border:1px solid #373956;background:#0b0d1c;line-height:0}
  canvas{display:block;width:100%;height:auto;touch-action:none;cursor:crosshair}
  .bottom{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:15px 4px 3px;color:#858ba5;font-size:11px}
  .controls,.legend{display:flex;align-items:center;gap:13px;flex-wrap:wrap}
  .controls span,.legend span{display:flex;align-items:center;gap:6px}
  kbd{font:inherit;font-size:10px;color:#c9cbe0;background:#25283b;border:1px solid #40445a;border-radius:4px;padding:3px 5px;box-shadow:0 2px 0 #10121d}
  .legend strong{color:#b7bdd4;font-weight:600}
  .power{width:17px;height:17px;border-radius:5px;display:inline-grid;place-items:center;font-weight:900;font-size:10px;line-height:1}
  .wide{background:#1f514f;color:#8df8e1}.multi{background:#4a365d;color:#e1b5ff}
  .footer{display:flex;justify-content:space-between;align-items:center;padding:15px 4px 0;color:#676e8c;font-size:10px;letter-spacing:.04em}
  .footer span:last-child{color:#9097b1}
  @media(max-width:620px){
    .app{padding:17px 12px}
    header{margin-bottom:15px}.header-right{font-size:9px;gap:8px}.header-right .edition{display:none}
    .shell{padding:8px;border-radius:13px}.topbar{height:54px;padding:0 5px 8px}
    .game-title .icon{display:none}.game-title strong{font-size:11px}.game-title span{font-size:9px}
    .stats{gap:12px}.stat label{font-size:8px}.stat b{font-size:12px}
    .bottom{align-items:flex-start;flex-direction:column;padding:12px 4px 5px;gap:10px}
    .footer{font-size:9px}.controls,.legend{gap:10px}
  }
</style>
</head>
<body>
<main class="app">
  <header>
    <div class="brand">
      <div class="logo">
        <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="4" width="8" height="4" rx="1" fill="#A996FF"/><rect x="13" y="4" width="8" height="4" rx="1" fill="#75EBD7"/><rect x="7" y="10" width="10" height="4" rx="1" fill="#F9A4B8"/><rect x="8" y="19" width="8" height="2" rx="1" fill="#E7E8FF"/><circle cx="12" cy="16.5" r="1.5" fill="#E7E8FF"/></svg>
      </div>
      <div class="brand-text">NEON BREAKOUT<small>THE ARCADE COLLECTION</small></div>
    </div>
    <div class="header-right"><span class="edition">VOL. 01 &nbsp; / &nbsp; ARCADE</span><span class="live">READY TO PLAY</span></div>
  </header>

  <section class="shell" aria-label="Breakout game">
    <div class="topbar">
      <div class="game-title"><div class="icon">✳</div><div><strong>Brick by brick</strong><span>Clear the board. Keep the ball alive.</span></div></div>
      <div class="stats">
        <div class="stat"><label>SCORE</label><b id="score">00000</b></div>
        <div class="stat"><label>LIVES</label><b class="hearts" id="lives">♥ ♥ ♥</b></div>
        <div class="stat"><label>LEVEL</label><b>01</b></div>
      </div>
    </div>
    <div class="canvas-wrap">
      <canvas id="game" width="1036" height="540" aria-label="Breakout game board. Click or press a key to start. Use the mouse or arrow keys to move the paddle."></canvas>
    </div>
    <div class="bottom">
      <div class="controls">
        <span>MOVE</span><span><kbd>←</kbd><kbd>→</kbd></span><span>or mouse</span>
        <span><kbd>SPACE</kbd> pause</span>
      </div>
      <div class="legend">
        <span><i class="power wide">↔</i><strong>Wide paddle</strong> 10s</span>
        <span><i class="power multi">✣</i><strong>Multi-ball</strong></span>
      </div>
    </div>
  </section>
  <div class="footer"><span>BUILT FOR A LITTLE BREAK</span><span>ONE LEVEL &nbsp;·&nbsp; ENDLESS GOOD VIBES</span></div>
</main>

<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const scoreEl = document.getElementById('score');
  const livesEl = document.getElementById('lives');
  const W = canvas.width, H = canvas.height;
  const paddleY = 495, paddleH = 12, baseWidth = 112;
  const cols = 10, rows = 6, gap = 8, brickW = 86, brickH = 25;
  const brickStartX = (W - (cols * brickW + (cols - 1) * gap)) / 2;
  const brickStartY = 76;
  const rowColors = ['#ed879f','#f3a98e','#edc87e','#86d7c4','#84badc','#a79bdf'];
  let state, lastTime = 0, keys = {}, mouseX = W/2, mouseActive = false;
  let demoTimer = 0;

  function roundRect(x,y,w,h,r,fill,stroke){
    ctx.beginPath(); ctx.roundRect(x,y,w,h,r);
    if(fill){ctx.fillStyle=fill;ctx.fill()}
    if(stroke){ctx.strokeStyle=stroke;ctx.stroke()}
  }
  function newBall(x,y,vx,vy){return {x,y,vx,vy,r:7,trail:[]}}
  function reset(demo=false){
    state = {
      mode: demo ? 'demo' : 'playing', score:0,lives:3,paddleX:W/2,
      paddleW:baseWidth,wideUntil:0,balls:[newBall(W/2,paddleY-18,175,-300)],
      bricks:[],drops:[],particles:[],time:0,shake:0
    };
    for(let r=0;r<rows;r++) for(let c=0;c<cols;c++){
      const tough = (r===0 && c%3===1) || (r===2 && c%3===0) || (r===4 && c%4===2);
      let power = null;
      if ((r===1 && (c===1 || c===7)) || (r===4 && c===4)) power='wide';
      if ((r===2 && (c===4 || c===8)) || (r===5 && c===1)) power='multi';
      state.bricks.push({x:brickStartX+c*(brickW+gap),y:brickStartY+r*(brickH+gap),w:brickW,h:brickH,
        hp:tough?2:1,maxHp:tough?2:1,color:rowColors[r],power,alive:true});
    }
    mouseX=W/2;mouseActive=false;demoTimer=0;updateUI();
  }
  function updateUI(){
    scoreEl.textContent=String(state.score).padStart(5,'0');
    livesEl.textContent=Array.from({length:3},(_,i)=>i<state.lives?'♥':'♡').join(' ');
  }
  function start(){
    if(state.mode==='demo'||state.mode==='over'||state.mode==='won'){reset(false)}
  }
  function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
  function spawnParticles(x,y,color){
    for(let i=0;i<13;i++){
      let a=Math.random()*Math.PI*2,s=40+Math.random()*190;
      state.particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-30,life:.35+Math.random()*.45,max:.8,size:2+Math.random()*4,color});
    }
  }
  function breakBrick(b){
    b.hp--;
    state.score += b.hp>0 ? 10 : (b.maxHp===2?150:100);
    if(b.hp<=0){
      b.alive=false;spawnParticles(b.x+b.w/2,b.y+b.h/2,b.color);
      if(b.power) state.drops.push({x:b.x+b.w/2,y:b.y+b.h/2,type:b.power,vy:95,phase:Math.random()*6});
      state.shake=2.5;
    } else {
      spawnParticles(b.x+b.w/2,b.y+b.h/2,b.color);
    }
    updateUI();
  }

  // Swept point against a rectangle expanded by the ball radius.
  // Returns the earliest impact, preventing tunneling even at high speed.
  function sweep(ball,rect,dt){
    const minX=rect.x-ball.r,maxX=rect.x+rect.w+ball.r;
    const minY=rect.y-ball.r,maxY=rect.y+rect.h+ball.r;
    let tx1=-Infinity,tx2=Infinity,ty1=-Infinity,ty2=Infinity;
    if(Math.abs(ball.vx)<1e-8){if(ball.x<minX||ball.x>maxX)return null}
    else {tx1=(minX-ball.x)/ball.vx;tx2=(maxX-ball.x)/ball.vx;if(tx1>tx2)[tx1,tx2]=[tx2,tx1]}
    if(Math.abs(ball.vy)<1e-8){if(ball.y<minY||ball.y>maxY)return null}
    else {ty1=(minY-ball.y)/ball.vy;ty2=(maxY-ball.y)/ball.vy;if(ty1>ty2)[ty1,ty2]=[ty2,ty1]}
    const enter=Math.max(tx1,ty1),exit=Math.min(tx2,ty2);
    if(enter>exit || exit<0 || enter < -0.00001 || enter>dt)return null;
    const t=Math.max(0,enter);
    return {t,nx:tx1>ty1?(ball.vx>0?-1:1):0,ny:ty1>=tx1?(ball.vy>0?-1:1):0};
  }
  function moveBall(ball,dt){
    let remaining=dt,iterations=0;
    while(remaining>0.000001 && iterations++<12){
      let hit=null;
      function choose(h,type,obj){if(h && h.t>=0 && h.t<=remaining && (!hit||h.t<hit.t))hit={...h,type,obj}}
      if(ball.vx<0)choose({t:(ball.r-ball.x)/ball.vx,nx:1,ny:0},'wall');
      if(ball.vx>0)choose({t:(W-ball.r-ball.x)/ball.vx,nx:-1,ny:0},'wall');
      if(ball.vy<0)choose({t:(ball.r-ball.y)/ball.vy,nx:0,ny:1},'wall');
      if(ball.vy>0 && ball.y<=paddleY+paddleH+ball.r){
        choose(sweep(ball,{x:state.paddleX-state.paddleW/2,y:paddleY,w:state.paddleW,h:paddleH},remaining),'paddle');
      }
      for(const b of state.bricks)if(b.alive)choose(sweep(ball,b,remaining),'brick',b);
      if(!hit){ball.x+=ball.vx*remaining;ball.y+=ball.vy*remaining;break}
      ball.x+=ball.vx*hit.t;ball.y+=ball.vy*hit.t;remaining-=hit.t;
      if(hit.type==='paddle' && ball.vy>0){
        const offset=clamp((ball.x-state.paddleX)/(state.paddleW/2),-1,1);
        const speed=Math.max(345,Math.min(470,Math.hypot(ball.vx,ball.vy)+2));
        const angle=offset*1.14;
        ball.vx=Math.sin(angle)*speed;ball.vy=-Math.cos(angle)*speed;
        ball.y=Math.min(ball.y,paddleY-ball.r-.15);
        state.shake=1;
      }else{
        if(hit.nx)ball.vx=-ball.vx;
        if(hit.ny)ball.vy=-ball.vy;
        if(hit.type==='brick')breakBrick(hit.obj);
        ball.x+=hit.nx*.03;ball.y+=hit.ny*.03;
      }
      remaining=Math.max(0,remaining-.00001);
    }
  }
  function activate(type){
    if(type==='wide'){state.wideUntil=state.time+10}
    else {
      const source=state.balls[0];
      if(source){
        const speed=Math.max(345,Math.hypot(source.vx,source.vy));
        const base=Math.atan2(source.vy,source.vx);
        while(state.balls.length<3){
          const angle=base+(state.balls.length===1?-.55:.55);
          let vy=Math.sin(angle)*speed;
          if(Math.abs(vy)<130)vy=vy<0?-130:130;
          state.balls.push(newBall(source.x,source.y,Math.cos(angle)*speed,vy));
        }
      }
    }
  }
  function update(dt){
    if(state.mode==='paused'||state.mode==='over'||state.mode==='won')return;
    state.time+=dt;state.shake=Math.max(0,state.shake-dt*10);
    const targetWidth=state.time<state.wideUntil?170:baseWidth;
    state.paddleW += (targetWidth-state.paddleW)*Math.min(1,dt*8);
    if(state.mode==='demo'){
      demoTimer+=dt;
      let target=state.balls.filter(b=>b.vy>0).sort((a,b)=>b.y-a.y)[0]||state.balls[0];
      if(target){
        const travel=Math.max(0,(paddleY-target.y)/Math.max(1,target.vy));
        let px=target.x+target.vx*Math.min(travel,2);
        const span=W-target.r*2;
        px=((px-target.r)%(2*span)+2*span)%(2*span);
        if(px>span)px=2*span-px;
        const aim=px+target.r+Math.sin(state.time*.85)*25;
        state.paddleX+=clamp(aim-state.paddleX,-420*dt,420*dt);
      }
    }else if(keys.ArrowLeft||keys.ArrowRight){
      state.paddleX+=((keys.ArrowRight?1:0)-(keys.ArrowLeft?1:0))*600*dt;
    }else if(mouseActive){state.paddleX=mouseX}
    state.paddleX=clamp(state.paddleX,state.paddleW/2+5,W-state.paddleW/2-5);
    for(const ball of state.balls){
      ball.trail.unshift({x:ball.x,y:ball.y});
      if(ball.trail.length>7)ball.trail.pop();
      moveBall(ball,dt);
    }
    state.balls=state.balls.filter(b=>b.y<H+20);
    if(!state.balls.length){
      state.lives--;updateUI();
      if(state.lives<=0){
        if(state.mode==='demo'){reset(true);return}
        state.mode='over';return;
      }
      state.balls.push(newBall(state.paddleX,paddleY-18,(Math.random()>.5?1:-1)*175,-300));
    }
    for(const d of state.drops){
      d.y+=d.vy*dt;d.phase+=dt*3;
      if(d.y+11>=paddleY && d.y-11<=paddleY+paddleH && Math.abs(d.x-state.paddleX)<state.paddleW/2+10){
        activate(d.type);d.caught=true;
      }
    }
    state.drops=state.drops.filter(d=>!d.caught&&d.y<H+20);
    for(const p of state.particles){p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=240*dt;p.life-=dt}
    state.particles=state.particles.filter(p=>p.life>0);
    if(!state.bricks.some(b=>b.alive)){
      if(state.mode==='demo'){reset(true);return}
      state.mode='won';
    }
    if(state.mode==='demo'&&demoTimer>65)reset(true);
  }

  function powerIcon(type,x,y,size){
    ctx.save();ctx.translate(x,y);
    ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.font=`800 ${size}px system-ui,sans-serif`;
    ctx.fillText(type==='wide'?'↔':'✣',0,1);
    ctx.restore();
  }
  function draw(){
    ctx.clearRect(0,0,W,H);
    const bg=ctx.createLinearGradient(0,0,0,H);
    bg.addColorStop(0,'#111426');bg.addColorStop(1,'#0a1020');
    ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);
    ctx.save();
    if(state.shake>0)ctx.translate((Math.random()-.5)*state.shake,(Math.random()-.5)*state.shake);
    // Subtle grid and ambient lighting.
    ctx.strokeStyle='rgba(139,151,199,.045)';ctx.lineWidth=1;
    for(let x=0;x<W;x+=46){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke()}
    for(let y=0;y<H;y+=46){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke()}
    const glow=ctx.createRadialGradient(W/2,190,10,W/2,190,480);
    glow.addColorStop(0,'rgba(112,91,170,.12)');glow.addColorStop(1,'rgba(15,17,32,0)');
    ctx.fillStyle=glow;ctx.fillRect(0,0,W,H);
    ctx.fillStyle='#626985';ctx.font='700 10px system-ui';ctx.letterSpacing='2px';
    ctx.fillText('LEVEL 01',brickStartX,38);
    ctx.textAlign='right';ctx.fillText('BREAK THEM ALL',W-brickStartX,38);ctx.textAlign='left';ctx.letterSpacing='0px';

    for(const b of state.bricks)if(b.alive){
      ctx.save();
      ctx.shadowColor=b.color;ctx.shadowBlur=9;ctx.globalAlpha=.15;
      roundRect(b.x,b.y,b.w,b.h,5,b.color);ctx.restore();
      roundRect(b.x,b.y,b.w,b.h,5,b.color);
      ctx.fillStyle='rgba(255,255,255,.19)';roundRect(b.x+2,b.y+2,b.w-4,3,2,'rgba(255,255,255,.22)');
      ctx.fillStyle='rgba(27,28,48,.13)';roundRect(b.x+2,b.y+b.h-5,b.w-4,3,1,'rgba(27,28,48,.13)');
      if(b.maxHp===2){
        ctx.strokeStyle='rgba(40,38,59,.38)';ctx.lineWidth=1;
        roundRect(b.x+3,b.y+3,b.w-6,b.h-6,3,null,'rgba(42,40,64,.28)');
        if(b.hp===2){ctx.fillStyle='rgba(38,37,57,.38)';ctx.beginPath();ctx.arc(b.x+9,b.y+12.5,2,0,Math.PI*2);ctx.fill()}
        else{
          ctx.strokeStyle='rgba(40,36,54,.8)';ctx.lineWidth=1.6;ctx.lineCap='round';ctx.beginPath();
          ctx.moveTo(b.x+b.w*.53,b.y+2);ctx.lineTo(b.x+b.w*.46,b.y+9);ctx.lineTo(b.x+b.w*.55,b.y+13);ctx.lineTo(b.x+b.w*.47,b.y+23);
          ctx.moveTo(b.x+b.w*.46,b.y+9);ctx.lineTo(b.x+b.w*.38,b.y+13);
          ctx.moveTo(b.x+b.w*.55,b.y+13);ctx.lineTo(b.x+b.w*.63,b.y+17);ctx.stroke();
        }
      }
      if(b.power){ctx.fillStyle='rgba(37,35,58,.68)';powerIcon(b.power,b.x+b.w/2,b.y+b.h/2,15)}
    }
    for(const d of state.drops){
      const color=d.type==='wide'?'#72e9d5':'#d3a6f7';
      ctx.save();ctx.shadowColor=color;ctx.shadowBlur=15;
      roundRect(d.x-11,d.y-11,22,22,7,color);ctx.restore();
      ctx.fillStyle='#20243a';powerIcon(d.type,d.x,d.y,15);
    }
    for(const p of state.particles){
      ctx.globalAlpha=Math.min(1,p.life/.35);roundRect(p.x,p.y,p.size,p.size,1,p.color);
    }
    ctx.globalAlpha=1;
    for(const ball of state.balls){
      ball.trail.forEach((t,i)=>{ctx.globalAlpha=(1-i/ball.trail.length)*.13;ctx.fillStyle='#a7e9f8';ctx.beginPath();ctx.arc(t.x,t.y,ball.r*(1-i/ball.trail.length*.65),0,Math.PI*2);ctx.fill()});
      ctx.globalAlpha=1;ctx.save();ctx.shadowColor='#9af6ff';ctx.shadowBlur=19;ctx.fillStyle='#efffff';ctx.beginPath();ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2);ctx.fill();ctx.restore();
      ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(ball.x-2,ball.y-2,2,0,Math.PI*2);ctx.fill();
    }
    // Paddle and its glow.
    ctx.save();ctx.shadowColor='#75e7e8';ctx.shadowBlur=19;
    const pg=ctx.createLinearGradient(0,paddleY,0,paddleY+paddleH);
    pg.addColorStop(0,'#d4ffff');pg.addColorStop(.38,'#8cf3ec');pg.addColorStop(1,'#54bccc');
    roundRect(state.paddleX-state.paddleW/2,paddleY,state.paddleW,paddleH,7,pg);ctx.restore();
    roundRect(state.paddleX-state.paddleW/2+10,paddleY+2,state.paddleW-20,2,1,'rgba(255,255,255,.5)');
    ctx.strokeStyle='rgba(123,220,230,.12)';ctx.beginPath();ctx.moveTo(0,H-18);ctx.lineTo(W,H-18);ctx.stroke();
    if(state.wideUntil>state.time){
      const left=Math.ceil(state.wideUntil-state.time);
      ctx.textAlign='center';ctx.font='700 10px system-ui';ctx.fillStyle='#83e8db';ctx.fillText('WIDE '+left+'s',state.paddleX,paddleY+28);ctx.textAlign='left';
    }
    ctx.restore();

    if(state.mode==='demo'){
      // A compact prompt allows the live game to remain visible.
      const pw=340,ph=73,px=(W-pw)/2,py=H-125;
      ctx.save();ctx.shadowColor='#000';ctx.shadowBlur=24;
      roundRect(px,py,pw,ph,12,'rgba(18,21,38,.94)','rgba(148,153,198,.35)');ctx.restore();
      ctx.textAlign='center';ctx.fillStyle='#f7f7ff';ctx.font='700 15px system-ui';
      ctx.fillText('Click or press a key to start',W/2,py+29);
      ctx.fillStyle='#9ba1bd';ctx.font='11px system-ui';
      ctx.fillText('DEMO PLAYING  ·  YOUR TURN WHEN READY',W/2,py+50);
      ctx.textAlign='left';
    }else if(state.mode==='over'||state.mode==='won'||state.mode==='paused'){
      ctx.fillStyle='rgba(7,9,19,.75)';ctx.fillRect(0,0,W,H);
      const pw=340,ph=170,px=(W-pw)/2,py=(H-ph)/2;
      roundRect(px,py,pw,ph,15,'#1a1d31','rgba(168,170,212,.4)');
      ctx.textAlign='center';
      ctx.fillStyle=state.mode==='won'?'#91f2da':'#f7c3ce';
      ctx.font='700 11px system-ui';ctx.letterSpacing='2px';
      ctx.fillText(state.mode==='won'?'BOARD CLEARED':state.mode==='over'?'GAME OVER':'TAKE A BREATH',W/2,py+34);
      ctx.letterSpacing='0px';ctx.fillStyle='#fff';ctx.font='800 25px system-ui';
      ctx.fillText(state.mode==='won'?'You did it!':state.mode==='over'?'Nice run!':'Paused',W/2,py+70);
      ctx.font='12px system-ui';ctx.fillStyle='#a3aac4';
      ctx.fillText(state.mode==='paused'?'Your game is waiting for you.':'Final score  ·  '+String(state.score).padStart(5,'0'),W/2,py+96);
      roundRect(W/2-112,py+114,224,37,8,'#a6eee1');
      ctx.fillStyle='#142735';ctx.font='700 12px system-ui';
      ctx.fillText(state.mode==='paused'?'Press space to resume':'Click or press R to restart',W/2,py+137);
      ctx.textAlign='left';
    }
  }
  function frame(t){
    if(!lastTime)lastTime=t;
    let dt=Math.min((t-lastTime)/1000,.05);lastTime=t;
    // Small fixed slices keep motion stable and collision work bounded.
    while(dt>0){const step=Math.min(dt,1/120);update(step);dt-=step}
    draw();requestAnimationFrame(frame);
  }
  function setPointer(e){
    const r=canvas.getBoundingClientRect();
    mouseX=clamp((e.clientX-r.left)*W/r.width,0,W);mouseActive=true;
  }
  canvas.addEventListener('pointermove',setPointer);
  canvas.addEventListener('pointerdown',e=>{
    setPointer(e);
    if(state.mode==='demo'||state.mode==='over'||state.mode==='won'){start();setPointer(e)}
    else if(state.mode==='paused')state.mode='playing';
  });
  window.addEventListener('keydown',e=>{
    if(['ArrowLeft','ArrowRight',' ','Spacebar'].includes(e.key))e.preventDefault();
    if(state.mode==='demo'){start();if(e.key==='ArrowLeft'||e.key==='ArrowRight')keys[e.key]=true;return}
    if(state.mode==='over'||state.mode==='won'){if(e.key.toLowerCase()==='r'||e.key==='Enter'||e.key===' ')reset(false);return}
    if(e.key===' '||e.key==='Spacebar'){if(!e.repeat)state.mode=state.mode==='paused'?'playing':'paused';return}
    if(e.key==='ArrowLeft'||e.key==='ArrowRight'){keys[e.key]=true;mouseActive=false}
  });
  window.addEventListener('keyup',e=>{if(e.key==='ArrowLeft'||e.key==='ArrowRight')keys[e.key]=false});
  window.addEventListener('blur',()=>{keys={}});
  reset(true);draw();requestAnimationFrame(frame);
})();
</script>
</body>
</html>