Models 44

Claude Haiku 5.5

by AnthropicReasoningClean
Open in new window

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

How it went

Total time
1 min 37 s
Cost
$0.012
Output tokens
23,234
Spent thinking
14,831 tokens
Speed
245 tok/s
Console errors
0
First token after
2.4 s
First visible text after
1 min 13 s
Prompt tokens
750
Code size
16.8 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"medium"}
Max output
32,000 tokens
Generated
Nov 10, 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 · 589 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake</title>
<style>
  html, body {
    margin: 0;
    height: 100%;
    background: radial-gradient(circle at 50% 15%, #16264a 0%, #070b16 70%);
    color: #e6edf8;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    overflow-x: hidden;
  }
  body {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding: 12px;
    box-sizing: border-box;
  }
  #app {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    width: 560px;
    max-width: 100%;
  }
  header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    box-sizing: border-box;
    gap: 10px;
  }
  h1 {
    margin: 0;
    font-size: 26px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: #9ff5c4;
    text-shadow: 0 0 18px rgba(159, 245, 196, 0.35);
  }
  .stats { display: flex; gap: 10px; }
  .stat {
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
    padding: 6px 12px;
    text-align: center;
    min-width: 72px;
  }
  .stat span {
    display: block;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    opacity: 0.6;
  }
  .stat b {
    display: block;
    font-size: 20px;
    font-variant-numeric: tabular-nums;
  }
  .stat b.pop { animation: pop 0.3s ease-out; }
  @keyframes pop {
    0% { transform: scale(1); }
    40% { transform: scale(1.25); color: #ffd166; }
    100% { transform: scale(1); }
  }
  #board {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
  }
  canvas {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 16px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.08);
    cursor: pointer;
  }
  #overlay {
    position: absolute;
    inset: 0;
    display: none;
    pointer-events: none;
    align-items: center;
    justify-content: center;
    border-radius: 16px;
  }
  #overlay.on { display: flex; }
  #overlay.banner { align-items: flex-end; padding-bottom: 18px; box-sizing: border-box; }
  #overlay.modal { background: rgba(3, 6, 14, 0.45); }
  .pill {
    background: rgba(7, 11, 22, 0.85);
    border: 1px solid rgba(159, 245, 196, 0.45);
    color: #9ff5c4;
    padding: 10px 18px;
    border-radius: 999px;
    font-size: 15px;
    text-align: center;
    animation: breathe 1.8s ease-in-out infinite;
  }
  @keyframes breathe {
    0%, 100% { box-shadow: 0 0 0 rgba(159, 245, 196, 0); }
    50% { box-shadow: 0 0 22px rgba(159, 245, 196, 0.35); }
  }
  .card {
    background: rgba(7, 11, 22, 0.85);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 18px;
    padding: 22px 30px;
    text-align: center;
    max-width: 80%;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  }
  .card h2 { margin: 0 0 6px; font-size: 28px; }
  .card .big { margin: 0; font-size: 36px; font-weight: 700; font-variant-numeric: tabular-nums; }
  .card .tag { margin: 6px 0 0; color: #ffd166; font-weight: 600; }
  .card .sub { margin: 10px 0 0; font-size: 14px; opacity: 0.7; }
  .hint {
    margin: 0;
    font-size: 12px;
    opacity: 0.55;
    text-align: center;
  }
</style>
</head>
<body>
<div id="app">
  <header>
    <h1>Snake</h1>
    <div class="stats">
      <div class="stat"><span>Score</span><b id="score">0</b></div>
      <div class="stat"><span>Best</span><b id="best">0</b></div>
    </div>
  </header>
  <div id="board">
    <canvas id="c"></canvas>
    <div id="overlay"></div>
  </div>
  <p class="hint">Arrows / WASD steer &middot; P pause &middot; Space or Enter restart</p>
</div>

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

  const GRID = 24;
  const canvas = document.getElementById('c');
  const ctx = canvas.getContext('2d');
  const boardEl = document.getElementById('board');
  const appEl = document.getElementById('app');
  const overlay = document.getElementById('overlay');
  const scoreEl = document.getElementById('score');
  const bestEl = document.getElementById('best');

  const DIRS = {
    ArrowUp: { x: 0, y: -1 }, w: { x: 0, y: -1 },
    ArrowDown: { x: 0, y: 1 }, s: { x: 0, y: 1 },
    ArrowLeft: { x: -1, y: 0 }, a: { x: -1, y: 0 },
    ArrowRight: { x: 1, y: 0 }, d: { x: 1, y: 0 }
  };
  const NEIGHBORS = [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }];

  let size = 400, cell = 1, dpr = 1;
  let snake = [], prev = [], dir = { x: 1, y: 0 }, queue = [];
  let food = null;
  let score = 0, best = 0;
  let mode = 'demo';          // 'demo' | 'play' | 'over'
  let paused = false;
  let frozen = false;         // true while dead (demo waiting or game over)
  let acc = 0, tickMs = 125;
  let particles = [];
  let demoTimer = null;
  let lastTime = performance.now();
  let runNewBest = false;

  const key = (x, y) => x * GRID + y;
  const copy = arr => arr.map(s => ({ x: s.x, y: s.y }));

  /* ---------- Layout ---------- */
  function layout() {
    const availW = Math.min(window.innerWidth - 24, 560);
    const availH = window.innerHeight - 210;
    size = Math.floor(Math.max(240, Math.min(availW, availH)));
    appEl.style.width = size + 'px';
    dpr = window.devicePixelRatio || 1;
    canvas.width = Math.round(size * dpr);
    canvas.height = Math.round(size * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    cell = size / GRID;
  }

  /* ---------- HUD & overlay ---------- */
  function updateHUD() {
    scoreEl.textContent = score;
    bestEl.textContent = best;
  }
  function popEl(el) {
    el.classList.remove('pop');
    void el.offsetWidth;
    el.classList.add('pop');
  }
  function showOverlay(html, cls) {
    overlay.innerHTML = html;
    overlay.className = 'on ' + cls;
  }
  function hideOverlay() {
    overlay.className = '';
    overlay.innerHTML = '';
  }

  /* ---------- Game state ---------- */
  function spawnFood() {
    const occupied = new Set(snake.map(s => key(s.x, s.y)));
    const free = [];
    for (let x = 0; x < GRID; x++) {
      for (let y = 0; y < GRID; y++) {
        if (!occupied.has(key(x, y))) free.push({ x, y });
      }
    }
    if (!free.length) return null;
    return free[Math.floor(Math.random() * free.length)];
  }

  function reset() {
    clearTimeout(demoTimer);
    const cx = Math.floor(GRID / 2), cy = Math.floor(GRID / 2);
    snake = [{ x: cx, y: cy }, { x: cx - 1, y: cy }, { x: cx - 2, y: cy }, { x: cx - 3, y: cy }];
    prev = copy(snake);
    dir = { x: 1, y: 0 };
    queue = [];
    score = 0;
    tickMs = 125;
    acc = 0;
    frozen = false;
    paused = false;
    particles = [];
    food = spawnFood();
    updateHUD();
  }

  function startPlay() {
    mode = 'play';
    runNewBest = false;
    reset();
    hideOverlay();
  }

  function die() {
    frozen = true;
    prev = copy(snake);
    burst(snake[0].x, snake[0].y, '#ff5470', 26);
    if (mode === 'demo') {
      demoTimer = setTimeout(() => {
        if (mode === 'demo') reset();
      }, 900);
    } else {
      mode = 'over';
      if (score > best) { best = score; runNewBest = true; }
      updateHUD();
      const tag = runNewBest && score > 0 ? '<p class="tag">New best!</p>' : '';
      showOverlay(
        '<div class="card"><h2>Game over</h2>' +
        '<p class="big">Score ' + score + '</p>' + tag +
        '<p class="sub">Press Space or Enter to restart</p></div>',
        'modal'
      );
    }
  }

  function togglePause() {
    if (mode !== 'play') return;
    paused = !paused;
    if (paused) {
      showOverlay('<div class="card"><h2>Paused</h2><p class="sub">Press P to resume</p></div>', 'modal');
    } else {
      hideOverlay();
    }
  }

  /* ---------- Simulation ---------- */
  function step() {
    if (mode === 'demo') aiDecide();
    if (queue.length) dir = queue.shift();

    const head = snake[0];
    const nx = head.x + dir.x, ny = head.y + dir.y;
    const eating = !!food && nx === food.x && ny === food.y;
    const out = nx < 0 || ny < 0 || nx >= GRID || ny >= GRID;

    let hit = out;
    if (!hit) {
      // When not eating, the tail moves away, so it is safe to move into.
      const limit = eating ? snake.length : snake.length - 1;
      for (let i = 0; i < limit; i++) {
        if (snake[i].x === nx && snake[i].y === ny) { hit = true; break; }
      }
    }
    if (hit) { die(); return; }

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

    if (eating) {
      const eaten = food;
      score += 10;
      burst(eaten.x, eaten.y, '#ffd166', 16);
      burst(eaten.x, eaten.y, '#ff4d6d', 12);
      food = spawnFood();
      tickMs = Math.max(65, 125 - score * 0.5);
      if (mode === 'play' && score > best) best = score;
      updateHUD();
      popEl(scoreEl);
      if (mode === 'play') popEl(bestEl);
    } else {
      snake.pop();
    }
  }

  // Simple AI for demo mode: BFS toward food avoiding the body, with a safe fallback.
  function aiDecide() {
    const blocked = new Set();
    for (let i = 0; i < snake.length - 1; i++) blocked.add(key(snake[i].x, snake[i].y));
    const head = snake[0];

    if (food) {
      const parent = new Map();
      parent.set(key(head.x, head.y), null);
      const queueBfs = [head];
      let found = false;
      while (queueBfs.length) {
        const c = queueBfs.shift();
        if (c.x === food.x && c.y === food.y) { found = true; break; }
        for (const d of NEIGHBORS) {
          const nx = c.x + d.x, ny = c.y + d.y;
          if (nx < 0 || ny < 0 || nx >= GRID || ny >= GRID) continue;
          const k = key(nx, ny);
          if (parent.has(k) || blocked.has(k)) continue;
          parent.set(k, c);
          queueBfs.push({ x: nx, y: ny });
        }
      }
      if (found) {
        let c = food;
        let p = parent.get(key(c.x, c.y));
        while (p && !(p.x === head.x && p.y === head.y)) {
          c = p;
          p = parent.get(key(c.x, c.y));
        }
        queue = [{ x: c.x - head.x, y: c.y - head.y }];
        return;
      }
    }

    // Fallback: pick the safe move with the most open neighbours.
    let bestDir = null, bestScore = -1;
    for (const d of NEIGHBORS) {
      const nx = head.x + d.x, ny = head.y + d.y;
      if (nx < 0 || ny < 0 || nx >= GRID || ny >= GRID) continue;
      if (blocked.has(key(nx, ny))) continue;
      let open = 0;
      for (const e of NEIGHBORS) {
        const ax = nx + e.x, ay = ny + e.y;
        if (ax >= 0 && ay >= 0 && ax < GRID && ay < GRID && !blocked.has(key(ax, ay))) open++;
      }
      const s = open + Math.random() * 0.5;
      if (s > bestScore) { bestScore = s; bestDir = d; }
    }
    queue = bestDir ? [{ x: bestDir.x, y: bestDir.y }] : [];
  }

  /* ---------- Particles ---------- */
  function burst(gx, gy, color, n) {
    for (let i = 0; i < n; i++) {
      const a = Math.random() * Math.PI * 2;
      const sp = 2 + Math.random() * 4.5;
      const life = 380 + Math.random() * 320;
      particles.push({
        x: gx + 0.5, y: gy + 0.5,
        vx: Math.cos(a) * sp, vy: Math.sin(a) * sp,
        life, max: life, color
      });
    }
  }
  function updateParticles(dt) {
    for (const p of particles) {
      p.life -= dt;
      p.x += p.vx * dt / 1000;
      p.y += p.vy * dt / 1000;
      p.vx *= Math.pow(0.92, dt / 16);
      p.vy *= Math.pow(0.92, dt / 16);
    }
    particles = particles.filter(p => p.life > 0);
  }

  /* ---------- Rendering ---------- */
  function bodyColor(i, n) {
    const f = i / n;
    return `hsl(${152 - 24 * f}, 70%, ${52 - 18 * f}%)`;
  }

  function render(t, now) {
    ctx.clearRect(0, 0, size, size);

    // Background
    const bg = ctx.createLinearGradient(0, 0, size, size);
    bg.addColorStop(0, '#132447');
    bg.addColorStop(1, '#0a1226');
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, size, size);

    // Grid
    ctx.strokeStyle = 'rgba(255,255,255,0.035)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    for (let i = 1; i < GRID; i++) {
      const p = Math.round(i * cell) + 0.5;
      ctx.moveTo(p, 0); ctx.lineTo(p, size);
      ctx.moveTo(0, p); ctx.lineTo(size, p);
    }
    ctx.stroke();

    // Food
    if (food) {
      const fx = (food.x + 0.5) * cell, fy = (food.y + 0.5) * cell;
      const r = cell * (0.3 + 0.03 * Math.sin(now / 200));
      ctx.save();
      ctx.shadowColor = '#ff4d6d';
      ctx.shadowBlur = cell * 0.9;
      ctx.fillStyle = '#ff4d6d';
      ctx.beginPath(); ctx.arc(fx, fy, r, 0, Math.PI * 2); ctx.fill();
      ctx.restore();
      ctx.fillStyle = 'rgba(255,255,255,0.55)';
      ctx.beginPath(); ctx.arc(fx - r * 0.35, fy - r * 0.35, r * 0.25, 0, Math.PI * 2); ctx.fill();
      ctx.fillStyle = '#4ade80';
      ctx.beginPath();
      ctx.ellipse(fx + r * 0.25, fy - r * 0.95, r * 0.38, r * 0.2, -0.5, 0, Math.PI * 2);
      ctx.fill();
    }

    // Snake positions interpolated between ticks
    const n = snake.length;
    const pts = snake.map((s, i) => {
      const p = prev[Math.min(i, prev.length - 1)] || s;
      return {
        x: (p.x + (s.x - p.x) * t + 0.5) * cell,
        y: (p.y + (s.y - p.y) * t + 0.5) * cell
      };
    });

    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.lineWidth = cell * 0.8;
    for (let i = n - 1; i >= 1; i--) {
      ctx.strokeStyle = bodyColor(i, n);
      ctx.beginPath();
      ctx.moveTo(pts[i].x, pts[i].y);
      ctx.lineTo(pts[i - 1].x, pts[i - 1].y);
      ctx.stroke();
    }
    for (let i = n - 1; i >= 1; i--) {
      ctx.fillStyle = bodyColor(i, n);
      ctx.beginPath();
      ctx.arc(pts[i].x, pts[i].y, cell * 0.4, 0, Math.PI * 2);
      ctx.fill();
    }

    // Head
    const h = pts[0];
    const fx = dir.x, fy = dir.y;
    const sx = -fy, sy = fx; // perpendicular
    ctx.save();
    ctx.shadowColor = 'rgba(110, 255, 170, 0.9)';
    ctx.shadowBlur = cell * 0.6;
    ctx.fillStyle = 'hsl(140, 85%, 58%)';
    ctx.beginPath(); ctx.arc(h.x, h.y, cell * 0.46, 0, Math.PI * 2); ctx.fill();
    ctx.restore();

    // Tongue (flicks occasionally)
    if (Math.sin(now / 260) > 0.85) {
      const tx = h.x + fx * cell * 0.44, ty = h.y + fy * cell * 0.44;
      const ex = h.x + fx * cell * 0.68, ey = h.y + fy * cell * 0.68;
      ctx.strokeStyle = '#ff4d6d';
      ctx.lineWidth = cell * 0.06;
      ctx.beginPath();
      ctx.moveTo(tx, ty); ctx.lineTo(ex, ey);
      ctx.moveTo(ex, ey); ctx.lineTo(ex + fx * cell * 0.1 + sx * cell * 0.1, ey + fy * cell * 0.1 + sy * cell * 0.1);
      ctx.moveTo(ex, ey); ctx.lineTo(ex + fx * cell * 0.1 - sx * cell * 0.1, ey + fy * cell * 0.1 - sy * cell * 0.1);
      ctx.stroke();
    }

    // Eyes facing the direction of travel
    for (const side of [-1, 1]) {
      const ex = h.x + fx * cell * 0.16 + sx * side * cell * 0.2;
      const ey = h.y + fy * cell * 0.16 + sy * side * cell * 0.2;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath(); ctx.arc(ex, ey, cell * 0.12, 0, Math.PI * 2); ctx.fill();
      ctx.fillStyle = '#0b1020';
      ctx.beginPath();
      ctx.arc(ex + fx * cell * 0.05, ey + fy * cell * 0.05, cell * 0.06, 0, Math.PI * 2);
      ctx.fill();
    }

    // Particles
    for (const p of particles) {
      const a = Math.max(0, p.life / p.max);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x * cell, p.y * cell, cell * (0.06 + 0.12 * a), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  /* ---------- Main loop ---------- */
  function frame(now) {
    const dt = Math.min(100, now - lastTime);
    lastTime = now;

    if (!paused && !frozen) {
      acc += dt;
      while (acc >= tickMs && !frozen) {
        acc -= tickMs;
        step();
      }
    }

    updateParticles(dt);
    render(Math.min(1, acc / tickMs), now);
    requestAnimationFrame(frame);
  }

  /* ---------- Input ---------- */
  function enqueue(d) {
    const last = queue.length ? queue[queue.length - 1] : dir;
    if (d.x === last.x && d.y === last.y) return;   // same direction
    if (d.x === -last.x && d.y === -last.y) return; // reverse onto itself
    if (queue.length < 3) queue.push({ x: d.x, y: d.y });
  }

  window.addEventListener('keydown', e => {
    const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
    const isDir = Object.prototype.hasOwnProperty.call(DIRS, k);
    const isAction = k === ' ' || k === 'Enter' || k === 'p' || k === 'P';
    if (!isDir && !isAction) return;
    e.preventDefault();

    if (mode === 'demo') { startPlay(); return; }

    if (mode === 'over') {
      if (k === ' ' || k === 'Enter') startPlay();
      return;
    }

    // mode === 'play'
    if (k === 'p' || k === 'P') { togglePause(); return; }
    if (paused) return;
    if (isDir) enqueue(DIRS[k]);
  });

  boardEl.addEventListener('click', () => {
    if (mode === 'demo' || mode === 'over') startPlay();
  });

  window.addEventListener('resize', layout);

  /* ---------- Boot ---------- */
  layout();
  reset();
  prev = copy(snake);
  mode = 'demo';
  showOverlay('<div class="pill">Click or press a key to start</div>', 'banner');
  requestAnimationFrame(t => {
    lastTime = t;
    frame(t);
  });
})();
</script>
</body>
</html>
Reasoning trace (14,831 tokens)
The next thinking just repeats the opening of the earlier notes, so there's nothing new to add here. Still to pin down: the input buffer, which should check each new direction against the last one actually applied rather than the pending queue.

