Models 44
Train snakeOne-line

Claude Sonnet 5.5

by Anthropicreasoning defaultClean
Open in new window

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

How it went

Total time
2 min 16 s
Cost
$0.200
Output tokens
19,853
Spent thinking
7,188 tokens
Speed
149.3 tok/s
Console errors
0
First token after
2.7 s
First visible text after
1 min 11 s
Prompt tokens
548
Code size
21.3 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-sonnet-5.5
Reasoning sent
model default
Max output
128,000 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 · 557 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Little Train Station – Snake</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;height:100%;background:#1b2838;color:#fff;font-family:"Trebuchet MS",Verdana,system-ui,sans-serif;overflow:hidden}
  body{display:flex;flex-direction:column}
  header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 14px;background:#142030;border-bottom:3px solid #f4b942;flex:none;flex-wrap:wrap}
  h1{font-size:clamp(15px,3.6vw,22px);margin:0;letter-spacing:.5px;white-space:nowrap}
  h1 span{color:#f4b942}
  .stats{display:flex;gap:8px;font-size:clamp(11px,2.8vw,14px)}
  .stat{background:#22344b;border-radius:8px;padding:4px 10px;white-space:nowrap}
  .stat b{color:#f4b942;font-size:1.15em;margin-left:4px}
  #mode{background:#2a9d8f}
  #mode.play{background:#e76f51}
  #stage{flex:1;min-height:0;position:relative;display:flex;align-items:center;justify-content:center;padding:6px}
  canvas{display:block;border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,.5);touch-action:none}
  #overlay{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;pointer-events:none;
    background:rgba(20,32,48,.82);border:3px solid #f4b942;border-radius:16px;padding:16px 26px;max-width:min(92%,520px);
    box-shadow:0 8px 30px rgba(0,0,0,.45);transition:opacity .3s}
  #overlay.hide{opacity:0}
  #overlay h2{margin:0 0 6px;font-size:clamp(18px,4.6vw,30px);color:#f4b942}
  #overlay p{margin:4px 0;font-size:clamp(12px,3.2vw,16px);color:#dfe8f3}
  #overlay .go{font-size:clamp(15px,3.8vw,20px);font-weight:bold;color:#fff;animation:pulse 1.2s infinite}
  @keyframes pulse{50%{opacity:.45}}
</style>
</head>
<body>
<header>
  <h1>🚂 Little <span>Train</span> Station</h1>
  <div class="stats">
    <div class="stat" id="mode">AUTOPILOT</div>
    <div class="stat">Score<b id="score">0</b></div>
    <div class="stat">Passengers<b id="pax">0</b></div>
    <div class="stat">Best<b id="best">0</b></div>
  </div>
</header>
<div id="stage">
  <canvas id="c"></canvas>
  <div id="overlay">
    <h2 id="ot">All aboard!</h2>
    <p id="om">Pick up the waiting passengers – every one adds a colorful wagon.</p>
    <p class="go" id="og">Click or press a key to start</p>
    <p id="oh" style="font-size:12px;opacity:.8">Arrow keys / WASD / swipe to steer · don't hit the walls or your own wagons</p>
  </div>
</div>

<script>
(()=>{
const cv=document.getElementById('c'), ctx=cv.getContext('2d');
const stage=document.getElementById('stage');
const $=id=>document.getElementById(id);
const elScore=$('score'), elPax=$('pax'), elBest=$('best'), elMode=$('mode');
const ov=$('overlay'), ot=$('ot'), om=$('om'), og=$('og'), oh=$('oh');

const PALETTE=['#ef476f','#ffd166','#06d6a0','#118ab2','#9b5de5','#f78c3a','#4cc9f0','#80b918'];
const SHIRTS=['#e63946','#f1c453','#2ec4b6','#3a86ff','#8338ec','#fb8500','#ff70a6','#70e000','#ffffff'];
const SKINS=['#f6d1b1','#e0ac84','#c68642','#8d5524','#ffdbac'];
const HATS=[null,'#222','#e63946','#3a86ff','#f4b942'];
const rnd=a=>a[Math.floor(Math.random()*a.length)];
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));

let cols=20, rows=14, cell=32, dpr=1;
let best=0;

// game state
let mode='demo'; // demo | play | over
let cur, prev, dir, dirQueue, carInfo, passengers, score, pax, interval, acc, tickCount;
let particles=[], floaters=[], smokeT=0, overAt=0, demoRestartAt=0, headAngle=0, angles=[];
let flowers=[];
let time=0;

function layout(){
  const W=stage.clientWidth-12, H=stage.clientHeight-12;
  dpr=Math.min(window.devicePixelRatio||1,2);
  cell=Math.max(10,Math.floor(Math.min(W/cols,H/rows)));
  cv.style.width=cols*cell+'px'; cv.style.height=rows*cell+'px';
  cv.width=cols*cell*dpr; cv.height=rows*cell*dpr;
}
function chooseGrid(){
  const W=stage.clientWidth-12, H=stage.clientHeight-12;
  const target=W<600?30:40;
  cols=clamp(Math.floor(W/target),12,26);
  rows=clamp(Math.floor(H/target),9,17);
}

function makeFlowers(){
  flowers=[];
  const n=cols*rows/6|0;
  for(let i=0;i<n;i++) flowers.push({x:Math.random()*cols,y:Math.random()*rows,c:rnd(['#fff','#ffe66d','#ff9ecd','#cdb4ff']),r:Math.random()*.04+.03});
}

function newCar(){
  return {color:PALETTE[carInfoCount++%PALETTE.length], shirt:rnd(SHIRTS), skin:rnd(SKINS), shirt2:rnd(SHIRTS), skin2:rnd(SKINS)};
}
let carInfoCount=0;

function reset(){
  const sy=Math.floor(rows/2), sx=Math.floor(cols/3);
  cur=[{x:sx,y:sy},{x:sx-1,y:sy},{x:sx-2,y:sy}];
  prev=cur.map(c=>({...c}));
  dir={x:1,y:0}; dirQueue=[];
  carInfoCount=Math.floor(Math.random()*PALETTE.length);
  carInfo=[newCar(),newCar()];
  passengers=[]; particles=[]; floaters=[];
  score=0; pax=0; interval=150; acc=0; tickCount=0;
  headAngle=0; angles=[0,0,0];
  spawnPassenger(); spawnPassenger();
  updateHud();
}

function spawnPassenger(){
  for(let tries=0;tries<500;tries++){
    const x=Math.floor(Math.random()*cols), y=Math.floor(Math.random()*rows);
    if(cur.some(c=>c.x===x&&c.y===y)) continue;
    if(passengers.some(p=>p.x===x&&p.y===y)) continue;
    // avoid spawning directly in front of head
    if(Math.abs(x-cur[0].x)+Math.abs(y-cur[0].y)<3) continue;
    passengers.push({x,y,shirt:rnd(SHIRTS),skin:rnd(SKINS),hat:rnd(HATS),bag:Math.random()<.5,seed:Math.random()*10,age:0});
    return;
  }
}

function updateHud(){
  elScore.textContent=score; elPax.textContent=pax; elBest.textContent=best;
}

function setMode(m){
  mode=m;
  elMode.textContent=m==='play'?'YOU DRIVE':(m==='over'?'DERAILED':'AUTOPILOT');
  elMode.className='stat'+(m==='play'?' play':'');
  if(m==='play'){ov.classList.add('hide');}
  else{
    ov.classList.remove('hide');
    if(m==='demo'){
      ot.textContent='All aboard!';
      om.textContent='Pick up the waiting passengers – every one adds a colorful wagon.';
      og.textContent='Click or press a key to start';
      oh.style.display='';
    } else {
      ot.textContent='Derailed! 💥';
      om.textContent='You carried '+pax+' passenger'+(pax===1?'':'s')+' · score '+score;
      og.textContent='Click or press a key to play again';
      oh.style.display='none';
    }
  }
}

function startPlay(){
  reset(); setMode('play');
}

// ---------- AI ----------
const DIRS=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
function inb(x,y){return x>=0&&y>=0&&x<cols&&y<rows}
function blockedSet(){
  const s=new Set();
  for(let i=0;i<cur.length-1;i++) s.add(cur[i].y*cols+cur[i].x);
  return s;
}
function flood(sx,sy,blocked){
  const seen=new Set([sy*cols+sx]); const q=[[sx,sy]]; let n=0;
  while(q.length){
    const [x,y]=q.pop(); n++;
    for(const d of DIRS){
      const nx=x+d.x, ny=y+d.y;
      if(!inb(nx,ny)) continue;
      const k=ny*cols+nx;
      if(seen.has(k)||blocked.has(k)) continue;
      seen.add(k); q.push([nx,ny]);
    }
  }
  return n;
}
function aiDir(){
  const head=cur[0], blocked=blockedSet();
  const cands=DIRS.filter(d=>!(d.x===-dir.x&&d.y===-dir.y&&cur.length>1))
    .filter(d=>{const nx=head.x+d.x, ny=head.y+d.y; return inb(nx,ny)&&!blocked.has(ny*cols+nx);});
  if(!cands.length) return dir;
  // BFS to nearest passenger
  const targets=new Set(passengers.map(p=>p.y*cols+p.x));
  const par=new Map(); const start=head.y*cols+head.x;
  par.set(start,-1);
  const q=[[head.x,head.y]]; let found=-1;
  const first=new Map();
  for(let qi=0;qi<q.length&&found<0;qi++){
    const [x,y]=q[qi];
    for(const d of DIRS){
      const nx=x+d.x, ny=y+d.y; if(!inb(nx,ny)) continue;
      const k=ny*cols+nx;
      if(par.has(k)||blocked.has(k)) continue;
      if(qi===0&&d.x===-dir.x&&d.y===-dir.y) continue;
      par.set(k,y*cols+x);
      first.set(k, qi===0?d:first.get(y*cols+x));
      if(targets.has(k)){found=k;break;}
      q.push([nx,ny]);
    }
  }
  const tail=cur[cur.length-1];
  if(found>=0){
    const d=first.get(found);
    const nx=head.x+d.x, ny=head.y+d.y;
    const b2=new Set(blocked); b2.add(start);
    if(flood(nx,ny,b2)>=cur.length+2) return d;
  }
  // fallback: most open space, tie → closer to tail
  let bestD=cands[0], bestS=-1;
  for(const d of cands){
    const nx=head.x+d.x, ny=head.y+d.y;
    const b2=new Set(blocked); b2.add(start);
    let s=flood(nx,ny,b2)*100-(Math.abs(nx-tail.x)+Math.abs(ny-tail.y));
    if(s>bestS){bestS=s;bestD=d;}
  }
  return bestD;
}

// ---------- step ----------
function step(){
  if(mode==='demo') dir=aiDir();
  else if(dirQueue.length){
    const d=dirQueue.shift();
    if(!(d.x===-dir.x&&d.y===-dir.y)) dir=d;
  }
  const head=cur[0];
  const nh={x:head.x+dir.x,y:head.y+dir.y};
  const eatIdx=passengers.findIndex(p=>p.x===nh.x&&p.y===nh.y);
  const grow=eatIdx>=0;
  let dead=!inb(nh.x,nh.y);
  if(!dead){
    const lim=grow?cur.length:cur.length-1;
    for(let i=0;i<lim;i++) if(cur[i].x===nh.x&&cur[i].y===nh.y){dead=true;break;}
  }
  if(dead){ crash(); return; }
  const old=cur;
  cur=[nh,...old];
  if(!grow) cur.pop();
  prev=cur.map((_,i)=>({...old[Math.min(i,old.length-1)]}));
  tickCount++;
  if(grow){
    const p=passengers.splice(eatIdx,1)[0];
    const ci=newCar(); ci.shirt=p.shirt; ci.skin=p.skin;
    carInfo.push(ci);
    pax++; score+=10+Math.floor(cur.length/3);
    interval=Math.max(80,150-pax*2.5);
    burst((p.x+.5)*cell,(p.y+.5)*cell,p.shirt);
    floaters.push({x:(p.x+.5)*cell,y:(p.y+.2)*cell,t:0,txt:'+'+(10+Math.floor((cur.length-1)/3))});
    spawnPassenger();
    if(passengers.length<2&&Math.random()<.5) spawnPassenger();
    if(score>best) best=score;
    updateHud();
  }
}

function crash(){
  const hx=(cur[0].x+.5)*cell, hy=(cur[0].y+.5)*cell;
  for(let i=0;i<24;i++) particles.push({x:hx,y:hy,vx:(Math.random()-.5)*260,vy:(Math.random()-.5)*260,life:1,c:rnd(['#ffd166','#ef476f','#fff','#999']),r:cell*(.05+Math.random()*.08)});
  for(let i=0;i<8;i++) smoke(hx,hy,2);
  if(score>best) best=score;
  updateHud();
  if(mode==='play'){ overAt=performance.now(); setMode('over'); }
  else { demoRestartAt=performance.now()+1300; mode='demoDead'; }
}

function burst(x,y,c){
  for(let i=0;i<16;i++){
    const a=Math.random()*Math.PI*2, v=60+Math.random()*120;
    particles.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v-40,life:1,c:i%2?c:'#ffd166',r:cell*(.04+Math.random()*.05)});
  }
}
function smoke(x,y,k){
  smokes.push({x,y,vx:(Math.random()-.5)*14+8,vy:-18-Math.random()*14,r:cell*(.07+Math.random()*.05)*(k||1),life:1});
}
const smokes=[];

