Claude Haiku 5.5
by AnthropicReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 6 min 6 s
- Cost
- $0.045
- Output tokens
- 89,806
- Spent thinking
- 76,016 tokens
- Speed
- 246.7 tok/s
- Console errors
- 0
- First token after
- 2.1 s
- First visible text after
- 5 min 29 s
- Prompt tokens
- 750
- Code size
- 27.2 KB
- Finish reason
- stop
- Served by
- Claude Platform on AWS
- Model id
anthropic/claude-haiku-5.5- Reasoning sent
{"effort":"xhigh"}- Max output
- 128,000 tokens
- Generated
- Nov 10, 2026
What it was asked
The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 931 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Snake</title>
<style>
:root{
--bg:#070b17;
--text:#e9eefc;
--muted:#7f8bab;
--mint:#8dff8a;
--teal:#2dd4bf;
--glass:rgba(255,255,255,.045);
--line:rgba(255,255,255,.08);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{
height:100vh;
height:100dvh;
display:flex;
flex-direction:column;
overflow:hidden;
color:var(--text);
background:
radial-gradient(900px 520px at 50% -12%, rgba(45,212,191,.16), transparent 70%),
radial-gradient(700px 480px at 50% 115%, rgba(141,255,138,.09), transparent 70%),
var(--bg);
font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
-webkit-font-smoothing:antialiased;
user-select:none;
-webkit-user-select:none;
}
header{
flex:0 0 auto;
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
padding:16px 22px 4px;
}
.brand{display:flex;align-items:center;gap:10px;font-weight:800;font-size:18px;letter-spacing:.24em}
.logo{
width:14px;height:14px;border-radius:50%;
background:radial-gradient(circle at 35% 35%,#e4ffd2,#7dff6a 45%,#14a86a);
box-shadow:0 0 14px rgba(125,255,106,.7);
}
.brand .word{
background:linear-gradient(90deg,#d9ffc9,#8dff8a 45%,#2dd4bf);
-webkit-background-clip:text;
background-clip:text;
color:transparent;
}
.stats{display:flex;gap:10px}
.stat{
min-width:112px;
padding:8px 14px 9px;
border-radius:14px;
background:var(--glass);
border:1px solid var(--line);
text-align:right;
}
.stat-label{display:block;font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--muted)}
.stat-value{
display:block;
font-size:24px;
font-weight:700;
line-height:1.15;
font-variant-numeric:tabular-nums;
transform-origin:100% 50%;
}
.stat-value.bump{animation:bump .36s ease}
@keyframes bump{
0%{transform:scale(1)}
35%{transform:scale(1.25);color:#fff6a8}
100%{transform:scale(1)}
}
main{
flex:1 1 auto;
min-height:0;
display:flex;
align-items:center;
justify-content:center;
padding:10px 12px;
}
.board{
position:relative;
border-radius:16px;
overflow:hidden;
touch-action:none;
cursor:pointer;
box-shadow:
0 0 0 1px rgba(141,255,138,.16),
0 30px 80px rgba(0,0,0,.6),
0 0 100px rgba(45,212,191,.12);
}
.board canvas{display:block;width:100%;height:100%}
.badge{
position:absolute;
left:12px;
top:12px;
display:flex;
align-items:center;
gap:7px;
padding:5px 11px;
border-radius:999px;
background:rgba(4,8,18,.6);
border:1px solid var(--line);
font-size:11px;
letter-spacing:.14em;
text-transform:uppercase;
color:#cfeee6;
transition:opacity .3s ease;
pointer-events:none;
}
.badge.hide{opacity:0}
.badge i{
width:7px;height:7px;border-radius:50%;
background:var(--mint);
box-shadow:0 0 8px var(--mint);
animation:blink 1.4s infinite;
}
@keyframes blink{50%{opacity:.2}}
.overlay{
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
padding:18px;
background:rgba(4,7,16,.38);
opacity:0;
pointer-events:none;
transition:opacity .35s ease;
text-align:center;
}
.overlay.show{opacity:1}
.card{
width:100%;
max-width:340px;
padding:22px 26px 20px;
border-radius:20px;
background:rgba(12,18,38,.8);
border:1px solid rgba(255,255,255,.1);
box-shadow:0 24px 60px rgba(0,0,0,.5);
backdrop-filter:blur(10px);
-webkit-backdrop-filter:blur(10px);
transform:translateY(10px) scale(.97);
transition:transform .4s cubic-bezier(.2,.9,.3,1.2);
}
.overlay.show .card{transform:none}
.start{margin:0 0 8px;font-size:clamp(17px,3.6vw,21px);font-weight:700;color:#f0fff5;letter-spacing:.01em}
.title{margin:0 0 6px;font-size:clamp(24px,5vw,34px);font-weight:800;letter-spacing:.02em}
.label{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin-top:10px}
.big{font-size:52px;font-weight:800;line-height:1.1;font-variant-numeric:tabular-nums}
.sub{margin:0;color:var(--muted);font-size:14px;line-height:1.55}
.sub + .sub{margin-top:6px}
.chip{
display:inline-block;
margin-top:10px;
padding:4px 11px;
border-radius:999px;
background:linear-gradient(90deg,#ffe08a,#ff9f43);
color:#2a1700;
font-size:11px;
font-weight:800;
letter-spacing:.12em;
text-transform:uppercase;
}
.hint{margin-top:14px;color:#c9d3ea}
kbd{
display:inline-block;
min-width:24px;
padding:2px 7px;
margin:0 2px;
border-radius:7px;
background:rgba(255,255,255,.1);
border:1px solid rgba(255,255,255,.2);
border-bottom-width:2px;
font:600 12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
color:#fff;
}
footer{
flex:0 0 auto;
padding:4px 12px 14px;
text-align:center;
color:var(--muted);
font-size:12px;
letter-spacing:.02em;
}
footer kbd{font-size:11px;min-width:20px;padding:1px 6px}
@media (max-height:520px){ footer{display:none} }
@media (max-width:520px){
header{padding:12px 14px 2px}
.brand{font-size:15px;gap:8px}
.stat{min-width:86px;padding:6px 10px}
.stat-value{font-size:19px}
.stat-label{font-size:9px}
.big{font-size:42px}
}
</style>
</head>
<body>
<header>
<div class="brand"><span class="logo"></span><span class="word">SNAKE</span></div>
<div class="stats">
<div class="stat"><span class="stat-label">Score</span><span class="stat-value" id="score">0</span></div>
<div class="stat"><span class="stat-label">Best</span><span class="stat-value" id="best">0</span></div>
</div>
</header>
<main>
<div class="board" id="board">
<canvas id="cv" aria-label="Snake game board"></canvas>
<div class="badge" id="badge"><i></i><span id="badgeText">Autoplay demo</span></div>
<div class="overlay" id="overlay" role="status" aria-live="polite"><div class="card" id="card"></div></div>
</div>
</main>
<footer>Arrows / WASD steer · <kbd>P</kbd> pause · <kbd>Space</kbd> / <kbd>Enter</kbd> restart</footer>
<script>
(() => {
'use strict';
/* ---------- constants ---------- */
const COLS = 24, ROWS = 24, N = COLS * ROWS;
const TAU = Math.PI * 2;
const UP = { x: 0, y: -1 }, DOWN = { x: 0, y: 1 };
const LEFT = { x: -1, y: 0 }, RIGHT = { x: 1, y: 0 };
const DIRS = [UP, DOWN, LEFT, RIGHT];
const DX = [1, -1, 0, 0], DY = [0, 0, 1, -1];
const KEY_DIR = Object.assign(Object.create(null), {
ArrowUp: UP, ArrowDown: DOWN, ArrowLeft: LEFT, ArrowRight: RIGHT,
w: UP, s: DOWN, a: LEFT, d: RIGHT,
W: UP, S: DOWN, A: LEFT, D: RIGHT
});
const EAT_COLORS = ['#ff5d6c', '#ffb347', '#ffe066', '#ffffff', '#8dff8a'];
const DEMO_TICK = 110;
const SWIPE_MIN = 24;
/* ---------- DOM ---------- */
const $ = (id) => document.getElementById(id);
const boardEl = $('board');
const canvas = $('cv');
const ctx = canvas.getContext('2d');
const mainEl = boardEl.parentElement;
const overlayEl = $('overlay');
const cardEl = $('card');
const badgeEl = $('badge');
const badgeText = $('badgeText');
const scoreEl = $('score');
const bestEl = $('best');
/* ---------- helpers ---------- */
const idx = (x, y) => y * COLS + x;
const same = (a, b) => a.x === b.x && a.y === b.y;
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
const lerp = (a, b, t) => a + (b - a) * t;
const rnd = (lo, hi) => lo + Math.random() * (hi - lo);
const copyCells = (arr) => arr.map((s) => ({ x: s.x, y: s.y }));
/* ---------- state ---------- */
let dpr = 1, scale = 1;
let phase = 'demo'; // 'demo' | 'play' | 'paused' | 'over'
let snake = [], prev = [];
let dir = RIGHT, queue = [];
let food = null;
let score = 0, best = 0, eaten = 0, runStartBest = 0, sinceFood = 0;
let alive = true, acc = 0, deadWait = 0;
let winFlag = false, isNewBest = false;
let headAng = 0, popT = 0, shake = 0, flash = 0, clock = 0;
let particles = [], rings = [], texts = [];
let lastT = null;
let pointerStart = null;
const occ = new Uint8Array(N);
const bfsDist = new Int16Array(N);
const bfsQ = new Int16Array(N);
/* ---------- layout ---------- */
function layout() {
dpr = Math.min(window.devicePixelRatio || 1, 3);
const availW = mainEl.clientWidth - 24;
const availH = mainEl.clientHeight - 20;
const avail = Math.max(120, Math.min(availW, availH));
const cell = Math.max(5, Math.floor(avail / COLS));
const css = cell * COLS;
boardEl.style.width = css + 'px';
boardEl.style.height = css + 'px';
canvas.width = Math.round(css * dpr);
canvas.height = canvas.width;
scale = canvas.width / COLS;
}
/* ---------- UI ---------- */
function bumpEl(el) {
el.classList.remove('bump');
void el.offsetWidth;
el.classList.add('bump');
}
function updateHUD(bump) {
scoreEl.textContent = score;
bestEl.textContent = best;
if (bump) bumpEl(scoreEl);
}
function updateOverlay() {
let html = '', show = true;
if (phase === 'demo') {
html = '<p class="start">Click or press a key to start</p>' +
'<p class="sub">Arrows or WASD to steer</p>';
} else if (phase === 'paused') {
html = '<h2 class="title">Paused</h2>' +
'<p class="sub">Press <kbd>P</kbd> to resume</p>';
} else if (phase === 'over') {
html = '<h2 class="title">' + (winFlag ? 'Perfect board!' : 'Game over') + '</h2>' +
'<div class="label">Score</div>' +
'<div class="big">' + score + '</div>' +
'<div class="sub">Best ' + best + '</div>' +
(isNewBest ? '<span class="chip">New best!</span>' : '') +
'<p class="sub hint">Press <kbd>Space</kbd> or <kbd>Enter</kbd> to play again</p>';
} else {
show = false;
}
cardEl.innerHTML = html;
overlayEl.classList.toggle('show', show);
}
function updateBadge() {
badgeText.textContent = 'Autoplay demo';
badgeEl.classList.toggle('hide', phase !== 'demo');
}
/* ---------- game flow ---------- */
function resetGame(nextPhase) {
phase = nextPhase;
alive = true;
acc = 0;
deadWait = 0;
sinceFood = 0;
winFlag = false;
isNewBest = false;
const cx = COLS >> 1, cy = ROWS >> 1;
snake = [];
for (let i = 0; i < 4; i++) snake.push({ x: cx - i, y: cy });
prev = copyCells(snake);
dir = RIGHT;
queue = [];
score = 0;
eaten = 0;
headAng = 0;
popT = 0; shake = 0; flash = 0;
particles = []; rings = []; texts = [];
spawnFood();
updateHUD(false);
updateOverlay();
updateBadge();
}
function startPlay(d) {
resetGame('play');
runStartBest = best;
if (d) enqueue(d);
}
function togglePause() {
if (phase === 'play') phase = 'paused';
else if (phase === 'paused') phase = 'play';
else return;
updateOverlay();
updateBadge();
}
// Queue a direction. Validated against the last queued heading (or the
// current heading), so quick successive presses are all honored while a
// reversal onto the body is never possible.
function enqueue(d) {
const last = queue.length ? queue[queue.length - 1] : dir;
if (d.x === last.x && d.y === last.y) return; // same heading
if (d.x + last.x === 0 && d.y + last.y === 0) return; // reversal
if (queue.length >= 3) return;
queue.push(d);
}
function handleDir(d) {
if (phase === 'demo') startPlay(d);
else if (phase === 'play') enqueue(d);
}
function primaryAction() {
if (phase === 'demo' || phase === 'over') startPlay(null);
else if (phase === 'paused') togglePause();
}
function tickMs() {
if (phase === 'demo') return DEMO_TICK;
return Math.max(70, 124 - eaten * 2);
}
function syncPrev() {
prev = copyCells(snake);
}
function spawnFood() {
occ.fill(0);
for (const s of snake) occ[idx(s.x, s.y)] = 1;
const free = [];
for (let i = 0; i < N; i++) if (!occ[i]) free.push(i);
if (!free.length) { food = null; return; }
const pick = free[(Math.random() * free.length) | 0];
food = { x: pick % COLS, y: (pick / COLS) | 0 };
}
function burst(x, y, n, colors, smin, smax, life) {
for (let i = 0; i < n; i++) {
const a = Math.random() * TAU;
const s = rnd(smin, smax);
const l = life * rnd(0.6, 1.25);
particles.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s,
life: l,
max: l,
r: rnd(0.05, 0.12),
c: colors[(Math.random() * colors.length) | 0]
});
}
}
function onEat() {
eaten++;
sinceFood = 0;
score += 10;
const prevBest = best;
if (phase === 'play' && score > best) best = score;
const fx = food.x + 0.5, fy = food.y + 0.5;
burst(fx, fy, 28, EAT_COLORS, 2.5, 7.5, 0.6);
rings.push({ x: fx, y: fy, r0: 0.35, grow: 1.0, life: 0.45, max: 0.45 });
texts.push({ x: fx, y: fy - 0.25, text: '+10', life: 0.9, max: 0.9 });
popT = 0.2;
updateHUD(true);
if (best > prevBest) bumpEl(bestEl);
spawnFood();
if (!food) {
// The board is completely filled.
alive = false;
syncPrev();
if (phase === 'demo') {
deadWait = 2.5;
} else {
winFlag = true;
phase = 'over';
isNewBest = score > runStartBest;
updateOverlay();
updateBadge();
}
}
}
function die() {
alive = false;
acc = 0;
syncPrev();
shake = 0.5;
flash = 0.7;
const hx = snake[0].x + 0.5, hy = snake[0].y + 0.5;
burst(hx, hy, 30, ['#8dff8a', '#2dd4bf', '#ffffff', '#ff5d6c'], 3, 9, 0.8);
rings.push({ x: hx, y: hy, r0: 0.3, grow: 1.4, life: 0.6, max: 0.6 });
if (phase === 'demo') {
deadWait = 1.6;
} else {
phase = 'over';
isNewBest = score > 0 && score > runStartBest;
updateOverlay();
updateBadge();
}
}
// One simulation step (movement of the whole snake by one cell).
function tick() {
if (phase === 'demo') {
if (sinceFood > 500) { resetGame('demo'); return; } // safety for demo loops
queue = [aiChoose()];
}
prev = copyCells(snake);
if (queue.length) dir = queue.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) { die(); return; }
const eat = food !== null && same(nh, food);
// The tail moves away on a normal step, so it is not an obstacle then.
const limit = eat ? snake.length : snake.length - 1;
for (let i = 0; i < limit; i++) {
if (same(snake[i], nh)) { die(); return; }
}
snake.unshift(nh);
if (eat) onEat();
else snake.pop();
sinceFood++;
}
/* ---------- demo AI ---------- */
// Flood fill from start over free cells. `extra` is a cell that counts as
// passable even if occupied (the tail, which will move away).
// Fills bfsDist and returns the number of reachable cells.
function floodFrom(start, extra) {
bfsDist.fill(-1);
let qh = 0, qt = 0, count = 0;
bfsQ[qt++] = start;
bfsDist[start] = 0;
while (qh < qt) {
const c = bfsQ[qh++];
count++;
const cx = c % COLS, cy = (c / COLS) | 0;
for (let k = 0; k < 4; k++) {
const nx = cx + DX[k], ny = cy + DY[k];
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const n = ny * COLS + nx;
if (bfsDist[n] !== -1) continue;
if (occ[n] && n !== extra) continue;
bfsDist[n] = bfsDist[c] + 1;
bfsQ[qt++] = n;
}
}
return count;
}
function aiChoose() {
const head = snake[0];
const tail = snake[snake.length - 1];
const neck = snake[snake.length - 2];
let bestDir = dir, bestScore = -Infinity;
for (const d of DIRS) {
if (d.x + dir.x === 0 && d.y + dir.y === 0) continue; // never reverse
const nx = head.x + d.x, ny = head.y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const eat = food !== null && nx === food.x && ny === food.y;
occ.fill(0);
for (const s of snake) occ[idx(s.x, s.y)] = 1;
if (!eat) occ[idx(tail.x, tail.y)] = 0;
const ni = idx(nx, ny);
if (occ[ni]) continue;
occ[ni] = 1;
// Tail cell after this move (it stays put when eating).
const nt = eat ? idx(tail.x, tail.y) : idx(neck.x, neck.y);
const area = floodFrom(ni, nt);
const tailOK = bfsDist[nt] >= 0; // can still reach own tail
let foodD = -1;
if (eat) foodD = 0;
else if (food) foodD = bfsDist[idx(food.x, food.y)];
const sc = (tailOK ? 1e6 : 0) +
(foodD >= 0 ? 1e5 - foodD * 100 : 0) +
area;
if (sc > bestScore) { bestScore = sc; bestDir = d; }
}
return bestDir;
}
/* ---------- update loop ---------- */
function updateEffects(dt) {
clock += dt;
popT = Math.max(0, popT - dt);
shake = Math.max(0, shake - dt);
flash = Math.max(0, flash - dt);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
p.x += p.vx * dt;
p.y += p.vy * dt;
const k = Math.exp(-3 * dt);
p.vx *= k;
p.vy *= k;
}
for (let i = rings.length - 1; i >= 0; i--) {
rings[i].life -= dt;
if (rings[i].life <= 0) rings.splice(i, 1);
}
for (let i = texts.length - 1; i >= 0; i--) {
const tx = texts[i];
tx.life -= dt;
tx.y -= dt * 0.9;
if (tx.life <= 0) texts.splice(i, 1);
}
}
function simulate(dt) {
updateEffects(dt);
// Smooth the head's visual rotation toward its heading.
let diff = Math.atan2(dir.y, dir.x) - headAng;
diff = Math.atan2(Math.sin(diff), Math.cos(diff));
headAng += diff * Math.min(1, dt * 16);
if (phase === 'over' || phase === 'paused') return;
if (!alive) {
if (phase === 'demo') {
deadWait -= dt;
if (deadWait <= 0) resetGame('demo');
}
return;
}
acc += dt * 1000;
let guard = 0;
while (alive && (phase === 'play' || phase === 'demo') && acc >= tickMs() && guard++ < 4) {
acc -= tickMs();
tick();
}
}
/* ---------- rendering ---------- */
function draw() {
const W = canvas.width;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = '#0a1326';
ctx.fillRect(0, 0, W, W);
// Everything below is drawn in cell units.
ctx.setTransform(scale, 0, 0, scale, 0, 0);
if (shake > 0) {
const a = shake * 0.6;
ctx.translate(rnd(-a, a), rnd(-a, a));
}
drawBoard();
drawFood();
drawSnake(clamp(acc / tickMs(), 0, 1));
drawEffects();
}
function drawBoard() {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
ctx.fillStyle = ((x + y) & 1) ? '#0e1a31' : '#0b1528';
ctx.fillRect(x, y, 1, 1);
}
}
ctx.fillStyle = 'rgba(255,255,255,0.035)';
for (let i = 1; i < COLS; i++) ctx.fillRect(i - 0.01, 0, 0.02, ROWS);
for (let j = 1; j < ROWS; j++) ctx.fillRect(0, j - 0.01, COLS, 0.02);
ctx.strokeStyle = 'rgba(141,255,138,0.28)';
ctx.lineWidth = 0.06;
ctx.strokeRect(0.03, 0.03, COLS - 0.06, ROWS - 0.06);
}
function drawFood() {
if (!food) return;
const x = food.x + 0.5, y = food.y + 0.5;
const pulse = 1 + 0.05 * Math.sin(clock * 5);
// glow halo
const g = ctx.createRadialGradient(x, y, 0.05, x, y, 0.9);
g.addColorStop(0, 'rgba(255,90,110,0.45)');
g.addColorStop(1, 'rgba(255,90,110,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, 0.9, 0, TAU);
ctx.fill();
// apple body
const r = 0.34 * pulse;
const ag = ctx.createRadialGradient(x - 0.1, y - 0.1, 0.02, x, y + 0.02, r);
ag.addColorStop(0, '#ffa3ab');
ag.addColorStop(0.45, '#ff4d6a');
ag.addColorStop(1, '#b3123a');
ctx.fillStyle = ag;
ctx.beginPath();
ctx.arc(x, y + 0.02, r, 0, TAU);
ctx.fill();
// highlight
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.beginPath();
ctx.ellipse(x - 0.13, y - 0.06, 0.055, 0.035, -0.7, 0, TAU);
ctx.fill();
// stem and leaf
ctx.strokeStyle = '#7a4a24';
ctx.lineWidth = 0.045;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x, y - 0.3);
ctx.quadraticCurveTo(x + 0.02, y - 0.4, x + 0.06, y - 0.44);
ctx.stroke();
ctx.fillStyle = '#4ade80';
ctx.beginPath();
ctx.ellipse(x + 0.14, y - 0.38, 0.13, 0.06, -0.5, 0, TAU);
ctx.fill();
}
function drawSnake(t) {
const n = snake.length;
const px = new Float32Array(n), py = new Float32Array(n);
for (let i = 0; i < n; i++) {
const a = prev[Math.min(i, prev.length - 1)];
const b = snake[i];
px[i] = lerp(a.x, b.x, t) + 0.5;
py[i] = lerp(a.y, b.y, t) + 0.5;
}
const dk = flash > 0 ? Math.min(1, flash / 0.7) * 0.6 : 0; // red flash on crash
const colAt = (f) => {
let r = 160 - 140 * f, g = 255 - 85 * f, b = 120 + 30 * f;
if (dk > 0) {
r = lerp(r, 255, dk);
g = lerp(g, 84, dk);
b = lerp(b, 110, dk);
}
return 'rgb(' + (r | 0) + ',' + (g | 0) + ',' + (b | 0) + ')';
};
const widthAt = (f) => 0.84 - 0.42 * f;
const fOf = (i) => (n > 1 ? i / (n - 1) : 0);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// outline pass
ctx.strokeStyle = 'rgba(3,12,22,0.95)';
for (let i = n - 1; i >= 1; i--) {
ctx.lineWidth = widthAt(fOf(i)) + 0.13;
ctx.beginPath();
ctx.moveTo(px[i], py[i]);
ctx.lineTo(px[i - 1], py[i - 1]);
ctx.stroke();
}
// body pass (tail drawn first, so the head ends up on top)
for (let i = n - 1; i >= 1; i--) {
const f = fOf(i);
ctx.strokeStyle = colAt(f);
ctx.lineWidth = widthAt(f);
ctx.beginPath();
ctx.moveTo(px[i], py[i]);
ctx.lineTo(px[i - 1], py[i - 1]);
ctx.stroke();
}
// head
const hx = px[0], hy = py[0];
const fx = Math.cos(headAng), fy = Math.sin(headAng); // forward
const sx = -fy, sy = fx; // side
const hr = 0.44 + (popT / 0.2) * 0.07;
// tongue flicks every few seconds
if (alive && (clock % 2.6) < 0.5) {
const bx = hx + fx * hr * 0.9, by = hy + fy * hr * 0.9;
const tx = hx + fx * (hr + 0.32), ty = hy + fy * (hr + 0.32);
ctx.strokeStyle = '#ff5d6c';
ctx.lineWidth = 0.05;
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(tx, ty);
ctx.moveTo(tx, ty);
ctx.lineTo(tx + fx * 0.12 + sx * 0.08, ty + fy * 0.12 + sy * 0.08);
ctx.moveTo(tx, ty);
ctx.lineTo(tx + fx * 0.12 - sx * 0.08, ty + fy * 0.12 - sy * 0.08);
ctx.stroke();
}
const rx = hr * 1.1, ry = hr * 0.98;
ctx.shadowColor = 'rgba(141,255,138,0.6)';
ctx.shadowBlur = 14 * dpr;
ctx.fillStyle = 'rgba(3,12,22,0.95)';
ctx.beginPath();
ctx.ellipse(hx, hy, rx + 0.065, ry + 0.065, headAng, 0, TAU);
ctx.fill();
ctx.shadowBlur = 0;
const hg = ctx.createRadialGradient(hx - 0.1, hy - 0.12, 0.02, hx, hy, rx * 1.15);
hg.addColorStop(0, '#f2ffe2');
hg.addColorStop(0.45, colAt(0));
hg.addColorStop(1, colAt(0.25));
ctx.fillStyle = hg;
ctx.beginPath();
ctx.ellipse(hx, hy, rx, ry, headAng, 0, TAU);
ctx.fill();
// eyes, placed toward the front of the head
const ex = hx + fx * 0.12, ey = hy + fy * 0.12;
const eyeOff = 0.2, eyeR = 0.135;
const eyes = [
[ex + sx * eyeOff, ey + sy * eyeOff],
[ex - sx * eyeOff, ey - sy * eyeOff]
];
for (const [x, y] of eyes) {
if (alive) {
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(x, y, eyeR, 0, TAU);
ctx.fill();
// pupil looks forward
ctx.fillStyle = '#07101f';
ctx.beginPath();
ctx.arc(x + fx * 0.05, y + fy * 0.05, 0.075, 0, TAU);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(x + fx * 0.02 - 0.03, y + fy * 0.02 - 0.03, 0.022, 0, TAU);
ctx.fill();
} else {
// crossed-out eyes after a crash
const r = 0.09;
ctx.strokeStyle = '#07101f';
ctx.lineWidth = 0.055;
ctx.beginPath();
ctx.moveTo(x - r, y - r); ctx.lineTo(x + r, y + r);
ctx.moveTo(x + r, y - r); ctx.lineTo(x - r, y + r);
ctx.stroke();
}
}
}
function drawEffects() {
ctx.globalCompositeOperation = 'lighter';
for (const r of rings) {
const k = r.life / r.max;
const rad = r.r0 + (1 - k) * r.grow;
ctx.strokeStyle = 'rgba(255,220,160,' + (k * 0.8).toFixed(3) + ')';
ctx.lineWidth = 0.02 + 0.08 * k;
ctx.beginPath();
ctx.arc(r.x, r.y, rad, 0, TAU);
ctx.stroke();
}
for (const p of particles) {
const k = p.life / p.max;
ctx.globalAlpha = clamp(k * 1.2, 0, 1);
ctx.fillStyle = p.c;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * (0.4 + 0.6 * k), 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
if (texts.length) {
ctx.font = '800 0.58px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (const tx of texts) {
ctx.globalAlpha = clamp((tx.life / tx.max) * 1.5, 0, 1);
ctx.fillStyle = '#fff3c4';
ctx.fillText(tx.text, tx.x, tx.y);
}
ctx.globalAlpha = 1;
}
}
function frame(now) {
if (lastT === null) lastT = now;
const dt = clamp((now - lastT) / 1000, 0, 0.1);
lastT = now;
simulate(dt);
draw();
requestAnimationFrame(frame);
}
/* ---------- input ---------- */
window.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
const d = KEY_DIR[e.key];
if (d) {
e.preventDefault();
handleDir(d);
return;
}
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (e.repeat) return;
if (phase === 'demo' || phase === 'over') startPlay(null);
return;
}
if (e.key === 'p' || e.key === 'P') {
e.preventDefault();
if (e.repeat) return;
if (phase === 'demo') startPlay(null);
else togglePause();
return;
}
// Any other key starts the game from the demo.
if (phase === 'demo') startPlay(null);
});
boardEl.addEventListener('pointerdown', (e) => {
pointerStart = { x: e.clientX, y: e.clientY };
});
boardEl.addEventListener('pointerup', (e) => {
if (!pointerStart) return;
const dx = e.clientX - pointerStart.x;
const dy = e.clientY - pointerStart.y;
pointerStart = null;
if (Math.max(Math.abs(dx), Math.abs(dy)) >= SWIPE_MIN) {
const d = Math.abs(dx) > Math.abs(dy)
? (dx > 0 ? RIGHT : LEFT)
: (dy > 0 ? DOWN : UP);
handleDir(d);
} else {
primaryAction();
}
});
boardEl.addEventListener('pointercancel', () => { pointerStart = null; });
/* ---------- boot ---------- */
layout();
resetGame('demo');
window.addEventListener('resize', layout);
if ('ResizeObserver' in window) new ResizeObserver(layout).observe(mainEl);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (76,016 tokens)
I'm planning a single-file Snake game with everything inline and no localStorage, so the best score will just live in a variable for the session. The snake should play itself in demo mode on load, with a "Click or press a key to start" overlay, and I'm working out how that start trigger should work when the user clicks or presses a key. So demo mode runs the AI on load, and a keypress or click hands over to the real player game. After game over, Space or Enter restarts, and I'm leaning toward restarting straight into play rather than back into demo. Direction input gets a small queue of two or three moves per tick, each checked against the last queued direction so the snake can't double back on itself. I'm working out how the interpolation should handle a growing snake, since the tail doesn't drop on that step and the previous-position snapshot has to account for the length change. Working out the interpolation for segment positions: the new head comes from the old head, and every other segment takes the position of the segment just ahead of it from before the step, so each one's starting point is the previous snapshot shifted by one index. Still working out the rendering: I'm drawing the snake as per-segment circles with a gradient from head to tail, plus eyes on the head. Still working out the rendering: the body gets drawn as tapering segments with a gradient, and then the head gets eyes that face the direction of travel. I'm settling the visuals now: food as a pulsing glowing gem with a burst of fading particles when it's eaten, maybe a small "+10" popup and a bit of screen shake. I'm keeping the grid at 24×24 and sizing the canvas to fit the available space, using devicePixelRatio so it stays crisp. The layout is a header with score and session-best cards, the board centered, and a small controls hint underneath, with a stacked version for mobile. I'm sizing the canvas from the container and window height, with a dpr-scaled backing store so it stays sharp, and I'm settling on a board that fits a 375px phone as well as a desktop. I'm working out the overlay for the demo state, where the AI keeps playing behind a small translucent card in the middle of the board that says to click or press a key to start. I'm settling the demo overlay and the states: paused shows "Paused" with a P-to-resume hint, and game over shows the score and best with a Space or Enter restart prompt. For demo mode, I'll have the AI restart automatically after a short pause when it dies, and keep the start message visible. I'm settling the AI for demo mode: it will path toward food with BFS while treating body cells as obstacles, and when no path exists it will pick the safe move that leaves the largest reachable area. I'm settling on the AI's move choice now: for each safe candidate it will simulate the new head, check the BFS distance to food, and estimate how much open space is reachable from there, so it avoids moves that trap it. I'm settling the AI's candidate check: any move that isn't a reversal and lands on a safe cell gets evaluated, with the tail counted as blocked unless the snake is eating that step. For each candidate I'll project the new head and body, then look at how much open space it leaves and how far the food is by BFS. I'm weighing a score for each candidate move: if food is reachable, I'd combine a base value with the open area after the move and subtract the distance to food, so safe, roomy moves win over just the fastest path. If food isn't reachable, I'd rank moves by how much open area they leave, since survival matters more then. I'm settling the AI's scoring: if a move leaves food reachable, it gets a large base bonus, plus a bit for the open area it leaves and a small penalty for the distance to food. Next I'm setting up the game state with a 24×24 grid and the snake stored as a head-first array of cells. I'm setting up the game loop with a fixed-step accumulator driven by requestAnimationFrame, so the tick rate can speed up as the snake grows and the render can interpolate between steps. I'm handling the game's loop timing: a fixed-step accumulator with the frame delta clamped, so a hidden tab doesn't cause a huge jump, and the leftover time gives the interpolation factor for rendering. For the death moment I'm planning a short screen shake and a red flash on the head, and I'll build the particle system with per-particle velocity, life, and color for the food burst. I'm settling the visual palette now: a dark background, a lime-to-teal gradient for the snake, and a red apple with a leaf and glow for the food. I'm also deciding how the score and session-best readout should update, including whether demo runs should count toward the best. I'm deciding how the best score should work: it should only update from real player games, not from the AI's demo runs. During the demo the HUD will show the current run's score with a small "DEMO" badge on the board, so it's clear it isn't the player's score. I'm sorting out the game-over and pause controls: Space or Enter restarts after a game over, with a click doing the same, and the overlay will say so. P toggles pause, and the paused overlay will tell the player to press P to resume. I'm deciding how keyboard input should start a human game from demo mode: any key or click begins play, and an arrow key pressed there should also set the first direction. I'm checking the initial direction against the snake's starting heading so a reversal is rejected. I'm settling the input handling now: the first keypress or click starts the game from demo, with the keyboard listener on the window so it still works after the canvas is clicked. I'll stop arrow keys and space from scrolling the page, and map WASD and the arrow keys to the four directions. Writing the input handling for arrow keys and space now, so the page doesn't scroll. For the queue, I'm checking each new direction against the last planned one: same direction or a reversal gets dropped, and the queue is capped at three so quick presses still register in order. Each queued direction gets applied one per tick, so quick presses still come out in the right order. I'm also checking that the queue stays capped at three and that the initial heading is set on game start. Next is the movement step: computing the new head position and checking it for collisions. I'm checking the movement rules now: walls end the game, and the body check skips the tail cell when the snake isn't eating, since that cell frees up this tick. If it is eating, the full body counts as blocked. On a successful move I'll add the new head, then either award points and spawn food or drop the tail, after saving a snapshot of the body beforehand. I'm checking how each segment should interpolate between the previous and new positions when the snake grows, and whether the new tail segment would jump visually. Now I'm checking how the interpolation handles the body segments: the old position of each segment moves to the next index in the new body, so each segment should slide from its old cell toward the new one rather than sit still. I'm checking how the segments move: the old index j becomes new index j+1, so each physical segment should travel from its old cell to the next cell along the body, and I need to get the from and to positions right so the slide doesn't come out flat. I'm checking the segment interpolation again, since the snake's body is just a list of cell positions, so the segments themselves don't move, only their indices shift. What I need is the visual slide: each segment should travel from where it was last tick toward its new spot along the path. I'm checking how the segments should slide: the head goes from its old cell to the new one, and the body segments shift their indices, so the drawing has to interpolate each segment between where it was and where it lands. I'm checking the interpolation per index: each body segment should slide from its previous cell to its new one, so lerp prev[i] to cur[i]. When the snake grows, the new tail segment has no previous cell, so it starts from the old tail position. I'm checking the per-index interpolation against a concrete case: when the head moves right, each segment's from and to cells line up one step apart, so a plain lerp from prev[i] to cur[i] slides the whole body forward. My earlier claim that it moved backwards was wrong. So the per-index lerp is settled: each segment slides from prev[i] to cur[i], with the index clamped when the snake grows. The new tail lands on the old tail cell, so it doesn't jump. Checking the rendering side now: each segment's interpolated position is a straight lerp between its previous and new cell, and since neighboring cells are one step apart, nothing cuts across the grid on a turn. I'm also deciding whether the eye direction should follow the updated heading immediately or wait for the interpolation to finish, and the head turning at tick start looks fine to me. I'm going to smooth the head's angle by easing it toward the target heading each frame, so the eyes turn gradually rather than snapping. For the demo mode, the AI should pick its move before each step and enqueue it, but only if it's a valid turn from the current direction. Demo mode will have the AI pick a direction at the start of each tick and queue it, so the step that follows always uses a fresh, valid choice. Now I'm working out the game loop: snapshot the body before moving, pop the next queued direction, and check the new head for collisions, ending the round if it crashes. In demo mode a crash still plays the shake and particle effects, then restarts the demo after a short pause. Food placement also needs handling for the case where the snake fills the board. I'm settling on the demo AI picking its move each tick, and the food respawn picks randomly from the free cells, which is fine at this board size. When the AI dies, I'll reset after a short delay and keep the demo running, and I'm weighing whether a slightly faster demo tick is worth it. I'm settling on demo speed around 90ms per tick, a bit slower than normal play, so the AI's moves are easy to follow. The normal tick speeds up with each food eaten, down to a floor around 65ms. Still writing the game loop... Now I'm working out the particle effects: each particle gets a position, velocity, and fade-out lifetime, and the score popups are floating text that drifts up and fades. On death I'll add a screen shake that decays over about a third of a second, a red flash on the head, and a small burst of body particles, with the board drawn on a canvas and the overlay as a separate DOM element. Still writing the HTML layout... Still working on the layout: the board's wrapper will be sized from the space left between the header and footer, using the smaller of the available width and height. Still writing the layout code... Still writing the canvas drawing code... Still writing the canvas drawing code... Still writing the canvas drawing code... Still writing the canvas drawing code... Still working on the snake's drawing: the eyes sit on either side of the head, and I'm easing its heading toward the travel direction each frame so it turns smoothly. The body colors will blend from lime to teal along its length, with a darker outline underneath. Still writing the canvas drawing code... Still writing the canvas drawing code... Still working on the overlay styling: a translucent dark backdrop with a glass-style card over the board. I'm also sketching the pause and game-over screens, plus a small "DEMO" badge in the corner. Now I'm sketching the game's state machine: a phase that's either the autoplay demo, active play, paused, or game over. Resetting a round sets the snake back to its starting cells and direction, clears the queue and score, and places a fresh food. Now I'm defining the start logic: a keypress or click during the demo resets into a fresh play round, applying the arrow direction if one was pressed. In play, arrows and WASD queue turns, P pauses, and Space is ignored, while paused only P resumes and on game over Space or Enter restarts straight away. I'm settling the input handling details: holding an arrow key repeats the same direction, which the duplicate check already ignores, and restart keys will skip auto-repeat so they don't fire twice. Clicks follow the same rules as keys, so a click during demo starts play and a click while paused resumes. Now I'm settling the death handling. In play, a crash moves the phase to over, updates the best score, and shows the game-over overlay with the shake and particles. In demo, the phase stays on demo and a short countdown of about 1.4 seconds runs in the frame loop before the round resets. Now I'm making the death handling the same visually in both modes: the snake stays on screen with its head tinted red, and the shake runs for about 0.4 seconds. In play, the game-over card just fades in right away instead of waiting, so a key press during the fade still restarts cleanly. Now I'm settling how the best score updates: it rises live as food is eaten, and on death I'll flag a new best if this run beat the session best it started with. For the demo, I'm weighing whether the HUD should show the AI's score ticking up under the DEMO badge, or stay at zero until play begins. I'm still weighing how the demo's score should look on the HUD: showing the AI's score under a DEMO label, while keeping the session best tied only to human games, versus hiding it until play starts. Showing it with a label seems clearer, though a score sitting next to a best of 0 could look like a bug. I'm still weighing whether the demo's score should show on the HUD. Letting it count toward the best would be odd since it isn't the player's score, but showing a score next to a best of 0 could look like a bug. Arcade attract modes often display a high score from the AI run, so that may be the cleanest option. I'm weighing whether the best score should also count the demo run, the way arcade attract modes show a high score. Keeping it to human runs feels more honest, so the demo's score just shows under the DEMO badge while Best only moves when the player plays. Now I'm working out how the AI picks its moves: it considers each direction except straight back, skips any that would leave the board, and scores the remaining options by how close they bring it to the food. Still working on the AI's move check: each candidate step has to avoid the body, but the tail counts as free when the snake isn't eating, since it will move away. I'm settling the AI's scoring: a move that reaches the food scores well above one that doesn't, with the distance to food pulling the score down and the reachable open area as a tiebreaker so it avoids boxing itself in. Now I'm refining the AI's scoring: reachable food gets a large bonus, a shorter path to it scores higher, and open area breaks ties. I'm also weighing whether to check that the tail stays reachable after the move, so the snake doesn't trap itself on a long run. I'm scoring each candidate move in priority order: first whether the tail is still reachable after the move (a sign the snake won't trap itself), then whether the food is reachable and how close it is, with the open area as the final tiebreaker. I'm still working out the AI's scoring: the tail check goes first to avoid trapping itself, then reachable food and its distance, with open area breaking ties. To test whether the tail stays reachable after the move, I'll run the search with the new tail cell treated as passable, since it will move away. Still working on the AI's move check: for each candidate step I'm running three searches over the free cells, one for a path to the food, one for the reachable area, and one to confirm the tail stays reachable, with the tail treated as passable since it moves away. Still writing the AI's move check: for each candidate step I skip anything that runs into the body, letting the tail count as free when the snake isn't eating, and then I build the new body and find which cell will be the new tail. Still writing the AI's move check: each candidate direction gets scored on whether the tail stays reachable, then whether the food is reachable and how close, with open area as the final tiebreaker. Now I'm working out how food spawns, picking a random cell from the free ones so it never lands on the snake. For eating, I'm planning a burst of about 26 particles in warm colors, plus an expanding ring and a "+10" popup at the food's position. Now I'm adding the visual touches for eating: when the snake's head brightens for a moment, and how the canvas should scale for high-DPI screens. Still writing the page styles: a dark background, a panel tint, and accent colors for the snake and text, with the body set to fill the full viewport height. Now I'm laying out the header and stats bar: a flex row with the SNAKE brand on one side and two stat cards on the other, with the card labels small and muted and the numbers set to tabular figures so they don't jitter as the score changes. Still writing the page styles: the main area centers the board, the board wrapper gets a rounded frame with a soft glow, and the canvas fills it. Still writing the page styles: the start card gets a centered, rounded panel with a muted subtitle and key-hint chips, and the DEMO badge sits in the top-left corner of the board. Now I'm adding the badge and the overlay text: a small pill in the top-left corner that reads DEMO · AI during the attract run, and a start card that shows the title and the "Click or press a key to start" prompt. I'm setting up the swipe support now: a pointer-based gesture on the board that turns a drag longer than about 24px into a direction change, with a plain click still starting the game. I also want `touch-action: none` on the board so a swipe doesn't scroll the page. Now I'm wiring up swipe handling: a drag longer than about 24px counts as a direction change, while a short tap still triggers the main action. In demo mode, a swipe should just start the game moving in that direction. Now I'm wiring up the keyboard handler, so a direction key starts a demo run or steers the snake in play, and any other key starts a fresh game. Still writing the keyboard handler: direction keys go to handleDirection, and Space or Enter triggers the primary action unless it's a held repeat, with P toggling pause and any other key starting play from demo. I'm setting the key mapping now: in demo mode any key starts the game, with P starting it too rather than pausing. Once playing, P toggles pause, and Space or Enter only restarts after a game over. I'm leaving arrow keys ignored while paused or over, and skipping key presses that include Ctrl, Meta, or Alt so browser shortcuts still work. Next I'm setting up the overlay states, starting with the demo card's "SNAKE" title and the "Click or press a key to start" prompt, and I'm keeping the DOM updates static so nothing gets injected. Now I'm drafting the start-card and overlay copy, with the "Click or press a key to start" prompt and the status states for paused and game over. Still writing the page markup and styles: a dark gradient background, a light text color, and the body set to a system font stack. Still writing the page styles: the header is a flex row with the brand on the left, and I'm setting up the logo mark next to the SNAKE wordmark. Still writing the page styles: the logo gets a glowing green dot, and the SNAKE wordmark uses a gradient text fill. The stat cards sit on the right with small labels and right-aligned values. Still writing the page styles: the stat labels are small uppercase muted text, and the values use tabular numbers at a larger size, with a short scale pulse on the value when the score changes. Still writing the page styles: the board gets a rounded frame with a soft green and teal glow and touch handling turned off, and the canvas fills it. The overlay is laid over the board, fading in with a dim backdrop, and its card is starting to animate in. Still writing the page styles: the card gets a translucent panel with a faint border and blur, and the title and subtitle get their sizes and muted color. Still writing the page styles: the big score number gets a large bold tabular-figure size, and the keyboard hint chips get a small monospace look. Still writing the page styles: the badge sits in the top-left corner of the board as a small translucent pill with spaced uppercase text. Still writing the page styles: the badge's blinking dot and the footer's key hints, plus a mobile breakpoint that tightens the header, stat cards, and brand text. Still writing the markup: the header is a flex row with the brand on the left and the score and best stat cards on the right, and I'm setting up the main area next. Still writing the markup: the board holds the canvas, a DEMO badge in the corner, and the overlay card that starts visible. Still writing the canvas setup: I'm wiring up the layout function, which measures the main area so the board fits the available space. Still working out the layout math: the board is sized from the available space in the main area, so each cell gets a whole-number pixel size. I'm also settling on the device pixel ratio so the canvas stays sharp on high-density screens. Still working on the layout math: the canvas scales so each grid cell maps to a whole-pixel size, and I'm checking that the board fits a small phone screen and a larger desktop viewport. Still writing the game state: the snake, direction, food, and score variables, plus particle and ring arrays for effects. Still writing the game state: I'm setting the starting snake to four segments near the center, facing right, and working out a tick speed that's slower in demo mode and quickens as the snake eats more food. Still writing the reset logic: the snake, direction, queue, score, and timers all get cleared, and food is spawned before the HUD refreshes. Still writing the frame loop: it measures the time since the last frame, clamps it so a stalled tab doesn't jump ahead, and then runs the update and render steps. For the effects, I'm stepping particles forward with a little drag and gravity and removing them when their life runs out, while rings expand and fade and floating texts drift upward. Still writing the game loop: the effects, head rotation, and demo reset timer are updated each frame, and the movement tick runs on an accumulator that speeds up as the snake eats. Still writing the game loop: I'm working through how the movement tick uses the accumulator, so each tick runs at the current speed and the render step interpolates between the previous and current positions. Still writing the tick logic: each step takes a queued or AI-chosen direction, saves the previous positions for interpolation, and checks whether the next head cell is food or a collision. Still writing the tick logic: the body check runs over the snake without its tail when the snake isn't eating, and over the full body when it is, so a hit against a wall or itself triggers the death handler. Now I'm working through the death handling: when a tick kills the snake, the body stays where it was, so the previous-position snapshot matches the current one and there's no visual movement. The onEat step is next, bumping the score and the eaten count, updating the best score only during real play, then starting a short head pop, a burst of particles at the food, and a floating "+10" text. Still writing the death handler: the snake stops in place, a shake and a burst of particles play at the head, and the demo restarts after a short wait while a real game goes to the game-over state. Still writing the death handler: I'm working out how to detect a new best, comparing the final score against the best from before this run. Next I'm settling the start and pause behavior: starting a run resets the game and records the best score as it stood at that moment, and a pause just toggles the play state while the overlay and badge update. Still writing the game logic: restarting from the game-over screen resets the run and records the current best as the starting point. For queued turns, I'm checking each new direction against the last queued one (or the current direction), dropping repeats and reversals, and capping the queue at three. I'm checking the queue logic against the snake's movement: if two keys are pressed quickly, say Up then Left while moving right, each new direction is compared with the last queued one, and a perpendicular turn is still allowed, so both presses get honored. The queue check holds up: the first queued turn is always checked against the current direction, since the direction only changes when a queued turn is consumed. So I'm now going through the enqueue cases and confirming there's no way a reversal slips in. Still writing the food spawn: I'm collecting every cell the snake doesn't occupy and picking one at random for the next food. If no free cells are left, the food is cleared and the run counts as a win. Now I'm handling the case where the board fills up with no free cell left for food: the run would count as a win, and I need the game not to crash when food is null, so I'm guarding the eat check and the render and AI paths against it. Still writing the AI: I'm building the candidate moves from the four directions and dropping any that would reverse the current direction. Still writing the AI move check: for each candidate direction, I'm simulating the occupancy after the move, with the tail cleared unless the snake is eating. Still writing the AI: for each candidate move, I'm checking that the new head doesn't hit the body, then working out how far the food is from there. Still writing the AI's reachability check: I'm setting up a breadth-first search over the free cells, using a distance array and a queue, so it can measure how far the food is from the new head. Still writing the breadth-first search: I'm setting up a queue and distance array so it can measure the path length from the new head to the food, with one cell allowed through even when it's occupied. Still writing the breadth-first search: I'm checking whether the tail cell can be reached from the new head, since a full BFS will tell me the reachable area and whether a path to the food exists. Still writing the BFS: one run per candidate, with the new tail cell passable, gives the reachable area and the distance to the food, and whether the tail stays reachable. Still writing the AI scoring: each candidate gets a score that rewards keeping the tail reachable, then the shortest path to food, then the size of the reachable area. Still working out the flood-fill check: one breadth-first pass per candidate move, with the new tail cell treated as passable, gives both the reachable area and whether the tail and food are reachable from there. I'm settling on global names for the distance array, queue, and count so the search doesn't allocate on every call. Checking the render timing next: the interpolation factor comes from the accumulator over the tick length, and a dead snake is unchanged, so its previous and current positions match. Still writing the render: I'm working out the interpolation factor from the accumulator over the tick length, and noting that a dead snake's previous and current positions already match, so there's nothing to animate. Still writing the render: I'm clearing the canvas, applying a shake offset that scales with the remaining shake time, and then drawing the background, food, snake, particles, rings, and floating text in turn. Still writing the render: I'm drawing the background as a checkered grid of cells each frame, since about 576 small fills per frame is fine, and I'm considering a radial gradient over the board for a vignette. Still writing the food draw: I'm drawing a pulsing red glow around the apple, then a radial-gradient body offset slightly up and to the left for a highlight. Still writing the apple: I'm adding a small white highlight and a brown stem with a green leaf, then moving on to the snake's drawing. Still writing the snake draw: I'm computing each segment's interpolated position from the previous and current snake positions, then coloring the body along a gradient from the head's green to the tail's teal. A dead-flash effect comes next. Still writing the snake draw: I'm mixing the body colors toward red by a death factor so the snake flashes on death, then starting the outline pass, drawing each segment with a darker stroke that gets thinner toward the tail. Still writing the snake outline: I'm drawing each body segment with an opaque dark stroke, since semi-transparent outlines would double up and look darker where segments join. Still writing the body pass: I'm stroking each segment in the segment's own color, drawing from tail toward head so the head segments sit on top. Still writing the body pass: I'm considering a thin, faint white stroke down the middle of each segment for a shine, though I'm not sure it's worth the extra draw calls yet. Still writing the snake's head: I'm sizing it up with a pop effect that grows and then shrinks after eating, and I'm considering a squarer shape with a radial gradient to give it some depth. Still writing the head: I'm drawing it as an ellipse oriented along the snake's heading, then placing two eyes forward of center, each with a dark pupil. Still writing the snake's head and face: I'm working out a death state where the eyes become X's, and a tongue flick that shows only while alive. After that comes the particle pass, drawn additively and fading out, and then the rings. Still writing the text labels: I'm setting the font in cell units so it scales with the board, centering it, and fading it with alpha. The glows will come from gradients rather than shadow blur. Still writing the particles: I'm adding a shadow glow to the head, then spawning short bursts of small colored particles on each eat, and pulsing the score box by restarting its CSS class. Now I'm setting the badge text for demo mode and deciding how it should show during play and pause, and I'll make the score's value element bump on each change, with the best score updated alongside it. I'm laying out the overlay for each phase now. The demo overlay gets a large "Click or press a key to start" heading with a small hint about steering and pausing, the paused one just says "Paused" with a resume hint, and the game-over card shows the final score, the best score, and a new-best chip. I'm adding an accessibility live-region role to the overlay, and noting that clicks on the board should trigger the start, resume, or restart action in the paused and game-over phases. I'm also weighing whether to auto-pause when the page is hidden, since a play-phase game left running in the background would be unfair, though I'm unsure whether that could interfere with screenshots. Now I'm wiring up the resize handling: a window resize listener calls layout(), and I'm also considering a ResizeObserver on the main element so the board sizes correctly even if the container changes without a window resize. Then the game starts in demo mode with a fresh snake and food, and the loop kicks off. Still writing the input handling: I'm wiring pointer events on the board, treating a drag of at least 24 pixels as a swipe that sets direction, and anything shorter as a tap. In demo mode a swipe starts play, and I'm deciding what a swipe should do on the game-over screen. I'm settling the input rules now: a tap in demo mode starts play, a tap while paused resumes, and a tap on the game-over screen restarts. Swipes in game-over get ignored, and a tap during play does nothing. I'm settling the timing now: the snake starts at a 120ms tick, drops by about 2.2ms per food down to a 62ms floor, and demo mode runs at 100ms so the AI movement is easy to watch. Still writing the frame loop: I'm using the animation-frame timestamp for both the previous and current time, clamping the elapsed step to a sane range, then simulating and drawing each frame. Still writing the game loop: I'm stepping the snake forward on a fixed tick, with the head turning smoothly toward its heading, and skipping the step once the snake is dead. Still writing the game loop: I'm finishing the fixed-tick accumulator, running as many ticks as elapsed time allows (capped at five per frame) and only in play or demo phases, while the death and game-over states return early. Still writing the tick timing: I'm working out how the tick interval depends on the phase, using the demo speed when the AI is driving. Still writing the tick timing: I'm setting the demo interval to a fixed 100ms, and for play the interval shortens as food is eaten, down to a floor. Still writing the overlay: I'm building the start, paused, and game-over markup for each phase, with the start card showing the "Click or press a key to start" prompt. Still writing the overlay: the demo card shows the "Click or press a key to start" prompt with a hint about steering and pausing, and I'm working out the paused and game-over markup next. Still writing the demo overlay: I'm building the badge so it shows the autoplay state in demo mode, with its text set alongside the rest of the HUD. Now I'm thinking through the best-score update and when the game-over state should count as a new best: the best only changes during play, so at death I just compare the score against the run's starting best. Still writing the death handling: I'm setting the snake to dead, zeroing the accumulator, and triggering the shake and flash effects along with a burst and a ring at the head. In demo mode it waits a short moment before resetting, and in play it moves to the game-over phase and checks whether the score beats the run's starting best. Still writing the game logic: I'm handling the win case in onEat, so if no food can be placed the snake is marked dead, and in demo mode it waits a bit before resetting, while in play it moves to the game-over state with the perfect-board title. Still writing the game setup: I'm checking the start-of-round spawn logic so food never lands on the snake's new head, and confirming that onEat runs after the snake has already grown. Still writing the state setup: I'm declaring the game phase, snake and previous-snake arrays, direction queue, food, and the score, best, and eaten counters. Still writing the initial state: I'm setting the alive flag, accumulator, and death timer, then placing the snake in the center and resetting the direction, queue, and score counters. Still writing the layout code: I'm sizing the board from the main element's width and height, keeping the cell size at a minimum of 8px, and then setting the canvas dimensions to match the device pixel ratio. Still writing the layout: I'm checking that the board never goes below 192px on small screens, and that the canvas CSS fills the board element. Still writing the input helpers: I'm defining small utilities for indexing cells, comparing positions, clamping, and random ranges, and then starting the direction queue logic that rejects a turn identical to the last queued direction. Still writing the key handling: I'm mapping arrow keys and WASD to their directions and feeding them into the same queue that blocks reversals. Still writing the input handling: I'm mapping arrow keys and WASD to directions, and for key repeats I'm deciding whether to ignore them outside demo mode. Still writing the key handling: I'm handling the arrow keys and WASD by feeding them into the same queue, and for repeats I'm letting direction keys through while only ignoring them for the space, Enter, and P keys outside demo mode. Still writing the input handling: I'm finishing the direction handler so it rejects reversals and only accepts a move when it's valid for the current heading. Now I'm wiring up the toggle, so pressing P during play flips the phase to paused and back again, and then the overlay and badge get refreshed to match. Still writing the pointer input: I'm tracking where each pointer press starts, and on release a drag of at least 24px becomes a direction while a short tap counts as the primary action. Still writing the tick: I'm building the AI choice into the queue during demo, saving a copy of the snake's previous positions, and then applying the next queued direction to the heading. Still writing the tick: I'm handling the collision checks, where the snake's own body only counts if it isn't about to move away. Still writing the score update: I'm bumping the eaten count and adding 10 points, and in play the best only rises if the new score beats it. Still writing the event handling: I'm working out how the end-of-board win case should set the flags and overlay, and I'm deciding how the HUD should refresh after eating. Still writing the spawn logic: I'm clearing the occupancy grid, marking the snake's cells, and collecting the free ones so the food picks a random open spot. Still writing the AI: I'm looping over the four directions, skipping the reverse of the current heading, and scoring each step by where it lands. Still writing the AI's move scoring: I'm skipping any cell that's occupied or off the board, treating the tail as vacated unless the snake is eating, and tracking what the snake's body will look like after the step. Still writing the AI's scoring: I'm working out how each candidate move gets ranked, with the tail-reachability check and the distance to food feeding into the score. Still writing the AI's scoring: I'm checking whether the tail cell is reachable from each candidate move, and when eating I'm working out how the food's distance gets folded into the score. Still writing the BFS: I'm setting up a queue-based flood fill from the start cell that counts reachable cells and records distances, checking each neighbor before adding it. Still writing the AI's BFS: I'm checking the tail-reachability and food distance for each candidate, and I'm moving on to the reachable-cell count. Still writing the death and effects code: I'm adding particle bursts that spread outward with random angles and speeds. Still writing the particle update: I'm stepping each particle's lifetime down, removing the ones that have expired, moving the rest by their velocity, and applying a drag so they slow over time. Still writing the particle and draw code: I'm removing expired particles and ring effects, fading the text popups, and decaying the pop, shake, and flash timers. Still writing the render pass: I'm clearing the canvas to the background color, applying the shake offset if it's active, and then drawing the board, food, snake, and effects in order before resetting the transform. Still writing the board drawing: I'm looping over the cells to paint a checkerboard in two shades, and I'm weighing whether to pre-render it to an offscreen canvas so it doesn't cost a full redraw every frame. Still writing the render code: I'm pre-rendering the checkerboard to an offscreen canvas so it isn't redrawn cell by cell every frame, and then blitting it at identity transform before the shake offset is applied. Still writing the drawing code: I'm rendering the food, with a pulsing glow that depends on the current time. Still writing the food drawing: I'm adding a pulsing halo around the apple, and now I'm starting the apple's own radial gradient. Still writing the apple: I'm finishing the gradient's inner shading and drawing the body with its highlight, and now I'm starting the stem. Still writing the apple's stem and leaf: I'm drawing a curved stem off the top and a green leaf beside it. Still writing the apple's stem: I'm nudging its end down a little so it sits cleanly on the top of the fruit. Next up is the snake, where I'm starting to compute each segment's interpolated position between the previous and current tick. Still writing the snake's interpolated positions: I'm lerping each segment between its previous and current location by the tick fraction, and a grown segment reuses the last previous cell. Still writing the snake's colors and widths: I'm shifting each segment's color from green toward the tail's darker shade, and when the snake dies I'm blending the colors toward red. Still writing the outline pass: I'm stroking each body segment in dark navy, a bit wider than its fill, so the snake gets a clean edge. Still writing the body pass: I'm drawing each segment from tail toward neck in its shaded color and width, so the body reads as a smooth gradient. Still writing the body pass: I'm stroking each segment from tail toward neck, and the slightly wider caps at the joints give the snake a faintly segmented look that I'm willing to accept. Still writing the snake's highlight: I'm adding a faint light stroke along each segment, and I'm unsure whether the joints will read as scales or just a segmented body. Still writing the snake's head: I'm working out the forward and side vectors from the heading, and I'm setting the head radius so it swells slightly after eating. Next is the tongue, which flicks out briefly on a short cycle while the snake is alive. Still writing the tongue: I'm placing it just past the head's front edge and drawing a small forked tip, flicking it out for a brief moment on a slow cycle. Still writing the head: I'm drawing an elongated dark ellipse oriented along the heading, with a soft green glow around it. Still writing the eyes: I'm placing them just forward of the head center and offsetting them to either side. Still writing the eyes: I'm drawing each one as a white circle with a dark pupil that looks forward, and I'm adding a small highlight on top. Still writing the eyes: I'm finishing the pupil highlights and drawing the X-shaped eyes for when the snake is dead. Still writing the eyes and head: I'm checking the eye offsets so they sit inside the head radius, and now I'm moving on to the particle effects. Still writing the particle effects: I'm drawing the ring pulses with a fading stroke that widens as they age, and then each particle as a shrinking dot that fades out. Still writing the text popups: I'm drawing each floating label in a warm cream color, with its opacity fading as its lifetime runs out. Still writing the particle sizes and the death flash: I'm making the particle radius a small fraction of a cell and working out the flash intensity, which fades from its peak over the first part of its timer. Still writing the HUD update: I'm pushing the score and best values into their elements, and when a point is scored I'm restarting the bump animation so the number pulses. Still writing the final pieces: I'm wrapping up the head and eye drawing, then the effects, text popups, and HUD bump. Next I'm laying out the initial sequence so the board, demo run, and animation loop start together. Still writing the eyes and the overlay: I'm checking the pupil and highlight offsets, then working out how the demo overlay's "click or press a key" prompt should be shown. I'm checking whether the demo AI will actually play well: I'm simulating the first move from the starting position, where the snake heads right and can turn up or down. I'll pick the candidate with a shortest path to the food and use the open area as a tiebreaker, and I'm wondering whether that heuristic might make it wander oddly, but that's acceptable for a demo. I'm checking whether the AI can get stuck in a loop: if food is reachable at some distance, the move toward it should shorten that distance, since freed tail cells only open up paths. I'm adding a safety valve for the demo: if the AI goes more than a few hundred steps without eating, I'll reset the demo run, since a cycle is possible when the tail-clearance check overrides the distance-reducing move. Now I'm checking the tail collision: when the snake doesn't eat, the tail cell is freed, so the check skips the last segment. In the AI, that same freed tail is cleared from the occupancy map, so the two stay consistent. I also want to confirm that the AI never reverses direction, and that it picks its next move from the current heading. Still writing the footer and the focus rules: I'm keeping the controls line short and leaving out any touch hint. Next I'm checking that quick key presses queue up in order, so a fast up-then-left still gets both turns, and that the queue also blocks reversing onto the body. Now I'm checking the wrap-up: the eaten and death handling, the particle burst from the head on death, and the eyes facing the heading, all of which still need to read smoothly. I'm also confirming that the best score updates for the session, and that the initial demo state shows the "Click or press a key to start" prompt. Now I'm laying out the overlay's text styles, sizing the start line and title and setting the text colors. I'm also checking that the header still fits on a 375px screen, which it does with room to spare, and that the body won't scroll sideways. Checking the rest of the page styling: the header still fits a 375px screen, and the board is capped so it never overflows horizontally. Still writing the page styles: I'm setting up the color variables, box sizing, and the full-height body with its layered background gradients. Still writing the page styles: I'm laying out the body as a full-height flex column with layered background gradients, and setting up the header's brand row with its small glowing logo. Still writing the page styles: I'm adding the header's stat cards with a minimum width and some padding. Still writing the stat cards: I'm setting the bump keyframes so the score number scales up briefly and settles back. Still writing the badge and the layout: I'm positioning the status badge in the board's top-left corner, with its padding and gap set so it reads clearly over the play area. Still writing the overlay styles: I'm laying out the start card as a full-size flex layer over the board, centered so the prompt sits on top of the demo run. Still writing the overlay card styles: I'm setting its width, padding, glassy background, and a slight lift-and-scale transition so it settles into place when shown. Still writing the subtitle styles and the color variables for the card text. Still writing the page styles: I'm finishing the subtitle spacing and the start chip, then the keyboard key styling. Still writing the page styles: I'm adding the mobile breakpoint, tightening the header padding, the brand size, and the stat cards so the layout fits a narrow screen. Still writing the page styles: I'm finishing the overlay and stat label sizing for smaller screens, and setting up the demo start message and the page body layout. Still writing the markup: I'm laying out the header, main board with its canvas, badge, and overlay card, plus the footer hint with its key labels. Next I'm setting up the keyboard mapping for the directions, so the arrow keys and WASD both feed into the same turn logic. Still writing the script: I'm setting up the arrow and WASD key mapping that feeds both into the same turn logic, plus the constants for the grid, tick rate, and eat-burst colors, and grabbing the board, canvas, and its drawing context. Still writing the script: I'm declaring the canvas sizing state, the game phase, the snake and its previous positions, and the counters for score, best, and steps since the last meal. Still writing the script: I'm setting up the shared lookups for the grid's occupancy map and the BFS distance and queue buffers, plus small helpers for indexing cells, comparing positions, clamping, interpolation, random ranges, and the full turn. Still writing the layout function: I'm reading the main area's size, picking a cell size that fits the board into the space, and setting the canvas resolution for the device pixel ratio. Now I'm checking the layout math: the board is sized from the main area's space, with the canvas resolution scaled for the device pixel ratio, and the observer should keep it in step without feedback loops. Still writing the game-state reset: I'm setting the phase, clearing the alive and wait timers, placing a four-segment snake at the center heading right, and resetting the queue and score. Still writing the game-state reset: I'm clearing the score, the effects, and the snake's motion state, then placing the first food and refreshing the HUD and overlay. Now I'm finishing the reset: the head angle goes back to zero so the snake faces right, and then I'm writing the overlay update, which picks what to show for the demo, paused, and game-over phases. Still writing the overlay: I'm composing the game-over card with the score, best, and an optional new-best chip, plus the restart hint. I'm writing the overlay's badge logic: the demo phase gets the default badge with "Autoplay demo", the paused phase gets the paused styling, and every other phase hides the badge. Still writing the script: I'm updating the HUD to show the score and best, bumping the score when it changes, and planning to bump the best readout when a meal beats the previous record. Still writing the die function: I'm setting the alive flag off, zeroing the accumulator, and kicking off the shake and flash effects, then spawning a burst of particles at the head's position. Still writing the die function: I'm marking the snake dead, spawning a burst of particles and an expanding ring at the head, and then either starting the demo's respawn wait or moving into the game-over phase with the new-best check. Now I'm finishing the death handler and the simulation step, which updates the effects and smooths the head's turn toward the current direction before checking whether the game is paused or over. Still writing the simulation step: I'm updating the effects, smoothing the head's turn toward the current direction, then running the fixed-rate ticks while the snake is alive and the game is in play or demo. Still writing the game loop: I'm wiring the fixed-rate tick so the demo runs faster while the player's tick speeds up as they eat, with a small cap on catch-up ticks per frame. Still writing the tick: I'm stepping the snake forward, checking for wall and self collisions, and growing or trimming the tail depending on whether it ate. I'm adding a demo-only safeguard: if the AI goes too long without eating, the game resets itself. Still writing the onEat handler: I'm bumping the eaten count, resetting the steps since food, and adding the points to the score. Still writing the onEat handler: I'm recording the new best if the run beat it, spawning a burst and ring at the eaten food, then placing the next food and checking whether the board is full. Still writing the game logic: I'm finishing the onEat fix so a win sets the game-over state with the new-best check, and then starting the effects update that tracks the clock, the pop, shake, and flash timers. Still writing the effects update: I'm decaying the particles, rings, and floating score texts each frame so they fade out and drift upward. Still writing the burst helper: I'm looping over the particle count, giving each one a random direction and speed, and pushing it into the list with a jittered lifetime. Still writing the animation helpers: I'm sizing each particle's speed and lifetime so the burst spreads a short distance and fades out. Still writing the draw routine: I'm clearing the canvas with the background color, setting the scale transform, and starting to apply a screen shake offset. Still writing the draw routine: I'm finishing the shake offset and the board, food, and snake passes, with the snake's position interpolated by how far the accumulator has run toward the next tick. Still writing the win and death handling: I'm setting the previous-frame snapshot to a copy of the current snake in both the death and win paths so the drawing doesn't lag a tick behind. Still writing the board draw: I'm filling the checkerboard cells, then starting the faint grid lines across the columns. Still writing the board draw: I'm adding the faint grid lines across the columns and rows, then outlining the board's border in a translucent green. Still writing the draw routine: I'm sizing each snake segment by interpolating between its previous and current positions, so the body moves smoothly between ticks. Still writing the draw helpers: I'm setting up the colour and width functions for the snake segments, with the colour shifting toward red during the death flash. Still writing the outline and body passes: I'm stroking each segment in a dark outline first, then drawing the coloured body on top, with the colour and width shifting toward the tail. Still writing the sheen pass: I'm drawing a thin highlight along the body's centerline. Still writing the sheen and head: I'm dropping the highlight stroke as it would look beaded, leaning on a thicker dark rim and per-segment colour shifts for shape, and checking that the colour steps along the body stay smooth. Still writing the head: I'm computing the forward and side vectors from the head angle, sizing the head radius with its pop, and drawing the forked tongue while the snake is alive. Still writing the head: I'm finishing the tongue's forked tip, then laying out the head's rounded shape with a green glow behind it. Still writing the head: I'm layering a dark rim under a radial gradient so the head reads as a rounded shape with a bright highlight, and the glow is set on the rim. Still writing the eyes: I'm placing them on either side of the head's centerline, offset forward from the head, with the second one still being set up. Still writing the eyes: I'm drawing the white of each eye and starting the dark pupil offset along the heading. Still writing the eyes: I'm finishing the pupil highlight and the closed-eye crosses for when the snake is dead, then checking that the eyes sit inside the head's outline. Still writing the eyes: I'm checking that both eyes stay inside the head's outline, and the forward offset keeps them well clear of the front edge. Still writing the effects: I'm setting the ring's radius to grow as its life runs out and fading the lines as it shrinks, then picking the font for the floating score text. Still writing the AI: I'm setting up the head, tail, and neck positions and starting to loop over the possible directions, skipping any that would reverse the snake straight back into itself. Still writing the AI: I'm checking each candidate move's neighbour cell, marking the snake's body as occupied, and then flood-filling from the next cell to see how much open space is left. Still writing the AI: I'm looping over the candidate moves, scoring each one by whether the tail stays reachable, how close it leads to the food, and how much open space it leaves, then keeping the best. Checking the flood-fill and BFS reachability for each candidate, I'm confirming the open cell is free or the target cell itself, and that a food-eating move with no food on the board gets treated as non-eating. The score adds a large bonus for keeping the tail reachable, a smaller one for a shorter path to food, and the open area left over. Next I'm checking the setup order: the snake gets set and the previous-frame snapshot is taken before spawnFood runs, and resetGame clears the food state for demo mode too. The overlay should stay up when the AI dies and the game restarts, and layout runs first at script load since the script sits at the end of the body. Next I'm checking how demo mode handles a quick second keypress before the first tick, which gets queued behind the first. Then I'm confirming that a downward press from the start position doesn't collide with the neck. I'm checking the eye and head orientation against canvas coordinates, since y grows downward. A downward heading gives a rotation of π/2, so the head's long axis runs down and the eyes sit forward and to the left as expected. I'm also confirming that the rotation on the ellipse call lines up with the radii. Still writing the head: I'm setting the highlight's centre slightly up and to the left of the head, so the lighting reads as coming from the top-left. The eyes get drawn after the head, and I'm checking the head's width and outline thickness against the body segments. Still writing the head and eyes: I'm checking the body's width taper and the food's radius, and I'm confirming the canvas draw calls are cheap enough to run every frame. I'm checking the best-score logic, which starts at zero, and the keyboard shortcut for 'p' that should start a play from demo mode. After a game over, arrow keys do nothing, and Space or Enter begins a fresh game. Checking the overlay's fade-in: it starts at zero opacity and eases up over about 0.35 seconds, so it should be fully visible well before the 3-second screenshot. I'm also making sure the card's transform transition plays cleanly on the first show, and that a canvas resize doesn't leave stale pixels since every frame gets redrawn anyway. I'm checking the key handler now, specifically that a lookup like `KEY_DIR[e.key]` can't pick up an inherited property, since key names are plain strings and none would match something like `constructor`. Now I'm putting together the final code, and I'm checking the key-lookup table so inherited properties can't match. I'm also confirming the top score shows in demo mode, and the best score appears in the overlay after a game over. Checking the tail-collision case: on eating, the tail stays put, so the collision check covers every segment. I'm also dropping the idea of resetting the queue on death, since it isn't needed. Still writing the code: I'm tuning the speed curve so it levels off at 70 ms once the score gets high, and I'm adding the "+10" score popup and a mobile footer hint. Still writing the code: I'm finishing the final file layout, with the best-score logic, the demo speed, and the death-state sync all wired together. Still writing the code: I'm setting up the key table so each arrow key and WASD maps to a direction, with the lookup kept free of inherited properties. Still writing the code: I'm defining the key-direction table with WASD and arrow keys, and the constants for the board size, tick speeds, and swipe threshold. Still writing the code: I'm setting up the helper functions for indexing, comparing cells, clamping, and copying the snake body, then the state variables for the display scale and the current game phase. Still writing the code: I'm setting up the state variables for the game phase, snake and food, score and best, plus the particle and overlay effects, along with the typed arrays for occupancy and BFS. Still writing the code: I'm finishing the helper functions and the speed settings, using a clamped cell size so the board fits the available space in both portrait and landscape. Checking the pause handling next: pressing P during play should freeze the game and bring up the overlay. Still writing the code: I'm checking the pointer handling and the CSS overlay positioning, and making sure the card's heading margins are reset so the title sits correctly. Still writing the layout code: I'm setting the cell size from the available space in the main area, with a minimum so the board never collapses. Still writing the layout code: I'm sizing the cell from the space in the main area, with a floor so the board never collapses, then setting the board and canvas dimensions to match. Still writing the code: I'm working out the board's timing for the demo, including the check that a sudden second keypress is queued behind the first tick. Still writing the reset logic: I'm placing the snake at the centre with four segments, heading right, clearing the queue and score, and spawning fresh food before refreshing the HUD and overlay. Checking the keydown path next: in demo, any key that isn't handled starts a game, and in game-over only Space or Enter restarts. Still writing the code: I'm finishing the reset and keydown handling, so any key in demo starts a game and only Space or Enter restarts after game over. Still finishing the badge markup: it's an element with a dot and an "Autoplay demo" label, and I'm adding a CSS rule so it fades out once the game leaves demo mode.