Models 44

Nemotron 3 Nano Omni (free)

by NVIDIAReasoningClean
Open in new window

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

How it went

Total time
2 min 49 s
Cost
$0
Output tokens
16,792
Spent thinking
13,560 tokens
Speed
99.5 tok/s
Console errors
0
First token after
240 ms
First visible text after
2 min 20 s
Prompt tokens
517
Code size
9.3 KB
Finish reason
stop
Served by
Nvidia
Model id
nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free
Reasoning sent
model default
Max output
32,000 tokens
Generated
Nov 9, 2026

What it was asked

The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
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 · 367 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Snake Demo</title>
<style>
  body {
    margin:0;
    padding:0;
    height:100vh;
    display:flex;
    justify-content:center;
    align-items:center;
    background:#111;
    position:relative;
    overflow:hidden;
  }
  canvas {
    background:#222;
    border:2px solid #555;
    width:80vw;
    max-width:800px;
    height:auto;
  }
  .overlay {
    position:absolute;
    top:50%;
    left:50%;
    transform:translate(-50%,-50%);
    color:#fff;
    font-size:24px;
    background:rgba(0,0,0,0.6);
    padding:20px;
    border-radius:8px;
    text-align:center;
    z-index:10;
    display:none;
    pointer-events:auto;
  }
  #hud {
    position:absolute;
    top:10px;
    left:10px;
    color:#fff;
    font-family:sans-serif;
    font-size:16px;
    background:rgba(0,0,0,0.5);
    padding:5px 10px;
    border-radius:4px;
    pointer-events:none;
  }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="startOverlay" class="overlay">Click or press a key to start</div>
<div id="hud">Score: 0   Best: 0</div>

<script>
let canvas, ctx, cellSize;
let snake = [];
let direction = {dx:0, dy:0};
const queue = [];
let food = {x:0, y:0};
let score = 0;
let bestScore = 0;
let paused = false;
let gameState = 'playing'; // 'playing','paused','gameover'
const particles = [];

canvas = document.getElementById('game');
ctx = canvas.getContext('2d');

// Set fixed canvas size for simplicity
function init() {
  canvas.width = 800;
  canvas.height = 600;
  cellSize = canvas.width / 20; // 20x20 grid
  // Reset game state
  snake = [];
  const startX = Math.floor(canvas.width / 2 / cellSize);
  const startY = Math.floor(canvas.height / 2 / cellSize);
  snake = [
    {x:startX, y:startY},
    {x:startX-1, y:startY},
    {x:startX-2, y:startY}
  ];
  direction = {dx:1, dy:0};
  queue.length = 0;
  score = 0;
  bestScore = 0;
  paused = false;
  gameState = 'playing';
  placeFood();
  // Show start overlay
  const overlay = document.getElementById('startOverlay');
  overlay.style.display = 'block';
  requestAnimationFrame(gameLoop);
}

// Place food at a random empty cell
function placeFood() {
  let valid;
  do {
    food.x = Math.floor(Math.random() * 20);
    food.y = Math.floor(Math.random() * 20);
    valid = true;
    for (const seg of snake) {
      if (seg.x === food.x && seg.y === food.y) {
        valid = false;
        break;
      }
    }
  } while (!valid);
}

// Simple AI direction (towards food, avoiding opposite)
function getAI() {
  const dx = food.x - snake[0].x;
  const dy = food.y - snake[0].y;
  let target = {dx:0, dy:0};
  if (Math.abs(dx) > Math.abs(dy)) {
    target.dx = dx > 0 ? 1 : -1;
  } else {
    target.dy = dy > 0 ? 1 : -1;
  }
  const dirs = [{dx:1,dy:0},{dx:-1,dy:0},{dx:0,dy:1},{dx:0,dy:-1}];
  let chosen = null;
  for (const d of dirs) {
    if (d.dx !== -direction.dx || d.dy !== -direction.dy) {
      if (d.dx === target.dx && d.dy === target.dy) {
        chosen = d;
        break;
      }
    }
  }
  if (!chosen) {
    for (const d of dirs) {
      if (d.dx !== -direction.dx || d.dy !== -direction.dy) {
        chosen = d;
        break;
      }
    }
  }
  return chosen;
}

// Particle burst when eating
function createParticles(x, y) {
  for (let i = 0; i < 8; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = 2 + Math.random() * 3;
    particles.push({
      x: (x + 0.5) * cellSize,
      y: (y + 0.5) * cellSize,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      radius: cellSize * 0.1,
      life: 1
    });
  }
}

// Key handling
canvas.addEventListener('keydown', e => {
  if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
    if (direction.dy === 0) queue.push({dx:0, dy:1});
  } else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') {
    if (direction.dy === 0) queue.push({dx:0, dy:-1});
  } else if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
    if (direction.dx === 0) queue.push({dx:-1, dy:0});
  } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
    if (direction.dx === 0) queue.push({dx:1, dy:0});
  } else if (e.key === ' ') {
    if (gameState === 'gameover') init();
  } else if (e.key === 'Enter') {
    if (gameState === 'gameover') init();
  } else if (e.key === 'p' || e.key === 'P') {
    paused = !paused;
  }
  // Hide start overlay on any key press (except restart keys)
  const overlay = document.getElementById('startOverlay');
  if (overlay && overlay.style.display !== 'none' && !(e.key === ' ' || e.key === 'Enter')) {
    overlay.style.display = 'none';
  }
});

canvas.addEventListener('click', () => {
  canvas.focus();
  const overlay = document.getElementById('startOverlay');
  if (overlay) overlay.style.display = 'none';
});

// Game loop
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

// Update snake position and handle collisions
function update() {
  if (paused || gameState !== 'playing') return;

  // Process queued user directions
  if (queue.length > 0) {
    const dir = queue.shift();
    if (!(dir.dx === -direction.dx && dir.dy === -direction.dy)) {
      direction = dir;
    }
  } else {
    // No user input, let AI drive
    const aiDir = getAI();
    if (!(aiDir.dx === -direction.dx && aiDir.dy === -direction.dy)) {
      direction = aiDir;
    }
  }

  // Move head
  const head = {
    x: snake[0].x + direction.dx,
    y: snake[0].y + direction.dy
  };

  // Wall collision
  if (head.x < 0 || head.x >= 20 || head.y < 0 || head.y >= 20) {
    endGame();
    return;
  }

  // Self collision (excluding tail that will be removed)
  const selfCollides = snake.some((seg, idx) => {
    if (idx === snake.length - 1) return false; // tail will move away
    return seg.x === head.x && seg.y === head.y;
  });

  if (selfCollides) {
    endGame();
    return;
  }

  // Add new head
  snake.unshift(head);

  // Check food
  if (head.x === food.x && head.y === food.y) {
    score += 10;
    if (score > bestScore) bestScore = score;
    createParticles(head.x, head.y);
    placeFood();
  } else {
    // Normal move: remove tail
    snake.pop();
  }
}

