Models 44
Train snakeOne-line

GLM 5.3

by Z.aiReasoningClean
Open in new window

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

How it went

Total time
52.7 s
Cost
$0.012
Output tokens
5,986
Spent thinking
958 tokens
Speed
115.9 tok/s
Console errors
0
First token after
1.1 s
First visible text after
13.2 s
Prompt tokens
363
Code size
14.1 KB
Finish reason
stop
Served by
Decart
Model id
z-ai/glm-5.3
Reasoning sent
{"effort":"low"}
Max output
943,718 tokens
Generated
Nov 12, 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 · 349 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake Express — The Little Train</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { height:100%; }
  body {
    font-family: "Trebuchet MS", "Segoe UI", sans-serif;
    background: linear-gradient(180deg,#7ec850 0%, #5aa838 100%);
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    min-height:100vh; overflow:hidden; color:#3a2a18;
  }
  h1 {
    font-size:26px; letter-spacing:2px; color:#fff;
    text-shadow: 0 2px 0 rgba(0,0,0,.25); margin-bottom:6px; text-align:center;
  }
  h1 span { color:#ffd23e; }
  #hud {
    display:flex; gap:14px; align-items:center; margin-bottom:8px;
    background:rgba(0,0,0,.28); padding:6px 16px; border-radius:20px; color:#fff;
    font-size:14px; font-weight:bold;
  }
  #hud .pill { background:rgba(255,255,255,.15); padding:3px 12px; border-radius:12px; }
  #wrap { position:relative; line-height:0; }
  canvas {
    border-radius:14px; border:6px solid #8a5a2b; box-shadow: 0 10px 30px rgba(0,0,0,.35);
    max-width: 96vw; max-height: 70vh; width:auto; height:auto;
  }
  #overlay {
    position:absolute; inset:0; display:flex; flex-direction:column;
    align-items:center; justify-content:center; line-height:1.5;
    background:rgba(20,10,0,.45); border-radius:14px; color:#fff; text-align:center;
    transition: opacity .3s;
  }
  #overlay.hidden { opacity:0; pointer-events:none; }
  #overlay h2 { font-size:34px; letter-spacing:3px; text-shadow:0 3px 0 rgba(0,0,0,.4); }
  #overlay p { font-size:16px; margin-top:6px; opacity:.95; }
  #overlay .blink { animation: blink 1.1s infinite; margin-top:14px; font-weight:bold; color:#ffd23e; }
  @keyframes blink { 50% { opacity:.25; } }
  #hint { margin-top:10px; color:#eaffdd; font-size:13px; opacity:.85; text-align:center; }
  @media (max-width:500px){ h1{font-size:19px;} #overlay h2{font-size:24px;} }
</style>
</head>
<body>
  <h1>🚂 SNAKE <span>EXPRESS</span></h1>
  <div id="hud">
    <div class="pill">Passengers: <span id="score">0</span></div>
    <div class="pill">Wagons: <span id="wagons">0</span></div>
    <div class="pill">Best trip: <span id="best">0</span></div>
  </div>
  <div id="wrap">
    <canvas id="cv" width="960" height="600"></canvas>
    <div id="overlay">
      <h2 id="ovTitle">ALL ABOARD!</h2>
      <p id="ovText">A little train picks up passengers from the platforms.</p>
      <p class="blink" id="ovBlink">▶ Click or press any key to drive the train yourself</p>
    </div>
  </div>
  <div id="hint">Arrow keys / WASD to steer · eat passengers to grow · don't crash!</div>

<script>
(() => {
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const COLS=24, ROWS=15, CELL=40, W=COLS*CELL, H=ROWS*CELL;
const PALETTE=['#e74c3c','#f39c12','#f1c40f','#2ecc71','#3498db','#9b59b6','#1abc9c','#e91e8c'];
const OVER=document.getElementById('overlay');

let snake, dir, nextDirs, passengers, smoke, sparks, score, best=0, alive, mode; // mode: 'demo'|'play'|'over'
let stepMs=130, lastStep=0;

function cellFree(x,y, ignoreTail){
  if(x<0||y<0||x>=COLS||y>=ROWS) return false;
  for(let i=0;i<snake.length-(ignoreTail?1:0);i++)
    if(snake[i].x===x&&snake[i].y===y) return false;
  return true;
}
function spawnPassenger(){
  let x,y,tries=0;
  do{ x=(Math.random()*COLS)|0; y=(Math.random()*ROWS)|0; tries++; }
  while((!cellFree(x,y) || Math.abs(x-snake[0].x)+Math.abs(y-snake[0].y)<4) && tries<500);
  passengers.push({x, y, color:PALETTE[(Math.random()*PALETTE.length)|0],
    phase:Math.random()*6.28, born:performance.now()});
}
function reset(){
  snake=[]; const sx=6, sy=7;
  for(let i=0;i<4;i++) snake.push({x:sx-i,y:sy,px:sx-i,py:sy,color: i===0?'#333':PALETTE[i%PALETTE.length]});
  dir={x:1,y:0}; nextDirs=[]; passengers=[]; smoke=[]; sparks=[]; score=0;
  for(let i=0;i<3;i++) spawnPassenger();
  alive=true; lastStep=performance.now(); updateHUD();
}
function updateHUD(){
  document.getElementById('score').textContent=score;
  document.getElementById('wagons').textContent=snake.length-1;
  document.getElementById('best').textContent=best;
}

// ---------- AI (demo mode) ----------
function bfsDir(){
  const target = passengers[0];
  if(!target) return dir;
  // pick nearest passenger
  let tg = passengers.reduce((a,b)=>
    dist(snake[0],a)<dist(snake[0],b)?a:b);
  const key=(x,y)=>y*COLS+x;
  const prev=new Map(); const q=[snake[0]]; prev.set(key(snake[0].x,snake[0].y),null);
  const blocked=new Set();
  for(let i=0;i<snake.length-1;i++) blocked.add(key(snake[i].x,snake[i].y));
  let found=null;
  while(q.length){
    const c=q.shift();
    if(c.x===tg.x&&c.y===tg.y){ found=c; break; }
    for(const d of [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]){
      const nx=c.x+d.x, ny=c.y+d.y, k=key(nx,ny);
      if(nx<0||ny<0||nx>=COLS||ny>=ROWS||blocked.has(k)||prev.has(k)) continue;
      prev.set(k,c); q.push({x:nx,y:ny});
    }
  }
  if(found){ // walk back to first step
    let cur=found, p=prev.get(key(cur.x,cur.y));
    while(p && p!==snake[0]){ cur=p; p=prev.get(key(cur.x,cur.y)); }
    if(p===snake[0]) return {x:cur.x-snake[0].x, y:cur.y-snake[0].y};
  }
  // fallback: any safe non-reverse move
  const opts=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]
    .filter(d=>!(d.x===-dir.x&&d.y===-dir.y)&&cellFree(snake[0].x+d.x,snake[0].y+d.y,true));
  if(opts.length){ // prefer continuing straight
    const s=opts.find(d=>d.x===dir.x&&d.y===dir.y);
    return s||opts[0];
  }
  return {x:-dir.x,y:-dir.y};
}
const dist=(a,b)=>Math.abs(a.x-b.x)+Math.abs(a.y-b.y);

// ---------- Game step ----------
function step(){
  if(mode==='demo'){ const d=bfsDir(); if(d) nextDirs=[d]; }
  while(nextDirs.length){
    const d=nextDirs.shift();
    if(!(d.x===-dir.x&&d.y===-dir.y)){ dir=d; break; }
  }
  const head=snake[0];
  const nx=head.x+dir.x, ny=head.y+dir.y;
  if(!cellFree(nx,ny,false)){
    alive=false;
    if(mode==='play'){
      mode='over';
      OVER.classList.remove('hidden');
      document.getElementById('ovTitle').textContent='💥 DERAILMENT!';
      document.getElementById('ovText').textContent=`You picked up ${score} passengers on this trip.`;
      document.getElementById('ovBlink').textContent='▶ Press SPACE or click to try again';
    } else reset();
    return;
  }
  // move: shift prev positions
  for(let i=snake.length-1;i>0;i--){ snake[i].px=snake[i].x; snake[i].py=snake[i].y; snake[i].x=snake[i-1].x; snake[i].y=snake[i-1].y; }
  head.px=head.x; head.py=head.y; head.x=nx; head.y=ny;

  // smoke from chimney
  smoke.push({x:(nx+0.5)*CELL, y:(ny+0.5)*CELL, r:5, a:0.7, vx:dir.x*0.6, vy:-1.2 - Math.random()});

  // passenger pickup?
  const pi=passengers.findIndex(p=>p.x===nx&&p.y===ny);
  if(pi>=0){
    const p=passengers.splice(pi,1)[0];
    const tail=snake[snake.length-1];
    snake.push({x:tail.x,y:tail.y,px:tail.x,py:tail.y,color:p.color});
    score++; if(score>best) best=score;
    for(let i=0;i<14;i++) sparks.push({x:(nx+.5)*CELL,y:(ny+.5)*CELL,
      vx:(Math.random()-.5)*4, vy:(Math.random()-.5)*4-1, a:1, color:p.color});
    spawnPassenger(); updateHUD();
  }
}

// ---------- Drawing ----------
function rr(x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }

function drawBG(t){
  ctx.fillStyle='#d9b77e'; ctx.fillRect(0,0,W,H);
  for(let y=0;y<ROWS;y++){
    ctx.fillStyle= y%2 ? '#d4b075':'#dcbf88';
    ctx.fillRect(0,y*CELL,W,CELL);
  }
  // gravel speckles + ties
  ctx.strokeStyle='rgba(122,84,42,.18)'; ctx.lineWidth=3;
  for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
    ctx.strokeRect(x*CELL+4,y*CELL+4,CELL-8,CELL-8);
  }
  ctx.fillStyle='rgba(120,90,50,.15)';
  for(let i=0;i<90;i++){ const x=(i*137)%W, y=(i*89)%H; ctx.fillRect(x,y,3,3); }
  // rails (two horizontal lines per row to suggest track)
  ctx.strokeStyle='rgba(90,90,95,.10)'; ctx.lineWidth=5;
  for(let y=0;y<=ROWS;y++){ ctx.beginPath(); ctx.moveTo(0,y*CELL); ctx.lineTo(W,y*CELL); ctx.stroke(); }
}

function drawPassenger(p,t){
  const x=p.x*CELL, y=p.y*CELL;
  const bob=Math.sin(t/300+p.phase)*2;
  // platform
  ctx.fillStyle='#b9bec6'; rr(x+3,y+CELL-16,CELL-6,13,4); ctx.fill();
  ctx.fillStyle='#8f959e'; ctx.fillRect(x+3,y+CELL-16,CELL-6,3);
  ctx.fillStyle='#c8a15a'; ctx.fillRect(x+8,y+CELL-20,5,6); // bench leg area
  // person
  const cx=x+CELL/2, by=y+CELL-16+bob;
  ctx.fillStyle='rgba(0,0,0,.15)'; ctx.beginPath(); ctx.ellipse(cx,by,9,3,0,0,7); ctx.fill();
  ctx.fillStyle=p.color; rr(cx-7,by-18,14,18,4); ctx.fill();       // coat
  ctx.fillStyle='#f5c9a0'; ctx.beginPath(); ctx.arc(cx,by-24,7,0,7); ctx.fill(); // head
  ctx.fillStyle='#4a3521'; ctx.beginPath(); ctx.arc(cx,by-28,7,Math.PI,0); ctx.fill(); // hair
  ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(cx-2.5,by-24,1.2,0,7); ctx.arc(cx+2.5,by-24,1.2,0,7); ctx.fill();
  // waving arm
  ctx.strokeStyle=p.color; ctx.lineWidth=4; ctx.lineCap='round';
  const wave=Math.sin(t/150+p.phase)*0.6;
  ctx.beginPath(); ctx.moveTo(cx+6,by-15); ctx.lineTo(cx+11,by-22+wave*5); ctx.stroke();
  // hat
  ctx.fillStyle='#333'; ctx.fillRect(cx-8,by-31,16,3); ctx.fillRect(cx-5,by-35,10,5);
}

function angleFor(d){ return d.x===1?0 : d.x===-1?Math.PI : d.y===1?Math.PI/2 : -Math.PI/2; }

function drawTrain(t, frac){
  const N=snake.length;
  for(let i=N-1;i>=0;i--){
    const s=snake[i];
    const ix=s.px+(s.x-s.px)*frac, iy=s.py+(s.y-s.py)*frac;
    const cx=(ix+.5)*CELL, cy=(iy+.5)*CELL;
    const ang = i===0 ? angleFor(dir) : angleFor({x:snake[i-1].px-s.px+0.0001, y:snake[i-1].py-s.py});
    ctx.save(); ctx.translate(cx,cy); ctx.rotate(i===0?angleFor(dir):ang);
    if(i===0) drawLoco(t); else drawWagon(s,t,i);
    ctx.restore();
  }
}

function drawWagon(s,t,i){
  const c=CELL, w=c-8, h=c-16;
  // body
  ctx.fillStyle='#3a3a3f'; rr(-w/2-1,-h/2-1,w+2,h+2,7); ctx.fill();      // rim
  ctx.fillStyle=s.color; rr(-w/2,-h/2,w,h,6); ctx.fill();
  ctx.fillStyle='rgba(255,255,255,.28)'; rr(-w/2+3,-h/2+3,w-6,7,3); ctx.fill(); // roof highlight
  // windows
  ctx.fillStyle='#bfe6f5';
  rr(-10,-6,8,10,2); ctx.fill(); rr(3,-6,8,10,2); ctx.fill();
  // stripe
  ctx.fillStyle='rgba(0,0,0,.2)'; ctx.fillRect(-w/2, h/2-5, w, 4);
  // wheels
  const spin=t/120;
  for(const wx of [-9,9]){
    ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(wx,h/2+3,6,0,7); ctx.fill();
    ctx.strokeStyle='#888'; ctx.lineWidth=2;
    for(let k=0;k<2;k++){ const a=spin+k*Math.PI/2;
      ctx.beginPath(); ctx.moveTo(wx-Math.cos(a)*5, h/2+3-Math.sin(a)*5);
      ctx.lineTo(wx+Math.cos(a)*5, h/2+3+Math.sin(a)*5); ctx.stroke(); }
    ctx.fillStyle='#bbb'; ctx.beginPath(); ctx.arc(wx,h/2+3,1.8,0,7); ctx.fill();
  }
  // coupling
  ctx.strokeStyle='#555'; ctx.lineWidth=3;
  ctx.beginPath(); ctx.moveTo(-w/2-1,0); ctx.lineTo(-w/2-5,0); ctx.stroke();
}

function drawLoco(t){
  const c=CELL, w=c-6, h=c-14;
  // chimney
  ctx.fillStyle='#222'; rr(8,-h/2-9,9,10,2); ctx.fill();
  ctx.fillStyle='#333'; rr(6,-h/2-12,13,5,2); ctx.fill();
  // boiler
  ctx.fillStyle='#e8452c'; rr(-w/2,-h/2,w,h,7); ctx.fill();
  ctx.fillStyle='#c22f18'; rr(-w/2,h/2-7,w,7,4); ctx.fill();
  ctx.fillStyle='rgba(255,255,255,.3)'; rr(-w/2+4,-h/2+3,8,h-8,4); ctx.fill();
  // front face plate
  ctx.fillStyle='#f7d34a'; rr(w/2-8,-h/2+2,8,h-4,3); ctx.fill();
  // headlight
  ctx.fillStyle='#fff8c0'; ctx.beginPath(); ctx.arc(w/2-4,0,3.5,0,7); ctx.fill();
  // cab window
  ctx.fillStyle='#bfe6f5'; rr(-14,-8,10,12,2); ctx.fill();
  // cowcatcher
  ctx.fillStyle='#444';
  ctx.beginPath(); ctx.moveTo(w/2,-4); ctx.lineTo(w/2+7,6); ctx.lineTo(w/2,10); ctx.closePath(); ctx.fill();
  // wheels
  const spin=t/120;
  for(const [wx,r] of [[-10,5],[10,7]]){
    ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(wx,h/2+4,r,0,7); ctx.fill();
    ctx.strokeStyle='#999'; ctx.lineWidth=2;
    for(let k=0;k<2;k++){ const a=spin+k*Math.PI/2;
      ctx.beginPath(); ctx.moveTo(wx-Math.cos(a)*(r-1), h/2+4-Math.sin(a)*(r-1));
      ctx.lineTo(wx+Math.cos(a)*(r-1), h/2+4+Math.sin(a)*(r-1)); ctx.stroke(); }
    ctx.fillStyle='#ddd'; ctx.beginPath(); ctx.arc(wx,h/2+4,2,0,7); ctx.fill();
  }
  // coupling to wagons
  ctx.strokeStyle='#555'; ctx.lineWidth=3;
  ctx.beginPath(); ctx.moveTo(-w/2,0); ctx.lineTo(-w/2-5,0); ctx.stroke();
}

// ---------- Main loop ----------
function loop(t){
  requestAnimationFrame(loop);
  if(alive && t-lastStep>=stepMs){ lastStep=t; step(); }
  const frac=alive? Math.min(1,(t-lastStep)/stepMs) : 1;
  drawBG(t);
  for(const p of passengers) drawPassenger(p,t);
  // sparks
  for(let i=sparks.length-1;i>=0;i--){ const s=sparks[i];
    s.x+=s.vx; s.y+=s.vy; s.vy+=0.15; s.a-=0.04;
    if(s.a<=0){sparks.splice(i,1);continue;}
    ctx.globalAlpha=s.a; ctx.fillStyle=s.color; ctx.fillRect(s.x-2,s.y-2,4,4); ctx.globalAlpha=1;
  }
  drawTrain(t,frac);
  // smoke
  for(let i=smoke.length-1;i>=0;i--){ const s=smoke[i];
    s.x+=s.vx; s.y+=s.vy; s.r+=0.25; s.a-=0.012;
    if(s.a<=0){smoke.splice(i,1);continue;}
    ctx.globalAlpha=s.a*0.8; ctx.fillStyle='#f2f2f2';
    ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,7); ctx.fill(); ctx.globalAlpha=1;
  }
}

