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
- 39.8 s
- Cost
- $0.0064
- Output tokens
- 12,779
- Spent thinking
- 0 tokens
- Speed
- 325.3 tok/s
- Console errors
- 0
- First token after
- 531 ms
- First visible text after
- 531 ms
- Prompt tokens
- 534
- Code size
- 35.7 KB
- Finish reason
- stop
- Served by
- Wafer
- Model id
deepseek/deepseek-v4.1-flash-1017- Reasoning sent
{"effort":"max"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 1224 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; }
html, body {
height: 100%;
background: #0a0e1a;
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
}
#wrap {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(circle at 50% 0%, #16203a 0%, #0a0e1a 55%, #05070d 100%);
}
canvas {
display: block;
border-radius: 14px;
box-shadow:
0 0 0 1px rgba(120,180,255,.18),
0 0 60px rgba(60,120,255,.22),
0 24px 70px rgba(0,0,0,.75);
touch-action: none;
max-width: 100%;
max-height: 100%;
}
/* click-to-start overlay */
#startOverlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 18px;
pointer-events: none;
opacity: 1;
transition: opacity .35s ease;
z-index: 5;
}
#startOverlay.hide { opacity: 0; }
.start-card {
padding: 26px 44px;
border-radius: 18px;
background: rgba(8,13,26,.78);
border: 1px solid rgba(110,170,255,.35);
box-shadow: 0 0 50px rgba(70,130,255,.35), inset 0 0 40px rgba(70,130,255,.08);
backdrop-filter: blur(6px);
text-align: center;
}
.start-card h1 {
font-size: clamp(22px, 4.2vw, 40px);
letter-spacing: .16em;
text-transform: uppercase;
font-weight: 800;
background: linear-gradient(180deg, #eaf3ff, #7fb6ff 55%, #3f7de0);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 2px 12px rgba(90,150,255,.55));
}
.start-card p {
margin-top: 12px;
font-size: clamp(12px, 1.6vw, 15px);
color: #9fb6d6;
letter-spacing: .06em;
}
.pulse { animation: pulse 1.6s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: .55 } 50% { opacity: 1 } }
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>
<div id="startOverlay">
<div class="start-card">
<h1>Breakout</h1>
<p class="pulse">Click or press a key to start</p>
<p style="margin-top:6px;font-size:12px;opacity:.55">Mouse or ← → to move · Catch power-ups</p>
</div>
</div>
</div>
<script>
(function () {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const startOverlay = document.getElementById('startOverlay');
// ---------------------------------------------------------------- constants
const W = 960;
const H = 640;
const HUD_H = 54;
const COLS = 11;
const ROWS = 7;
const BRICK_TOP = HUD_H + 46;
const BRICK_GAP = 6;
const BRICK_H = 24;
const FIELD_LEFT = 26;
const FIELD_RIGHT = W - 26;
const BRICK_W = (FIELD_RIGHT - FIELD_LEFT - BRICK_GAP * (COLS - 1)) / COLS;
const PADDLE_Y = H - 46;
const PADDLE_H = 14;
const PADDLE_W_BASE = 112;
const PADDLE_W_WIDE = 176;
const BALL_R = 8;
const BALL_SPEED = 430;
const MAX_BOUNCE = 62 * Math.PI / 180; // sharp angle at paddle edges
const COLORS = [
{ base: '#ff4d6d', light: '#ff8fa3', dark: '#b31f3c' },
{ base: '#ff9f43', light: '#ffc98a', dark: '#b96a13' },
{ base: '#ffd93d', light: '#fff08a', dark: '#b89a10' },
{ base: '#4ade80', light: '#9bf0ba', dark: '#1e8f4a' },
{ base: '#38bdf8', light: '#93dcff', dark: '#1072a8' },
{ base: '#a78bfa', light: '#cdbcff', dark: '#6544c9' },
{ base: '#f472b6', light: '#ffb0d4', dark: '#b02f76' }
];
// ---------------------------------------------------------------- state
let state = 'demo'; // demo | playing | gameover | victory
let started = false;
let demoAI = true;
let score = 0;
let lives = 3;
let level = 1;
let paddle = {
x: W / 2,
w: PADDLE_W_BASE,
targetW: PADDLE_W_BASE,
vx: 0
};
let keyLeft = false, keyRight = false;
let mouseX = null;
let balls = [];
let bricks = [];
let particles = [];
let powerups = [];
let floaters = [];
let wideTimer = 0; // seconds remaining on wide paddle
let shake = 0;
let flash = 0;
let time = 0;
let launchCooldown = 0;
let banner = null; // {text, t}
// ---------------------------------------------------------------- helpers
const rand = (a, b) => a + Math.random() * (b - a);
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
function roundRect(c, x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
c.beginPath();
c.moveTo(x + r, y);
c.arcTo(x + w, y, x + w, y + h, r);
c.arcTo(x + w, y + h, x, y + h, r);
c.arcTo(x, y + h, x, y, r);
c.arcTo(x, y, x + w, y, r);
c.closePath();
}
// ---------------------------------------------------------------- level
// types: 1 = normal, 2 = tough (2 hits), 3 = tough + powerup, 4 = normal + powerup
const LEVEL_MAP = [
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[1, 1, 3, 1, 1, 1, 1, 1, 4, 1, 1],
[1, 2, 1, 1, 3, 2, 2, 4, 1, 1, 2],
[2, 1, 1, 1, 2, 1, 1, 2, 1, 3, 1],
[1, 1, 4, 1, 1, 2, 1, 1, 1, 1, 1],
[1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1],
[1, 1, 1, 3, 1, 1, 4, 1, 1, 1, 1]
];
function buildLevel() {
bricks = [];
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const t = LEVEL_MAP[r][c];
if (!t) continue;
const tough = (t === 2 || t === 3);
bricks.push({
x: FIELD_LEFT + c * (BRICK_W + BRICK_GAP),
y: BRICK_TOP + r * (BRICK_H + BRICK_GAP),
w: BRICK_W,
h: BRICK_H,
color: COLORS[r % COLORS.length],
hp: tough ? 2 : 1,
maxHp: tough ? 2 : 1,
power: (t === 3 || t === 4) ? (Math.random() < 0.5 ? 'wide' : 'multi') : null,
alive: true,
flashT: 0,
shakeT: 0,
hitT: 0
});
}
}
}
// ---------------------------------------------------------------- balls
function makeBall(x, y, angle, speed) {
return {
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
r: BALL_R,
trail: [],
stuck: false
};
}
function resetBallOnPaddle() {
balls = [{
x: paddle.x,
y: PADDLE_Y - BALL_R - 2,
vx: 0,
vy: 0,
r: BALL_R,
trail: [],
stuck: true
}];
launchCooldown = 0.15;
}
function launchBalls() {
let launched = false;
for (const b of balls) {
if (b.stuck) {
const a = -Math.PI / 2 + rand(-0.32, 0.32);
b.vx = Math.cos(a) * BALL_SPEED;
b.vy = Math.sin(a) * BALL_SPEED;
b.stuck = false;
launched = true;
}
}
if (launched) launchCooldown = 0.12;
}
// ---------------------------------------------------------------- game flow
function resetGame() {
score = 0;
lives = 3;
level = 1;
wideTimer = 0;
paddle.w = PADDLE_W_BASE;
paddle.targetW = PADDLE_W_BASE;
paddle.x = W / 2;
particles = [];
powerups = [];
floaters = [];
buildLevel();
resetBallOnPaddle();
shake = 0;
flash = 0;
banner = null;
state = 'playing';
}
function startGame() {
if (started) return;
started = true;
demoAI = false;
state = 'playing';
startOverlay.classList.add('hide');
resetGame();
// auto-launch shortly so the very first moment is lively
setTimeout(() => { if (state === 'playing') launchBalls(); }, 550);
}
// ---------------------------------------------------------------- input
function onMove(clientX) {
const rect = canvas.getBoundingClientRect();
const sx = (clientX - rect.left) / rect.width * W;
mouseX = sx;
if (!demoAI) paddle.x = clamp(sx, paddle.w / 2, W - paddle.w / 2);
}
canvas.addEventListener('mousemove', e => onMove(e.clientX));
canvas.addEventListener('touchmove', e => {
if (e.touches.length) { onMove(e.touches[0].clientX); e.preventDefault(); }
}, { passive: false });
canvas.addEventListener('pointerdown', e => {
if (!started) { startGame(); return; }
launchBalls();
});
window.addEventListener('mousedown', e => {
if (!started) { startGame(); return; }
});
window.addEventListener('keydown', e => {
if (!started) { startGame(); }
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') { keyLeft = true; e.preventDefault(); }
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { keyRight = true; e.preventDefault(); }
if (e.key === ' ' || e.key === 'Spacebar' || e.key === 'Enter') {
e.preventDefault();
if (state === 'gameover' || state === 'victory') { resetGame(); }
else launchBalls();
}
if ((e.key === 'r' || e.key === 'R') && (state === 'gameover' || state === 'victory')) {
resetGame();
}
});
window.addEventListener('keyup', e => {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keyLeft = false;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keyRight = false;
});
// ---------------------------------------------------------------- particles
function spawnParticles(x, y, color, n) {
for (let i = 0; i < n; i++) {
const a = rand(0, Math.PI * 2);
const sp = rand(50, 330);
particles.push({
x, y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 40,
life: rand(0.35, 0.85),
maxLife: 0.85,
size: rand(1.6, 4.2),
color,
rot: rand(0, 6.28),
rotV: rand(-10, 10)
});
}
}
function spawnFloater(x, y, text, color) {
floaters.push({ x, y, text, color, t: 0, life: 0.9 });
}
// ---------------------------------------------------------------- collision
// circle vs AABB, resolve minimal penetration and reflect
function circleRectResolve(ball, rx, ry, rw, rh) {
const cx = clamp(ball.x, rx, rx + rw);
const cy = clamp(ball.y, ry, ry + rh);
const dx = ball.x - cx;
const dy = ball.y - cy;
const d2 = dx * dx + dy * dy;
if (d2 > ball.r * ball.r) return null;
let nx, ny, pen;
if (d2 > 0.0001) {
const d = Math.sqrt(d2);
nx = dx / d; ny = dy / d;
pen = ball.r - d;
} else {
// ball centre inside rect: push out along smallest axis
const left = ball.x - rx, right = rx + rw - ball.x;
const top = ball.y - ry, bottom = ry + rh - ball.y;
const m = Math.min(left, right, top, bottom);
if (m === left) { nx = -1; ny = 0; pen = left + ball.r; }
else if (m === right) { nx = 1; ny = 0; pen = right + ball.r; }
else if (m === top) { nx = 0; ny = -1; pen = top + ball.r; }
else { nx = 0; ny = 1; pen = bottom + ball.r; }
}
return { nx, ny, pen };
}
function reflect(ball, nx, ny, pen) {
ball.x += nx * (pen + 0.01);
ball.y += ny * (pen + 0.01);
const dot = ball.vx * nx + ball.vy * ny;
if (dot < 0) {
ball.vx -= 2 * dot * nx;
ball.vy -= 2 * dot * ny;
}
}
// substepping so the ball can never tunnel through bricks/paddle
function moveBall(ball, dt) {
const speed = Math.hypot(ball.vx, ball.vy);
const dist = speed * dt;
const steps = Math.max(1, Math.ceil(dist / (ball.r * 0.6)));
const sdt = dt / steps;
for (let s = 0; s < steps; s++) {
ball.x += ball.vx * sdt;
ball.y += ball.vy * sdt;
// walls
if (ball.x - ball.r < 0) { ball.x = ball.r; ball.vx = Math.abs(ball.vx); }
else if (ball.x + ball.r > W) { ball.x = W - ball.r; ball.vx = -Math.abs(ball.vx); }
if (ball.y - ball.r < HUD_H) { ball.y = HUD_H + ball.r; ball.vy = Math.abs(ball.vy); }
// paddle
const px = paddle.x - paddle.w / 2;
const py = PADDLE_Y;
const hit = circleRectResolve(ball, px, py, paddle.w, PADDLE_H);
if (hit && ball.vy > 0) {
ball.y = py - ball.r - 0.5;
// reflection angle depends on hit position
const rel = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
const ang = -Math.PI / 2 + rel * MAX_BOUNCE;
const sp = clamp(Math.hypot(ball.vx, ball.vy) * 1.008, BALL_SPEED, BALL_SPEED * 1.55);
ball.vx = Math.cos(ang) * sp;
ball.vy = Math.sin(ang) * sp;
// slight influence from paddle motion
ball.vx += paddle.vx * 0.06;
spawnParticles(ball.x, py, '#7fd0ff', 7);
}
// bricks
for (const br of bricks) {
if (!br.alive) continue;
const h = circleRectResolve(ball, br.x, br.y, br.w, br.h);
if (h) {
reflect(ball, h.nx, h.ny, h.pen);
hitBrick(br, ball);
break;
}
}
// safety: keep inside field
if (ball.y > H + 60) return false;
}
return true;
}
function hitBrick(br, ball) {
br.hp--;
br.flashT = 0.16;
br.shakeT = 0.18;
if (br.hp <= 0) {
br.alive = false;
score += br.maxHp === 2 ? 150 : 100;
spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color.base, 22);
spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color.light, 10);
if (br.power) {
powerups.push({
x: br.x + br.w / 2,
y: br.y + br.h / 2,
vy: 120,
kind: br.power,
t: 0,
r: 15
});
}
shake = Math.min(shake + 2.5, 9);
checkVictory();
} else {
spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color.light, 8);
score += 25;
}
}
function checkVictory() {
if (bricks.every(b => !b.alive) && state === 'playing') {
state = 'victory';
flash = 0.6;
mouseX = mouseX;
}
}
// ---------------------------------------------------------------- power-ups
function collectPowerup(p) {
if (p.kind === 'wide') {
wideTimer = 10;
paddle.targetW = PADDLE_W_WIDE;
spawnFloater(paddle.x, PADDLE_Y - 26, 'WIDE PADDLE', '#7dffb0');
banner = { text: 'WIDE PADDLE', t: 1.4, color: '#7dffb0' };
} else {
spawnFloater(paddle.x, PADDLE_Y - 26, 'MULTI-BALL', '#8fd0ff');
banner = { text: 'MULTI-BALL', t: 1.4, color: '#8fd0ff' };
const src = balls.find(b => !b.stuck) || balls[0];
const bx = src ? src.x : paddle.x;
const by = src ? src.y : PADDLE_Y - 30;
const baseSp = BALL_SPEED;
for (let i = 0; i < 2; i++) {
const a = -Math.PI / 2 + (i === 0 ? -0.72 : 0.72) + rand(-0.12, 0.12);
balls.push(makeBall(bx, by, a, baseSp));
}
}
score += 75;
flash = Math.max(flash, 0.18);
}
// ---------------------------------------------------------------- update
function update(dt) {
time += dt;
if (shake > 0) shake = Math.max(0, shake - dt * 26);
if (flash > 0) flash = Math.max(0, flash - dt * 2.2);
if (banner) { banner.t -= dt; if (banner.t <= 0) banner = null; }
for (const br of bricks) {
if (br.flashT > 0) br.flashT -= dt;
if (br.shakeT > 0) br.shakeT -= dt;
}
// paddle target width
if (wideTimer > 0) {
wideTimer -= dt;
if (wideTimer <= 0) {
wideTimer = 0;
paddle.targetW = PADDLE_W_BASE;
}
}
paddle.w += (paddle.targetW - paddle.w) * Math.min(1, dt * 10);
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
// paddle movement
const prevX = paddle.x;
const keySpeed = 620;
if (state === 'playing' || state === 'demo') {
if (keyLeft && !keyRight) paddle.x -= keySpeed * dt;
else if (keyRight && !keyLeft) paddle.x += keySpeed * dt;
}
// demo AI
if (demoAI && started === false) {
const active = balls.filter(b => !b.stuck);
let target = W / 2;
if (active.length) {
// aim at the lowest ball, predicting near paddle line
let best = active[0];
for (const b of active) if (b.y > best.y) best = b;
const lead = best.x + best.vx * 0.16;
target = lead;
// avoid edges
target = clamp(target, paddle.w / 2 + 12, W - paddle.w / 2 - 12);
} else {
target = paddle.x + Math.sin(time * 0.6) * 20;
}
const diff = target - paddle.x;
paddle.x += clamp(diff, -640 * dt, 640 * dt);
}
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
paddle.vx = (paddle.x - prevX) / Math.max(dt, 0.0001);
// balls
if (launchCooldown > 0) launchCooldown -= dt;
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
if (b.stuck) {
b.x = paddle.x;
b.y = PADDLE_Y - b.r - 2;
if (state === 'demo' && launchCooldown <= 0) launchBalls();
continue;
}
const alive = moveBall(b, dt);
// trail
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 9) b.trail.shift();
if (!alive) {
spawnParticles(b.x, H - 8, '#5b7fb5', 14);
balls.splice(i, 1);
}
}
if (balls.length === 0 && state === 'playing') {
lives--;
if (lives <= 0) {
state = 'gameover';
shake = 8;
} else {
wideTimer = 0;
paddle.targetW = PADDLE_W_BASE;
resetBallOnPaddle();
banner = { text: 'BALL LOST', t: 1.1, color: '#ff8fa3' };
}
}
// powerups
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.t += dt;
p.y += p.vy * dt;
p.vy = Math.min(p.vy + 40 * dt, 240);
if (p.y - p.r > H) { powerups.splice(i, 1); continue; }
// catch with paddle
if (p.y + p.r > PADDLE_Y && p.y - p.r < PADDLE_Y + PADDLE_H &&
p.x > paddle.x - paddle.w / 2 - p.r && p.x < paddle.x + paddle.w / 2 + p.r) {
collectPowerup(p);
spawnParticles(p.x, p.y, p.kind === 'wide' ? '#7dffb0' : '#8fd0ff', 18);
powerups.splice(i, 1);
continue;
}
}
// particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
p.vy += 620 * dt;
p.vx *= (1 - 1.1 * dt);
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.rotV * dt;
}
// floaters
for (let i = floaters.length - 1; i >= 0; i--) {
const f = floaters[i];
f.t += dt;
f.y -= 42 * dt;
if (f.t > f.life) floaters.splice(i, 1);
}
}
// ---------------------------------------------------------------- drawing
function drawBackground() {
// deep gradient
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#111a2e');
g.addColorStop(0.55, '#0c1221');
g.addColorStop(1, '#070a13');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// top glow
const rg = ctx.createRadialGradient(W / 2, HUD_H + 40, 10, W / 2, HUD_H + 40, 520);
rg.addColorStop(0, 'rgba(70,130,255,.10)');
rg.addColorStop(1, 'rgba(70,130,255,0)');
ctx.fillStyle = rg;
ctx.fillRect(0, 0, W, H);
// subtle grid
ctx.strokeStyle = 'rgba(90,140,220,.055)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x <= W; x += 48) { ctx.moveTo(x + 0.5, HUD_H); ctx.lineTo(x + 0.5, H); }
for (let y = HUD_H; y <= H; y += 48) { ctx.moveTo(0, y + 0.5); ctx.lineTo(W, y + 0.5); }
ctx.stroke();
// side rails
ctx.fillStyle = 'rgba(120,180,255,.07)';
ctx.fillRect(0, HUD_H, 6, H - HUD_H);
ctx.fillRect(W - 6, HUD_H, 6, H - HUD_H);
}
function drawHUD() {
ctx.save();
// bar
const g = ctx.createLinearGradient(0, 0, 0, HUD_H);
g.addColorStop(0, 'rgba(20,32,58,.95)');
g.addColorStop(1, 'rgba(12,19,34,.95)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, HUD_H);
ctx.fillStyle = 'rgba(110,170,255,.22)';
ctx.fillRect(0, HUD_H - 1, W, 1);
// score
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
ctx.font = '600 13px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = 'rgba(150,180,220,.7)';
ctx.fillText('SCORE', 26, HUD_H / 2 - 11);
ctx.font = '800 25px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#eaf3ff';
ctx.fillText(String(score).padStart(5, '0'), 26, HUD_H / 2 + 9);
// lives
ctx.textAlign = 'right';
ctx.font = '600 13px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = 'rgba(150,180,220,.7)';
ctx.fillText('LIVES', W - 26, HUD_H / 2 - 11);
for (let i = 0; i < 3; i++) {
const bx = W - 26 - (2 - i) * 26;
const by = HUD_H / 2 + 10;
ctx.beginPath();
ctx.arc(bx, by, 6.5, 0, Math.PI * 2);
if (i < lives) {
const bg = ctx.createRadialGradient(bx - 2, by - 2, 1, bx, by, 7);
bg.addColorStop(0, '#cfe6ff');
bg.addColorStop(1, '#4a9bff');
ctx.fillStyle = bg;
ctx.fill();
ctx.shadowColor = '#4a9bff';
ctx.shadowBlur = 10;
ctx.fill();
ctx.shadowBlur = 0;
} else {
ctx.strokeStyle = 'rgba(120,160,210,.28)';
ctx.lineWidth = 1.4;
ctx.stroke();
}
}
// level pill
ctx.textAlign = 'center';
ctx.font = '700 12px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = 'rgba(150,180,220,.55)';
ctx.fillText('LEVEL ' + level, W / 2, HUD_H / 2);
// powerup timer bar (center, under level text)
if (wideTimer > 0) {
const bw = 132, bh = 5;
const bx = W / 2 - bw / 2, by = HUD_H / 2 + 12;
ctx.fillStyle = 'rgba(125,255,176,.16)';
roundRect(ctx, bx, by, bw, bh, 3);
ctx.fill();
ctx.fillStyle = '#7dffb0';
roundRect(ctx, bx, by, bw * (wideTimer / 10), bh, 3);
ctx.fill();
ctx.shadowColor = '#7dffb0';
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowBlur = 0;
}
ctx.restore();
}
function drawBrick(br) {
let ox = 0, oy = 0;
if (br.shakeT > 0) {
const k = br.shakeT / 0.18;
ox = rand(-2.6, 2.6) * k;
oy = rand(-1.6, 1.6) * k;
}
const x = br.x + ox, y = br.y + oy, w = br.w, h = br.h;
const col = br.color;
const cracked = br.hp < br.maxHp;
ctx.save();
// body
const g = ctx.createLinearGradient(x, y, x, y + h);
if (cracked) {
g.addColorStop(0, 'rgba(70,88,120,.95)');
g.addColorStop(1, 'rgba(38,50,74,.95)');
} else {
g.addColorStop(0, col.light);
g.addColorStop(0.42, col.base);
g.addColorStop(1, col.dark);
}
ctx.fillStyle = g;
roundRect(ctx, x, y, w, h, 5);
ctx.fill();
// top highlight
if (!cracked) {
ctx.fillStyle = 'rgba(255,255,255,.28)';
roundRect(ctx, x + 2, y + 2, w - 4, h * 0.32, 3);
ctx.fill();
}
// border
ctx.strokeStyle = cracked ? 'rgba(150,180,220,.35)' : 'rgba(0,0,0,.28)';
ctx.lineWidth = 1;
roundRect(ctx, x + 0.5, y + 0.5, w - 1, h - 1, 5);
ctx.stroke();
// cracks
if (cracked) {
ctx.strokeStyle = 'rgba(200,220,255,.75)';
ctx.lineWidth = 1.3;
ctx.beginPath();
const cx = x + w * 0.32, cy = y + h * 0.18;
ctx.moveTo(cx, cy);
ctx.lineTo(cx + 8, cy + 6);
ctx.lineTo(cx + 3, cy + 13);
ctx.lineTo(cx + 12, cy + h - 3);
ctx.stroke();
ctx.beginPath();
const cx2 = x + w * 0.68, cy2 = y + h * 0.92;
ctx.moveTo(cx2, cy2);
ctx.lineTo(cx2 - 7, cy2 - 7);
ctx.lineTo(cx2 - 1, cy2 - 14);
ctx.stroke();
ctx.beginPath();
const cx3 = x + w * 0.52, cy3 = y + h * 0.1;
ctx.moveTo(cx3, cy3);
ctx.lineTo(cx3 + 5, cy3 + 9);
ctx.stroke();
}
// power-up icon badge
if (br.power) {
const cx = x + w / 2, cy = y + h / 2;
const rr = 9;
const isWide = br.power === 'wide';
const ic = isWide ? '#7dffb0' : '#8fd0ff';
ctx.globalAlpha = cracked ? 0.85 : 1;
ctx.beginPath();
ctx.arc(cx, cy, rr, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(8,14,26,.72)';
ctx.fill();
ctx.strokeStyle = ic;
ctx.lineWidth = 1.4;
ctx.stroke();
ctx.shadowColor = ic;
ctx.shadowBlur = 10;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = ic;
ctx.strokeStyle = ic;
if (isWide) {
// two arrows pointing outward
ctx.lineWidth = 1.8;
roundRect(ctx, cx - 4.5, cy - 1.6, 9, 3.2, 1.4);
ctx.fill();
ctx.beginPath();
ctx.moveTo(cx - 4.5, cy - 4.6); ctx.lineTo(cx - 7.4, cy); ctx.lineTo(cx - 4.5, cy + 4.6);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(cx + 4.5, cy - 4.6); ctx.lineTo(cx + 7.4, cy); ctx.lineTo(cx + 4.5, cy + 4.6);
ctx.closePath(); ctx.fill();
} else {
// three dots = multi ball
for (let i = 0; i < 3; i++) {
const a = -Math.PI / 2 + (i - 1) * 0.9;
ctx.beginPath();
ctx.arc(cx + Math.cos(a) * 4.2, cy + Math.sin(a) * 4.2 + 1.4, 2.1, 0, Math.PI * 2);
ctx.fill();
}
ctx.beginPath();
ctx.arc(cx, cy - 4.2, 2.4, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// hit flash
if (br.flashT > 0) {
ctx.globalAlpha = clamp(br.flashT / 0.16, 0, 1) * 0.85;
ctx.fillStyle = '#ffffff';
roundRect(ctx, x, y, w, h, 5);
ctx.fill();
ctx.globalAlpha = 1;
}
ctx.restore();
}
function drawPaddle() {
const x = paddle.x - paddle.w / 2;
const y = PADDLE_Y;
const w = paddle.w, h = PADDLE_H;
ctx.save();
// glow
ctx.shadowColor = wideTimer > 0 ? '#7dffb0' : '#4a9bff';
ctx.shadowBlur = 22;
const g = ctx.createLinearGradient(x, y, x, y + h);
if (wideTimer > 0) {
g.addColorStop(0, '#d6ffe8');
g.addColorStop(0.5, '#7dffb0');
g.addColorStop(1, '#22a866');
} else {
g.addColorStop(0, '#e6f3ff');
g.addColorStop(0.5, '#78c2ff');
g.addColorStop(1, '#1f66c9');
}
ctx.fillStyle = g;
roundRect(ctx, x, y, w, h, h / 2);
ctx.fill();
ctx.shadowBlur = 0;
// inner shine
ctx.fillStyle = 'rgba(255,255,255,.5)';
roundRect(ctx, x + 6, y + 3, w - 12, 3, 1.5);
ctx.fill();
// end caps
ctx.fillStyle = 'rgba(255,255,255,.35)';
ctx.beginPath(); ctx.arc(x + h / 2, y + h / 2, 3, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(x + w - h / 2, y + h / 2, 3, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
function drawBall(b) {
// trail
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const k = i / b.trail.length;
ctx.globalAlpha = k * 0.32;
ctx.fillStyle = '#7fd0ff';
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * (0.35 + k * 0.6), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.save();
ctx.shadowColor = 'rgba(140,210,255,.95)';
ctx.shadowBlur = 20;
const g = ctx.createRadialGradient(b.x - b.r * 0.35, b.y - b.r * 0.4, b.r * 0.1, b.x, b.y, b.r);
g.addColorStop(0, '#ffffff');
g.addColorStop(0.5, '#dcefff');
g.addColorStop(1, '#6fc0ff');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawPowerups() {
for (const p of powerups) {
const isWide = p.kind === 'wide';
const ic = isWide ? '#7dffb0' : '#8fd0ff';
const bob = Math.sin(p.t * 6) * 2;
ctx.save();
ctx.translate(p.x, p.y + bob);
ctx.shadowColor = ic;
ctx.shadowBlur = 18;
// capsule
const g = ctx.createLinearGradient(0, -p.r, 0, p.r);
g.addColorStop(0, 'rgba(20,34,58,.95)');
g.addColorStop(1, 'rgba(10,18,32,.95)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(0, 0, p.r, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = ic;
ctx.lineWidth = 2;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = ic;
if (isWide) {
roundRect(ctx, -4.5, -1.6, 9, 3.2, 1.4); ctx.fill();
ctx.beginPath(); ctx.moveTo(-4.8, -5); ctx.lineTo(-8, 0); ctx.lineTo(-4.8, 5); ctx.closePath(); ctx.fill();
ctx.beginPath(); ctx.moveTo(4.8, -5); ctx.lineTo(8, 0); ctx.lineTo(4.8, 5); ctx.closePath(); ctx.fill();
} else {
ctx.beginPath(); ctx.arc(0, -4.6, 2.6, 0, Math.PI * 2); ctx.fill();
for (let i = 0; i < 2; i++) {
const a = Math.PI / 2 + (i === 0 ? -0.8 : 0.8);
ctx.beginPath();
ctx.arc(Math.cos(a) * 4.6, Math.sin(a) * 3.4 + 1.5, 2.3, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
}
function drawParticles() {
for (const p of particles) {
const a = clamp(p.life / p.maxLife, 0, 1);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.75);
ctx.restore();
}
ctx.globalAlpha = 1;
}
function drawFloaters() {
for (const f of floaters) {
const a = 1 - f.t / f.life;
ctx.globalAlpha = clamp(a, 0, 1);
ctx.font = '800 16px "Segoe UI", system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = f.color;
ctx.shadowColor = f.color;
ctx.shadowBlur = 12;
ctx.fillText(f.text, f.x, f.y);
ctx.shadowBlur = 0;
ctx.globalAlpha = 1;
}
}
function drawBanner() {
if (!banner) return;
const k = clamp(banner.t / 1.4, 0, 1);
const a = k > 0.75 ? (1 - k) / 0.25 : Math.min(1, k / 0.3);
ctx.save();
ctx.globalAlpha = clamp(a, 0, 1);
ctx.font = '800 20px "Segoe UI", system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = banner.color;
ctx.shadowColor = banner.color;
ctx.shadowBlur = 16;
ctx.fillText(banner.text, W / 2, PADDLE_Y - 78);
ctx.restore();
}
function drawEndScreen(victory) {
ctx.save();
ctx.fillStyle = 'rgba(5,8,16,.78)';
ctx.fillRect(0, 0, W, H);
const cx = W / 2, cy = H / 2;
const isV = victory;
// panel
const pw = 460, ph = 288;
ctx.save();
ctx.shadowColor = isV ? 'rgba(125,255,176,.4)' : 'rgba(255,90,120,.35)';
ctx.shadowBlur = 50;
const g = ctx.createLinearGradient(0, cy - ph / 2, 0, cy + ph / 2);
g.addColorStop(0, '#152136');
g.addColorStop(1, '#0a1120');
ctx.fillStyle = g;
roundRect(ctx, cx - pw / 2, cy - ph / 2, pw, ph, 20);
ctx.fill();
ctx.restore();
ctx.strokeStyle = isV ? 'rgba(125,255,176,.55)' : 'rgba(255,120,150,.5)';
ctx.lineWidth = 1.6;
roundRect(ctx, cx - pw / 2, cy - ph / 2, pw, ph, 20);
ctx.stroke();
// title
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '800 42px "Segoe UI", system-ui, sans-serif';
const tg = ctx.createLinearGradient(0, cy - 100, 0, cy - 46);
if (isV) {
tg.addColorStop(0, '#e8fff2');
tg.addColorStop(0.55, '#7dffb0');
tg.addColorStop(1, '#25b06c');
} else {
tg.addColorStop(0, '#ffe9ee');
tg.addColorStop(0.55, '#ff6b8a');
tg.addColorStop(1, '#b31f3c');
}
ctx.fillStyle = tg;
ctx.shadowColor = isV ? 'rgba(125,255,176,.6)' : 'rgba(255,90,120,.6)';
ctx.shadowBlur = 26;
ctx.fillText(isV ? 'VICTORY' : 'GAME OVER', cx, cy - 74);
ctx.shadowBlur = 0;
ctx.font = '600 14px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = 'rgba(160,190,225,.75)';
ctx.fillText(isV ? 'All bricks destroyed — nice work.' : 'You ran out of lives.', cx, cy - 26);
// score box
ctx.font = '600 13px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = 'rgba(150,180,220,.6)';
ctx.fillText('FINAL SCORE', cx, cy + 18);
ctx.font = '800 40px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#eaf3ff';
ctx.fillText(String(score), cx, cy + 56);
// restart button
const bw = 250, bh = 50;
const bx = cx - bw / 2, by = cy + 88;
const bg = ctx.createLinearGradient(bx, by, bx, by + bh);
if (isV) {
bg.addColorStop(0, '#8dffbe');
bg.addColorStop(1, '#26a86a');
} else {
bg.addColorStop(0, '#7fb6ff');
bg.addColorStop(1, '#2a63c4');
}
ctx.save();
ctx.shadowColor = isV ? 'rgba(125,255,176,.55)' : 'rgba(90,150,255,.55)';
ctx.shadowBlur = 24;
ctx.fillStyle = bg;
roundRect(ctx, bx, by, bw, bh, 25);
ctx.fill();
ctx.restore();
ctx.strokeStyle = 'rgba(255,255,255,.35)';
ctx.lineWidth = 1;
roundRect(ctx, bx, by, bw, bh, 25);
ctx.stroke();
ctx.font = '800 16px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#08111f';
ctx.fillText('RESTART \u21B5', cx, by + bh / 2 + 1);
ctx.font = '500 12px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = 'rgba(150,180,220,.55)';
ctx.fillText('Click / press Space or R', cx, by + bh + 22);
// store button rect for hit test
endButton = { x: bx, y: by, w: bw, h: bh };
ctx.restore();
}
let endButton = null;
// ---------------------------------------------------------------- loop
let lastT = performance.now();
function frame(now) {
let dt = (now - lastT) / 1000;
lastT = now;
dt = Math.min(dt, 1 / 30);
update(dt);
// draw
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, W, H);
const sh = shake > 0 ? shake : 0;
if (sh > 0.05) {
ctx.translate(rand(-sh, sh) * 0.7, rand(-sh, sh) * 0.7);
}
drawBackground();
drawHUD();
for (const br of bricks) if (br.alive) drawBrick(br);
drawPowerups();
drawParticles();
drawPaddle();
for (const b of balls) drawBall(b);
drawFloaters();
drawBanner();
if (flash > 0) {
ctx.globalAlpha = flash * 0.35;
ctx.fillStyle = '#aad4ff';
ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = 1;
}
if (state === 'gameover') drawEndScreen(false);
else if (state === 'victory') drawEndScreen(true);
// demo hint on canvas
if (!started) {
// keep the demo lively: auto launch
if (state === 'demo' || !started) {
// handled in update via launchCooldown
}
}
requestAnimationFrame(frame);
}
// demo ball must be launched
function demoLaunch() {
if (!started) {
for (const b of balls) {
if (b.stuck) {
const a = -Math.PI / 2 + rand(-0.32, 0.32);
b.vx = Math.cos(a) * BALL_SPEED;
b.vy = Math.sin(a) * BALL_SPEED;
b.stuck = false;
}
}
}
}
// ---------------------------------------------------------------- boot
function resize() {
const pad = 0;
const availW = window.innerWidth - pad * 2;
const availH = window.innerHeight - pad * 2;
const scale = Math.min(availW / W, availH / H, 1.55);
canvas.style.width = (W * scale) + 'px';
canvas.style.height = (H * scale) + 'px';
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
// init
resize();
buildLevel();
resetBallOnPaddle();
paddle.x = W / 2;
// demo mode: auto launch and restart when balls die or level clears
setInterval(() => {
if (started) return;
if (state === 'playing' || state === 'demo') {
if (state === 'demo') { /* noop */ }
if (balls.every(b => b.stuck)) demoLaunch();
}
}, 400);
// demo safety: if balls run out before start, respawn; if bricks cleared, rebuild
const demoTick = setInterval(() => {
if (started) { clearInterval(demoTick); return; }
if (balls.length === 0) {
lives = 3;
resetBallOnPaddle();
}
if (bricks.every(b => !b.alive)) {
buildLevel();
powerups = [];
}
}, 600);
// click on end-screen restart button
canvas.addEventListener('click', e => {
if (state === 'gameover' || state === 'victory') {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width * W;
const y = (e.clientY - rect.top) / rect.height * H;
if (!endButton || (x > endButton.x && x < endButton.x + endButton.w &&
y > endButton.y && y < endButton.y + endButton.h)) {
resetGame();
} else {
resetGame();
}
}
});
// demo AI needs a non-stuck ball; make sure the very first paint is active
setTimeout(() => { if (!started) demoLaunch(); }, 500);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>