Models 44
Train snakeOne-line

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
6 min 16 s
Cost
$0.018
Output tokens
29,817
Spent thinking
17,905 tokens
Speed
79.4 tok/s
Console errors
0
First token after
542 ms
First visible text after
4 min 52 s
Prompt tokens
532
Code size
33.8 KB
Finish reason
stop
Served by
Together
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"high"}
Max output
64,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 · 894 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Platform Picker — Railway Snake</title>
<style>
  :root{
    --bg:#070b12;
    --panel:#0e1622;
    --line:rgba(255,255,255,.09);
    --amber:#ffb545;
    --teal:#4be3c1;
    --text:#e9eff8;
    --muted:#8ea0b6;
  }
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;background:var(--bg);color:var(--text);}
  body{
    min-height:100vh;
    background:
      radial-gradient(1100px 620px at 18% -12%, #1a2740 0%, rgba(10,15,24,0) 62%),
      radial-gradient(900px 520px at 95% 110%, #102b2a 0%, rgba(7,11,18,0) 60%),
      var(--bg);
    font-family:"Trebuchet MS","Segoe UI",system-ui,-apple-system,sans-serif;
    -webkit-font-smoothing:antialiased;
    display:flex;justify-content:center;
  }
  .app{width:min(1120px,100%);padding:16px 18px 22px;}
  .topbar{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:12px;}
  .brand{display:flex;align-items:center;gap:12px;}
  .logo{
    width:48px;height:48px;border-radius:15px;display:grid;place-items:center;flex:none;
    background:linear-gradient(150deg,#ffd06b,#e8663d 70%);
    box-shadow:0 10px 24px rgba(232,102,61,.32), inset 0 1px 0 rgba(255,255,255,.45);
  }
  .logo svg{width:30px;height:30px;display:block;}
  h1{font-size:21px;margin:0;letter-spacing:.4px;font-weight:800;}
  .subtitle{font-size:12px;color:var(--muted);margin-top:2px;letter-spacing:.3px;}
  .right{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
  .badge{
    display:inline-flex;align-items:center;gap:7px;font-size:11px;letter-spacing:1.3px;font-weight:700;
    text-transform:uppercase;padding:7px 11px;border-radius:999px;border:1px solid var(--line);
    background:rgba(75,227,193,.08);color:var(--teal);
  }
  .badge .dot{width:7px;height:7px;border-radius:50%;background:var(--teal);box-shadow:0 0 10px var(--teal);animation:pulse 1.6s infinite;}
  .badge.manual{background:rgba(255,181,69,.09);color:var(--amber);}
  .badge.manual .dot{background:var(--amber);box-shadow:0 0 10px var(--amber);}
  @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.8)}}
  button{
    font-family:inherit;cursor:pointer;color:var(--text);font-size:12.5px;font-weight:700;letter-spacing:.3px;
    background:rgba(255,255,255,.055);border:1px solid var(--line);border-radius:10px;padding:8px 13px;
    transition:background .15s,transform .1s,border-color .15s;
  }
  button:hover{background:rgba(255,255,255,.11);border-color:rgba(255,255,255,.2);}
  button:active{transform:translateY(1px);}
  button.primary{background:linear-gradient(145deg,#ffc968,#ef8a3c);color:#26160a;border-color:transparent;box-shadow:0 8px 20px rgba(239,138,60,.25);}
  .hud{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:11px;}
  .stat{
    border:1px solid var(--line);border-radius:14px;padding:8px 13px 9px;
    background:linear-gradient(180deg,rgba(255,255,255,.055),rgba(255,255,255,.018));
  }
  .stat .label{font-size:9.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);font-weight:700;}
  .stat .value{font-size:21px;font-weight:800;font-variant-numeric:tabular-nums;margin-top:1px;}
  .stat .value small{font-size:12px;color:var(--muted);font-weight:700;}
  .stat.s1 .value{color:var(--amber)}
  .stat.s2 .value{color:var(--teal)}
  .stage{
    position:relative;border:1px solid var(--line);border-radius:20px;padding:8px;
    background:linear-gradient(180deg,#0d1420,#090e16);
    box-shadow:0 26px 64px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.05);
  }
  canvas{display:block;width:100%;height:auto;border-radius:14px;background:#0a1018;cursor:crosshair;}
  .banner{
    position:absolute;left:50%;bottom:20px;transform:translateX(-50%);
    display:flex;align-items:center;gap:10px;max-width:94%;
    background:rgba(9,14,23,.88);border:1px solid rgba(255,255,255,.16);
    border-radius:999px;padding:9px 17px;font-size:12.5px;color:#dfe8f4;
    box-shadow:0 12px 30px rgba(0,0,0,.45);backdrop-filter:blur(8px);
  }
  .banner .kbd{
    background:linear-gradient(180deg,#223049,#182234);border:1px solid #3a4a68;border-bottom-width:2px;
    border-radius:6px;padding:2px 7px;font-size:11px;font-weight:700;color:#fff;
  }
  .banner b{color:var(--amber);}
  .banner.hide{display:none;}
  .overlay{
    position:absolute;inset:8px;display:none;place-items:center;border-radius:14px;
    background:rgba(6,10,17,.76);backdrop-filter:blur(4px);z-index:5;
  }
  .overlay.show{display:grid;}
  .ov-card{text-align:center;padding:22px 26px;border-radius:16px;border:1px solid var(--line);background:rgba(15,22,34,.92);box-shadow:0 20px 50px rgba(0,0,0,.5);}
  .ov-card h2{margin:0 0 6px;font-size:24px;color:var(--amber);}
  .ov-card p{margin:0 0 14px;color:var(--muted);font-size:13px;}
  .controls{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:11px;color:var(--muted);font-size:12px;}
  .chips{display:flex;gap:7px;align-items:center;flex-wrap:wrap;}
  .chip{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--line);border-radius:8px;padding:4px 8px;background:rgba(255,255,255,.035);}
  kbd{background:#1b2434;border:1px solid #34425c;border-bottom-width:2px;border-radius:5px;padding:1px 6px;font-size:10.5px;color:#dbe6f5;font-weight:700;}
  .tip{color:#b9c6d8;}
  .tip b{color:var(--amber);}
  @media (max-width:680px){
    .app{padding:10px 10px 16px}
    h1{font-size:17px}
    .subtitle{font-size:11px}
    .hud{grid-template-columns:repeat(2,1fr);gap:8px}
    .stat .value{font-size:18px}
    .logo{width:40px;height:40px;border-radius:12px}
    .banner{bottom:12px;font-size:11px;padding:7px 12px;text-align:center;border-radius:14px;}
    .controls{font-size:11px}
  }
</style>
</head>
<body>
<div class="app">
  <div class="topbar">
    <div class="brand">
      <div class="logo" aria-hidden="true">
        <svg viewBox="0 0 32 32" fill="none">
          <path d="M5 22h21" stroke="#2b1608" stroke-width="2.4" stroke-linecap="round"/>
          <rect x="8" y="8" width="13" height="13" rx="3" fill="#fff1d6"/>
          <rect x="10" y="11" width="5" height="5" rx="1.4" fill="#2b1608"/>
          <path d="M21 12h4v6h-4z" fill="#fff1d6"/>
          <path d="M23 12V9h3v3" fill="#2b1608"/>
          <circle cx="11" cy="23" r="2.6" fill="#2b1608"/><circle cx="20" cy="23" r="2.6" fill="#2b1608"/>
        </svg>
      </div>
      <div>
        <h1>Platform Picker</h1>
        <div class="subtitle">A little train, a busy station, a long line of waiting passengers.</div>
      </div>
    </div>
    <div class="right">
      <span class="badge" id="modeBadge"><span class="dot"></span><span id="modeText">Autopilot demo</span></span>
      <button id="autoBtn">Autopilot</button>
      <button id="restartBtn">Restart</button>
    </div>
  </div>

  <div class="hud">
    <div class="stat s1"><div class="label">Score</div><div class="value" id="score">0</div></div>
    <div class="stat s2"><div class="label">On board</div><div class="value" id="aboard">0<small> / 6</small></div></div>
    <div class="stat"><div class="label">Waiting</div><div class="value" id="waiting">0</div></div>
    <div class="stat"><div class="label">Best run</div><div class="value" id="best">0</div></div>
  </div>

  <div class="stage" id="stage">
    <canvas id="game" width="960" height="520" aria-label="Railway snake game"></canvas>
    <div class="banner" id="banner">
      <span class="kbd">CLICK</span><span class="kbd">or press a key</span>
      <span>to take the controls — <b>autopilot is already running</b></span>
    </div>
    <div class="overlay" id="overlay">
      <div class="ov-card">
        <h2 id="ovTitle">Off the rails!</h2>
        <p id="ovText">The carriages came off the tracks. Give it another go.</p>
        <button class="primary" id="ovBtn">Restart run</button>
      </div>
    </div>
  </div>

  <div class="controls">
    <div class="chips">
      <span class="chip"><kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> to drive</span>
      <span class="chip"><kbd>Space</kbd> pause</span>
      <span class="chip"><kbd>R</kbd> restart</span>
    </div>
    <div class="tip">Pick up <b>passengers</b> on the platforms, then stop at <b>Central Station</b> to drop them off.</div>
  </div>
</div>

<script>
(function(){
  "use strict";

  /* ---------------- setup ---------------- */
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const COLS = 24, ROWS = 13, CELL = 40, W = COLS*CELL, H = ROWS*CELL;
  const STEP = 165; // ms per grid cell

  function fit(){
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = W*dpr; canvas.height = H*dpr;
    ctx.setTransform(dpr,0,0,dpr,0,0);
  }
  fit();
  window.addEventListener('resize', fit);

  /* ---------------- world ---------------- */
  const TRACK_ROWS = [2,6,10];
  const CONN_COLS = [2,8,14,20];
  const track = new Set();
  const k = (r,c) => r+','+c;
  TRACK_ROWS.forEach(r => { for(let c=1;c<=22;c++) track.add(k(r,c)); });
  CONN_COLS.forEach(c => { for(let r=2;r<=10;r++) track.add(k(r,c)); });

  const STATION = {r:6,c:12};
  const STATION_COLS = [9,10,11,12,13];
  const platformRows = [3,5,7,9];

  const platformCells = [];
  platformRows.forEach(r => {
    for(let c=1;c<=22;c++){
      if(CONN_COLS.indexOf(c) !== -1) continue;
      if(r===5 && STATION_COLS.indexOf(c) !== -1) continue;
      platformCells.push({r,c});
    }
  });

  const BENCHES = [{r:3,c:12},{r:5,c:6},{r:7,c:17},{r:9,c:9},{r:3,c:20}];
  const LAMPS   = [{r:3,c:4},{r:5,c:16},{r:7,c:6},{r:9,c:13},{r:7,c:21}];

  const SHIRTS = ['#ff7a6b','#ffd166','#6ee7b7','#60a5fa','#c084fc','#f9a8d4','#facc15','#5eead4'];
  const HAIRS  = ['#2c2330','#4a2f22','#1f2937','#7c4a2d','#3f2d56','#c98a4b'];
  const NAMES  = ['Mia','Theo','Ada','Ivy','Leo','Zoe','Finn','Ruby','Otto','Nora','Kai','Elsa'];

  const WAGON_COLORS = ['#f5a524','#22b8a6','#8b5cf6'];

  /* ---------------- state ---------------- */
  let P = [];                 // path cells: [head, w1, w2, w3, tailHistory]
  let dir = {r:0,c:1};
  let pendingDir = null;
  let mode = 'demo';
  let moveTimer = 0, last = performance.now(), acc = 0;
  let passengers = [];
  let wagons = [ {p:[]}, {p:[]}, {p:[]} ];
  let effects = [], particles = [], steam = [];
  let score = 0, best = 0, aboard = 0;
  let crashed = false, paused = false;
  let spawnTimer = 0;
  let uid = 1;
  let targetMarker = null, markerTimer = 0;

  const els = {
    score: document.getElementById('score'),
    aboard: document.getElementById('aboard'),
    waiting: document.getElementById('waiting'),
    best: document.getElementById('best'),
    banner: document.getElementById('banner'),
    badge: document.getElementById('modeBadge'),
    modeText: document.getElementById('modeText'),
    overlay: document.getElementById('overlay'),
    ovTitle: document.getElementById('ovTitle'),
    ovText: document.getElementById('ovText')
  };

  /* ---------------- helpers ---------------- */
  function rr(x,y,w,h,r){
    ctx.beginPath();
    const rad = Math.min(r, Math.abs(w)/2, Math.abs(h)/2);
    ctx.moveTo(x+rad,y);
    ctx.arcTo(x+w,y,x+w,y+h,rad);
    ctx.arcTo(x+w,y+h,x,y+h,rad);
    ctx.arcTo(x,y+h,x,y,rad);
    ctx.arcTo(x,y,x+w,y,rad);
    ctx.closePath();
  }
  function cellCenter(cell){ return {x:cell.c*CELL+CELL/2, y:cell.r*CELL+CELL/2}; }
  function lerp(a,b,t){ return a+(b-a)*t; }
  function lerpCell(a,b,t){ return {r:lerp(a.r,b.r,t), c:lerp(a.c,b.c,t)}; }
  function dirBetween(a,b){
    const dr=b.r-a.r, dc=b.c-a.c;
    if(dr===0 && dc>0) return {r:0,c:1};
    if(dr===0 && dc<0) return {r:0,c:-1};
    if(dr>0) return {r:1,c:0};
    if(dr<0) return {r:-1,c:0};
    return {r:0,c:1};
  }
  function angleOf(d){ return Math.atan2(d.r,d.c); }
  function neighbors(cell){
    const out=[];
    const cand=[{r:cell.r-1,c:cell.c},{r:cell.r+1,c:cell.c},{r:cell.r,c:cell.c-1},{r:cell.r,c:cell.c+1}];
    for(const c of cand) if(track.has(k(c.r,c.c))) out.push(c);
    return out;
  }
  function bfs(start, blocked){
    const dist=new Map(), prev=new Map(), q=[start];
    dist.set(k(start.r,start.c),0);
    let head=0;
    while(head<q.length){
      const cur=q[head++];
      const d=dist.get(k(cur.r,cur.c));
      for(const nb of neighbors(cur)){
        const key=k(nb.r,nb.c);
        if(dist.has(key)) continue;
        if(blocked && blocked.has(key)) continue;
        dist.set(key,d+1); prev.set(key,cur); q.push(nb);
      }
    }
    return {dist,prev};
  }
  function reconstruct(prev, target){
    const path=[target]; let cur=target;
    while(prev.has(k(cur.r,cur.c))){ cur=prev.get(k(cur.r,cur.c)); path.push(cur); }
    path.reverse();
    return path;
  }
  function rand(a){ return a[Math.floor(Math.random()*a.length)]; }

  /* ---------------- passengers ---------------- */
  function approachFor(cell){
    if(cell.r===3) return {r:2,c:cell.c};
    if(cell.r===5) return {r:6,c:cell.c};
    if(cell.r===7) return {r:6,c:cell.c};
    return {r:10,c:cell.c};
  }
  function spawnPassenger(forceCell){
    let cell = forceCell;
    if(!cell){
      const free = platformCells.filter(c => !passengers.some(p=>p.r===c.r&&p.c===c.c));
      if(!free.length) return;
      cell = rand(free);
    }
    const ap = approachFor(cell);
    const p = {
      id: uid++,
      r:cell.r, c:cell.c, ar:ap.r, ac:ap.c,
      color: rand(SHIRTS), hair: rand(HAIRS), name: rand(NAMES),
      phase: Math.random()*Math.PI*2, board:0
    };
    passengers.push(p);
  }
  function initialPassengers(){
    passengers = [];
    const seeds = [{r:3,c:6},{r:3,c:10},{r:5,c:17},{r:7,c:11},{r:7,c:18},{r:9,c:15}];
    for(const s of seeds) spawnPassenger(s);
  }

  /* ---------------- game flow ---------------- */
  function reset(){
    P = [{r:2,c:5},{r:2,c:4},{r:2,c:3},{r:2,c:2},{r:2,c:1}];
    dir = {r:0,c:1}; pendingDir = null;
    score = 0; aboard = 0;
    wagons = [{p:[]},{p:[]},{p:[]}];
    effects = []; particles = []; steam = [];
    crashed = false; paused = false;
    moveTimer = 0; spawnTimer = 0;
    initialPassengers();
    els.overlay.classList.remove('show');
    updateHUD();
  }

  function addEffect(x,y,text,color){
    effects.push({x,y,text,color,t:0,life:1.25});
  }
  function burst(x,y,color,n){
    for(let i=0;i<n;i++){
      const a=Math.random()*Math.PI*2, s=40+Math.random()*110;
      particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-30,life:.7+Math.random()*.5,t:0,color});
    }
  }

  function boardPassenger(p){
    // find a wagon with space
    for(let i=0;i<wagons.length;i++){
      if(wagons[i].p.length < 2){
        wagons[i].p.push({color:p.color,hair:p.hair,name:p.name});
        aboard++;
        score += 10;
        const c = cellCenter({r:p.ar,c:p.ac});
        addEffect(c.x,c.y-10,'+10  '+p.name+' aboard',p.color);
        burst(c.x,c.y,p.color,14);
        return true;
      }
    }
    return false;
  }
  function deliver(){
    if(aboard<=0) return;
    const bonus = aboard*20;
    score += bonus;
    const c = cellCenter(STATION);
    addEffect(c.x,c.y-16,'DELIVERED  +'+bonus,'#ffd166');
    burst(c.x,c.y,'#ffd166',30);
    for(let i=0;i<aboard;i++) burst(c.x+(Math.random()*30-15),c.y+(Math.random()*20-10), '#4be3c1',4);
    wagons = [{p:[]},{p:[]},{p:[]}];
    aboard = 0;
  }

  function aiNextCell(){
    const head = P[0];
    const blocked = new Set([k(P[1].r,P[1].c), k(P[2].r,P[2].c)]);
    const needDeliver = aboard >= 4 || passengers.length === 0;
    let targets;
    if(needDeliver){
      targets = [{cell:STATION, kind:'station'}];
    } else {
      targets = passengers.map(p => ({cell:{r:p.ar,c:p.ac}, kind:'passenger', p}));
    }
    let res = bfs(head, blocked);
    let bestT = null, bestD = Infinity;
    for(const t of targets){
      const d = res.dist.get(k(t.cell.r,t.cell.c));
      if(d!==undefined && d>0 && d<bestD){ bestD=d; bestT=t; }
    }
    if(!bestT){
      res = bfs(head, null);
      for(const t of targets){
        const d = res.dist.get(k(t.cell.r,t.cell.c));
        if(d!==undefined && d>0 && d<bestD){ bestD=d; bestT=t; }
      }
      if(!bestT) return null;
      const path = reconstruct(res.prev, bestT.cell);
      let nxt = path[1];
      if(!nxt) return null;
      if(blocked.has(k(nxt.r,nxt.c))){
        // fall back to any neighbour that decreases distance
        let bestNb=null, bestDist=Infinity;
        for(const nb of neighbors(head)){
          if(blocked.has(k(nb.r,nb.c))) continue;
          const d = res.dist.get(k(nb.r,nb.c));
          if(d!==undefined && d<bestDist){ bestDist=d; bestNb=nb; }
        }
        return bestNb;
      }
      return nxt;
    }
    const path = reconstruct(res.prev, bestT.cell);
    return path[1] || null;
  }

  function step(){
    if(crashed || paused) return;
    let next = null;
    if(mode === 'demo'){
      next = aiNextCell();
      if(!next) next = {r:P[0].r+dir.r, c:P[0].c+dir.c};
    } else {
      const d = pendingDir || dir;
      const cand = {r:P[0].r+d.r, c:P[0].c+d.c};
      if(track.has(k(cand.r,cand.c))) next = cand;
      else next = {r:P[0].r+dir.r, c:P[0].c+dir.c};
    }
    if(!next || !track.has(k(next.r,next.c))){ pendingDir=null; return; }

    // collision with the middle of the train
    for(let i=1;i<=2;i++){
      if(P[i].r===next.r && P[i].c===next.c){ crash(); return; }
    }

    dir = dirBetween(P[0], next);
    P.unshift(next);
    if(P.length>5) P.pop();

    // pickup
    for(let i=passengers.length-1;i>=0;i--){
      const p = passengers[i];
      if(p.ar===next.r && p.ac===next.c){
        if(boardPassenger(p)) passengers.splice(i,1);
      }
    }
    // station
    if(next.r===STATION.r && next.c===STATION.c) deliver();

    if(score>best) best = score;
    updateHUD();
  }

  function crash(){
    crashed = true;
    const c = cellCenter(P[0]);
    burst(c.x,c.y,'#ff7a6b',26);
    els.ovTitle.textContent = 'Off the rails!';
    els.ovText.textContent = 'The carriages derailed. Score so far: ' + score + '.';
    els.overlay.classList.add('show');
    updateHUD();
  }

  /* ---------------- input ---------------- */
  function setMode(m){
    mode = m;
    pendingDir = null;
    els.banner.classList.add('hide');
    if(m==='manual'){
      els.badge.classList.add('manual');
      els.modeText.textContent = 'Manual control';
    } else {
      els.badge.classList.remove('manual');
      els.modeText.textContent = 'Autopilot demo';
    }
  }
  function takeControl(){
    if(crashed) return;
    if(mode!=='manual') setMode('manual');
  }

  window.addEventListener('keydown', function(e){
    const key = e.key.toLowerCase();
    if(['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d',' '].indexOf(key)!==-1) e.preventDefault();
    if(key==='r'){ reset(); setMode(mode); return; }
    if(key===' '){
      if(!crashed){ paused = !paused; showPauseBanner(); }
      return;
    }
    const dirs = {
      arrowup:{r:-1,c:0}, w:{r:-1,c:0},
      arrowdown:{r:1,c:0}, s:{r:1,c:0},
      arrowleft:{r:0,c:-1}, a:{r:0,c:-1},
      arrowright:{r:0,c:1}, d:{r:0,c:1}
    };
    const nd = dirs[key];
    if(nd){ takeControl(); queueDir(nd); }
  });
  function queueDir(nd){
    if(crashed) return;
    const cur = pendingDir || dir;
    if(nd.r===-cur.r && nd.c===-cur.c) return; // no reversing
    const cand = {r:P[0].r+nd.r, c:P[0].c+nd.c};
    if(!track.has(k(cand.r,cand.c))) return;
    // don't steer straight into the carriages
    for(let i=1;i<=2;i++) if(P[i].r===cand.r && P[i].c===cand.c) return;
    pendingDir = nd;
  }
  canvas.addEventListener('pointerdown', function(){ takeControl(); });
  document.getElementById('autoBtn').addEventListener('click', function(){
    if(crashed) reset();
    setMode('demo');
    els.banner.classList.add('hide');
  });
  document.getElementById('restartBtn').addEventListener('click', function(){ reset(); });
  document.getElementById('ovBtn').addEventListener('click', function(){ reset(); });

  function showPauseBanner(){
    if(paused){
      els.banner.innerHTML = '<span class="kbd">PAUSED</span><span>Press <b>Space</b> to resume</span>';
      els.banner.classList.remove('hide');
    } else {
      els.banner.classList.add('hide');
    }
  }

  /* ---------------- HUD ---------------- */
  function updateHUD(){
    els.score.textContent = score;
    els.aboard.innerHTML = aboard + '<small> / 6</small>';
    els.waiting.textContent = passengers.length;
    els.best.textContent = best;
  }

  /* ---------------- rendering ---------------- */
  // deterministic ground speckles
  const speckles = [];
  for(let i=0;i<260;i++){
    speckles.push({x:Math.random()*W, y:Math.random()*H, r:Math.random()*1.8+0.4, a:Math.random()*0.05+0.012});
  }

  function drawGround(){
    const g = ctx.createLinearGradient(0,0,W,H);
    g.addColorStop(0,'#101a26');
    g.addColorStop(.55,'#0c1420');
    g.addColorStop(1,'#0a1119');
    ctx.fillStyle=g; ctx.fillRect(0,0,W,H);

    // soft moonlight pools
    const pool = ctx.createRadialGradient(W*0.22,H*0.1,20,W*0.22,H*0.1,340);
    pool.addColorStop(0,'rgba(90,140,200,.10)'); pool.addColorStop(1,'rgba(90,140,200,0)');
    ctx.fillStyle=pool; ctx.fillRect(0,0,W,H);

    ctx.fillStyle='#9fc4e8';
    for(const s of speckles){ ctx.globalAlpha=s.a; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,7); ctx.fill(); }
    ctx.globalAlpha=1;

    // faint grid
    ctx.strokeStyle='rgba(255,255,255,.028)'; ctx.lineWidth=1;
    ctx.beginPath();
    for(let x=0;x<=W;x+=CELL){ ctx.moveTo(x+.5,0); ctx.lineTo(x+.5,H); }
    for(let y=0;y<=H;y+=CELL){ ctx.moveTo(0,y+.5); ctx.lineTo(W,y+.5); }
    ctx.stroke();
  }

  function drawTrack(){
    // ballast
    ctx.fillStyle='#232c35';
    for(const r of TRACK_ROWS){ rr(CELL, r*CELL+5, 21*CELL, 30, 9); ctx.fill(); }
    for(const c of CONN_COLS){ rr(c*CELL+5, 2*CELL, 30, 8*CELL, 9); ctx.fill(); }
    // gravel speckle on ballast
    ctx.fillStyle='rgba(255,255,255,.05)';
    for(const r of TRACK_ROWS){
      for(let x=CELL+6; x<22*CELL; x+=11){
        ctx.fillRect(x, r*CELL+8+((x/11)%3)*8, 3,2);
      }
    }
    // sleepers
    ctx.fillStyle='#40352a';
    for(const r of TRACK_ROWS){
      for(let x=CELL+4; x<22*CELL; x+=15){ ctx.fillRect(x, r*CELL+7, 7, 26); }
    }
    for(const c of CONN_COLS){
      for(let y=2*CELL+4; y<10*CELL; y+=15){ ctx.fillRect(c*CELL+7, y, 26, 7); }
    }
    // rails
    const railGrad = ctx.createLinearGradient(0,0,0,H);
    railGrad.addColorStop(0,'#8fa3b4'); railGrad.addColorStop(.5,'#cfdde8'); railGrad.addColorStop(1,'#7f93a4');
    ctx.strokeStyle=railGrad; ctx.lineWidth=3; ctx.lineCap='round';
    for(const r of TRACK_ROWS){
      ctx.beginPath(); ctx.moveTo(CELL+4, r*CELL+12); ctx.lineTo(22*CELL+34, r*CELL+12); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(CELL+4, r*CELL+24); ctx.lineTo(22*CELL+34, r*CELL+24); ctx.stroke();
    }
    for(const c of CONN_COLS){
      ctx.beginPath(); ctx.moveTo(c*CELL+12, 2*CELL+6); ctx.lineTo(c*CELL+12, 10*CELL+32); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(c*CELL+24, 2*CELL+6); ctx.lineTo(c*CELL+24, 10*CELL+32); ctx.stroke();
    }
  }

  function drawPlatforms(t){
    for(const cell of platformCells){
      const x=cell.c*CELL+2, y=cell.r*CELL+3, w=CELL-4, h=CELL-6;
      ctx.fillStyle='#333c48'; rr(x,y,w,h,5); ctx.fill();
      ctx.strokeStyle='rgba(255,255,255,.07)'; ctx.lineWidth=1; ctx.stroke();
      ctx.fillStyle='rgba(0,0,0,.16)'; ctx.fillRect(x+3,y+h/2-1,w-6,1.5);
      const topSide = (cell.r===3 || cell.r===7);
      ctx.fillStyle='#e3b53c';
      if(topSide) ctx.fillRect(x+4,y+3,w-8,2.5);
      else ctx.fillRect(x+4,y+h-6,w-8,2.5);
    }
    // benches
    for(const b of BENCHES){
      const x=b.c*CELL+CELL/2, y=b.r*CELL+CELL/2;
      ctx.fillStyle='rgba(0,0,0,.28)'; rr(x-11,y-5,22,12,3); ctx.fill();
      ctx.fillStyle='#6b4a2f'; rr(x-10,y-7,20,7,2.5); ctx.fill();
      ctx.fillStyle='#865c3b'; rr(x-10,y-1,20,5,2); ctx.fill();
      ctx.fillStyle='#2a3442'; ctx.fillRect(x-7,y+5,3,4); ctx.fillRect(x+4,y+5,3,4);
    }
    // lamps
    for(const l of LAMPS){
      const x=l.c*CELL+CELL/2, y=l.r*CELL+CELL/2;
      const g=ctx.createRadialGradient(x,y,2,x,y,26);
      g.addColorStop(0,'rgba(255,203,106,.45)'); g.addColorStop(1,'rgba(255,203,106,0)');
      ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,26,0,7); ctx.fill();
      ctx.fillStyle='#1d2530'; ctx.beginPath(); ctx.arc(x,y,4.5,0,7); ctx.fill();
      ctx.fillStyle='#ffd98a'; ctx.beginPath(); ctx.arc(x,y-1,2.6,0,7); ctx.fill();
    }
    // station canopy
    const sx=9*CELL+2, sy=5*CELL+1, sw=5*CELL-4, sh=CELL-2;
    ctx.fillStyle='#1e2734'; rr(sx,sy,sw,sh,7); ctx.fill();
    ctx.fillStyle='#2c3949'; rr(sx+5,sy+4,sw-10,sh-9,5); ctx.fill();
    for(let i=0;i<5;i++){
      const lx=sx+16+i*36;
      ctx.fillStyle='#ffd27a'; ctx.beginPath(); ctx.arc(lx,sy+7,2.2,0,7); ctx.fill();
    }
    ctx.fillStyle='#ffce6a';
    ctx.font='bold 9px "Trebuchet MS", sans-serif';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText('CENTRAL STATION', sx+sw/2, sy+sh-9);
    // station marker on the track
    const sc=cellCenter(STATION);
    ctx.save();
    ctx.translate(sc.x,sc.y);
    ctx.rotate(t*0.9);
    ctx.strokeStyle='rgba(255,206,106,.9)'; ctx.lineWidth=2.4;
    ctx.setLineDash([7,7]); ctx.beginPath(); ctx.arc(0,0,14,0,7); ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();
    ctx.fillStyle='rgba(255,206,106,.16)';
    ctx.beginPath(); ctx.arc(sc.x,sc.y,12+Math.sin(t*3)*1.5,0,7); ctx.fill();
    ctx.fillStyle='#ffce6a';
    ctx.font='bold 8px "Trebuchet MS", sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText('STOP', sc.x, sc.y+1);
  }

  function drawPerson(x,y,color,hair,phase,wave){
    ctx.save();
    ctx.translate(x,y);
    const bob=Math.sin(phase)*1.3;
    ctx.fillStyle='rgba(0,0,0,.32)';
    ctx.beginPath(); ctx.ellipse(0,11,8,3.4,0,0,7); ctx.fill();
    ctx.fillStyle=color; rr(-6.5,-4+bob,13,15,4.5); ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.16)'; rr(-6.5,-4+bob,13,4,2); ctx.fill();
    ctx.fillStyle='#f2c7a0'; ctx.beginPath(); ctx.arc(0,-9.5+bob,5.4,0,7); ctx.fill();
    ctx.fillStyle=hair;
    ctx.beginPath(); ctx.arc(0,-11+bob,5.4,Math.PI*1.05,Math.PI*2.0); ctx.fill();
    ctx.strokeStyle=color; ctx.lineWidth=3; ctx.lineCap='round';
    ctx.beginPath();
    if(wave){ ctx.moveTo(4.5,-1+bob); ctx.lineTo(9,-7+bob+Math.sin(phase*2.2)*2.2); }
    else { ctx.moveTo(4.5,0+bob); ctx.lineTo(7.5,4+bob); }
    ctx.stroke();
    ctx.restore();
  }

  function drawWaitingPassengers(t){
    for(const p of passengers){
      const c=cellCenter(p);
      // little suitcase
      ctx.fillStyle='#7b5a3c'; rr(c.x+7,c.y+3,9,7,2); ctx.fill();
      ctx.strokeStyle='#a5825f'; ctx.lineWidth=1; ctx.beginPath(); ctx.arc(c.x+11.5,c.y+3,3,Math.PI,0); ctx.stroke();
      // waiting marker
      const bob=Math.sin(t*2+p.phase)*1.6;
      ctx.fillStyle='rgba(255,255,255,.14)';
      ctx.beginPath(); ctx.arc(c.x,c.y-21+bob,2.4,0,7); ctx.fill();
      drawPerson(c.x,c.y,p.color,p.hair,t*2+p.phase,true);
    }
  }

  function drawLoco(x,y,ang){
    ctx.save(); ctx.translate(x,y); ctx.rotate(ang);
    ctx.fillStyle='rgba(0,0,0,.38)'; rr(-18,-12,38,26,7); ctx.fill();
    // chassis
    ctx.fillStyle='#141b26'; rr(-17,-13,34,26,6); ctx.fill();
    // wheels
    ctx.fillStyle='#0b1017';
    const wheels=[[-11,-14],[0,-14],[9,-14],[-11,14],[0,14],[9,14]];
    for(const w of wheels){ ctx.beginPath(); ctx.arc(w[0],w[1],3.6,0,7); ctx.fill(); }
    // boiler
    const g=ctx.createLinearGradient(0,-10,0,10);
    g.addColorStop(0,'#e2695c'); g.addColorStop(.55,'#c9483f'); g.addColorStop(1,'#8f2f2c');
    ctx.fillStyle=g; rr(-3,-10.5,19,21,7); ctx.fill();
    ctx.fillStyle='#f7d488'; rr(2,-10.5,3.2,21,1.4); ctx.fill();
    // cab
    const cg=ctx.createLinearGradient(0,-11,0,11);
    cg.addColorStop(0,'#3f77c0'); cg.addColorStop(1,'#24508c');
    ctx.fillStyle=cg; rr(-16,-11.5,12,23,4.5); ctx.fill();
    ctx.fillStyle='#bfe7ff'; rr(-13,-7.5,6,15,2); ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.5)'; rr(-12.2,-6.6,2.4,5,1); ctx.fill();
    // chimney
    ctx.fillStyle='#1d2530'; ctx.beginPath(); ctx.arc(9.5,-1.5,5.2,0,7); ctx.fill();
    ctx.fillStyle='#3a4656'; ctx.beginPath(); ctx.arc(9.5,-1.5,3.1,0,7); ctx.fill();
    // headlight
    const hg=ctx.createRadialGradient(17,0,1,17,0,26);
    hg.addColorStop(0,'rgba(255,240,190,.85)'); hg.addColorStop(1,'rgba(255,220,140,0)');
    ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(17,0,26,0,7); ctx.fill();
    ctx.fillStyle='#fff4cd'; ctx.beginPath(); ctx.arc(16.5,0,3.6,0,7); ctx.fill();
    // buffer
    ctx.fillStyle='#44506180'; rr(14,-9.5,4,19,2); ctx.fill();
    ctx.restore();
  }

  function drawWagon(x,y,ang,idx){
    const base=WAGON_COLORS[idx];
    ctx.save(); ctx.translate(x,y); ctx.rotate(ang);
    ctx.fillStyle='rgba(0,0,0,.34)'; rr(-16,-11,33,24,6); ctx.fill();
    ctx.fillStyle='#141b26'; rr(-15.5,-12,31,24,5); ctx.fill();
    const g=ctx.createLinearGradient(0,-10,0,10);
    g.addColorStop(0,base); g.addColorStop(1,'rgba(0,0,0,.35)');
    ctx.fillStyle=g; rr(-13.5,-10,27,20,4); ctx.fill();
    ctx.fillStyle='rgba(0,0,0,.22)'; rr(-11.5,-8,23,16,3); ctx.fill();
    // seats
    ctx.fillStyle='rgba(255,255,255,.14)';
    rr(-10,-6,9,12,2); ctx.fill(); rr(1,-6,9,12,2); ctx.fill();
    // passengers
    const list=wagons[idx].p;
    const slots=[-5.5,5.5];
    for(let i=0;i<list.length;i++){
      const p=list[i], sx=slots[i];
      ctx.fillStyle=p.color; rr(sx-4,-5,8,10,3); ctx.fill();
      ctx.fillStyle='#f2c7a0'; ctx.beginPath(); ctx.arc(sx,-5.5,3.6,0,7); ctx.fill();
      ctx.fillStyle=p.hair; ctx.beginPath(); ctx.arc(sx,-6.6,3.6,Math.PI*1.05,Math.PI*2); ctx.fill();
    }
    // coupling + lantern
    ctx.fillStyle='#3d4757'; rr(-17,-2.5,4,5,1.5); ctx.fill();
    ctx.fillStyle='#ffe9a8'; ctx.beginPath(); ctx.arc(13.5,0,1.9,0,7); ctx.fill();
    ctx.restore();
  }

  function drawTrain(t){
    const pr = Math.min(1, moveTimer/STEP);
    const pos=[], ang=[];
    pos[0]=lerpCell(P[1],P[0],pr);
    pos[1]=lerpCell(P[2],P[1],pr);
    pos[2]=lerpCell(P[3],P[2],pr);
    pos[3]=lerpCell(P[4],P[3],pr);
    ang[0]=angleOf(dirBetween(P[1],P[0]));
    ang[1]=angleOf(dirBetween(P[2],P[1]));
    ang[2]=angleOf(dirBetween(P[3],P[2]));
    ang[3]=angleOf(dirBetween(P[4],P[3]));
    for(let i=3;i>=1;i--){
      const c=cellCenter(pos[i]);
      drawWagon(c.x,c.y,ang[i],i-1);
    }
    const c=cellCenter(pos[0]);
    drawLoco(c.x,c.y,ang[0]);
    return {head:c, angle:ang[0]};
  }

  function drawMarker(t){
    if(!targetMarker) return;
    const c=cellCenter(targetMarker.cell);
    const bob=Math.sin(t*3)*2.5;
    ctx.save();
    ctx.translate(c.x, c.y-26+bob);
    ctx.rotate(t*1.6);
    ctx.fillStyle= targetMarker.kind==='station' ? '#ffd166' : '#4be3c1';
    ctx.shadowColor=ctx.fillStyle; ctx.shadowBlur=12;
    ctx.beginPath();
    ctx.moveTo(0,-6); ctx.lineTo(6,0); ctx.lineTo(0,6); ctx.lineTo(-6,0); ctx.closePath(); ctx.fill();
    ctx.restore();
    // faint beam
    ctx.strokeStyle= targetMarker.kind==='station' ? 'rgba(255,209,102,.28)' : 'rgba(75,227,193,.25)';
    ctx.lineWidth=1.5; ctx.setLineDash([4,5]);
    ctx.beginPath(); ctx.moveTo(c.x,c.y-24+bob); ctx.lineTo(c.x,c.y-4); ctx.stroke();
    ctx.setLineDash([]);
  }

  function drawEffects(dt){
    for(let i=effects.length-1;i>=0;i--){
      const e=effects[i]; e.t+=dt;
      if(e.t>e.life){ effects.splice(i,1); continue; }
      const a=Math.max(0,1-e.t/e.life);
      ctx.globalAlpha=a;
      ctx.fillStyle=e.color;
      ctx.font='bold 13px "Trebuchet MS", sans-serif';
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.fillText(e.text, e.x, e.y-e.t*26);
      ctx.globalAlpha=1;
    }
    for(let i=particles.length-1;i>=0;i--){
      const p=particles[i]; p.t+=dt;
      if(p.t>p.life){ particles.splice(i,1); continue; }
      p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=240*dt;
      ctx.globalAlpha=Math.max(0,1-p.t/p.life);
      ctx.fillStyle=p.color;
      ctx.fillRect(p.x-1.6,p.y-1.6,3.2,3.2);
      ctx.globalAlpha=1;
    }
  }

  function drawSteam(dt){
    for(let i=steam.length-1;i>=0;i--){
      const s=steam[i]; s.t+=dt; s.x+=s.vx*dt; s.y+=s.vy*dt;
      if(s.t>s.life){ steam.splice(i,1); continue; }
      ctx.globalAlpha=Math.max(0,(1-s.t/s.life)*0.4);
      ctx.fillStyle='#dfe9f5';
      ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,7); ctx.fill();
      ctx.globalAlpha=1;
    }
  }

  function updateMarker(){
    markerTimer -= 1;
    if(markerTimer>0 && targetMarker) return;
    markerTimer = 12;
    if(aboard>=4 || passengers.length===0){
      targetMarker = {cell:STATION, kind:'station'};
    } else {
      const res=bfs(P[0], null);
      let bestP=null, bd=Infinity;
      for(const p of passengers){
        const d=res.dist.get(k(p.ar,p.ac));
        if(d!==undefined && d<bd){ bd=d; bestP=p; }
      }
      targetMarker = bestP ? {cell:{r:bestP.ar,c:bestP.ac}, kind:'passenger'} : {cell:STATION,kind:'station'};
    }
  }

  function vignette(){
    const g=ctx.createRadialGradient(W/2,H/2,H*0.42,W/2,H/2,H*0.95);
    g.addColorStop(0,'rgba(0,0,0,0)'); g.addColorStop(1,'rgba(0,0,0,.55)');
    ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
  }

  /* ---------------- main loop ---------------- */
  function frame(now){
    const dt = Math.min(0.05, (now-last)/1000);
    last = now;
    const t = now/1000;

    if(!paused && !crashed){
      moveTimer += dt*1000;
      while(moveTimer >= STEP){ moveTimer -= STEP; step(); }
      // spawn replacement passengers
      spawnTimer += dt;
      if(passengers.length < 6 && spawnTimer > 1.1){ spawnTimer=0; spawnPassenger(); }
      // steam from the chimney
      if(Math.random() < 0.5){
        const hp=cellCenter(P[0]);
        const a=angleOf(dirBetween(P[1],P[0]));
        const ox=hp.x+Math.cos(a)*10 - Math.sin(a)*(-2);
        const oy=hp.y+Math.sin(a)*10 + Math.cos(a)*(-2);
        steam.push({x:ox,y:oy,vx:-Math.cos(a)*16+(Math.random()*14-7),vy:-Math.sin(a)*16+(Math.random()*14-7),r:2.4+Math.random()*2,t:0,life:.8+Math.random()*.5});
      }
    }
    updateMarker();

    ctx.clearRect(0,0,W,H);
    drawGround();
    drawTrack();
    drawPlatforms(t);
    drawWaitingPassengers(t);
    drawMarker(t);
    const trainInfo = drawTrain(t);
    drawEffects(dt);
    drawSteam(dt);
    vignette();

    requestAnimationFrame(frame);
  }

  reset();
  setMode('demo');
  els.banner.classList.remove('hide');
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (17,905 tokens)
We need produce single HTML file with self-contained game. Need design high quality. Snake game train theme, locomotive and colorful wagons, passengers waiting on platform, picking them up. Need demo mode AI, overlay "Click or press key to start", interesting visible first paint, grid, platforms, passengers, wagons, etc.

