Models 44

Claude Sonnet 5.5

by Anthropicreasoning 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 19 s
Cost
$0.123
Output tokens
12,192
Spent thinking
3,835 tokens
Speed
158.8 tok/s
Console errors
0
First token after
2.3 s
First visible text after
38.0 s
Prompt tokens
750
Code size
16.4 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-sonnet-5.5
Reasoning sent
model default
Max output
128,000 tokens
Generated
Nov 12, 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 · 392 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Snake</title>
<style>
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    background:radial-gradient(ellipse at 50% 30%,#14242e 0%,#0b1319 70%);
    color:#d7e6ea;font-family:"Segoe UI",system-ui,-apple-system,Roboto,sans-serif;
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:14px;overflow:hidden;user-select:none;-webkit-user-select:none;
  }
  .hud{display:flex;align-items:center;justify-content:space-between;gap:12px;width:var(--size)}
  h1{font-size:clamp(18px,3.2vw,26px);letter-spacing:.25em;font-weight:800;color:#6df2a0;text-shadow:0 0 18px rgba(80,255,160,.45)}
  .pills{display:flex;gap:10px}
  .pill{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.09);border-radius:12px;padding:6px 14px;min-width:84px;text-align:center}
  .pill small{display:block;font-size:10px;letter-spacing:.18em;color:#8aa3ab;text-transform:uppercase}
  .pill b{font-size:20px;font-variant-numeric:tabular-nums;display:inline-block;transition:transform .15s}
  .pill b.bump{transform:scale(1.35);color:#ffe27a}
  :root{--size:min(94vw,calc(100vh - 150px),640px)}
  .wrap{position:relative;width:var(--size);height:var(--size);border-radius:16px;
    box-shadow:0 0 0 2px rgba(109,242,160,.25),0 20px 60px rgba(0,0,0,.6),0 0 60px rgba(60,220,140,.12);touch-action:none}
  canvas{width:100%;height:100%;display:block;border-radius:16px}
  .overlay{position:absolute;inset:0;border-radius:16px;display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:10px;text-align:center;background:rgba(6,12,16,.55);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);
    padding:16px;transition:opacity .25s;cursor:pointer}
  .overlay.hidden{opacity:0;pointer-events:none}
  .overlay h2{font-size:clamp(28px,7vw,48px);letter-spacing:.14em;color:#6df2a0;text-shadow:0 0 24px rgba(80,255,160,.5)}
  .overlay h2.bad{color:#ff6b6b;text-shadow:0 0 24px rgba(255,80,80,.5)}
  .overlay .big{font-size:clamp(18px,4vw,26px);font-weight:700}
  .overlay .msg{font-size:clamp(14px,2.6vw,18px);color:#e9f5f0;animation:blink 1.4s ease-in-out infinite}
  .overlay .sub{font-size:13px;color:#8aa3ab;line-height:1.6}
  kbd{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.18);border-radius:5px;padding:1px 6px;font-size:12px}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.45}}
  .foot{font-size:12px;color:#6f8890;text-align:center;padding:0 10px}
  @media (max-height:520px){.foot{display:none}:root{--size:min(94vw,calc(100vh - 90px),640px)}}
</style>
</head>
<body>
  <div class="hud">
    <h1>SNAKE</h1>
    <div class="pills">
      <div class="pill"><small>Score</small><b id="score">0</b></div>
      <div class="pill"><small>Best</small><b id="best">0</b></div>
    </div>
  </div>
  <div class="wrap" id="wrap">
    <canvas id="c"></canvas>
    <div class="overlay" id="ov"></div>
  </div>
  <div class="foot"><kbd>←↑↓→</kbd> / <kbd>WASD</kbd> move &nbsp;·&nbsp; <kbd>P</kbd> pause &nbsp;·&nbsp; <kbd>Space</kbd>/<kbd>Enter</kbd> restart &nbsp;·&nbsp; swipe on touch</div>

<script>
(() => {
  const N = 24, CS = 28, W = N * CS;
  const cv = document.getElementById('c'), ctx = cv.getContext('2d');
  const scale = Math.min(2, window.devicePixelRatio || 1);
  cv.width = cv.height = W * scale;
  ctx.scale(scale, scale);

  const elScore = document.getElementById('score'), elBest = document.getElementById('best');
  const ov = document.getElementById('ov'), wrap = document.getElementById('wrap');

  const DIRS = { up:{x:0,y:-1}, down:{x:0,y:1}, left:{x:-1,y:0}, right:{x:1,y:0} };
  const KEYS = {
    ArrowUp:'up', KeyW:'up', ArrowDown:'down', KeyS:'down',
    ArrowLeft:'left', KeyA:'left', ArrowRight:'right', KeyD:'right'
  };

  let snake, prev, dir, queue, food, score = 0, best = 0;
  let state = 'ready';           // ready (demo) | playing | paused | over
  let demo = true, dead = false, deadT = 0, won = false;
  let acc = 0, particles = [], floaters = [], shakeT = 0, time = 0, ovShown = '';

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

  function reset(isDemo) {
    demo = isDemo;
    snake = [{x:9,y:12},{x:8,y:12},{x:7,y:12}];
    prev = snake.map(s => ({...s}));
    dir = DIRS.right; queue = [];
    score = 0; dead = false; deadT = 0; won = false; acc = 0;
    particles = []; floaters = []; shakeT = 0;
    spawnFood(); updateHud();
  }

  function spawnFood() {
    const occ = new Set(snake.map(s => s.y * N + s.x));
    const free = [];
    for (let i = 0; i < N * N; i++) if (!occ.has(i)) free.push(i);
    if (!free.length) { food = null; return false; }
    const k = free[Math.floor(Math.random() * free.length)];
    food = { x: k % N, y: Math.floor(k / N), born: time };
    return true;
  }

  function updateHud(bump) {
    elScore.textContent = score; elBest.textContent = best;
    if (bump) { elScore.classList.add('bump'); setTimeout(() => elScore.classList.remove('bump'), 150); }
  }

  function setDir(name) {
    const d = DIRS[name];
    const last = queue.length ? queue[queue.length - 1] : dir;
    if (eq(d, last) || opposite(d, last)) return;
    if (queue.length < 3) queue.push(d);
  }

  /* ---------- AI for demo mode ---------- */
  function aiDir() {
    const blocked = new Set();
    for (let i = 0; i < snake.length - 1; i++) blocked.add(snake[i].y * N + snake[i].x);
    const head = snake[0], tail = snake[snake.length - 1];
    const list = Object.values(DIRS);
    const inb = (x, y) => x >= 0 && y >= 0 && x < N && y < N;

    function bfs(target, extraBlock) {
      const seen = new Map(); const q = [];
      for (const d of list) {
        if (opposite(d, dir)) continue;
        const x = head.x + d.x, y = head.y + d.y, k = y * N + x;
        if (!inb(x, y) || blocked.has(k) || seen.has(k)) continue;
        seen.set(k, d); q.push({x, y});
      }
      while (q.length) {
        const c = q.shift(), fd = seen.get(c.y * N + c.x);
        if (c.x === target.x && c.y === target.y) return fd;
        for (const d of list) {
          const x = c.x + d.x, y = c.y + d.y, k = y * N + x;
          if (!inb(x, y) || blocked.has(k) || seen.has(k)) continue;
          seen.set(k, fd); q.push({x, y});
        }
      }
      return null;
    }
    function flood(sx, sy) {
      const seen = new Set([sy * N + sx]); const q = [{x:sx,y:sy}]; let n = 0;
      while (q.length) {
        const c = q.pop(); n++;
        for (const d of list) {
          const x = c.x + d.x, y = c.y + d.y, k = y * N + x;
          if (!inb(x, y) || blocked.has(k) || seen.has(k) || (x === head.x && y === head.y)) continue;
          seen.add(k); q.push({x, y});
        }
      }
      return n;
    }
    if (food) {
      const d = bfs(food);
      if (d && flood(head.x + d.x, head.y + d.y) >= snake.length + 2) return d;
    }
    const dt = bfs(tail);
    if (dt) return dt;
    let bestD = dir, bestN = -1;
    for (const d of list) {
      if (opposite(d, dir)) continue;
      const x = head.x + d.x, y = head.y + d.y;
      if (!inb(x, y) || blocked.has(y * N + x)) continue;
      const n = flood(x, y);
      if (n > bestN) { bestN = n; bestD = d; }
    }
    return bestD;
  }

  /* ---------- game step ---------- */
  function tick() {
    if (demo) dir = aiDir(); else if (queue.length) dir = queue.shift();
    const h = snake[0];
    const nh = { x: h.x + dir.x, y: h.y + dir.y };
    const eating = food && eq(nh, food);
    let hit = nh.x < 0 || nh.y < 0 || nh.x >= N || nh.y >= N;
    if (!hit) {
      const lim = eating ? snake.length : snake.length - 1;
      for (let i = 0; i < lim; i++) if (eq(snake[i], nh)) { hit = true; break; }
    }
    if (hit) { die(); return; }
    prev = snake.map(s => ({...s}));
    snake.unshift(nh);
    if (eating) {
      score += 10;
      burst(food.x, food.y);
      floaters.push({ x: (food.x + .5) * CS, y: (food.y + .5) * CS, t: 0 });
      if (!demo && score > best) best = score;
      updateHud(true);
      if (!spawnFood()) { won = true; die(); }
    } else snake.pop();
  }

  function die() {
    dead = true; deadT = 0; shakeT = .35; acc = 0;
    if (!demo) { state = 'over'; if (score > best) best = score; updateHud(); }
  }

  function burst(cx, cy) {
    const x = (cx + .5) * CS, y = (cy + .5) * CS;
    const cols = ['#ff5a5a', '#ff9a5a', '#ffe27a', '#6df2a0'];
    for (let i = 0; i < 22; i++) {
      const a = Math.random() * Math.PI * 2, s = 60 + Math.random() * 190;
      particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s,
        life: 0, max: .45 + Math.random() * .4, r: 2 + Math.random() * 3, c: cols[i % cols.length] });
    }
  }

  /* ---------- rendering ---------- */
  function rr(x, y, w, h, r) {
    ctx.beginPath(); ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
  }

  function drawBoard() {
    for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
      ctx.fillStyle = (x + y) & 1 ? '#122029' : '#0f1b23';
      ctx.fillRect(x * CS, y * CS, CS, CS);
    }
  }

  function drawFood() {
    if (!food) return;
    const pulse = 1 + Math.sin((time - food.born) * 6) * .07;
    const pop = Math.min(1, (time - food.born) * 5);
    const cx = (food.x + .5) * CS, cy = (food.y + .5) * CS, r = CS * .34 * pulse * pop;
    const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, CS * .9);
    g.addColorStop(0, 'rgba(255,90,90,.45)'); g.addColorStop(1, 'rgba(255,90,90,0)');
    ctx.fillStyle = g; ctx.fillRect(cx - CS, cy - CS, CS * 2, CS * 2);
    const b = ctx.createRadialGradient(cx - r * .35, cy - r * .35, r * .1, cx, cy, r);
    b.addColorStop(0, '#ff9b9b'); b.addColorStop(1, '#e0343f');
    ctx.fillStyle = b; ctx.beginPath(); ctx.arc(cx, cy, r, 0, 7); ctx.fill();
    ctx.fillStyle = '#6df2a0'; ctx.beginPath();
    ctx.ellipse(cx + r * .35, cy - r * 1.0, r * .45, r * .22, -.6, 0, 7); ctx.fill();
    ctx.strokeStyle = '#5a3b1e'; ctx.lineWidth = 1.6; ctx.beginPath();
    ctx.moveTo(cx, cy - r * .6); ctx.lineTo(cx, cy - r * 1.0); ctx.stroke();
  }

  function drawSnake(e) {
    const n = snake.length;
    const pts = snake.map((c, i) => {
      const p = prev[Math.min(i, prev.length - 1)];
      return { x: (p.x + (c.x - p.x) * e + .5) * CS, y: (p.y + (c.y - p.y) * e + .5) * CS };
    });
    ctx.lineCap = 'round'; ctx.lineJoin = 'round';
    // shadow
    ctx.strokeStyle = 'rgba(0,0,0,.35)'; ctx.lineWidth = CS * .8;
    ctx.beginPath(); pts.forEach((p, i) => i ? ctx.lineTo(p.x + 2, p.y + 3) : ctx.moveTo(p.x + 2, p.y + 3)); ctx.stroke();
    const flash = dead && Math.floor(deadT * 12) % 2 === 0;
    for (let i = n - 1; i > 0; i--) {
      const t = i / Math.max(1, n - 1);
      ctx.lineWidth = CS * (.8 - .22 * t);
      ctx.strokeStyle = flash ? '#ff8d8d' : `hsl(${150 - t * 18},${70 - t * 10}%,${50 - t * 16 + (i % 2) * 3}%)`;
      ctx.beginPath(); ctx.moveTo(pts[i].x, pts[i].y); ctx.lineTo(pts[i - 1].x, pts[i - 1].y); ctx.stroke();
    }
    // head
    const hp = pts[0];
    const hg = ctx.createRadialGradient(hp.x - 4, hp.y - 4, 2, hp.x, hp.y, CS * .55);
    hg.addColorStop(0, flash ? '#ffc0c0' : '#a8ffcb'); hg.addColorStop(1, flash ? '#ff6b6b' : '#26c46c');
    ctx.fillStyle = hg; ctx.beginPath(); ctx.arc(hp.x, hp.y, CS * .48, 0, 7); ctx.fill();
    const d = dir, px = -d.y, py = d.x;
    for (const s of [-1, 1]) {
      const ex = hp.x + d.x * CS * .14 + px * s * CS * .2, ey = hp.y + d.y * CS * .14 + py * s * CS * .2;
      ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(ex, ey, CS * .14, 0, 7); ctx.fill();
      ctx.fillStyle = dead ? '#c22' : '#10202a'; ctx.beginPath();
      ctx.arc(ex + d.x * CS * .05, ey + d.y * CS * .05, CS * .075, 0, 7); ctx.fill();
    }
  }

  function drawFx(dt) {
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i]; p.life += dt;
      if (p.life >= p.max) { particles.splice(i, 1); continue; }
      p.x += p.vx * dt; p.y += p.vy * dt; p.vx *= .93; p.vy *= .93;
      const a = 1 - p.life / p.max;
      ctx.globalAlpha = a; ctx.fillStyle = p.c;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r * a + .5, 0, 7); ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.font = '700 15px "Segoe UI",sans-serif'; ctx.textAlign = 'center';
    for (let i = floaters.length - 1; i >= 0; i--) {
      const f = floaters[i]; f.t += dt;
      if (f.t > .8) { floaters.splice(i, 1); continue; }
      ctx.globalAlpha = 1 - f.t / .8; ctx.fillStyle = '#ffe27a';
      ctx.fillText('+10', f.x, f.y - 14 - f.t * 30);
    }
    ctx.globalAlpha = 1;
  }

  /* ---------- overlay ---------- */
  function setOverlay() {
    let key = '';
    if (state === 'ready') key = 'ready';
    else if (state === 'paused') key = 'paused';
    else if (state === 'over' && deadT > .5) key = 'over' + score + best + won;
    if (key === ovShown) return;
    ovShown = key;
    if (!key) { ov.classList.add('hidden'); return; }
    ov.classList.remove('hidden');
    if (state === 'ready') ov.innerHTML =
      '<h2>SNAKE</h2><div class="msg">Click or press a key to start</div>' +
      '<div class="sub"><kbd>←↑↓→</kbd> / <kbd>WASD</kbd> to steer · <kbd>P</kbd> to pause<br>Demo mode: the AI is playing</div>';
    else if (state === 'paused') ov.innerHTML =
      '<h2>PAUSED</h2><div class="msg">Press P or click to resume</div>';
    else ov.innerHTML =
      '<h2 class="bad">' + (won ? 'YOU WIN!' : 'GAME OVER') + '</h2>' +
      '<div class="big">Score: ' + score + '</div><div class="sub">Best: ' + best + '</div>' +
      '<div class="msg">Press Space or Enter to restart</div>';
  }

  /* ---------- main loop ---------- */
  let last = performance.now();
  function frame(now) {
    const dt = Math.min(.05, (now - last) / 1000); last = now; time += dt;
    const running = state === 'playing' || state === 'ready';
    const interval = demo ? 70 : Math.max(70, 125 - (score / 10) * 2.5);
    if (running && !dead) {
      acc += dt * 1000;
      while (acc >= interval && !dead) { acc -= interval; tick(); }
    }
    if (dead) {
      deadT += dt;
      if (demo && deadT > 1) reset(true);
    }
    const e = dead || state === 'paused' && false ? 1 : Math.min(1, acc / interval);

    ctx.save();
    ctx.clearRect(0, 0, W, W);
    if (shakeT > 0) { shakeT -= dt; const m = shakeT * 22; ctx.translate((Math.random() - .5) * m, (Math.random() - .5) * m); }
    drawBoard(); drawFood(); drawSnake(e); drawFx(dt);
    ctx.restore();
    setOverlay();
    requestAnimationFrame(frame);
  }

  /* ---------- input ---------- */
  function startGame(first) {
    reset(false); state = 'playing'; ovShown = '';
    if (first) setDir(first);
  }
  function primaryAction() {
    if (state === 'ready') startGame();
    else if (state === 'over' && deadT > .3) startGame();
    else if (state === 'paused') state = 'playing';
  }

  window.addEventListener('keydown', ev => {
    const code = ev.code;
    const isDir = code in KEYS;
    if (isDir || code === 'Space' || code === 'Enter') ev.preventDefault();
    if (ev.repeat && !isDir) return;
    if (state === 'ready') {
      if (['ShiftLeft','ShiftRight','ControlLeft','ControlRight','AltLeft','AltRight','MetaLeft','MetaRight','Tab'].includes(code)) return;
      startGame(isDir && KEYS[code] !== 'left' ? KEYS[code] : null);
      return;
    }
    if (code === 'KeyP') {
      if (state === 'playing') state = 'paused'; else if (state === 'paused') state = 'playing';
      return;
    }
    if (state === 'over') {
      if ((code === 'Space' || code === 'Enter') && deadT > .3) startGame();
      return;
    }
    if (state === 'paused') {
      if (code === 'Space' || code === 'Enter') state = 'playing';
      return;
    }
    if (isDir && state === 'playing') setDir(KEYS[code]);
  });

  let sx = 0, sy = 0, down = false;
  wrap.addEventListener('pointerdown', e => { down = true; sx = e.clientX; sy = e.clientY; });
  window.addEventListener('pointerup', e => {
    if (!down) return; down = false;
    const dx = e.clientX - sx, dy = e.clientY - sy;
    if (Math.max(Math.abs(dx), Math.abs(dy)) < 24) {
      if (e.target === wrap || wrap.contains(e.target)) primaryAction();
      return;
    }
    const name = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'down' : 'up');
    if (state === 'ready') startGame(name !== 'left' ? name : null);
    else if (state === 'playing') setDir(name);
  });
  window.addEventListener('blur', () => { if (state === 'playing') state = 'paused'; });

  reset(true);
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (3,835 tokens)
I'm planning a polished Snake game with an AI demo mode and a start overlay that says "Click or press a key to start." The canvas will be a 24x24 grid drawn at a fixed internal resolution, and I'm weighing how to scale it responsively with the CSS size.