// ---------- Input / modes ----------
function startPlay(){
  if(mode==='play') return;
  if(mode==='over' || !alive) reset();
  mode='play';
  OVER.classList.add('hidden');
}
function setOverlayDemo(){
  mode='demo';
  OVER.classList.remove('hidden');
  document.getElementById('ovTitle').textContent='ALL ABOARD!';
  document.getElementById('ovText').textContent='A little train picks up passengers from the platforms.';
  document.getElementById('ovBlink').textContent='▶ Click or press any key to drive the train yourself';
}
window.addEventListener('keydown', e=>{
  const k=e.key.toLowerCase();
  if(mode!=='play'){
    startPlay();
    if(!['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d',' '].includes(k)) return;
  }
  if(k===' '&&mode==='over'){ reset(); mode='play'; OVER.classList.add('hidden'); return; }
  const map={arrowup:[0,-1],w:[0,-1],arrowdown:[0,1],s:[0,1],arrowleft:[-1,0],a:[-1,0],arrowright:[1,0],d:[1,0]};
  if(map[k]){ e.preventDefault(); nextDirs.push({x:map[k][0],y:map[k][1]}); }
});
cv.addEventListener('pointerdown', ()=>{ if(mode!=='play') startPlay(); else mode='play'; });

reset();
setOverlayDemo();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (958 tokens)
Train snake game. Grid-based, locomotive head with smoke, colorful wagons, passengers on platforms (stations). Start in self-playing demo mode with "Click or press a key to start" message. Arrow keys/WASD control.

