DeepSeek V4.1 Flash 1017
by DeepSeekReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 8 s
- Cost
- $0.0061
- Output tokens
- 7,638
- Spent thinking
- 0 tokens
- Speed
- 126.3 tok/s
- Console errors
- 0
- First token after
- 8.0 s
- First visible text after
- 8.0 s
- Prompt tokens
- 509
- Code size
- 22.1 KB
- Finish reason
- stop
- Served by
- OpenInference
- Model id
deepseek/deepseek-v4.1-flash-1017- Reasoning sent
{"effort":"none"}- Max output
- 64,000 tokens
- Generated
- Nov 10, 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 · 760 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a14;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Segoe UI', Arial, sans-serif;
overflow: hidden;
}
#game {
display: block;
border-radius: 8px;
box-shadow: 0 0 40px rgba(80, 120, 255, 0.25);
max-width: 100vw;
max-height: 100vh;
touch-action: none;
}
#overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(2px);
z-index: 10;
cursor: pointer;
transition: opacity 0.3s;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
#overlay .msg {
color: #e8eefc;
font-size: clamp(18px, 3vw, 30px);
font-weight: 700;
letter-spacing: 1px;
text-align: center;
padding: 20px 32px;
background: rgba(20, 30, 60, 0.7);
border: 1px solid rgba(120, 160, 255, 0.4);
border-radius: 12px;
box-shadow: 0 0 30px rgba(80, 120, 255, 0.3);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.03); }
}
</style>
</head>
<body>
<canvas id="game" width="1280" height="800"></canvas>
<div id="overlay"><div class="msg">Click or press a key to start</div></div>
<script>
(function(){
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const W = 1280, H = 800;
// Scale to fit viewport
function fit(){
const s = Math.min(window.innerWidth / W, window.innerHeight / H);
canvas.style.width = (W*s)+'px';
canvas.style.height = (H*s)+'px';
}
window.addEventListener('resize', fit);
fit();
// Game constants
const COLS = 12;
const ROWS = 7;
const BRICK_TOP = 90;
const BRICK_GAP = 4;
const BRICK_H = 28;
const BRICK_W = (W - 40) / COLS - BRICK_GAP;
const PADDLE_Y = H - 60;
const PADDLE_H = 18;
const PADDLE_W = 130;
const PADDLE_WIDE = 200;
const BALL_R = 9;
const PADDLE_SPEED = 700;
const START_SPEED = 380;
const state = {
mode: 'menu', // menu | playing | gameover | win
paddle: { x: W/2 - PADDLE_W/2, w: PADDLE_W, targetW: PADDLE_W, wideTimer: 0 },
balls: [],
bricks: [],
powerups: [],
particles: [],
score: 0,
lives: 3,
time: 0,
mouseX: W/2,
keys: {},
demo: false,
demoAI: { target: W/2, timer: 0 },
lastT: 0,
shake: 0,
};
function resetBricks(){
state.bricks = [];
for (let r = 0; r < ROWS; r++){
for (let c = 0; c < COLS; c++){
const x = 20 + c * (BRICK_W + BRICK_GAP);
const y = BRICK_TOP + r * (BRICK_H + BRICK_GAP);
// tougher bricks in some rows
const tough = (r >= 2 && r <= 4) && ((r + c) % 3 === 0) || (r === 1 && c % 4 === 0);
const power = tough ? (c === 0 || c === 5) : ((c === 3 || c === 8) && r === 2);
const hue = 200 + r * 22;
state.bricks.push({
x, y, w: BRICK_W, h: BRICK_H,
hp: tough ? 2 : 1,
maxHp: tough ? 2 : 1,
alive: true,
power: power,
hue: hue,
hitFlash: 0,
});
}
}
}
function newBall(x, y, vx, vy){
return { x, y, vx, vy, r: BALL_R, trail: [] };
}
function resetGame(){
state.paddle = { x: W/2 - PADDLE_W/2, w: PADDLE_W, targetW: PADDLE_W, wideTimer: 0 };
state.paddle.x = W/2 - state.paddle.w/2;
state.balls = [newBall(W/2, PADDLE_Y - BALL_R - 2, START_SPEED*0.5, -START_SPEED)];
state.powerups = [];
state.particles = [];
state.score = 0;
state.lives = 3;
state.time = 0;
state.shake = 0;
resetBricks();
}
function startGame(){
state.mode = 'playing';
state.demo = false;
resetGame();
overlay.classList.add('hidden');
}
function startDemo(){
state.mode = 'playing';
state.demo = true;
resetGame();
overlay.classList.remove('hidden');
}
function gameOver(){
state.mode = 'gameover';
}
function win(){
state.mode = 'win';
}
// Input
function handleKey(e){
if (e.type === 'keydown' && state.mode === 'menu'){
startGame();
}
if (state.mode === 'playing' && !state.demo){
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') e.preventDefault();
state.keys[e.key] = true;
}
if (state.mode === 'gameover' || state.mode === 'win'){
if (e.type === 'keydown' && (e.key === ' ' || e.key === 'Enter')) {
startGame();
}
}
}
window.addEventListener('keydown', handleKey);
window.addEventListener('keyup', e => { state.keys[e.key] = false; });
function updateMouse(e){
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width * W;
state.mouseX = x;
}
canvas.addEventListener('mousemove', updateMouse);
canvas.addEventListener('touchmove', e => {
e.preventDefault();
const t = e.touches[0];
const rect = canvas.getBoundingClientRect();
state.mouseX = (t.clientX - rect.left) / rect.width * W;
}, { passive: false });
canvas.addEventListener('mousedown', () => {
if (state.mode === 'menu') startGame();
else if (state.mode === 'gameover' || state.mode === 'win') startGame();
});
overlay.addEventListener('mousedown', e => {
e.preventDefault();
if (state.mode === 'menu') startGame();
else if (state.mode === 'gameover' || state.mode === 'win') startGame();
});
window.addEventListener('keydown', e => {
if (state.mode === 'menu') startGame();
});
// ---- Collision helpers ----
function circleRectCollide(cx, cy, r, rx, ry, rw, rh){
const nx = Math.max(rx, Math.min(cx, rx + rw));
const ny = Math.max(ry, Math.min(cy, ry + rh));
const dx = cx - nx, dy = cy - ny;
const d2 = dx*dx + dy*dy;
if (d2 > r*r) return null;
const d = Math.sqrt(d2) || 0.0001;
return { nx: dx/d, ny: dy/d, dist: d, nx0: nx, ny0: ny };
}
function spawnParticles(x, y, hue, count){
for (let i = 0; i < count; i++){
const a = Math.random() * Math.PI * 2;
const sp = 60 + Math.random() * 220;
state.particles.push({
x, y, vx: Math.cos(a)*sp, vy: Math.sin(a)*sp,
life: 0.5 + Math.random()*0.5, maxLife: 0.5 + Math.random()*0.5,
size: 2 + Math.random()*4, hue,
});
}
}
function addPowerup(x, y, type){
state.powerups.push({ x, y, w: 30, h: 22, vy: 130, type, t: 0 });
}
function updatePaddle(dt){
const p = state.paddle;
// target width
if (p.wideTimer > 0){
p.wideTimer -= dt;
p.targetW = PADDLE_WIDE;
} else {
p.targetW = PADDLE_W;
}
// smooth width
const center = p.x + p.w/2;
p.w += (p.targetW - p.w) * Math.min(1, dt*10);
p.x = center - p.w/2;
if (state.demo){
// AI follows predicted ball
const ai = state.demoAI;
ai.timer -= dt;
if (ai.timer <= 0){
ai.timer = 0.15;
// find ball with lowest y (closest to paddle)
let best = null;
for (const b of state.balls){
if (b.vy > 0 && (!best || b.y > best.y)) best = b;
}
if (best){
// predict x at paddle y
const ty = PADDLE_Y - BALL_R;
let dy = ty - best.y;
if (dy > 0){
let t = dy / best.vy;
let px = best.x + best.vx * t;
// bounce off walls
const span = W - 2*BALL_R;
px = ((px - BALL_R) % (2*span) + 2*span) % (2*span) + BALL_R;
if (px > W - BALL_R) px = W - BALL_R - (px - (W - BALL_R));
ai.target = px + (Math.random()-0.5)*40;
}
} else {
ai.target = W/2;
}
}
const target = ai.target;
const diff = target - (p.x + p.w/2);
if (Math.abs(diff) > 4){
p.x += Math.sign(diff) * Math.min(Math.abs(diff), PADDLE_SPEED * dt);
}
} else {
let move = 0;
if (state.keys['ArrowLeft']) move -= 1;
if (state.keys['ArrowRight']) move += 1;
if (move !== 0){
p.x += move * PADDLE_SPEED * dt;
} else {
// mouse follow
const targetCenter = state.mouseX;
const diff = targetCenter - (p.x + p.w/2);
if (Math.abs(diff) > 2){
p.x += Math.sign(diff) * Math.min(Math.abs(diff), PADDLE_SPEED * dt * 1.2);
}
}
}
// clamp
if (p.x < 0) p.x = 0;
if (p.x + p.w > W) p.x = W - p.w;
}
function updateBalls(dt){
const p = state.paddle;
const toRemove = [];
for (let i = 0; i < state.balls.length; i++){
const b = state.balls[i];
// substeps to prevent tunneling
const speed = Math.hypot(b.vx, b.vy);
const steps = Math.max(1, Math.ceil(speed * dt / (BALL_R * 0.8)));
const sdt = dt / steps;
for (let s = 0; s < steps; s++){
b.x += b.vx * sdt;
b.y += b.vy * sdt;
// walls
if (b.x - b.r < 0){ b.x = b.r; b.vx = Math.abs(b.vx); }
if (b.x + b.r > W){ b.x = W - b.r; b.vx = -Math.abs(b.vx); }
if (b.y - b.r < 0){ b.y = b.r; b.vy = Math.abs(b.vy); }
// paddle collision
const pRect = { x: p.x, y: PADDLE_Y, w: p.w, h: PADDLE_H };
if (b.vy > 0){
const hit = circleRectCollide(b.x, b.y, b.r, pRect.x, pRect.y, pRect.w, pRect.h);
if (hit){
// reposition
const overlap = b.r - hit.dist;
b.x += hit.nx * overlap;
b.y += hit.ny * overlap;
// relative x position
const rel = (b.x - (p.x + p.w/2)) / (p.w/2);
const clamped = Math.max(-1, Math.min(1, rel));
// angle: -60 to +60 deg
const maxAngle = Math.PI/3;
const angle = clamped * maxAngle;
const sp = Math.max(speed, START_SPEED);
// ensure upward
b.vx = Math.sin(angle) * sp;
b.vy = -Math.abs(Math.cos(angle)) * sp;
b.y = pRect.y - b.r - 0.5;
state.shake = Math.max(state.shake, 3);
}
}
// bricks
for (const br of state.bricks){
if (!br.alive) continue;
const hit = circleRectCollide(b.x, b.y, b.r, br.x, br.y, br.w, br.h);
if (hit){
// reposition
const overlap = b.r - hit.dist;
b.x += hit.nx * overlap;
b.y += hit.ny * overlap;
// reflect
if (Math.abs(hit.nx) > Math.abs(hit.ny)){
b.vx = -b.vx * 1.0;
} else {
b.vy = -b.vy;
}
br.hp--;
br.hitFlash = 1;
if (br.hp <= 0){
br.alive = false;
state.score += 10;
spawnParticles(br.x + br.w/2, br.y + br.h/2, br.hue, 14);
if (br.power) addPowerup(br.x + br.w/2, br.y + br.h/2, br.power === 'w' ? 'wide' : 'multi');
state.shake = Math.max(state.shake, 5);
} else {
state.score += 2;
spawnParticles(br.x + br.w/2, br.y + br.h/2, br.hue, 5);
state.shake = Math.max(state.shake, 2);
}
break;
}
}
// bottom
if (b.y - b.r > H){
toRemove.push(i);
break;
}
}
}
for (let i = toRemove.length - 1; i >= 0; i--){
state.balls.splice(toRemove[i], 1);
}
if (state.balls.length === 0 && state.mode === 'playing'){
state.lives--;
if (state.lives <= 0){
gameOver();
} else {
state.balls.push(newBall(p.x + p.w/2, PADDLE_Y - BALL_R - 2, START_SPEED*0.5, -START_SPEED));
}
}
// check win
if (state.mode === 'playing' && state.bricks.every(b => !b.alive)){
win();
}
}
function updatePowerups(dt){
for (let i = state.powerups.length - 1; i >= 0; i--){
const pu = state.powerups[i];
pu.y += pu.vy * dt;
pu.t += dt;
const p = state.paddle;
if (pu.y + pu.h > PADDLE_Y && pu.y < PADDLE_Y + PADDLE_H && pu.x + pu.w > p.x && pu.x < p.x + p.w){
if (pu.type === 'wide'){
p.wideTimer = 10;
} else {
// multi
const center = p.x + p.w/2;
const src = state.balls[0];
if (src){
for (let k = 0; k < 2; k++){
const a = (k === 0 ? -0.5 : 0.5) + (Math.random()-0.5)*0.2;
const sp = Math.hypot(src.vx, src.vy);
state.balls.push(newBall(center, PADDLE_Y - BALL_R - 2, Math.sin(a)*sp, -Math.cos(a)*sp));
}
}
}
spawnParticles(pu.x + pu.w/2, pu.y + pu.h/2, 60, 12);
state.powerups.splice(i, 1);
} else if (pu.y > H + 20){
state.powerups.splice(i, 1);
}
}
}
function updateParticles(dt){
for (let i = state.particles.length - 1; i >= 0; i--){
const p = state.particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 220 * dt;
p.life -= dt;
if (p.life <= 0) state.particles.splice(i, 1);
}
}
// ---- Draw ----
function drawBackground(){
// subtle gradient
const g = ctx.createRadialGradient(W/2, H/2, 50, W/2, H/2, W*0.8);
g.addColorStop(0, '#141a2e');
g.addColorStop(1, '#070a14');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// grid lines
ctx.strokeStyle = 'rgba(80,120,255,0.05)';
ctx.lineWidth = 1;
for (let x = 0; x < W; x += 40){
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y < H; y += 40){
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
}
function drawBricks(){
for (const b of state.bricks){
if (!b.alive) continue;
const r = 6;
// base
ctx.beginPath();
ctx.roundRect(b.x, b.y, b.w, b.h, r);
const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
if (b.hp > 1){
grad.addColorStop(0, `hsl(${b.hue}, 70%, 55%)`);
grad.addColorStop(1, `hsl(${b.hue}, 70%, 38%)`);
} else {
grad.addColorStop(0, `hsl(${b.hue}, 60%, 45%)`);
grad.addColorStop(1, `hsl(${b.hue}, 60%, 30%)`);
}
ctx.fillStyle = grad;
ctx.fill();
// highlight
ctx.strokeStyle = `hsla(${b.hue}, 90%, 80%, 0.5)`;
ctx.lineWidth = 1.5;
ctx.stroke();
// crack lines if damaged
if (b.hp < b.maxHp){
ctx.strokeStyle = 'rgba(10,10,20,0.75)';
ctx.lineWidth = 2;
ctx.beginPath();
const cx = b.x + b.w/2, cy = b.y + b.h/2;
ctx.moveTo(cx - 8, cy - 4);
ctx.lineTo(cx - 2, cy + 2);
ctx.lineTo(cx + 3, cy - 6);
ctx.lineTo(cx + 9, cy + 4);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx - 2, cy + 2);
ctx.lineTo(cx - 6, cy + 9);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + 3, cy - 6);
ctx.lineTo(cx + 2, cy - 11);
ctx.stroke();
}
// power icon
if (b.power){
const cx = b.x + b.w/2, cy = b.y + b.h/2;
ctx.save();
ctx.translate(cx, cy);
if (b.power === 'wide'){
// arrows
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath();
ctx.moveTo(-10, 0); ctx.lineTo(-4, -4); ctx.lineTo(-4, 4); ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(10, 0); ctx.lineTo(4, -4); ctx.lineTo(4, 4); ctx.closePath(); ctx.fill();
ctx.fillRect(-4, -2, 8, 4);
} else {
// three dots / ball
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath(); ctx.arc(-6, -2, 3, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(6, -2, 3, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(0, 4, 3, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
// flash
if (b.hitFlash > 0){
ctx.fillStyle = `rgba(255,255,255,${b.hitFlash*0.5})`;
ctx.beginPath();
ctx.roundRect(b.x, b.y, b.w, b.h, r);
ctx.fill();
}
}
}
function drawPaddle(){
const p = state.paddle;
const y = PADDLE_Y;
const grad = ctx.createLinearGradient(p.x, y, p.x, y + PADDLE_H);
grad.addColorStop(0, '#5a8cff');
grad.addColorStop(0.5, '#2a5fff');
grad.addColorStop(1, '#1a3aa0');
ctx.beginPath();
ctx.roundRect(p.x, y, p.w, PADDLE_H, 9);
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = 'rgba(180,210,255,0.8)';
ctx.lineWidth = 1.5;
ctx.stroke();
// glow
ctx.shadowColor = 'rgba(80,140,255,0.8)';
ctx.shadowBlur = 18;
ctx.beginPath();
ctx.roundRect(p.x, y, p.w, PADDLE_H, 9);
ctx.stroke();
ctx.shadowBlur = 0;
}
function drawBalls(){
for (const b of state.balls){
// trail
ctx.save();
for (let i = 0; i < b.trail.length; i++){
const t = b.trail[i];
const a = (i / b.trail.length) * 0.4;
ctx.fillStyle = `rgba(255,255,255,${a})`;
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * (0.4 + 0.5 * i/b.trail.length), 0, Math.PI*2);
ctx.fill();
}
ctx.restore();
// glow
ctx.shadowColor = 'rgba(200,230,255,0.9)';
ctx.shadowBlur = 16;
ctx.fillStyle = '#f0f8ff';
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI*2);
ctx.fill();
ctx.shadowBlur = 0;
// inner highlight
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath();
ctx.arc(b.x - b.r*0.3, b.y - b.r*0.3, b.r*0.4, 0, Math.PI*2);
ctx.fill();
}
}
function drawPowerups(){
for (const pu of state.powerups){
const pulse = 1 + Math.sin(pu.t*8)*0.08;
ctx.save();
ctx.translate(pu.x + pu.w/2, pu.y + pu.h/2);
ctx.scale(pulse, pulse);
ctx.rotate(Math.sin(pu.t*3)*0.1);
// glow
ctx.shadowColor = pu.type === 'wide' ? 'rgba(80,255,160,0.9)' : 'rgba(255,120,80,0.9)';
ctx.shadowBlur = 14;
// capsule
const g = ctx.createLinearGradient(0, -pu.h/2, 0, pu.h/2);
if (pu.type === 'wide'){
g.addColorStop(0, '#7dffb0');
g.addColorStop(1, '#1a9e5a');
} else {
g.addColorStop(0, '#ff9a7d');
g.addColorStop(1, '#c02a1a');
}
ctx.fillStyle = g;
ctx.beginPath();
ctx.roundRect(-pu.w/2, -pu.h/2, pu.w, pu.h, pu.h/2);
ctx.fill();
ctx.shadowBlur = 0;
// icon
ctx.fillStyle = 'rgba(255,255,255,0.95)';
if (pu.type === 'wide'){
ctx.beginPath();
ctx.moveTo(-10, 0); ctx.lineTo(-4, -4); ctx.lineTo(-4, 4); ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(10, 0); ctx.lineTo(4, -4); ctx.lineTo(4, 4); ctx.closePath(); ctx.fill();
ctx.fillRect(-4, -2, 8, 4);
} else {
ctx.beginPath(); ctx.arc(-6, -2, 2.5, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(6, -2, 2.5, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(0, 4, 2.5, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
}
function drawParticles(){
for (const p of state.particles){
const a = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = `hsla(${p.hue}, 90%, 65%, ${a})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * a, 0, Math.PI*2);
ctx.fill();
}
}
function drawHUD(){
// score
ctx.save();
ctx.font = 'bold 22px Arial';
ctx.textAlign = 'left';
ctx.fillStyle = '#e8eefc';
ctx.fillText('SCORE ' + state.score, 24, 40);
// lives
ctx.textAlign = 'right';
for (let i = 0; i < state.lives; i++){
const cx = W - 32 - i*30;
const cy = 32;
ctx.fillStyle = '#5a8cff';
ctx.beginPath();
ctx.roundRect(cx - 12, cy - 7, 24, 14, 7);
ctx.fill();
ctx.strokeStyle = 'rgba(180,210,255,0.7)';
ctx.lineWidth = 1.5;
ctx.stroke();
}
ctx.fillStyle = '#e8eefc';
ctx.font = 'bold 18px Arial';
ctx.fillText('LIVES', W - 32 - state.lives*30 - 20, 40);
// wide timer
if (state.paddle.wideTimer > 0){
ctx.textAlign = 'left';
ctx.fillStyle = '#7dffb0';
ctx.font = 'bold 16px Arial';
ctx.fillText('WIDE PADDLE ' + state.paddle.wideTimer.toFixed(1) + 's', 24, 64);
}
ctx.restore();
}
function drawEndScreen(title, subtitle, color){
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.fillStyle = color;
ctx.font = 'bold 64px Arial';
ctx.shadowColor = color;
ctx.shadowBlur = 30;
ctx.fillText(title, W/2, H/2 - 40);
ctx.shadowBlur = 0;
ctx.fillStyle = '#e8eefc';
ctx.font = 'bold 28px Arial';
ctx.fillText(subtitle, W/2, H/2 + 30);
ctx.font = '20px Arial';
ctx.fillStyle = 'rgba(200,220,255,0.8)';
ctx.fillText('Click or press Space to restart', W/2, H/2 + 90);
ctx.restore();
}
// ---- Main loop ----
function update(dt){
if (state.mode === 'playing'){
updatePaddle(dt);
updateBalls(dt);
updatePowerups(dt);
updateParticles(dt);
// ball trail
for (const b of state.balls){
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 12) b.trail.shift();
}
// hit flash decay
for (const br of state.bricks){
if (br.hitFlash > 0) br.hitFlash = Math.max(0, br.hitFlash - dt*4);
}
// shake decay
if (state.shake > 0) state.shake = Math.max(0, state.shake - dt*20);
} else {
updateParticles(dt);
if (state.shake > 0) state.shake = Math.max(0, state.shake - dt*20);
}
}
function render(){
ctx.save();
if (state.shake > 0.5){
const s = state.shake;
ctx.translate((Math.random()-0.5)*s, (Math.random()-0.5)*s);
}
drawBackground();
drawBricks();
drawPowerups();
drawPaddle();
drawBalls();
drawParticles();
ctx.restore();
drawHUD();
if (state.mode === 'gameover'){
drawEndScreen('GAME OVER', 'Score: ' + state.score, '#ff5a6a');
} else if (state.mode === 'win'){
drawEndScreen('VICTORY!', 'Score: ' + state.score, '#7dffb0');
}
}
function loop(t){
const dt = Math.min(0.033, (t - state.lastT) / 1000 || 0);
state.lastT = t;
update(dt);
render();
requestAnimationFrame(loop);
}
// Start in demo mode
startDemo();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>