Gemini 3.8 Flash
by GoogleReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 41.9 s
- Cost
- $0.028
- Output tokens
- 7,333
- Spent thinking
- 0 tokens
- Speed
- 181.4 tok/s
- Console errors
- 0
- First token after
- 1.4 s
- First visible text after
- 1.4 s
- Prompt tokens
- 524
- Code size
- 18.4 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-3.8-flash- Reasoning sent
{"effort":"low"}- Max output
- 65,536 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 · 711 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background: radial-gradient(circle at center, #111428 0%, #060814 100%);
color: #e0e6ed;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
#wrapper {
position: relative;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8), 0 0 30px rgba(0, 195, 255, 0.15);
border-radius: 12px;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, 0.1);
}
canvas {
display: block;
background: #080a16;
}
.hint {
margin-top: 12px;
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div id="wrapper">
<canvas id="game" width="800" height="600"></canvas>
</div>
<div class="hint">Use Mouse or Left/Right Arrow Keys • Press 'P' for Pause</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
// Game State
let isAutoplay = true;
let isGameOver = false;
let isVictory = false;
let isPaused = false;
let score = 0;
let lives = 3;
// Paddle
const PADDLE_BASE_WIDTH = 110;
const PADDLE_HEIGHT = 14;
let paddleWidth = PADDLE_BASE_WIDTH;
let paddleX = (W - paddleWidth) / 2;
let paddleY = H - 36;
let paddleSpeed = 8;
let leftPressed = false;
let rightPressed = false;
let widePaddleTimer = 0;
// Balls
let balls = [];
const BALL_RADIUS = 6.5;
const BASE_BALL_SPEED = 5.8;
// Bricks
const BRICK_ROWS = 7;
const BRICK_COLS = 11;
const BRICK_W = 62;
const BRICK_H = 22;
const BRICK_PADDING_X = 8;
const BRICK_PADDING_Y = 8;
const BRICK_OFFSET_TOP = 55;
const BRICK_OFFSET_LEFT = (W - (BRICK_COLS * (BRICK_W + BRICK_PADDING_X) - BRICK_PADDING_X)) / 2;
let bricks = [];
// Particles & Powerups
let particles = [];
let powerups = [];
// Colors
const ROW_COLORS = [
{ fill: '#ff2a6d', stroke: '#ff7597' }, // Top: Pink
{ fill: '#ff5e00', stroke: '#ffa266' }, // Orange
{ fill: '#ffb703', stroke: '#ffe066' }, // Yellow
{ fill: '#05d550', stroke: '#61f893' }, // Green
{ fill: '#00f0ff', stroke: '#8bf7ff' }, // Cyan
{ fill: '#7928ca', stroke: '#b773ff' }, // Purple
{ fill: '#4361ee', stroke: '#8ba2ff' } // Blue
];
function initGame() {
score = 0;
lives = 3;
isGameOver = false;
isVictory = false;
widePaddleTimer = 0;
paddleWidth = PADDLE_BASE_WIDTH;
paddleX = (W - paddleWidth) / 2;
particles = [];
powerups = [];
initBricks();
resetBalls();
}
function initBricks() {
bricks = [];
for (let r = 0; r < BRICK_ROWS; r++) {
bricks[r] = [];
for (let c = 0; c < BRICK_COLS; c++) {
let isTough = (r === 0 || r === 1 || ((r + c) % 4 === 0 && r < 4));
let hp = isTough ? 2 : 1;
// Power-up chance
let powerupType = null;
if (Math.random() < 0.22) {
powerupType = Math.random() < 0.5 ? 'wide' : 'multiball';
}
bricks[r][c] = {
x: BRICK_OFFSET_LEFT + c * (BRICK_W + BRICK_PADDING_X),
y: BRICK_OFFSET_TOP + r * (BRICK_H + BRICK_PADDING_Y),
w: BRICK_W,
h: BRICK_H,
hp: hp,
maxHp: hp,
powerup: powerupType,
color: ROW_COLORS[r]
};
}
}
}
function resetBalls() {
balls = [{
x: paddleX + paddleWidth / 2,
y: paddleY - BALL_RADIUS - 2,
vx: (Math.random() > 0.5 ? 1 : -1) * (BASE_BALL_SPEED * 0.5),
vy: -BASE_BALL_SPEED,
radius: BALL_RADIUS,
trail: []
}];
}
function spawnPowerup(x, y, type) {
powerups.push({
x: x,
y: y,
w: 24,
h: 24,
vy: 2.2,
type: type,
pulse: 0
});
}
function spawnBrickParticles(x, y, w, h, color) {
const count = 14;
for (let i = 0; i < count; i++) {
particles.push({
x: x + Math.random() * w,
y: y + Math.random() * h,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6,
size: Math.random() * 3 + 2,
color: color,
life: 1.0,
decay: Math.random() * 0.03 + 0.02
});
}
}
// Input Handlers
window.addEventListener('keydown', (e) => {
if (isAutoplay && (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === ' ' || e.key === 'Enter')) {
isAutoplay = false;
initGame();
return;
}
if ((isGameOver || isVictory) && (e.key === ' ' || e.key === 'Enter')) {
isAutoplay = false;
initGame();
return;
}
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') leftPressed = true;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') rightPressed = true;
if (e.key === 'p' || e.key === 'P') isPaused = !isPaused;
});
window.addEventListener('keyup', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') leftPressed = false;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') rightPressed = false;
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
if (!isAutoplay) {
paddleX = mouseX - paddleWidth / 2;
clampPaddle();
}
});
canvas.addEventListener('mousedown', () => {
if (isAutoplay || isGameOver || isVictory) {
isAutoplay = false;
initGame();
}
});
function clampPaddle() {
if (paddleX < 8) paddleX = 8;
if (paddleX + paddleWidth > W - 8) paddleX = W - 8 - paddleWidth;
}
// Continuous collision detection between ball and rectangle
function checkBallRectCollision(ball, rect) {
// Find closest point on rect to ball center
let closestX = Math.max(rect.x, Math.min(ball.x, rect.x + rect.w));
let closestY = Math.max(rect.y, Math.min(ball.y, rect.y + rect.h));
let distX = ball.x - closestX;
let distY = ball.y - closestY;
let distSquared = distX * distX + distY * distY;
if (distSquared <= ball.radius * ball.radius) {
// Overlapping: determine normal
let penX = (rect.w / 2 + ball.radius) - Math.abs(ball.x - (rect.x + rect.w / 2));
let penY = (rect.h / 2 + ball.radius) - Math.abs(ball.y - (rect.y + rect.h / 2));
if (penX < penY) {
return { hit: true, normalX: ball.x > rect.x + rect.w / 2 ? 1 : -1, normalY: 0 };
} else {
return { hit: true, normalX: 0, normalY: ball.y > rect.y + rect.h / 2 ? 1 : -1 };
}
}
return { hit: false };
}
function update() {
if (isPaused) return;
// Powerup wide paddle timer
if (widePaddleTimer > 0) {
widePaddleTimer--;
paddleWidth = PADDLE_BASE_WIDTH * 1.45;
if (widePaddleTimer <= 0) {
paddleWidth = PADDLE_BASE_WIDTH;
}
}
// Autoplay AI or Keyboard input
if (isAutoplay) {
if (balls.length > 0) {
// Track lowest or closest ball
let targetBall = balls.reduce((prev, curr) => (curr.y > prev.y ? curr : prev), balls[0]);
let targetX = targetBall.x - paddleWidth / 2;
paddleX += (targetX - paddleX) * 0.16;
}
} else {
if (leftPressed) paddleX -= paddleSpeed;
if (rightPressed) paddleX += paddleSpeed;
}
clampPaddle();
// Sub-stepping for robust collision preventing tunneling
const STEPS = 3;
for (let s = 0; s < STEPS; s++) {
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
b.x += b.vx / STEPS;
b.y += b.vy / STEPS;
// Wall collisions
if (b.x - b.radius < 0) {
b.x = b.radius;
b.vx = Math.abs(b.vx);
} else if (b.x + b.radius > W) {
b.x = W - b.radius;
b.vx = -Math.abs(b.vx);
}
if (b.y - b.radius < 0) {
b.y = b.radius;
b.vy = Math.abs(b.vy);
}
// Bottom boundary
if (b.y - b.radius > H) {
balls.splice(i, 1);
continue;
}
// Paddle collision
const paddleRect = { x: paddleX, y: paddleY, w: paddleWidth, h: PADDLE_HEIGHT };
let pCol = checkBallRectCollision(b, paddleRect);
if (pCol.hit && b.vy > 0) {
b.y = paddleY - b.radius; // snap to top of paddle
// Calculate bounce angle: [-1 to 1] from center
let hitOffset = (b.x - (paddleX + paddleWidth / 2)) / (paddleWidth / 2);
hitOffset = Math.max(-0.95, Math.min(0.95, hitOffset));
let maxAngle = (72 * Math.PI) / 180; // 72 degrees max
let angle = hitOffset * maxAngle;
let speed = Math.hypot(b.vx, b.vy);
speed = Math.min(speed * 1.015, BASE_BALL_SPEED * 1.5); // Slight acceleration
b.vx = speed * Math.sin(angle);
b.vy = -Math.abs(speed * Math.cos(angle));
// Paddle impact particles
for (let p = 0; p < 5; p++) {
particles.push({
x: b.x,
y: paddleY,
vx: (Math.random() - 0.5) * 4,
vy: -Math.random() * 3 - 1,
size: 2,
color: '#00f0ff',
life: 0.8,
decay: 0.05
});
}
}
// Brick collisions
let hitAnyBrick = false;
for (let r = 0; r < BRICK_ROWS && !hitAnyBrick; r++) {
for (let c = 0; c < BRICK_COLS && !hitAnyBrick; c++) {
let brick = bricks[r][c];
if (!brick || brick.hp <= 0) continue;
let col = checkBallRectCollision(b, brick);
if (col.hit) {
hitAnyBrick = true;
brick.hp--;
score += 10;
// Reflect velocity based on surface normal
if (col.normalX !== 0) b.vx = -b.vx;
if (col.normalY !== 0) b.vy = -b.vy;
spawnBrickParticles(brick.x, brick.y, brick.w, brick.h, brick.color.fill);
// Brick destroyed
if (brick.hp <= 0) {
score += 25;
if (brick.powerup) {
spawnPowerup(brick.x + brick.w / 2, brick.y + brick.h / 2, brick.powerup);
}
}
}
}
}
}
}
// Record ball trail
balls.forEach(b => {
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 6) b.trail.shift();
});
// Check death & lives
if (balls.length === 0) {
lives--;
if (lives <= 0) {
isGameOver = true;
} else {
resetBalls();
}
}
// Check victory
let activeBricks = 0;
for (let r = 0; r < BRICK_ROWS; r++) {
for (let c = 0; c < BRICK_COLS; c++) {
if (bricks[r][c].hp > 0) activeBricks++;
}
}
if (activeBricks === 0) {
isVictory = true;
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= p.decay;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
// Update powerups
for (let i = powerups.length - 1; i >= 0; i--) {
let pw = powerups[i];
pw.y += pw.vy;
pw.pulse += 0.08;
// Check collision with paddle
if (
pw.y + pw.h / 2 >= paddleY &&
pw.y - pw.h / 2 <= paddleY + PADDLE_HEIGHT &&
pw.x >= paddleX - 4 &&
pw.x <= paddleX + paddleWidth + 4
) {
// Apply powerup
if (pw.type === 'wide') {
widePaddleTimer = 60 * 10; // 10 seconds at 60fps
} else if (pw.type === 'multiball') {
let currentCount = balls.length;
for (let j = 0; j < Math.min(2, 6 - currentCount); j++) {
let ref = balls[0] || { x: paddleX + paddleWidth / 2, y: paddleY - 10, vx: 3, vy: -5 };
balls.push({
x: ref.x,
y: ref.y,
vx: (Math.random() - 0.5) * 8,
vy: -Math.abs(ref.vy || BASE_BALL_SPEED),
radius: BALL_RADIUS,
trail: []
});
}
}
// Sparkle effect
for (let k = 0; k < 12; k++) {
particles.push({
x: pw.x,
y: paddleY,
vx: (Math.random() - 0.5) * 5,
vy: -Math.random() * 4 - 1,
size: 3,
color: pw.type === 'wide' ? '#00f0ff' : '#ff2a6d',
life: 1.0,
decay: 0.04
});
}
powerups.splice(i, 1);
continue;
}
if (pw.y > H + 30) {
powerups.splice(i, 1);
}
}
}
// Drawing routines
function draw() {
ctx.clearRect(0, 0, W, H);
// Subtle background grid
ctx.strokeStyle = 'rgba(255, 255, 255, 0.025)';
ctx.lineWidth = 1;
for (let x = 0; x < W; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = 0; y < H; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
// Draw Bricks
for (let r = 0; r < BRICK_ROWS; r++) {
for (let c = 0; c < BRICK_COLS; c++) {
let b = bricks[r][c];
if (b.hp <= 0) continue;
ctx.save();
ctx.shadowBlur = 8;
ctx.shadowColor = b.color.fill;
// Body
let grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
if (b.hp === 2) {
grad.addColorStop(0, '#ffffff');
grad.addColorStop(0.3, b.color.fill);
grad.addColorStop(1, '#0c1024');
} else {
grad.addColorStop(0, b.color.fill);
grad.addColorStop(1, '#0c1024');
}
ctx.fillStyle = grad;
ctx.strokeStyle = b.color.stroke;
ctx.lineWidth = 1.5;
roundRect(ctx, b.x, b.y, b.w, b.h, 4, true, true);
// Crack texture if tough brick is damaged (hp === 1 and maxHp === 2)
if (b.maxHp === 2 && b.hp === 1) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(b.x + b.w * 0.25, b.y + 2);
ctx.lineTo(b.x + b.w * 0.45, b.y + b.h * 0.55);
ctx.lineTo(b.x + b.w * 0.35, b.y + b.h * 0.8);
ctx.lineTo(b.x + b.w * 0.65, b.y + b.h - 2);
ctx.moveTo(b.x + b.w * 0.45, b.y + b.h * 0.55);
ctx.lineTo(b.x + b.w * 0.75, b.y + b.h * 0.4);
ctx.stroke();
}
// Power-up marker icon
if (b.powerup) {
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
let icon = b.powerup === 'wide' ? '⟷' : '✦';
ctx.fillText(icon, b.x + b.w / 2, b.y + b.h / 2);
}
ctx.restore();
}
}
// Draw Particles
particles.forEach(p => {
ctx.save();
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.color;
ctx.shadowBlur = 6;
ctx.shadowColor = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
// Draw Powerups
powerups.forEach(pw => {
ctx.save();
ctx.translate(pw.x, pw.y);
let scale = 1 + Math.sin(pw.pulse) * 0.12;
ctx.scale(scale, scale);
let col = pw.type === 'wide' ? '#00f0ff' : '#ff2a6d';
ctx.shadowBlur = 12;
ctx.shadowColor = col;
ctx.fillStyle = '#0f1428';
ctx.strokeStyle = col;
ctx.lineWidth = 2;
roundRect(ctx, -pw.w / 2, -pw.h / 2, pw.w, pw.h, 6, true, true);
ctx.fillStyle = col;
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(pw.type === 'wide' ? 'W' : '3×', 0, 1);
ctx.restore();
});
// Draw Paddle
ctx.save();
let padGrad = ctx.createLinearGradient(paddleX, paddleY, paddleX, paddleY + PADDLE_HEIGHT);
if (widePaddleTimer > 0) {
padGrad.addColorStop(0, '#8bf7ff');
padGrad.addColorStop(1, '#0084ff');
ctx.shadowColor = '#00f0ff';
} else {
padGrad.addColorStop(0, '#ffffff');
padGrad.addColorStop(1, '#05d550');
ctx.shadowColor = '#05d550';
}
ctx.shadowBlur = 14;
ctx.fillStyle = padGrad;
roundRect(ctx, paddleX, paddleY, paddleWidth, PADDLE_HEIGHT, 7, true, false);
// Paddle inner accent line
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(paddleX + 8, paddleY + 3);
ctx.lineTo(paddleX + paddleWidth - 8, paddleY + 3);
ctx.stroke();
ctx.restore();
// Draw Balls
balls.forEach(b => {
// Trail
b.trail.forEach((pt, idx) => {
ctx.beginPath();
ctx.arc(pt.x, pt.y, b.radius * (idx / b.trail.length) * 0.8, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 240, 255, ${idx * 0.08})`;
ctx.fill();
});
// Ball core
ctx.save();
ctx.shadowBlur = 14;
ctx.shadowColor = '#00f0ff';
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
// HUD
ctx.save();
ctx.font = 'bold 15px sans-serif';
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
// Score
ctx.textAlign = 'left';
ctx.fillText(`SCORE ${score}`, 24, 28);
// Wide Paddle Indicator
if (widePaddleTimer > 0) {
let secs = (widePaddleTimer / 60).toFixed(1);
ctx.fillStyle = '#00f0ff';
ctx.textAlign = 'center';
ctx.fillText(`WIDE PADDLE: ${secs}s`, W / 2, 28);
}
// Lives
ctx.textAlign = 'right';
ctx.fillStyle = '#ff2a6d';
let hearts = '';
for (let l = 0; l < lives; l++) hearts += '♥ ';
ctx.fillText(hearts.trim(), W - 24, 28);
ctx.restore();
// Overlay Messages
if (isAutoplay) {
drawOverlayBanner('NEON BREAKOUT', 'CLICK OR PRESS ANY KEY TO PLAY', '#00f0ff');
} else if (isGameOver) {
drawOverlayBanner('GAME OVER', 'PRESS SPACE OR CLICK TO RETRY', '#ff2a6d');
} else if (isVictory) {
drawOverlayBanner('VICTORY!', 'CONGRATULATIONS! CLICK TO PLAY AGAIN', '#05d550');
} else if (isPaused) {
drawOverlayBanner('PAUSED', 'PRESS P TO RESUME', '#ffb703');
}
}
function drawOverlayBanner(title, subtitle, color) {
ctx.save();
ctx.fillStyle = 'rgba(6, 8, 20, 0.72)';
ctx.fillRect(0, 0, W, H);
ctx.shadowColor = color;
ctx.shadowBlur = 20;
ctx.fillStyle = '#ffffff';
ctx.font = '900 42px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(title, W / 2, H / 2 - 12);
ctx.shadowBlur = 8;
ctx.fillStyle = color;
ctx.font = 'bold 14px sans-serif';
ctx.letterSpacing = '1px';
ctx.fillText(subtitle, W / 2, H / 2 + 30);
ctx.restore();
}
function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
// Initial setup
initGame();
loop();
</script>
</body>
</html>