We should craft polished canvas game in 1280x800 responsive. Need avoid horizontal scroll. Use canvas 960x600 maybe responsive CSS width. UI around. Game mechanics: train head moves grid-like continuous direction turning at cells. Wagons follow path history. Passengers on platforms along track edges? "picking up passengers waiting on the platform". Let's design:

Canvas 1000x600 world with grid 20 cols x 12 rows, cell 40. Track occupies maybe rows 1..10 full width as railway line. Platforms between tracks? Perhaps two horizontal tracks at rows 3 and 7, station platforms alternating. Snake-style movement on a grid with direction changes; passengers on platform cells adjacent to track. Picking up: when train head reaches a passenger's adjacent track cell, load passenger into nearest wagon, wagon gains passenger, score. Each wagon has capacity 3, visible passenger icons.

Let's make train occupies snake path: locomotive + 3 wagons (total 4 segments). Motion continuous at ~140 px/s, direction snaps cardinal; turning at cell boundaries. Use path history points each cell; body follows.

Layout: canvas 1000x560, grid cell 40, cols 25, rows 14. Railway tracks horizontal at rows 2, 5, 8, 11? Actually rows of tracks: e.g. tracks at y rows 2,5,8,11 separated by platform strips. Station platforms: rows between tracks (rows 3-4, 6-7, 9-10), with platform tiles. Passengers stand on platform tiles adjacent to a track (row track+1 below or track-1 above). Train moves along horizontal tracks and can switch tracks vertically at intersections? Complex.

