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 17 s
- Cost
- $0
- Output tokens
- 7,310
- Spent thinking
- 0 tokens
- Speed
- 164.1 tok/s
- Console errors
- 0
- First token after
- 1 min 32 s
- First visible text after
- 1 min 32 s
- Prompt tokens
- 531
- Code size
- 20.8 KB
- Finish reason
- stop
- Served by
- Nvidia
- Model id
nvidia/nemotron-3-ultra-550b-a55b:free- Reasoning sent
{"effort":"none"}- 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 · 732 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0d0d1a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Segoe UI', system-ui, sans-serif;
color: #fff;
overflow: hidden;
}
#gameCanvas {
display: block;
border-radius: 8px;
box-shadow: 0 0 30px rgba(0,200,255,0.3), 0 20px 40px rgba(0,0,0,0.5);
background: #111;
}
#ui {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 800px;
padding: 12px 20px;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
}
#ui .stat { display: flex; align-items: center; gap: 8px; }
#ui .lives { color: #ff4757; }
#ui .score { color: #ffd700; }
#ui .level { color: #2ed573; }
#overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(13, 13, 26, 0.92);
backdrop-filter: blur(4px);
border-radius: 8px;
pointer-events: none;
transition: opacity 0.3s ease;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
#overlay h1 {
font-size: 48px;
font-weight: 800;
background: linear-gradient(135deg, #00c8ff, #ff6b6b, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
text-shadow: 0 0 40px rgba(0,200,255,0.5);
}
#overlay p { color: #aaa; margin-bottom: 24px; font-size: 18px; }
#overlay button {
padding: 14px 40px;
font-size: 18px;
font-weight: 700;
border: none;
border-radius: 50px;
background: linear-gradient(135deg, #00c8ff, #007bff);
color: #fff;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 20px rgba(0,200,255,0.4);
}
#overlay button:hover { transform: translateY(-2px); box-shadow: 0 6px 30px rgba(0,200,255,0.6); }
#overlay button:active { transform: translateY(0); }
.powerup-icon { font-size: 14px; }
@media (max-width: 850px) {
#gameCanvas { width: 100vw; height: auto; max-height: 70vh; border-radius: 0; }
#ui { max-width: 100vw; padding: 10px 16px; font-size: 14px; }
#overlay h1 { font-size: 36px; }
#overlay p { font-size: 16px; }
}
</style>
</head>
<body>
<div id="ui">
<div class="stat lives">LIVES: <span id="livesDisplay">3</span></div>
<div class="stat score">SCORE: <span id="scoreDisplay">0</span></div>
<div class="stat level">LEVEL: <span id="levelDisplay">1</span></div>
</div>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="overlay">
<h1 id="overlayTitle">BREAKOUT</h1>
<p id="overlayText">Click or press a key to start</p>
<button id="startBtn">START GAME</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
const paddle = { x: W/2 - 50, y: H - 40, w: 100, h: 14, baseW: 100, color: '#00c8ff', speed: 0, wideUntil: 0 };
const balls = [];
const bricks = [];
const particles = [];
const powerups = [];
let score = 0;
let lives = 3;
let gameState = 'start'; // start, playing, paused, gameover, victory
let animationId = null;
let keys = { left: false, right: false };
let mouseX = W/2;
let aiMode = true;
let lastTime = 0;
// Brick types
const BRICK_TYPES = {
normal: { hits: 1, color: '#ff6b6b', points: 10, powerupChance: 0.15 },
tough: { hits: 2, color: '#ffa502', points: 25, powerupChance: 0.25, crackedColor: '#cc8400' }
};
const POWERUP_TYPES = {
wide: { color: '#2ed573', icon: '▲', label: 'WIDE PADDLE', duration: 10000 },
multiball: { color: '#ff4757', icon: '●', label: 'MULTI-BALL', duration: 0 }
};
function createBricks() {
bricks.length = 0;
const rows = 6;
const cols = 10;
const brickW = 70;
const brickH = 24;
const padding = 6;
const offsetX = (W - (cols * brickW + (cols-1)*padding)) / 2;
const offsetY = 60;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const isTough = (r < 2) || (r === 2 && (c === 0 || c === cols-1)) || (Math.random() < 0.15 && r > 1);
const type = isTough ? 'tough' : 'normal';
const hasPowerup = Math.random() < BRICK_TYPES[type].powerupChance;
const powerupType = hasPowerup ? (Math.random() < 0.6 ? 'wide' : 'multiball') : null;
bricks.push({
x: offsetX + c * (brickW + padding),
y: offsetY + r * (brickH + padding),
w: brickW,
h: brickH,
type,
hitsLeft: BRICK_TYPES[type].hits,
maxHits: BRICK_TYPES[type].hits,
color: BRICK_TYPES[type].color,
points: BRICK_TYPES[type].points,
powerup: powerupType,
cracked: false
});
}
}
}
function createBall(x, y, vx, vy) {
return {
x, y,
r: 8,
vx, vy,
baseSpeed: 5.5,
trail: [],
maxTrail: 8
};
}
function resetBall(ball) {
ball.x = paddle.x + paddle.w / 2;
ball.y = paddle.y - 10;
ball.vx = (Math.random() - 0.5) * 4;
ball.vy = -ball.baseSpeed;
ball.trail = [];
}
function spawnPowerup(x, y, type) {
powerups.push({
x, y,
w: 24, h: 24,
vy: 2,
type,
rotation: 0,
bobOffset: Math.random() * Math.PI * 2
});
}
function spawnParticles(x, y, color, count = 12) {
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 / count) * i + Math.random() * 0.5;
const speed = 2 + Math.random() * 4;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
radius: 3 + Math.random() * 3,
color,
life: 1,
decay: 0.015 + Math.random() * 0.02
});
}
}
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
function updatePaddle(dt) {
if (aiMode && gameState === 'playing') {
// Simple AI: track the first ball
const targetBall = balls[0];
if (targetBall) {
const targetX = targetBall.x - paddle.w / 2;
const diff = targetX - paddle.x;
paddle.x += diff * 0.08;
}
} else {
if (keys.left) paddle.x -= 10 * dt * 60;
if (keys.right) paddle.x += 10 * dt * 60;
// Mouse control (when not AI)
if (!keys.left && !keys.right && mouseX !== undefined) {
paddle.x += (mouseX - paddle.w/2 - paddle.x) * 0.3;
}
}
paddle.x = clamp(paddle.x, 0, W - paddle.w);
// Check wide paddle timeout
if (paddle.wideUntil > 0 && Date.now() > paddle.wideUntil) {
paddle.w = paddle.baseW;
paddle.wideUntil = 0;
}
}
function updateBalls(dt) {
for (let i = balls.length - 1; i >= 0; i--) {
const ball = balls[i];
// Trail
ball.trail.unshift({ x: ball.x, y: ball.y });
if (ball.trail.length > ball.maxTrail) ball.trail.pop();
ball.x += ball.vx * dt * 60;
ball.y += ball.vy * dt * 60;
// Wall collisions
if (ball.x - ball.r <= 0) { ball.x = ball.r; ball.vx = Math.abs(ball.vx); }
if (ball.x + ball.r >= W) { ball.x = W - ball.r; ball.vx = -Math.abs(ball.vx); }
if (ball.y - ball.r <= 0) { ball.y = ball.r; ball.vy = Math.abs(ball.vy); }
// Paddle collision
if (ball.y + ball.r >= paddle.y && ball.y - ball.r <= paddle.y + paddle.h &&
ball.x + ball.r >= paddle.x && ball.x - ball.r <= paddle.x + paddle.w) {
const hitPos = (ball.x - paddle.x) / paddle.w; // 0 to 1
const angle = (hitPos - 0.5) * Math.PI * 0.75; // -67.5 to 67.5 degrees
const speed = Math.sqrt(ball.vx*ball.vx + ball.vy*ball.vy);
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.abs(Math.cos(angle) * speed);
ball.y = paddle.y - ball.r;
// Add slight speed increase
const newSpeed = Math.min(speed * 1.005, 14);
const currentSpeed = Math.sqrt(ball.vx*ball.vx + ball.vy*ball.vy);
if (currentSpeed > 0) {
ball.vx = (ball.vx / currentSpeed) * newSpeed;
ball.vy = (ball.vy / currentSpeed) * newSpeed;
}
}
// Brick collisions
for (let j = bricks.length - 1; j >= 0; j--) {
const brick = bricks[j];
if (ball.x + ball.r > brick.x && ball.x - ball.r < brick.x + brick.w &&
ball.y + ball.r > brick.y && ball.y - ball.r < brick.y + brick.h) {
// Determine collision side
const overlapX = Math.min(ball.x + ball.r, brick.x + brick.w) - Math.max(ball.x - ball.r, brick.x);
const overlapY = Math.min(ball.y + ball.r, brick.y + brick.h) - Math.max(ball.y - ball.r, brick.y);
if (overlapX < overlapY) {
// Horizontal collision
if (ball.x < brick.x + brick.w/2) { ball.x = brick.x - ball.r; ball.vx = -Math.abs(ball.vx); }
else { ball.x = brick.x + brick.w + ball.r; ball.vx = Math.abs(ball.vx); }
} else {
// Vertical collision
if (ball.y < brick.y + brick.h/2) { ball.y = brick.y - ball.r; ball.vy = -Math.abs(ball.vy); }
else { ball.y = brick.y + brick.h + ball.r; ball.vy = Math.abs(ball.vy); }
}
// Damage brick
brick.hitsLeft--;
if (brick.type === 'tough' && brick.hitsLeft === 1) {
brick.cracked = true;
}
if (brick.hitsLeft <= 0) {
score += brick.points;
updateScore();
spawnParticles(brick.x + brick.w/2, brick.y + brick.h/2, brick.color, 16);
if (brick.powerup) {
spawnPowerup(brick.x + brick.w/2, brick.y + brick.h/2, brick.powerup);
}
bricks.splice(j, 1);
}
break; // Only one brick collision per frame
}
}
// Ball lost
if (ball.y - ball.r > H) {
balls.splice(i, 1);
if (balls.length === 0) {
loseLife();
}
}
}
}
function updatePowerups(dt) {
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt * 60;
p.rotation += 0.02 * dt * 60;
p.bobOffset += 0.05 * dt * 60;
// Paddle collision
if (p.y + p.h >= paddle.y && p.y <= paddle.y + paddle.h &&
p.x + p.w >= paddle.x && p.x <= paddle.x + paddle.w) {
applyPowerup(p.type);
powerups.splice(i, 1);
} else if (p.y > H) {
powerups.splice(i, 1);
}
}
}
function applyPowerup(type) {
const pu = POWERUP_TYPES[type];
if (type === 'wide') {
paddle.w = paddle.baseW * 1.5;
paddle.wideUntil = Date.now() + pu.duration;
} else if (type === 'multiball') {
const mainBall = balls[0];
if (mainBall) {
const speed = Math.sqrt(mainBall.vx*mainBall.vx + mainBall.vy*mainBall.vy);
for (let i = 0; i < 2; i++) {
const angle = (Math.random() - 0.5) * Math.PI * 0.5;
const vx = Math.sin(angle) * speed;
const vy = -Math.abs(Math.cos(angle) * speed);
balls.push(createBall(mainBall.x, mainBall.y, vx, vy));
}
}
}
// Visual feedback
spawnParticles(paddle.x + paddle.w/2, paddle.y, pu.color, 20);
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt * 60;
p.y += p.vy * dt * 60;
p.vy += 0.15 * dt * 60; // gravity
p.life -= p.decay * dt * 60;
p.radius *= 0.985;
if (p.life <= 0 || p.radius < 0.5) particles.splice(i, 1);
}
}
function loseLife() {
lives--;
updateLives();
if (lives <= 0) {
gameState = 'gameover';
showOverlay('GAME OVER', `Final Score: ${score}`, 'TRY AGAIN');
} else {
resetBall(balls[0] || createBall(0,0,0,0));
if (balls.length === 0) balls.push(createBall(0,0,0,0));
paddle.w = paddle.baseW;
paddle.wideUntil = 0;
paddle.x = W/2 - paddle.w/2;
}
}
function checkVictory() {
if (bricks.length === 0) {
gameState = 'victory';
showOverlay('VICTORY!', `Score: ${score} | Lives: ${lives}`, 'PLAY AGAIN');
}
}
function updateScore() { document.getElementById('scoreDisplay').textContent = score; }
function updateLives() { document.getElementById('livesDisplay').textContent = lives; }
function showOverlay(title, text, btnText) {
document.getElementById('overlayTitle').textContent = title;
document.getElementById('overlayText').textContent = text;
document.getElementById('startBtn').textContent = btnText;
document.getElementById('overlay').classList.remove('hidden');
}
function hideOverlay() {
document.getElementById('overlay').classList.add('hidden');
}
function startGame() {
if (gameState === 'playing') return;
score = 0;
lives = 3;
updateScore();
updateLives();
createBricks();
balls.length = 0;
balls.push(createBall(W/2, paddle.y - 10, 0, 0));
resetBall(balls[0]);
particles.length = 0;
powerups.length = 0;
paddle.w = paddle.baseW;
paddle.wideUntil = 0;
paddle.x = W/2 - paddle.w/2;
gameState = 'playing';
aiMode = false;
hideOverlay();
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function togglePause() {
if (gameState === 'playing') {
gameState = 'paused';
showOverlay('PAUSED', 'Press P or click to resume', 'RESUME');
} else if (gameState === 'paused') {
gameState = 'playing';
hideOverlay();
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
}
function gameLoop(time) {
if (gameState !== 'playing') return;
const dt = (time - lastTime) / 1000;
lastTime = time;
updatePaddle(dt);
updateBalls(dt);
updatePowerups(dt);
updateParticles(dt);
checkVictory();
render();
requestAnimationFrame(gameLoop);
}
function render() {
// Clear with slight trail effect
ctx.fillStyle = 'rgba(17, 17, 27, 0.3)';
ctx.fillRect(0, 0, W, H);
// Background grid
ctx.strokeStyle = 'rgba(0, 200, 255, 0.03)';
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
// Bricks
for (const brick of bricks) {
const bt = BRICK_TYPES[brick.type];
const ratio = brick.hitsLeft / brick.maxHits;
// Glow
ctx.shadowColor = brick.color;
ctx.shadowBlur = 8;
// Main brick
const grad = ctx.createLinearGradient(brick.x, brick.y, brick.x, brick.y + brick.h);
grad.addColorStop(0, brick.color);
grad.addColorStop(1, bt.crackedColor || brick.color);
ctx.fillStyle = grad;
roundRect(ctx, brick.x, brick.y, brick.w, brick.h, 4);
ctx.fill();
// Crack effect for tough bricks
if (brick.cracked) {
ctx.strokeStyle = bt.crackedColor;
ctx.lineWidth = 2;
ctx.globalAlpha = 0.6;
ctx.beginPath();
ctx.moveTo(brick.x + 5, brick.y + brick.h - 5);
ctx.lineTo(brick.x + brick.w - 5, brick.y + 5);
ctx.moveTo(brick.x + brick.w/2, brick.y + 3);
ctx.lineTo(brick.x + brick.w/2 - 8, brick.y + brick.h - 3);
ctx.moveTo(brick.x + brick.w/2, brick.y + 3);
ctx.lineTo(brick.x + brick.w/2 + 8, brick.y + brick.h - 3);
ctx.stroke();
ctx.globalAlpha = 1;
}
// Powerup indicator
if (brick.powerup) {
const pu = POWERUP_TYPES[brick.powerup];
ctx.fillStyle = pu.color;
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(pu.icon, brick.x + brick.w/2, brick.y + brick.h/2 + 1);
}
ctx.shadowBlur = 0;
}
// Powerups
for (const p of powerups) {
const pu = POWERUP_TYPES[p.type];
const bob = Math.sin(p.bobOffset) * 4;
ctx.save();
ctx.translate(p.x + p.w/2, p.y + p.h/2 + bob);
ctx.rotate(p.rotation);
// Glow
ctx.shadowColor = pu.color;
ctx.shadowBlur = 15;
// Box
const grad = ctx.createRadialGradient(0, 0, 0, 0, 0, p.w/2);
grad.addColorStop(0, pu.color);
grad.addColorStop(1, '#1a1a2e');
ctx.fillStyle = grad;
roundRect(ctx, -p.w/2, -p.h/2, p.w, p.h, 4);
ctx.fill();
// Icon
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(pu.icon, 0, 1);
ctx.restore();
}
// Particles
for (const p of particles) {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Balls with trails
for (const ball of balls) {
// Trail
for (let i = ball.trail.length - 1; i >= 0; i--) {
const t = ball.trail[i];
const alpha = (1 - i / ball.trail.length) * 0.4;
const radius = ball.r * (1 - i / ball.trail.length * 0.5);
ctx.globalAlpha = alpha;
ctx.fillStyle = '#00c8ff';
ctx.beginPath();
ctx.arc(t.x, t.y, radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Ball glow
ctx.shadowColor = '#00c8ff';
ctx.shadowBlur = 15;
const grad = ctx.createRadialGradient(ball.x, ball.y, 0, ball.x, ball.y, ball.r);
grad.addColorStop(0, '#fff');
grad.addColorStop(0.5, '#00c8ff');
grad.addColorStop(1, '#007bff');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
// Paddle
ctx.shadowColor = paddle.color;
ctx.shadowBlur = 10;
const pGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.h);
pGrad.addColorStop(0, '#4df0ff');
pGrad.addColorStop(1, paddle.color);
ctx.fillStyle = pGrad;
roundRect(ctx, paddle.x, paddle.y, paddle.w, paddle.h, 6);
ctx.fill();
// Paddle highlight
ctx.fillStyle = 'rgba(255,255,255,0.2)';
roundRect(ctx, paddle.x + 2, paddle.y + 2, paddle.w - 4, 4, 3);
ctx.fill();
ctx.shadowBlur = 0;
// Wide paddle indicator
if (paddle.wideUntil > 0) {
const remaining = (paddle.wideUntil - Date.now()) / 10000;
ctx.fillStyle = 'rgba(46, 213, 115, 0.5)';
ctx.fillRect(paddle.x, paddle.y - 4, paddle.w * remaining, 3);
}
}
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();
}
// Input handling
document.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = true;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = true;
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (gameState === 'start' || gameState === 'gameover' || gameState === 'victory') startGame();
else if (gameState === 'playing' || gameState === 'paused') togglePause();
}
if (e.key === 'p' || e.key === 'P') togglePause();
});
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;
});
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
if (gameState === 'start') { aiMode = false; }
});
canvas.addEventListener('click', e => {
if (gameState === 'start' || gameState === 'gameover' || gameState === 'victory') {
startGame();
} else if (gameState === 'paused') {
togglePause();
}
aiMode = false;
});
// Touch support
canvas.addEventListener('touchmove', e => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
mouseX = e.touches[0].clientX - rect.left;
if (gameState === 'start') { aiMode = false; }
}, { passive: false });
canvas.addEventListener('touchstart', e => {
if (gameState === 'start' || gameState === 'gameover' || gameState === 'victory') {
startGame();
} else if (gameState === 'paused') {
togglePause();
}
aiMode = false;
}, { passive: true });
// Initial render for start screen
function renderStart() {
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, W, H);
// Draw some demo bricks
createBricks();
for (const brick of bricks.slice(0, 20)) {
const bt = BRICK_TYPES[brick.type];
const grad = ctx.createLinearGradient(brick.x, brick.y, brick.x, brick.y + brick.h);
grad.addColorStop(0, brick.color);
grad.addColorStop(1, bt.crackedColor || brick.color);
ctx.fillStyle = grad;
roundRect(ctx, brick.x, brick.y, brick.w, brick.h, 4);
ctx.fill();
}
bricks.length = 0;
// Demo paddle
ctx.fillStyle = '#00c8ff';
roundRect(ctx, paddle.x, paddle.y, paddle.w, paddle.h, 6);
ctx.fill();
// Demo ball
ctx.shadowColor = '#00c8ff';
ctx.shadowBlur = 15;
const grad = ctx.createRadialGradient(W/2, H/2 - 100, 0, W/2, H/2 - 100, 8);
grad.addColorStop(0, '#fff');
grad.addColorStop(0.5, '#00c8ff');
grad.addColorStop(1, '#007bff');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(W/2, H/2 - 100, 8, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
renderStart();
</script>
</body>
</html>