// ---------- input ----------
function steer(d){
  if(mode!=='play') return;
  const last=dirQueue.length?dirQueue[dirQueue.length-1]:dir;
  if(d.x===-last.x&&d.y===-last.y) return;
  if(d.x===last.x&&d.y===last.y) return;
  if(dirQueue.length<3) dirQueue.push(d);
}
function trigger(){
  if(mode==='demo'||mode==='demoDead'){startPlay();return true;}
  if(mode==='over'){ if(performance.now()-overAt>450){startPlay();} return true;}
  return false;
}
window.addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  const map={arrowup:{x:0,y:-1},w:{x:0,y:-1},arrowdown:{x:0,y:1},s:{x:0,y:1},arrowleft:{x:-1,y:0},a:{x:-1,y:0},arrowright:{x:1,y:0},d:{x:1,y:0}};
  if(map[k]||k===' ') e.preventDefault();
  if(mode!=='play'){
    const wasDemo=(mode!=='over');
    if(trigger()&&wasDemo&&map[k]){ // use the pressed key as first direction if not reverse
      if(!(map[k].x===-1)) dir=map[k];
    }
    return;
  }
  if(map[k]) steer(map[k]);
});
let ptr=null;
cv.addEventListener('pointerdown',e=>{
  if(mode!=='play'){trigger();return;}
  ptr={x:e.clientX,y:e.clientY};
});
window.addEventListener('pointerup',e=>{
  if(!ptr) return;
  const dx=e.clientX-ptr.x, dy=e.clientY-ptr.y; ptr=null;
  if(Math.hypot(dx,dy)<20) return;
  steer(Math.abs(dx)>Math.abs(dy)?{x:Math.sign(dx),y:0}:{x:0,y:Math.sign(dy)});
});
cv.addEventListener('pointermove',e=>{
  if(!ptr||mode!=='play') return;
  const dx=e.clientX-ptr.x, dy=e.clientY-ptr.y;
  if(Math.hypot(dx,dy)>34){
    steer(Math.abs(dx)>Math.abs(dy)?{x:Math.sign(dx),y:0}:{x:0,y:Math.sign(dy)});
    ptr={x:e.clientX,y:e.clientY};
  }
});
// ensure overlay clicks work too
stage.addEventListener('pointerdown',e=>{ if(e.target!==cv&&mode!=='play') trigger(); });

