Muse Ember
by MetaReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 31.1 s
- Cost
- $0.0071
- Output tokens
- 14,131
- Spent thinking
- 4,286 tokens
- Speed
- 487.7 tok/s
- Console errors
- 0
- First token after
- 2.1 s
- First visible text after
- 14.9 s
- Prompt tokens
- 526
- Code size
- 28.1 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
{"effort":"high"}- Max output
- 943,718 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 · 660 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>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:radial-gradient(1000px 600px at 50% -10%, #1b2350 0%, #0b0f26 45%, #05060f 100%);
color:#eef2ff;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
min-height:100vh;display:flex;flex-direction:column;align-items:center;
padding:14px 12px 18px;overflow-x:hidden;
}
.wrap{width:100%;max-width:920px}
header{text-align:center;margin-bottom:10px}
h1{
font-size:clamp(28px,5vw,42px);letter-spacing:.14em;font-weight:900;line-height:1;
background:linear-gradient(90deg,#6ef3ff,#7c8cff,#ff6ec7,#ffd166);
-webkit-background-clip:text;background-clip:text;color:transparent;
text-shadow:0 0 28px rgba(124,140,255,.25);
}
.sub{margin-top:5px;font-size:12px;letter-spacing:.28em;color:#8fa0d8;text-transform:uppercase}
.hud{
display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:stretch;
margin:10px 0 10px;
}
.pill{
flex:1 1 auto;min-width:92px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);
border-radius:12px;padding:7px 10px;text-align:center;backdrop-filter:blur(6px);
}
.pill .label{font-size:10px;letter-spacing:.18em;color:#8fa0d8;text-transform:uppercase}
.pill .value{font-size:19px;font-weight:800;line-height:1.15;color:#fff;font-variant-numeric:tabular-nums}
#widePill{display:none}
#widePill.on{display:block}
#wideBarOuter{height:5px;background:rgba(255,255,255,.15);border-radius:99px;margin-top:4px;overflow:hidden}
#wideBar{height:100%;width:100%;background:linear-gradient(90deg,#6ef3ff,#7c8cff);border-radius:99px}
.btns{display:flex;gap:6px}
.iconBtn{
cursor:pointer;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.07);color:#fff;
border-radius:12px;padding:0 12px;font-size:16px;min-height:44px;
}
.iconBtn:hover{background:rgba(255,255,255,.14)}
.stage{
position:relative;border-radius:18px;overflow:hidden;
border:1px solid rgba(140,160,255,.35);
box-shadow:0 0 0 4px rgba(80,100,255,.08),0 18px 50px rgba(0,0,0,.55),0 0 36px rgba(110,243,255,.14);
background:#070a18;line-height:0;
}
canvas{width:100%;height:auto;display:block;touch-action:none}
#demoBanner{
position:absolute;top:12px;left:50%;transform:translateX(-50%);
line-height:1.2;white-space:nowrap;max-width:94%;
background:rgba(8,10,28,.78);border:1px solid rgba(110,243,255,.55);color:#dffcff;
padding:8px 14px;border-radius:999px;font-size:13px;font-weight:700;letter-spacing:.02em;
box-shadow:0 0 18px rgba(110,243,255,.35);backdrop-filter:blur(6px);
animation:pulse 1.6s ease-in-out infinite;z-index:5;text-align:center;
}
@keyframes pulse{0%,100%{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.04)}}
#overlay{
position:absolute;inset:0;display:none;align-items:center;justify-content:center;
background:rgba(4,6,18,.72);backdrop-filter:blur(3px);line-height:1.4;z-index:6;padding:16px;
}
#overlay.show{display:flex}
.card{
text-align:center;background:rgba(12,16,38,.92);border:1px solid rgba(255,255,255,.16);
border-radius:18px;padding:22px 20px;max-width:420px;width:100%;
box-shadow:0 20px 50px rgba(0,0,0,.5);
}
.card h2{font-size:34px;letter-spacing:.08em;margin-bottom:6px}
.card p{color:#aeb9e6;font-size:14px;margin-bottom:14px}
.card .final{font-size:18px;color:#fff;font-weight:800;margin-bottom:16px}
.playBtn{
cursor:pointer;border:none;border-radius:999px;padding:13px 26px;font-size:16px;font-weight:900;letter-spacing:.04em;
color:#07101f;background:linear-gradient(90deg,#6ef3ff,#7c8cff);box-shadow:0 8px 24px rgba(110,243,255,.35);
}
.playBtn:hover{filter:brightness(1.08)} .playBtn:active{transform:scale(.98)}
.controls{
display:flex;flex-wrap:wrap;gap:6px 14px;justify-content:center;margin-top:10px;
color:#8fa0d8;font-size:12.5px;text-align:center;line-height:1.5;
}
.controls b{color:#e8edff;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.13);border-radius:6px;padding:1px 6px;font-size:11px}
.legend{margin-top:8px;display:flex;flex-wrap:wrap;gap:8px;justify-content:center;font-size:12px;color:#aeb9e6}
.legend span{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);padding:4px 9px;border-radius:999px}
@media(max-width:560px){ .pill{min-width:76px;padding:6px 6px} .pill .value{font-size:16px} #demoBanner{font-size:11px;padding:7px 10px} }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>NEON BREAKOUT</h1>
<div class="sub">Brick Breaker • 3 Lives • Power-Ups</div>
</header>
<div class="hud">
<div class="pill"><div class="label">Score</div><div class="value" id="scoreVal">0</div></div>
<div class="pill"><div class="label">Best</div><div class="value" id="bestVal">0</div></div>
<div class="pill"><div class="label">Lives</div><div class="value" id="livesVal">♥♥♥</div></div>
<div class="pill"><div class="label">Bricks Left</div><div class="value" id="bricksVal">70</div></div>
<div class="pill"><div class="label">Balls</div><div class="value" id="ballsVal">1</div></div>
<div class="pill" id="widePill"><div class="label">Wide Paddle</div><div class="value" id="wideVal">10s</div><div id="wideBarOuter"><div id="wideBar"></div></div></div>
<div class="btns">
<button class="iconBtn" id="pauseBtn" title="Pause (P)">⏸</button>
<button class="iconBtn" id="soundBtn" title="Sound (M)">🔊</button>
<button class="iconBtn" id="restartBtn" title="Restart (R)">↻</button>
</div>
</div>
<div class="stage" id="stage">
<canvas id="game" width="900" height="600"></canvas>
<div id="demoBanner">▶ DEMO PLAYING • Click or press a key to take control</div>
<div id="overlay">
<div class="card">
<h2 id="ovTitle">GAME OVER</h2>
<p id="ovText">The bricks win this time.</p>
<div class="final" id="ovScore">Score: 0</div>
<button class="playBtn" id="playAgainBtn">▶ PLAY AGAIN</button>
</div>
</div>
</div>
<div class="controls">
<span><b>Mouse</b> move paddle</span>
<span><b>← →</b> / <b>A D</b> move</span>
<span><b>Space</b> launch / pause</span>
<span><b>P</b> pause</span>
<span><b>R</b> restart</span>
<span><b>M</b> mute</span>
</div>
<div class="legend">
<span>⬜ Steel brick = 2 hits & cracks</span>
<span>🟦 <b>W</b> = Wide paddle 10s</span>
<span>🟪 <b>×3 ✦</b> = Multi-ball</span>
</div>
</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const scoreEl=document.getElementById('scoreVal'), bestEl=document.getElementById('bestVal'),
livesEl=document.getElementById('livesVal'), bricksEl=document.getElementById('bricksVal'),
ballsEl=document.getElementById('ballsVal'), widePill=document.getElementById('widePill'),
wideVal=document.getElementById('wideVal'), wideBar=document.getElementById('wideBar'),
demoBanner=document.getElementById('demoBanner'), overlay=document.getElementById('overlay'),
ovTitle=document.getElementById('ovTitle'), ovText=document.getElementById('ovText'),
ovScore=document.getElementById('ovScore');
// ---------- constants ----------
const COLS=10, ROWS=7, GAP=7, TOP=64, LEFT=30;
const BRICK_W=(W-LEFT*2-(COLS-1)*GAP)/COLS, BRICK_H=22;
const BASE_PADDLE_W=132, WIDE_PADDLE_W=208, PADDLE_H=14;
const ROW_COLORS=['#ff5a6e','#ff8c42','#ffd166','#4ade80','#6ef3ff','#7c8cff','#ff6ec7'];
const POWER_MAP={'1,2':'wide','1,7':'multi','3,4':'wide','3,8':'multi','5,1':'multi','5,6':'wide'};
// stars
const stars=[]; for(let i=0;i<90;i++) stars.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*1.6+0.4,a:Math.random()*0.5+0.2});
// ---------- state ----------
let mode='demo'; // demo | manual
let gameState='playing'; // playing | waiting | paused | over | win
let score=0, best=0, lives=3;
let bricks=[], balls=[], particles=[], powerups=[], floaters=[];
let wideTimer=0, announce=null, autoLaunchTimer=0, elapsed=0, muted=false;
let mouseX=W/2, mouseActive=false;
const keys={};
const paddle={x:W/2-BASE_PADDLE_W/2, y:H-42, w:BASE_PADDLE_W, h:PADDLE_H, speed:560, targetX:null};
let audioCtx=null;
function initAudio(){ if(!audioCtx){ try{ audioCtx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(audioCtx&&audioCtx.state==='suspended') audioCtx.resume(); }
function beep(f,d=0.07,type='square',vol=0.12){
if(muted||!audioCtx) return;
try{
const o=audioCtx.createOscillator(), g=audioCtx.createGain();
o.type=type; o.frequency.value=f;
g.gain.setValueAtTime(vol,audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+d);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime+d);
}catch(e){}
}
// ---------- helpers ----------
function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
function rrect(x,y,w,h,r){
r=Math.min(r,w/2,h/2);
ctx.beginPath();
ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
}
function isTough(r,c){
if(r===2 && c>=2 && c<=7) return true;
if(r===4 && (c===1||c===4||c===5||c===8)) return true;
if(r===1 && (c===0||c===9)) return true;
if(r===5 && (c===4||c===5)) return true;
if(r===6 && (c===2||c===7)) return true;
return false;
}
function buildBricks(){
bricks=[];
for(let r=0;r<ROWS;r++) for(let c=0;c<COLS;c++){
const tough=isTough(r,c);
bricks.push({
x:LEFT+c*(BRICK_W+GAP), y:TOP+r*(BRICK_H+GAP), w:BRICK_W, h:BRICK_H,
color:tough?'#8ea2c9':ROW_COLORS[r], row:r, col:c,
hp:tough?2:1, maxHp:tough?2:1, tough, alive:true,
power:POWER_MAP[r+','+c]||null
});
}
}
function makeBall(x,y,angleDeg,speed){
const a=angleDeg*Math.PI/180;
return {x,y,r:8,vx:Math.sin(a)*speed,vy:-Math.cos(a)*speed,speed,trail:[],stuck:false};
}
function spawnLaunchBall(){
balls=[makeBall(paddle.x+paddle.w/2, paddle.y-12, (Math.random()*24-12), 380)];
balls[0].stuck=true; gameState='waiting';
}
function launchBalls(){
balls.forEach(b=>{ if(b.stuck){ b.stuck=false; const a=(Math.random()*24-12)*Math.PI/180; b.vy=-Math.cos(a)*b.speed; b.vx=Math.sin(a)*b.speed; }});
if(balls.length) gameState='playing';
}
function resetGame(autoLaunch=true){
score=0; lives=3; wideTimer=0; particles=[]; powerups=[]; floaters=[]; announce=null; autoLaunchTimer=0;
paddle.w=BASE_PADDLE_W; paddle.x=W/2-paddle.w/2;
buildBricks();
balls=[makeBall(paddle.x+paddle.w/2, paddle.y-12, -8, 380)];
balls[0].stuck=false; gameState='playing';
overlay.classList.remove('show');
updateHUD();
}
function bricksLeft(){ return bricks.filter(b=>b.alive).length; }
function updateHUD(){
scoreEl.textContent=score;
bestEl.textContent=best;
livesEl.textContent='♥'.repeat(Math.max(0,lives))||'—';
bricksEl.textContent=bricksLeft();
ballsEl.textContent=balls.length;
if(wideTimer>0){ widePill.classList.add('on'); wideVal.textContent=Math.ceil(wideTimer)+'s'; wideBar.style.width=(wideTimer/10*100)+'%'; }
else widePill.classList.remove('on');
}
function showAnnounce(text,color){ announce={text,color,t:2.2}; }
function addFloater(x,y,text,color){ floaters.push({x,y,text,color,t:1}); }
// ---------- particles ----------
function burst(x,y,color,n=16){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, s=60+Math.random()*260;
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,life:0.5+Math.random()*0.5,t:0,color,size:2+Math.random()*4});
}
}
// ---------- brick damage ----------
function damageBrick(b, ball){
b.hp--;
if(b.hp>0){
// cracked tough brick
burst(ball.x,ball.y,'#cbd5ff',8);
beep(180,0.08,'square',0.12);
addFloater(b.x+b.w/2,b.y+b.h/2,'CRACK!','#cbd5ff');
score+=25;
} else {
b.alive=false;
const pts=b.tough?100:50;
score+=pts; best=Math.max(best,score);
burst(b.x+b.w/2,b.y+b.h/2,b.color,b.tough?22:16);
addFloater(b.x+b.w/2,b.y+b.h/2,'+'+pts,'#ffffff');
beep(b.tough?520:660,0.08,'triangle',0.14);
if(b.power){
powerups.push({x:b.x+b.w/2,y:b.y+b.h/2,vy:150,w:38,h:20,type:b.power});
}
if(bricksLeft()===0){
gameState='win';
setTimeout(()=>{ if(gameState==='win') showEnd(true); },500);
}
}
updateHUD();
}
// ---------- powerups ----------
function applyPowerup(p){
if(p.type==='wide'){
wideTimer=10; paddle.w=WIDE_PADDLE_W;
paddle.x=clamp(paddle.x,6,W-6-paddle.w);
showAnnounce('WIDE PADDLE! 10s','#6ef3ff'); beep(880,0.15,'sine',0.16);
} else {
const src=balls[0];
if(src){
if(balls.length<3){
const baseA=Math.atan2(src.vx,-src.vy)*180/Math.PI;
[-26,26].forEach(off=>{
if(balls.length>=3) return;
const a=(baseA+off)*Math.PI/180;
balls.push({x:src.x,y:src.y,r:8,vx:Math.sin(a)*src.speed,vy:-Math.cos(a)*src.speed,speed:src.speed,trail:[],stuck:false});
});
} else { score+=200; best=Math.max(best,score); addFloater(p.x,p.y-14,'+200','#ff6ec7'); }
}
showAnnounce('MULTI-BALL! ×3','#ff6ec7'); beep(990,0.15,'sine',0.16);
}
updateHUD();
}
// ---------- collisions ----------
function collidePaddle(b, prevY){
if(b.vy<=0) return;
if(b.y+b.r>=paddle.y && b.y-b.r<=paddle.y+paddle.h && b.x+b.r>=paddle.x && b.x-b.r<=paddle.x+paddle.w){
// ensure it was above before (prevents side tunneling weirdness)
if(prevY+b.r>paddle.y+6) return;
const center=paddle.x+paddle.w/2;
const rel=clamp((b.x-center)/(paddle.w/2),-1,1);
const ang=rel*(Math.PI*0.38); // up to ~68°
b.speed=Math.min(580,b.speed+4);
b.vx=Math.sin(ang)*b.speed;
b.vy=-Math.cos(ang)*b.speed;
b.y=paddle.y-b.r-0.5;
beep(440+Math.abs(rel)*220,0.06,'triangle',0.11);
burst(b.x,paddle.y,'#6ef3ff',5);
}
}
function collideBricks(b, prevX, prevY){
for(const br of bricks){
if(!br.alive) continue;
const cx=clamp(b.x,br.x,br.x+br.w), cy=clamp(b.y,br.y,br.y+br.h);
const dx=b.x-cx, dy=b.y-cy;
if(dx*dx+dy*dy < b.r*b.r){
// decide axis from previous position
if(prevY+b.r<=br.y+2){ b.vy=-Math.abs(b.vy); b.y=br.y-b.r-0.5; }
else if(prevY-b.r>=br.y+br.h-2){ b.vy=Math.abs(b.vy); b.y=br.y+br.h+b.r+0.5; }
else if(prevX+b.r<=br.x+2){ b.vx=-Math.abs(b.vx); b.x=br.x-b.r-0.5; }
else if(prevX-b.r>=br.x+br.w-2){ b.vx=Math.abs(b.vx); b.x=br.x+br.w+b.r+0.5; }
else {
// fallback: push along smallest penetration
const overlapL=(b.x+b.r)-br.x, overlapR=(br.x+br.w)-(b.x-b.r);
const overlapT=(b.y+b.r)-br.y, overlapB=(br.y+br.h)-(b.y-b.r);
const m=Math.min(overlapL,overlapR,overlapT,overlapB);
if(m===overlapT){b.vy=-Math.abs(b.vy);b.y=br.y-b.r-0.5;}
else if(m===overlapB){b.vy=Math.abs(b.vy);b.y=br.y+br.h+b.r+0.5;}
else if(m===overlapL){b.vx=-Math.abs(b.vx);b.x=br.x-b.r-0.5;}
else {b.vx=Math.abs(b.vx);b.x=br.x+br.w+b.r+0.5;}
}
// renormalise to preserved speed (slight speedup)
b.speed=Math.min(580,b.speed+2);
const len=Math.hypot(b.vx,b.vy)||1;
b.vx=b.vx/len*b.speed; b.vy=b.vy/len*b.speed;
// avoid near-horizontal loops
if(Math.abs(b.vy)<b.speed*0.22){ b.vy=(b.vy<0?-1:1)*b.speed*0.22; const l=Math.hypot(b.vx,b.vy); b.vx=b.vx/l*b.speed; b.vy=b.vy/l*b.speed; }
damageBrick(br,b);
break; // one brick per substep
}
}
}
function stepBall(b, dt){
const dist=Math.hypot(b.vx,b.vy)*dt;
const steps=Math.max(1,Math.min(24,Math.ceil(dist/(b.r*0.5))));
const sdt=dt/steps;
for(let i=0;i<steps;i++){
const prevX=b.x, prevY=b.y;
b.x+=b.vx*sdt; b.y+=b.vy*sdt;
// walls
if(b.x-b.r<6){ b.x=6+b.r; b.vx=Math.abs(b.vx); beep(300,0.04,'square',0.06); }
if(b.x+b.r>W-6){ b.x=W-6-b.r; b.vx=-Math.abs(b.vx); beep(300,0.04,'square',0.06); }
if(b.y-b.r<6){ b.y=6+b.r; b.vy=Math.abs(b.vy); beep(300,0.04,'square',0.06); }
collidePaddle(b,prevY);
collideBricks(b,prevX,prevY);
}
b.trail.push({x:b.x,y:b.y}); if(b.trail.length>10) b.trail.shift();
}
// ---------- update ----------
function update(dt){
elapsed+=dt;
// paddle control
if(gameState==='playing'||gameState==='waiting'){
if(mode==='demo'){
// AI tracks most threatening ball
let target=paddle.x+paddle.w/2;
const falling=balls.filter(b=>b.vy>0);
if(falling.length){ falling.sort((a,b2)=>b2.y-a.y); target=falling[0].x + falling[0].vx*0.12; }
else if(balls.length) target=balls[0].x;
const diff=target-(paddle.x+paddle.w/2);
const maxMove=paddle.speed*1.15*dt;
paddle.x+=clamp(diff,-maxMove,maxMove);
} else {
let move=0;
if(keys['ArrowLeft']||keys['a']||keys['A']) move-=1;
if(keys['ArrowRight']||keys['d']||keys['D']) move+=1;
if(move!==0){ paddle.x+=move*paddle.speed*dt; mouseActive=false; }
else if(mouseActive && paddle.targetX!==null){
// smooth follow mouse
const diff=paddle.targetX-(paddle.x+paddle.w/2);
paddle.x+=diff*Math.min(1,dt*22);
}
}
paddle.x=clamp(paddle.x,6,W-6-paddle.w);
}
if(wideTimer>0){ wideTimer-=dt; if(wideTimer<=0){ wideTimer=0; paddle.w=BASE_PADDLE_W; paddle.x=clamp(paddle.x,6,W-6-paddle.w); showAnnounce('WIDE ENDS','#8fa0d8'); } }
if(gameState==='waiting'){
balls.forEach(b=>{ if(b.stuck){ b.x=paddle.x+paddle.w/2; b.y=paddle.y-b.r-1; }});
if(mode==='demo'){ autoLaunchTimer-=dt; if(autoLaunchTimer<=0) launchBalls(); }
}
if(gameState==='playing'){
// balls
for(let i=balls.length-1;i>=0;i--){
const b=balls[i];
if(b.stuck) continue;
stepBall(b,dt);
if(b.y-b.r>H+10){
balls.splice(i,1);
burst(b.x,H-10,'#ff5a6e',8);
if(balls.length===0){
lives--; beep(140,0.3,'sawtooth',0.16);
updateHUD();
if(lives<=0){ gameState='over'; showEnd(false); }
else { spawnLaunchBall(); autoLaunchTimer=1.0; showAnnounce('BALL LOST — '+lives+(lives===1?' LIFE':' LIVES')+' LEFT','#ff8c42'); }
} else {
showAnnounce('BALL LOST','#ff8c42');
}
}
}
// powerups
for(let i=powerups.length-1;i>=0;i--){
const p=powerups[i]; p.y+=p.vy*dt;
// catch
if(p.y+p.h/2>=paddle.y && p.y-p.h/2<=paddle.y+paddle.h && p.x+p.w/2>=paddle.x && p.x-p.w/2<=paddle.x+paddle.w){
applyPowerup(p); powerups.splice(i,1); continue;
}
if(p.y>H+20) powerups.splice(i,1);
}
}
// particles & floaters always animate (even on end screens)
for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.t+=dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=420*dt; if(p.t>=p.life) particles.splice(i,1); }
for(let i=floaters.length-1;i>=0;i--){ const f=floaters[i]; f.t-=dt; f.y-=34*dt; if(f.t<=0) floaters.splice(i,1); }
if(announce){ announce.t-=dt; if(announce.t<=0) announce=null; }
updateHUD();
}
// ---------- draw ----------
function draw(){
// bg
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#0d1333'); g.addColorStop(0.55,'#0a0f2a'); g.addColorStop(1,'#070918');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// stars
stars.forEach(s=>{ ctx.globalAlpha=s.a*(0.7+0.3*Math.sin(elapsed*2+s.r*10)); ctx.fillStyle='#cfe6ff'; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,7); ctx.fill(); });
ctx.globalAlpha=1;
// subtle grid
ctx.strokeStyle='rgba(120,140,255,0.06)'; ctx.lineWidth=1;
for(let x=0;x<=W;x+=45){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
for(let y=0;y<=H;y+=45){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
// walls glow
ctx.strokeStyle='rgba(110,243,255,0.5)'; ctx.lineWidth=3; ctx.shadowBlur=10; ctx.shadowColor='#6ef3ff';
ctx.beginPath(); ctx.moveTo(6,6); ctx.lineTo(6,H); ctx.moveTo(6,6); ctx.lineTo(W-6,6); ctx.moveTo(W-6,6); ctx.lineTo(W-6,H); ctx.stroke();
ctx.shadowBlur=0;
// bottom danger line
ctx.strokeStyle='rgba(255,90,110,0.35)'; ctx.setLineDash([8,8]); ctx.beginPath(); ctx.moveTo(6,H-4); ctx.lineTo(W-6,H-4); ctx.stroke(); ctx.setLineDash([]);
// bricks
for(const b of bricks){
if(!b.alive) continue;
ctx.save();
if(b.tough){
const gg=ctx.createLinearGradient(b.x,b.y,b.x,b.y+b.h);
gg.addColorStop(0,'#c7d4f2'); gg.addColorStop(0.5,'#8ea2c9'); gg.addColorStop(1,'#5f729e');
ctx.fillStyle=gg; rrect(b.x,b.y,b.w,b.h,5); ctx.fill();
ctx.strokeStyle=b.hp===1?'#ffd166':'#e6ecff'; ctx.lineWidth=b.hp===1?2:1.5; rrect(b.x+1,b.y+1,b.w-2,b.h-2,4); ctx.stroke();
// rivets
ctx.fillStyle='rgba(20,30,60,0.55)';
[[b.x+6,b.y+5],[b.x+b.w-6,b.y+5],[b.x+6,b.y+b.h-5],[b.x+b.w-6,b.y+b.h-5]].forEach(([rx,ry])=>{ctx.beginPath();ctx.arc(rx,ry,1.8,0,7);ctx.fill();});
if(b.hp===1){ // cracks
ctx.strokeStyle='#1a2440'; ctx.lineWidth=1.6; ctx.beginPath();
ctx.moveTo(b.x+10,b.y+3); ctx.lineTo(b.x+18,b.y+10); ctx.lineTo(b.x+14,b.y+18);
ctx.moveTo(b.x+b.w-12,b.y+4); ctx.lineTo(b.x+b.w-20,b.y+11); ctx.lineTo(b.x+b.w-16,b.y+19);
ctx.moveTo(b.x+b.w/2-4,b.y+2); ctx.lineTo(b.x+b.w/2+3,b.y+10); ctx.lineTo(b.x+b.w/2-2,b.y+b.h-3);
ctx.stroke();
}
} else {
const gg=ctx.createLinearGradient(b.x,b.y,b.x,b.y+b.h);
gg.addColorStop(0,b.color); gg.addColorStop(1,'rgba(0,0,0,0.35)');
ctx.fillStyle=b.color; rrect(b.x,b.y,b.w,b.h,5); ctx.globalAlpha=0.25; ctx.fillStyle=gg; rrect(b.x,b.y,b.w,b.h,5); ctx.fill(); ctx.globalAlpha=1;
ctx.fillStyle=b.color; rrect(b.x,b.y,b.w,b.h,5); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.35)'; rrect(b.x+3,b.y+2,b.w-6,6,3); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.22)'; ctx.lineWidth=1; rrect(b.x+0.5,b.y+0.5,b.w-1,b.h-1,5); ctx.stroke();
}
// powerup icon badge
if(b.power){
const cx=b.x+b.w/2, cy=b.y+b.h/2, r=9;
ctx.save(); ctx.shadowBlur=8; ctx.shadowColor=b.power==='wide'?'#6ef3ff':'#ff6ec7';
ctx.fillStyle='#ffffff'; ctx.beginPath(); ctx.arc(cx,cy,r,0,7); ctx.fill();
ctx.shadowBlur=0; ctx.fillStyle=b.power==='wide'?'#0077ff':'#c026d3';
ctx.font='900 9px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(b.power==='wide'?'W':'×3',cx,cy+0.5);
ctx.restore();
}
ctx.restore();
}
// powerups falling
for(const p of powerups){
ctx.save();
ctx.shadowBlur=14; ctx.shadowColor=p.type==='wide'?'#6ef3ff':'#ff6ec7';
ctx.fillStyle=p.type==='wide'?'#0891ff':'#d946ef';
rrect(p.x-p.w/2,p.y-p.h/2,p.w,p.h,10); ctx.fill();
ctx.shadowBlur=0; ctx.strokeStyle='rgba(255,255,255,0.85)'; ctx.lineWidth=1.5; rrect(p.x-p.w/2,p.y-p.h/2,p.w,p.h,10); ctx.stroke();
ctx.fillStyle='#fff'; ctx.font='900 11px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(p.type==='wide'?'↔ W':'✦ ×3',p.x,p.y+0.5);
ctx.restore();
}
// paddle
ctx.save();
ctx.shadowBlur=16; ctx.shadowColor='#6ef3ff';
const pg=ctx.createLinearGradient(paddle.x,0,paddle.x+paddle.w,0);
pg.addColorStop(0,'#22d3ee'); pg.addColorStop(0.5,'#e0f7ff'); pg.addColorStop(1,'#818cf8');
ctx.fillStyle=pg; rrect(paddle.x,paddle.y,paddle.w,paddle.h,8); ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle='rgba(255,255,255,0.85)'; rrect(paddle.x+4,paddle.y+2,paddle.w-8,4,2); ctx.fill();
ctx.fillStyle='rgba(10,20,60,0.35)'; rrect(paddle.x+paddle.w/2-18,paddle.y+6,36,3,2); ctx.fill();
ctx.restore();
// balls + trails
for(const b of balls){
b.trail.forEach((t,i)=>{
ctx.globalAlpha=(i/b.trail.length)*0.28; ctx.fillStyle='#9beaff';
ctx.beginPath(); ctx.arc(t.x,t.y,b.r*(i/b.trail.length)*0.8,0,7); ctx.fill();
});
ctx.globalAlpha=1;
ctx.save(); ctx.shadowBlur=16; ctx.shadowColor='#ffffff';
const bg=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,b.r+2);
bg.addColorStop(0,'#ffffff'); bg.addColorStop(1,'#a5f3fc');
ctx.fillStyle=bg; ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,7); ctx.fill(); ctx.restore();
}
// particles
for(const p of particles){
ctx.globalAlpha=1-(p.t/p.life); ctx.fillStyle=p.color;
ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,7); ctx.fill();
}
ctx.globalAlpha=1;
// floaters
ctx.textAlign='center'; ctx.textBaseline='middle';
for(const f of floaters){
ctx.globalAlpha=Math.min(1,f.t*1.6); ctx.fillStyle=f.color; ctx.font='800 14px system-ui';
ctx.fillText(f.text,f.x,f.y);
}
ctx.globalAlpha=1;
// announce
if(announce){
ctx.globalAlpha=Math.min(1,announce.t);
ctx.save(); ctx.shadowBlur=16; ctx.shadowColor=announce.color;
ctx.fillStyle=announce.color; ctx.font='900 30px system-ui';
ctx.fillText(announce.text,W/2,210);
ctx.restore(); ctx.globalAlpha=1;
}
// waiting / paused hints
if(gameState==='waiting'){
ctx.fillStyle='rgba(238,242,255,0.95)'; ctx.font='800 15px system-ui';
ctx.fillText(mode==='demo'?'Launching…':'SPACE / CLICK TO LAUNCH',W/2,H-90);
}
if(gameState==='paused'){
ctx.fillStyle='rgba(4,6,18,0.55)'; ctx.fillRect(0,0,W,H);
ctx.fillStyle='#fff'; ctx.font='900 34px system-ui'; ctx.fillText('PAUSED',W/2,H/2-8);
ctx.font='600 14px system-ui'; ctx.fillStyle='#aeb9e6'; ctx.fillText('Press P / Space to resume',W/2,H/2+22);
}
if(gameState==='win'||gameState==='over'){
// dim handled by overlay; keep scene visible behind
}
}
// ---------- loop ----------
let last=performance.now();
function loop(now){
const dt=Math.min(0.033,(now-last)/1000); last=now;
if(gameState!=='paused') update(dt);
draw();
requestAnimationFrame(loop);
}
// ---------- end screens ----------
function showEnd(won){
if(won){ best=Math.max(best,score); ovTitle.textContent='🏆 VICTORY!'; ovTitle.style.color='#ffd166'; ovText.textContent='You cleared every brick. Brilliant!'; }
else { ovTitle.textContent='GAME OVER'; ovTitle.style.color='#ff5a6e'; ovText.textContent='Out of lives — the bricks win this time.'; }
ovScore.textContent='Score: '+score+' • Best: '+best;
overlay.classList.add('show');
beep(won?880:160,0.35,'sine',0.16);
}
// ---------- input ----------
function takeControl(){
initAudio();
if(mode==='demo'){ mode='manual'; demoBanner.style.display='none'; }
}
function canvasPos(e){
const r=canvas.getBoundingClientRect();
return (e.clientX-r.left)*(W/r.width);
}
canvas.addEventListener('pointermove',e=>{
mouseX=canvasPos(e); mouseActive=true; paddle.targetX=mouseX;
});
canvas.addEventListener('pointerdown',e=>{
takeControl(); mouseX=canvasPos(e); mouseActive=true; paddle.targetX=mouseX;
if(gameState==='waiting') launchBalls();
});
window.addEventListener('pointerdown',e=>{
// clicks outside canvas also take control (except buttons handled separately)
if(e.target.closest('button')) return;
takeControl();
if(gameState==='waiting' && !overlay.classList.contains('show')) launchBalls();
});
window.addEventListener('keydown',e=>{
const k=e.key;
if(['ArrowLeft','ArrowRight',' ','ArrowDown','ArrowUp'].includes(k)) e.preventDefault();
keys[k]=true;
takeControl();
if(overlay.classList.contains('show')){
if(k==='Enter'||k===' '||k==='r'||k==='R'){ resetGame(); mode='manual'; demoBanner.style.display='none'; }
return;
}
if(k===' '||k==='Spacebar'){
if(gameState==='waiting') launchBalls();
else if(gameState==='playing'||gameState==='paused') togglePause();
}
if(k==='p'||k==='P'||k==='Escape') togglePause();
if(k==='r'||k==='R'){ resetGame(); }
if(k==='m'||k==='M') toggleMute();
});
window.addEventListener('keyup',e=>{ keys[e.key]=false; });
function togglePause(){
if(gameState==='playing') gameState='paused';
else if(gameState==='paused') gameState='playing';
}
function toggleMute(){
muted=!muted; document.getElementById('soundBtn').textContent=muted?'🔇':'🔊';
}
document.getElementById('pauseBtn').addEventListener('click',()=>{ initAudio(); takeControl(); togglePause(); });
document.getElementById('soundBtn').addEventListener('click',()=>{ initAudio(); toggleMute(); });
document.getElementById('restartBtn').addEventListener('click',()=>{ initAudio(); takeControl(); demoBanner.style.display='none'; resetGame(); });
document.getElementById('playAgainBtn').addEventListener('click',()=>{ initAudio(); mode='manual'; demoBanner.style.display='none'; resetGame(); });
// ---------- start (demo auto-play) ----------
resetGame();
mode='demo';
demoBanner.style.display='block';
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (4,286 tokens)
Implementing Breakout in vanilla JS canvas with paddle controls, collision handling, power-ups, particles, lives, and self-playing demo mode.Adding attract-mode AI, power-up capsules, multi-ball logic, and substep collision handling.Extending collision with substep AABB resolution, paddle angle control, and HUD plus demo overlay.Implementing brick layout, toughness, power-ups, ball physics, and game state with substep collision and rendering.Defining per-row brick configs, power-up placement, HUD updates, overlays, input handling, and demo mode transitions.Adding audio beep, roundRect fallback, ball trail, brick patterns, powerup mapping, and overlay reset logic.