Models 44
Train snakeOne-line

Claude Haiku 5.5

by AnthropicReasoningClean
Open in new window

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

How it went

Total time
5 min 13 s
Cost
$0.037
Output tokens
74,775
Spent thinking
61,735 tokens
Speed
240.5 tok/s
Console errors
0
First token after
2.5 s
First visible text after
4 min 38 s
Prompt tokens
548
Code size
22.6 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"high"}
Max output
128,000 tokens
Generated
Nov 10, 2026

What it was asked

The prompt
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
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 · 741 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Choo-Choo Snake</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;height:100%}
  body{
    font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    color:#243447;
    background:linear-gradient(180deg,#7ec8f0 0%,#cdeeff 55%,#eaf7dc 100%);
    height:100vh;overflow:hidden;display:flex;flex-direction:column;
  }
  header{display:flex;align-items:center;justify-content:space-between;gap:10px 16px;padding:10px 16px 4px;flex-wrap:wrap}
  .brand{display:flex;align-items:center;gap:10px}
  .brand h1{margin:0;font-size:22px;letter-spacing:.3px;font-weight:800}
  .brand small{display:block;font-size:12px;opacity:.75;font-weight:500}
  .stats{display:flex;gap:8px;flex-wrap:wrap;align-items:stretch}
  .stat{background:#fff;border-radius:12px;padding:6px 12px;box-shadow:0 2px 0 rgba(0,0,0,.08);font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:#5b6b7b}
  .stat b{display:block;font-size:18px;color:#243447;letter-spacing:0;text-transform:none;line-height:1.1}
  #consist{display:flex;gap:3px;flex-wrap:wrap;max-width:240px;margin-top:4px;min-height:12px;align-items:center}
  #consist i{width:12px;height:12px;border-radius:4px;display:inline-block;border:1px solid rgba(0,0,0,.2)}
  main{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:12px}
  #stage{position:relative;line-height:0}
  canvas{
    display:block;border-radius:16px;cursor:pointer;touch-action:none;
    box-shadow:0 0 0 8px #8d6e4b,0 0 0 10px #5d4037,0 14px 40px rgba(0,0,0,.3);
  }
  #tag{position:absolute;top:10px;left:10px;background:rgba(20,30,45,.8);color:#fff;font-size:12px;font-weight:700;letter-spacing:.8px;padding:5px 10px;border-radius:8px;pointer-events:none;line-height:1.3}
  #banner{
    position:absolute;left:50%;bottom:14px;transform:translateX(-50%);
    background:#ffca28;color:#243447;font-weight:800;padding:9px 18px;border-radius:999px;
    font-size:15px;line-height:1.3;white-space:nowrap;box-shadow:0 4px 0 #c79100;pointer-events:none;
    animation:pulse 1.3s ease-in-out infinite;
  }
  @keyframes pulse{0%,100%{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.05)}}
  #card{
    position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
    background:rgba(255,255,255,.96);padding:18px 22px;border-radius:16px;text-align:center;
    box-shadow:0 10px 30px rgba(0,0,0,.25);max-width:80%;pointer-events:none;line-height:1.35;
  }
  #card h2{margin:0 0 6px;font-size:24px;color:#c62828}
  #card p{margin:0;font-size:14px}
  .hidden{display:none!important}
  footer{font-size:12px;text-align:center;padding:4px 10px 8px;opacity:.8;line-height:1.4}
  @media (max-width:480px){
    .brand h1{font-size:18px}
    .brand small{font-size:11px}
    .stat{padding:4px 8px}
    .stat b{font-size:15px}
    #consistBox{display:none}
    #banner{font-size:13px;padding:7px 12px}
    #tag{font-size:10px;padding:4px 7px}
    footer{font-size:11px}
  }
</style>
</head>
<body>
<header>
  <div class="brand">
    <svg width="46" height="30" viewBox="0 0 46 30" aria-hidden="true">
      <rect x="2" y="10" width="26" height="14" rx="3" fill="#d32f2f"/>
      <rect x="6" y="3" width="10" height="9" rx="2" fill="#263238"/>
      <rect x="28" y="6" width="14" height="18" rx="3" fill="#43a047"/>
      <rect x="31" y="9" width="8" height="5" rx="1" fill="#e3f6ff"/>
      <circle cx="10" cy="26" r="3.5" fill="#263238"/>
      <circle cx="22" cy="26" r="3.5" fill="#263238"/>
      <circle cx="35" cy="26" r="3.5" fill="#263238"/>
      <rect x="26" y="8" width="5" height="3" rx="1" fill="#ffee58"/>
    </svg>
    <div>
      <h1>Choo-Choo Snake</h1>
      <small>Pick up waiting passengers and grow your train</small>
    </div>
  </div>
  <div class="stats">
    <div class="stat">Passengers<b id="sDel">0</b></div>
    <div class="stat">Cars<b id="sCars">0</b></div>
    <div class="stat">Best<b id="sBest">0</b></div>
    <div class="stat" id="consistBox"><span>Consist</span><div id="consist"></div></div>
  </div>
</header>

<main>
  <div id="stage">
    <canvas id="c"></canvas>
    <div id="tag">DEMO · AI CONDUCTOR</div>
    <div id="banner">▶ Click or press a key to start</div>
    <div id="card" class="hidden"><h2 id="cardH">Derailed!</h2><p id="cardP"></p></div>
  </div>
</main>

<footer>Steer with arrow keys, WASD or swipe. Hit the edges or your own train and you derail. The AI conductor drives until you take over.</footer>

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

const COLS = 24, ROWS = 16, N = COLS * ROWS;
const DIRS = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
const KEYDIR = {
  ArrowRight:DIRS[0], ArrowDown:DIRS[1], ArrowLeft:DIRS[2], ArrowUp:DIRS[3],
  d:DIRS[0], s:DIRS[1], a:DIRS[2], w:DIRS[3],
  D:DIRS[0], S:DIRS[1], A:DIRS[2], W:DIRS[3]
};
const WAGON = ['#ffb300','#43a047','#1e88e5','#8e24aa','#00acc1','#f4511e','#fdd835','#d81b60','#7cb342','#5e35b1'];
const SKIN  = ['#f6d2b0','#e8b48a','#c68a5a','#8d5a3b','#ffe0c2'];
const HAIR  = ['#3e2723','#1a1a1a','#8d6e63','#c0841a','#5d4037','#6a1b9a'];
const PLATS = [[2,2,5,1],[17,2,5,1],[2,13,5,1],[17,13,5,1],[10,7,4,2]];

const isPlat = new Uint8Array(N), platIdx = [];
for (const [x,y,w,h] of PLATS)
  for (let j=y;j<y+h;j++) for (let i=x;i<x+w;i++) {
    const k=j*COLS+i; if(!isPlat[k]){ isPlat[k]=1; platIdx.push(k); }
  }
const plat = (x,y) => x>=0 && y>=0 && x<COLS && y<ROWS && isPlat[y*COLS+x]===1;

const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
const bg = document.createElement('canvas');
const bctx = bg.getContext('2d');
const main = document.querySelector('main');
const $ = id => document.getElementById(id);
const el = {
  del:$('sDel'), cars:$('sCars'), best:$('sBest'), consist:$('consist'),
  tag:$('tag'), banner:$('banner'), card:$('card'), cardH:$('cardH'), cardP:$('cardP')
};

let dpr = 1, U = 20, CW = 0, CH = 0;
let mode = 'demo', lastPlay = false;
let train = [], passengers = new Map(), fx = [], smoke = [];
let dir = {x:1,y:0}, queue = [];
let acc = 0, deadT = 0, spawnT = 0, shake = 0, time = 0, DT = 0.016;
let delivered = 0, best = 0;

const idx = (x,y) => y*COLS + x;
const rnd = (a,b) => a + Math.random()*(b-a);
const pick = arr => arr[(Math.random()*arr.length)|0];
const carStyle = () => ({color:pick(WAGON), skin:pick(SKIN), hair:pick(HAIR)});
const tickMs = () => Math.max(85, 135 - train.length*1.1);

function rr(c,x,y,w,h,r){
  c.beginPath();
  c.moveTo(x+r,y);
  c.arcTo(x+w,y,x+w,y+h,r);
  c.arcTo(x+w,y+h,x,y+h,r);
  c.arcTo(x,y+h,x,y,r);
  c.arcTo(x,y,x+w,y,r);
  c.closePath();
}

/* ---------- game setup ---------- */

