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
2 min 3 s
Cost
$0.014
Output tokens
28,828
Spent thinking
19,649 tokens
Speed
239.3 tok/s
Console errors
0
First token after
2.1 s
First visible text after
1 min 38 s
Prompt tokens
548
Code size
16.4 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"medium"}
Max output
128,000 tokens
Generated
Nov 11, 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 · 546 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;min-height:100%}
body{
  background:radial-gradient(ellipse at top,#27405a,#0f1b26 70%);
  color:#f5f1e6;
  font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  display:flex;flex-direction:column;align-items:center;
  padding:10px 12px;overflow-x:hidden;
}
header{
  width:100%;max-width:960px;display:flex;flex-wrap:wrap;
  align-items:center;justify-content:space-between;gap:8px 16px;
}
h1{margin:0;font-size:clamp(20px,3vw,28px);letter-spacing:.5px}
.stats{display:flex;gap:8px;flex-wrap:wrap}
.stat{
  background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);
  border-radius:10px;padding:4px 10px;font-size:13px;opacity:.92;
}
.stat b{font-size:16px;margin-left:4px;color:#ffd166}
#stage{
  position:relative;margin-top:10px;border-radius:14px;
  box-shadow:0 10px 40px rgba(0,0,0,.5);overflow:hidden;line-height:0;
}
canvas{display:block;touch-action:none;cursor:pointer;border-radius:14px}
#badge{
  position:absolute;top:10px;left:10px;background:rgba(0,0,0,.45);color:#ffd166;
  font:700 11px/1 system-ui,sans-serif;letter-spacing:2px;padding:6px 9px;border-radius:999px;
}
#msg{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  background:rgba(10,18,28,.85);border:2px solid #ffd166;color:#fff;
  padding:12px 20px;border-radius:999px;font-size:clamp(14px,2.2vw,18px);
  font-weight:600;text-align:center;max-width:90%;line-height:1.3;
  animation:pulse 1.6s ease-in-out infinite;pointer-events:none;
}
#msg.hide{display:none}
@keyframes pulse{
  0%,100%{transform:translate(-50%,-50%) scale(1)}
  50%{transform:translate(-50%,-50%) scale(1.04)}
}
.hint{margin:10px 0 0;font-size:13px;opacity:.7;text-align:center}
</style>
</head>
<body>
<header>
  <h1>🚂 Choo-Choo Snake</h1>
  <div class="stats">
    <span class="stat">Score<b id="score">0</b></span>
    <span class="stat">Passengers<b id="pax">0</b></span>
    <span class="stat">Wagons<b id="wag">0</b></span>
    <span class="stat">Best<b id="best">0</b></span>
  </div>
</header>

<div id="stage">
  <canvas id="c"></canvas>
  <div id="badge">DEMO</div>
  <div id="msg">Click or press a key to start</div>
</div>

<p class="hint">Arrows / WASD or swipe to steer · pick up the waiting passengers and couple them as wagons</p>

