Models 44

Dots3-Note Preview (free)

by Dots StudioReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
2 min 45 s
Cost
$0
Output tokens
14,473
Spent thinking
10,082 tokens
Speed
88 tok/s
Console errors
0
First token after
725 ms
First visible text after
2 min 6 s
Prompt tokens
507
Code size
13.6 KB
Finish reason
stop
Served by
AtlasCloud
Model id
dots-studio/dots-3-note-preview:free
Reasoning sent
model default
Max output
32,000 tokens
Generated
Nov 9, 2026

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 · 559 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
  body {
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background: #111;
    font-family: Arial, sans-serif;
    overflow: hidden;
    position: relative;
  }
  #gameCanvas {
    background: #000;
    border-radius: 8px;
    box-shadow: 0 0 20px rgba(0,255,204,0.5);
    display: block;
    max-width: 100%;
    height: auto;
    z-index: 1;
  }
  #overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.85);
    display: none;
    justify-content: center;
    align-items: center;
    color: #fff;
    font-size: 28px;
    text-align: center;
    z-index: 10;
    flex-direction: column;
    cursor: pointer;
  }
  #overlay-message {
    padding: 30px;
    background: rgba(0,0,0,0.6);
    border-radius: 12px;
    box-shadow: 0 0 15px rgba(0,255,204,0.4);
    max-width: 90%;
  }
  #overlay-message h1 {
    margin: 0 0 15px 0;
    color: #00ffcc;
    text-shadow: 0 0 10px #00ffcc;
  }
  #overlay-message p {
    margin: 10px 0;
    font-size: 20px;
  }
  .hidden {
    display: none !important;
  }
</style>
</head>
<body>
  <canvas id="gameCanvas" width="800" height="600"></canvas>
  <div id="overlay">
    <div id="overlay-message">
      <h1 id="overlay-title">Breakout</h1>
      <p id="overlay-text">Click or press a key to start</p>
    </div>
  </div>

<script>
// ====== Game Constants & Variables ======
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;

const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlay-title');
const overlayText = document.getElementById('overlay-text');

// Game objects
let paddle = {
  x: W/2 - 50,
  y: H - 40,
  w: 100,
  h: 15,
  baseW: 100,
  currentW: 100,
  speed: 10,
  color: '#00ffcc'
};

let balls = [];
let bricks = [];
let particles = [];
let powerUps = [];

let lives = 3;
let score = 0;
let state = 'menu'; // 'menu', 'playing', 'gameover', 'victory'
let demoMode = true;
let aiActive = true;

let widerPaddleTimer = null;
const widerPaddleDuration = 10000; // 10 seconds

// Input
const keys = {};
const mouse = { x: 0, y: 0 };

// ====== Utility Functions ======
function rand(min, max) { return Math.random() * (max - min) + min; }

function createParticles(x, y, color, count = 10) {
  for (let i = 0; i < count; i++) {
    particles.push({
      x, y,
      vx: rand(-4, 4),
      vy: rand(-4, 4),
      life: rand(30, 50),
      color,
      size: rand(2, 5)
    });
  }
}

function resetGame() {
  balls = [];
  balls.push({
    x: W/2,
    y: H - 60,
    vx: 4 * (Math.random() < 0.5 ? -1 : 1),
    vy: -4,
    radius: 8,
    color: '#ffffff'
  });

  paddle.x = W/2 - paddle.baseW/2;
  paddle.currentW = paddle.baseW;
  lives = 3;
  score = 0;
  particles = [];
  powerUps = [];

  if (widerPaddleTimer) {
    clearTimeout(widerPaddleTimer);
    widerPaddleTimer = null;
  }

  createBricks();
}

function createBricks() {
  bricks = [];
  const brickW = 80;
  const brickH = 30;
  const paddingX = 10;
  const paddingY = 10;
  const cols = Math.floor((W - paddingX * 2) / (brickW + paddingX));
  const rows = 6;
  const startX = (W - cols * (brickW + paddingX) + paddingX) / 2;
  const startY = 60;

  const types = ['normal', 'tough'];
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const type = types[Math.floor(Math.random() * types.length)];
      const health = type === 'normal' ? 1 : 2;
      let powerUp = 0;
      if (Math.random() < 0.2) {
        powerUp = Math.random() < 0.5 ? 1 : 2; // 1 = wider, 2 = multi-ball
      }
      bricks.push({
        x: startX + c * (brickW + paddingX),
        y: startY + r * (brickH + paddingY),
        w: brickW,
        h: brickH,
        health,
        maxHealth: health,
        type,
        powerUp,
        color: type === 'normal' ? '#ff6b6b' : '#ffa502',
        cracked: false
      });
    }
  }
}

function startDemo() {
  state = 'menu';
  demoMode = true;
  aiActive = true;
  overlayTitle.textContent = 'Breakout';
  overlayText.textContent = 'Click or press a key to start';
  overlay.classList.remove('hidden');
  resetGame();
}

function startPlayerGame() {
  state = 'playing';
  demoMode = false;
  aiActive = false;
  overlay.classList.add('hidden');
  resetGame();
}

function loseLife() {
  lives--;
  balls = [];
  balls.push({
    x: W/2,
    y: H - 60,
    vx: 4 * (Math.random() < 0.5 ? -1 : 1),
    vy: -4,
    radius: 8,
    color: '#ffffff'
  });
  paddle.x = W/2 - paddle.baseW/2;
  if (widerPaddleTimer) {
    clearTimeout(widerPaddleTimer);
    widerPaddleTimer = null;
    paddle.currentW = paddle.baseW;
  }
}

function showOverlay(title, text) {
  overlayTitle.textContent = title;
  overlayText.textContent = text;
  overlay.classList.remove('hidden');
}

// ====== Input Handling ======
window.addEventListener('keydown', (e) => {
  keys[e.key] = true;
  if (['ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
    e.preventDefault();
  }
  if (state === 'menu' || state === 'gameover' || state === 'victory') {
    startPlayerGame();
  }
});

window.addEventListener('keyup', (e) => {
  keys[e.key] = false;
});

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  mouse.x = e.clientX - rect.left;
  mouse.y = e.clientY - rect.top;
});

overlay.addEventListener('click', () => {
  if (state === 'menu' || state === 'gameover' || state === 'victory') {
    startPlayerGame();
  }
});

// ====== Game Loop ======
let lastTime = 0;
function gameLoop(timestamp) {
  if (!lastTime) lastTime = timestamp;
  const dt = timestamp - lastTime;
  lastTime = timestamp;

  update(dt);
  draw();
  requestAnimationFrame(gameLoop);
}