Alternative simpler and clear: The snake moves on a grid board, and stations are platform tiles at board edges; passengers wait on platform cells that are adjacent to track cells. Train passes and picks up. Track board itself is a grid of rail cells with junctions; snake path on all cells looks like tracks everywhere though.

Better: board grid is "railway network": horizontal main line through middle plus loops. But snake needs space.

I can design all grid cells as track (like a rail yard), with platforms rendered as small platform strips along certain cells. Passengers on platform pads adjacent to track; the train cannot go on platform cells (they are obstacles but pickups occur when passing adjacent). Since train is a snake on grid, track everywhere is fine but visually noisy.

Compromise: The railway is a set of horizontal tracks at rows 2, 6, 10, plus vertical connector tracks at columns 3, 11, 19 and columns 7, 15, 23 etc. Snake travels on track cells only; platforms are the gaps between tracks; passengers wait on gap cells adjacent to track. Train picks passenger when its head enters a track cell adjacent to passenger cell. Passengers count increases; deliver them at depot station to score.

Snake-like: valid moves = track cells. Need generate a rail network graph that supports winding. Let's define track cell set programmatically:
- Horizontal rows: 2, 6, 10 across cols 1..23
- Vertical connectors: cols 2, 10, 18 across rows 2..10 (full), plus cols 6, 14, 22 across rows 2..6 and rows 6..10? If full vertical connectors of all rows, lots of crossings.