// End game and show overlay
function endGame() {
  gameState = 'gameover';
}

// Draw everything
function draw() {
  // Clear
  ctx.fillStyle = '#222';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Grid
  ctx.strokeStyle = '#444';
  ctx.lineWidth = 1;
  for (let i = 0; i <= 20; i++) {
    ctx.beginPath();
    ctx.moveTo(i * cellSize, 0);
    ctx.lineTo(i * cellSize, canvas.height);
    ctx.stroke();
  }
  for (let i = 0; i <= 20; i++) {
    ctx.beginPath();
    ctx.moveTo(0, i * cellSize);
    ctx.lineTo(canvas.width, i * cellSize);
    ctx.stroke();
  }

  // Food
  ctx.fillStyle = '#ff0000';
  ctx.beginPath();
  ctx.arc((food.x + 0.5) * cellSize, (food.y + 0.5) * cellSize, cellSize / 4, 0, Math.PI * 2);
  ctx.fill();

  // Particles
  ctx.fillStyle = '#0ff';
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
    ctx.fill();
    p.life -= 0.05;
    if (p.life <= 0) particles.splice(i, 1);
  }

  // Snake
  for (let i = 0; i < snake.length; i++) {
    const seg = snake[i];
    const isHead = i === 0;
    const hue = isHead ? 120 : 100; // head green, body darker
    ctx.fillStyle = `hsl(${hue}, 80%, 50%)`;
    ctx.fillRect((seg.x + 0.1) * cellSize, (seg.y + 0.1) * cellSize, cellSize * 0.8, cellSize * 0.8);

    // Eyes
    if (isHead) {
      const eyeSize = cellSize * 0.2;
      let eye1, eye2;
      if (direction.dx === 1) { // right
        eye1 = {x: seg.x + 0.75, y: seg.y + 0.375};
        eye2 = {x: seg.x + 0.75, y: seg.y + 0.625};
      } else if (direction.dx === -1) { // left
        eye1 = {x: seg.x + 0.25, y: seg.y + 0.375};
        eye2 = {x: seg.x + 0.25, y: seg.y + 0.625};
      } else if (direction.dy === 1) { // down
        eye1 = {x: seg.x + 0.375, y: seg.y + 0.75};
        eye2 = {x: seg.x + 0.625, y: seg.y + 0.75};
      } else if (direction.dy === -1) { // up
        eye1 = {x: seg.x + 0.375, y: seg.y + 0.25};
        eye2 = {x: seg.x + 0.625, y: seg.y + 0.25};
      }
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc((eye1.x + 0.5) * cellSize, (eye1.y + 0.5) * cellSize, eyeSize, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc((eye2.x + 0.5) * cellSize, (eye2.y + 0.5) * cellSize, eyeSize, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  // HUD
  ctx.fillStyle = '#fff';
  ctx.font = '16px sans-serif';
  ctx.fillText(`Score: ${score}   Best: ${bestScore}`, 10, 20);

  if (paused) {
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'center';
    ctx.font = '30px sans-serif';
    ctx.fillText('Paused', canvas.width / 2, canvas.height / 2);
  }

  if (gameState === 'gameover') {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'center';
    ctx.font = '24px sans-serif';
    ctx.fillText('Game Over', canvas.width / 2, canvas.height / 2 - 20);
    ctx.font = '18px sans-serif';
    ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2);
    ctx.fillText('Press Space or Enter to restart', canvas.width / 2, canvas.height / 2 + 20);
  }
}

// Start the game
init();
</script>
</body>
</html>
Reasoning trace (13,560 tokens)
We need to produce a single HTML file with canvas, vanilla JS, CSS inline. Must meet many constraints.

We need a grid 20x20 cells centered. Use canvas size maybe 800x600. Each cell size = canvas width / 20, height same. Center canvas on page using flexbox.

Controls: arrow keys and WASD. Need to handle quick successive key presses: we can store direction queue and only apply if not opposite direction. Also need to ensure snake cannot reverse onto itself: when moving, we must prevent turning 180 degrees (i.e., current direction opposite of next). So we need to track direction and only allow changes if new direction != opposite of current direction.

Movement smooth: use requestAnimationFrame and update position each frame based on speed (e.g., 10 cells per second). Snake moves continuously; we can have a timer that updates position every tick.

Food: random position not on snake. When snake head collides with food, increase score by 10, grow snake (add a segment), spawn new food not on snake.

Game over: if head hits wall (outside canvas) or self (collision with body). Show overlay with score. Space or Enter restarts, P pauses.

Visual polish: snake head distinct (maybe different color), eyes facing direction (draw two circles). Particle burst when eating: maybe small circles that fade.

Score display: current score and best score (store in localStorage? Not allowed). But we can store best score in a variable in memory; session persists across page reloads? Not required; just keep best score variable, update if current > best.

Need to ensure no external resources. Use canvas only.

Need to ensure page looks right in 1280x800 and adapt down to 375px wide. Use responsive canvas: set canvas width/height based on container width, maybe use CSS to center and max-width.

We need to ensure that after 3 seconds a screenshot is taken automatically (the sandbox does that). So we need something visible on first paint: maybe the game start screen with "Click or press a key to start". But we also need self-playing demo mode: an AI plays automatically, so we need to start the game automatically after a short delay? The requirement: "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 can have a message overlay, and behind it an AI snake moves automatically. When user clicks or presses a key, the overlay disappears and user controls.

Thus we need to implement a demo mode where an AI follows a simple algorithm (e.g., move towards food). The AI should be visible before user interaction. The overlay says "Click or press a key to start". After click, we switch to player control.

We also need to ensure that the game starts automatically (self-play) before user interaction, but the overlay is shown. So maybe we start the game loop immediately, with AI controlling snake, and show overlay. When user clicks, we set a flag to allow player input, hide overlay.

Need to ensure that the snake cannot reverse onto itself: same rule for AI and player.

Particle burst: when eating food, create small circles that expand and fade.

Score display: top of canvas maybe.

Best score: keep variable bestScore; update when game over if current > best.

Implementation details:

- Canvas: set width = 800, height = 600 (or responsive). Use CSS to center.

- Grid: cellSize = canvas.width / 20. We'll compute based on canvas size.

- Snake: array of cells {x, y}. Head is first element. Direction stored as {dx, dy} each cell is 1 cell step.

- Movement: each frame (requestAnimationFrame) we update head position by adding direction. Then we check collisions.

- Since movement is continuous, we need to ensure snake moves in whole cells. We'll keep snake position as integer grid coordinates. Each frame we move head by direction, then we check if head cell is out of bounds or collides with body (except the tail which moves away). For self-collision, we need to check all body cells except the tail if the tail is moving out (i.e., if we are not growing). Implementation: after moving head, if not growing, remove tail cell (pop) and then check if new head collides with any remaining body cells.

- Food: random cell not occupied by snake.

- Eating: if head cell equals food cell, increase score by 10, do not pop tail (so snake grows). Spawn new food at random empty cell.

- Particle burst: create array of particles with x, y, radius, velocity, life. On eating, push several particles at head position.

- Visuals: draw background grid (light gray). Draw snake: head color (e.g., green), body color (darker green). For eyes: draw two circles on head based on direction (e.g., if direction is up, eyes at top-left/right? Actually facing direction: eyes could be two circles at front of head, maybe left/right relative to direction). Simpler: draw two small circles at head position offset by half cell in direction perpendicular? Let's think: If snake moves right, eyes could be at top-left and top-right of head? Maybe easier: draw two circles at head position with small offset: for direction up, eyes at (x-0.25, y-0.25) and (x+0.25, y-0.25). For direction down, at (x-0.25, y+0.25) and (x+0.25, y+0.25). For left/right, at (x-0.25, y-0.25) and (x-0.25, y+0.25) etc. We'll implement a function.

- Score overlay: show current score and best score in a div positioned above canvas.

- Pause: pressing 'p' toggles pause; when paused, snake stops moving (direction unchanged). Overlay maybe shows "Paused".

- Restart: after game over, show overlay with score and "Press Space or Enter to restart". Also hide overlay when game starts.

- Demo AI: we need a simple AI that decides direction each frame. Could use BFS towards food: choose direction that reduces Manhattan distance to food, avoiding opposite direction. Simpler: if food is above, move up, etc. Ensure not reverse.

- Need to ensure that quick successive key presses are honored: we can store a queue of direction changes; when key down, we push direction if not opposite of current direction. Then in each frame, we apply the next queued direction (if any) and clear queue. This ensures that multiple presses are processed.

- Ensure that arrow keys and WASD are handled: map key codes: ArrowUp (38), ArrowDown (40), ArrowLeft (37), ArrowRight (39); KeyW (87), KeyA (65), KeyS (83), KeyD (68). Also prevent default scrolling.

- Ensure no external resources.

- Ensure canvas fits viewport: use CSS to set width: 80vw; max-width: 800px; height: auto; but need to maintain aspect ratio. Could set canvas width/height attributes to match CSS size. Use container with flex center.

Simplify: set canvas width=800, height=600; CSS: canvas { width: 80vw; max-width: 800px; height: auto; } This will scale down on smaller screens, but grid cells may become small; still okay.

But requirement: adapt gracefully down to 375px wide, no horizontal scrollbar. Using max-width ensures that.

Now implement.

Potential pitfalls: need to ensure that the game loop runs even when paused; we can skip updating position but still draw.

Implement game state machine: 'playing', 'paused', 'gameover'.

Initialize snake length 3 (head + 2 body). Position initial at center of grid: e.g., x = 10, y = 10 (cells). Direction initially right.

Food: place at random empty cell.

Score = 0, bestScore = 0.

Demo AI: we can implement a function getAI direction(snake, food) that returns direction vector. We'll use simple logic: if food.y < snake.head.y then direction up else if food.y > snake.head.y then down else if food.x < snake.head.x then left else right. But need to avoid opposite direction: if current direction is up and food is below, we should not reverse (i.e., cannot move down). So we need to ensure that the AI does not choose opposite direction. So we can compute desired direction based on food location, then if that direction is opposite of current direction, pick a perpendicular direction (e.g., if current direction is up and food is below, choose left or right). Simpler: choose direction that reduces distance but not opposite; if opposite, choose any direction that is not opposite (maybe random among allowed). For demo, we can just move towards food if possible else random.

Implement key handling: on keydown, if game state is 'playing' or 'paused' (maybe allow after start), we add direction to queue if not opposite of current direction.

When game over, we set state to 'gameover' and show overlay.

Restart: on space/enter when state is 'gameover', reset snake, score, bestScore (maybe keep best), place food, set state to 'playing'.

Pause: on 'p' toggle pause; if paused, we stop updating snake position but still draw.

Now code.

We'll write HTML with style: body { margin:0; display:flex; justify-content:center; align-items:center; height:100vh; background:#111; } canvas { background:#222; border:2px solid #555; }

Overlay: a div with id 'overlay' position absolute centered, color white, font-size 24px, text-align center; maybe also show score.

But we need to show score and best score continuously, maybe a separate div at top.

Simplify: create a div with id 'hud' positioned top left, style.

Now write JS.

We'll need requestAnimationFrame loop.

Let's outline variables:

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const cellSize = canvas.width / 20; // since grid 20x20

snake = []; // array of {x,y}
direction = {dx:0, dy:0};
queue = []; // direction changes pending
food = {x:0, y:0};
score = 0;
bestScore = 0;
paused = false;
gameState = 'playing'; // 'playing','paused','gameover'

Initialize function:

function init() {
  // set canvas size
  canvas.width = 800;
  canvas.height = 600;
  // init snake
  const startX = Math.floor(canvas.width / 2 / cellSize);
  const startY = Math.floor(canvas.height / 2 / cellSize);
  snake = [{x:startX, y:startY}, {x:startX-1, y:startY}, {x:startX-2, y:startY}];
  direction = {dx:1, dy:0};
  queue = [];
  // food
  placeFood();
  score = 0;
  bestScore = 0;
  gameState = 'playing';
  paused = false;
  // start loop
  requestAnimationFrame(gameLoop);
}

function placeFood() {
  let valid;
  do {
    food.x = Math.floor(Math.random() * 20);
    food.y = Math.floor(Math.random() * 20);
    valid = true;
    for (const seg of snake) {
      if (seg.x === food.x && seg.y === food.y) {
        valid = false;
        break;
      }
    }
  } while (!valid);
}

function getAI() {
  // simple AI: move towards food, avoid opposite direction
  const dx = food.x - snake[0].x;
  const dy = food.y - snake[0].y;
  let target = {dx:0, dy:0};
  if (Math.abs(dx) > Math.abs(dy)) {
    target.dx = dx>0?1:-1;
  } else {
    target.dy = dy>0?1:-1;
  }
  // Determine possible directions
  const dirs = [{dx:1,dy:0},{dx:-1,dy:0},{dx:0,dy:1},{dx:0,dy:-1}];
  // Find direction that is not opposite of current direction
  let chosen = null;
  for (const d of dirs) {
    if (d.dx !== -direction.dx || d.dy !== -direction.dy) {
      // not opposite
      if (d.dx===target.dx && d.dy===target.dy) {
        chosen = d;
        break;
      }
    }
  }
  // if none found (shouldn't happen), pick any non-opposite
  if (!chosen) {
    for (const d of dirs) {
      if (d.dx !== -direction.dx || d.dy !== -direction.dy) {
        chosen = d;
        break;
      }
    }
  }
  return chosen;
}

Key handling:

canvas.addEventListener('keydown', e => {
  if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
    if (direction.dy !== 0) return; // prevent reversing vertical while moving horizontally? Actually need to prevent opposite direction: if current direction is left/right, up/down is allowed; if current direction is up/down, cannot go opposite (down). So we check opposite.
    if (direction.dy === 0) queue.push({dx:0, dy:1});
  } else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') {
    if (direction.dy === 0) queue.push({dx:0, dy:-1});
  } else if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
    if (direction.dx !== 0) return;
    queue.push({dx:-1, dy:0});
  } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
    if (direction.dx === 0) queue.push({dx:1, dy:0});
  } else if (e.key === ' ') {
    // restart if gameover
    if (gameState === 'gameover') {
      init();
    }
  } else if (e.key === 'Enter') {
    if (gameState === 'gameover') {
      init();
    }
  } else if (e.key === 'p' || e.key === 'P') {
    paused = !paused;
  }
});