// ====== Update Logic ======
function update(dt) {
  // Paddle movement
  if (demoMode && aiActive) {
    // AI: move toward average ball X
    if (balls.length > 0) {
      let avgX = 0;
      for (const ball of balls) avgX += ball.x;
      avgX /= balls.length;
      paddle.x = avgX - paddle.currentW / 2;
    }
  } else {
    // Keyboard
    if (keys['ArrowLeft']) {
      paddle.x -= paddle.speed;
      if (paddle.x < 0) paddle.x = 0;
    }
    if (keys['ArrowRight']) {
      paddle.x += paddle.speed;
      if (paddle.x + paddle.currentW > W) paddle.x = W - paddle.currentW;
    }
    // Mouse
    if (mouse.x > 0) {
      paddle.x = mouse.x - paddle.currentW / 2;
      if (paddle.x < 0) paddle.x = 0;
      if (paddle.x + paddle.currentW > W) paddle.x = W - paddle.currentW;
    }
  }

  // Update balls
  for (let i = balls.length - 1; i >= 0; i--) {
    const ball = balls[i];
    ball.x += ball.vx;
    ball.y += ball.vy;

    // Walls
    if (ball.x - ball.radius < 0) {
      ball.x = ball.radius;
      ball.vx = -ball.vx;
    } else if (ball.x + ball.radius > W) {
      ball.x = W - ball.radius;
      ball.vx = -ball.vx;
    }
    if (ball.y - ball.radius < 0) {
      ball.y = ball.radius;
      ball.vy = -ball.vy;
    } else if (ball.y + ball.radius > H) {
      balls.splice(i, 1);
      if (balls.length === 0) loseLife();
      continue;
    }

    // Paddle collision
    if (ball.vy > 0) {
      if (
        ball.x > paddle.x - ball.radius &&
        ball.x < paddle.x + paddle.currentW + ball.radius &&
        ball.y + ball.radius > paddle.y &&
        ball.y - ball.radius < paddle.y + paddle.h
      ) {
        const hitPos = (ball.x - paddle.x) / paddle.currentW; // 0..1
        const maxAngle = 75 * Math.PI / 180;
        const angle = (hitPos - 0.5) * 2 * maxAngle;
        const speed = Math.hypot(ball.vx, ball.vy);
        ball.vx = speed * Math.sin(angle);
        ball.vy = -speed * Math.cos(angle);
        ball.y = paddle.y - ball.radius; // prevent sticking
      }
    }

    // Brick collision
    for (let j = 0; j < bricks.length; j++) {
      const brick = bricks[j];
      if (
        ball.x + ball.radius > brick.x &&
        ball.x - ball.radius < brick.x + brick.w &&
        ball.y + ball.radius > brick.y &&
        ball.y - ball.radius < brick.y + brick.h
      ) {
        // Determine collision axis
        const overlapX = Math.min(
          ball.x + ball.radius - brick.x,
          brick.x + brick.w - ball.x + ball.radius
        );
        const overlapY = Math.min(
          ball.y + ball.radius - brick.y,
          brick.y + brick.h - ball.y + ball.radius
        );

        if (overlapX < overlapY) {
          ball.vx = -ball.vx;
          if (ball.x < brick.x + brick.w / 2) {
            ball.x = brick.x - ball.radius;
          } else {
            ball.x = brick.x + brick.w + ball.radius;
          }
        } else {
          ball.vy = -ball.vy;
          if (ball.y < brick.y + brick.h / 2) {
            ball.y = brick.y - ball.radius;
          } else {
            ball.y = brick.y + brick.h + ball.radius;
          }
        }

        // Damage brick
        brick.health--;
        if (brick.maxHealth > 1 && brick.health === brick.maxHealth - 1) {
          brick.cracked = true;
        }
        if (brick.health <= 0) {
          createParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, brick.color);
          score += 10;
          if (brick.powerUp !== 0) {
            powerUps.push({
              type: brick.powerUp,
              x: brick.x + brick.w / 2,
              y: brick.y + brick.h / 2,
              w: 30,
              h: 30,
              vy: 4,
              color: brick.powerUp === 1 ? '#00ffcc' : '#ffcc00'
            });
          }
          bricks.splice(j, 1);
          j--;
        }
        break; // only one brick per ball per frame
      }
    }
  }

  // Update particles
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.2; // gravity
    p.life -= 1;
    if (p.life <= 0) particles.splice(i, 1);
  }

  // Update power-ups
  for (let i = powerUps.length - 1; i >= 0; i--) {
    const p = powerUps[i];
    p.y += p.vy;

    // Check paddle catch
    if (
      p.y + p.h > paddle.y &&
      p.y < paddle.y + paddle.h &&
      p.x + p.w > paddle.x &&
      p.x < paddle.x + paddle.currentW
    ) {
      if (p.type === 1) {
        // Wider paddle
        paddle.currentW = paddle.baseW * 1.5;
        if (widerPaddleTimer) clearTimeout(widerPaddleTimer);
        widerPaddleTimer = setTimeout(() => {
          paddle.currentW = paddle.baseW;
          widerPaddleTimer = null;
        }, widerPaddleDuration);
      } else if (p.type === 2) {
        // Multi-ball: add two extra balls
        for (let k = 0; k < 2; k++) {
          const angle = rand(-Math.PI / 4, Math.PI / 4);
          const speed = 5;
          balls.push({
            x: p.x,
            y: p.y,
            vx: speed * Math.cos(angle),
            vy: -speed * Math.sin(angle),
            radius: 8,
            color: '#ffffff'
          });
        }
      }
      powerUps.splice(i, 1);
    } else if (p.y > H) {
      powerUps.splice(i, 1);
    }
  }

  // Game state checks
  if (lives <= 0) {
    state = 'gameover';
    showOverlay('Game Over', 'Click to restart');
  } else if (bricks.length === 0) {
    state = 'victory';
    showOverlay('Victory!', 'Click to restart');
  }
}