Now I'm laying out the rest of the Snake game: the demo AI that paths toward food, how the overlay and start state switch between demo and real play, and the keyboard controls.

Still sorting out the timing for the game loop: a tick every 110 ms that speeds up a little as the score grows, with the drawing done per frame and the snake's segments interpolated between the previous and current positions. The tricky part is the tail when the snake grows, so I'm keeping the previous-position array in step with the current one.

I'm working out how the snake's body should animate between ticks: each segment should slide from its old position into the spot the segment ahead of it just vacated. When the snake grows, the new tail simply sits where the old tail was, so nothing visibly jumps.

Now I'm working through how the snake moves between ticks: each segment slides from its old spot into the one the segment ahead just left, so the head leads and the body follows. When the snake grows, the new tail sits exactly where the old tail was, so nothing jumps. I'm also planning a particle burst and a quick scale pulse when food gets eaten, and I'm starting to think through the collision check for the next head position.

I'm settling on the collision rule: the tail only stays out of the check when the snake isn't eating, since a growing snake's tail doesn't move away. Then I'm moving on to how the keyboard input gets queued and compared against the last applied direction.

Next I'm working out the demo AI's pathing: a breadth-first search from the head toward the food across open cells, treating the body as blocked but the tail as free. If no route exists, I'll fall back to whichever safe move leaves the most open space, always avoiding a direct reversal.

