Xing4.0 29B A4B (free)
by XingChenReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 31 s
- Cost
- $0
- Output tokens
- 8,432
- Spent thinking
- 0 tokens
- Speed
- 56 tok/s
- Console errors
- 0
- First token after
- 331 ms
- First visible text after
- 331 ms
- Prompt tokens
- 535
- Code size
- 18.8 KB
- Finish reason
- stop
- Served by
- Phala
- Model id
xingchen-agi/xing4.0-29b-a4b:free- Reasoning sent
{"effort":"none"}- Max output
- 32,000 tokens
- Generated
- Nov 9, 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 · 686 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Brick Breaker</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a2e; font-family: 'Segoe UI', Arial, sans-serif; }
#game-container { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; }
canvas { display: block; image-rendering: crisp-edges; max-width: 100%; max-height: 100%; touch-action: none; cursor: pointer; }
</style>
</head>
<body>
<div id="game-container">
<canvas id="game" width="800" height="600"></canvas>
</div>
<script>
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 800, H = 600;
function resize() {
const scale = Math.min(window.innerWidth / W, window.innerHeight / H);
const cw = Math.floor(W * scale);
const ch = Math.floor(H * scale);
canvas.style.width = cw + 'px';
canvas.style.height = ch + 'px';
}
resize();
window.addEventListener('resize', resize);
// ---- Game State ----
const STATE = { MENU: 0, PLAYING: 1, OVER: 2, VICTORY: 3 };
let state = STATE.MENU;
let score = 0;
let lives = 3;
const paddle = {
x: W / 2, y: H - 30, w: 120, h: 14, speed: 8,
get left() { return this.x - this.w / 2; },
get right() { return this.x + this.w / 2; },
};
const balls = [];
class Ball {
constructor(vx = 0, vy = 0) {
this.r = 8;
this.speed = 5.5;
this.trail = [];
this.updateVelocity(vx, vy);
}
updateVelocity(vx, vy) {
const mag = Math.hypot(vx, vy);
if (mag === 0) return;
this.vx = (vx / mag) * this.speed;
this.vy = (vy / mag) * this.speed;
}
launch() {
if (Math.abs(this.vy) < 0.5) {
const angle = (Math.random() - 0.5) * 0.6 - Math.PI / 2;
this.updateVelocity(Math.cos(angle) * this.speed, Math.sin(angle) * this.speed);
}
}
draw() {
// trail
for (let i = 0; i < this.trail.length; i++) {
const t = this.trail[i];
const a = (i / this.trail.length) * 0.4;
ctx.fillStyle = `rgba(100, 200, 255, ${a})`;
ctx.beginPath();
ctx.arc(t.x, t.y, this.r * (i / this.trail.length), 0, Math.PI * 2);
ctx.fill();
}
ctx.save();
ctx.shadowColor = '#64c8ff';
ctx.shadowBlur = 12;
ctx.fillStyle = '#e8f4ff';
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
move() {
this.trail.push({ x: this.x, y: this.y });
if (this.trail.length > 8) this.trail.shift();
this.x += this.vx;
this.y += this.vy;
}
}
class Particle {
constructor(x, y, color) {
this.x = x; this.y = y;
this.vx = (Math.random() - 0.5) * 7;
this.vy = (Math.random() - 0.5) * 7 - 1;
this.life = 1;
this.decay = 0.02 + Math.random() * 0.02;
this.color = color;
this.size = 2 + Math.random() * 3;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.15;
this.life -= this.decay;
}
draw() {
if (this.life <= 0) return;
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
ctx.globalAlpha = 1;
}
}
const particles = [];
class PowerUp {
constructor(x, y) {
this.x = x; this.y = y;
this.vy = 2.5;
this.type = Math.random() < 0.5 ? 'wide' : 'multi';
this.w = 22; this.h = 22;
this.bobOff = Math.random() * Math.PI * 2;
}
draw() {
const bx = this.x;
const by = this.y + Math.sin(Date.now() * 0.005 + this.bobOff) * 3;
ctx.save();
ctx.shadowColor = '#ffff00';
ctx.shadowBlur = 10;
// glow circle
ctx.fillStyle = '#fff8cc';
ctx.beginPath();
ctx.arc(bx, by, 10, 0, Math.PI * 2);
ctx.fill();
// icon
ctx.fillStyle = this.type === 'wide' ? '#44ddaa' : '#ff6644';
if (this.type === 'wide') {
// arrow icon
ctx.fillRect(bx - 7, by - 5, 14, 3);
ctx.fillRect(bx - 5, by - 3, 3, 6);
ctx.fillRect(bx + 2, by - 3, 3, 6);
} else {
// star icon
ctx.fillRect(bx - 5, by - 7, 4, 3);
ctx.fillRect(bx + 1, by - 7, 4, 3);
ctx.fillRect(bx - 2, by - 6, 4, 3);
}
ctx.restore();
}
fall() { this.y += this.vy; }
}
class Brick {
constructor(x, y, w, h, hp, type) {
this.x = x; this.y = y;
this.w = w; this.h = h;
this.hp = hp;
this.maxHp = hp;
this.type = type; // 0 normal, 1 tough
this.hitFlash = 0;
this.powerUp = type === 2 ? Math.random() < 0.35 : Math.random() < 0.12;
this.cracks = 0; // cracks after first hit for tough bricks
this.alpha = 1;
}
hit() {
this.hp--;
this.cracks = 1;
this.hitFlash = 1;
if (this.hp <= 0) {
return true; // destroyed
}
return false;
}
draw() {
const colors = [
['#e74c3c', '#c0392b'],
['#f39c12', '#e67e22'],
['#3498db', '#2980b9'],
['#9b59b6', '#8e44ad'],
];
const base = colors[this.type] || colors[0];
// brick body
ctx.fillStyle = this.hitFlash > 0 ? '#ffffff' : base[0];
ctx.fillRect(this.x, this.y, this.w, this.h);
// highlight
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(this.x, this.y, this.w, this.h * 0.35);
// border
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(this.x, this.y, this.w, this.h);
// crack lines
if (this.cracks > 0) {
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 1;
const cx = this.x + this.w / 2;
const cy = this.y + this.h / 2;
if (this.type === 1) {
ctx.beginPath();
ctx.moveTo(cx - this.w * 0.3, cy - this.h * 0.3);
ctx.lineTo(cx, cy);
ctx.lineTo(cx + this.w * 0.15, cy + this.h * 0.2);
ctx.lineTo(cx + this.w * 0.35, cy);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(cx + this.w * 0.2, cy - this.h * 0.25);
ctx.lineTo(cx - this.w * 0.05, cy);
ctx.moveTo(cx - this.w * 0.15, cy + this.h * 0.15);
ctx.lineTo(cx + this.w * 0.2, cy + this.h * 0.25);
ctx.stroke();
}
this.cracks--;
}
// power-up indicator
if (this.powerUp && this.hp > 0) {
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('★', this.x + this.w / 2, this.y + this.h / 2 + 1);
}
if (this.hp === 0) {
this.alpha -= 0.1;
if (this.alpha < 0) this.alpha = 0;
ctx.globalAlpha = this.alpha;
ctx.fillStyle = base[0];
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.globalAlpha = 1;
}
}
}
// ---- Level 1 ----
function createLevel1() {
bricks = [];
const rows = 6;
const cols = 12;
const bw = 62;
const bh = 22;
const padX = 40;
const gap = 6;
const totalW = cols * bw + (cols - 1) * gap;
const startX = (W - totalW) / 2;
const startY = 80;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let type = 0;
let hp = 1;
// row patterns: mix types
const pattern = [
[0,0,0,0,0,0,0,0,0,0,0,0], // row 0
[1,1,1,1,1,1,1,1,1,1,1,1], // row 1 tough
[0,0,0,0,2,2,0,0,0,2,2,0], // row 2
[0,1,1,1,1,1,1,1,1,1,1,0], // row 3
[2,0,0,0,0,0,0,0,0,0,0,2], // row 4
[1,0,2,2,0,0,0,0,0,2,2,1], // row 5
];
const p = pattern[r];
if (p[c] === 1) { type = 1; hp = 2; }
else if (p[c] === 2) { type = 2; hp = 1; }
else { type = 0; hp = 1; }
bricks.push(new Brick(startX + c * (bw + gap), startY + r * (bh + gap), bw, bh, hp, type));
}
}
}
let bricks = [];
let wideTimer = 0;
// ---- Input ----
const keys = {};
let mouseActive = false;
let mouseX = W / 2;
document.addEventListener('keydown', e => {
keys[e.key] = true;
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === ' ' || e.code === 'Space') && state === STATE.PLAYING) {
balls.forEach(b => b.launch());
}
if (e.key === 'r' || e.key === 'R') { restartGame(); }
});
document.addEventListener('keyup', e => { keys[e.key] = false; });
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) * (W / rect.width);
mouseActive = true;
});
canvas.addEventListener('mousedown', () => {});
function startBall() {
const angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.6;
balls.push(new Ball(Math.cos(angle) * 5.5, Math.sin(angle) * 5.5));
}
function spawnMultiBalls() {
balls.forEach(b => {
if (b.trail.length > 0) {
const oldV = { x: b.vx, y: b.vy };
b.updateVelocity(0, 0);
const a = Math.atan2(b.vy, b.vx);
for (let k = -1; k <= 1; k += 2) {
const newAngle = a + k * 0.35;
balls.push(new Ball(Math.cos(newAngle) * b.speed, Math.sin(newAngle) * b.speed));
}
b.updateVelocity(oldV.x, oldV.y);
} else {
balls.push(new Ball(-2, -3));
balls.push(new Ball(2, -3));
balls.push(new Ball(-5.5, -5.5));
}
});
}
function applyPowerUp(type) {
if (type === 'wide') {
paddle.w = 200;
wideTimer = 600; // 10 seconds at 60fps
} else if (type === 'multi') {
spawnMultiBalls();
}
}
function restartGame() {
score = 0;
lives = 3;
balls.length = 0;
particles.length = 0;
paddle.w = 120;
createLevel1();
wideTimer = 0;
state = STATE.PLAYING;
setTimeout(() => startBall(), 500);
}
// ---- Update ----
function update() {
if (state !== STATE.PLAYING) return;
// paddle movement
if (keys['ArrowLeft'] || keys['a'] || keys['A']) paddle.x -= paddle.speed;
if (keys['ArrowRight'] || keys['d'] || keys['D']) paddle.x += paddle.speed;
if (mouseActive) {
const target = Math.max(paddle.left, Math.min(paddle.right, mouseX));
paddle.x += (target - paddle.x) * 0.35;
}
paddle.x = Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, paddle.x));
// power-up timer
if (wideTimer > 0) {
wideTimer--;
if (wideTimer <= 0) { paddle.w = 120; }
}
// balls
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
b.move();
// walls
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); }
if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); }
if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); }
// bottom — lose ball
if (b.y - b.r > H) {
balls.splice(i, 1);
continue;
}
// paddle collision
if (b.vy > 0 && b.y + b.r >= paddle.y - 2 && b.y - b.r <= paddle.y + paddle.h + 2 &&
b.x >= paddle.left && b.x <= paddle.right) {
b.y = paddle.y - b.r;
const rel = (b.x - paddle.x) / (paddle.w / 2); // -1 to 1
const angle = rel * (Math.PI / 3); // up to 60 degrees
const speed = Math.max(b.speed, Math.hypot(b.vx, b.vy));
b.updateVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed);
}
// brick collisions
for (let j = 0; j < bricks.length; j++) {
const br = bricks[j];
if (br.hp <= 0 && br.alpha === 0) continue;
if (b.x + b.r > br.x && b.x - b.r < br.x + br.w &&
b.y + b.r > br.y && b.y - b.r < br.y + br.h) {
// determine collision side
const overlapL = (b.x + b.r) - br.x;
const overlapR = (br.x + br.w) - (b.x - b.r);
const overlapT = (b.y + b.r) - br.y;
const overlapB = (br.y + br.h) - (b.y - b.r);
const minOverlap = Math.min(overlapL, overlapR, overlapT, overlapB);
if (minOverlap === overlapT || minOverlap === overlapB) {
b.vy *= -1;
} else {
b.vx *= -1;
}
// hit brick
if (br.hit()) {
score += 10;
// particles
const colors = br.type === 1 ? ['#f39c12', '#e67e22'] : ['#3498db', '#2980b9'];
for (let p = 0; p < 12; p++) {
particles.push(new Particle(br.x + br.w / 2, br.y + br.h / 2, colors[0]));
particles.push(new Particle(br.x + br.w / 2, br.y + br.h / 2, colors[1]));
}
if (br.powerUp) {
powerups.push(new PowerUp(br.x + br.w / 2, br.y + br.h / 2));
}
} else {
// hit flash effect
for (let p = 0; p < 5; p++) {
particles.push(new Particle(br.x + br.w / 2, br.y + br.h / 2, '#ffffff'));
}
}
break; // one collision per frame per ball
}
}
}
// remove dead balls
for (let i = balls.length - 1; i >= 0; i--) {
if (balls[i].y - balls[i].r > H) {
balls.splice(i, 1);
}
}
// powerups fall
for (let i = powerups.length - 1; i >= 0; i--) {
const pu = powerups[i];
pu.fall();
if (pu.y > H) { powerups.splice(i, 1); continue; }
// catch
if (pu.y + pu.h / 2 >= paddle.y && pu.y - pu.h / 2 <= paddle.y + paddle.h &&
pu.x >= paddle.left && pu.x <= paddle.right) {
applyPowerUp(pu.type);
powerups.splice(i, 1);
}
}
// particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].life <= 0) particles.splice(i, 1);
}
// lose life
if (balls.length === 0) {
lives--;
if (lives <= 0) {
state = STATE.OVER;
} else {
balls.push(new Ball(0, 0)); // resting ball for input
setTimeout(() => startBall(), 300);
}
}
// victory check
let allDestroyed = true;
for (const br of bricks) {
if (br.hp > 0) { allDestroyed = false; break; }
}
if (allDestroyed && balls.length > 0) {
// give bonus score and replace level
score += 200;
setTimeout(() => {
if (state === STATE.PLAYING) {
state = STATE.VICTORY;
}
}, 800);
state = STATE.VICTORY_CHECK; // prevent multiple triggers
}
// actually let's just trigger victory properly
if (allDestroyed) {
state = STATE.VICTORY;
}
}
let powerups = [];
function drawPaddle() {
ctx.save();
ctx.shadowColor = '#4488ff';
ctx.shadowBlur = 15;
// gradient
const g = ctx.createLinearGradient(paddle.left, paddle.y, paddle.right, paddle.y + paddle.h);
g.addColorStop(0, '#5599ff');
g.addColorStop(0.5, '#3377ee');
g.addColorStop(1, '#2255cc');
ctx.fillStyle = g;
roundRect(ctx, paddle.left, paddle.y, paddle.w, paddle.h, 7);
ctx.fill();
ctx.strokeStyle = 'rgba(150,200,255,0.6)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.restore();
}
function roundRect(c, x, y, w, h, r) {
c.beginPath();
c.moveTo(x + r, y);
c.arcTo(x + w, y, x + w, y + h, r);
c.arcTo(x + w, y + h, x, y + h, r);
c.arcTo(x, y + h, x, y, r);
c.arcTo(x, y, x + w, y, r);
c.closePath();
}
function drawUI() {
ctx.save();
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.font = 'bold 20px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('SCORE: ' + score, 16, 14);
ctx.fillText('LIVES: ', 16, 42);
for (let i = 0; i < lives; i++) {
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.arc(70 + i * 22, 56, 8, 0, Math.PI * 2);
ctx.fill();
}
// wide indicator
if (wideTimer > 0) {
ctx.fillStyle = '#44ddaa';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'right';
const sec = Math.ceil(wideTimer / 60);
ctx.fillText('WIDE PADDLE: ' + sec + 's', W - 16, 14);
}
ctx.restore();
}
function drawBackground() {
// gradient bg
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0a0a2e');
grad.addColorStop(1, '#1a1a4e');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// grid dots
ctx.fillStyle = 'rgba(255,255,255,0.04)';
for (let x = 0; x < W; x += 40) {
for (let y = 0; y < H; y += 40) {
ctx.fillRect(x, y, 1, 1);
}
}
}
function render() {
drawBackground();
if (state === STATE.MENU) {
drawMenu();
return;
}
if (state === STATE.OVER || state === STATE.VICTORY) {
// still draw bricks and balls in background
drawAllEntities();
drawOverlay();
return;
}
drawAllEntities();
drawUI();
}
function drawAllEntities() {
// bricks
for (const br of bricks) { br.draw(); }
// powerups
for (const pu of powerups) { pu.draw(); }
// paddle
drawPaddle();
// balls
for (const b of balls) { b.draw(); }
// particles
for (const p of particles) { p.draw(); }
}
function drawOverlay() {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (state === STATE.OVER) {
ctx.font = 'bold 52px "Segoe UI", Arial, sans-serif';
ctx.fillText('GAME OVER', W / 2, H / 2 - 60);
ctx.font = 'bold 24px "Segoe UI", Arial, sans-serif';
ctx.fillText('Final Score: ' + score, W / 2, H / 2 + 10);
ctx.font = 'bold 20px "Segoe UI", Arial, sans-serif';
ctx.fillStyle = '#f39c12';
ctx.fillText('Click or press R to restart', W / 2, H / 2 + 60);
} else if (state === STATE.VICTORY) {
ctx.font = 'bold 52px "Segoe UI", Arial, sans-serif';
ctx.fillStyle = '#f1c40f';
ctx.fillText('VICTORY!', W / 2, H / 2 - 60);
ctx.font = 'bold 24px "Segoe UI", Arial, sans-serif';
ctx.fillText('Score: ' + score, W / 2, H / 2 + 10);
ctx.font = 'bold 20px "Segoe UI", Arial, sans-serif';
ctx.fillStyle = '#44ddaa';
ctx.fillText('Click or press R to play again', W / 2, H / 2 + 60);
}
}
function drawMenu() {
ctx.fillStyle = 'rgba(10,10,46,0.85)';
ctx.fillRect(0, 0, W, H);
// decorative bricks
const t = Date.now() * 0.001;
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 14; c++) {
const bx = 100 + c * 50;
const by = 200 + r * 50 + Math.sin(t + c * 0.5) * 8;
ctx.fillStyle = ['#e74c3c','#f39c12','#3498db','#9b59b6'][r % 4];
ctx.globalAlpha = 0.3;
ctx.fillRect(bx, by, 48, 20);
}
}
ctx.globalAlpha = 1;
// title
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 60px "Segoe UI", Arial, sans-serif';
ctx.shadowColor = '#4488ff';
ctx.shadowBlur = 20;
ctx.fillText('BRICK BREAKER', W / 2, 180);
ctx.shadowBlur = 0;
ctx.font = 'bold 20px "Segoe UI", Arial, sans-serif';
ctx.fillStyle = '#9bb8ff';
ctx.fillText('Click or press any key to start', W / 2, 260);
// controls hint
ctx.font = '16px "Segoe UI", Arial, sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.fillText('Mouse or Arrow Keys to move paddle', W / 2, 310);
ctx.fillText('Space to launch ball, R to restart', W / 2, 336);
ctx.fillText('★ Bricks with star carry power-ups', W / 2, 362);
}
// handle menu start
let menuStarted = false;
document.addEventListener('keydown', e => {
if (!menuStarted && (e.key === 'Enter' || e.key === ' ')) {
menuStarted = true;
restartGame();
}
}, { once: true });
canvas.addEventListener('click', () => {
if (state === STATE.MENU) {
restartGame();
} else if (state === STATE.OVER || state === STATE.VICTORY) {
restartGame();
}
});
// ---- Main Loop ----
function loop() {
if (state === STATE.PLAYING) {
update();
}
render();
requestAnimationFrame(loop);
}
// init
createLevel1();
loop();
</script>
</body>
</html>