// ====== Render ======
function draw() {
  // Background gradient
  const bg = ctx.createLinearGradient(0, 0, 0, H);
  bg.addColorStop(0, '#1a1a2e');
  bg.addColorStop(1, '#16213e');
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, W, H);

  // Bricks
  for (const brick of bricks) {
    ctx.save();
    ctx.fillStyle = brick.color;
    if (brick.cracked) ctx.fillStyle = '#ff9f43';
    ctx.fillRect(brick.x, brick.y, brick.w, brick.h);

    // Crack lines
    if (brick.cracked) {
      ctx.strokeStyle = '#000';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(brick.x + 5, brick.y + 5);
      ctx.lineTo(brick.x + brick.w - 5, brick.y + brick.h - 5);
      ctx.moveTo(brick.x + brick.w - 5, brick.y + 5);
      ctx.lineTo(brick.x + 5, brick.y + brick.h - 5);
      ctx.stroke();
    }

    // Power-up icon
    if (brick.powerUp !== 0) {
      ctx.fillStyle = brick.powerUp === 1 ? '#00ffcc' : '#ffcc00';
      ctx.font = '20px Arial';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(brick.powerUp === 1 ? 'W' : 'M', brick.x + brick.w / 2, brick.y + brick.h / 2);
    }
    ctx.restore();
  }

  // Paddle (with gradient)
  const paddleGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.h);
  paddleGrad.addColorStop(0, '#00ffcc');
  paddleGrad.addColorStop(1, '#00b894');
  ctx.fillStyle = paddleGrad;
  ctx.fillRect(paddle.x, paddle.y, paddle.currentW, paddle.h);

  // Balls
  for (const ball of balls) {
    ctx.fillStyle = ball.color;
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fill();
  }

  // Particles
  for (const p of particles) {
    ctx.fillStyle = p.color;
    ctx.globalAlpha = Math.max(0, p.life / 50);
    ctx.fillRect(p.x, p.y, p.size, p.size);
  }
  ctx.globalAlpha = 1;

  // Power-ups
  for (const p of powerUps) {
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x, p.y, p.w, p.h);
    ctx.fillStyle = '#000';
    ctx.font = '20px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(p.type === 1 ? 'W' : 'M', p.x + p.w / 2, p.y + p.h / 2);
  }

  // UI
  ctx.fillStyle = '#ffffff';
  ctx.font = '20px Arial';
  ctx.textAlign = 'left';
  ctx.fillText('Score: ' + score, 10, 30);
  ctx.textAlign = 'right';
  ctx.fillText('Lives: ' + lives, W - 10, 30);
}

// ====== Start ======
window.addEventListener('load', () => {
  startDemo();
  requestAnimationFrame(gameLoop);
});
</script>
</body>
</html>
Reasoning trace (10,082 tokens)
We need to produce a single HTML file with canvas, vanilla JS. Must be polished. Must include mouse and arrow keys control. Bounce angle depends on hit position on paddle. At least 6 rows of bricks, mixing normal and tougher bricks (2 hits, crack after first). Some bricks have power-up icon; when broken they drop power-up, catching with paddle gives wider paddle (10 sec) or multi-ball (3 balls). Ball must never pass through bricks or paddle. Particles when brick breaks. 3 lives, score displayed. Game over and victory screens with restart option.

Also need to ensure it's self-contained, no external resources except maybe CDN? Probably not needed. Use inline CSS and JS.

We need to output exactly one fenced code block with the HTML file. No extra text.

We need to ensure it works in a 1280x800 viewport, adapt down to 375px. So we should set canvas size maybe 800x600? But we can make it responsive. Use CSS to scale.

We need to implement game logic: paddle, ball, bricks, power-ups, lives, score, particles.

Also need to handle game over and victory screens with restart.

We need to include a self-playing demo mode? The instruction 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 the game can be judged in action without playing." So we need to show a start screen, and then after user click or key, start game. But also we can have an AI that plays automatically? Or we can just start the game and have the paddle controlled by mouse/keys; but 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 we need to implement an AI that controls paddle automatically, maybe moving towards ball, until user interacts. Or we can just show a start overlay and when clicked, start game. But the phrase "self-playing demo mode (a simple AI plays)" suggests that the game should be playable automatically, maybe with AI controlling paddle, so that the game can be judged without user input. So we need to implement an AI that moves paddle to follow ball, and the game runs automatically. Then when user clicks or presses key, the AI stops and user takes control. Or maybe we can just have the AI always control paddle until user clicks? The instruction: "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." So we should show a start screen with message, and while waiting, maybe run a demo where AI plays. Or after click, start game with user control. But they want the game to be judged in action without playing, so we need to have an AI that plays automatically for a few seconds. So we can implement a simple AI that moves paddle towards ball's x position. Then when user clicks, we switch to user control.

We need to ensure the AI is enabled initially, and after user input, disable AI and allow mouse/keys.

Also need to handle multiple balls from power-up.

We need to ensure ball never passes through bricks or paddle. So we need proper collision detection.

Particles when brick breaks: spawn small particles.

We need to display score and lives.

Game over when ball falls below paddle (lose life). Victory when all bricks destroyed.

We need to have restart option on game over and victory screens.

We need to handle power-ups: wider paddle (10 seconds) and multi-ball (3 balls). When catching power-up, apply effect.

Power-up icons on some bricks; when broken, they drop a power-up item that falls down; if paddle catches it (i.e., intersects), apply effect.

We need to ensure that power-ups are visible.

We need to mix normal bricks and tougher bricks (2 hits). Tougher bricks crack after first hit.

At least 6 rows of bricks. We can create a grid.

We need to handle mouse movement and arrow keys.

We need to make it polished: nice visuals, colors, UI.

We'll create a single HTML file with canvas, style, script.

We'll implement:

- Canvas size: maybe 800x600, but responsive.

- Game state: running, paused, gameOver, victory.

- Paddle: position, width, height, base width, current width (for power-up), maybe color.

- Ball: position, velocity, radius.

- Bricks: array with properties: x, y, width, height, health, maxHealth, type (normal, tough), powerUp (none, widerPaddle, multiBall), cracked (bool), color.

- Particles: array of particles with position, velocity, life, color.

- Power-ups: falling items with type, position, velocity.

- Lives: 3.

- Score.

- Input: mouse move, arrow keys.

- AI: if demo mode, paddle moves towards ball.

- Collision detection: circle-rectangle for ball with bricks and paddle. For paddle, maybe treat as rectangle. For bricks, rectangle.

- Bounce angle: compute relative hit position on paddle. The paddle width, ball radius. When ball hits paddle, compute hit position = ball.x - paddle.x. Then normalize to -1..1. Then angle = hitPos * maxAngle (e.g., 75 degrees). Then set ball velocity accordingly.

- For bricks, bounce ball back.

- When ball hits brick, reduce health. If health <=0, break brick, spawn particles, maybe drop power-up.

- Power-up falling: move down; if intersects paddle, apply effect and remove.

