Mistral Small 4
by MistralReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 2 s
- Cost
- $0.0087
- Output tokens
- 13,200
- Spent thinking
- 7,273 tokens
- Speed
- 213.1 tok/s
- Console errors
- 0
- First token after
- 542 ms
- First visible text after
- 36.7 s
- Prompt tokens
- 532
- Code size
- 20.9 KB
- Finish reason
- stop
- Served by
- Mistral
- Model id
mistralai/mistral-small-2603- Reasoning sent
{"effort":"high"}- Max output
- 209,715 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 761 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout Game</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
user-select: none;
}
#gameCanvas {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
display: block;
}
.game-container {
position: relative;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: none;
}
.menu-text, .game-over-text, .victory-text {
background: rgba(0, 0, 0, 0.8);
padding: 20px 40px;
border-radius: 10px;
text-align: center;
pointer-events: none;
margin-bottom: 20px;
}
.menu-text h1, .game-over-text h1, .victory-text h1 {
font-size: 48px;
margin-bottom: 20px;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
.menu-text p, .game-over-text p, .victory-text p {
font-size: 20px;
margin-bottom: 10px;
}
.controls-text {
font-size: 18px;
margin-top: 20px;
opacity: 0.8;
}
@media (max-width: 768px) {
.menu-text h1, .game-over-text h1, .victory-text h1 {
font-size: 32px;
}
.menu-text p, .game-over-text p, .victory-text p {
font-size: 16px;
}
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas"></canvas>
<div class="overlay">
<div class="menu-text" id="menuText">
<h1>BREAKOUT</h1>
<p>Click or press any key to start</p>
<p class="controls-text">Use mouse or arrow keys to move paddle</p>
<p class="controls-text">Catch power-ups (W = wide paddle, M = multi-ball)</p>
</div>
<div class="game-over-text" id="gameOverText" style="display: none;">
<h1>GAME OVER</h1>
<p id="finalScore">Final Score: 0</p>
<p class="controls-text">Click or press any key to restart</p>
</div>
<div class="victory-text" id="victoryText" style="display: none;">
<h1>VICTORY!</h1>
<p id="victoryScore">Final Score: 0</p>
<p class="controls-text">Click or press any key to play again</p>
</div>
</div>
</div>
<script>
// Game constants
const COLORS = {
paddle: '#3498db',
ball: '#e74c3c',
brickNormal: '#2ecc71',
brickTough: '#f39c12',
brickPower: '#9b59b6',
powerUpWide: '#3498db',
powerUpMulti: '#e67e22',
particle: '#ffffff'
};
// Game state
const gameStates = {
MENU: 'MENU',
PLAYING: 'PLAYING',
GAME_OVER: 'GAME_OVER',
VICTORY: 'VICTORY'
};
let canvas, ctx;
let gameState = gameStates.MENU;
let score = 0;
let lives = 3;
let level = 1;
let bricks = [];
let balls = [];
let powerUps = [];
let particles = [];
let paddle;
let mouseX = 0;
let keys = {};
let powerUpActive = null;
let powerUpTimer = 0;
let lastTime = 0;
let demoBall = null;
// Initialize game
function init() {
canvas = document.getElementById('gameCanvas');
ctx = canvas.getContext('2d');
// Set canvas size
resizeCanvas();
// Event listeners
window.addEventListener('resize', resizeCanvas);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('click', startGame);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Create paddle
paddle = new Paddle();
// Create initial level
createLevel();
// Start game loop
requestAnimationFrame(gameLoop);
}
// Main game loop
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
const deltaTime = (timestamp - lastTime) / 1000;
lastTime = timestamp;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Update game state
update(deltaTime);
// Render
render();
// Continue loop
requestAnimationFrame(gameLoop);
}
// Update game state
function update(deltaTime) {
if (gameState === gameStates.PLAYING) {
// Update paddle (player controlled)
paddle.update(false);
// Update balls
for (let i = balls.length - 1; i >= 0; i--) {
balls[i].update(deltaTime);
checkWallCollisions(balls[i]);
checkPaddleCollision(balls[i]);
checkBrickCollisions(balls[i]);
}
// Remove inactive balls
balls = balls.filter(ball => ball.active);
// If no balls left, lose a life
if (balls.length === 0) {
lives--;
if (lives <= 0) {
gameState = gameStates.GAME_OVER;
document.getElementById('finalScore').textContent = `Final Score: ${score}`;
} else {
// Reset paddle and add new ball
paddle.reset();
balls.push(new Ball());
}
}
// Update power-ups
for (let i = powerUps.length - 1; i >= 0; i--) {
powerUps[i].update(deltaTime);
// Check if caught by paddle
if (powerUps[i].y + powerUps[i].radius >= paddle.y &&
powerUps[i].x > paddle.x &&
powerUps[i].x < paddle.x + paddle.width) {
activatePowerUp(powerUps[i].type);
powerUps.splice(i, 1);
}
// Remove if off screen
if (powerUps[i].y > canvas.height) {
powerUps.splice(i, 1);
}
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update(deltaTime);
if (particles[i].alpha <= 0) {
particles.splice(i, 1);
}
}
// Check if all bricks are destroyed
if (bricks.length === 0) {
// Level complete
if (level < 3) {
level++;
createLevel();
} else {
gameState = gameStates.VICTORY;
document.getElementById('victoryScore').textContent = `Final Score: ${score}`;
}
}
// Update power-up timer
if (powerUpActive) {
powerUpTimer -= deltaTime;
if (powerUpTimer <= 0) {
deactivatePowerUp();
}
}
} else if (gameState === gameStates.MENU) {
// AI demo mode - move paddle automatically
paddle.update(true);
// Keep one ball in demo
if (!demoBall || !demoBall.active) {
demoBall = new Ball();
}
demoBall.update(deltaTime);
checkWallCollisions(demoBall);
checkPaddleCollision(demoBall);
checkBrickCollisions(demoBall);
// Remove dead balls in demo
if (demoBall && !demoBall.active) {
demoBall = null;
}
}
}
// Render game
function render() {
// Draw background
drawBackground();
// Draw bricks
bricks.forEach(brick => brick.draw());
// Draw balls
balls.forEach(ball => ball.draw());
// Draw paddle
paddle.draw();
// Draw power-ups
powerUps.forEach(powerUp => powerUp.draw());
// Draw particles
particles.forEach(particle => particle.draw());
// Draw UI
drawUI();
// Draw game state overlays
if (gameState === gameStates.MENU) {
document.getElementById('menuText').style.display = 'flex';
document.getElementById('gameOverText').style.display = 'none';
document.getElementById('victoryText').style.display = 'none';
} else if (gameState === gameStates.GAME_OVER) {
document.getElementById('menuText').style.display = 'none';
document.getElementById('gameOverText').style.display = 'flex';
document.getElementById('victoryText').style.display = 'none';
} else if (gameState === gameStates.VICTORY) {
document.getElementById('menuText').style.display = 'none';
document.getElementById('gameOverText').style.display = 'none';
document.getElementById('victoryText').style.display = 'flex';
}
}
// Paddle class
class Paddle {
constructor() {
this.width = 100;
this.height = 15;
this.x = (canvas.width - this.width) / 2;
this.y = canvas.height - this.height - 20;
this.speed = 8;
this.color = COLORS.paddle;
this.dx = 0;
this.originalWidth = this.width;
}
update(aiControlled) {
if (aiControlled) {
// Simple AI: follow the ball
if (demoBall) {
const targetX = demoBall.x;
if (targetX < this.x + this.width/2 - 20) {
this.dx = -this.speed;
} else if (targetX > this.x + this.width/2 + 20) {
this.dx = this.speed;
} else {
this.dx = 0;
}
}
} else {
// Keyboard control
if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
this.dx = -this.speed;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
this.dx = this.speed;
} else {
this.dx = 0;
}
}
// Move paddle
this.x += this.dx;
// Boundary check
if (this.x < 0) this.x = 0;
if (this.x + this.width > canvas.width) this.x = canvas.width - this.width;
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.roundRect(this.x, this.y, this.width, this.height, 5);
ctx.fill();
// Draw center indicator
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(this.x + this.width/2 - 2, this.y - 5, 4, 10);
}
reset() {
this.width = this.originalWidth;
this.x = (canvas.width - this.width) / 2;
}
}
// Ball class
class Ball {
constructor() {
this.radius = 8;
this.reset();
}
reset() {
this.x = canvas.width / 2;
this.y = canvas.height / 2;
this.dx = 4 * (Math.random() > 0.5 ? 1 : -1);
this.dy = -4;
this.active = true;
}
update(deltaTime) {
this.x += this.dx;
this.y += this.dy;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = COLORS.ball;
ctx.fill();
ctx.closePath();
}
}
// Brick class
class Brick {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.width = 75;
this.height = 25;
this.type = type; // 'normal', 'tough', 'power'
this.hits = type === 'tough' ? 2 : 1;
this.cracked = false;
this.color = type === 'normal' ? COLORS.brickNormal :
type === 'tough' ? COLORS.brickTough : COLORS.brickPower;
this.active = true;
}
draw() {
if (!this.active) return;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.roundRect(this.x, this.y, this.width, this.height, 3);
ctx.fill();
// Draw crack for tough bricks that have been hit once
if (this.type === 'tough' && this.cracked) {
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(this.x + 5, this.y + 5);
ctx.lineTo(this.x + this.width - 5, this.y + this.height - 5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(this.x + this.width - 5, this.y + 5);
ctx.lineTo(this.x + 5, this.y + this.height - 5);
ctx.stroke();
}
}
hit() {
this.hits--;
if (this.hits <= 0) {
this.active = false;
// Create particles
createParticles(this.x + this.width/2, this.y + this.height/2, this.color);
// Chance to drop power-up
if (this.type === 'power' || Math.random() < 0.2) {
powerUps.push(new PowerUp(this.x + this.width/2, this.y));
}
return true;
} else if (this.type === 'tough') {
this.cracked = true;
}
return false;
}
}
// PowerUp class
class PowerUp {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 10;
this.type = Math.random() < 0.5 ? 'wide' : 'multi'; // 50% chance for each
this.speed = 3;
this.color = this.type === 'wide' ? COLORS.powerUpWide : COLORS.powerUpMulti;
this.active = true;
}
update(deltaTime) {
this.y += this.speed;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
// Draw icon
ctx.fillStyle = 'white';
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (this.type === 'wide') {
ctx.fillText('W', this.x, this.y);
} else {
ctx.fillText('M', this.x, this.y);
}
}
}
// Particle class
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.radius = Math.random() * 3 + 1;
this.dx = (Math.random() - 0.5) * 4;
this.dy = (Math.random() - 0.5) * 4;
this.alpha = 1;
this.color = color;
this.decay = Math.random() * 0.02 + 0.01;
}
update(deltaTime) {
this.x += this.dx;
this.y += this.dy;
this.alpha -= this.decay * deltaTime * 60;
}
draw() {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
ctx.restore();
}
}
// Helper functions
function createParticles(x, y, color) {
for (let i = 0; i < 30; i++) {
particles.push(new Particle(x, y, color));
}
}
function checkWallCollisions(ball) {
// Top wall
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.dy = -ball.dy;
}
// Left wall
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.dx = -ball.dx;
}
// Right wall
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius;
ball.dx = -ball.dx;
}
}
function checkPaddleCollision(ball) {
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) {
// Calculate bounce angle based on where ball hits paddle
const relativeIntersectX = (paddle.x + paddle.width/2) - ball.x;
const normalizedRelativeIntersectX = relativeIntersectX / (paddle.width/2);
const bounceAngle = normalizedRelativeIntersectX * (Math.PI / 3); // Max 60 degrees from vertical
// Reflect ball with new angle
const speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
ball.dx = speed * Math.sin(bounceAngle);
ball.dy = -speed * Math.cos(bounceAngle);
// Ensure ball moves upward
if (ball.dy > 0) ball.dy = -ball.dy;
}
}
function checkBrickCollisions(ball) {
for (let i = bricks.length - 1; i >= 0; i--) {
const brick = bricks[i];
if (ball.x + ball.radius > brick.x &&
ball.x - ball.radius < brick.x + brick.width &&
ball.y + ball.radius > brick.y &&
ball.y - ball.radius < brick.y + brick.height) {
// Calculate which side was hit
const dx = ball.x - (brick.x + brick.width/2);
const dy = ball.y - (brick.y + brick.height/2);
const angle = Math.atan2(dy, dx);
// Reflect ball
ball.dx = Math.sin(angle) * 5;
ball.dy = Math.cos(angle) * 5;
// Hit the brick
if (brick.hit()) {
score += brick.type === 'tough' ? 30 : brick.type === 'power' ? 50 : 10;
bricks.splice(i, 1);
}
// Ensure ball moves away from brick
if (ball.dx > 0) ball.dx = Math.abs(ball.dx);
else ball.dx = -Math.abs(ball.dx);
if (ball.dy > 0) ball.dy = Math.abs(ball.dy);
else ball.dy = -Math.abs(ball.dy);
break; // Only handle one collision per frame
}
}
}
function activatePowerUp(type) {
if (type === 'wide') {
paddle.width = paddle.originalWidth * 1.5;
powerUpActive = 'wide';
powerUpTimer = 10; // 10 seconds
} else if (type === 'multi') {
// Add 2 more balls
for (let i = 0; i < 2; i++) {
balls.push(new Ball());
// Position new balls around the main ball
balls[balls.length - 1].x = paddle.x + paddle.width/2;
balls[balls.length - 1].y = paddle.y - 20;
// Randomize direction slightly
balls[balls.length - 1].dx = (Math.random() - 0.5) * 6;
balls[balls.length - 1].dy = -4 - Math.random() * 2;
}
powerUpActive = 'multi';
powerUpTimer = 10; // 10 seconds
}
}
function deactivatePowerUp() {
if (powerUpActive === 'wide') {
paddle.width = paddle.originalWidth;
}
powerUpActive = null;
powerUpTimer = 0;
}
function createLevel() {
bricks = [];
const rows = 6;
const cols = Math.floor(canvas.width / 80);
const brickWidth = canvas.width / cols - 2;
const brickHeight = 25;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
// Skip some positions for power-up bricks
if (Math.random() < 0.1 && r > 1) {
bricks.push(new Brick(c * (brickWidth + 2), 50 + r * (brickHeight + 10), 'power'));
} else if (Math.random() < 0.3) {
bricks.push(new Brick(c * (brickWidth + 2), 50 + r * (brickHeight + 10), 'tough'));
} else {
bricks.push(new Brick(c * (brickWidth + 2), 50 + r * (brickHeight + 10), 'normal'));
}
}
}
}
function handleMouseMove(e) {
if (gameState === gameStates.PLAYING) {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
paddle.x = mouseX - paddle.width/2;
// Boundary check
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
}
}
function handleKeyDown(e) {
keys[e.key] = true;
// Start game on any key press in menu or game over/victory screens
if ((gameState === gameStates.MENU || gameState === gameStates.GAME_OVER || gameState === gameStates.VICTORY) && e.key) {
startGame();
}
}
function handleKeyUp(e) {
keys[e.key] = false;
}
function startGame() {
if (gameState === gameStates.MENU || gameState === gameStates.GAME_OVER || gameState === gameStates.VICTORY) {
gameState = gameStates.PLAYING;
score = 0;
lives = 3;
level = 1;
bricks = [];
balls = [];
powerUps = [];
particles = [];
demoBall = null;
// Create paddle and level
paddle.reset();
createLevel();
// Add initial ball
balls.push(new Ball());
}
}
function resizeCanvas() {
const container = document.body;
const size = Math.min(container.clientWidth, container.clientHeight * 1.6);
canvas.width = size;
canvas.height = size / 1.6;
}
function drawBackground() {
// Gradient background
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#1a1a2e');
gradient.addColorStop(1, '#16213e');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawUI() {
// Score
ctx.fillStyle = 'white';
ctx.font = 'bold 20px Arial';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${score}`, 20, 30);
// Lives
ctx.fillText(`Lives: ${lives}`, 20, 60);
// Level
ctx.textAlign = 'right';
ctx.fillText(`Level: ${level}`, canvas.width - 20, 30);
// Power-up timer if active
if (powerUpActive) {
ctx.textAlign = 'center';
const timerText = powerUpActive === 'wide' ? 'WIDE PADDLE' : 'MULTI-BALL';
ctx.fillText(`${timerText} (${powerUpTimer.toFixed(1)}s)`, canvas.width/2, 30);
}
}
// Start the game
init();
</script>
</body>
</html>Reasoning trace (7,273 tokens)
I need to create a polished Breakout game in a single HTML file with canvas. Let me break down the requirements:
1. **Game mechanics**:
- Paddle controlled by mouse and arrow keys
- Bounce angle depends on where ball hits paddle (center = straight, edges = sharp angle)
- One level with at least 6 rows of bricks
- Mix of normal bricks (1 hit) and tough bricks (2 hits, cracks after first hit)
- Power-ups: wider paddle (10s) and multi-ball (3 balls)
- Ball never passes through bricks/paddle
- Particles when bricks break
- 3 lives, score display
- Game over and victory screens with restart
2. **Technical constraints**:
- Single HTML file, no external resources except CDN URLs if needed
- Everything inline: CSS in <style>, JS in <script>
- Must work in 1280x800 viewport and adapt to 375px
- First paint must show meaningful content
- Games must start in self-playing demo mode behind "Click or press a key to start"
- Zero console errors in latest Chrome
3. **Structure planning**:
- Canvas setup with proper dimensions
- Game state management (menu, playing, game over, victory)
- Paddle class with mouse and keyboard controls
- Ball class with physics (velocity, position, collision detection)
- Brick class with different types (normal, tough, power-up)
- Power-up class that drops from bricks and can be caught
- Particle system for brick breaking effects
- Score, lives, level tracking
- Input handling for mouse and keyboard
- Responsive design with proper scaling
Let me think about the bounce angle physics:
- When ball hits paddle, the angle should depend on where it hits
- Center hit: straight up (0 degrees relative to vertical)
- Left edge: bounce left at sharper angle
- Right edge: bounce right at sharper angle
- I can calculate the offset from paddle center and use that to determine the angle
Power-ups:
- Need to spawn when tough bricks or power-up bricks are broken
- Two types: wide paddle (10s duration) and multi-ball (3 balls)
- Should fall down and be catchable by paddle
- Need visual representation
Particles:
- When brick breaks, emit particles in different colors based on brick type
- Should be small circles that fade out
Game states:
- MENU: Show instructions, "Click to start"
- PLAYING: Game is active
- GAME_OVER: Player lost all lives
- VICTORY: All bricks destroyed
I'll need to handle:
- Collision detection between ball and bricks/paddle/walls
- Power-up collection
- Timer for power-ups
- Multi-ball management
- Lives and score tracking
Let me sketch the HTML structure:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Breakout Game</title>
<style>
/* CSS here */
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// Game code here
</script>
</body>
</html>
```
For the canvas, I'll make it responsive using width/height attributes and CSS to scale.
Let me plan the JavaScript structure:
```javascript
// Game constants
const COLORS = {
paddle: '#3498db',
ball: '#e74c3c',
brickNormal: '#2ecc71',
brickTough: '#f39c12',
brickPower: '#9b59b6',
powerUpWide: '#3498db',
powerUpMulti: '#e67e22'
};
// Game variables
let canvas, ctx;
let gameState = 'MENU'; // MENU, PLAYING, GAME_OVER, VICTORY
let score = 0;
let lives = 3;
let level = 1;
let bricks = [];
let balls = [];
let powerUps = [];
let particles = [];
let paddle;
let mouseX = 0;
let keys = {};
let powerUpActive = null;
let powerUpTimer = 0;
// Initialize game
function init() {
canvas = document.getElementById('gameCanvas');
ctx = canvas.getContext('2d');
// Set canvas size
resizeCanvas();
// Event listeners
window.addEventListener('resize', resizeCanvas);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('click', startGame);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Create paddle
paddle = new Paddle();
// Create initial level
createLevel();
// Start game loop
gameLoop();
}
// Game loop
function gameLoop(timestamp) {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Update game state
update();
// Render
render();
// Continue loop
requestAnimationFrame(gameLoop);
}
// Update game state
function update() {
if (gameState === 'PLAYING') {
// Update paddle
paddle.update();
// Update balls
for (let i = balls.length - 1; i >= 0; i--) {
balls[i].update();
// Check collisions
checkWallCollisions(balls[i]);
checkPaddleCollision(balls[i]);
checkBrickCollisions(balls[i]);
}
// Remove dead balls
balls = balls.filter(ball => ball.active);
// If no balls left, lose a life
if (balls.length === 0) {
lives--;
if (lives <= 0) {
gameState = 'GAME_OVER';
} else {
// Reset paddle and add new ball
paddle.reset();
balls.push(new Ball());
}
}
// Update power-ups
for (let i = powerUps.length - 1; i >= 0; i--) {
powerUps[i].update();
// Check if caught by paddle
if (powerUps[i].y + powerUps[i].radius >= paddle.y &&
powerUps[i].x > paddle.x &&
powerUps[i].x < paddle.x + paddle.width) {
activatePowerUp(powerUps[i].type);
powerUps.splice(i, 1);
}
// Remove if off screen
if (powerUps[i].y > canvas.height) {
powerUps.splice(i, 1);
}
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].alpha <= 0) {
particles.splice(i, 1);
}
}
// Check if all bricks are destroyed
if (bricks.length === 0) {
// Level complete
if (level < 3) {
level++;
createLevel();
} else {
gameState = 'VICTORY';
}
}
// Update power-up timer
if (powerUpActive) {
powerUpTimer -= 1/60;
if (powerUpTimer <= 0) {
deactivatePowerUp();
}
}
}
}
// Render game
function render() {
// Draw background
drawBackground();
// Draw bricks
bricks.forEach(brick => brick.draw());
// Draw balls
balls.forEach(ball => ball.draw());
// Draw paddle
paddle.draw();
// Draw power-ups
powerUps.forEach(powerUp => powerUp.draw());
// Draw particles
particles.forEach(particle => particle.draw());
// Draw UI
drawUI();
// Draw game state overlays
if (gameState === 'MENU') {
drawMenu();
} else if (gameState === 'GAME_OVER') {
drawGameOver();
} else if (gameState === 'VICTORY') {
drawVictory();
}
}
// Helper classes
class Paddle {
constructor() {
this.width = 100;
this.height = 15;
this.x = (canvas.width - this.width) / 2;
this.y = canvas.height - this.height - 20;
this.speed = 8;
this.color = COLORS.paddle;
this.dx = 0;
this.originalWidth = this.width;
}
update() {
// Keyboard control
if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
this.dx = -this.speed;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
this.dx = this.speed;
} else {
this.dx = 0;
}
// Move paddle
this.x += this.dx;
// Boundary check
if (this.x < 0) this.x = 0;
if (this.x + this.width > canvas.width) this.x = canvas.width - this.width;
// Mouse control
// Already handled by mouse move event
}
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
// Draw center indicator
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(this.x + this.width/2 - 2, this.y - 5, 4, 10);
}
reset() {
this.width = this.originalWidth;
this.x = (canvas.width - this.width) / 2;
}
}
class Ball {
constructor() {
this.radius = 8;
this.reset();
}
reset() {
this.x = canvas.width / 2;
this.y = canvas.height / 2;
this.dx = 4 * (Math.random() > 0.5 ? 1 : -1);
this.dy = -4;
this.active = true;
}
update() {
this.x += this.dx;
this.y += this.dy;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = COLORS.ball;
ctx.fill();
ctx.closePath();
}
}
class Brick {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.width = 75;
this.height = 25;
this.type = type; // 'normal', 'tough', 'power'
this.hits = type === 'tough' ? 2 : 1;
this.cracked = false;
this.color = type === 'normal' ? COLORS.brickNormal :
type === 'tough' ? COLORS.brickTough : COLORS.brickPower;
this.active = true;
}
draw() {
if (!this.active) return;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
// Draw crack for tough bricks that have been hit once
if (this.type === 'tough' && this.cracked) {
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(this.x + 5, this.y + 5);
ctx.lineTo(this.x + this.width - 5, this.y + this.height - 5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(this.x + this.width - 5, this.y + 5);
ctx.lineTo(this.x + 5, this.y + this.height - 5);
ctx.stroke();
}
}
hit() {
this.hits--;
if (this.hits <= 0) {
this.active = false;
// Create particles
createParticles(this.x + this.width/2, this.y + this.height/2, this.color);
// Chance to drop power-up
if (this.type === 'power' || Math.random() < 0.2) {
powerUps.push(new PowerUp(this.x + this.width/2, this.y));
}
return true;
} else if (this.type === 'tough') {
this.cracked = true;
}
return false;
}
}
class PowerUp {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 10;
this.type = Math.random() < 0.5 ? 'wide' : 'multi'; // 50% chance for each
this.speed = 3;
this.color = this.type === 'wide' ? COLORS.powerUpWide : COLORS.powerUpMulti;
this.active = true;
}
update() {
this.y += this.speed;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
// Draw icon
ctx.fillStyle = 'white';
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (this.type === 'wide') {
ctx.fillText('W', this.x, this.y);
} else {
ctx.fillText('M', this.x, this.y);
}
}
}
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.radius = Math.random() * 3 + 1;
this.dx = (Math.random() - 0.5) * 4;
this.dy = (Math.random() - 0.5) * 4;
this.alpha = 1;
this.color = color;
this.decay = Math.random() * 0.02 + 0.01;
}
update() {
this.x += this.dx;
this.y += this.dy;
this.alpha -= this.decay;
}
draw() {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
ctx.restore();
}
}
// Helper functions
function createParticles(x, y, color) {
for (let i = 0; i < 30; i++) {
particles.push(new Particle(x, y, color));
}
}
function checkWallCollisions(ball) {
// Top wall
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.dy = -ball.dy;
}
// Left wall
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.dx = -ball.dx;
}
// Right wall
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius;
ball.dx = -ball.dx;
}
}
function checkPaddleCollision(ball) {
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) {
// Calculate bounce angle based on where ball hits paddle
const relativeIntersectX = (paddle.x + paddle.width/2) - ball.x;
const normalizedRelativeIntersectX = relativeIntersectX / (paddle.width/2);
const bounceAngle = normalizedRelativeIntersectX * (Math.PI / 3); // Max 60 degrees
// Reflect ball with new angle
const speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
ball.dx = speed * Math.sin(bounceAngle);
ball.dy = -speed * Math.cos(bounceAngle);
// Ensure ball moves upward
if (ball.dy > 0) ball.dy = -ball.dy;
}
}
function checkBrickCollisions(ball) {
for (let i = bricks.length - 1; i >= 0; i--) {
const brick = bricks[i];
if (ball.x + ball.radius > brick.x &&
ball.x - ball.radius < brick.x + brick.width &&
ball.y + ball.radius > brick.y &&
ball.y - ball.radius < brick.y + brick.height) {
// Calculate which side was hit
const dx = ball.x - (brick.x + brick.width/2);
const dy = ball.y - (brick.y + brick.height/2);
const angle = Math.atan2(dy, dx);
// Reflect ball
ball.dx = Math.sin(angle) * 5;
ball.dy = Math.cos(angle) * 5;
// Hit the brick
if (brick.hit()) {
score += brick.type === 'tough' ? 30 : brick.type === 'power' ? 50 : 10;
bricks.splice(i, 1);
}
// Ensure ball moves away from brick
if (ball.dx > 0) ball.dx = Math.abs(ball.dx);
else ball.dx = -Math.abs(ball.dx);
if (ball.dy > 0) ball.dy = Math.abs(ball.dy);
else ball.dy = -Math.abs(ball.dy);
break; // Only handle one collision per frame
}
}
}
function activatePowerUp(type) {
if (type === 'wide') {
paddle.width = paddle.originalWidth * 1.5;
powerUpActive = 'wide';
powerUpTimer = 10; // 10 seconds
} else if (type === 'multi') {
// Add 2 more balls
for (let i = 0; i < 2; i++) {
balls.push(new Ball());
// Position new balls around the main ball
balls[balls.length - 1].x = paddle.x + paddle.width/2;
balls[balls.length - 1].y = paddle.y - 20;
// Randomize direction slightly
balls[balls.length - 1].dx = (Math.random() - 0.5) * 6;
balls[balls.length - 1].dy = -4 - Math.random() * 2;
}
powerUpActive = 'multi';
powerUpTimer = 10; // 10 seconds
}
}
function deactivatePowerUp() {
if (powerUpActive === 'wide') {
paddle.width = paddle.originalWidth;
}
powerUpActive = null;
powerUpTimer = 0;
}
function createLevel() {
bricks = [];
const rows = 6;
const cols = Math.floor(canvas.width / 80);
const brickWidth = canvas.width / cols - 2;
const brickHeight = 25;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
// Skip some positions for power-up bricks
if (Math.random() < 0.1 && r > 1) {
bricks.push(new Brick(c * (brickWidth + 2), 50 + r * (brickHeight + 10), 'power'));
} else if (Math.random() < 0.3) {
bricks.push(new Brick(c * (brickWidth + 2), 50 + r * (brickHeight + 10), 'tough'));
} else {
bricks.push(new Brick(c * (brickWidth + 2), 50 + r * (brickHeight + 10), 'normal'));
}
}
}
}
function handleMouseMove(e) {
if (gameState === 'PLAYING') {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
paddle.x = mouseX - paddle.width/2;
// Boundary check
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
}
}
function handleKeyDown(e) {
keys[e.key] = true;
}
function handleKeyUp(e) {
keys[e.key] = false;
}
function startGame() {
if (gameState === 'MENU') {
gameState = 'PLAYING';
// Start with one ball
balls.push(new Ball());
} else if (gameState === 'GAME_OVER' || gameState === 'VICTORY') {
// Reset game
gameState = 'PLAYING';
score = 0;
lives = 3;
level = 1;
bricks = [];
balls = [];
powerUps = [];
particles = [];
createLevel();
balls.push(new Ball());
}
}
function resizeCanvas() {
const container = document.body;
const size = Math.min(container.clientWidth, container.clientHeight * 1.6);
canvas.width = size;
canvas.height = size / 1.6;
}
function drawBackground() {
// Gradient background
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#1a1a2e');
gradient.addColorStop(1, '#16213e');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawUI() {
// Score
ctx.fillStyle = 'white';
ctx.font = 'bold 20px Arial';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${score}`, 20, 30);
// Lives
ctx.fillText(`Lives: ${lives}`, 20, 60);
// Level
ctx.textAlign = 'right';
ctx.fillText(`Level: ${level}`, canvas.width - 20, 30);
// Power-up timer if active
if (powerUpActive) {
ctx.textAlign = 'center';
const timerText = powerUpActive === 'wide' ? 'WIDE PADDLE' : 'MULTI-BALL';
ctx.fillText(`${timerText} (${powerUpTimer.toFixed(1)}s)`, canvas.width/2, 30);
}
}
function drawMenu() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.fillText('BREAKOUT', canvas.width/2, canvas.height/2 - 50);
ctx.font = '24px Arial';
ctx.fillText('Click or press a key to start', canvas.width/2, canvas.height/2);
ctx.font = '18px Arial';
ctx.fillText('Use mouse or arrow keys to move paddle', canvas.width/2, canvas.height/2 + 50);
ctx.fillText('Catch power-ups (W = wide paddle, M = multi-ball)', canvas.width/2, canvas.height/2 + 80);
}
function drawGameOver() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2 - 30);
ctx.font = '24px Arial';
ctx.fillText(`Final Score: ${score}`, canvas.width/2, canvas.height/2);
ctx.font = '18px Arial';
ctx.fillText('Click or press a key to restart', canvas.width/2, canvas.height/2 + 50);
}
function drawVictory() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.fillText('VICTORY!', canvas.width/2, canvas.height/2 - 30);
ctx.font = '24px Arial';
ctx.fillText(`Final Score: ${score}`, canvas.width/2, canvas.height/2);
ctx.font = '18px Arial';
ctx.fillText('Click or press a key to play again', canvas.width/2, canvas.height/2 + 50);
}
// Start the game
init();
```
Wait, I need to make sure the game starts in demo mode. The requirement says "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message".
So I need to add an AI paddle that moves automatically in demo mode. Let me modify the paddle update to handle demo mode:
```javascript
update() {
if (gameState === 'PLAYING') {
if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
this.dx = -this.speed;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
this.dx = this.speed;
} else {
// In demo mode, move paddle automatically
if (Math.random() < 0.02) {
this.dx = Math.random() > 0.5 ? this.speed : -this.speed;
}
}
// ... rest of update
}
}
```
Actually, the requirement says "self-playing demo mode" which suggests the AI should play the game automatically. But the paddle is controlled by mouse/keys. Let me re-read: "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message"
This means the game should start with an AI controlling the paddle to demonstrate gameplay. So I need to add AI paddle logic that moves automatically when in demo mode (before the game starts).
Let me revise the paddle class and game state handling:
```javascript
class Paddle {
// ... existing code
update(aiControlled = false) {
if (aiControlled) {
// Simple AI: follow the ball
const targetX = balls.length > 0 ? balls[0].x : canvas.width/2;
if (targetX < this.x + this.width/2 - 20) {
this.dx = -this.speed;
} else if (targetX > this.x + this.width/2 + 20) {
this.dx = this.speed;
} else {
this.dx = 0;
}
} else {
// Keyboard control
if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
this.dx = -this.speed;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
this.dx = this.speed;
} else {
this.dx = 0;
}
}
this.x += this.dx;
if (this.x < 0) this.x = 0;
if (this.x + this.width > canvas.width) this.x = canvas.width - this.width;
}
}
```
And in the game loop:
```javascript
function update() {
if (gameState === 'PLAYING') {
paddle.update(false); // Player-controlled
// ... rest of update
} else if (gameState === 'MENU') {
// AI demo mode
paddle.update(true);
if (balls.length === 0) {
balls.push(new Ball());
}
// Update the ball
if (balls.length > 0) {
balls[0].update();
checkWallCollisions(balls[0]);
checkPaddleCollision(balls[0]);
checkBrickCollisions(balls[0]);
}
}
}
```
Actually, I think the requirement means that when the game first loads, it should show the menu with a demo of the game playing itself. So I need to:
1. Start in MENU state
2. In MENU state, run an AI-controlled demo
3. Show "Click to start" overlay
4. When clicked, switch to PLAYING state with player control
Let me revise the update function:
```javascript
function update() {
if (gameState === 'PLAYING') {
paddle.update(false); // Player-controlled
// Update balls
for (let i = balls.length - 1; i >= 0; i--) {
balls[i].update();
checkWallCollisions(balls[i]);
checkPaddleCollision(balls[i]);
checkBrickCollisions(balls[i]);
}
// ... rest of playing state logic
} else if (gameState === 'MENU') {
// AI demo mode - move paddle to follow ball
paddle.update(true);
// Keep one ball in demo
if (balls.length === 0) {
balls.push(new Ball());
}
if (balls.length > 0) {
balls[0].update();
checkWallCollisions(balls[0]);
checkPaddleCollision(balls[0]);
checkBrickCollisions(balls[0]);
// Remove dead balls in demo
if (!balls[0].active) {
balls = [];
}
}
}
// ... rest of update logic (power-ups, particles, etc.)
}
```
Now let me check all requirements again:
1. ✓ Paddle controlled with mouse and left/right arrow keys
2. ✓ Bounce angle depends on where ball hits paddle (center = straight, edges = sharp angle)
3. ✓ One level with at least 6 rows of bricks
4. ✓ Mix of normal bricks (1 hit) and tough bricks (2 hits, cracks after first)
5. ✓ Some bricks carry power-up icons (W for wide paddle, M for multi-ball)
6. ✓ Power-ups drop and can be caught
7. ✓ Ball never passes through bricks/paddle (collision detection)
8. ✓ Particles when brick breaks
9. ✓ 3 lives, score displayed
10. ✓ Game over and victory screens with restart
11. ✓ Starts in self-playing demo mode behind "Click to start" message
12. ✓ Single HTML file, no external resources
13. ✓ Responsive design
14. ✓ First paint shows meaningful content
I need to make sure the bounce angle is correct. Let me refine the checkPaddleCollision function:
```javascript
function checkPaddleCollision(ball) {
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) {
// Calculate bounce angle based on where ball hits paddle
const relativeIntersectX = (paddle.x + paddle.width/2) - ball.x;
const normalizedRelativeIntersectX = relativeIntersectX / (paddle.width/2);
const bounceAngle = normalizedRelativeIntersectX * (Math.PI / 3); // Max 60 degrees from vertical
// Reflect ball with new angle
const speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
ball.dx = speed * Math.sin(bounceAngle);
ball.dy = -speed * Math.cos(bounceAngle);
// Ensure ball moves upward
if (ball.dy > 0) ball.dy = -ball.dy;
}
}
```
This should give a nice angle effect where hitting the center gives a straight up bounce, and hitting the edges gives a sharper angle.
Now let me check the power-up system:
- Wide paddle: increases paddle width by 50% for 10 seconds
- Multi-ball: adds 2 more balls for 10 seconds
- Power-ups drop from bricks (20% chance from normal bricks, 100% from power bricks)
- Power-ups are caught when paddle overlaps them
I think this covers everything. Let me now write the complete HTML file with all the code properly integrated.
I need to make sure:
- The canvas is centered and responsive
- The game starts in demo mode
- All visual elements are polished
- The game is playable after clicking
Let me write the final code: