Hamburger breakoutOne-line
Dots3-Note Preview (free)
by Dots StudioReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 3 min 14 s
- Cost
- $0
- Output tokens
- 19,956
- Spent thinking
- 13,295 tokens
- Speed
- 103.4 tok/s
- Console errors
- 1
- First token after
- 704 ms
- First visible text after
- 2 min 29 s
- Prompt tokens
- 356
- Code size
- 16.9 KB
- Finish reason
- stop
- Served by
- AtlasCloud
- Model id
dots-studio/dots-3-note-preview:free- Reasoning sent
model default- Max output
- 32,000 tokens
- Generated
- Nov 9, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
Failed to load resource: the server responded with a status of 404 (Not Found)
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 · 612 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hamburger Breakout</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Arial Rounded MT Bold', 'Arial', sans-serif;
overflow: hidden;
}
.container {
position: relative;
width: 100%;
max-width: 800px;
padding: 20px;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #fff5f5;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
touch-action: none;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 245, 245, 0.92);
border-radius: 16px;
text-align: center;
padding: 20px;
z-index: 10;
}
.overlay h1 {
color: #d35400;
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 0 #f9e79f;
}
.overlay p {
color: #e67e22;
font-size: 1.2rem;
margin-bottom: 20px;
}
.overlay button {
background: #e67e22;
color: white;
border: none;
padding: 12px 30px;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
box-shadow: 0 4px 0 #d35400;
}
.overlay button:hover {
background: #d35400;
transform: translateY(2px);
box-shadow: 0 2px 0 #d35400;
}
.overlay button:active {
transform: translateY(4px);
box-shadow: 0 0 0 #d35400;
}
.hidden {
display: none;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 15px;
color: #d35400;
font-size: 1.2rem;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="overlay" id="startScreen">
<h1>🍔 Hamburger Breakout 🍔</h1>
<p>Break all the bricks! The paddle is a delicious hamburger.</p>
<p>Move with mouse/touch or arrow keys.</p>
<button id="startBtn">Click or Press Key to Start</button>
</div>
<div class="stats">
<div>Score: <span id="score">0</span></div>
<div>Lives: <span id="lives">3</span></div>
<div>Level: <span id="level">1</span></div>
</div>
</div>
<script>
// Game constants
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const startScreen = document.getElementById('startScreen');
const startBtn = document.getElementById('startBtn');
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const levelEl = document.getElementById('level');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// Audio context
let audioContext;
function playSound(frequency, duration, type = 'square') {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = frequency;
oscillator.type = type;
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
oscillator.start();
oscillator.stop(audioContext.currentTime + duration);
}
// Game state
let game = {
playing: false,
demoMode: true,
ball: {
x: canvasWidth / 2,
y: canvasHeight - 80,
dx: 4,
dy: -4,
radius: 8
},
paddle: {
x: canvasWidth / 2 - 60,
y: canvasHeight - 50,
width: 120,
height: 40,
speed: 8
},
bricks: [],
hearts: [],
score: 0,
lives: 3,
level: 1,
brickRows: 5,
brickCols: 10,
brickWidth: 70,
brickHeight: 25,
brickPadding: 10,
brickOffsetTop: 60,
brickOffsetLeft: 30
};
// Brick colors
const brickColors = ['#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db', '#9b59b6', '#e91e63'];
// Input handling
let keys = {};
let mouseX = canvasWidth / 2;
let touchX = canvasWidth / 2;
let isTouch = false;
window.addEventListener('keydown', (e) => {
keys[e.key] = true;
if (game.demoMode && (e.key === ' ' || e.key === 'Enter')) {
startPlayerGame();
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) * (canvasWidth / rect.width);
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
isTouch = true;
const rect = canvas.getBoundingClientRect();
touchX = (e.touches[0].clientX - rect.left) * (canvasWidth / rect.width);
}, { passive: false });
startBtn.addEventListener('click', startPlayerGame);
canvas.addEventListener('click', startPlayerGame);
function startPlayerGame() {
if (game.demoMode) {
// Resume audio context on user gesture
if (audioContext && audioContext.state === 'suspended') {
audioContext.resume();
}
game.demoMode = false;
startScreen.classList.add('hidden');
// Reset game for player
resetBall();
createBricks();
game.lives = 3;
game.score = 0;
game.level = 1;
updateStats();
}
}
function resetBall() {
game.ball.x = canvasWidth / 2;
game.ball.y = canvasHeight - 80;
// Random angle
const angle = Math.random() * Math.PI - Math.PI/2;
const speed = 5;
game.ball.dx = Math.cos(angle) * speed;
game.ball.dy = -Math.abs(Math.sin(angle) * speed);
if (Math.abs(game.ball.dx) < 1) game.ball.dx = 1.5;
}
function createBricks() {
game.bricks = [];
for (let r = 0; r < game.brickRows; r++) {
for (let c = 0; c < game.brickCols; c++) {
const brick = {
x: c * (game.brickWidth + game.brickPadding) + game.brickOffsetLeft,
y: r * (game.brickHeight + game.brickPadding) + game.brickOffsetTop,
width: game.brickWidth,
height: game.brickHeight,
color: brickColors[r % brickColors.length],
hits: 1,
visible: true
};
game.bricks.push(brick);
}
}
}
// Game loop
function gameLoop() {
if (!game.playing) return;
update();
draw();
requestAnimationFrame(gameLoop);
}
function update() {
// Move paddle
if (isTouch) {
game.paddle.x = touchX - game.paddle.width / 2;
} else if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
game.paddle.x -= game.paddle.speed;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
game.paddle.x += game.paddle.speed;
} else {
game.paddle.x = mouseX - game.paddle.width / 2;
}
// Clamp paddle
if (game.paddle.x < 0) game.paddle.x = 0;
if (game.paddle.x + game.paddle.width > canvasWidth) game.paddle.x = canvasWidth - game.paddle.width;
// Demo mode AI
if (game.demoMode) {
const paddleCenter = game.paddle.x + game.paddle.width / 2;
const diff = game.ball.x - paddleCenter;
if (Math.abs(diff) > 2) {
game.paddle.x += diff * 0.1;
}
if (game.paddle.x < 0) game.paddle.x = 0;
if (game.paddle.x + game.paddle.width > canvasWidth) game.paddle.x = canvasWidth - game.paddle.width;
}
// Move ball
game.ball.x += game.ball.dx;
game.ball.y += game.ball.dy;
// Ball wall collisions
if (game.ball.x + game.ball.radius > canvasWidth || game.ball.x - game.ball.radius < 0) {
game.ball.dx = -game.ball.dx;
}
if (game.ball.y - game.ball.radius < 0) {
game.ball.dy = -game.ball.dy;
}
// Ball bottom collision
if (game.ball.y + game.ball.radius > canvasHeight) {
game.lives--;
updateStats();
if (game.lives <= 0) {
gameOver();
} else {
resetBall();
}
}
// Ball-paddle collision
if (
game.ball.y + game.ball.radius > game.paddle.y &&
game.ball.x > game.paddle.x &&
game.ball.x < game.paddle.x + game.paddle.width &&
game.ball.dy > 0
) {
const hitPos = (game.ball.x - (game.paddle.x + game.paddle.width / 2)) / (game.paddle.width / 2);
const angle = hitPos * (Math.PI / 3);
const speed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
game.ball.dx = Math.sin(angle) * speed;
game.ball.dy = -Math.cos(angle) * speed;
// Slight speed increase
const currentSpeed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
if (currentSpeed < 8) {
game.ball.dx *= 1.02;
game.ball.dy *= 1.02;
}
// Play sound
playSound(330, 0.05, 'sine');
}
// Ball-brick collisions
for (let brick of game.bricks) {
if (brick.visible) {
if (
game.ball.x + game.ball.radius > brick.x &&
game.ball.x - game.ball.radius < brick.x + brick.width &&
game.ball.y + game.ball.radius > brick.y &&
game.ball.y - game.ball.radius < brick.y + brick.height
) {
game.ball.dy = -game.ball.dy;
brick.hits--;
if (brick.hits <= 0) {
brick.visible = false;
game.score += 10;
createHearts(brick.x + brick.width/2, brick.y + brick.height/2);
updateStats();
// Play sound
playSound(440, 0.1, 'square');
}
}
}
}
// Update hearts
for (let heart of game.hearts) {
heart.x += heart.dx;
heart.y += heart.dy;
heart.dy += 0.2;
heart.life--;
if (heart.life <= 0) {
heart.visible = false;
}
}
game.hearts = game.hearts.filter(h => h.visible);
// Check win
if (game.bricks.every(b => !b.visible)) {
levelUp();
}
}
function createHearts(x, y) {
const heartCount = 15;
const colors = ['#ff6b6b', '#ff8e8e', '#ffa8a8', '#ffd43d', '#ffec99'];
for (let i = 0; i < heartCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 4 + 1;
game.hearts.push({
x: x,
y: y,
dx: Math.cos(angle) * speed,
dy: Math.sin(angle) * speed - 2,
life: Math.random() * 60 + 40,
visible: true,
color: colors[Math.floor(Math.random() * colors.length)],
size: Math.random() * 8 + 6
});
}
}
function levelUp() {
game.level++;
updateStats();
resetBall();
createBricks();
// Increase ball speed
const speed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
game.ball.dx *= 1.1;
game.ball.dy *= 1.1;
}
function updateStats() {
scoreEl.textContent = game.score;
livesEl.textContent = game.lives;
levelEl.textContent = game.level;
}
function gameOver() {
game.playing = false;
startScreen.querySelector('h1').textContent = 'Game Over';
startScreen.querySelector('p').innerHTML = `Your score: ${game.score}<br>Click to play again!`;
startScreen.querySelector('button').textContent = 'Play Again';
startScreen.classList.remove('hidden');
// Restart demo behind overlay
game.demoMode = true;
game.playing = true;
resetBall();
createBricks();
game.lives = 3;
game.score = 0;
game.level = 1;
updateStats();
gameLoop();
}
// Drawing functions
function draw() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
drawBackground();
for (let brick of game.bricks) {
if (brick.visible) drawBrick(brick);
}
for (let heart of game.hearts) {
if (heart.visible) drawHeart(heart.x, heart.y, heart.size, heart.color, heart.life);
}
drawBall();
drawHamburgerPaddle();
}
function drawBackground() {
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
gradient.addColorStop(0, '#fff5f5');
gradient.addColorStop(1, '#ffeaa7');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
for (let x = 0; x <= canvasWidth; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvasHeight);
ctx.stroke();
}
for (let y = 0; y <= canvasHeight; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvasWidth, y);
ctx.stroke();
}
}
function drawBrick(brick) {
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fillStyle = brick.color;
roundRect(ctx, brick.x, brick.y, brick.width, brick.height, 5);
ctx.fill();
ctx.shadowColor = 'rgba(255,255,255,0.4)';
ctx.shadowBlur = 2;
ctx.shadowOffsetX = -1;
ctx.shadowOffsetY = -1;
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.beginPath();
ctx.moveTo(brick.x + 5, brick.y + 2);
ctx.lineTo(brick.x + brick.width - 5, brick.y + 2);
ctx.lineTo(brick.x + brick.width - 2, brick.y + 5);
ctx.lineTo(brick.x + 2, brick.y + 5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
}
function drawBall() {
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
const gradient = ctx.createRadialGradient(
game.ball.x - 2, game.ball.y - 2, 1,
game.ball.x, game.ball.y, game.ball.radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.5, '#f0f0f0');
gradient.addColorStop(1, '#cccccc');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(game.ball.x, game.ball.y, game.ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHamburgerPaddle() {
const p = game.paddle;
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 4;
// Bottom bun
ctx.fillStyle = '#d4a05a';
roundRect(ctx, p.x, p.y + p.height - 12, p.width, 12, 6);
ctx.fill();
ctx.fillStyle = '#e8c98a';
ctx.fillRect(p.x + 5, p.y + p.height - 12, p.width - 10, 3);
// Patty
ctx.fillStyle = '#5d4037';
roundRect(ctx, p.x + 5, p.y + p.height - 20, p.width - 10, 8, 3);
ctx.fill();
// Cheese
ctx.fillStyle = '#fbc02d';
ctx.fillRect(p.x + 5, p.y + p.height - 24, p.width - 10, 4);
// Lettuce
ctx.fillStyle = '#4caf50';
ctx.beginPath();
ctx.moveTo(p.x + 5, p.y + p.height - 28);
for (let i = 0; i < p.width - 10; i += 5) {
ctx.quadraticCurveTo(p.x + 5 + i + 2.5, p.y + p.height - 32, p.x + 5 + i + 5, p.y + p.height - 28);
}
ctx.lineTo(p.x + p.width - 5, p.y + p.height - 16);
ctx.lineTo(p.x + 5, p.y + p.height - 16);
ctx.closePath();
ctx.fill();
// Top bun
ctx.fillStyle = '#d4a05a';
roundRect(ctx, p.x, p.y, p.width, 12, 6);
ctx.fill();
ctx.fillStyle = '#b8860b';
ctx.fillRect(p.x + 5, p.y + 9, p.width - 10, 3);
// Sesame seeds
ctx.fillStyle = '#f5f5dc';
for (let i = 0; i < p.width - 10; i += 10) {
ctx.beginPath();
ctx.arc(p.x + 5 + i, p.y + 6, 1.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawHeart(x, y, size, color, life) {
ctx.save();
ctx.globalAlpha = Math.max(0, life / 100);
ctx.fillStyle = color;
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 3;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.translate(x, y);
ctx.scale(size, size);
ctx.beginPath();
ctx.moveTo(0, 0.2);
ctx.bezierCurveTo(0, 0, -0.5, 0, -0.5, 0.5);
ctx.bezierCurveTo(-0.5, 1, 0, 1.2, 0, 1.5);
ctx.bezierCurveTo(0, 1.2, 0.5, 1, 0.5, 0.5);
ctx.bezierCurveTo(0.5, 0, 0, 0, 0, 0.2);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Initialize demo mode
window.addEventListener('load', () => {
game.demoMode = true;
game.playing = true;
createBricks();
updateStats();
gameLoop();
// Show start screen
startScreen.classList.remove('hidden');
});
</script>
</body>
</html>Reasoning trace (13,295 tokens)
The user wants a breakout game where:
- The paddle is a hamburger
- Every brick you break explodes into tons of little hearts
- It should be a self-contained web demo (HTML/CSS/JS)
- No external dependencies (except maybe CDN, but let's keep it all inline)
- Should look good in 1280x800 and adapt down to 375px
- Should have a self-playing demo mode behind a "Click or press a key to start" message
- No localStorage, etc.
Let's design the game:
- Canvas-based breakout game.
- Paddle: hamburger (drawn with CSS or canvas? Canvas might be easier for the hearts explosion, but hamburger can be drawn with CSS or canvas. Let's draw everything on canvas for simplicity, but we can also use a CSS hamburger paddle. Actually, drawing a hamburger on canvas is fun. Or we can use an emoji 🍔? But emoji might not be reliable. Let's draw a simple hamburger with canvas: bun top, lettuce, patty, cheese, bun bottom. Or we can use a CSS-styled div for the paddle. But the paddle needs to move, and we need collision detection. Canvas is standard for breakout. Let's draw the hamburger on canvas.
- Bricks: colorful bricks. When broken, they explode into little hearts (drawn on canvas as small hearts, or use heart emoji? Heart emoji might be too big and not scalable. Let's draw hearts with canvas using bezier curves or just a simple heart shape).
- Game loop, physics, score, lives, levels.
- Self-playing demo mode: AI controls the paddle to follow the ball.
- Start screen: "Click or press a key to start".
Let's plan the canvas size: responsive, maybe 800x600 or similar, but we need to fit in 1280x800. We can make the canvas centered, with max-width: 100%.
Let's write the code.
We'll have:
- Canvas element
- CSS to center it, style the start message, etc.
- JavaScript game logic
Game elements:
- Ball: circle
- Paddle: hamburger shape (we can draw it as a rounded rectangle with layers)
- Bricks: rectangles with different colors
- Hearts: small heart shapes that appear on brick break, with random velocities and gravity, fading out.
Let's code the hamburger paddle:
- Bun top: rounded rectangle, light brown
- Lettuce: green wavy line or rectangle
- Patty: dark brown rectangle
- Cheese: yellow rectangle
- Bun bottom: rounded rectangle, light brown
We can draw it as a composite shape. The paddle width can be around 120px, height around 40px.
Hearts: we can draw a heart using canvas path:
```javascript
function drawHeart(ctx, x, y, size, color) {
ctx.save();
ctx.translate(x, y);
ctx.scale(size, size);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0, -0.5, -1, -0.5, -1, 0);
ctx.bezierCurveTo(-1, 1, 0, 1.5, 0, 2);
ctx.bezierCurveTo(0, 1.5, 1, 1, 1, 0);
ctx.bezierCurveTo(1, -0.5, 0, -0.5, 0, 0);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
}
```
But we need to scale it properly. Actually, a simpler heart shape:
```javascript
ctx.beginPath();
ctx.moveTo(x, y + size/2);
ctx.bezierCurveTo(x, y, x - size/2, y, x - size/2, y + size/2);
ctx.bezierCurveTo(x - size/2, y + size, x, y + size * 1.5, x, y + size * 1.5);
ctx.bezierCurveTo(x, y + size * 1.5, x + size/2, y + size, x + size/2, y + size/2);
ctx.bezierCurveTo(x + size/2, y, x, y, x, y + size/2);
ctx.closePath();
```
We can adjust.
Let's design the level: a grid of bricks. Different rows different colors.
When a brick is hit, it loses health (maybe some bricks need multiple hits? Let's keep it simple: one hit breaks them, but we can add multi-hit bricks later. For now, single hit).
Explosion: when a brick breaks, create N hearts (e.g., 10-20) at the brick position, with random velocities, gravity, and fade.
Controls: mouse/touch to move paddle, or keyboard arrows. For demo mode, AI moves paddle to align with ball's x position.
Start message: overlay on canvas or a separate div. We'll use a div over the canvas.
Let's write the full HTML file.
We need to make it responsive: canvas should scale with CSS, but internal resolution can be fixed (e.g., 800x600). We'll set canvas width and height attributes to 800x600, and CSS max-width: 100%, height: auto.
Let's code.
I'll structure the code:
- HTML: canvas, start screen, score/lives display (maybe overlay or below canvas)
- CSS: styling
- JS: game loop, draw functions, update functions, input handling, AI demo mode
Let's start writing.
We'll have a state object:
```
state = {
playing: false,
demoMode: true, // starts in demo mode until user clicks
ball: {x, y, dx, dy, radius},
paddle: {x, y, width, height},
bricks: [],
hearts: [],
score: 0,
lives: 3,
level: 1,
// etc.
}
```
Brick generation: function to create bricks for current level.
Game loop: requestAnimationFrame.
Collision detection: ball with walls, paddle, bricks.
Paddle movement: constrained to canvas.
Input: mouse move, touch move, keyboard arrows. Also click/key to start.
AI: if demoMode and not playing, paddle follows ball.x.
When game over, reset.
Let's write the code now.
I'll make it visually appealing: colorful bricks, nice hamburger, hearts explosion, particles.
Let's also add some sound? No, we can't use external audio files, and no base64 audio unless we generate it. Let's skip sound for simplicity, or use Web Audio API to generate simple beeps? Might be complex. Let's skip sound.
Let's write the code.
We'll use a single canvas for everything.
Let's define colors for bricks: red, orange, yellow, green, blue, purple, pink.
Brick rows: 5 rows, 10 columns? Let's adjust based on canvas width. Canvas width 800, brick width 80, so 10 columns. Height 20, so 5 rows = 100px. Top offset 60.
Paddle: width 120, height 40. Bottom margin 30.
Ball: radius 8.
Let's code.
I'll write the HTML file with embedded CSS and JS.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hamburger Breakout</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Arial Rounded MT Bold', 'Arial', sans-serif;
overflow: hidden;
}
.container {
position: relative;
width: 100%;
max-width: 800px;
padding: 20px;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #fff5f5;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
touch-action: none;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 245, 245, 0.92);
border-radius: 16px;
text-align: center;
padding: 20px;
z-index: 10;
}
.overlay h1 {
color: #d35400;
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 0 #f9e79f;
}
.overlay p {
color: #e67e22;
font-size: 1.2rem;
margin-bottom: 20px;
}
.overlay button {
background: #e67e22;
color: white;
border: none;
padding: 12px 30px;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
box-shadow: 0 4px 0 #d35400;
}
.overlay button:hover {
background: #d35400;
transform: translateY(2px);
box-shadow: 0 2px 0 #d35400;
}
.overlay button:active {
transform: translateY(4px);
box-shadow: 0 0 0 #d35400;
}
.hidden {
display: none;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 15px;
color: #d35400;
font-size: 1.2rem;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="overlay" id="startScreen">
<h1>🍔 Hamburger Breakout 🍔</h1>
<p>Break all the bricks! The paddle is a delicious hamburger.</p>
<p>Move with mouse/touch or arrow keys.</p>
<button id="startBtn">Click or Press Key to Start</button>
</div>
<div class="stats">
<div>Score: <span id="score">0</span></div>
<div>Lives: <span id="lives">3</span></div>
<div>Level: <span id="level">1</span></div>
</div>
</div>
<script>
// Game constants
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const startScreen = document.getElementById('startScreen');
const startBtn = document.getElementById('startBtn');
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const levelEl = document.getElementById('level');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// Game state
let game = {
playing: false,
demoMode: true,
ball: {
x: canvasWidth / 2,
y: canvasHeight - 80,
dx: 4,
dy: -4,
radius: 8
},
paddle: {
x: canvasWidth / 2 - 60,
y: canvasHeight - 50,
width: 120,
height: 40,
speed: 8
},
bricks: [],
hearts: [],
score: 0,
lives: 3,
level: 1,
brickRows: 5,
brickCols: 10,
brickWidth: 70,
brickHeight: 25,
brickPadding: 10,
brickOffsetTop: 60,
brickOffsetLeft: 30
};
// Brick colors
const brickColors = ['#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db', '#9b59b6', '#e91e63'];
// Input handling
let keys = {};
let mouseX = canvasWidth / 2;
let touchX = canvasWidth / 2;
let isTouch = false;
window.addEventListener('keydown', (e) => {
keys[e.key] = true;
if (e.key === ' ' || e.key === 'Enter') {
startGame();
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) * (canvasWidth / rect.width);
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
isTouch = true;
const rect = canvas.getBoundingClientRect();
touchX = (e.touches[0].clientX - rect.left) * (canvasWidth / rect.width);
}, { passive: false });
startBtn.addEventListener('click', startGame);
window.addEventListener('keydown', (e) => {
if ((e.key === ' ' || e.key === 'Enter') && !game.playing) {
startGame();
}
});
function startGame() {
game.playing = true;
game.demoMode = false;
startScreen.classList.add('hidden');
resetBall();
createBricks();
gameLoop();
}
function resetBall() {
game.ball.x = canvasWidth / 2;
game.ball.y = canvasHeight - 80;
// Random angle
const angle = Math.random() * Math.PI - Math.PI/2; // between -90 and 90 degrees from upward? Actually we want it to go up and left/right.
const speed = 5;
game.ball.dx = Math.cos(angle) * speed;
game.ball.dy = -Math.abs(Math.sin(angle) * speed); // ensure it goes up
// But we need to ensure it doesn't go straight up/down.
if (Math.abs(game.ball.dx) < 1) game.ball.dx = 1.5;
}
function createBricks() {
game.bricks = [];
for (let r = 0; r < game.brickRows; r++) {
for (let c = 0; c < game.brickCols; c++) {
const brick = {
x: c * (game.brickWidth + game.brickPadding) + game.brickOffsetLeft,
y: r * (game.brickHeight + game.brickPadding) + game.brickOffsetTop,
width: game.brickWidth,
height: game.brickHeight,
color: brickColors[r % brickColors.length],
hits: 1, // for now, 1 hit breaks
visible: true
};
game.bricks.push(brick);
}
}
}
// Game loop
function gameLoop() {
if (!game.playing) return;
update();
draw();
requestAnimationFrame(gameLoop);
}
function update() {
// Move paddle
if (isTouch) {
game.paddle.x = touchX - game.paddle.width / 2;
} else if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
game.paddle.x -= game.paddle.speed;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
game.paddle.x += game.paddle.speed;
} else {
// Mouse control
game.paddle.x = mouseX - game.paddle.width / 2;
}
// Clamp paddle
if (game.paddle.x < 0) game.paddle.x = 0;
if (game.paddle.x + game.paddle.width > canvasWidth) game.paddle.x = canvasWidth - game.paddle.width;
// Demo mode AI
if (game.demoMode) {
const paddleCenter = game.paddle.x + game.paddle.width / 2;
const diff = game.ball.x - paddleCenter;
if (Math.abs(diff) > 2) {
game.paddle.x += diff * 0.1; // smooth follow
}
// Clamp
if (game.paddle.x < 0) game.paddle.x = 0;
if (game.paddle.x + game.paddle.width > canvasWidth) game.paddle.x = canvasWidth - game.paddle.width;
}
// Move ball
game.ball.x += game.ball.dx;
game.ball.y += game.ball.dy;
// Ball wall collisions
if (game.ball.x + game.ball.radius > canvasWidth || game.ball.x - game.ball.radius < 0) {
game.ball.dx = -game.ball.dx;
}
if (game.ball.y - game.ball.radius < 0) {
game.ball.dy = -game.ball.dy;
}
// Ball bottom collision (lose life)
if (game.ball.y + game.ball.radius > canvasHeight) {
game.lives--;
updateStats();
if (game.lives <= 0) {
gameOver();
} else {
resetBall();
}
}
// Ball-paddle collision
if (
game.ball.y + game.ball.radius > game.paddle.y &&
game.ball.x > game.paddle.x &&
game.ball.x < game.paddle.x + game.paddle.width &&
game.ball.dy > 0 // only if moving down
) {
// Calculate hit position on paddle
const hitPos = (game.ball.x - (game.paddle.x + game.paddle.width / 2)) / (game.paddle.width / 2);
// Change angle based on hit position
const angle = hitPos * (Math.PI / 3); // max 60 degrees
const speed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
game.ball.dx = Math.sin(angle) * speed;
game.ball.dy = -Math.cos(angle) * speed;
// Add a little speed each hit
const currentSpeed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
if (currentSpeed < 8) {
game.ball.dx *= 1.02;
game.ball.dy *= 1.02;
}
}
// Ball-brick collisions
for (let brick of game.bricks) {
if (brick.visible) {
if (
game.ball.x + game.ball.radius > brick.x &&
game.ball.x - game.ball.radius < brick.x + brick.width &&
game.ball.y + game.ball.radius > brick.y &&
game.ball.y - game.ball.radius < brick.y + brick.height
) {
game.ball.dy = -game.ball.dy;
brick.hits--;
if (brick.hits <= 0) {
brick.visible = false;
game.score += 10;
createHearts(brick.x + brick.width/2, brick.y + brick.height/2);
updateStats();
}
}
}
}
// Update hearts
for (let heart of game.hearts) {
heart.x += heart.dx;
heart.y += heart.dy;
heart.dy += 0.2; // gravity
heart.life--;
if (heart.life <= 0) {
heart.visible = false;
}
}
// Remove invisible hearts
game.hearts = game.hearts.filter(h => h.visible);
// Check win condition
if (game.bricks.every(b => !b.visible)) {
levelUp();
}
}
function createHearts(x, y) {
const heartCount = 15;
const colors = ['#ff6b6b', '#ff8e8e', '#ffa8a8', '#ffd43d', '#ffec99'];
for (let i = 0; i < heartCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 4 + 1;
game.hearts.push({
x: x,
y: y,
dx: Math.cos(angle) * speed,
dy: Math.sin(angle) * speed - 2, // initial upward burst
life: Math.random() * 60 + 40, // frames
visible: true,
color: colors[Math.floor(Math.random() * colors.length)],
size: Math.random() * 8 + 6
});
}
}
function levelUp() {
game.level++;
updateStats();
// Reset ball and create new bricks
resetBall();
createBricks();
// Optionally increase ball speed or brick hits
// For now, just increase ball speed a bit
const speed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
game.ball.dx *= 1.1;
game.ball.dy *= 1.1;
}
function updateStats() {
scoreEl.textContent = game.score;
livesEl.textContent = game.lives;
levelEl.textContent = game.level;
}
function gameOver() {
game.playing = false;
startScreen.querySelector('h1').textContent = 'Game Over';
startScreen.querySelector('p').innerHTML = `Your score: ${game.score}<br>Click to play again!`;
startScreen.querySelector('button').textContent = 'Play Again';
startScreen.classList.remove('hidden');
}
// Drawing functions
function draw() {
// Clear canvas
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw background pattern (optional)
drawBackground();
// Draw bricks
for (let brick of game.bricks) {
if (brick.visible) {
drawBrick(brick);
}
}
// Draw hearts
for (let heart of game.hearts) {
if (heart.visible) {
drawHeart(heart.x, heart.y, heart.size, heart.color, heart.life);
}
}
// Draw ball
drawBall();
// Draw paddle (hamburger)
drawHamburgerPaddle();
}
function drawBackground() {
// Simple gradient background
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
gradient.addColorStop(0, '#fff5f5');
gradient.addColorStop(1, '#ffeaa7');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// Draw some subtle grid lines
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
for (let x = 0; x <= canvasWidth; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvasHeight);
ctx.stroke();
}
for (let y = 0; y <= canvasHeight; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvasWidth, y);
ctx.stroke();
}
}
function drawBrick(brick) {
// Brick with 3D effect
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
// Main brick
ctx.fillStyle = brick.color;
roundRect(ctx, brick.x, brick.y, brick.width, brick.height, 5);
ctx.fill();
// Highlight
ctx.shadowColor = 'rgba(255,255,255,0.4)';
ctx.shadowBlur = 2;
ctx.shadowOffsetX = -1;
ctx.shadowOffsetY = -1;
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.beginPath();
ctx.moveTo(brick.x + 5, brick.y + 2);
ctx.lineTo(brick.x + brick.width - 5, brick.y + 2);
ctx.lineTo(brick.x + brick.width - 2, brick.y + 5);
ctx.lineTo(brick.x + 2, brick.y + 5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
}
function drawBall() {
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
// Ball with gradient
const gradient = ctx.createRadialGradient(
game.ball.x - 2, game.ball.y - 2, 1,
game.ball.x, game.ball.y, game.ball.radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.5, '#f0f0f0');
gradient.addColorStop(1, '#cccccc');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(game.ball.x, game.ball.y, game.ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHamburgerPaddle() {
const p = game.paddle;
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 4;
// Bottom bun
ctx.fillStyle = '#d4a05a';
roundRect(ctx, p.x, p.y + p.height - 12, p.width, 12, 6);
ctx.fill();
// Bottom bun top highlight
ctx.fillStyle = '#e8c98a';
ctx.fillRect(p.x + 5, p.y + p.height - 12, p.width - 10, 3);
// Patty
ctx.fillStyle = '#5d4037';
roundRect(ctx, p.x + 5, p.y + p.height - 20, p.width - 10, 8, 3);
ctx.fill();
// Cheese
ctx.fillStyle = '#fbc02d';
ctx.fillRect(p.x + 5, p.y + p.height - 24, p.width - 10, 4);
// Lettuce
ctx.fillStyle = '#4caf50';
// Wavy lettuce
ctx.beginPath();
ctx.moveTo(p.x + 5, p.y + p.height - 28);
for (let i = 0; i < p.width - 10; i += 5) {
ctx.quadraticCurveTo(p.x + 5 + i + 2.5, p.y + p.height - 32, p.x + 5 + i + 5, p.y + p.height - 28);
}
ctx.lineTo(p.x + p.width - 5, p.y + p.height - 16);
ctx.lineTo(p.x + 5, p.y + p.height - 16);
ctx.closePath();
ctx.fill();
// Top bun
ctx.fillStyle = '#d4a05a';
roundRect(ctx, p.x, p.y, p.width, 12, 6);
ctx.fill();
// Top bun bottom shadow
ctx.fillStyle = '#b8860b';
ctx.fillRect(p.x + 5, p.y + 9, p.width - 10, 3);
// Sesame seeds
ctx.fillStyle = '#f5f5dc';
for (let i = 0; i < p.width - 10; i += 10) {
ctx.beginPath();
ctx.arc(p.x + 5 + i, p.y + 6, 1.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawHeart(x, y, size, color, life) {
ctx.save();
ctx.globalAlpha = Math.max(0, life / 100);
ctx.fillStyle = color;
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 3;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.translate(x, y);
ctx.scale(size, size);
ctx.beginPath();
ctx.moveTo(0, 0.5);
ctx.bezierCurveTo(0, 0, -0.5, 0, -0.5, 0.5);
ctx.bezierCurveTo(-0.5, 1, 0, 1.2, 0, 1.5);
ctx.bezierCurveTo(0, 1.2, 0.5, 1, 0.5, 0.5);
ctx.bezierCurveTo(0.5, 0, 0, 0, 0, 0.5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Start demo mode
function startDemo() {
game.demoMode = true;
game.playing = true;
startScreen.classList.add('hidden');
createBricks();
gameLoop();
}
// Auto-start demo after a short delay, or wait for user input
// Let's wait for user input to start, but also have a demo mode that plays automatically if user doesn't interact?
// The requirement: "Games must start in a self-playing demo mode behind a 'Click or press a key to start' message"
// So we show the message, and if the user clicks/presses key, we start the actual game (with their control).
// But we can also have a demo mode that plays automatically if they don't interact? The message says "behind" the message, so the demo is running but hidden? Actually, it says "behind a 'Click or press a key to start' message, so the game can be judged in action without playing."
// That implies the demo is running and visible? But the message is on top. So the user sees the game playing behind the overlay. So we should start the demo immediately, and the overlay covers it. When user clicks, we hide overlay and let them play.
// Let's adjust: start the game loop immediately in demo mode, but show the start screen overlay. The game runs behind it. When user clicks, we hide overlay and set demoMode=false.
// So:
// - On load, start demo mode (game.playing = true, game.demoMode = true)
// - Show start screen overlay
// - Game loop runs, drawing behind the overlay
// - When user clicks or presses key, hide overlay, set demoMode=false, and reset ball? Or continue? Better to reset and let them play.
// Let's modify:
// We'll start the game loop immediately with demo mode.
// The start screen overlay is on top.
// When user clicks, we hide overlay, set demoMode=false, and reset ball to start a fresh game.
// But we need to handle the case where the user clicks while the game is already in demo mode. We'll reset the game for the player.
// Let's change the initialization:
window.addEventListener('load', () => {
// Start demo mode immediately
game.demoMode = true;
game.playing = true;
createBricks();
updateStats();
gameLoop();
// Show start screen
startScreen.classList.remove('hidden');
});
// Modify startBtn and keydown to just hide overlay and start player game
function startPlayerGame() {
if (game.demoMode) {
game.demoMode = false;
startScreen.classList.add('hidden');
// Reset game for player
resetBall();
createBricks();
game.lives = 3;
game.score = 0;
game.level = 1;
updateStats();
}
}
startBtn.addEventListener('click', startPlayerGame);
window.addEventListener('keydown', (e) => {
if ((e.key === ' ' || e.key === 'Enter') && game.demoMode) {
startPlayerGame();
}
});
// Also allow clicking the canvas to start
canvas.addEventListener('click', startPlayerGame);
</script>
</body>
</html>
```
Wait, there are some issues:
- The start screen overlay covers the canvas, but the game loop is running behind it. That's good.
- However, the user might want to use keyboard while the overlay is up. We already have keydown listener for space/enter.
- The demo mode AI follows the ball. But the ball might go out of bounds and the game resets. That's fine for demo.
- When the user clicks to start, we reset everything. But we need to make sure the game loop continues running. It does.
But there's a problem: the game loop is running continuously, but we have `if (!game.playing) return;` at the start of gameLoop. Since we set `game.playing = true` on load, it runs. When the user clicks, we set `game.demoMode = false` but `game.playing` remains true. So it continues.
But what if the user's game ends (game over)? We have `gameOver()` which sets `game.playing = false` and shows the start screen again. But then the game loop stops. We need to restart the demo mode when the start screen is shown again? The requirement says "behind a 'Click or press a key to start' message", so we should probably restart the demo when the overlay appears. Let's modify `gameOver()` to also restart demo mode.
Also, the start screen overlay is shown on game over, but the game loop is stopped. We need to restart the demo behind the overlay. So in `gameOver()`, we should set `game.demoMode = true` and `game.playing = true` and restart the loop.
Let's adjust:
```javascript
function gameOver() {
game.playing = false;
startScreen.querySelector('h1').textContent = 'Game Over';
startScreen.querySelector('p').innerHTML = `Your score: ${game.score}<br>Click to play again!`;
startScreen.querySelector('button').textContent = 'Play Again';
startScreen.classList.remove('hidden');
// Restart demo mode behind the overlay
game.demoMode = true;
game.playing = true;
resetBall();
createBricks();
game.lives = 3;
game.score = 0;
game.level = 1;
updateStats();
gameLoop();
}
```
But wait, if we restart the demo immediately, the user might see the game resetting behind the overlay. That's fine.
Also, we need to make sure the start screen overlay is not hidden when the game is over. We already remove 'hidden' class.
Now, about the hamburger paddle: I drew it with layers. It looks okay but might be improved. The hearts explosion: I drew hearts using bezier curves. They look like hearts.
Let's test the heart shape:
```javascript
ctx.beginPath();
ctx.moveTo(0, 0.5);
ctx.bezierCurveTo(0, 0, -0.5, 0, -0.5, 0.5);
ctx.bezierCurveTo(-0.5, 1, 0, 1.2, 0, 1.5);
ctx.bezierCurveTo(0, 1.2, 0.5, 1, 0.5, 0.5);
ctx.bezierCurveTo(0.5, 0, 0, 0, 0, 0.5);
ctx.closePath();
```
This should draw a heart centered at (0,0) with size scaled. But the heart might be upside down? Let's check: The top of the heart is at y=0.5? Actually, the heart shape: the two lobes at the top, point at the bottom. In my code, I start at (0, 0.5) which is the bottom point? Let's trace:
- moveTo(0, 0.5): this is the bottom point? Actually, typical heart: bottom point is at (0, 1) or something. Let's adjust.
A standard heart path (from top-left lobe to top-right lobe to bottom point):
```
ctx.moveTo(0, 0); // top-left? No.
```
Better to use a known heart path. Let's use:
```javascript
ctx.beginPath();
ctx.moveTo(0, 0.2);
ctx.bezierCurveTo(0, 0, -0.5, 0, -0.5, 0.5);
ctx.bezierCurveTo(-0.5, 1, 0, 1.2, 0, 1.5);
ctx.bezierCurveTo(0, 1.2, 0.5, 1, 0.5, 0.5);
ctx.bezierCurveTo(0.5, 0, 0, 0, 0, 0.2);
ctx.closePath();
```
This might work. But let's test mentally: The heart should have two rounded tops and a pointed bottom. The point is at (0, 1.5) in my code. The tops are at (-0.5, 0.5) and (0.5, 0.5). So the heart is oriented with point down. That's correct.
But the scaling: we do `ctx.scale(size, size)`. So if size=10, the heart will be about 15 units tall (from y=0.2 to y=1.5) and 1 unit wide? Actually, x goes from -0.5 to 0.5, so width 1 unit. So aspect ratio is 1:1.5. That's okay for a heart shape.
We can also rotate or adjust. It should look like a heart.
Let's also add some particle effects for the explosion? The hearts are already particles.
Now, the brick colors: we have 7 colors, 5 rows, so they will repeat.
The hamburger paddle: I used roundRect for buns, and wavy lettuce. It might look okay.
Let's also add some polish:
- Ball trail? Not necessary.
- Sound? We can't easily add sound without external files. We could use Web Audio API to generate a simple beep on brick break or paddle hit. Let's add a simple beep function using Web Audio API. It's inline and no external files.
Let's add sound:
```javascript
let audioContext;
function playSound(frequency, duration, type = 'square') {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = frequency;
oscillator.type = type;
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
oscillator.start();
oscillator.stop(audioContext.currentTime + duration);
}
```
Then call it on brick break (e.g., playSound(440, 0.1)) and paddle hit (playSound(330, 0.05)). But we need to resume audio context on user gesture. We can resume on startPlayerGame.
Let's add that.
Also, we need to make sure the game is responsive. The canvas has CSS width: 100%, height: auto. The internal resolution is 800x600. That's fine.
Now, let's review the code for any errors.
- The `roundRect` function is defined but not used everywhere? I used it for bricks and buns.
- The `drawHeart` function uses `ctx.translate` and `ctx.scale`. It should be fine.
- The `createHearts` function creates hearts with random velocities. The `dy` has an initial upward burst: `Math.sin(angle) * speed - 2`. That's fine.
- The heart `life` is in frames, and we fade by setting `globalAlpha = life / 100`. But life decreases by 1 each frame, so it fades over 100 frames (about 1.6 seconds at 60fps). That's good.
Now, about the demo mode: on load, we start the game loop and show the overlay. The user sees the game playing behind the overlay. When they click, we hide overlay and let them play. But we also reset the game. That's good.
But what if the user doesn't click and just watches? The demo plays indefinitely. That's fine.
Now, let's check the start screen overlay: it has a button and text. The button says "Click or Press Key to Start". We have event listeners on the button, keydown, and canvas click. That should cover it.
One issue: the keydown listener for space/enter is added twice: once on window and once on window? Actually, I added it in the script: `window.addEventListener('keydown', ...)` for starting, and also inside the `startPlayerGame` logic. But I also have a listener for arrow keys etc. That's fine.
Let's clean up the code and make sure it's well-structured.
I'll write the final code now.
We also need to handle the case where the user clicks the canvas while the overlay is hidden? That's fine, it doesn't do anything.
Let's also add a instructions screen or just the start screen.
Now, let's write the complete HTML file.
I'll include the sound, but make sure it's not annoying. Maybe only on brick break.
Let's finalize.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hamburger Breakout</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Arial Rounded MT Bold', 'Arial', sans-serif;
overflow: hidden;
}
.container {
position: relative;
width: 100%;
max-width: 800px;
padding: 20px;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #fff5f5;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
touch-action: none;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 245, 245, 0.92);
border-radius: 16px;
text-align: center;
padding: 20px;
z-index: 10;
}
.overlay h1 {
color: #d35400;
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 0 #f9e79f;
}
.overlay p {
color: #e67e22;
font-size: 1.2rem;
margin-bottom: 20px;
}
.overlay button {
background: #e67e22;
color: white;
border: none;
padding: 12px 30px;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
box-shadow: 0 4px 0 #d35400;
}
.overlay button:hover {
background: #d35400;
transform: translateY(2px);
box-shadow: 0 2px 0 #d35400;
}
.overlay button:active {
transform: translateY(4px);
box-shadow: 0 0 0 #d35400;
}
.hidden {
display: none;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 15px;
color: #d35400;
font-size: 1.2rem;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="overlay" id="startScreen">
<h1>🍔 Hamburger Breakout 🍔</h1>
<p>Break all the bricks! The paddle is a delicious hamburger.</p>
<p>Move with mouse/touch or arrow keys.</p>
<button id="startBtn">Click or Press Key to Start</button>
</div>
<div class="stats">
<div>Score: <span id="score">0</span></div>
<div>Lives: <span id="lives">3</span></div>
<div>Level: <span id="level">1</span></div>
</div>
</div>
<script>
// Game constants
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const startScreen = document.getElementById('startScreen');
const startBtn = document.getElementById('startBtn');
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const levelEl = document.getElementById('level');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// Audio context
let audioContext;
function playSound(frequency, duration, type = 'square') {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = frequency;
oscillator.type = type;
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
oscillator.start();
oscillator.stop(audioContext.currentTime + duration);
}
// Game state
let game = {
playing: false,
demoMode: true,
ball: {
x: canvasWidth / 2,
y: canvasHeight - 80,
dx: 4,
dy: -4,
radius: 8
},
paddle: {
x: canvasWidth / 2 - 60,
y: canvasHeight - 50,
width: 120,
height: 40,
speed: 8
},
bricks: [],
hearts: [],
score: 0,
lives: 3,
level: 1,
brickRows: 5,
brickCols: 10,
brickWidth: 70,
brickHeight: 25,
brickPadding: 10,
brickOffsetTop: 60,
brickOffsetLeft: 30
};
// Brick colors
const brickColors = ['#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db', '#9b59b6', '#e91e63'];
// Input handling
let keys = {};
let mouseX = canvasWidth / 2;
let touchX = canvasWidth / 2;
let isTouch = false;
window.addEventListener('keydown', (e) => {
keys[e.key] = true;
if (game.demoMode && (e.key === ' ' || e.key === 'Enter')) {
startPlayerGame();
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) * (canvasWidth / rect.width);
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
isTouch = true;
const rect = canvas.getBoundingClientRect();
touchX = (e.touches[0].clientX - rect.left) * (canvasWidth / rect.width);
}, { passive: false });
startBtn.addEventListener('click', startPlayerGame);
canvas.addEventListener('click', startPlayerGame);
function startPlayerGame() {
if (game.demoMode) {
// Resume audio context on user gesture
if (audioContext && audioContext.state === 'suspended') {
audioContext.resume();
}
game.demoMode = false;
startScreen.classList.add('hidden');
// Reset game for player
resetBall();
createBricks();
game.lives = 3;
game.score = 0;
game.level = 1;
updateStats();
}
}
function resetBall() {
game.ball.x = canvasWidth / 2;
game.ball.y = canvasHeight - 80;
// Random angle
const angle = Math.random() * Math.PI - Math.PI/2;
const speed = 5;
game.ball.dx = Math.cos(angle) * speed;
game.ball.dy = -Math.abs(Math.sin(angle) * speed);
if (Math.abs(game.ball.dx) < 1) game.ball.dx = 1.5;
}
function createBricks() {
game.bricks = [];
for (let r = 0; r < game.brickRows; r++) {
for (let c = 0; c < game.brickCols; c++) {
const brick = {
x: c * (game.brickWidth + game.brickPadding) + game.brickOffsetLeft,
y: r * (game.brickHeight + game.brickPadding) + game.brickOffsetTop,
width: game.brickWidth,
height: game.brickHeight,
color: brickColors[r % brickColors.length],
hits: 1,
visible: true
};
game.bricks.push(brick);
}
}
}
// Game loop
function gameLoop() {
if (!game.playing) return;
update();
draw();
requestAnimationFrame(gameLoop);
}
function update() {
// Move paddle
if (isTouch) {
game.paddle.x = touchX - game.paddle.width / 2;
} else if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
game.paddle.x -= game.paddle.speed;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
game.paddle.x += game.paddle.speed;
} else {
game.paddle.x = mouseX - game.paddle.width / 2;
}
// Clamp paddle
if (game.paddle.x < 0) game.paddle.x = 0;
if (game.paddle.x + game.paddle.width > canvasWidth) game.paddle.x = canvasWidth - game.paddle.width;
// Demo mode AI
if (game.demoMode) {
const paddleCenter = game.paddle.x + game.paddle.width / 2;
const diff = game.ball.x - paddleCenter;
if (Math.abs(diff) > 2) {
game.paddle.x += diff * 0.1;
}
if (game.paddle.x < 0) game.paddle.x = 0;
if (game.paddle.x + game.paddle.width > canvasWidth) game.paddle.x = canvasWidth - game.paddle.width;
}
// Move ball
game.ball.x += game.ball.dx;
game.ball.y += game.ball.dy;
// Ball wall collisions
if (game.ball.x + game.ball.radius > canvasWidth || game.ball.x - game.ball.radius < 0) {
game.ball.dx = -game.ball.dx;
}
if (game.ball.y - game.ball.radius < 0) {
game.ball.dy = -game.ball.dy;
}
// Ball bottom collision
if (game.ball.y + game.ball.radius > canvasHeight) {
game.lives--;
updateStats();
if (game.lives <= 0) {
gameOver();
} else {
resetBall();
}
}
// Ball-paddle collision
if (
game.ball.y + game.ball.radius > game.paddle.y &&
game.ball.x > game.paddle.x &&
game.ball.x < game.paddle.x + game.paddle.width &&
game.ball.dy > 0
) {
const hitPos = (game.ball.x - (game.paddle.x + game.paddle.width / 2)) / (game.paddle.width / 2);
const angle = hitPos * (Math.PI / 3);
const speed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
game.ball.dx = Math.sin(angle) * speed;
game.ball.dy = -Math.cos(angle) * speed;
// Slight speed increase
const currentSpeed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
if (currentSpeed < 8) {
game.ball.dx *= 1.02;
game.ball.dy *= 1.02;
}
// Play sound
playSound(330, 0.05, 'sine');
}
// Ball-brick collisions
for (let brick of game.bricks) {
if (brick.visible) {
if (
game.ball.x + game.ball.radius > brick.x &&
game.ball.x - game.ball.radius < brick.x + brick.width &&
game.ball.y + game.ball.radius > brick.y &&
game.ball.y - game.ball.radius < brick.y + brick.height
) {
game.ball.dy = -game.ball.dy;
brick.hits--;
if (brick.hits <= 0) {
brick.visible = false;
game.score += 10;
createHearts(brick.x + brick.width/2, brick.y + brick.height/2);
updateStats();
// Play sound
playSound(440, 0.1, 'square');
}
}
}
}
// Update hearts
for (let heart of game.hearts) {
heart.x += heart.dx;
heart.y += heart.dy;
heart.dy += 0.2;
heart.life--;
if (heart.life <= 0) {
heart.visible = false;
}
}
game.hearts = game.hearts.filter(h => h.visible);
// Check win
if (game.bricks.every(b => !b.visible)) {
levelUp();
}
}
function createHearts(x, y) {
const heartCount = 15;
const colors = ['#ff6b6b', '#ff8e8e', '#ffa8a8', '#ffd43d', '#ffec99'];
for (let i = 0; i < heartCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 4 + 1;
game.hearts.push({
x: x,
y: y,
dx: Math.cos(angle) * speed,
dy: Math.sin(angle) * speed - 2,
life: Math.random() * 60 + 40,
visible: true,
color: colors[Math.floor(Math.random() * colors.length)],
size: Math.random() * 8 + 6
});
}
}
function levelUp() {
game.level++;
updateStats();
resetBall();
createBricks();
// Increase ball speed
const speed = Math.sqrt(game.ball.dx * game.ball.dx + game.ball.dy * game.ball.dy);
game.ball.dx *= 1.1;
game.ball.dy *= 1.1;
}
function updateStats() {
scoreEl.textContent = game.score;
livesEl.textContent = game.lives;
levelEl.textContent = game.level;
}
function gameOver() {
game.playing = false;
startScreen.querySelector('h1').textContent = 'Game Over';
startScreen.querySelector('p').innerHTML = `Your score: ${game.score}<br>Click to play again!`;
startScreen.querySelector('button').textContent = 'Play Again';
startScreen.classList.remove('hidden');
// Restart demo behind overlay
game.demoMode = true;
game.playing = true;
resetBall();
createBricks();
game.lives = 3;
game.score = 0;
game.level = 1;
updateStats();
gameLoop();
}
// Drawing functions
function draw() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
drawBackground();
for (let brick of game.bricks) {
if (brick.visible) drawBrick(brick);
}
for (let heart of game.hearts) {
if (heart.visible) drawHeart(heart.x, heart.y, heart.size, heart.color, heart.life);
}
drawBall();
drawHamburgerPaddle();
}
function drawBackground() {
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
gradient.addColorStop(0, '#fff5f5');
gradient.addColorStop(1, '#ffeaa7');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
for (let x = 0; x <= canvasWidth; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvasHeight);
ctx.stroke();
}
for (let y = 0; y <= canvasHeight; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvasWidth, y);
ctx.stroke();
}
}
function drawBrick(brick) {
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fillStyle = brick.color;
roundRect(ctx, brick.x, brick.y, brick.width, brick.height, 5);
ctx.fill();
ctx.shadowColor = 'rgba(255,255,255,0.4)';
ctx.shadowBlur = 2;
ctx.shadowOffsetX = -1;
ctx.shadowOffsetY = -1;
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.beginPath();
ctx.moveTo(brick.x + 5, brick.y + 2);
ctx.lineTo(brick.x + brick.width - 5, brick.y + 2);
ctx.lineTo(brick.x + brick.width - 2, brick.y + 5);
ctx.lineTo(brick.x + 2, brick.y + 5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
}
function drawBall() {
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
const gradient = ctx.createRadialGradient(
game.ball.x - 2, game.ball.y - 2, 1,
game.ball.x, game.ball.y, game.ball.radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.5, '#f0f0f0');
gradient.addColorStop(1, '#cccccc');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(game.ball.x, game.ball.y, game.ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHamburgerPaddle() {
const p = game.paddle;
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 4;
// Bottom bun
ctx.fillStyle = '#d4a05a';
roundRect(ctx, p.x, p.y + p.height - 12, p.width, 12, 6);
ctx.fill();
ctx.fillStyle = '#e8c98a';
ctx.fillRect(p.x + 5, p.y + p.height - 12, p.width - 10, 3);
// Patty
ctx.fillStyle = '#5d4037';
roundRect(ctx, p.x + 5, p.y + p.height - 20, p.width - 10, 8, 3);
ctx.fill();
// Cheese
ctx.fillStyle = '#fbc02d';
ctx.fillRect(p.x + 5, p.y + p.height - 24, p.width - 10, 4);
// Lettuce
ctx.fillStyle = '#4caf50';
ctx.beginPath();
ctx.moveTo(p.x + 5, p.y + p.height - 28);
for (let i = 0; i < p.width - 10; i += 5) {
ctx.quadraticCurveTo(p.x + 5 + i + 2.5, p.y + p.height - 32, p.x + 5 + i + 5, p.y + p.height - 28);
}
ctx.lineTo(p.x + p.width - 5, p.y + p.height - 16);
ctx.lineTo(p.x + 5, p.y + p.height - 16);
ctx.closePath();
ctx.fill();
// Top bun
ctx.fillStyle = '#d4a05a';
roundRect(ctx, p.x, p.y, p.width, 12, 6);
ctx.fill();
ctx.fillStyle = '#b8860b';
ctx.fillRect(p.x + 5, p.y + 9, p.width - 10, 3);
// Sesame seeds
ctx.fillStyle = '#f5f5dc';
for (let i = 0; i < p.width - 10; i += 10) {
ctx.beginPath();
ctx.arc(p.x + 5 + i, p.y + 6, 1.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawHeart(x, y, size, color, life) {
ctx.save();
ctx.globalAlpha = Math.max(0, life / 100);
ctx.fillStyle = color;
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 3;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.translate(x, y);
ctx.scale(size, size);
ctx.beginPath();
ctx.moveTo(0, 0.2);
ctx.bezierCurveTo(0, 0, -0.5, 0, -0.5, 0.5);
ctx.bezierCurveTo(-0.5, 1, 0, 1.2, 0, 1.5);
ctx.bezierCurveTo(0, 1.2, 0.5, 1, 0.5, 0.5);
ctx.bezierCurveTo(0.5, 0, 0, 0, 0, 0.2);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Initialize demo mode
window.addEventListener('load', () => {
game.demoMode = true;
game.playing = true;
createBricks();
updateStats();
gameLoop();
// Show start screen
startScreen.classList.remove('hidden');
});
</script>
</body>
</html>
```
I think this meets the requirements. Let's double-check:
- Hamburger paddle: yes, drawn with canvas.
- Bricks explode into hearts: yes, on break, 15 hearts explode with random velocities and fade.
- Self-playing demo mode: yes, starts automatically on load, AI controls paddle.
- "Click or press a key to start" message: yes, overlay with button and key listener.
- Responsive: canvas scales with viewport, max-width 800px.
- No external dependencies: all inline.
- No localStorage, etc.
- Looks good in 1280x800: canvas is centered, stats below.
- First paint: the demo game is running behind the overlay, so something is visible.
Potential issues:
- The audio context might be blocked until user gesture. We resume on startPlayerGame, but the demo mode might play sounds without user gesture? Actually, we call playSound in the demo mode as well (brick break, paddle hit). That might be blocked. But it's okay if it's silent until user interaction. The requirement doesn't require sound.
- The hamburger paddle might not look perfect, but it's recognizable.
- The hearts might be small, but "tons of little hearts" is satisfied with 15 per brick.
We can adjust the heart size and count if needed. But it's fine.
Let's output the code.