- Effects: wider paddle: increase paddle width for 10 seconds, then revert. Multi-ball: spawn additional balls (maybe 2 extra) for a limited time? The requirement says "multi-ball (3 balls)". So when catching, we get 3 balls total (original plus 2 new). Could be permanent until all balls lost? Usually multi-ball power-up gives extra balls that stay until lost. We'll implement that when catching, we add two new balls with random directions, and keep them all. If any ball goes out, lose life? Typically you lose a life if any ball falls. But with multiple balls, you might lose only when all balls are gone. We'll implement that: maintain array of balls; when a ball falls, remove it; if no balls left, lose life. When power-up caught, spawn two extra balls.

- Ensure ball never passes through bricks or paddle: we need continuous collision detection? For simplicity, we can use discrete but with small steps. But we can ensure by checking if ball moves inside brick, then push out. Or use proper collision response.

We'll implement a simple physics loop with small time steps.

We'll also add sound? Not required, but can add simple beep using WebAudio? Not necessary.

We need to display start screen with "Click or press a key to start". While waiting, AI plays? Or we can just show start screen and when clicked, start game. But they want self-playing demo mode behind the message. So we can have the game running in background with AI controlling paddle, but show overlay. When user clicks, hide overlay and let user control.

We'll implement:

- State: 'menu', 'playing', 'gameover', 'victory'.

- In menu, show overlay. Also run a demo: set AI active, start game loop but with overlay. The game loop will update everything, but we can also pause? Actually we can run the game loop continuously, but if state is menu, we still update AI and ball movement, but we show overlay. When user clicks, set state to playing, disable AI.

But careful: The AI should play automatically, so we need to simulate a game. We could just start a new game automatically and let AI control paddle. The user can then take over by clicking.

We'll implement AI: if demoMode true, paddle.x = ball.x - paddle.width/2, but clamp to canvas.

We'll also need to handle multiple balls in AI? The AI can just target the average x of balls.

We'll set demoMode true initially. When user presses key or clicks, set demoMode false and state to playing (if not already). Also we need to reset game? Probably start fresh.

We'll create a function startGame() that resets everything.

We'll also need to handle restart from game over or victory screens.

We'll design UI: score and lives displayed on top.

We'll make it look polished with gradients, shadows.

We'll also add pause? Not required.

Now, let's plan the code structure.

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Breakout</title>
<style>
  /* CSS */
</style>
</head>
<body>
  <canvas id="gameCanvas"></canvas>
  <script>
    // JS
  </script>
</body>
</html>

We'll set canvas size to 800x600, but make it responsive with CSS max-width:100%; height:auto; but we need to maintain aspect ratio. We'll set canvas width and height attributes, then CSS to scale.

We'll center canvas.

We'll create overlay for start, game over, victory.

We'll use flexbox for overlays.

Now, implement game logic.

We'll define constants:

canvas, ctx, width, height.

paddle = { x, y, w, h, baseW, currentW, speed, color }

ball = { x, y, vx, vy, radius, color }

balls array.

bricks = array.

particles = array.

powerUps = array.

lives, score.

state: 'menu', 'playing', 'gameover', 'victory'.

demoMode = true.

lastTime = 0.

We'll requestAnimationFrame loop.

Functions:

- init(): set up canvas, create bricks, reset game.

- reset(): reset paddle, ball, bricks, lives, score, particles, powerUps, balls.

- startGame(): set state to playing, reset, demoMode false? Actually when user clicks start, we set demoMode false and start game. But we also need to allow AI to play initially. So we can have a separate startDemo() that runs AI.

Simplify: On load, we show menu overlay, but also start a demo game in background with AI. The user can click to start their own game. So we need two modes: demo and real. When user clicks, we reset and start real game with user control.

Thus, we can have a function startDemo() that sets demoMode true, resets game, runs loop. Then overlay with "Click or press a key to start". When user input, we call startPlayerGame() which sets demoMode false, resets game, hides overlay.

But we need to ensure that during demo, the game is visible behind overlay. The overlay can be semi-transparent.

We'll create overlay element with id 'overlay' and message.

We'll style it.

Now, implement movement:

- Mouse: canvas.addEventListener('mousemove', (e) => { if (!demoMode) { paddle.x = e.offsetX - paddle.currentW/2; clamp; } });

But offset relative to canvas. We'll get bounding rect.

- Arrow keys: window.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') move paddle left; if 'ArrowRight' move right; also maybe start game on any key.

We'll also handle touch? Not required.

We'll implement paddle movement with smoothness.

Now, bricks generation:

We'll create rows, columns. At least 6 rows.

Define brick dimensions: width = 80, height = 30, padding.

We'll have grid: cols = 10, rows = 6.

Brick types: normal (health 1), tough (health 2). Some have power-up.

We'll assign random type.

We'll also assign power-up with small probability.

We'll create bricks array with properties: x, y, w, h, health, maxHealth, type, powerUp (0 none, 1 wider, 2 multi), color, cracked (bool).

When hit, decrement health. If health == maxHealth-1 and maxHealth>1, set cracked true, maybe change color.

When health reaches 0, remove brick, spawn particles, maybe drop power-up.

Particles: each particle has x, y, vx, vy, life, color, size. Update: move, fade, remove when life <=0.

Power-up: when brick breaks, if powerUp !=0, create powerUp object with type, x, y, w, h, vy, color, icon. It falls down. If it intersects paddle, apply effect and remove.

Effects:

- Wider paddle: set paddle.currentW = paddle.baseW * 1.5 for 10 seconds. Use a timer.

- Multi-ball: add two new balls with random direction.

We'll track active power-up timers: widerPaddleTimer.

We'll also need to handle multiple balls: when a ball falls (y > canvas.height + ball.radius), remove it. If balls.length == 0, lose life.

If lives reach 0, gameOver.

If bricks.length == 0, victory.

Collision detection:

- Ball with walls: left, right, top bounce. Bottom: lose ball.

- Ball with paddle: if ball moving down and intersects paddle rectangle, compute hit position, set angle.

- Ball with bricks: if ball moving up/down and intersects brick rectangle, bounce.

We need to prevent ball passing through bricks or paddle. Use continuous? We can use simple: after updating position, check collisions and resolve.

We'll implement a function updateBall(ball) that moves ball, then checks collisions.

But with multiple balls, we need to handle each.

We'll also handle power-up falling.

We'll also handle paddle widening timer.

Now, drawing:

- Draw background gradient.

- Draw paddle.

- Draw balls.

- Draw bricks.