But need to ensure that queue processing only occurs when gameState is 'playing' or not paused? Actually we want AI to move even when paused? Probably not; but we can still process queue when not paused. If paused, we shouldn't change direction.

Simplify: only allow queue changes when !paused && (gameState === 'playing' || gameState === 'paused'??). But after game over, we need to allow restart. We'll handle restart separately.

Now gameLoop:

function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

function update() {
  if (paused || gameState !== 'playing') return; // if paused, no movement; if gameover, loop may have ended.

  // process queued direction changes
  if (queue.length > 0) {
    const dir = queue.shift();
    // ensure not opposite
    if (!(dir.dx === -direction.dx && dir.dy === -direction.dy)) {
      direction = dir;
    }
  }

  // move snake
  const head = {x: snake[0].x + direction.dx, y: snake[0].y + direction.dy};

  // wall collision
  if (head.x < 0 || head.x >= 20 || head.y < 0 || head.y >= 20) {
    endGame();
    return;
  }

  // self collision (excluding tail if not growing)
  const selfCollision = snake.some((seg, idx) => {
    // if we are growing (food eaten), tail not removed yet, so we need to exclude the tail cell that will be removed next frame.
    // Simpler: check all body cells except the last one (tail) because after moving, tail will be popped if not growing.
    if (idx === snake.length - 1) return false; // tail will move away
    return seg.x === head.x && seg.y === head.y;
  });

  if (selfCollision) {
    endGame();
    return;
  }

  // add new head
  snake.unshift(head);

  // check food
  if (head.x === food.x && head.y === food.y) {
    score += 10;
    if (score > bestScore) bestScore = score;
    // grow: do not pop tail
    placeFood();
    // particle burst
    createParticles(head.x, head.y);
  } else {
    // normal move: remove tail
    snake.pop();
  }
}