<script>
(function(){
const COLS = 24, ROWS = 18;
const PAL = ['#ef476f','#ffd166','#06d6a0','#118ab2','#8338ec','#fb8500','#ff006e','#2ec4b6'];
const $ = id => document.getElementById(id);
const canvas = $('c');
const ctx = canvas.getContext('2d');

let cell = 30, dpr = 1;
let snake = [], colors = [], dir = {x:1,y:0}, queue = [];
let passengers = [];
let mode = 'demo';          // 'demo' | 'play' | 'over'
let alive = false;
let delivered = 0, best = 0;
let particles = [], floaters = [];
let deadTimer = 0, stepAcc = 0, smokeT = 0;
let flash = 0, time = 0, lastT = performance.now();

function resize(){
  dpr = window.devicePixelRatio || 1;
  const avail = Math.min(window.innerWidth - 24, 960);
  const headerH = document.querySelector('header').offsetHeight;
  const maxH = Math.max(200, window.innerHeight - headerH - 80);
  cell = Math.max(8, Math.floor(Math.min(avail / COLS, maxH / ROWS)));
  const W = cell * COLS, H = cell * ROWS;
  canvas.style.width = W + 'px';
  canvas.style.height = H + 'px';
  canvas.width = Math.round(W * dpr);
  canvas.height = Math.round(H * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

function spawnPassenger(){
  for(let i = 0; i < 500; i++){
    const x = 1 + Math.floor(Math.random() * (COLS - 2));
    const y = 1 + Math.floor(Math.random() * (ROWS - 2));
    if(snake.some(s => s.x === x && s.y === y)) continue;
    if(passengers.some(p => p.x === x && p.y === y)) continue;
    passengers.push({x, y, col: PAL[Math.floor(Math.random() * PAL.length)], ph: Math.random() * 6.28});
    return;
  }
}

function resetGame(){
  const sy = Math.floor(ROWS / 2);
  snake = [{x:6,y:sy},{x:5,y:sy},{x:4,y:sy},{x:3,y:sy}];
  colors = [PAL[0], PAL[1], PAL[2]];
  dir = {x:1, y:0};
  queue = [];
  passengers = [];
  for(let i = 0; i < 3; i++) spawnPassenger();
  particles = []; floaters = [];
}

function startPlay(){
  mode = 'play';
  delivered = 0;
  resetGame();
  alive = true;
  stepAcc = 0;
  setMsg();
}

function setMsg(){
  const m = $('msg'), b = $('badge');
  if(mode === 'demo'){
    m.textContent = 'Click or press a key to start';
    m.classList.remove('hide');
    b.style.display = '';
  } else if(mode === 'over'){
    m.textContent = 'Derailed! Score ' + delivered * 10 + ' — click or press a key to ride again';
    m.classList.remove('hide');
    b.style.display = 'none';
  } else {
    m.classList.add('hide');
    b.style.display = 'none';
  }
}

function floodSize(sx, sy, blocked, limit){
  if(blocked.has(sx + ',' + sy)) return 0;
  const seen = new Set([sx + ',' + sy]);
  const st = [[sx, sy]];
  let n = 0;
  const D = [[1,0],[0,1],[-1,0],[0,-1]];
  while(st.length && n < limit){
    const [x, y] = st.pop();
    n++;
    for(const [dx, dy] of D){
      const nx = x + dx, ny = y + dy, k = nx + ',' + ny;
      if(nx >= 0 && ny >= 0 && nx < COLS && ny < ROWS && !blocked.has(k) && !seen.has(k)){
        seen.add(k); st.push([nx, ny]);
      }
    }
  }
  return n;
}

// Simple AI: BFS to nearest passenger avoiding the body; otherwise maximise free space
function aiChoose(){
  const blocked = new Set();
  for(let i = 0; i < snake.length - 1; i++) blocked.add(snake[i].x + ',' + snake[i].y);
  const pset = new Set(passengers.map(p => p.x + ',' + p.y));
  const h = snake[0];
  const DIRS = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
  const ok = (x, y) => x >= 0 && y >= 0 && x < COLS && y < ROWS && !blocked.has(x + ',' + y);
  const valid = DIRS.filter(d => !(d.x === -dir.x && d.y === -dir.y) && ok(h.x + d.x, h.y + d.y));
  if(!valid.length) return dir;

  const seen = new Set();
  const q = [];
  for(const d of valid){
    const x = h.x + d.x, y = h.y + d.y;
    seen.add(x + ',' + y);
    q.push({x, y, first: d});
  }
  let qi = 0;
  while(qi < q.length){
    const n = q[qi++];
    if(pset.has(n.x + ',' + n.y)) return n.first;
    for(const d of DIRS){
      const x = n.x + d.x, y = n.y + d.y, k = x + ',' + y;
      if(!seen.has(k) && ok(x, y)){
        seen.add(k);
        q.push({x, y, first: n.first});
      }
    }
  }
  let best = valid[0], bs = -1;
  for(const d of valid){
    const s = floodSize(h.x + d.x, h.y + d.y, blocked, 300);
    if(s > bs){ bs = s; best = d; }
  }
  return best;
}

function die(){
  alive = false;
  flash = 1;
  const h = snake[0];
  burst((h.x + 0.5) * cell, (h.y + 0.5) * cell);
  if(mode === 'demo'){
    deadTimer = 1.4;
  } else {
    mode = 'over';
    best = Math.max(best, delivered * 10);
    setMsg();
  }
}

function step(){
  if(mode === 'demo') queue = [aiChoose()];
  if(queue.length){
    const d = queue.shift();
    if(!(d.x === -dir.x && d.y === -dir.y)) dir = d;
  }
  const h = snake[0];
  const nx = h.x + dir.x, ny = h.y + dir.y;
  if(nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS){ die(); return; }
  for(let i = 0; i < snake.length - 1; i++){
    if(snake[i].x === nx && snake[i].y === ny){ die(); return; }
  }
  const pi = passengers.findIndex(p => p.x === nx && p.y === ny);
  snake.unshift({x:nx, y:ny});
  if(pi >= 0){
    const p = passengers[pi];
    passengers.splice(pi, 1);
    colors.unshift(p.col);          // new wagon right behind the locomotive
    delivered++;
    floaters.push({x:nx, y:ny, t:0, text:'+10'});
    spawnPassenger();
    while(passengers.length < 3) spawnPassenger();
  } else {
    snake.pop();
  }
}

function steer(dv){
  if(mode !== 'play'){ startPlay(); }
  const last = queue.length ? queue[queue.length - 1] : dir;
  if(dv.x === last.x && dv.y === last.y) return;
  if(dv.x === -last.x && dv.y === -last.y) return;
  queue.push(dv);
  if(queue.length > 2) queue.shift();
}

const MAP = {
  ArrowUp:{x:0,y:-1}, ArrowDown:{x:0,y:1}, ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0},
  w:{x:0,y:-1}, s:{x:0,y:1}, a:{x:-1,y:0}, d:{x:1,y:0}
};
document.addEventListener('keydown', e => {
  const kk = e.key.length === 1 ? e.key.toLowerCase() : e.key;
  const dv = MAP[kk];
  if(dv || e.key === ' ') e.preventDefault();
  if(mode !== 'play' && !dv){ startPlay(); return; }
  if(mode !== 'play'){ startPlay(); }
  if(dv) steer({x:dv.x, y:dv.y});
});

canvas.addEventListener('click', () => { if(mode !== 'play') startPlay(); });

let tStart = null;
canvas.addEventListener('touchstart', e => {
  const t = e.touches[0];
  tStart = {x:t.clientX, y:t.clientY};
  if(mode !== 'play') startPlay();
}, {passive:true});
canvas.addEventListener('touchend', e => {
  if(!tStart) return;
  const t = e.changedTouches[0];
  const dx = t.clientX - tStart.x, dy = t.clientY - tStart.y;
  tStart = null;
  if(Math.max(Math.abs(dx), Math.abs(dy)) < 20) return;
  if(Math.abs(dx) > Math.abs(dy)) steer({x: dx > 0 ? 1 : -1, y: 0});
  else steer({x: 0, y: dy > 0 ? 1 : -1});
}, {passive:true});

// ---- effects ----
function emitSmoke(){
  const h = snake[0];
  const a = Math.atan2(dir.y, dir.x);
  const cx = (h.x + 0.5) * cell + Math.cos(a) * 0.14 * cell;
  const cy = (h.y + 0.5) * cell + Math.sin(a) * 0.14 * cell;
  particles.push({
    x:cx, y:cy,
    vx:-Math.cos(a) * cell * 0.5 + (Math.random() - 0.5) * cell * 0.2,
    vy:-Math.sin(a) * cell * 0.5 - cell * 0.35,
    t:0, life:1 + Math.random() * 0.5
  });
}
function burst(x, y){
  for(let i = 0; i < 14; i++){
    const a = Math.random() * Math.PI * 2, s = cell * (0.5 + Math.random() * 1.5);
    particles.push({x, y, vx:Math.cos(a)*s, vy:Math.sin(a)*s - cell*0.3, t:0, life:0.8 + Math.random()*0.5});
  }
}
function updateFx(dt){
  for(const p of particles){ p.t += dt; p.x += p.vx * dt; p.y += p.vy * dt; }
  particles = particles.filter(p => p.t < p.life);
  for(const f of floaters) f.t += dt / 0.9;
  floaters = floaters.filter(f => f.t < 1);
}

// ---- drawing helpers ----
function rr(x, y, w, h, r){
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}
function withLocal(cx, cy, ang, fn){
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(ang);
  ctx.scale(cell, cell);
  fn();
  ctx.restore();
}

function drawGround(){
  for(let y = 0; y < ROWS; y++){
    for(let x = 0; x < COLS; x++){
      ctx.fillStyle = (x + y) % 2 ? '#6fb564' : '#66ad5b';
      ctx.fillRect(x * cell, y * cell, cell, cell);
    }
  }
  ctx.strokeStyle = '#4f9448';
  ctx.lineWidth = Math.max(1, cell * 0.06);
  ctx.lineCap = 'round';
  for(let y = 0; y < ROWS; y++){
    for(let x = 0; x < COLS; x++){
      if((x * 37 + y * 91) % 11 !== 0) continue;
      const cx = (x + 0.5) * cell, cy = (y + 0.5) * cell;
      for(let k = -1; k <= 1; k++){
        ctx.beginPath();
        ctx.moveTo(cx + k * cell * 0.12, cy + cell * 0.18);
        ctx.lineTo(cx + k * cell * 0.14, cy - cell * 0.02);
        ctx.stroke();
      }
    }
  }
  const W = COLS * cell, H = ROWS * cell;
  const lw = Math.max(2, cell * 0.16);
  ctx.strokeStyle = '#f4f1de';
  ctx.lineWidth = lw;
  ctx.strokeRect(lw / 2, lw / 2, W - lw, H - lw);
}

function drawPax(p){
  const cx = (p.x + 0.5) * cell, cy = (p.y + 0.5) * cell;
  const t = time;
  ctx.strokeStyle = `rgba(255,209,102,${0.35 + 0.25 * Math.sin(t * 3 + p.ph)})`;
  ctx.lineWidth = Math.max(2, cell * 0.06);
  ctx.beginPath();
  ctx.arc(cx, cy, cell * (0.44 + 0.03 * Math.sin(t * 3 + p.ph)), 0, Math.PI * 2);
  ctx.stroke();

  ctx.fillStyle = '#c9c2b0';
  rr(cx - cell * 0.4, cy - cell * 0.4, cell * 0.8, cell * 0.8, cell * 0.1);
  ctx.fill();

  const bob = Math.sin(t * 4 + p.ph) * 0.02;
  const wave = Math.sin(t * 6 + p.ph);
  withLocal(cx, cy, 0, () => {
    ctx.fillStyle = p.col;
    rr(-0.13, -0.02 + bob, 0.26, 0.24, 0.08);
    ctx.fill();
    ctx.fillStyle = '#f1c27d';
    ctx.beginPath();
    ctx.arc(0, -0.12 + bob, 0.11, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#3d2b1f';
    ctx.beginPath();
    ctx.arc(0, -0.13 + bob, 0.115, Math.PI * 1.05, Math.PI * 1.95);
    ctx.fill();
    ctx.strokeStyle = p.col;
    ctx.lineWidth = 0.07;
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(0.12, 0.02 + bob);
    ctx.lineTo(0.26, -0.14 + wave * 0.08 + bob);
    ctx.stroke();
    ctx.fillStyle = '#8d5a2b';
    ctx.fillRect(-0.24, 0.12, 0.14, 0.1);
  });
}

function drawLoco(cx, cy, ang){
  withLocal(cx, cy, ang, () => {
    ctx.fillStyle = '#222';
    for(const x of [-0.32, 0.1]){
      ctx.fillRect(x, -0.38, 0.16, 0.09);
      ctx.fillRect(x, 0.29, 0.16, 0.09);
    }
    ctx.fillStyle = '#d62828';
    rr(-0.4, -0.28, 0.7, 0.56, 0.14); ctx.fill();
    ctx.fillStyle = '#9d0208';
    rr(-0.42, -0.3, 0.26, 0.6, 0.1); ctx.fill();
    ctx.fillStyle = '#a8dadc';
    rr(-0.35, -0.2, 0.12, 0.4, 0.05); ctx.fill();
    ctx.fillStyle = '#ffb703';
    ctx.fillRect(-0.1, -0.2, 0.38, 0.05);
    ctx.fillStyle = '#444';
    ctx.beginPath();
    ctx.moveTo(0.28, -0.28); ctx.lineTo(0.46, 0); ctx.lineTo(0.28, 0.28);
    ctx.closePath(); ctx.fill();
    ctx.fillStyle = '#2b2b2b';
    ctx.beginPath(); ctx.arc(0.14, 0, 0.11, 0, Math.PI * 2); ctx.fill();
    ctx.fillStyle = '#555';
    ctx.beginPath(); ctx.arc(0.12, -0.03, 0.04, 0, Math.PI * 2); ctx.fill();
    ctx.fillStyle = '#fff3b0';
    ctx.beginPath(); ctx.arc(0.44, 0, 0.05, 0, Math.PI * 2); ctx.fill();
  });
}

function drawCar(cx, cy, ang, col, idx){
  withLocal(cx, cy, ang, () => {
    ctx.fillStyle = '#222';
    for(const x of [-0.3, 0.16]){
      ctx.fillRect(x, -0.38, 0.14, 0.09);
      ctx.fillRect(x, 0.29, 0.14, 0.09);
    }
    ctx.fillStyle = col;
    rr(-0.38, -0.28, 0.76, 0.56, 0.1); ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    ctx.fillRect(-0.38, -0.03, 0.76, 0.06);
    const wins = [-0.26, -0.02, 0.22];
    for(let i = 0; i < wins.length; i++){
      ctx.fillStyle = '#cdeffd';
      rr(wins[i], -0.22, 0.18, 0.14, 0.03); ctx.fill();
      rr(wins[i], 0.08, 0.18, 0.14, 0.03); ctx.fill();
    }
    // a passenger riding in one of the windows
    const hx = wins[idx % 3] + 0.09;
    ctx.fillStyle = '#f1c27d';
    ctx.beginPath(); ctx.arc(hx, -0.15, 0.055, 0, Math.PI * 2); ctx.fill();
  });
}

function drawSnake(){
  const n = snake.length;
  const cen = s => [(s.x + 0.5) * cell, (s.y + 0.5) * cell];
  // couplers
  ctx.strokeStyle = '#2d2d2d';
  ctx.lineWidth = cell * 0.1;
  ctx.lineCap = 'round';
  for(let i = 1; i < n; i++){
    const [ax, ay] = cen(snake[i]);
    const [bx, by] = cen(snake[i - 1]);
    ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
  }
  // wagons, tail first
  for(let i = n - 1; i >= 1; i--){
    const [cx, cy] = cen(snake[i]);
    const [px, py] = cen(snake[i - 1]);
    const ang = Math.atan2(py - cy, px - cx);
    drawCar(cx, cy, ang, colors[i - 1], i);
  }
  // locomotive
  const [hx, hy] = cen(snake[0]);
  drawLoco(hx, hy, Math.atan2(dir.y, dir.x));
}

function draw(){
  const W = COLS * cell, H = ROWS * cell;
  ctx.clearRect(0, 0, W, H);
  drawGround();
  for(const p of passengers) drawPax(p);
  drawSnake();

  for(const p of particles){
    const k = p.t / p.life;
    ctx.fillStyle = `rgba(235,235,235,${0.55 * (1 - k)})`;
    ctx.beginPath();
    ctx.arc(p.x, p.y, cell * (0.12 + 0.2 * k), 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.font = `bold ${Math.round(cell * 0.5)}px system-ui, sans-serif`;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  for(const f of floaters){
    ctx.fillStyle = `rgba(255,209,102,${1 - f.t})`;
    ctx.fillText(f.text, (f.x + 0.5) * cell, (f.y + 0.5) * cell - f.t * cell * 1.2);
  }
  if(flash > 0){
    ctx.fillStyle = `rgba(230,57,70,${flash * 0.3})`;
    ctx.fillRect(0, 0, W, H);
  }
}

function hud(){
  $('score').textContent = delivered * 10;
  $('pax').textContent = delivered;
  $('wag').textContent = colors.length;
  $('best').textContent = best;
}

function frame(now){
  const dt = Math.min(0.05, (now - lastT) / 1000);
  lastT = now;
  time += dt;
  flash = Math.max(0, flash - dt * 2);

  if(alive){
    stepAcc += dt;
    const T = mode === 'demo' ? 0.11 : 0.13;
    while(alive && stepAcc >= T){ stepAcc -= T; step(); }
    smokeT += dt;
    if(smokeT > 0.12){ smokeT = 0; emitSmoke(); }
  } else if(mode === 'demo'){
    deadTimer -= dt;
    if(deadTimer <= 0){
      resetGame();
      alive = true;
      stepAcc = 0;
    }
  }
  updateFx(dt);
  draw();
  hud();
  requestAnimationFrame(frame);
}

window.addEventListener('resize', resize);
resize();
resetGame();
alive = true;     // demo mode: AI drives the train behind the start message
setMsg();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (19,649 tokens)
I need to put together a single HTML file for a snake-style game where the snake is a train, with a colorful locomotive and wagons that grow as it picks up waiting passengers on platforms. It should open straight into a self-playing demo behind a "click or press a key to start" prompt, and I want it compact but polished on a grid-based canvas.

I'm sketching out the visuals next: a grid ground with a locomotive head that has a chimney and cab windows, followed by colorful wagons that link together. For the AI, I'm planning a breadth-first search toward the nearest waiting passenger while avoiding the body, with walls that end the game to keep some tension, and the demo restarts automatically after a short pause.

Now I'm laying out the canvas: a 24x18 grid that scales to fit its container, with devicePixelRatio handled for sharp rendering. Drawing a grassy board with passengers on small grey platform pads, then building the train from segments where the locomotive is the head and the wagons trail behind with rounded bodies and connectors.

Now I'm working out how the wagons move and grow: the train steps about nine times a second, and the passengers wave on their own timer. When the locomotive reaches a waiting passenger, the score goes up, a new wagon takes that passenger's shirt color, and a "+1" floats up. I'm keeping three passengers on the board at all times, replacing each one as it's picked up.

Now I'm working out how eating works: when the train grabs a passenger, the head moves forward without dropping the tail, so the train grows by one. Each wagon keeps the color of the passenger it picked up. For the AI, I'm running a breadth-first search from the locomotive to the nearest passenger, treating every body segment except the tail as blocked, and taking the first step of that path. If that fails, it will pick the safe move that leaves the most open space, and it won't reverse direction. When the demo ends, it resets after about 1.5 seconds with a fresh train, and the score stays on screen. Player input will come from arrow keys and WASD, queued as the next direction.

I'm settling on how the game should be played now: the player takes control, and any key or click in demo mode hands over to a fresh player game. After a game over, the overlay will wait for a key press to begin a new one. I'm also putting a small HUD above the canvas to show score, passengers delivered, and the session's best.

Now I'm sizing the canvas to the window: a fixed 24x18 grid, with the width capped at 960px and the height limited so the header still fits on screen. The cell size comes from whichever dimension is tighter, and the board gets centered.

Still writing the game state setup...

Next I'm settling the step logic: the snake moves one cell per tick, and the new head is checked against walls and against every body segment except the last one, since the tail vacates its cell as the train advances.

Still working through the step logic: the new head gets checked against the walls and against every body segment except the tail, since the tail vacates its cell. If the train lands on a passenger, that passenger is removed and replaced, and the old head becomes the first wagon, taking the passenger's color.

I'm working out the wagon colors now: each segment keeps its own color as the body moves, and when the train eats, the old head should become a wagon carrying the passenger's color rather than the locomotive changing into one. So the locomotive stays a fixed head object while I add a new one in front each step.

I'm settling on a simpler data model: the snake stays as a list of positions, and the wagon colors live in a separate array one shorter than the body. When the train eats, a new color goes at the front of that array, right behind the locomotive; otherwise the positions just shift forward.

I'm settling the wagon colors by index: each color in the array lines up with a body segment behind the locomotive. When a passenger is picked up, its color goes at the front of that array, so the new wagon sits right behind the engine and the existing wagons keep their order along the train.

Still writing the drawing code for the train segments...

Still writing the train drawing: the locomotive gets rotated toward its heading with a chimney, cab, and cow catcher, and the cars get wheels, windows, and a passenger silhouette.

Still working on the passenger drawing and the ground, plus smoke puffs and "+1" floaters that update each frame. Next up is the AI: a breadth-first search from the locomotive that treats the body as blocked.

Still writing the AI: a breadth-first search from the locomotive that records the first step toward the nearest passenger, with a fallback that picks the safest direction with the most open space.

Now I'm settling how death and restarts work: a demo death triggers a reset after about 1.5 seconds, while a player death sends the game to an over state with a "Derailed!" overlay. Any key or click then starts a fresh play game, and a key pressed during the demo hands control to the player.

Still writing the HTML scaffold: the header with the title and the passenger, cars, and best stats.

Still writing the page styling: a dark background, the grass-green board, and a pulsing message pill at the bottom of the stage.

Still writing the canvas setup: the board gets sized to the window, with the cell size taken from whichever dimension is tighter once the header and padding are accounted for.

Still writing the canvas sizing: the stage is capped at 960px wide, and the cell size is floored to keep the grid whole. Next I'm defining the game state variables, including the snake positions, wagon colors, direction, passenger list, mode, and score.

Still writing the game reset: the score and delivered counts go back to zero, the snake starts as a short train in the middle row heading right, with three passengers placed on the platforms.

Still writing the game step: I'm picking the next direction from the queue and ignoring any move that would reverse straight back into the body.

Still writing the step logic: the head moves forward, and a move into a wall or the body ends the run. If the new head lands on a passenger, that passenger is removed from the platform and the train grows.

Still writing the step logic: when the train catches a passenger, that passenger comes off the platform, a new wagon color goes at the front of the list, and a new passenger spawns in its place.

Still working through the death handling: in demo mode the game waits a short moment before resetting, while a player's crash moves it to the over state, and I'm tracking that with an alive flag so the step stops running after a crash.

Still writing the game loop: the frame function handles the demo's death timer and resets the board when it runs out, while the step timer advances the snake only while it's alive.

Still writing the train's drawing code...

Still writing the locomotive's body shapes: a red boiler with a darker cab at the back holding a dark glass window, plus a round smokestack at the front.

Still writing the train's drawing details: the locomotive gets a front cowcatcher, a yellow headlight, and a dark chimney, while each wagon is a colored rounded rectangle with its own wheels.

Still writing the car drawing: each wagon gets rotated to face the segment ahead of it, so it sits along the track.

Still writing the train drawing: connectors go down first, then the wagons from tail to head with their windows and wheels, and the locomotive drawn last on top.

Still writing the passenger drawing: a gray platform pad, then a bobbing head, a shirt-colored body, and a waving arm, with a suitcase at one side.

Still working on the ground and walls: a checkered grass base with small tufts and a light border around the board. Next I'm adding a red death flash that fades out, and floating score text.

Still writing the AI: a breadth-first search that scores each starting direction, skipping the body cells and the reverse move.

Still writing the AI's pathfinding: a breadth-first search that checks each popped cell for a passenger and returns the first step if it finds one, with a fallback that floods each safe direction and picks the one with the most open space.

Now I'm wiring up the input: arrow keys and WASD steer the train, and any key or click starts a play round from the demo or the derailed screen. Touch swipes will steer too.

Still writing the touch and input handling: swipes past a small threshold steer by their dominant axis, and a touch in demo mode starts a round. The HUD text gets refreshed as things change, and the best score only counts in play mode.

Still writing the draw function: I'm clearing the canvas and checking the ground pattern, with alternating grass shades on each cell.

Still writing the draw function: I'm filling the ground with alternating grass shades, then adding tufts, a light border, passengers, the train, particles, and floating text.

Still writing the locomotive's wheels: dark rectangles set above and below the body, drawn in the same local units as the rest of the train.

Still writing the locomotive's cab and window: the body is a red rounded shape with a darker cab at the back holding a window.

Still writing the locomotive's cab, window, cowcatcher, gold stripe, smokestack, and headlight.

Still writing the locomotive's smaller details: a yellow light at the cowcatcher tip, and the smokestack's position is what I'll use to place the smoke.

Still writing the car drawing: wheels go under each wagon, then the colored body, a white stripe, and two rows of windows.

Still writing the car's end couplers and the window heads, a small head in each window top.

Still writing the snake's connectors and car orientation, aligning each wagon to face the one ahead of it.

Still writing the train's render order: connectors first, then the cars from tail to head, with the locomotive drawn last on top.

Still working on the particles and floating text: the smoke drifts backward and rises, fading as it grows, the score popups float upward and fade over about a second, and a death burst scatters a dozen particles around the head.

Still working on the page layout and overlay placement: the message sits centered over the board, the DEMO badge goes top-left, and the body is a centered column with the header and stats above the canvas.

Next I'm settling the message overlay: it shows the "Click or press a key to start" prompt in demo mode, a derailed message with the score when the run ends, and hides itself during play.

Still writing the game logic: when the snake dies, a flash and a burst of particles play, and in demo mode a short timer resets the board and keeps the demo running. In play mode the state switches to the derailed screen and the best score is updated. Starting a round resets the board and steering keys are live.

Still writing the keyboard handler: I'm mapping the arrow keys and WASD, both cases, to direction vectors.

Still writing the keyboard handler: the arrow keys and WASD map to direction vectors, and the first press outside play mode starts a round. In play, each key queues a turn, ignoring reverses, and only the last couple of inputs are kept.

Next I'm wiring up touch and click: a swipe past a small threshold steers the train by its dominant axis, and a tap or click outside of play starts a round. I'm also adding a resize listener that redraws the board.

Still writing the AI's pathfinding: I'm building a blocked set from the body cells and a set of passenger positions, then filtering the four directions down to the valid moves that aren't a reverse.

Still writing the AI's pathfinding: after the breadth-first search runs out of reachable passengers, I'm scoring each valid direction by how much open space a flood fill finds from there.

Still writing the flood fill and dead-end handling for the AI, keeping the passenger spawn and demo behavior simple.

Still writing the rendering: the passenger shirts and wagons take their colors from the palette, and the "+10" floaters pop up in gold as passengers are picked up.

Still writing the page's stylesheet: the body is a centered column with a dark radial background, and the header holds the title and the stat boxes.

Still writing the stylesheet: the stage wraps the canvas with rounded corners and a soft shadow, and the DEMO badge and the centered message pill are positioned over it.

Still writing the stylesheet: the message pill ignores pointer input, pulses gently in the center of the board, and gets hidden while play is running. Next I'm finishing the page styles and then the script itself.

Still writing the draw helpers: a rounded-rectangle path, a local-coordinate wrapper that scales and rotates around each piece, and the ground loop that starts laying down the alternating grass cells.

Still writing the ground: alternating grass cells, a few tufts, and a light border around the board.

Still drawing the passenger: a pulsing gold ring around a pad, with the shirt and head placed in local units.

Still writing the waving passenger: a short arm stroke in the shirt color, lifted and swaying with a sine wave.

Still writing the locomotive's suitcase and passenger details, then working out the frame loop's timing.

Still writing the frame loop: the time step is capped, the snake advances on a fixed interval in demo or play, and the death timer restarts the demo.

Still writing the frame's helpers: the smoke spawns just ahead of the locomotive's nose, drifting backward and rising as it fades.

Still writing the particle and floater updates, then the death burst, then the HUD values.

I'm settling how the message overlay behaves: in demo mode it shows the start prompt and the DEMO badge, when the run ends it shows the derailed message with the score, and during play it hides both. I'm also deciding that the score comes straight from the delivered passengers times ten, so it doesn't need its own variable.

Still writing the passenger spawner: it picks a random inner cell that the snake and other passengers aren't on, and gives it a palette color.

Still writing the flood fill: it walks outward from the candidate cell through open neighbors, stopping early once a set limit is reached.