function spawnPassenger(){
  const occ = new Set(train.map(s => idx(s.x,s.y)));
  const free = platIdx.filter(k => !passengers.has(k) && !occ.has(k));
  if (!free.length) return;
  const k = pick(free);
  passengers.set(k, {
    color:pick(WAGON), skin:pick(SKIN), hair:pick(HAIR),
    ph:Math.random()*6.28, born:time
  });
}

function init(){
  train = [
    {x:7,y:8,px:7,py:8},
    {x:6,y:8,px:6,py:8,...carStyle()},
    {x:5,y:8,px:5,py:8,...carStyle()}
  ];
  dir = {x:1,y:0};
  queue = [];
  acc = 0; deadT = 0; spawnT = 0; delivered = 0;
  passengers.clear(); fx = []; smoke = [];
  for (let i=0;i<5;i++) spawnPassenger();
  updateHUD();
}

/* ---------- AI conductor ---------- */

function floodArea(sx,sy,blocked){
  const seen = new Uint8Array(N);
  const q = [idx(sx,sy)];
  seen[q[0]] = 1;
  let n = 0;
  for (let qi=0; qi<q.length; qi++){
    const c = q[qi]; n++;
    const cx = c%COLS, cy = (c/COLS)|0;
    for (const d of DIRS){
      const x = cx+d.x, y = cy+d.y;
      if (x<0||y<0||x>=COLS||y>=ROWS) continue;
      const k = idx(x,y);
      if (blocked[k] || seen[k]) continue;
      seen[k] = 1; q.push(k);
    }
  }
  return n;
}

function aiChoose(){
  const L = train.length, h = train[0], hk = idx(h.x,h.y);
  const blocked = new Uint8Array(N);
  for (let i=0;i<L-1;i++) blocked[idx(train[i].x,train[i].y)] = 1;
  const ok = (x,y) => x>=0 && y>=0 && x<COLS && y<ROWS && !blocked[idx(x,y)];

  // BFS to the nearest waiting passenger
  const par = new Int32Array(N).fill(-1);
  const seen = new Uint8Array(N);
  const q = [hk];
  seen[hk] = 1;
  let found = -1;
  for (let qi=0; qi<q.length && found<0; qi++){
    const c = q[qi], cx = c%COLS, cy = (c/COLS)|0;
    for (const d of DIRS){
      const x = cx+d.x, y = cy+d.y;
      if (!ok(x,y)) continue;
      const k = idx(x,y);
      if (seen[k]) continue;
      seen[k] = 1; par[k] = c;
      if (passengers.has(k)) { found = k; break; }
      q.push(k);
    }
  }
  let step = -1;
  if (found >= 0){
    let c = found;
    while (par[c] !== hk) c = par[c];
    step = c;
  }

  let bestD = null, bestS = -Infinity;
  for (const d of DIRS){
    const x = h.x+d.x, y = h.y+d.y;
    if (!ok(x,y)) continue;
    const k = idx(x,y);
    const area = floodArea(x,y,blocked);
    let sc = area;
    if (k === step && area >= Math.min(L, 60)) sc += 1e6;
    if (sc > bestS){ bestS = sc; bestD = d; }
  }
  return bestD || dir;
}

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

function stepGame(){
  const d = mode === 'demo' ? aiChoose() : (queue.length ? queue.shift() : dir);
  dir = d;
  const h = train[0];
  const nx = h.x+d.x, ny = h.y+d.y;
  if (nx<0 || ny<0 || nx>=COLS || ny>=ROWS) { crash(); return; }

  const pk = idx(nx,ny);
  const eat = passengers.has(pk);
  const L = train.length;
  const count = eat ? L : L-1;
  for (let i=0;i<count;i++){
    if (train[i].x===nx && train[i].y===ny){ crash(); return; }
  }

  const tail = train[L-1], tx = tail.x, ty = tail.y;
  for (let i=L-1;i>=1;i--){
    const s = train[i], p = train[i-1];
    s.px = s.x; s.py = s.y;
    s.x = p.x;  s.y = p.y;
  }
  h.px = h.x; h.py = h.y;
  h.x = nx;   h.y = ny;

  if (eat){
    const p = passengers.get(pk);
    passengers.delete(pk);
    train.push({x:tx,y:ty,px:tx,py:ty,color:p.color,skin:p.skin,hair:p.hair});
    delivered++;
    burst(nx,ny,p.color,16);
    popText(nx,ny,'+1');
    updateHUD();
  }
}

function crash(){
  const h = train[0];
  mode = 'dead'; deadT = 0; acc = 0; shake = 0.6; queue = [];
  for (const s of train){ s.px = s.x; s.py = s.y; }
  if (lastPlay) best = Math.max(best, delivered);
  burst(h.x,h.y,'#ffca28',24);
  for (let i=0;i<6;i++)
    smoke.push({x:h.x+.5,y:h.y+.5,vx:rnd(-.9,.9),vy:rnd(-1,-.2),life:1.8,max:1.8,r:.2});
  el.banner.textContent = lastPlay ? '▶ Click or press a key to ride again' : '▶ Click or press a key to start';
  el.banner.classList.remove('hidden');
  showCard();
  updateHUD();
}

function burst(x,y,color,n){
  for (let i=0;i<n;i++){
    const a = Math.random()*6.283, sp = rnd(1.5,4.5);
    fx.push({x:x+.5,y:y+.5,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:.7,max:.7,r:rnd(.05,.1),
             c: Math.random()<.3 ? '#ffffff' : color, g:0});
  }
}

function popText(x,y,txt){
  fx.push({x:x+.5,y:y+.5,vx:0,vy:-1.6,life:1,max:1,txt});
}

function queueTurn(d){
  const last = queue.length ? queue[queue.length-1] : dir;
  if (d.x===last.x && d.y===last.y) return;
  if (d.x===-last.x && d.y===-last.y) return;
  if (queue.length < 3) queue.push(d);
}

function startPlay(d){
  if (mode === 'play') return;
  init();
  mode = 'play'; lastPlay = true;
  if (d && !(d.x===-dir.x && d.y===-dir.y)) dir = d;
  el.card.classList.add('hidden');
  el.banner.classList.add('hidden');
  el.tag.textContent = 'ON BOARD · arrows / WASD / swipe';
}

function resetDemo(){
  mode = 'demo'; lastPlay = false;
  init();
  el.card.classList.add('hidden');
  el.banner.textContent = '▶ Click or press a key to start';
  el.banner.classList.remove('hidden');
  el.tag.textContent = 'DEMO · AI CONDUCTOR';
}

function showCard(){
  el.card.classList.remove('hidden');
  el.cardH.textContent = 'Derailed!';
  if (lastPlay){
    el.cardP.textContent = `You delivered ${delivered} passenger${delivered===1?'':'s'}. Press a key or tap to ride again.`;
  } else {
    el.cardP.textContent = 'The AI conductor took a wrong turn. A new train is leaving soon…';
  }
}

function updateHUD(){
  el.del.textContent = delivered;
  el.cars.textContent = train.length-1;
  el.best.textContent = best;
  el.consist.innerHTML = '';
  const cars = train.slice(1);
  for (const c of cars.slice(0,18)){
    const i = document.createElement('i');
    i.style.background = c.color;
    el.consist.appendChild(i);
  }
  if (cars.length > 18){
    const m = document.createElement('span');
    m.textContent = '+' + (cars.length-18);
    m.style.fontSize = '11px';
    el.consist.appendChild(m);
  }
  if (!cars.length) el.consist.textContent = '—';
}

/* ---------- layout & background ---------- */

function layout(){
  dpr = Math.min(2, window.devicePixelRatio || 1);
  const aw = main.clientWidth - 24, ah = main.clientHeight - 24;
  U = Math.max(10, Math.floor(Math.min(aw/COLS, ah/ROWS)));
  CW = U*COLS; CH = U*ROWS;
  cv.style.width = CW+'px';
  cv.style.height = CH+'px';
  cv.width = Math.round(CW*dpr);
  cv.height = Math.round(CH*dpr);
  bg.width = cv.width; bg.height = cv.height;
  prerender();
}

