gpt-oss-120b
by OpenAIReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 4 min 31 s
- Cost
- $0.0028
- Output tokens
- 23,812
- Spent thinking
- 19,810 tokens
- Speed
- 88.3 tok/s
- Console errors
- 1
- First token after
- 795 ms
- First visible text after
- 3 min 51 s
- Prompt tokens
- 572
- Code size
- 13.4 KB
- Finish reason
- stop
- Served by
- PrimeIntellect
- Model id
openai/gpt-oss-120b- Reasoning sent
{"effort":"high"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
Failed to execute 'arc' on 'CanvasRenderingContext2D': The radius provided (-2.28) is negative.
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 · 497 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Breakout Demo</title>
<style>
html,body{
margin:0;padding:0;height:100%;overflow:hidden;
background:#111;color:#fff;font-family:Arial,Helvetica,sans-serif;
display:flex;justify-content:center;align-items:center;
}
canvas{
border:2px solid #555;background:#222;
max-width:100%;width:100%;height:auto;display:block;
}
#msg{
position:fixed;inset:0;
display:flex;justify-content:center;align-items:center;
background:rgba(0,0,0,0.7);color:#fff;font-size:2rem;
text-align:center;pointer-events:none;
}
#msg.hidden{display:none;}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="msg" class="hidden"></div>
<script>
(()=>{
// ---------- Global variables ----------
let canvas, ctx, WIDTH, HEIGHT;
// Game constants
const ROWS = 6;
const COLS = 10;
const BRICK_PADDING = 4;
const BRICK_OFFSET_TOP = 60;
const BRICK_OFFSET_LEFT = 20;
const POWERUP_PROB = 0.12; // 12% of bricks carry a power‑up
const TOUGH_PROB = 0.2; // 20% of bricks need 2 hits
// ---------- Game state ----------
const state = {
started: false,
isDemo: true,
paddle: null,
balls: [],
bricks: [],
powerUps: [],
particles: [],
lives: 3,
score: 0,
gameOver: false,
victory: false,
ballSpeed: 250, // will be recalculated
paddleSpeed: 500 // will be recalculated
};
// ---------- Initialization ----------
function init(){
canvas=document.getElementById('gameCanvas');
ctx=canvas.getContext('2d');
resizeCanvas();
initGameObjects();
attachEvents();
showOverlay('Click or press any key to start');
requestAnimationFrame(gameLoop);
}
// Resize canvas to fit viewport, keep 4:3 ratio
function resizeCanvas(){
const maxW=Math.min(600,window.innerWidth*0.9);
const maxH=maxW*0.75;
canvas.width=maxW;
canvas.height=maxH;
WIDTH=canvas.width; HEIGHT=canvas.height;
}
// Create paddle, balls, bricks, etc.
function initGameObjects(){
// ---- Paddle ----
const pw=WIDTH*0.15;
const ph=12;
state.paddle={
x:(WIDTH-pw)/2,
y:HEIGHT-ph-10,
width:pw,
height:ph,
originalWidth:pw,
speed:WIDTH*1.5,
moveLeft:false,
moveRight:false,
wideTimer:0
};
state.paddleSpeed=state.paddle.speed;
// ---- Ball speed ----
state.ballSpeed=WIDTH*0.35;
// ---- Lives / Score ----
state.lives=3; state.score=0;
state.gameOver=false; state.victory=false;
// ---- Containers ----
state.balls=[]; state.bricks=[]; state.powerUps=[]; state.particles=[];
// ---- Bricks ----
createBricks();
// ---- Initial ball ----
spawnBall();
}
// Build a 6‑row brick layout with mixed durability and power‑ups
function createBricks(){
const brickW=(WIDTH-2*BRICK_OFFSET_LEFT-(COLS-1)*BRICK_PADDING)/COLS;
const brickH=14;
const colors={normal:'#0088CC',tough:'#B22222',cracked:'#F08080'};
for(let r=0;r<ROWS;r++){
for(let c=0;c<COLS;c++){
const x=BRICK_OFFSET_LEFT+c*(brickW+BRICK_PADDING);
const y=BRICK_OFFSET_TOP+r*(brickH+BRICK_PADDING);
const isTough=Math.random()<TOUGH_PROB;
const maxHits=isTough?2:1;
const hasPower=Math.random()<POWERUP_PROB;
const power=hasPower?(Math.random()<0.5?'wide':'multi'):null;
state.bricks.push({
x,y,width:brickW,height:brickH,
maxHits, hits:maxHits,
alive:true,
baseColor:isTough?colors.tough:colors.normal,
crackedColor:colors.cracked,
color:isTough?colors.tough:colors.normal,
power
});
}
}
}
// Spawn a single ball centered on the paddle with a random upward angle
function spawnBall(){
const r=8;
const sx=state.paddle.x+state.paddle.width/2;
const sy=state.paddle.y-r-2;
const angle=(Math.random()<0.5?-1:1)*Math.PI/4; // ±45°
const dx=state.ballSpeed*Math.sin(angle);
const dy=-state.ballSpeed*Math.cos(angle);
state.balls.push({x:sx,y:sy,radius:r,speed:state.ballSpeed,dx,dy,color:'#FFF'});
}
// ---------- Overlay UI ----------
function showOverlay(html){
const el=document.getElementById('msg');
el.innerHTML=html;
el.classList.remove('hidden');
}
function hideOverlay(){
document.getElementById('msg').classList.add('hidden');
}
// ---------- Event handling ----------
function attachEvents(){
// Mouse paddle control (only after player starts)
canvas.addEventListener('mousemove',e=>{
if(!state.started||state.isDemo)return;
const rect=canvas.getBoundingClientRect();
const mx=e.clientX-rect.left;
let nx=mx-state.paddle.width/2;
nx=Math.max(0,Math.min(WIDTH-state.paddle.width,nx));
state.paddle.x=nx;
});
// Keyboard controls + start trigger
document.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft'||e.key==='ArrowRight')e.preventDefault();
if(!state.started) startPlayer();
else{
if(e.key==='ArrowLeft') state.paddle.moveLeft=true;
if(e.key==='ArrowRight') state.paddle.moveRight=true;
}
});
document.addEventListener('keyup',e=>{
if(!state.started)return;
if(e.key==='ArrowLeft') state.paddle.moveLeft=false;
if(e.key==='ArrowRight') state.paddle.moveRight=false;
});
// Click to start or restart
document.addEventListener('click',()=>{
if(!state.started) startPlayer();
else if(state.gameOver||state.victory) resetGame();
});
}
// Transition from demo to player control
function startPlayer(){
if(state.gameOver||state.victory)return;
state.started=true;
state.isDemo=false;
hideOverlay();
}
// Reset everything for a fresh game
function resetGame(){
initGameObjects();
state.isDemo=true;
state.started=false;
showOverlay('Click or press any key to start');
}
// ---------- Game loop ----------
let lastTime=performance.now();
function gameLoop(ts){
const dt=Math.min((ts-lastTime)/1000,0.02); // cap delta for stability
update(dt);
render();
lastTime=ts;
requestAnimationFrame(gameLoop);
}
// ---------- Update ----------
function update(dt){
// ---- Demo AI paddle ----
if(state.isDemo && state.balls.length){
const target=state.balls[0];
const desiredX=target.x-state.paddle.width/2;
if(Math.abs(state.paddle.x-desiredX)>2){
const dir=desiredX>state.paddle.x?1:-1;
state.paddle.x+=dir*state.paddle.speed*dt;
state.paddle.x=Math.max(0,Math.min(WIDTH-state.paddle.width,state.paddle.x));
}
}
// ---- Player paddle movement ----
if(state.paddle.moveLeft) state.paddle.x-=state.paddle.speed*dt;
if(state.paddle.moveRight) state.paddle.x+=state.paddle.speed*dt;
state.paddle.x=Math.max(0,Math.min(WIDTH-state.paddle.width,state.paddle.x));
// ---- Wide paddle timer ----
if(state.paddle.wideTimer>0){
state.paddle.wideTimer-=dt;
if(state.paddle.wideTimer<=0){
state.paddle.wideTimer=0;
state.paddle.width=state.paddle.originalWidth;
state.paddle.x=Math.max(0,Math.min(WIDTH-state.paddle.width,state.paddle.x));
}
}
// ---- Balls ----
for(let i=state.balls.length-1;i>=0;i--){
updateBall(state.balls[i],dt);
}
// ---- Power‑ups ----
for(let i=state.powerUps.length-1;i>=0;i--){
const pu=state.powerUps[i];
pu.y+=pu.vy*dt;
// Catch?
if(
pu.y+pu.radius>state.paddle.y &&
pu.y-pu.radius<state.paddle.y+state.paddle.height &&
pu.x+pu.radius>state.paddle.x &&
pu.x-pu.radius<state.paddle.x+state.paddle.width
){
applyPowerUp(pu.type);
state.powerUps.splice(i,1);
}else if(pu.y-pu.radius>HEIGHT){
state.powerUps.splice(i,1);
}
}
// ---- Particles ----
for(let i=state.particles.length-1;i>=0;i--){
const p=state.particles[i];
p.x+=p.vx*dt; p.y+=p.vy*dt;
p.life+=dt;
if(p.life>=p.maxLife) state.particles.splice(i,1);
}
// ---- Victory check ----
if(!state.victory && state.bricks.every(b=>!b.alive)){
state.victory=true;
showOverlay('Victory!<br>Click to restart');
}
}
// Ball physics, collisions with walls/paddle/bricks
function updateBall(ball,dt){
let nx=ball.x+ball.dx*dt;
let ny=ball.y+ball.dy*dt;
// Left / right walls
if(nx-ball.r<0){ nx=ball.r; ball.dx=Math.abs(ball.dx); }
else if(nx+ball.r>WIDTH){ nx=WIDTH-ball.r; ball.dx=-Math.abs(ball.dx); }
// Top wall
if(ny-ball.r<0){ ny=ball.r; ball.dy=Math.abs(ball.dy); }
// Paddle (only when moving down)
if(
ball.dy>0 &&
ny+ball.r>state.paddle.y &&
ny-ball.r<state.paddle.y+state.paddle.height &&
nx+ball.r>state.paddle.x &&
nx-ball.r<state.paddle.x+state.paddle.width
){
const hit=((nx-(state.paddle.x+state.paddle.width/2))/(state.paddle.width/2));
const maxAngle=Math.PI*5/12; //75°
const angle=hit*maxAngle;
ball.dx=ball.speed*Math.sin(angle);
ball.dy=-ball.speed*Math.cos(angle);
ny=state.paddle.y-ball.r-1;
}
// Bricks
for(let i=0;i<state.bricks.length;i++){
const br=state.bricks[i];
if(!br.alive)continue;
if(
nx+ball.r>br.x && nx-ball.r<br.x+br.width &&
ny+ball.r>br.y && ny-ball.r<br.y+br.height
){
// Determine the side of impact
const overlapL=nx+ball.r-br.x;
const overlapR=br.x+br.width-(nx-ball.r);
const overlapT=ny+ball.r-br.y;
const overlapB=br.y+br.height-(ny-ball.r);
const minO=Math.min(overlapL,overlapR,overlapT,overlapB);
if(minO===overlapL){
ball.dx=-Math.abs(ball.dx); nx=br.x-ball.r;
}else if(minO===overlapR){
ball.dx=Math.abs(ball.dx); nx=br.x+br.width+ball.r;
}else if(minO===overlapT){
ball.dy=-Math.abs(ball.dy); ny=br.y-ball.r;
}else{
ball.dy=Math.abs(ball.dy); ny=br.y+br.height+ball.r;
}
handleBrickHit(br);
break; // one brick per frame
}
}
// Apply new position
ball.x=nx; ball.y=ny;
// Bottom out
if(ball.y-ball.r>HEIGHT){
const idx=state.balls.indexOf(ball);
if(idx>-1) state.balls.splice(idx,1);
state.lives--;
if(state.lives<=0){
state.gameOver=true;
showOverlay('Game Over<br>Click to restart');
}else{
if(state.balls.length===0) spawnBall();
}
}
}
// Brick hit logic, scoring, particles, power‑up drops
function handleBrickHit(br){
br.hits--;
if(br.maxHits===2 && br.hits===1){
br.color=br.crackedColor;
}
if(br.hits<=0){
br.alive=false;
state.score+=br.maxHits===1?10:20;
spawnParticles(br.x+br.width/2, br.y+br.height/2, br.baseColor);
if(br.power) spawnPowerUp(br.x+br.width/2, br.y+br.height/2, br.power);
}
}
// Simple particle burst
function spawnParticles(x,y,col){
for(let i=0;i<15;i++){
const ang=Math.random()*Math.PI*2;
const spd=Math.random()*60+40;
state.particles.push({
x,y,
vx:Math.cos(ang)*spd,
vy:Math.sin(ang)*spd,
life:0,
maxLife:0.6,
size:2+Math.random()*2,
color:col
});
}
}
// Power‑up drop
function spawnPowerUp(x,y,type){
state.powerUps.push({x,y,radius:8,vy:100,type});
}
// Apply power‑up effect
function applyPowerUp(type){
if(type==='wide'){
if(state.paddle.wideTimer<=0){
state.paddle.width=state.paddle.originalWidth*1.5;
state.paddle.x=Math.max(0,Math.min(WIDTH-state.paddle.width,state.paddle.x));
}
state.paddle.wideTimer=10; // seconds
}else if(type==='multi'){
for(let i=0;i<2;i++){
const ang=(Math.random()*Math.PI/3)-Math.PI/6; // -30°..30°
const dx=state.ballSpeed*Math.sin(ang);
const dy=-state.ballSpeed*Math.cos(ang);
state.balls.push({
x:state.paddle.x+state.paddle.width/2,
y:state.paddle.y-10,
radius:8,
speed:state.ballSpeed,
dx,dy,
color:'#FFF'
});
}
}
}
// ---------- Rendering ----------
function render(){
ctx.clearRect(0,0,WIDTH,HEIGHT);
// Background
ctx.fillStyle='#222';
ctx.fillRect(0,0,WIDTH,HEIGHT);
// Bricks
state.bricks.forEach(br=>{
if(!br.alive)return;
ctx.fillStyle=br.color;
ctx.fillRect(br.x,br.y,br.width,br.height);
ctx.strokeStyle='#111';
ctx.strokeRect(br.x,br.y,br.width,br.height);
if(br.power){
ctx.fillStyle=br.power==='wide'?'#FFD700':'#00FFFF';
const rad=Math.min(br.width,br.height)*0.3;
ctx.beginPath();
ctx.arc(br.x+br.width/2,br.y+br.height/2,rad,0,2*Math.PI);
ctx.fill();
ctx.fillStyle='#000';
ctx.font=`${rad}px Arial`;
ctx.textAlign='center';
ctx.textBaseline='middle';
ctx.fillText(br.power==='wide'?'W':'M',br.x+br.width/2,br.y+br.height/2);
}
});
// Paddle
ctx.fillStyle='#EEE';
ctx.fillRect(state.paddle.x,state.paddle.y,state.paddle.width,state.paddle.height);
ctx.strokeStyle='#AAA';
ctx.strokeRect(state.paddle.x,state.paddle.y,state.paddle.width,state.paddle.height);
// Balls
state.balls.forEach(b=>{
ctx.fillStyle=b.color;
ctx.beginPath();
ctx.arc(b.x,b.y,b.radius,0,2*Math.PI);
ctx.fill();
});
// Power‑ups
state.powerUps.forEach(pu=>{
ctx.fillStyle=pu.type==='wide'?'#FFD700':'#00FFFF';
ctx.beginPath();
ctx.arc(pu.x,pu.y,pu.radius,0,2*Math.PI);
ctx.fill();
ctx.fillStyle='#000';
ctx.font=`${pu.radius}px Arial`;
ctx.textAlign='center';
ctx.textBaseline='middle';
ctx.fillText(pu.type==='wide'?'W':'M',pu.x,pu.y);
});
// Particles
state.particles.forEach(p=>{
const a=1-p.life/p.maxLife;
ctx.globalAlpha=a;
ctx.fillStyle=p.color;
ctx.beginPath();
ctx.arc(p.x,p.y,p.size,0,2*Math.PI);
ctx.fill();
ctx.globalAlpha=1;
});
// HUD
ctx.fillStyle='#FFF';
ctx.font='16px Arial';
ctx.textAlign='left';
ctx.fillText(`Score: ${state.score}`,10,20);
ctx.textAlign='right';
ctx.fillText(`Lives: ${state.lives}`,WIDTH-10,20);
}
// ---------- Start ----------
window.addEventListener('load',init);
})(); // end IIFE
</script>
</body>
</html>Reasoning trace (19,810 tokens)
The task: Build a polished Breakout (brick breaker) game in single HTML file using vanilla JavaScript and a <canvas>. Requirements include:
- Paddle controlled with mouse and left/right arrow keys.
- Bounce angle off paddle depends on where 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 have a power-up icon; when broken they drop it, and catching it with paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls).
- Ball must never pass through bricks or paddle (collision detection; handle tunneling).
- Particles when a brick breaks.
- 3 lives, score displayed. Game over and victory screens with restart option.
Constraints: All code should be inline. Must be self-contained. Must be responsive for 1280x800 down to 375px. Voice: no external network requests unless explicitly allowed. Should be in one fenced code block containing HTML. Should not use disallowed features, but can use requestAnimationFrame, canvas, events, etc. Must be robust. Should display something visible on first paint: the game arena with paddles and a start message.
Also must include "Click or press a key to start" message, and for keyboard events need to give that prompt. Also provide auto demonstration: We should implement a simple AI controlling the paddle to follow ball until start, or maybe start with demo AI after pressing? The instructions for games: "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". So there must be an AI playing the game automatically while the start screen is displayed: maybe the AI controls the paddle and catches power-ups, etc. Or the AI only after pressing? The requirement: a self-playing demo mode behind the start message. So the game screen is visible, the game starts automatically in AI mode but the UI says "Click or press a key to start". After clicking, the game should become interactive with the player controlling the paddle.
Thus we need to manage two modes: demo mode (AI) and player mode. The start screen overlays a message, maybe a semi-transparent overlay that says "Click or press a key to start". The game starts anyway (ball moving, paddle AI moving). When user clicks or presses any key, the overlay disappears and controls shift to player (mouse or keyboard). That satisfies the requirement.
Alternatively we could have the demo mode start after clicking. But spec says "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". So preview is visible while AI is playing, user can start playing. We should ensure demonstration is visible on screenshot after 3 seconds. Must have something moving: ball bouncing, bricks breaking, particles etc. So we will implement full gameplay, start the ball automatically, AI paddle will follow ball.
Must ensure ball never passes through bricks or paddle. For that we need to implement collision detection time-of-impact for ball steps per frame, maybe use small radius and cheat with checking predicted next position and adjusting if collision. Typically with small time steps it works. At 60 FPS, ball speed modest, collisions with bricks should be fine. To guarantee, we can use a loop to check collisions against bricks and paddle after moving. If ball intersects, we adjust velocities accordingly; ensure ball doesn't get stuck inside. Using "if ball.y + ball.radius > paddle.y && ball.x > paddle.x && ball.x < paddle.x+ paddle.width" detection. We can adjust ball's position after collision.
We also need to handle bounce angle off paddle: The angle (dx) depends on offset from paddle center. Implementation: let hitPos = (ball.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2); // -1 left, 0 center, 1 right. Then set ball.dx = ball.speed * hitPos. Use speed normalization: set speed constant, compute dy = -Math.sqrt(speed^2 - dx^2). That yields angle.
Bricks: We need at least 6 rows, mixing normal and tougher bricks (2 hits). Use array of brick objects with x, y, width, height, hitsRemaining (1 for normal, 2 for tough). For visual cracking: we can draw a different fill style or add a crack pattern visible after first hit. Use different colors: for tough bricks, use a darker fill then after first hit we change color to lighter indicating damage. Or we can draw cracks using lines.
Also need power-ups: Some bricks carry a power-up. We can assign a type: "wide" or "multiball". When brick is destroyed, spawn a falling power-up object with x coordinate at brick center and y at brick's y. The power-up falls at constant speed. On collection, the effect triggers: widen paddle for 10 seconds, or create two extra balls (for total of three balls). Implementation: maintain an array of balls (including main ball and extra balls) for multi-ball. Multi-ball should last until balls are lost (fall below bottom). For demonstration, we can allow each ball to share lives? Or each ball lost reduces lives only when all balls are lost? Typically breakout: ball loss reduces life. For simplicity, treat a loss of any ball reduces one life, and if more than one ball, losing one lifeline reduces lives accordingly. But we may keep original spec: 3 lives. For multi-ball, we add two extra balls to the balls array; each ball behaves same physics. The lives count decrements when any ball goes below bottom (the ball removed). If no balls left (i.e., all balls lost) then life lost and new ball spawns only if lives left.
Better: Keep global lives count; if any ball falls, we decrease lives and reset to single ball if lives left. But with multi-ball, we likely want to keep other balls alive; but spec didn't detail. We can implement simpler: Each ball that falls reduces a life, and if lives > 0 then we spawn a new ball at paddle position but continue with remaining balls. That's simpler.
Power-up: "wide paddle" effect lasts 10 seconds. Expand paddle width, then revert. Multi-ball: add two balls.
Particle system: When brick destroyed, spawn particles emanating outward: small circles that fade out.
Score: increment per brick destroyed (normal: 10 points; tough: 20 after final hit; power-ups maybe extra). Display lives and score on top.
Game over: When lives become 0, show "Game Over" overlay with restart button. Victory: When all bricks destroyed, show "Victory!" overlay with restart button.
All interactive via mouse for paddle: track mouse movement relative to canvas, set paddle.x accordingly. For keyboard: left/right arrow keys adjust paddle movement.
Responsive: Canvas size maybe relative to window width. We'll set canvas width to min(600, window.innerWidth - 20). Height proportionally maybe 400. Or maintain aspect ratio. Use requestAnimationFrame for loop.
The page should adapt gracefully to small screens down to 375px. Use CSS to center canvas, set container max width.
We'll also need to ensure the game runs with zero errors.
Add global variables: canvas, ctx.
Define classes or objects for Ball, Paddle, Brick, PowerUp, Particle.
Define game state object: {balls, paddle, bricks, powerUps, particles, score, lives, running, demoMode, lastTime, paddleWidthOriginal, paddleWidthTimer}.
Initialize bricks: Use loops for rows and columns; define brick width = (canvas.width - 2 * offsetX - (columns-1)*brickPadding)/columns. Row count 6, col count maybe 10. Mix tough bricks: Randomly assign 20% tough bricks. Mix power-ups: assign occasional bricks have power-up (maybe 10% chance). Ensure at least one of each type.
Implement physics: In each frame, for each ball:
- Move according to velocity * deltaTime.
- Check collision with walls (left/right/top).
- Paddle collision: if ball.y+ball.r > paddle.y && ball.y - ball.r < paddle.y + paddle.height && ball.x > paddle.x && ball.x < paddle.x + paddle.width and ball.dy > 0 then reflect.
-- Compute hitPos fraction: (ball.x - (paddle.x + paddle.width/2)) / (paddle.width/2). Compute new dx = ball.speed * hitPos * 0.8? We'll want proper speed magnitude. Then dy = -Math.sqrt(ball.speed^2 - dx*dx). Or keep ball speed constant, adjust direction.
- Brick collision: for each brick that is active, check if ball overlaps (ball.x+ball.r > brick.x && ball.x-ball.r < brick.x+brick.width && ball.y+ball.r > brick.y && ball.y-ball.r < brick.y+brick.height). If collision, handle: decrement brick.hitsRemaining; if hitsRemaining becomes 0, brick is destroyed, increase score, spawn particles, spawn power-up if any. Determine collision side to adjust ball direction. Usually detection with simple approach: redirect ball's velocity based on which side collided. Use ball's previous position to infer side. Or compute difference distances to edges.
Simplify: In detection loop, when collision discovered, we reverse dy (vertical) if ball is above or below brick's center; else reverse dx. However need to handle proper bounce off corners. With small steps, this likely works.
- Check ball out of bottom: if ball.y - ball.r > canvas.height => ball lost. Remove ball from balls array. Decrease lives; if lives >0, spawn new ball at paddle if no active balls (or even if some remain). Initially keep ball count = 1.
- Power-up movement: Update y coordinate, check if reached bottom beyond paddle => remove. If collides with paddle: apply effect; remove power-up.
- Wide paddle effect: When active, enlarge paddle width (perhaps to 150% of original). Start timer (10 seconds). Use requestAnimationFrame to track time; after effect time ends revert.
- Multi-ball: When collected, add two extra balls with random initial velocity but direction upward.
- Show score and lives as text over canvas (maybe top left for score, top right for lives). Keep UI overlay.
- Overlays for start message, pause overlay maybe not needed, but for start we overlay a semi-transparent dark background with "Click or press any key to start".
- On start: overlay removed and player control enabled (in demo mode we may still have AI controlling). Use global variable isDemo = true; after click sets isDemo=false.
AI mode: For demo, paddle moves towards ball.x (center). Use simple AI: set paddle.x = ball.x - paddle.width/2 (with some speed limit). That will let paddle automatically position. For multi-ball scenario, AI may target the nearest ball? Use ball with lowest y maybe. Simpler: track first ball or average. Use ball[0] as target.
Also AI should 'catch' falling power-ups: simply set paddle.x accordingly. Since paddle moves because of ball target anyway, may catch.
Implement event listeners:
- mousemove: if not demo, set paddle.x = e.offsetX - paddle.width/2 within bounds.
- keydown: left/right arrow: set paddle.dx movement; keyup: stop movement.
- Use `requestAnimationFrame` loop: function update(dt) { // dt in seconds.
Time delta: get timestamp.
Implement scaling: The canvas will have set width/height attributes in CSS to fit responsive container. Use CSS for canvas scaling with width: 100%; height: auto; preserve aspect ratio.
Initialization: On window load, set canvas size based on container or window. Set up game objects.
Game loop: RequestAnimationFrame.
Potential improvements: Use double buffering? Not needed.
Performance: Particles might be many; limit count.
Implement cheat for ball through bricks: Ensure detection reduces velocity after collision.
Implement robust resetting: On restart: Reset lives to 3, score to 0, reinitialize bricks, balls, power-ups, particles, etc.
Add sound? Not allowed (maybe okay but not required). We'll not include audio to keep simple.
Responsive: Implement resizing event: adjust canvas size and recalc brick layout? For simplicity, we can set canvas size relative to window at load; not dynamic when resizing.
But requirement: The page must look right in 1280x800 viewport and adapt gracefully down to 375px wide. So we can set CSS: canvas { max-width: 100%; height: auto; display: block; margin: 0 auto; } Set container to such that canvas scale. We'll set width determined in JS at load: canvas.width = Math.min(600, window.innerWidth * 0.9); canvas.height = canvas.width * (3/4); That gives good proportion.
We need to ensure the bricks adjust accordingly each load. We'll rebuild bricks with dynamic brick width based on canvas width.
Let's think about details for bounce angle.
Suppose paddle width = variable w; paddle center = x0 = paddle.x + w/2. Offset = ball.x - x0. Normalized offset = offset/(w/2). Then angle = normalized offset * maxBounceAngle, where maxBounceAngle maybe 75 degrees (1.308 rad). Then ball speed s pre-defined. Compute velocities: ball.dx = s * Math.sin(angle), ball.dy = -s * Math.cos(angle). This ensures magnitude s.
We'll define ball.speed constant for all balls; but multiball may have same speed.
Thus we need to define maxAngle: 75 deg.
Implement as: let hitPos = (ball.x - paddle.x) / paddle.width; // 0 left, 1 right; map to -1..1: let rel = (hitPos - 0.5) * 2; let maxAngle = Math.PI * 5/12; // 75 deg
let angle = rel * maxAngle; ball.dx = ball.speed * Math.sin(angle); ball.dy = -ball.speed * Math.cos(angle); That's good.
Now add Brush for ball speed: perhaps initial speed 300 pixels per second (depending canvas width). Use dt to move: ball.x += ball.dx * dt; ball.y += ball.dy * dt.
Now implement bricks: rows6, cols10. Brick width = (canvas.width - 2*offsetX - (cols-1)*brickPadding)/cols. Use offsetX=10, offsetY= 60 (for top margin), brickHeight=20, padding=5.
Tough bricks: we can random assign tough with a probability; but ensure at least some. Similarly for power-up bricks.
We'll represent each brick as object {x, y, width, height, hits:1 or 2, power: null|type, color: baseColor, cracked: false}. We'll set colors: if hits==2: baseColor = "#B22222" (firebrick) and crackedColor = "#F08080" (lightcoral). For normal: baseColor = "#0088CC". When hits==2 and after first hit (hits>1?), we set hitsRemaining to 2 initially; when hit, decrement to 1 -> change color to crackedColor.
Add power-ups: type either "wide" or "multi"; Power-up icons could be drawn using simple shapes: maybe a small rectangle with letter "W" for wide and "M" for multiball. Or use small colored circles.
Implementation: On brick destruction, if brick.power is defined, spawn a power-up: {x: brick.x+brick.width/2, y: brick.y, radius:8, type: brick.power, vy: 100 (pixels per second)}.
When power-up collides with paddle: if rectangle intersects, apply effect.
Effect: "wide": increase paddle.width = originalWidth * 1.5; set timer. After 10 seconds (10_000ms), revert to original.
"multi": spawn two extra balls: for each new ball, position at paddle.y - ball.radius, and assign random angle within range +-45 degrees from upward vertical. Use same speed.
Particle system: When brick destroyed, spawn several particles: for i=0..10: {x: brick.x+ brick.width/2, y: brick.y+ brick.height/2, vx: random*200-100, vy: random*200-100, lifetime: 0.5, size: 2, color: brick.color}. In each frame update particles: position, fade out (reduce alpha), shrink.
Now implement main code:
Structure:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Breakout Game</title> <style> ... </style> </head> <body> <canvas id="gameCanvas"></canvas> <div id="overlay"></div> <script> ... </script> </body> </html>
Overlay for start, game over, victory.
Show scoreboard: Within overlay or drawn on canvas? Perhaps draw on canvas the score and lives top-left and top-right. Or we can use a separate UI: For simplicity draw on canvas.
But overlay shows messages. The overlay can have style absolute, centered text, fill background with perhaps semi-transparent. Show "Click or press any key to start". After game over, overlay changes to "Game Over - Click to restart" etc.
Implementation logic: In game state, "started" boolean flag for user control. isDemo = true initially. On document click or keydown, set started=true, isDemo=false, hide overlay.
Also, after game ends or victory, show overlay message with "Game Over - Click to restart". Clicking will call resetGame().
Also handle keydown events for paddle when started.
Make sure to set event listener for keydown for arrow left/right on whole window. In demo mode, which sets paddle.x based on AI; we must ignore user arrows.
Ensure not to use blocked features.
Potential intangible: 'keydown' for start detection as well. The overlay may hide but we need to be able to start with key too; can listen for keydown event and if not started, start.
Potential concurrency: Timer for wide paddle must be cleared on reset.
Ensure no horizontal overflow: Ensure canvas container in CSS. Use overflow hidden.
The code should be requestAnimationFrame loop.
Let me plan the implementation code.
**HTML**:
- typical meta and title.
**CSS**:
- Use body: margin:0; display:flex; justify-content:center; align-items:center; height:100vh; background:#111; color:#fff; font-family:Arial,Helvetica,sans-serif;
- canvas: border:2px solid #555; background:#222; width:100%; max-width:600px; height:auto; display:block;
- #overlay: position:absolute; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); display:flex; justify-content:center; align-items:center; color:#fff; font-size:2rem; text-align:center; pointer-events:none; (but pointer-events may be needed for click? We'll set overlay to capture events but still allow click? We'll maybe use <div id="ui"> with button). Or we can keep overlay text and also listen to click anywhere.
Better: We'll have <div id="uiOverlay"> with pointer-events:none for text only overlay that not intercepts click, but we also attach click to document. Or we create a separate message element with pointer-events: none.
Simplify: Use an element that overlays but we still detect click on document; so overlay can have pointer-events:none. That way click passes through to canvas (which has event listener for click). We'll do that.
Set overlay style: position: absolute; width: 100vw; height: 100vh; left:0; top:0; etc.
Add #uiOverlay: same.
**JS**:
We'll have modules or functions.
First global variables:
let canvas, ctx, width, height;
let gameState = {...};
function init() { canvas = document.getElementById('gameCanvas'); ctx = canvas.getContext('2d'); resizeCanvas(); createGameObjects(); // start animation ... }
function resizeCanvas() { width = Math.min(600, window.innerWidth*0.9); height = width * 0.75; canvas.width = width; canvas.height = height; // also may need to recalc positions if window resized after start; optional. }
Add window.addEventListener('resize', () => { resizeCanvas(); /* optionally adjust bricks?*/ });
Define createGameObjects: create paddle, ball(s), bricks, etc.
Define paddle object: {x, y, width, height, speed (for AI), originalWidth, wideTimer =0}. Provide methods.
Define ball constructor: {x, y, radius, speed, dx, dy, color}.
Define createBall(): returns ball object at start position: ball.x = paddle.x + paddle.width/2; ball.y = paddle.y - ball.radius - 2; // initial up; set initial angle: e.g., 45 degrees upward.
But we need to start with ball moving. Use random direction: angle between -45 and 45 degrees.
Define bricks: like earlier.
Define powerUps: array.
Define particles.
Define startDemo(): set flags.
Define game loop: let lastTimestamp = performance.now();
function gameLoop(timestamp) { let dt = (timestamp - lastTimestamp) / 1000; if (dt > 0.2) dt = 0.2; update(dt); draw(); lastTimestamp = timestamp; requestAnimationFrame(gameLoop); }
function update(dt) { // update paddle, AI, balls, power-ups, particles, timers
if(state.isDemo) { // simple AI: move paddle towards target ball.
let target = state.balls[0]; // first ball
let targetX = target.x - state.paddle.width/2; // center on ball
if (Math.abs(state.paddle.x - targetX) > 2) { // maybe move with speed limit
let dir = targetX > state.paddle.x ? 1 : -1; state.paddle.x += dir * state.paddle.speed * dt; // clamp
}}
if (state.paddle.moveLeft) state.paddle.x -= state.paddle.speed * dt; if(state.paddle.moveRight) state.paddle.x += state.paddle.speed * dt;
Clamp paddle.x to 0..canvas.width - paddle.width.
Update wide timer: if state.paddle.wideTimer > 0 -> state.paddle.wideTimer -= dt; if <=0 revert width.
Update balls: for each ball, update position and check collisions.
We'll need an array of balls: state.balls = [].
Ball movement and collisions:
function updateBall(ball, dt) {
let nextX = ball.x + ball.dx * dt;
let nextY = ball.y + ball.dy * dt;
// wall collisions
// left/right
if (nextX - ball.radius < 0) { nextX = ball.radius; ball.dx = Math.abs(ball.dx); }
if (nextX + ball.radius > width) { nextX = width - ball.radius; ball.dx = -Math.abs(ball.dx); }
// top
if (nextY - ball.radius < 0) { nextY = ball.radius; ball.dy = Math.abs(ball.dy); }
// paddle collision: only when ball moving down
if (ball.dy > 0 && nextY + ball.radius > state.paddle.y && nextY - ball.radius < state.paddle.y + state.paddle.height && nextX + ball.radius > state.paddle.x && nextX - ball.radius < state.paddle.x + state.paddle.width) {
// compute offset from paddle center
let hitPos = (nextX - (state.paddle.x + state.paddle.width / 2)) / (state.paddle.width / 2); // -1..1
hitPos = Math.max(-1, Math.min(1, hitPos)); // clamp
let maxAngle = Math.PI * 5 / 12; // 75 deg
let angle = hitPos * maxAngle;
ball.dx = ball.speed * Math.sin(angle);
ball.dy = -ball.speed * Math.cos(angle);
// reposition ball just above paddle to avoid sticking
nextY = state.paddle.y - ball.radius - 1;
}
// brick collisions
// iterate bricks that are alive
for(let i=state.bricks.length-1; i>=0; i--) {
let br = state.bricks[i];
if (!br.alive) continue;
if (nextX + ball.radius > br.x && nextX - ball.radius < br.x + br.width && nextY + ball.radius > br.y && nextY - ball.radius < br.y + br.height) {
// determine side:
// compute overlap distances
let overlapLeft = (nextX + ball.radius) - br.x;
let overlapRight = (br.x + br.width) - (nextX - ball.radius);
let overlapTop = (nextY + ball.radius) - br.y;
let overlapBottom = (br.y + br.height) - (nextY - ball.radius);
// find smallest overlap
let minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
// reflect based on which side
if (minOverlap === overlapLeft) {
// hit left side
ball.dx = -Math.abs(ball.dx);
nextX = br.x - ball.radius;
} else if (minOverlap === overlapRight) {
ball.dx = Math.abs(ball.dx);
nextX = br.x + br.width + ball.radius;
} else if (minOverlap === overlapTop) {
ball.dy = -Math.abs(ball.dy);
nextY = br.y - ball.radius;
} else {
ball.dy = Math.abs(ball.dy);
nextY = br.y + br.height + ball.radius;
}
// brick hit
brickHit(br);
break; // only handle one brick per frame for simplicity
}
}
// update ball position
ball.x = nextX;
ball.y = nextY;
// out of bottom?
if (ball.y - ball.radius > height) {
// ball lost
lostBall(ball);
}
}
Now implement brickHit(br): decrement br.hits; if hits==1 and br.maxHits==2, then change color to cracked. If hits<=0, mark as dead, increase score accordingly, spawn particles, spawn power-up if any.
function brickHit(br) {
br.hits--;
if(br.maxHits===2 && br.hits===1) {
// cracked state
br.color = br.crackedColor; // previously defined or we can set fill style to something else
}
if(br.hits<=0){
br.alive = false;
// score
state.score += br.maxHits===1 ? 10 : 20;
// particles
spawnParticles(br.x + br.width/2, br.y + br.height/2, br.baseColor);
// power-up
if (br.power) {
spawnPowerUp(br.x + br.width/2, br.y + br.height/2, br.power);
}
}
}
br.maxHits property initial defined (either 1 or 2). br.hits initially = br.maxHits.
Now power-ups:
function spawnPowerUp(x, y, type) {
state.powerUps.push({x:x, y:y, radius:8, type:type, vy:100}); // speed
}
updatePowerUps(dt):
for each p: p.y += p.vy*dt; if p.y - p.radius > height) remove; if collides with paddle (similar collision detection) then applyEffect(type) and remove.
function applyEffect(type) {
if(type === 'wide'){
if(state.paddle.wideTimer <= 0){
state.paddle.width = state.paddle.originalWidth * 1.5;
// adjust x to keep paddle within bounds?
}
state.paddle.wideTimer = 10; // seconds
} else if(type === 'multi'){
// add two extra balls
for(let i=0;i<2;i++){
let angle = (Math.random()*Math.PI/3) - Math.PI/6; // -30..30 deg
let dx = Math.cos(angle) * state.ballSpeed;
let dy = -Math.sin(angle) * state.ballSpeed;
let newBall = {x: state.paddle.x + state.paddle.width/2, y: state.paddle.y - 8, radius:8, speed: state.ballSpeed, dx:dx, dy:dy, color:'#FFF'};
state.balls.push(newBall);
}
}
}
Note: ball radius maybe 8.
Now lostBall(ball):
function lostBall(ball) {
// remove from balls array
let idx = state.balls.indexOf(ball);
if(idx > -1) state.balls.splice(idx, 1);
// lose a life
state.lives--;
if(state.lives <=0){
state.gameOver = true;
showOverlay('Game Over<br>Click to restart');
} else {
// if no balls left, spawn a new one after short delay (maybe immediate)
if(state.balls.length===0) {
spawnBall();
}
}
}
function spawnBall() {
// start new ball on paddle
let ball = {x: state.paddle.x + state.paddle.width/2, y: state.paddle.y - 10, radius:8, speed: state.ballSpeed, dx:0, dy:0, color:'#FFF'};
// give initial direction upward at random angle 45° left or right
let angle = (Math.random() < 0.5 ? -1 : 1) * (Math.PI/4);
ball.dx = ball.speed * Math.sin(angle);
ball.dy = -ball.speed * Math.cos(angle);
state.balls.push(ball);
}
During demo mode, we may want to spawn ball at start and maintain it moving.
Now particles:
function spawnParticles(x,y, color) {
for(let i=0;i<15;i++){
let angle = Math.random() * Math.PI *2;
let speed = Math.random()*60 + 40;
state.particles.push({
x:x, y:y,
vx: Math.cos(angle)*speed,
vy: Math.sin(angle)*speed,
life:0.5,
maxLife:0.5,
size:3 + Math.random()*2,
color: color
});
}
}
updateParticles(dt):
for each p: p.x += p.vx*dt, p.y += p.vy*dt; p.life+=dt;
if(p.life >= p.maxLife) remove.
Now draw function: Clear canvas, draw background maybe.
Draw bricks: iterate bricks that alive.
ctx.fillStyle = br.color;
ctx.fillRect(br.x, br.y, br.width, br.height);
// optional stroke.
If br.power exists and alive, draw an icon: simple shape like a small circle with different color and letter to indicate type. Use fill style distinct: maybe yellow for wide, cyan for multi.
Draw paddle: ctx.fillStyle = '#eee'; ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
Draw balls: for each ball draw circle: ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius,0,2*Math.PI); ctx.fillStyle = ball.color; ctx.fill();
Draw power-ups: for each power-up draw small shape: squares or circles with type letter.
Draw particles: for each particle, compute alpha = 1 - (p.life/p.maxLife). ctx.fillStyle = `rgba(${p.color},${alpha})`; But since color is hex string (#...), need to convert to rgba. Might use separate r,g,b values. Use color = p.color (string). For simplicity, use 'white', and use `ctx.globalAlpha = alpha` while drawing then `ctx.globalAlpha = 1` after.
Draw HUD: Score and Lives: ctx.font = "16px Arial"; ctx.fillStyle = "#FFF"; ctx.fillText(`Score: ${state.score}`, 10, 20); ctx.fillText(`Lives: ${state.lives}`, width-80,20);
If overlay message (isDemo or gameOver/victory): draw overlay with semi-transparent rectangle and text as DOM overlay.
Implementation of overlay: We can have a DOM element with ID overlay and set its innerHTML accordingly. Use CSS to center and style.
Now define showOverlay(message) function: set overlay.innerHTML = `<div>${message}</div>`; overlay.style.display = 'flex'; UI overlay displayed. When game starts, hide overlay: overlay.style.display = 'none';
In demo mode, the overlay says "Click or press any key to start". But we also want to show game underlying, so overlay pointer-events: none; But we need to click on canvas to start; we can have overlay not intercept pointer events. So overlay style pointer-events: none; So click events fall through to canvas. We'll attach event listeners to document for click/keydown.
When game over/victory, overlay should be visible with pointer-events:auto (so user can click overlay?). But we'll just use document click to restart regardless.
Simplify: Use a central overlay that is always there but pointer-events disabled except after overlay text displayed? Actually can have overlay but we can also detect click anywhere to restart. Simpler: we never need overlay to intercept clicks; we just display message as HTML overlay with pointer-events:none. So clicking anywhere triggers resetting.
Implementation: In overlay style set pointer-events:none, it will not block events. So we can have overlay always present; set its innerHTML to appropriate message as needed.
Alright.
Now try to conceive the code in correct ordering.
Potential issues:
- The AI will move paddle based on ball still moving, but if ball is lost (life), the AI will continue perhaps moving to non-existent ball. So after ball lost and before new ball spawns, we need to make sure AI doesn't break; we can handle missing ball's target. In updateDemopaddle: if state.balls.length >0, track first ball, else maybe keep paddle stationary.
- In AI mode, we can set paddle speed typical speed maybe 300 px/sec.
- The paddle controlled by arrow keys: We'll set state.paddle.moveLeft = false, moveRight = false. Add keydown event: if key=== "ArrowLeft" set moveLeft = true, if ArrowRight set moveRight = true; keyup set them false.
- When user moves mouse: For non-demo mode, set paddle.x = e.offsetX - paddle.width/2; (clamp). Should also check overlapping with canvas coordinate: pointer events give offsetX relative to canvas. Add event listener on canvas.
- However, we also need to lock into the start overlay: When isDemo==true, we still register mouse move events possibly but we want them to be ignored until start.
- Implementation of pointer events: On canvas, add 'mousemove' event that sets paddle.x only if !state.isDemo and state.started (maybe uses started boolean). Similarly for keyboard arrow keys.
Edge Cases: The ball must not pass through bricks or paddle. With our detection algorithm, if ball's velocity large enough to skip detection in one frame, it might go through. But with moderate speeds and dt limited to ~1/60 sec (0.0167), overlap may be caught. We'll clip dt to max 0.015 maybe. In update, we can ensure dt <= 0.02. Already we have dt > 0.2 -> dt =0.2 (makes large dt if frame stops >200ms). That's fine.
Processing bricks collision: We break after hitting first brick to avoid multiple collisions. That's okay.
Potential improvement: After collision we shouldn't skip detection for other bricks in same frame; but fine.
Now define all relevant constants:
- PADDLE_HEIGHT = 12 or 12 px.
- PADDLE_SPEED = 400 px/s for player and AI? The original may be 600.
- BALL_SPEED = 300 maybe, adjustable.
- BALL_RADIUS = 8.
- PARTICLE_COLOR: perhaps use brick baseColor.
We'll implement Variation: For tough bricks color: baseColor = '#B22222' (red) after first hit crackedColor = '#F08080'.
Normal bricks: baseColor = '#0088CC'.
Power-ups: type 'wide' => icon color '#FFD700' (gold) maybe with "W". type 'multi' => 'cyan' with "M". For simplicity draw small letter on top.
We'll define brickPowerProbability = 0.12 or 12%.
Now compute layout:
const cols = 10; const rows = 6; const brickPadding = 4; const offsetX = 20 (maybe left margin). offsetY = 60.
brickWidth = (width - offsetX*2 - (cols-1)*brickPadding) / cols; brickHeight = 12 (maybe 15). But we have to compute width of canvas before building bricks. So in createBricks, after canvas size known.
Ok.
Now the overlay message: "Click or press any key to start". After start, hide overlay (maybe set its innerHTML to empty). Or overlay shows nothing. At game over/victory, overlay shows message. To differentiate between overlay types, we can have function setOverlay(message, show) where hide sets blank.
Simplify: Use DOM <div id="msg"> for central message.
Implementation details for overlay:
<div id="msg"></div> positioned fixed, width:100vw, height:100vh, top:0, left:0, display:flex, align-items:center, justify-content:center, pointer-events:none; color: white; font-size: 2rem; text-align: center; background: rgba(0,0,0,0.5);. In CSS set #msg.hidden {display:none;}. For visible, we set text.
At start: message "Click or press any key to start". When hide, add class hidden.
When game over/victory: set message; remove hidden class. Since pointer-events none, clicking anywhere passes through to underlying canvas; we can listen for click and treat as game restart if game over/victory.
Add event listeners:
document.addEventListener('keydown', function(e){ if (!state.started) { startGame(); } else {if(e.key === 'ArrowLeft') state.paddle.moveLeft = true; else if(e.key === 'ArrowRight') state.paddle.moveRight = true;}});
document.addEventListener('keyup', function(e){ if(state.started){if(e.key === 'ArrowLeft') state.paddle.moveLeft = false; else if(e.key === 'ArrowRight') state.paddle.moveRight = false;}});
canvas.addEventListener('mousemove', function(e){ if(state.started && !state.isDemo){ let rect = canvas.getBoundingClientRect(); let mouseX = e.clientX - rect.left; state.paddle.x = mouseX - state.paddle.width/2; // clamp. }});
document.addEventListener('click', function(){ if (!state.started) { startGame(); } else if (state.gameOver || state.victory) { resetGame(); } });
But need to treat startGame: set state.started = true; state.isDemo = false; hide message overlay: set class hidden.
If state.gameOver or state.victory overlays are visible; clicking resets; also we can show restart button. But simpler: clicking anywhere triggers restart.
Also important: Player can start and then later game over then restart.
Now need to implement the demo mode.
On init, set started = false; isDemo = true; call startDemoBalls (spawn a ball and start). Actually we could spawn balls and start moving them when demo is on. So we call spawnBall() after creating paddle etc. Then set isDemo flag. Then overlay shows message.
But the game must start playing automatically, which includes AI moving paddle and ball bouncing. We'll start loop anyway; the isDemo flag controls AI. So in update loop if isDemo && state.balls length >0, we AI move.
But we need to set state.started false initially; isDemo true; but balls still moving. So we just call spawnBall() during creation.
Thus the ball moves automatically behind the start overlay.
Potential problem: The AI moves only when isDemo is true; but also we might want to move paddle based on user input after start. So we set state.isDemo and state.started flags.
Define state = {
started: false,
isDemo: true,
paddle: {...}
balls: [],
bricks: [],
powerUps: [],
particles: [],
lives: 3,
score: 0,
gameOver: false,
victory: false,
ballSpeed: 300,
};
Add set overlay.
Now orientation: The demo will automatically break bricks etc, so after a few seconds maybe all bricks cleared (victory). That would cause victory overlay, which might obscure the demo. But we need something visible on first paint after 3 seconds. Possibly before victory. So we need to ensure the demonstration covers enough action. Could ensure that the demo will not finish before screenshot (3 sec). Maybe limit difficulty: make brick hits more than enough that not all cleared quickly. Use at least 6 rows and many tough bricks.
But the demo has AI controlling paddle targeted at ball(s). Might break many bricks within 3 seconds if speeds high. But we can tune speeds to slower to show ball movement and collisions but not clear all bricks. For instance ball speed 250; brick layout 10 columns *6 rows = 60 bricks; each brick requires at least 1 hit to break; with 250 px/s speed, ball travels maybe 400-800px per second; might break maybe dozens of bricks within 3 secs? Might be okay but not many, and maybe there will be some remain; it's fine. The screenshot will show part of bricks missing. Not a problem.
But the spec says "The ball must never pass through bricks or the paddle." So need to avoid tunneling. Use small time step.
Now final piece: Reset game.
Function resetGame(): Reset lives, score, bricks, balls, etc. set state.gameOver = false; state.victory = false; hide overlay; spawn initial ball; set state.isDemo = false; state.started = false; but we want to show overlay again? Actually after restart, we likely want to start again with demo mode? Might be nice. The spec didn't say; but we can show overlay "Click or press any key to start". So we can set state.isDemo = true; and show start overlay again. That way the demo will again show AI.
Thus clicking after gameover triggers resetGame.
Implement:
function resetGame(){
state.lives = 3;
state.score = 0;
state.gameOver = false;
state.victory = false;
state.isDemo = true;
state.started = false;
state.paddle.width = state.paddle.originalWidth;
state.paddle.wideTimer = 0;
state.bricks = [];
state.powerUps = [];
state.particles = [];
state.balls = [];
createBricks(); // repopulate.
spawnBall();
showOverlay('Click or press any key to start');
}
Now for victory detection:
In update, after bricks iteration maybe we check if all bricks dead: if (state.bricks.every(b=>!b.alive)) => state.victory = true; showOverlay('Victory!<br>Click to restart').
But careful: When in demo mode, we want to still show victory message? Actually after all bricks cleared, game over will happen; but live demonstration could be considered victory. For screenshot after 3 seconds, we want something still there. To avoid immediate victory before screenshot, we can make bricks many, or limit speed. That should be fine.
Now finalize drawing logic.
Also need to draw cracked bricks after first hit: we have br.color property updated.
In drawBricks:
if (br.alive) { ctx.fillStyle = br.color; ctx.fillRect(br.x, br.y, br.width, br.height); // optionally stroke.
// draw power-up icon on intact bricks only? Possibly only on intact or else as indicator.
if (br.power && br.alive) { // draw small icon on top of brick
ctx.fillStyle = br.power === 'wide' ? '#FFD700' : '#00FFFF';
ctx.beginPath();
ctx.arc(br.x + br.width/2, br.y + br.height/2, br.width/4, 0, 2*Math.PI);
ctx.fill();
// Optionally draw letter
ctx.fillStyle = '#000';
ctx.font = `${br.width/4}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(br.power === 'wide' ? 'W' : 'M', br.x+br.width/2, br.y+br.height/2);
}
}
But maybe we want to show a small icon separate from brick color. Keep simple.
Now implement paddle drawing:
ctx.fillStyle = '#EEE';
ctx.fillRect(state.paddle.x, state.paddle.y, state.paddle.width, state.paddle.height);
Also draw paddle outline.
Now ball drawing: for each ball.
Now power-ups drawing: for each power-up, draw small circle with color.
Draw particles: for each particle, set globalAlpha.
Implementation details: Manage globalAlpha: set ctx.globalAlpha = alpha; draw; then ctx.globalAlpha = 1 after each particle or after loop.
Now implement game loop start:
window.requestAnimationFrame(gameLoop);
But we need requestAnimationFrame first call: let lastTime = performance.now(); requestAnimationFrame(gameLoop);
Now careful that the game updates if paused (e.g., not started). But we continue simulation in demo mode; after start we have moving ball; if not started we are in demo mode where isDemo = true. Actually started only indicates player started. So regardless of started, simulation runs. So it's fine.
But need to differentiate between AI vs player input.
Now event handling details:
We need to handle keydown for start and for paddle movement; also for space? We'll just use Arrow keys for movement.
Now we need to ensure that the arrow keys don't scroll page. In keydown there is default scrolling; we can call e.preventDefault() for ArrowLeft/ArrowRight.
Add: document.addEventListener('keydown', function(e){ if(e.key === 'ArrowLeft' || e.key === 'ArrowRight'){ e.preventDefault(); } // rest...});
Now handle case where a new ball is added after multi-ball: It should have proper speed and direction. Use ballSpeed constant.
Now after wide paddle effect ends, we revert paddle width; but if player currently moving, we need to adjust x if paddle beyond bounds after shrinking. Have code to clamp.
We'll keep a paddle.originalWidth property = initial width; Set it unchanged.
Implementation of wide effect: When power-up collected:
if (!state.paddle.wideTimer) // not active
state.paddle.width = state.paddle.originalWidth * 1.5;
else
// extend timer, keep width unchanged.
In update loop: if (state.paddle.wideTimer > 0) { state.paddle.wideTimer -= dt; if(state.paddle.wideTimer <= 0) { state.paddle.wideTimer = 0; state.paddle.width = state.paddle.originalWidth; // clamp x if needed } }
Now set ball speed:
state.ballSpeed = width*0.5? Actually need to calibrate speed based on canvas width: maybe 300 or 400.
We can compute ballSpeed = width * 0.8; Oops too fast. For 500 width, 300 is moderate. Use constant ballSpeed = width * 0.6 (like 300 for width=500). That will adapt to smaller screens.
Maybe: let baseSpeed = width * 0.6; So ball speed approx 0.6 * width per second. That would be ~360 for 600 width, ~210 for 350 width.
Alternatively we could keep speed constant and adapt velocities accordingly.
We can compute speed = Math.min(width, 600) * 0.5. That works.
We'll compromise: const BALL_SPEED = width * 0.5; That's good.
Now compute paddle speed as width * 1.5 perhaps (fast enough). Set PADDLE_SPEED = width * 1.5 (200-600). Or use constant 500.
Better: let paddleSpeed = width * 1.2; So dimension.
Now define state variables at init after canvas sizing.
Now code progression.
Potential errors: When we compute ball.x - paddle.x inside AI, ensure paddle width correct.
After width changes due to power-up, need to adjust AI collisions.
Now time to write code.
Add comment header.
**Implementation Outline**:
```
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Breakout Demo</title>
<style>
body,html{margin:0;padding:0;height:100%;overflow:hidden;background:#111;display:flex;justify-content:center;align-items:center;color:#fff;font-family:Arial,Helvetica,sans-serif;}
canvas{border:2px solid #555;background:#222;max-width:100%;height:auto;display:block;}
#msg{
position:fixed;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;
background:rgba(0,0,0,0.6);
color:#fff; font-size:2rem; text-align:center;pointer-events:none;
}
#msg.hidden{display:none;}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="msg" class="hidden"></div>
<script>
(() => {
// Game constants
let canvas, ctx;
let WIDTH, HEIGHT;
const ROWS = 6;
const COLS = 10;
const BRICK_PADDING = 4;
const BRICK_OFFSET_TOP = 60;
const BRICK_OFFSET_LEFT = 20;
const MAX_POWERUP_PROB = 0.12; // 12% of bricks have power-up
const MAX_TOUGH_PROB = 0.2; // 20% tough bricks
// Game state
const state = {
started: false,
isDemo: true,
paddle: null,
balls: [],
bricks: [],
powerUps: [],
particles: [],
lives: 3,
score: 0,
gameOver: false,
victory: false,
ballSpeed: 300,
paddleSpeed: 500,
};
// Helper functions
function init() {
canvas = document.getElementById('gameCanvas');
ctx = canvas.getContext('2d');
resizeCanvas();
initGameObjects();
attachEvents();
showOverlay('Click or press any key to start');
requestAnimationFrame(gameLoop);
}
// Resize canvas based on viewport, keep aspect ratio 4:3
function resizeCanvas() {
const maxW = Math.min(600, window.innerWidth * 0.9);
const maxH = maxW * 0.75;
canvas.width = maxW;
canvas.height = maxH;
WIDTH = canvas.width;
HEIGHT = canvas.height;
}
// Initialize paddle, balls, bricks, etc.
function initGameObjects() {
// paddle
const paddleWidth = WIDTH * 0.15;
const paddleHeight = 12;
state.paddle = {
x: (WIDTH - paddleWidth) / 2,
y: HEIGHT - paddleHeight - 10,
width: paddleWidth,
height: paddleHeight,
originalWidth: paddleWidth,
speed: WIDTH * 1.2,
moveLeft: false,
moveRight: false,
wideTimer: 0,
};
state.paddleSpeed = state.paddle.speed;
// balls placeholder; spawn initial ball later
state.balls = [];
// bricks
createBricks();
// power-ups and particles empty
state.powerUps = [];
state.particles = [];
// lives/score reset
state.lives = 3;
state.score = 0;
state.gameOver = false;
state.victory = false;
// compute speed based on canvas size
state.ballSpeed = WIDTH * 0.5;
// initial ball
spawnBall();
}
// Create brick layout
function createBricks() {
state.bricks = [];
const brickWidth = (WIDTH - 2 * BRICK_OFFSET_LEFT - (COLS - 1) * BRICK_PADDING) / COLS;
const brickHeight = 14;
const colors = {
normal: '#0088CC',
tough: '#B22222',
cracked: '#F08080',
};
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const x = BRICK_OFFSET_LEFT + col * (brickWidth + BRICK_PADDING);
const y = BRICK_OFFSET_TOP + row * (brickHeight + BRICK_PADDING);
// Decide if this brick is tough
const isTough = Math.random() < MAX_TOUGH_PROB;
const maxHits = isTough ? 2 : 1;
// Decide if it carries a power-up
const hasPower = Math.random() < MAX_POWERUP_PROB;
const powerType = hasPower ? (Math.random() < 0.5 ? 'wide' : 'multi') : null;
const brick = {
x,
y,
width: brickWidth,
height: brickHeight,
maxHits,
hits: maxHits,
alive: true,
baseColor: isTough ? colors.tough : colors.normal,
crackedColor: colors.cracked,
color: isTough ? colors.tough : colors.normal,
power: powerType,
};
state.bricks.push(brick);
}
}
}
// Spawn a new ball centered on paddle with random angle upward
function spawnBall() {
const ballRadius = 8;
const startX = state.paddle.x + state.paddle.width / 2;
const startY = state.paddle.y - ballRadius - 2;
const angle = (Math.random() < 0.5 ? -1 : 1) * Math.PI / 4; // ±45°
const dx = state.ballSpeed * Math.sin(angle);
const dy = -state.ballSpeed * Math.cos(angle);
const ball = {
x: startX,
y: startY,
radius: ballRadius,
speed: state.ballSpeed,
dx,
dy,
color: '#FFF',
};
state.balls.push(ball);
}
// Show overlay message
function showOverlay(html) {
const el = document.getElementById('msg');
el.innerHTML = html;
el.classList.remove('hidden');
}
// Hide overlay
function hideOverlay() {
const el = document.getElementById('msg');
el.classList.add('hidden');
}
// Event handling
function attachEvents() {
// Mouse movement for paddle (only after player started)
canvas.addEventListener('mousemove', (e) => {
if (!state.started || state.isDemo) return;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
let newX = mouseX - state.paddle.width / 2;
newX = Math.max(0, Math.min(WIDTH - state.paddle.width, newX));
state.paddle.x = newX;
});
// Keyboard controls
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault();
}
if (!state.started) {
startPlayerControl();
} else {
if (e.key === 'ArrowLeft') state.paddle.moveLeft = true;
if (e.key === 'ArrowRight') state.paddle.moveRight = true;
}
});
document.addEventListener('keyup', (e) => {
if (!state.started) return;
if (e.key === 'ArrowLeft') state.paddle.moveLeft = false;
if (e.key === 'ArrowRight') state.paddle.moveRight = false;
});
// Click to start or restart
document.addEventListener('click', () => {
if (!state.started) {
startPlayerControl();
} else if (state.gameOver || state.victory) {
resetGame();
}
});
}
// Transition from demo to player control
function startPlayerControl() {
if (state.gameOver || state.victory) return;
state.started = true;
state.isDemo = false;
hideOverlay();
}
// Restart whole game
function resetGame() {
// Clear everything and reinitialize
state.isDemo = true;
state.started = false;
state.paddle.width = state.paddle.originalWidth;
state.paddle.wideTimer = 0;
state.balls = [];
state.powerUps = [];
state.particles = [];
state.lives = 3;
state.score = 0;
state.gameOver = false;
state.victory = false;
createBricks();
spawnBall();
showOverlay('Click or press any key to start');
}
// Main game loop
let lastTime = performance.now();
function gameLoop(timestamp) {
const dt = Math.min((timestamp - lastTime) / 1000, 0.02); // cap delta time
update(dt);
render();
lastTime = timestamp;
requestAnimationFrame(gameLoop);
}
// Update game state
function update(dt) {
// AI paddle movement while in demo mode
if (state.isDemo && state.balls.length) {
// Simple AI: move towards the first ball's x
const targetBall = state.balls[0];
const targetX = targetBall.x - state.paddle.width / 2;
if (Math.abs(state.paddle.x - targetX) > 2) {
const dir = targetX > state.paddle.x ? 1 : -1;
state.paddle.x += dir * state.paddle.speed * dt;
// Clamp
state.paddle.x = Math.max(0, Math.min(WIDTH - state.paddle.width, state.paddle.x));
}
}
// Keyboard paddle movement (player)
if (state.paddle.moveLeft) {
state.paddle.x -= state.paddle.speed * dt;
}
if (state.paddle.moveRight) {
state.paddle.x += state.paddle.speed * dt;
}
// Clamp after player movement
state.paddle.x = Math.max(0, Math.min(WIDTH - state.paddle.width, state.paddle.x));
// Wide paddle timer
if (state.paddle.wideTimer > 0) {
state.paddle.wideTimer -= dt;
if (state.paddle.wideTimer <= 0) {
state.paddle.wideTimer = 0;
state.paddle.width = state.paddle.originalWidth;
// Ensure paddle stays inside bounds after shrinking
state.paddle.x = Math.max(0, Math.min(WIDTH - state.paddle.width, state.paddle.x));
}
}
// Update balls
for (let i = state.balls.length - 1; i >= 0; i--) {
const ball = state.balls[i];
updateBall(ball, dt);
}
// Update power-ups
for (let i = state.powerUps.length - 1; i >= 0; i--) {
const pu = state.powerUps[i];
pu.y += pu.vy * dt;
// Collision with paddle
if (
pu.y + pu.radius > state.paddle.y &&
pu.y - pu.radius < state.paddle.y + state.paddle.height &&
pu.x + pu.radius > state.paddle.x &&
pu.x - pu.radius < state.paddle.x + state.paddle.width
) {
applyPowerUp(pu.type);
state.powerUps.splice(i, 1);
} else if (pu.y - pu.radius > HEIGHT) {
// Missed
state.powerUps.splice(i, 1);
}
}
// Update particles
for (let i = state.particles.length - 1; i >= 0; i--) {
const p = state.particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life += dt;
if (p.life >= p.maxLife) {
state.particles.splice(i, 1);
}
}
// Victory check
if (!state.victory && state.bricks.every(b => !b.alive)) {
state.victory = true;
showOverlay('Victory!<br>Click to restart');
}
}
// Ball physics and collisions
function updateBall(ball, dt) {
let nextX = ball.x + ball.dx * dt;
let nextY = ball.y + ball.dy * dt;
// Wall collisions (left/right)
if (nextX - ball.radius < 0) {
nextX = ball.radius;
ball.dx = Math.abs(ball.dx);
} else if (nextX + ball.radius > WIDTH) {
nextX = WIDTH - ball.radius;
ball.dx = -Math.abs(ball.dx);
}
// Ceiling collision
if (nextY - ball.radius < 0) {
nextY = ball.radius;
ball.dy = Math.abs(ball.dy);
}
// Paddle collision (only when moving downwards)
if (
ball.dy > 0 &&
nextY + ball.radius > state.paddle.y &&
nextY - ball.radius < state.paddle.y + state.paddle.height &&
nextX + ball.radius > state.paddle.x &&
nextX - ball.radius < state.paddle.x + state.paddle.width
) {
// Calculate hit position relative to paddle center (-1 .. 1)
const hitPos = (nextX - (state.paddle.x + state.paddle.width / 2)) / (state.paddle.width / 2);
const maxBounce = Math.PI * 5 / 12; // 75 degrees
const angle = hitPos * maxBounce;
ball.dx = ball.speed * Math.sin(angle);
ball.dy = -ball.speed * Math.cos(angle);
// reposition ball just above paddle to avoid sticking
nextY = state.paddle.y - ball.radius - 1;
}
// Brick collisions
for (let i = 0; i < state.bricks.length; i++) {
const br = state.bricks[i];
if (!br.alive) continue;
if (
nextX + ball.radius > br.x &&
nextX - ball.radius < br.x + br.width &&
nextY + ball.radius > br.y &&
nextY - ball.radius < br.y + br.height
) {
// Determine the side of impact
const overlapLeft = nextX + ball.radius - br.x;
const overlapRight = br.x + br.width - (nextX - ball.radius);
const overlapTop = nextY + ball.radius - br.y;
const overlapBottom = br.y + br.height - (nextY - ball.radius);
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapLeft) {
ball.dx = -Math.abs(ball.dx);
nextX = br.x - ball.radius;
} else if (minOverlap === overlapRight) {
ball.dx = Math.abs(ball.dx);
nextX = br.x + br.width + ball.radius;
} else if (minOverlap === overlapTop) {
ball.dy = -Math.abs(ball.dy);
nextY = br.y - ball.radius;
} else {
ball.dy = Math.abs(ball.dy);
nextY = br.y + br.height + ball.radius;
}
// Brick hit processing
handleBrickHit(br);
// Only one brick collision per frame for a ball
break;
}
}
// Apply new position
ball.x = nextX;
ball.y = nextY;
// Bottom out: lose a life / remove ball
if (ball.y - ball.radius > HEIGHT) {
// Remove ball
const idx = state.balls.indexOf(ball);
if (idx !== -1) state.balls.splice(idx, 1);
state.lives--;
if (state.lives <= 0) {
state.gameOver = true;
showOverlay('Game Over<br>Click to restart');
} else {
// If no balls left, spawn a fresh ball
if (state.balls.length === 0) {
spawnBall();
}
}
}
}
// Process a brick being hit
function handleBrickHit(brick) {
brick.hits--;
if (brick.maxHits === 2 && brick.hits === 1) {
// Show cracked state
brick.color = brick.crackedColor;
}
if (brick.hits <= 0) {
brick.alive = false;
// Score
state.score += brick.maxHits === 1 ? 10 : 20;
// Particles
spawnParticles(brick.x + brick.width / 2, brick.y + brick.height / 2, brick.baseColor);
// Power-up spawn
if (brick.power) {
spawnPowerUp(brick.x + brick.width / 2, brick.y + brick.height / 2, brick.power);
}
}
}
// Spawn particles on brick break
function spawnParticles(x, y, color) {
for (let i = 0; i < 15; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 60 + 40;
state.particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 0,
maxLife: 0.6,
size: 2 + Math.random() * 2,
color,
});
}
}
// Spawn a falling power-up
function spawnPowerUp(x, y, type) {
state.powerUps.push({
x,
y,
radius: 8,
vy: 100,
type,
});
}
// Apply power-up effect
function applyPowerUp(type) {
if (type === 'wide') {
if (state.paddle.wideTimer <= 0) {
state.paddle.width = state.paddle.originalWidth * 1.5;
// Keep paddle within bounds
state.paddle.x = Math.max(0, Math.min(WIDTH - state.paddle.width, state.paddle.x));
}
state.paddle.wideTimer = 10; // seconds
} else if (type === 'multi') {
// Add two extra balls
for (let i = 0; i < 2; i++) {
const angle = (Math.random() * Math.PI / 3) - Math.PI / 6; // -30°..30°
const dx = state.ballSpeed * Math.sin(angle);
const dy = -state.ballSpeed * Math.cos(angle);
const ball = {
x: state.paddle.x + state.paddle.width / 2,
y: state.paddle.y - 10,
radius: 8,
speed: state.ballSpeed,
dx,
dy,
color: '#FFF',
};
state.balls.push(ball);
}
}
}
// Rendering
function render() {
// Clear
ctx.clearRect(0, 0, WIDTH, HEIGHT);
// Draw background (optional)
ctx.fillStyle = '#222';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Draw bricks
state.bricks.forEach(brick => {
if (!brick.alive) return;
ctx.fillStyle = brick.color;
ctx.fillRect(brick.x, brick.y, brick.width, brick.height);
// Outline
ctx.strokeStyle = '#111';
ctx.strokeRect(brick.x, brick.y, brick.width, brick.height);
// Power-up indicator
if (brick.power) {
ctx.fillStyle = brick.power === 'wide' ? '#FFD700' : '#00FFFF';
const radius = Math.min(brick.width, brick.height) * 0.3;
ctx.beginPath();
ctx.arc(brick.x + brick.width / 2, brick.y + brick.height / 2, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#000';
ctx.font = `${radius}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(brick.power === 'wide' ? 'W' : 'M', brick.x + brick.width / 2, brick.y + brick.height / 2);
}
});
// Draw paddle
ctx.fillStyle = '#EEE';
ctx.fillRect(state.paddle.x, state.paddle.y, state.paddle.width, state.paddle.height);
ctx.strokeStyle = '#AAA';
ctx.strokeRect(state.paddle.x, state.paddle.y, state.paddle.width, state.paddle.height);
// Draw balls
state.balls.forEach(ball => {
ctx.fillStyle = ball.color;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, 2 * Math.PI);
ctx.fill();
});
// Draw power-ups
state.powerUps.forEach(pu => {
ctx.fillStyle = pu.type === 'wide' ? '#FFD700' : '#00FFFF';
ctx.beginPath();
ctx.arc(pu.x, pu.y, pu.radius, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#000';
ctx.font = `${pu.radius}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(pu.type === 'wide' ? 'W' : 'M', pu.x, pu.y);
});
// Draw particles
state.particles.forEach(p => {
const alpha = 1 - p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
// HUD
ctx.fillStyle = '#FFF';
ctx.font = '16px Arial';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${state.score}`, 10, 20);
ctx.textAlign = 'right';
ctx.fillText(`Lives: ${state.lives}`, WIDTH - 10, 20);
}
// Initialize after page load
window.addEventListener('load', init);
})(); // IIFE
</script>
</body>
</html>
```
Make sure all code is inside one fenced HTML code block. Ensure overlay initially visible with start message. The demo running because spawnBall is called and AI moves paddle, ball bounces.
Potential issues:
- The AI paddle speed may be too high. Control maintain.
- The demo balls move; when ball collides with bottom, we lose a life and new ball spawns. That may happen quickly. Lives=3, after 3 deaths game over. Demo may end before screenshot. To keep demo alive, we can set infinite lives for demo? Or not lose lives in demo mode. However spec says 3 lives. But screenshot is taken after 3 seconds; if the demo is quick enough to lose all lives before 3 seconds, the overlay will show game over, which still visible. But we need visible gameplay; ideally game over screen is also visible. But demonstration might show gameplay earlier.
Better to keep lives high enough to avoid game over quickly. With ball speed moderate, 3 lives may be enough for 3 seconds.
Alternatively, we can in demo mode ignore losing lives. But that would violate the rules? It doesn't explicitly forbid ignoring lives in demo; but we could keep lives but not decrease? However the spec says 3 lives, no mention of cheat. It might be acceptable to let demo have its own lives, but still show them on screen; losing lives is part of gameplay. So fine.
Ball velocity: ball speed maybe 250. At 600 width, the ball may bounce many times quickly and potentially break bricks. That could lead to all bricks cleared quickly, causing victory before 3 seconds. That can happen if ball speed is high. Let's reduce speed to something moderate like 200. Use state's ballSpeed = WIDTH * 0.35 perhaps. Let's adjust.
State.ballSpeed = WIDTH * 0.35; For width 600, that gives 210. For width 350, gives 122.5. Good.
Update ball speed after resizing? We can set after resizeCanvas.
Also adjust paddle speed: state.paddle.speed = WIDTH * 1.5; Good.
Check code: In initGameObjects we set paddle speed; after resizing we set ball speed.
Now after calling resetGame we need to call initGameObjects? Actually after resetting we call createBricks, spawnBall but paddle width may need to reinitialize width if we haven't reinitialized paddle object. But we have paddle already set in initGameObjects; after reset we re-use same paddle but may need to set its x and width. Let's incorporate resetting inside resetGame: state.paddle.x = (WIDTH - state.paddle.originalWidth)/2; state.paddle.width = state.paddle.originalWidth; state.paddle.wideTimer=0; We'll ensure.
Simplify: Rather than reuse paddle object across resets, we can reinitialize it similarly as initGameObjects does with paddle, but must preserve same object. We'll set a function resetPaddle().
Better: In resetGame call initGameObjects again? That would recreate bricks and also paddle. But we need to preserve state ready. We'll do:
function resetGame() {
hideOverlay(); // we'll hide then show later
// Reinitialize everything from scratch
initGameObjects();
state.isDemo = true;
state.started = false;
showOverlay('Click or press any key to start');
}
But initGameObjects also calls spawnBall. That will create paddle, bricks, lives etc. However let us be careful not to duplicate event listeners. initGameObjects can be called multiple times; but it's okay because event listeners were attached only once in attachEvents (called once at init). So calling initGameObjects again resets all state fields; but `initGameObjects` defines `state.paddle` anew and sets state.ballSpeed based on current canvas size. However we have `state` object defined earlier; reassigning paddle to new object fine.
But `initGameObjects` also sets `state.balls = []`, etc.
Thus resetGame can call initGameObjects.
But we need to re-run createBricks (calls `state.bricks = []` and uses `state.brick`?). Already in initGameObjects we call createBricks.
So resetGame = () => { initGameObjects(); state.isDemo = true; state.started = false; showOverlay('Click or press any key to start'); };
We must ensure that if we call initGameObjects again, it does not create duplicate timers, but it's fine.
Potential issue: When resetting, we may lose the overlay hidden or not. We'll set overlay accordingly.
Now event handling: When game over or victory, the overlay message appears; click will trigger resetGame which resets everything and will display overlay again. Good.
Now we need to address demonstration: When the demo runs (isDemo true), the overlay is visible. However the code also draws overlay DOM element which may cover the canvas but pointer-events none. The message "Click or press any key to start". The canvas is underneath and visible, ball moving. Good.
Need to make sure `hideOverlay` called on start; else overlay stays on top (but pointer-events none, but visually drawn). We hide it.
Now we need to ensure that the paddle doesn't get stuck on walls due to AI overshoot. The AI moves with speed and clamps.
Need to ensure that the ball never passes through bricks or paddle. Our simple approach might still allow tunneling if dt is large; we capped dt at 0.02 (20ms) which is decent. At ball speed ~210, distance per frame is ~4.2px for 0.02s; bricks width maybe > 30, so detection fine.
Check for brick collisions: After adjusting ball position we break out of loop to avoid multiple collisions. That's fine.
Edge Cases: ball may hit a corner between two bricks but we may handle only one brick. That's okay.
Now testing: Need to ensure everything works without errors.
Testing mental steps:
- Page loads, overlay shown "Click or press any key to start". The canvas appears with border and black background. The ball spawns at paddle initial location, moving at random angle (maybe upward). Since isDemo=true, AI moves paddle towards ball. The demo runs; ball bounces.
- On collision with brick: brick hit, reduces hits. Tough bricks change color. Brick may drop power-up.
- Particle animation appears.
- Score increments.
- Lives displayed. Lives reduce when ball falls.
- After all bricks cleared, overlay shows "Victory!".
- After game over, overlay shows "Game Over". Both overlay messages have pointer-events none, but clicking anywhere triggers resetGame because document click event catches.
- ResetGame calls initGameObjects which re-initializes paddle, bricks, balls, lives, etc. Then it sets isDemo true and started false, and shows start overlay again. The demo starts automatically because spawnBall is called and AI moves.
Thus screenshot after 3 seconds should show ball and bricks partly broken.
Now respond with final code block.
But also need to ensure that we have CSS for the overlay. Since we use pointer-events none, we get click events pass through. However we used overlay shown initially; but if overlay has pointer-events none, it will not block clicks; but if we want to hide overlay after start we hide it anyway.
But we must implement overlay style accordingly. Let's define `#msg` style as currently. Use `.hidden` class to hide.
Write CSS:
```
#msg {
position: absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
background:rgba(0,0,0,0.7);
color:#fff;
font-size:2rem;
text-align:center;
pointer-events:none;
}
#msg.hidden { display:none; }
```
Adjustment: Use `inset:0` for top/left/right/bottom.
Ensure body has position: relative; Not needed, as absolute in viewport relative to body.
Need to set body margin zero and overflow hidden, ensure canvas centered: Use flex.
I used `body,html{margin:0;padding:0;height:100%;overflow:hidden;background:#111;display:flex;justify-content:center;align-items:center;color:#fff;font-family:Arial,Helvetica,sans-serif;}` but we also have the overlay absolutely positioned on the whole viewport; fine.
Now finalize code.
Potential errors:
- In code we used `state.paddle.speed` property with speed variable; but also we defined `state.paddleSpeed`. In our code we used `state.paddle.speed` for both paddle speed and AI movement. Actually we defined in initGameObjects: state.paddle.speed = WIDTH * 1.2; and also set state.paddleSpeed = state.paddle.speed; Later in update we use `state.paddle.speed` in AI movement and moveLeft/moveRight. That's consistent.
- However we also used `state.paddle.speed` for AI: `state.paddle.x += dir * state.paddle.speed * dt;`. So fine.
- In function attachEvents keyboard movement uses `state.paddle.speed` for player movement as well. Good.
- We also kept 'state.paddleSpeed' unused, we can remove or keep.
- For spawning extra balls we used state.balls.push(ball). The new ball objects need speed property? Already ball has speed: state.ballSpeed later needed for updating collisions? In updateBall we used ball.speed? Actually in updateBall we use ball.dx and ball.dy, but we also used ball.speed for bounce angle: we compute bounce angle using ball.speed constant. In spawnBall we set speed property; but for extra balls we need set speed property as well. In applyPowerUp we set ball.speed missing? Actually we set ball = {x:..., y:..., radius:8, speed: state.ballSpeed, dx, dy, color: '#FFF'} but we didn't assign speed property? We did: const ball = { x:..., y:..., radius:8, speed: state.ballSpeed, dx, dy, color:'#FFF' }. However earlier code for 'spawnBall' sets ball {x, y, radius, speed, dx, dy, color}. So fine.
- In updateBall we read ball.speed for bounce angle: `ball.dx = ball.speed * Math.sin(angle);` So we need ball.speed defined for all balls. Ensure both spawnBall and extra balls set speed.
- In spawnBall we set ball.speed: ball.speed = state.ballSpeed (already set). Yes.
- In applyPowerUp for multi-ball we need to set speed property same as state.ballSpeed. In our code we set ball = {..., speed: state.ballSpeed, dx, dy, color: '#FFF'}; Okay.
- In updateBall we used const ball = ? but we don't use ball.speed for collision with walls etc. That's fine.
- In updateBall after handling collisions we reassign ball.x and ball.y accordingly.
- In handleBrickHit we refer to brick.maxHits, brick.hits, brick.crackedColor etc.
- In creating bricks we set brick.crackedColor as colors.cracked; brick.color initially baseColor.
- For bricks that are normal, crackedColor is defined but not used (unused). For tough bricks we set crackedColor.
- For crack visual after first hit we set brick.color=brick.crackedColor. That works.
- For bricks that are tough with only one hit from start, not possible.
- For brick outline we do ctx.strokeStyle '#111'; fine.
- For power-up icon we drew a small circle and letter. Good.
- For the overlay message style: Use `#msg.hidden` to hide; we call showOverlay(innerHTML) that removes class hidden; hideOverlay adds hidden.
- At start, after init, we call showOverlay('Click or press any key to start'); In init we also call hideOverlay right? Actually we call showOverlay; hideOverlay is only called after start. Initially overlay visible. In gameLoop we are rendering canvas beneath.
- On game start we call hideOverlay; good.
- On victory or game over we call showOverlay accordingly.
- Since overlay pointer-events:none, clicking passes through; but we have document click event that will trigger start or reset, as needed.
Potential issues: need to prevent multiple start triggers. In keydown we check !state.started then startPlayerControl. That sets started true and isDemo false. In document click we also check if !state.started then start; else if gameOver or victory then resetGame.
- If user clicks before keydown scenario while demo still active, startPlayerControl will be called; we hide overlay and set isDemo false; the AI stops moving (as isDemo false), but the paddle may still have moveLeft or moveRight? None, so it stays.
- The ball continues.
- Keyboard control will be active as moveLeft/moveRight flags set.
- Ensure no accidental double start: after start, startPlayerControl does not set isDemo false when state.gameOver or victory, but start only before game over. If user clicks after game over, resetGame resets isDemo true and so start overlay shown. Good.
Potential issues: While demo is active, the AI paddle may respond to the ball only; but if at any point there are multiple balls (from multi-ball) AI may not target them. That's okay.
Now responsive: The canvas max width is set to 100% in CSS, though we set canvas width attribute directly; CSS width: 100% will stretch to container. In our container (body) we have display flex with align center, but we may have margin. The canvas width is set to actual pixel width via canvas.width, but CSS width: 100% will stretch to container width (which could be container's width). Since body flex sets center but doesn't explicitly limit width, the canvas's CSS width 100% will fill the available width of body (which is full viewport); we may want to set max-width:600px; Actually we set canvas style as display:block; but we haven't set max-width.
We want to allow canvas to adapt gracefully: As view width decreases, we want canvas width to shrink accordingly. In CSS we used max-width:100% which ensures it's not larger than container; but container width is the viewport width due to flex layout, so canvas width may fill entire view width, causing height scaling accordingly due to height:auto.
But we also set canvas attribute width/height in pixels (e.g., 600 width, 450 height). The CSS width:100% will upsize the canvas (scale) to fit the viewport width, stretching the content? Canvas scaling will scale up pixel drawing accordingly; proportions preserved but may have low resolution. That's okay.
To make it responsive, maybe we set CSS:
canvas {
max-width: 100%;
width: 100%;
height: auto;
}
Thus canvas will scale to fill available width; with width attribute (pixel) we get crisp for high DPI? Actually scaling will cause blur at low res but okay.
Alternatively we set width: 100%; height: auto; and add `image-rendering: pixelated`? Not necessary.
Thus the game fits all viewports.
Now check the loop: In gameLoop we compute dt = Math.min((timestamp - lastTime)/1000,0.02). Using 20ms cap ensures not overshoot.
Potential issues: The dt may be less than actual for high fps; fine.
Other things: On brick collision, we reposition ball accordingly and break, but not update ball.speed property; for bounce off paddle we use ball.speed for angle calculations. For brick collisions we reflect dx/dy directly and keep speed unchanged. This keeps ball speed constant.
Now test potential errors.
Test: In spawnBall, we have ball.radius = 8, but we also need ball.radius property for collisions.
Demonstrate: Once ball is moving, it will collide with top, bounce, hit paddle correctly.
Potential bug: In spawnBall we set ball.speed property but we also set dx and dy based on ball.speed? In spawnBall we set dx = ball.speed * Math.sin(angle); ball.dy = -ball.speed * Math.cos(angle); That uses ball.speed after we set ball.speed earlier? We previously set ball.speed = state.ballSpeed before computing dx/dy? Actually we defined ball like:
```
const ballRadius = 8;
const startX = state.paddle.x + state.paddle.width / 2;
const startY = state.paddle.y - ballRadius - 2;
const angle = (Math.random() < 0.5 ? -1 : 1) * Math.PI / 4; // ±45°
const dx = state.ballSpeed * Math.sin(angle);
const dy = -state.ballSpeed * Math.cos(angle);
const ball = {
x: startX,
y: startY,
radius: ballRadius,
speed: state.ballSpeed,
dx,
dy,
color: '#FFF',
};
```
Yes we compute dx,dy based on state.ballSpeed, which is set earlier.
Now for extra balls we do:
```
const dx = state.ballSpeed * Math.sin(angle);
const dy = -state.ballSpeed * Math.cos(angle);
const ball = {
x: state.paddle.x + state.paddle.width / 2,
y: state.paddle.y - 10,
radius: 8,
speed: state.ballSpeed,
dx,
dy,
color: '#FFF',
};
```
Yes.
Now what about when bounce off paddle sets ball.dx/dy based on ball.speed (ball.speed) which remains constant.
After brick collision we reflect ball.dx/dy but not adjust speed; that might slightly change speed due to float errors? The speed magnitude may change; but anyway we rely on ball.dx/dy magnitude not exactly ball.speed but it's okay. The bounce angle can be computed by angle change only on paddle.
Now ensure that after a brick hit we break out of loop; after break the ball may have moved again in next frame. Possibly ball could be stuck inside brick if we didn't push out enough. Our reposition after impact ensures ball is placed just outside brick on whichever side overlapped.
Now test fuzzy.
Potential visible bug: When ball hits bottom, we remove it from balls and decrement life. We also check state.lives <=0 then game over. On ball removal we also if state.balls is empty and lives >0, we spawn a new ball.
But we also spawn new ball at spawnBall which uses state.paddle.x + state.paddle.width/2; but if state.isDemo is true (demo mode) and lives >0, after losing ball, the AI will still be moving but may spawn new ball. That good.
Potential bug: If ball hits paddle after losing ball? Not relevant.
Now for power-up effect wide, we set state.paddle.wideTimer = 10 (seconds). Since update subtract dt each frame, after 10 seconds width resets.
One more function: In applyPowerUp, we spawn multi-ball extra balls but we do not set ball.angle if it collides with paddle later. That's fine.
Now overall code appears ready.
Need to consider viewport scaling; might be better to adjust `canvas` CSS to `max-width: 95vw; height: auto;`.
Make sure to have no external network calls.
Now finalize code inside a fenced code block with html.
Double-check that we used only allowed features: no localStorage. no alert. no external requests. No use of `window.open`. fine.
Double-check for any errors:
- In `state.paddle.moveLeft` and `moveRight` we set them based on keydown/keyup only if state.started. Good.
- In `state.isDemo` we still handle AI movement with `if (state.isDemo && state.balls.length)`. AI stops after start.
- In `states` we defined `paddle.speed` after init. In `attachEvents`, we reference `state.paddle.speed`. ok.
Potential oversight: After `resetGame`, we call `initGameObjects();` which also sets `state.paddle.speed = WIDTH * 1.2` again and `state.ballSpeed` etc. Then we set `state.isDemo = true; state.started = false; showOverlay(...);` That's fine.
But we need to also ensure that `state.lives` is set to 3 and `state.score` to 0 inside `initGameObjects`. Yes we set state.lives = 3 and score=0. Good.
Now check that `state.isDemo = true;` but we also need to call `spawnBall` inside initGameObjects which adds a ball; good.
Now ensure that after start, we hide overlay with hideOverlay and set state.isDemo=false, state.started=true.
Now ensure that `hideOverlay` sets class hidden and maybe also empties innerHTML? It just adds class hidden but we might want to keep innerHTML but hidden; fine.
Now style overlay: Use `position: fixed` for covering entire viewport; but pointer-events none ensures clicks aren't blocked.
Now ensure that overlay’s `innerHTML` can contain HTML like `<br>`. Use `showOverlay('Click or press any key to start')` without `<br>`; and for victory and game over we include `<br>`.
Now check that after resetting, overlay visible again.
Now check for any missing global variable: We used `state` throughout.
Now check for variable naming collisions: There's `state.paddle.speed` used for both moving paddle and AI; In `attachEvents` on keydown, we also use `state.paddle.moveLeft`, which is fine.
Now check for any `let` conflict: At outer scope we have `let canvas, ctx, WIDTH, HEIGHT;` and state variables.
Now ensure that `canvas` is correctly retrieved; id 'gameCanvas'.
Now need to ensure that global functions are defined before used when needed; we define them in order.
Now we need to embed everything inside <script> after style.
Potential performance: Doing many loops each frame (balls loops, bricks loops). It's fine.
Now final code.
Make sure to include meta `<!DOCTYPE html>` at top.
Will provide final snippet.
Potential improvements: Add responsive resizing event handler that updates canvas and resets game? Not needed.
Now send final answer.