- Draw particles.

- Draw power-ups.

- Draw UI: score, lives.

- Draw overlays.

Now, let's write code.

We'll use requestAnimationFrame loop with timestamp.

We'll implement step function.

We'll also need to handle resizing.

Now, let's produce the final HTML.

We'll include CSS for styling, canvas, overlays.

We'll make it look polished.

We'll include instructions.

Now, write code.

We'll start with HTML skeleton.

We'll include meta viewport.

We'll set canvas width=800 height=600.

We'll style body to display flex, justify center, align center, height 100vh, background #111.

We'll style canvas with background #000, border-radius, box-shadow.

We'll create overlay div with id 'overlay', position absolute, top 0, left 0, width 100%, height 100%, display flex, justify center, align center, background rgba(0,0,0,0.7), color #fff, font-family Arial, z-index 10.

We'll have button for restart? Actually we can just click to restart.

We'll show message.

Now, JavaScript.

We'll define variables.

We'll write functions.

Let's start coding.

We'll need to handle the demo mode: initially, we set state = 'menu', demoMode = true, but we also start the game loop and run a demo game. So we need to initialize a demo game.

We'll create function startDemo() that resets game and sets demoMode true, then shows overlay with message.

We'll also have function startPlayerGame() that resets game, sets demoMode false, hides overlay.

We'll add event listeners for canvas click and keydown to start player game.

But we also need to allow user to take over during demo? The instruction says "Click or press a key to start". So when user clicks or presses key, we start the real game. So we can just have overlay on top, and when user clicks, we start real game.

Thus, we can have:

- On load, init game, start demo loop (AI plays), show overlay.

- When user clicks or presses key, hide overlay, set demoMode false, reset game? Or we can keep current state but switch control to user. But better to reset to fresh game.

We'll reset.

Now, implement AI: simple: paddle.x = average ball x - paddle.width/2.

But we need to ensure AI doesn't cheat? It's fine.

We'll also have AI move smoothly.

Now, let's write code.

We'll define constants:

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;

But we may need to adjust for responsive? We'll keep fixed size.

We'll set canvas width=800 height=600.

Now, game objects.

We'll create classes or plain objects.

We'll use plain objects for simplicity.

Let's outline:

let paddle = {
  x: W/2 - 50,
  y: H - 40,
  w: 100,
  h: 15,
  baseW: 100,
  currentW: 100,
  speed: 10,
  color: '#00ffcc'
};

let balls = []; // array of ball objects

let bricks = [];

let particles = [];

let powerUps = [];

let lives = 3;
let score = 0;
let state = 'menu'; // 'menu', 'playing', 'gameover', 'victory'
let demoMode = true;
let overlay = document.getElementById('overlay');
let overlayMessage = document.getElementById('overlay-message');

// Power-up timers
let widerPaddleTimer = null;
let widerPaddleDuration = 10000; // 10 sec

// Input
let keys = {};
let mouse = { x: 0, y: 0, clicked: false };

// AI
let aiActive = true;

Now, functions:

function init() {
  // create bricks
  createBricks();
  // reset game
  resetGame();
  // start demo
  startDemo();
  // event listeners
  window.addEventListener('keydown', (e) => {
    keys[e.key] = true;
    if (e.key === ' ' || e.key === 'Enter') {
      // start game
      if (state === 'menu' || state === 'gameover' || state === 'victory') {
        startPlayerGame();
      }
    }
  });
  canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    mouse.x = e.clientX - rect.left;
    mouse.y = e.clientY - rect.top;
  });
  canvas.addEventListener('click', () => {
    if (state === 'menu' || state === 'gameover' || state === 'victory') {
      startPlayerGame();
    }
  });
  // also arrow keys for paddle
  window.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft') {
      paddle.x -= paddle.speed;
      if (paddle.x < 0) paddle.x = 0;
    }
    if (e.key === 'ArrowRight') {
      paddle.x += paddle.speed;
      if (paddle.x + paddle.currentW > W) paddle.x = W - paddle.currentW;
    }
  });
  // start loop
  requestAnimationFrame(gameLoop);
}

function createBricks() {
  bricks = [];
  const brickW = 80;
  const brickH = 30;
  const paddingX = 10;
  const paddingY = 10;
  const cols = Math.floor((W - paddingX*2) / (brickW + paddingX));
  const rows = 6;
  const startX = (W - cols * (brickW + paddingX) + paddingX) / 2;
  const startY = 60;
  const types = ['normal', 'tough'];
  const powerUps = [0, 1, 2]; // 0 none, 1 wider, 2 multi
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      let type = types[Math.floor(Math.random() * types.length)];
      let health = type === 'normal' ? 1 : 2;
      let powerUp = 0;
      if (Math.random() < 0.2) {
        powerUp = Math.random() < 0.5 ? 1 : 2;
      }
      bricks.push({
        x: startX + c * (brickW + paddingX),
        y: startY + r * (brickH + paddingY),
        w: brickW,
        h: brickH,
        health: health,
        maxHealth: health,
        type: type,
        powerUp: powerUp,
        color: type === 'normal' ? '#ff6b6b' : '#ffa502',
        cracked: false
      });
    }
  }
}

function resetGame() {
  balls = [];
  // create ball
  balls.push({
    x: W/2,
    y: H - 60,
    vx: 4 * (Math.random() < 0.5 ? -1 : 1),
    vy: -4,
    radius: 8,
    color: '#fff'
  });
  paddle.x = W/2 - paddle.baseW/2;
  paddle.currentW = paddle.baseW;
  lives = 3;
  score = 0;
  particles = [];
  powerUps = [];
  // clear timer
  if (widerPaddleTimer) {
    clearTimeout(widerPaddleTimer);
    widerPaddleTimer = null;
  }
  // create bricks
  createBricks();
}

function startDemo() {
  state = 'menu';
  demoMode = true;
  overlayMessage.innerHTML = 'Click or press a key to start';
  overlay.style.display = 'flex';
  // reset game for demo
  resetGame();
  // set AI active
  aiActive = true;
}

function startPlayerGame() {
  state = 'playing';
  demoMode = false;
  overlay.style.display = 'none';
  resetGame();
  aiActive = false;
}

function gameLoop(timestamp) {
  if (!lastTime) lastTime = timestamp;
  const dt = timestamp - lastTime;
  lastTime = timestamp;
  update(dt);
  draw();
  requestAnimationFrame(gameLoop);
}