function prerender(){
  const g = bctx;
  g.setTransform(1,0,0,1,0,0);
  g.clearRect(0,0,bg.width,bg.height);
  g.setTransform(dpr,0,0,dpr,0,0);

  const grd = g.createLinearGradient(0,0,0,CH);
  grd.addColorStop(0,'#aed581');
  grd.addColorStop(1,'#8bc34a');
  g.fillStyle = grd;
  g.fillRect(0,0,CW,CH);

  let seed = 11;
  const r = () => { seed = (seed*16807) % 2147483647; return seed/2147483647; };

  g.fillStyle = 'rgba(56,110,30,.18)';
  for (let i=0;i<Math.round(CW*CH/600);i++){
    g.beginPath();
    g.ellipse(r()*CW, r()*CH, U*(0.08+r()*0.1), U*0.05, 0, 0, 6.283);
    g.fill();
  }

  for (let y=0;y<ROWS;y++) for (let x=0;x<COLS;x++){
    if (plat(x,y)) continue;
    if (r() < 0.05){
      const cols = ['#fff176','#ffffff','#f48fb1','#ce93d8'];
      g.fillStyle = cols[Math.floor(r()*cols.length)];
      g.beginPath();
      g.arc((x+.2+r()*.6)*U, (y+.2+r()*.6)*U, U*0.07, 0, 6.283);
      g.fill();
    }
  }

  for (const k of platIdx){
    const x = k%COLS, y = (k/COLS)|0, px = x*U, py = y*U;
    g.fillStyle = '#d7cfbb';
    g.fillRect(px,py,U,U);
    g.strokeStyle = 'rgba(120,105,80,.35)';
    g.lineWidth = 1;
    g.strokeRect(px+.5,py+.5,U-1,U-1);
    g.fillStyle = '#fbc02d';
    const e = Math.max(2, U*0.1);
    if (!plat(x,y-1)) g.fillRect(px,py,U,e);
    if (!plat(x,y+1)) g.fillRect(px,py+U-e,U,e);
    if (!plat(x-1,y)) g.fillRect(px,py,e,U);
    if (!plat(x+1,y)) g.fillRect(px+U-e,py,e,U);
  }
}

/* ---------- drawing helpers ---------- */

function drawHead(x,y,r,skin,hair){
  ctx.fillStyle = skin;
  ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill();
  ctx.fillStyle = hair;
  ctx.beginPath();
  ctx.arc(x,y-r*0.12,r*0.98,Math.PI*1.02,Math.PI*1.98);
  ctx.closePath(); ctx.fill();
}

function drawWagon(x,y,a,seg){
  const L = U*0.86, W = U*0.64;
  ctx.save();
  ctx.translate(x,y); ctx.rotate(a);
  ctx.fillStyle = 'rgba(30,50,20,.22)';
  rr(ctx,-L/2+U*0.05,-W/2+U*0.07,L,W,U*0.12); ctx.fill();
  ctx.fillStyle = '#263238';
  for (const wx of [-L*0.3, L*0.3]){
    ctx.fillRect(wx-U*0.08,-W/2-U*0.04,U*0.16,U*0.08);
    ctx.fillRect(wx-U*0.08, W/2-U*0.04,U*0.16,U*0.08);
  }
  ctx.fillStyle = seg.color;
  rr(ctx,-L/2,-W/2,L,W,U*0.12); ctx.fill();
  ctx.strokeStyle = 'rgba(0,0,0,.28)';
  ctx.lineWidth = Math.max(1, U*0.04);
  ctx.stroke();
  ctx.fillStyle = 'rgba(255,255,255,.35)';
  rr(ctx,-L/2+U*0.08,-W/2+U*0.06,L-U*0.16,U*0.1,U*0.05); ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,.15)';
  rr(ctx,-L/2+U*0.08,W/2-U*0.16,L-U*0.16,U*0.1,U*0.05); ctx.fill();
  ctx.fillStyle = '#e3f6ff';
  rr(ctx,-L*0.2,-W*0.2,L*0.4,W*0.4,U*0.06); ctx.fill();
  ctx.strokeStyle = 'rgba(38,50,56,.45)';
  ctx.lineWidth = 1; ctx.stroke();
  drawHead(0,0,U*0.1,seg.skin,seg.hair);
  ctx.restore();
}

function drawLoco(x,y,a){
  const L = U, W = U*0.78;
  ctx.save();
  ctx.translate(x,y); ctx.rotate(a);

  ctx.fillStyle = 'rgba(30,50,20,.22)';
  rr(ctx,-L/2+U*0.05,-W/2+U*0.07,L,W,U*0.14); ctx.fill();

  ctx.fillStyle = '#212121';
  for (const wx of [-L*0.26, 0, L*0.24]){
    ctx.fillRect(wx-U*0.08,-W/2-U*0.05,U*0.16,U*0.1);
    ctx.fillRect(wx-U*0.08, W/2-U*0.05,U*0.16,U*0.1);
  }

  // cab
  ctx.fillStyle = '#37474f';
  rr(ctx,-L/2,-W/2,L*0.36,W,U*0.1); ctx.fill();
  ctx.fillStyle = '#90caf9';
  rr(ctx,-L/2+U*0.07,-W*0.28,U*0.1,W*0.56,U*0.04); ctx.fill();

  // boiler
  ctx.fillStyle = '#d32f2f';
  rr(ctx,-L*0.2,-W*0.36,L*0.52,W*0.72,U*0.12); ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,.25)';
  ctx.fillRect(-L*0.12,-W*0.3,L*0.3,U*0.05);

  // smokebox
  ctx.fillStyle = '#263238';
  rr(ctx,L*0.26,-W*0.32,L*0.18,W*0.64,U*0.08); ctx.fill();

  // cowcatcher
  ctx.fillStyle = '#ffca28';
  ctx.beginPath();
  ctx.moveTo(L*0.42,-W*0.38);
  ctx.lineTo(L*0.5+U*0.12,0);
  ctx.lineTo(L*0.42, W*0.38);
  ctx.closePath(); ctx.fill();

  // dome & chimney
  ctx.fillStyle = '#ffd54f';
  ctx.beginPath(); ctx.arc(-L*0.02,0,W*0.2,0,6.283); ctx.fill();
  ctx.fillStyle = '#111';
  ctx.beginPath(); ctx.arc(L*0.12,0,W*0.2,0,6.283); ctx.fill();
  ctx.fillStyle = '#616161';
  ctx.beginPath(); ctx.arc(L*0.12,0,W*0.11,0,6.283); ctx.fill();

  // headlight glow + lamp
  const hl = ctx.createRadialGradient(L*0.5,0,0,L*0.5,0,U*0.7);
  hl.addColorStop(0,'rgba(255,241,118,.5)');
  hl.addColorStop(1,'rgba(255,241,118,0)');
  ctx.fillStyle = hl;
  ctx.beginPath(); ctx.arc(L*0.5,0,U*0.7,0,6.283); ctx.fill();
  ctx.fillStyle = '#fff59d';
  ctx.beginPath(); ctx.arc(L*0.46,0,U*0.08,0,6.283); ctx.fill();

  ctx.restore();
}

function drawPassenger(cx,cy,p){
  const k = Math.min(1, (time-p.born)/0.45);
  const e = 1 - Math.pow(1-k,3);
  const bob = Math.sin(time*5+p.ph)*U*0.025;
  const wave = Math.sin(time*9+p.ph);
  ctx.save();
  ctx.translate(cx,cy+bob);
  ctx.scale(e,e);

  ctx.fillStyle = 'rgba(40,60,20,.25)';
  ctx.beginPath(); ctx.ellipse(0,U*0.36,U*0.2,U*0.06,0,0,6.283); ctx.fill();

  ctx.fillStyle = '#8d5a2b';
  rr(ctx,-U*0.38,U*0.08,U*0.17,U*0.18,U*0.03); ctx.fill();
  ctx.fillStyle = '#5d3a1a';
  ctx.fillRect(-U*0.38,U*0.15,U*0.17,U*0.03);

  ctx.fillStyle = '#37474f';
  ctx.fillRect(-U*0.09,U*0.24,U*0.07,U*0.12);
  ctx.fillRect( U*0.02,U*0.24,U*0.07,U*0.12);

  ctx.fillStyle = p.color;
  rr(ctx,-U*0.15,-U*0.03,U*0.3,U*0.3,U*0.1); ctx.fill();

  ctx.strokeStyle = p.skin;
  ctx.lineWidth = U*0.07;
  ctx.lineCap = 'round';
  ctx.beginPath();
  ctx.moveTo(U*0.13,U*0.02);
  ctx.lineTo(U*0.24,-U*0.1+wave*U*0.05);
  ctx.stroke();

  drawHead(0,-U*0.17,U*0.15,p.skin,p.hair);
  ctx.restore();
}

/* ---------- render ---------- */