Next I'm setting the game state flow: the demo runs first behind a banner asking for a key or click to start, and the first input switches it into real play.

I'm settling the demo mode's score handling: demo runs don't count toward the best score, and only real play updates it. Pressing a key or clicking during the demo switches to real play, and in play the arrows or WASD steer, P pauses, and Space or Enter restarts after a game over.

Now I'm settling the timing for input and pause: the loop accumulates elapsed time only while the game isn't paused, steps the snake each full tick, and uses the leftover fraction to position the drawing between ticks.

Now I'm sketching the particle burst for when food gets eaten and the pulsing glow on the food itself. For the snake, I'm drawing each segment as a rounded piece joined to its neighbors with thick round-capped lines, so the body looks continuous and shifts color from head to tail.

Still writing the snake's rendering: each joint gets a circle in the same color so the body looks continuous. The head is a brighter circle with eyes angled along the current direction, and everything is placed by interpolating between the previous and current positions.

I'm settling the responsive sizing: the canvas takes the smaller of the window width (capped at 600px, minus margins) and the height left after the header, with a 260px floor so it stays usable on small screens.

Now I'm settling the initial snake at length four in the center heading right, with the food spawned on a random free cell, and the demo AI's breadth-first search skipping the tail since it will move away.

I'm working out the demo AI's fallback: if breadth-first search finds no path to the food, it just takes the first safe non-reversing move, and it accepts that the snake will eventually trap itself and restart. Next I'm settling the body blocking, treating every segment except the tail as occupied, since the tail will move out of the way when the snake eats.

