DeepSeek V4 Pro 0813
by DeepSeekreasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 3 min 8 s
- Cost
- $0.080
- Output tokens
- 19,965
- Spent thinking
- 6,683 tokens
- Speed
- 108.2 tok/s
- Console errors
- 0
- First token after
- 3.4 s
- First visible text after
- 1 min 9 s
- Prompt tokens
- 578
- Code size
- 56.3 KB
- Finish reason
- stop
- Served by
- Nebius
- Model id
deepseek/deepseek-v4-pro-0813- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1348 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout - Brick Breaker</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a0f;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
cursor: default;
}
.game-container {
position: relative;
max-width: 100vw;
max-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
display: block;
width: min(800px, 100vw, calc(100vh * 4 / 3));
height: auto;
aspect-ratio: 4 / 3;
border-radius: 10px;
box-shadow: 0 0 60px rgba(100, 150, 255, 0.15), 0 0 120px rgba(50, 80, 200, 0.08);
background: #0d0d14;
cursor: none;
}
.start-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
border-radius: 10px;
background: rgba(0, 0, 0, 0.45);
transition: opacity 0.4s ease;
}
.start-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.start-message {
text-align: center;
color: #fff;
padding: 30px 40px;
background: rgba(15, 15, 30, 0.9);
border-radius: 16px;
border: 2px solid rgba(120, 170, 255, 0.35);
box-shadow: 0 0 40px rgba(80, 120, 255, 0.25), inset 0 0 30px rgba(40, 60, 140, 0.2);
backdrop-filter: blur(4px);
animation: pulse-border 2s ease-in-out infinite;
}
.start-message h2 {
font-size: clamp(1.8rem, 4vw, 2.8rem);
font-weight: 800;
letter-spacing: 4px;
color: #c8d8ff;
text-shadow: 0 0 30px rgba(120, 170, 255, 0.7), 0 0 60px rgba(80, 120, 255, 0.4);
margin-bottom: 10px;
}
.start-message p {
font-size: clamp(0.9rem, 1.8vw, 1.2rem);
color: #90a8cc;
letter-spacing: 1px;
animation: blink 1.5s ease-in-out infinite;
}
@keyframes pulse-border {
0%,
100% {
border-color: rgba(120, 170, 255, 0.35);
box-shadow: 0 0 40px rgba(80, 120, 255, 0.25), inset 0 0 30px rgba(40, 60, 140, 0.2);
}
50% {
border-color: rgba(160, 200, 255, 0.6);
box-shadow: 0 0 60px rgba(100, 150, 255, 0.4), inset 0 0 40px rgba(60, 80, 180, 0.3);
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
@media (max-width: 500px) {
canvas {
border-radius: 6px;
}
.start-message {
padding: 20px 24px;
border-radius: 12px;
}
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="startOverlay" class="start-overlay">
<div class="start-message">
<h2>BREAKOUT</h2>
<p>Click or press any key to start</p>
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('startOverlay');
// Game constants
const CANVAS_W = 800;
const CANVAS_H = 600;
const HUD_HEIGHT = 52;
const GAME_TOP = HUD_HEIGHT + 5;
const PADDLE_Y = 545;
const PADDLE_HEIGHT = 14;
const PADDLE_WIDTH_NORMAL = 104;
const PADDLE_WIDTH_WIDE = 158;
const BALL_RADIUS = 7;
const BALL_SPEED_BASE = 5.2;
const BALL_SPEED_MAX = 7.2;
const BRICK_ROWS = 7;
const BRICK_COLS = 10;
const BRICK_WIDTH = 66;
const BRICK_HEIGHT = 20;
const BRICK_GAP_X = 8;
const BRICK_GAP_Y = 7;
const BRICKS_TOP = GAME_TOP + 16;
const BRICKS_LEFT = (CANVAS_W - (BRICK_COLS * (BRICK_WIDTH + BRICK_GAP_X) - BRICK_GAP_X)) / 2;
const POWERUP_FALL_SPEED = 2.6;
const POWERUP_SIZE = 28;
const WIDE_PADDLE_DURATION = 600; // 10 seconds at 60fps
const MAX_LIVES = 3;
const SUB_STEP = 3.0; // max pixels per sub-step for ball movement
// Brick colors by row
const BRICK_COLORS = [
'#ff5566', // row 0 - red
'#ff8844', // row 1 - orange
'#ffcc33', // row 2 - yellow
'#44dd88', // row 3 - green
'#4488ff', // row 4 - blue
'#9966ff', // row 5 - purple
'#ff66aa', // row 6 - pink
];
// Power-up positions: row, col, type (0=wide, 1=multiball)
const POWERUP_PLACEMENTS = [
{ row: 1, col: 2, type: 0 },
{ row: 1, col: 7, type: 1 },
{ row: 2, col: 5, type: 0 },
{ row: 3, col: 3, type: 1 },
{ row: 3, col: 8, type: 0 },
{ row: 5, col: 5, type: 1 },
{ row: 6, col: 2, type: 0 },
];
// Game state
let gameState = 'demo'; // 'demo', 'playing', 'gameover', 'victory'
let score = 0;
let lives = MAX_LIVES;
let bricks = [];
let balls = [];
let particles = [];
let powerupDrops = [];
let paddle = {
x: CANVAS_W / 2 - PADDLE_WIDTH_NORMAL / 2,
y: PADDLE_Y,
width: PADDLE_WIDTH_NORMAL,
height: PADDLE_HEIGHT,
targetX: CANVAS_W / 2 - PADDLE_WIDTH_NORMAL / 2,
};
let widePaddleTimer = 0;
let keys = { left: false, right: false };
let mouseX = CANVAS_W / 2;
let mouseActive = false;
let aiTargetX = CANVAS_W / 2;
let aiSpeed = 3.5;
let frameCount = 0;
let screenShake = 0;
let ballTrail = [];
let lastBallLossCheck = 0;
let bricksDestroyed = 0;
let totalBricks = 0;
let gameOverDisplayTimer = 0;
let victoryDisplayTimer = 0;
let autoRestartTimer = 0;
let needRestart = false;
// ----- Brick creation -----
function createBricks() {
bricks = [];
bricksDestroyed = 0;
totalBricks = 0;
for (let r = 0; r < BRICK_ROWS; r++) {
for (let c = 0; c < BRICK_COLS; c++) {
const isTough = (r >= 3 && r <= 4) || r === 6;
const powerupIdx = POWERUP_PLACEMENTS.findIndex(p => p.row === r && p.col === c);
const hasPowerup = powerupIdx !== -1;
const powerupType = hasPowerup ? POWERUP_PLACEMENTS[powerupIdx].type : -1;
bricks.push({
row: r,
col: c,
x: BRICKS_LEFT + c * (BRICK_WIDTH + BRICK_GAP_X),
y: BRICKS_TOP + r * (BRICK_HEIGHT + BRICK_GAP_Y),
width: BRICK_WIDTH,
height: BRICK_HEIGHT,
color: BRICK_COLORS[r],
hits: 0,
maxHits: isTough ? 2 : 1,
isTough: isTough,
hasPowerup: hasPowerup,
powerupType: powerupType,
alive: true,
crackSeed: Math.random() * 1000,
breakAnim: 0,
breakAnimActive: false,
});
totalBricks++;
}
}
}
// ----- Ball management -----
function createBall(x, y, angleDeg) {
const angleRad = (angleDeg * Math.PI) / 180;
const speed = Math.min(BALL_SPEED_BASE + bricksDestroyed * 0.025, BALL_SPEED_MAX);
const vx = speed * Math.sin(angleRad);
const vy = -speed * Math.cos(angleRad);
return {
x: x,
y: y,
vx: vx,
vy: vy,
radius: BALL_RADIUS,
speed: speed,
prevX: x,
prevY: y,
stuckTimer: 0,
};
}
function spawnBallOnPaddle() {
const px = paddle.x + paddle.width / 2;
const py = paddle.y - BALL_RADIUS - 2;
const angle = (Math.random() * 50 - 25); // -25 to +25 degrees from vertical
balls = [createBall(px, py, angle)];
}
function spawnMultiBall() {
const existing = balls.filter(b => b.y < CANVAS_H + 50);
if (existing.length === 0) return;
const refBall = existing[0];
const newBalls = [];
const angles = [-25, 25];
for (const a of angles) {
if (existing.length + newBalls.length < 3) {
const speed = refBall.speed;
const angleRad = (a * Math.PI) / 180;
newBalls.push({
x: refBall.x,
y: refBall.y,
vx: speed * Math.sin(angleRad),
vy: -speed * Math.cos(angleRad),
radius: BALL_RADIUS,
speed: speed,
prevX: refBall.x,
prevY: refBall.y,
stuckTimer: 0,
});
}
}
balls = balls.filter(b => b.y < CANVAS_H + 50).concat(newBalls);
if (balls.length > 3) balls = balls.slice(0, 3);
}
// ----- Particle system -----
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.5 + Math.random() * 3.5;
const size = 2 + Math.random() * 3.5;
const life = 25 + Math.random() * 35;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1,
size: size,
life: life,
maxLife: life,
color: color,
gravity: 0.06,
});
}
}
function spawnPowerupCatchEffect(x, y, type) {
const colors = type === 0 ? ['#66aaff', '#88ccff', '#aaddff'] : ['#66ff88', '#88ffaa', '#aaffcc'];
for (let i = 0; i < 25; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 3;
const size = 2 + Math.random() * 3;
const life = 30 + Math.random() * 30;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1.5,
size: size,
life: life,
maxLife: life,
color: colors[Math.floor(Math.random() * colors.length)],
gravity: 0.04,
});
}
}
// ----- Power-up drops -----
function spawnPowerupDrop(x, y, type) {
powerupDrops.push({
x: x,
y: y,
type: type,
vy: POWERUP_FALL_SPEED,
width: POWERUP_SIZE,
height: POWERUP_SIZE,
bobPhase: Math.random() * Math.PI * 2,
});
}
// ----- Collision detection -----
function circleRectCollision(cx, cy, cr, rx, ry, rw, rh) {
const closestX = Math.max(rx, Math.min(cx, rx + rw));
const closestY = Math.max(ry, Math.min(cy, ry + rh));
const dx = cx - closestX;
const dy = cy - closestY;
return (dx * dx + dy * dy) < (cr * cr);
}
function getCollisionNormal(cx, cy, cr, rx, ry, rw, rh) {
const closestX = Math.max(rx, Math.min(cx, rx + rw));
const closestY = Math.max(ry, Math.min(cy, ry + rh));
const dx = cx - closestX;
const dy = cy - closestY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
// Ball center inside rectangle - push out along the smallest penetration axis
const leftPen = cx - rx;
const rightPen = rx + rw - cx;
const topPen = cy - ry;
const bottomPen = ry + rh - cy;
const minPen = Math.min(leftPen, rightPen, topPen, bottomPen);
if (minPen === leftPen) return { nx: -1, ny: 0 };
if (minPen === rightPen) return { nx: 1, ny: 0 };
if (minPen === topPen) return { nx: 0, ny: -1 };
return { nx: 0, ny: 1 };
}
return { nx: dx / dist, ny: dy / dist };
}
// ----- Paddle bounce angle -----
function getPaddleBounceAngle(ballX, paddleX, paddleWidth) {
const centerX = paddleX + paddleWidth / 2;
const offset = (ballX - centerX) / (paddleWidth / 2);
const clampedOffset = Math.max(-1, Math.min(1, offset));
const maxAngleDeg = 65;
const angleDeg = clampedOffset * maxAngleDeg;
return angleDeg;
}
function applyPaddleBounce(ball, paddleX, paddleWidth) {
const angleDeg = getPaddleBounceAngle(ball.x, paddleX, paddleWidth);
const angleRad = (angleDeg * Math.PI) / 180;
const speed = Math.min(BALL_SPEED_BASE + bricksDestroyed * 0.025, BALL_SPEED_MAX);
// Add a tiny bit of randomness to avoid perfectly horizontal loops
const jitter = (Math.random() - 0.5) * 2;
const finalAngle = angleDeg + jitter;
const finalRad = (finalAngle * Math.PI) / 180;
ball.vx = speed * Math.sin(finalRad);
ball.vy = -speed * Math.cos(finalRad);
ball.speed = speed;
// Ensure minimum vertical velocity
const minVy = speed * 0.25;
if (Math.abs(ball.vy) < minVy) {
ball.vy = -minVy;
const vxMag = Math.sqrt(Math.max(0, speed * speed - ball.vy * ball.vy));
ball.vx = ball.vx >= 0 ? vxMag : -vxMag;
}
ball.y = paddleY - ball.radius - 0.5;
ball.stuckTimer = 0;
}
// ----- Break a brick -----
function breakBrick(brick) {
if (!brick.alive) return;
brick.hits++;
if (brick.hits >= brick.maxHits) {
brick.alive = false;
brick.breakAnimActive = true;
brick.breakAnim = 1.0;
bricksDestroyed++;
const points = brick.isTough ? 20 : 10;
score += points;
spawnParticles(
brick.x + brick.width / 2,
brick.y + brick.height / 2,
brick.color,
brick.isTough ? 22 : 16
);
screenShake = Math.min(screenShake + 2, 6);
if (brick.hasPowerup) {
spawnPowerupDrop(brick.x + brick.width / 2 - POWERUP_SIZE / 2,
brick.y + brick.height / 2,
brick.powerupType);
}
} else {
// First hit on tough brick - show crack
spawnParticles(
brick.x + brick.width / 2,
brick.y + brick.height / 2,
brick.color,
8
);
screenShake = Math.min(screenShake + 1, 3);
}
}
// ----- Check all bricks destroyed -----
function allBricksDestroyed() {
return bricks.every(b => !b.alive);
}
// ----- Update functions -----
function updatePaddlePlayer() {
const speed = 7.5;
if (keys.left) {
paddle.x -= speed;
}
if (keys.right) {
paddle.x += speed;
}
if (mouseActive) {
paddle.x = mouseX - paddle.width / 2;
}
paddle.x = Math.max(4, Math.min(CANVAS_W - paddle.width - 4, paddle.x));
}
function updatePaddleDemo() {
// AI: track the ball
const targetBall = balls.find(b => b.vy > 0 && b.y > 200) || balls[0];
if (!targetBall) {
aiTargetX = CANVAS_W / 2;
} else {
// Predict where ball will be when it reaches paddle level
const timeToPaddle = (PADDLE_Y - targetBall.y) / Math.max(1, targetBall.vy);
let predictedX = targetBall.x + targetBall.vx * timeToPaddle;
// Account for wall bounces
while (predictedX < BALL_RADIUS || predictedX > CANVAS_W - BALL_RADIUS) {
if (predictedX < BALL_RADIUS) predictedX = 2 * BALL_RADIUS - predictedX;
if (predictedX > CANVAS_W - BALL_RADIUS) predictedX = 2 * (CANVAS_W - BALL_RADIUS) - predictedX;
}
// Add slight inaccuracy
const inaccuracy = Math.sin(frameCount * 0.05) * 18;
aiTargetX = predictedX + inaccuracy;
// Also try to catch falling power-ups
for (const pu of powerupDrops) {
if (pu.y > 300 && pu.y < 520) {
const puCenter = pu.x + pu.width / 2;
if (Math.abs(puCenter - aiTargetX) < 80) {
aiTargetX = puCenter;
break;
}
}
}
}
const centerX = paddle.x + paddle.width / 2;
const diff = aiTargetX - centerX;
const maxStep = aiSpeed + Math.random() * 1.5;
if (Math.abs(diff) > 2) {
paddle.x += Math.max(-maxStep, Math.min(maxStep, diff * 0.25));
}
paddle.x = Math.max(4, Math.min(CANVAS_W - paddle.width - 4, paddle.x));
}
function updateBalls() {
const speedCap = Math.min(BALL_SPEED_BASE + bricksDestroyed * 0.025, BALL_SPEED_MAX);
const toRemove = [];
for (let bi = balls.length - 1; bi >= 0; bi--) {
const ball = balls[bi];
ball.prevX = ball.x;
ball.prevY = ball.y;
// Re-normalize speed occasionally to prevent drift
const currentSpeed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (Math.abs(currentSpeed - speedCap) > 0.3) {
const ratio = speedCap / currentSpeed;
ball.vx *= ratio;
ball.vy *= ratio;
}
// Sub-stepping for collision detection
const totalDist = currentSpeed;
const numSteps = Math.max(1, Math.ceil(totalDist / SUB_STEP));
const stepX = ball.vx / numSteps;
const stepY = ball.vy / numSteps;
let collided = false;
for (let s = 0; s < numSteps && !collided; s++) {
ball.x += stepX;
ball.y += stepY;
// Wall collisions
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.vx = Math.abs(ball.vx);
collided = true;
} else if (ball.x + ball.radius > CANVAS_W) {
ball.x = CANVAS_W - ball.radius;
ball.vx = -Math.abs(ball.vx);
collided = true;
}
if (ball.y - ball.radius < HUD_HEIGHT) {
ball.y = HUD_HEIGHT + ball.radius;
ball.vy = Math.abs(ball.vy);
collided = true;
}
// Paddle collision (only when moving downward)
if (!collided && ball.vy > 0) {
const paddleTop = paddle.y;
const paddleBottom = paddle.y + paddle.height;
const ballBottom = ball.y + ball.radius;
const ballPrevBottom = ball.prevY + ball.radius;
if (ballBottom >= paddleTop && ballPrevBottom <= paddleTop + 2 &&
ball.x + ball.radius > paddle.x && ball.x - ball.radius < paddle.x + paddle.width) {
applyPaddleBounce(ball, paddle.x, paddle.width);
collided = true;
// Small particles on paddle hit
for (let i = 0; i < 5; i++) {
particles.push({
x: ball.x,
y: paddle.y,
vx: (Math.random() - 0.5) * 3,
vy: -Math.random() * 2 - 0.5,
size: 1.5 + Math.random() * 2,
life: 18 + Math.random() * 15,
maxLife: 33,
color: '#88bbff',
gravity: 0.03,
});
}
}
}
// Brick collisions
if (!collided) {
for (const brick of bricks) {
if (!brick.alive || brick.breakAnimActive) continue;
if (circleRectCollision(ball.x, ball.y, ball.radius,
brick.x, brick.y, brick.width, brick.height)) {
const normal = getCollisionNormal(ball.x, ball.y, ball.radius,
brick.x, brick.y, brick.width, brick.height);
// Determine which side the ball hit
if (Math.abs(normal.nx) > Math.abs(normal.ny)) {
// Horizontal collision
ball.vx = normal.nx > 0 ? Math.abs(ball.vx) : -Math.abs(ball.vx);
ball.x += normal.nx * (ball.radius + 0.5);
} else {
// Vertical collision
ball.vy = normal.ny > 0 ? Math.abs(ball.vy) : -Math.abs(ball.vy);
ball.y += normal.ny * (ball.radius + 0.5);
}
breakBrick(brick);
collided = true;
break;
}
}
}
// Bottom - ball lost
if (!collided && ball.y - ball.radius > CANVAS_H + 10) {
toRemove.push(bi);
collided = true;
break;
}
ball.prevX = ball.x;
ball.prevY = ball.y;
}
// Update trail
if (!collided) {
ball.prevX = ball.x;
ball.prevY = ball.y;
}
}
// Remove balls that went off bottom
for (const idx of toRemove.sort((a, b) => b - a)) {
balls.splice(idx, 1);
}
// If no balls remain, lose a life
if (balls.length === 0 && (gameState === 'playing' || gameState === 'demo')) {
lives--;
if (lives <= 0) {
if (gameState === 'playing') {
gameState = 'gameover';
gameOverDisplayTimer = 0;
} else {
// Demo mode auto-restart
resetGame();
}
} else {
spawnBallOnPaddle();
// Small pause effect
spawnParticles(CANVAS_W / 2, PADDLE_Y - 20, '#ff6666', 15);
}
}
}
function updatePowerups() {
for (let i = powerupDrops.length - 1; i >= 0; i--) {
const pu = powerupDrops[i];
pu.y += pu.vy;
pu.bobPhase += 0.08;
// Check paddle catch
const puX = pu.x + pu.width / 2;
const puY = pu.y + pu.height / 2;
if (puY + pu.height / 2 >= paddle.y && puY - pu.height / 2 <= paddle.y + paddle.height &&
puX >= paddle.x && puX <= paddle.x + paddle.width) {
// Caught!
if (pu.type === 0) {
widePaddleTimer = WIDE_PADDLE_DURATION;
spawnPowerupCatchEffect(puX, paddle.y, 0);
score += 15;
} else {
spawnMultiBall();
spawnPowerupCatchEffect(puX, paddle.y, 1);
score += 25;
}
powerupDrops.splice(i, 1);
continue;
}
// Check if below screen
if (pu.y > CANVAS_H + 20) {
powerupDrops.splice(i, 1);
}
}
// Update wide paddle timer
if (widePaddleTimer > 0) {
widePaddleTimer--;
if (widePaddleTimer === 0) {
// Revert paddle width
const centerX = paddle.x + paddle.width / 2;
paddle.x = centerX - PADDLE_WIDTH_NORMAL / 2;
}
}
paddle.width = widePaddleTimer > 0 ? PADDLE_WIDTH_WIDE : PADDLE_WIDTH_NORMAL;
// Keep paddle in bounds after size change
paddle.x = Math.max(4, Math.min(CANVAS_W - paddle.width - 4, paddle.x));
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += p.gravity;
p.vx *= 0.99;
p.life--;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
if (particles.length > 500) particles.splice(0, particles.length - 500);
}
function updateBrickBreakAnimations() {
for (const brick of bricks) {
if (brick.breakAnimActive && brick.breakAnim > 0) {
brick.breakAnim -= 0.06;
if (brick.breakAnim <= 0) {
brick.breakAnim = 0;
brick.breakAnimActive = false;
}
}
}
}
function updateTrail() {
for (const ball of balls) {
ballTrail.push({ x: ball.x, y: ball.y, life: 14 });
}
for (let i = ballTrail.length - 1; i >= 0; i--) {
ballTrail[i].life--;
if (ballTrail[i].life <= 0) ballTrail.splice(i, 1);
}
if (ballTrail.length > 300) ballTrail.splice(0, ballTrail.length - 300);
}
function updateScreenShake() {
if (screenShake > 0) screenShake *= 0.88;
if (screenShake < 0.05) screenShake = 0;
}
function resetGame() {
score = 0;
lives = MAX_LIVES;
createBricks();
paddle.x = CANVAS_W / 2 - PADDLE_WIDTH_NORMAL / 2;
paddle.width = PADDLE_WIDTH_NORMAL;
paddle.y = PADDLE_Y;
widePaddleTimer = 0;
particles = [];
powerupDrops = [];
ballTrail = [];
screenShake = 0;
bricksDestroyed = 0;
spawnBallOnPaddle();
gameState = 'playing';
needRestart = false;
}
function resetForDemo() {
score = 0;
lives = MAX_LIVES;
createBricks();
paddle.x = CANVAS_W / 2 - PADDLE_WIDTH_NORMAL / 2;
paddle.width = PADDLE_WIDTH_NORMAL;
paddle.y = PADDLE_Y;
widePaddleTimer = 0;
particles = [];
powerupDrops = [];
ballTrail = [];
screenShake = 0;
bricksDestroyed = 0;
spawnBallOnPaddle();
gameState = 'demo';
needRestart = false;
gameOverDisplayTimer = 0;
victoryDisplayTimer = 0;
}
function update() {
frameCount++;
if (gameState === 'demo') {
updatePaddleDemo();
updateBalls();
updatePowerups();
updateParticles();
updateBrickBreakAnimations();
updateTrail();
updateScreenShake();
if (allBricksDestroyed()) {
// Demo: victory - auto restart after a beat
victoryDisplayTimer++;
if (victoryDisplayTimer > 120) {
resetForDemo();
}
}
if (lives <= 0 && balls.length === 0) {
gameOverDisplayTimer++;
if (gameOverDisplayTimer > 90) {
resetForDemo();
}
}
} else if (gameState === 'playing') {
updatePaddlePlayer();
updateBalls();
updatePowerups();
updateParticles();
updateBrickBreakAnimations();
updateTrail();
updateScreenShake();
if (allBricksDestroyed()) {
gameState = 'victory';
victoryDisplayTimer = 0;
}
if (lives <= 0 && balls.length === 0) {
gameState = 'gameover';
gameOverDisplayTimer = 0;
}
} else if (gameState === 'gameover' || gameState === 'victory') {
updateParticles();
updateBrickBreakAnimations();
updateTrail();
updateScreenShake();
if (gameState === 'gameover') gameOverDisplayTimer++;
else victoryDisplayTimer++;
}
}
// ----- Rendering -----
function drawHUD() {
// HUD background
const hudGrad = ctx.createLinearGradient(0, 0, 0, HUD_HEIGHT);
hudGrad.addColorStop(0, 'rgba(10, 12, 25, 0.95)');
hudGrad.addColorStop(1, 'rgba(10, 12, 25, 0.7)');
ctx.fillStyle = hudGrad;
ctx.fillRect(0, 0, CANVAS_W, HUD_HEIGHT);
ctx.fillStyle = 'rgba(120, 170, 255, 0.25)';
ctx.fillRect(0, HUD_HEIGHT - 1, CANVAS_W, 1);
// Score
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#e0e8ff';
ctx.font = 'bold 18px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('SCORE', 18, 20);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 24px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText(score.toString().padStart(6, '0'), 18, 42);
// Center: power-up status
ctx.textAlign = 'center';
if (widePaddleTimer > 0) {
const secs = Math.ceil(widePaddleTimer / 60);
ctx.fillStyle = '#66aaff';
ctx.font = 'bold 14px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('WIDE PADDLE: ' + secs + 's', CANVAS_W / 2, 26);
ctx.fillStyle = 'rgba(100, 170, 255, 0.7)';
ctx.fillRect(CANVAS_W / 2 - 60, 36, 120, 4);
ctx.fillStyle = '#66ccff';
ctx.fillRect(CANVAS_W / 2 - 60, 36, (widePaddleTimer / WIDE_PADDLE_DURATION) * 120, 4);
} else if (balls.length > 1) {
ctx.fillStyle = '#66ff88';
ctx.font = 'bold 14px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('MULTI-BALL (' + balls.length + ')', CANVAS_W / 2, 26);
} else {
ctx.fillStyle = 'rgba(140, 160, 200, 0.6)';
ctx.font = '13px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('BREAKOUT', CANVAS_W / 2, 26);
}
// Lives
ctx.textAlign = 'right';
ctx.fillStyle = '#e0e8ff';
ctx.font = 'bold 18px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('LIVES', CANVAS_W - 18, 20);
// Draw life icons (small paddles)
for (let i = 0; i < MAX_LIVES; i++) {
const lx = CANVAS_W - 30 - i * 28;
const ly = 38;
if (i < lives) {
ctx.fillStyle = '#4488ff';
ctx.shadowColor = '#4488ff';
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.roundRect(lx - 10, ly - 4, 20, 8, 4);
ctx.fill();
ctx.shadowBlur = 0;
} else {
ctx.fillStyle = 'rgba(60, 70, 90, 0.4)';
ctx.beginPath();
ctx.roundRect(lx - 10, ly - 4, 20, 8, 4);
ctx.fill();
}
}
ctx.shadowBlur = 0;
}
function drawPaddle() {
const paddleGrad = ctx.createLinearGradient(0, paddle.y, 0, paddle.y + paddle.height);
if (widePaddleTimer > 0) {
paddleGrad.addColorStop(0, '#66ccff');
paddleGrad.addColorStop(0.5, '#4488dd');
paddleGrad.addColorStop(1, '#2266aa');
} else {
paddleGrad.addColorStop(0, '#aaccff');
paddleGrad.addColorStop(0.5, '#6688cc');
paddleGrad.addColorStop(1, '#334466');
}
ctx.fillStyle = paddleGrad;
ctx.shadowColor = widePaddleTimer > 0 ? '#66ccff' : '#4488cc';
ctx.shadowBlur = widePaddleTimer > 0 ? 20 : 10;
ctx.beginPath();
ctx.roundRect(paddle.x, paddle.y, paddle.width, paddle.height, 7);
ctx.fill();
ctx.shadowBlur = 0;
// Paddle shine
const shineGrad = ctx.createLinearGradient(0, paddle.y, 0, paddle.y + paddle.height / 2);
shineGrad.addColorStop(0, 'rgba(255,255,255,0.35)');
shineGrad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = shineGrad;
ctx.beginPath();
ctx.roundRect(paddle.x, paddle.y, paddle.width, paddle.height / 2, 7);
ctx.fill();
}
function drawBricks() {
for (const brick of bricks) {
if (!brick.alive && !brick.breakAnimActive) continue;
if (brick.breakAnimActive) {
const alpha = Math.max(0, brick.breakAnim);
ctx.globalAlpha = alpha;
// Scale down
const scale = Math.max(0.1, brick.breakAnim);
const cx = brick.x + brick.width / 2;
const cy = brick.y + brick.height / 2;
const w = brick.width * scale;
const h = brick.height * scale;
ctx.fillStyle = brick.color;
ctx.beginPath();
ctx.roundRect(cx - w / 2, cy - h / 2, w, h, 5);
ctx.fill();
ctx.globalAlpha = 1;
continue;
}
// Brick body
ctx.fillStyle = brick.color;
ctx.shadowColor = brick.color;
ctx.shadowBlur = 4;
ctx.beginPath();
ctx.roundRect(brick.x, brick.y, brick.width, brick.height, 5);
ctx.fill();
ctx.shadowBlur = 0;
// Brick shine
const shineGrad = ctx.createLinearGradient(0, brick.y, 0, brick.y + brick.height);
shineGrad.addColorStop(0, 'rgba(255,255,255,0.25)');
shineGrad.addColorStop(0.4, 'rgba(255,255,255,0.05)');
shineGrad.addColorStop(1, 'rgba(0,0,0,0.1)');
ctx.fillStyle = shineGrad;
ctx.beginPath();
ctx.roundRect(brick.x + 2, brick.y + 2, brick.width - 4, brick.height - 4, 4);
ctx.fill();
// Tough brick cracks after first hit
if (brick.isTough && brick.hits === 1) {
ctx.strokeStyle = 'rgba(30, 30, 40, 0.6)';
ctx.lineWidth = 1.5;
const cx = brick.x + brick.width / 2;
const cy = brick.y + brick.height / 2;
const seed = brick.crackSeed;
ctx.beginPath();
ctx.moveTo(cx - brick.width * 0.25, cy - brick.height * 0.3);
ctx.lineTo(cx + brick.width * 0.05, cy);
ctx.lineTo(cx - brick.width * 0.15, cy + brick.height * 0.35);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + brick.width * 0.2, cy - brick.height * 0.35);
ctx.lineTo(cx - brick.width * 0.1, cy);
ctx.lineTo(cx + brick.width * 0.18, cy + brick.height * 0.3);
ctx.stroke();
// Small debris marks
ctx.fillStyle = 'rgba(30,30,40,0.5)';
ctx.fillRect(cx - 2, cy - 3, 3, 3);
ctx.fillRect(cx + 4, cy + 2, 2, 3);
}
// Power-up icon on brick
if (brick.hasPowerup) {
const iconX = brick.x + brick.width / 2;
const iconY = brick.y + brick.height / 2;
ctx.fillStyle = brick.isTough ? 'rgba(255,255,255,0.85)' : 'rgba(30,30,50,0.85)';
ctx.beginPath();
ctx.arc(iconX, iconY, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = brick.powerupType === 0 ? '#4488cc' : '#33aa55';
ctx.font = 'bold 9px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(brick.powerupType === 0 ? 'W' : 'M', iconX, iconY + 0.5);
}
}
}
function drawPowerupDrops() {
for (const pu of powerupDrops) {
const bobOffset = Math.sin(pu.bobPhase) * 3;
const displayY = pu.y + bobOffset;
const cx = pu.x + pu.width / 2;
const cy = displayY + pu.height / 2;
// Glow
ctx.shadowColor = pu.type === 0 ? '#66aaff' : '#66ff88';
ctx.shadowBlur = 14;
ctx.fillStyle = pu.type === 0 ? '#4488cc' : '#33aa55';
ctx.beginPath();
ctx.roundRect(pu.x, displayY, pu.width, pu.height, 8);
ctx.fill();
ctx.shadowBlur = 0;
// Border
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.roundRect(pu.x, displayY, pu.width, pu.height, 8);
ctx.stroke();
// Icon
ctx.fillStyle = '#fff';
ctx.font = 'bold 14px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(pu.type === 0 ? 'W' : 'M', cx, cy + 1);
}
}
function drawBalls() {
for (const ball of balls) {
// Ball glow
ctx.shadowColor = '#ffffff';
ctx.shadowBlur = 12;
const ballGrad = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.radius);
ballGrad.addColorStop(0, '#ffffff');
ballGrad.addColorStop(0.6, '#ddeeff');
ballGrad.addColorStop(1, '#8899bb');
ctx.fillStyle = ballGrad;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Small shine dot
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath();
ctx.arc(ball.x - 2, ball.y - 2.5, 2, 0, Math.PI * 2);
ctx.fill();
}
}
function drawTrail() {
for (const t of ballTrail) {
const alpha = t.life / 14 * 0.35;
ctx.fillStyle = 'rgba(150, 190, 255, ' + alpha + ')';
ctx.beginPath();
ctx.arc(t.x, t.y, BALL_RADIUS * 0.5, 0, Math.PI * 2);
ctx.fill();
}
}
function drawParticles() {
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawBackground() {
// Subtle background gradient
const bgGrad = ctx.createLinearGradient(0, 0, 0, CANVAS_H);
bgGrad.addColorStop(0, '#12121c');
bgGrad.addColorStop(0.5, '#0d0d16');
bgGrad.addColorStop(1, '#08080e');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// Subtle grid pattern
ctx.strokeStyle = 'rgba(60, 80, 120, 0.07)';
ctx.lineWidth = 1;
for (let x = 0; x < CANVAS_W; x += 40) {
ctx.beginPath();
ctx.moveTo(x, HUD_HEIGHT);
ctx.lineTo(x, CANVAS_H);
ctx.stroke();
}
for (let y = HUD_HEIGHT; y < CANVAS_H; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(CANVAS_W, y);
ctx.stroke();
}
}
function drawGameOverScreen() {
const alpha = Math.min(1, gameOverDisplayTimer / 30);
ctx.fillStyle = 'rgba(0, 0, 0, ' + (0.65 * alpha) + ')';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.globalAlpha = alpha;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#ff4466';
ctx.font = 'bold 42px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.shadowColor = '#ff2244';
ctx.shadowBlur = 20;
ctx.fillText('GAME OVER', CANVAS_W / 2, CANVAS_H / 2 - 40);
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('Score: ' + score, CANVAS_W / 2, CANVAS_H / 2 + 15);
ctx.fillStyle = '#aabbdd';
ctx.font = '18px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('Press R or click to restart', CANVAS_W / 2, CANVAS_H / 2 + 55);
ctx.globalAlpha = 1;
}
function drawVictoryScreen() {
const alpha = Math.min(1, victoryDisplayTimer / 30);
ctx.fillStyle = 'rgba(0, 0, 0, ' + (0.55 * alpha) + ')';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.globalAlpha = alpha;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#66ffaa';
ctx.font = 'bold 42px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.shadowColor = '#44ff88';
ctx.shadowBlur = 25;
ctx.fillText('VICTORY!', CANVAS_W / 2, CANVAS_H / 2 - 40);
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('Score: ' + score, CANVAS_W / 2, CANVAS_H / 2 + 15);
ctx.fillStyle = '#aabbdd';
ctx.font = '18px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.fillText('Press R or click to play again', CANVAS_W / 2, CANVAS_H / 2 + 55);
ctx.globalAlpha = 1;
// Celebration particles
if (frameCount % 8 === 0) {
const colors = ['#66ffaa', '#ffcc44', '#ff6688', '#66aaff', '#aa66ff'];
spawnParticles(
Math.random() * CANVAS_W,
Math.random() * CANVAS_H * 0.6 + HUD_HEIGHT,
colors[Math.floor(Math.random() * colors.length)],
4
);
}
}
function render() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
// Screen shake offset
const shakeX = screenShake > 0 ? (Math.random() - 0.5) * screenShake * 2 : 0;
const shakeY = screenShake > 0 ? (Math.random() - 0.5) * screenShake * 2 : 0;
ctx.save();
ctx.translate(shakeX, shakeY);
drawBackground();
drawTrail();
drawPowerupDrops();
drawBricks();
drawPaddle();
drawBalls();
drawParticles();
drawHUD();
ctx.restore();
if (gameState === 'gameover') {
drawGameOverScreen();
} else if (gameState === 'victory') {
drawVictoryScreen();
}
// Demo mode indicator
if (gameState === 'demo') {
ctx.fillStyle = 'rgba(140, 160, 200, 0.45)';
ctx.font = '12px "Segoe UI", "Helvetica Neue", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('DEMO MODE - AI Playing', CANVAS_W / 2, HUD_HEIGHT + 5);
}
}
// ----- Game loop -----
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// ----- Input handling -----
function startGame() {
if (gameState === 'demo') {
resetGame();
} else if (gameState === 'gameover' || gameState === 'victory') {
resetGame();
}
overlay.classList.add('hidden');
}
function handleRestart() {
if (gameState === 'gameover' || gameState === 'victory') {
resetGame();
}
}
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = CANVAS_W / rect.width;
const canvasMouseX = (e.clientX - rect.left) * scaleX;
mouseX = Math.max(0, Math.min(CANVAS_W, canvasMouseX));
mouseActive = true;
});
canvas.addEventListener('mouseleave', () => {
mouseActive = false;
});
canvas.addEventListener('click', () => {
if (gameState === 'demo') {
startGame();
} else if (gameState === 'gameover' || gameState === 'victory') {
handleRestart();
}
});
document.addEventListener('keydown', (e) => {
if (gameState === 'demo') {
startGame();
return;
}
if (gameState === 'gameover' || gameState === 'victory') {
if (e.key === 'r' || e.key === 'R' || e.key === ' ' || e.key === 'Enter') {
handleRestart();
}
return;
}
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
keys.left = true;
mouseActive = false;
}
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
keys.right = true;
mouseActive = false;
}
if (e.key === 'r' || e.key === 'R') {
if (gameState === 'playing') {
resetGame();
}
}
e.preventDefault();
});
document.addEventListener('keyup', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
keys.left = false;
}
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
keys.right = false;
}
});
// Touch input for mobile
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = CANVAS_W / rect.width;
const touch = e.touches[0];
const canvasMouseX = (touch.clientX - rect.left) * scaleX;
mouseX = Math.max(0, Math.min(CANVAS_W, canvasMouseX));
mouseActive = true;
}, { passive: false });
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameState === 'demo') {
startGame();
} else if (gameState === 'gameover' || gameState === 'victory') {
handleRestart();
} else {
const rect = canvas.getBoundingClientRect();
const scaleX = CANVAS_W / rect.width;
const touch = e.touches[0];
const canvasMouseX = (touch.clientX - rect.left) * scaleX;
mouseX = Math.max(0, Math.min(CANVAS_W, canvasMouseX));
mouseActive = true;
}
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
mouseActive = false;
});
// RoundRect polyfill for older browsers
if (!ctx.roundRect) {
ctx.roundRect = function(x, y, w, h, radius) {
if (typeof radius === 'number') radius = [radius, radius, radius, radius];
const r = radius.map(v => Math.min(v, Math.min(w, h) / 2));
this.moveTo(x + r[0], y);
this.lineTo(x + w - r[1], y);
this.quadraticCurveTo(x + w, y, x + w, y + r[1]);
this.lineTo(x + w, y + h - r[2]);
this.quadraticCurveTo(x + w, y + h, x + w - r[2], y + h);
this.lineTo(x + r[3], y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r[3]);
this.lineTo(x, y + r[0]);
this.quadraticCurveTo(x, y, x + r[0], y);
this.closePath();
return this;
};
}
// ----- Initialize -----
function init() {
createBricks();
paddle.x = CANVAS_W / 2 - PADDLE_WIDTH_NORMAL / 2;
paddle.width = PADDLE_WIDTH_NORMAL;
paddle.y = PADDLE_Y;
spawnBallOnPaddle();
gameState = 'demo';
gameOverDisplayTimer = 0;
victoryDisplayTimer = 0;
needRestart = false;
frameCount = 0;
gameLoop();
}
init();
})();
</script>
</body>
</html>Reasoning trace (6,683 tokens)
The user wants a complete Breakout game in a single HTML file. Let me plan out all the features:
1. Canvas-based game
2. Paddle controlled by mouse and arrow keys
3. Bounce angle depends on where ball hits paddle
4. At least 6 rows of bricks with normal (1 hit) and tough (2 hits) bricks
5. Power-up drops: wider paddle (10s) and multi-ball (3 balls)
6. No ball passing through bricks or paddle - need proper collision detection
7. Particles when bricks break
8. 3 lives, score display
9. Game over and victory screens with restart
10. Self-playing demo mode with "Click or press a key to start" message
Let me build this carefully. I need to make sure:
- The ball never passes through bricks or paddle (continuous collision detection or careful step-based movement)
- The demo AI plays automatically
- Everything is visible in the first few seconds
Let me structure the code:
- HTML structure with canvas
- CSS for styling
- JavaScript game engine
For collision detection to prevent passing through:
- Move the ball in small steps (sub-steps) each frame
- Or calculate exact collision time
I'll use sub-stepping for the ball movement to ensure no tunneling through bricks or paddle.
For the demo mode:
- Simple AI that tracks the ball's x position with the paddle
- Shows "Click or press a key to start" overlay
Let me write the complete code now.
Key design decisions:
- Canvas size: I'll use a responsive canvas that fits the viewport, around 800x600 for the game area
- Brick layout: ~10 columns, 7 rows (mixing normal and tough bricks)
- Power-up bricks: certain bricks have power-up icons
- Colors for bricks: various colors per row
- Ball speed increases slightly as bricks are destroyed
Let me think about the brick layout:
- 7 rows × 10 columns = 70 bricks
- Row colors from top to bottom: red, orange, yellow, green, blue, purple, pink
- Some bricks are "tough" (need 2 hits) - maybe every 4th brick or so
- Power-up bricks: maybe 6-8 bricks randomly carry power-up icons
For the power-up icons, I'll draw them on the bricks before they're broken:
- "W" for wider paddle (blue)
- "M" for multi-ball (green)
When a power-up brick is broken, the power-up falls down. If caught by paddle:
- Wider paddle: paddle width increases for 10 seconds (with visual timer)
- Multi-ball: spawn 2 additional balls (total 3)
Particles: when a brick breaks, spawn colored particles that fly out and fade.
Ball collision with paddle:
- Calculate where the ball hits relative to paddle center
- Offset ranges from -1 to 1
- Ball angle = offset * max_angle (e.g., 60 degrees from vertical)
- Ball speed is constant in magnitude
For brick collision:
- Need to detect which side of the brick the ball hits
- Reverse velocity accordingly
- For tough bricks, track hit count and show cracks
Let me also handle the multi-ball case - all balls need to be tracked, and the game is over when all balls are lost.
Game states:
- "demo" - AI plays, showing "Click or press a key to start"
- "playing" - user controls
- "gameover" - show game over screen
- "victory" - show victory screen
For restart: click button or press R key
Let me code this up now.
For the canvas, I'll make it responsive but keep a fixed game coordinate system. Let's use 800×600 as the game area and scale it.
Actually, I should think about the viewport requirements: 1280×800 and down to 375px. I'll make the canvas responsive with CSS but keep the internal resolution at maybe 800×600 or similar, scaled appropriately.
Let me use a canvas that's sized to fit the viewport with some padding, maintaining aspect ratio.
I'll structure the code with:
- Game constants
- Game state object
- Brick creation
- Ball class/object
- Paddle object
- Power-up drop objects
- Particle system
- Collision detection
- Rendering functions
- Game loop
- Input handling
- Demo AI
Let me be careful about sub-stepping for the ball movement. I'll move the ball in steps no larger than a few pixels to ensure no tunneling.
Actually, for the bricks, I can use a simpler approach: since bricks are arranged in a grid, I can check for AABB collision and determine the side of collision based on the ball's position relative to the brick. For preventing tunneling, I'll move the ball in sub-steps.
Let me write the full code now. I'll be thorough.
I need to think about the power-up system more carefully:
- When a brick that has a power-up is broken, spawn a falling power-up from the brick's position
- The power-up falls at a constant speed
- If the paddle catches it, apply the effect
- If it goes below the screen, it's gone
For multi-ball:
- When activated, keep the existing ball and add 2 more balls at the same position with slightly different angles
- All balls are tracked in an array
For wider paddle:
- Paddle width increases from ~100 to ~150 for 10 seconds
- Show a visual indicator (glow or different color)
- After 10 seconds, revert
For the score:
- Normal brick: 10 points
- Tough brick: 20 points (on final hit)
- Power-up catch: bonus points?
Let me also add a nice visual style with gradients, shadows, and rounded corners for the bricks.
Now for the demo AI: it should track the ball's x position smoothly and move the paddle toward it. It can also use some smoothing to look natural.
Let me also consider: should the demo AI also catch power-ups? That would be nice but complex. I'll have it track the ball, and if a power-up is falling nearby, it might catch it too. Actually, let me keep it simple - the demo AI tracks the ball, and if the ball is lost, it continues from there. The demo will show the game playing automatically.
Let me now write the complete code.
Actually, I want to make sure the game looks polished and professional. Let me add:
- A nice title/header area
- Score and lives display with good typography
- Background gradient
- Brick animations
- Ball trail effect
- Particle explosions
- Power-up drop animations
- Smooth transitions between states
Let me finalize the brick layout. I'll use:
- 10 columns, 7 rows
- Row 0: red (1 hit)
- Row 1: orange (1 hit)
- Row 2: yellow (1 hit)
- Row 3: green (2 hits - tough)
- Row 4: blue (2 hits - tough)
- Row 5: purple (1 hit)
- Row 6: pink (2 hits - tough)
Power-up bricks will be randomly placed - maybe 8 of them, with some "W" and some "M" icons.
Brick dimensions: I'll calculate based on canvas width and number of columns with some padding.
For the canvas, I'll use 780×560 as the game area (leaving room for HUD at top).
Actually, let me reconsider the layout. I'll use the full canvas height for the game, with HUD overlay at the top. The canvas could be 800×600, with the top 50px for HUD and the rest for gameplay.
Let me set:
- Canvas: 800×600 internal resolution
- HUD: top 50px (score on left, lives on right, power-up indicator center)
- Game area: from y=50 to y=600
- Bricks: start at y=80, with 10 columns × 7 rows
- Paddle: near bottom (y=550)
- Ball starts above paddle
Brick dimensions:
- Available width: 800 - 40 (padding) = 760
- 10 columns: each brick + gap = 76px
- Brick width: 68px, gap: 8px
- Brick height: 20px, vertical gap: 6px
- 7 rows starting at y=80 → total brick area height: 7*20 + 6*6 = 140+36 = 176
- Last row at y=80+176-20 = 236
Paddle:
- Width: 100 (normal) or 150 (wide)
- Height: 14px
- Y position: 550 (or 560)
Ball:
- Radius: 7
- Speed: starts at about 4-5 px/frame
This all fits well.
Let me now write the full code. I need to be careful about making everything work together smoothly.
One thing to note: for the multi-ball, when the ball goes below the screen, I need to handle it. If there are multiple balls and one goes below, that ball is removed. When all balls are gone, lose a life.
For lives: 3 lives. When all balls are lost, decrement life and respawn ball(s). If lives reach 0, game over.
Let me also make sure the game over screen has a clear restart button.
For the victory condition: all bricks destroyed. Show victory screen with final score.
Now, one more thing - the "demo mode" for judging. The instructions say:
"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."
So I need:
1. The game starts in demo mode with AI controlling the paddle
2. An overlay says "Click or press a key to start"
3. When user clicks or presses a key, the game transitions to player control
The demo should show the game playing - bricks being broken, particles, power-ups dropping, etc.
OK, I think I have a clear plan. Let me write the code now.
I also need to handle the "ball must never pass through bricks or the paddle" requirement carefully. I'll use sub-stepping where the ball moves in increments no larger than the ball's radius, checking for collisions at each step.
Actually, for bricks specifically, since the ball is small (radius 7) and bricks are 68×20, I need to be careful. Let me use a max sub-step size of about 4-5 pixels.
For the paddle, the paddle is only 14px tall, so the ball could potentially pass through if moving fast. I'll check paddle collision specifically.
For the ball-paddle collision, I'll do it after moving the ball, checking if the ball overlaps with the paddle rectangle, and if so, resolve it by placing the ball above the paddle and computing the bounce.
Actually, a cleaner approach: move the ball in sub-steps, and after each sub-step, check for collisions with the paddle and bricks. For the paddle, check if the ball's bottom edge crossed the paddle's top edge and the ball is within the paddle's horizontal range.
Let me simplify: in the game loop, move the ball in sub-steps. For each sub-step:
1. Move ball by sub-step distance
2. Check wall collisions (left, right, top)
3. Check paddle collision (ball moving down, crossed paddle top)
4. Check brick collisions (AABB overlap with each brick)
5. Check bottom wall (ball lost)
This way the ball won't tunnel through anything.
For the ball-paddle collision, I need to detect when the ball crosses the paddle's top edge while moving downward. The paddle is a rectangle: paddleX to paddleX + paddleWidth, paddleY to paddleY + paddleHeight. The ball should bounce when:
- Ball's bottom (ball.y + ball.radius) >= paddleY
- Ball's previous bottom < paddleY (was above)
- Ball's x is within paddleX - ball.radius to paddleX + paddleWidth + ball.radius
- Ball is moving downward
Then compute the offset and set the new velocity.
For bricks, after moving the ball in a sub-step, check if the ball overlaps with any brick. If so, resolve.
Actually, a more precise way for bricks is to check if the ball's path intersects the brick. Since we're using sub-steps, we can check if the ball's center is within the brick (expanded by ball radius) and handle accordingly.
Let me use circle-rectangle collision for bricks. For each brick, check if the ball's circle overlaps the brick's rectangle. If so, determine the collision normal based on the overlap.
For simplicity and robustness, I'll:
1. Find all bricks that the ball overlaps with
2. For the closest brick, determine which side to bounce off
3. Reverse the appropriate velocity component
To determine which side, I'll check which side of the brick the ball center is closest to, or use the overlap amounts.
Actually, let me use a simpler approach that works well for breakout:
- Compute the ball's position in the sub-step
- Check for overlap with each brick
- If overlapping, check the ball's velocity direction to determine which side it hit
- Reverse the appropriate component
A common approach is to check the horizontal and vertical distances from the ball center to the brick center, and use the axis with the smallest overlap.
Let me think more carefully. For circle-rectangle collision detection:
- Find closest point on rectangle to circle center
- If distance < radius, there's a collision
- The collision normal is from closest point to circle center
For resolution:
- If |normal.x| > |normal.y|, it's a horizontal collision (bounce X)
- Otherwise, vertical collision (bounce Y)
This should work well.
Let me now write the complete game code. I'll aim for clean, well-organized code.
Actually, one more consideration: the power-up bricks need to "visibly carry a power-up icon". I'll draw a small icon (W or M) on those bricks. When the brick is broken (tough bricks need 2 hits), the power-up drops.
For tough bricks: after 1 hit, they show cracks (lines drawn on them). After 2 hits, they break.
Let me code this all up now.
I also want to add a nice ball trail effect - drawing semi-transparent circles behind the ball.
And for particles - when a brick breaks, spawn 10-20 small colored particles that fly out in random directions and fade.
Let me also add a subtle glow to the ball and power-ups.
For the score, I'll show it prominently in the HUD.
OK, let me write the complete HTML file now.
For canvas sizing for responsiveness - I'll use CSS to make the canvas fit the viewport while maintaining aspect ratio. The internal resolution will be 800×600.
Actually, to keep things simpler and ensure crisp rendering, I'll make the canvas 800×600 and use CSS to scale it. I'll set the canvas's style width and height based on the container.
Let me also think about how to handle the canvas scaling for the 375px viewport. The canvas should scale down proportionally, maintaining the 4:3 aspect ratio (800:600).
For the CSS:
```css
canvas {
max-width: 100%;
max-height: 100vh;
aspect-ratio: 4/3;
}
```
Or I could use a container with a max-width and center it.
Actually, let me use a slightly different approach. I'll have the canvas be 800×600 internally and scale it with CSS to fit the viewport. I'll set:
- The canvas element's width and height attributes to 800 and 600
- CSS to scale it down for smaller viewports
For the overlay message (click to start), I'll position it absolutely over the canvas.
Let me finalize the design and write the complete code.
I realize I should also consider what happens when the game is in demo mode and a ball is lost. The demo should continue - just respawn the ball and continue playing. Same for lives - the demo can lose lives and respawn, showing the full game loop.
In demo mode:
- Game plays with AI controlling paddle
- Power-ups can be caught by AI
- Bricks break, particles spawn, score increases
- If ball is lost, respawn (and possibly show lives decrement)
- If all lives lost, reset to demo mode (or show game over briefly then reset)
- Actually, for the demo, it's better to just auto-restart if game over or victory happens
Let me handle it this way: in demo mode, if game over or victory, show the appropriate screen briefly and then auto-restart after a few seconds. But since we want the demo to show gameplay, I'll make the demo just run indefinitely, resetting when needed.
Actually, for simplicity in demo mode:
- If all bricks are destroyed (victory), reset the level
- If all lives are lost (game over), reset the level
- In both cases, continue in demo mode
When the player starts playing (click/keypress), the state transitions to "playing" mode, and then game over/victory shows the appropriate screen with a restart button.
Now, I also need to handle the pointer events on the canvas for the player's mouse control. The mouse should control the paddle's x position when in "playing" mode.
For keyboard: left/right arrow keys move the paddle.
Let me also display the power-up timer for the wide paddle (showing remaining time).
OK, I have a comprehensive plan. Let me write the code now. I'll aim for about 500-700 lines of well-structured JavaScript.
One more thing: the ball's starting position and angle. I'll start the ball on the paddle center, and launch it at a random angle (between -30° and +30° from vertical, or maybe a wider range but not too horizontal).
Let me write the code. I'll be very careful about the collision detection to ensure the ball never passes through things.
For the sub-stepping:
- Ball speed is around 4-6 pixels per frame at 60fps
- I'll use sub-steps of max 3-4 pixels to avoid tunneling
- For the paddle, since it's 14px tall, sub-steps of 3-4px should be fine
- For bricks, since they're 20px tall, same sub-step size is fine
Actually, let me calculate: at 60fps, a speed of 5px/frame = 300px/second. Sub-stepping at 2.5px gives about 2-3 sub-steps per frame, which is manageable.
I'll set max sub-step to 3 pixels.
Let me start writing the code now. I'll structure it clearly.
Here's my plan for the code structure:
```
1. Constants and configuration
2. Game state variables
3. Brick class/creation
4. Ball class
5. Paddle class
6. Power-up drops
7. Particles
8. Input handling
9. Game logic (update functions)
10. Rendering functions
11. Demo AI
12. Game loop
13. Initialization
```
I'll use plain objects/functions rather than classes for simplicity, or I could use classes for clarity. Let me use a mix - objects for game entities and functions for logic.
Actually, let me just use a clean functional approach with game state stored in an object.
Time to write the code. Let me make it thorough and polished.
For the tough bricks with cracks: after the first hit, I'll draw some crack lines on the brick. I'll pre-define a few crack patterns or just draw 2-3 lines randomly when hit.
For power-up icons on bricks: I'll draw a small "W" or "M" in a circle on the brick.
Let me now code everything.
Alright, I'll also need to think about the power-up falling speed. Maybe 2-3 pixels per frame. The paddle needs to catch it.
Paddle Y position: let me set it at y=555 (near the bottom of the 600px canvas). Paddle top is at 555, bottom at 555+14=569. The bottom of the canvas is 600, so there's 31px below the paddle.
Ball starts at paddle center, y=548 (above paddle top).
For the power-ups to be catchable, they fall from the brick area (y~80-250) down to the paddle (y~555). That takes a while but works.
Let me also give the power-up drops a visual style - a falling capsule/rounded rectangle with the icon.
One final thing: when the ball hits the bottom (goes below 600), it's lost. If there are multiple balls, that ball is removed. When no balls remain, a life is lost.
OK, writing the code now. I'll be comprehensive.
I should also consider the edge case where the ball hits the paddle at a very steep angle. The spec says "center = straight up, edges = sharp angle", so:
- offset = (ball.x - paddle.center_x) / (paddle.width / 2)
- offset ranges from -1 to 1
- angle from vertical = offset * max_angle (e.g., 60°)
- Ball speed remains constant
For the ball velocity:
- speed = constant (e.g., 5.5)
- angle from vertical = offset * 60° (in radians)
- vx = speed * sin(angle) = speed * sin(offset * 60°)
- vy = -speed * cos(angle) = -speed * cos(offset * 60°)
This gives straight up at center and sharp angles at edges.
For the angle, offset=±1 gives ±60° from vertical, which is already pretty sharp. Maybe use 65° for max edge angle.
Alright, writing the code now for real.
I need to also handle the multi-ball properly. When multi-ball activates:
- Keep existing ball(s)
- Spawn 2 more balls at the same position as the ball that triggered the catch
- Give them slightly different angles
The total should be 3 balls (or more if there were already multiple balls).
Actually, "multi-ball (3 balls)" means 3 balls total. If there's already 1 ball, add 2 more. If somehow there are already 2, add 1 more. But in normal gameplay, there will be 1 ball, so we add 2 to get 3.
For the spawn angles, I'll give them different angles from the original ball.
Now, when tracking multiple balls, I need to update each ball's position, check collisions for each ball, etc. This adds some complexity but is manageable.
Let me also handle the edge case where a ball goes downward past the paddle but the paddle catches it on the side. I'll only bounce the ball when it hits the top surface of the paddle.
OK, I really need to write the code now. Let me draft it completely.
I also want to make the game feel polished with:
- Smooth paddle movement (lerp for mouse, direct for keyboard)
- Ball trail effect
- Screen shake on brick break (subtle)
- Nice color scheme
- Animated brick destruction (scale out + particles)
For the canvas scaling on smaller screens, I'll use CSS to keep it responsive.
Let me write everything now. I'll target about 600-800 lines.
Actually, one more important thing - I need to handle the case where the ball gets stuck bouncing horizontally. I'll add a minimum vertical velocity component to prevent the ball from getting stuck in a horizontal loop. Also, I'll slightly adjust the ball's angle if it becomes too horizontal.
Also, the ball should speed up slightly as the game progresses (after breaking many bricks) to keep the game exciting. I'll add a small speed increase proportional to bricks destroyed.
Let me finalize:
- Initial ball speed: 5.0
- Max speed: 7.5
- Speed increase: 0.02 per brick destroyed (so after 50 bricks, speed increases by 1.0)
Actually, let me keep it simpler. Speed stays at base + small incremental. I'll just cap the speed.
OK, writing code now. No more planning.
Hmm, let me also think carefully about which bricks have power-ups. I want the demo to show power-up drops, so I need several power-up bricks. Let me place them deterministically:
- Columns 2, 5, 8 in row 1: "W" power-ups
- Columns 3, 6, 9 in row 3: "M" power-ups
- Columns 4, 7 in row 5: alternates
Or I could randomize them. For the demo to reliably show power-ups, deterministic placement is better. Let me place:
- Row 1: W at columns 2 and 7
- Row 2: M at columns 3 and 8
- Row 3: W at column 5
- Row 4: M at columns 2 and 6
- Row 5: W at columns 3 and 8
- Row 6: M at column 5
That's 11 power-up bricks spread across rows. Maybe that's too many. Let me use about 6-8:
- Row 1: W at col 2, M at col 7
- Row 2: W at col 5
- Row 3: M at col 3, W at col 8
- Row 5: M at col 5
- Row 6: W at col 2
That's 7 power-up bricks. Reasonable.
OK NOW writing the code for real.
One more thing about the demo mode - the AI needs to actually play the game. It should:
1. Track the ball's x position
2. Move the paddle toward the ball (with some speed limit)
3. Optionally try to catch power-ups too
For a good demo, the AI should be competent but not perfect. It should be able to break many bricks and show all the game features.
The AI will:
- Track the ball when it's falling (moving downward)
- Position the paddle under the ball
- For power-ups, if a power-up is falling near the paddle's position, try to catch it (this might be complex, but I can have the AI prioritize based on distance)
Actually, for simplification, the AI will just track the ball. Power-ups will be caught naturally if they fall near where the AI positions the paddle.
Let me have the AI add some human-like lag: instead of moving directly to the ball's x, it moves toward it with a max speed and some small random offset.
OK, truly writing the code now.
For the restart mechanism on game over/victory: a button click or pressing R or Space.
I also need to display the score, lives, and current power-up status. I'll render these on the canvas using the 2D context (not HTML overlays) for simplicity and integration with the game rendering.
For the "Click or press a key to start" message, I'll use an HTML overlay div positioned over the canvas. This is cleaner than canvas-rendered text for this purpose.
Actually, I need to think about the demo mode more carefully. The instructions say:
"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 demo starts immediately with AI playing, and there's an overlay message. When the user clicks or presses a key, the overlay disappears and the user takes control.
But wait - in demo mode, the game needs to actually be playing. The AI controls the paddle. Bricks are broken, score increases, particles show, power-ups drop and are caught. This should all be visible.
When the user clicks/presses a key:
- The game transitions to "playing" mode
- The overlay disappears
- The user controls the paddle
- The game continues from where the demo left off (or resets? I think it should continue, or maybe reset to give the user a fresh start)
Actually, I think resetting to a fresh state when the user takes over is cleaner. The demo shows the game in action, and when the user wants to play, they start fresh. Let me do this:
- Demo mode: AI plays, overlay shows
- User clicks/presses key: transition to playing mode, reset the game to initial state (fresh bricks, 3 lives, 0 score), overlay hidden, user controls paddle
This way the demo doesn't leave the game in a weird state for the user.
Actually, hmm, resetting means the demo's progress is lost. But for the judging screenshot at 3 seconds, the demo will be visible behind the overlay message. That's the key point.
Let me go with: demo mode plays the game with AI. When user clicks/presses key, reset and switch to player mode.
Let me finalize the structure and write the code.
For the particle system, I'll use a simple array of particle objects with position, velocity, color, size, and life. Each frame, update position and decrement life. When life <= 0, remove from array.
Particles on brick break:
- 15-20 particles per brick
- Random velocities in all directions
- Color matched to the brick's color
- Size 2-4px
- Life 30-60 frames
- Fade out
OK, I'm going to write the entire code now. I'll be careful and thorough.
I realize I should also handle the case where the mouse enters/leaves the canvas. The paddle should follow the mouse only when the mouse is over the canvas (or I can use the entire window). I'll use canvas mouse events for this.
For keyboard, I'll track left/right arrow key states and move the paddle accordingly in the update loop.
For the paddle, when using keyboard, the paddle moves at a constant speed. When using mouse, it follows the mouse's x position.
In demo mode, the AI controls the paddle (overriding any user input).
OK, writing the actual code now. This response is getting long but I want to make sure everything is thought through.
Let me draft the complete HTML file:
```html
<!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>
/* CSS here */
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="startOverlay" class="start-overlay">
<div class="start-message">
<h2>BREAKOUT</h2>
<p>Click or press any key to start</p>
</div>
</div>
</div>
<script>
// JavaScript here
</script>
</body>
</html>
```
This is the basic structure. Now for the detailed implementation...
Let me carefully write the full JavaScript. I'll organize it into sections.
Actually, I think I should just write it all out now in one go. It'll be a long code block but that's what's needed.