function render(t){
  ctx.setTransform(1,0,0,1,0,0);
  ctx.drawImage(bg,0,0);
  ctx.setTransform(dpr,0,0,dpr,0,0);
  if (shake > 0){
    ctx.translate(rnd(-1,1)*shake*6, rnd(-1,1)*shake*6);
  }

  for (const [k,p] of passengers){
    drawPassenger((k%COLS+.5)*U, (((k/COLS)|0)+.5)*U, p);
  }

  const L = train.length;
  const rp = train.map(s => ({
    x: s.px + (s.x-s.px)*t,
    y: s.py + (s.y-s.py)*t
  }));
  const ang = rp.map((p,i) => {
    const a = i===0 ? p : rp[i-1];
    const b = i===0 ? rp[1] : p;
    return Math.atan2(a.y-b.y, a.x-b.x);
  });

  // smoke emission from the chimney
  if (mode !== 'dead' && Math.random() < DT*9){
    const a0 = ang[0];
    smoke.push({
      x: rp[0].x+.5+Math.cos(a0)*0.12,
      y: rp[0].y+.5+Math.sin(a0)*0.12,
      vx: -Math.cos(a0)*0.5 + rnd(-.15,.15),
      vy: -Math.sin(a0)*0.5 + rnd(-.15,.15) - 0.1,
      life:1.3, max:1.3, r:0.12
    });
  }

  // couplers
  ctx.strokeStyle = '#37474f';
  ctx.lineCap = 'round';
  ctx.lineWidth = U*0.12;
  ctx.beginPath();
  for (let i=0;i<L-1;i++){
    ctx.moveTo((rp[i].x+.5)*U,(rp[i].y+.5)*U);
    ctx.lineTo((rp[i+1].x+.5)*U,(rp[i+1].y+.5)*U);
  }
  ctx.stroke();

  // wagons (tail first so the locomotive sits on top)
  for (let i=L-1;i>=1;i--){
    drawWagon((rp[i].x+.5)*U,(rp[i].y+.5)*U,ang[i],train[i]);
  }
  drawLoco((rp[0].x+.5)*U,(rp[0].y+.5)*U,ang[0]);

  // smoke
  for (const p of smoke){
    const k = p.life/p.max;
    ctx.fillStyle = `rgba(255,255,255,${(0.6*k).toFixed(3)})`;
    ctx.beginPath();
    ctx.arc(p.x*U,p.y*U,p.r*U*(1+(1-k)*0.8),0,6.283);
    ctx.fill();
  }

  // particles & popups
  for (const p of fx){
    const k = p.life/p.max;
    if (p.txt){
      ctx.globalAlpha = k;
      ctx.font = `800 ${Math.round(U*0.75)}px system-ui,sans-serif`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.lineWidth = Math.max(2,U*0.12);
      ctx.strokeStyle = 'rgba(0,0,0,.5)';
      ctx.strokeText(p.txt,p.x*U,p.y*U);
      ctx.fillStyle = '#ffffff';
      ctx.fillText(p.txt,p.x*U,p.y*U);
      ctx.globalAlpha = 1;
    } else {
      ctx.globalAlpha = Math.max(0,k);
      ctx.fillStyle = p.c;
      ctx.beginPath();
      ctx.arc(p.x*U,p.y*U,p.r*U*(0.5+k*0.5),0,6.283);
      ctx.fill();
      ctx.globalAlpha = 1;
    }
  }
}

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

let last = performance.now();
function frame(now){
  const dt = Math.min(0.05, (now-last)/1000);
  last = now;
  DT = dt;
  time += dt;

  if (mode === 'dead'){
    deadT += dt;
    if (deadT > 2.8) resetDemo();
  } else {
    acc += dt*1000;
    let n = 0;
    while (acc >= tickMs() && n < 4 && mode !== 'dead'){
      acc -= tickMs();
      stepGame();
      n++;
    }
    acc = Math.min(acc, tickMs());
  }

  spawnT += dt;
  if (spawnT > 1.2 && passengers.size < 7){
    spawnT = 0;
    spawnPassenger();
  }

  shake = Math.max(0, shake-dt);
  for (const p of fx){
    p.life -= dt;
    p.x += p.vx*dt; p.y += p.vy*dt;
    if (p.g) p.vy += p.g*dt;
  }
  fx = fx.filter(p => p.life > 0);
  for (const p of smoke){
    p.life -= dt;
    p.x += p.vx*dt; p.y += p.vy*dt;
    p.r += dt*0.3;
  }
  smoke = smoke.filter(p => p.life > 0);

  render(mode === 'dead' ? 1 : acc/tickMs());
  requestAnimationFrame(frame);
}

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

window.addEventListener('keydown', e => {
  const d = KEYDIR[e.key];
  if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
  if (['Shift','Control','Alt','Meta','Tab','CapsLock'].includes(e.key)) return;
  if (mode === 'demo'){
    startPlay(d || null);
  } else if (mode === 'dead'){
    if (deadT > 0.8) startPlay(d || null);
  } else if (mode === 'play' && d){
    queueTurn(d);
  }
});

let p0 = null;
cv.addEventListener('pointerdown', e => {
  p0 = {x:e.clientX, y:e.clientY};
  if (mode === 'demo') startPlay(null);
  else if (mode === 'dead' && deadT > 0.8) startPlay(null);
});
cv.addEventListener('pointerup', e => {
  if (!p0 || mode !== 'play'){ p0 = null; return; }
  const dx = e.clientX-p0.x, dy = e.clientY-p0.y;
  p0 = null;
  if (Math.max(Math.abs(dx),Math.abs(dy)) < 16) return;
  if (Math.abs(dx) > Math.abs(dy)) queueTurn(dx>0 ? DIRS[0] : DIRS[2]);
  else queueTurn(dy>0 ? DIRS[1] : DIRS[3]);
});

/* ---------- boot ---------- */

layout();
if (typeof ResizeObserver !== 'undefined'){
  new ResizeObserver(() => layout()).observe(main);
} else {
  window.addEventListener('resize', layout);
}
resetDemo();
requestAnimationFrame(frame);

})();
</script>
</body>
</html>
Reasoning trace (61,735 tokens)
I'm working out a snake game where the snake is a train: a locomotive head with colorful wagons that pick up passengers at platforms. The constraints are a single inline HTML file, a self-playing demo behind a "Click or press a key to start" prompt visible on first paint, and no localStorage. I'm starting to sketch a canvas grid of roughly 24 by 16 cells with responsive sizing.

I'm sketching the wagons as colored bodies with windows, and the passengers as small waving dots on platforms that board when the locomotive reaches them and add a wagon in their color. Walls and the snake's own body will derail the train, and in demo mode an AI will path with BFS toward the nearest waiting passenger, falling back to any safe move.

I'm laying out the platforms as small blocks of two-by-two or three-by-one cells, with passengers standing on them and a new one appearing at a random empty spot after each pickup, capped at about six waiting. Passengers board when the locomotive drives onto their cell, and I'm tying each passenger's color to the wagon it adds.

I'm planning the on-screen details now: a counter for passengers and a "Next stop" display, grass ground with rails under the train, and a gray platform style with yellow edges, plus waving and bobbing passengers. For motion, I'm looking at a fixed logic tick that speeds up as the train grows, with rendering interpolated between each segment's previous and current positions.

I'm working out how the train's segments move between ticks: saving the positions before each step so I can interpolate the rendering, and handling the new wagon added when the train grows by giving it the old tail position as its starting point.

I'm settling on the interpolation approach: each segment will lerp from where it was in the previous step to where it is now. When the train grows, the new tail segment starts at the old tail position, so the visual stays continuous.

I'm checking the index bookkeeping for the interpolation: after a move, each body segment lands on the cell its predecessor just vacated, so I need to pin down which old position each new segment should start its lerp from.

I'm settling the index math for the interpolation: each body segment takes the spot its predecessor just vacated, so its starting point is its old position. The head starts from its old cell and moves to the new one, and when the train grows, the new tail is the only piece I still need to pin down.

I'm settling the prev-position rule for the lerp: each segment starts from the old position at its index, clamped to the old tail, which covers both the normal move and the growth case. Then I'll render between prev and current over the tick. Collisions are the next thing to pin down: whether the tail cell counts as blocked when the train isn't growing.