I'm setting up the keyboard handling now: any key or a click during the demo starts real play, and the arrow keys and WASD steer while the page scroll gets suppressed.

Now I'm settling how a demo death restarts: rather than waiting on a timer, I'll reset the demo straight away so the snake plays again without a pause.

I'm settling the game loop's pause handling and death flow: the step is skipped while the snake is frozen after a crash, and the demo just resets after a short delay.

I'm setting up the state flags now: a mode that's demo, play, or over, plus a paused flag that only matters during play and a frozen flag for when the snake has died and is waiting. The frame loop only advances the simulation when neither paused nor frozen, accumulating elapsed time into fixed ticks and using the leftover fraction to position the drawing.

Now I'm working through the step logic: in demo mode the AI picks a direction first, then any queued turn is applied, and I check whether the next head cell is out of bounds or hits the body, with the tail only counted as safe when the snake isn't eating. If it collides, the snake dies; otherwise I save the previous positions for interpolation, move the head forward, and drop the tail unless food was eaten, which adds to the score, triggers a burst, spawns new food, and speeds up the tick.

Still working out the death handling: if the snake crashes during the demo, it freezes and the demo resets after a short delay, but in real play it goes to the game over state and records the best score.

Still working out the demo AI: a breadth-first search over the grid, treating the body as blocked except the tail, to find a route to the food. Once a path is found, I'll take just its first step as the next move.

