GPT-6.1 Terra
by OpenAIReasoningClean
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 / 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 · 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>