I'm settling on the collision rule: when the train isn't growing, the head can move into the cell the tail is vacating, so I check against the body minus its last segment, and when it is growing I check against the full body. For the game-over state, the crash shows a flash and a "Derailed!" overlay that restarts after 2.5 seconds in demo mode. I'm also deciding how the start state works: the game opens in AI demo mode behind the "Click or press a key to start" prompt, and the first key press switches to human control.

I'm weighing how the demo banner should look: a small pill at the bottom of the board that pulses, shown only in demo mode and hidden once the player takes over. After a human game over I'd drop back into demo mode with that prompt again. I also want to stop arrow keys from scrolling the page, and I'll add light swipe support for touch.

I'm sizing the canvas to fit its container while keeping the 24 by 16 grid's aspect ratio, so a 375px phone gets roughly 15px cells. I'll scale for devicePixelRatio so it stays crisp. For layout, I'm picturing a simple column: a header with the title and stats (passengers delivered, best, train length), the centered canvas, and a footer with the controls hint.

I'm sketching the station layout now: four platforms in the corners of the board, at roughly columns 2–6 and 17–21 on rows 2 and 13, plus a smaller one in the middle.

I'm deciding that platforms stay drivable, so the train can stop over them, and I'm planning a static set of platforms where passengers wait. Each platform cell can hold one passenger with its own color, capped at six waiting and refilled every so often, and the AI will head for the nearest one with BFS. I'm also pulling a fixed eight-color palette for the wagons, reusing those colors for passengers so each pickup matches the wagon it adds.

Now I'm settling how the train grows: wagons get added per passenger, so the snake gets long, and I'm checking that the 24×16 board can hold it before the game gets too tight. The AI will eventually run out of room and derail, which is acceptable. I'm also noting that if the AI has no safe move left, that's a game over.

For drawing, I'm thinking about the ground as a light grass color with a bit of texture, and then drawing each body segment as a rounded wagon shape with a roof stripe, windows, and wheels.

I'm now describing how the locomotive will look: a bigger red-and-black head with a cab, a chimney that puffs out smoke, and a cow-catcher facing the direction of travel. I'll draw the head in local coordinates rotated to its heading, and I'm still deciding how to smooth the rotation when the train turns.

I'm weighing whether wagons should be drawn unrotated or turned to face their direction of travel. Drawing each wagon as a plain rounded square would read clearly, but orienting them along the track looks more like a real train, so I'm considering rotating each one toward its movement.

Still working out how the wagons should be angled: for the middle segments on corners, the direction between their neighbors comes out diagonal, so rotating them that way may look off, and I'm leaning toward using a simpler per-segment heading for the tail.

I'm working out how each wagon should be angled, and the simplest axis-aligned choice is to point every wagon toward the head, using the vector from its own position to its predecessor's. That keeps the orientation clean even on corners. For the locomotive, I'm taking the heading from the step between the first two segments, which should follow the interpolated motion smoothly.

Still settling the head's angle: it can come from the last move direction, which snaps on a turn but keeps things simple. For the wagons, I'm rotating each one toward the segment ahead of it, so the corners look smooth even though the rotation snaps by 90 degrees.

Still writing the wagon drawing: a rounded body with a roof stripe, two small windows along the top edge, and a tiny passenger head peeking out of one.

Still working on the wagons: I'm adding a small skin-toned head with a hair arc at the center of each one, then the wheels.

Still writing the wheels and the locomotive body: two dark rounded rects along the long edges, then a red boiler, a dark cab at the back with a window, and a chimney near the front.

Still writing the locomotive details: a cowcatcher at the front, a yellow headlight with a glow, and smoke particles from the chimney that drift backward and fade. I'm also planning a generic particle system for pickup bursts and a floating "+1" pop, then starting on how waiting passengers look on the platforms.

Still writing the platform tiles: concrete gray cells with a yellow stripe along the top edge, plus a sign post marking each station.

I'm now adding a station sign on each platform, and I'm drawing the edge of the board as a wooden frame.

I'm settling the game loop now: a tick interval that shrinks as the train grows, render interpolation between ticks, and a small input queue so quick key presses can't reverse the snake into itself. For the demo AI, I'm starting to list the obstacles, treating the body as blocked except the tail, since that cell frees up on the move.

I'm laying out the AI's obstacle set and its path search: BFS from the head over free cells to find the nearest passenger, and if none is reachable, falling back to whichever safe neighbor opens the largest flood-fill area.

Now I'm setting up the game-over case: when the AI has no safe move, the train derails into a crash state with steam and spark particles and a "DERAILED!" overlay.

I'm settling on the game-over flow: after a crash, the derailment card stays up for about two seconds, then the game resets to demo mode with the start banner. If a human presses a key during the card, I'll start a new game right away, and I'm also planning to keep the best score in memory.

I'm settling the start-up behavior: the demo runs with a "Click or press a key to start" banner, and any key or a click on the canvas begins a human game, with arrows, WASD, space, or enter all counting. The AI turns off once a human is in control, and I'm keeping the controls minimal with no pause key.

I'm settling the HUD now: a top bar with the title, a line for passengers delivered, train length, and best score, plus a row of small colored squares showing the most recent wagon colors.

I'm setting up the HUD row of colored chips for the last ten wagons, which should only take about ten lines. For the layout, I'm using a soft sky gradient behind a flex column, with the canvas area taking the remaining height after the header and footer. I'll size the canvas in JavaScript from the available width and height, keeping whole cells, and watch the wrapper with a ResizeObserver.

Still writing the page styles and the canvas sizing, with the layout flexing so the board fills the space after the header.

Still writing the page styles: a sky gradient background, a flex column layout, a header with the title and a row of white stat chips.

Still writing the page styles: the stat chips, the consist strip, and the main area that holds the board.

Still writing the page styles: the banner, the footer, and the consist chips in the header, which will wrap onto a new line when the screen is narrow.

Still writing the stylesheet and the JavaScript setup: the page, the header stats, and the canvas-sizing code are the parts I'm laying out now. Next I'm defining the board grid and a palette for the wagon colors, with the locomotive's red sitting close to one of them.

Next I'm defining the color palette for the wagons, keeping the locomotive's dark body and red stripe distinct from them. Then I'm setting up the canvas state: the cell size, the device pixel ratio, and the platform cells the passengers wait on.

Still writing the game state: the train array and its previous positions, the direction and input queue, the mode flag, the tick timing, the score and best counters, and the particle lists.

Still working on the board layout, sketching the platform positions: a few short bars at the corners and a wider block near the middle, all kept clear of the starting spot.

I'm fixing the layout of the four corner platforms and the center one, with the center block at columns 10 to 13 and rows 7 to 8. I'm also setting the passenger rules: spawn one every 1200 ms while fewer than six are waiting, on platform cells the train isn't occupying. When the head reaches a passenger's cell, that passenger is removed, its color joins the wagons, and the train grows.

Still working out how each wagon stores its passenger's color and skin tone, with the new wagon joining at the tail when the train grows.

Still working out how the cars store their colors when the train shifts, so each wagon keeps its passenger's color as it moves along the body.

Still working out how the wagons store their passengers' colors as the train moves, so each car keeps its color when a new one is added at the tail.

I'm checking how the grown train keeps its wagon colors: when the head moves forward on a boarding step, the old tail stays where it was, so the new wagon's passenger color lands at the end of the body without any extra bookkeeping.

I'm settling how the wagons persist: when the train grows, the old tail wagon stays in place and a new colored wagon takes the tail position, so each physical wagon keeps its own passenger's color as it moves.

Still working out how the wagons keep their colors as the train moves: the physical wagons shift along the body, so the color array has to shift with them.

Still working out how the body shifts: each segment takes the position of the one before it, so I'm storing the previous coordinates on every segment and moving them in place from tail to head.

Still working out the position updates: each segment takes the previous segment's spot, and a growing train adds a new wagon at the tail.

I'm deciding that the tail should stay put on a growth step, so the new wagon appears in the cell the tail just left, and the old tail shifts forward with the rest of the body.

Still working through how the segments shift on a growth step, checking whether the old tail cell ends up occupied by the new last segment.

I'm checking the index shift on a growth step: the tail segment moves into the cell of the one before it, so the old tail's cell ends up free and the body slides forward one place along the array.

Still working out the growth step: the body shifts forward, and the new wagon goes into the cell the old tail just vacated. I need to save that old tail position before the shift so I can place the new segment there.

Still working through the collision check in the step: I'm checking the new head position against the body and walls, and since the last segment vacates on a normal move, I only need to test the segments before it, unless the train is growing.

Still working on the step function: the human input queue and the AI direction choice come first, then the new head position is computed from the current direction.