// ---------- drawing ----------
function rr(x,y,w,h,r){ctx.beginPath();ctx.roundRect(x,y,w,h,r);}

function drawGround(){
  const s=cell;
  for(let y=0;y<rows;y++)for(let x=0;x<cols;x++){
    ctx.fillStyle=(x+y)%2?'#8fc16a':'#98c973';
    ctx.fillRect(x*s,y*s,s+.5,s+.5);
  }
  for(const f of flowers){
    ctx.fillStyle=f.c; ctx.beginPath(); ctx.arc(f.x*s,f.y*s,f.r*s,0,7); ctx.fill();
  }
}

function trainPoints(t){
  const pts=[];
  for(let i=0;i<cur.length;i++){
    const a=prev[i], b=cur[i];
    pts.push({x:(a.x+(b.x-a.x)*t+.5)*cell, y:(a.y+(b.y-a.y)*t+.5)*cell});
  }
  return pts;
}

function drawTrack(pts){
  ctx.save();
  ctx.lineJoin='round'; ctx.lineCap='butt';
  const path=()=>{ctx.beginPath();ctx.moveTo(pts[0].x,pts[0].y);for(let i=1;i<pts.length;i++)ctx.lineTo(pts[i].x,pts[i].y);};
  // extend slightly beyond tail
  const per=cell*.26;
  const travel=((tickCount+tcur)*cell)%per;
  ctx.setLineDash([]);
  ctx.strokeStyle='rgba(60,50,35,.35)'; ctx.lineWidth=cell*.9; ctx.lineCap='round'; path(); ctx.stroke();
  ctx.lineCap='butt';
  ctx.setLineDash([cell*.11,per-cell*.11]); ctx.lineDashOffset=travel;
  ctx.strokeStyle='#7a5230'; ctx.lineWidth=cell*.78; path(); ctx.stroke();
  ctx.setLineDash([]);
  ctx.strokeStyle='#d5dbe0'; ctx.lineWidth=cell*.5; path(); ctx.stroke();
  ctx.strokeStyle='#6d5d4b'; ctx.lineWidth=cell*.4; path(); ctx.stroke();
  ctx.setLineDash([cell*.11,per-cell*.11]); ctx.lineDashOffset=travel;
  ctx.strokeStyle='#7a5230'; ctx.lineWidth=cell*.4; path(); ctx.stroke();
  ctx.restore();
}