Now I'm picking the next step for the demo's fallback, choosing a random safe move since the path logic already covered the food. I'm also noting that the queued direction gets applied to the step as usual. For food placement, I'll pick a random free cell and fall back to scanning the grid if random tries don't land on one.

Now I'm setting up the HTML scaffold: a header with the score and best stats, then a board wrapper holding the canvas and a hidden overlay card for the start, pause, and game-over messages.

Still writing the page markup: a short hint line under the board, and a banner-style overlay for the demo start prompt, with a centered card for pause and game-over messages.

I'm settling the overlay and click handling: the overlay ignores pointer input so clicks land on the board itself, and keyboard input is picked up at the window level. Then I'm sizing the canvas to the smaller of the window width (capped at 560px, minus padding) and the height left after the header, with a 240px floor.

Still settling the layout: the canvas gets sized to the smaller of the window width (capped at 560px) and the leftover height after the header, with the body centered and horizontal overflow hidden.

Still writing the render pass: I'm clearing the canvas, filling the background, and drawing faint grid lines, then the food as a pulsing glowing circle. For the snake, I'm interpolating each segment between its previous and current positions.

Still writing the render pass: I'm drawing the snake body from tail to head so the head overlaps the segments behind it, with each segment colored and drawn as a thick line between neighboring points.