Still writing the AI helper: the next step is marking the body cells as blocked and then running a search from the head toward the nearest passenger.

Still writing the AI helper: I'm making the pathfinding search that runs from the head toward the nearest passenger, then tracing back from the target through the parent links to find the first step.

Still working on the AI helper: I'm checking each of the four directions for safety, and where the search's first step is safe with enough room to flood-fill, I'll take it, otherwise I'll pick the direction with the most open space.

Still working out the AI's game-over handling: after the 2.4-second death pause the demo restarts on its own, but a human player's score card has to stay up until a key is pressed.

I'm settling the game-over flow: after a human's death, a keypress past the short pause starts a new run right away, and otherwise the demo restarts on its own after about 2.6 seconds. The banner reads "Click or press a key to start" in demo mode and is hidden during play, while the death card shows the passengers delivered and asks for a key or click to ride again. A click in demo mode starts play, and a click on the death card restarts too, but clicks during play do nothing.

Now I'm settling the demo-mode scoring: the HUD tallies passengers for the current run, and I'm weighing whether the best score should count demo runs or only human play, leaning toward play-only so the number means something.

Now I'm laying out the rendering step: the background gets pre-rendered once to an offscreen canvas, with grass, darker patches, and platform tiles, so each frame only has to draw the passengers, the train, and the particles.

Still working on the background: I'm drawing the grass base with seeded speckles and a faint checker, then the platform tiles with yellow edge strips where they meet open ground.

I'm settling the rim: rather than drawing anything on the border, I'll leave it as a plain wooden frame from CSS. Inside the board, I'm considering faint sleeper lines under the non-platform cells, but I'm leaning toward keeping them very low-alpha so they don't clutter the train.

Still writing passenger drawing code...

Still writing passenger drawing code...

Still writing the draw code for the train: I'm computing interpolated positions for each segment, drawing the wagons from tail to head so the locomotive sits on top, and working out the head's angle from its movement.

I'm checking how the head's angle should be computed during interpolation: right after a step, the head's position and the second segment's previous position still reflect the last move, so the angle comes from the previous direction, and it only switches to the new heading once the move completes.

I'm picking the head's facing from the vector between the head and the segment behind it, since that interpolates smoothly across the tick, and I'll use the forward neighbor's direction for each wagon.

Still working out the direction of the wagons and head, using the vector between each segment and its neighbour. I'm checking that this vector can't collapse to zero, since a reverse move is never allowed.

Still writing the wagon drawing function: I'm setting up the local transform and the wagon's rounded body and shadow, using sizes scaled from the cell.

Still writing the wagon's wheels: two pairs of dark bars along each side, placed at the front and back of the body.

Still writing the wagon's body and window details.

Still writing the locomotive: I'm setting the boiler's length and width from the cell size, then working through its wheels and shadow.

Still writing the locomotive: I'm laying out the cab, cowcatcher, and boiler from the front of the engine back.

Still writing the locomotive: I'm adding the boiler, smokebox, and chimney from the cab forward.

Still writing the locomotive's details: the headlight with its glow, the cowcatcher at the front, and the smoke that will rise from the chimney.

Still writing the particle system: I'm adding smoke puffs from the chimney that drift backward and fade out, and a generic effects list where "+1" score popups float upward.

I'm adding the pickup burst next: fourteen particles with random velocities in the passenger's color, plus a bold "+1" popup with a dark outline. I'm leaving out emoji and sound for now, since audio has autoplay quirks I'd rather avoid.

I'm settling the rest of the UI: a sound would add charm, but it brings autoplay and robustness issues, so I'll leave it out. The crash will be a sparks overlay with the train shaking briefly, and the start banner, the death card, and the HUD values (delivered, length, best, consist chips) will live in DOM elements over the canvas, since text is easier to manage there.

Still writing the HUD updates and the resize logic: I'm refreshing the consist chips after each pickup or reset, capping them at twenty wagons with a "+N" overflow, and sizing the cell from the available main area with a minimum cell size.

Still working on the layout: I'm sizing the cell from the available main area with a floor of 10 pixels, so a narrow phone screen still gets a readable grid. The main area takes the leftover height, and the banner will sit at the bottom of the stage.

Still writing the keyboard handler and layout: I'm mapping arrow keys and WASD to the four directions, and the main area's height and width will set the cell size with a floor of 10 pixels.

Still writing the keyboard handler: I'm ignoring a few non-game keys like Shift and Tab, and any key press in the demo starts a game.

I'm wiring up the input queue: a new turn is compared against the last queued direction, and reversals or repeats get dropped, with the queue capped at a few entries.

Still writing the pointer handling: on pointerdown I'm recording the start point, and if the game is in demo mode that press starts a run.

Still writing the game loop: I'm setting up the frame timing with a capped delta so a slow tab doesn't skip ahead.

Still writing the game loop: I'm stepping the simulation at a fixed tick with an accumulator, with the dead state counting down to a reset, and spawning passengers on a timer while keeping the count under seven.

Still writing the game loop: I'm stepping the simulation at a fixed tick, and when a collision happens the train goes to the dead state and the loop stops stepping. The demo restarts after a short delay, and the tick speed is clamped between a fast and slow limit that shrinks as the train grows.

Still writing the game loop: I'm computing the tick length from the snake's length so it gets a little faster as the train grows, and seeding five passengers at startup with a spawn timer of about 1.2 seconds.

Still writing the game step: I'm picking the direction from the queue or the AI, then checking whether the next cell holds a passenger so the train knows to grow.

Still writing the step: I'm computing the new head position, then checking bounds and whether it runs into the body, with the tail left out unless the train is growing.

Still writing the step: I'm updating each body segment to follow the one ahead of it, working from the tail toward the head, and then moving the head to the new cell.

Still writing the game step: after the move, I'm growing the train if it picked up a passenger, adding a new wagon at the old tail spot and clearing that passenger, then triggering the pickup burst, counting the delivery, and refreshing the HUD.

Still writing the AI: I'm running a breadth-first search from the head to find the nearest passenger, treating body segments as blocked cells.

Still writing the breadth-first search: I'm tracking parent cells so I can trace the first step back from the nearest passenger to the head.

Still writing the breadth-first search's path reconstruction: I'm walking the parent links back from the found passenger until I reach the cell adjacent to the head, which gives the first step.

Still writing the AI: I'm scanning the four directions from the head, skipping a reversal and any move that hits a blocked cell, and scoring each safe move by how much open space lies beyond it.

Still writing the AI's scoring: I'm weighing the breadth-first path to the passenger against the open space each safe move leaves, and I'm planning to favor the path step only when the room beyond it is large enough.

Still writing the AI's flood-fill scoring: I'm checking how much open space lies beyond each candidate move, and I'll fall back to the highest-scoring safe direction if no path step qualifies.

Still writing the AI's flood-fill threshold: I'm weighing the breadth-first path against how much open space lies past it, and I'll take that step only when the reachable area is at least as large as the train.

Still writing the render pass: I'm clearing the canvas, then drawing the cached background image across the full stage.

Still writing the render pass: I'm drawing the background image with an identity transform, then restoring the device-pixel scale. Next comes the passengers on their platforms, then the smoke, the train, and finally the particles and popups.

Writing the rounded-rectangle helper by hand with arcTo, so it works in browsers without ctx.roundRect...

Still writing the constants: I'm defining the grid size, the four movement directions, the key mappings, and the wagon, skin, and hair color palettes.

Still writing the constants: I'm laying out the platform cells in a flat lookup and marking which grid squares are platform tiles.

Still writing the background: I'm prerendering the grass base and a faint checker pattern onto an offscreen canvas, then scattering seeded tufts across it.

Still writing the background: I'm drawing the platform tiles as pale paving squares with a thin outline, and I'm working on the yellow safety edges next.

Still writing the background: I'm drawing the platform tiles as pale paving squares with a thin outline, adding yellow safety edges wherever a tile borders open track.

Still writing the decorative edges: I'm settling on a wooden frame around the canvas through CSS rather than drawing a track ring. Then I'll add the passenger drawing per frame, the canvas corner rounding, and the HUD with passengers, cars, and best.

Still writing the markup: I'm adding a card element for the derailed message, with the delivery count shown on death and a restart note when it's the demo, plus a blinking start banner.

Still writing the markup: I'm adding the overlay text for the start banner and the on-board tag, and laying out the consist strip in the header.

Still writing the page styles: I'm setting up the body gradient, a column layout with the header across the top, and the header's flex spacing for the brand and stats.