function drawPassenger(p){
  const s=cell;
  const age=Math.min(1,p.age/.35);
  const sc=age<1?1-Math.pow(1-age,3)*0+0:1;
  const cx=(p.x+.5)*s, cy=(p.y+.5)*s;
  ctx.save(); ctx.translate(cx,cy);
  const pop=age<1?(0.4+0.6*age+Math.sin(age*Math.PI)*.2):1;
  ctx.scale(pop,pop);
  // platform
  rr(-.44*s,-.44*s,.88*s,.88*s,.1*s); ctx.fillStyle='#d9d3c5'; ctx.fill();
  ctx.lineWidth=Math.max(1.5,s*.05); ctx.strokeStyle='#f4b942'; ctx.setLineDash([s*.1,s*.07]);
  rr(-.38*s,-.38*s,.76*s,.76*s,.06*s); ctx.stroke(); ctx.setLineDash([]);
  rr(-.44*s,.36*s,.88*s,.08*s,.04*s); ctx.fillStyle='rgba(0,0,0,.18)'; ctx.fill();
  // person
  const bob=Math.sin(time*5+p.seed)*.03*s;
  ctx.translate(0,bob);
  // legs
  ctx.fillStyle='#34405a'; ctx.fillRect(-.09*s,.16*s,.07*s,.14*s); ctx.fillRect(.02*s,.16*s,.07*s,.14*s);
  // bag
  if(p.bag){ctx.fillStyle='#8a5a2b'; rr(.12*s,.1*s,.15*s,.17*s,.03*s); ctx.fill(); ctx.fillStyle='#5b3a19'; ctx.fillRect(.17*s,.07*s,.05*s,.04*s);}
  // body
  rr(-.14*s,-.06*s,.28*s,.28*s,.07*s); ctx.fillStyle=p.shirt; ctx.fill();
  ctx.strokeStyle='rgba(0,0,0,.25)'; ctx.lineWidth=1; ctx.stroke();
  // waving arm
  ctx.save(); ctx.translate(-.14*s,-.01*s); ctx.rotate(-1.9+Math.sin(time*9+p.seed)*.5);
  ctx.fillStyle=p.shirt; rr(-.03*s,0,.06*s,.17*s,.03*s); ctx.fill();
  ctx.fillStyle=p.skin; ctx.beginPath(); ctx.arc(0,.18*s,.04*s,0,7); ctx.fill();
  ctx.restore();
  // head
  ctx.fillStyle=p.skin; ctx.beginPath(); ctx.arc(0,-.15*s,.12*s,0,7); ctx.fill();
  ctx.strokeStyle='rgba(0,0,0,.25)'; ctx.stroke();
  // face
  ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(-.045*s,-.16*s,.016*s,0,7); ctx.arc(.045*s,-.16*s,.016*s,0,7); ctx.fill();
  ctx.strokeStyle='#a33'; ctx.lineWidth=Math.max(1,s*.015); ctx.beginPath(); ctx.arc(0,-.13*s,.04*s,.2,Math.PI-.2); ctx.stroke();
  if(p.hat){ctx.fillStyle=p.hat; ctx.beginPath(); ctx.arc(0,-.19*s,.125*s,Math.PI,0); ctx.fill(); ctx.fillRect(-.16*s,-.195*s,.32*s,.03*s);}
  ctx.restore();
}