function endGame() {
  gameState = 'gameover';
  // show overlay later
}

Now draw function:

function draw() {
  // clear
  ctx.fillStyle = '#222';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // draw grid
  ctx.strokeStyle = '#444';
  ctx.lineWidth = 1;
  for (let i = 0; i <= 20; i++) {
    ctx.beginPath();
    ctx.moveTo(i * cellSize, 0);
    ctx.lineTo(i * cellSize, canvas.height);
    ctx.stroke();
  }
  for (let i = 0; i <= 20; i++) {
    ctx.beginPath();
    ctx.moveTo(0, i * cellSize);
    ctx.lineTo(canvas.width, i * cellSize);
    ctx.stroke();
  }

  // draw food
  ctx.fillStyle = '#ff0000';
  ctx.beginPath();
  ctx.arc((food.x + 0.5) * cellSize, (food.y + 0.5) * cellSize, cellSize / 4, 0, Math.PI * 2);
  ctx.fill();

  // draw particles
  ctx.fillStyle = '#0ff';
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
    ctx.fill();
    p.life -= 0.05;
    if (p.life <= 0) particles.splice(i, 1);
  }

  // draw snake
  for (let i = 0; i < snake.length; i++) {
    const seg = snake[i];
    const isHead = i === 0;
    const hue = isHead ? 120 : 100; // head green, body darker
    ctx.fillStyle = `hsl(${hue}, 80%, 50%)`;
    ctx.fillRect((seg.x + 0.1) * cellSize, (seg.y + 0.1) * cellSize, cellSize * 0.8, cellSize * 0.8);

    // draw eyes
    if (isHead) {
      const eyeSize = cellSize * 0.2;
      const offsetX = (seg.x + (direction.dx === 0 ? 0.5 : direction.dx === 1 ? 0.75 : direction.dx === -1 ? 0.25 : 0.5)) * cellSize;
      const offsetY = (seg.y + (direction.dy === 0 ? 0.5 : direction.dy === 1 ? 0.75 : direction.dy === -1 ? 0.25 : 0.5)) * cellSize;
      // Actually we need eyes positioned at front of head relative to direction.
      // Let's compute eye positions:
      let eye1, eye2;
      if (direction.dx === 1) { // right
        eye1 = {x: seg.x + 0.75, y: seg.y + 0.375};
        eye2 = {x: seg.x + 0.75, y: seg.y + 0.625};
      } else if (direction.dx === -1) { // left
        eye1 = {x: seg.x + 0.25, y: seg.y + 0.375};
        eye2 = {x: seg.x + 0.25, y: seg.y + 0.625};
      } else if (direction.dy === 1) { // down
        eye1 = {x: seg.x + 0.375, y: seg.y + 0.75};
        eye2 = {x: seg.x + 0.625, y: seg.y + 0.75};
      } else if (direction.dy === -1) { // up
        eye1 = {x: seg.x + 0.375, y: seg.y + 0.25};
        eye2 = {x: seg.x + 0.625, y: seg.y + 0.25};
      }
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc((eye1.x + 0.5) * cellSize, (eye1.y + 0.5) * cellSize, eyeSize, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc((eye2.x + 0.5) * cellSize, (eye2.y + 0.5) * cellSize, eyeSize, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  // HUD
  ctx.fillStyle = '#fff';
  ctx.font = '16px sans-serif';
  ctx.fillText(`Score: ${score}   Best: ${bestScore}`, 10, 20);
  if (paused) {
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'center';
    ctx.font = '30px sans-serif';
    ctx.fillText('Paused', canvas.width / 2, canvas.height / 2);
  }
  if (gameState === 'gameover') {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'center';
    ctx.font = '24px sans-serif';
    ctx.fillText('Game Over', canvas.width / 2, canvas.height / 2 - 20);
    ctx.font = '18px sans-serif';
    ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2);
    ctx.fillText('Press Space or Enter to restart', canvas.width / 2, canvas.height / 2 + 20);
  }
}

Particles array:

const particles = [];

function createParticles(x, y) {
  for (let i = 0; i < 8; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = 2 + Math.random() * 3;
    particles.push({
      x: (x + 0.5) * cellSize,
      y: (y + 0.5) * cellSize,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      radius: cellSize * 0.1,
      life: 1
    });
  }
}

Now overlay message before start: we need a message "Click or press a key to start". Since we have demo AI playing, we can start the game immediately, but show overlay. The overlay can be a div with id 'startOverlay' positioned center, style.

When page loads, we call init() which starts game loop and shows overlay. The overlay will be visible because we set its display to block. When user clicks or presses any key, we hide overlay and maybe set a flag to allow input (but input already allowed). However we need to ensure that before click, the game is still running (AI moving). That's fine.

Implementation: create overlay div:

<div id="startOverlay" class="overlay">Click or press a key to start</div>

CSS: .overlay {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  font-size: 24px;
  text-align: center;
  background: rgba(0,0,0,0.6);
  padding: 20px;
  border-radius: 8px;
  pointer-events: auto;
  z-index: 10;
}

But we need to ensure it's above canvas. Since canvas is in body with flex centering, we may need to set position relative to canvas. Simpler: make overlay a separate div placed at center of page using flex.

But we also need to ensure that the overlay appears on top of canvas. Since canvas is part of page flow, we can set overlay position absolute and higher z-index.

Simplify: create a div with class 'overlay' and style:

.overlay {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  font-size: 24px;
  background: rgba(0,0,0,0.7);
  padding: 20px;
  border-radius: 8px;
  z-index: 10;
  pointer-events: auto;
  text-align: center;
}

But we need to place it inside body, not relative to canvas. Since body is positioned with flex centering, we can set body {position: relative;} then overlay absolute.

Let's set body style: position: relative; margin:0; display:flex; justify-content:center; align-items:center; height:100vh; background:#111;

Then overlay will be centered.

Now need to hide overlay after click or key press. In key handler, if gameState is not 'gameover' maybe we hide overlay. Also in click handler (we can add click listener on canvas to start). But spec says "Click or press a key to start". So we can hide overlay on any key press or click on canvas.

Implementation: in key handler, if (e.key !== ' ' && e.key !== 'Enter' && gameState !== 'gameover') { document.getElementById('startOverlay').style.display = 'none'; } Also in click event on canvas, hide overlay.

But need to ensure that after hide, the game loop continues (already running). The overlay will disappear.

Potential conflict: when game over, overlay shows restart message; after restart, we need to hide overlay again. In init, after resetting, overlay should be hidden (maybe we set display none). We'll manage.

Simplify: In init, after setting gameState = 'playing', we also hide overlay (if visible). So overlay is only shown initially.

Implementation steps:

- At start, show overlay: document.getElementById('startOverlay').style.display = 'block';
- In key handler, if (e.key === ' ' || e.key === 'Enter' || e.key === 'ArrowUp' ...) { hide overlay if visible and gameState !== 'gameover' }.

But we need to allow space/enter to restart after game over; but before game over, we want overlay hidden after first key press.

Simplify: In key handler, if (document.getElementById('startOverlay').style.display !== 'none') { document.getElementById('startOverlay').style.display = 'none'; } This will hide overlay on any key press (including space/enter). That's fine because after game over we will show a different overlay (the game over overlay). So initial overlay disappears.

Now ensure that the canvas receives keyboard events after click: we need to ensure that after overlay hide, the canvas gets focus? Not necessary; key events are captured globally.

But requirement: "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." So we need to ensure that before clicking, the canvas may not receive key events? Actually the overlay is displayed; after clicking, the canvas should get focus. We can add a click listener on canvas that calls focus() and then hide overlay. But we also allow any key press to start; maybe we can just hide overlay on any key press, but ensure that canvas gets focus after click. Let's add a click listener on canvas that calls canvas.focus() and hides overlay.

Implementation:

canvas.addEventListener('click', () => {
  canvas.focus();
  const overlay = document.getElementById('startOverlay');
  if (overlay) overlay.style.display = 'none';
});

Now we need to ensure that the overlay is visible initially. In HTML, we include the overlay div after canvas.

Now we need to ensure that the game loop runs before user interaction. init() will start loop and show overlay. That's fine.

Now we need to ensure that the code runs without errors. Let's think about potential errors:

- Using canvas.width and height before setting attributes; we set them in init. That's fine.

- The particles array may cause errors if not defined; we define it globally.

- In draw, we reference particles; need to ensure it's defined.

- In createParticles, we use cellSize; fine.

- In drawing snake eyes, we compute positions; need to ensure that direction values are correct (dx, dy are 0,1,-1). In update we set direction from queue; direction is always one of those.

- In update, we check selfCollision: we exclude tail cell (snake.length-1). However if snake length is 1 (should not happen), then tail index = 0, we exclude it, but there is no other cell, so selfCollision will be false; okay.

- In endGame, we set gameState = 'gameover'; then draw will show overlay.

- In restart (init), we reset everything; need to hide overlay again (maybe we set display none). We'll hide overlay at start of init.

Potential issue: The overlay may be hidden but the game over overlay also uses same overlay element? We have two overlays: start overlay and game over overlay (drawn on canvas). The start overlay is a separate div; game over overlay is drawn on canvas, not a separate element. So after game over, we need to hide start overlay (if still visible) and show game over overlay on canvas. That's fine.

Now ensure that the best score persists across game overs; we keep bestScore variable, but not stored in localStorage (not allowed). That's okay; it's per session.

Now ensure that the page is responsive: we set canvas width 800, height 600; CSS makes canvas width: 80vw; max-width: 800px; So on small screens, canvas scales down, grid cells become smaller but still visible. The game adapts.

Now we need to ensure that the page looks right in 1280x800 viewport and down to 375px. With max-width 800px, the canvas will be 800px wide on large screens, which fits 1280 width. On 375px width, canvas will be 375px (since max-width 800, but width is 80vw which at 375px viewport is 300px? Actually 80vw of 375px is 300px, which is less than max-width, so canvas width will be 300px, height will be 225px (maintaining ratio). That's okay.

Now we need to ensure that there is no horizontal scrollbar: using max-width and not setting width larger than viewport.

Now final code structure:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Snake Demo</title>
<style>
  body {
    margin:0;
    padding:0;
    height:100vh;
    display:flex;
    justify-content:center;
    align-items:center;
    background:#111;
    position:relative; /* for overlay */
    overflow:hidden;
  }
  canvas {
    background:#222;
    border:2px solid #555;
    width:80vw;
    max-width:800px;
    height:auto;
  }
  .overlay {
    position:absolute;
    top:50%;
    left:50%;
    transform:translate(-50%,-50%);
    color:#fff;
    font-size:24px;
    background:rgba(0,0,0,0.6);
    padding:20px;
    border-radius:8px;
    text-align:center;
    z-index:10;
    pointer-events:auto;
  }
  #hud {
    position:absolute;
    top:10px;
    left:10px;
    color:#fff;
    font-family:sans-serif;
    font-size:16px;
    background:rgba(0,0,0,0.5);
    padding:5px 10px;
    border-radius:4px;
    pointer-events:none;
  }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="startOverlay" class="overlay">Click or press a key to start</div>
<div id="hud">Score: 0   Best: 0</div>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const cellSize = canvas.width / 20; // 20x20 grid

// Game variables
let snake = [];
let direction = {dx:0, dy:0};
const queue = [];
let food = {x:0, y:0};
let score = 0;
let bestScore = 0;
let paused = false;
let gameState = 'playing'; // 'playing','paused','gameover'

const particles = [];

// Initialize game
function init() {
  // Set canvas size (fixed for simplicity)
  canvas.width = 800;
  canvas.height = 600;
  // Reset game state
  snake = [];
  const startX = Math.floor(canvas.width / 2 / cellSize);
  const startY = Math.floor(canvas.height / 2 / cellSize);
  snake = [
    {x:startX, y:startY},
    {x:startX-1, y:startY},
    {x:startX-2, y:startY}
  ];
  direction = {dx:1, dy:0};
  queue.length = 0;
  score = 0;
  bestScore = 0;
  paused = false;
  gameState = 'playing';
  placeFood();
  // Hide start overlay
  const overlay = document.getElementById('startOverlay');
  if (overlay) overlay.style.display = 'none';
  requestAnimationFrame(gameLoop);
}

// Place food at random empty cell
function placeFood() {
  let valid;
  do {
    food.x = Math.floor(Math.random() * 20);
    food.y = Math.floor(Math.random() * 20);
    valid = true;
    for (const seg of snake) {
      if (seg.x === food.x && seg.y === food.y) {
        valid = false;
        break;
      }
    }
  } while (!valid);
}

// Simple AI direction
function getAI() {
  const dx = food.x - snake[0].x;
  const dy = food.y - snake[0].y;
  let target = {dx:0, dy:0};
  if (Math.abs(dx) > Math.abs(dy)) {
    target.dx = dx > 0 ? 1 : -1;
  } else {
    target.dy = dy > 0 ? 1 : -1;
  }
  const dirs = [{dx:1,dy:0},{dx:-1,dy:0},{dx:0,dy:1},{dx:0,dy:-1}];
  let chosen = null;
  for (const d of dirs) {
    if (d.dx !== -direction.dx || d.dy !== -direction.dy) {
      if (d.dx === target.dx && d.dy === target.dy) {
        chosen = d;
        break;
      }
    }
  }
  if (!chosen) {
    for (const d of dirs) {
      if (d.dx !== -direction.dx || d.dy !== -direction.dy) {
        chosen = d;
        break;
      }
    }
  }
  return chosen;
}

// Particle burst
function createParticles(x, y) {
  for (let i = 0; i < 8; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = 2 + Math.random() * 3;
    particles.push({
      x: (x + 0.5) * cellSize,
      y: (y + 0.5) * cellSize,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      radius: cellSize * 0.1,
      life: 1
    });
  }
}

// Key handling
canvas.addEventListener('keydown', e => {
  if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
    if (direction.dy === 0) queue.push({dx:0, dy:1});
  } else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') {
    if (direction.dy === 0) queue.push({dx:0, dy:-1});
  } else if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
    if (direction.dx === 0) queue.push({dx:-1, dy:0});
  } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
    if (direction.dx === 0) queue.push({dx:1, dy:0});
  } else if (e.key === ' ') {
    if (gameState === 'gameover') init();
  } else if (e.key === 'Enter') {
    if (gameState === 'gameover') init();
  } else if (e.key === 'p' || e.key === 'P') {
    paused = !paused;
  }
  // Hide start overlay on any key press (except restart keys)
  const overlay = document.getElementById('startOverlay');
  if (overlay && overlay.style.display !== 'none' && !(e.key === ' ' || e.key === 'Enter')) {
    overlay.style.display = 'none';
  }
});

