Models 44

Xing4.0 29B A4B (free)

by XingChenReasoningClean
Open in new window

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>