Nemotron 3 Nano Omni (free)
by NVIDIAReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 3 min 5 s
- Cost
- $0
- Output tokens
- 19,547
- Spent thinking
- 16,124 tokens
- Speed
- 105.9 tok/s
- Console errors
- 1
- First token after
- 683 ms
- First visible text after
- 2 min 35 s
- Prompt tokens
- 531
- Code size
- 10.4 KB
- Finish reason
- stop
- Served by
- Nvidia
- Model id
nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free- Reasoning sent
model default- Max output
- 32,000 tokens
- Generated
- Nov 9, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
Identifier 'paddle' has already been declared
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 · 416 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Breakout</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#000;}
canvas{display:block;width:100%;height:auto;background:#111;}
#overlay{position:absolute;color:#fff;font-size:24px;top:0;left:0;width:100%;text-align:center;pointer-events:none;}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="overlay"></div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
// canvas size (responsive)
function resize() {
canvas.width = 800;
canvas.height = 600;
}
window.addEventListener('resize', resize);
resize();
// game state variables
let demoMode = true;
let gameOver = false;
let victory = false;
let score = 0;
let lives = 3;
let widerPaddleTimer = 0;
let multiballTimer = 0;
let particles = [];
// game objects
let bricks = [];
let powerUps = [];
let balls = [];
let paddle;
// classes
class Paddle {
constructor(w, h) {
this.w = w; this.h = h;
this.x = 0; this.y = 0;
}
draw() {
ctx.fillStyle = '#0f0';
ctx.fillRect(this.x, this.y, this.w, this.h);
}
followBall() {
if (!demoMode) return;
const b = balls[0];
this.x = b.x - this.w/2;
this.x = Math.max(0, Math.min(this.x, canvas.width - this.w));
}
move(dx) {
this.x += dx;
this.x = Math.max(0, Math.min(this.x, canvas.width - this.w));
}
}
class Ball {
constructor(r) {
this.r = r;
this.x = canvas.width/2;
this.y = canvas.height/2;
this.vx = 3;
this.vy = 3;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI*2);
ctx.fillStyle = '#ff0';
ctx.fill();
}
update(paddle, bricks) {
this.x += this.vx;
this.y += this.vy;
// wall collisions
if (this.x - this.r <= 0) {
this.x = this.r;
this.vx = -this.vx;
}
if (this.x + this.r >= canvas.width) {
this.x = canvas.width - this.r;
this.vx = -this.vx;
}
if (this.y - this.r <= 0) {
this.y = this.r;
this.vy = -this.vy;
}
// paddle collision
if (this.y + this.r >= paddle.y && this.x > paddle.x && this.x < paddle.x + paddle.w) {
const offset = (this.x + this.r - (paddle.x + paddle.w/2)) / (paddle.w/2);
const maxAngle = Math.PI/4;
const angle = offset * maxAngle;
const speed = Math.hypot(this.vx, this.vy);
this.vx = speed * Math.cos(angle);
this.vy = -speed * Math.sin(angle);
this.y = paddle.y - this.r;
}
// brick collisions
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
const ballLeft = this.x - this.r;
const ballRight = this.x + this.r;
const ballTop = this.y - this.r;
const ballBottom = this.y + this.r;
if (ballRight > b.x && ballLeft < b.x + b.w && ballBottom > b.y && ballTop < b.y + b.h) {
const dx = Math.min(b.x + b.w - this.x, this.x + this.r - b.x);
const dy = Math.min(b.y + b.h - this.y, this.y + this.r - b.y);
if (dx < dy) {
if (this.x + this.r > b.x + b.w) this.x = b.x + b.w - this.r;
else this.x = b.x + this.r;
this.vx = -this.vx;
} else {
if (this.y + this.r > b.y + b.h) {
this.y = b.y + b.h - this.r;
this.vy = -this.vy;
} else {
this.y = b.y - this.r;
this.vy = -this.vy;
}
}
if (b.health > 0) {
b.health--;
if (b.health === 0) {
particles.push(...createParticles(b.x, b.y, b.w, b.h));
if (b.powerUp) {
powerUps.push(new PowerUp(b.x + b.w/2, b.y + b.h/2, b.powerUp));
}
bricks.splice(i, 1);
score += 10;
} else if (b.health === 1 && b.type === 'tough') {
b.cracked = true;
}
}
break;
}
}
}
}
class Brick {
constructor(x, y, w, h, health, type='normal', powerUp=null) {
this.x = x; this.y = y; this.w = w; this.h = h;
this.health = health;
this.type = type;
this.cracked = false;
this.powerUp = powerUp;
}
draw() {
if (this.cracked) {
ctx.fillStyle = '#888';
} else {
ctx.fillStyle = this.type === 'tough' ? '#a0522d' : '#8b0000';
}
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
class PowerUp {
constructor(x, y, type) {
this.x = x; this.y = y; this.type = type;
this.vy = 2;
this.size = 10;
}
draw() {
ctx.fillStyle = this.type === 'wider' ? '#0ff' : '#f0f';
ctx.fillRect(this.x - this.size/2, this.y - this.size/2, this.size, this.size);
}
update() {
this.y += this.vy;
}
}
class Particle {
constructor(x, y, size, color) {
this.x = x; this.y = y; this.size = size; this.color = color;
this.alpha = 1;
this.life = 10;
}
update() {
this.y -= 0.5;
this.alpha -= 0.05;
this.life--;
}
draw() {
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
function createParticles(x, y, w, h) {
const particles = [];
const color = '#fff';
const size = 3;
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 3;
particles.push(new Particle(x + w/2, y + h/2, size, color));
}
return particles;
}
// initialize bricks
function initBricks() {
const rowHeight = 30;
const cols = 10;
const marginX = 20;
const marginY = 20;
for (let row = 0; row < 6; row++) {
for (let col = 0; col < cols; col++) {
const x = marginX + col * (canvas.width - 2*marginX) / cols - (canvas.width - 2*marginX)/2;
const y = marginY + row * rowHeight;
const health = (row % 2 === 0) ? 1 : 2;
const tough = (row % 2 === 1);
const type = tough ? 'tough' : 'normal';
const powerUpChance = Math.random() < 0.2;
const powerUpType = powerUpChance ? (Math.random() < 0.5 ? 'wider' : 'multiball') : null;
bricks.push(new Brick(x, y, (canvas.width - 2*marginX)/cols, rowHeight, health, type, powerUpType));
}
}
}
// paddle and ball initialisation (will be set in resetGame)
let paddle = new Paddle(120, 15);
let balls = []; // will be filled in resetGame
// input handling
canvas.addEventListener('mousemove', e => {
if (!demoMode) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
paddle.x = mx - paddle.w/2;
paddle.x = Math.max(0, Math.min(paddle.x, canvas.width - paddle.w));
}
});
canvas.addEventListener('keydown', e => {
if (demoMode) {
demoMode = false;
// reset ball and paddle
balls = [];
const b = new Ball(8);
b.x = canvas.width/2;
b.y = canvas.height/2;
b.vx = 3;
b.vy = 3;
balls.push(b);
paddle.x = (canvas.width - paddle.w)/2;
} else {
const step = 6;
if (e.key === 'ArrowLeft') {
paddle.move(-step);
} else if (e.key === 'ArrowRight') {
paddle.move(step);
}
}
});
canvas.addEventListener('click', () => {
if (demoMode) {
demoMode = false;
balls = [];
const b = new Ball(8);
b.x = canvas.width/2;
b.y = canvas.height/2;
b.vx = 3;
b.vy = 3;
balls.push(b);
paddle.x = (canvas.width - paddle.w)/2;
}
});
// game loop
function loop() {
demoAI();
// update balls
for (let i = balls.length-1; i >= 0; i--) {
const b = balls[i];
b.update(paddle, bricks);
}
// update power-ups and check catch
for (let i = powerUps.length-1; i >= 0; i--) {
const pu = powerUps[i];
pu.update();
for (let j = balls.length-1; j >= 0; j--) {
const b = balls[j];
if (b.y + b.r >= pu.y - pu.size/2 && b.y - b.r <= pu.y + pu.size/2 &&
b.x > pu.x - pu.size/2 && b.x < pu.x + pu.size/2) {
if (pu.type === 'wider') {
paddle.w = Math.min(canvas.width - 10, paddle.w * 1.5);
widerPaddleTimer = 10 * 60;
} else if (pu.type === 'multiball') {
const maxBalls = lives;
const canAdd = Math.min(2, maxBalls - balls.length);
for (let k = 0; k < canAdd; k++) {
const eb = new Ball(8);
eb.x = b.x;
eb.y = b.y;
eb.vx = (Math.random() - 0.5) * 2;
eb.vy = (Math.random() - 0.5) * 2;
balls.push(eb);
}
}
powerUps.splice(i, 1);
break;
}
}
}
// timers
if (widerPaddleTimer > 0) widerPaddleTimer--;
if (multiballTimer > 0) multiballTimer--;
// ball loss handling
for (let i = balls.length-1; i >= 0; i--) {
const b = balls[i];
if (b.y - b.r > canvas.height) {
balls.splice(i, 1);
lives--;
if (lives > 0) {
const nb = new Ball(8);
nb.x = canvas.width/2;
nb.y = canvas.height/2;
nb.vx = 3;
nb.vy = 3;
balls.push(nb);
} else {
gameOver = true;
}
}
}
// draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
bricks.forEach(b => b.draw());
powerUps.forEach(p => p.draw());
particles.forEach(p => {
p.update();
p.draw();
});
paddle.draw();
for (let i = 0; i < balls.length; i++) {
balls[i].draw();
}
// UI
ctx.fillStyle = '#fff';
ctx.font = '16px sans-serif';
ctx.fillText(`Score: ${score}`, 10, 20);
ctx.fillText(`Lives: ${lives}`, 10, 40);
// overlay messages
if (demoMode) {
overlay.textContent = 'Click or press a key to start';
} else if (gameOver) {
overlay.textContent = 'Game Over. Press R to restart';
} else if (victory) {
overlay.textContent = 'You Win! Press R to restart';
}
requestAnimationFrame(loop);
}
// demo AI
function demoAI() {
if (!demoMode) return;
const b = balls[0];
paddle.x = b.x - paddle.w/2;
paddle.x = Math.max(0, Math.min(paddle.x, canvas.width - paddle.w));
}
// reset game
function resetGame() {
bricks.length = 0;
initBricks();
score = 0;
lives = 3;
balls = [];
const b = new Ball(8);
b.x = canvas.width/2;
b.y = canvas.height/2;
b.vx = 3;
b.vy = 3;
balls.push(b);
paddle.w = 120;
paddle.x = (canvas.width - paddle.w) / 2;
powerUps.length = 0;
widerPaddleTimer = 0;
multiballTimer = 0;
particles.length = 0;
gameOver = false;
victory = false;
overlay.textContent = '';
}
// start the loop
requestAnimationFrame(loop);
// restart on 'R' after game over or victory
window.addEventListener('keydown', e => {
if ((gameOver || victory) && e.key.toLowerCase() === 'r') {
resetGame();
}
});
</script>
</body>
</html>Reasoning trace (16,124 tokens)
We need to produce a single HTML file with canvas, vanilla JS, meeting all requirements. Let's outline.
Canvas size: responsive to viewport 1280x800, adapt to smaller. Use CSS to set width:100%; height:auto? Better set canvas width/height attributes to actual pixel values, maybe 800x600? But requirement: look right in 1280x800 and adapt down to 375px wide. So we need responsive canvas: set CSS width:100%; height:auto; and set canvas width/height to a base size, e.g., 800x600, then scale via CSS. Or we can set width/height to window.innerWidth/Height but maintain aspect ratio. Simpler: set canvas width = 800, height = 600 (aspect 4:3). Then CSS: canvas { width:100%; height:auto; } This will scale to fit width.
Need paddle controlled with mouse (move horizontally) and left/right arrow keys. So we can listen to mousemove on canvas to set paddle x, and keydown for arrows to also set.
Ball bounce angle depends on paddle contact point: compute offset = (ball.x - paddle.centerX) / (paddleHalfWidth). Then angle = offset * maxAngle (maybe 45 deg). Use dy sign based on direction.
Bricks: at least 6 rows. We'll create a 2D array of brick objects. Each brick has health (1 or 2). Normal brick: health 1, break on hit. Tougher brick: health 2, need 2 hits, show crack after first hit (maybe change sprite or color). We'll just change color to gray after first hit.
Power-up icons: some bricks have a flag indicating power-up type. When broken, drop a power-up object that falls down. Catching with paddle: if paddle catches (ball.y >= paddle.y and ball.x within paddle width), apply effect: wider paddle for 10 seconds (increase paddle width), or multi-ball for 3 balls (spawn extra balls). We'll implement timer for effects.
Particles: simple small circles that fade out when brick breaks.
Lives: 3. Score displayed top left.
Game over screen when ball goes below paddle. Victory screen when all bricks broken.
Restart option: maybe press R or click to restart.
Demo mode: auto start with AI playing? The requirement: "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 we need a demo mode that automatically plays (ball moving, paddle following ball automatically) until user clicks or presses a key to start controlling. So initially, we show a message overlay "Click or press a key to start". While that is shown, we have an AI controlling paddle to follow ball (simple). When user interacts, we switch to player control.
Simplify: In demo mode, we let the ball move automatically, paddle moves to center or follow ball. We'll have a flag demoMode = true initially. On first keypress or click on canvas, set demoMode = false and enable player control.
Need to ensure that before user interaction, the canvas receives keyboard events only after click. So we can add event listener for any keypress or click on canvas to start.
Also need to ensure that the ball never passes through bricks or paddle: we need collision detection to prevent ball from moving through. Standard bounce.
Implement main loop using requestAnimationFrame.
Let's design classes:
- Canvas: width 800 height 600.
- Paddle: x, y, width, height, dy (for AI). In demo mode, we can set paddle.y fixed (maybe near bottom). In player mode, paddle moves with mouse or keys.
- Ball: x, y, vx, vy, radius. On each frame update position, handle collisions with walls (top, sides) and paddle.
- Brick: array of bricks with x,y, w,h, health (1 or 2), cracked boolean.
- PowerUp: x,y, type ( 'wider' or 'multiball' ), falling.
- Particles: array of {x,y,size,alpha,life}.
- Game state: lives=3, score=0, gameOver=false, victory=false, demoMode=true.
Demo AI: simple: paddle moves towards ball's x position (center). So paddle.x = ball.x - paddle.width/2 (clamped). Also maybe move slowly.
When user clicks or presses a key, we set demoMode = false; we stop AI control and let user control via mouse or keys.
Implementation details:
- Mouse move on canvas: set paddleX = e.offsetX - paddle.width/2; clamp within canvas.
- Key down: left/right arrow to move paddle relative to current position (e.g., paddle.x += speed * direction). In demo mode, ignore.
- Ball movement: each frame, ball.x += vx, ball.y += vy.
- Collision with top wall: if ball.y - radius <= 0 => bounce y direction (vy = -vy). Also maybe increase speed? Not needed.
- Collision with side walls: if ball.x - radius <= 0 => vx = -vx; if ball.x + radius >= canvas.width => vx = -vx.
- Collision with paddle: if ball.y + radius >= paddle.y and ball.x between paddle.x and paddle.x + paddle.width => compute offset = (ball.x + radius - (paddle.x + paddle.width/2)) / (paddle.width/2). Then angle = offset * maxAngle (e.g., Math.PI/4). Set vy = -vy * (1 + 0.5*Math.abs(offset)) maybe. Also adjust vx based on angle: vx = vx * Math.cos(angle) ??? Simpler: set vx = offset * speed (maybe). We'll just set vx = (offset) * ballSpeed (maybe 5). Keep vy positive (downwards) reversed.
- Brick collision: iterate bricks, if ball collides (distance less than radius). For each brick, if health >0: decrement health, if health becomes 0 => brick broken -> create particles, drop powerup if flag, else just remove. If health was 2 and now becomes 1 => mark cracked (change color). Also need to ensure ball does not pass through: after collision, reverse vy and maybe adjust x to avoid penetration.
- Power-up drop: create object with x = brick.x + brick.w/2, y = brick.y, type. It falls down (vy positive). When paddle catches (ball.y + radius >= powerup.y && ball.x within powerup.x +/- powerup.w/2) => apply effect. For wider paddle: increase paddle width by factor (e.g., 1.5) for 10 seconds (timer). For multi-ball: create two extra balls (maybe same initial position/velocity). We'll limit max balls to 3 (including original). Use timers.
- Particles: simple canvas draw of small circles with decreasing alpha.
- Game over: if ball.y - radius > canvas.height => lose a life. Decrease lives, reset ball to start position, reset paddle position (center). If lives == 0 => gameOver = true.
- Victory: if bricks array empty => victory.
- Restart: press R or click restart button overlay.
- UI: draw score, lives, maybe a start message overlay.
- Ensure no external resources: all drawn via canvas.
- Ensure the code runs with zero errors.
Potential issues: handling multiple balls for multi-ball; need to manage array of balls.
Simplify: keep a main ball, and if multi-ball active, we spawn extra balls stored in array balls = [mainBall, ...]. Each frame update all balls. For simplicity, treat all balls same; when multi-ball ends after timer, remove extra balls.
Implement timers using performance.now or Date.
Let's outline code structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Breakout</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#000;}
canvas{display:block;width:100%;height:auto;background:#111;}
#overlay{position:absolute;color:#fff;font-size:24px;top:0;left:0;width:100%;text-align:center;pointer-events:none;}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="overlay"></div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
// Resize canvas to match CSS size
function resize() {
canvas.width = 800; // base size
canvas.height = 600;
}
window.addEventListener('resize', resize);
resize();
let demoMode = true;
let gameOver = false;
let victory = false;
// Game objects
class Paddle {
constructor(w, h) {
this.w = w; this.h = h;
this.x = 0; this.y = 0;
}
draw() {
ctx.fillStyle = '#0f0';
ctx.fillRect(this.x, this.y, this.w, this.h);
}
// AI control
followBall(ball) {
// move to center of ball, clamped
this.x = Math.max(0, Math.min(this.x + (ball.x - this.x), this.w));
// Actually we want to set directly:
this.x = ball.x - this.w/2;
this.x = Math.max(0, Math.min(this.x, canvas.width - this.w));
}
// Player control
move(dx) {
this.x += dx;
this.x = Math.max(0, Math.min(this.x, canvas.width - this.w));
}
}
class Ball {
constructor(r) {
this.r = r;
this.x = canvas.width/2;
this.y = canvas.height/2;
this.vx = 3;
this.vy = 3;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI*2);
ctx.fillStyle = '#ff0';
ctx.fill();
}
// update position and handle collisions
update(paddle, bricks) {
this.x += this.vx;
this.y += this.vy;
// wall collisions
if (this.x - this.r <= 0) { this.x = this.r; this.vx = -this.vx; }
if (this.x + this.r >= canvas.width) { this.x = canvas.width - this.r; this.vx = -this.vx; }
if (this.y - this.r <= 0) { this.y = this.r; this.vy = -this.vy; }
// paddle collision
if (this.y + this.r >= paddle.y && this.x > paddle.x && this.x < paddle.x + paddle.w) {
// compute offset
const offset = (this.x + this.r - (paddle.x + paddle.w/2)) / (paddle.w/2);
const maxAngle = Math.PI/4; // 45 degrees
const angle = offset * maxAngle;
// keep speed magnitude
const speed = Math.hypot(this.vx, this.vy);
this.vx = speed * Math.cos(angle);
this.vy = -speed * Math.sin(angle);
// prevent sticking
this.y = paddle.y - this.r;
}
// brick collisions
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
// simple AABB vs circle
const ballLeft = this.x - this.r;
const ballRight = this.x + this.r;
const ballTop = this.y - this.r;
const ballBottom = this.y + this.r;
if (ballRight > b.x && ballLeft < b.x + b.w && ballBottom > b.y && ballTop < b.y + b.h) {
// resolve penetration
// find smallest penetration vector
const dx = Math.min(b.x + b.w - this.x, this.x + this.r - b.x);
const dy = Math.min(b.y + b.h - this.y, this.y + this.r - b.y);
if (dx < dy) {
// horizontal collision
if (this.x + this.r > b.x + b.w) this.x = b.x + b.w - this.r;
else this.x = b.x + this.r;
this.vx = -this.vx;
} else {
// vertical collision
if (this.y + this.r > b.y + b.h) {
this.y = b.y + b.h - this.r;
this.vy = -this.vy;
} else {
this.y = b.y - this.r;
this.vy = -this.vy;
}
}
// reduce speed a bit?
// after hit, break brick
if (b.health > 0) {
b.health--;
if (b.health === 0) {
// brick broken
// create particles
particles.push(...createParticles(b.x, b.y, b.w, b.h));
// power-up drop
if (b.powerUp) {
powerUps.push(new PowerUp(b.x + b.w/2, b.y + b.h/2, b.powerUp));
}
// remove brick
bricks.splice(i,1);
// increase score
score += 10;
} else if (b.health === 1 && b.type === 'tough') {
// crack visual
b.cracked = true;
}
}
break; // exit loop after first brick hit
}
}
}
}
class Brick {
constructor(x, y, w, h, health, type='normal') {
this.x = x; this.y = y; this.w = w; this.h = h;
this.health = health; // 1 or 2
this.type = type; // 'normal' or 'tough'
this.cracked = false;
}
draw() {
if (this.cracked) {
ctx.fillStyle = '#888';
} else {
ctx.fillStyle = this.type === 'tough' ? '#a0522d' : '#8b0000';
}
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
class PowerUp {
constructor(x, y, type) {
this.x = x; this.y = y; this.type = type; // 'wider' or 'multiball'
this.vy = 2;
this.size = 10;
}
draw() {
ctx.fillStyle = this.type === 'wider' ? '#0ff' : '#f0f';
ctx.fillRect(this.x - this.size/2, this.y - this.size/2, this.size, this.size);
}
update() {
this.y += this.vy;
}
}
class Particle {
constructor(x, y, size, color) {
this.x = x; this.y = y; this.size = size; this.color = color;
this.alpha = 1;
this.life = 10; // frames
}
update() {
this.y -= 0.5; // rise
this.alpha -= 0.05;
this.life--;
}
draw() {
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
function createParticles(x, y, w, h) {
const particles = [];
const color = '#fff';
const size = 3;
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 3;
particles.push(new Particle(x + w/2, y + h/2, size, color));
}
return particles;
}
// Game state
const paddle = new Paddle(120, 15);
const ball = new Ball(8);
const bricks = [];
const powerUps = [];
let score = 0;
let lives = 3;
let extraBalls = 0; // count of extra balls active
let widerPaddleTimer = 0;
let multiballTimer = 0;
// Demo AI paddle movement (simple)
function demoAI() {
if (!demoMode) return;
// make paddle follow ball's x
paddle.x = ball.x - paddle.w/2;
paddle.x = Math.max(0, Math.min(paddle.x, canvas.width - paddle.w));
}
// Initialize bricks: 6 rows, each row maybe 10 bricks
function initBricks() {
const rowHeight = 30;
const cols = 10;
const marginX = 20;
const marginY = 20;
for (let row = 0; row < 6; row++) {
for (let col = 0; col < cols; col++) {
const x = marginX + col * (canvas.width - 2*marginX) / cols - (canvas.width - 2*marginX)/2;
const y = marginY + row * rowHeight;
const health = (row % 2 === 0) ? 1 : 2; // alternate rows tough
const tough = (row % 2 === 1);
const type = tough ? 'tough' : 'normal';
bricks.push(new Brick(x, y, (canvas.width - 2*marginX)/cols, rowHeight, health, type));
}
}
}
initBricks();
// Input handling
canvas.addEventListener('mousemove', e => {
if (!demoMode) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
paddle.x = mx - paddle.w/2;
paddle.x = Math.max(0, Math.min(paddle.x, canvas.width - paddle.w));
}
});
canvas.addEventListener('keydown', e => {
if (demoMode) {
// any key starts the game
demoMode = false;
// reset ball position
ball.x = canvas.width/2;
ball.y = canvas.height/2;
ball.vx = 3;
ball.vy = 3;
// reset paddle
paddle.x = (canvas.width - paddle.w)/2;
// reset lives etc?
// keep lives same
} else {
// player control
const step = 6;
if (e.key === 'ArrowLeft') {
paddle.move(-step);
} else if (e.key === 'ArrowRight') {
paddle.move(step);
}
}
});
// also allow click to start
canvas.addEventListener('click', () => {
if (demoMode) {
demoMode = false;
ball.x = canvas.width/2;
ball.y = canvas.height/2;
ball.vx = 3;
ball.vy = 3;
paddle.x = (canvas.width - paddle.w)/2;
}
});
// Game loop
function loop(timestamp) {
// demo AI step
demoAI();
// update objects
ball.update(paddle, bricks);
// update power-ups
for (let i = powerUps.length-1; i >=0; i--) {
const pu = powerUps[i];
pu.update();
// check catch
if (ball.y + ball.r >= pu.y - pu.size/2 && ball.y - ball.r <= pu.y + pu.size/2 &&
ball.x > pu.x - pu.size/2 && ball.x < pu.x + pu.size/2) {
// apply effect
if (pu.type === 'wider') {
paddle.w = Math.min(canvas.width - 10, paddle.w * 1.5);
widerPaddleTimer = 10 * 60; // 10 seconds at 60fps
} else if (pu.type === 'multiball') {
// create 2 extra balls
for (let j = 0; j < 2; j++) {
const b = new Ball(8);
b.x = ball.x;
b.y = ball.y;
b.vx = (Math.random() - 0.5) * 2;
b.vy = (Math.random() - 0.5) * 2;
balls.push(b);
extraBalls++;
}
multiballTimer = 3 * 60; // 3 seconds
}
powerUps.splice(i,1);
}
}
// update extra balls
for (let i = balls.length-1; i >=0; i--) {
const b = balls[i];
b.update(paddle, bricks);
// check if extra ball fell below paddle -> lose life?
if (b.y - b.r > canvas.height) {
// remove extra ball
balls.splice(i,1);
extraBalls--;
}
}
// update wider paddle timer
if (widerPaddleTimer > 0) {
widerPaddleTimer--;
if (widerPaddleTimer === 0) {
// restore paddle width
paddle.w = 120;
}
}
// update multiball timer
if (multiballTimer > 0) {
multiballTimer--;
if (multiballTimer === 0) {
// remove extra balls
balls.splice(1, balls.length-1); // keep original ball
extraBalls = 0;
}
}
// check ball below paddle -> lose life
if (ball.y - ball.r > canvas.height) {
lives--;
if (lives > 0) {
// reset ball and paddle
ball.x = canvas.width/2;
ball.y = canvas.height/2;
ball.vx = 3;
ball.vy = 3;
paddle.x = (canvas.width - paddle.w)/2;
} else {
gameOver = true;
}
}
// check victory
if (bricks.length === 0 && !victory) {
victory = true;
}
// draw everything
// clear
ctx.clearRect(0,0,canvas.width,canvas.height);
// draw bricks
bricks.forEach(b => b.draw());
// draw power-ups
powerUps.forEach(p => p.draw());
// draw particles
particles.forEach(p => {
p.update();
p.draw();
});
// draw paddle and balls
paddle.draw();
// main ball
ball.draw();
// extra balls
for (let i = 1; i < balls.length; i++) {
balls[i].draw();
}
// UI
ctx.fillStyle = '#fff';
ctx.font = '16px sans-serif';
ctx.fillText(`Score: ${score}`, 10, 20);
ctx.fillText(`Lives: ${lives}`, 10, 40);
// overlay messages
if (demoMode) {
overlay.textContent = 'Click or press a key to start';
} else if (gameOver) {
overlay.textContent = 'Game Over. Press R to restart';
} else if (victory) {
overlay.textContent = 'You Win! Press R to restart';
}
// restart handling
if (gameOver || victory) {
if (e.key && e.key.toLowerCase() === 'r') {
// reset game
initBricks();
score = 0;
lives = 3;
extraBalls = 0;
widerPaddleTimer = 0;
multiballTimer = 0;
balls = [ball]; // reset extra balls array
paddle.w = 120;
paddle.x = (canvas.width - paddle.w)/2;
ball.x = canvas.width/2;
ball.y = canvas.height/2;
ball.vx = 3;
ball.vy = 3;
gameOver = false;
victory = false;
overlay.textContent = '';
}
}
requestAnimationFrame(loop);
}
// particles array
const particles = [];
requestAnimationFrame(loop);
</script>
</body>
</html>
```
Need to ensure that we have a variable `balls` array to hold main ball and extra balls. In code above we used `balls` but not defined earlier. Let's adjust: At top define `let balls = [ball];` after creating ball. Also need to adjust code that references `balls` array for extra balls. Also need to adjust the draw loop: we used `balls` variable but not defined earlier. Let's restructure.
Simplify: Keep `balls` array containing all active balls. Initially `balls = [ball]`. In update, we need to iterate over `balls` and update each. For main ball we also need to handle collisions; but collisions with bricks etc should be same for all balls. However the paddle collision detection currently uses `paddle` variable (single). For extra balls we need to use same paddle; we can just treat all balls same.
Simplify: Instead of separate `ball` object, we can have an array `balls`. The main ball is the first element. In demo mode we keep only that ball. When multi-ball active we push extra balls. In update loop, we loop through `balls` and for each we call `update(paddle, bricks)`. However we need to handle paddle collision for each ball: we currently check `ball.update(paddle, bricks)`. If we have multiple balls, we need to pass paddle and bricks to each; they all share same paddle and bricks.
But we also need to handle the case where ball goes below paddle: for each ball we check if its y - r > canvas.height, then lose a life (but only for the main ball? Actually each ball is a life; if any ball goes below, we lose a life? In classic breakout, you lose a ball, not a life per ball; but requirement says 3 lives, meaning you have 3 balls. So each ball counts as a life; if any ball falls, you lose one life, and maybe remove that ball. But we also have extra balls from multi-ball; losing a ball reduces lives count? Let's think: Lives = number of balls currently active (including extra). Starting with 1 ball (main). When multi-ball active, extra balls increase lives count. So lives = balls.length. But requirement says 3 lives, so we need to enforce max 3 balls? Or we can treat lives as number of balls remaining (including extra). Starting with 1 ball (1 life). When multi-ball active, you have more balls, but you still have 3 lives total (maybe you can have up to 3 balls). Simpler: we keep lives = 3 initially, and when a ball is lost (falls below paddle), we decrement lives and remove that ball. If lives reaches 0 -> game over. Multi-ball adds extra balls but we cap total balls to 3? Let's implement: lives = 3; balls array holds up to 3 balls. When multi-ball spawns extra balls, we increase lives count temporarily (but max 3). When a ball is lost, lives-- and ball removed. When lives reaches 0 -> game over. Also when victory, we ignore lives.
Simplify: We'll keep `lives` as number of remaining balls (including main). Initially `lives = 1`? But requirement says 3 lives. So we set `lives = 3`. At start we have 1 ball; we need to spawn 2 extra balls? Not necessary; we can just treat each ball as a life, and start with 3 balls. Let's start with 3 balls: create three balls with same initial position/velocity but maybe different direction. However requirement: game starts with a single ball, but we can have 3 lives meaning you have 3 balls total (maybe you can have up to 3 balls on screen). Starting with 1 ball, you have 3 lives meaning you can lose 2 balls before game over. So we can start with 1 ball, lives=3. When a ball falls, lives--, and if lives>0 we respawn that ball (maybe the same ball). But we need to keep track of which ball fell. Simpler: we treat each ball individually; when any ball goes below paddle, we remove that ball and decrement lives. If lives becomes 0 -> game over. At respawn, we recreate that ball (maybe the main ball). For simplicity, we can just have a single ball; lives count is separate; when ball goes below, we decrement lives, reset ball position, and continue. That matches classic breakout: you have 3 lives, each loss resets ball. So we don't need multiple balls for lives; extra balls from multi-ball are separate power-up effect, not lives.
Thus we keep `balls = [ball]` only. The extra balls from multi-ball are separate objects that behave like normal balls (they also can fall). But they are not tied to lives count; they just add difficulty. However requirement "3 lives" likely means you have 3 attempts (balls). So we need to handle losing a ball (i.e., ball goes below paddle) as losing a life, decreasing lives, and respawning ball. The extra balls from multi-ball should also be counted as separate balls that can be lost; but they are not part of lives count? Probably they are just extra balls, you can lose them but you still have lives count unchanged? Hmm ambiguous.
Simplify: We'll treat lives as number of balls currently active (including extra). Starting with 1 ball, lives=3 (meaning you can have up to 3 balls total). When a ball is lost (falls below paddle), we decrement lives and remove that ball. If lives becomes 0 -> game over. When multi-ball power-up is collected, we add extra balls up to the remaining lives (so total balls <= lives). This ensures you never exceed 3 balls. That's more consistent.
Implementation: lives = 3; balls = [ball]; When multi-ball active, we add extra balls but ensure total balls <= lives. When a ball is lost (y - r > canvas.height), we remove that ball, lives--, and if lives>0 we respawn a ball (maybe the main ball). We'll need to keep track of which ball is "main" vs extra; but for simplicity we can treat all balls same; when any ball is lost, we decrement lives, and if lives>0 we create a new ball (maybe same as original). We'll also need to adjust paddle count? Paddle only one.
Let's restructure:
- `let lives = 3;` (remaining lives)
- `let balls = [];` initially we push one ball: `balls.push(new Ball(8));`
- In game loop, we iterate over `balls` and update each. For each ball we need paddle and bricks; same for all.
- Collision detection for each ball with paddle: we need to check each ball individually. In `Ball.update` we currently assumes a single paddle variable. We'll modify `Ball.update` to accept paddle and bricks, and also maybe a reference to the ball's own properties; it's fine.
- When a ball goes below paddle (y - r > canvas.height), we remove that ball from `balls`, decrement `lives`. If lives > 0, we respawn a ball (maybe same as original). Also we need to reset its position and velocity.
- For the main ball, we also need to handle the case where ball goes below paddle after losing a life; we reset.
- For extra balls from multi-ball: we add new Ball instances to `balls`. They also can be lost; when they are lost, we decrement lives (if lives>0) and remove them. But we need to ensure we don't go below 0 lives.
- When lives reaches 0 -> gameOver = true.
- Score increments per brick broken (maybe 10 per brick). Keep score.
- Lives display: show "Lives: X".
- The paddle width when wider power-up is active: we already have `paddle.w` variable; we need to adjust its width accordingly.
- For the wider paddle timer: we set `widerPaddleTimer` to 10 seconds (600 frames if 60fps). We'll decrement each frame and restore width when zero.
- For multi-ball timer: `multiballTimer` 3 seconds (180 frames). While active, extra balls are allowed; after timer ends we remove extra balls (but keep main ball). Also we need to ensure total balls <= lives (maybe we just allow extra balls while timer active, then remove them; lives unchanged).
- Ensure that when a ball is lost (falls below paddle), we decrement lives and respawn a ball (if lives>0). Also we need to ensure that we don't exceed lives count when spawning extra balls.
- Also need to ensure that the ball never passes through bricks or paddle: our collision detection should handle that.
Now adjust code accordingly.
Potential issues: In `Ball.update`, we need to know which ball is colliding with paddle; we currently use `paddle` variable passed. That's fine.
But we need to ensure that after hitting a brick, we don't allow ball to continue moving through; we already reverse direction.
Now we need to adjust the drawing of balls: we have main ball plus extra balls. We'll loop over `balls` and draw each.
Now we need to adjust the initial paddle width: `paddle.w = 120;`.
Now we need to adjust the overlay message to show "Click or press a key to start". We'll keep that.
Now we need to ensure that after game over or victory, pressing 'R' restarts the game: we need to reset all variables (bricks, score, lives, balls, powerUps, timers). We'll implement a `resetGame()` function.
Let's rewrite parts:
- At top: `let lives = 3; let balls = []; // will hold all balls` then `balls.push(new Ball(8));` after creating ball.
- In `initBricks()` we already create bricks.
- In `resetGame()` we will reinitialize everything: bricks, score=0, lives=3, balls = [new Ball(...)]; paddle.w=120; paddle.x = (canvas.width-paddle.w)/2; powerUps=[]; widerPaddleTimer=0; multiballTimer=0; particles=[]; gameOver=false; victory=false; overlay.textContent=''.
- In the click/keydown start handling: we set demoMode = false; also we need to reset ball and paddle positions (maybe not full reset). But we can just set demoMode false and ensure ball and paddle are active.
- In the loop, after updating balls, we need to check each ball for falling below paddle. We'll loop through `balls` and for each ball, if (ball.y - ball.r > canvas.height) { // lost life
// remove ball
const idx = balls.indexOf(ball);
balls.splice(idx,1);
lives--;
if (lives > 0) {
// respawn ball
const newBall = new Ball(8);
newBall.x = canvas.width/2;
newBall.y = canvas.height/2;
newBall.vx = 3;
newBall.vy = 3;
balls.push(newBall);
} else {
gameOver = true;
}
} }
But we need to be careful: we are iterating over `balls` and modifying it; better to copy array or iterate backwards.
We'll do:
```js
for (let i = balls.length-1; i >=0; i--) {
const b = balls[i];
if (b.y - b.r > canvas.height) {
// lost life
balls.splice(i,1);
lives--;
if (lives > 0) {
const nb = new Ball(8);
nb.x = canvas.width/2;
nb.y = canvas.height/2;
nb.vx = 3;
nb.vy = 3;
balls.push(nb);
} else {
gameOver = true;
}
}
}
```
Now we also need to handle paddle width restoration after wider power-up timer.
Now adjust code for ball update: we need to pass paddle and bricks to each ball; currently `ball.update(paddle, bricks);` but we have multiple balls. We'll loop:
```js
for (let i = balls.length-1; i >=0; i--) {
const b = balls[i];
b.update(paddle, bricks);
}
```
But `Ball.update` currently uses `paddle` variable (single). That's fine; we pass same paddle.
Now need to adjust the paddle drawing: use `paddle.w` which may be changed.
Now adjust the overlay messages: we need to show lives count.
Now ensure that the code runs without errors.
Potential missing variables: `balls` array defined earlier; also need to ensure that after resetting game, we reinitialize `balls` and `paddle` etc.
Let's rewrite the script portion with these changes.
We'll also need to adjust the part where we handle power-up catch: we need to check each ball? Actually power-up is caught by any ball; we currently check `ball` (the main ball) for catch. For extra balls, we may also want them to be able to catch power-ups. So we need to loop over balls and see if any ball catches the power-up. Simpler: after moving power-ups, we can check each ball: if power-up.y + size/2 >= ball.y - ball.r && ball.x within power-up.x +/- size/2. We'll implement a function `catchPowerUp()`.
Simplify: after updating power-ups, we can loop over balls and see if any ball collides with each power-up; if so, apply effect and remove power-up.
Implementation:
```js
// after updating power-ups positions
for (let i = powerUps.length-1; i >=0; i--) {
const pu = powerUps[i];
pu.update();
// check catch with any ball
for (let j = balls.length-1; j >=0; j--) {
const b = balls[j];
if (b.y + b.r >= pu.y - pu.size/2 && b.y - b.r <= pu.y + pu.size/2 &&
b.x > pu.x - pu.size/2 && b.x < pu.x + pu.size/2) {
// apply effect
if (pu.type === 'wider') {
paddle.w = Math.min(canvas.width - 10, paddle.w * 1.5);
widerPaddleTimer = 10 * 60;
} else if (pu.type === 'multiball') {
// add extra balls up to remaining lives
const maxBalls = lives; // total balls allowed
// currently we have balls.length balls; we can add up to maxBalls - balls.length
const canAdd = Math.min(2, maxBalls - balls.length);
for (let k = 0; k < canAdd; k++) {
const eb = new Ball(8);
eb.x = b.x;
eb.y = b.y;
eb.vx = (Math.random() - 0.5) * 2;
eb.vy = (Math.random() - 0.5) * 2;
balls.push(eb);
}
}
powerUps.splice(i,1);
break; // exit inner loop
}
}
}
```
Now we need to adjust `Ball.update` to not rely on a global `ball` variable; it's fine.
Now ensure that the ball collision with paddle uses `paddle` variable (the same for all balls). In `Ball.update`, we compute offset based on `paddle.x`. That's fine.
Now ensure that after a ball is lost (falls below paddle), we also need to check if that ball collided with paddle earlier; but we already handle paddle collision before moving ball; if ball goes below paddle, it means it passed through without hitting paddle (maybe after bounce off wall). So we need to handle that.
Now we need to adjust the `demoAI` function: currently it moves paddle to ball.x - paddle.w/2; but if there are multiple balls, we might want to follow the main ball (first ball). We'll just follow the first ball (balls[0]) for AI.
Now we need to ensure that the overlay message is cleared when game starts.
Now we need to ensure that the canvas is responsive: we set canvas.width = 800; canvas.height = 600; CSS width:100%; So it's responsive.
Now we need to ensure that there are no external resources; we only use canvas and JS.
Now we need to ensure that we have at least 6 rows of bricks: we have 6 rows.
Now we need to ensure that some bricks are tough (need 2 hits). We'll set alternate rows as tough (row%2==1). That's fine.
Now we need to ensure that some bricks have power-up icon: we can assign a random chance (e.g., 20%) to have power-up when creating bricks. We'll modify initBricks to include a flag.
Let's modify initBricks:
```js
function initBricks() {
const rowHeight = 30;
const cols = 10;
const marginX = 20;
const marginY = 20;
for (let row = 0; row < 6; row++) {
for (let col = 0; col < cols; col++) {
const x = marginX + col * (canvas.width - 2*marginX) / cols - (canvas.width - 2*marginX)/2;
const y = marginY + row * rowHeight;
const health = (row % 2 === 0) ? 1 : 2; // alternate rows tough
const tough = (row % 2 === 1);
const type = tough ? 'tough' : 'normal';
// random power-up chance
const powerUpChance = Math.random() < 0.2; // 20%
const powerUpType = powerUpChance ? (Math.random() < 0.5 ? 'wider' : 'multiball') : null;
bricks.push(new Brick(x, y, (canvas.width - 2*marginX)/cols, rowHeight, health, type, powerUpType));
}
}
}
```
But Brick constructor currently has (x,y,w,h,health,type). We'll need to extend to include powerUpType. Let's modify Brick class:
```js
class Brick {
constructor(x, y, w, h, health, type='normal', powerUp=null) {
this.x = x; this.y = y; this.w = w; this.h = h;
this.health = health;
this.type = type;
this.cracked = false;
this.powerUp = powerUp; // null or 'wider'/'multiball'
}
draw() {
if (this.cracked) {
ctx.fillStyle = '#888';
} else {
ctx.fillStyle = this.type === 'tough' ? '#a0522d' : '#8b0000';
}
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
```
Now adjust Brick creation accordingly.
Now we need to adjust the code that draws power-ups: we already have powerUps array; we need to push powerUp objects when brick is broken and has powerUp.
Now ensure that we have `particles` array defined earlier; we already have.
Now we need to ensure that we have `balls` array defined; we will define `let balls = [];` at top, then after creating ball we push.
Now we need to ensure that we have `paddle` variable defined; we have.
Now we need to adjust the code for resetting after game over or victory: we need to reset `balls`, `paddle`, `bricks`, `score`, `lives`, `powerUps`, timers, particles.
Let's implement `resetGame()`:
```js
function resetGame() {
// reset bricks
bricks.length = 0;
initBricks();
// reset score
score = 0;
// reset lives
lives = 3;
// reset balls
balls = [];
balls.push(new Ball(8));
// reset paddle
paddle.w = 120;
paddle.x = (canvas.width - paddle.w) / 2;
// reset power-ups
powerUps.length = 0;
// reset timers
widerPaddleTimer = 0;
multiballTimer = 0;
// reset particles
particles.length = 0;
// reset flags
gameOver = false;
victory = false;
overlay.textContent = '';
}
```
Now we need to call `resetGame()` initially after initializing bricks? Actually we need to initialize bricks first, then call resetGame to set ball, paddle, etc. But we already have initBricks called before start. However we need to ensure that after game over/victory we call resetGame.
Now we need to adjust the start handling: currently we set demoMode false and reset ball position etc. But we also need to ensure that we have correct ball count (lives). Since lives = 3, we need to have 3 balls initially? Wait requirement: 3 lives. Starting with 1 ball means you have 2 lives left. But maybe we should start with 3 balls (3 lives). Let's read requirement: "3 lives". Usually breakout gives you 3 balls (lives). So we should start with 3 balls (i.e., lives = 3, and balls array contains 3 balls). Let's adjust: At start, we create 3 balls with same initial position but maybe different velocities? Or we can just create 3 identical balls; they will all move together initially. Simpler: create 3 balls with same position and same velocity; they will act as separate lives. When any ball falls, we lose a life (decrement lives) and remove that ball; if lives >0 we respawn that ball (maybe same as original). But we need to keep track of lives count; initially lives = 3, balls = [ball1, ball2, ball3]; each ball has its own position/velocity; we can randomize velocities slightly.
Simplify: create 3 balls with same position but different velocities (random). But for demo mode, we may want them to move together; but okay.
Implementation: after initBricks, we can call resetGame which will set lives=3 and create 3 balls.
Let's modify resetGame to create 3 balls:
```js
function resetGame() {
bricks.length = 0;
initBricks();
score = 0;
lives = 3;
// create 3 balls
balls = [];
for (let i = 0; i < 3; i++) {
const b = new Ball(8);
b.x = canvas.width/2;
b.y = canvas.height/2;
// random slight velocity differences
b.vx = (Math.random() - 0.5) * 2;
b.vy = (Math.random() - 0.5) * 2;
balls.push(b);
}
paddle.w = 120;
paddle.x = (canvas.width - paddle.w) / 2;
powerUps.length = 0;
widerPaddleTimer = 0;
multiballTimer = 0;
particles.length = 0;
gameOver = false;
victory = false;
overlay.textContent = '';
}
```
Now at the very beginning after canvas setup we call `resetGame();` to initialize.
Now we need to adjust the start overlay: when demoMode is true, we show "Click or press a key to start". When user clicks or presses a key, we set demoMode = false; also we may want to ensure that the ball is moving (maybe already moving). In demo mode, the AI paddle follows ball; the ball moves automatically. So we need to ensure that ball is moving in demo mode. In our loop, we call demoAI which moves paddle, but ball.update is called each frame regardless of demoMode, so ball will move. So fine.
Now we need to ensure that after clicking, the user can control paddle via mouse or keys; we already have mousemove and keydown listeners that only act when demoMode is false. In demoMode true, they are ignored.
Now we need to ensure that the game ends correctly: when lives reach 0, gameOver = true; overlay shows "Game Over". Also we need to allow restart via 'R' key.
Now we need to ensure that the code runs with zero errors. Let's test mentally.
Potential errors:
- `paddle` variable may be undefined if we refer to it before initialization. We'll initialize paddle before resetGame.
- `balls` array defined before use.
- `particles` array defined.
- `score`, `lives` defined.
- `widerPaddleTimer`, `multiballTimer` defined.
- `canvas` width/height set.
- `ctx` defined.
- `overlay` defined.
- `demoMode` defined.
- `gameOver`, `victory` defined.
Now ensure that we don't have any stray references to `ball` (singular) after we switched to array. In many places we used `ball` variable; we need to replace with `balls[0]` maybe. Let's examine:
- In `demoAI()` we used `ball.x` to follow. Should be `balls[0].x`. Let's adjust:
```js
function demoAI() {
if (!demoMode) return;
// follow the first ball
const b = balls[0];
paddle.x = b.x - paddle.w/2;
paddle.x = Math.max(0, Math.min(paddle.x, canvas.width - paddle.w));
}
```
- In ball update loop: we currently have `for (let i = balls.length-1; i >=0; i--) { const b = balls[i]; b.update(paddle, bricks); }`. That's fine.
- In power-up catch detection: we loop over balls; we used `b` variable; fine.
- In the check for ball below paddle: we loop over balls and check each; fine.
- In the paddle collision detection inside Ball.update: we refer to `paddle` variable (the single paddle). That's fine.
- In the brick collision detection inside Ball.update: we refer to `bricks` array; fine.
- In the draw loop: we draw paddle once (paddle.draw()); then we draw each ball: `ball.draw();` but we need to draw all balls. So replace `ball.draw();` with loop:
```js
paddle.draw();
for (let i = 0; i < balls.length; i++) {
balls[i].draw();
}
```
- Also need to draw extra balls (they are part of balls array). So that's fine.
- In UI: we display lives: `ctx.fillText(`Lives: ${lives}`, 10, 40);` Good.
- In the overlay messages: we need to show lives maybe not needed; but we already show lives.
- In the restart handling: we need to listen for key 'r' when gameOver or victory is true. We'll add a keydown listener for 'r' that calls resetGame().
Let's add:
```js
window.addEventListener('keydown', e => {
if ((gameOver || victory) && e.key.toLowerCase() === 'r') {
resetGame();
}
});
```
But we also have earlier a keydown listener that starts the game if demoMode is true. That's fine; we need to ensure that after game over, pressing r resets.
Now we need to ensure that we don't have duplicate listeners causing conflict. The earlier keydown listener sets demoMode false and resets ball/paddle; after game over, pressing r will call resetGame which reinitializes everything, including balls, paddle, etc., and also sets demoMode? Not necessarily; we need to ensure demoMode remains false after game over, else the start overlay may reappear. In resetGame we don't touch demoMode; it stays false after user interaction. That's fine.
Now we need to ensure that after resetting, the overlay message disappears (we set overlay.textContent = '').
Now we need to ensure that the code doesn't reference `ball` (singular) after we changed to array; we must replace all occurrences.
Let's scan the code for `ball` usage:
- In `demoAI()` we used `ball.x`. Change to `balls[0].x`.
- In `resetGame()` we create 3 balls; we set `balls.push(new Ball(8));` only once; need to change to loop.
- In the loop where we update balls: we currently have `ball.update(paddle, bricks);` but need to loop.
- In the power-up catch detection we used `ball` variable; need to change to loop over balls.
- In the ball below paddle detection we used `ball.y - ball.r > canvas.height`; need to loop.
- In the UI we used `ball` maybe not; we used `ball` only in earlier code.
- In the initial ball creation: we had `const ball = new Ball(8);` but later we used `ball` variable. We need to replace with `balls = []; balls.push(new Ball(8));` etc.
- In the `demoAI` we used `ball.x`; adjust.
- In the `Ball.update` method, we refer to `paddle` variable (single) and `bricks`. That's fine.
- In the part where we handle ball falling below paddle: we currently have `if (ball.y - ball.r > canvas.height) { lives--; ... }`. Need to loop.
- In the part where we handle power-up catch: we used `if (ball.y + ball.r >= pu.y - pu.size/2 && ...)`. Need to loop.
- In the part where we handle extra balls from multi-ball: we used `balls.push(eb);` fine.
- In the draw loop: we have `ball.draw();` need to replace with loop.
- In the UI we have `ball` maybe not.
- In the `demoAI` we also used `ball` for following; need to adjust.
Now we need to adjust the code accordingly.
Let's rewrite the relevant sections with proper variable names.
Also need to ensure that `paddle` variable is defined before use. We'll define it before `resetGame()`.
Now let's rewrite the script portion step by step.
We'll start with HTML skeleton, then script.
We'll include CSS as before.
Now script:
```html
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
function resize() {
canvas.width = 800;
canvas.height = 600;
}
window.addEventListener('resize', resize);
resize();
let demoMode = true;
let gameOver = false;
let victory = false;
// Game state
let score = 0;
let lives = 3;
let widerPaddleTimer = 0;
let multiballTimer = 0;
let particles = [];
// Game objects
class Paddle {
constructor(w, h) {
this.w = w; this.h = h;
this.x = 0; this.y = 0;
}
draw() {
ctx.fillStyle = '#0f0';
ctx.fillRect(this.x, this.y, this.w, this.h);
}
// AI control (demo mode)
followBall() {
if (!demoMode) return;
const b = balls[0];
this.x = b.x - this.w/2;
this.x = Math.max(0, Math.min(this.x, canvas.width - this.w));
}
// Player control
move(dx) {
this.x += dx;
this.x = Math.max(0, Math.min(this.x, canvas.width - this.w));
}
}
class Ball {
constructor(r) {
this.r = r;
this.x = canvas.width/2;
this.y = canvas.height/2;
this.vx = 3;
this.vy = 3;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI*2);
ctx.fillStyle = '#ff0';
ctx.fill();
}
update(paddle, bricks) {
this.x += this.vx;
this.y += this.vy;
// wall collisions
if (this.x - this.r <= 0) {
this.x = this.r;
this.vx = -this.vx;
}
if (this.x + this.r >= canvas.width) {
this.x = canvas.width - this.r;
this.vx = -this.vx;
}
if (this.y - this.r <= 0) {
this.y = this.r;
this.vy = -this.vy;
}
// paddle collision
if (this.y + this.r >= paddle.y && this.x > paddle.x && this.x < paddle.x + paddle.w) {
const offset = (this.x + this.r - (paddle.x + paddle.w/2)) / (paddle.w/2);
const maxAngle = Math.PI/4;
const angle = offset * maxAngle;
const speed = Math.hypot(this.vx, this.vy);
this.vx = speed * Math.cos(angle);
this.vy = -speed * Math.sin(angle);
this.y = paddle.y - this.r;
}
// brick collisions
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
const ballLeft = this.x - this.r;
const ballRight = this.x + this.r;
const ballTop = this.y - this.r;
const ballBottom = this.y + this.r;
if (ballRight > b.x && ballLeft < b.x + b.w && ballBottom > b.y && ballTop < b.y + b.h) {
// resolve penetration
const dx = Math.min(b.x + b.w - this.x, this.x + this.r - b.x);
const dy = Math.min(b.y + b.h - this.y, this.y + this.r - b.y);
if (dx < dy) {
// horizontal
if (this.x + this.r > b.x + b.w) this.x = b.x + b.w - this.r;
else this.x = b.x + this.r;
this.vx = -this.vx;
} else {
// vertical
if (this.y + this.r > b.y + b.h) {
this.y = b.y + b.h - this.r;
this.vy = -this.vy;
} else {
this.y = b.y - this.r;
this.vy = -this.vy;
}
}
// break brick
if (b.health > 0) {
b.health--;
if (b.health === 0) {
// brick broken
particles.push(...createParticles(b.x, b.y, b.w, b.h));
if (b.powerUp) {
powerUps.push(new PowerUp(b.x + b.w/2, b.y + b.h/2, b.powerUp));
}
bricks.splice(i, 1);
score += 10;
} else if (b.health === 1 && b.type === 'tough') {
b.cracked = true;
}
}
break;
}
}
}
}
class Brick {
constructor(x, y, w, h, health, type='normal', powerUp=null) {
this.x = x; this.y = y; this.w = w; this.h = h;
this.health = health;
this.type = type;
this.cracked = false;
this.powerUp = powerUp;
}
draw() {
if (this.cracked) {
ctx.fillStyle = '#888';
} else {
ctx.fillStyle = this.type === 'tough' ? '#a0522d' : '#8b0000';
}
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
class PowerUp {
constructor(x, y, type) {
this.x = x; this.y = y; this.type = type;
this.vy = 2;
this.size = 10;
}
draw() {
ctx.fillStyle = this.type === 'wider' ? '#0ff' : '#f0f';
ctx.fillRect(this.x - this.size/2, this.y - this.size/2, this.size, this.size);
}
update() {
this.y += this.vy;
}
}
function createParticles(x, y, w, h) {
const particles = [];
const color = '#fff';
const size = 3;
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 3;
particles.push(new Particle(x + w/2, y + h/2, size, color));
}
return particles;
}
class Particle {
constructor(x, y, size, color) {
this.x = x; this.y = y; this.size = size; this.color = color;
this.alpha = 1;
this.life = 10;
}
update() {
this.y -= 0.5;
this.alpha -= 0.05;
this.life--;
}
draw() {
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
// init bricks
function initBricks() {
const rowHeight = 30;
const cols = 10;
const marginX = 20;
const marginY = 20;
for (let row = 0; row < 6; row++) {
for (let col = 0; col < cols; col++) {
const x = marginX + col * (canvas.width - 2*marginX) / cols - (canvas.width - 2*marginX)/2;
const y = marginY + row * rowHeight;
const health = (row % 2 === 0) ? 1 : 2; // alternate rows tough
const tough = (row % 2 === 1);
const type = tough ? 'tough' : 'normal';
const powerUpChance = Math.random() < 0.2;
const powerUpType = powerUpChance ? (Math.random() < 0.5 ? 'wider' : 'multiball') : null;
bricks.push(new Brick(x, y, (canvas.width - 2*marginX)/cols, rowHeight, health, type, powerUpType));
}
}
}
const paddle = new Paddle(120, 15);
let balls = []; // will be filled in resetGame
initBricks();
resetGame(); // initialise game state
// Input handling
canvas.addEventListener('mousemove', e => {
if (!demoMode) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
paddle.x = mx - paddle.w/2;
paddle.x = Math.max(0, Math.min(paddle.x, canvas.width - paddle.w));
}
});
canvas.addEventListener('keydown', e => {
if (demoMode) {
demoMode = false;
// reset ball and paddle positions
balls = [];
const b = new Ball(8);
b.x = canvas.width/2;
b.y = canvas.height/2;
b.vx = 3;
b.vy = 3;
balls.push(b);
paddle.x = (canvas.width - paddle.w)/2;
} else {
const step = 6;
if (e.key === 'ArrowLeft') {
paddle.move(-step);
} else if (e.key === 'ArrowRight') {
paddle.move(step);
}
}
});
canvas.addEventListener('click', () => {
if (demoMode) {
demoMode = false;
// reset ball and paddle
balls = [];
const b = new Ball(8);
b.x = canvas.width/2;
b.y = canvas.height/2;
b.vx = 3;
b.vy = 3;
balls.push(b);
paddle.x = (canvas.width - paddle.w)/2;
}
});
// Game loop
function loop(timestamp) {
demoAI();
// update balls
for (let i = balls.length-1; i >=0; i--) {
const b = balls[i];
b.update(paddle, bricks);
}
// update power-ups and check catch
for (let i = powerUps.length-1; i >=0; i--) {
const pu = powerUps[i];
pu.update();
// check catch with any ball
for (let j = balls.length-1; j >=0; j--) {
const b = balls[j];
if (b.y + b.r >= pu.y - pu.size/2 && b.y - b.r <= pu.y + pu.size/2 &&
b.x > pu.x - pu.size/2 && b.x < pu.x + pu.size/2) {
if (pu.type === 'wider') {
paddle.w = Math.min(canvas.width - 10, paddle.w * 1.5);
widerPaddleTimer = 10 * 60;
} else if (pu.type === 'multiball') {
const maxBalls = lives; // total balls allowed
const canAdd = Math.min(2, maxBalls - balls.length);
for (let k = 0; k < canAdd; k++) {
const eb = new Ball(8);
eb.x = b.x;
eb.y = b.y;
eb.vx = (Math.random() - 0.5) * 2;
eb.vy = (Math.random() - 0.5) * 2;
balls.push(eb);
}
}
powerUps.splice(i,1);
break;
}
}
}
// update timers
if (widerPaddleTimer > 0) widerPaddleTimer--;
if (multiballTimer > 0) multiballTimer--;
// check ball loss
for (let i = balls.length-1; i >=0; i--) {
const b = balls[i];
if (b.y - b.r > canvas.height) {
// lose a life
balls.splice(i,1);
lives--;
if (lives > 0) {
const nb = new Ball(8);
nb.x = canvas.width/2;
nb.y = canvas.height/2;
nb.vx = 3;
nb.vy = 3;
balls.push(nb);
} else {
gameOver = true;
}
}
}
// draw everything
ctx.clearRect(0,0,canvas.width,canvas.height);
// bricks
bricks.forEach(b => b.draw());
// power-ups
powerUps.forEach(p => p.draw());
// particles
particles.forEach(p => {
p.update();
p.draw();
});
// paddle and balls
paddle.draw();
for (let i = 0; i < balls.length; i++) {
balls[i].draw();
}
// UI
ctx.fillStyle = '#fff';
ctx.font = '16px sans-serif';
ctx.fillText(`Score: ${score}`, 10, 20);
ctx.fillText(`Lives: ${lives}`, 10, 40);
// overlay messages
if (demoMode) {
overlay.textContent = 'Click or press a key to start';
} else if (gameOver) {
overlay.textContent = 'Game Over. Press R to restart';
} else if (victory) {
overlay.textContent = 'You Win! Press R to restart';
}
}
// Demo AI
function demoAI() {
if (!demoMode) return;
const b = balls[0];
paddle.x = b.x - paddle.w/2;
paddle.x = Math.max(0, Math.min(paddle.x, canvas.width - paddle.w));
}
// Reset game
function resetGame() {
// reset bricks
bricks.length = 0;
initBricks();
// reset score
score = 0;
// reset lives
lives = 3;
// reset balls
balls = [];
const b = new Ball(8);
b.x = canvas.width/2;
b.y = canvas.height/2;
b.vx = 3;
b.vy = 3;
balls.push(b);
// reset paddle
paddle.w = 120;
paddle.x = (canvas.width - paddle.w) / 2;
// reset power-ups
powerUps.length = 0;
// reset timers
widerPaddleTimer = 0;
multiballTimer = 0;
// reset particles
particles.length = 0;
// reset flags
gameOver = false;
victory = false;
overlay.textContent = '';
}
// Start the loop
requestAnimationFrame(loop);
// Restart on 'R' after game over or victory
window.addEventListener('keydown', e => {
if ((gameOver || victory) && e.key.toLowerCase() === 'r') {
resetGame();
}
});
</script>
```
Now we need to ensure that `bricks`, `powerUps`, `particles`, `paddle`, `balls`, `score`, `lives`, `widerPaddleTimer`, `multiballTimer` are declared globally before use. In the script we have them defined before use (paddle, balls, etc). However we need to declare `let bricks = []; let powerUps = []; let particles = [];` etc. Let's add those declarations at top after canvas setup.
Let's adjust: after `const overlay = document.getElementById('overlay');` we can add:
```js
let bricks = [];
let powerUps = [];
let particles = [];
```
Also need to declare `let paddle;` before using. We'll define paddle after objects definitions, but before `initBricks();` we need to have paddle defined. In current code we have `const paddle = new Paddle(120, 15);` before initBricks. That's fine.
Now ensure that `balls` is declared before resetGame uses it. We'll add `let balls = [];` after the other declarations.
Now ensure that `score`, `lives`, `widerPaddleTimer`, `multiballTimer` are declared.
Let's add:
```js
let score = 0;
let lives = 3;
let widerPaddleTimer = 0;
let multiballTimer = 0;
```
Now we need to ensure that `demoMode`, `gameOver`, `victory` are declared.
Add:
```js
let demoMode = true;
let gameOver = false;
let victory = false;
```
Now we need to ensure that `canvas` width/height are set before using in `Ball` constructor (ball.x = canvas.width/2). That's fine after resize.
Now we need to ensure that `ctx` is defined before drawing.
Now we need to ensure that `overlay` is defined.
Now we need to ensure that we have `requestAnimationFrame` loop.
Now we need to ensure that we have `window.addEventListener('keydown', ...)` for restart.
Now we need to ensure that we have `canvas.addEventListener('click', ...)` for start.
Now we need to ensure that we have `canvas.addEventListener('mousemove', ...)` for paddle control.
Now we need to ensure that we have `canvas.addEventListener('keydown', ...)` for start (demo mode) and for player control.
Now we need to ensure that we have `resetGame()` called initially after setting up bricks.
Now we need to ensure that we have `demoAI()` called each frame.
Now we need to ensure that we have `requestAnimationFrame(loop);` at end.
Now we need to ensure that we have no stray references to `ball` singular.
Now let's rewrite the script with proper declarations and ensure all variables are defined.
We'll produce final code with all these declarations.
Potential small issues: In `demoAI()` we used `paddle.followBall()` but we changed to direct setting. Let's adjust.
Now let's rewrite the script fully with all declarations and ensure consistency.
We'll produce final HTML file.