canvas.addEventListener('click', () => {
  canvas.focus();
  const overlay = document.getElementById('startOverlay');
  if (overlay) overlay.style.display = 'none';
});

// Game loop
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

// Update snake position and handle collisions
function update() {
  if (paused || gameState !== 'playing') return;

  // Process queued direction changes
  if (queue.length > 0) {
    const dir = queue.shift();
    if (!(dir.dx === -direction.dx && dir.dy === -direction.dy)) {
      direction = dir;
    }
  }

  // Move head
  const head = {
    x: snake[0].x + direction.dx,
    y: snake[0].y + direction.dy
  };

  // Wall collision
  if (head.x < 0 || head.x >= 20 || head.y < 0 || head.y >= 20) {
    endGame();
    return;
  }

  // Self collision (excluding tail that will be removed)
  const selfCollides = snake.some((seg, idx) => {
    if (idx === snake.length - 1) return false; // tail will move away
    return seg.x === head.x && seg.y === head.y;
  });

  if (selfCollides) {
    endGame();
    return;
  }

  // Add new head
  snake.unshift(head);

  // Check food
  if (head.x === food.x && head.y === food.y) {
    score += 10;
    if (score > bestScore) bestScore = score;
    createParticles(head.x, head.y);
    placeFood();
  } else {
    // Normal move: remove tail
    snake.pop();
  }
}