Still writing the page styles: I'm finishing the header's heading and stat blocks, then laying out the consist strip with small colored tiles for each wagon.

Still writing the stylesheet: I'm styling the canvas with rounded corners and a wooden frame made of layered shadows, plus the stage wrapper and main layout.

Still writing the stylesheet: I'm sizing the stage banner and tag so they sit over the canvas without blocking clicks.

Still writing the stylesheet: I'm styling the banner's pulse animation and the derailed card's position and text, so they sit centered over the canvas.

Still writing the page styles: I'm finishing the small footer text and the mobile media query, which tightens the header and banner sizing on narrow screens.

Still writing the page markup: I'm finishing the header with the loco icon, brand text, and stat blocks, then wrapping the canvas in the main stage.

Still writing the page markup: I'm adding the canvas tag, the demo label, the start banner, and the derailed card that will sit over the stage.

Still writing the constants: I'm setting up the grid dimensions, the four movement directions, and the key mappings for arrows and WASD.

Still writing the constants: I'm setting up the wagon, skin, and hair color palettes, then marking the platform cells on the grid.

Still writing the canvas setup: I'm grabbing the 2D context, the stage and main elements, and an offscreen canvas for the background, plus a small helper for lookups by element ID.

Still writing the rounded-rectangle helper: I'm drawing the corner arcs by hand so the canvas works in browsers without roundRect.

Still writing the rounded-rectangle helper: I'm drawing the corner arcs by hand, and I'll keep the radius small so the arcs don't break.

Still writing the game setup: I'm placing the starting train with its colored wagons, resetting the direction, queue, and passenger state, and clearing any effects.

Still writing the spawn logic: I'm placing passengers on free platform tiles, skipping any cell already occupied by the train or another passenger, and each one gets a random color palette and a spawn timestamp so it can animate in.

Still writing the game loop: I'm starting the step function that moves the train each tick, with the tick speed easing off as the train grows.

Still writing the game step: I'm picking the next direction (the AI chooses in demo mode, otherwise the queued input), then checking the head's next cell for walls, passengers, and the train's own body.

Still writing the game step: I'm applying the body shift and head move, and when the train eats a passenger I'm adding a new wagon at the old tail with that passenger's colors, then logging the delivery with a burst and a +1 popup.

Still writing the crash handler: I'm setting the game to dead, resetting the death timer, and adding a short screen shake, then saving the best score if this was a played game.

Still writing the crash handler: I'm generating the spark particles and the derailed card, which shows the delivery count and a restart note.

Still writing the startup logic: I'm wiring up the card's text for a human derailment and the demo restart, then the start-play function that switches the mode and sets the direction.

I'm finishing the input handler now, mapping the arrow and WASD keys to directions so that pressing one starts play, while a reversal back into the snake's own neck gets ignored.

Still writing the input handler: I'm wiring the key listener so arrows and WASD pick a direction, and scrolling keys get blocked from moving the page.

Still writing the pointer controls: I'm wiring up the touch handler so a tap or swipe can start play or steer the train.

Still writing the input handler's pointer swipe: I'm working out the swipe direction from the drag distance, and a tiny movement is ignored.

Still writing the layout code: I'm sizing the canvas from the available space, setting its pixel buffer for the device pixel ratio, and pre-rendering the background.

Still working on the layout sizing: I'm reasoning through the main padding and box-shadow frame, and whether a width margin of about 24 pixels keeps the frame visible inside the viewport.

Still working out the layout sizing: I'm settling on the padding and frame margins, and I'm deciding how much vertical room to leave so the shadow frame doesn't overlap the header or footer.

Still working out the layout sizing: I'm setting the canvas to fill the main area minus its padding, and checking the cell size on a desktop viewport and on a small phone screen.

Still writing the frame loop: I'm wiring the resize observer to relayout on resize, then stepping the game at a tick rate that catches up on elapsed time.

Still writing the frame loop: I'm clamping the accumulated time after the step loop, and resetting it when the train crashes.

Still writing the frame loop: I'm adding a smoke puff from the locomotive each frame, with the spawn chance scaled to the elapsed time, and computing its position in the render step.

Still writing the frame loop: I'm storing the frame delta globally, then updating the particle effects and smoke puffs and easing the screen shake down before rendering.

Still writing the render pass: I'm drawing the background, applying the screen shake offset, and then looping over the passengers on their platform tiles.

Still writing the render pass: I'm working out the rendered head and wagon positions by interpolating between each segment's previous and current cell, so the train glides smoothly between ticks.

I'm working out how the head gets drawn after a crash: the snake's head didn't move that tick, so its previous and current positions still differ and it would render one cell back. I'll copy the current position into the previous one for every segment inside the crash handler, so the train stays put, and make sure the same is set up at init.

Still writing the render pass: I'm working out each segment's facing angle from its neighbor toward the front of the train, with the head using the direction toward the second segment, so the sprites turn correctly.

Still working on the smoke puffs: I'm working out where the chimney sits by rotating an offset from the locomotive's head along its facing angle, and I'm deciding whether to store the puff positions in pixels rather than cells.

Still writing the render math: I'm switching the train's positions to cell units, so the smoke and particles keep working when the canvas resizes, and converting to pixels only at draw time.

Still writing the smoke spawn: I'm placing each puff just in front of the chimney, offset along the locomotive's facing angle, and converting from cell units as I go.

Still writing the render code: I'm drawing the train's couplers in dark slate, then the wagons and locomotive, with the head's center placed from its interpolated cell position.

Still writing the render code: I'm drawing the train's couplers as thick slate lines from each segment's center to the next, so they show in the gaps between wagons.

Still writing the wagon drawing: I'm setting up the wagon's size and saving the canvas state before drawing its body.

Still writing the wagon drawing: I'm drawing its shadow and wheels, then starting the colored body.

Still writing the wagon drawing: I'm drawing the wagon's bottom stripe and the rounded body outline.

Still writing the wagon drawing: I'm adding the window and a passenger's head peeking out of it.

Still writing the head drawing: I'm working out how the hair sits on top of the passenger's head, using the upper arc of the circle.

Still writing the loco drawing: I'm setting up its body size and saving the canvas state before placing it at its interpolated position and facing angle.

Still writing the locomotive: I'm drawing its shadow and wheel sets, then moving on to the body.

Still writing the locomotive: I'm drawing the cowcatcher at the front and the red boiler body.

Still writing the locomotive: I'm adding the boiler bands and the cab, and I'm starting the cab windows.

Still writing the locomotive: I'm placing the chimney on the boiler and drawing it as a dark circle.

Still writing the locomotive: I'm laying out the boiler, smokebox, and cowcatcher positions along its length.

Still writing the locomotive: I'm placing the chimney near the front of the boiler as a dark ring, and putting the dome slightly behind center so it doesn't clash with the stack.

Still writing the locomotive: I'm placing the cab at the rear and adding its rear window, then the boiler, smokebox, dome, chimney, and headlamp in order along its length.

Still writing the locomotive: I'm adding a rear window on the cab, then a radial headlight glow, and I'm setting up the smoke puffs to fade and grow over their lifetime.

Still writing the particle code: I'm drawing the floating popups and particles with fade-out alpha and a slight upward drift.

Still writing the passenger drawing: I'm setting up a bobbing offset for the figure.

Still writing the passenger: I'm adding a bobbing offset and scaling the figure in as it boards, then drawing its suitcase and shirt.

Still writing the passenger: I'm placing the suitcase, shirt, legs, head, and waving arm around the figure, with a ground shadow beneath it.

Still writing the HUD code: I'm updating the delivered, wagon, and best-score readouts, then building the consist chips for each wagon's color.

Still writing the HUD: I'm working out how the consist chips show the wagons' colors, with a cap on how many appear and an overflow indicator for longer trains.

Still writing the HUD and mode switching: I'm setting the banner to read "Click or press a key to start" in demo mode, hiding it during play, and switching it to "ride again" text after a crash.

Still writing the game loop: I'm settling on the AI demo's start and what the banner says in each state, switching it to "ride again" after a crash.

Still writing the flood fill: I'm counting reachable cells from a candidate position while skipping blocked ones, so the AI can tell whether a move leaves enough room.

Still writing the pathfinding: I'm running a breadth-first search from the head over open cells, recording each cell's parent so I can trace back the first step toward a target.

Still writing the AI: I'm working out the next-step candidates from the path search, starting with the cell that lets the head reach a passenger.