function drawWagon(info){
  const s=cell, L=.92*s, W=.64*s;
  // wheels
  ctx.fillStyle='#222';
  for(const x of [-.28,.28]){
    ctx.fillRect((x-.09)*s,-W/2-.035*s,.18*s,.07*s);
    ctx.fillRect((x-.09)*s,W/2-.035*s,.18*s,.07*s);
  }
  // coupler
  ctx.fillStyle='#333'; ctx.fillRect(-L/2-.1*s,-.03*s,.12*s,.06*s);
  // body
  rr(-L/2,-W/2,L,W,.1*s); ctx.fillStyle=info.color; ctx.fill();
  ctx.lineWidth=Math.max(1.5,s*.04); ctx.strokeStyle='rgba(0,0,0,.35)'; ctx.stroke();
  // roof
  rr(-L/2+.06*s,-W/2+.06*s,L-.12*s,W-.12*s,.07*s); ctx.fillStyle='rgba(255,255,255,.22)'; ctx.fill();
  // roof ridge
  ctx.fillStyle='rgba(0,0,0,.12)'; ctx.fillRect(-L/2+.1*s,-.012*s,L-.2*s,.024*s);
  // passenger hatches
  const pp=[[-.2,info.shirt,info.skin],[.2,info.shirt2,info.skin2]];
  for(const [x,sh,sk] of pp){
    rr((x-.13)*s,-.2*s,.26*s,.4*s,.06*s); ctx.fillStyle='rgba(20,25,40,.75)'; ctx.fill();
    ctx.fillStyle=sh; ctx.beginPath(); ctx.ellipse(x*s,0,.05*s,.15*s,0,0,7); ctx.fill();
    ctx.fillStyle=sk; ctx.beginPath(); ctx.arc(x*s,0,.075*s,0,7); ctx.fill();
  }
}

