Nemotron 3 Ultra (free)
by NVIDIAReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 20 s
- Cost
- $0
- Output tokens
- 9,213
- Spent thinking
- 325 tokens
- Speed
- 193.2 tok/s
- Console errors
- 0
- First token after
- 1 min 32 s
- First visible text after
- 1 min 40 s
- Prompt tokens
- 531
- Code size
- 26.3 KB
- Finish reason
- stop
- Served by
- Nvidia
- Model id
nvidia/nemotron-3-ultra-550b-a55b:free- Reasoning sent
{"effort":"high"}- Max output
- 32,000 tokens
- Generated
- Nov 9, 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 · 852 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #e0e0e0;
overflow: hidden;
}
#gameContainer {
position: relative;
width: 100%;
max-width: 800px;
aspect-ratio: 5 / 4;
}
canvas {
display: block;
width: 100%;
height: 100%;
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.1);
background: #0a0a15;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 8px;
background: rgba(10, 10, 21, 0.95);
backdrop-filter: blur(8px);
padding: 2rem;
text-align: center;
z-index: 10;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.overlay.hidden { opacity: 0; visibility: hidden; pointer-events: none; }
.overlay h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 700;
background: linear-gradient(135deg, #ffd700, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: 2px;
}
.overlay p { font-size: clamp(1rem, 2.5vw, 1.2rem); opacity: 0.8; margin-bottom: 1.5rem; }
.btn {
padding: 0.875rem 2.5rem;
font-size: 1.1rem;
font-weight: 600;
border: none;
border-radius: 50px;
cursor: pointer;
background: linear-gradient(135deg, #ff6b35, #f7931e);
color: #fff;
box-shadow: 0 4px 20px rgba(255, 107, 53, 0.4);
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 6px 30px rgba(255, 107, 53, 0.6); }
.btn:active { transform: translateY(0); }
.stats {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 800px;
margin-top: 1rem;
padding: 0 0.5rem;
font-size: clamp(0.85rem, 2vw, 1rem);
}
.stat { display: flex; align-items: center; gap: 0.5rem; }
.stat-label { opacity: 0.6; }
.stat-value { font-weight: 700; color: #ffd700; }
.lives-display { display: flex; gap: 0.35rem; }
.life { width: 18px; height: 18px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, #ff6b35, #c0392b); box-shadow: 0 0 10px #ff6b35; }
.life.lost { background: #333; box-shadow: none; opacity: 0.3; }
@media (max-width: 400px) { .stats { flex-wrap: wrap; gap: 0.5rem; justify-content: center; } }
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="canvas"></canvas>
<div id="startOverlay" class="overlay">
<h1>BREAKOUT</h1>
<p>Click or press any key to start</p>
<button class="btn" id="startBtn">Start Game</button>
</div>
<div id="gameOverOverlay" class="overlay hidden">
<h1>GAME OVER</h1>
<p id="finalScore">Score: 0</p>
<button class="btn" id="restartBtn">Play Again</button>
</div>
<div id="victoryOverlay" class="overlay hidden">
<h1>VICTORY!</h1>
<p id="victoryScore">Score: 0</p>
<button class="btn" id="victoryRestartBtn">Play Again</button>
</div>
</div>
<div class="stats">
<div class="stat"><span class="stat-label">Score:</span><span class="stat-value" id="scoreDisplay">0</span></div>
<div class="stat"><span class="stat-label">Lives:</span><div class="lives-display" id="livesDisplay"></div></div>
<div class="stat"><span class="stat-label">Level:</span><span class="stat-value" id="levelDisplay">1</span></div>
</div>
<script>
// ===== CONFIG =====
const CONFIG = {
canvasWidth: 800,
canvasHeight: 640,
paddleHeight: 16,
paddleWidth: 100,
paddleSpeed: 8,
ballRadius: 8,
ballSpeed: 5,
brickRows: 6,
brickCols: 10,
brickPadding: 4,
brickOffsetTop: 60,
brickOffsetLeft: 30,
lives: 3,
powerUpDropChance: 0.25,
powerUpFallSpeed: 2,
powerUpDuration: 10000,
particleCount: 12,
demoMode: true
};
// ===== GAME STATE =====
const state = {
running: false,
demoMode: true,
score: 0,
lives: CONFIG.lives,
level: 1,
paddleX: 0,
paddleWidth: CONFIG.paddleWidth,
paddleTargetX: 0,
balls: [],
bricks: [],
powerUps: [],
particles: [],
powerUpTimers: { widePaddle: 0, multiBall: 0 },
keys: { left: false, right: false },
mouseX: 0
};
// ===== CANVAS SETUP =====
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
const container = document.getElementById('gameContainer');
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
ctx.scale(dpr, dpr);
state.paddleX = (CONFIG.canvasWidth - state.paddleWidth) / 2;
state.paddleTargetX = state.paddleX;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// ===== UTILITIES =====
const rand = (min, max) => Math.random() * (max - min) + min;
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
function circleRectCollide(cx, cy, r, rx, ry, rw, rh) {
const closestX = clamp(cx, rx, rx + rw);
const closestY = clamp(cy, ry, ry + rh);
const dx = cx - closestX;
const dy = cy - closestY;
return dx * dx + dy * dy < r * r;
}
function rectRectCollide(x1, y1, w1, h1, x2, y2, w2, h2) {
return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
}
// ===== BRICK CREATION =====
function createBricks() {
state.bricks = [];
const brickWidth = (CONFIG.canvasWidth - CONFIG.brickOffsetLeft * 2 - CONFIG.brickPadding * (CONFIG.brickCols - 1)) / CONFIG.brickCols;
const brickHeight = 28;
const colors = [
{ base: '#e74c3c', light: '#ff6b5b', dark: '#c0392b' },
{ base: '#e67e22', light: '#ff9f43', dark: '#d35400' },
{ base: '#f39c12', light: '#f5b041', dark: '#d4ac0d' },
{ base: '#2ecc71', light: '#58d68d', dark: '#27ae60' },
{ base: '#3498db', light: '#5dade2', dark: '#2980b9' },
{ base: '#9b59b6', light: '#bb8fce', dark: '#8e44ad' }
];
for (let r = 0; r < CONFIG.brickRows; r++) {
for (let c = 0; c < CONFIG.brickCols; c++) {
const x = CONFIG.brickOffsetLeft + c * (brickWidth + CONFIG.brickPadding);
const y = CONFIG.brickOffsetTop + r * (brickHeight + CONFIG.brickPadding);
const colorSet = colors[r % colors.length];
const isTough = Math.random() < 0.35 && r > 1;
const hasPowerUp = !isTough && Math.random() < CONFIG.powerUpDropChance;
const powerUpType = hasPowerUp ? (Math.random() < 0.5 ? 'widePaddle' : 'multiBall') : null;
state.bricks.push({
x, y, width: brickWidth, height: brickHeight,
color: colorSet.base, lightColor: colorSet.light, darkColor: colorSet.dark,
hits: isTough ? 2 : 1, maxHits: isTough ? 2 : 1,
hasPowerUp, powerUpType, destroyed: false
});
}
}
}
// ===== BALL CREATION =====
function createBall(x, y, vx, vy) {
return {
x, y, vx, vy, radius: CONFIG.ballRadius,
trail: [], maxTrail: 8
};
}
function launchBall(x, y, angle = -Math.PI / 2, speed = CONFIG.ballSpeed) {
state.balls.push(createBall(x, y, Math.cos(angle) * speed, Math.sin(angle) * speed));
}
// ===== POWER-UPS =====
function spawnPowerUp(x, y, type) {
state.powerUps.push({ x, y, type, width: 24, height: 24, vy: CONFIG.powerUpFallSpeed, rotation: 0 });
}
function activatePowerUp(type) {
const now = Date.now();
if (type === 'widePaddle') {
state.powerUpTimers.widePaddle = now + CONFIG.powerUpDuration;
state.paddleWidth = CONFIG.paddleWidth * 1.6;
} else if (type === 'multiBall') {
state.powerUpTimers.multiBall = now + CONFIG.powerUpDuration;
const mainBall = state.balls[0];
if (mainBall) {
launchBall(mainBall.x, mainBall.y, -Math.PI / 2 - 0.5, CONFIG.ballSpeed);
launchBall(mainBall.x, mainBall.y, -Math.PI / 2 + 0.5, CONFIG.ballSpeed);
}
}
}
function updatePowerUpTimers() {
const now = Date.now();
if (state.powerUpTimers.widePaddle && now > state.powerUpTimers.widePaddle) {
state.paddleWidth = CONFIG.paddleWidth;
state.powerUpTimers.widePaddle = 0;
}
if (state.powerUpTimers.multiBall && now > state.powerUpTimers.multiBall) {
// Keep only first ball
if (state.balls.length > 1) state.balls.length = 1;
state.powerUpTimers.multiBall = 0;
}
}
// ===== PARTICLES =====
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
const angle = rand(0, Math.PI * 2);
const speed = rand(1, 5);
state.particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1, decay: rand(0.015, 0.03),
size: rand(2, 6),
color: color
});
}
}
function updateParticles() {
for (let i = state.particles.length - 1; i >= 0; i--) {
const p = state.particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.08; // gravity
p.life -= p.decay;
if (p.life <= 0) state.particles.splice(i, 1);
}
}
// ===== COLLISION HANDLING =====
function handleBallBrickCollision(ball) {
for (let i = state.bricks.length - 1; i >= 0; i--) {
const brick = state.bricks[i];
if (brick.destroyed) continue;
if (circleRectCollide(ball.x, ball.y, ball.radius, brick.x, brick.y, brick.width, brick.height)) {
// Determine collision side
const ballLeft = ball.x - ball.radius;
const ballRight = ball.x + ball.radius;
const ballTop = ball.y - ball.radius;
const ballBottom = ball.y + ball.radius;
const brickLeft = brick.x;
const brickRight = brick.x + brick.width;
const brickTop = brick.y;
const brickBottom = brick.y + brick.height;
const overlapLeft = ballRight - brickLeft;
const overlapRight = brickRight - ballLeft;
const overlapTop = ballBottom - brickTop;
const overlapBottom = brickBottom - ballTop;
const minOverlapX = Math.min(overlapLeft, overlapRight);
const minOverlapY = Math.min(overlapTop, overlapBottom);
if (minOverlapX < minOverlapY) {
// Horizontal collision
ball.vx = -ball.vx;
if (overlapLeft < overlapRight) ball.x = brickLeft - ball.radius;
else ball.x = brickRight + ball.radius;
} else {
// Vertical collision
ball.vy = -ball.vy;
if (overlapTop < overlapBottom) ball.y = brickTop - ball.radius;
else ball.y = brickBottom + ball.radius;
}
// Damage brick
brick.hits--;
if (brick.hits === 1 && brick.maxHits === 2) {
// Cracked state
spawnParticles(ball.x, ball.y, brick.color, 6);
} else if (brick.hits <= 0) {
brick.destroyed = true;
state.score += brick.maxHits === 2 ? 20 : 10;
spawnParticles(brick.x + brick.width/2, brick.y + brick.height/2, brick.color, CONFIG.particleCount);
if (brick.hasPowerUp) spawnPowerUp(brick.x + brick.width/2, brick.y + brick.height/2, brick.powerUpType);
}
updateScoreDisplay();
return true;
}
}
return false;
}
function handleBallPaddleCollision(ball) {
const paddleY = CONFIG.canvasHeight - CONFIG.paddleHeight - 20;
const paddleTop = paddleY;
const paddleBottom = paddleY + CONFIG.paddleHeight;
const paddleLeft = state.paddleX;
const paddleRight = state.paddleX + state.paddleWidth;
if (ball.y + ball.radius >= paddleTop && ball.y - ball.radius <= paddleBottom &&
ball.x + ball.radius >= paddleLeft && ball.x - ball.radius <= paddleRight &&
ball.vy > 0) {
// Calculate hit position (-1 to 1)
const hitPos = (ball.x - (state.paddleX + state.paddleWidth / 2)) / (state.paddleWidth / 2);
const clampedHitPos = clamp(hitPos, -1, 1);
// Angle ranges from -60° to 60° from vertical
const maxAngle = Math.PI / 3; // 60 degrees
const angle = clampedHitPos * maxAngle;
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.cos(angle) * speed;
// Ensure ball is above paddle
ball.y = paddleTop - ball.radius;
// Add slight speed increase over time
const newSpeed = Math.min(speed * 1.005, CONFIG.ballSpeed * 1.8);
const currentAngle = Math.atan2(ball.vy, ball.vx);
ball.vx = Math.cos(currentAngle) * newSpeed;
ball.vy = Math.sin(currentAngle) * newSpeed;
return true;
}
return false;
}
function handleWallCollision(ball) {
if (ball.x - ball.radius <= 0) {
ball.x = ball.radius;
ball.vx = Math.abs(ball.vx);
} else if (ball.x + ball.radius >= CONFIG.canvasWidth) {
ball.x = CONFIG.canvasWidth - ball.radius;
ball.vx = -Math.abs(ball.vx);
}
if (ball.y - ball.radius <= 0) {
ball.y = ball.radius;
ball.vy = Math.abs(ball.vy);
}
}
// ===== DEMO AI =====
function updateDemoAI() {
if (!state.demoMode || state.balls.length === 0) return;
// Track the lowest ball
let targetBall = state.balls[0];
for (const ball of state.balls) {
if (ball.y > targetBall.y) targetBall = ball;
}
// Predict where ball will hit paddle level
const paddleY = CONFIG.canvasHeight - CONFIG.paddleHeight - 20;
if (targetBall.vy > 0) {
const timeToPaddle = (paddleY - targetBall.y) / targetBall.vy;
let predictedX = targetBall.x + targetBall.vx * timeToPaddle;
// Account for wall bounces
while (predictedX < 0 || predictedX > CONFIG.canvasWidth) {
if (predictedX < 0) predictedX = -predictedX;
if (predictedX > CONFIG.canvasWidth) predictedX = 2 * CONFIG.canvasWidth - predictedX;
}
state.paddleTargetX = predictedX - state.paddleWidth / 2;
} else {
state.paddleTargetX = CONFIG.canvasWidth / 2 - state.paddleWidth / 2;
}
state.paddleTargetX = clamp(state.paddleTargetX, 0, CONFIG.canvasWidth - state.paddleWidth);
}
// ===== UPDATE =====
function update() {
if (!state.running) return;
updateDemoAI();
// Paddle movement (smooth follow for demo, direct for player)
if (state.demoMode) {
state.paddleX += (state.paddleTargetX - state.paddleX) * 0.15;
} else {
if (state.keys.left) state.paddleX -= CONFIG.paddleSpeed;
if (state.keys.right) state.paddleX += CONFIG.paddleSpeed;
state.paddleX = clamp(state.paddleX, 0, CONFIG.canvasWidth - state.paddleWidth);
}
// Update balls
for (let i = state.balls.length - 1; i >= 0; i--) {
const ball = state.balls[i];
// Trail
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > ball.maxTrail) ball.trail.shift();
ball.x += ball.vx;
ball.y += ball.vy;
handleWallCollision(ball);
handleBallBrickCollision(ball);
handleBallPaddleCollision(ball);
// Ball lost
if (ball.y - ball.radius > CONFIG.canvasHeight) {
state.balls.splice(i, 1);
}
}
// All balls lost
if (state.balls.length === 0) {
state.lives--;
updateLivesDisplay();
if (state.lives <= 0) {
gameOver();
} else {
// Reset ball after short delay
setTimeout(() => {
if (state.running) {
launchBall(CONFIG.canvasWidth / 2, CONFIG.canvasHeight - CONFIG.paddleHeight - 40);
}
}, 1000);
}
}
// Power-ups
for (let i = state.powerUps.length - 1; i >= 0; i--) {
const pu = state.powerUps[i];
pu.y += pu.vy;
pu.rotation += 0.03;
// Check paddle collision
const paddleY = CONFIG.canvasHeight - CONFIG.paddleHeight - 20;
if (pu.y + pu.height >= paddleY && pu.y <= paddleY + CONFIG.paddleHeight &&
pu.x + pu.width >= state.paddleX && pu.x <= state.paddleX + state.paddleWidth) {
activatePowerUp(pu.type);
state.powerUps.splice(i, 1);
} else if (pu.y > CONFIG.canvasHeight) {
state.powerUps.splice(i, 1);
}
}
updatePowerUpTimers();
updateParticles();
// Victory check
if (state.bricks.every(b => b.destroyed)) {
victory();
}
}
// ===== RENDER =====
function drawPaddle() {
const y = CONFIG.canvasHeight - CONFIG.paddleHeight - 20;
const x = state.paddleX;
const w = state.paddleWidth;
const h = CONFIG.paddleHeight;
const r = 8;
// Glow
const gradient = ctx.createLinearGradient(x, y, x, y + h);
gradient.addColorStop(0, '#ff6b35');
gradient.addColorStop(0.5, '#ff9f43');
gradient.addColorStop(1, '#e67e22');
ctx.shadowColor = '#ff6b35';
ctx.shadowBlur = 15;
ctx.fillStyle = gradient;
roundRect(ctx, x, y, w, h, r);
ctx.fill();
ctx.shadowBlur = 0;
// Highlight
ctx.fillStyle = 'rgba(255,255,255,0.2)';
roundRect(ctx, x + 2, y + 2, w - 4, h / 2, r);
ctx.fill();
}
function drawBall(ball) {
// Trail
for (let i = 0; i < ball.trail.length; i++) {
const t = ball.trail[i];
const alpha = (i / ball.trail.length) * 0.4;
const size = ball.radius * (i / ball.trail.length);
ctx.globalAlpha = alpha;
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(t.x, t.y, size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Ball glow
ctx.shadowColor = '#ffd700';
ctx.shadowBlur = 12;
// Ball gradient
const grad = ctx.createRadialGradient(
ball.x - ball.radius/3, ball.y - ball.radius/3, 0,
ball.x, ball.y, ball.radius
);
grad.addColorStop(0, '#fff8dc');
grad.addColorStop(0.5, '#ffd700');
grad.addColorStop(1, '#e6c200');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
function drawBrick(brick) {
if (brick.destroyed) return;
const { x, y, width, height, color, lightColor, darkColor, hits, maxHits, hasPowerUp, powerUpType } = brick;
const r = 4;
// Base
const grad = ctx.createLinearGradient(x, y, x, y + height);
grad.addColorStop(0, lightColor);
grad.addColorStop(0.5, color);
grad.addColorStop(1, darkColor);
ctx.fillStyle = grad;
roundRect(ctx, x, y, width, height, r);
ctx.fill();
// Crack effect for 2-hit bricks after first hit
if (maxHits === 2 && hits === 1) {
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.beginPath();
// Draw a few crack lines
const cx = x + width/2, cy = y + height/2;
for (let i = 0; i < 3; i++) {
const angle = rand(0, Math.PI * 2);
const len = rand(width * 0.2, width * 0.4);
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(angle) * len, cy + Math.sin(angle) * len * 0.5);
}
ctx.stroke();
}
// Highlight
ctx.fillStyle = 'rgba(255,255,255,0.15)';
roundRect(ctx, x + 2, y + 2, width - 4, height / 2, r);
ctx.fill();
// Power-up indicator
if (hasPowerUp) {
ctx.save();
ctx.translate(x + width/2, y + height/2);
ctx.rotate(Date.now() * 0.002);
ctx.fillStyle = powerUpType === 'widePaddle' ? '#2ecc71' : '#9b59b6';
ctx.shadowColor = ctx.fillStyle;
ctx.shadowBlur = 8;
// Icon shape
if (powerUpType === 'widePaddle') {
// Double arrow
ctx.beginPath();
ctx.moveTo(-8, 0); ctx.lineTo(0, -6); ctx.lineTo(0, -2); ctx.lineTo(8, -2); ctx.lineTo(8, 2); ctx.lineTo(0, 2); ctx.lineTo(0, 6); ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(8, 0); ctx.lineTo(0, -6); ctx.lineTo(0, -2); ctx.lineTo(-8, -2); ctx.lineTo(-8, 2); ctx.lineTo(0, 2); ctx.lineTo(0, 6); ctx.closePath();
ctx.fill();
} else {
// Three dots
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.arc(i * 6, 0, 3, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
}
function drawPowerUps() {
for (const pu of state.powerUps) {
ctx.save();
ctx.translate(pu.x + pu.width/2, pu.y + pu.height/2);
ctx.rotate(pu.rotation);
const grad = ctx.createRadialGradient(0, 0, 0, 0, 0, pu.width/2);
const color = pu.type === 'widePaddle' ? '#2ecc71' : '#9b59b6';
grad.addColorStop(0, color);
grad.addColorStop(1, pu.type === 'widePaddle' ? '#27ae60' : '#8e44ad');
ctx.fillStyle = grad;
ctx.shadowColor = color;
ctx.shadowBlur = 10;
roundRect(ctx, -pu.width/2, -pu.height/2, pu.width, pu.height, 6);
ctx.fill();
ctx.shadowBlur = 0;
// Icon
ctx.fillStyle = 'rgba(255,255,255,0.9)';
if (pu.type === 'widePaddle') {
ctx.fillRect(-7, -2, 14, 4);
ctx.fillRect(-5, -5, 2, 10);
ctx.fillRect(3, -5, 2, 10);
} else {
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.arc(i * 5, 0, 2.5, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
}
function drawParticles() {
for (const p of state.particles) {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawBackground() {
// Subtle grid pattern
ctx.strokeStyle = 'rgba(255,255,255,0.02)';
ctx.lineWidth = 1;
for (let x = 0; x <= CONFIG.canvasWidth; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, CONFIG.canvasHeight); ctx.stroke();
}
for (let y = 0; y <= CONFIG.canvasHeight; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(CONFIG.canvasWidth, y); ctx.stroke();
}
}
function render() {
// Clear
ctx.fillStyle = '#0a0a15';
ctx.fillRect(0, 0, CONFIG.canvasWidth, CONFIG.canvasHeight);
drawBackground();
drawParticles();
for (const brick of state.bricks) drawBrick(brick);
drawPowerUps();
drawPaddle();
for (const ball of state.balls) drawBall(ball);
// Power-up indicators
if (state.powerUpTimers.widePaddle) drawPowerUpIndicator('widePaddle', state.powerUpTimers.widePaddle);
if (state.powerUpTimers.multiBall) drawPowerUpIndicator('multiBall', state.powerUpTimers.multiBall);
}
function drawPowerUpIndicator(type, endTime) {
const remaining = (endTime - Date.now()) / CONFIG.powerUpDuration;
if (remaining <= 0) return;
const x = type === 'widePaddle' ? 20 : 20;
const y = type === 'widePaddle' ? 20 : 50;
ctx.fillStyle = type === 'widePaddle' ? '#2ecc71' : '#9b59b6';
ctx.globalAlpha = 0.7 + 0.3 * Math.sin(Date.now() * 0.005);
roundRect(ctx, x, y, 120 * remaining, 8, 4);
ctx.fill();
ctx.globalAlpha = 1;
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// ===== UI UPDATES =====
function updateScoreDisplay() {
document.getElementById('scoreDisplay').textContent = state.score;
}
function updateLivesDisplay() {
const container = document.getElementById('livesDisplay');
container.innerHTML = '';
for (let i = 0; i < CONFIG.lives; i++) {
const div = document.createElement('div');
div.className = 'life' + (i >= state.lives ? ' lost' : '');
container.appendChild(div);
}
}
// ===== GAME FLOW =====
function startGame() {
state.running = true;
state.demoMode = false;
state.score = 0;
state.lives = CONFIG.lives;
state.level = 1;
state.paddleWidth = CONFIG.paddleWidth;
state.paddleX = (CONFIG.canvasWidth - state.paddleWidth) / 2;
state.paddleTargetX = state.paddleX;
state.powerUpTimers = { widePaddle: 0, multiBall: 0 };
state.balls = [];
state.powerUps = [];
state.particles = [];
createBricks();
launchBall(CONFIG.canvasWidth / 2, CONFIG.canvasHeight - CONFIG.paddleHeight - 40);
updateScoreDisplay();
updateLivesDisplay();
document.getElementById('levelDisplay').textContent = state.level;
hideOverlays();
}
function startDemo() {
state.running = true;
state.demoMode = true;
state.score = 0;
state.lives = CONFIG.lives;
state.level = 1;
state.paddleWidth = CONFIG.paddleWidth;
state.paddleX = (CONFIG.canvasWidth - state.paddleWidth) / 2;
state.paddleTargetX = state.paddleX;
state.powerUpTimers = { widePaddle: 0, multiBall: 0 };
state.balls = [];
state.powerUps = [];
state.particles = [];
createBricks();
launchBall(CONFIG.canvasWidth / 2, CONFIG.canvasHeight - CONFIG.paddleHeight - 40);
updateScoreDisplay();
updateLivesDisplay();
document.getElementById('levelDisplay').textContent = state.level;
hideOverlays();
}
function hideOverlays() {
document.getElementById('startOverlay').classList.add('hidden');
document.getElementById('gameOverOverlay').classList.add('hidden');
document.getElementById('victoryOverlay').classList.add('hidden');
}
function showOverlay(id) {
document.getElementById(id).classList.remove('hidden');
}
function gameOver() {
state.running = false;
state.demoMode = false;
document.getElementById('finalScore').textContent = `Final Score: ${state.score}`;
showOverlay('gameOverOverlay');
}
function victory() {
state.running = false;
state.demoMode = false;
document.getElementById('victoryScore').textContent = `Final Score: ${state.score}`;
showOverlay('victoryOverlay');
}
// ===== INPUT =====
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') state.keys.left = true;
if (e.key === 'ArrowRight') state.keys.right = true;
if (!state.running && (e.key === ' ' || e.key === 'Enter')) {
if (state.demoMode) startGame();
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'ArrowLeft') state.keys.left = false;
if (e.key === 'ArrowRight') state.keys.right = false;
});
canvas.addEventListener('mousemove', (e) => {
if (!state.running || state.demoMode) return;
const rect = canvas.getBoundingClientRect();
const scaleX = CONFIG.canvasWidth / rect.width;
state.mouseX = (e.clientX - rect.left) * scaleX;
state.paddleX = clamp(state.mouseX - state.paddleWidth / 2, 0, CONFIG.canvasWidth - state.paddleWidth);
});
canvas.addEventListener('click', () => {
if (state.demoMode) startGame();
});
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('restartBtn').addEventListener('click', startGame);
document.getElementById('victoryRestartBtn').addEventListener('click', startGame);
// Any key to start from demo
window.addEventListener('keydown', (e) => {
if (state.demoMode && state.running) {
startGame();
}
});
// ===== MAIN LOOP =====
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// Start demo mode immediately
startDemo();
gameLoop();
</script>
</body>
</html>Reasoning trace (325 tokens)
The user wants a polished Breakout game in a single HTML file. Let me break down the requirements: 1. Paddle controlled by mouse AND left/right arrow keys 2. Bounce angle depends on hit position on paddle 3. One level with at least 6 rows of bricks, mixing normal (1 hit) and tougher (2 hits with visible crack) bricks 4. Some bricks have power-up icons that drop when broken: - Wider paddle (10 seconds) - Multi-ball (3 balls) 5. Ball must never pass through bricks or paddle (proper collision detection) 6. Particles when brick breaks 7. 3 lives, score display 8. Game over and victory screens with restart option 9. Self-playing demo mode behind "Click or press a key to start" message 10. Must work in 1280x800 and adapt to 375px wide 11. No horizontal scrollbar 12. All inline CSS/JS, no external resources 13. No localStorage, alert, etc. Let me design this carefully. Key technical considerations: - Canvas rendering - Game loop with requestAnimationFrame - Proper collision detection (AABB for paddle, circle-rect for ball-brick) - Ball physics with angle based on paddle hit position - Power-up system - Particle system - State management (menu, playing, game over, victory) - Demo AI for self-playing mode - Responsive design I'll create a complete, polished implementation.