function update(dt) {
  // handle AI
  if (demoMode && aiActive) {
    // move paddle towards average ball x
    if (balls.length > 0) {
      let avgX = 0;
      for (let ball of balls) avgX += ball.x;
      avgX /= balls.length;
      paddle.x = avgX - paddle.currentW/2;
      // clamp
      if (paddle.x < 0) paddle.x = 0;
      if (paddle.x + paddle.currentW > W) paddle.x = W - paddle.currentW;
    }
  } else {
    // handle user input: arrow keys
    if (keys['ArrowLeft']) {
      paddle.x -= paddle.speed;
      if (paddle.x < 0) paddle.x = 0;
    }
    if (keys['ArrowRight']) {
      paddle.x += paddle.speed;
      if (paddle.x + paddle.currentW > W) paddle.x = W - paddle.currentW;
    }
    // mouse control
    if (mouse.x > 0) {
      paddle.x = mouse.x - paddle.currentW/2;
      if (paddle.x < 0) paddle.x = 0;
      if (paddle.x + paddle.currentW > W) paddle.x = W - paddle.currentW;
    }
  }

  // update balls
  for (let i = balls.length - 1; i >= 0; i--) {
    let ball = balls[i];
    ball.x += ball.vx;
    ball.y += ball.vy;

    // wall collisions
    if (ball.x - ball.radius < 0) {
      ball.x = ball.radius;
      ball.vx = -ball.vx;
    } else if (ball.x + ball.radius > W) {
      ball.x = W - ball.radius;
      ball.vx = -ball.vx;
    }
    if (ball.y - ball.radius < 0) {
      ball.y = ball.radius;
      ball.vy = -ball.vy;
    } else if (ball.y + ball.radius > H) {
      // ball lost
      balls.splice(i, 1);
      if (balls.length === 0) {
        loseLife();
      }
      continue;
    }

    // paddle collision
    if (ball.vy > 0) {
      if (ball.x > paddle.x - ball.radius && ball.x < paddle.x + paddle.currentW + ball.radius &&
          ball.y + ball.radius > paddle.y && ball.y - ball.radius < paddle.y + paddle.h) {
        // bounce
        let hitPos = (ball.x - paddle.x) / paddle.currentW; // 0..1
        let angle = (hitPos - 0.5) * Math.PI / 2; // -45..45 deg? Actually we want center straight up, edges sharp angle.
        // We'll map to angle range: max angle 75 degrees from horizontal? Typically breakout uses angle based on hit position.
        // Let's compute: maxAngle = 75 * Math.PI/180;
        // angle = (hitPos - 0.5) * 2 * maxAngle; // -maxAngle..maxAngle
        // Then set vx = speed * sin(angle), vy = -speed * cos(angle)
        let speed = Math.sqrt(ball.vx*ball.vx + ball.vy*ball.vy);
        let maxAngle = 75 * Math.PI / 180;
        let ang = (hitPos - 0.5) * 2 * maxAngle;
        ball.vx = speed * Math.sin(ang);
        ball.vy = -speed * Math.cos(ang);
        // adjust position to avoid sticking
        ball.y = paddle.y - ball.radius;
      }
    }

    // brick collisions
    for (let j = 0; j < bricks.length; j++) {
      let brick = bricks[j];
      if (ball.x + ball.radius > brick.x && ball.x - ball.radius < brick.x + brick.w &&
          ball.y + ball.radius > brick.y && ball.y - ball.radius < brick.y + brick.h) {
        // bounce
        // determine which side hit
        // simple: reverse vy if hitting top/bottom, vx if sides
        // But we need to avoid passing through, so we can push out.
        // We'll compute overlap
        let overlapX = Math.min(ball.x + ball.radius - brick.x, brick.x + brick.w - ball.x + ball.radius);
        let overlapY = Math.min(ball.y + ball.radius - brick.y, brick.y + brick.h - ball.y + ball.radius);
        if (overlapX < overlapY) {
          ball.vx = -ball.vx;
          // push out
          if (ball.x < brick.x + brick.w/2) {
            ball.x = brick.x - ball.radius;
          } else {
            ball.x = brick.x + brick.w + ball.radius;
          }
        } else {
          ball.vy = -ball.vy;
          if (ball.y < brick.y + brick.h/2) {
            ball.y = brick.y - ball.radius;
          } else {
            ball.y = brick.y + brick.h + ball.radius;
          }
        }
        // damage brick
        brick.health--;
        if (brick.maxHealth > 1 && brick.health === brick.maxHealth - 1) {
          brick.cracked = true;
        }
        if (brick.health <= 0) {
          // break brick
          createParticles(brick.x + brick.w/2, brick.y + brick.h/2, brick.color);
          score += 10;
          // drop power-up
          if (brick.powerUp !== 0) {
            powerUps.push({
              type: brick.powerUp,
              x: brick.x + brick.w/2,
              y: brick.y + brick.h/2,
              w: 30,
              h: 30,
              vy: 4,
              color: brick.powerUp === 1 ? '#00ffcc' : '#ffcc00'
            });
          }
          bricks.splice(j, 1);
          j--;
        }
        break; // only one brick per frame
      }
    }
  }

  // update particles
  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.2; // gravity
    p.life -= 1;
    if (p.life <= 0) {
      particles.splice(i, 1);
    }
  }

  // update power-ups
  for (let i = powerUps.length - 1; i >= 0; i--) {
    let p = powerUps[i];
    p.y += p.vy;
    // check paddle collision
    if (p.y + p.h > paddle.y && p.y < paddle.y + paddle.h &&
        p.x + p.w > paddle.x && p.x < paddle.x + paddle.currentW) {
      // apply effect
      if (p.type === 1) {
        // wider paddle
        paddle.currentW = paddle.baseW * 1.5;
        // clear previous timer
        if (widerPaddleTimer) clearTimeout(widerPaddleTimer);
        widerPaddleTimer = setTimeout(() => {
          paddle.currentW = paddle.baseW;
          widerPaddleTimer = null;
        }, widerPaddleDuration);
      } else if (p.type === 2) {
        // multi-ball: add two new balls
        for (let k = 0; k < 2; k++) {
          let angle = Math.random() * Math.PI/2 - Math.PI/4; // random direction
          let speed = 5;
          balls.push({
            x: p.x,
            y: p.y,
            vx: speed * Math.cos(angle),
            vy: -speed * Math.sin(angle),
            radius: 8,
            color: '#fff'
          });
        }
      }
      powerUps.splice(i, 1);
    } else if (p.y > H) {
      powerUps.splice(i, 1);
    }
  }

  // check game over / victory
  if (lives <= 0) {
    state = 'gameover';
    showOverlay('Game Over', 'Click to restart');
  } else if (bricks.length === 0) {
    state = 'victory';
    showOverlay('Victory!', 'Click to restart');
  }
}

