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
58.2 s
Cost
$0.0073
Output tokens
14,393
Spent thinking
5,599 tokens
Speed
255.7 tok/s
Console errors
0
First token after
1.9 s
First visible text after
32.3 s
Prompt tokens
750
Code size
17.8 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"low"}
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 · 674 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>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    height: 100%;
    background: radial-gradient(ellipse at top, #1b2440 0%, #0a0e1a 70%);
    color: #e6f1ff;
    font-family: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
    overflow: hidden;
  }
  body {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 12px;
  }
  h1 {
    font-size: 28px;
    letter-spacing: 6px;
    font-weight: 300;
    color: #7ef0c0;
    text-shadow: 0 0 18px rgba(126, 240, 192, 0.55);
  }
  .hud {
    display: flex;
    gap: 12px;
    width: 100%;
    max-width: 620px;
    justify-content: space-between;
  }
  .card {
    flex: 1;
    background: rgba(255,255,255,0.05);
    border: 1px solid rgba(126,240,192,0.18);
    border-radius: 12px;
    padding: 8px 14px;
    text-align: center;
    backdrop-filter: blur(4px);
  }
  .card .label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 2px;
    opacity: 0.6;
  }
  .card .value {
    font-size: 24px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }
  .board {
    position: relative;
    border-radius: 14px;
    padding: 6px;
    background: linear-gradient(145deg, rgba(126,240,192,0.25), rgba(90,140,255,0.15));
    box-shadow: 0 20px 60px rgba(0,0,0,0.55), 0 0 40px rgba(126,240,192,0.12);
  }
  canvas {
    display: block;
    border-radius: 10px;
    background: #0d1426;
    cursor: pointer;
  }
  .overlay {
    position: absolute;
    inset: 6px;
    border-radius: 10px;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    background: rgba(5, 8, 16, 0.72);
    backdrop-filter: blur(3px);
    text-align: center;
    padding: 16px;
    pointer-events: none;
  }
  .overlay.show { display: flex; }
  .overlay h2 {
    font-size: 30px;
    font-weight: 600;
    letter-spacing: 3px;
  }
  .overlay .big {
    font-size: 40px;
    font-weight: 700;
    color: #7ef0c0;
    font-variant-numeric: tabular-nums;
  }
  .overlay p { opacity: 0.75; font-size: 15px; }
  .hint {
    position: absolute;
    left: 50%;
    bottom: 18px;
    transform: translateX(-50%);
    background: rgba(126,240,192,0.15);
    border: 1px solid rgba(126,240,192,0.45);
    color: #bfffe3;
    padding: 8px 16px;
    border-radius: 999px;
    font-size: 14px;
    white-space: nowrap;
    animation: pulse 1.6s ease-in-out infinite;
    pointer-events: none;
  }
  .hint.hidden { display: none; }
  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
  }
  .controls {
    font-size: 12px;
    opacity: 0.55;
    text-align: center;
    max-width: 620px;
  }
  kbd {
    display: inline-block;
    padding: 1px 6px;
    border: 1px solid rgba(255,255,255,0.3);
    border-radius: 4px;
    font-family: inherit;
    margin: 0 2px;
  }
  @media (max-width: 420px) {
    h1 { font-size: 20px; letter-spacing: 4px; }
    .card .value { font-size: 18px; }
    .overlay h2 { font-size: 22px; }
    .overlay .big { font-size: 30px; }
    .hint { font-size: 12px; white-space: normal; text-align: center; width: 85%; }
  }
</style>
</head>
<body>
  <h1>SNAKE</h1>

  <div class="hud">
    <div class="card">
      <div class="label">Score</div>
      <div class="value" id="score">0</div>
    </div>
    <div class="card">
      <div class="label">Best (session)</div>
      <div class="value" id="best">0</div>
    </div>
  </div>

  <div class="board" id="board">
    <canvas id="c"></canvas>
    <div class="overlay" id="overlay">
      <h2 id="ovTitle">GAME OVER</h2>
      <div class="big" id="ovScore">0</div>
      <p id="ovSub">Press Space or Enter to restart</p>
    </div>
    <div class="hint" id="hint">Click or press a key to start</div>
  </div>

  <div class="controls">
    Move: <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> or arrow keys &nbsp;·&nbsp;
    Pause: <kbd>P</kbd> &nbsp;·&nbsp; Restart: <kbd>Space</kbd> / <kbd>Enter</kbd>
  </div>

