Hamburger breakoutOne-line
Claude Haiku 5.5
by AnthropicReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 3 s
- Cost
- $0.0075
- Output tokens
- 14,857
- Spent thinking
- 6,450 tokens
- Speed
- 243.3 tok/s
- Console errors
- 0
- First token after
- 2.0 s
- First visible text after
- 37.8 s
- Prompt tokens
- 542
- Code size
- 15.9 KB
- Finish reason
- stop
- Served by
- Claude Platform on AWS
- Model id
anthropic/claude-haiku-5.5- Reasoning sent
{"effort":"medium"}- Max output
- 32,000 tokens
- Generated
- Nov 10, 2026
What it was asked
The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
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 · 551 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Burger Breakout — Heartbreak Edition</title>
<style>
html, body {
margin: 0;
height: 100%;
background: radial-gradient(circle at 50% 40%, #3b2238, #1a0f1c 70%);
overflow: hidden;
font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
}
body { display: flex; align-items: center; justify-content: center; }
#wrap { position: relative; line-height: 0; }
canvas {
display: block;
border-radius: 14px;
box-shadow: 0 10px 40px rgba(0,0,0,.5), 0 0 0 3px rgba(255,255,255,.08);
cursor: none;
touch-action: none;
}
#msg {
position: absolute;
left: 50%;
top: 64%;
transform: translate(-50%, -50%);
background: rgba(255,255,255,.93);
color: #8a1537;
padding: 12px 22px 10px;
border-radius: 999px;
font-weight: 800;
font-size: clamp(14px, 2.2vw, 20px);
text-align: center;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 6px 20px rgba(0,0,0,.25);
transition: opacity .6s ease;
animation: pulse 1.6s ease-in-out infinite;
line-height: 1.2;
}
#msg small {
display: block;
font-weight: 600;
font-size: 0.66em;
color: #6b4a57;
margin-top: 3px;
}
#msg.hidden { opacity: 0; }
@keyframes pulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.05); }
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="c"></canvas>
<div id="msg">Click or press a key to start<small>Demo mode is playing: the hamburger is on autopilot</small></div>
</div>
<script>
(() => {
const W = 800, H = 600;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const msg = document.getElementById('msg');
let scale = 1, dpr = window.devicePixelRatio || 1;
function resize() {
dpr = window.devicePixelRatio || 1;
scale = Math.max(0.2, Math.min((innerWidth - 16) / W, (innerHeight - 16) / H));
canvas.style.width = (W * scale) + 'px';
canvas.style.height = (H * scale) + 'px';
canvas.width = Math.round(W * scale * dpr);
canvas.height = Math.round(H * scale * dpr);
}
window.addEventListener('resize', resize);
resize();
// ---------- Game state ----------
const COLORS = ['#ff5d73', '#ff9f1c', '#ffd23f', '#7ed957', '#3ec1d3', '#9d6bff'];
const HEART_COLORS = ['#ff2d55', '#ff6b8a', '#ff9ab0', '#e60039', '#ffc2cf', '#ff4d6d', '#ffffff'];
const PAD = { w: 124, h: 46, y: 548 };
let mode = 'demo'; // 'demo' or 'play'
let started = false;
let paddleX = W / 2; // center x
let mouseX = null;
let keyDir = 0;
let bricks = [], hearts = [];
let ball, lives, score, level, banner = { text: '', t: 0 };
let serveTimer = 1, gameOverTimer = 0, gameOver = false, squish = 0, shake = 0;
let time = 0;
function makeBricks() {
const arr = [];
const cols = 10, rows = 6, bw = 72, bh = 24, gap = 4;
const x0 = (W - (cols * (bw + gap) - gap)) / 2;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
arr.push({
x: x0 + c * (bw + gap),
y: 78 + r * (bh + gap),
w: bw, h: bh,
color: COLORS[r % COLORS.length],
alive: true
});
}
}
return arr;
}
function spawnBall() {
ball = { x: paddleX, y: PAD.y - 9, vx: 0, vy: 0, r: 7, attached: true };
}
function resetGame() {
bricks = makeBricks();
hearts = [];
lives = 3;
score = 0;
level = 1;
gameOver = false;
serveTimer = 1.2;
spawnBall();
showBanner('LEVEL 1', 1.6);
}
function showBanner(text, t) { banner = { text, t }; }
function speed() { return Math.min(9.5, 5 + (level - 1) * 0.45); }
// ---------- Hearts ----------
function burst(b) {
const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
const count = 55;
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 1.5 + Math.random() * 7.5;
hearts.push({
x: cx + (Math.random() - 0.5) * b.w * 0.8,
y: cy + (Math.random() - 0.5) * b.h * 0.8,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 2.2,
size: 4 + Math.random() * 9,
rot: Math.random() * Math.PI * 2,
vr: (Math.random() - 0.5) * 0.25,
life: 1.4 + Math.random() * 1.3,
max: 0,
color: HEART_COLORS[(Math.random() * HEART_COLORS.length) | 0],
pop: 0.15 + Math.random() * 0.25
});
hearts[hearts.length - 1].max = hearts[hearts.length - 1].life;
}
if (hearts.length > 2000) hearts.splice(0, hearts.length - 2000);
}
function heartPath(x, y, s) {
ctx.beginPath();
ctx.moveTo(x, y + s * 0.3);
ctx.bezierCurveTo(x - s * 0.5, y - s * 0.1, x - s * 0.25, y - s * 0.6, x, y - s * 0.25);
ctx.bezierCurveTo(x + s * 0.25, y - s * 0.6, x + s * 0.5, y - s * 0.1, x, y + s * 0.3);
ctx.closePath();
}
// ---------- Update ----------
function update(dt) {
time += dt / 60;
if (banner.t > 0) banner.t -= dt / 60;
if (shake > 0) shake *= Math.pow(0.85, dt);
squish *= Math.pow(0.8, dt);
// Paddle control
const half = PAD.w / 2;
if (mode === 'demo' || !started) {
const target = ball.attached ? paddleX : ball.x + Math.sin(time * 1.3) * 7;
const maxStep = 7.2 * dt;
paddleX += Math.max(-maxStep, Math.min(maxStep, target - paddleX));
} else {
if (keyDir !== 0) {
paddleX += keyDir * 9 * dt;
} else if (mouseX !== null) {
paddleX += (mouseX - paddleX) * Math.min(1, 0.35 * dt);
}
}
paddleX = Math.max(half, Math.min(W - half, paddleX));
// Ball serve / move
if (ball.attached) {
ball.x = paddleX;
ball.y = PAD.y - ball.r - 2;
serveTimer -= dt / 60;
if (serveTimer <= 0 && !gameOver) {
const a = (Math.random() - 0.5) * 0.8;
const sp = speed();
ball.vx = Math.sin(a) * sp;
ball.vy = -Math.cos(a) * sp;
ball.attached = false;
}
} else {
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
const r = ball.r;
if (ball.x < r) { ball.x = r; ball.vx = Math.abs(ball.vx); }
if (ball.x > W - r) { ball.x = W - r; ball.vx = -Math.abs(ball.vx); }
if (ball.y < r) { ball.y = r; ball.vy = Math.abs(ball.vy); }
// Paddle collision
const px = paddleX - half;
if (ball.vy > 0 && ball.y + r >= PAD.y && ball.y - r <= PAD.y + PAD.h * 0.5 &&
ball.x >= px - r && ball.x <= px + PAD.w + r) {
const t = Math.max(-1, Math.min(1, (ball.x - paddleX) / half));
const ang = t * 1.1;
const sp = speed();
ball.vx = Math.sin(ang) * sp;
ball.vy = -Math.cos(ang) * sp;
ball.y = PAD.y - r;
squish = 1;
}
// Brick collision (one brick per frame)
for (const b of bricks) {
if (!b.alive) continue;
const cx = Math.max(b.x, Math.min(ball.x, b.x + b.w));
const cy = Math.max(b.y, Math.min(ball.y, b.y + b.h));
const dx = ball.x - cx, dy = ball.y - cy;
if (dx * dx + dy * dy <= ball.r * ball.r) {
b.alive = false;
if (Math.abs(dx) > Math.abs(dy)) ball.vx *= -1;
else ball.vy *= -1;
if (dx === 0 && dy === 0) ball.vy *= -1;
burst(b);
shake = 6;
score += 10 * level;
break;
}
}
// Lost ball
if (ball.y > H + 20) {
lives--;
if (lives <= 0) {
gameOver = true;
gameOverTimer = 3.5;
showBanner('GAME OVER', 3.5);
} else {
spawnBall();
serveTimer = 1;
showBanner('LIFE LOST', 1);
}
}
// Cleared
if (!gameOver && bricks.every(b => !b.alive)) {
level++;
bricks = makeBricks();
spawnBall();
serveTimer = 1.4;
showBanner('LEVEL ' + level, 1.6);
}
}
if (gameOver) {
gameOverTimer -= dt / 60;
if (gameOverTimer <= 0) resetGame();
}
// Hearts physics
for (const h of hearts) {
h.vy += 0.09 * dt;
h.vx *= Math.pow(0.985, dt);
h.vy *= Math.pow(0.985, dt);
h.x += h.vx * dt;
h.y += h.vy * dt;
h.rot += h.vr * dt;
h.life -= dt / 60;
}
hearts = hearts.filter(h => h.life > 0);
}
// ---------- Drawing ----------
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#fff3ea');
g.addColorStop(1, '#ffd9e3');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'rgba(255,255,255,.35)';
for (let x = 20; x < W; x += 40) {
for (let y = 20; y < H; y += 40) {
ctx.beginPath();
ctx.arc(x, y, 1.6, 0, Math.PI * 2);
ctx.fill();
}
}
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawBricks() {
for (const b of bricks) {
if (!b.alive) continue;
// shadow
ctx.fillStyle = 'rgba(90,30,50,.25)';
roundRect(b.x + 2, b.y + 3, b.w, b.h, 7);
ctx.fill();
// body
ctx.fillStyle = b.color;
roundRect(b.x, b.y, b.w, b.h, 7);
ctx.fill();
// shine
ctx.fillStyle = 'rgba(255,255,255,.35)';
roundRect(b.x + 4, b.y + 3, b.w - 8, 6, 3);
ctx.fill();
// little heart emblem
ctx.fillStyle = 'rgba(255,255,255,.85)';
heartPath(b.x + b.w / 2, b.y + b.h / 2 + 1, 9);
ctx.fill();
}
}
// Hamburger paddle
function drawBurger(cx, y, w) {
const x = cx - w / 2;
const sq = squish * 5;
ctx.save();
ctx.translate(cx, y + PAD.h);
ctx.scale(1 + sq * 0.03, 1 - sq * 0.05);
ctx.translate(-cx, -(y + PAD.h));
const bottom = y + PAD.h;
// bottom bun
ctx.fillStyle = '#c9803a';
roundRect(x + 2, bottom - 10, w - 4, 10, 5);
ctx.fill();
ctx.fillStyle = '#e09a4f';
roundRect(x + 4, bottom - 10, w - 8, 4, 3);
ctx.fill();
// patty
ctx.fillStyle = '#5a2e14';
roundRect(x + 1, bottom - 17, w - 2, 8, 4);
ctx.fill();
ctx.fillStyle = '#7d4522';
roundRect(x + 6, bottom - 16, w - 12, 2.5, 2);
ctx.fill();
// cheese with drips
ctx.fillStyle = '#ffd23f';
ctx.beginPath();
ctx.moveTo(x + 1, bottom - 22);
ctx.lineTo(x + w - 1, bottom - 22);
ctx.lineTo(x + w - 1, bottom - 17);
for (let i = 0; i < 8; i++) {
const px2 = x + w - 1 - (i + 1) * (w - 2) / 8;
ctx.lineTo(px2, bottom - 17 + (i % 2 ? 1 : 3.5));
}
ctx.lineTo(x + 1, bottom - 17);
ctx.closePath();
ctx.fill();
// tomato slice
ctx.fillStyle = '#e53935';
roundRect(x + 4, bottom - 25, w - 8, 4.5, 2.5);
ctx.fill();
ctx.fillStyle = '#ff7a6b';
roundRect(x + 8, bottom - 24.5, w - 16, 1.5, 1);
ctx.fill();
// lettuce (wavy)
ctx.fillStyle = '#4fb34f';
ctx.beginPath();
const ly = bottom - 31;
ctx.moveTo(x, ly);
for (let i = 0; i <= 12; i++) {
const px2 = x + i * w / 12;
ctx.lineTo(px2, ly + (i % 2 ? 4.5 : 0));
}
ctx.lineTo(x + w, ly - 2);
ctx.lineTo(x, ly - 2);
ctx.closePath();
ctx.fill();
// top bun dome
ctx.fillStyle = '#eaa450';
ctx.beginPath();
const topY = y + 2;
ctx.moveTo(x, ly - 1);
ctx.bezierCurveTo(x - 2, topY - 2, x + w * 0.25, topY - 8, cx, topY - 8);
ctx.bezierCurveTo(x + w * 0.75, topY - 8, x + w + 2, topY - 2, x + w, ly - 1);
ctx.closePath();
ctx.fill();
// sesame seeds
ctx.fillStyle = '#fff6dc';
const seeds = [[0.22, -3], [0.36, -5.5], [0.5, -6.2], [0.64, -5.5], [0.78, -3], [0.44, -2.5], [0.58, -2.5]];
for (const [fx, fy] of seeds) {
ctx.beginPath();
ctx.ellipse(x + w * fx, topY + fy + 0.5, 2.2, 1.2, 0.4, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawBall() {
const g = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.r);
g.addColorStop(0, '#ffb3b3');
g.addColorStop(0.5, '#ff3b30');
g.addColorStop(1, '#a80f0a');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,.7)';
ctx.beginPath();
ctx.arc(ball.x - 2.2, ball.y - 2.4, 1.8, 0, Math.PI * 2);
ctx.fill();
}
function drawHearts() {
for (const h of hearts) {
const a = Math.max(0, Math.min(1, h.life / h.max * 1.6));
const pop = Math.min(1, (h.max - h.life) / h.pop * 1);
ctx.save();
ctx.globalAlpha = a;
ctx.translate(h.x, h.y);
ctx.rotate(h.rot);
ctx.fillStyle = h.color;
heartPath(0, 0, h.size * (0.4 + 0.6 * Math.min(1, pop)));
ctx.fill();
ctx.restore();
}
ctx.globalAlpha = 1;
}
function drawHeartIcon(x, y, s, color) {
ctx.fillStyle = color;
heartPath(x, y, s);
ctx.fill();
}
function drawHUD() {
ctx.fillStyle = '#7a1537';
ctx.font = '800 18px system-ui, Helvetica, Arial, sans-serif';
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
ctx.fillText('SCORE ' + score, 20, 30);
ctx.textAlign = 'center';
ctx.fillText('LEVEL ' + level, W / 2, 30);
ctx.textAlign = 'right';
ctx.fillText('LIVES', W - 100, 30);
for (let i = 0; i < 3; i++) {
drawHeartIcon(W - 70 + i * 22, 30, i < lives ? 16 : 7,
i < lives ? '#ff2d55' : 'rgba(122,21,55,.25)');
}
if (!started) {
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(122,21,55,.6)';
ctx.font = '700 12px system-ui, Helvetica, Arial, sans-serif';
ctx.fillText('DEMO', 20, H - 14);
}
}
function drawBanner() {
if (banner.t <= 0 || !banner.text) return;
const a = Math.min(1, banner.t * 2);
ctx.save();
ctx.globalAlpha = a;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '900 56px system-ui, Helvetica, Arial, sans-serif';
ctx.lineWidth = 8;
ctx.strokeStyle = 'rgba(255,255,255,.9)';
ctx.strokeText(banner.text, W / 2, 330);
ctx.fillStyle = '#ff2d55';
ctx.fillText(banner.text, W / 2, 330);
ctx.restore();
}
function draw() {
ctx.setTransform(scale * dpr, 0, 0, scale * dpr, 0, 0);
ctx.save();
if (shake > 0.5) ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
drawBackground();
drawBricks();
drawBurger(paddleX, PAD.y, PAD.w);
drawBall();
drawHearts();
drawHUD();
drawBanner();
ctx.restore();
}
// ---------- Input ----------
function start() {
if (started) return;
started = true;
mode = 'play';
msg.classList.add('hidden');
showBanner('GO!', 0.8);
}
function toCanvasX(clientX) {
const r = canvas.getBoundingClientRect();
return (clientX - r.left) / r.width * W;
}
canvas.addEventListener('pointermove', e => {
mouseX = toCanvasX(e.clientX);
if (started) keyDir = 0;
});
canvas.addEventListener('pointerleave', () => { mouseX = null; });
canvas.addEventListener('pointerdown', e => {
start();
mouseX = toCanvasX(e.clientX);
});
document.addEventListener('pointerdown', start);
window.addEventListener('keydown', e => {
start();
if (['ArrowLeft', 'a', 'A'].includes(e.key)) { keyDir = -1; e.preventDefault(); }
else if (['ArrowRight', 'd', 'D'].includes(e.key)) { keyDir = 1; e.preventDefault(); }
else if (e.key === ' ') {
e.preventDefault();
if (ball.attached) serveTimer = 0;
}
});
window.addEventListener('keyup', e => {
if (['ArrowLeft', 'a', 'A'].includes(e.key) && keyDir === -1) keyDir = 0;
if (['ArrowRight', 'd', 'D'].includes(e.key) && keyDir === 1) keyDir = 0;
});
// ---------- Loop ----------
resetGame();
let last = performance.now();
function frame(now) {
const dt = Math.min(3, (now - last) / 16.667);
last = now;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (6,450 tokens)
I'm building a single HTML breakout game where the paddle is a hamburger and the bricks burst into hearts, with a self-playing demo behind a "Click or press a key to start" prompt. The bricks, the paddle and an AI-controlled ball have to be visible on first paint, and I want it compact but polished. For the layout I'm thinking a canvas around 1280x800, and I'm starting on the paddle drawing: a rounded bun on top with sesame seeds, followed by the lettuce. Now I'm working out the rest of the paddle, with lettuce, patty, cheese, and a bottom bun, sized about 120 by 50. The bricks get food-like colors in rows, and the ball stays a small red circle. For the heart explosions, I'm planning particles drawn as heart paths in varied sizes and pink or red shades, drifting outward and fading. I'm settling the canvas scaling: a fixed 800x600 logical size, scaled to fit the window and letterboxed, with the backing store sized for the device pixel ratio and mouse coordinates mapped back through the bounding rect. For the physics I'm going dt-based with the ball around 5 px per frame, and laying out the bricks in 10 columns by 6 rows with a 72-pixel width and 4-pixel gap, offset to center them. Each brick gets its own food-style color, and when one breaks it spawns 40 to 60 hearts with random velocities that drift slightly upward and fade over about 1.5 seconds, drawn with a bezier heart function. Now I'm wiring up ball-paddle collisions so the rebound angle depends on where the ball hits, and handling walls and brick hits by checking which side was overlapped. Now I'm setting the paddle's hit box and layering its hamburger parts from top to bottom: sesame bun, lettuce, tomato, cheese, patty, and bottom bun, roughly 48 pixels tall. Now I'm sketching the paddle's drawing routine: the bottom bun first, then the patty, cheese, and tomato stacked above it, with the lettuce still to come. Still writing the paddle drawing code... Still working out the collision code: for each live brick I'm checking for a circle-rectangle overlap by finding the closest point on the brick to the ball's center, and when they overlap I'll reflect whichever axis the offset is larger on. Now I'm setting the paddle's hit zone at the bottom of the canvas and working out how the ball rebounds from it, with the angle depending on where it lands. I'm settling the demo and play controls: the AI tracks the ball with a small deliberate lag and error, while the mouse and arrow keys take over once the game starts. The first click or keypress hides the overlay, and I still need to decide when the ball launches, either automatically after about a second in demo or on click in play mode. Now I'm wiring up the game over, level clear, and overlay: after a loss in play mode it shows a "click to play again" message, in demo it resets on its own after three seconds, and clearing the bricks starts the next level with a short banner. Now I'm setting the background to a warm cream-to-pink gradient with a faint diagonal pattern, and giving the bricks rounded corners and a shine highlight. I'm also laying out the brick grid as 6 rows of 10 with food-colored rows, each brick 72 by 24 with a 4-pixel gap, centered starting around y=78. Still writing the game update loop... Still working through the ball's movement loop: it steps by its velocity each frame, bounces off the side and top walls, and the paddle keeps clamping to the canvas edges. Still writing the game loop, with the lives, serve timer, and dt capped at three frames. Still writing the heart path with bezier curves... Still writing the heart drawing code... Now I'm working out how the particles behave: they burst outward at random angles with a small upward bias, then slow down, fall gently, and fade over a second or two. For the HUD I'm putting the score at top left, the level in the middle, and the lives as heart icons at top right, all in the band above the bricks. I'm deciding where the "Click or press a key to start" pill goes. Placing it over the brick area would hide the game, so I'm weighing a spot between the bricks and the paddle, but I'm leaning toward just centering it in the container with a semi-transparent background. Next I'm placing the start message lower in the canvas, around the 64% mark, so it doesn't cover the bricks in the first frames. Input handling will start the game from a click or any keypress. Still writing the CSS and scaling code... Now I'm wiring the paddle controls: in demo mode it follows the ball, while in play mode the mouse position sets its target once the pointer has moved over the canvas, and the arrow or A/D keys nudge it instead. Space will launch the ball early, and I'm preventing default on those keys so the page doesn't scroll. Now I'm checking each brick for overlap with the ball, flipping the ball's direction along whichever axis it overlaps more, and bursting the brick into hearts when it breaks. Now I'm finishing the paddle collision: when the ball is descending and reaches the top part of the paddle within its width, I'll reset it just above the paddle and set the rebound angle from where it struck, with the speed kept the same. I'll also add a small squish to the paddle that decays. For the demo AI, I'm adding a sine-based offset to its target so it isn't perfect. Now I'm settling the scoring and level flow: once every brick is gone, the level goes up, the bricks are rebuilt, and the ball waits about 1.2 seconds before serving, with a "LEVEL n" banner. When the lives run out, a "GAME OVER" banner shows and the game resets automatically after three seconds in both modes. I'm skipping the small heart burst on a lost life, and the banner will be drawn centered on the canvas around y=300 with fading alpha. Still writing the game code, keeping it compact at around 250 lines.