Let's pick grid 24 cols x 13 rows. Tracks:
rows 2, 6, 10 full width cols 1-22.
vertical connectors at cols 2, 8, 14, 20 connecting row2..row10 (segments continuous).
Maybe add upper/lower loops: horizontal rows 4? That creates more.

A serpentine route: row2 goes right, down at col22 to row6, row6 goes left, down at col2 to row10, row10 right, etc. But extra connectors allow branching. Include connector cols: 2, 8, 14, 20 all rows 2..10. Then path can snake.

Platforms are between tracks: rows 3-5 (between row2 and row6) and rows 7-9. But vertical connectors cut through. Passengers placed at cells like (row3/4/5, various col) that are adjacent to a horizontal track cell (row2 or row6) and not on connector. E.g platform row3 directly below row2 track, or row5 directly above row6. Render platform as a wooden/stone strip in that row. Looks good.

Passenger pickup: when head enters track cell (r,c) whose neighbor is passenger platform cell (r±1,c) with passenger. Since platform at row3 is adjacent to track row2 (above). Also passenger at row5 adjacent to track row6 (below). Also vertical adjacency: platform cells adjacent to vertical connectors.

Simpler: passengers are at specific platform cells adjacent to a track cell designated. Each passenger has home track cell. When train head reaches that cell, passenger boards.

Wagons: train has locomotive + 3 wagons; each wagon has seats (2-3). Passengers board to first wagon with room. Visual passengers seated on wagon. On reaching station platform (a designated station cell, e.g. track row6 col12?), unload? Score based passengers delivered? But the user says picking up passengers waiting on platform, so score for pickups is fine; maybe a destination station at right side where passengers get off and score.

Let's do: each passenger has destination marker color maybe; score for pickup +10, when train arrives at "Central Station" track cell, unload all passengers and award bonus. Hmm demo AI must pick up passengers and deliver, showing action.

Actually could implement score increments on pickup with a "+10" popup and a little "Passengers aboard: N". Also a milestone every 5 passengers triggers celebratory whistle? Let's keep approachable.

Demo AI: follows a predefined circuit on track network, aiming for passenger track cells. Since graph is grid-based, could implement BFS path to nearest passenger target along track cells, moving head one cell at a time, while snake body follows. That's classic snake with AI: head at cell center, moves cell-by-cell on grid (step animation). Grid movement is easier for wagon following. Let's do grid-step movement with animation interpolation. Each tick (say 190ms) moves one cell. Head cell must be a track cell. Body = list of cells behind (not reversed path? for snake on grid, if reversing into body invalid). Use body array [head, next, ...], shift/push. Wagons at body[1], body[2], body[3]. Ensure new head not in body.

AI: BFS over track cells avoiding body (except tail can move). Choose target: nearest passenger's target track cell; then maybe station. But snake body blocking on graph with degree 2 mostly. Add connectors give choices.

Manual: arrow keys/WASD to set next direction, can't reverse. Track cell only.

To show pick-up: When head enters passenger.target cell, the passenger is removed, passenger count added to a wagon (occupancy), score +10 and floating text "Boarded!". Wagons visually colored (coral, teal, yellow) each with passengers shown as small heads/bodies seated.

Then maybe after 4 passengers, "Destination reached" - train head reaches station cell (e.g., track row6 col12?) unload and score +25 per passenger, reset. But AI target switching: passenger collection then delivery. AI plan: if aboard count >= 3, target station; else nearest passenger. On arrival station, unload.

Station: render a station building/platform along row 5 maybe with "CENTRAL STATION" sign and passengers waiting? Station platform cells adjacent to a track cell designated station track. Let's set station at track cell (row6, col11) with platform row5 above. Visual signage.

However track row6 spans, and passenger at row5 also occupies platform; station area maybe cols 9-13 on row5 with station platform. Fine.

Passengers waiting on platform: spawn initially 6 passengers on platform cells at rows 3 (below top track row2) and rows 7 (below mid track row6?) and row9 (above bottom track row10). Use varied platform cells.

Let's define grid: cols 24, rows 13, cell 40 => 960x520 canvas. Add UI panel outside? Canvas width 100%, max 960. Whole page with header title, HUD cards, canvas, side instructions? 1280x800 viewport: header ~80, canvas 520, footer ~... Good. Use dark night railway theme, stars, warm lamps, gradient.

Design layout: page background #0b1020 gradient with subtle radial. Container max-width 1100 centered, padding 24. Top bar: logo badge "RAILWAY SNAKE", title "Platform Picker" maybe. HUD row: Score, Passengers aboard, Distance, Best. Then canvas card with rounded corners, overlay start message centered: "Click or press a key to start" plus "Auto-pilot demo" and a play button. Since game starts in demo mode behind message but overlay? Requirement: self-playing demo mode behind a "Click or press a key to start" message. So demo runs and message visible; message maybe a small banner at top center of canvas, not blocking the view much, or overlay semi-transparent panel but game visible behind. Let's do a slim glass pill at bottom center of canvas: "Click or press a key to take the controls" and the AI train is already moving. Also maybe an overlay card center on first paint but it should not hide the game. Use bottom banner.

Also need pause states: before interaction demo playing; after user presses key, manual control enabled and banner hides. "Demo" indicator.

Visuals:
- Canvas draw: night sky gradient at top? But grid is railway map top-down. Render dark blue-green ground with grass texture? Top-down railway: dark navy ground with grass texture? Use alternating tile pattern. Render horizontal ballast strips under track rows, sleepers, rails.
- Track cell rendering: two rails along orientation. Determine orientation by neighbors: if left/right track neighbors => horizontal, if up/down => vertical, if both -> junction. Draw rails with steel gradient, ties (wooden sleepers).
- Platforms: tan/cream platform tiles with edge yellow line, benches, lamps, small station signs.
- Passengers: little pixel people with varied colors, waving arm animation.
- Train: locomotive with rounded body, chimney, headlight glow, cowcatcher; wagons colorful with windows/passengers.
- Direction of head determines orientation; body follows path.

Need body path continuity for rendering: use body cell list and smooth interpolation? Grid-step with animation tween between previous cell and next cell; store head from/to and progress. For body, each segment interpolates from body[i] to body[i-1] (the one ahead) based on progress? Snake body in a grid: body[i] position interpolates from old body[i] to old body[i-1] while moving, using progress. Standard: keep pathCells array with head first; on each step shift. For rendering, positions: head lerp(prevHead, newHead, p) actually head moves from old head cell to new. Let's store cells array where cells[0] is current head cell (the target) and we animate from cells[0] to previous position. Simpler: keep path array of cells visited, path[0]=new head. Render segment i at lerp(path[i+1], path[i], p) for i>0 (moving from old position toward ahead). Head at lerp(path[1], path[0], p) i.e., from old head to new head.