<script>
(() => {
  const COLS = 22;
  const ROWS = 22;
  const BASE_TICK = 140;
  const MIN_TICK = 70;
  const MAX_QUEUE = 3;

  const canvas = document.getElementById('c');
  const ctx = canvas.getContext('2d');
  const boardEl = document.getElementById('board');
  const overlay = document.getElementById('overlay');
  const ovTitle = document.getElementById('ovTitle');
  const ovScore = document.getElementById('ovScore');
  const ovSub = document.getElementById('ovSub');
  const hint = document.getElementById('hint');
  const scoreEl = document.getElementById('score');
  const bestEl = document.getElementById('best');

  const KEYMAP = {
    arrowup: [0, -1], w: [0, -1],
    arrowdown: [0, 1], s: [0, 1],
    arrowleft: [-1, 0], a: [-1, 0],
    arrowright: [1, 0], d: [1, 0]
  };

  let dpr = window.devicePixelRatio || 1;
  let cell = 20;

  let state = 'demo'; // demo | playing | paused | over
  let snake = [];
  let prevSnap = [];
  let dir = { x: 1, y: 0 };
  let queue = [];
  let food = { x: 0, y: 0 };
  let score = 0;
  let best = 0;
  let tickMs = BASE_TICK;
  let acc = 0;
  let particles = [];
  let shake = 0;
  let deathTimer = null;
  let time = 0;
  let lastNow = performance.now();

  function key(p) { return p.x + ',' + p.y; }

  function resize() {
    const avail = Math.min(window.innerWidth - 40, window.innerHeight - 230, 620);
    const size = Math.max(240, Math.floor(avail));
    cell = Math.floor(size / COLS);
    const w = cell * COLS;
    const h = cell * ROWS;
    canvas.style.width = w + 'px';
    canvas.style.height = h + 'px';
    canvas.width = Math.round(w * dpr);
    canvas.height = Math.round(h * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  function freeCells() {
    const occ = new Set(snake.map(key));
    const list = [];
    for (let x = 0; x < COLS; x++) {
      for (let y = 0; y < ROWS; y++) {
        if (!occ.has(x + ',' + y)) list.push({ x, y });
      }
    }
    return list;
  }

  function placeFood() {
    const list = freeCells();
    if (list.length === 0) { food = { x: -1, y: -1 }; return; }
    food = list[Math.floor(Math.random() * list.length)];
  }

  function resetSnake() {
    const cx = Math.floor(COLS / 2), cy = Math.floor(ROWS / 2);
    snake = [
      { x: cx, y: cy },
      { x: cx - 1, y: cy },
      { x: cx - 2, y: cy },
      { x: cx - 3, y: cy }
    ];
    prevSnap = snake.map(s => ({ ...s }));
    dir = { x: 1, y: 0 };
    queue = [];
    score = 0;
    tickMs = BASE_TICK;
    acc = 0;
    particles = [];
    shake = 0;
    placeFood();
    updateHud();
  }

  function updateHud() {
    scoreEl.textContent = score;
    bestEl.textContent = best;
  }

  function startDemo() {
    clearTimeout(deathTimer);
    resetSnake();
    state = 'demo';
    refreshUI();
  }

  function startGame() {
    clearTimeout(deathTimer);
    resetSnake();
    state = 'playing';
    refreshUI();
  }

  function refreshUI() {
    if (state === 'over') {
      overlay.classList.add('show');
      ovTitle.textContent = 'GAME OVER';
      ovScore.textContent = score;
      ovSub.textContent = 'Press Space or Enter to restart';
    } else if (state === 'paused') {
      overlay.classList.add('show');
      ovTitle.textContent = 'PAUSED';
      ovScore.textContent = score;
      ovSub.textContent = 'Press P to resume';
    } else {
      overlay.classList.remove('show');
    }
    hint.classList.toggle('hidden', state !== 'demo');
  }

  function setDir(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; // no reversing
    if (queue.length < MAX_QUEUE) queue.push(d);
  }

  // Demo AI: BFS to food avoiding body, fallback to any safe move
  function aiDir() {
    const dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
    const occ = new Set(snake.slice(0, -1).map(key));
    const head = snake[0];
    const startK = key(head);
    const prev = new Map();
    prev.set(startK, null);
    const q = [head];
    let found = false;
    let qi = 0;
    while (qi < q.length) {
      const c = q[qi++];
      if (c.x === food.x && c.y === food.y) { found = true; break; }
      for (const d of dirs) {
        const n = { x: c.x + d.x, y: c.y + d.y };
        if (n.x < 0 || n.y < 0 || n.x >= COLS || n.y >= ROWS) continue;
        const k = key(n);
        if (prev.has(k) || occ.has(k)) continue;
        prev.set(k, c);
        q.push(n);
      }
    }
    if (found) {
      let c = food;
      let p = prev.get(key(c));
      while (p && key(p) !== startK) {
        c = p;
        p = prev.get(key(c));
      }
      return { x: c.x - head.x, y: c.y - head.y };
    }
    // fallback: any safe move
    const safe = dirs.filter(d => {
      if (d.x === -dir.x && d.y === -dir.y) return false;
      const n = { x: head.x + d.x, y: head.y + d.y };
      return n.x >= 0 && n.y >= 0 && n.x < COLS && n.y < ROWS && !occ.has(key(n));
    });
    if (safe.length) return safe[Math.floor(Math.random() * safe.length)];
    return dir;
  }

  function die() {
    state = 'over';
    shake = 12;
    burst(snake[0].x, snake[0].y, '#ff6b6b', 26);
    if (score > best) best = score;
    updateHud();
    if (state === 'over') {
      // fall through to UI update below
    }
    // remember whether this death came from demo
    refreshUI();
  }

  let dying_from_demo = false;

  function tick() {
    if (state === 'demo') {
      queue = [aiDir()];
    }
    prevSnap = snake.map(s => ({ ...s }));

    if (queue.length) dir = queue.shift();

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

    if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) {
      return gameOver();
    }
    // tail moves away unless eating, so exclude it from collision check
    for (let i = 0; i < snake.length - 1; i++) {
      if (snake[i].x === nx && snake[i].y === ny) return gameOver();
    }

    const eat = nx === food.x && ny === food.y;
    snake.unshift({ x: nx, y: ny });

    if (eat) {
      score += 10;
      if (score > best) best = score;
      updateHud();
      burst(food.x, food.y, '#ffd166', 22);
      tickMs = Math.max(MIN_TICK, BASE_TICK - Math.floor(score / 10) * 3);
      placeFood();
    } else {
      snake.pop();
    }
  }

  function gameOver() {
    const wasDemo = state === 'demo';
    state = 'over';
    shake = 12;
    burst(snake[0].x, snake[0].y, '#ff6b6b', 26);
    updateHud();
    refreshUI();
    if (wasDemo) {
      // demo mode: restart automatically
      clearTimeout(deathTimer);
      deathTimer = setTimeout(() => {
        if (state === 'over') startDemo();
      }, 1400);
      overlay.classList.remove('show');
      state = 'over';
      // keep 'over' state briefly so the death is visible
      hint.classList.remove('hidden');
      hint.classList.add('hidden');
    }
  }

  function burst(cx, cy, color, n) {
    for (let i = 0; i < n; i++) {
      const a = Math.random() * Math.PI * 2;
      const sp = 1.5 + Math.random() * 4.5;
      particles.push({
        x: cx + 0.5, y: cy + 0.5,
        vx: Math.cos(a) * sp,
        vy: Math.sin(a) * sp - 1,
        life: 500 + Math.random() * 400,
        max: 900,
        r: 1.5 + Math.random() * 2.5,
        color
      });
    }
  }

  function updateParticles(dt) {
    for (const p of particles) {
      p.x += p.vx * dt / 1000 * 1.0;
      p.y += p.vy * dt / 1000 * 1.0;
      p.vy += 6 * dt / 1000;
      p.vx *= 0.97;
      p.life -= dt;
    }
    particles = particles.filter(p => p.life > 0);
  }

  function lerp(a, b, t) { return a + (b - a) * t; }

  function fromPos(i) {
    // new[i] came from old[i-1] (or old[0] for head)
    if (i === 0) return prevSnap[0] || snake[0];
    return prevSnap[i - 1] || snake[i];
  }

  function draw(t) {
    const W = COLS * cell, H = ROWS * cell;
    ctx.save();
    if (shake > 0) {
      ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
    }

    // background
    const bg = ctx.createLinearGradient(0, 0, W, H);
    bg.addColorStop(0, '#0f1830');
    bg.addColorStop(1, '#0a1020');
    ctx.fillStyle = bg;
    ctx.fillRect(-10, -10, W + 20, H + 20);

    // grid
    ctx.strokeStyle = 'rgba(126,240,192,0.06)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    for (let x = 0; x <= COLS; x++) {
      ctx.moveTo(x * cell + 0.5, 0);
      ctx.lineTo(x * cell + 0.5, H);
    }
    for (let y = 0; y <= ROWS; y++) {
      ctx.moveTo(0, y * cell + 0.5);
      ctx.lineTo(W, y * cell + 0.5);
    }
    ctx.stroke();

    // food
    if (food.x >= 0) {
      const fx = (food.x + 0.5) * cell, fy = (food.y + 0.5) * cell;
      const pulse = 1 + 0.08 * Math.sin(time / 180);
      const r = cell * 0.32 * pulse;
      ctx.save();
      ctx.shadowColor = '#ff6b8b';
      ctx.shadowBlur = cell * 0.9;
      const g = ctx.createRadialGradient(fx - r * 0.3, fy - r * 0.3, r * 0.1, fx, fy, r);
      g.addColorStop(0, '#ffd1dc');
      g.addColorStop(0.5, '#ff5c7c');
      g.addColorStop(1, '#c92a4f');
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(fx, fy, r, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    // snake: compute interpolated centers
    const n = snake.length;
    const centers = [];
    for (let i = 0; i < n; i++) {
      const from = fromPos(i);
      const to = snake[i];
      const px = lerp(from.x, to.x, t);
      const py = lerp(from.y, to.y, t);
      centers.push({ x: (px + 0.5) * cell, y: (py + 0.5) * cell });
    }

    const dead = state === 'over';
    const bodyW = cell * 0.82;

    // body connections (tail to head so head overlaps)
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    for (let i = n - 1; i >= 1; i--) {
      const a = centers[i], b = centers[i - 1];
      const f = i / n;
      const hue = 150 + f * 30;
      ctx.strokeStyle = `hsl(${hue}, 65%, ${dead ? 40 : 50 - f * 12}%)`;
      ctx.lineWidth = bodyW;
      ctx.beginPath();
      ctx.moveTo(a.x, a.y);
      ctx.lineTo(b.x, b.y);
      ctx.stroke();
    }
    // subtle inner highlight along body
    ctx.strokeStyle = 'rgba(255,255,255,0.12)';
    ctx.lineWidth = bodyW * 0.3;
    ctx.beginPath();
    for (let i = 0; i < n; i++) {
      if (i === 0) ctx.moveTo(centers[i].x, centers[i].y);
      else ctx.lineTo(centers[i].x, centers[i].y);
    }
    ctx.stroke();

    // joints (circles so body looks rounded everywhere)
    for (let i = n - 1; i >= 1; i--) {
      const f = i / n;
      const hue = 150 + f * 30;
      ctx.fillStyle = `hsl(${hue}, 65%, ${dead ? 40 : 50 - f * 12}%)`;
      ctx.beginPath();
      ctx.arc(centers[i].x, centers[i].y, bodyW / 2, 0, Math.PI * 2);
      ctx.fill();
    }

    // head
    const hd = centers[0];
    const dx = dir.x, dy = dir.y;
    const headR = cell * 0.5;
    const headColor = dead ? '#ff6b6b' : '#b6ff6b';
    ctx.save();
    ctx.shadowColor = dead ? 'rgba(255,80,80,0.8)' : 'rgba(182,255,107,0.7)';
    ctx.shadowBlur = cell * 0.6;
    const hg = ctx.createRadialGradient(hd.x - headR * 0.3, hd.y - headR * 0.3, headR * 0.1, hd.x, hd.y, headR);
    hg.addColorStop(0, dead ? '#ffd0d0' : '#eaffc4');
    hg.addColorStop(0.6, headColor);
    hg.addColorStop(1, dead ? '#a02020' : '#4fb83a');
    ctx.fillStyle = hg;
    ctx.beginPath();
    ctx.arc(hd.x, hd.y, headR, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    // eyes facing direction
    const px = -dy, py = dx; // perpendicular
    const eyeFwd = cell * 0.18;
    const eyeSide = cell * 0.2;
    const eyeR = cell * 0.13;
    const pupilR = cell * 0.065;
    for (const s of [-1, 1]) {
      const ex = hd.x + dx * eyeFwd + px * eyeSide * s;
      const ey = hd.y + dy * eyeFwd + py * eyeSide * s;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(ex, ey, eyeR, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = dead ? '#300' : '#0b1020';
      ctx.beginPath();
      ctx.arc(ex + dx * eyeR * 0.45, ey + dy * eyeR * 0.45, pupilR, 0, Math.PI * 2);
      ctx.fill();
    }

    // particles
    ctx.save();
    for (const p of particles) {
      const a = Math.max(0, p.life / p.max);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 8;
      ctx.beginPath();
      ctx.arc(p.x * cell, p.y * cell, p.r * (0.5 + a * 0.5), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();

    ctx.restore();
  }

  function frame(now) {
    const dt = Math.min(now - lastNow, 100);
    lastNow = now;
    time += dt;

    if (state === 'playing' || state === 'demo') {
      acc += dt;
      while (acc >= tickMs && (state === 'playing' || state === 'demo')) {
        acc -= tickMs;
        tick();
      }
    }
    if (shake > 0) shake = Math.max(0, shake - dt * 0.05);

    updateParticles(dt);

    let t = 1;
    if (state === 'playing' || state === 'demo') t = Math.min(1, acc / tickMs);
    draw(t);
    requestAnimationFrame(frame);
  }

  // input
  window.addEventListener('keydown', (e) => {
    if (e.ctrlKey || e.metaKey || e.altKey) return;
    const k = e.key.toLowerCase();
    const isDir = Object.prototype.hasOwnProperty.call(KEYMAP, k);
    if (isDir || k === ' ' || k === 'enter' || k === 'p') e.preventDefault();

    if (state === 'demo') {
      startGame();
      if (isDir) setDir({ x: KEYMAP[k][0], y: KEYMAP[k][1] });
      return;
    }
    if (state === 'over') {
      if (k === ' ' || k === 'enter') startGame();
      return;
    }
    if (k === 'p') {
      state = state === 'paused' ? 'playing' : 'paused';
      refreshUI();
      return;
    }
    if (state === 'playing' && isDir) {
      setDir({ x: KEYMAP[k][0], y: KEYMAP[k][1] });
    }
  });

  canvas.addEventListener('click', () => {
    if (state === 'demo') startGame();
  });
  boardEl.addEventListener('click', () => {
    if (state === 'demo') startGame();
  });

  window.addEventListener('resize', resize);

  // boot
  resize();
  resetSnake();
  best = 0;
  updateHud();
  startDemo();
  requestAnimationFrame((now) => { lastNow = now; frame(now); });
})();
</script>
</body>
</html>
Reasoning trace (5,599 tokens)
I need a single polished HTML file with a demo mode where the AI plays automatically behind a "Click or press a key to start" prompt. I want the movement to interpolate between cells for smoothness, buffer a couple of quick direction presses while validating them against the last queued direction, and add a game over overlay with Space/Enter to restart and P to pause, plus particles and a session best score kept in memory.

I'm sketching out the rest of the layout: a responsive canvas with a grid around 20x20, and each snake segment drawn by interpolating from its previous tick position to its current one for smooth motion. I'm also settling the head and body rendering and a tick speed that starts around 130ms and speeds up with each food eaten, down to a 60ms floor.

I'm settling on the keyboard handling next: arrow keys and Space get intercepted so the page doesn't scroll, P toggles pause, and presses feed a direction queue capped at three, each checked against the last queued direction so reversals and repeats get dropped.

I'm working out the tick logic: the snake's previous positions get saved, then the queued direction is applied and the new head position is computed. I'm checking for wall or body collisions, and when the head lands on food the snake grows instead of dropping its tail.

Still working out the collision and growth logic. I'm checking the head against the body minus the tail, since the tail moves away, and I'm planning to handle the interpolation by matching each segment to its prior position so new segments don't jump in from nowhere.

Working out how the snake's segments map between ticks: the head moves from its old cell to the new one, and each body segment takes over the cell the segment ahead of it occupied. So for drawing, each segment should lerp from its old position, which means the body indices shift by one, and any new tail segment from growth needs its own handling.

Writing the particle system and finishing the head drawing...

Next I'm setting the canvas to size responsively from the viewport, with devicePixelRatio scaling for crisp drawing. Then I'll handle the first input: any key or click during the demo should start a real game, and arrow keys should also set the direction right away.

Writing the resize logic now: the canvas size is the smaller of the window width, the window height minus the HUD space, and a 620px cap, with each cell sized to fit the grid.

Still writing the canvas sizing and key mapping...

Now I'm writing the keyboard handler: arrow keys and WASD map to directions, and the handler checks the key state, so it can start a game from demo, restart from game over, toggle pause with P, and queue direction presses while playing.

Writing the demo AI's path-finding now: a BFS from the head to the food, treating every snake cell except the tail as blocked, and taking the first step of the shortest route. I'm also making sure it never picks a move that doubles back onto the second segment.

Now I'm writing the demo AI's pathfinding: a BFS from the head to the food, with every snake cell except the tail treated as blocked, taking the first step of the shortest route and avoiding any move onto the second segment. If no path exists it falls back to a safe neighbor, and when the AI is in demo mode I'm feeding its chosen direction through the same queue as player input. Then I'm starting the game loop with a timestamp and an accumulator that drives the fixed tick.

I'm working through the tick function now: saving the previous snake positions, applying the next queued direction, and checking the new head for wall or body collisions. If it lands on food, the snake grows and I'll spawn particles and place new food; otherwise the tail pops off.

I'm noting that eating keeps the tail in place, and that the previous snake positions need clearing each tick. On death the state goes to over, and in demo mode it will restart after a short delay. The best score is a session variable updated as the score rises, and startGame will place the snake at the center, four cells long, facing right.

Next I'm setting up the start and game-over flow: startGame resets the snake to four cells facing right at a 140ms tick, and startDemo does the same but in demo state. Then I'm laying out the overlay DOM and drawing the food as a pulsing glowing circle on a dark gradient background.

Still writing the snake drawing: each segment gets interpolated between its old and new cell, with the body colored along a green hue ramp and joined by thick rounded lines.

Still writing the head and its eyes: the eyes sit forward of center and to either side, aimed along the current direction, and the head flashes red once the game is over.

Next I'm writing out the overlay and HUD markup, the canvas wrapper, and the controls footer, then sizing the canvas to fit the window.

Writing the BFS pathfinder for the demo AI now.

Now I'm working out how the AI finds food: a breadth-first search from the head, treating every body cell except the tail as blocked, then walking back from the food to find the first step. If no route exists, it falls back to a safe neighboring move.

Now I'm wrapping up the BFS logic: the occupied set leaves out the tail, so the AI can reuse that cell as it moves, and reversing is already ruled out because the second segment is blocked. Then I'll write out the final code.