Still writing the render pass: I'm drawing each body segment as a thick rounded line from tail toward head, colored along a gradient, then placing circles at each segment point.

Still writing the head's rendering: a brighter green circle at the front of the snake, with eyes placed to either side of its facing direction and small pupils offset forward, plus a soft green glow behind it.

Still working through the particle and score details now: bursts get spawned at the food or snake position, and the score label gets a quick pop each time it changes. I'm also tracking the best score, updating it whenever the snake eats during real play and again when the game ends.

Writing the keyboard handler now: arrow keys and WASD map to directions, and Space, Enter, or P are recognized too, with the default page behavior suppressed for those keys. In demo mode the first keypress starts real play, and on the game-over screen Space or Enter restarts it.

Now I'm handling the arrow-key and WASD input: each direction gets queued only if it isn't a repeat of the last planned move or a direct reversal of it, and the queue is capped at three turns. Pressing P is the only thing that toggles pause during play, and a queued sequence like up then left just plays out one step per tick.

Now I'm handling the demo's end-of-life case: if the snake crashes during the demo, it freezes and resets after a short delay.

Now I'm wiring up the demo-to-play transition: a timed-out demo reset only fires if the mode is still demo, so starting play mid-timeout keeps the new game intact. I'm also adding the overlay helper, with a bottom-center banner for the start prompt and a centered card for everything else, then the game-over text showing the score and a restart hint.