When moving right, old head at left, new head at right; lerp(old,new) works: head = lerp(path[1], path[0], p) = from old (path[1]) to new (path[0]) yes with p from 0->1. Body seg1 = lerp(path[2], path[1], p): old seg1 position was path[1]... wait old body = [oldHead, b1, b2]. path[1] after shift = old b1. At p=0, body seg1 should be old b1 = path[1]; lerp(path[2], path[1], 0)=path[2]. Wrong order. Use lerp(a,b,t)= a+(b-a)t. For seg1: from path[2] (old b1? let's recompute).

Old body list O = [h0, b1, b2, b3] (O[1]=b1). After push new head h1: path P = [h1, h0, b1, b2]. Seg i body pos should go from O[i] to O[i-1] for i>=1, i.e., seg1 from b1 to h0. In P terms O[i]=P[i+1], O[i-1]=P[i]. So seg i = lerp(P[i+1], P[i], p). At p=0 -> P[i+1]=b1 correct; p=1 -> P[i]=h0 correct. Head = from h0=P[1] to h1=P[0]: lerp(P[1],P[0],p). Great, consistent.

Rendering orientation for each segment: direction from its cell to the cell ahead.

Body length 4 (locomotive + 3 wagons). Wagon capacity 2 each maybe. Boarding: choose wagon with room, assign passenger color; show seated passengers in wagon.

Score: +10 per pickup; unload at station +20 each and a "Delivered!" effect. AI: if passengers aboard total >= 3 or no remaining waiting passengers, target station. Otherwise target nearest passenger's approach cell.

Passengers need an "approach cell" (track cell adjacent to platform). Platform cell (r,c) with approach (r-1,c) or (r+1,c) that is a track. Choose platform positions manually:
Grid rows 0..12, tracks rows 2, 6, 10 (rows 0-based). Platform rows: 3 (adjacent above? row3 adjacent to track row2 above: approach (2,c)), 5 (adjacent below to row6: approach (6,c)), 7 (adjacent above to row6: approach (6,c)), 9 (adjacent above to row10: approach (10,c)). Also row 4? not adjacent. Let's use platform rows 3,4? Only rows adjacent matter for visuals: platforms are long strips in row3, row5, row7, row9. Hmm rows 3 and 4 could both be platform but passengers only placed in adjacent-to-track rows.

Let's define platform rows: 3 (under top track), 5 (above mid track), 7 (under mid track), 9 (above bottom track). Render each platform row as a strip of platform tiles spanning certain column ranges (e.g., 2..21) with occasional gaps. Then passengers on those rows at columns where approach is track and no station conflict.

But a platform strip directly adjacent to track row might visually overlap rails? Cell row3 is just below row2: platform strip adjacent to the track. Fine.

Station: row5, cols 9..14 platform with station building drawn above? We can't draw outside the cell. We can draw a station canopy on row5 cells plus a sign spanning. Station approach cell: row6, col 11? Let's make station track cell (6, 11). Platform row5 col11 has building sign "CENTRAL". Draw shelter roof.

Track network: rows 2,6,10 across cols 1..22. Vertical connectors cols 2, 8, 14, 20 from row2 to row10 continuous (so rows 3,4,5,7,8,9 at those columns are track, breaking platform strips). Also additional branch? Enough.

AI BFS on track cells. Need the head able to move: initial head at (2,3) going right? Let's set head at row2 col4, direction right, body trailing at (2,3),(2,2),(2,1). Manual movement directions.

BFS target selection: avoid cells occupied by body (excluding tail maybe). BFS from head. If target unreachable, fallback to station.

Passengers spawn: list of platform positions with approach cells. Let's generate candidates:
row3: cols 4,6,10,12,16,18 (approach row2 same col)
row5: cols 4,6,17,19 (approach row6)
row7: cols 3,5,10,16,19,21 (approach row6)
row9: cols 4,7,11,15,18,21 (approach row10)
Exclude cols that are connectors (2,8,14,20) and station area.

Initially spawn 7 passengers at chosen positions. Respawn as collected, keeping ~6 waiting.

Visual details: passengers on platform: little person ~ 18px tall with head circle, body, waving arm; name/color.

Wagon passenger representation: seated small heads in windows.

Let's plan canvas drawing functions:

resize: canvas width 960 height 520 CSS width 100%. Use devicePixelRatio scaling: set canvas.width = 960*dpr etc, ctx.scale. Keep logical coordinates via ctx.setTransform(dpr,0,0,dpr,0,0). Container aspect ratio 960/520.

Ground: fill #101a24; draw subtle tiles grid lines; random grass speckles (seeded).

Track drawing: for each track cell, determine horizontal if neighbors left/right track; vertical if up/down; if both → junction. Draw ballast rounded rect slightly larger with color #26313c; sleepers: for horizontal: vertical small rects every ~10px; rails: two horizontal lines at y±6 with gradient color #8fa3b5, highlight. For vertical similar. For junction draw cross.

Simpler robust approach: draw track as segments along rows and columns:
- For each horizontal track row, draw a continuous band from col1 to col22 with ballast, ties, two rails. Then vertical connectors draw vertical band over it (with slightly different ballast) and rails. Junctions will look okay.
That's much easier and looks great. Draw ground, then horizontal bands: ballast rect x=col1*cell, y=row*cell+5, w=21*cell, h=30; ties every 16 px; rails at y+11 and y+19.
Vertical connectors: ballast rect x=col*cell+5, y=row2*cell, w=30, h=8*cell; ties horizontal every 16px; rails x+11, x+19.

Draw order: all ballast first, then ties and rails to avoid weird overlaps? Just draw horizontal then vertical; overlaps fine.

Platforms: draw platform strips: for each platform row segment, rounded rect spanning cells, color #cbb79a? Night theme: warm concrete #6f6558 with edge stripe yellow. Let's make platforms in mid-tone slate #3b4450 with amber safety line adjacent to track side. Hmm contrast with dark ground: use #4a5361 with top edge amber.

Platform row3 (below track row2): platform area cells row3, but track connectors cross at cols 2,8,14,20 so break segments. Draw per-cell platform tiles where col not connector? Then passenger can stand on a platform cell. Yes platform = individual cells at non-connector columns in those rows; draw as platform tile with bench occasionally. But contiguous look better: draw platform as tiles with small gaps (like paving slabs) — top-down station platforms. Good: each platform cell tile 36x34 with rounded corners, warm gray #565f6b, edge line.

Station row5 cols 9..14: draw a canopy: rounded rect dark roof with amber lamp, sign "CENTRAL STATION" above the track? We'll draw text on canvas.

Lamps: small posts with warm glow circles at platform cells occasionally.

Night ambience: vignette, subtle stars? Top-down, so maybe soft radial light around train headlight.

Train rendering:
Locomotive: length ~ cell size (40) but drawn as body along direction. Draw rounded rect centered at position, oriented along axis. Body width 26, length 34. Color deep blue #2f6df6? Let's make classic steam locomotive: dark green/teal body with red trim? Colorful: locomotive crimson #e05a4f, boiler dark, chimney black, headlight yellow glow at front.

Draw in local coordinates rotated to direction angle (0 = right). Front is +x.
- coupler small
- wheels: 3 dark circles with light rim along bottom? Since top-down, wheels along sides.
- body rounded rect x from -16 to 14, y -12..12.
- boiler circle at x 6
- cab at x -8: window
- chimney at front x 12: small dark circle
- headlight glow at x 16.

Wagon: rect length 30 width 24, colorful with roof? Top-down: show open-top cars with seats; passengers as small heads/bodies inside. Draw rounded rect color, darker border, interior seats rows, passengers seated (small circles with colored shirt), cargo? Each wagon has capacity 2.

Direction for wagons: from its pos to the ahead pos (the one in front) so the wagon's front faces forward.

Connection: small dark coupler bars between segments.

Animation easing: linear or smoothstep.

Effects: floating score text array {x,y,text,color,life}. Passenger boarding animation: passenger moves from platform to wagon? Could animate: on pickup, create particle burst and the passenger pops.

HUD updates via DOM elements: score, aboard, waiting, best (best in memory variable, no localStorage). "Best" maybe session best.

Also progress: "Next stop" indicator.

Controls: arrows/WASD, Space pause, R restart? Show in footer with keycaps. After user input, banner hides and mode becomes manual; show a "MANUAL" badge; add button "Resume demo" maybe a small button in header, allowed (no confirm). Buttons: "Autopilot" toggle, "Restart". Clicking canvas also takes control? Requirement says click or press key to start; clicking could just dismiss banner and switch to manual, but then user might click random and train derails? We can make click on canvas start manual control (arrow keys after). Keep it: any key or click switches to manual and hides banner. Also keep autopilot toggle button.

Pause on window blur? not necessary.

Ensure demo runs from first paint: start loop immediately in demo mode. But requirement "behind a Click or press a key to start message" - yes banner visible while demo plays.

Time-based step: use requestAnimationFrame with accumulator; step every 165ms in demo, maybe 150 manual. Use p interpolation.

Edge/collision: head must be on track; if the next cell isn't a track (e.g., manual turning off track) - ignore invalid direction (don't move, keep direction). If next cell is body → crash. In demo AI always valid. On manual crash: show "Off the rails!" overlay, flash, then reset after 1.5s keeping score? Provide restart button. Should handle.

Let me also handle the "awaiting passengers" count and spawn.

BFS implementation details:

trackSet of "r,c".
neighbors(cell): up/down/left/right if track.
AI chooses:
- target list: waiting passengers' approach cells; if aboardCount >= capacity threshold (4?) or no waiting → station cell.
- BFS from head to nearest target cell (choose nearest by BFS distance). BFS should avoid body cells except the tail cell? To be safe, avoid all body cells except we must be able to reverse? Not reverse allowed anyway. Since body length 4 and graph corridors, BFS avoiding body generally fine. If unreachable, try targets in order of distance ignoring body.

But there's subtlety: the snake body occupies cells; when head moves into the cell where tail currently is, that's legal (tail moves). Avoid body except last element (tail) is allowed.

Also AI might oscillate: if the target approach cell is where the passenger is, and path leads directly. BFS path avoids body; but when we reach target, we pick up passenger. Then recompute target.

To avoid head running into its own body while the path chosen goes along body route (because body follows the head through a corridor), BFS avoiding body might find a long alternate route — good.

Implementation of choosing next dir from BFS: path from head to target; next cell = path[1]; dir = delta.

If path length 1 (already at target), pickup happens upon arrival.

Special: train head at target cell and passenger not collected because passenger's approach cell maybe was passed? We collect on entering approach cell, so ok.

Station arrival: when head enters station cell, unload all passengers: score += 20 each, aboard = 0, effect.

However AI targets station only after aboard >= 3; with capacity 3 wagons *2 = 6, if 6 aboard then need unloading. Set threshold 4.

Manual mode doesn't auto-deliver; player must drive to station. Show station marker with beam.

Also show a subtle arrow indicator pointing to next objective? Maybe a compass marker.

Let's now write the code carefully.

HTML structure:

<div class="app">
 header: brand with SVG locomotive icon (inline svg allowed), title, subtitle; right: mode badge, buttons.
 hud: 4 stat cards.
 canvas-wrap: canvas + banner overlay + toast area?
 footer: controls legend + tip.
</div>

CSS: nice fonts: use system fonts (no external). font-family: 'Trebuchet MS', system-ui. Use letter-spacing.

Colors: bg #070b14, panel #0f1624, accent amber #ffb545, teal #46e0c0, blue #5b8cff.

Canvas card: border 1px rgba, box-shadow, border-radius 22px, padding 10px? Canvas inside.

Overlay banner: position absolute bottom 18px left 50% translateX; glass pill with pulsing dot.

Also on crash show overlay centered with message and "Press R to restart".

Let's write the JS.

Constants:
const COLS=24, ROWS=13, CELL=40, W=960, H=520.
TRACK_ROWS=[2,6,10], CONN_COLS=[2,8,14,20].
build track set:
for r of TRACK_ROWS: for c=1..22 add.
for c of CONN_COLS: for r=2..10 add.

Note row6 col? fine.

Platform candidate cells: rows [3,5,7,9], columns 1..22 excluding conn cols, and approach cell is track:
- row3 → approach row2 same col (track yes)
- row5 → approach row6
- row7 → approach row6
- row9 → approach row10
But row3 col where vertical connector? excluded. However approach for row3 is row2 always track. Good.
But also platform row5 cell adjacent to track row6 except cols where connector rows pass? conn cols excluded.

Also careful: platform cell row3 col2 excluded because connector passes through (r3,c2 is track). Yes excluded conn cols.

Also platform cells might overlap with station (row5 cols 9..14) - station handled separately, exclude those from passenger spawn but draw differently.

Passenger colors palette: #ff7a6b, #ffd166, #6ee7b7, #60a5fa, #c084fc, #f9a8d4, #facc15.

Passenger object: {r,c, approach:{r,c}, color, phase, name, id}.
Names: "Mia","Theo","Ada","Ivy","Leo","Zoe","Finn","Ruby","Otto","Nora".

Waiting spawn positions: choose from candidate list. Initial set of specific columns. Let's define platformPositions arrays:
row3: [4,6,10,12,16,18]
row5: [3,5,17,19] (excluding station 9..14)
row7: [3,6,11,15,18,21]
row9: [4,7,11,15,17,21]
Total 20 candidates. Spawn 7 initially random distinct. Respawn to keep 6 waiting.

Actually approach row for row3 is (2,c); but the train travels on row2 and platform row3 is below it. Good.

Station: platform row5, cols 9..14, approach cell (6,11)? Let's define station platform cells row5 cols 9..14, station track cell (6,12). Draw sign.

Render station: building/canopy on row5 cells: rounded dark roof spanning cols 9..14 (x 360..600), with amber lit windows; text "CENTRAL" small. Plus a glowing arch on track cell (6,12) maybe a sign post with "STOP" marker, drawn as amber circle ring.

Now the AI needs to reach station cell (6,12).

Draw order:
1. ground gradient + speckles + grid faint
2. ballast/track
3. platforms tiles, benches, lamps, station
4. passengers (waiting)
5. effects
6. train (segments from tail to head)
7. headlight glow overlay
8. labels / objective marker

Grid: 24*40=960 width, 13*40=520 height. Good.

Rendering platform tiles: for each platform cell (non-conn col in rows 3,5,7,9) draw tile. That's many tiles: rows 3,5,7,9 x 21 cols ~84 tiles. Fine.

Edge safety line: on the side adjacent to track: row3 adjacent above → line at top of tile; row5 adjacent below → line at bottom; row7 adjacent below (row6 above) → wait row7 adjacent to row6 which is above → line at top; row9 adjacent to row10 below → line at bottom.

Let's define platform row adjacency: row3: track row2 above → yellow line top. row5: track row6 below → line bottom. row7: track row6 above → line top. row9: track row10 below → line bottom.

Benches: at certain cells draw bench (brown slats). Lamps: at some cells, a lamp post with glow.

Passengers drawn standing at platform tile center, bobbing.

Track details: ballast band with rounded ends? Let's do:
horizontal band: fillStyle '#232c36'; rect x=40 (col1) to 920 (col22 end = 23*40=920), y = r*40+4, h=32. Sleepers: every 14px, rect 6x30 color #3a3129. Rails: two lines y+10 and y+22, color gradient #9fb0bd, with dark shadow below.
Vertical bands: x = c*40+4, w=32, from y=80 (row2) to 440 (row10 end=440). sleepers horizontal.