Writing the stylesheet, I'm sizing the canvas with a responsive width capped at 640px and a square aspect ratio. Next I'm working out a smooth-movement approach: a fixed logic tick with the render interpolating between the previous and current snake positions, so each segment glides from where it was to where it is now.

Next I'm setting up the snake's rendering: a thick rounded body drawn through interpolated segment positions with a darker outline, and a larger head whose eyes follow the current direction. Input gets a queue of up to three turns, each checked against the last queued direction so reversals and repeats are rejected. Collision handling covers the walls and the body, with the tail excluded when the snake isn't growing, and the death state freezes the snake in place with the head at its last valid cell.

Next I'm wiring up the particle burst when the snake eats, the pulsing food, and the checkerboard grid. For demo mode, I'm having the AI path toward food with BFS, falling back to a flood-fill check on reachable space or following the tail, and restarting after a death. I'm still weighing whether the best score should update during demo play too.

I'm settling the game states now: a demo state where the AI plays behind the start overlay, a playing state, a paused state toggled with P, and a game-over state. Pressing a key from the demo starts a real game, and I'm considering whether an arrow key should also set the initial direction. The tick starts around 110ms and speeds up slightly with score, down to 70ms, with demo mode fixed at 70ms.

Still wiring up the overlay and click handling for the start, over, and paused states...