function drawLoco(){
  const s=cell, L=.94*s, W=.66*s;
  ctx.fillStyle='#111';
  for(const x of [-.3,0,.3]){
    ctx.fillRect((x-.09)*s,-W/2-.035*s,.18*s,.07*s);
    ctx.fillRect((x-.09)*s,W/2-.035*s,.18*s,.07*s);
  }
  ctx.fillStyle='#333'; ctx.fillRect(-L/2-.1*s,-.03*s,.12*s,.06*s);
  // headlight cone
  const g=ctx.createLinearGradient(.45*s,0,1.5*s,0);
  g.addColorStop(0,'rgba(255,240,150,.4)'); g.addColorStop(1,'rgba(255,240,150,0)');
  ctx.fillStyle=g; ctx.beginPath(); ctx.moveTo(.46*s,-.08*s); ctx.lineTo(1.5*s,-.5*s); ctx.lineTo(1.5*s,.5*s); ctx.lineTo(.46*s,.08*s); ctx.fill();
  // cowcatcher
  ctx.fillStyle='#555'; ctx.beginPath(); ctx.moveTo(.44*s,-.27*s); ctx.lineTo(.56*s,0); ctx.lineTo(.44*s,.27*s); ctx.fill();
  // boiler
  rr(-.12*s,-.26*s,.6*s,.52*s,.2*s); ctx.fillStyle='#2b2d42'; ctx.fill();
  ctx.lineWidth=Math.max(1.5,s*.04); ctx.strokeStyle='rgba(0,0,0,.5)'; ctx.stroke();
  rr(-.08*s,-.2*s,.52*s,.14*s,.07*s); ctx.fillStyle='rgba(255,255,255,.12)'; ctx.fill();
  // gold bands
  ctx.fillStyle='#f4b942'; ctx.fillRect(.08*s,-.255*s,.04*s,.51*s); ctx.fillRect(.3*s,-.25*s,.04*s,.5*s);
  // cab
  rr(-L/2,-W/2,.44*s,W,.09*s); ctx.fillStyle='#e63946'; ctx.fill(); ctx.stroke();
  rr(-L/2+.05*s,-W/2+.05*s,.34*s,W-.1*s,.06*s); ctx.fillStyle='rgba(255,255,255,.2)'; ctx.fill();
  // dome
  ctx.fillStyle='#f4b942'; ctx.beginPath(); ctx.arc(.06*s,0,.09*s,0,7); ctx.fill(); ctx.stroke();
  // chimney
  ctx.fillStyle='#444'; ctx.beginPath(); ctx.arc(.34*s,0,.11*s,0,7); ctx.fill();
  ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(.34*s,0,.07*s,0,7); ctx.fill();
  // headlamp
  ctx.fillStyle='#fff3a0'; ctx.beginPath(); ctx.arc(.46*s,0,.055*s,0,7); ctx.fill();
}

function angDiff(a,b){let d=b-a;while(d>Math.PI)d-=Math.PI*2;while(d<-Math.PI)d+=Math.PI*2;return d;}