Since vertical bands drawn after horizontal, at junctions the vertical ballast covers horizontal rails → looks like crossing. Acceptable; better: draw all ballast first (both), then all ties, then all rails, so rails cross visibly. Let's do that:
- Pass A: horizontal ballast rects, vertical ballast rects.
- Pass B: ties horizontal & vertical.
- Pass C: rails horizontal & vertical (lines with slight gradient).

Rails at horizontal y = r*40+10 and +22 within band (band y+4..+36). Vertical rails at x = c*40+10 and +22. Band x = c*40+4..+36. Good.

Train drawing details, orient by angle:
function drawLocomotive(x,y,angle):
 ctx.save(); translate; rotate(angle);
 // shadow
 // cowcatcher at front
 ctx.fillStyle '#1c2531'; rounded rect etc.
 Boiler: rounded rect from -14 to 12, height 22, color '#e2564d' maybe main body color. Actually make locomotive body teal-blue with amber trim: body '#2b5fa8'? Let's do rich: main '#3f7fd6'? Hmm colorful wagons; locomotive crimson '#d94f45' with dark blue boiler? Let's design: body dark navy #22314d, boiler crimson #c9483f? I'll do:
 - chassis dark #1a2230 rounded rect -17..15, -13..13
 - boiler: rounded rect -6..14, -10..10 color '#d4554a' (red)
 - cab: rounded rect -16..-4, -11..11 color '#2e5f9e' with window '#bfe9ff'
 - chimney: small rect at 12..18 top? top-down chimney circle at (10,-?) Let's place chimney circle at front center (13,0) dark.
 - headlight: at front (16,0) bright circle + glow.
 - wheels: small dark circles at (-10,±13),(0,±13),(10,±13)
 - coupling bar at -18.
Also a little steam puff particles from chimney when moving — nice.

Wagons: rounded rect length 28 width 22, color varies; interior seats; passengers: draw small circles at seat positions with shirt color. Wagon chassis dark underneath.

Passengers seated: each wagon up to 2, drawn as small head circle + shoulders.

Pop passenger info: floating text "+10 Emma" etc.

Effects: array of {x,y,text,color,t,dur} rendered with alpha.

Also "steam" particles from chimney: array {x,y,vx,vy,life}.

Now state machine:

state = {mode:'demo'|'manual', running:true, crashed:false, score:0, best:0, aboard:0...}

Main loop rAF:
- accumulate dt; if running and not crashed: acc += dt; if acc > stepMs: step(); acc -= stepMs.
- progress p = min(1, acc/stepMs) used for interpolation... Actually progress should be acc/stepMs for current move; after step we reset acc to 0? Let's do: when acc >= step, we call step() which shifts body, then acc -= stepMs; but then interpolation uses acc/stepMs which is small → head jumps to new cell immediately. Better: separate: maintain moveT timer.

Approach: on each rAF, moveTimer += dt. When moveTimer >= STEP: moveTimer -= STEP; doStep(). For rendering, the current head position should interpolate from previous head cell to new head cell over the STEP duration. Use a "prevCells" snapshot? Let's store body as array of cells and a flag `hasPrev`.

Simplify: store path P array (cells), and render with t = moveTimer/STEP where P[0] is the *new* head already set. Then at t=0 head renders at P[1] (old head) and moves to P[0]. Yes, as derived: head = lerp(P[1], P[0], t). This works because P[0] set at doStep and moveTimer reset near 0. But when doStep occurs, moveTimer may be 0.05*STEP → t small → head near old position, continuous.

Body i position = lerp(P[i+1], P[i], t).

Orientation: for head, direction from P[1] to P[0]. For body i, direction from P[i+1] to P[i].

doStep():
 - compute next cell from direction (demo: AI decides).
 - check track & collision.
 - if invalid in manual: don't move (keep dir) but maybe nudge? If the requested dir invalid (off-track) just ignore; keep going.
 - collision with body (excluding tail): crash.
 - shift: P.unshift(next).
 - handle pickups: check passengers whose approach == next cell → board.
 - handle station arrival if next == station cell → deliver.

Board: find wagon index with space; wagon.passengers.push({color,name}); state.aboard++. score += 10; effect text.

If no wagon space (aboard == 6) then passenger stays waiting? Since we only board when space. If aboard==6 and head reaches approach, keep waiting.

Deliver at station: for each passenger in wagons, score += 20; effects; clear wagons; aboard=0; "Delivered!" toast.

Respawn: keep waiting count >= 5: after pickup, spawn new after a delay.

AI decision:
function aiNextDir():
  targets = [];
  if (aboard >= 4 || waiting.length===0) targets = [stationCell];
  else targets = waiting.map(p=>p.approach) with passenger object.
  For each target compute BFS path from head avoiding body (except tail), choose nearest by path length.
  If none reachable, fallback: BFS to station ignoring body; else continue current direction if valid.

BFS: use queue with Map key. Track set size ~ 24*3*... fine.

Implementation detail: BFS from head where start = head cell. Neighbors not in body (body = P[0..3] but head P[0] is current head; body cells are P[1],P[2],P[3] since P[0] is head). Note after doStep, P[0] is head and P[1..] are body. During rendering between steps, head is between P[0] and P[1] visually; but logically head is P[0]. AI computed at doStep time after shifting? Let's compute AI decision before shifting, using current head P[0] and body P[1..]. Then doStep moves. Let's restructure:

doStep():
  const next = computeNextCell(); // uses current P
  ...
  P.unshift(next)

computeNextCell for demo: BFS start = P[0], blocked = P.slice(1) (excluding tail P[3] allowed since it moves). Path next = path[1].
Manual: dir from pending input; next = head + dir; check.

For manual, direction input: queue or single pendingDir. If pendingDir is opposite of current dir, ignore. Keep dir; if next invalid (off track) → ignore the turn (stay dir) or allow turning but then next step off track → ignore? Better: accept only turns that lead to a track cell; validate on key press: check that head+dir is track and not body. That prevents derailing and keeps the game friendly. But then snake never crashes except into body. That's fine; crash possible by steering into own body.

Hmm, if player drives into body → crash and show overlay.

Also prevent driving into a platform (not track) — automatically.

Manual player needs guidance: objective marker: draw a pulsing arrow/chevron above the current target (nearest passenger or station) and a dotted line? Dotted path line along track from head to target might be helpful and cool. Let's draw subtle glowing path dots along BFS path to target in manual mode, and in demo maybe faint. Let's draw only the objective marker (a floating diamond above target cell) and the station beacon. Keep it clean.

UI HUD: Score (0), On board (0/6), Waiting (7), Best (0). Also "Speed" maybe. Let's do 4 cards: Score, On board, Waiting, Best run.

Also a "distance" maybe skip.

Toast area at top-left of canvas? Use canvas-drawn text effects instead.

Mode badge in header: "● AUTOPILOT DEMO" pulsing green; after manual "● MANUAL CONTROL".

Buttons: "Autopilot" (toggle back to demo), "Restart" (R).

Banner content: "Click or press a key to take the controls" with subtext "Autopilot is on — the train is already running".

Also ensure clicking on the canvas switches to manual.

Keyboard: arrow keys, WASD, space pause, R restart. Space might scroll page — preventDefault.

Let's write CSS with responsive: at max-width 700, header stacks, HUD 2x2 grid, canvas scales (canvas CSS width 100%, height auto). The canvas logical 960x520, at 375px width → 375x203, small but OK. Text drawn in canvas might get tiny; acceptable. Maybe reduce cell count on mobile? Not necessary.

Add footer controls chips.

Now write code fully. Watch for errors.

Detailed drawing functions:

function roundedRect(ctx,x,y,w,h,r){...}

Draw ground:
ctx.fillStyle gradient from '#101826' to '#0a1018'. fillRect.
Add subtle noise speckles: precompute array of {x,y,r,a} seeded random 200 dots, color '#1c2a33'.
Also faint grid lines every CELL with 'rgba(255,255,255,0.03)'.

Track drawing:
const hStarts=1, hEnds=22.

function drawTrack(){
  // ballast
  ctx.fillStyle='#232b33';
  for rows: roundRect(40, r*40+5, 21*40, 30, 8)
  for cols: roundRect(c*40+5, 80, 30, 360, 8)
  // ties
  ctx.fillStyle='#3b3128';
  horizontal ties: x from 44 step 16: rect(x, r*40+7, 7, 26)
  vertical ties: y from 84 step 16: rect(c*40+7, y, 26, 7)
  // rails
  for rows: lines at y=r*40+12 and +24, stroke '#93a6b5' width 3, plus highlight '#d8e6ef' width 1 offset.
  for cols: lines at x=c*40+12,+24.
}

Hmm band for horizontal row: y from r*40+5 to +35, rails at +12,+24 ok.

Vertical band: x c*40+5..+35, from y=80 to 440.

Platform tiles:
platformRows = [3,5,7,9]. For each c 1..22 excluding connCols: tile at (c*40+2, r*40+3, 36, 34) color '#39414d' with border '#4d5766'; add paving lines. Safety line side per row.

But wait: row5 cols 9..14 = station: draw differently (canopy).

Also note the connector columns rows 3..9 are track, so platform tiles excluded there — good.

Also platform row3 at col? For row3, track connectors pass at cols 2,8,14,20 excluded. Good.

Benches: choose cells like (3,6),(5,4),(7,16),(9,18): draw bench (two brown slats + legs).
Lamps: cells (3,10),(5,17),(7,5),(9,11): small circle glow.

Station drawing:
Station platform cells row5 cols 9..14 → draw a canopy structure: rounded rect x=9*40+2=362, y=5*40+2=202, w=6*40-4=236, h=36, color '#26303e' with amber lamps and sign text "CENTRAL STATION" centered in tiny letters (font 9px bold, letter spacing not available easily; just fillText). Then a sign post at station track cell (6,12)? Draw a marker ring on track: amber circle radius 13 at cell (6,12) with dashed rotating ring, plus a small "DEPOT" flag. Actually station approach should be clearly the place to deliver. Draw a golden arch/portal on track cell: two posts at sides, glowing amber. I'll draw a circular "H" style marker.

Passengers: drawPerson(x,y,color,phase,wave): shadow ellipse, body rounded rect, head circle, hair, arm waving.

Waiting passengers also have a speech bubble with name? Too much. Maybe small colored suitcase next to them.

Train: segments drawn from tail to head so head on top.

Let me define positions array:
pos[0] = head pos; pos[i] = body i (wagon i for i=1..3).
dir angles: head dir from P[1]→P[0]; wag dir from P[i+1]→P[i].

P[4] exists for wag3 orientation (P[4] needed). Body length 4 total: P[0..3] (head + 3). For wagon index 3 (last), direction from P[4] to P[3]; P[4] may be undefined initially. Ensure P length >= 5 by initializing with extra trailing cells. Initial: head (2,4)? Let's set P = [(2,4),(2,3),(2,2),(2,1),(2,0)]? But (2,0) not track (cols 1..22). Let's use head at (2,5) with trailing (2,4),(2,3),(2,2),(2,1). P[4]=(2,1) exists. Good: P indices 0..4 length 5. dir right.

Wait initial path: head (2,5), then (2,4) etc. Fine.

Wagon colors: ['#f2b134' amber, '#3ec9a7' teal, '#a78bfa' violet].

Wagon passengers seated positions: two seat slots at local x = -6 and +6, y=0 (top-down seats along center?). In top-down open wagon, seats in two rows: (x=-7,y=-5),(x=-7,y=5)? Let's do 2 seats at x=-7 and x=5, y=0 with body circle. Hmm top-down open wagon: draw interior rectangle, then passenger heads as circles with shoulders. Use 2 slots: (-6,0) and (6,0).

Actually with wagon length 30, seats at x=-7,+7.

If wagon has 1 passenger, place in first slot.

Locomotive orientation angle: dir vectors: right (1,0) → angle 0; down → PI/2; left → PI; up → -PI/2.

Draw headlight cone/glow: radial gradient at front.

Steam: spawn from chimney world position each ~0.12s when running.

Effects: floating text.

Now write code. Also add subtle "moon"/sky? Top-down no.

Edge vignette: radial gradient overlay.

Let me also implement crash: if manual and next in body → crash: mode stays manual but running=false; overlay "Off the rails!" with "Press R or click Restart". After crash, we can auto-reset after 2s? Let's show overlay with button.

Overlay DOM: .overlay centered with h2 and p and button "Restart run". Show on crash.

Demo mode never crashes hopefully. AI path avoids body; but there is a risk: the AI path may lead to head entering the cell that currently holds the tail? Allowed. Fine.

However AI could get stuck if the target is unreachable due to body. Our BFS ignoring body fallback: find any path to target ignoring body → but might crash later. Let's do two-tier: BFS avoiding body; if none, BFS allowing all cells; then check the chosen next cell is not a body cell (excluding tail) → if it is, try another neighbor direction that's free. Fallback: continue current direction if free.

Let me implement AI more robustly:

function findPath(targetSet, blockedSet):
 BFS over track cells from head; return map of predecessor.
For choosing target: for each waiting passenger, compute BFS dist (avoid body) and pick min. Then path array reconstruct.

To avoid recomputing BFS many times per step, it's fine (small graph, once per 165ms).

Let's compute distance map once from head with blocked=body(minus tail), then pick nearest target cell in that map. Then reconstruct path via prev map. If target unreachable in blocked map, compute another BFS without blocking and pick nearest target; reconstruct; then check first step isn't in body (minus tail) — if it is, try alternative neighbors.

Simpler: compute dist map (blocked). Targets sorted; pick reachable. If none reachable, use unblocked BFS; get path; then validate first step. Good enough.

Edge case: the head might be standing ON the target cell (passenger approach) but passenger not boarded? That happens only if boarding failed due to full wagons; then target = station anyway since aboard>=4? If aboard==6 and passenger remains, AI targets station (aboard>=4). Good.

