Models 44

Grok 4.7

by SpaceXAIreasoning defaultClean
Open in new window

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

How it went

Total time
18 min 54 s
Cost
$0.477
Output tokens
79,259
Spent thinking
67,644 tokens
Speed
70 tok/s
Console errors
0
First token after
1.3 s
First visible text after
17 min 30 s
Prompt tokens
1,732
Code size
29.5 KB
Finish reason
stop
Served by
xAI
Model id
x-ai/grok-4.7
Reasoning sent
model default
Max output
450,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 · 1101 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Snake</title>
<style>
  :root {
    --bg: #07090f;
    --ink: #e8eef8;
    --muted: #93a0bd;
    --mint: #7dffe0;
    --gold: #ffd166;
    --coral: #ff6b8a;
  }
  * { box-sizing: border-box; }
  html, body {
    margin: 0;
    height: 100%;
    overflow: hidden;
    background: var(--bg);
    color: var(--ink);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    user-select: none;
    -webkit-user-select: none;
    -webkit-tap-highlight-color: transparent;
  }
  body {
    display: flex;
    align-items: stretch;
    justify-content: center;
  }
  body::before {
    content: "";
    position: fixed;
    inset: -15%;
    pointer-events: none;
    background:
      radial-gradient(closest-side at 28% 18%, rgba(62, 224, 160, 0.09), transparent 70%),
      radial-gradient(closest-side at 82% 78%, rgba(255, 77, 109, 0.07), transparent 68%),
      radial-gradient(closest-side at 50% 46%, rgba(36, 64, 130, 0.22), transparent 72%);
    animation: drift 16s ease-in-out infinite alternate;
  }
  @keyframes drift {
    from { transform: translate3d(-1.5%, -1%, 0) scale(1); }
    to { transform: translate3d(1.5%, 1.2%, 0) scale(1.04); }
  }
  .app {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    max-width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 14px 12px 12px;
    gap: 10px;
  }
  header, footer {
    width: min(600px, 100%);
    max-width: 100%;
    flex: 0 0 auto;
  }
  header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
  }
  h1 {
    margin: 0;
    font-size: 28px;
    letter-spacing: 0.2em;
    font-weight: 800;
    display: flex;
    align-items: center;
    gap: 10px;
  }
  h1 .orb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 35%, #f3fff9, #3ee0a0 42%, #127a56);
    box-shadow: 0 0 14px rgba(62, 224, 160, 0.85);
    display: inline-block;
  }
  .stats { display: flex; gap: 8px; }
  .stat {
    min-width: 92px;
    padding: 7px 12px 8px;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.08);
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  .lbl {
    font-size: 10px;
    letter-spacing: 0.16em;
    color: var(--muted);
    font-weight: 700;
  }
  .stat.best .lbl::before { content: "★ "; color: var(--gold); }
  .num {
    font-size: 26px;
    font-weight: 800;
    line-height: 1;
    color: var(--mint);
    font-variant-numeric: tabular-nums;
    display: inline-block;
    transform-origin: left center;
  }
  .stat.best .num { color: var(--gold); }
  .num.bump { animation: pop 0.28s ease; }
  @keyframes pop {
    0% { transform: scale(1); }
    40% { transform: scale(1.22); }
    100% { transform: scale(1); }
  }
  #stage {
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  #wrap {
    position: relative;
    border-radius: 16px;
    overflow: hidden;
    background: #0c1224;
    box-shadow:
      0 24px 60px rgba(0, 0, 0, 0.48),
      0 0 0 1px rgba(255, 255, 255, 0.06),
      inset 0 1px 0 rgba(255, 255, 255, 0.08);
    touch-action: none;
    cursor: pointer;
  }
  canvas { display: block; background: #0c1224; }
  .demo-tag {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 4;
    font-size: 11px;
    letter-spacing: 0.18em;
    font-weight: 800;
    color: #06281c;
    background: #3ee0a0;
    padding: 4px 8px;
    border-radius: 999px;
    box-shadow: 0 0 16px rgba(62, 224, 160, 0.45);
  }
  .overlay {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
  }
  .overlay.dim { background: rgba(6, 8, 16, 0.5); }
  .hidden { display: none !important; }
  .card {
    max-width: calc(100% - 28px);
    background: rgba(8, 12, 22, 0.84);
    border: 1px solid rgba(125, 255, 224, 0.42);
    border-radius: 18px;
    padding: 16px 20px 14px;
    text-align: center;
    box-shadow: 0 16px 50px rgba(0, 0, 0, 0.45), 0 0 28px rgba(62, 224, 160, 0.08);
    animation: cardglow 2.6s ease-in-out infinite;
  }
  @keyframes cardglow {
    0%, 100% { box-shadow: 0 16px 50px rgba(0,0,0,0.45), 0 0 0 1px rgba(125,255,224,0.15); }
    50% { box-shadow: 0 16px 50px rgba(0,0,0,0.45), 0 0 26px rgba(62,224,160,0.22); }
  }
  .msg {
    font-size: clamp(16px, 3.6vw, 22px);
    font-weight: 750;
    font-weight: 700;
    letter-spacing: 0.01em;
    line-height: 1.3;
  }
  .sub, .hint {
    margin-top: 6px;
    color: rgba(232, 238, 248, 0.68);
    font-size: 13px;
    line-height: 1.35;
  }
  .kicker {
    letter-spacing: 0.22em;
    font-size: 13px;
    font-weight: 800;
    color: var(--coral);
  }
  .final-label {
    margin-top: 8px;
    font-size: 11px;
    letter-spacing: 0.18em;
    color: var(--muted);
    font-weight: 700;
  }
  .final {
    font-size: clamp(42px, 9vw, 68px);
    font-weight: 800;
    line-height: 0.95;
    font-variant-numeric: tabular-nums;
    text-shadow: 0 0 24px rgba(125, 255, 224, 0.35);
  }
  .over-best {
    margin-top: 4px;
    color: var(--gold);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  footer {
    text-align: center;
    color: var(--muted);
    font-size: 13px;
    line-height: 1.4;
    padding: 0 6px;
  }
  @media (max-width: 420px) {
    .app { padding: 10px 8px; gap: 8px; }
    h1 { font-size: 18px; letter-spacing: 0.12em; gap: 8px; }
    h1 .orb { width: 11px; height: 11px; }
    .stat { min-width: 0; padding: 6px 8px; }
    .num { font-size: 18px; }
    .lbl { font-size: 9px; }
    footer { font-size: 12px; }
    .card { padding: 14px 12px; }
  }
  @media (max-height: 520px) {
    .app { padding: 6px; gap: 6px; }
    h1 { font-size: 16px; }
    .stat { padding: 4px 8px; }
    .num { font-size: 16px; }
    footer { font-size: 11px; }
  }
</style>
</head>
<body>
<div class="app">
  <header id="top">
    <h1><i class="orb"></i>SNAKE</h1>
    <div class="stats">
      <div class="stat">
        <span class="lbl">SCORE</span>
        <span class="num" id="scoreEl">0</span>
      </div>
      <div class="stat best">
        <span class="lbl">BEST</span>
        <span class="num" id="bestEl">0</span>
      </div>
    </div>
  </header>
  <div id="stage">
    <div id="wrap">
      <canvas id="c" width="600" height="600"></canvas>
      <div class="demo-tag" id="demoTag">DEMO</div>
      <div class="overlay" id="startOverlay">
        <div class="card">
          <div class="msg">Click or press a key to start</div>
          <div class="sub">Demo playing · Arrows or WASD</div>
        </div>
      </div>
      <div class="overlay dim hidden" id="pauseOverlay">
        <div class="card">
          <div class="kicker" style="color:#7dffe0">PAUSED</div>
          <div class="hint">Press P to resume</div>
        </div>
      </div>
      <div class="overlay dim hidden" id="overOverlay">
        <div class="card">
          <div class="kicker" id="overKicker">GAME OVER</div>
          <div class="final-label">SCORE</div>
          <div class="final" id="finalScore">0</div>
          <div class="over-best" id="overBest">Best 0</div>
          <div class="hint">Space or Enter to restart</div>
        </div>
      </div>
    </div>
  </div>
  <footer id="foot">Arrows / WASD to move · P pause · Space or Enter restarts</footer>
</div>
<script>
'use strict';

const COLS = 20;
const ROWS = 20;
const N = COLS * ROWS;
const BASE = 145;

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const wrap = document.getElementById('wrap');
const scoreEl = document.getElementById('scoreEl');
const bestEl = document.getElementById('bestEl');
const startOverlay = document.getElementById('startOverlay');
const pauseOverlay = document.getElementById('pauseOverlay');
const overOverlay = document.getElementById('overOverlay');
const overKicker = document.getElementById('overKicker');
const finalScoreEl = document.getElementById('finalScore');
const overBestEl = document.getElementById('overBest');
const demoTag = document.getElementById('demoTag');

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

let view = 600;
let PAD = 14;
let cell = 28;
let dpr = 1;

let mode = 'demo';
let snake = [];
let prevSnake = [];
let dir = { x: 1, y: 0 };
let faceX = 1;
let faceY = 0;
let food = null;
let foodAge = 1;
let score = 0;
let best = 0;
let inputQueue = [];
let acc = 0.35 * BASE;
let gameTime = 0;
let particles = [];
let rings = [];
let floaters = [];
let chomp = 0;
let flash = 0;
let shake = 0;
let shownT = 0.35;
let ptr = null;

function cycleNumber(x, y) {
  if (x === 0) return (COLS - 1) * ROWS + (ROWS - 1 - y);
  const rowBase = y * (COLS - 1);
  if ((y & 1) === 0) return rowBase + (x - 1);
  return rowBase + (COLS - 1 - x);
}

function cellFromCycle(idx) {
  idx = ((idx % N) + N) % N;
  const zig = (COLS - 1) * ROWS;
  if (idx >= zig) return { x: 0, y: ROWS - 1 - (idx - zig) };
  const y = Math.floor(idx / (COLS - 1));
  const off = idx % (COLS - 1);
  const x = (y & 1) === 0 ? off + 1 : (COLS - 1 - off);
  return { x, y };
}

function layout() {
  const top = document.getElementById('top');
  const foot = document.getElementById('foot');
  const availW = Math.max(160, window.innerWidth - 20);
  const availH = Math.max(160, window.innerHeight - top.offsetHeight - foot.offsetHeight - 28);
  const size = Math.max(180, Math.floor(Math.min(availW, availH, 600)));
  view = size;
  PAD = Math.max(10, Math.round(size * 0.028));
  cell = (size - PAD * 2) / COLS;
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  wrap.style.width = size + 'px';
  wrap.style.height = size + 'px';
  canvas.style.width = size + 'px';
  canvas.style.height = size + 'px';
  canvas.width = Math.floor(size * dpr);
  canvas.height = Math.floor(size * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

function setupSnake() {
  if (mode === 'demo') {
    const len = 6;
    const headIdx = 45;
    snake = [];
    prevSnake = [];
    for (let i = 0; i < len; i++) {
      snake.push(cellFromCycle(headIdx - i));
      prevSnake.push(cellFromCycle(headIdx - i - 1));
    }
    dir = { x: snake[0].x - snake[1].x, y: snake[0].y - snake[1].y };
  } else {
    const y = (ROWS / 2) | 0;
    const x = 6;
    snake = [];
    for (let i = 0; i < 4; i++) snake.push({ x: x - i, y: y });
    prevSnake = snake.map(function (s) { return { x: s.x - 1, y: s.y }; });
    dir = { x: 1, y: 0 };
  }
  faceX = dir.x || 1;
  faceY = dir.y || 0;
}

function occupiedSet() {
  const set = new Set();
  for (let i = 0; i < snake.length; i++) set.add(snake[i].x + ',' + snake[i].y);
  return set;
}

function spawnFood() {
  const occ = occupiedSet();
  if (occ.size >= N) { food = null; return; }
  if (mode === 'demo') {
    const hIdx = cycleNumber(snake[0].x, snake[0].y);
    for (let d = 5; d < N; d++) {
      const c = cellFromCycle(hIdx + d);
      if (!occ.has(c.x + ',' + c.y)) {
        food = { x: c.x, y: c.y };
        foodAge = 0;
        return;
      }
    }
  }
  for (let attempt = 0; attempt < 60; attempt++) {
    const x = (Math.random() * COLS) | 0;
    const y = (Math.random() * ROWS) | 0;
    if (!occ.has(x + ',' + y)) {
      food = { x: x, y: y };
      foodAge = 0;
      return;
    }
  }
  for (let y = 0; y < ROWS; y++) {
    for (let x = 0; x < COLS; x++) {
      if (!occ.has(x + ',' + y)) {
        food = { x: x, y: y };
        foodAge = 0;
        return;
      }
    }
  }
  food = null;
}

function bump(el) {
  el.classList.remove('bump');
  void el.offsetWidth;
  el.classList.add('bump');
}

function updateHUD(popScore, popBest) {
  scoreEl.textContent = String(score);
  bestEl.textContent = String(best);
  if (popScore) bump(scoreEl);
  if (popBest) bump(bestEl);
}

function enqueue(x, y) {
  if (!x && !y) return;
  const last = inputQueue.length ? inputQueue[inputQueue.length - 1] : dir;
  if (x === last.x && y === last.y) return;
  if (x === -last.x && y === -last.y) return;
  if (inputQueue.length >= 3) return;
  inputQueue.push({ x: x, y: y });
}

function cellBlocked(x, y, ignoreTail) {
  if (x < 0 || y < 0 || x >= COLS || y >= ROWS) return true;
  const limit = ignoreTail ? snake.length - 1 : snake.length;
  for (let i = 0; i < limit; i++) {
    if (snake[i].x === x && snake[i].y === y) return true;
  }
  return false;
}

function aiDirection() {
  const head = snake[0];
  const hIdx = cycleNumber(head.x, head.y);
  const nxt = cellFromCycle(hIdx + 1);
  const eatingNext = !!(food && nxt.x === food.x && nxt.y === food.y);
  if (!cellBlocked(nxt.x, nxt.y, !eatingNext)) {
    const d = { x: nxt.x - head.x, y: nxt.y - head.y };
    if ((d.x || d.y) && !(d.x === -dir.x && d.y === -dir.y)) return d;
  }
  const options = [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }];
  for (let i = 0; i < options.length; i++) {
    const d = options[i];
    if (d.x === -dir.x && d.y === -dir.y) continue;
    const nx = head.x + d.x;
    const ny = head.y + d.y;
    const eating = !!(food && food.x === nx && food.y === ny);
    if (!cellBlocked(nx, ny, !eating)) return d;
  }
  return dir;
}

function cellCenter(x, y) {
  return {
    x: PAD + x * cell + cell / 2,
    y: PAD + y * cell + cell / 2
  };
}

function burst(x, y) {
  const colors = ['#ff4d6d', '#ffd166', '#fff6f8', '#ff8fab', '#ffe08a', '#7dffe0'];
  for (let i = 0; i < 18; i++) {
    const a = (i / 18) * Math.PI * 2 + (Math.random() - 0.5) * 0.45;
    const sp = cell * (1.3 + Math.random() * 2.3);
    particles.push({
      x: x,
      y: y,
      vx: Math.cos(a) * sp,
      vy: Math.sin(a) * sp,
      life: 1,
      decay: 0.75 + Math.random() * 0.55,
      size: cell * (0.06 + Math.random() * 0.1),
      color: colors[i % colors.length],
      rect: i % 4 === 0
    });
  }
  if (particles.length > 90) particles.splice(0, particles.length - 90);
  rings.push({ x: x, y: y, life: 1, maxR: cell * 1.7 });
  if (rings.length > 8) rings.shift();
}

function endRound(won) {
  if (mode === 'demo') {
    score = 0;
    setupSnake();
    spawnFood();
    foodAge = 1;
    updateHUD(false, false);
    return false;
  }
  mode = won ? 'win' : 'over';
  flash = won ? 0.35 : 0.7;
  shake = won ? 2 : 8;
  overKicker.textContent = won ? 'YOU WIN' : 'GAME OVER';
  overKicker.style.color = won ? '#7dffe0' : '#ff6b8a';
  finalScoreEl.textContent = String(score);
  overBestEl.textContent = 'Best ' + best;
  overOverlay.classList.remove('hidden');
  pauseOverlay.classList.add('hidden');
  return false;
}

function tick() {
  if (mode === 'demo') {
    const d = aiDirection();
    if (d) dir = d;
  } else if (inputQueue.length) {
    const n = inputQueue[0];
    if (n.x === -dir.x && n.y === -dir.y) inputQueue.shift();
    else dir = inputQueue.shift();
  }

  const head = snake[0];
  const nh = { x: head.x + dir.x, y: head.y + dir.y };
  if (nh.x < 0 || nh.y < 0 || nh.x >= COLS || nh.y >= ROWS) return endRound(false);

  const eating = !!(food && nh.x === food.x && nh.y === food.y);
  const limit = eating ? snake.length : Math.max(0, snake.length - 1);
  for (let i = 0; i < limit; i++) {
    if (snake[i].x === nh.x && snake[i].y === nh.y) return endRound(false);
  }

  const old = snake.map(function (s) { return { x: s.x, y: s.y }; });
  if (eating) {
    snake = [nh].concat(old);
    prevSnake = old.concat([old[old.length - 1]]);
  } else {
    snake = [nh].concat(old.slice(0, -1));
    prevSnake = old;
  }

  if (eating) {
    const prevBest = best;
    score += 10;
    if (score > best) best = score;
    updateHUD(true, best !== prevBest);
    const p = cellCenter(nh.x, nh.y);
    burst(p.x, p.y);
    floaters.push({ x: p.x, y: p.y - cell * 0.15, life: 1, vy: cell * 1.15 });
    if (floaters.length > 8) floaters.shift();
    chomp = 1;
    if (snake.length >= N) return endRound(true);
    spawnFood();
  }
  return true;
}

function stepDuration() {
  if (mode === 'demo') return BASE;
  return Math.max(82, BASE - Math.floor(score / 50) * 8);
}

function updateFX(dt) {
  const s = dt / 1000;
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    p.x += p.vx * s;
    p.y += p.vy * s;
    const drag = Math.exp(-2.4 * s);
    p.vx *= drag;
    p.vy *= drag;
    p.life -= p.decay * s;
  }
  particles = particles.filter(function (p) { return p.life > 0; });
  for (let i = 0; i < rings.length; i++) rings[i].life -= s / 0.48;
  rings = rings.filter(function (r) { return r.life > 0; });
  for (let i = 0; i < floaters.length; i++) {
    floaters[i].y -= floaters[i].vy * s;
    floaters[i].life -= s / 0.7;
  }
  floaters = floaters.filter(function (f) { return f.life > 0; });
  if (chomp > 0) chomp = Math.max(0, chomp - s / 0.16);
  if (flash > 0) flash = Math.max(0, flash - s / 0.42);
  if (shake > 0) shake = Math.max(0, shake - dt / 28);
  if (foodAge < 1) foodAge = Math.min(1, foodAge + s / 0.16);
  const rate = 1 - Math.exp(-dt / 70);
  faceX += (dir.x - faceX) * rate;
  faceY += (dir.y - faceY) * rate;
  const m = Math.hypot(faceX, faceY) || 1;
  faceX /= m;
  faceY /= m;
}

function segXY(i, t) {
  const a = prevSnake[i] || snake[i];
  const b = snake[i];
  return {
    x: PAD + (a.x + (b.x - a.x) * t) * cell + cell / 2,
    y: PAD + (a.y + (b.y - a.y) * t) * cell + cell / 2
  };
}

function strokePath(pts) {
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
  ctx.stroke();
}

function mixRGB(a, b, t) {
  const r = Math.round(a[0] + (b[0] - a[0]) * t);
  const g = Math.round(a[1] + (b[1] - a[1]) * t);
  const bl = Math.round(a[2] + (b[2] - a[2]) * t);
  return 'rgb(' + r + ',' + g + ',' + bl + ')';
}

function drawBoard() {
  ctx.fillStyle = '#0c1224';
  ctx.fillRect(0, 0, view, view);

  const gx = PAD;
  const gy = PAD;
  const gw = COLS * cell;
  const gh = ROWS * cell;
  ctx.fillStyle = '#10182e';
  ctx.fillRect(gx - 4, gy - 4, gw + 8, gh + 8);

  for (let y = 0; y < ROWS; y++) {
    for (let x = 0; x < COLS; x++) {
      if ((x + y) & 1) {
        ctx.fillStyle = 'rgba(255,255,255,0.02)';
        ctx.fillRect(gx + x * cell, gy + y * cell, cell, cell);
      }
    }
  }

  ctx.strokeStyle = 'rgba(255,255,255,0.04)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  for (let i = 0; i <= COLS; i++) {
    const x = gx + i * cell;
    ctx.moveTo(x, gy);
    ctx.lineTo(x, gy + gh);
  }
  for (let j = 0; j <= ROWS; j++) {
    const y = gy + j * cell;
    ctx.moveTo(gx, y);
    ctx.lineTo(gx + gw, y);
  }
  ctx.stroke();

  const L = Math.max(10, cell * 0.55);
  ctx.strokeStyle = 'rgba(125, 255, 224, 0.55)';
  ctx.lineWidth = 2;
  ctx.lineCap = 'square';
  const x0 = gx - 3, y0 = gy - 3, x1 = gx + gw + 3, y1 = gy + gh + 3;
  const corners = [[x0, y0, 1, 1], [x1, y0, -1, 1], [x0, y1, 1, -1], [x1, y1, -1, -1]];
  for (let i = 0; i < corners.length; i++) {
    const c = corners[i];
    ctx.beginPath();
    ctx.moveTo(c[0], c[1] + c[3] * L);
    ctx.lineTo(c[0], c[1]);
    ctx.lineTo(c[0] + c[2] * L, c[1]);
    ctx.stroke();
  }

  const vg = ctx.createRadialGradient(view / 2, view / 2, view * 0.22, view / 2, view / 2, view * 0.72);
  vg.addColorStop(0, 'rgba(0,0,0,0)');
  vg.addColorStop(1, 'rgba(0,0,0,0.3)');
  ctx.fillStyle = vg;
  ctx.fillRect(0, 0, view, view);
}

function drawFood() {
  if (!food) return;
  const p = cellCenter(food.x, food.y);
  const grow = 0.45 + 0.55 * Math.min(1, foodAge);
  const pulse = 1 + Math.sin(gameTime / 190) * 0.07;
  const r = cell * 0.27 * pulse * grow;
  if (r <= 0.4) return;

  ctx.save();
  ctx.shadowColor = '#ff4d6d';
  ctx.shadowBlur = cell * 0.85;
  const g = ctx.createRadialGradient(p.x - r * 0.35, p.y - r * 0.4, r * 0.1, p.x, p.y, r);
  g.addColorStop(0, '#ffe4ea');
  g.addColorStop(0.42, '#ff4d6d');
  g.addColorStop(1, '#c21f42');
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
  ctx.fill();
  ctx.restore();

  ctx.fillStyle = 'rgba(255,255,255,0.78)';
  ctx.beginPath();
  ctx.arc(p.x - r * 0.32, p.y - r * 0.34, Math.max(0.8, r * 0.26), 0, Math.PI * 2);
  ctx.fill();

  if (cell >= 15) {
    ctx.strokeStyle = '#6b3a22';
    ctx.lineWidth = Math.max(1, cell * 0.055);
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(p.x + r * 0.05, p.y - r * 0.75);
    ctx.quadraticCurveTo(p.x + r * 0.35, p.y - r * 1.25, p.x + r * 0.1, p.y - r * 1.55);
    ctx.stroke();
    ctx.fillStyle = '#3dce7a';
    ctx.beginPath();
    ctx.ellipse(p.x + r * 0.55, p.y - r * 1.2, r * 0.42, r * 0.2, -0.7, 0, Math.PI * 2);
    ctx.fill();
  }

  for (let i = 0; i < 3; i++) {
    const a = gameTime / 420 + i * (Math.PI * 2 / 3);
    const ox = Math.cos(a) * cell * 0.46;
    const oy = Math.sin(a) * cell * 0.3;
    ctx.fillStyle = 'rgba(255, 224, 150, 0.85)';
    ctx.beginPath();
    ctx.arc(p.x + ox, p.y + oy, Math.max(1, cell * 0.045), 0, Math.PI * 2);
    ctx.fill();
  }
}

function drawEyes(hx, hy, headR) {
  const fx = faceX, fy = faceY;
  const px = -fy, py = fx;
  const eyeR = Math.min(headR * 0.3, Math.max(1.45, cell * 0.125));
  const spread = Math.min(headR * 0.4, headR - eyeR - 0.4);
  const fwd = Math.min(headR * 0.16, headR - eyeR - 0.4);
  for (let s = -1; s <= 1; s += 2) {
    const ex = hx + fx * fwd + px * spread * s;
    const ey = hy + fy * fwd + py * spread * s;
    ctx.fillStyle = 'rgba(6, 30, 20, 0.45)';
    ctx.beginPath();
    ctx.arc(ex, ey, eyeR * 1.28, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#f6fffb';
    ctx.beginPath();
    ctx.arc(ex, ey, eyeR, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#10241c';
    ctx.beginPath();
    ctx.arc(ex + fx * eyeR * 0.4, ey + fy * eyeR * 0.4, eyeR * 0.5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,0.95)';
    ctx.beginPath();
    ctx.arc(ex - eyeR * 0.28, ey - eyeR * 0.3, Math.max(0.6, eyeR * 0.22), 0, Math.PI * 2);
    ctx.fill();
  }
}

function drawTongue(hx, hy, headR) {
  const phase = (gameTime % 860) / 860;
  if (phase < 0.64) return;
  const ext = Math.sin(((phase - 0.64) / 0.36) * Math.PI);
  const fx = faceX, fy = faceY;
  const px = -fy, py = fx;
  const len = headR * 0.62 * ext;
  const bx = hx + fx * headR * 0.9;
  const by = hy + fy * headR * 0.9;
  const tx = bx + fx * len;
  const ty = by + fy * len;
  ctx.save();
  ctx.strokeStyle = '#ff5d7a';
  ctx.lineWidth = Math.max(1, cell * 0.05);
  ctx.lineCap = 'round';
  ctx.beginPath();
  ctx.moveTo(bx, by);
  ctx.lineTo(tx, ty);
  ctx.moveTo(tx, ty);
  ctx.lineTo(tx + fx * len * 0.42 + px * len * 0.3, ty + fy * len * 0.42 + py * len * 0.3);
  ctx.moveTo(tx, ty);
  ctx.lineTo(tx + fx * len * 0.42 - px * len * 0.3, ty + fy * len * 0.42 - py * len * 0.3);
  ctx.stroke();
  ctx.restore();
}

function drawSnake(t) {
  if (snake.length < 2) return;
  const pts = [];
  for (let i = snake.length - 1; i >= 0; i--) pts.push(segXY(i, t));

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

  ctx.strokeStyle = 'rgba(62, 224, 160, 0.16)';
  ctx.lineWidth = cell * 0.98;
  ctx.shadowColor = 'rgba(62, 224, 160, 0.5)';
  ctx.shadowBlur = cell * 0.65;
  strokePath(pts);
  ctx.shadowBlur = 0;
  ctx.shadowColor = 'transparent';

  ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
  ctx.lineWidth = cell * 0.82;
  strokePath(pts);

  ctx.lineWidth = cell * 0.68;
  const denom = Math.max(1, pts.length - 2);
  for (let i = 0; i < pts.length - 1; i++) {
    const u = i / denom;
    ctx.strokeStyle = mixRGB([14, 92, 64], [198, 255, 232], u);
    ctx.beginPath();
    ctx.moveTo(pts[i].x, pts[i].y);
    ctx.lineTo(pts[i + 1].x, pts[i + 1].y);
    ctx.stroke();
  }

  ctx.strokeStyle = 'rgba(255,255,255,0.2)';
  ctx.lineWidth = Math.max(1, cell * 0.14);
  strokePath(pts);
  ctx.restore();

  const head = pts[pts.length - 1];
  const hx = head.x;
  const hy = head.y;
  const headR = cell * 0.48 * (1 + chomp * 0.1);
  const fx = faceX, fy = faceY;

  ctx.save();
  ctx.shadowColor = 'rgba(20, 90, 64, 0.55)';
  ctx.shadowBlur = cell * 0.35;
  const hg = ctx.createRadialGradient(hx - fx * headR * 0.15, hy - fy * headR * 0.15, headR * 0.15, hx, hy, headR);
  hg.addColorStop(0, '#1f8f62');
  hg.addColorStop(1, '#0b3d2a');
  ctx.fillStyle = hg;
  ctx.beginPath();
  ctx.arc(hx, hy, headR, 0, Math.PI * 2);
  ctx.fill();
  ctx.restore();

  ctx.strokeStyle = 'rgba(220, 255, 240, 0.35)';
  ctx.lineWidth = Math.max(1, cell * 0.055);
  ctx.beginPath();
  ctx.arc(hx, hy, headR, 0, Math.PI * 2);
  ctx.stroke();

  ctx.fillStyle = '#49e0aa';
  ctx.beginPath();
  ctx.ellipse(
    hx + fx * headR * 0.28,
    hy + fy * headR * 0.28,
    headR * 0.48,
    headR * 0.34,
    Math.atan2(fy, fx),
    0,
    Math.PI * 2
  );
  ctx.fill();

  drawEyes(hx, hy, headR);
  drawTongue(hx, hy, headR);
}

function drawFX() {
  for (let i = 0; i < rings.length; i++) {
    const r = rings[i];
    ctx.strokeStyle = 'rgba(255, 209, 102, ' + (r.life * 0.75) + ')';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(r.x, r.y, r.maxR * (1 - r.life) + cell * 0.15, 0, Math.PI * 2);
    ctx.stroke();
  }
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    ctx.globalAlpha = Math.max(0, Math.min(1, p.life));
    ctx.fillStyle = p.color;
    ctx.beginPath();
    if (p.rect) {
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.rotate(p.life * 3);
      ctx.fillRect(-p.size, -p.size, p.size * 2, p.size * 2);
      ctx.restore();
    } else {
      ctx.arc(p.x, p.y, Math.max(0.6, p.size), 0, Math.PI * 2);
      ctx.fill();
    }
  }
  ctx.globalAlpha = 1;

  ctx.save();
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.font = '800 ' + Math.max(12, Math.floor(cell * 0.5)) + 'px ui-sans-serif, system-ui, sans-serif';
  for (let i = 0; i < floaters.length; i++) {
    const f = floaters[i];
    ctx.globalAlpha = Math.max(0, f.life);
    ctx.fillStyle = '#ffe08a';
    ctx.shadowColor = 'rgba(0,0,0,0.45)';
    ctx.shadowBlur = 4;
    ctx.fillText('+10', f.x, f.y);
  }
  ctx.restore();
  ctx.globalAlpha = 1;

  if (flash > 0) {
    ctx.fillStyle = 'rgba(255, 70, 90, ' + (flash * 0.32) + ')';
    ctx.fillRect(0, 0, view, view);
  }
}

function draw(t) {
  shownT = t;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, view, view);
  ctx.save();
  if (shake > 0) {
    ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
  }
  drawBoard();
  drawFood();
  drawSnake(t);
  drawFX();
  ctx.restore();
}

function hideStart() {
  startOverlay.classList.add('hidden');
  demoTag.classList.add('hidden');
}

function resetBoard() {
  inputQueue = [];
  particles = [];
  rings = [];
  floaters = [];
  flash = 0;
  shake = 0;
  chomp = 0;
  score = 0;
  setupSnake();
  spawnFood();
  acc = 0.35 * BASE;
  updateHUD(false, false);
}

function startPlayer(key) {
  if (mode !== 'demo') return;
  mode = 'playing';
  resetBoard();
  foodAge = 1;
  hideStart();
  pauseOverlay.classList.add('hidden');
  overOverlay.classList.add('hidden');
  if (key && DIR_KEYS[key]) {
    enqueue(DIR_KEYS[key][0], DIR_KEYS[key][1]);
  }
}

function restart() {
  mode = 'playing';
  resetBoard();
  foodAge = 1;
  hideStart();
  pauseOverlay.classList.add('hidden');
  overOverlay.classList.add('hidden');
}

function togglePause() {
  if (mode === 'playing') {
    mode = 'paused';
    pauseOverlay.classList.remove('hidden');
  } else if (mode === 'paused') {
    mode = 'playing';
    pauseOverlay.classList.add('hidden');
  }
}

let last = 0;
function loop(now) {
  if (!last) last = now;
  let dt = now - last;
  last = now;
  if (dt < 0) dt = 0;
  if (dt > 100) dt = 100;

  let t = 1;
  if (mode === 'playing' || mode === 'demo') {
    const stepMs = stepDuration();
    acc += dt;
    let alive = true;
    while (alive && acc >= stepMs) {
      acc -= stepMs;
      alive = tick();
    }
    if (!alive && mode !== 'demo' && mode !== 'playing') acc = 0;
    if (mode === 'playing' || mode === 'demo') t = Math.min(1, acc / stepMs);
    else t = 1;
  }

  if (mode !== 'paused') {
    gameTime += dt;
    updateFX(dt);
  }
  draw(t);
  requestAnimationFrame(loop);
}

function onKey(e) {
  const key = e.key;
  const handled = key.indexOf('Arrow') === 0 || key === ' ' || key === 'Enter' || key === 'p' || key === 'P' || !!DIR_KEYS[key];
  if (handled) e.preventDefault();
  if (e.repeat && mode !== 'demo') return;

  if (mode === 'demo') {
    if (handled || key.length === 1) startPlayer(key);
    return;
  }
  if (mode === 'over' || mode === 'win') {
    if (key === ' ' || key === 'Enter') restart();
    return;
  }
  if (key === 'p' || key === 'P') {
    togglePause();
    return;
  }
  if (mode === 'paused') return;
  if (DIR_KEYS[key]) enqueue(DIR_KEYS[key][0], DIR_KEYS[key][1]);
}

window.addEventListener('keydown', onKey, { passive: false });
window.addEventListener('pointerdown', function (e) {
  if (mode === 'demo') {
    startPlayer();
    ptr = { x: e.clientX, y: e.clientY };
    return;
  }
  if (mode === 'over' || mode === 'win') {
    restart();
    return;
  }
  if (mode === 'playing') ptr = { x: e.clientX, y: e.clientY };
});
window.addEventListener('pointerup', function (e) {
  if (!ptr || mode !== 'playing') { ptr = null; return; }
  const dx = e.clientX - ptr.x;
  const dy = e.clientY - ptr.y;
  ptr = null;
  if (Math.hypot(dx, dy) < 28) return;
  if (Math.abs(dx) > Math.abs(dy)) enqueue(Math.sign(dx), 0);
  else enqueue(0, Math.sign(dy));
});
window.addEventListener('pointercancel', function () { ptr = null; });
wrap.addEventListener('contextmenu', function (e) { e.preventDefault(); });
window.addEventListener('resize', function () {
  layout();
  draw(shownT);
});

layout();
mode = 'demo';
setupSnake();
spawnFood();
foodAge = 1;
acc = 0.35 * BASE;
updateHUD(false, false);
draw(0.35);
requestAnimationFrame(loop);
</script>
</body>
</html>
Reasoning trace (67,644 tokens)
I'm starting to build a polished Snake game in a single HTML file using vanilla JavaScript and a canvas.

I'm mapping out the core mechanics: a 20×20 grid, WASD and arrow key controls with queued input, a snake that cannot reverse onto itself, food that never spawns on the snake, and game over with score overlay and restart/pause controls.

I'm adding visual polish: smooth movement, a snake head with eyes facing its direction, and a particle burst when food is eaten. The game will track current and best session scores. I'm also planning a self-playing demo mode behind the start screen, and designing a dark, elegant theme with neon emerald accents and glowing food. The layout must be responsive from 1280×800 down to 375px with no scrollbars or console errors.

Smooth interpolation will animate the snake between grid cells. The score will sit at the top of the canvas. The best score will live only in memory. The demo overlay will be semi-transparent so the AI can play visibly underneath. The canvas will scale responsively.

The grid will be 20×20 cells, sized dynamically to the viewport. Smooth movement will interpolate the snake’s visual position between grid cells over a 120ms timer. The input queue will hold up to two or three pending direction changes per tick, and the snake will never reverse onto itself. The demo AI will use greedy pathfinding toward food while avoiding walls and its own body, ensuring the AI plays for several seconds before the user takes over.

I've defined four game states: demo, playing, paused, and game over. When the user clicks or presses a key during the demo, the game will start a fresh session rather than letting them take over the AI snake. This keeps the transition clean and avoids inheriting a weird AI position.

Any key or click starts the demo; arrow and WASD keys then take over. Space and Enter restart only after game over, not during active play. Food particles will spawn at the food location and fade out. I'm now finalizing the color palette.

The visual design is now fully defined. The background will be deep navy with a radial gradient. The board will use a lighter tone with subtle grid lines. The snake body will be a rounded gradient from teal to emerald. The head will be larger and brighter with white sclera and black pupils offset to face the snake’s direction. Food will be a pulsing red-gold orb with a soft glow. The layout will be a centered card with a title, a score row, the canvas, and a bottom hint. At 1280×800 the canvas will be about 560 pixels. At 375 pixels the cell size will shrink to fit.

I'm calculating exact canvas dimensions to prevent scrollbars. At 800 pixels wide the board plus header and footer fits within the viewport. At 375 pixels the board shrinks to about 343 pixels. I'm using a flex container to scale the canvas to available space. I've also started the implementation with constants for 20 columns, 20 rows, and a base speed.

I've settled on a 140-millisecond tick interval. The snake will interpolate visually between grid positions using requestAnimationFrame. Each segment stores its grid coordinates, and the tail stays fixed during growth so the snake can expand smoothly.

I'm now sketching the core game loop. Each tick the snake head advances one cell in the current direction, and the tail is only removed when the snake is not growing. I'm also defining a Particle class for the food burst, planning to spawn 18 to 24 particles with velocity and lifespan. Food will be placed randomly on an empty cell. I'm beginning to draft the input queue logic.

The input queue now rejects reverse and duplicate directions, keeping only up to three pending changes. On each tick, the next valid direction is dequeued. The demo AI will score each of the four cardinal directions and prefer the one that moves the snake closer to the food.

The demo AI will also weigh open space via flood fill to avoid trapping itself. The start overlay will be semi-transparent with glowing text and allow clicks underneath. Pressing P during the demo will start the game, while during active play it toggles a "PAUSED" overlay.

The game over overlay will display "GAME OVER", the current score, and a restart prompt. The snake will be drawn as a thick stroke with round caps through interpolated points, with a gradient body color and a head circle on top. Each segment stores its previous and current grid positions to interpolate smoothly between ticks.

I'm refining the snake's visual geometry. A polyline with round joins and a lineWidth of 85 percent of the cell size will replace separate circles to keep the body continuous and avoid gaps at corners. The head will be a slightly larger circle with eyes drawn on top.

I've pinned down the exact eye geometry: pupils sit 0.18 cell units forward of the head, offset 0.12 cell units perpendicular to the snake's direction. Food will pulse and glow via a sine-wave scale animation. I'm adding a subtle score pop when the snake eats, a faint head glow, and a vignette on the grid background. The color palette is now fixed at deep navy #0b1020 for the background and #12182b for the board.

The snake body will use a teal-to-emerald gradient, the head will be bright emerald, and food will be a glowing coral diamond with a gold center. Particles will mix the food color with white and gold. The UI structure is now sketched as a header with the title and score row, followed by a canvas board wrapper.

The overlays will be HTML divs rather than canvas text, keeping the start, pause, and game-over messages crisp and scalable. The footer will display the controls. The start overlay must remain clearly visible so the demo can be captured.

The first paint will draw the board, snake, food, and scores immediately before the animation loop starts. The resize function will scale the canvas to the smaller of the wrapper’s width or height, set the device pixel ratio, and apply a transform so the board stays crisp on high-DPI screens.

The CSS layout is now defined. The page uses a flex container with hidden overflow, and the game card is capped at 640 pixels wide and 800 pixels tall. The board wrapper is a square aspect ratio with a max-height of 100 viewport minus 160 pixels to keep the game visible on small screens.

The CSS aspect-ratio and max-height approach is being replaced with a JavaScript layout function. The board wrapper will be sized dynamically to fit the header and footer, keeping overlays aligned with the canvas.

The layout function now caps the board at 600 pixels wide and 200 pixels tall, subtracting header, footer, and padding from the viewport. The canvas will be sized to the smaller of the available width and height. The game speed is fixed at 7 ticks per second, or about 143 milliseconds per move, so the snake interpolates smoothly without feeling too fast.

The snake speed will stay constant. Growth will be handled by setting a pending flag so the tail is not removed on the next move. Food will spawn on a random empty cell after each eat, and the code will guard against infinite loops if the board fills up.

Restart will reset the snake to a four-cell horizontal segment centered left, facing right, clear the score, spawn new food, and purge all particles. The demo AI will evaluate each cardinal direction, rejecting the reverse of the current heading and any move that hits a wall or the snake’s body except the tail.

The demo AI now scores each valid direction by distance to food and open space. It prefers moves that bring the snake closer to the food while avoiding traps, and slightly favors continuing straight.

The demo AI now adds a 1.5-point bonus for continuing straight. The flood-fill helper treats the snake body as blocked, except the tail when the snake is not growing, and caps reachable cells at 80 for performance.

The demo will begin with a five-cell snake at (5,10) facing right, with food placed at (9,10) so the AI consumes it within one to two seconds and the particle burst is visible immediately.

The 3-second screenshot will likely miss the particle burst, since each burst lasts only about 0.6 seconds. The snake with eyes and the score are the main visual hooks. The food will stay close so the snake can consume it quickly, and no fake ambient particles will be added.

I'll extend the particle burst to 1.2 seconds so it might catch the screenshot. A small "demo" badge will mark the preview mode. I'm also adding an eat flash on the board and beginning the keyboard input listener.

The keyboard handler now blocks default scrolling for all game controls. In demo mode, pressing any direction key immediately starts the game and queues that direction for the snake. During the game-over screen, Space or Enter triggers a restart.

The pause toggle now uses the P key. Clicks will only start the demo, not restart the game or resume from pause. The WASD and arrow key mappings are being defined.

The keyboard mapping now covers all arrow keys and WASD. The snake will queue up to three pending directions per tick, applying one per frame. It will never reverse onto itself by rejecting any queued direction that is the exact opposite of the last queued or currently active heading.

The reverse check now compares against the most recently queued direction rather than the current heading, so a quick left-right sequence while moving up is rejected while a down-left sequence is allowed. The animation loop will use a 140 millisecond step interval with a 50 millisecond cap on frame delta to keep movement smooth.

The animation loop now accumulates frame time and runs the AI and game logic in a tight while loop, advancing by exactly one step per tick. The interpolation factor is being reconsidered so the snake renders smoothly between steps rather than snapping to the next position.

The interpolation model is now fixed. The snake stores its previous and current target cells, and the progress ratio equals the accumulated frame time divided by the step interval. This keeps the snake visually near its starting cell at the beginning of each move and at its destination just before the next step.

The snake will freeze at the collision cell when the game ends mid-step. Particle bursts will be paused during the pause overlay and continue briefly after game over so the burst can finish.

The food pulse will use performance.now() so it keeps animating during pause. The draw function clears the canvas and renders the grid, food, snake, and particles in order. Each snake segment is positioned by interpolating between its previous and current target cells using the step progress ratio.

The step function now applies queued directions one at a time, rejecting any that would reverse the snake. It calculates the new head position and immediately checks for wall collisions before updating the snake array.

The step function now checks for self-collision against the snake body, excluding the tail when the snake is not growing. It saves the previous positions before updating the array, then unshifts the new head and either decrements the growth counter or pops the tail. The previous array is kept at the old length so interpolation between the old and new head positions works correctly.

The previous and current snake arrays must be kept at the same length so interpolation works correctly. When the snake grows, the tail segment stays fixed, so the previous array needs to be padded with an extra entry at the head to match the new current array length.

The snake will be drawn as a smooth pixel trail along the history of head positions, rather than as discrete segments with interpolation.

The snake will be drawn as a smooth pixel trail along the history of head positions, rather than as discrete segments with interpolation.

The snake will be drawn as a smooth rounded line through its interpolated head positions, with round joins at corners for a polished look.

The snake head will carry eyes that face its current movement direction, snapping instantly to new headings or easing slightly for polish. Particle bursts will originate at the food center in pixel space and scatter outward.

The score and best-score displays will update the DOM each frame. The session best persists in a variable and resets only on a full page reload. Visual polish includes a soft shadow under the snake, a rounded inset board border with subtle inner glow, tabular numerals for the score, a faint checkerboard grid, and a floating +10 text that rises and fades when the snake eats. The color scheme will be a refined dark arcade look.

The background will use a radial gradient from deep blue to near-black with faint noise. The title will read "SERPENT" or "SNAKE" in emerald with wide letter-spacing. Food will be a glowing jewel with an inner gradient and sparkle, matching the neon aesthetic. Overlays will be centered via CSS flexbox.

The overlay will use a semi-transparent dark blur with rounded corners and pointer-events disabled so clicks pass through to the game. A document-level click listener will start the game on any click. The game-over overlay will be more opaque than the pause overlay so the score remains readable.

The start overlay will use a semi-transparent dark background so the snake remains visible behind a pill-shaped message card. A "DEMO" badge will sit in the corner of the board.

The demo badge will disappear once the game starts. A "PAUSED" overlay will appear during pause, and the footer will always display the controls. The layout will scale down at 375px width with compact headers and a 12px footer, using flex shrink to prevent overflow. The body will hide horizontal overflow and scale content if the viewport is too short. Box-sizing border-box will keep the canvas from causing scrollbars.

Canvas rendering will use device pixel ratio scaling to keep the grid crisp on high-DPI screens. The first frame will be drawn immediately before the animation loop starts so the board is visible right away. The initial snake and food will be placed in a way that creates an interesting opening path rather than a straight line off the board. During the demo, the AI will run continuously in the game loop, so the snake will be visibly moving within the first few seconds.

A persistent sparkle will be added to the food so its glow remains visible even if the AI spawns the second food far away. The snake will have already eaten once or twice within the first three seconds, so the demo will still show movement and score changes. Particle bursts will be treated as a bonus effect rather than a guaranteed visual.

A soft shadow and vignette will be drawn on the canvas. The snake head will use a larger circle with white eyes and black pupils offset toward the movement direction. A small forked tongue will flick every second to add character and make the head more distinct in screenshots.

The title will include a small snake icon drawn as inline SVG or CSS. The demo AI will automatically restart instantly after its death so the animation continues uninterrupted for the judge.

The die() function will skip the game-over overlay during demo mode and instead reset the round instantly. The AI will be hardened to survive at least thirty seconds on a 20×20 grid. It will avoid moves that reduce reachable space below the snake length, and among safe moves it will prefer the one that minimizes Manhattan distance to the food.

The AI will prefer straight movement to keep the snake looking smooth. If chasing food would trap the snake in a space smaller than its length, it will wander in the largest available area instead. A full Hamiltonian cycle is overkill for a 20×20 grid, so a simple zigzag path is being considered as a more practical fallback.

The AI will follow a Hamiltonian cycle around the board. It can shortcut toward food when the tail is behind on the cycle, avoiding self-collision. A simple zigzag cycle is being drafted for the 20×20 grid.

The Hamiltonian cycle is being defined as a zigzag across the grid, with the leftmost column serving as the return path from bottom to top. A cell index formula is being drafted to map every position to its sequential visit order.

The zigzag cycle is proving tricky to connect cleanly at the bottom row. A standard even-width cycle pattern is being adopted instead, using a `hamNext` helper that moves right on even rows and left on odd rows, with special handling for the leftmost column and bottom edge.

The `cycleNumber` helper is being verified against a 4×4 grid to confirm the leftmost column returns from bottom to top and the remaining columns alternate direction.

The 4×4 grid test confirmed the cycle closes cleanly from (0,0) back to (1,0). The leftmost column returns from bottom to top while the remaining columns zigzag, and this pattern holds for even grid widths.

The cycle breaks on odd row counts because the last zigzag row ends at the right edge, not the left column. The 20×20 grid satisfies the even-row requirement. The AI will follow the cycle but can shortcut ahead toward food when the food’s cycle index lies between the head and tail.

A `forwardDist` helper is being drafted to measure cycle distance between two indices. The AI will shortcut toward food only when the next cell’s index is closer to the food and the tail is not between them. The `aiDir` function is being structured to compute the head and tail cycle indices.

The `aiDir` function is being implemented. It computes the head, tail, and food cycle indices, then measures forward distances to both. The snake will evaluate its four valid neighbors, preferring the natural cycle next step or a shortcut toward food.

The AI's neighbor evaluation now rejects moves that hit the snake body except the tail when the snake is not growing. Each valid neighbor is scored by its cycle distance from the head: the natural next step gets 100, and a shortcut toward food that does not pass the tail gets 200 plus that distance. This ranks safe cycle-following above shortcuts, but shortcuts are still preferred when they bring the snake closer to food.

The AI will prefer the neighbor that is farthest along the cycle toward food, scoring it 150 plus that distance. Any legal move is a fallback. A shortcut can still cause self-collision because the snake body may no longer occupy a contiguous cycle segment after shortcuts.

The AI shortcut is being proven safe. Because the snake body always occupies a contiguous arc of the cycle, jumping ahead to an empty cell between head and tail preserves that invariant. The snake remains a single connected segment, so the shortcut cannot cause self-collision.

A `cellFromCycle` helper is being drafted to map cycle indices back to grid coordinates. The snake will be initialized by placing its starting cells along contiguous positions on the cycle.

The `cellFromCycle` helper is verified against a 4×4 grid. The snake will be initialized with five cells starting at cycle index 30, with each body segment placed at the previous index.

The snake's initial direction is set from the neck to the head. The previous snake state is stored so the first frame renders the body one step behind the head, preventing the snake from appearing frozen before the first animation tick.

The demo AI will queue a direction at the start of each step and the game consumes it during the step. Duplicate directions are skipped to prevent unnecessary reversals. Food will be placed a few cells ahead on the cycle so the snake can take a visible shortcut immediately.

A three-entry input queue will buffer player keystrokes. The AI will set its next direction directly instead of queuing it, preventing conflicts with leftover player inputs.

The demo AI will call `step()` directly for its next direction instead of using the input queue. The player queue will be cleared when the user starts playing. When the snake hits a wall, the animation freezes at the last valid position so the head stops just before the boundary.

The pause state will freeze the accumulated step time and reset the last timestamp on resume. The animation loop clamps each frame delta to 100 ms, advances the accumulator, and processes steps in a tight loop until the accumulator empties.

Particles will update continuously during both demo and play, including after game over so they fade out. Food particles will spawn at the food pixel position before the food moves. A floating +10 score text will appear at the food location. The score element will use a CSS class toggle for a pop effect. No sound effects will be added. The start overlay will run the demo automatically.

The exact start message will read "Click or press a key to start". A click listener will activate the game and focus the iframe so keyboard input works immediately. Keydown events will also be captured in case the user presses a key before clicking.

Arrow keys, WASD, and Space will have default browser behavior suppressed to prevent scrolling and page navigation. Space will restart the game after a game over, while Enter and Space during play will be ignored. If a player presses a direction key before the game starts, that input will be applied immediately for responsiveness. When switching from demo to play, the board will reset so the player sees a clean snake.

A `beginPlay` function will switch the game from demo to play, hide the start overlay, reset the snake, and apply any initial key press immediately. A click listener will trigger this transition from the demo screen. Clicking the game-over overlay will also restart the game for extra polish, though Space and Enter remain the required restart keys.

The best score will remain visible at all times. The visual polish is being refined for a polished screenshot: a dark vertical gradient background, subtle grid lines, a soft center radial highlight, food with a glow effect, and a snake body drawn as a gradient stroke with a thinner inner highlight. The snake head will be a gradient circle with eyes and a tongue. Particles will be rendered on top.

The snake body will be drawn as a continuous gradient stroke from tail to head using a linear gradient from dark green to bright teal. The line will be rounded with a width of 78% of the cell size. Shadow blur for the food glow is considered acceptable at this small scale.

The snake head will be drawn as a larger circle in a brighter color so it covers the stroke end and sits well with the eyes. The eye positions are calculated using the snake's facing angle, offset by a small distance in the perpendicular direction. Each eye is rendered as a white circle with a smaller pupil.

The snake head will interpolate its facing vector toward the current direction over 80 ms. The tongue will flicker red at a fixed rate.

The snake head will be drawn as a larger circle in a brighter color so it covers the stroke end and sits well with the eyes. The eye positions are calculated using the snake's facing angle, offset by a small distance in the perpendicular direction. Each eye is rendered as a white circle with a smaller pupil.

The snake head will interpolate its facing vector toward the current direction over 80 ms. The tongue will flicker red at a fixed rate.

Food will be drawn as a pulsing red circle with a white highlight and a small leaf shape to resemble an apple. A burst particle effect will spawn 22 colored fragments at the food location when the snake eats.

The burst particle system is being implemented. Each fragment spawns at the food location with a random angle and speed, a lifespan decay rate, and a random size and color. A ring effect will also appear at the food position. The particle update loop is being drafted to process these fragments each frame.

Particle velocity decays by 5% each second. Fragments are filtered out once their lifespan expires. Floating score text fades in and out using its remaining life. The grid lines are drawn as faint white strokes at one pixel width.

The grid will use both faint vertical and horizontal lines and tiny white dots at each intersection. The board will sit edge to edge with no padding.

Rounded canvas corners would clip the snake at the edges. The playfield will be inset by a few pixels inside the canvas, with a rounded rectangle background drawn behind the grid.

The playfield will be inset by 10 pixels inside the canvas. Cell size is calculated from the remaining space. Wall collisions will use the grid bounds, while the inset area forms the visual border. The snake will move every 135 milliseconds for a snappy feel.

The snake will cross a 20×20 board in about 2.7 seconds. A brief speed boost on eating food was considered but rejected. The score will update instantly, and the best score will live-update during play whenever the current score exceeds it. Layout guards will prevent division by zero if the canvas is too small, and the script will run after the CSS is applied to avoid layout timing issues.

A window resize listener will recalculate cell size and center the grid. The game will use only local fonts and avoid external resources, alerts, or localStorage. The snake's facing vector will be normalized to a unit length. Direction input is ignored while paused or after game over to prevent queued turns.

Direction keys will queue for the snake while the game is in demo mode. The P key will start play rather than pause the demo, so it will not trigger an immediate pause. Restarting from the game-over overlay will enter playing mode directly.

The title will display "BEST" with a Unicode crown icon. The snake path will use round joins at each cell center so turns look like a continuous tube.

The snake body will use a 0.72×cell stroke width with round caps so segments overlap and look solid. The head will be drawn at 0.42×cell radius, slightly larger than the body. Interpolated segment positions between cells will create smooth diagonal cuts at corners rather than sharp grid turns.

A small "DEMO" tag will sit at the top-left of the board so judges can tell the game is in demo mode. The start message will pulse gently via CSS for a polished look. The game-over score will display as a large number.

A footer will display the controls and restart hint, scaling down and wrapping on narrow screens. The header will use a wide flex layout with a large title and a compact control strip.

The layout is being finalized with a flexbox centering strategy. The stage will flex to fill available space, and a media query will shrink the title and stats on screens narrower than 420 pixels.

The canvas will be sized dynamically from the stage element. A subtle animated radial gradient will be added to the body background for depth. Food spawn logic will check every snake cell to ensure food never appears on the snake.

The game step function will queue the snake’s next direction, compute the new head, check for wall or self-collision, and only then rebuild the snake and spawn new food. This ordering guarantees the snake never moves onto a cell that would immediately collide with itself.

The snake will grow by exactly one cell on the same move that it eats food. The tail will not be removed during that step, so the snake length increases immediately without a delayed counter.

Self-collision checks will include the tail when the snake is about to grow, and exclude it otherwise. The snake always starts at length four, so a length-one edge case is not a concern.

The demo AI only needs to keep the snake on a valid cycle. When the player takes over, the snake can be reset to any shape. The demo will restart on its own cycle, but if the snake fills the entire 20×20 board, spawnFood will fail and the snake will die. A win condition for a fully filled board was considered but rejected.

A fully filled board will trigger a win state instead of a game over. After the snake eats food and its length reaches the total cell count, the game will display a "YOU WIN" overlay with the score and allow restarting via Space or Enter. The demo AI will also reset automatically when it achieves this win.

The demo AI will verify that each cycle step moves the snake to an adjacent cell. If no legal cycle move exists, it will fall back to a safe neighbor or die and reset. A board-filling win is considered unlikely in the demo's short runtime.

The demo AI will place food roughly eight cells ahead of the snake so the snake can see it and eat it within about one second. The snake will start at cycle index 40 with its body along indices 39 through 36, facing the next cell in the cycle.

The demo AI will place food at fixed cycle positions ahead of the snake so it eats roughly once per second. This makes the particle burst more likely to appear in a screenshot. Player mode will still use random food spawns.

The demo AI will search six to ten cells ahead on the cycle for an empty spot, falling back to random placement if the path is blocked. A soft glow border will be drawn inside the canvas to frame the grid.

The direction queue will be tested to confirm that rapid successive presses are honored across ticks, while reverse commands are rejected to prevent the snake from turning onto itself.

The keydown handler will ignore `e.repeat` events to prevent the direction queue from filling with repeated presses.

A CSS scale animation will be applied to the score element to make updates feel more dynamic. The win overlay will use the same restart keys as the game-over screen.

No touch controls or swipe gestures will be added. The game is designed for desktop with keyboard input, and the demo AI will keep it visible on mobile screens.

Touch controls will be added as an optional enhancement. A pointer-down event will start play from the demo state. A pointer-up event will read the swipe distance and enqueue the corresponding horizontal or vertical direction. This keeps the game playable on mobile without requiring it.

A mode guard will prevent the game from starting twice if both a document click and a canvas pointerdown fire. The canvas will use `touch-action: none` to block native scroll gestures. The first frame will be drawn synchronously to avoid a blank screen. A high-quality background pattern will be added.

The start overlay will be a translucent glass card so the snake remains visible through it. The session best score will initialize at zero and will not include any demo points, resetting when the player begins. A screenshot at the three-second mark should capture the snake having moved roughly twenty-two cells, eaten twice or three times, and displaying a score of twenty to thirty with particles and glowing food.

The demo AI will update the displayed score during its run so the UI looks alive. The session best score will remain zero until the player begins, which means the score may briefly exceed the best during the demo.

The session best score will reset to zero when the player begins, so the AI's demo points do not carry over and the player starts with a clean slate.

The session best score will include demo achievements. It will not reset when the player begins, so the displayed best reflects the highest score reached since the page loaded.

The demo AI will update the displayed score and best score so the UI stays consistent and never shows a score higher than the best. When the player begins, the round score resets to zero while the session best remains. The demo will also restart automatically after death without a game-over flicker. The visual palette will use emerald snake, coral food, and a midnight blue background with a distinct head.

A thin white highlight stroke will be drawn over the snake body to create a glossy center line. A soft green shadow will be cast beneath the snake for depth.

The step function will now read the demo AI or the input queue, reject any direction that would reverse the snake, and compute the new head position. The interpolation arrays for smooth movement must also be adjusted when the snake grows versus when it moves without growing.

The step function now validates the new head position against the grid edges and checks for self-collision using the snake length adjusted for the upcoming food. When the snake grows, the new head is inserted at the front and the previous tail is duplicated to preserve the interpolation path; when it moves without growing, the tail is removed and the previous positions are shifted.

The step function now awards ten points, updates the HUD, triggers a particle burst at the food position, and spawns new food. If the snake fills the entire grid, the round ends as a win. The interpolation array lengths were verified to match the snake length in both growth and movement cases.

The `endRound` function will reset the demo state by clearing the score, repositioning the snake, respawning food, and zeroing the accumulator to prevent the game loop from advancing the snake instantly after a death. In player mode, the function will freeze the accumulator at one step and switch to the win or game-over overlay.

The game loop will apply the demo AI before each step and break immediately if the round ends. Player input will be cleared when play begins so the AI cannot be interrupted. The AI direction function will be written to avoid reversing the snake.

The demo AI now computes cycle indices for the snake head, tail, and food to measure distances along the grid. It evaluates each of the four cardinal directions while rejecting any that would reverse the snake, and discards moves that would hit the grid edges.

The demo AI now checks each candidate move against the snake body to avoid collisions, using the snake length adjusted for the upcoming food. It scores a move at cycle distance one as 1000 to favor safe forward movement.

The demo AI now scores candidate moves by cycle distance: 2000 plus the step distance for safe forward progress, and 10 minus the distance for legal but off-cycle moves. It returns the best direction, or keeps the current one if no valid move exists.

The demo AI's shortcut scoring is being refined. Because the snake only checks immediate neighbors, each candidate move has a small step distance. The AI will favor the largest legal jump toward food that does not pass the tail, rather than simply the closest neighbor.

The AI can take large shortcuts by moving to a cell that is adjacent in space but further along the cycle path. For example, from a cell on the left edge, the snake might jump to a cell on the right edge that is much later in the cycle, provided the move is safe and the tail is not in the way.

If food is behind the snake on the cycle, the AI may take large shortcuts and move chaotically, but the occupancy and tail checks keep it safe. When no food is present, the AI will default to following the cycle naturally by setting the food index to the next cell.

The forward distance from head to tail is N minus the snake length, so the AI can safely take shortcuts when the snake is short and restricts them as it grows. Natural follow is always available as a fallback, but shortcuts are preferred when they score higher.

The AI's shortcut logic is verified to handle the tail cell safely. When the snake is nearly full, the next cycle cell is the tail, so the shortcut condition fails. Natural follow still scores 1000 and the occupancy check allows the snake to move into the tail cell, preventing a collision.

A near-full-grid edge case was noted: if the snake grows into the tail cell that also holds food, the move is rejected and the AI may die. The AI logic is otherwise considered solid. An inner wall border will be added for visual polish. The layout function will compute the canvas size from the stage element's available width and height, clamping it to a minimum of 80 pixels.

The canvas will render at a device-pixel ratio of up to 2x for sharper visuals. The layout function computes a padding of at least 8 pixels or 2% of the canvas size, and the cell size is derived from the padded area divided by the column count. The HTML structure nests the canvas inside a wrapper div that matches the canvas dimensions exactly, so overlays can be positioned directly over the drawing surface.

The wrapper div will be sized to match the canvas exactly. A CSS min-height was considered to ensure the canvas is visible before JavaScript runs, but the synchronous script at the end of the body typically completes before the first paint, so the canvas will be drawn before the browser renders it.

The layout function will fall back to window dimensions if the stage element has no computed height, using the top and bottom bars as fixed offsets. It caps the available width at 640 pixels and the height at 160 pixels minimum.

The layout will use direct window dimensions instead of flexbox for reliability. A faint checkerboard background will be drawn behind the grid cells so the layout remains visible in screenshots. A subtle cyan border will be added around the canvas.

The canvas wrapper will use a rounded border and hidden overflow so the canvas fills it cleanly. The start overlay will feature a dark translucent card with a cyan border, a soft emerald glow, and a large emerald diamond title. Text will scale responsively using clamp to stay readable in screenshots.

The start overlay will display the exact text "Click or press a key to start" with slight letter spacing. The game-over overlay will show a "GAME OVER" kicker, the final score, and a hint to press Space or Enter to restart. The pause overlay will display "PAUSED" and a hint to press P to resume. Both overlays are hidden by default and centered via flex.

The score will use tabular numerals for consistent width. A gold +10 floater will appear on each food pickup. The food renderer will guard against undefined food. A cellCenter helper will compute exact cell centers.

A `segXY` helper will interpolate between the previous and current snake segment positions to render smooth body segments. The snake's facing vector will ease toward the current direction each frame using a fast exponential decay, keeping the head and eyes aligned with the movement path.

On game over, the snake eyes will face the direction of the crash. A red vignette flash and camera shake will be added to the end-of-round effect, with the shake decaying each frame.

The camera shake and red vignette decay will use delta time instead of a per-frame multiplier. The page will use overflow hidden on the root elements to prevent scrollbars. The canvas will receive a tabindex so clicks focus the game and keydown events reach the window listeners.

Keydown listeners will be attached to the window, with preventDefault applied only to handled keys to avoid interfering with normal browser behavior. The game will begin in demo mode immediately, so the AI makes its first moves within the first frames. The initial prevSnake array will be seeded with the previous cycle positions so the first paint shows the snake one cell behind its current head, and the interpolation will animate it forward into place.

The snake will start at cycle index 42 with a length of five cells. The previous positions array will be offset one step back from the current snake so the first frame interpolates the body forward into place.

The snake will start with a half-step interpolation so the first paint shows a continuous moving pose rather than a grid-snapped body. At a 375-pixel layout, the eye radius will be about 1.8 pixels, keeping the snake’s eyes visible even at small cell sizes.

The snake eye radius will be clamped to a minimum of 2.2 pixels and scaled to 13% of the cell size. Pupils will be at least 1.2 pixels and 45% of the eye radius. At the smallest layout size the eyes may exceed the head diameter, so the eye distance will scale with the cell size to prevent overlap.

The snake head will use a distinct color so it remains recognizable even without eyes. Eye radius is now clamped to 32% of the head radius, with a minimum of 1.6 pixels and a maximum of 12% of the cell size. The design is complete and ready for careful code review.

Canvas state leaks will be prevented by wrapping effects in save and restore. The win overlay will display "CLEARED" or "YOU WIN" with the score and a restart hint. A small green demo tag will sit at the top-left corner and hide during active play.

The particle burst will use 18 short-lived particles in mixed square and circle shapes. The code is expected to be 600–800 lines. A keyboard hint will sit inside the start overlay. A reverse-direction guard will also be added to the step function as a final safety net.

Food will scale up from zero on spawn. The score will be emerald and the best score amber or gold. Snake segments may be drawn as rounded capsules with a per-segment gradient.

The snake will be drawn as a stroke with a distinct head, rather than rounded capsules. The canvas wrapper will use a 12-pixel inset padding and an 18-pixel border radius. This clips the outer padding area but not the snake itself, though corner cells may be slightly clipped at the edges.

The corner-cell head is 24 pixels from the canvas edge, safely beyond the 18-pixel border-radius clip zone, so the snake will not be clipped at the corners.

The border radius was reduced to 10 pixels and the padding to 8 pixels. This keeps the snake head safely away from the canvas corners even at the smallest layout size.

A viewport meta tag will be added for mobile scaling. A subtle moving sheen will be applied to the snake using a time-based gradient. The overlay will use backdrop-filter to blur the canvas behind it. The body element will have its focus outline removed to avoid odd browser highlights. The code is now ready to be written.

The game will run at a steady 7.2 cells per second, with the AI demo stepping every 140 milliseconds so it remains visible and fluid. A slight speed increase every 50 points is being considered for the player.

The player will gain speed every 40 points, while the demo mode stays locked at the base speed so it remains judged at a steady pace.

The game-over overlay will display the session best score alongside the current score. The AI pathing logic is being verified against the 20×20 grid before writing the final code.

A silent validation will verify the snake’s cycle path is continuous and adjacent on the grid. If the cycle is broken, the demo will fall back to greedy AI to prevent a broken path from crashing the game.

The cycle path will be validated silently to avoid any console output. A manual check of the cycle mapping for a 20×20 grid confirms the inverse logic is correct.

The particle burst will use one large soft glow and sixteen smaller particles. The glow will linger for 1.5 seconds at the food position to extend the eating visual. Food will spawn eight cells ahead of the snake during demo mode.

The first animation frame will be clamped to 80 milliseconds to prevent a huge delta from breaking the step loop. The game loop will use a function to keep the step interval dynamic.

The delta-time clamp is set to 100 milliseconds so the snake does not fast-forward when the tab is backgrounded. On game over, particles continue updating so their bursts finish, but they freeze during pause.

Food pulse and floating particles will advance using a paused-free game timer so they keep animating during pause. A small apple stem will be added to the food for instant visual recognition.

The rendering order is finalized: background, grid, food, snake, particles, floaters, and flash overlay. Food will carry a faint path glow beneath it. The demo tag will sit under the centered start card so both remain visible.

The start card will be hidden and the game-over overlay shown when the game ends. A key pressed to begin the game is queued and applied on the next step, so the snake cannot reverse onto itself during the first move. This slight delay is acceptable for responsiveness.

A footer with key icons and a brief movement hint will sit below the board. The start card will use a dark glass background for high contrast against the snake.

A CSS pulse animation will glow the start card border. Score labels will sit above the numbers. Snake eyes will gain a tiny white highlight for extra polish. The file is ready to be written.

The food spawn logic now tries a cycle-based scan first, then random attempts, then a full grid scan to guarantee a valid empty cell.

The title will use a CSS diamond mark instead of a Unicode snake emoji to avoid inconsistent font rendering.

The restart function will reset the score to zero while preserving the session best, clear all particles and overlays, and reinitialize the snake and food. The game-over overlay will display "YOU WIN" when the snake clears the grid.

A `startPlayer` helper will switch from demo to player mode and apply the initial direction key. The pause toggle will preserve the last frame timestamp to prevent a large delta spike when resuming.

The step interval is recalculated inside the loop so a score change mid-step does not corrupt the timing. After a death, the interpolation factor is clamped to one so the snake freezes in its final position.

The tick function will now enforce the reverse-direction guard at the very start of each step. It discards any queued input that would make the snake turn directly back onto itself. The demo AI will also be wired directly into tick so it updates the snake’s direction every frame.

The snake head will use a rounded rectangle with a darker outline so it reads clearly even in a still image. The eyes will face the snake’s direction with a 90-millisecond ease for snappy responsiveness. A radial gradient will give the head a soft emerald glow.

The snake head will use a dark green stroke to stand out from the body. A neck overlap will seal the gap between the head and the tube. Floating score labels will use a font size of at least ten pixels to prevent rendering errors at fractional cell sizes.

The score font will scale with cell size at 700 weight. Canvas CSS dimensions will be integers to prevent blurry scaling, with internal resolution adjusted for device pixel ratio. The game wrapper will clip overflow and use a flex column layout.

The canvas will use a dark blue CSS background so the board appears immediately before JavaScript runs. The entire game will be delivered as a single fenced code block.

A brief scale pop will animate the snake head when it eats. The best score will also animate when it updates. A shorthand querySelector helper is being added for the DOM elements.

The game-over overlay will display "Best 40" and show a zero score if the snake dies immediately. The canvas will carry an aria-hidden attribute. A pointerdown listener on the window will start the demo on any click, while keydown handles the player controls.

The game will rely solely on pointerdown to avoid double events. A pointerup listener will track finger or mouse movement during active play.

Pointer movement during play will be interpreted as directional input, with a 24-pixel dead zone to ignore accidental drags. A click on the game-over overlay will restart the game, supplementing the Space and Enter restart keys. The body will not block touch actions globally; only the game wrapper will use `touch-action: none` to prevent pull-to-refresh.

The body will use `touch-action: manipulation` and `user-select: none` to prevent text selection and unwanted touch gestures. A manual `roundRectPath` helper will draw the snake head instead of relying on `ctx.roundRect`, ensuring compatibility across browsers.

A radial gradient vignette will cast a soft dark shadow across the board to deepen the visual depth.

The vignette will be drawn before the snake so it does not obscure the head. The layout is being optimized for a 1280×800 screenshot and a 375px mobile breakpoint, with a media query to stack the title and score labels vertically on narrow screens.

The header will be set to the board width via JavaScript so the title and score labels align flush with the board edges. On narrow screens the header shrinks to 18px title text, 16px score numbers, and 9px labels, while the footer wraps to two lines.

A thin accent line will sit beneath the header. The header width is set to the board size, but its height is independent, so the layout stays stable. At 375px the header content still fits within the available space.

A layout bug was found: locking the header width to the board size causes horizontal overflow on very narrow screens, because the title and score boxes exceed the available width. The header will be allowed to span the full viewport width instead, or the score labels will be shrunk to prevent overflow.

The header will use `min(640px, 100%)` and `max-width: 100%` so it never overflows its content. Score boxes will have a `min-width` of 68 pixels, and the title will use `min-width: 0` to prevent flex shrink. This keeps the header from forcing a narrow width that would break the layout on short viewports.

The header will wrap to a second line when the viewport is narrow, preventing horizontal overflow. The body will clip any remaining overflow with `overflow-x: hidden` and `overflow: hidden` to guarantee no scrollbars appear. The best score will initialize to zero so it is visible on the first paint.

The demo will update the session best score so it never falls behind the live score. The best score will reset only on the first player start, keeping the demo’s progress visible while still giving the player a fair challenge.

The session best will no longer reset on the first player start. Instead, the HUD will always display the greater of the live score and the session best, so the best score never appears lower than the current score. A separate `playerBest` variable will track the true session high score, while the displayed best is computed dynamically.

The best score will no longer reset when the player starts. It will only rise and persist across the entire session, including the demo. This prevents the best from dropping to zero after a long demo run and keeps the displayed value consistent.

An emerald grid frame will sit inset by one pixel for a subtle depth effect. A snake length counter was considered but skipped to avoid clutter. The final file is expected to be around seven hundred lines. The smooth movement progress formula is being double-checked to ensure the accumulator correctly tracks how far into the next interval the snake has already moved.

The interpolation bug was found: the accumulator is divided by stepMs before the snake has actually moved, so the progress value lands at the start of the previous interval instead of the new one. The snake must first complete its tick, then the interpolation should begin from the old cell toward the new cell over the next interval.

The interpolation logic is confirmed correct. After each tick, the accumulator holds the leftover time, so the snake starts near its previous cell and smoothly moves toward the new target. When the accumulator nears the interval length, the snake reaches the new cell and the next tick begins from there. This creates a continuous, gapless animation without any visual jump.

The snake will visually trail its logical position by one move. Collision detection fires at the start of each interval, so the snake appears to enter a wall or food before the visual head actually reaches it. This one-frame delay is acceptable and matches the smooth interpolation.

The collision timing is being traced to confirm the snake never visually enters a wall. Because the game over triggers at the start of the interval, the snake snaps to the last valid cell before the visual head reaches the collision point. A timeline is being drawn to verify this behavior.

The collision trace confirms the snake never visually enters a wall. It arrives at the cell before the boundary, the tick detects the wall, and the game over snaps the snake to that last valid cell. The visual head never crosses the wall.

Food timing is also verified. The snake logically moves onto the food cell at tick time, then visually starts from the old head toward the food cell and reaches it at the end of the interval.

The score and particle burst currently fire at the start of each tick, before the snake visually reaches the food. This creates a one-step visual lag. The logic should instead apply the move during the tick and only trigger score and particles once the snake fully arrives at the target cell.

The particle burst is currently 145 milliseconds too early, firing while the snake head is still leaving the previous cell. The fix is to delay score and particle effects until the interpolation reaches full arrival at t=1.

A simpler model is being explored: keep the snake visually locked to the cell it just completed, and animate toward the next cell computed in advance. The move would only commit at the end of the interval, when the interpolation wraps back to zero. This would place the head at the food cell visually the instant the burst spawns, eliminating the one-step lag.

The tick fires exactly when the snake visually arrives at the target cell. At that instant the interpolation wraps from one back to zero, the head is already at the food, and the burst spawns. The snake then moves to the next cell. This confirms the existing interpolation model already satisfies the timing requirement.

The snake's visual positions are confirmed to stay perfectly continuous across ticks. After each move, the new previous array is set to the old current array, so the snake snaps back to its exact visual cell the instant the interpolation resets to zero. This guarantees no visual jump or gap between frames.

The snake direction vector is being checked against the cycle path. It currently points from the second-to-last segment to the head, which may not match the outgoing direction at a turn. If the head turns, the new head cell could land in the wrong place.

The initial direction vector was pointing from the second-to-last segment to the head, which would misplace the snake at a corner. It should instead point from the head to the next cycle cell. The AI will update the direction each tick before moving.

The initial direction vector will point from the neck to the head, matching the snake's visual facing so its eyes look toward movement. The AI's reverse check will still block the snake from turning backward.

The initial direction vector will point from the neck to the head, matching the incoming travel direction so the snake's eyes face its movement during the first interval. The previous head cell is confirmed to be snake[1], validating this setup.

The body segments are confirmed to connect smoothly without overlapping. The snake will never be empty, so a length guard is unnecessary. The implementation is ready to be written.

The start overlay will be hidden instantly with a CSS class instead of fading, since the screenshot requires it to remain visible. A second line will be added to the start card to display the arrow and WASD controls.

The start overlay will display the exact phrase "Click or press a key to start". The demo AI shortcut is being reviewed for safety: it may temporarily break the contiguous cycle invariant, but the occupied-cell check prevents any crash.

The shortcut validity proof is being verified. A shortcut to a cell between head and tail on the cycle, provided it is adjacent, preserves the contiguous body arc. The new head and tail remain connected through the old body, so the snake stays intact.

The shortcut safety proof is broken. A jump longer than one step leaves empty cycle cells between the old head and the new head. The body is no longer a contiguous arc. The invariant fails.

The shortcut logic is abandoned. It can trap the snake in a pocket and cause death. The demo AI will only follow the cycle, never shortcut.

The snake will sweep the grid in a lawnmower pattern, eating food as it passes through each cell. This predictable behavior is reliable and visually clear, ensuring the demo survives the full board without dying.

The AI direction function is being drafted. It computes the next cycle cell and points the snake forward. It blocks reverse moves and falls back to any safe direction if the next cell is occupied.

Food is placed seven cells ahead on the cycle, so the snake will always reach it. Even as the snake grows long, the food stays safely ahead until the board is nearly full. The sweeping lawnmower pattern is simple and reliable, and the snake’s eyes turn naturally at corners. This should produce a clear, polished screenshot.

The lawnmower sweep will last about three seconds, covering roughly one full row, so the snake will look alive and dynamic. A `cellBlocked` helper is being added to check walls and body occupancy, with an optional tail exclusion for safe fallback moves.

The snake will skip its tail segment when checking the next cycle cell for occupancy, preventing it from colliding with itself during a move. Food will be placed on the cycle path ahead so the snake naturally passes through it. The implementation is now ready to be written.

A detailed apple-shaped food graphic is being designed with a red body, darker bottom gradient, green leaf, brown stem, white highlight, and pulsing outer glow. Three orbiting sparkles will circle the food using gameTime for a subtle, always-visible effect. Particle bursts will use red, gold, and white tones.

The full HTML file is being composed now. The pause overlay will remain hidden during the demo. Key listeners will use preventDefault on arrow keys and WASD to stop browser scrolling and find-as-you-type behavior. The game over overlay will handle pointer events through the window listener.

A key map for WASD and arrow keys is being defined. Pause and restart keys will be handled separately. The pause hint will remain visible in the footer. Pausing will dim the canvas with an overlay instead of stopping the animation loop.

The pause overlay freezes the snake mid-interpolation because the accumulator stops advancing while the canvas still redraws. Demo resets clear the score and food without triggering the death flash or shake effects.

The game over overlay will display the final score, a win or lose message, and the session best. A red tint flash will render across the canvas during death, decaying over time. The best score is already updated when the snake eats food.

The shake effect will be applied only to the canvas context, leaving HTML overlays untouched. A CSS keyframe animation will make score numbers pop and scale up briefly when points are added.

Score and best elements will trigger a CSS pop animation on every point gain. The best score is kept in a local variable, so no localStorage or external resources are used. A window resize listener will recalculate the grid layout and redraw the canvas.

The resize handler will call `draw(lastT)` immediately after `layout()` to prevent a flash of cleared canvas. Layout reapplies the canvas transform, so the context state is preserved. A `prefers-reduced-motion` check is unnecessary.

The food graphic is being simplified for small grids. When the cell size drops below 14 pixels, the apple leaf will be removed and the food will render as a glowing orb instead. A soft inner highlight will be added to the board background. The `drawFood` function will pulse the radius using sine and fade the glow as the food ages.

The food gradient is being defined with a soft pink center fading to deep red. A white highlight is being drawn at the top-left of the apple. A brown stem is being added for cells larger than 16 pixels.

The food stem and leaf are now fully drawn with quadratic curves and a green ellipse. A burst function is being created to spawn 18 colored particles in a radial pattern around the food.

Each burst particle is assigned a random color, speed, and size. The update loop moves particles with velocity decay and removes dead ones. A radial ring effect will expand outward from the food.

The effect update loop now handles radial rings, floating particles, and the chomp flash. Food age fades in as the snake approaches. Snake eyes interpolate smoothly toward the current direction.

The snake eyes normalize their direction vector each frame. The radial ring effect draws a fading yellow stroke that expands outward from the food. Floating particles are rendered as translucent gold dots that fade away over time.

Floating score text will render as large bold centered numbers above the snake. Subtle darker segment bands will be drawn as circles along the body path to define individual snake segments. The snake will be drawn with a layered stroke effect: a wide transparent emerald glow, the main stroke, and a thin highlight stroke.

The snake body is being drawn by sampling segment positions at each frame and tracing them with a wide emerald stroke and soft shadow. Tail and head endpoints are extracted from the traced points for the head and eye drawing.

The snake body is being drawn with a three-layered stroke: a wide emerald glow, a main emerald gradient from dark green to cyan, and a faint white highlight. The head will scale slightly when the snake chomps. The trace helper is being defined for the path calls.

The trace helper now begins a fresh path for each stroke layer. The snake head will flick its tongue using gameTime. Pupils will offset in the facing direction with a white highlight. The board background drawing is being added next.

The board background is being drawn with a dark blue base and a slightly inset panel. A subtle checkerboard pattern of faint white cells is layered across the grid. Thin white grid lines are being sketched to define the cell boundaries.

A radial vignette is being added to darken the canvas edges. A border around the playfield is also being considered.

A faint cyan border is being drawn around the playfield to mark the wall boundary. The visual system is now complete. The HUD will update live during the demo so the score is visible immediately and reaches a non-zero value within one second.

The start overlay will use a transparent background so the snake and food remain visible. The centered card will not obscure the snake, which begins near the top-left of the board. Food is placed nearby at the top-right. This ensures the game state is clearly visible in the screenshot.

The snake will remain in the upper rows for the first few seconds, keeping it above the centered start card. The food sits near the top-right, so the game state stays clearly visible in the screenshot.

The food spawn offset is being set to five cells ahead of the snake head. This creates a second eat near 1.4 seconds and a third near 2.8 seconds. The particle burst from the final eat will still be visible at the three-second screenshot.

The food spawn offset is fixed at five cells ahead of the snake head. Particle bursts will last about one second. The initial snake occupies indices 42 through 38, so food at index 47 is guaranteed to be free. The start overlay and demo tag are visible by default. The pause and game-over overlays are hidden. Starting the game will hide both overlays, reset the score to zero, and switch the mode to playing while preserving the session best score.

The session best score will carry over from the demo into the player game. The score display resets to zero when the player starts. Particles are cleared on start. Food spawns randomly for the player, never on top of the snake.

A `freshRound` helper is being extracted to centralize the reset logic for both the demo and player start paths. It clears all game state, resets the snake and food, and applies the initial movement direction.

The initial page load will set mode to demo and call setupSnake and spawnFood directly, skipping the shared reset helper to avoid overwriting the session best score. The enqueue function will discard duplicate consecutive inputs to prevent the snake from reversing onto itself.

The input queue is capped at three buffered direction changes. A queued direction indicator was considered but skipped as unnecessary. The game-over card will mirror the start card’s styling for visual cohesion.

A fixed palette of dark blue, emerald, coral, gold, and muted gray is being locked in for all visuals. The title will use wide letter spacing and a heavy weight. A subtle CSS inset highlight is being added to the board wrapper to create a faint top edge glow.

The canvas will be sized exactly to the board wrapper so overlays align perfectly. The demo tag will sit above the dimmed overlays with a higher z-index so it remains visible in the corner.

The pause and game-over overlays will use a dark translucent background so the snake remains faintly visible behind them. A subtitle or footer speed line was considered but skipped. The final output will be a single fenced code block containing the complete HTML file.

The demo AI now resets the snake and score on death instead of pausing. A tail cell is treated as free unless the snake is actively eating, which prevents the snake from reversing onto itself.

The demo AI follows a valid cycle path, so it never selects a direction that would crash the snake. The initial snake placement guarantees the next cell is always empty, preventing an infinite reset loop.

The demo AI will run for roughly 58 seconds before resetting, since it cannot survive a full 400-cell cycle without food. When the snake nears maximum length, it enters a stable orbit where the tail vacates the next cell, keeping the body size constant unless food is eaten on the final cells.

The demo AI will also reset on a full-board win, not just on death. Food spawn loops are bounded to prevent infinite loops. The AI direction helper now checks whether the next cycle cell contains food before choosing a move.

The snake head will be enlarged with a distinct ring so it stands out clearly in the screenshot. The eyes are drawn as a separate layer to avoid being covered by the body or the tongue. The tongue starts at the head edge, so it will not overlap the eyes.

A media query will shrink the header padding and font sizes when the viewport height drops below 520 pixels. The final HTML file is now being composed.

The start card message will use a clamped font size so it never overflows the card on narrow viewports. The card itself will be limited to 90% width with tighter padding on mobile. The text will wrap and center, and `white-space: normal` will be set to allow line breaks.

A subtle moving gradient is being added to the page background using CSS radial gradients. The effect will use faint emerald, coral, and blue glows that shift across the viewport for a premium, polished feel.

A score chip will flash emerald whenever the snake eats food. The snake grows by exactly one cell during an eat, and the self-hit check compares against the new head position before the tail is removed. No extra length indicator is needed because the snake's growth is clearly visible.

The snake head will receive a directional-facing shine so it remains readable even at smaller cell sizes. Eye dimensions are being increased further so the eyes stay clearly visible in the screenshot.

The eye drawing logic is being tightened so the pupils stay fully inside the head and do not overflow. A dark color-scheme meta tag will be added to the HTML.

The demo snake will be six cells long for a more impressive screenshot. Its head will sit at index 48, placing it slightly deeper into the row.

The snake head is fixed at index 45, placing it at x=8, y=2. Food spawns at index 50, five cells ahead on the same row. This creates a six-cell horizontal snake facing right, with food clearly visible to its right. The layout is intentionally composed so the first paint and the three-second snapshot both show a clean, upper-middle composition.

The board padding will scale proportionally to the canvas size, and cell dimensions will remain fractional so the grid fills the space exactly without gaps. The final code is now being written.

The canvas transform will be reset at the start of every draw call to prevent the shake effect from accumulating across frames.

The shake effect will be wrapped in a save and restore block so the canvas transform does not accumulate across frames. The device pixel ratio will be stored in a layout variable for reuse. The pause overlay will keep the snake partially visible behind a 55% dark tint, and the controls hint will be added inside the pause card. The game-over score will be displayed in a large, prominent font.

Space and Enter will only restart the game after a win or loss; they will be ignored during active play and while paused. Arrow keys pressed during pause will be buffered so the snake turns immediately upon resuming, but they will not unpause the game.

Direction keys pressed during pause will be ignored rather than buffered, so accidental bumps do not trigger a turn on resume.

A pointer-based swipe control is being added alongside keyboard input. Clicking the demo board starts the player and records the pointer position. During active play, pointerdown stores the starting coordinates so pointerup can calculate swipe direction and enqueue a turn.

Swipe detection now ignores movements shorter than 28 pixels. The enqueue helper will discard any zero-vector input to prevent invalid turns. A pointercancel handler is being added to clear the stored pointer state.

A context menu will be blocked on the canvas wrapper to prevent long-press interference. The score display will use tabular numerals to keep digits from jittering as they grow. The score bump animation may slightly overflow its padded container, but that is acceptable.

The HTML score elements will default to zero so they render before JavaScript executes. The cycle-number helper and its inverse are being written to map snake indices to grid coordinates for the demo AI path.

The snake will always spawn at the same fixed index and length on every restart, so the demo composition stays consistent rather than randomizing.

Player restarts will place the snake at the board center facing right, while the demo AI keeps its fixed cycle-aligned start.

The snake face vector will be reset inside setupSnake. Food age will be zeroed on spawn. The demo loop will only zero the accumulated animation time when the game mode changes to over, not when the demo resets, preventing a visual snap-back.

The tick function now checks for self-collision against the new head position before updating the snake array. When the snake eats food, it grows by inserting the new head at the front and duplicating the tail segment; otherwise it shifts the tail off. The score increases by ten points, the best score updates if necessary, the HUD refreshes, and a particle burst fires at the food's center.

The tick function now spawns a floating particle above the food and sets a chomp flag. It also checks for a win by testing if the snake has filled the entire board. The prevSnake array is assigned directly from the old array because the objects are never mutated in place.

The score bump animation is being triggered by toggling a CSS class after forcing a reflow. A bug was spotted where the best-score chip would flash on every score update because the condition checked the new best value instead of the previous one. The fix is to capture the old best score before updating it.

The wall-hit death animation will flash the board border red. The footer will list all controls so they remain visible in the screenshot. The best-score label will read "BEST" without the word "session".

The grid will remain 20 by 20 cells. A BASE_STEP constant of 142 milliseconds is being defined. The win overlay will reuse the same restart instructions as the game-over screen. The HTML document structure is now being drafted.

The HTML document structure is now being drafted. It includes a viewport meta tag, a title, and a canvas element sized 560 by 560 pixels. The page body contains a header with an orb icon and a stats bar holding the score and best-score labels, both defaulting to zero. The canvas sits inside a wrapper div within the stage container.

The canvas width and height attributes provide a fallback size before JavaScript runs, but drawing will be deferred until after layout so the board fills the viewport correctly. The stage container uses flexbox to center the wrapper both vertically and horizontally.

The layout function will subtract the header and footer heights plus padding from the available viewport space, then cap the board width at 680 pixels. The wrap container will be sized to the smaller of that remaining space and the window width.

The board size is being capped at 600 pixels to leave breathing room and avoid an edge-to-edge fit.

The board size is now capped at 600 pixels. The header and footer will be constrained to that same width so they stay centered with the board. JavaScript will set the header and footer max-width to match the board size whenever the board shrinks.

The header and footer will be sized purely in CSS, never in JavaScript. They will use `min(600px, calc(100% - 8px))` so they can grow wider than the board and wrap naturally, avoiding horizontal scroll when the board shrinks below 180 pixels.

The app container will use `overflow: hidden` to prevent horizontal scroll. Box-sizing will be added to the CSS. Particle bursts will use 18 particles, scaled down for smaller cells so they do not look like noise. The canvas drawing will use save and restore for alpha blending on rings and floaters. Three separate arrays will hold particles, rings, and floaters instead of a single typed array. Floaters will include a font fallback to avoid errors if the chosen font fails.

Empty snake and food guards are added to prevent drawing errors. The demo tag will pulse subtly. The start card will include a small row of keycaps under the message for visual polish, and a subtitle will clarify that the demo is playing and can be taken over at any time.

The snake head will use a thin darker outline and a slightly cartoonish eye style. The tongue will flick only during a specific phase so it may not appear in every screenshot. A second smaller scale highlight is unnecessary. Segment separators at the joints were considered but skipped to avoid visual clutter at speed. The final draw order is being reviewed.

The final draw order is confirmed: reset transform, board, food and sparkles, snake body with head and eyes, rings, particles, floaters, flash, then restore. A soft shadow under the food was considered but skipped. The score numbers will be emerald and the best-score number gold so they read instantly in the screenshot. A thin underline on the title was also considered but omitted.

The code will be written in a single file without external fonts or images. A regular canvas will be used instead of OffscreenCanvas. Frame deltas will be clamped to 100 milliseconds to prevent large jumps when the tab regains focus. The snake will pause while the page is hidden. A chomp mouth was considered but skipped to avoid clashing with the eyes.

The snake head will use a `faceX` and `faceY` vector that eases toward the current direction so the eyes rotate smoothly during turns. The `drawEyes` function calculates eye positions from the head center, the face vector, and a perpendicular axis, with eye size clamped to a minimum of 1.7 pixels and a maximum of 34 percent of the head radius.

The eye drawing code uses a light skin tone for the iris, a dark pupil offset along the face vector, and a white highlight. The highlight is not direction-dependent, but the pupil orientation tracks the snake's heading.

The snake head will be a slightly larger circle than the body segments so the silhouette is clearly distinct. The head radius is being set to 0.5 times the cell size and the body stroke width to 0.68 times the cell size. This creates a visible size difference that is more noticeable than the original 0.46 versus 0.37 ratio.

The eye positions were verified to fit within the larger head radius. The body stroke width is now 0.70 times the cell size, the glow stroke width is 0.92 times the cell size, and the head radius is 0.48 times the cell size with a slight chomp expansion. The implementation is ready to be written out.

The code will use template strings and `Array.prototype.concat` while avoiding optional chaining and `Object.assign` for clarity. Food checks will use simple `&&` expressions. The CSS will be compact but complete. The start overlay will not obscure the snake because the demo keeps the snake at the top of the board, and the player overlay is hidden once the game begins.

Particles will alternate between circles and squares for visual variety. Their decay rate is slowed so bursts last about 1.4 seconds. A bold score floater with a slight shadow will appear when food is eaten.

Canvas shadow effects will be applied to the score floater and reset with save and restore. The file is being kept under 650 lines by writing efficiently without sacrificing polish. The sandbox should allow requestAnimationFrame, and the game topic is acceptable.

localStorage and alerts will be avoided. Pointer events and keyboard input will require a click to activate. The canvas context will be fetched directly without a fallback. Image smoothing will remain enabled for smooth circles. A subtle inner shadow will be drawn around the board.

The stepMs timing calculation is being reviewed for a potential bug. Computing it once per frame before the while loop is acceptable because the speed mismatch is only one frame. Recomputing t with the same stepMs used for the loop would be more consistent within the frame. Switching from demo to player mode is safe because the change happens on the next frame.

The initial accumulator is set to 0.35 times the base step of 145 milliseconds. This ensures the first frame draws the snake mid-glide instead of snapping to a grid tick. The game loop will begin with this precomputed accumulator and a single initial draw before entering the animation loop.

The canvas is cleared by setting its width, which is safe because no external fonts are used. A CSS inner glow will be added to the game wrapper. The final score overlay will use the same large gold and white type as the live score.

The game-over overlay will use a large emerald kicker for wins and a pink kicker for losses. The score will scale with clamp and use tabular numerals. A faint hint text will sit below the score.

The score label will sit above the large number, and the best score will appear as "Best 20" in gold. A few edge-case guards were added to the snake update logic for empty arrays and single-segment collisions, though they should never trigger in practice. The final artifact is ready to be written out.

The demo snake will use a clean lawnmower sweep rather than shortcutting toward food. It moves right across row 2, turns down to row 3, then left across row 3. By the three-second mark it will be facing left with eyes aligned to the left.

The snake will be about ten cells long when the three-second mark arrives, having eaten four times. The final frame will show a bent snake with eyes facing left, and the last particle burst will land precisely at the two-second mark.

The exact tick timing is being verified. The first food is reached at 674 ms, the second at 1399 ms, the third at 2124 ms, the fourth at 2849 ms, and the fifth at 3574 ms. The burst from the fourth eat lands at 2.85 s, so it will still be fully visible at the three-second mark. The food spawn logic is also being checked to ensure it places the next food five cells ahead of the snake head after each eat.

Screen shake will only trigger on death, not on eat, to keep the final screenshot clean. The snake head will briefly expand via a chomp effect at the exact moment of collision.

A golden flash ring will accompany each food collision. The game-over overlay will use a semi-transparent dark background so the snake remains visible underneath the score card.

A rounded rectangle clip will keep the grid from squaring off against the rounded wrapper edges. The canvas and wrapper will share the same dark background to prevent any white flash.

The CSS font stack is finalized as `ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`. Letter-spacing will be added to the score kicker. A guard is being added to prevent the canvas from initializing with zero height if layout measurements fail.

A decorative corner bracket will mark the board edges for a sharper HUD feel.

The visual design is finalized and ready to be written out. The score will remain in the HTML HUD rather than being drawn onto the canvas. The code will be organized into ten sections: constants, DOM references, cycle helpers, layout, game logic, effects, drawing, the animation loop, input, and initialization.

A dark outline stroke will be drawn behind the snake body to separate it from the grid. The head circle will cover the stroke's round cap at the front, while the tail cap remains visible. The draw order is shadow glow, then the dark outline, then the colored stroke.

The snake body will be drawn as individual segment lines with a color gradient interpolated between tail and head. This keeps the hue consistent through bends instead of using a single linear gradient across the whole path.

The snake body will use a custom `lerpColor` function to interpolate between three emerald tones: dark tail, bright mid-body, and pale head. Each segment will be drawn as a separate line so round caps overlap at joints and the color gradient stays continuous around corners.

The snake will get a single thin white inner highlight stroke across its entire body rather than just the front half. Dorsal scale ticks were considered but skipped to avoid visual clutter. The developer is now writing the complete file. They also verified that every required visual element will be clearly visible for automated evaluation, including the centered grid, snake head and eyes, food, score display, start message, demo movement over three seconds, and the particle burst at the three-second mark.

The start card and demo tag will be made highly visible with stronger contrast. The layout will be balanced: title at top, snake in the upper board, card centered, food near the snake, footer below, and scores in the top right. A soft glow will be added behind the food so it remains clearly visible.

The food spawn logic was verified to never place food on the snake. The demo food lead is checked against occupied cells, and the search scans upward from five steps ahead to find an empty spot. This prevents collisions even when the snake grows long enough that the lead cell might otherwise land on the tail.

The food position is copied into a fresh object to avoid reference issues. A CSS rule will remove keyboard focus outlines since the game has no focusable controls. The developer is now writing the final file.

The developer verified that the die function does not recurse into tick, and that startPlayer exits immediately when the mode is not demo. They also noticed that restart() does not check the mode flag, which could allow a race if both pointer and key events fire during startup.

A `resetBoard` helper is being added to clear all game state arrays and effect counters in one place. The demo initialization will call setup functions directly instead of routing through `freshRound`, which would incorrectly zero the score.

A hard cap of 80 particles will be added to prevent memory bloat from runaway bursts. The developer is now reviewing the smooth-movement requirement before writing the final code.

Smooth movement is handled by interpolation. The session best score will be stored in memory and labeled "BEST", starting at zero. A star icon will appear next to the best score when it is greater than zero. The start overlay will use a max-width of 92% and text wrapping to prevent mobile clipping.

The base step duration is fixed at 145 ms. Player speed scales down by 8 ms per 50 points, with a hard floor of 80 ms. Pausing will use a CSS overlay so the snake colors stay true underneath. When paused mid-step, the snake will appear frozen between cells.

The start overlay will remain visible until the first paint. A pointer cursor will be set on the game wrapper. No noscript fallback is needed. LocalStorage is not used.

The snake head will include a forked tongue facing the snake's movement direction. Pupil direction will be smoothed to animate through quick turns. No external APIs, eval, or document.write will be used.

The snake head's forked tongue is being drawn with a pulsing sine-wave extension. It retracts when the 800 ms animation phase is below 0.62 and extends to 55 % of the head radius when above. The stroke color is set to #ff5d7a and the line cap is rounded.

The forked tongue is now fully drawn with a pulsing extension and forked tip. The developer is verifying that the center overlay card will not obscure the snake during the demo, by calculating the snake head's position at the three-second mark.

The developer confirmed the center overlay card will not obscure the snake during the demo. At the three-second mark the snake head sits near the top of the board, well above the centered card. Food and particle bursts spawned in that same top area will also remain visible. They are now re-checking the exact eat timing to ensure the burst stays in view.

The exact tick-by-tick demo sequence was traced to confirm the snake head and its particle burst remain visible above the centered overlay card. The developer is now checking whether the snake body crosses the card area.

The snake body stays in the top rows, so the centered overlay card will not block it. The start card will remain centered rather than shifting lower, since the snake sits at the top and the message must stay clear.

Score numbers will use 28 px bold for readability in screenshots, scaling down to 20 px on narrow screens. A CSS animation will be added to the demo tag. The developer is now writing the complete HTML file.

The developer is adding a zeroing of `shadowBlur` immediately after the snake glow stroke to prevent the highlight stroke from picking up the shadow.

The snake body is drawn in three layers: a dark outline, a color-mixed gradient from dark tail to pale head, and a faint white inner highlight. The head point is extracted from the final segment for the next drawing pass.

The developer confirmed the color gradient runs from tail to head. They verified that round caps on shared segment endpoints overlap fully, so no gaps appear at joints. They also noted that per-segment strokes already have zero shadow blur.

The developer added a `mixRGB` helper to interpolate between two RGB color arrays. They confirmed that overlapping snake caps at joints will not cause visible double-blending because the later segment simply covers the joint with its own color. The snake head will be drawn after the body so it sits on top. They are now adding the food sparkle effect and apple icon, and believe the implementation is complete.

The food pulse will use a default scale of 1 so it is fully visible on the first frame. The developer is also weighing whether to initialize foodAge at 1 to prevent the initial food from appearing tiny.

The initial food will be full size on the first paint by setting `foodAge` to 1 after the initial spawn. Later food spawns will reset `foodAge` to 0 so the food pops in from a small size. The demo food at the three-second mark will already be nearly fully grown.

The food spawn animation is set to 0.15 seconds. The developer decided against adding faint grid dots and soft shadows, keeping the checkerboard lines subtle. The page background will stretch to the full viewport with overflow hidden, while the game board stays centered.

The footer text is being shortened to "Arrows / WASD · P pause · Space/Enter restart" so it stays on one line on narrow viewports and the layout shrinks the board cleanly without overflow.

The developer decided to keep the longer start message in the overlay and allow the footer to wrap naturally, since the text contains spaces. They confirmed no extra CSS is needed for hyphenation or overflow wrapping, and that the code will use clean indentation.

The `setupSnake` function is being written. In demo mode it creates a 6-cell snake starting at index 45 and facing left. In play mode it places the snake at the left edge near the vertical center.

The player snake is placed at x=6 with length 4, facing right and centered vertically. The demo snake direction is confirmed as right. A "Board cleared" win message will be added. The developer considers the design finalized and is ready to write the file.

The game over overlay will display "GAME OVER" with the final score and best score shown as large numbers. A CSS `.hidden` class will handle overlay visibility. The demo AI loop is guarded so it only runs after the snake is initialized, and a safety check in the tick function will die if the snake array is empty.

The snake head is being recolored from near-white to a richer green so the white eyes remain visible against the head.

A darker eye socket will be drawn behind each white eye to ensure the pupils remain visible against the snake head's green gradient.

The snake head is being refined into a deep green base with a lighter snout ellipse facing the snake's direction. White eyes will sit inside darker sockets for maximum contrast against the bright emerald body.

The snake head is now drawn with a deep green radial gradient from #1f8f62 to #0c4a32, offset slightly forward to match the snake's direction. A soft emerald shadow is applied to the head for depth.

The snout ellipse is drawn with its long axis aligned to the snake's facing direction using atan2. The eye and tongue drawing functions are then called to complete the head.

A thin lighter outline will be drawn around the snake head to separate it from the body tube. The developer is now assembling the final HTML file with organized CSS and JavaScript. Tiny texture spots on the body were considered but skipped.

The snake head is being recolored from near-white to a richer green so the white eyes remain visible against the head.

A darker eye socket will be drawn behind each white eye to ensure the pupils remain visible against the snake head's green gradient.

The snake head is being refined into a deep green base with a lighter snout ellipse facing the snake's direction. White eyes will sit inside darker sockets for maximum contrast against the bright emerald body.

The snake head is now drawn with a deep green radial gradient from #1f8f62 to #0c4a32, offset slightly forward to match the snake's direction. A soft emerald shadow is applied to the head for depth.

The snout ellipse is drawn with its long axis aligned to the snake's facing direction using atan2. The eye and tongue drawing functions are then called to complete the head.

A thin lighter outline will be drawn around the snake head to separate it from the body tube. The developer

The developer decided against setting a large initial canvas width in the HTML to prevent a brief flash of the default 300×150 size before the script runs. The script will set the correct dimensions before the first paint, so no extra HTML attributes are needed.

The script is placed at the end of the document so the browser blocks the first paint until it executes, preventing any flash of the default canvas size. The developer will also set the canvas width and height attributes to 600 and immediately apply the layout in JavaScript. They are now composing the full source code and confirmed that the key queue handles rapid successive presses across tick boundaries.

The start card will use a solid translucent background instead of backdrop-filter blur to avoid muddy visuals in screenshots. A few light code comments will be added for clarity.

The start overlay will display "Demo playing · Arrows or WASD" as its subtitle. The required phrase is kept exactly as specified. A subtle opacity pulse was considered for the message text but rejected to preserve full readability for screenshots. Only the border glow will pulse. The developer is now writing the final source code.

CSS will disable text selection and tap highlights to prevent drag artifacts. The canvas will use `display: block` to remove inline gaps, and `image-rendering: auto` will keep visuals crisp.

A `px` helper will keep coordinate math concise. Gold-ringed food will be drawn with a stroke. Score floaters reading "+10" will appear above the snake head, capped at eight. The demo AI will simply follow the cycle path, since food always sits ahead in that mode.

The demo best score will carry over into the player session. The best score will only reset when the player first takes over, so the screenshot can show the demo's climbing score.

The developer will reset the best score to zero when the player first takes over, ensuring the session starts fresh. The demo high score will still be visible in the screenshot before the click.

The best score will never decrease. It accumulates across the entire page session, including the demo, so the label stays accurate. The developer is now writing the final code.

Eye radii are being recalculated for every grid size. At the smallest 180-pixel layout the eyes will be clamped to 1.5 pixels. At the default 375-pixel layout they will be about 2.2 pixels. The developer will also clamp the eye spread so the eyes never overlap the snake head.

The resize handler will call layout and draw directly without debouncing. The game over overlay will freeze the final frame at full opacity.