Design: canvas-based, tile grid maybe 24×16, nice visuals — grass/track theme. Passengers appear as little people with colored coats standing on a platform tile. Locomotive picks them up, wagons grow, each passenger goes into a wagon with matching color? Keep simple: picking passenger adds wagon.

Self-play AI: pathfind to nearest passenger avoiding self (BFS). Draw locomotive with chimney smoke, wagons colored.

Let me write it compactly.

Canvas ~ 960×640 inside page, scale with CSS. Grid cell 40px, 24×16.

Implementation:
- snake array of {x,y,color}
- direction, nextDir queue
- passengers: array of positions, each with color, spawn on random free cell, drawn standing on platform (small tile with "PLATFORM" look).
- BFS from head to nearest passenger, avoiding body. If no path, any safe move.
- Smoke particles from loco.
- Score = passengers delivered? Just passengers picked. Maybe deliver: after picking N passengers, they disembark... keep simple: score = passengers picked.
- Game over on wall/self → reset in demo mode, in play mode show game over overlay with restart.

Draw: track ties background subtle grid, locomotive front with face, chimney, wagons with wheels, windows. Round rect wagons.

Speed: step every 130ms; animate smooth interpolation? Keep stepped but smoke + wheels animated via rAF drawing between steps using interpolation factor t. I'll interpolate positions for smoothness — store prev positions.

