Train snakeOne-line
GLM 5.3
by Z.aiReasoningClean
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.