Still working out the snake rendering: each segment's position gets interpolated between its previous and current spot, and the body is drawn as a thick rounded path.

Still working out the snake's body: I'm drawing it as a series of short round-capped line segments from tail to head, each with its own green shade, and tapering the width toward the tail so the head overlaps the body cleanly.

Now I'm settling the head and eyes: a larger circle with the eyes offset along the current direction, and the food as a pulsing red apple with a small leaf.

Now I'm handling the edge case where no free cells remain, which ends the game as a win. Then I'm adding particles on eating, a floating "+10" text, a score bump, and a brief shake on death. Last, I'm deciding that the game-over overlay appears after a short delay, while restart keys only work once it's visible.

I'm working out the game loop now: each tick applies the next queued turn, or the AI's choice in demo mode, then checks the new head against the walls and the body, with the tail allowed to move out of the way unless the snake is growing. If the head lands on food, the score goes up, a new food spawns, and particles burst out; otherwise the tail drops off.

I'm now working out the scoring and direction rules: eating adds 10 points, and each direction request gets checked against the last queued turn so reversals and repeats are dropped. Arrow keys and WASD both count, and I'm handling the AI's movement choice as the next piece.

I'm working out the AI's pathing: first a BFS toward the food, taking the first step of that path, and checking that the move leaves enough reachable space with a flood fill that treats the tail as passable. If that fails, I'll fall back to a path toward the tail.

Now I'm settling the AI's fallback and the tick timing: the snake starts at the middle-left with length three, and the speed eases from about 125ms down toward 70ms as food is eaten. I'm also laying out the header with score and best in pill boxes, plus touch swipes on the canvas wrapper.

Now I'm wrapping up the input handling: a pointer press that moves less than about 20px counts as a tap and starts, restarts, or resumes, while a longer swipe sets the direction and also starts a ready game. I'm also styling the overlays with a backdrop blur and blinking text, drawing the board as a rounded checkerboard with a glowing border, and clearing the canvas each frame before writing out the final code.