Simpler: draw with interpolation using prev grid positions and t = elapsed/stepTime. That's nicer. Snake cells store cur and prev.

Write code.

Colors palette: red, orange, yellow, green, blue, purple, teal, pink.

Passenger: little person: head circle, body rect in color, standing on gray platform tile with bench maybe. Waving arm animation.

HUD: score, best (no localStorage — just in memory), length. Title "Trainsnake".

Start overlay: "Click or press a key to start". Demo AI plays behind.

Key handling: window keydown; since iframe needs click for keys, click also starts. Space to restart after game over.

Let me write the code.

AI: BFS on grid to nearest passenger avoiding snake body (excluding tail maybe). Get first step. If none safe, pick any safe dir not reverse.

Respawn passengers: keep 3 on board.

Edge wrap? No, walls = game over.

Draw head as locomotive: dark body with rounded front, chimney with smoke puffs (particles), cowcatcher. Direction-dependent rotation — draw with ctx.translate+rotate.

Wheels: black circles rotating angle spokes.

Let me write ~350 lines.

Game over in play mode: overlay "GAME OVER — press SPACE". Demo mode: just reset silently.

Code:

```js
const COLS=24, ROWS=15, CELL=40; canvas 960x600.
```

Passengers spawn not on snake and not adjacent to head? Just free cell.

Interpolation drawing: for each segment i, pos = lerp(prev[i], cur[i], t). When growing, new tail segment prev = cur (appears at tail spot—fine, set prev=old tail pos).

BFS returns direction step. Apply nextDir queue from input only in play mode.

Smoke particles spawn at loco chimney each step, drift up, fade.

Draw order: background, track, platforms+passengers, snake (tail→head), smoke, HUD via DOM.

Background: light green field with subtle darker grid; a river? keep simple: grass texture circles maybe skip. Draw checker light.

Track: draw a rail line under the snake path? Just draw ties grid faintly everywhere (like all ground is track bed) — draw light brown gravel cells with subtle border. Simpler: gravel-colored board with cross ties faint.

I'll make board look like railway yard: sandy background, faint tie lines per row.

Write it.

HUD DOM: top bar with title, score, wagons count.

Also show little icons. Fine.

Passenger pickup: when head enters passenger cell → remove passenger, add wagon segment with passenger's color (nice touch: wagon color matches passenger coat), score++, spawn new passenger, puff of confetti spark.

Game speed constant 130ms.

Let's write final file.