Hamburger breakoutOne-line
gpt-oss-120b
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 31.2 s
- Cost
- $0.0089
- Output tokens
- 14,801
- Spent thinking
- 12,618 tokens
- Speed
- 478.7 tok/s
- Console errors
- 0
- First token after
- 298 ms
- First visible text after
- 26.6 s
- Prompt tokens
- 421
- Code size
- 8.0 KB
- Finish reason
- stop
- Served by
- Groq
- Model id
openai/gpt-oss-120b- Reasoning sent
{"effort":"high"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 2026
What it was asked
The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
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 · 282 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Breakout with Hamburger Paddle and Heart Explosion</title>
<style>
html,body{
height:100%;
margin:0;
background:#111;
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-family:Arial,Helvetica,sans-serif;
}
#gameContainer{
position:relative;
max-width:800px;
width:100%;
}
canvas{
display:block;
width:100%;
height:auto;
background:#000;
border:2px solid #fff;
}
#overlay{
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
background:rgba(0,0,0,0.6);
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-size:1.5rem;
text-align:center;
z-index:2;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="overlay">Click or press any key to start</div>
</div>
<script>
(() => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
// Game objects
const ball = {
x: canvas.width / 2,
y: canvas.height - 60,
radius: 8,
dx: 3,
dy: -3
};
const paddle = {
width: 80,
height: 30,
x: (canvas.width - 80) / 2,
y: canvas.height - 40,
speed: 5
};
let useAI = true; // AI controls paddle initially
// Bricks
const brickRowCount = 5;
const brickColumnCount = 10;
const brickPadding = 5;
const brickOffsetTop = 50;
const brickOffsetLeft = 30;
const brickWidth = (canvas.width - 2 * brickOffsetLeft - (brickColumnCount - 1) * brickPadding) / brickColumnCount;
const brickHeight = 20;
const bricks = [];
for (let r = 0; r < brickRowCount; r++) {
for (let c = 0; c < brickColumnCount; c++) {
const brickX = brickOffsetLeft + c * (brickWidth + brickPadding);
const brickY = brickOffsetTop + r * (brickHeight + brickPadding);
const hue = ((r * brickColumnCount + c) * 12) % 360;
bricks.push({
x: brickX,
y: brickY,
width: brickWidth,
height: brickHeight,
status: 1,
color: `hsl(${hue}, 70%, 60%)`
});
}
}
// Heart particles
const hearts = [];
// Drawing functions
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.closePath();
}
function drawHamburger(ctx, x, y, w, h) {
const bunHeight = h * 0.35;
const pattyHeight = h * 0.3;
const bunRadiusX = w / 2;
const bunRadiusY = bunHeight / 2;
const cx = x + w / 2;
// top bun
ctx.fillStyle = '#d2691e';
ctx.beginPath();
ctx.ellipse(cx, y + bunRadiusY, bunRadiusX, bunRadiusY, 0, 0, Math.PI * 2);
ctx.fill();
// patty
ctx.fillStyle = '#8b4513';
ctx.fillRect(x, y + bunHeight, w, pattyHeight);
// bottom bun
ctx.fillStyle = '#d2691e';
ctx.beginPath();
ctx.ellipse(cx, y + bunHeight + pattyHeight + bunRadiusY, bunRadiusX, bunRadiusY, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawBricks() {
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
if (b.status) {
ctx.fillStyle = b.color;
ctx.fillRect(b.x, b.y, b.width, b.height);
}
}
}
function drawHeart(ctx, x, y, size, opacity) {
ctx.save();
ctx.translate(x, y);
const scale = size / 30; // base heart size ~30
ctx.scale(scale, scale);
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.bezierCurveTo(-10, -20, -20, -5, 0, 10);
ctx.bezierCurveTo(20, -5, 10, -20, 0, -10);
ctx.closePath();
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
ctx.fill();
ctx.restore();
}
function drawHearts() {
for (let i = 0; i < hearts.length; i++) {
const p = hearts[i];
drawHeart(ctx, p.x, p.y, p.size, p.opacity);
}
}
// Game logic
function update() {
// Move ball
ball.x += ball.dx;
ball.y += ball.dy;
// Wall collisions
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.dx = -ball.dx;
}
if (ball.y - ball.radius < 0) {
ball.dy = -ball.dy;
} else if (ball.y + ball.radius > canvas.height) {
// Bounce off bottom to keep demo running
ball.dy = -ball.dy;
}
// Paddle collision
if (ball.y + ball.radius >= paddle.y && ball.y + ball.radius <= paddle.y + paddle.height &&
ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
ball.dy = -ball.dy;
const hitPos = (ball.x - paddle.x) / paddle.width;
ball.dx = (hitPos - 0.5) * 6;
}
// Brick collisions
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
if (b.status) {
if (ball.x > b.x && ball.x < b.x + b.width &&
ball.y - ball.radius < b.y + b.height && ball.y + ball.radius > b.y) {
ball.dy = -ball.dy;
b.status = 0;
createHeartExplosion(b.x + b.width / 2, b.y + b.height / 2);
}
}
}
// Update hearts
for (let i = hearts.length - 1; i >= 0; i--) {
const p = hearts[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 1;
p.opacity = Math.max(p.life / p.maxLife, 0);
if (p.life <= 0) {
hearts.splice(i, 1);
}
}
// AI paddle movement
if (useAI) {
const paddleCenter = paddle.x + paddle.width / 2;
const diff = ball.x - paddleCenter;
if (Math.abs(diff) > 5) {
paddle.x += Math.sign(diff) * paddle.speed;
}
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawHamburger(ctx, paddle.x, paddle.y, paddle.width, paddle.height);
drawHearts();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
function createHeartExplosion(x, y) {
const count = 30 + Math.random() * 10;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 2 + 1;
hearts.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: Math.random() * 8 + 8,
life: Math.random() * 30 + 30,
maxLife: Math.random() * 30 + 30,
opacity: 1
});
}
}
// Start demo loop
requestAnimationFrame(loop);
// Interaction to hide overlay and enable player control
function startGame() {
overlay.style.display = 'none';
useAI = false;
canvas.addEventListener('mousemove', onMouseMove);
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
paddle.x = mouseX - paddle.width / 2;
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
}
overlay.addEventListener('click', startGame);
document.addEventListener('keydown', startGame);
})();
</script>
</body>
</html>Reasoning trace (12,618 tokens)
We need to output a self-contained HTML file with inline CSS/JS for a breakout game where the paddle is a hamburger (i.e., an image or shape resembling a hamburger). The bricks, when broken, explode into many little hearts. The game must be visible on first paint, showing the game in action, perhaps with an AI playing automatically in demo mode. Also must show a "Click or press a key to start" message for keyboard events. The page should be responsive.
We need to ensure that the code runs with no errors, no external resources except allowed CDNs. We can embed the hamburger and heart as SVG shapes or base64 images.
We need to create a simple breakout game. Usually involves a ball, paddle, bricks arranged in rows. The paddle is a rectangle that we replace with a hamburger shape. The bricks are colored rectangles. When the ball collides with a brick, it disappears and triggers an explosion of hearts: small heart particles that radiate outward, perhaps using canvas.
We can implement the entire game using canvas. The paddle can be drawn using a custom shape: draw a hamburger: a bun top (ellipse), a patty (rectangle), and bun bottom (ellipse). Or we can draw an image of a hamburger using base64. Simpler: we can draw a rectangle with a burger pattern. But we can also use an image for the paddle: a small PNG base64. But the prompt says no external images except CDNs explicitly allowed by the task. We can embed base64 data URLs, which are internal. That's okay.
Better to use pure canvas drawing: draw the paddle as a hamburger shape: top bun: a semi-ellipse or rectangle with rounded top, patty rectangle, bottom bun shape. We can draw a simple representation.
Bricks: simple rectangles in rows, maybe with different colors.
When a brick is hit, we spawn heart particles: we can predefine a heart shape path, drawn small hearts with random velocities. Then they fade out.
For AI playing: we can have a simple AI that moves the paddle to follow the ball's x-coordinate with some delay or speed limit.
The game should start automatically after a brief "Click or press any key to start" overlay, which also allows keyboard events after clicking. However, the game can be set to start automatically with AI playing; but the requirement: "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message, so the game can be judged in action without playing." So we need to show a message overlay that says "Click or press any key to start" and behind it the AI plays the game. So the page on first paint shows the game in action (ball bouncing, bricks being broken, hearts exploding) but also a message overlay. The AI is moving the paddle. So the screenshot after 3 seconds will capture the game playing.
We must ensure the message overlay does not block the view of the game entirely; we can show the message in a semi-transparent overlay or at top with small opacity. It should be visible but not obstruct the game.
After user interaction (click or keypress), we can hide the overlay and perhaps enable user control (arrow keys or mouse). But that's optional.
Also, we need to make it responsive: canvas width fits container, maybe full width, max height maybe 80vh.
Implementation details: We'll set up a <canvas id="gameCanvas"> with width and height based on window innerWidth and innerHeight, but limited to 1280x800. We'll adjust on resize.
The game loop: using requestAnimationFrame.
Game objects: ball (x,y,radius,dx,dy), paddle (x,y,width,height) but with custom drawing shape: we can draw a hamburger shape using arcs. Let's define paddle width = 80, height = 20 maybe, but for hamburger shape we draw top bun: a half-ellipse (or full ellipse with flat bottom). Actually easier: draw three arcs: top bun (semi-ellipse), patty (rectangle), bottom bun (semi-ellipse). Or just draw a simple rectangle with a pattern.
But we need a clear visual of a hamburger. Let's define a function drawHamburger(x, y, width) which draws: top bun - a rectangle with rounded top corners, patty - a smaller rectangle, bottom bun - rectangle with rounded bottom corners.
Simplify: draw a rectangle for paddle background and then overlay small shapes: we can use path for top bun: arc from left to right with radius width/2 for top, then line down for patty and bottom bun. Or we can draw a simple representation: top bun as a brown ellipse, patty as a darker ellipse, bottom bun as another ellipse. Could be more intuitive: draw three circles aligned horizontally? Or vertically? Usually a burger is stacked vertically: bun top, patty, bun bottom. So we can draw three arcs stacked vertically.
We can set paddle height ~ 30px. We'll draw top bun as an ellipse of width (paddleWidth) and height (paddleHeight/3), at the top part of paddle. Then patty as a rectangle of same width and smaller height maybe 0.4*height, placed below top bun. Then bottom bun as another ellipse.
So the paddle shape: overall height ~ 30. Let's define paddleHeight = 30; top bun height = 10, patty height = 10, bottom bun height = 10. Or we can use arcs.
Simplify: draw the top bun as an arc from 0 to Math.PI (semi-circle) with radius width/2, at y position = paddleY + topBunRadius (like half circle). Then draw rectangle for patty from y = topBunRadius to topBunRadius + pattyHeight. Then bottom bun as another half-circle.
But half-circles are horizontally oriented; we want them horizontal like a burger.
Better: draw top bun: an ellipse horizontally oriented (like a wide shape). Use ctx.ellipse. Then patty: rectangle. Then bottom bun: another ellipse.
We can use ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle). For top bun: radiusX = paddleWidth/2, radiusY = paddleHeight/6. For bottom bun: same but y offset.
Simplify: draw top bun with radiusY = 0.5*heightPart. Then patty rectangle between them.
Alternatively, we can draw a single rectangle with a pattern, but the prompt suggests the paddle is a hamburger, so visual representation matters.
Implementation: drawHamburger(paddle) function:
```
function drawHamburger(ctx, x, y, w) {
const bunHeight = w * 0.15; // relative
const pattyHeight = w * 0.1;
const totalHeight = bunHeight*2 + pattyHeight;
const centerX = x + w/2;
const topBunCenterY = y + bunHeight/2;
const pattyY = y + bunHeight;
const bottomBunCenterY = pattyY + pattyHeight + bunHeight/2;
// top bun
ctx.fillStyle = '#d2691e'; // chocolate brown
ctx.beginPath();
ctx.ellipse(centerX, topBunCenterY, w/2, bunHeight/2, 0, 0, Math.PI*2);
ctx.fill();
// patty
ctx.fillStyle = '#8b4513'; // darker brown
ctx.fillRect(x, pattyY, w, pattyHeight);
// bottom bun
ctx.fillStyle = '#d2691e';
ctx.beginPath();
ctx.ellipse(centerX, bottomBunCenterY, w/2, bunHeight/2, 0, 0, Math.PI*2);
ctx.fill();
}
```
But that draws the bun as full ellipses (circular shape). It will appear like a burger shape.
We can set the paddle width maybe 80. So w=80, bunHeight ~12, pattyHeight ~8, totalHeight ~32. That's fine.
Now we need to integrate the paddle shape into the collision detection. The paddle is still a rectangle for collision detection (i.e., bounding box). So we can treat paddle.x, paddle.y, paddle.width, paddle.height. But the visual shape extends beyond bounding box? Actually the bounding box includes the buns and patty. So define paddle.height = totalHeight.
Thus we need to compute totalHeight based on width. Or we can set explicit paddleHeight = 32 and draw accordingly.
Simplify: define paddle = {x: ..., y: canvas.height - 30, width: 80, height: 30}. Then in drawHamburger we will treat height accordingly. Let's define bunHeight = paddle.height * 0.35 = 10.5; pattyHeight = paddle.height * 0.3 = 9; totalHeight = bunHeight*2+pattyHeight = 30 (as desired). So use that.
Thus the drawHamburger will compute bunHeight and pattyHeight based on paddle.height.
Implementation: drawHamburger(ctx, paddle.x, paddle.y, paddle.width, paddle.height). In function we compute bunHeight = paddleHeight * 0.35, pattyHeight = paddleHeight * 0.3, etc.
Now bricks: define brick rows and columns. For each brick we can have status (alive/dead). When ball collides, set status false and spawn heart particles.
Heart particles: We can define a small shape for heart. We can draw heart shape using path. For each particle: position, velocity, life (duration), size, rotation maybe.
Define function drawHeart(ctx, x, y, size, color). The heart shape path can be drawn by using two arcs and a triangle.
Simplify: heart shape path:
```
ctx.beginPath();
ctx.moveTo(x, y);
ctx.bezierCurveTo(x, y - size/2, x - size, y - size/2, x - size, y);
ctx.bezierCurveTo(x - size, y + size/2, x, y + size, x, y + size);
ctx.bezierCurveTo(x, y + size, x + size, y + size/2, x + size, y);
ctx.bezierCurveTo(x + size, y - size/2, x, y - size/2, x, y);
ctx.closePath();
ctx.fill();
```
But that draws a heart shape. But perhaps we can use a simpler shape: small heart emoji? Not allowed external images.
Better to implement a heart shape using path.
Define heart particles with random directions, speed, fade.
When brick destroyed, spawn N particles (like 20-30) of hearts around the brick location.
Now AI: a simple AI that moves paddle.x toward ball.x with a limited speed: paddle.speed = 5. So each frame, if paddle.x + paddle.width/2 < ball.x - tolerance, move right; else if > ball.x + tolerance, move left.
Also, if user interacts (click or key), we can remove AI and enable user control: for mouse movement, we can attach mousemove to canvas and set paddle.x accordingly. Or we can keep AI but also allow user override.
We need to show "Click or press any key to start" overlay. Initially, we can show the overlay and also start AI. When user clicks anywhere or presses any key, we hide overlay and perhaps enable user control (mouse). But the game continues anyway.
Responsive design: canvas should fill width of container. We can set canvas width to window.innerWidth * 0.98 and height to window.innerHeight * 0.9. Or we can set a fixed aspect ratio.
We'll also handle window resize: adjust canvas dimensions and rescale positions proportionally? Might be complex. Instead, we can set a fixed canvas size (like 800x600) but scale via CSS transform. But the prompt requires adapt gracefully down to 375px wide without horizontal scroll.
Simplest: set canvas width to Math.min(800, window.innerWidth - 20), height to Math.min(600, (canvas.width * 3/4) maybe). Or use 4:3 aspect ratio.
Let's set width = Math.min(800, window.innerWidth - 20); height = width * 3/4 (or 600). So for smaller screens, canvas shrinks.
Better: define a container <div id="gameContainer"> with width: 100%; max-width: 800px; margin: auto. Inside, canvas width=100% height auto with maintain aspect ratio using CSS padding hack or set width and height attributes equal to dimensions and style width:100%; height:auto.
Simplify: use <canvas id="gameCanvas" width="800" height="600"></canvas> and style it to max-width:100%; height:auto; to be responsive.
Thus the canvas draws at 800x600 resolution (pixel). The actual display scales via CSS. That is okay.
Now the code: we need a <script> that sets up canvas, game objects, loop.
Now details:
- ball: radius = 8. Starting position: x = canvas.width/2, y = canvas.height - 60 (above paddle). dx = 3, dy = -3.
- collision detection with walls: reflect dx when x < radius or > width - radius. Reflect dy when y < radius (top). If ball touches bottom (y > height - radius) => lose life (but for demo we can just bounce back from bottom). But to keep simple, bounce off bottom.
- paddle collision: if ball's y+radius >= paddle.y and ball's x is within paddle.x to paddle.x + paddle.width, then reflect dy and maybe adjust dx based on where it hits (optional). We'll just reverse dy.
- bricks: create 5 rows, 10 columns. Each brick width = (canvas.width - (cols+1)*brickPadding)/cols; brick height = 20. Padding = 5. Brick top offset = 50. Brick color random.
- Brick removal: status = 1 (alive) or 0 (dead). On collision, set status 0, spawn hearts.
- Heart particles: each particle has x,y,vx,vy,size,life,opacity. On update, apply velocity, reduce life, reduce opacity. Draw as heart shape with fillStyle = `rgba(255,0,0,${opacity})`.
- Explosion: spawn N particles with random angle and speed.
- AI: simple function updatePaddleAI() that moves paddle towards ball.x.
- User input: after overlay removal, we enable mousemove: set paddle.x = mouseX - paddle.width/2 (clamp). Also arrow keys or left/right.
- Overlay: <div id="overlay"> <div class="message">Click or press any key to start</div> </div> styled to cover canvas with semi-transparent background.
- Initially, overlay is visible; the game loop runs anyway, AI moves paddle.
- On user click or keydown: overlay hidden, add event listeners for mousemove and keydown for paddle control.
- Ensure that after user interaction, paddle stops AI? We can have a variable `useAI = true` initially; on user interaction set useAI = false.
Now, about heart explosion: "explode into tons of little hearts". So we need many hearts. Maybe spawn 30-50 hearts per brick.
We should ensure performance is okay: for 5 rows * 10 columns = 50 bricks, each may generate 30 hearts, total 1500 hearts over game, which is fine.
Now, the canvas coordinate system: use canvas.width and height.
We need to manage scaling: The canvas is drawn at 800x600 resolution; CSS scales. But we need to handle mouse positions accordingly: we need to map clientX to canvas coordinates. Use getBoundingClientRect to compute scale factor.
Alternatively, we can set canvas width and height attributes to the actual pixel size after scaling to maintain crispness. But for simplicity, use fixed 800x600 and map mouse positions.
Implementation steps:
1. In HTML, container with canvas and overlay.
2. CSS: style body: display flex, align center, justify center, height 100vh, margin 0, background color #111 (dark). Canvas border: 2px solid #fff.
3. overlay: absolute positioned over container, full size, background rgba(0,0,0,0.6), display flex align center justify center, color #fff, font size maybe 24px, pointer-events none? Actually we need to capture click; we can set pointer-events to auto.
Better: overlay covers the canvas and intercepts clicks; on click we hide overlay. But we also want AI playing behind; overlay is semi-transparent so it's visible. We'll set overlay to have pointer events to capture click, but also allow pointer events to pass through? Actually we need to capture click for overlay to hide; after hide, we want events to go to canvas. So it's fine.
Make overlay's style: position absolute, top:0 left:0 width:100% height:100%, background:rgba(0,0,0,0.5), display:flex; justify-content:center; align-items:center; color:#fff; font-size: 1.5rem; text-align:center; z-index:2.
Container: position relative.
Canvas: width:100%; height:auto.
Now the JavaScript: after DOMContentLoaded, get canvas, ctx.
Define objects: ball, paddle, bricks, hearts.
Initialize bricks.
Define functions: drawBall, drawPaddle (calls drawHamburger), drawBricks, drawHearts, update.
In update loop: clear canvas, draw bricks, draw ball, draw paddle, draw hearts, update positions.
Collision detection:
- Ball-wall collisions.
- Ball-paddle collisions.
- Ball-brick collisions: for each brick alive, check collision: if ball.x > brick.x && ball.x < brick.x+brick.width && ball.y - ball.radius < brick.y+brick.height && ball.y + ball.radius > brick.y. If colliding, reverse dy, set brick.status=0, spawn hearts.
Spawn hearts: function createHeartExplosion(x, y) spawns hearts at that point.
Heart drawing: for each heart in array, draw heart shape at its position with size (maybe 6) and opacity.
Now AI: if useAI, move paddle.x towards ball.x with speed 5: if ball.x < paddle.x + paddle.width/2 - 5, move left; else if ball.x > paddle.x + paddle.width/2 + 5, move right. Clamp within canvas boundaries.
User control: after overlay removed, use mousemove: get canvas mouse x relative to canvas, set paddle.x = mouseX - paddle.width/2, clamp.
Also optional: arrow keys for left/right.
Now game over: not needed.
Now we need to ensure the game is visible on first paint: we start loop immediately. The overlay covers but semi-transparent. So screenshot will show the game in action.
Now, we must ensure the code runs with no errors. Also ensure no external network requests except allowed. We'll not use any CDN.
Potential errors: Need to handle canvas scaling for mouse events. Use event.offsetX? That gives relative to canvas element after CSS scaling? Actually offsetX is relative to the canvas element's CSS layout (i.e., relative to displayed size). If we have CSS scaling, offsetX may be scaled proportionally? Let's test: offsetX gives pixel coordinate relative to element's CSS layout, which may be scaled. But we can compute scale factor: scaleX = canvas.width / canvas.clientWidth; same for Y. Then canvasX = event.offsetX * scaleX.
Simplify: we can use event.clientX minus canvasRect.left, then multiply by scaleX. So implement that.
Now, for the ball speed, we need to ensure the ball doesn't go too fast.
Now, let's draft the code.
We'll start with <!DOCTYPE html> and <html>.
Inside <head>, <meta charset="UTF-8"> <title>Breakout with Hamburger Paddle and Heart Explosion</title>.
Add style block for body, container, canvas, overlay.
Add script block after HTML elements.
Make sure no console errors.
Potential issues: using requestAnimationFrame before canvas is ready. Use window.onload or DOMContentLoaded.
Now details of the drawing functions:
- drawBall: ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI*2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.closePath();
- drawHamburger: as defined.
Define function drawHamburger(ctx, x, y, w, h):
```
function drawHamburger(ctx, x, y, w, h) {
const bunHeight = h * 0.35;
const pattyHeight = h * 0.3;
const bunRadiusX = w / 2;
const bunRadiusY = bunHeight / 2;
const centerX = x + w / 2;
// top bun
ctx.fillStyle = '#d2691e';
ctx.beginPath();
ctx.ellipse(centerX, y + bunRadiusY, bunRadiusX, bunRadiusY, 0, 0, Math.PI * 2);
ctx.fill();
// patty
ctx.fillStyle = '#8b4513';
ctx.fillRect(x, y + bunHeight, w, pattyHeight);
// bottom bun
ctx.fillStyle = '#d2691e';
ctx.beginPath();
ctx.ellipse(centerX, y + bunHeight + pattyHeight + bunRadiusY, bunRadiusX, bunRadiusY, 0, 0, Math.PI * 2);
ctx.fill();
}
```
Note: This draws the top and bottom buns as full ellipses (circles). That may look like buns with no flat bottom; but it's okay.
Alternatively, we could draw only half ellipses using arcs, but full ellipse is fine.
Now, for the paddle collision detection, use bounding box of paddle.
Define paddle = { x: (canvas.width - paddleWidth)/2, y: canvas.height - paddleHeight - 10, width: paddleWidth, height: paddleHeight, speed: 5 };
Now implement AI movement: if useAI, move.
Define update function:
```
function update() {
// move ball
ball.x += ball.dx;
ball.y += ball.dy;
// wall collisions
if(ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.dx = -ball.dx;
}
if(ball.y - ball.radius < 0) {
ball.dy = -ball.dy;
} else if(ball.y + ball.radius > canvas.height) {
// bounce off bottom
ball.dy = -ball.dy;
}
// paddle collision
if(ball.y + ball.radius >= paddle.y && ball.y + ball.radius <= paddle.y + paddle.height && ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
ball.dy = -ball.dy;
// optionally adjust dx based on hit point
let hitPos = (ball.x - paddle.x) / paddle.width;
ball.dx = (hitPos - 0.5) * 6; // change direction based on where it hits
}
// brick collisions
for(let i=0; i<bricks.length; i++) {
let b = bricks[i];
if(b.status) {
if(ball.x > b.x && ball.x < b.x + b.width && ball.y - ball.radius < b.y + b.height && ball.y + ball.radius > b.y) {
ball.dy = -ball.dy;
b.status = 0;
createHeartExplosion(b.x + b.width/2, b.y + b.height/2);
}
}
}
// update hearts
for(let i = hearts.length - 1; i >= 0; i--) {
let p = hearts[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 1;
p.opacity = Math.max(p.life / p.maxLife, 0);
if(p.life <= 0) hearts.splice(i, 1);
}
// AI paddle movement
if(useAI) {
const paddleCenter = paddle.x + paddle.width / 2;
const diff = ball.x - paddleCenter;
if(Math.abs(diff) > 5) {
paddle.x += Math.sign(diff) * paddle.speed;
}
// clamp
if(paddle.x < 0) paddle.x = 0;
if(paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
}
}
```
Now draw function:
```
function draw() {
ctx.clearRect(0,0,canvas.width,canvas.height);
// draw bricks
for(let i = 0; i < bricks.length; i++) {
const b = bricks[i];
if(b.status) {
ctx.fillStyle = b.color;
ctx.fillRect(b.x, b.y, b.width, b.height);
}
}
// draw ball
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI*2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.closePath();
// draw paddle as hamburger
drawHamburger(ctx, paddle.x, paddle.y, paddle.width, paddle.height);
// draw hearts
for(let i=0; i<hearts.length; i++) {
const p = hearts[i];
drawHeart(ctx, p.x, p.y, p.size, p.opacity);
}
}
```
Now heart shape drawing:
```
function drawHeart(ctx, x, y, size, opacity) {
ctx.save();
ctx.translate(x, y);
ctx.scale(size/30, size/30); // base size 30 for scaling
ctx.beginPath();
ctx.moveTo(0, -15);
ctx.bezierCurveTo(-15, -30, -30, -7, 0, 15);
ctx.bezierCurveTo(30, -7, 15, -30, 0, -15);
ctx.closePath();
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
ctx.fill();
ctx.restore();
}
```
But need to define size and base shape accordingly. Let's design heart path with size scaling.
Alternatively, we can create a heart path using arcs:
Simpler: draw heart as a polygon with two arcs. Let's define a function that draws a heart shape using moveTo, arc, lineTo.
But above approach using bezier curves works: we start at top point (0,-15) then left side curve to bottom point (0,15) then right side curve back.
But need to scale based on size. The base shape uses size of maybe 30 for width and height. Then we can scale accordingly.
Thus in drawHeart, we call drawHeart with p.x, p.y, p.size, p.opacity. p.size is maybe 15-20.
Set p.size = random between 8-15.
Now createHeartExplosion: generate many particles.
```
function createHeartExplosion(x, y) {
const count = 30;
for(let i=0; i<count; i++) {
const angle = Math.random()*2*Math.PI;
const speed = Math.random()*3 + 1;
hearts.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: Math.random()*8 + 8,
life: Math.random()*30 + 30,
maxLife: Math.random()*30 + 30,
opacity: 1
});
}
}
```
Now the ball and paddle speeds: ball.dx and dy initial random or predetermined.
Define ball = { x: canvas.width/2, y: canvas.height-30, radius: 8, dx: 3, dy: -3 };
Define paddle speed = 5.
Define useAI = true initially.
Define overlay visible = true. On click or keydown, hide overlay and set useAI = false (maybe after user wants to control). But we can still let AI continue while user can also control; but perhaps we set useAI = false to enable user control.
Add event listeners:
```
overlay.addEventListener('click', startGame);
document.addEventListener('keydown', startGame);
```
function startGame() { overlay.style.display = 'none'; useAI = false; // add mousemove listener etc }
Add mousemove to canvas:
```
canvas.addEventListener('mousemove', function(e){
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
paddle.x = mouseX - paddle.width/2;
// clamp
if(paddle.x < 0) paddle.x = 0;
if(paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
});
```
Now, the game loop:
```
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
```
Now we need to initialize bricks.
Define brickRowCount = 5, brickColumnCount = 10, brickWidth = (canvas.width - (brickColumnCount+1)*brickPadding) / brickColumnCount, brickHeight = 20, brickPadding = 5, brickOffsetTop = 50, brickOffsetLeft = 30.
Initialize bricks array:
```
let bricks = [];
for(let r=0; r<brickRowCount; r++) {
for(let c=0; c<brickColumnCount; c++) {
const brickX = brickOffsetLeft + c * (brickWidth + brickPadding);
const brickY = brickOffsetTop + r * (brickHeight + brickPadding);
const color = `hsl(${(r*brickColumnCount + c) * 12 % 360}, 70%, 60%)`;
bricks.push({
x: brickX,
y: brickY,
width: brickWidth,
height: brickHeight,
status: 1,
color: color
});
}
}
```
Now hearts array: let hearts = [].
Now we need to handle resizing: if window is resized, the canvas remains at 800x600, but CSS scales. We can add an event listener to adjust canvas size if needed. But for simplicity, we can keep canvas fixed at 800x600; CSS scaling ensures responsiveness.
Now we need to ensure the overlay message is visible and clear.
Add CSS:
```
html,body{
height:100%;
margin:0;
background:#111;
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-family:Arial,Helvetica,sans-serif;
}
#gameContainer{
position:relative;
max-width:800px;
width:100%;
}
canvas{
display:block;
width:100%;
height:auto;
background:#000;
border:2px solid #fff;
}
#overlay{
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
background:rgba(0,0,0,0.6);
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-size:1.5rem;
text-align:center;
z-index:2;
}
```
Now script:
Wrap everything in an IIFE or after DOMContentLoaded.
Potential issues: The ball may get stuck in paddle if speed not correct. The AI may keep moving paddle into ball collision; it's okay.
Now we need to ensure that the heart shape draws correctly. The function drawHeart uses ctx.translate(x,y) then scale. We need to define the base shape coordinates accordingly. Our base shape coordinates assume a coordinate system where the heart is centered at origin? We started at moveTo(0,-15) (top of heart). Then bezier curves to bottom point (0,15). That shape is symmetric. We'll then fill with color.
But after scaling, the shape size will be size/30 times base. For size ~10-20, shape will be small.
Alternatively, we can predefine heart shape path as function drawHeartAt(x,y,size,opacity).
Testing shape: We can try base heart size width approx 30 and height 30.
Better to compute base shape:
```
function drawHeart(ctx, x, y, size, opacity) {
ctx.save();
ctx.translate(x, y);
const scale = size / 30;
ctx.scale(scale, scale);
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.bezierCurveTo(-10, -20, -20, -5, 0, 10);
ctx.bezierCurveTo(20, -5, 10, -20, 0, -10);
ctx.closePath();
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
ctx.fill();
ctx.restore();
}
```
But need to adjust.
We can also use simpler shape: draw a small heart using polygon path:
```
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x - size/2, y - size/2);
ctx.arc(x - size/4, y - size/2, size/4, Math.PI, 0);
ctx.arc(x + size/4, y - size/2, size/4, Math.PI, 0);
ctx.lineTo(x, y);
ctx.closePath();
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
ctx.fill();
```
But that might be a bit off. Better to use bezier.
I'll stick with a simple approach: draw heart using two circles and a triangle:
```
function drawHeart(ctx, x, y, size, opacity) {
const topRadius = size / 2;
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
// left circle
ctx.beginPath();
ctx.arc(-topRadius/2, -topRadius/2, topRadius/2, 0, Math.PI * 2);
ctx.arc(topRadius/2, -topRadius/2, topRadius/2, 0, Math.PI * 2);
ctx.moveTo(-topRadius, 0);
ctx.lineTo(0, topRadius);
ctx.lineTo(topRadius, 0);
ctx.closePath();
ctx.fill();
ctx.restore();
}
```
But this draws a shape similar to a heart. However, the arcs may overlap correctly.
Alternatively, we can use path that approximates heart shape with two arcs and a triangle.
Better approach: use path: start at bottom point, go up left side, curve around left bump, then right bump, close.
```
ctx.beginPath();
ctx.moveTo(0, size/2);
ctx.bezierCurveTo(-size/2, size/2, -size/2, 0, 0, -size/2);
ctx.bezierCurveTo(size/2, 0, size/2, size/2, 0, size/2);
ctx.closePath();
```
That is a heart shape using two quadratic curves.
But I think the earlier bezier approach with base shape is fine.
Let's refine heart drawing.
We can use:
```
function drawHeart(ctx, x, y, size, opacity) {
const top = size * 0.3;
const bottom = size;
const width = size;
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
ctx.beginPath();
ctx.moveTo(0, bottom);
ctx.bezierCurveTo(-width/2, bottom - top, -width/2, -top, 0, -top);
ctx.bezierCurveTo(width/2, -top, width/2, bottom - top, 0, bottom);
ctx.closePath();
ctx.fill();
ctx.restore();
}
```
Better: using width = size, top = size*0.3. Let's test: For size=10, bottom = 10, top=3. So shape will be small heart. For larger size, shape bigger.
We'll need to offset the shape: we set origin at (0,0) then move to bottom point (0, bottom). That may draw a heart anchored at bottom. But we can shift origin accordingly: we want heart centered at given x,y. So we can adjust translation accordingly: ctx.translate(x, y - size/2) maybe.
Better: define heart path with center at (0,0). For simplicity, use the path we used earlier with moveTo(0, -10) etc. Let's adopt that.
Thus:
```
function drawHeart(ctx, x, y, size, opacity) {
ctx.save();
ctx.translate(x, y);
const s = size / 30; // base size is 30
ctx.scale(s, s);
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.bezierCurveTo(-10, -20, -20, -5, 0, 10);
ctx.bezierCurveTo(20, -5, 10, -20, 0, -10);
ctx.closePath();
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
ctx.fill();
ctx.restore();
}
```
Base shape uses height approx 20 and width approx 20. So size scaling works.
Now the createHeartExplosion uses random size between 8 and 15. The hearts will be drawn accordingly.
Now test overall.
Now we need to ensure that everything runs in sandbox with no errors. Let's check for potential errors.
- The overlay element: we need to set overlay variable to document.getElementById('overlay').
- The startGame function: we need to remove the overlay display and set useAI to false, maybe also remove event listeners for overlay.
- The keydown listener: we need to call startGame only once; but it's fine if called multiple times.
- Use of `canvas.clientWidth` inside mousemove; ensure canvas is defined.
- Use of `ctx` variable, defined after getting canvas context.
Potential issues:
- In update, ball.y + ball.radius > canvas.height triggers bounce off bottom; that may cause the ball to never go below bottom. In breakout, when ball hits bottom, usually game over, but for demo we bounce. That will keep ball bouncing.
- The ball's dx may become zero if ball hits paddle exactly center; but we set dx based on hitPos (ball.x - paddle.x)/paddle.width - 0.5 times 6; that may set dx to zero. That is okay; ball may move vertically, but still bounce off walls.
- Brick collision detection: we only reverse dy and set status. If ball hits brick from side, it might need dx reversed, but not required for demo.
- The AI moves paddle based on ball.x, but if ball moves too fast horizontally, paddle may not keep up; but speed is 5 and ball.dx is up to ~6. That is okay.
- The heart particles: they are drawn after clearing canvas, but hearts may be drawn behind bricks and ball? The order: bricks, ball, paddle, hearts. But hearts should be drawn on top maybe after ball. But hearts are created at brick location, and then move; we can draw hearts after everything else, so they appear on top.
Thus, after paddle draw, draw hearts.
Alternatively, we may draw hearts before ball for layering. But it's okay.
- The heart drawing uses ctx.save and restore; that is fine.
- The overlay covers the canvas; the canvas will still be drawn behind but not visible due to overlay's opacity. However, we want the AI to be visible behind overlay. The overlay with rgba black 0.6 will dim the canvas but still allow visibility. That is acceptable.
- The overlay is visible on first paint, but we must ensure it does not block pointer events for AI; AI runs regardless of overlay.
- The game loop runs automatically; the AI moves paddle.
- The screenshot after 3 seconds will show the canvas with overlay but with underlying game visible.
Potential improvement: add some initial ball speed and direction to ensure bricks get broken quickly. Starting ball at center bottom moving up-right will hit bricks soon. Good.
Now test code manually.
Full HTML:
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Breakout with Hamburger Paddle and Heart Explosion</title>
<style>
html,body{
height:100%;
margin:0;
background:#111;
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-family:Arial,Helvetica,sans-serif;
}
#gameContainer{
position:relative;
max-width:800px;
width:100%;
}
canvas{
display:block;
width:100%;
height:auto;
background:#000;
border:2px solid #fff;
}
#overlay{
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
background:rgba(0,0,0,0.6);
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-size:1.5rem;
text-align:center;
z-index:2;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="overlay">Click or press any key to start</div>
</div>
<script>
(() => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
// Game objects
const ball = {
x: canvas.width / 2,
y: canvas.height - 60,
radius: 8,
dx: 3,
dy: -3
};
const paddle = {
width: 80,
height: 30,
x: (canvas.width - 80) / 2,
y: canvas.height - 40,
speed: 5
};
let useAI = true; // AI controls paddle initially
// Bricks
const brickRowCount = 5;
const brickColumnCount = 10;
const brickPadding = 5;
const brickOffsetTop = 50;
const brickOffsetLeft = 30;
const brickWidth = (canvas.width - 2 * brickOffsetLeft - (brickColumnCount - 1) * brickPadding) / brickColumnCount;
const brickHeight = 20;
const bricks = [];
for (let r = 0; r < brickRowCount; r++) {
for (let c = 0; c < brickColumnCount; c++) {
const brickX = brickOffsetLeft + c * (brickWidth + brickPadding);
const brickY = brickOffsetTop + r * (brickHeight + brickPadding);
const hue = ((r * brickColumnCount + c) * 12) % 360;
bricks.push({
x: brickX,
y: brickY,
width: brickWidth,
height: brickHeight,
status: 1,
color: `hsl(${hue}, 70%, 60%)`
});
}
}
// Heart particles
const hearts = [];
// Drawing functions
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.closePath();
}
function drawHamburger(ctx, x, y, w, h) {
const bunHeight = h * 0.35;
const pattyHeight = h * 0.3;
const bunRadiusX = w / 2;
const bunRadiusY = bunHeight / 2;
const cx = x + w / 2;
// top bun
ctx.fillStyle = '#d2691e';
ctx.beginPath();
ctx.ellipse(cx, y + bunRadiusY, bunRadiusX, bunRadiusY, 0, 0, Math.PI * 2);
ctx.fill();
// patty
ctx.fillStyle = '#8b4513';
ctx.fillRect(x, y + bunHeight, w, pattyHeight);
// bottom bun
ctx.fillStyle = '#d2691e';
ctx.beginPath();
ctx.ellipse(cx, y + bunHeight + pattyHeight + bunRadiusY, bunRadiusX, bunRadiusY, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawBricks() {
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
if (b.status) {
ctx.fillStyle = b.color;
ctx.fillRect(b.x, b.y, b.width, b.height);
}
}
}
function drawHeart(ctx, x, y, size, opacity) {
ctx.save();
ctx.translate(x, y);
const scale = size / 30; // base heart size ~30
ctx.scale(scale, scale);
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.bezierCurveTo(-10, -20, -20, -5, 0, 10);
ctx.bezierCurveTo(20, -5, 10, -20, 0, -10);
ctx.closePath();
ctx.fillStyle = `rgba(255,0,0,${opacity})`;
ctx.fill();
ctx.restore();
}
function drawHearts() {
for (let i = 0; i < hearts.length; i++) {
const p = hearts[i];
drawHeart(ctx, p.x, p.y, p.size, p.opacity);
}
}
// Game logic
function update() {
// Move ball
ball.x += ball.dx;
ball.y += ball.dy;
// Wall collisions
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.dx = -ball.dx;
}
if (ball.y - ball.radius < 0) {
ball.dy = -ball.dy;
} else if (ball.y + ball.radius > canvas.height) {
// Bounce off bottom to keep demo running
ball.dy = -ball.dy;
}
// Paddle collision
if (ball.y + ball.radius >= paddle.y && ball.y + ball.radius <= paddle.y + paddle.height &&
ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
ball.dy = -ball.dy;
// Adjust dx based on hit location for more lively motion
const hitPos = (ball.x - paddle.x) / paddle.width;
ball.dx = (hitPos - 0.5) * 6;
}
// Brick collisions
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
if (b.status) {
if (ball.x > b.x && ball.x < b.x + b.width &&
ball.y - ball.radius < b.y + b.height && ball.y + ball.radius > b.y) {
ball.dy = -ball.dy;
b.status = 0;
createHeartExplosion(b.x + b.width / 2, b.y + b.height / 2);
}
}
}
// Update hearts
for (let i = hearts.length - 1; i >= 0; i--) {
const p = hearts[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 1;
p.opacity = Math.max(p.life / p.maxLife, 0);
if (p.life <= 0) {
hearts.splice(i, 1);
}
}
// AI paddle movement
if (useAI) {
const paddleCenter = paddle.x + paddle.width / 2;
const diff = ball.x - paddleCenter;
if (Math.abs(diff) > 5) {
paddle.x += Math.sign(diff) * paddle.speed;
}
// Clamp within bounds
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawHamburger(ctx, paddle.x, paddle.y, paddle.width, paddle.height);
drawHearts();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
// Heart explosion
function createHeartExplosion(x, y) {
const count = 30 + Math.random() * 10;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 2 + 1;
hearts.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: Math.random() * 8 + 8,
life: Math.random() * 30 + 30,
maxLife: Math.random() * 30 + 30,
opacity: 1
});
}
}
// Start demo
requestAnimationFrame(loop);
// Interaction to hide overlay and enable player control
function startGame() {
overlay.style.display = 'none';
useAI = false;
// Enable mouse control
canvas.addEventListener('mousemove', onMouseMove);
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
paddle.x = mouseX - paddle.width / 2;
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
}
overlay.addEventListener('click', startGame);
document.addEventListener('keydown', startGame);
})();
</script>
</body>
</html>
```
Now we need to ensure the code uses only allowed features, no network requests. No external images. The code is inline. The overlay is visible on first paint.
Now verify potential errors:
- In drawHeart, using ctx.scale(scale, scale); but we also translate to x,y. The base shape uses coordinates relative to (0,0) as center? Actually we use moveTo(0,-10) etc. So the shape is centered at (0,0)? Not exactly, because the shape extends from -10 to +10 in both x and y? The shape's bottom point is at y=10 (from moveTo (0,-10) to bezier curves and back). The shape's bounding box is roughly from x=-20 to 20 and y=-20 to 10 maybe. So the translation to (x,y) places the shape at that position. Might need offset for better centering. However, it's okay; hearts will appear around the explosion center.
- In drawHeart, the base shape has coordinates that may not be centered exactly. We can accept that.
- In AI movement, we need to clamp paddle.x after moving. Done.
- In paddle.y definition: we set y = canvas.height - 40. Paddle height is 30, so bottom of paddle is at y+height = canvas.height - 10. That leaves 10 px margin at bottom. Good.
- Ball initial position y = canvas.height - 60, above paddle.
- Ball initial dy = -3, moving upward.
- Ball initial dx = 3, moving right.
- On paddle collision, we adjust dx = (hitPos - 0.5) * 6; that gives dx in [-3, 3]. If hitPos = 0 (left edge), dx = -3; if hitPos = 1 (right edge), dx = 3. Good.
- On bottom bounce, we set dy = -dy; ball bounces, so no game over.
- Brick collision: after hitting brick, we reverse dy and set brick.status=0 and create hearts. That will cause ball to bounce upward perhaps.
- Heart explosion: spawns hearts with random speed 1-3. They have life ~30-60 frames, fade out.
- Use of Math.random for colors and hearts.
- The overlay is placed above canvas with absolute position and z-index:2, canvas is default with z-index:0 (implicitly). The overlay will be visible and capture clicks.
- When startGame is called, overlay.style.display = 'none', useAI = false, and we add mousemove listener.
- We also add document keydown listener to start game; after that, maybe we want to remove keydown listener. Not necessary.
Potential improvements:
- On keydown we might also want to support arrow keys for paddle. But not required.
- The overlay text: "Click or press any key to start". Good.
- The canvas is responsive: CSS width:100% ensures scaling to container width. Max width:800px. For small viewports, canvas will shrink.
- The canvas height is auto; preserving aspect ratio of 800x600.
- The body is flex container centering.
- The code runs on page load.
Potential issues:
- The hearts may be drawn too large relative to base shape if size scaling is not appropriate.
- The heart shape might be too large or offset.
We can test a bit logically: Suppose size = 10, then scale = 10/30 = 0.333. So base shape coordinates range from -20 to 20 in x, scaled to -6.66 to 6.66. y range maybe -20 to 10 scaled to -6.66 to 3.33. So heart fits within approx 13x10 size. That seems okay.
Now we need to ensure there are no console errors like undefined variables.
- In createHeartExplosion, we use Math.random()*10 + 30 for count? Actually we set const count = 30 + Math.random() * 10; That yields non-integer. Loop i < count will be okay because i++ integer; but count may be fractional, e.g., 35.2; loop runs until i<35.2 => i goes 0..35 inclusive? Actually i will go up to 35, as i = 35 < 35.2 true, i=36 false. So it will produce floor(count) + 1 hearts. That's fine.
- In hearts array, we use property life and maxLife; we compute opacity = life / maxLife each frame. The maxLife is random each heart. That works.
- The heart drawing uses size to scale the shape; size is random between 8 and 16; scale will be e.g., size/30 = 0.27 to 0.53, giving appropriate size.
- The heart shape uses ctx.fillStyle with rgba(255,0,0,opacity). Good.
- In update, we check brick collision and reverse dy only; but if ball hits a brick from the side, reversing dy might cause ball to bounce incorrectly. But for demo, it's okay.
- The ball may sometimes get stuck inside a brick due to overlapping after collision detection; but we only reverse dy. Might cause ball to get stuck and not break further bricks, but it's fine.
- The AI may be moving paddle quickly to follow ball; but it's okay.
- The overlay is displayed initially. The AI runs behind. The screenshot after 3 seconds will show the overlay covering the game but visible behind.
- After user clicks, overlay disappears; AI stops, and mouse control is enabled. Not needed for demo.
Potential improvements: maybe the AI can be disabled after user interacts. Already done.
Potential issues: The overlay may hide the canvas entirely (like black background). But because overlay background is rgba(0,0,0,0.6) it's partially transparent black, so the game behind is visible. That meets requirement that something meaningful must be visible on first paint; overlay should be semi-transparent to still see game. But maybe better to set overlay background to transparent and just show text. However, the requirement does not forbid overlay covering entire canvas. It says "Games must start in a self-playing demo mode behind a 'Click or press a key to start' message, so the game can be judged in action without playing." That suggests overlay should be behind a message but not block view; i.e., overlay with message and maybe dimming. Our overlay dims but shows underlying game, which is fine.
One more thought: we might want to hide the overlay after some time? Not required.
Now test code mentally:
On load, IIFE runs.
canvas = getElementById, ctx, overlay.
Define ball and paddle.
useAI = true.
Create bricks.
Hearts array empty.
Define draw functions.
Define update.
Define loop.
Define createHeartExplosion.
Start loop via requestAnimationFrame.
Add overlay click listener and keydown.
Now first paint: loop runs, update moves ball, draws bricks, ball, hamburger paddle, hearts. Since overlay is covering, we see underlying game dimmed.
Now after user interacts, overlay disappears.
Now user can move paddle.
All good.
Potential errors:
- In drawHeart, using ctx.translate(x,y) but not adjusting for shape center; shape's origin is at (0,0) which we treat as center of shape? Actually shape's top is at y=-10 before scaling. The shape's bounding box extends from y=-10 to y=10 (approx). So shape's center is about (0,0). Actually shape is symmetric vertically around y=0? Let's see: path moves from (0,-10) then left curve to (0,10), then right curve back to (0,-10). So shape is symmetric about y=0. So shape center is (0,0). Good. So translation to (x,y) puts heart centered at (x,y). Good.
Now verify that shape draws correctly.
Now we need to ensure we haven't used any banned features: no localStorage, etc.
All good.
Now ensure the HTML is well-formed: <html> <head> <style>... </style> </head> <body> ... </body> </html>. Yes.
Now final step: output the code in a single fenced block with html.
We need to output exactly ONE fenced code block containing the complete file: ```html. No text before or after.
Thus output:
```html
... code ...
```
Will check for any stray backticks. Provide final code.