Station delivery trigger: when head enters station cell (6,12). Note that station cell is a normal track cell; when train passes over it, delivery occurs.

The AI must target station cell exactly; BFS dist map includes it.

Also after delivering, wagons empty, AI resumes picking passengers.

Let's write spawn logic: keep count of waiting; target 6. spawnPassenger() picks random candidate not occupied and not station area.

Initial spawn: choose from candidates randomly 6.

Now let's ensure passengers appear visible within first 3 seconds: yes initial 6 waiting.

Also show a small "boarding" animation: when pickup, animate the passenger flying to the wagon? Could add a particle that travels; skip, use burst + text.

Let me write the HTML now.

Also must avoid external resources. Use inline SVG for logo.

Accessibility: canvas aria-label.

Let me write final code.

CSS specifics:

:root{--bg:#070b12;--panel:#0e1622;--line:rgba(255,255,255,.08);--amber:#ffb545;--teal:#4be3c1;--text:#e8eef7;--muted:#8fa0b5}
body{margin:0;background:radial-gradient(1200px 700px at 20% -10%, #16233b 0%, #070b12 60%);color:var(--text);font-family:...;min-height:100vh;display:flex;justify-content:center}
.app{width:min(1120px,100%);padding:18px 20px 22px}
.topbar{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:14px}
.brand{display:flex;align-items:center;gap:12px}
.logo{width:46px;height:46px;border-radius:14px;background:linear-gradient(145deg,#ffcf6b,#e8663d);display:grid;place-items:center;box-shadow:0 8px 20px rgba(232,102,61,.35)}
h1{font-size:22px;margin:0;letter-spacing:.3px}
.subtitle{font-size:12.5px;color:var(--muted)}
.hud{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:12px}
.stat{background:linear-gradient(180deg,rgba(255,255,255,.055),rgba(255,255,255,.02));border:1px solid var(--line);border-radius:14px;padding:9px 14px}
.stat .label{font-size:10.5px;letter-spacing:1.4px;text-transform:uppercase;color:var(--muted)}
.stat .value{font-size:22px;font-weight:700;font-variant-numeric:tabular-nums}
.stage{position:relative;border-radius:20px;border:1px solid var(--line);background:#0a1018;padding:8px;box-shadow:0 24px 60px rgba(0,0,0,.45)}
canvas{display:block;width:100%;height:auto;border-radius:14px;background:#0a1018}
.banner{position:absolute;left:50%;bottom:22px;transform:translateX(-50%);background:rgba(10,16,26,.86);border:1px solid rgba(255,255,255,.14);backdrop-filter:blur(8px);padding:10px 18px;border-radius:999px;display:flex;gap:10px;align-items:center;font-size:13px;white-space:nowrap}
@media small: font-size 11px; padding 8px 12px; white-space normal; text-align:center
kbd{background:#1b2434;border:1px solid #33405a;border-bottom-width:2px;border-radius:6px;padding:2px 6px;font-size:11px}
.overlay{position:absolute;inset:8px;display:none;place-items:center;background:rgba(5,9,16,.72);border-radius:14px;backdrop-filter:blur(3px)}
.overlay.show{display:grid}
.controls{display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-top:12px;color:var(--muted);font-size:12px}
.badge{...}

Media query max-width 640: .hud{grid-template-columns:repeat(2,1fr)} .app{padding:12px} h1 font 18px.

Now the JS code. Let me write it fully and carefully.

I'll write:

const canvas=document.getElementById('game'); const ctx=canvas.getContext('2d');
function fit(){const dpr=Math.min(window.devicePixelRatio||1,2);canvas.width=W*dpr;canvas.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);}
fit(); window.addEventListener('resize',fit);

State variables as described.

Random seeded? Use Math.random fine.

Let me code the drawing of people:

function drawPerson(x,y,color,phase,wave){
 ctx.save(); ctx.translate(x,y);
 // shadow
 ctx.fillStyle='rgba(0,0,0,.3)'; ctx.beginPath(); ctx.ellipse(0,10,8,3.5,0,0,7); ctx.fill();
 const bob=Math.sin(phase)*1.2;
 // body
 ctx.fillStyle=color; rr(-6,-4+bob,12,14,4); fill
 // head
 ctx.fillStyle='#f3c9a4'; circle(0,-9+bob,5.2)
 // hair
 ctx.fillStyle='#2a2028'; arc(0,-11+bob,5.2,PI,0) fill
 // waving arm
 ctx.strokeStyle=color; lineWidth=3; beginPath; if wave: moveTo(4,-1+bob) lineTo(9,-7+bob+Math.sin(phase*2)*2) else lineTo(7,3+bob); stroke
 ctx.restore();
}

Seated passenger in wagon: small: head circle r3.5 skin, body arc in shirt color.

Colors for waiting: shirt color, hair color variety.

Now the train render:

function drawTrain(){
  const positions = computePositions();
  // draw wagons from last to first
  for(let i=3;i>=1;i--) drawWagon(positions[i], wagonAngle(i), i-1);
  drawLoco(positions[0], headAngle());
}

computePositions:
const t=Math.min(1,moveTimer/STEP);
positions[0] = lerpCell(P[1],P[0],t)
for i=1..3: positions[i]=lerpCell(P[i+1],P[i],t)

Angles: angle from cell difference: for head: P[0]-P[1]; for wagon i: P[i]-P[i+1].

Note P[i] coordinates: P[i].r, P[i].c.

drawLoco(x,y,ang):
ctx.save();translate(x,y);rotate(ang);
 // shadow
 ctx.fillStyle='rgba(0,0,0,.35)'; rr(-18,-12,36,24,6); fill
 // chassis
 ctx.fillStyle='#161d28'; rr(-17,-13,34,26,6); fill
 // boiler
 ctx.fillStyle='#c9483f'; rr(-4,-10,20,20,7); fill
 // boiler band
 ctx.fillStyle='#f7d488'; rr(2,-10,3,20,1); fill
 // cab
 ctx.fillStyle='#2f5f9e'; rr(-16,-11,11,22,4); fill
 // window
 ctx.fillStyle='#bfe7ff'; rr(-13,-7,6,14,2); fill
 // chimney
 ctx.fillStyle='#20262f'; circle(12,0,5); fill; inner '#39414d' circle(12,0,3)
 // headlight
 glow: radial gradient at (18,0) r 22 with amber; then circle(17,0,3.5) '#fff3c4'
 // wheels
 ctx.fillStyle='#0e131b'; circles at (-11,±13),(0,±13),(9,±13) r3.5 with rim
 // front buffer/cowcatcher
 ctx.fillStyle='#3a4353'; rr(14,-9,4,18,2)
 ctx.restore();

Hmm chimney at front overlapping headlight at same x; move chimney to (10,-?) For top-down, chimney is a circle on boiler front center (12,0) and headlight could be at front top (16,-4)? Let's place headlight at (17,0) and chimney at (10,-6)? Let's just: chimney circle at (8,0) on boiler, headlight at (17,0).

drawWagon(x,y,ang,idx):
color=WAGON_COLORS[idx]
ctx.save();translate;rotate
 shadow, chassis rr(-16,-12,32,24,5) '#141b26'
 body rr(-14,-10,28,20,4) color
 interior rr(-12,-8,24,16,3) darker shade of color (use rgba black .18)
 seats: draw seat cushions
 passengers: slots at x=-7 and 7
 // coupling at back -16
ctx.restore

Wagon colors bright: '#f5a524','#22b8a6','#8b5cf6'.

Wagon passengers seated: slot positions [-7,7]; if 1 passenger use -7.

Also draw small lantern on wagon front.

Now effects render: text with alpha and slight rise.

Steam particles: circles white alpha.

Now the pickup/destination logic in code.

const passengers=[]; each {id,r,c,ar,ac,color,hair,name,phase,boarding?}

When board: remove from passengers array; find wagon with <2 passengers; push {color,name}; state.aboard++; score+=10; addEffect at cell center of approach: '+10 '+name? maybe just '+10'.

Add a "combo" if pickup quickly: skip.

When station arrival: if state.aboard>0: bonus = 20*aboard; score += bonus; effect 'DELIVERED +'+bonus; wagons cleared; aboard=0.

Wait station arrival cell (6,12): each time head enters, deliver.

HUD update function updates DOM text.

"Waiting" count = passengers.length.

Best: max score achieved; update best when score changes.

Mode functions: setMode('manual'|'demo'). Banner show/hide.

Key handling: window.addEventListener('keydown', e=>{ if keys include, e.preventDefault(); if(e.repeat) return for direction? allow repeat maybe; if mode demo → setMode manual; set direction.

Direction queue: pending array; on key, validate against current direction chain.

Let me store dir vector for head = P[0]-P[1]. For manual input, we set nextDir if not opposite of current effective dir and if target cell is track.

For simplicity: single nextDir variable. When a key is pressed: candidate = vec; currentDir = dirVector(); if opposite(candidate, currentDir) ignore; else if track has head+candidate and not body (except tail) → nextDir=candidate. In computeNext for manual: if nextDir valid use it else current dir.

Actually we should allow the queued turn even if the cell is body? No, avoid.

The AI demo: compute next each step.

Let's also handle: after user takes control mid-move, fine.

Now: "Click or press a key to start" message: we show banner. Clicking canvas → manual. Also clicking header buttons shouldn't count (they're outside canvas, but banner says click... maybe click anywhere). I'll attach click on canvas only and banner says "Click the map or press a key".

Buttons: Autopilot button toggles back to demo (banner stays hidden, badge shows AUTOPILOT). Restart button resets game (score 0, train reset, passengers respawn).

Overlay crash: show with restart button.

Let me also add a pause on 'P' or Space: toggles paused, show banner "Paused — press Space to resume". Use banner for that too.

Now write the final HTML. Let's be thorough with code.

Potential bug: BFS target at passenger approach where the passenger is standing on the platform row3 and approach (2,c). The train travelling along row2 passes the platform cell. Good.

Let me define platform candidates with approach mapping:
for row3: ac row2; row5: ac row6; row7: ac row6; row9: ac row10.
Wait row7 is below row6: approach row6. Yes.
Station platform row5 cols 9..14 → but approach row6.

Candidates excluding station columns in row5 (9..14). Also exclude station approach column? Station track cell is (6,12); passengers at row5 col 12 excluded since station area. Row7 col 11,15 etc fine.

Now, does a waiting passenger's approach cell coincide with the station cell (6,12)? Row7 col12 not in candidates. Good.

Initial passengers: pick 6 random candidates but ensure spread across map. Use a shuffled candidate list, take first 6.

Respawn: when passengers.length < 6, schedule spawn every ~1.5s: use setTimeout? Use a timer in loop: spawnCooldown. Let's just spawn immediately when below 6 after a small delay tracked in loop.

To make it look natural, initial 6.

Now AI path to passenger: the approach cell is on a track row; the train must reach that exact cell. BFS finds it.

One concern: The AI might approach a passenger from a direction and pass through, collecting. Good.

Another: AI target selection may cause it to reverse into body because the shortest path goes along the corridor where its body lies. BFS avoiding body cells handles that.

But careful: BFS starts at head; the neighbors include P[1] (neck) which is blocked; good.

Potential deadlock: The head is in a corridor with the body behind in the same corridor (row2) and the target is ahead → fine.

Another deadlock: body occupies the only path forward (e.g., in a vertical connector). The graph has loops so usually alternatives exist.

Let me make the network more connected: add horizontal short branch rows 4 and 8? That would make a richer network. Let's add extra track rows: row4 spanning cols 2..20? That would cross platform rows 3,5 making the network grid-like. Might look like a full grid which is fine for a rail yard. But platforms become fragmented. Let's keep 3 rows + 4 connectors. The loops: rows 2,6,10 with connectors at 2,8,14,20 gives a grid of 3x4 nodes — plenty of loops.

Body occupies 3 cells; with such a network, deadlock rare.

But the AI path may take a weird route; still fine visually.

Hmm: the graph nodes: row2 cols1-22, row6 cols1-22, row10 cols1-22, vertical at cols 2,8,14,20 rows2-10. Between row2 and row6 there's a long vertical segment; passengers on row3/row5 platforms are adjacent to rows2/6. Good.

The initial train head at (2,5) moving right toward (2,22)? Distance 17 cells → 17*165ms = 2.8s before reaching the right end where it can turn down at col20 or 22? Connectors at col20, so at (2,20) it can go down. AI will pick nearest passenger approach, which might be at row2 col4 (behind!) — but that's blocked by body; BFS will route around. Good.

To make the demo visually interesting early, maybe put initial head at (2,3) and have passengers ahead. Let's set initial head (2,4) with body trailing (2,3),(2,2),(2,1) and extra (2,0)? (2,0) not track. Path array P needs length 5; use P[4]=(2,1) and P[3]=(2,2), P[2]=(2,3), P[1]=(2,4)? Wait order: P[0]=head. Let's set P = [ {r:2,c:5},{r:2,c:4},{r:2,c:3},{r:2,c:2},{r:2,c:1} ]. Then P[4]=(2,1) exists for orientation. Good, head at (2,5).

Initial passengers: ensure at least a couple along row2 to the right, e.g. at row3 col6, col10, col12, plus row7 col11, row9 col15, row5 col17. Let's hand-pick initial 6: (3,6),(3,10),(5,17),(7,11),(7,18),(9,15). approach for (3,6) is (2,6) — just 1 cell ahead of head at (2,5)! Great, immediate pickup on first step. Nice for screenshot? At 3s, ~18 steps, many pickups.

Good.

Now the deliver cycle: after 4 aboard, AI goes to station (6,12). Then delivers.

Let's write the code.

One more detail: "Pickup" should look like the passenger boarding: add a little effect of a person jumping? Add effect text "BOARD +10" and a burst of colored particles.

Let me write particle system: effects = [{type:'text'|'burst', ...}].

Simplify: effects array with {x,y,text,color,t}. And particles array for confetti on delivery.

I'll implement both quickly.

Let me now write everything.

Also `best` persists in memory (no storage). Fine.

Write drawOrder each frame: ~60fps with many draw ops; fine.

Let me write the code text now, carefully.

```js
const COLS=24, ROWS=13, CELL=40, W=COLS*CELL, H=ROWS*CELL;
const TRACK_ROWS=[2,6,10];
const CONN_COLS=[2,8,14,20];
const track=new Set();
const key=(r,c)=>r+','+c;
TRACK_ROWS.forEach(r=>{for(let c=1;c<=22;c++)track.add(key(r,c));});
CONN_COLS.forEach(c=>{for(let r=2;r<=10;r++)track.add(key(r,c));});
```
Note overlap fine.

Platform cells set:
```js
const platformCells=[]; // {r,c}
[3,5,7,9].forEach(r=>{for(let c=1;c<=22;c++){ if(CONN_COLS.includes(c))continue; platformCells.push({r,c}); }});
```
But row5 cols 9..14 are station cells; exclude from normal platform drawing and from passengers.

STATION={r:6,c:12}; stationPlatform row5 cols 9..14.

Wait: station platform cells in row5 at cols 9..14 — but connector at col14 crosses row5! Col14 is a connector column (rows2-10), so (5,14) is track, not platform. Station platform should then be row5 cols 9..13 (excluding 14). Let's set station platform cols 9..13 and canopy spans that. And the approach/delivery cell (6,12) is on row6 track. Fine.

Also row5 col8 connector. So station platform cols 9..13 contiguous. Good.

Candidates list (platform cells excluding station):
row3: cols 1,3,4,5,6,7,9,10,11,12,13,15,16,17,18,19,21,22
row5: cols 1,3,4,5,6,7,15,16,17,18,19,21,22
row7: same as row3
row9: same as row3

I'll generate programmatically: for each platform row, cols 1..22 excluding conn cols and excluding station cols for row5.

Now let's code.

For the AI target list, waiting passengers with approach cells.

BFS function:

```js
function bfs(start, blocked){
  const dist=new Map(), prev=new Map(), q=[start];
  dist.set(key(start.r,start.c),0);
  while(q.length){
    const cur=q.shift();
    const d=dist.get(key(cur.r,cur.c));
    for(const nb of neighbors(cur)){
      const k=key(nb.r,nb.c);
      if(dist.has(k))continue;
      if(blocked && blocked.has(k)) continue;
      dist.set(k,d+1); prev.set(k,cur); q.push(nb);
    }
  }
  return {dist,prev};
}
```
Neighbors of a track cell: 4-dir neighbors that are in track set. But for vertical connectors the horizontal row cells adjacent to connector at row3? (3,2) is track and (3,1) is not track (row3 only exists at connector cols). fine.

reconstruct(prev, target) → array from target back to start; reverse.

AI:
```js
function aiNextCell(){
  const head=P[0];
  const blocked=new Set(P.slice(1,4).map(c=>key(c.r,c.c))); // body cells except tail? P.slice(1,4) includes neck..P[3]=tail. Allow tail: exclude P[3].
```
Body cells: P[1],P[2],P[3]; tail P[3] may be entered legally. So blocked = P[1],P[2].

```js
  const blocked=new Set([key(P[1].r,P[1].c),key(P[2].r,P[2].c)]);
  let targets=[];
  const needDeliver = state.aboard>=4 || waiting.length===0;
  if(needDeliver) targets=[{cell:STATION,type:'station'}];
  else targets = passengers.map(p=>({cell:{r:p.ar,c:p.ac},type:'passenger',p}));
  let {dist,prev}=bfs(head,blocked);
  let best=null,bestD=Infinity;
  for(const t of targets){ const d=dist.get(key(t.cell.r,t.cell.c)); if(d!==undefined && d<bestD){bestD=d;best=t;} }
  if(!best){ ({dist,prev}=bfs(head,null)); for(...same...) }
  if(!best) return null;
  const path=reconstruct(prev,best.cell);
  // path[0]=head, path[1]=next
  let next=path[1];
  if(!next) return null;
  // safety: ensure next not blocked (except tail)
  if(blocked.has(key(next.r,next.c))) { try alternative neighbors free }
  return next;
}
```
Note: with blocked excluding tail, path might go into tail cell then beyond—legal.

Also for path reconstruction when target==head, path length1 → no next. Then pick another target; handle: if bestD===0 skip that target and pick next. I'll filter d>0 when choosing best, but if all are 0 (e.g., passenger at current approach but couldn't board because full) then needDeliver handles.

Hmm scenario: aboard<4 but passenger's approach is the current head cell — that means we arrived there but didn't board (wagon full). But wagon full implies aboard=6 ≥4 → needDeliver. Fine.

Fallback if no path: keep current direction if valid & free.

Manual direction handling: pendingDir.

doStep():
```js
function step(){
  if(state.crashed) return;
  let next;
  if(mode==='demo') next=aiNextCell();
  else {
    const dir = pendingDir || dirVec();
    next={r:P[0].r+dir.r,c:P[0].c+dir.c};
    if(!track.has(key(next.r,next.c))){ pendingDir=null; next={r:P[0].r+dirVec().r,c:P[0].c+dirVec().c}; }
  }
  if(!next){ return; }
  // collision
  for(let i=1;i<P.length-?;i++) ...
```
Collision: next must not be in body cells P[1..P[3]? tail P[3] allowed if it moves away; standard snake allows moving into tail cell since tail vacates. Our body is P[1],P[2],P[3] where P[3] is tail. So collision check against P[1],P[2] only (since P[3] vacates). But visually the train's tail is at P[3]; entering it means the train's rear overlaps... In snake it's allowed because the tail moves. I'll allow P[3].

Actually wait: our body length is 4 cells: head P[0] + wagons P[1..3]. If the head moves into P[3], the wagon that was at P[3] moves to P[2]... no: after shift P becomes [next, P0, P1, P2] and we truncate to length 5. Let's maintain P length 5 always: P.unshift(next); if(P.length>5) P.pop(). Then body P[1],P[2],P[3] and P[4] used only for orientation. Hmm P[4] is the position vacated by the tail — it's part of the path history for smooth orientation. So P length 5: [head, b1, b2, b3, old-tail]. The tail is P[3]; entering P[3] is legal (it moves to P[2]?? no).

Let's think: positions render: head lerp(P1,P0); wagon1 lerp(P2,P1); wagon2 lerp(P3,P2); wagon3 lerp(P4,P3). So the actual occupied cells logically: head P0, w1 P1, w2 P2, w3 P3. When we unshift next: new P0=next, P1=oldP0 (head), P2=oldP1(w1), P3=oldP2(w2), P4=oldP3(w3). So new w3 cell = oldP2, and old w3 cell (oldP3) is vacated. So head entering oldP3 (tail) is legal. Collision set = {P1,P2} currently (w1,w2 positions) — wait P3 is w3's cell = oldP2. Hmm let me recompute with the array before shift: P=[h,w1,w2,w3,extra]. occupied: h=P0,w1=P1,w2=P2,w3=P3. After shift occupied: next=P0, w1=P1(old h), w2=P2(old w1), w3=P3(old w2). So old w3 (P3) is freed. Thus collision cells are P0 (head, no), P1(w1), P2(w2); P3 (w3) is freed this step — entering it is legal in snake terms? In classic snake, moving into the tail cell is legal because tail moves out. Here the train's rear wagon moves forward too, so yes, entering P3 is legal as the rear wagon vacates it.

So blocked set for movement: {P1, P2} only. Good, matches earlier.

Render interpolation with P[4] = old w3 cell: wagon3 lerp(P4,P3): at t=0 wagon3 at P4 = old w3 cell (correct old position), t=1 at P3 = old w2 cell (correct new position). 

For collision in AI BFS blocked = {P1,P2}. But visually the body occupies P1..P3 during the move. Entering P3 is fine.

Hmm but in AI BFS, we compute at the start of a step (positions logical), so blocked {P1,P2} correct.

Also P[4] must exist: keep P length 5, initialized with 5 entries. After unshift+pop, length stays 5. Good.

Reset: P=[{2,5},{2,4},{2,3},{2,2},{2,1}].

Now station delivery: when next equals STATION (r6,c12) → deliver.

Pickups: when next equals a passenger's approach → board.

Also if head is at station cell but doesn't "enter" (starts there) no delivery. Fine.

Let me write render for objective marker: target = current AI target / for manual nearest passenger approach; draw a pulsing diamond above the target cell center with a small chevron. Also draw a soft line? Just marker.

For manual, target = station if aboard>=4 else nearest waiting passenger (by Manhattan/BFS). We'll compute marker target in updateHUD maybe every frame using BFS — expensive but fine at 60fps? BFS on ~80 cells is trivial. But let's compute every 10 frames.

Draw marker: small rotating diamond at (cell center x, y-14) amber with glow, plus a subtle vertical beam.

Now code the whole thing. Also on demo mode the marker shows where it's heading (nice).

Let me write the final file.

Also pause functionality: state.paused. Banner message changes.

Banner DOM update function setBanner(html) or hide.

Let's code DOM elements:
- badgeMode: span with id modeBadge
- scoreEl, aboardEl, waitingEl, bestEl
- banner with id banner and text
- overlay with id overlay, title, text, button.

I'll write updateHUD().

Time to write it all out. I'll be careful with template strings inside the HTML script (no issue).

Also: prevent default scrolling for arrow keys/space.

Let me write.

For the steam particles: spawn at chimney world pos. Need chimney world position: head position + rotated offset (8,0) → but chimney is on boiler; use offset (10,0) rotated by head angle. Use the head's rendered position.

Alright, writing final code now.

One more: the "waiting" HUD shows passengers count. "On board" shows aboard/6.

Also score popups.

Let me write drawing functions in order.

drawPlatforms:
```js
for(const cell of platformCells){
  const {r,c}=cell;
  const x=c*CELL+2,y=r*CELL+3,w=CELL-4,h=CELL-6;
  ctx.fillStyle='#2f3743'; rr(x,y,w,h,5); fill
  ctx.strokeStyle='rgba(255,255,255,.06)'; lineWidth=1; stroke
  // paving joint
  ctx.fillStyle='rgba(0,0,0,.18)'; ctx.fillRect(x+2,y+h/2-1,w-4,2);
  // safety line
  const side = (r===3||r===7)?'top':'bottom';
  ctx.fillStyle='#e8b93c';
  if(side==='top') ctx.fillRect(x+3,y+3,w-6,3); else ctx.fillRect(x+3,y+h-6,w-6,3);
}
```
Station cells handled separately (skip in loop). I'll build platformCells excluding station ones and draw station canopy separately.

Benches & lamps at chosen coordinates; ensure they aren't station cells.

drawStation():
```js
const x=9*CELL+2, y=5*CELL+2, w=5*CELL-4, h=CELL-4;
ctx.fillStyle='#252d3a'; rr(x,y,w,h,6); fill
ctx.fillStyle='#334052'; rr(x+4,y+3,w-8,h-8,4); fill
// lamps
...
ctx.fillStyle='#ffce6a'; ctx.font='bold 9px ...'; ctx.textAlign='center'; ctx.fillText('CENTRAL STATION', x+w/2, y+22);
```
Also a beacon marker on the track cell: draw dashed circle rotating at (STATION.c*CELL+CELL/2, STATION.r*CELL+CELL/2).

But careful: the track cell (6,12) overlaps row6 rails; drawing a ring on top of rails is fine (it's a stop marker).

Also draw sign "STOP" small.

Let me now write the entire file. I'll aim for correctness.

Also check the 'key' function name conflicts with nothing. Use `k(r,c)`.

Let me write.

For fonts in canvas: 'bold 11px system-ui, sans-serif'.

Final code below.

I should double check the AI won't crash when a passenger's approach requires going around a body. BFS blocked {P1,P2}. At the moment of computing, head is at P0. Suppose the head is in a corridor row2 and the body trails behind; the blocked cells are behind → path forward fine.

Edge: head in vertical connector col2 rows2-6, body behind in the connector, target elsewhere → BFS can go forward or backward (backward blocked by P1) → forward. Fine.

Risk of head hitting w1/w2: BFS avoids.

The AI's chosen next is path[1], guaranteed track.

When the BFS without blocking is used and path[1] is in blocked set, check and if blocked, pick another neighbor that is free and continues (just choose any free neighbor leading to target eventually). I'll implement: if blocked.has(next) → look through neighbors of head that are not blocked and not visited... simply choose a neighbor not blocked, preferring one whose dist in unblocked map is smaller. Let me implement using the unblocked dist map: choose neighbor minimizing dist. That's a decent fallback.

Fine.

Write now. Also add slight chance the AI wanders if no passengers and no station reachable — station always reachable.

Let me write the code in full.

Also note: when mode switches to manual, pendingDir = null.

Reset function:
```js
function reset(){
  P=[{r:2,c:5},{r:2,c:4},{r:2,c:3},{r:2,c:2},{r:2,c:1}];
  dir=RIGHT; pendingDir=null;
  state.score=0; state.aboard=0; wagons=[{passengers:[]},{...},{...}];
  passengers.length=0; initial spawn;
  effects=[]; particles=[];
  moveTimer=0; state.crashed=false; overlay hide;
  updateHUD();
}
```

Now writing.

I'll include a small tutorial strip under canvas: "Tip: collect passengers from platform edges, then stop at Central Station to drop them off."

Let's produce the final answer.