Writing the game loop's key input and layout helpers now.

Now I'm setting up the initial snake at the center of the grid, moving right, and placing food on a random free cell.

Now I'm writing the step function: first the demo AI picks a direction if needed, then any queued turn gets applied. From there I work out the next head cell and check whether it's food, out of bounds, or hitting the body, with the tail allowed to move away when the snake isn't eating.

Now I'm finishing the step logic: if the snake eats, the score goes up and the food respawns, with the eaten cell as the burst origin, and the best score updates during real play. If it isn't eating, the tail gets dropped. Collision checks only exclude the tail when the snake isn't growing.

Still writing the death handling: the snake's position is saved, a burst goes off at the head, and in demo mode the game resets after a short delay, while in real play the mode switches to game over and the best score updates. Next I'm resetting the game state and starting on the demo AI's blocked-cell set.

Still working out the demo's pathfinding: I'm building a set of blocked cells from the body (leaving out the tail, which will move away), then running a breadth-first search from the head toward the food, tracking each cell's parent so the path can be traced back.

Still working out the demo's pathfinding: I'm walking back from the food through each cell's recorded parent until I reach the head, so the first step of that route becomes the next direction.

Still writing the path walk-back: I'm following each cell's recorded parent from the food until I reach the cell directly beside the head, and that cell gives the next direction.

Still working out the demo's fallback: when no path to the food exists, I'm scoring each safe direction by how many open neighboring cells it leads to and picking the best one, and if nothing is safe the snake just keeps going and will crash.

Still writing the particle code: the particles fade out as they age, and I'm giving each burst a random spread and a short lifetime, spawned at the center of the cell where the event happened.

Still writing the frame loop: I'm clamping the elapsed time, advancing the simulation in fixed ticks unless paused or frozen, then updating particles and rendering with interpolation.

Still working on the overlay helper: the showOverlay function fills in the banner or card with the given HTML and class, and I'm making sure the HUD updates along with it.

Still writing the overlay: the helper swaps in new HTML and class names, and for the game-over card I'm adding the final score, a new-best tag when it applies, and a restart hint.

Still wrapping up the canvas: clicking the board starts play in either demo or game-over mode, and the keyboard handler ignores P on the game-over screen. Space is suppressed so the page doesn't scroll, and I'm moving on to the board's gradient background and rounded border.

Still writing the head: I'm drawing a green circle with two eyes that point in the direction of travel, and every so often a short red tongue flicks out.

Still writing the head: the body gets drawn in two passes, lines first from tail toward head and then the circles, with the head drawn last so it sits on top.

Still writing the draw code: the body lines and rounded joints are already set, and I'm adding a glow on the head only to keep the body cheap to draw. Now I'm starting on the food, placing it at the center of its cell and sizing the radius to pulse slightly.

Still writing the food: I'm drawing it at the center of its cell with a pulsing radius and a glow, adding a small highlight, and topping it with a green leaf.

Still writing the page styles: I'm setting up the dark radial background, the centered flex layout, and a narrow column for the game and header.

Still writing the page styles: I'm setting the app width to match the board and laying out the header with the title and stat boxes, plus a relative position on the board.

Still writing the page styles: I'm finishing the canvas border and shadow, then the overlay layout, where a banner pins to the bottom of the board and the pill gets a thin green border.

Still writing the page styles: the pill gets rounded corners and a pulsing animation, the card gets a blurred dark background, and I'm setting up the heading, big score, and hint text sizes.

Now I'm wrapping up the overlay and page styling: the modal gets a light dimmed backdrop, and the hint line spells out the controls for steering, pausing, and restarting. Last, I'm making sure the layout runs before the first frame and that a resize listener keeps the board sized correctly.