Models 44

MiMo-V2.6-Pro

by Xiaomireasoning defaultClean
Open in new window

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

How it went

Total time
1 min 50 s
Cost
$0.012
Output tokens
15,136
Spent thinking
7,051 tokens
Speed
138.4 tok/s
Console errors
0
First token after
631 ms
First visible text after
1 min 6 s
Prompt tokens
497
Code size
24.6 KB
Finish reason
stop
Served by
DeepInfra
Model id
xiaomi/mimo-v2.6-pro
Reasoning sent
model default
Max output
131,072 tokens
Generated
Nov 13, 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 · 1011 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Snake</title>
<style>
  :root {
    --bg: #070b14;
    --panel: #0d1526;
    --line: rgba(148, 197, 255, 0.13);
    --mint: #34d399;
    --cyan: #22d3ee;
    --rose: #fb7185;
    --amber: #fbbf24;
    --text: #eaf2ff;
    --muted: #8aa4c0;
  }

  * { box-sizing: border-box; }

  html, body {
    margin: 0;
    padding: 0;
    width: 100%;
    min-height: 100%;
    overflow-x: hidden;
  }

  body {
    background:
      radial-gradient(1100px 680px at 50% -12%, #16294180 0%, #070b14 62%),
      linear-gradient(180deg, #070b14 0%, #05080f 100%);
    color: var(--text);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
      Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    display: flex;
    justify-content: center;
    align-items: flex-start;
  }

  .app {
    width: 100%;
    max-width: 1180px;
    min-height: 100vh;
    padding: 20px clamp(12px, 3vw, 34px) 16px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(12px, 2.2vh, 20px);
  }

  /* ---------- header ---------- */

  .topbar {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    flex-wrap: wrap;
  }

  .brand {
    display: flex;
    align-items: center;
    gap: 13px;
    min-width: 0;
  }

  .logo {
    position: relative;
    width: 44px;
    height: 44px;
    flex: 0 0 auto;
    border-radius: 14px;
    background: linear-gradient(150deg, #22d3ee 0%, #34d399 52%, #0ea5e9 100%);
    box-shadow: 0 12px 30px rgba(34, 211, 238, 0.28);
    overflow: hidden;
  }

  .logo::before,
  .logo::after {
    content: "";
    position: absolute;
    border-radius: 50%;
    background: #06121f;
  }

  .logo::before {
    width: 9px;
    height: 9px;
    left: 10px;
    top: 11px;
    box-shadow: 12px 12px 0 #06121f;
  }

  .logo::after {
    width: 5px;
    height: 5px;
    right: 10px;
    top: 12px;
    background: #04222c;
    opacity: 0.65;
  }

  .brand h1 {
    margin: 0;
    font-size: clamp(19px, 2.5vw, 27px);
    font-weight: 800;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  .brand p {
    margin: 5px 0 0;
    color: var(--muted);
    font-size: 11px;
    letter-spacing: 0.19em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  .stats {
    display: flex;
    gap: 10px;
  }

  .stat {
    min-width: 94px;
    padding: 9px 16px 11px;
    border: 1px solid var(--line);
    border-radius: 15px;
    background:
      linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.015));
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.22);
  }

  .stat span {
    display: block;
    font-size: 9.5px;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: var(--muted);
  }

  .stat strong {
    display: block;
    margin-top: 3px;
    font-size: 25px;
    line-height: 1.15;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
  }

  .stat.score strong { color: var(--mint); }
  .stat.best strong { color: var(--amber); }
  .stat.length strong { color: var(--cyan); }

  /* ---------- board ---------- */

  .stage {
    width: 100%;
    display: flex;
    justify-content: center;
  }

  .board-wrap {
    position: relative;
    width: min(620px, calc(100vw - 26px), calc(100vh - 236px));
    aspect-ratio: 1 / 1;
    border-radius: 22px;
    overflow: hidden;
    cursor: pointer;
    background: #0a1322;
    box-shadow:
      0 34px 80px rgba(0, 0, 0, 0.55),
      0 0 0 1px rgba(126, 200, 255, 0.16),
      0 0 70px rgba(34, 211, 238, 0.11);
    -webkit-tap-highlight-color: transparent;
  }

  canvas {
    display: block;
    width: 100%;
    height: 100%;
  }

  /* ---------- overlay ---------- */

  .overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 9px;
    padding: 24px;
    text-align: center;
    background:
      radial-gradient(120% 92% at 50% 42%, rgba(7, 11, 20, 0.5) 0%, rgba(7, 11, 20, 0.88) 100%);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    transition: opacity 0.24s ease;
    pointer-events: none;
  }

  .overlay.hidden {
    opacity: 0;
  }

  .ov-kicker {
    font-size: 10px;
    letter-spacing: 0.32em;
    text-transform: uppercase;
    color: var(--cyan);
    font-weight: 700;
  }

  .ov-title {
    margin: 0;
    font-size: clamp(21px, 3.6vw, 34px);
    font-weight: 800;
    letter-spacing: 0.015em;
    line-height: 1.16;
    text-wrap: balance;
  }

  .ov-text {
    margin: 2px 0 0;
    max-width: 330px;
    color: var(--muted);
    font-size: 13.5px;
    line-height: 1.65;
  }

  .ov-text b {
    color: var(--text);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }

  .ov-btn {
    margin-top: 11px;
    padding: 11px 21px;
    border-radius: 999px;
    background: linear-gradient(135deg, #22d3ee 0%, #34d399 100%);
    color: #04202b;
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 0.19em;
    text-transform: uppercase;
    box-shadow: 0 14px 34px rgba(34, 211, 238, 0.28);
  }

  /* ---------- footer ---------- */

  .hints {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
  }

  .hint {
    padding: 7px 13px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.028);
    color: var(--muted);
    font-size: 10.5px;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  .hint b {
    color: var(--text);
    font-weight: 700;
    letter-spacing: 0.12em;
  }

  @media (max-width: 620px) {
    .app { padding-top: 16px; gap: 13px; }

    .topbar {
      flex-direction: column;
      align-items: stretch;
      gap: 13px;
    }

    .brand { padding-left: 2px; }

    .stats {
      width: 100%;
      gap: 8px;
    }

    .stat {
      flex: 1 1 0;
      min-width: 0;
      padding: 8px 11px 10px;
    }

    .stat strong { font-size: 21px; }
    .stat span { font-size: 8.5px; letter-spacing: 0.18em; }

    .board-wrap {
      width: min(calc(100vw - 22px), calc(100vh - 268px));
      border-radius: 18px;
    }

    .hint {
      font-size: 9px;
      letter-spacing: 0.11em;
      padding: 6px 10px;
    }
  }
</style>
</head>
<body>

<div class="app">

  <header class="topbar">
    <div class="brand">
      <div class="logo" aria-hidden="true"></div>
      <div>
        <h1>Neon Snake</h1>
        <p>Arcade grid runner</p>
      </div>
    </div>

    <div class="stats">
      <div class="stat score">
        <span>Score</span>
        <strong id="score">0</strong>
      </div>
      <div class="stat best">
        <span>Best</span>
        <strong id="best">0</strong>
      </div>
      <div class="stat length">
        <span>Length</span>
        <strong id="length">5</strong>
      </div>
    </div>
  </header>

  <main class="stage">
    <div class="board-wrap" id="boardWrap">
      <canvas id="game"></canvas>

      <div class="overlay" id="overlay">
        <div class="ov-kicker" id="ovKicker">Demo mode · AI playing</div>
        <h2 class="ov-title" id="ovTitle">Click or press a key to start</h2>
        <p class="ov-text" id="ovText">Arrow keys or WASD to steer · <b>P</b> to pause</p>
        <div class="ov-btn" id="ovBtn">Start game</div>
      </div>
    </div>
  </main>

  <footer class="hints">
    <div class="hint"><b>↑ ↓ ← →</b> &nbsp;Move</div>
    <div class="hint"><b>W A S D</b> &nbsp;Move</div>
    <div class="hint"><b>P</b> &nbsp;Pause</div>
    <div class="hint"><b>Space / Enter</b> &nbsp;Restart</div>
    <div class="hint">Food = <b>+10</b></div>
  </footer>

</div>

<script>
(() => {
  "use strict";

  /* ============================================================
     Setup
     ============================================================ */

  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const boardWrap = document.getElementById("boardWrap");

  const scoreEl = document.getElementById("score");
  const bestEl = document.getElementById("best");
  const lengthEl = document.getElementById("length");

  const overlay = document.getElementById("overlay");
  const ovKicker = document.getElementById("ovKicker");
  const ovTitle = document.getElementById("ovTitle");
  const ovText = document.getElementById("ovText");
  const ovBtn = document.getElementById("ovBtn");

  const COLS = 24;
  const ROWS = 24;
  const STEP_MS = 112;          // one grid move per step
  const MAX_QUEUE = 3;

  const DIRS = {
    up:    { x: 0, y: -1, name: "up" },
    right: { x: 1, y: 0,  name: "right" },
    down:  { x: 0, y: 1,  name: "down" },
    left:  { x: -1, y: 0, name: "left" }
  };

  const KEY_DIRS = {
    arrowup: DIRS.up, w: DIRS.up,
    arrowright: DIRS.right, d: DIRS.right,
    arrowdown: DIRS.down, s: DIRS.down,
    arrowleft: DIRS.left, a: DIRS.left
  };

  let size = 0;      // css pixel size of the square canvas
  let cell = 0;      // css pixel size of one grid cell
  let dpr = 1;

  let state = "demo";            // demo | playing | paused | gameover
  let snake = [];
  let prevRender = [];
  let direction = DIRS.right;
  let queue = [];
  let food = { x: 16, y: 12 };
  let particles = [];

  let score = 0;
  let best = 0;

  let accumulator = 0;
  let lastTime = performance.now();

  /* ============================================================
     Utilities
     ============================================================ */

  const keyOf = (x, y) => x + "," + y;
  const opposite = (a, b) => a.x === -b.x && a.y === -b.y;
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  const lerp = (a, b, t) => a + (b - a) * t;

  function resize() {
    const rect = boardWrap.getBoundingClientRect();
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    size = Math.max(1, rect.width);
    canvas.width = Math.round(size * dpr);
    canvas.height = Math.round(size * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    cell = size / COLS;
  }

  function updateHUD() {
    scoreEl.textContent = String(score);
    bestEl.textContent = String(best);
    lengthEl.textContent = String(snake.length);
  }

  function updateOverlay() {
    if (state === "playing") {
      overlay.classList.add("hidden");
      return;
    }

    overlay.classList.remove("hidden");

    if (state === "demo") {
      ovKicker.textContent = "Demo mode · AI playing";
      ovTitle.textContent = "Click or press a key to start";
      ovText.innerHTML = "Arrow keys or WASD to steer · <b>P</b> to pause";
      ovBtn.textContent = "Start game";
    } else if (state === "paused") {
      ovKicker.textContent = "Paused";
      ovTitle.textContent = "Take a breath";
      ovText.innerHTML = "Press <b>P</b> or <b>Space</b> to keep going";
      ovBtn.textContent = "Resume";
    } else if (state === "gameover") {
      ovKicker.textContent = "Game over";
      ovTitle.textContent = score >= best && score > 0 ? "New session best!" : "Run ended";
      ovText.innerHTML =
        "Score <b>" + score + "</b> · Best <b>" + best + "</b> · Length <b>" + snake.length + "</b>";
      ovBtn.textContent = "Play again";
    }
  }

  /* ============================================================
     Game state
     ============================================================ */

  function spawnFood() {
    const occupied = new Set(snake.map(s => keyOf(s.x, s.y)));
    let x, y, guard = 0;
    do {
      x = Math.floor(Math.random() * COLS);
      y = Math.floor(Math.random() * ROWS);
      guard++;
    } while (occupied.has(keyOf(x, y)) && guard < 2000);
    food = { x, y };
  }

  function resetSnake() {
    const cy = Math.floor(ROWS / 2);
    const cx = Math.floor(COLS / 2) - 2;
    snake = [];
    for (let i = 0; i < 5; i++) {
      snake.push({ x: cx - i, y: cy });
    }
    direction = DIRS.right;
    queue = [];
    prevRender = snake.map(s => ({ x: s.x, y: s.y }));
  }

  function startGame() {
    score = 0;
    particles = [];
    resetSnake();
    spawnFood();
    accumulator = 0;
    state = "playing";
    updateHUD();
    updateOverlay();
  }

  function startDemo() {
    score = 0;
    particles = [];
    resetSnake();
    spawnFood();
    accumulator = 0;
    state = "demo";
    updateHUD();
    updateOverlay();
  }

  function endGame() {
    if (score > best) best = score;
    state = "gameover";
    updateHUD();
    updateOverlay();
  }

  /* ============================================================
     Input
     ============================================================ */

  function queueDirection(dir) {
    const last = queue.length ? queue[queue.length - 1] : direction;
    if (opposite(dir, last)) return;               // never reverse into yourself
    if (dir.x === last.x && dir.y === last.y) return;
    if (queue.length < MAX_QUEUE) queue.push(dir); // honour quick successive presses
  }

  window.addEventListener("keydown", (e) => {
    const k = e.key.toLowerCase();
    const isDir = Object.prototype.hasOwnProperty.call(KEY_DIRS, k);

    if (isDir || k === " " || k === "enter" || k === "p") {
      e.preventDefault();
    }

    // Pause toggle only while a real run is live
    if (k === "p" && (state === "playing" || state === "paused")) {
      state = state === "playing" ? "paused" : "playing";
      accumulator = 0;
      updateOverlay();
      return;
    }

    if (state === "demo") {
      startGame();
      if (isDir) queueDirection(KEY_DIRS[k]);
      return;
    }

    if (state === "gameover") {
      if (k === " " || k === "enter") startGame();
      return;
    }

    if (state === "paused") {
      if (k === " " || k === "enter") {
        state = "playing";
        accumulator = 0;
        updateOverlay();
      }
      return;
    }

    if (isDir) queueDirection(KEY_DIRS[k]);
  }, { passive: false });

  boardWrap.addEventListener("click", () => {
    if (state === "demo" || state === "gameover") {
      startGame();
    } else if (state === "paused") {
      state = "playing";
      accumulator = 0;
      updateOverlay();
    }
  });

  window.addEventListener("resize", resize);

  /* ============================================================
     Demo AI — greedy move choice with a flood-fill safety check
     ============================================================ */

  function floodCount(sx, sy, blocked) {
    const seen = new Set([keyOf(sx, sy)]);
    const stack = [[sx, sy]];
    let count = 0;

    while (stack.length && count < 260) {
      const [x, y] = stack.pop();
      count++;
      for (const d of [DIRS.up, DIRS.right, DIRS.down, DIRS.left]) {
        const nx = x + d.x;
        const ny = y + d.y;
        if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
        const k = keyOf(nx, ny);
        if (seen.has(k) || blocked.has(k)) continue;
        seen.add(k);
        stack.push([nx, ny]);
      }
    }
    return count;
  }

  function chooseDemoDirection() {
    const head = snake[0];
    const tailFree = new Set(
      snake.slice(0, Math.max(0, snake.length - 1)).map(s => keyOf(s.x, s.y))
    );

    let bestDir = direction;
    let bestScore = -Infinity;

    for (const d of [DIRS.up, DIRS.right, DIRS.down, DIRS.left]) {
      if (opposite(d, direction)) continue;

      const nx = head.x + d.x;
      const ny = head.y + d.y;
      if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;

      const willEat = nx === food.x && ny === food.y;
      const bodyBlocked = willEat
        ? new Set(snake.map(s => keyOf(s.x, s.y)))
        : tailFree;

      if (bodyBlocked.has(keyOf(nx, ny))) continue;

      const space = floodCount(nx, ny, tailFree);
      const dist = Math.abs(nx - food.x) + Math.abs(ny - food.y);

      let s = space * 1.6 - dist * 2.4;
      if (d === direction) s += 0.9;
      if (willEat) s += 14;

      if (s > bestScore) {
        bestScore = s;
        bestDir = d;
      }
    }
    return bestDir;
  }

  /* ============================================================
     Simulation step
     ============================================================ */

  function stepGame() {
    if (state === "demo") {
      queue.length = 0;
      direction = chooseDemoDirection();
    } else if (queue.length) {
      const next = queue.shift();
      if (!opposite(next, direction)) direction = next;
    }

    const head = snake[0];
    const nx = head.x + direction.x;
    const ny = head.y + direction.y;

    const willEat = nx === food.x && ny === food.y;

    // Walls
    if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) {
      if (state === "demo") {
        startDemo();
        return;
      }
      endGame();
      return;
    }

    // Body (the tail cell vacates this step unless we are growing)
    const bodyCheck = willEat ? snake : snake.slice(0, Math.max(0, snake.length - 1));
    for (let i = 0; i < bodyCheck.length; i++) {
      if (bodyCheck[i].x === nx && bodyCheck[i].y === ny) {
        if (state === "demo") {
          startDemo();
          return;
        }
        endGame();
        return;
      }
    }

    // Keep a render source of the same length for smooth interpolation
    prevRender = snake.map(s => ({ x: s.x, y: s.y }));

    snake.unshift({ x: nx, y: ny });

    if (willEat) {
      score += 10;
      if (score > best) best = score;
      spawnFood();
      burst(nx, ny);
    } else {
      snake.pop();
    }

    // Pad the interpolation source for a freshly grown tail
    if (prevRender.length < snake.length) {
      prevRender.push({ x: snake[snake.length - 1].x, y: snake[snake.length - 1].y });
    }

    updateHUD();
  }

  /* ============================================================
     Particles
     ============================================================ */

  const PARTICLE_COLORS = ["#fb7185", "#fbbf24", "#34d399", "#22d3ee", "#f472b6"];

  function burst(gx, gy) {
    const cx = gx + 0.5;
    const cy = gy + 0.5;

    for (let i = 0; i < 22; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = 1.8 + Math.random() * 5.2;
      particles.push({
        x: cx,
        y: cy,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed,
        life: 0.42 + Math.random() * 0.5,
        maxLife: 0.42 + Math.random() * 0.5,
        size: 0.07 + Math.random() * 0.11,
        color: PARTICLE_COLORS[(Math.random() * PARTICLE_COLORS.length) | 0]
      });
    }
  }

  function updateParticles(dt) {
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life -= dt;
      if (p.life <= 0) {
        particles.splice(i, 1);
        continue;
      }
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vx *= 0.965;
      p.vy = p.vy * 0.965 + 3.4 * dt;
    }
  }

  /* ============================================================
     Rendering
     ============================================================ */

  function drawBoard() {
    // Base
    ctx.fillStyle = "#0a1322";
    ctx.fillRect(0, 0, size, size);

    // Subtle checkerboard
    for (let y = 0; y < ROWS; y++) {
      for (let x = 0; x < COLS; x++) {
        if ((x + y) % 2 === 0) {
          ctx.fillStyle = "rgba(120, 190, 255, 0.028)";
          ctx.fillRect(x * cell, y * cell, cell, cell);
        }
      }
    }

    // Grid lines
    ctx.lineWidth = 1;
    ctx.strokeStyle = "rgba(126, 190, 255, 0.07)";
    ctx.beginPath();
    for (let i = 1; i < COLS; i++) {
      const p = Math.round(i * cell) + 0.5;
      ctx.moveTo(p, 0);
      ctx.lineTo(p, size);
    }
    for (let i = 1; i < ROWS; i++) {
      const p = Math.round(i * cell) + 0.5;
      ctx.moveTo(0, p);
      ctx.lineTo(size, p);
    }
    ctx.stroke();
  }

  function drawFood(now) {
    const pulse = 1 + Math.sin(now / 240) * 0.07;
    const cx = (food.x + 0.5) * cell;
    const cy = (food.y + 0.5) * cell;
    const r = cell * 0.33 * pulse;

    ctx.save();
    ctx.shadowColor = "rgba(251, 113, 133, 0.75)";
    ctx.shadowBlur = cell * 0.95;

    const g = ctx.createRadialGradient(
      cx - r * 0.35, cy - r * 0.4, r * 0.12,
      cx, cy, r * 1.25
    );
    g.addColorStop(0, "#ffd2d9");
    g.addColorStop(0.35, "#fb7185");
    g.addColorStop(1, "#e11d48");

    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.arc(cx, cy, r, 0, Math.PI * 2);
    ctx.fill();

    // Leaf / stem accent
    ctx.shadowBlur = 0;
    ctx.strokeStyle = "rgba(52, 211, 153, 0.95)";
    ctx.lineWidth = cell * 0.11;
    ctx.lineCap = "round";
    ctx.beginPath();
    ctx.moveTo(cx + r * 0.1, cy - r * 0.85);
    ctx.quadraticCurveTo(cx + r * 0.55, cy - r * 1.35, cx + r * 1.05, cy - r * 1.05);
    ctx.stroke();

    ctx.restore();
  }

  function drawSnake(t) {
    const pts = [];
    for (let i = 0; i < snake.length; i++) {
      const from = prevRender[i] || prevRender[prevRender.length - 1] || snake[i];
      const to = snake[i];
      pts.push({
        x: (lerp(from.x, to.x, t) + 0.5) * cell,
        y: (lerp(from.y, to.y, t) + 0.5) * cell
      });
    }
    if (!pts.length) return;

    const grad = ctx.createLinearGradient(0, 0, size, size);
    grad.addColorStop(0, "#22d3ee");
    grad.addColorStop(0.45, "#34d399");
    grad.addColorStop(1, "#14b8a6");

    // Soft glow under the body
    ctx.save();
    ctx.lineJoin = "round";
    ctx.lineCap = "round";

    ctx.strokeStyle = "rgba(34, 211, 238, 0.20)";
    ctx.lineWidth = cell * 0.95;
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
    ctx.stroke();

    // Main body
    ctx.strokeStyle = grad;
    ctx.lineWidth = cell * 0.62;
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
    ctx.stroke();

    // Inner highlight
    ctx.strokeStyle = "rgba(255, 255, 255, 0.13)";
    ctx.lineWidth = cell * 0.2;
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y - cell * 0.1);
    for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y - cell * 0.1);
    ctx.stroke();

    // Tail cap
    const tail = pts[pts.length - 1];
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(tail.x, tail.y, cell * 0.31, 0, Math.PI * 2);
    ctx.fill();

    // ---- Head ----
    const head = pts[0];
    const d = direction;
    const perp = { x: -d.y, y: d.x };

    ctx.shadowColor = "rgba(52, 211, 153, 0.55)";
    ctx.shadowBlur = cell * 0.65;

    const headGrad = ctx.createRadialGradient(
      head.x - cell * 0.14, head.y - cell * 0.16, cell * 0.05,
      head.x, head.y, cell * 0.58
    );
    headGrad.addColorStop(0, "#b9ffe4");
    headGrad.addColorStop(0.42, "#4be3a8");
    headGrad.addColorStop(1, "#12b3a2");

    ctx.fillStyle = headGrad;
    ctx.beginPath();
    ctx.arc(head.x, head.y, cell * 0.44, 0, Math.PI * 2);
    ctx.fill();

    ctx.shadowBlur = 0;

    // Eyes facing the movement direction
    const fwd = cell * 0.115;
    const side = cell * 0.175;

    for (const s of [1, -1]) {
      const ex = head.x + d.x * fwd + perp.x * side * s;
      const ey = head.y + d.y * fwd + perp.y * side * s;

      ctx.fillStyle = "#f4fbff";
      ctx.beginPath();
      ctx.arc(ex, ey, cell * 0.118, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = "#07131f";
      ctx.beginPath();
      ctx.arc(
        ex + d.x * cell * 0.055,
        ey + d.y * cell * 0.055,
        cell * 0.062,
        0,
        Math.PI * 2
      );
      ctx.fill();
    }

    ctx.restore();
  }

  function drawParticles() {
    for (const p of particles) {
      const a = clamp(p.life / p.maxLife, 0, 1);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x * cell, p.y * cell, p.size * cell * (0.5 + a * 0.8), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function drawFrame(now, t) {
    drawBoard();
    drawFood(now);
    drawSnake(t);
    drawParticles();

    // Board edge
    ctx.strokeStyle = "rgba(126, 200, 255, 0.18)";
    ctx.lineWidth = 2;
    ctx.strokeRect(1, 1, size - 2, size - 2);
  }

  /* ============================================================
     Main loop
     ============================================================ */

  function frame(now) {
    const dt = Math.min((now - lastTime) / 1000, 0.1);
    lastTime = now;

    if (state === "playing" || state === "demo") {
      accumulator += dt * 1000;
      let guard = 0;
      while (accumulator >= STEP_MS && guard < 8) {
        accumulator -= STEP_MS;
        stepGame();
        guard++;
      }
    }

    updateParticles(dt);
    drawFrame(now, accumulator / STEP_MS);

    requestAnimationFrame(frame);
  }

  /* ============================================================
     Boot
     ============================================================ */

  resize();
  startDemo();
  requestAnimationFrame((t) => {
    lastTime = t;
    frame(t);
  });
})();
</script>

</body>
</html>
Reasoning trace (7,051 tokens)
We need output single html code block. Need build polished. Need self-playing demo mode with "Click or press a key to start". But game must show meaningful first paint. Let's design.

Canvas responsive square, grid 24x24 maybe. Smooth movement: render interpolated positions between grid ticks. Input queue honoring quick presses, prevent reverse relative to last queued direction. Food never on snake.

Demo mode: AI plays automatically behind overlay "Click or press a key to start". Actually requirement: games must start self-playing demo mode behind message. So state 'demo' where AI snake plays, overlay message with score maybe. On click/any key, restart to playing. Keyboard listener needs click? window keydown gets after focus/iframe click. Fine.

Session best score via in-memory variable (sessionStorage not allowed? "session" best — use variable; localStorage prohibited).

Visual: dark background? Snake game. Use CSS gradients fine. Canvas drawing: board with subtle checker, rounded cells, glow. Head distinct eyes. Smooth: interpolation with tick accumulator. Particle burst.

Careful: no external fonts. Use system font. Layout at 1280x800: header score cards, canvas size min( calc(100vh - stuff), 620px). Board centered. Controls hint.

Implement fixed timestep with rAF and accumulator. Movement tick = 118ms. Smooth interpolation: store prevSnake array and snake. Render each segment at lerp(prev, current) but wrapping not used (walls kill), so fine. Snake body positions: when moving, snake array new head + body; prevSnake is old array. For rendering, need align: segment i of new snake corresponds to prev segment i-1? Actually old snake positions: head at old[0], body old[1..]. New snake: head = old[0]+dir, new[1]=old[0], new[i]=old[i-1]. So new segment i moves from old[i-1] to new[i] (=old[i-1]) for i>=1 — no movement. That gives static body with head moving, which is actually correct-ish but looks like the whole snake follows smoothly? Visually typical smooth snake interpolates each segment from its previous position to new position, where new[i] = old[i-1], so body segments stay put and shift at tick... Hmm the whole snake geometry with head interpolating toward next cell looks like snake lengthening. Better approach: interpolate along path positions: render body segment i at lerp(old[i], new[i], t) but new[i]=old[i-1], so it moves forward one cell over the tick. That gives each segment moving one cell, including tail retracting — but when growing, tail doesn't move. Actually correct rendering: each snake piece occupies a cell during the tick; the visual body should travel. Standard "smooth movement" interpolation: draw head interpolated from old head to new head, and draw body from old positions with each segment interpolated toward the position of segment ahead. Using lerp(old[i], new[i], t) where new[i]=old[i-1] works: segment i moves from old[i] toward old[i-1]. Segment 0 moves old[0]→new head. Yes that's right and it's what I said: new[i] for i>=1 = old[i-1]. So interpolation from old[i] to new[i] = old[i-1], moving forward. Good. But if grown, snake length increases by 1; the new last segment new[n-1] = old[n-2], and there is no old[n-1]; set prev for it = old[n-2] (stationary) or just handle. Simpler: keep renderFrom array of same length as new snake: renderFrom[0] = old[0], renderFrom[i] = old[i] ?? old[i-1]. Fine.

But drawing connected rounded snake via circles per segment plus thick line path: use interpolated positions and stroke path with round line joins/caps. That gives nice smooth snake. Then head as larger rounded circle at pos 0, eyes facing dir. Need distinct head.

Snake tick state: eat -> grow (don't pop tail). Implementation: unshift new head; if food eaten, keep tail; else pop.

Game over overlay with score + best, Space/Enter restart.

Particles: array with x,y (canvas pixel coords in grid units maybe), vx, vy, life, color. Update with dt in rAF.

Food pulse animation.

Grid 24x24, cell size computed from canvas width / COLS. Use devicePixelRatio.

Canvas resize: set width = clientWidth*dpr, height same. Board CSS aspect-ratio 1/1.

Layout: page flex column, header top with title left and stats right; board; footer hints. At 375 width: header stacks, board width calc(100vw - 24px). Keep total fits.

Demo AI: simple pathing. Compute a safe move: candidates avoiding walls/self, prefer direction toward food with BFS-ish greedy + safety (flood fill area after move > snake length maybe). Keep it reasonably simple but competent. Use BFS to food? Could implement greedy with safety check: for each candidate direction not reverse, simulate move, count reachable free space (flood fill limited), score = distance to food negative + space. Choose best. Demo plays indefinitely-ish; when dies, auto restart demo after short pause.

Demo overlay message: "Click or press a key to start" plus small "Demo mode · AI is playing". Should not block canvas visibility — use semi-transparent gradient overlay panel.

Important: overlay only visible in demo; on start it hides. Requirement game start demo mode behind message — yes.

Also "P pauses" — pause overlay with "Paused — press P to resume". Space/Enter restart on gameover; on pause P resumes. Clicking canvas starts / resumes.

Input queue: queue max 2-3, on keydown compute direction; reject if same as last effective or reverse of last queued/current. Then applied at each tick: if queue length, pop and set direction.

Prevent default for arrows/space to avoid scroll.

Best score session: update when eating / game over.

Score display: score, best. Maybe also length. "SCORE" and "BEST".

Let's write code carefully.

Canvas drawing details:
- background: fill '#0b1220'; draw grid cells subtle alternating with alpha, plus board border.
- Actually CSS canvas has border radius; draw full board.

Colors: deep navy backdrop with teal/lime snake? Let's pick palette: bg #070b14, board #0d1626, grid lines #16243a, snake body gradient from #34d399 to #22d3ee, head #a7f3d0? Food coral #fb7185 with glow. Accent amber for scores.

Page background: radial gradient dark with subtle. Fine.

Rounded stroke path: strokeStyle gradient? Use ctx.createLinearGradient across board for snake. Set lineWidth = cell*0.62, lineJoin round, lineCap round. Draw path through interpolated points. Then draw highlight stroke thinner with rgba(255,255,255,0.12) offset? Could draw a second path with lineWidth cell*0.22 and rgba white .10 slightly offset via translate. Keep simple.

Head: circle radius cell*0.42, fill head color, then eyes: compute perpendicular offsets. Direction vector (dx,dy). Eye positions = head + dir*0.13*cell ± perp*0.17*cell. Pupil black radius 0.07 cell offset toward direction 0.035 cell. Draw white eyes radius 0.105 cell.

Food: draw glowing circle with pulsing radius, plus small leaf/stem maybe. Draw outer glow via shadowBlur.

Particles in board coordinate (cell units) to handle resize: store x,y in cell units, vx,vy cells/sec. Draw at x*cell.

Game loop: let lastTime, accumulator. On each frame dt = min((now-last)/1000, 0.1). If state playing or demo: accumulator += dt*1000; while accumulator >= STEP: stepGame(); accumulator -= STEP. render interpolation t = accumulator/STEP. Particles update always (even paused? pause freeze maybe update particles fine). Food pulse uses time.

State: 'demo','playing','paused','gameover'. Demo step uses AI to set direction queue.

Game over: on collision in stepGame: state='gameover'; overlay; update best.

Demo death: set state='demo' but reset game and demo pause timer 700ms maybe. Just reset immediately and keep playing behind overlay; that's fine but slightly jarring. Could set demoResetAt and freeze briefly. Keep simple: on demo death, resetGame(true) and continue.

Input handling:
window.addEventListener('keydown', e => {
 const k=e.key.toLowerCase();
 if (['arrowup',...,' ','w','a','s','d','p','enter'].includes(k)) e.preventDefault();
 if (k==='p') { if(state==='playing') state='paused'; else if(state==='paused') state='playing'; return; }
 if (state==='gameover' && (k===' '||k==='enter')) { startGame(); return; }
 if (state==='demo') { if (k===' '||k==='enter'||direction key) startGame(); ... } 
 if (state==='paused') { if (k===' '||k==='enter') state='playing'; maybe }
 directions -> queueDirection
});

Careful: in demo, pressing arrow should start game AND apply the direction. Order: startGame() then queue.

Click on canvas: if demo -> startGame(); if gameover -> startGame(); if paused -> resume.

Also "Click or press a key to start" message. Add subtle "P pause · Arrows / WASD move" hints outside overlay.

Demo AI logic: function chooseDemoDirection() {
 const head = snake[0]; const dirs = [up,right,down,left] filtered not reverse of direction and not colliding immediately;
 For each candidate: simulate new head; if collision skip; compute dist = |nx-food.x|+|ny-food.y|; space = floodCount(nx,ny, snake body excluding tail) capped at e.g. 120; score = -dist*2 + space*1.2 + (dir===currentDirection ? 0.5 : 0); plus avoid adjacency to own body? flood handles.
 pick max. If none, keep current (will die).
}
floodCount BFS with set of occupied (snake minus tail since tail moves).

Note demo direction must not reverse onto itself: candidates exclude opposite of current direction.

Input queue handling in demo AI: set direction directly at tick.

Rendering interpolation for demo too.

Score display update in DOM each change.

Layout HTML:
<div class="app">
 <header class="topbar"><div class="brand">...<h1>NEON SNAKE</h1><p>...</p></div><div class="stats"><div class="stat"><span>Score</span><strong id="score">0</strong></div>... </div></header>
 <main class="stage"><div class="board-wrap"><canvas id="game"></canvas><div class="overlay" id="overlay">...</div></div></main>
 <footer class="hints">...</footer>
</div>

Need board size responsive and centered; .board-wrap width: min(620px, calc(100vw - 32px), calc(100vh - 220px)) — but using vh with min could be odd. Use width: min(620px, calc(100vw - 28px), calc(100vh - 210px)); aspect-ratio 1. At 800 height: 800-210=590 -> 590 wide, good. At 375 width: 347. Good.

Total height: header ~90 + board 590 + footer 60 + gaps = ~770 fits 800. Fine.

On small screens header stacks, maybe height larger; use media query reduce sizes and board min with vh. Fine — no horizontal scrollbar: set body overflow-x hidden, use min() with 100vw.

Canvas crispness: on resize set canvas.width = rect.width*dpr etc, ctx.setTransform(dpr,0,0,dpr,0,0) and use CSS pixel coords. Store size = rect.width (square).

Render function uses cell = size / COLS.

Overlay CSS: absolute inset 0, display flex, pointer-events none maybe but click handled on board-wrap. Overlay background: radial-gradient plus backdrop blur. Keep text legible but board visible: rgba(7,11,20,0.72).

Overlay content states via JS set overlay innerHTML? Simpler: overlay has elements: .ov-kicker, .ov-title, .ov-text, .ov-btn, .ov-score. Update text per state.

Demo overlay: title "Click or press a key to start", kicker "DEMO MODE · AI PLAYING", text "Arrow keys or WASD to move · P to pause". Button-ish "START GAME".

Game over: kicker "GAME OVER", title "Run ended", text "Score 120 · Best 240", hint "Press Space or Enter to restart".

Paused: kicker "PAUSED", title "Take a breath", text "Press P to resume".

Ensure overlay doesn't intercept clicks: pointer-events:none, board-wrap handles click.

Now write code.

Potential issue: key ' ' keydown -> e.key is ' '. lowercase fine.

Session best: let best = 0.

Food spawn: while true pick random cell not in snake set.

stepGame():
- apply input queue: if queue.length: const nd = queue.shift(); if not opposite of direction -> direction = nd.
- prevSnake = snake.map(...)
- newHead = ...
- collision: walls or snake body (excluding tail if not growing? For self collision, if the snake isn't growing, the tail cell will move away, so colliding with tail is allowed). Standard: check against snake.slice(0, snake.length-1) when not eating. But we don't know eating until after computing new head. Compute willEat = newHead equals food. body to check = willEat ? snake : snake.slice(0,-1). Fine.
- if collision -> gameOver.
- snake.unshift(newHead); if willEat: score+=10, spawn food, particles, best update; else snake.pop().

Render interpolation: need renderFrom array length snake.length. Compute after step: prevRender = old snake. In render: for i in snake: from = prevRender[i] || prevRender[prevRender.length-1] ... Actually when grown, prevRender length = old length = new length -1; new last index = old length. from = prevRender[i] || prevRender[i-1] || snake[i]. Use that.

But when i indexes beyond prevRender length-1: use prevRender[prevRender.length-1].

Also after reset (startGame), prevRender = snake copy so t interpolation no movement.

Demo mode reset at start: snake initial length 5 moving right, food random.

Initial demo snake should be positioned mid-grid. score 0.

Let me also handle "Quick successive key presses must both be honored" — queue length 3 and applied one per tick.

Now, particles: on eat, spawn 18 particles at food center, colors from palette ['#fb7185','#fbbf24','#34d399','#22d3ee'], velocity random outward 2-7 cells/s, life 0.5-0.9s, size 0.07-0.16 cell.

Render particles after snake/food, before overlay (overlay is DOM anyway).

Food drawing: shadowBlur maybe expensive but fine at 60fps with one food.

Grid: draw lines each cell? 24 lines fine. Or checkerboard rects 576 — fine.

Let me write the CSS with nice typography: font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial. Letter-spacing.

Header brand: little logo square with SVG? Could use CSS square with snake-ish dot. Inline SVG allowed (it's in HTML). Keep simple: a rounded square with gradient and a small dot pattern.

Now produce the final code, checking syntax mentally.

JS structure:

const canvas = ..., ctx = canvas.getContext('2d');
const COLS = 24, ROWS = 24, STEP = 110;
let size = 0, cell = 0, dpr = 1;
function resize() { const rect = canvas.getBoundingClientRect(); dpr = Math.min(window.devicePixelRatio||1, 2); size = rect.width; canvas.width = Math.round(size*dpr); canvas.height = Math.round(size*dpr); ctx.setTransform(dpr,0,0,dpr,0,0); cell = size/COLS; }

Since canvas CSS width/height 100%, rect.width==rect.height.

Direction object: const DIRS = {up:{x:0,y:-1}, ...}. direction = DIRS.right.

opposite(a,b) => a.x===-b.x && a.y===-b.y.

queueDirection(d): const last = queue.length ? queue[queue.length-1] : direction; if (d.x===-last.x && d.y===-last.y) return; if (d.x===last.x&&d.y===last.y) return; if(queue.length<3) queue.push(d).

Also if snake length is 1 no reverse restriction but fine.

startGame(): state='playing'; score=0; snake=..., direction=DIRS.right, queue=[], particles=[], spawnFood(), prevRender=snake.map(...); accumulator=0; updateOverlay(); updateHUD();

resetDemo(): same but state='demo'.

Demo AI in stepGame: if (state==='demo') direction = chooseDemoDirection(); Actually apply queue first then AI override. Do: if state demo { direction = ai; queue.length=0; } else apply queue.

Let's handle render loop:
function frame(now) { const dt = Math.min((now - last)/1000, 0.1); last=now; if (state==='playing'||state==='demo'){ acc += dt*1000; while(acc>=STEP){ stepGame(); acc-=STEP; } } updateParticles(dt); draw(acc/STEP); requestAnimationFrame(frame); }

If state paused/gameover, acc stays; t computed but no step. Fine — draw(t).

updateParticles: p.x += p.vx*dt; p.y += p.vy*dt; p.vy += 4*dt (gravity in cell units?) slight. p.life -= dt.

Now the AI flood fill: 
function floodCount(sx,sy, blocked) {
 const seen = new Set([sx+','+sy]); const q=[[sx,sy]]; let n=0;
 while(q.length && n<200){ const [x,y]=q.pop(); n++; for(const d of Object.values(DIRS)){...if in bounds && !blocked.has(key) && !seen.has(key)) {seen.add(key);q.push([nx,ny]);}}
 return n;
}
blocked set built from snake minus tail.

chooseDemoDirection(): 
const head=snake[0]; let bestD=direction, bestScore=-Infinity;
for(const d of [DIRS.up,DIRS.right,DIRS.down,DIRS.left]) {
 if (opposite(d,direction)) continue;
 const nx=head.x+d.x, ny=head.y+d.y;
 if(nx<0||...) continue;
 const willEat = nx===food.x&&ny===food.y;
 const bodySet = new Set((willEat?snake:snake.slice(0,-1)).map(s=>s.key));
 if(bodySet.has(nx+','+ny)) continue;
 const blocked = new Set(snake.slice(0,-1).map(...)); blocked.add? For flood, the new head is occupied too. Compute flood from new head with blocked = snake minus tail (the tail moves). Actually flood from new head; blocked includes snake body except tail.
 const space = floodCount(nx,ny,blocked);
 const dist = Math.abs(nx-food.x)+Math.abs(ny-food.y);
 let sc = space*1.5 - dist*2.4 + (d===direction?0.8:0) + (willEat?12:0);
 // penalty for moving near own neck? skip
 if(sc>bestScore){bestScore=sc;bestD=d;}
}
return bestD;
Need snake segments have x,y; I'll store plain objects {x,y}.

Food spawn uses Set of keys.

Now draw:

function draw(t) {
 ctx.clearRect(0,0,size,size);
 // board bg
 ctx.fillStyle='#0a1322'; roundRect? canvas fills whole. Use fillRect.
 // checker
 for y,x: fillStyle = ((x+y)%2? 'rgba(255,255,255,0.018)':'rgba(255,255,255,0.032)') — hmm make subtle.
 // grid lines
 ctx.strokeStyle='rgba(120,180,220,0.07)'; lineWidth=1; for i...
 // vignette maybe skip.
 drawFood();
 drawSnake(t);
 drawParticles();
 // border
 ctx.strokeStyle='rgba(120,200,255,0.18)'; lineWidth=2; strokeRect(1,1,size-2,size-2);
}

drawSnake: build points array: for i, from = prevRender[i] || prevRender[prevRender.length-1] || snake[i]; p = lerp(from, snake[i], t) + 0.5 cell center. Path stroke with gradient.

Gradient: createLinearGradient(0,0,size,size) with '#2dd4bf','#22d3ee','#34d399'.

Then body highlight: stroke same path with lineWidth cell*0.2, strokeStyle 'rgba(255,255,255,0.13)'.

Head: compute hp = points[0]. radius cell*0.44. Shadow glow: ctx.shadowColor 'rgba(52,211,153,0.55)', shadowBlur cell*0.5.

Eyes facing direction: direction vector uses current direction. perp = {-dy, dx}. eye offset forward 0.12*cell, side 0.17*cell. White ellipse/circle radius 0.115*cell; pupil radius 0.06*cell offset forward 0.045*cell.

Also draw a small tail taper? The stroke with round caps is fine.

Food draw: center = (food.x+0.5)*cell etc. pulse = 1 + Math.sin(time/220)*0.06. glow via shadowBlur cell*0.8, shadowColor '#fb7185'. circle radius cell*0.34*pulse. Inner highlight small white circle offset.

Time variable: performance.now().

OK. Also handle resize with ResizeObserver or window resize.

One thing: canvas rect width computed before layout? Use requestAnimationFrame(init) or resize after DOM load (script at end of body). Add window.addEventListener('resize', resize); resize() immediately.

Now HUD update: scoreEl.textContent = score; bestEl.textContent = best; maybe length stat too — requirement score & best. Add "Length" for polish.

Overlay update function:
function updateOverlay(){
 if(state==='demo'){ kicker 'DEMO MODE · AI PLAYING'; title 'Click or press a key to start'; text 'Arrow keys or WASD to move · P to pause'; action 'START GAME'; overlay.classList.remove('hidden') }
 else if state playing: hidden
 ...
}

Overlay visible in game over: show score.

Also maybe "Press P to pause" hints in footer.

Now write the actual final HTML. Ensure no errors: careful with object key naming and functions defined before use (function declarations hoisted).

Let me write it out.

Also "in-page text describing how to use" restriction — but the task explicitly requires the message "Click or press a key to start", so hints are fine.

Edge: pressing Space in demo: startGame() and don't queue direction. Fine.

Key handler code:

window.addEventListener('keydown', (e)=>{
 const k = e.key.toLowerCase();
 const isDir = k in KEYDIRS;
 if (isDir || k===' ' || k==='enter' || k==='p') e.preventDefault();
 if (k==='p'){ if(state==='playing'){state='paused';} else if(state==='paused'){state='playing';} updateOverlay(); return; }
 if (state==='gameover'){ if(k===' '||k==='enter'){ startGame(); } return; }
 if (state==='paused'){ if(k===' '||k==='enter'){state='playing';updateOverlay();} return; }
 if (state==='demo'){ startGame(); if(isDir) queueDirection(KEYDIRS[k]); return; }
 if (isDir) queueDirection(KEYDIRS[k]);
});

KEYDIRS = {arrowup:DIRS.up, w:DIRS.up, ...}.

Note 'p' key when state demo: handled before start — pressing P in demo just... would not start. Requirement "Click or press a key to start" — any key should start. Move demo check earlier? Let's: if state demo and k!=='p' -> startGame... Hmm "press a key to start" includes P. Let's handle: if (state==='demo'){ startGame(); if(isDir) queue...; if(k==='p'){ /* started, then pause? */ } return; } — simpler: in demo, any key starts game; if it's 'p', also toggle pause? Not needed. Just start.

So reorder: if (k==='p' && (state==='playing'||state==='paused')) toggle; else if state demo start; etc.

Fine.

Click handler on boardWrap.

Also add `tabindex="0"` maybe. Not needed since window listener.

Now writing final answer with only code block.

One more detail: `roundRect` support in Chrome — fine but I'll avoid it.

CSS specifics:

:root { --bg:#070b14; --panel:#0d1526; --line: rgba(148,197,255,.12); --mint:#34d399; --cyan:#22d3ee; --rose:#fb7185; --amber:#fbbf24; --text:#e8f1ff; --muted:#8aa2bd; }

body { min-height:100vh; display:flex; justify-content:center; background: radial-gradient(1200px 700px at 50% -10%, #12233c 0%, #070b14 60%); color:var(--text); font: ... ; overflow-x:hidden; }

.app { width:100%; max-width:1180px; padding: 22px clamp(14px,3vw,34px) 18px; display:flex; flex-direction:column; align-items:center; gap: clamp(12px,2vh,20px); }

.topbar { width:100%; display:flex; align-items:flex-end; justify-content:space-between; gap:18px; flex-wrap:wrap; }

.brand { display:flex; align-items:center; gap:12px; }
.logo { width:42px;height:42px;border-radius:13px; background:linear-gradient(150deg,#22d3ee,#34d399 55%, #0ea5e9); box-shadow: 0 10px 28px rgba(34,211,238,.28); position:relative;}
.logo::after { content:''; position:absolute; inset:12px; border-radius:50%; background:#06111f; box-shadow: 9px 0 0 -3px #06111f? } hmm. Simpler: logo has ::before dot.

h1 font-size clamp(20px,2.4vw,28px); letter-spacing .18em; font-weight 800.

.stats { display:flex; gap:10px; }
.stat { min-width:92px; padding:10px 16px 11px; border:1px solid var(--line); border-radius:14px; background: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.015)); }
.stat span { display:block; font-size:10px; letter-spacing:.22em; color:var(--muted); text-transform:uppercase;}
.stat strong { font-size:26px; font-variant-numeric: tabular-nums; }
.stat.best strong { color: var(--amber) } .stat.score strong { color: var(--mint) }

.stage { width:100%; display:flex; justify-content:center; }
.board-wrap { position:relative; width:min(620px, calc(100vw - 28px), calc(100vh - 232px)); aspect-ratio:1/1; border-radius:22px; overflow:hidden; box-shadow: 0 30px 70px rgba(0,0,0,.55), 0 0 0 1px rgba(126,200,255,.14), 0 0 60px rgba(34,211,238,.10); cursor:pointer; }

Hmm at 800px height: 800-232=568 board. Topbar ~70 + gaps 30 + footer 40 = 140 -> 568+140 = 708 fits. Good.

canvas { display:block; width:100%; height:100%; }

.overlay { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; text-align:center; padding:22px; background: radial-gradient(120% 90% at 50% 40%, rgba(7,11,20,.55), rgba(7,11,20,.86)); backdrop-filter: blur(3px); transition: opacity .25s ease; }
.overlay.hidden { opacity:0; pointer-events:none; }

.ov-kicker letter-spacing .28em; font-size 10px; color var(--cyan);
.ov-title font-size clamp(20px, 3.4vw, 32px); font-weight 800; letter-spacing .01em;
.ov-text color muted font-size 14px max-width 320px line-height 1.6;
.ov-btn margin-top 8px; padding 11px 20px; border-radius 999px; background linear-gradient(135deg,#22d3ee,#34d399); color:#04202b; font-weight 800; letter-spacing .12em; font-size 11px;

.hints { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; }
.hint { padding:7px 13px; border:1px solid var(--line); border-radius:999px; color:var(--muted); font-size:11px; letter-spacing:.12em; text-transform:uppercase; background:rgba(255,255,255,.025);}
.hint b { color:var(--text); font-weight:700; }

Media query max-width 560: topbar column align flex-start? Keep stats full width with 3 columns equal; board width calc(100vw - 24px). Reduce hints font.

Fine. Write the code now. Also ensure `updateOverlay` sets button text.

For game over overlay text: `Score ${score} · Best ${best}`.

Let me finalize the JS carefully including initial call: resize(); resetDemo(); requestAnimationFrame(frame). Need last = performance.now().

Careful with `frame` first call: set last inside.

Write out now.