let tcur=0;
function render(dt){
  ctx.setTransform(dpr,0,0,dpr,0,0);
  drawGround();
  const t=(mode==='over'||mode==='demoDead')?1:tcur;
  const pts=trainPoints(t);
  drawTrack(pts);
  for(const p of passengers) drawPassenger(p);

  // angles
  const targetHead=Math.atan2(dir.y,dir.x);
  headAngle+=angDiff(headAngle,targetHead)*Math.min(1,dt*16);
  angles[0]=headAngle;
  for(let i=1;i<pts.length;i++){
    const dx=pts[i-1].x-pts[i].x, dy=pts[i-1].y-pts[i].y;
    if(Math.hypot(dx,dy)>cell*.05){
      const a=Math.atan2(dy,dx);
      angles[i]=angles[i]===undefined?a:angles[i]+angDiff(angles[i],a)*Math.min(1,dt*20);
    } else if(angles[i]===undefined) angles[i]=angles[i-1];
  }
  for(let i=pts.length-1;i>=1;i--){
    ctx.save(); ctx.translate(pts[i].x,pts[i].y); ctx.rotate(angles[i]);
    // shadow
    ctx.fillStyle='rgba(0,0,0,.22)'; rr(-.46*cell+2,-.32*cell+3,.92*cell,.64*cell,.1*cell); ctx.fill();
    drawWagon(carInfo[i-1]||carInfo[carInfo.length-1]);
    ctx.restore();
  }
  ctx.save(); ctx.translate(pts[0].x,pts[0].y); ctx.rotate(headAngle);
  ctx.fillStyle='rgba(0,0,0,.22)'; rr(-.47*cell+2,-.33*cell+3,.94*cell,.66*cell,.1*cell); ctx.fill();
  drawLoco();
  ctx.restore();

  // smoke emission
  if(mode==='play'||mode==='demo'){
    smokeT-=dt;
    if(smokeT<=0){
      smokeT=.09;
      const cx=pts[0].x+Math.cos(headAngle)*.34*cell, cy=pts[0].y+Math.sin(headAngle)*.34*cell;
      smoke(cx,cy,1);
    }
  }
  for(let i=smokes.length-1;i>=0;i--){
    const p=smokes[i]; p.life-=dt*.6; p.x+=p.vx*dt; p.y+=p.vy*dt; p.r+=dt*cell*.1;
    if(p.life<=0){smokes.splice(i,1);continue;}
    ctx.fillStyle='rgba(245,245,250,'+(p.life*.55).toFixed(3)+')';
    ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill();
  }
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i]; p.life-=dt*1.6; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=260*dt;
    if(p.life<=0){particles.splice(i,1);continue;}
    ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.c;
    ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill(); ctx.globalAlpha=1;
  }
  for(let i=floaters.length-1;i>=0;i--){
    const f=floaters[i]; f.t+=dt;
    if(f.t>1){floaters.splice(i,1);continue;}
    ctx.globalAlpha=1-f.t; ctx.font='bold '+Math.round(cell*.55)+'px Trebuchet MS, sans-serif';
    ctx.textAlign='center'; ctx.lineWidth=3; ctx.strokeStyle='#14202f'; ctx.fillStyle='#ffd166';
    const y=f.y-f.t*cell*1.1; ctx.strokeText(f.txt,f.x,y); ctx.fillText(f.txt,f.x,y); ctx.globalAlpha=1;
  }
}

// ---------- loop ----------
let last=performance.now();
function frame(now){
  const dt=Math.min(.05,(now-last)/1000); last=now; time+=dt;
  for(const p of passengers) p.age+=dt;
  if(mode==='play'||mode==='demo'){
    acc+=dt*1000;
    let guard=0;
    while(acc>=interval&&guard++<4&&(mode==='play'||mode==='demo')){ acc-=interval; step(); }
    tcur=(mode==='play'||mode==='demo')?clamp(acc/interval,0,1):1;
  } else if(mode==='demoDead'){
    tcur=1;
    if(now>demoRestartAt){ reset(); mode='demo'; }
  }
  render(dt);
  requestAnimationFrame(frame);
}

function init(){
  chooseGrid(); layout(); makeFlowers(); reset(); setMode('demo');
  // spawned prev so first frame is valid
  requestAnimationFrame(t=>{last=t;frame(t);});
}
window.addEventListener('resize',layout);
init();
})();
</script>
</body>
</html>
Reasoning trace (7,188 tokens)
I'm planning a snake game with a train theme, including an AI self-playing demo mode and a "Click or press a key to start" prompt, all in one HTML file using canvas. I'm sketching a grid of about 24 by 16 with a responsive canvas, where the snake is a train with a chimney, smoke, and colored wagons, and passengers on platform tiles serve as food.

Still writing the implementation plan for the canvas sizing, game state machine, and tick-based smooth movement...

Still working out how the train's segments interpolate between ticks: the previous positions take the old current positions, and the newly grown tail segment stays where the old tail was.

