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
2 min 48 s
Cost
$0.020
Output tokens
40,728
Spent thinking
30,799 tokens
Speed
244.9 tok/s
Console errors
0
First token after
2.1 s
First visible text after
2 min 21 s
Prompt tokens
750
Code size
18.7 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"high"}
Max output
128,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 · 643 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{
  min-height:100vh;
  display:flex;align-items:center;justify-content:center;
  background:radial-gradient(1200px 700px at 50% -10%,#1b2a4a 0%,#0b1020 55%,#05070d 100%);
  color:#e6ecff;
  font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  overflow-x:hidden;
  user-select:none;
  -webkit-user-select:none;
}
main{
  display:flex;flex-direction:column;align-items:center;gap:14px;
  width:100%;padding:12px;
}
.hud{
  display:flex;align-items:center;justify-content:space-between;
  width:100%;max-width:620px;gap:12px;
}
.brand{display:flex;align-items:center;gap:8px}
.gt{
  font-weight:800;letter-spacing:.2em;font-size:clamp(16px,2.4vw,22px);
  background:linear-gradient(90deg,#6ef3a5,#3bd1ff);
  -webkit-background-clip:text;background-clip:text;color:transparent;
}
.tag{
  font-size:10px;font-weight:700;letter-spacing:.14em;
  padding:3px 8px;border-radius:999px;
  background:rgba(110,243,165,.14);color:#6ef3a5;
}
.tag[data-mode="paused"]{background:rgba(255,209,102,.16);color:#ffd166}
.tag[data-mode="over"]{background:rgba(255,107,127,.18);color:#ff8a9b}
.tag[data-mode="demo"]{background:rgba(120,160,255,.16);color:#9ab8ff}
.stats{display:flex;gap:10px}
.stat{
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.08);
  border-radius:12px;padding:6px 14px;min-width:84px;text-align:center;
}
.stat span{
  display:block;font-size:11px;text-transform:uppercase;
  letter-spacing:.14em;color:#8b98c4;
}
.stat b{font-size:20px;font-variant-numeric:tabular-nums}
.board{
  position:relative;border-radius:14px;overflow:hidden;
  box-shadow:0 20px 60px rgba(0,0,0,.55),0 0 0 1px rgba(120,160,255,.15);
}
canvas{display:block;touch-action:none;cursor:pointer}
.overlay{
  position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  background:rgba(4,7,16,.42);
  opacity:0;pointer-events:none;
  transition:opacity .25s ease;
}
.overlay.show{opacity:1;pointer-events:auto;cursor:pointer}
.card{
  text-align:center;padding:18px 22px;border-radius:16px;
  background:rgba(12,18,34,.82);
  border:1px solid rgba(255,255,255,.1);
  max-width:86%;
}
.card h2{font-size:clamp(22px,4vw,30px);margin-bottom:8px;letter-spacing:.04em}
.card p{color:#c3cdee;font-size:clamp(13px,2vw,15px);line-height:1.55}
.card .big{
  font-size:clamp(28px,5vw,40px);font-weight:700;color:#fff;
  margin:4px 0 6px;font-variant-numeric:tabular-nums;
}
.blink{animation:blink 1.4s ease-in-out infinite}
@keyframes blink{50%{opacity:.4}}
.badge{
  display:inline-block;margin:4px 0 8px;padding:3px 10px;border-radius:999px;
  background:#ff6b81;color:#fff;font-size:12px;font-weight:700;letter-spacing:.1em;
}
kbd{
  display:inline-block;padding:1px 6px;border-radius:5px;
  background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.18);
  font-family:inherit;font-size:.92em;color:#fff;
}
.shake{animation:shake .35s}
@keyframes shake{
  0%,100%{transform:translate(0,0)}
  20%{transform:translate(-6px,2px)}
  40%{transform:translate(5px,-2px)}
  60%{transform:translate(-4px,1px)}
  80%{transform:translate(2px,0)}
}
.hint{font-size:12px;color:#7d8ab3;text-align:center;letter-spacing:.03em;max-width:620px}
@media (max-width:420px){
  .stat{min-width:66px;padding:5px 8px}
  .stat b{font-size:16px}
  .tag{display:none}
}
</style>
</head>
<body>
<main>
  <header class="hud" id="hud">
    <div class="brand">
      <span class="gt">SNAKE</span>
      <span id="tag" class="tag" data-mode="demo">DEMO</span>
    </div>
    <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 class="board" id="board">
    <canvas id="c" aria-label="Snake game board"></canvas>
    <div class="overlay" id="overlay">
      <div class="card">
        <h2 id="ovTitle"></h2>
        <p id="ovMsg"></p>
        <p id="ovSub"></p>
      </div>
    </div>
  </div>

  <p class="hint" id="hint">Arrows / WASD steer &middot; <kbd>P</kbd> pause &middot; <kbd>Space</kbd> / <kbd>Enter</kbd> restart</p>
</main>

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

const COLS = 24, ROWS = 24, N = COLS * ROWS;
const $ = id => document.getElementById(id);
const canvas = $('c'), ctx = canvas.getContext('2d');
const board = $('board'), overlay = $('overlay');
const ovTitle = $('ovTitle'), ovMsg = $('ovMsg'), ovSub = $('ovSub');
const scoreEl = $('score'), bestEl = $('best'), tagEl = $('tag');
const hud = $('hud'), hint = $('hint');

const KEYDIR = {
  ArrowUp:{x:0,y:-1}, ArrowDown:{x:0,y:1}, ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0},
  w:{x:0,y:-1}, W:{x:0,y:-1}, s:{x:0,y:1}, S:{x:0,y:1},
  a:{x:-1,y:0}, A:{x:-1,y:0}, d:{x:1,y:0}, D:{x:1,y:0}
};
const DIR_LIST = [{x:0,y:-1},{x:0,y:1},{x:-1,y:0},{x:1,y:0}];
const NB = [[1,0],[-1,0],[0,1],[0,-1]];

let cell = 20, boardPx = 0, dpr = 1;
let mode = 'demo';                 // 'demo' | 'play' | 'paused' | 'over'
let snake = [], prev = [];
let dir = {x:1,y:0}, queue = [];
let food = null, score = 0, best = 0;
let acc = 0, tickMs = 95;
let freezeUntil = 0;
let particles = [];
let lastNewBest = false;
let lastNow = performance.now(), clock = 0;

const same = (a, b) => a.x === b.x && a.y === b.y;
const lerp = (a, b, t) => a + (b - a) * t;

/* ---------- state helpers ---------- */

function spawnFood() {
  const occ = new Uint8Array(N);
  for (const p of snake) occ[p.y * COLS + p.x] = 1;
  const free = [];
  for (let i = 0; i < N; i++) if (!occ[i]) free.push(i);
  if (!free.length) return null;
  const k = free[Math.floor(Math.random() * free.length)];
  return { x: k % COLS, y: Math.floor(k / COLS) };
}

function updateSpeed() {
  tickMs = mode === 'demo' ? 95 : Math.max(60, 130 - Math.floor(score / 10) * 6);
}

function updateHud() {
  scoreEl.textContent = score;
  bestEl.textContent = best;
  const labels = { demo:'DEMO', play:'LIVE', paused:'PAUSED', over:'GAME OVER' };
  tagEl.textContent = labels[mode];
  tagEl.dataset.mode = mode;
}

function reset() {
  const cx = COLS >> 1, cy = ROWS >> 1;
  snake = [{x:cx,y:cy},{x:cx-1,y:cy},{x:cx-2,y:cy}];
  prev = snake.map(p => ({x:p.x, y:p.y}));
  dir = {x:1,y:0};
  queue = [];
  score = 0;
  acc = 0;
  particles = [];
  freezeUntil = 0;
  updateSpeed();
  food = spawnFood();
  updateHud();
}

function startDemo() {
  mode = 'demo';
  reset();
  syncOverlay();
}

function startPlay(d) {
  mode = 'play';
  reset();
  if (d) enqueue(d);
  syncOverlay();
}

function enqueue(d) {
  const last = queue.length ? queue[queue.length - 1] : dir;
  if (d.x === last.x && d.y === last.y) return;        // same direction, nothing to do
  if (d.x === -last.x && d.y === -last.y) return;      // never reverse
  if (queue.length >= 3) return;
  queue.push({x:d.x, y:d.y});
}

function syncOverlay() {
  let show = true, t = '', m = '', s = '';
  if (mode === 'demo') {
    t = 'SNAKE';
    m = '<span class="blink">Click or press a key to start</span>';
    s = 'Watching the AI play &middot; steer with arrows or WASD';
  } else if (mode === 'paused') {
    t = 'Paused';
    m = 'Press <kbd>P</kbd> to resume';
  } else if (mode === 'over') {
    t = 'Game over';
    m = 'Score<div class="big">' + score + '</div>';
    s = (lastNewBest ? '<span class="badge">NEW BEST</span><br>' : '') +
        'Press <kbd>Space</kbd> or <kbd>Enter</kbd> to restart';
  } else {
    show = false;
  }
  ovTitle.textContent = t;
  ovMsg.innerHTML = m;
  ovSub.innerHTML = s;
  overlay.classList.toggle('show', show);
  updateHud();
}

function shakeBoard() {
  board.classList.remove('shake');
  void board.offsetWidth;
  board.classList.add('shake');
}

/* ---------- particles ---------- */

function burst(x, y, death) {
  const n = death ? 40 : 24;
  const palette = death
    ? ['#ff6b7f', '#ffb3bd', '#ffffff', '#ff4d6d']
    : ['#ff5a6e', '#ffd166', '#ffffff', '#6ef3a5'];
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2;
    const sp = (death ? 3 : 2) + Math.random() * (death ? 6 : 5);
    particles.push({
      x: x + 0.5, y: y + 0.5,
      vx: Math.cos(a) * sp, vy: Math.sin(a) * sp,
      life: 0, max: 450 + Math.random() * 450,
      color: palette[Math.floor(Math.random() * palette.length)]
    });
  }
}

function updateParticles(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; }
    const k = Math.pow(0.96, dt / 16.7);
    p.vx *= k; p.vy *= k;
    p.x += p.vx * dt / 1000;
    p.y += p.vy * dt / 1000;
  }
}

/* ---------- game logic ---------- */

function die() {
  const head = snake[0];
  burst(head.x, head.y, true);
  acc = tickMs;                       // keep the final frame fully drawn (no backtrack)
  if (mode === 'demo') {
    freezeUntil = performance.now() + 1500;
    return;
  }
  lastNewBest = score > 0 && score > best;
  best = Math.max(best, score);
  mode = 'over';
  shakeBoard();
  syncOverlay();
}

function step() {
  if (queue.length) dir = queue.shift();
  const old = snake;
  const head = old[0];
  const nx = { x: head.x + dir.x, y: head.y + dir.y };

  if (nx.x < 0 || nx.y < 0 || nx.x >= COLS || nx.y >= ROWS) { die(); return; }

  const eating = food && same(nx, food);
  const body = eating ? old : old.slice(0, -1);   // tail moves away unless we grow
  if (body.some(p => same(p, nx))) { die(); return; }

  const next = [nx, ...old];
  if (!eating) next.pop();

  // Interpolation sources: each segment's position in the previous tick
  prev = next.map((_, i) => ({ ...old[Math.min(i, old.length - 1)] }));
  snake = next;

  if (eating) {
    score += 10;
    updateSpeed();
    burst(nx.x, nx.y, false);
    food = spawnFood();
    if (!food) { die(); return; }     // board completely filled
  }
  updateHud();
}

/* ---------- demo AI ---------- */

function scan(sx, sy, blocked) {
  const seen = new Uint8Array(N), q = new Int32Array(N), dd = new Int32Array(N);
  const fi = food ? food.y * COLS + food.x : -1;
  let h = 0, t = 0;
  const s = sy * COLS + sx;
  q[t++] = s; seen[s] = 1;
  let dist = -1;
  while (h < t) {
    const c = q[h++];
    if (c === fi && dist < 0) dist = dd[c];
    const cx = c % COLS, cy = (c - cx) / COLS;
    for (const [ox, oy] of NB) {
      const nx = cx + ox, ny = cy + oy;
      if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
      const ni = ny * COLS + nx;
      if (seen[ni] || blocked[ni]) continue;
      seen[ni] = 1;
      dd[ni] = dd[c] + 1;
      q[t++] = ni;
    }
  }
  return { dist, area: t };
}

function aiDir() {
  const blocked = new Uint8Array(N);
  for (let i = 0; i < snake.length - 1; i++) blocked[snake[i].y * COLS + snake[i].x] = 1;
  const head = snake[0];
  let choice = null, bestS = -Infinity;
  for (const d of DIR_LIST) {
    if (d.x === -dir.x && d.y === -dir.y) continue;
    const nx = head.x + d.x, ny = head.y + d.y;
    if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
    if (blocked[ny * COLS + nx]) continue;
    const b = blocked.slice();
    b[ny * COLS + nx] = 1;
    const r = scan(nx, ny, b);
    const reach = r.dist >= 0;
    // Prefer moves that reach food, then moves that keep lots of room (avoid traps)
    const s = (reach ? 100000 - r.dist * 20 : 0) + Math.min(r.area, 600) * 10;
    if (s > bestS) { bestS = s; choice = d; }
  }
  return choice || dir;
}

/* ---------- input ---------- */

window.addEventListener('keydown', e => {
  const k = e.key;
  if (KEYDIR[k]) {
    e.preventDefault();
    if (e.repeat) return;
    const d = KEYDIR[k];
    if (mode === 'demo') startPlay(d);
    else if (mode === 'play') enqueue(d);
    return;
  }
  if (k === ' ' || k === 'Enter') {
    e.preventDefault();
    if (e.repeat) return;
    if (mode === 'demo' || mode === 'over') startPlay();
    return;
  }
  if (k === 'p' || k === 'P') {
    if (mode === 'play') { mode = 'paused'; syncOverlay(); }
    else if (mode === 'paused') { mode = 'play'; syncOverlay(); }
  }
});

window.addEventListener('blur', () => {
  if (mode === 'play') { mode = 'paused'; syncOverlay(); }
});

board.addEventListener('click', () => {
  if (mode === 'demo' || mode === 'over') startPlay();
  else if (mode === 'paused') { mode = 'play'; syncOverlay(); }
});

let touchX = 0, touchY = 0;
board.addEventListener('touchstart', e => {
  touchX = e.touches[0].clientX; touchY = e.touches[0].clientY;
}, { passive: true });
board.addEventListener('touchend', e => {
  const t = e.changedTouches[0];
  const dx = t.clientX - touchX, dy = t.clientY - touchY;
  if (Math.max(Math.abs(dx), Math.abs(dy)) < 24) return;
  const d = Math.abs(dx) > Math.abs(dy)
    ? { x: Math.sign(dx), y: 0 }
    : { x: 0, y: Math.sign(dy) };
  if (mode === 'demo') startPlay(d);
  else if (mode === 'play') enqueue(d);
}, { passive: true });

/* ---------- layout ---------- */

function layout() {
  const hudH = hud.offsetHeight, hintH = hint.offsetHeight;
  const availH = window.innerHeight - hudH - hintH - 64;
  const availW = window.innerWidth - 24;
  const size = Math.min(availW, 620, Math.max(availH, 240));
  cell = Math.max(8, Math.floor(size / COLS));
  boardPx = cell * COLS;
  dpr = window.devicePixelRatio || 1;

  canvas.style.width = boardPx + 'px';
  canvas.style.height = boardPx + 'px';
  canvas.width = Math.round(boardPx * dpr);
  canvas.height = Math.round(boardPx * dpr);
  board.style.width = boardPx + 'px';
  board.style.height = boardPx + 'px';
  hud.style.maxWidth = boardPx + 'px';
  hint.style.maxWidth = boardPx + 'px';
}

/* ---------- rendering ---------- */

function drawBackground() {
  const g = ctx.createLinearGradient(0, 0, 0, boardPx);
  g.addColorStop(0, '#14203d');
  g.addColorStop(1, '#0a1024');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, boardPx, boardPx);
  ctx.fillStyle = 'rgba(255,255,255,0.028)';
  for (let y = 0; y < ROWS; y++)
    for (let x = 0; x < COLS; x++)
      if ((x + y) & 1) ctx.fillRect(x * cell, y * cell, cell, cell);
}