function loseLife() {
  lives--;
  // reset ball
  balls = [];
  balls.push({
    x: W/2,
    y: H - 60,
    vx: 4 * (Math.random() < 0.5 ? -1 : 1),
    vy: -4,
    radius: 8,
    color: '#fff'
  });
  // reset paddle position?
  paddle.x = W/2 - paddle.baseW/2;
  // if wider, revert?
  if (widerPaddleTimer) {
    clearTimeout(widerPaddleTimer);
    widerPaddleTimer = null;
    paddle.currentW = paddle.baseW;
  }
}

function createParticles(x, y, color) {
  for (let i = 0; i < 10; i++) {
    particles.push({
      x: x,
      y: y,
      vx: (Math.random() - 0.5) * 6,
      vy: (Math.random() - 0.5) * 6,
      life: 30 + Math.random() * 20,
      color: color,
      size: Math.random() * 4 + 2
    });
  }
}

function showOverlay(title, message) {
  overlayMessage.innerHTML = `${title}<br>${message}`;
  overlay.style.display = 'flex';
}

function draw() {
  // clear
  ctx.fillStyle = '#111';
  ctx.fillRect(0, 0, W, H);

  // draw bricks
  for (let brick of bricks) {
    ctx.save();
    if (brick.cracked) {
      ctx.fillStyle = '#ff9f43';
    } else {
      ctx.fillStyle = brick.color;
    }
    ctx.fillRect(brick.x, brick.y, brick.w, brick.h);
    // draw crack lines if cracked
    if (brick.cracked) {
      ctx.strokeStyle = '#000';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(brick.x + 5, brick.y + 5);
      ctx.lineTo(brick.x + brick.w - 5, brick.y + brick.h - 5);
      ctx.moveTo(brick.x + brick.w - 5, brick.y + 5);
      ctx.lineTo(brick.x + 5, brick.y + brick.h - 5);
      ctx.stroke();
    }
    // draw power-up icon
    if (brick.powerUp !== 0) {
      ctx.fillStyle = brick.powerUp === 1 ? '#00ffcc' : '#ffcc00';
      ctx.font = '20px Arial';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(brick.powerUp === 1 ? 'W' : 'M', brick.x + brick.w/2, brick.y + brick.h/2);
    }
    ctx.restore();
  }

  // draw paddle
  ctx.fillStyle = paddle.color;
  ctx.fillRect(paddle.x, paddle.y, paddle.currentW, paddle.h);
  // add gradient
  // maybe ignore

  // draw balls
  for (let ball of balls) {
    ctx.fillStyle = ball.color;
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fill();
  }

  // draw particles
  for (let p of particles) {
    ctx.fillStyle = p.color;
    ctx.globalAlpha = p.life / 50;
    ctx.fillRect(p.x, p.y, p.size, p.size);
  }
  ctx.globalAlpha = 1;

  // draw power-ups
  for (let p of powerUps) {
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x, p.y, p.w, p.h);
    // draw icon
    ctx.fillStyle = '#000';
    ctx.font = '20px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(p.type === 1 ? 'W' : 'M', p.x + p.w/2, p.y + p.h/2);
  }

  // draw UI
  ctx.fillStyle = '#fff';
  ctx.font = '20px Arial';
  ctx.textAlign = 'left';
  ctx.fillText('Score: ' + score, 10, 30);
  ctx.textAlign = 'right';
  ctx.fillText('Lives: ' + lives, W - 10, 30);
}

// Start
window.onload = init;

We need to handle the overlay HTML elements.

We'll add div with id="overlay" and inner div with id="overlay-message".

We'll style them.

Also we need to ensure that the canvas is behind overlay.

We'll set position relative on body or container.

Let's write HTML:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
  body {
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background: #111;
    font-family: Arial, sans-serif;
    overflow: hidden;
  }
  #gameCanvas {
    background: #000;
    border-radius: 8px;
    box-shadow: 0 0 20px rgba(0,255,204,0.5);
    display: block;
    max-width: 100%;
    height: auto;
  }
  #overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.8);
    display: none;
    justify-content: center;
    align-items: center;
    color: #fff;
    font-size: 24px;
    text-align: center;
    z-index: 10;
    flex-direction: column;
  }
  #overlay-message {
    padding: 20px;
    background: rgba(0,0,0,0.5);
    border-radius: 10px;
  }
  #overlay-message button {
    margin-top: 20px;
    padding: 10px 20px;
    font-size: 18px;
    background: #00ffcc;
    border: none;
    border-radius: 5px;
    cursor: pointer;
  }
</style>
</head>
<body>
  <canvas id="gameCanvas" width="800" height="600"></canvas>
  <div id="overlay">
    <div id="overlay-message"></div>
  </div>
  <script>
    // ... code ...
  </script>
</body>
</html>

But we need to ensure the canvas is behind overlay. We'll set body position relative.

Also we need to handle the fact that the overlay covers the canvas, so mouse events on canvas will be blocked when overlay visible. That's fine.

Now, we need to incorporate the script.

We'll put the script inside the HTML.

We need to ensure that the game loop runs continuously, even when overlay is shown, so that the demo plays behind.

Our current code starts the loop on init, and overlay is shown initially. The loop runs, update and draw, but overlay is drawn on top via HTML, so the game is still running behind.

But we need to ensure that during demo, the AI controls paddle and the game runs.

Our update function checks demoMode and aiActive. Initially demoMode true, aiActive true.

But we also need to handle the case where user clicks to start: we call startPlayerGame which sets demoMode false, resets game, hides overlay.

But we also need to allow user to use arrow keys and mouse. In update, we check if demoMode false, then handle input.

But we also have keydown listener that sets keys. However, we also have a keydown listener for starting game. We need to ensure that arrow keys don't trigger start when overlay is visible. We can check if state is menu, then any key starts game.

We'll modify keydown listener:

window.addEventListener('keydown', (e) => {
  keys[e.key] = true;
  // if overlay is visible, start game on any key except maybe arrow keys? But we can start on any key.
  if (state === 'menu' || state === 'gameover' || state === 'victory') {
    startPlayerGame();
  }
});

But we also need to handle arrow keys for paddle when game is playing. That is already in update.

But we also have a separate keydown listener for arrow keys that moves paddle directly. That might conflict with the keys object. We can just rely on the keys object and update loop.

