Claude Sonnet 5.5
by Anthropicreasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 42 s
- Cost
- $0.162
- Output tokens
- 16,028
- Spent thinking
- 4,908 tokens
- Speed
- 160.5 tok/s
- Console errors
- 0
- First token after
- 2.2 s
- First visible text after
- 46.5 s
- Prompt tokens
- 793
- Code size
- 19.4 KB
- Finish reason
- stop
- Served by
- Claude Platform on AWS
- Model id
anthropic/claude-sonnet-5.5- Reasoning sent
model default- 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 · 490 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>
html,body{margin:0;height:100%;background:#05060d;overflow:hidden;font-family:system-ui,sans-serif}
body{display:flex;align-items:center;justify-content:center}
canvas{display:block;background:#0a0c1a;box-shadow:0 0 60px rgba(80,120,255,.25);border-radius:6px;cursor:none;touch-action:none}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
const W = 800, H = 600, TOP = 48;
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
let scale = 1;
function resize() {
const dpr = window.devicePixelRatio || 1;
const w = Math.min(window.innerWidth, window.innerHeight * W / H);
const h = w * H / W;
cv.style.width = w + 'px';
cv.style.height = h + 'px';
cv.width = Math.round(w * dpr);
cv.height = Math.round(h * dpr);
scale = cv.width / W;
}
window.addEventListener('resize', resize);
resize();
// ---------- state ----------
const COLS = 10, ROWS = 7, BW = 67, BH = 24, GAP = 5;
const OFFX = (W - (COLS * BW + (COLS - 1) * GAP)) / 2, OFFY = 84;
const HUES = [350, 22, 46, 130, 180, 215, 275];
const PY = 556, PH = 14, BASE_W = 110, WIDE_W = 184, R = 7;
let phase = 'demo'; // demo | play | over | win
let score, lives, balls, bricks, drops, parts, paddle, wideT, hits, speed, shake, time = 0, phaseTime = 0, demoLaunch;
const keys = {};
let mouseActive = false;
const stars = Array.from({length: 60}, () => ({x: Math.random() * W, y: Math.random() * H, s: Math.random() * 1.5 + .3, v: Math.random() * 8 + 3}));
function newBall(stuck) {
return {x: paddle.x, y: PY - R - 1, vx: 0, vy: 0, stuck: !!stuck, trail: []};
}
function makeBricks() {
const list = [];
const pool = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) pool.push([r, c]);
const pu = new Map();
const picks = pool.slice().sort(() => Math.random() - .5).slice(0, 10);
picks.forEach((p, i) => pu.set(p[0] * COLS + p[1], i % 2 ? 'multi' : 'wide'));
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const tough = (r + c) % 3 === 0;
const crack = [];
let cx = BW * (.3 + Math.random() * .4), cy = 0;
crack.push([cx, 0]);
for (let k = 1; k <= 4; k++) { cx += (Math.random() - .5) * 18; cy = BH * k / 4; crack.push([Math.max(3, Math.min(BW - 3, cx)), cy]); }
list.push({
x: OFFX + c * (BW + GAP), y: OFFY + r * (BH + GAP), r, c,
hue: HUES[r], hp: tough ? 2 : 1, max: tough ? 2 : 1,
power: pu.get(r * COLS + c) || null, flash: 0, crack, alive: true
});
}
return list;
}
function resetGame() {
score = 0; lives = 3; hits = 0; speed = 400;
paddle = {x: W / 2, w: BASE_W};
wideT = 0; drops = []; parts = []; shake = 0;
bricks = makeBricks();
balls = [newBall(true)];
demoLaunch = 0.9;
}
resetGame();
function startGame() {
resetGame();
phase = 'play'; phaseTime = 0;
}
// ---------- input ----------
function toGame(e) {
const r = cv.getBoundingClientRect();
return (e.clientX - r.left) / r.width * W;
}
cv.addEventListener('pointermove', e => {
if (phase === 'play') { paddle.x = clampPaddle(toGame(e)); mouseActive = true; }
});
cv.addEventListener('pointerdown', e => {
if (phase === 'demo') { startGame(); paddle.x = clampPaddle(toGame(e)); return; }
if (phase === 'over' || phase === 'win') { if (phaseTime > .5) startGame(); return; }
if (phase === 'play') { paddle.x = clampPaddle(toGame(e)); launch(); }
});
window.addEventListener('keydown', e => {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', ' '].includes(e.key)) e.preventDefault();
keys[e.key] = true;
if (phase === 'demo') { startGame(); return; }
if (phase === 'over' || phase === 'win') { if (phaseTime > .5) startGame(); return; }
if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'Enter') launch();
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') mouseActive = false;
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
function clampPaddle(x) { return Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, x)); }
function launch() {
for (const b of balls) if (b.stuck) {
b.stuck = false;
const a = (Math.random() < .5 ? -1 : 1) * (0.2 + Math.random() * .2);
b.vx = speed * Math.sin(a); b.vy = -speed * Math.cos(a);
}
}
// ---------- helpers ----------
function normalize(b) {
let m = Math.hypot(b.vx, b.vy) || 1;
b.vx *= speed / m; b.vy *= speed / m;
const minY = speed * 0.26;
if (Math.abs(b.vy) < minY) {
b.vy = (b.vy < 0 ? -1 : 1) * minY;
const sx = Math.sqrt(speed * speed - b.vy * b.vy);
b.vx = (b.vx < 0 ? -1 : 1) * sx;
}
}
function burst(x, y, hue, n, power) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2, s = (power || 1) * (60 + Math.random() * 200);
parts.push({x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 40, life: .5 + Math.random() * .6, max: 1.1,
size: 2 + Math.random() * 4, hue, rot: Math.random() * 6, vr: (Math.random() - .5) * 12});
}
}
function hitBrick(br) {
br.hp--; hits++;
speed = Math.min(560, 400 + hits * 1.6);
br.flash = .15;
if (br.hp <= 0) {
br.alive = false;
score += br.max === 2 ? 30 : 10;
burst(br.x + BW / 2, br.y + BH / 2, br.hue, 16, 1);
shake = Math.min(6, shake + 2.5);
if (br.power) drops.push({x: br.x + BW / 2, y: br.y + BH / 2, type: br.power, vy: 140});
} else {
score += 5;
burst(br.x + BW / 2, br.y + BH / 2, 0, 7, .7);
shake = Math.min(6, shake + 1);
}
}
function applyPower(t) {
if (t === 'wide') wideT = 10;
else {
const src = balls.find(b => !b.stuck) || balls[0];
let i = 0;
while (balls.length < 3) {
const ang = Math.atan2(src.vy, src.vx) + (i++ % 2 ? .4 : -.4);
const sp = Math.hypot(src.vx, src.vy) || speed;
balls.push({x: src.x, y: src.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp, stuck: false, trail: []});
}
}
burst(paddle.x, PY, t === 'wide' ? 205 : 285, 18, 1);
}
// ---------- update ----------
function stepBall(b, dt) {
b.x += b.vx * dt; b.y += b.vy * dt;
// walls
if (b.x < R) { b.x = R; b.vx = Math.abs(b.vx); }
if (b.x > W - R) { b.x = W - R; b.vx = -Math.abs(b.vx); }
if (b.y < TOP + R) { b.y = TOP + R; b.vy = Math.abs(b.vy); }
// paddle
const hw = paddle.w / 2;
if (b.vy > 0 && b.y + R >= PY && b.y <= PY + PH * .5 && b.x >= paddle.x - hw - R * .6 && b.x <= paddle.x + hw + R * .6) {
const off = Math.max(-1, Math.min(1, (b.x - paddle.x) / hw));
const a = off * 1.12; // ~64deg max
b.vx = speed * Math.sin(a); b.vy = -speed * Math.cos(a);
b.y = PY - R;
burst(b.x, PY, 215, 4, .5);
}
// bricks
let best = null, bd = 1e9, bpx = 0, bpy = 0;
for (const br of bricks) {
if (!br.alive) continue;
const px = Math.max(br.x, Math.min(b.x, br.x + BW));
const py = Math.max(br.y, Math.min(b.y, br.y + BH));
const d = Math.hypot(b.x - px, b.y - py);
if (d < R && d < bd) { bd = d; best = br; bpx = px; bpy = py; }
}
if (best) {
let nx, ny;
if (bd < 1e-6) { // center inside brick: pick nearest face
const l = b.x - best.x, r = best.x + BW - b.x, t = b.y - best.y, bt = best.y + BH - b.y;
const m = Math.min(l, r, t, bt);
nx = ny = 0;
if (m === l) { nx = -1; bpx = best.x; bpy = b.y; }
else if (m === r) { nx = 1; bpx = best.x + BW; bpy = b.y; }
else if (m === t) { ny = -1; bpy = best.y; bpx = b.x; }
else { ny = 1; bpy = best.y + BH; bpx = b.x; }
} else { nx = (b.x - bpx) / bd; ny = (b.y - bpy) / bd; }
const dot = b.vx * nx + b.vy * ny;
if (dot < 0) { b.vx -= 2 * dot * nx; b.vy -= 2 * dot * ny; }
b.x = bpx + nx * (R + .05); b.y = bpy + ny * (R + .05);
hitBrick(best);
normalize(b);
}
}
function updateGame(dt) {
// paddle control
if (phase === 'demo') aiControl(dt);
else {
const k = (keys.ArrowRight ? 1 : 0) - (keys.ArrowLeft ? 1 : 0);
if (k) paddle.x += k * 720 * dt;
}
paddle.w += ((wideT > 0 ? WIDE_W : BASE_W) - paddle.w) * Math.min(1, dt * 10);
paddle.x = clampPaddle(paddle.x);
if (wideT > 0) wideT = Math.max(0, wideT - dt);
// balls
for (const b of balls) {
if (b.stuck) { b.x = paddle.x; b.y = PY - R - 1; b.trail.length = 0; continue; }
b.trail.push([b.x, b.y]); if (b.trail.length > 9) b.trail.shift();
const dist = Math.hypot(b.vx, b.vy) * dt;
const n = Math.max(1, Math.ceil(dist / 3));
for (let i = 0; i < n; i++) stepBall(b, dt / n);
}
balls = balls.filter(b => b.stuck || b.y < H + 20);
if (balls.length === 0) {
lives--; shake = 8;
drops = []; wideT = 0;
if (lives <= 0) {
if (phase === 'demo') { resetGame(); return; }
phase = 'over'; phaseTime = 0;
} else balls = [newBall(true)];
demoLaunch = .9;
}
// drops
for (const d of drops) {
d.y += d.vy * dt;
if (d.y > PY - 8 && d.y < PY + PH + 10 && Math.abs(d.x - paddle.x) < paddle.w / 2 + 16) {
d.dead = true; applyPower(d.type); score += 25;
} else if (d.y > H + 20) d.dead = true;
}
drops = drops.filter(d => !d.dead);
for (const br of bricks) if (br.flash > 0) br.flash -= dt;
// win
if (!bricks.some(b => b.alive)) {
if (phase === 'demo') { resetGame(); return; }
score += lives * 100;
phase = 'win'; phaseTime = 0;
for (let i = 0; i < 8; i++) burst(100 + Math.random() * 600, 150 + Math.random() * 200, Math.random() * 360, 18, 1.4);
}
}
function aiControl(dt) {
const st = balls.find(b => b.stuck);
if (st) { demoLaunch -= dt; if (demoLaunch <= 0) launch(); }
let target = paddle.x;
const live = balls.filter(b => !b.stuck);
let threat = null;
for (const b of live) if (b.vy > 0 && (!threat || b.y > threat.y)) threat = b;
const drop = drops.find(d => d.y > 300);
if (drop && (!threat || threat.y < 380)) target = drop.x;
else if (threat) target = threat.x + Math.sin(time * 1.7) * paddle.w * .32;
else if (live[0]) target = live[0].x;
const d = target - paddle.x, m = 620 * dt;
paddle.x += Math.max(-m, Math.min(m, d));
}
function update(dt) {
time += dt; phaseTime += dt;
for (const s of stars) { s.y += s.v * dt; if (s.y > H) { s.y = 0; s.x = Math.random() * W; } }
for (const p of parts) { p.life -= dt; p.vy += 520 * dt; p.x += p.vx * dt; p.y += p.vy * dt; p.rot += p.vr * dt; }
parts = parts.filter(p => p.life > 0);
shake *= Math.pow(.02, dt);
if (phase === 'demo' || phase === 'play') updateGame(dt);
else if (phase === 'win' && Math.random() < dt * 2) burst(100 + Math.random() * 600, 120 + Math.random() * 200, Math.random() * 360, 14, 1.2);
}
// ---------- drawing ----------
function rr(x, y, w, h, r) { ctx.beginPath(); ctx.roundRect(x, y, w, h, r); }
function drawIcon(type, x, y, s) {
ctx.save(); ctx.translate(x, y); ctx.scale(s, s);
ctx.strokeStyle = '#fff'; ctx.fillStyle = '#fff'; ctx.lineWidth = 1.8; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
if (type === 'wide') {
ctx.beginPath(); ctx.moveTo(-7, 0); ctx.lineTo(7, 0);
ctx.moveTo(-7, 0); ctx.lineTo(-4, -3); ctx.moveTo(-7, 0); ctx.lineTo(-4, 3);
ctx.moveTo(7, 0); ctx.lineTo(4, -3); ctx.moveTo(7, 0); ctx.lineTo(4, 3);
ctx.stroke();
} else {
for (const [dx, dy] of [[-4.5, 2.5], [4.5, 2.5], [0, -3.5]]) { ctx.beginPath(); ctx.arc(dx, dy, 2.4, 0, 7); ctx.fill(); }
}
ctx.restore();
}
function drawBrick(br) {
const {x, y} = br;
const tough = br.max === 2, dmg = tough && br.hp === 1;
ctx.save();
ctx.shadowColor = `hsla(${br.hue},90%,60%,.45)`; ctx.shadowBlur = 8;
const g = ctx.createLinearGradient(0, y, 0, y + BH);
if (tough && !dmg) { g.addColorStop(0, `hsl(${br.hue},35%,52%)`); g.addColorStop(1, `hsl(${br.hue},40%,26%)`); }
else if (dmg) { g.addColorStop(0, `hsl(${br.hue},60%,60%)`); g.addColorStop(1, `hsl(${br.hue},65%,40%)`); }
else { g.addColorStop(0, `hsl(${br.hue},85%,64%)`); g.addColorStop(1, `hsl(${br.hue},80%,42%)`); }
ctx.fillStyle = g; rr(x, y, BW, BH, 5); ctx.fill();
ctx.shadowBlur = 0;
// highlight
ctx.fillStyle = 'rgba(255,255,255,.22)'; rr(x + 2, y + 2, BW - 4, 5, 3); ctx.fill();
if (tough) {
ctx.strokeStyle = dmg ? 'rgba(210,215,230,.55)' : 'rgba(225,230,245,.95)'; ctx.lineWidth = 2;
rr(x + 1, y + 1, BW - 2, BH - 2, 4); ctx.stroke();
ctx.fillStyle = 'rgba(230,235,250,.8)';
if (!dmg) for (const [dx, dy] of [[5, 5], [BW - 5, 5], [5, BH - 5], [BW - 5, BH - 5]]) { ctx.beginPath(); ctx.arc(x + dx, y + dy, 1.3, 0, 7); ctx.fill(); }
}
if (dmg) { // cracks
ctx.save(); ctx.beginPath(); ctx.roundRect(x, y, BW, BH, 5); ctx.clip();
ctx.strokeStyle = 'rgba(10,10,20,.85)'; ctx.lineWidth = 1.6; ctx.lineJoin = 'round';
ctx.beginPath(); br.crack.forEach((p, i) => i ? ctx.lineTo(x + p[0], y + p[1]) : ctx.moveTo(x + p[0], y + p[1]));
const m = br.crack[2]; ctx.moveTo(x + m[0], y + m[1]); ctx.lineTo(x + m[0] + 14, y + m[1] - 4); ctx.lineTo(x + m[0] + 22, y + m[1] + 3);
ctx.moveTo(x + m[0], y + m[1]); ctx.lineTo(x + m[0] - 13, y + m[1] + 5);
ctx.stroke();
ctx.strokeStyle = 'rgba(255,255,255,.35)'; ctx.lineWidth = .8; ctx.translate(1, 1);
ctx.beginPath(); br.crack.forEach((p, i) => i ? ctx.lineTo(x + p[0], y + p[1]) : ctx.moveTo(x + p[0], y + p[1])); ctx.stroke();
ctx.restore();
}
if (br.power) {
const cx = x + BW / 2, cy = y + BH / 2;
ctx.fillStyle = br.power === 'wide' ? '#1d7bff' : '#a43bff';
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(cx, cy, 9.5, 0, 7); ctx.fill(); ctx.stroke();
drawIcon(br.power, cx, cy, .85);
}
if (br.flash > 0) { ctx.fillStyle = `rgba(255,255,255,${br.flash * 4})`; rr(x, y, BW, BH, 5); ctx.fill(); }
ctx.restore();
}
function drawText(t, x, y, size, color, align, weight) {
ctx.font = `${weight || 700} ${size}px system-ui, sans-serif`;
ctx.textAlign = align || 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = color; ctx.fillText(t, x, y);
}
function draw() {
ctx.setTransform(scale, 0, 0, scale, 0, 0);
// background
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#12183a'); bg.addColorStop(1, '#070812');
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = 'rgba(120,150,255,.05)'; ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x <= W; x += 40) { ctx.moveTo(x, TOP); ctx.lineTo(x, H); }
for (let y = TOP; y <= H; y += 40) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
ctx.stroke();
ctx.fillStyle = '#fff';
for (const s of stars) { ctx.globalAlpha = .15 + s.s * .2; ctx.fillRect(s.x, s.y, s.s, s.s); }
ctx.globalAlpha = 1;
ctx.save();
if (shake > .2) ctx.translate((Math.random() - .5) * shake, (Math.random() - .5) * shake);
for (const br of bricks) if (br.alive) drawBrick(br);
// drops
for (const d of drops) {
ctx.save(); ctx.translate(d.x, d.y);
const c = d.type === 'wide' ? 215 : 280;
ctx.shadowColor = `hsl(${c},100%,60%)`; ctx.shadowBlur = 14;
const g = ctx.createLinearGradient(0, -10, 0, 10);
g.addColorStop(0, `hsl(${c},90%,65%)`); g.addColorStop(1, `hsl(${c},85%,40%)`);
ctx.fillStyle = g; rr(-18, -10, 36, 20, 10); ctx.fill();
ctx.shadowBlur = 0; ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; rr(-18, -10, 36, 20, 10); ctx.stroke();
drawIcon(d.type, 0, 0, 1);
ctx.restore();
}
// paddle
const hw = paddle.w / 2;
ctx.save();
const blink = wideT > 0 && wideT < 2 && Math.floor(time * 6) % 2;
ctx.shadowColor = wideT > 0 ? '#3d9bff' : '#6d8cff'; ctx.shadowBlur = 18;
const pg = ctx.createLinearGradient(0, PY, 0, PY + PH);
pg.addColorStop(0, blink ? '#fff' : '#e8efff'); pg.addColorStop(.5, wideT > 0 ? '#3d9bff' : '#7e93ff'); pg.addColorStop(1, '#2c3a9a');
ctx.fillStyle = pg; rr(paddle.x - hw, PY, paddle.w, PH, 7); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,.4)'; rr(paddle.x - hw + 6, PY + 2, paddle.w - 12, 3, 2); ctx.fill();
ctx.restore();
// balls
for (const b of balls) {
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i], f = (i + 1) / b.trail.length;
ctx.fillStyle = `rgba(160,200,255,${f * .25})`;
ctx.beginPath(); ctx.arc(t[0], t[1], R * f * .9, 0, 7); ctx.fill();
}
ctx.save();
ctx.shadowColor = '#9fd0ff'; ctx.shadowBlur = 16;
const rg = ctx.createRadialGradient(b.x - 2, b.y - 2, 1, b.x, b.y, R);
rg.addColorStop(0, '#fff'); rg.addColorStop(1, '#8ec5ff');
ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, 7); ctx.fill();
ctx.restore();
}
// particles
for (const p of parts) {
const a = Math.max(0, Math.min(1, p.life / p.max * 1.5));
ctx.save(); ctx.globalAlpha = a; ctx.translate(p.x, p.y); ctx.rotate(p.rot);
ctx.fillStyle = p.hue === 0 ? '#e8ecff' : `hsl(${p.hue},90%,62%)`;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
}
ctx.restore();
// HUD
ctx.fillStyle = 'rgba(8,10,25,.9)'; ctx.fillRect(0, 0, W, TOP);
ctx.fillStyle = 'rgba(120,150,255,.35)'; ctx.fillRect(0, TOP - 1, W, 1);
drawText('SCORE', 20, 24, 12, '#7f8fd0', 'left');
drawText(String(score).padStart(6, '0'), 76, 25, 22, '#fff', 'left');
drawText('BREAKOUT', W / 2, 25, 18, '#9fb2ff', 'center', 800);
for (let i = 0; i < 3; i++) {
const x = W - 30 - i * 26, on = i < lives;
ctx.save(); ctx.translate(x, 25);
ctx.fillStyle = on ? '#ff4d6d' : 'rgba(255,255,255,.12)';
if (on) { ctx.shadowColor = '#ff4d6d'; ctx.shadowBlur = 10; }
ctx.beginPath();
ctx.moveTo(0, 8); ctx.bezierCurveTo(-14, -2, -8, -12, 0, -4); ctx.bezierCurveTo(8, -12, 14, -2, 0, 8); ctx.fill();
ctx.restore();
}
if (wideT > 0) {
ctx.fillStyle = 'rgba(255,255,255,.1)'; rr(W - 220, 18, 90, 10, 5); ctx.fill();
ctx.fillStyle = '#3d9bff'; rr(W - 220, 18, 90 * wideT / 10, 10, 5); ctx.fill();
drawText('WIDE', W - 228, 24, 11, '#7fb8ff', 'right');
}
// overlays
if (phase === 'demo') {
ctx.fillStyle = 'rgba(5,7,20,.6)'; rr(W / 2 - 250, 345, 500, 130, 16); ctx.fill();
ctx.strokeStyle = 'rgba(130,160,255,.5)'; ctx.lineWidth = 1.5; rr(W / 2 - 250, 345, 500, 130, 16); ctx.stroke();
drawText('BREAKOUT', W / 2, 382, 36, '#fff', 'center', 900);
const pulse = .65 + .35 * Math.sin(time * 4);
drawText('Click or press a key to start', W / 2, 425, 22, `rgba(160,200,255,${pulse})`);
drawText('Mouse or ← → to move • catch power-ups', W / 2, 455, 14, '#8a97c8', 'center', 500);
} else if (phase === 'play') {
if (balls.some(b => b.stuck)) {
const pulse = .6 + .4 * Math.sin(time * 5);
drawText('Click or press Space to launch', W / 2, 470, 18, `rgba(190,215,255,${pulse})`);
}
} else {
ctx.fillStyle = 'rgba(4,5,15,.72)'; ctx.fillRect(0, TOP, W, H - TOP);
const win = phase === 'win';
const k = Math.min(1, phaseTime * 3);
ctx.save(); ctx.translate(W / 2, 320); ctx.scale(.85 + .15 * k, .85 + .15 * k); ctx.globalAlpha = k;
ctx.fillStyle = 'rgba(14,18,48,.95)'; rr(-230, -130, 460, 260, 20); ctx.fill();
ctx.strokeStyle = win ? '#ffd24d' : '#ff4d6d'; ctx.lineWidth = 2; rr(-230, -130, 460, 260, 20); ctx.stroke();
ctx.shadowColor = win ? '#ffd24d' : '#ff4d6d'; ctx.shadowBlur = 20;
drawText(win ? 'YOU WIN!' : 'GAME OVER', 0, -78, 48, win ? '#ffd24d' : '#ff4d6d', 'center', 900);
ctx.shadowBlur = 0;
drawText('FINAL SCORE', 0, -25, 13, '#8a97c8');
drawText(String(score), 0, 8, 40, '#fff', 'center', 800);
ctx.fillStyle = '#4a63ff'; rr(-110, 52, 220, 44, 22); ctx.fill();
drawText('PLAY AGAIN', 0, 75, 18, '#fff', 'center', 800);
drawText('Click or press any key', 0, 112, 12, '#8a97c8', 'center', 500);
ctx.restore();
}
}
let last = performance.now();
function loop(now) {
const dt = Math.min(1 / 30, (now - last) / 1000); last = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (4,908 tokens)
I'm working out how to build a polished single-file Breakout game, and it needs a self-playing demo mode that waits behind a "Click or press a key to start" message. I'm planning a fixed 800x600 logical canvas that scales with CSS to fit its container, with resize handling that accounts for devicePixelRatio, and four states: demo, playing, over, and win. I'm now settling on the ball and paddle mechanics: the ball stays on the paddle until a click or key press, with an auto-launch after about two seconds as a fallback. For collisions I'm planning small sub-steps of roughly 4px, resolving bricks by the nearest point on each rectangle, and setting the paddle bounce angle from where the ball hits it. I'm setting the brick layout next: a 10-by-7 grid sized to the 800-pixel width, with some tougher bricks that take two hits and show cracks once damaged. I'm also placing a few power-up bricks that drop capsules for a wider paddle or extra balls. I'm adding the multi-ball power-up, which keeps spawning balls until there are at least three, and the ball count going to zero costs a life. I'm also planning falling particles with gravity and a slight screen shake, scoring 10 per normal brick and 20 for a tough one, and drawing lives as small circles. The win state triggers when every brick is gone, and clicking or pressing Enter or Space restarts from the game-over screen. I'm setting the demo's paddle to follow the lowest ball, with a sine offset so it hits at varied angles, and it auto-launches. When a game ends in demo, it restarts on its own shortly after. Mouse movement and arrow keys both move the paddle, with touch handled through pointer events. The ball launches on a click, the space bar, or the up arrow, or else after about 2.5 seconds with a "Click or Space to launch" hint. I'm settling the start behavior: the first click moves from demo into play, resetting the game with the ball sitting on the paddle, and keyboard input only counts once the user has clicked. I'm skipping sound to avoid complications, and turning to the visual style, with a dark gradient backdrop, a glowing ball with a trail, and a top HUD for score and lives. Still writing the game code... Still working through the brick layout, sizing the rows and spacing so the grid ends above the paddle area. Still writing the collision code, now for the brick hits: for each substep I check which living brick the ball touches by finding the closest point on each rectangle, and I'll resolve the one with the deepest overlap and reflect the velocity off its surface normal. Still working on the collision code: the paddle bounce is now the focus, so the ball reflects when it's moving down and crosses the paddle's top edge, with the exit angle set by where it hits. Still writing the game code... Still writing the game code... Still writing the game code, now moving on to the input handling: the pointer and keyboard listeners that start the game and restart it from the game-over screen. Still writing the game code, now covering the demo's paddle logic: it targets the nearest falling ball, with a slight sine offset so it hits at varied angles, and moves at a capped speed. Still writing the game code... Still writing the game code, now on the brick flash, the launch hint, and the demo overlay panel. Still writing the game code, now on the wide-paddle and multi-ball power-up rendering, plus the phase setup for demo, play, over, and win. Still writing the game code, now on the demo's end-of-game handling: when a life is lost or the bricks are cleared in demo mode, it resets straight away rather than waiting on a timer. Still writing the game code, now on ball speed normalization and the wall bounces, with the top wall sitting just below the HUD. Still finishing up the game code...