// End game and show overlay
function endGame() {
  gameState = 'gameover';
}

// Draw everything
function draw() {
  // Clear
  ctx.fillStyle = '#222';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Grid
  ctx.strokeStyle = '#444';
  ctx.lineWidth = 1;
  for (let i = 0; i <= 20; i++) {
    ctx.beginPath();
    ctx.moveTo(i * cellSize, 0);
    ctx.lineTo(i * cellSize, canvas.height);
    ctx.stroke();
  }
  for (let i = 0; i <= 20; i++) {
    ctx.beginPath();
    ctx.moveTo(0, i * cellSize);
    ctx.lineTo(canvas.width, i * cellSize);
    ctx.stroke();
  }

  // Food
  ctx.fillStyle = '#ff0000';
  ctx.beginPath();
  ctx.arc((food.x + 0.5) * cellSize, (food.y + 0.5) * cellSize, cellSize / 4, 0, Math.PI * 2);
  ctx.fill();

  // Particles
  ctx.fillStyle = '#0ff';
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
    ctx.fill();
    p.life -= 0.05;
    if (p.life <= 0) particles.splice(i, 1);
  }

  // Snake
  for (let i = 0; i < snake.length; i++) {
    const seg = snake[i];
    const isHead = i === 0;
    const hue = isHead ? 120 : 100; // head green, body darker
    ctx.fillStyle = `hsl(${hue}, 80%, 50%)`;
    ctx.fillRect((seg.x + 0.1) * cellSize, (seg.y + 0.1) * cellSize, cellSize * 0.8, cellSize * 0.8);

    // Eyes
    if (isHead) {
      const eyeSize = cellSize * 0.2;
      let eye1, eye2;
      if (direction.dx === 1) { // right
        eye1 = {x: seg.x + 0.75, y: seg.y + 0.375};
        eye2 = {x: seg.x + 0.75, y: seg.y + 0.625};
      } else if (direction.dx === -1) { // left
        eye1 = {x: seg.x + 0.25, y: seg.y + 0.375};
        eye2 = {x: seg.x + 0.25, y: seg.y + 0.625};
      } else if (direction.dy === 1) { // down
        eye1 = {x: seg.x + 0.375, y: seg.y + 0.75};
        eye2 = {x: seg.x + 0.625, y: seg.y + 0.75};
      } else if (direction.dy === -1) { // up
        eye1 = {x: seg.x + 0.375, y: seg.y + 0.25};
        eye2 = {x: seg.x + 0.625, y: seg.y + 0.25};
      }
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc((eye1.x + 0.5) * cellSize, (eye1.y + 0.5) * cellSize, eyeSize, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc((eye2.x + 0.5) * cellSize, (eye2.y + 0.5) * cellSize, eyeSize, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  // HUD
  ctx.fillStyle = '#fff';
  ctx.font = '16px sans-serif';
  ctx.fillText(`Score: ${score}   Best: ${bestScore}`, 10, 20);

  if (paused) {
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'center';
    ctx.font = '30px sans-serif';
    ctx.fillText('Paused', canvas.width / 2, canvas.height / 2);
  }

  if (gameState === 'gameover') {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'center';
    ctx.font = '24px sans-serif';
    ctx.fillText('Game Over', canvas.width / 2, canvas.height / 2 - 20);
    ctx.font = '18px sans-serif';
    ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2);
    ctx.fillText('Press Space or Enter to restart', canvas.width / 2, canvas.height / 2 + 20);
  }
}

// Start the game
init();
</script>
</body>
</html>

Check for any missing semicolons, syntax errors.

Potential issue: In init, we set canvas.width = 800; canvas.height = 600; but earlier we set CSS width:80vw; max-width:800px; So setting width attribute to 800 will make canvas fill CSS width (since CSS width is 80vw which may be less). That's fine.

But we need to recompute cellSize after setting canvas size: cellSize = canvas.width / 20; we compute after init? In code we compute cellSize before init (outside). Actually we defined const cellSize = canvas.width / 20; before init, but canvas.width is 0 at that time (since not set). So cellSize will be 0 initially, causing errors later. We need to compute cellSize after canvas size is set. So move const cellSize = canvas.width / 20; inside init, after setting canvas size. Or compute after setting width.

Let's adjust: In init, after setting canvas.width = 800; canvas.height = 600; compute cellSize = canvas.width / 20; Also need to recompute cellSize for drawing (since cellSize used in draw). So modify init accordingly.

Also need to ensure that cellSize is used consistently; we used cellSize in many places (grid drawing, food position, particles). So compute after canvas size.

Let's edit init:

function init() {
  // Set canvas size (fixed for simplicity)
  canvas.width = 800;
  canvas.height = 600;
  // Recompute cell size
  const cellSizeLocal = canvas.width / 20; // local variable
  // But we need to use cellSize in other functions; we can make it a global constant.
}

Better: define cellSize as a global constant after canvas size is known. Since we cannot change CSS width (which scales), we keep canvas size fixed 800x600; then cellSize = 800/20 = 40. That's fine.

So modify: after setting canvas size, set cellSize = canvas.width / 20; Also need to update any references to cellSize (like in draw). Since we use cellSize variable globally, we need to set it inside init after canvas size.

Let's adjust code:

In global scope, we can declare let cellSize; then in init after canvas size, set cellSize = canvas.width / 20;

But we also need cellSize for drawing; we can recompute in draw each frame? Not ideal. Simpler: compute cellSize once after canvas size, and keep it constant.

Thus modify init:

function init() {
  canvas.width = 800;
  canvas.height = 600;
  cellSize = canvas.width / 20; // global cellSize
  // rest...
}

But we need to ensure cellSize is accessible in draw. Since draw uses cellSize variable, it's fine.

Now adjust code accordingly.

Also need to adjust placeFood function to use cellSize (it uses cellSize for scaling). That's fine.

Now check that we didn't use cellSize before it's defined. In placeFood, we compute food.x and food.y as grid indices, not using cellSize. So fine.

In createParticles, we use cellSize to compute positions; fine.

Now ensure that the code uses cellSize after init.

Now also ensure that we didn't reference cellSize in the initial code before init (like in the grid drawing loop). In draw, we use cellSize for grid lines; that's after init, so fine.

Now we need to adjust the code accordingly.

Let's rewrite the relevant parts:

- Remove the earlier const cellSize definition before init.

- Add let cellSize; at top.

- In init, after setting canvas size, set cellSize = canvas.width / 20;

Now check other uses: In draw, we have loops that use cellSize for grid lines: for (let i = 0; i <= 20; i++) { ctx.moveTo(i * cellSize, 0); ... } That's fine.

Also for food position: (food.x + 0.5) * cellSize; fine.

Now ensure that the start overlay is hidden after init; we already do that.

Now ensure that the overlay is displayed initially: we need to set its display to block in HTML or via JS. In HTML we have <div id="startOverlay" class="overlay">Click or press a key to start</div>. By default, it may be hidden (display:none). We need to show it initially. So we need CSS to set display:block? Actually we can set style.display = 'block' in JS after page load, but easier: set CSS to display:block. Let's modify CSS: .overlay { ...; display: none; } Then in JS after init, we set overlay.style.display = 'block'; Wait we want overlay visible initially, then hidden after first key press. So we need to set display block initially. Let's set CSS: .overlay { display: block; } But then we need to hide it later. So we can set display:none initially in CSS, then in init we set display='block'.

Simplify: In CSS, set .overlay { display: none; } Then in init, after setting up, we set overlay.style.display = 'block'; Then in key handler we hide it.

Let's adjust CSS: .overlay { position:absolute; ...; display: none; } Then in init: const overlay = document.getElementById('startOverlay'); overlay.style.display = 'block';

Now ensure that after init we also hide overlay if gameState is not gameover (i.e., playing). In init we set overlay.style.display = 'block'; then later in key handler we hide it.

Now check that we didn't have any other errors.

Potential issue: In draw, we reference cellSize which is now defined after init; but draw may be called before init? No, gameLoop starts after init, so first draw occurs after init, so cellSize is defined.

Now check that we didn't use cellSize in the initial code before init (like in the CSS). That's fine.

Now ensure that the code runs without errors: we need to test mentally.

Potential bug: In update, we check selfCollision using snake.some; but we exclude tail (idx === snake.length - 1). However if snake length is 1 (should not happen), then idx === 0, which equals snake.length - 1 (0), so we would exclude tail and not detect self collision. But snake length is at least 3 initially, and grows, so fine.

Potential bug: In createParticles, we push particles at head position; but after moving head, the head cell may be same as food cell; we create particles there, fine.

Potential bug: In draw, we draw grid lines after clearing; but we also draw snake and food; order fine.

Potential bug: In HUD, we draw score always; fine.

Potential bug: In pause overlay, we fill rectangle with rgba(0,0,0,0.5) covering whole canvas, then draw "Paused". That's fine.

Potential bug: In game over overlay, we fill rectangle with rgba(0,0,0,0.7) covering whole canvas, then draw text. Fine.

Potential bug: The overlay div may block canvas click; but we add click listener on canvas that focuses and hides overlay. That should work.

Now ensure that the page meets requirement: "Something meaningful must be visible on first paint". At first paint, we have the start overlay and the canvas with grid and maybe snake (initial positions) and food. The start overlay covers part of canvas, but there is still visible grid and snake, so meaningful.

Also "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". Our init starts the game loop immediately, with AI controlling snake (the AI direction is computed each frame). However, in our code, the AI direction is computed in getAI, but we never actually use it because we rely on queue for direction changes from keys. The AI is not integrated; we need to have the AI move the snake automatically. Currently, the snake only moves when the user presses keys (or we process queue). The AI direction is computed but never applied. So we need to modify the update function to also apply AI direction when no key queue is present (i.e., when not paused and gameState playing). But the requirement is that the demo mode is self-playing; meaning the snake should move automatically, not wait for user input. So we need to have the AI decide direction each frame and set direction accordingly, unless user presses a key (which should override). The queue mechanism currently only processes key presses; we need to also have AI direction set each frame.

Simplify: In update, before processing queue, we can compute AI direction and push it to queue if no user input? But we need to ensure that user key presses override AI direction. The queue currently holds pending direction changes; we can also push AI direction each frame if there is no pending user direction and the game is playing.

Simplify: In update, after checking paused, we can compute AI direction and set direction = AI direction (unless user has pressed a key that changed direction). But we need to avoid reversing. So we can compute AI direction and then if it's not opposite to current direction, set direction = AI direction. However, if user pressed a key, we have queued direction; we should respect that.

Simplify further: We'll modify the update function to first check if there is a queued direction (user input). If yes, apply it (as before). If not, compute AI direction and set direction accordingly (ensuring not opposite). That will make the snake move automatically.

Thus modify update:

function update() {
  if (paused || gameState !== 'playing') return;

  // Process user queued direction changes
  if (queue.length > 0) {
    const dir = queue.shift();
    if (!(dir.dx === -direction.dx && dir.dy === -direction.dy)) {
      direction = dir;
    }
  } else {
    // No user input, use AI
    const aiDir = getAI();
    // Ensure not opposite
    if (!(aiDir.dx === -direction.dx && aiDir.dy === -direction.dy)) {
      direction = aiDir;
    }
  }

  // Move head...
}

Now the snake will move automatically using AI when no key presses.

But we need to ensure that the AI direction is not opposite; we already check.

Now ensure that the AI direction is computed each frame; getAI uses food position and snake head; that's fine.

Now we need to ensure that the snake moves each frame; currently we only move when direction changes (which occurs each frame via AI or user). So the snake will move each frame.

Now we need to ensure that the initial snake movement is visible; after init, the snake will move automatically (AI direction initially right). So it will move right, which is fine.

Now we need to ensure that the overlay is still displayed initially, and that the AI movement is visible.

Now we need to adjust the code accordingly.

Let's edit the update function.

Also need to ensure that the AI direction is computed correctly: getAI returns a direction object; we need to set direction = aiDir; but we must ensure that direction is updated even if queue is empty.

Implementation:

function update() {
  if (paused || gameState !== 'playing') return;

  // Handle queued user directions
  if (queue.length > 0) {
    const dir = queue.shift();
    if (!(dir.dx === -direction.dx && dir.dy === -direction.dy)) {
      direction = dir;
    }
  } else {
    // No user input, let AI drive
    const aiDir = getAI();
    // Avoid reversing
    if (!(aiDir.dx === -direction.dx && aiDir.dy === -direction.dy)) {
      direction = aiDir;
    }
  }

  // Move head...
}

Now we need to ensure that the AI direction is not None; getAI always returns a direction.

Now the snake will move automatically.

Now we need to ensure that the start overlay is hidden after first key press; but the AI will be moving before any key press, which is fine.

Now we need to ensure that the overlay is displayed initially; we set overlay.style.display = 'block' in init.

Now we need to adjust the code accordingly.

Let's rewrite the relevant parts in the final code.

Also ensure that the overlay is hidden when game over? The game over overlay is drawn on canvas; the start overlay is separate.

Now we need to adjust the code for cellSize: define let cellSize; then in init after canvas size set cellSize = canvas.width / 20;

Now adjust draw function to use cellSize (already does). Ensure that we use cellSize for grid lines etc.

Now we need to adjust the code for the start overlay: In CSS set .overlay { display: none; } Then in init: const overlay = document.getElementById('startOverlay'); overlay.style.display = 'block';

Now ensure that after init we also hide overlay if gameState is not gameover? Actually we want overlay visible until user interacts. So we set display block. Then in key handler we hide it (if not restart). That's fine.

Now ensure that after game over, the start overlay remains hidden (since we hide it on any key press). But after game over, we may want to show a different overlay (the game over overlay on canvas). That's fine.

Now we need to ensure that the AI is indeed moving; test mentally: At init, snake head direction is right (dx=1, dy=0). getAI will compute direction towards food. If food is to the right, AI will also go right (same direction). If food is elsewhere, AI will adjust. So snake will move.

Now we need to ensure that quick successive key presses are honored: The queue processes each key press; if user presses right then left quickly, the queue will have two directions; but we prevent opposite direction (if left is opposite of right, we ignore it). So snake will continue moving right (since left is opposite). That's correct.

Now ensure that the snake cannot reverse onto itself: The self-collision check excludes tail; that's correct.

Now ensure that the particle burst appears when eating; we call createParticles when food eaten; fine.

Now ensure that the score and best score are displayed; fine.

Now ensure that the page is responsive: we set canvas width 800, height 600; CSS width:80vw; max-width:800px; So on small screens, canvas scales down; grid cells become smaller but still visible.

Now ensure no external resources; all inline.

Now ensure that there are no console errors: we need to verify that all variables are defined before use.

Potential issue: In draw, we reference particles array; it's defined globally.

Potential issue: In update, we reference snake, direction, food, etc. All defined.

Potential issue: In getAI, we use snake[0] which is head; fine.

Potential issue: In createParticles, we use cellSize; fine.

Now final code.

Let's produce final HTML with modifications.