We'll remove the direct arrow key listener in init and just use keys object.

In update, we check keys['ArrowLeft'] etc.

We also need to prevent default behavior of arrow keys scrolling page. We can add e.preventDefault() in keydown listener if keys are arrow.

We'll add:

window.addEventListener('keydown', (e) => {
  keys[e.key] = true;
  if (['ArrowLeft','ArrowRight',' '].includes(e.key)) {
    e.preventDefault();
  }
  if (state === 'menu' || state === 'gameover' || state === 'victory') {
    startPlayerGame();
  }
});

Similarly for keyup.

We'll add keyup listener to set keys false.

Now, we need to handle mouse movement: we set mouse.x, mouse.y. In update, we use mouse.x to position paddle. But we need to ensure that mouse movement only when game is playing and not demo. In update, we check if !demoMode, then we can use mouse.x.

But we also need to ensure that mouse movement doesn't interfere with overlay. It's fine.

Now, we need to handle the case where user clicks on canvas to start. We have click listener.

Now, we need to ensure that the game loop runs at 60fps, but we use requestAnimationFrame.

Now, we need to test logic.

Potential issues:

- Ball stuck in brick? We have push out.

- Multiple balls: we need to handle each ball's collision with paddle and bricks.

- Power-up wider paddle: we need to reset width after 10 sec, but if game ends before, we clear timer.

- Multi-ball: we add two new balls. They should be active.

- When ball falls, we remove it. If no balls left, loseLife.

- Lose life: reset ball, maybe also reset paddle width.

- Victory condition: bricks.length === 0.

- Game over: lives <= 0.

Now, we need to ensure that the ball never passes through bricks or paddle. Our collision detection uses after moving ball, then check overlap. But if ball moves fast, it might pass through. We can mitigate by using smaller step or continuous collision. But for simplicity, we can use the approach of predicting next position and checking collision before moving. However, we can also use the current method with small time step. Since we use requestAnimationFrame, dt may be large on low fps. We can clamp dt or use fixed step.

We can implement a fixed step loop: use accumulator.

But to keep it simple, we can use dt to scale movement, but we already move by constant speed. Actually we don't use dt in movement; we just move by constant velocity. So if fps drops, movement may be larger per frame? Actually we move by velocity per frame, not per second. That could cause passing through if velocity is high. But we can keep velocity low (4 px/frame) and at 60fps it's fine. If fps drops, the movement per frame is still 4px, but time between frames is larger, so effectively speed increases. But we can clamp dt.

We can use a simple approach: use a fixed step of 16ms. But we can also just use dt to multiply velocity, but then we need to adjust.

Simplify: we can use a physics step of 16ms, and run multiple steps if dt large. But for this demo, it's okay.

We'll add a simple clamp: if dt > 32, treat as 32.

But we don't use dt currently. We'll incorporate dt to scale movement.

We'll change update to accept dt and scale velocities.

But we need to adjust collision detection accordingly.

Alternatively, we can keep constant speed and rely on small enough speed.

Given the requirements, it's probably fine.

Now, we need to ensure that the paddle doesn't move off screen.

We already clamp.

Now, we need to ensure that the ball bounce angle is correct.

We used hitPos from 0 to 1, then angle = (hitPos - 0.5) * 2 * maxAngle. That gives angle in radians from -maxAngle to maxAngle. Then vx = speed * sin(ang), vy = -speed * cos(ang). That should send ball upward with varying angle.

But we need to ensure that the ball always goes upward after hitting paddle. Our vy becomes negative, so it goes up.

Now, we need to test.

Now, we need to add particles when brick breaks.

We have createParticles.

Now, we need to draw particles with globalAlpha.

Now, we need to ensure that the overlay is shown on top.

We'll set #overlay display: flex; but initially none. We'll show via JS.

Now, we need to handle restart: when user clicks on overlay, we call startPlayerGame which resets.

But we also need to allow user to click on "Restart" button? We can just make the whole overlay clickable.

We'll add a button inside overlay-message, but we can also just detect click on overlay.

We'll add a button with id "startBtn" and text "Start Game". Then add event listener.

But the instruction says "Click or press a key to start". So we can just have the overlay with message, and clicking anywhere starts.

We'll make the overlay cover the canvas, and add click listener to the overlay.

We'll modify HTML: overlay div with id="overlay", inside a div with id="overlay-message". We'll add a button maybe.

But we can just set overlay.addEventListener('click', startPlayerGame).

We'll do that.

Now, we need to ensure that the game is playable on mobile? Not required.

Now, we need to ensure that the canvas is responsive: we set max-width:100%; height:auto; but we also need to maintain aspect ratio. We can set width:100%; height:auto; but then the canvas intrinsic size is 800x600, and CSS will scale it. However, the coordinate system inside canvas remains 800x600. So mouse events need to be scaled. We already compute mouse position relative to canvas using bounding rect, which accounts for scaling. So it's fine.

Now, we need to ensure that the canvas is centered and fits within viewport.

We'll set body display flex, justify center, align center, height 100vh.

Canvas max-width:100%; height:auto; will scale down.

Now, we need to ensure that the game is playable at 375px width. The canvas will scale down, but the game logic uses 800x600 internally, so it will be scaled. That's fine.

Now, we need to add some visual polish: background gradient, paddle gradient, etc.

We can add gradients.

We'll modify draw functions.

For background, we can draw a gradient.

For paddle, we can add a gradient.

We'll also add a subtle glow.

But keep it simple.

Now, we need to ensure that the game over and victory screens have a restart option. Our overlay click restarts.

We'll change overlay message accordingly.

Now, we need to ensure that the game starts in demo mode with AI playing. Our startDemo function sets state to 'menu', shows overlay, resets game, sets aiActive true. The game loop runs, and AI controls paddle. The ball moves, bricks break, etc. The user can watch.

When user clicks, startPlayerGame resets game and sets demoMode false, hides overlay.

But we also need to ensure that the AI doesn't interfere after user starts. We set aiActive false.

Now, we need to handle the case where user clicks while game is already over or victory. Our startPlayerGame resets.

Now, we need to ensure that the game loop continues to run when overlay is shown. Yes.

Now, we need to ensure that the game loop doesn't pause when user clicks away? Not needed.

Now, we need to add sound? Not required.

Now, we need to ensure that the code runs without errors.

We'll write the complete HTML.

Let's produce the final code.

We'll include all CSS and JS.

We'll also add a small instruction text.

Now, let's write the final answer.