Still writing the AI: I'm working out how the flood-fill area and the path step combine into a score for each of the four directions, with a large bonus for the step that leads toward a passenger when there's enough open room.

I'm weighing a threshold for the flood-fill check: whether the area left after a move is big enough to stay safe, possibly tied to the snake's length or a fixed cap. A fixed cap of about 30 cells seems too weak on a large board with a long train, so I'm reconsidering the cutoff.

Still checking the flood-fill threshold: I'm weighing whether the reachable area should be compared against the snake's length, capped so a long train doesn't make the check impossible.

Still writing the crash handler: I'm resetting the demo state, clearing the queue, and snapshotting the train's positions before the death animation.

Still writing the crash burst: I'm spawning spark particles from the head in random directions with gold, orange, and grey colors that fade out over about a second.

Still writing the crash handler: I'm adding a puff of smoke and making the particles drift, then setting the HUD, the card, and the banner text.

Still writing the pre-render: I'm setting up the offscreen canvas transform before drawing the static background.

Still writing the pre-render: I'm laying out the background gradient and grass tufts on the offscreen canvas.

Still writing the pre-render: I'm drawing the platform tiles and giving the paving a thin brown outline.

Still writing the pre-render: I'm adding a stroke outline to each platform cell and marking its corners.

Still writing the game loop: I'm stepping the simulation on a fixed tick and computing delta time each frame, with the death timer running while the crash animation plays.

Still writing the game loop: I'm stepping the simulation on a fixed tick, clamping the accumulator so a slow frame can't run away, and starting to set up the periodic passenger spawn timer.

Still writing the game loop: I'm stepping the simulation on a fixed tick and computing delta time each frame, with the death timer running while the crash animation plays.

Still writing the frame loop: I'm stepping the particles and smoke each frame, drifting them by delta time, growing the smoke radius, and dropping anything whose life has run out.

Still writing the passenger draw: I'm easing the figure's scale in as it boards, using a cubic ease-out on the elapsed time since it spawned.

Still writing the passenger: I'm drawing the figure's shadow and suitcase, offset by the bobbing and easing.

Still writing the passenger: I'm drawing the legs, the body, and the start of the waving arm.

Still writing the passenger: I'm adding the arm's stroke, then the head, and closing out the save state.

Still writing the head: I'm filling the circle with the skin color, then starting the hair cap on top.

Still writing the head: I'm drawing the hair as a top half-disc above the face, shifted up a bit so it reads as the top of the head.

Still writing the HUD: I'm filling the consist chips with each wagon's color, capping them at eighteen and adding a count for any extra cars, with a dash when the train has none.

Still writing the init: I'm setting up the starting train layout and the initial snake position.

Still writing the init: I'm placing the three starting cars just behind the head on the same row, each with a random wagon color, skin tone, and hair.

Still writing the locomotive: I'm setting up its dimensions and the rotated frame, with a soft shadow beneath it.

Still writing the locomotive: I'm finishing the cab and the boiler, then adding the cowcatcher at the front.

Still writing the locomotive: I'm filling the cab in dark slate and adding a light blue window on its side, then drawing the red boiler on top.

Still writing the locomotive: I'm adding the cowcatcher triangle at the front and a small dome on top.

Still writing the locomotive: I'm adding the chimney and the headlamp, then the grey smokestack cap on top.

Still writing the headlight: I'm setting up a radial glow at the front of the locomotive.

Still writing the headlight: I'm placing a warm lamp near the front of the locomotive, drawn over the cowcatcher so it reads as the lit lamp.

Still writing the locomotive: I'm adding a soft glow at the front and a small lamp over the cowcatcher.

Still writing the smoke: I'm drawing the chimney puffs from the stack position, drifting them up with a little random sideways wobble.

Still writing the smoke drawing: I'm drawing each puff at its position in cell units, scaled to pixels, so the coordinates stay consistent with the particles and sparks.

Now I'm wrapping up the sparkle particles and the pop-up text, which drift upward by a set amount each second and fade out over time. The smoke puffs get drawn at a size that grows as they age, and I'm checking that the footer copy tells the player to use the arrow keys, WASD or swipe, and that the demo conductor takes over until they step in.

I'm checking the keyboard handling next: the arrow keys should be blocked from scrolling in demo mode too, and I'm confirming the window-level listener still catches input when the canvas isn't focused.

I'm checking the input handling on the first frames: the demo shouldn't need a dead-check there, and resetting the demo after a crash is already covered. I'm also confirming that layout can run before init, since it doesn't touch the train, and that the initial resize just redraws the pre-rendered background.

I'm checking the layout CSS now: the main area's height comes from the flex layout and won't grow with the canvas, and the frame shadow fits inside the padding without needing overflow hidden. I'm also making sure the stage has zero line-height so the canvas doesn't leave a gap underneath.

Now I'm checking the layout sizes at phone width, where the header wraps onto a second row and the stats sit below the brand.

Still writing the logo: I'm adding a small inline SVG icon of the locomotive, with a red boiler, a dark cab, a green tender, wheels, and a yellow lamp.

Now I'm checking the stat styling: the "Passengers" label and its number need the uppercase transform only on the label, so the number keeps its own case. The consist chips will be 12px and wrap onto a second row on desktop, capped at 360px wide. I'm also settling the demo AI's pace at around 135ms per tick, which is fine for a short loop.

Still writing the game loop: I'm setting the timestep to a fixed 16 milliseconds and building the snake logic around it.

Still writing the game constants: I'm setting up the palettes for the wagon colors and skin tones.

Still writing the setup: I'm defining the platform tiles and marking which cells belong to them, then setting up the canvas and its offscreen background.

Still writing the game setup: I'm setting up the screen and game state variables, along with the list of helper functions for the snake logic.

Still writing the helpers: I'm adding a burst function that spawns a ring of colored sparks at a cell, each with a random angle, speed and short lifetime.

Still writing the crash effects: I'm adding the sparks and the rising smoke puffs.

Still writing the crash effects: I'm adding a ring of sparks at the head and then looping over the rest of the train.

Still writing the crash effects: I'm adding the smoke puffs drifting up from the head, then resetting the banner text and showing the card.

Still writing the init: I'm setting up the starting train, with the head and its first wagons placed on the grid.

I'm setting up the starting train with the head and two wagons on the grid, then resetting the direction, queue and timers. The passenger list and effects get cleared, and I'm spawning five passengers before refreshing the HUD.

Still writing the game setup: I'm wiring up the restart path, so the demo resets its state and shows the start banner again, with the tag switching back to the demo conductor.

Still writing the render function: I'm setting up the frame so the background gets drawn first, then the game elements on top.

Still writing the render pass: I'm clearing the frame, drawing the pre-rendered background first, and then applying a small screen shake to the train only.

Still writing the render pass: I'm working out how each segment's angle is computed from the interpolated positions, so the locomotive and wagons line up with their direction of travel.

Still writing the render pass: I'm working out the shake offsets so they apply only to the train, then drawing the couplers and wagons.

I'm setting the shake offset so the whole scene jitters together, passengers included, and I'll apply that translate right after the background is drawn.

Still writing the layout: I'm sizing the canvas from the main area's space, clamping the cell size so it never drops below 10px, then setting the CSS and backing-store dimensions to match the device pixel ratio.

Still writing the flower pattern: I'm scattering a few small colored dots across the background, skipping any cell that belongs to a platform.

Writing the drawWagon function now, with a rounded body at the wagon's size and a rotation applied so it lines up with its direction of travel.

Still writing the wagon body: I'm drawing the shadow, the wheels, and a rounded colored body, with a dark outline to finish it off.

Still writing the wagon highlights: I'm adding a light glint along the top edge, a shadow strip along the bottom, and a pale window in the middle of the body.

Still writing the wagon window: I'm tightening its height so it clears the top highlight strip.

Now I'm finishing the window: I'm sizing it to sit within the body's height with the head centered in it, then keeping it stroked so the outline holds.

Still writing the wagon window: I'm getting the head-and-window sizing right, then finishing the stroke, and the locomotive gets drawn the same way.

Still writing the game logic: I'm checking that the head can move into the tail's cell, since the tail vacates as the train moves, and that a newly eaten passenger gets a wagon at the old tail position.

Now I'm finishing the header markup, putting the "Consist" label in a small span so only it gets the uppercase transform, with the chips in a flex-wrapping container below it.

Still writing the header: I'm sizing the stat chips and the mobile breakpoint, then deciding the footer text.