Still working out how each car is drawn: its angle comes from the car ahead of it, with the head using the direction of travel, and I'm checking how that angle behaves at corners and for the newly grown tail.

Next I'm settling the train's head angle, easing it toward the direction of travel. Then the wagons get a body color with two little windows, each showing a passenger's head. I'm also weighing how to draw the track: rails and sleepers only under the cells the train occupies, rather than covering the whole board.

Still working out the track under the train: I'm weighing a thick dark bed along the path, with sleepers drawn as short dashes, rather than trying to offset two rails from the polyline.

Still working out the track under the train: the rails are the tricky part, since the sleepers would show through any gap if I stroke the rails over them, so I'm weighing layering a thinner bed on top of the rail lines.

I'm working through the track under the train now: sleepers and silver rails layered over a ballast bed, following the path through each car's center. I'm leaning toward keeping the track only under the train, though a permanent trail could look nicer. I'm also thinking about adding fixed platform strips along the top and bottom edges, with passengers waiting there.

Next I'm adding little platform tiles under each passenger, with a yellow edge and a safety line, and animating passengers with a bob and a waving arm. I'm also planning smoke puffs rising from the chimney, a "+10" pop on pickup, and skipping audio. For the AI, I'm thinking of a BFS toward the nearest passenger that routes around the body, and falling back to the move that keeps the most open space if no path exists.

Now I'm planning the game's controls and screens: arrow keys and WASD, swipe on touch, and an overlay that says "Click or press a key to start," which stays semi-transparent over the board during the AI demo. I'll track score, passengers carried, and the best score for the session, with "Little Train Station" as the title.

Next I'm settling the board size: I'll pick the grid's columns and rows from the available area at startup, targeting roughly 36px cells and clamping the counts, then only rescale the cell size on resize.

Still writing the train drawing: each car is a rounded body in its wagon color, with a lighter roof stripe and two small windows showing passenger heads.

Still writing the locomotive: a dark rounded body with a red stripe, a boiler at the front, and a raised cabin and chimney toward the rear.

Still working out the locomotive's top-down details: a chimney ring toward the front, a gold dome, a yellow headlight glow, and a cab roof at the rear. I'm also planning to derive the smoke origin from the head's position and angle.

Now I'm setting up the tick logic: each step takes the queued direction, checks the new head against the walls and the body, and if it's clear, prepends the new head and drops the tail unless a pending growth is being used up.

Still writing the tick logic: each interpolation point pulls from the old positions, and when a passenger is eaten the growth flag keeps the tail in place for one step. I'm assigning colors to the cars so that each new wagon takes the color of the passenger it picked up.

Now I'm setting up the input handling: any key starts play from the demo, and arrow keys set the opening direction to the right. After a game over I'm allowing a restart after a short delay, with clicks and taps doing the same, and a swipe on touch picks the new direction once it's clearly moved.

Still working out the AI's move choice: I'm running a BFS from the head toward the nearest passenger, treating the body as blocked, with the tail counted as free since it moves away.

Still working out the cell sizing and layout: I'm deriving the grid's column and row counts from the window's aspect at startup, then only rescaling the cell size on resize.

I'm sizing the grid from the stage's dimensions, aiming for cells near 38px and clamping the column and row counts, then working out the scale and how the canvas is laid out.

Still drawing the platform passengers: each waiting figure sits on a concrete tile with a yellow edge, bobbing gently and waving an arm.

Now I'm adding the pickup effects, with floating "+10" text and sparkles in the passenger's shirt color. Smoke puffs rise from the chimney every so often, and a crash will send a burst of them from the head. The speed scales with how many passengers have been carried.

Still writing the track drawing: I'm mapping the cars' centers into pixel points from head to tail, and dashing the sleepers along that line with a short dash pattern.

Still working out the track sleepers: I'm using a dash pattern along the path through the cars' centers, with the dash offset tied to how far the head has traveled so the sleepers look fixed to the ground.

Still writing the locomotive's wheels and the drawing details that come after them, working out how the track's dash offset keeps the sleepers fixed to the ground as the train moves.

Still working on the locomotive's wheels and body: I'm placing the wheels and a charcoal boiler with gold bands, plus a red cabin at the rear.

Still writing the locomotive's cabin and wagon bodies: a red cabin with a lighter roof inset, plus wagons drawn as rounded boxes with a darker outline.

Still writing the train car details: each wagon gets a dark hatch on its roof with two passenger heads peeking out, their shoulders in the car's own shirt color.

Still writing the train car details: each wagon's shirt and skin colors are set per car, with passenger heads peeking through the roof hatch.