function drawFood() {
  if (!food) return;
  const pulse = 1 + Math.sin(clock / 180) * 0.07;
  const cx = (food.x + 0.5) * cell, cy = (food.y + 0.5) * cell;
  const r = cell * 0.36 * pulse;
  ctx.save();
  ctx.shadowColor = 'rgba(255,70,90,0.9)';
  ctx.shadowBlur = cell * 0.9;
  const rg = ctx.createRadialGradient(cx - r * 0.3, cy - r * 0.3, r * 0.1, cx, cy, r);
  rg.addColorStop(0, '#ffd6cf');
  rg.addColorStop(0.35, '#ff5a6e');
  rg.addColorStop(1, '#b3203a');
  ctx.fillStyle = rg;
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.fill();
  ctx.shadowBlur = 0;
  ctx.strokeStyle = '#5be08a';
  ctx.lineCap = 'round';
  ctx.lineWidth = cell * 0.08;
  ctx.beginPath();
  ctx.moveTo(cx, cy - r * 0.8);
  ctx.quadraticCurveTo(cx + r * 0.3, cy - r * 1.6, cx + r * 0.75, cy - r * 1.25);
  ctx.stroke();
  ctx.restore();
}

function drawSnake(t) {
  const n = snake.length;
  const P = new Array(n);
  for (let i = 0; i < n; i++) {
    const a = prev[i] || snake[i], b = snake[i];
    P[i] = {
      x: (lerp(a.x, b.x, t) + 0.5) * cell,
      y: (lerp(a.y, b.y, t) + 0.5) * cell
    };
  }
  const dead = mode === 'over';

  ctx.save();
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';

  // Body: tail -> neck, colour shifts from head to tail
  ctx.lineWidth = cell * 0.78;
  for (let i = n - 1; i >= 1; i--) {
    const f = i / Math.max(1, n - 1);
    ctx.strokeStyle = dead
      ? `hsl(355, ${60 - f * 10}%, ${50 - f * 22}%)`
      : `hsl(${152 - f * 36}, ${72 - f * 18}%, ${52 - f * 20}%)`;
    ctx.beginPath();
    ctx.moveTo(P[i].x, P[i].y);
    ctx.lineTo(P[i - 1].x, P[i - 1].y);
    ctx.stroke();
  }

  // Soft highlight along the body
  if (n > 1) {
    ctx.beginPath();
    ctx.moveTo(P[n - 1].x, P[n - 1].y);
    for (let i = n - 2; i >= 0; i--) ctx.lineTo(P[i].x, P[i].y);
    ctx.strokeStyle = 'rgba(255,255,255,0.10)';
    ctx.lineWidth = cell * 0.16;
    ctx.stroke();
  }

  // Head
  const h = P[0];
  const hr = cell * 0.44;
  const dx = dir.x, dy = dir.y;
  const px = -dy, py = dx;

  // Tongue flick (live play / demo only)
  if (!dead && Math.sin(clock / 260) > 0.55) {
    const x0 = h.x + dx * hr, y0 = h.y + dy * hr;
    const x1 = h.x + dx * (hr + cell * 0.3), y1 = h.y + dy * (hr + cell * 0.3);
    ctx.strokeStyle = '#ff5d7a';
    ctx.lineWidth = cell * 0.06;
    ctx.beginPath();
    ctx.moveTo(x0, y0);
    ctx.lineTo(x1, y1);
    ctx.lineTo(x1 + dx * cell * 0.1 + px * cell * 0.08, y1 + dy * cell * 0.1 + py * cell * 0.08);
    ctx.moveTo(x1, y1);
    ctx.lineTo(x1 + dx * cell * 0.1 - px * cell * 0.08, y1 + dy * cell * 0.1 - py * cell * 0.08);
    ctx.stroke();
  }

  const g = ctx.createRadialGradient(h.x - hr * 0.35, h.y - hr * 0.35, hr * 0.1, h.x, h.y, hr);
  if (dead) {
    g.addColorStop(0, '#ffb3bd');
    g.addColorStop(1, '#d7344d');
  } else {
    g.addColorStop(0, '#c9ffd9');
    g.addColorStop(0.5, '#3fe08a');
    g.addColorStop(1, '#14a360');
  }
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(h.x, h.y, hr, 0, Math.PI * 2);
  ctx.fill();

  // Eyes facing the direction of travel
  const ef = cell * 0.16, es = cell * 0.19, er = cell * 0.12;
  for (const sgn of [1, -1]) {
    const ex = h.x + dx * ef + px * es * sgn;
    const ey = h.y + dy * ef + py * es * sgn;
    ctx.fillStyle = '#ffffff';
    ctx.beginPath();
    ctx.arc(ex, ey, er, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#0b1226';
    ctx.beginPath();
    ctx.arc(ex + dx * er * 0.45, ey + dy * er * 0.45, er * 0.55, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,0.9)';
    ctx.beginPath();
    ctx.arc(ex + dx * er * 0.45 - er * 0.2, ey + dy * er * 0.45 - er * 0.2, er * 0.18, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.restore();
}

function drawParticles() {
  ctx.save();
  for (const p of particles) {
    const k = p.life / p.max;
    ctx.globalAlpha = 1 - k;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x * cell, p.y * cell, cell * 0.14 * (1 - k) + 1, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.restore();
}

function render(t) {
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  drawBackground();
  drawFood();
  drawSnake(t);
  drawParticles();
}

/* ---------- main loop ---------- */

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

  if (mode === 'demo' && freezeUntil && performance.now() >= freezeUntil) {
    startDemo();
  }

  const running = (mode === 'play' || mode === 'demo') && !freezeUntil;
  if (running) {
    acc += dt;
    let guard = 0;
    while (acc >= tickMs && guard++ < 3) {
      acc -= tickMs;
      if (mode === 'demo') queue = [aiDir()];
      step();
      if (!((mode === 'play' || mode === 'demo') && !freezeUntil)) break;
    }
  }

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

window.addEventListener('resize', layout);

/* ---------- boot ---------- */
layout();
startDemo();
requestAnimationFrame(t => { lastNow = t; frame(t); });
})();
</script>
</body>
</html>
Reasoning trace (30,799 tokens)
I'm setting up a self-playing Snake demo that starts behind a "Click or press a key to start" prompt, with a centered canvas whose cell size scales to fit the viewport.

I'm working out how the smooth movement will work: each tick I'll keep the previous segment positions and interpolate between them when rendering. When the snake grows, the new tail will take the old tail's position as its starting point.

I'm working through the segment update logic: after a tick, each body segment takes the spot of the one ahead of it, so I need to pair each new segment with the position it moved from so the interpolation slides it into place.

I'm checking the grown-tail case: the new tail has no earlier position, so it will just appear at the old tail's cell instead of sliding in.

I'm working out the rendering step: each frame I'll lerp every segment from its previous position to its current one, using a time accumulator divided by the tick interval, and I'm treating the grown tail as a segment that doesn't move. I also need to check for wall collisions when the head steps outside the grid.

I'm settling on the game-over handling and input queue: a head that hits the body only counts when the tail isn't moving away, and keys go into a short queue that's checked against the last queued direction, so quick double-presses both register across ticks. Then I'm turning to the demo AI, which will BFS toward the food while checking that the head still has enough reachable space afterward.

I'm settling the demo's mode handling: a title state where the AI plays behind the "Click or press a key to start" overlay, then playing, paused, and game-over states once the player takes over. I'm also deciding that Space or Enter restarts after game over, with a click doing the same.

I'm settling the demo's remaining details: the AI plays until a key or click starts a real game, and after a human game ends, Space or Enter restarts it. Keyboard and click both start the game, and arrow keys, WASD, and Space need their default scrolling suppressed. I'm tracking the session's best score only from human runs, and spawning a burst of about eighteen colored particles at the food when the snake eats it.

I'm laying out the page now: a 24x24 grid in a square canvas that scales to the viewport, with a header for title, score, and best, and a footer of hints. Next comes the look, a dark gradient with faint grid lines, a green-teal snake with a brighter head whose eyes follow its direction, and a pulsing red food dot, with the start prompt as an overlay on top of the canvas. The render size will be the cell count times the cell pixels, scaled by device pixel ratio.

I'm wrapping up the rendering loop: a fixed tick accumulator drives the game steps, and the leftover fraction gives the interpolation value for drawing. The tick interval shrinks a bit as the score climbs, with a floor so it never gets too fast. Particles keep updating even while paused, so the burst effects don't freeze.

Now I'm starting to sketch the demo AI, which will look at each possible direction except straight back into the body.

Still writing the demo AI: it's a BFS to the food with a flood-fill check to avoid traps, plus a blocked-cell grid built from the snake's segments.

I'm settling the demo's details: the AI will check its candidate moves against a blocked-cell grid, and after a game over it restarts on its own after about 1.5 seconds. I'm also adding a "DEMO" badge over the overlay so the autoplay is clearly labeled, and I'm still deciding what score the demo run should display.

I'm working out how Space and Enter restart a finished game, and how the P key pauses and resumes play. I'm also adding a shake effect on the canvas when the snake dies.

I'm laying out the overlay and header details now: the start prompt sits on the canvas in a DOM overlay, with a game-over message that includes the score and the restart keys. Clicking the canvas or the overlay also starts a game when the demo is running or the game is over.

I'm laying out the HTML structure now: a header with the title and score and best stats, a board holding the canvas with an overlay card for messages, and a footer with the controls hint.

Still writing the page code: the CSS sets a dark radial background, and the JavaScript sizes the board from the window, using a 24-by-24 grid with the cell size floored from the available width and height.

Still setting up the game state: a grid, the snake's positions and previous positions for interpolation, a direction queue, food, score, and a mode, with a reset that centers the snake and spawns the first piece of food.

Still writing the game loop: the step function will read the next queued direction, compute the new head, and check whether it eats the food, which decides whether the tail is excluded from the collision check.

Still writing the game step: the snake's segments get their previous positions saved so the renderer can interpolate them, and eating food grows the snake without moving the new tail.

I'm settling the demo's mode handling: a game-over state where, for the autoplay run, a flag tracks whether it's the demo, and the loop restarts it after a short delay.

I'm settling the demo's mode handling: the AI runs in a demo state behind the start overlay, and a death during the demo just schedules a restart without showing a game-over screen. The overlay keeps showing the start message while the demo runs, so I'll update it from one helper. For each tick in demo mode, I'll have the AI pick the direction and queue it before the step runs.

Still writing the keyboard handling: a direction map from arrow keys and WASD, where each new direction is queued only if it isn't the same as or opposite the last queued one, and the queue caps at three entries.

I'm deciding how the arrow keys behave while the demo or a finished game is showing: they'll start a fresh human game and queue the chosen direction, with the usual reversal check still applied.

I'm working out the keyboard handling for the demo and finished states: any key starts play in demo mode, while after game over only Space or Enter restarts, and arrows are ignored. During play, arrow and WASD presses get queued, and P toggles pause.

Still writing the game loop: I'm wrapping the frame function to cap the elapsed time, and the pause handling means the accumulator only grows while play or demo is running, so resuming won't cause a jump.

Still working through the game loop: I'm handling the demo's death case, where the AI restarts after a short wait instead of showing a game-over screen. I'll freeze the ticking during that wait and resume the demo once the timer passes.

I'm handling the demo's freeze after a death: when the AI's snake dies, the board stays visible for about 1.3 seconds before a fresh demo starts. During that window the ticking pauses, but drawing continues so the dead state still shows.

Still writing the game loop: the freeze timer resets the demo once it passes, and ticking is skipped while the snake's death is on display. Next I'm working out the particle update and the interpolated snake rendering.

I'm working out the snake's rendering: the head gets eyes placed on the side facing the current direction, and the body is drawn as thick round-capped lines between consecutive segment centers so it reads as one smooth shape. I'm also deciding whether the color should shift along the body, which would mean computing a per-segment hue.

Still writing the canvas drawing: the snake's body gets drawn as thick round-capped lines between consecutive segments, with each segment's color shifting along the body. Next comes the head, a bigger shape with eyes, and a red head when the game is over.

I'm working out the particle burst for the food: about 22 particles in the food's hue and bright yellow, each with a random direction, a speed of a few cells per second, and a life of half a second to just under a second. The overlay sits over the board with a dim, slightly blurred backdrop, shown by toggling a class. On death, I'm adding a shake class to the board and removing it once the animation ends.

Now I'm working out the overlay text for each state: the demo shows the title and start prompt, paused shows a resume hint, and game over shows the score with an optional new-best line.

Still writing the AI: I'm building a blocked-cell map from the snake's body, then scoring each candidate direction for the next move.

Still writing the AI: I'm scoring each non-reversing, unblocked direction by its BFS distance to the food and how much open space follows it, while making sure the snake isn't walking into a dead-end pocket.

Still writing the AI's move choice: I'm scoring each candidate direction, favoring ones that reach the food and keep more open space, with the distance to food as a tiebreaker.

I'm working out the AI's move choice: a blocked-cell map from the snake's body, then for each legal direction I check whether a path to the food exists and how much open space follows, preferring the option with more room first and the shorter path second.

Still writing the game logic: I'm choosing a random free cell for each new food by collecting the empty indices and picking one. The tick interval will speed up as the human eats more, down to a floor of 55 ms, while the demo holds steady at 95 ms.

Still writing the layout code: the board size is calculated from the window size minus the header and footer heights, then floored to whole cells, and the canvas and its container get that pixel size.

Still writing the page: I'm finishing the base styles, setting the body to a dark radial gradient with light text, and adding overflow-x hidden so nothing scrolls sideways before I write out the full file.

Still writing the page styles: I'm defining the header and stats layout, with the brand text in a green-to-blue gradient.

Still writing the page styles: I'm finishing the stat cards and the board's rounded frame with a soft shadow, and setting the canvas to display as a block so it sits flush inside the board.

Still writing the stylesheet: I'm styling the overlay card and its heading and paragraph text, and starting the large score line.

Still writing the stylesheet: I'm finishing the blinking prompt, the score badge, and the shake animation for the board.

Writing the shake keyframes and the hint text under the board.

Still writing the input handling: I'm mapping the arrow keys and WASD, both cases, to direction vectors, and grabbing the overlay, score, and best-score elements.

Still writing the game state: I'm declaring the grid size, the current mode, the snake and its previous positions, the queued turns, and the score and tick timing, along with the particle list and the freeze timer.

Still writing the game logic: I'm defining the food spawner, which collects the empty cells and picks one at random, and a reset function that places the snake at the center and sets the tick speed.

Still writing the game step: I'm reading the next direction from the queue, then working out the new head position and whether it lands on the food.

Still writing the game step: I'm checking for a wall or self collision before moving, then shifting the snake forward and filling the previous-position buffer, and when it eats, I'm bumping the score, updating the tick speed, and spawning a new food.

Still writing the game step: I'm working out how the snake's previous-position buffer lines up with its new segments, and handling the case where the board is full with no food left.

Still writing the game-over path: when a human dies, I'm bumping the best score if it beats the record, setting the board to its dead state, and bursting particles from the head.

Still writing the game loop: I'm checking whether the demo is in its frozen state after a crash, and once the freeze ends I'll reset the board so it starts over.

Still writing the game start: I'm restarting the board for a human run, then queueing the first direction if one was pressed and syncing the overlay.

Still writing the move logic: I'm defining a helper that rejects a direction if it matches the last queued one or reverses it, then caps the queue at three turns.

Still writing the keydown handler: I'm checking whether a pressed key maps to a direction, and I'll queue it if it isn't a reversal.

Still writing the keydown handler: I'm handling arrow and WASD presses by queuing a turn, and space or Enter to start a run from the demo or game-over screen, while P toggles pause.

Still writing the click handler: I'm making a click on the board start a run when the demo or game-over screen is showing.

Still writing the overlay sync: I'm setting the title to SNAKE and the message to the click-or-key prompt while the demo runs.

Still writing the overlay sync: I'm setting the demo title and prompt text, then the paused and game-over messages, with the best-run badge shown when a new record is set.

Still writing the overlay sync: I'm adding a blink class to the start prompt and handling the case where the food spawner finds no free cell, ending the run in that case.

I'm working through the page's HUD and the tag label, which will show a small DEMO marker while the demo is running.

Still writing the particle burst: it spawns more particles on a death than on food, each with a random outward velocity and lifetime, and I'm picking warm hues for the color.

Still writing the particle burst: I'm working out the particle colors, with death bursts in red and food bursts in warm tones, and each particle gets a fixed color when it spawns.

Still writing the render loop: I'm clearing the canvas, painting a dark vertical gradient for the background, and drawing the faint grid lines at each cell boundary.

Still writing the render loop: I'm drawing the food as a pulsing circle with a glow, sized from the cell size.

Still writing the food drawing: I'm adding a leaf-colored stem stroke on top of the glowing circle.

Still writing the snake drawing: I'm computing each segment's interpolated position from the previous and current positions, so the body moves smoothly between ticks.

Still writing the snake drawing: I'm coloring each body segment by its position, bright teal at the head and darker toward the tail, and stroking the segments with rounded caps.

Still writing the snake body: I'm drawing each segment as a rounded stroke colored by its position, and then laying a faint highlight along the whole body.

Still writing the head drawing: I'm placing the head circle with a gradient, red when the game is over and green otherwise, then working out the eye positions from the direction the snake is facing.

Still writing the particle draw: I'm looping over the particles, fading each one by its remaining life, and drawing the tongue flicks from the head with a small fork when the demo is running.

Still writing the particle draw: I'm fading each particle's alpha by its remaining life and drawing it as a small circle sized to the cell.

Still writing the game loop: I'm checking whether the demo is running and frozen, stepping the simulation at a fixed tick rate with a small cap on catch-up steps, and ending the loop early if the mode changes mid-tick.

Still writing the game loop: I'm stepping the simulation on a fixed tick, and resetting the accumulator whenever the game isn't running so resuming doesn't cause a jump.

Still working on the render loop: I'm deciding how the snake's drawn position should follow the tick, so pausing keeps the stored accumulator and the interpolation stays where it was.

I'm working out how the interpolated positions carry through: the previous and current snake arrays line up index by index, so the body can be drawn smoothly between ticks. When the snake dies, the body doesn't move, so I need the interpolation fraction to sit at the full tick rather than resetting to zero, which would make the snake jump back a step.

Still writing the death handling: when the snake dies, I'm setting the interpolation accumulator to a full tick so the body stays put instead of snapping back a cell. I'll also reset the accumulator and previous positions on a fresh start so the first frame has no backtrack.

Still writing the layout function: I'm sizing the canvas to the space left after the HUD and hint, capped at 620 pixels and never shrinking below 240, so on a phone the board fills the width.

Still writing the layout function: I'm computing the cell size from the space left, flooring it to whole pixels and keeping at least 8, then setting the board's pixel size and the canvas backing store to match the device pixel ratio.

Still writing the AI: I'm working out which directions are blocked for the demo snake before it picks a move.

Still writing the AI: I'm building a blocked-cell map from the snake's body and skipping any move that reverses or runs into a wall or body, then scoring each remaining direction.

I'm weighing the scoring formula for the demo AI: reachable food gets a large base score minus a small penalty per step of distance, and the remaining open area adds a tiebreak, capped so it can't outweigh reaching the food.

Still writing the AI: I'm scoring each candidate move by whether it can still reach the food, subtracting a little for each step of distance, and adding a tiebreak for how much open space remains, capped so it doesn't outweigh reaching the food.

Still writing the AI: I'm scoring each candidate by whether a path to the food exists, adding a tiebreak for open area, and falling back to the largest open area when no move reaches the food.

Still writing the search: I'm defining a single breadth-first scan that counts every reachable cell from the candidate move and records how many steps it takes to reach the food.

Still writing the search: I'm running one breadth-first pass that counts every reachable cell and records how many steps it takes to reach the food.

Still finishing the demo AI: I'm checking that the search handles trapped positions and resets cleanly on restart, and confirming the layout and overlay setup run on first paint.

Still checking the overlay and HUD setup, and confirming that the previous snake positions get copied at each mode switch so the first frame after a restart doesn't backtrack.

Still writing the demo's game loop and death handling: I'm finishing the shake effect and the restart on click.

Still writing the search: I'm finishing the breadth-first scan, which records the distance to the food and counts the reachable area in one pass.

Still writing the state setup: I'm placing the snake in the middle of the board with three segments and copying those positions into the previous-positions array so the first frame has no backtrack.

Still writing the state setup: I'm resetting the snake, direction, queue, score, and particles, then spawning food and refreshing the HUD, with the tick speed set by mode.

Still writing the die handler: I'm bursting particles at the head, holding the interpolation at a full tick so the body doesn't snap back, and freezing briefly in demo mode before restarting.

Still writing the death handler: I'm setting the interpolation to a full tick so the body holds, then either freezing briefly in demo mode or switching to game over and tracking whether it's a new best.

Still writing the game loop: I'm adding a particle update that ages each burst, slows it down over time, and removes it once its lifetime runs out.

Working out the particle drag: at 0.92 per frame the burst only travels about a cell, so I'm raising the drag to 0.96, which stretches it to roughly two cells. I'm also sizing each particle's drawn radius to shrink as its life runs out.

Still writing the draw routine: I'm mapping each segment's interpolated position to canvas pixels, then drawing the body in the usual round-cap style, with a red tint when the snake is dead.

Still writing the draw routine: I'm drawing the body as a series of strokes from tail to neck, with the color shifting along its length and a red tint when the snake is dead.

Still writing the draw routine: I'm adding a faint highlight stroke along the body's center line, then working out how the overlapping segment colors layer as I go from tail to neck.

Still writing the head: I'm drawing a radial gradient highlighted toward the upper left, shifting from pale green to a darker green at the edge, or to red when the snake is dead.

Still writing the eyes: I'm placing two white eyes on either side of the head, offset forward along the direction of travel, and starting the dark pupil fill.

Still writing the head: I'm adding the two eyes, offset forward along the direction of travel, and drawing the dark pupils.

Still writing the main loop: I'm capping the number of catch-up ticks per frame and stepping the game forward only while enough time has built up.

Still writing the game loop: I'm stepping the game forward in a loop and handing the AI's direction to each tick.

Still writing the overlay: I'm deciding which title, message, and status line to show for each mode, starting from a default where the overlay is visible.

Still writing the overlay: I'm picking the title, message, and status line for the demo, pause, and game-over modes, and showing the NEW BEST badge when the score beats the record.

Still writing the touch controls: I'm capturing the starting touch point so a swipe can be turned into a direction change.

Still writing the touch controls: I'm adding a swipe handler that reads the direction from the touch start and end points, ignoring short movements, and turns it into a move in demo or play mode.

Still writing the HTML: I'm adding the header with the title, the mode tag, and the score and best stats, and setting up the touch swipe handling on the board.

Still writing the HTML: I'm closing out the header and board wrapper, adding the canvas and the overlay card, and setting up the hint line below.

Still writing the HTML: I'm adding the logo and a small colored tag for the current mode, which turns red on game over and gold when paused.

Still writing the stylesheet: I'm sizing the stat boxes and the keyboard hints at the bottom, then adding a small-screen rule that shrinks the stats and hides the mode tag.

Still writing the layout: I'm checking that the board height accounts for the header, the hint line, and the 64px of padding and gaps, and that the body centers it vertically.

I'm drawing the background as a vertical gradient, then laying a faint checker pattern of lighter cells over it so the grid is visible.

Still writing the render routine: I'm drawing a checkerboard of faintly lighter cells over the background, and I'm ordering the draw so food comes first, then the snake, then the particles.

Checking the wraparound case: the head moves from the old position plus the direction, so index 0 has no movement issue. When eating, the new tail reuses the old tail's position, so it doesn't jump. I'm also making sure a reversal from demo mode is harmless, since a move equal to the current direction just gets ignored.

Still writing the event handling: I'm checking that a swipe or tap starting play doesn't also trigger a restart from the click, and I'm deciding to set touch-action to none on the canvas so the page doesn't scroll during play.

Still writing the AI: I'm building a blocked-cell map from the snake's body, then checking each direction except a reversal for walls and obstacles.

Still writing the AI: I'm checking each non-reversing direction for walls and obstacles, and scoring the candidates by whether they can reach the food and how much open space they leave behind.

Still writing the AI: I'm working out the scan's breadth-first search over the grid, using the blocked map and the food as the target.

Still writing the scan: I'm filling in the breadth-first search over the grid, recording the distance to the food when it's reached, and returning the result.

Still writing the scan: I'm guarding against a missing food position so the search doesn't crash when the board is full.

Still writing the finishing touches: I'm capturing the food's coordinates before a new one spawns, so the eat burst uses the head's position, and I'm adding a small helper for comparing positions.