Models 44
Train snakeOne-line

Claude Opus 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 0 s
Cost
$0.281
Output tokens
13,960
Spent thinking
2,979 tokens
Speed
119.4 tok/s
Console errors
0
First token after
3.3 s
First visible text after
37.3 s
Prompt tokens
548
Code size
17.3 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-opus-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 · 337 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Snake Express</title>
<style>
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%;overflow:hidden}
  body{background:linear-gradient(#2b3a67,#1b2440);font-family:"Trebuchet MS",Verdana,sans-serif;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:8px}
  h1{font-size:clamp(18px,3.4vw,30px);letter-spacing:1px;text-shadow:0 3px 0 #0006}
  h1 span{color:#ffd34d}
  #hud{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;font-size:clamp(12px,2vw,16px)}
  .pill{background:#ffffff1a;border:1px solid #ffffff30;border-radius:20px;padding:4px 12px}
  .pill b{color:#ffd34d}
  #wrap{position:relative;border-radius:14px;overflow:hidden;box-shadow:0 10px 30px #0008,0 0 0 4px #6b4a2b}
  canvas{display:block;cursor:pointer;touch-action:none}
  #help{font-size:12px;opacity:.7;text-align:center}
</style>
</head>
<body>
<h1>🚂 Snake <span>Express</span></h1>
<div id="hud">
  <div class="pill">Score <b id="score">0</b></div>
  <div class="pill">Passengers <b id="pass">0</b></div>
  <div class="pill">Best <b id="best">0</b></div>
  <div class="pill" id="mode">DEMO</div>
</div>
<div id="wrap"><canvas id="c"></canvas></div>
<div id="help">Arrow keys / WASD or swipe to steer · Don't crash into the fence or your own wagons!</div>
<script>
(()=>{
const COLS=24, ROWS=16;
const cv=document.getElementById('c'), ctx=cv.getContext('2d');
const $=id=>document.getElementById(id);
let C=30, W, H, dpr=1, bg=null;
const PALETTE=['#ff5d5d','#ffb234','#ffe14d','#5fd36a','#3fc1ff','#8a7bff','#ff7ad9','#2fe0c0'];
const SHIRTS=['#e63946','#f4a261','#2a9d8f','#457b9d','#9b5de5','#f15bb5','#00bbf9','#80b918'];
const SKINS=['#f5d0a9','#e0ac69','#c68642','#8d5524','#ffdbac'];
const HAIR=['#2b1b0e','#6b3e1f','#d9a441','#111','#a33'];

function resize(){
  const maxW=Math.min(window.innerWidth-24,1100);
  const maxH=window.innerHeight-130;
  C=Math.max(10,Math.floor(Math.min(maxW/COLS,maxH/ROWS)));
  W=C*COLS;H=C*ROWS;dpr=window.devicePixelRatio||1;
  cv.width=W*dpr;cv.height=H*dpr;cv.style.width=W+'px';cv.style.height=H+'px';
  buildBg();
}
function rnd(a,b){return a+Math.random()*(b-a)}
function buildBg(){
  bg=document.createElement('canvas');bg.width=W*dpr;bg.height=H*dpr;
  const g=bg.getContext('2d');g.scale(dpr,dpr);
  for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++){
    g.fillStyle=(x+y)%2?'#7ccf5a':'#74c654';g.fillRect(x*C,y*C,C,C);
  }
  // seeded-ish decorations
  let s=7;const r=()=>{s=(s*9301+49297)%233280;return s/233280};
  for(let i=0;i<COLS*ROWS*0.5;i++){
    const x=r()*W,y=r()*H,t=r();
    if(t<0.55){g.strokeStyle='#5fae43';g.lineWidth=1;g.beginPath();g.moveTo(x,y);g.lineTo(x-2,y-4);g.moveTo(x,y);g.lineTo(x+2,y-5);g.stroke();}
    else{g.fillStyle=['#fff','#ffe14d','#ff8fb1','#b9a6ff'][Math.floor(r()*4)];
      for(let k=0;k<4;k++){g.beginPath();g.arc(x+Math.cos(k*1.57)*2,y+Math.sin(k*1.57)*2,1.6,0,7);g.fill();}
      g.fillStyle='#f7b500';g.beginPath();g.arc(x,y,1.3,0,7);g.fill();}
  }
  // fence border
  g.strokeStyle='#8b5a2b';g.lineWidth=Math.max(2,C*0.12);g.strokeRect(2,2,W-4,H-4);
}

// ---------- state ----------
let segs, dir, queue, food, score, passengers, best=0, alive, demo=true, tickMs, acc=0, deadTimer=0;
let particles=[], floaters=[], shake=0, lastT=performance.now(), time=0;

function reset(){
  const sx=6, sy=8;
  segs=[];
  for(let i=0;i<4;i++){
    segs.push({x:sx-i,y:sy,px:sx-i-1,py:sy,a:0,pa:0,color:PALETTE[(i-1+PALETTE.length)%PALETTE.length],rider:i>0?mkRider():null});
  }
  dir={x:1,y:0};queue=[];score=0;passengers=0;alive=true;tickMs=demo?95:140;acc=0;deadTimer=0;
  placeFood();updHud();
}
function mkRider(){return{skin:SKINS[Math.floor(Math.random()*SKINS.length)],hair:HAIR[Math.floor(Math.random()*HAIR.length)]}}
function placeFood(){
  const occ=new Set(segs.map(s=>s.x+','+s.y));
  const free=[];
  for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++)if(!occ.has(x+','+y))free.push({x,y});
  const f=free[Math.floor(Math.random()*free.length)];
  food={x:f.x,y:f.y,shirt:SHIRTS[Math.floor(Math.random()*SHIRTS.length)],skin:SKINS[Math.floor(Math.random()*SKINS.length)],hair:HAIR[Math.floor(Math.random()*HAIR.length)],t:0};
}
function updHud(){
  $('score').textContent=score;$('pass').textContent=passengers;
  if(!demo&&score>best)best=score;
  $('best').textContent=best;$('mode').textContent=demo?'DEMO (AI driving)':'YOU ARE DRIVING';
}

// ---------- AI ----------
const DIRS=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
function blocked(){
  const b=new Uint8Array(COLS*ROWS);
  for(let i=0;i<segs.length-1;i++)b[segs[i].y*COLS+segs[i].x]=1;
  return b;
}
function inb(x,y){return x>=0&&y>=0&&x<COLS&&y<ROWS}
function flood(b,sx,sy){
  const seen=new Uint8Array(COLS*ROWS);let n=0;const st=[[sx,sy]];seen[sy*COLS+sx]=1;
  while(st.length){const[x,y]=st.pop();n++;
    for(const d of DIRS){const nx=x+d.x,ny=y+d.y;if(inb(nx,ny)&&!b[ny*COLS+nx]&&!seen[ny*COLS+nx]){seen[ny*COLS+nx]=1;st.push([nx,ny]);}}}
  return n;
}
function aiMove(){
  const h=segs[0],b=blocked();
  // BFS to food
  const prev=new Int32Array(COLS*ROWS).fill(-1);const start=h.y*COLS+h.x;prev[start]=start;
  const q=[start];let found=false;
  while(q.length){const c=q.shift();if(c===food.y*COLS+food.x){found=true;break;}
    const cx=c%COLS,cy=(c/COLS)|0;
    for(const d of DIRS){const nx=cx+d.x,ny=cy+d.y,ni=ny*COLS+nx;
      if(inb(nx,ny)&&!b[ni]&&prev[ni]<0){prev[ni]=c;q.push(ni);}}}
  const opts=[];
  for(const d of DIRS){if(d.x===-dir.x&&d.y===-dir.y)continue;
    const nx=h.x+d.x,ny=h.y+d.y;if(!inb(nx,ny)||b[ny*COLS+nx])continue;
    b[h.y*COLS+h.x]=1;const sp=flood(b,nx,ny);opts.push({d,sp});}
  if(!opts.length)return;
  if(found){
    let c=food.y*COLS+food.x;while(prev[c]!==start)c=prev[c];
    const nx=c%COLS,ny=(c/COLS)|0;const d={x:nx-h.x,y:ny-h.y};
    const o=opts.find(o=>o.d.x===d.x&&o.d.y===d.y);
    if(o&&o.sp>=segs.length+2){dir=o.d;return;}
  }
  opts.sort((a,b)=>b.sp-a.sp);
  // among best-space moves, prefer hugging (fewer free neighbours)
  dir=opts[0].d;
}

// ---------- step ----------
function step(){
  if(demo)aiMove();
  else{while(queue.length){const d=queue.shift();if(!(d.x===-dir.x&&d.y===-dir.y)&&!(d.x===dir.x&&d.y===dir.y)){dir=d;break;}}}
  const h=segs[0],nx=h.x+dir.x,ny=h.y+dir.y;
  const grow=nx===food.x&&ny===food.y;
  const checkLen=grow?segs.length:segs.length-1;
  let hit=!inb(nx,ny);
  for(let i=0;i<checkLen&&!hit;i++)if(segs[i].x===nx&&segs[i].y===ny)hit=true;
  if(hit){crash();return;}
  const old=segs.map(s=>({x:s.x,y:s.y,a:s.a}));
  if(grow){const t=old[old.length-1];
    segs.push({x:t.x,y:t.y,px:t.x,py:t.y,a:t.a,pa:t.a,color:PALETTE[(segs.length-1)%PALETTE.length],rider:{skin:food.skin,hair:food.hair},pop:0});}
  for(let i=0;i<old.length;i++){const s=segs[i];
    s.px=old[i].x;s.py=old[i].y;s.pa=s.a;
    if(i===0){s.x=nx;s.y=ny;}else{s.x=old[i-1].x;s.y=old[i-1].y;}
    s.a=Math.atan2(s.y-s.py,s.x-s.px);
  }
  if(grow){
    score+=10+Math.floor(segs.length/3);passengers++;
    for(let i=0;i<22;i++)particles.push({x:(food.x+.5)*C,y:(food.y+.5)*C,vx:rnd(-3,3),vy:rnd(-4,1),life:1,col:PALETTE[i%8],type:'conf',r:rnd(2,4)});
    floaters.push({x:(food.x+.5)*C,y:food.y*C,txt:'+'+(10+Math.floor((segs.length-1)/3))+' 🎟',life:1});
    placeFood();tickMs=Math.max(demo?70:75,tickMs-2.5);updHud();
  }
}
function crash(){
  alive=false;shake=12;
  const h=segs[0];
  for(let i=0;i<30;i++)particles.push({x:(h.x+.5)*C,y:(h.y+.5)*C,vx:rnd(-4,4),vy:rnd(-4,4),life:1,col:i%2?'#ff9d2e':'#555',type:'conf',r:rnd(2,5)});
  updHud();
}

// ---------- drawing ----------
function lerpA(a,b,t){let d=b-a;while(d>Math.PI)d-=2*Math.PI;while(d<-Math.PI)d+=2*Math.PI;return a+d*t}
function rr(x,y,w,h,r){ctx.beginPath();ctx.roundRect(x,y,w,h,r);}
function segPos(s,t){return{x:(s.px+(s.x-s.px)*t+.5)*C,y:(s.py+(s.y-s.py)*t+.5)*C,a:lerpA(s.pa,s.a,Math.min(1,t*1.6))}}

function drawTrack(pts){
  if(pts.length<2)return;
  ctx.lineCap='round';ctx.lineJoin='round';
  ctx.beginPath();ctx.moveTo(pts[0].x,pts[0].y);for(const p of pts)ctx.lineTo(p.x,p.y);
  ctx.strokeStyle='#b8a68a';ctx.lineWidth=C*.78;ctx.stroke();
  ctx.setLineDash([C*.14,C*.18]);ctx.lineCap='butt';ctx.strokeStyle='#7a4f2a';ctx.lineWidth=C*.66;ctx.stroke();ctx.setLineDash([]);
  ctx.lineCap='round';ctx.strokeStyle='#9aa3ad';ctx.lineWidth=C*.06;
  // rails approximated by two offset passes
  for(const off of[-.2,.2]){ctx.beginPath();
    for(let i=0;i<pts.length;i++){const p=pts[i],q=pts[Math.min(i+1,pts.length-1)],o=pts[Math.max(i-1,0)];
      const ang=Math.atan2(q.y-o.y,q.x-o.x);const x=p.x-Math.sin(ang)*off*C,y=p.y+Math.cos(ang)*off*C;
      i?ctx.lineTo(x,y):ctx.moveTo(x,y);}
    ctx.stroke();}
}

function drawLoco(p){
  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.a);const c=C;
  ctx.fillStyle='#0004';rr(-.46*c,-.3*c+3,.95*c,.62*c,.12*c);ctx.fill();
  // wheels
  ctx.fillStyle='#222';for(const wx of[-.3,-.02,.25])for(const wy of[-.34,.26])ctx.fillRect(wx*c,wy*c,.16*c,.08*c);
  // cab
  ctx.fillStyle='#c62828';rr(-.48*c,-.32*c,.4*c,.64*c,.08*c);ctx.fill();
  ctx.fillStyle='#8e1b1b';rr(-.44*c,-.27*c,.32*c,.54*c,.06*c);ctx.fill();
  ctx.fillStyle='#ffd34d';ctx.fillRect(-.47*c,-.08*c,.04*c,.16*c);
  // boiler
  const gr=ctx.createLinearGradient(0,-.24*c,0,.24*c);gr.addColorStop(0,'#3d8b5a');gr.addColorStop(.5,'#5fbf7f');gr.addColorStop(1,'#2a6b42');
  ctx.fillStyle=gr;rr(-.1*c,-.24*c,.52*c,.48*c,.22*c);ctx.fill();
  ctx.strokeStyle='#ffd34d';ctx.lineWidth=c*.04;for(const bx of[.05,.22]){ctx.beginPath();ctx.moveTo(bx*c,-.23*c);ctx.lineTo(bx*c,.23*c);ctx.stroke();}
  // dome & chimney
  ctx.fillStyle='#ffd34d';ctx.beginPath();ctx.arc(.12*c,0,.08*c,0,7);ctx.fill();
  ctx.fillStyle='#222';ctx.beginPath();ctx.arc(.31*c,0,.1*c,0,7);ctx.fill();ctx.fillStyle='#555';ctx.beginPath();ctx.arc(.31*c,0,.06*c,0,7);ctx.fill();
  // cowcatcher & light
  ctx.fillStyle='#ff5d5d';ctx.beginPath();ctx.moveTo(.42*c,-.26*c);ctx.lineTo(.54*c,0);ctx.lineTo(.42*c,.26*c);ctx.closePath();ctx.fill();
  ctx.fillStyle='#fff6b0';ctx.beginPath();ctx.arc(.45*c,0,.06*c,0,7);ctx.fill();
  // headlight beam
  ctx.fillStyle='rgba(255,246,176,.18)';ctx.beginPath();ctx.moveTo(.5*c,-.08*c);ctx.lineTo(1.2*c,-.3*c);ctx.lineTo(1.2*c,.3*c);ctx.lineTo(.5*c,.08*c);ctx.fill();
  ctx.restore();
}
function drawWagon(p,s,i){
  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.a);const c=C;
  const sc=s.pop!==undefined&&s.pop<1?.4+.6*s.pop:1;ctx.scale(sc,sc);
  ctx.fillStyle='#0004';rr(-.4*c,-.28*c+3,.8*c,.58*c,.12*c);ctx.fill();
  ctx.fillStyle='#222';for(const wx of[-.3,.16])for(const wy of[-.33,.25])ctx.fillRect(wx*c,wy*c,.14*c,.08*c);
  ctx.fillStyle=s.color;rr(-.4*c,-.29*c,.8*c,.58*c,.12*c);ctx.fill();
  ctx.fillStyle='#fff5';rr(-.36*c,-.25*c,.72*c,.12*c,.06*c);ctx.fill();
  ctx.fillStyle='#0002';rr(-.36*c,.13*c,.72*c,.12*c,.06*c);ctx.fill();
  // open top showing passenger
  ctx.fillStyle='#4a2f1a';rr(-.22*c,-.15*c,.44*c,.3*c,.06*c);ctx.fill();
  if(s.rider){
    const bob=Math.sin(time*8+i)*c*.02;
    ctx.fillStyle=s.rider.skin;ctx.beginPath();ctx.arc(bob,0,.11*c,0,7);ctx.fill();
    ctx.fillStyle=s.rider.hair;ctx.beginPath();ctx.arc(bob-.02*c,0,.09*c,Math.PI*.5,Math.PI*1.5);ctx.fill();
    // waving arm
    ctx.strokeStyle=s.rider.skin;ctx.lineWidth=c*.05;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(bob,-.08*c);ctx.lineTo(bob+Math.sin(time*10+i)*.08*c,-.24*c);ctx.stroke();
  }
  ctx.restore();
}
function drawPassenger(){
  const x=food.x*C,y=food.y*C,c=C;food.t+=.016;
  const appear=Math.min(1,food.t*3);
  ctx.save();ctx.translate(x+c/2,y+c/2);ctx.scale(appear,appear);ctx.translate(-c/2,-c/2);
  // platform
  ctx.fillStyle='#0003';rr(-.06*c,-.02*c+3,c*1.12,c*1.04,c*.12);ctx.fill();
  ctx.fillStyle='#cfd5dc';rr(-.06*c,-.02*c,c*1.12,c*1.04,c*.12);ctx.fill();
  ctx.fillStyle='#ffd34d';ctx.fillRect(-.06*c,c*.86,c*1.12,c*.08);
  ctx.fillStyle='#b3bac3';for(let k=1;k<4;k++)ctx.fillRect(k*c*.27-.06*c,0,1,c*.84);
  // sign
  ctx.fillStyle='#555';ctx.fillRect(c*.82,c*.12,c*.04,c*.6);
  ctx.fillStyle='#1e88e5';rr(c*.66,c*.06,c*.34,c*.2,3);ctx.fill();
  ctx.fillStyle='#fff';ctx.font=`bold ${c*.15}px sans-serif`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('🚉'===''?'':'S',c*.83,c*.165);
  // person
  const jump=Math.abs(Math.sin(time*5))*c*.06;
  const px=c*.4,py=c*.78-jump;
  ctx.fillStyle='#0003';ctx.beginPath();ctx.ellipse(px,c*.8,c*.16,c*.05,0,0,7);ctx.fill();
  ctx.strokeStyle='#333';ctx.lineWidth=c*.06;ctx.lineCap='round';
  ctx.beginPath();ctx.moveTo(px-.05*c,py);ctx.lineTo(px-.05*c,py-.18*c);ctx.moveTo(px+.05*c,py);ctx.lineTo(px+.05*c,py-.18*c);ctx.stroke();
  ctx.fillStyle=food.shirt;rr(px-.11*c,py-.42*c,.22*c,.27*c,.06*c);ctx.fill();
  ctx.strokeStyle=food.skin;ctx.beginPath();ctx.moveTo(px+.1*c,py-.38*c);ctx.lineTo(px+.2*c+Math.sin(time*9)*.06*c,py-.58*c);ctx.stroke();
  ctx.beginPath();ctx.moveTo(px-.1*c,py-.36*c);ctx.lineTo(px-.16*c,py-.2*c);ctx.stroke();
  ctx.fillStyle='#8d5524';ctx.fillRect(px-.2*c,py-.24*c,.08*c,.12*c);
  ctx.fillStyle=food.skin;ctx.beginPath();ctx.arc(px,py-.52*c,.11*c,0,7);ctx.fill();
  ctx.fillStyle=food.hair;ctx.beginPath();ctx.arc(px,py-.55*c,.11*c,Math.PI,0);ctx.fill();
  ctx.fillStyle='#222';ctx.fillRect(px-.05*c,py-.53*c,c*.025,c*.03);ctx.fillRect(px+.03*c,py-.53*c,c*.025,c*.03);
  ctx.restore();
  // pulsing ring
  ctx.strokeStyle=`rgba(255,211,77,${.6-.6*((time*1.2)%1)})`;ctx.lineWidth=2;
  ctx.beginPath();ctx.arc(x+c/2,y+c/2,c*(.6+.5*((time*1.2)%1)),0,7);ctx.stroke();
}

function render(t){
  ctx.setTransform(dpr,0,0,dpr,0,0);
  ctx.save();
  if(shake>0){ctx.translate(rnd(-shake,shake),rnd(-shake,shake));shake*=.88;if(shake<.3)shake=0;}
  ctx.drawImage(bg,0,0,W,H);
  const pts=segs.map(s=>segPos(s,t));
  const tail=segs[segs.length-1];
  drawTrack([...pts,{x:(tail.px+.5)*C,y:(tail.py+.5)*C}]);
  drawPassenger();
  // couplers
  ctx.strokeStyle='#333';ctx.lineWidth=C*.08;
  for(let i=0;i<pts.length-1;i++){ctx.beginPath();ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[i+1].x,pts[i+1].y);ctx.stroke();}
  for(let i=segs.length-1;i>0;i--)drawWagon(pts[i],segs[i],i);
  drawLoco(pts[0]);
  // smoke
  if(alive&&Math.random()<.5){const h=pts[0];particles.push({x:h.x+Math.cos(h.a)*C*.31,y:h.y+Math.sin(h.a)*C*.31,vx:rnd(-.3,.3),vy:rnd(-.8,-.3),life:1,type:'smoke',r:C*.12});}
  for(const p of particles){
    if(p.type==='smoke'){ctx.fillStyle=`rgba(240,240,240,${p.life*.55})`;ctx.beginPath();ctx.arc(p.x,p.y,p.r*(2-p.life),0,7);ctx.fill();}
    else{ctx.fillStyle=p.col;ctx.globalAlpha=p.life;ctx.fillRect(p.x,p.y,p.r,p.r);ctx.globalAlpha=1;}
  }
  ctx.textAlign='center';ctx.textBaseline='middle';
  for(const f of floaters){ctx.globalAlpha=f.life;ctx.fillStyle='#fff';ctx.strokeStyle='#0008';ctx.lineWidth=3;ctx.font=`bold ${Math.max(12,C*.55)}px Trebuchet MS`;ctx.strokeText(f.txt,f.x,f.y);ctx.fillText(f.txt,f.x,f.y);ctx.globalAlpha=1;}
  ctx.restore();
  // overlays
  if(demo){banner('Click or press a key to start','🚂 Demo mode — the AI is driving the train',.5);}
  else if(!alive){banner('Derailed! 💥','Score '+score+' · Click or press a key to ride again',.5);}
}
function banner(a,b){
  const bw=Math.min(W*.9,560),bh=Math.max(70,C*2.8);
  ctx.fillStyle='rgba(20,28,55,.78)';rr((W-bw)/2,H*.5-bh/2,bw,bh,14);ctx.fill();
  ctx.strokeStyle='#ffd34d';ctx.lineWidth=2;ctx.stroke();
  const pulse=1+Math.sin(time*4)*.04;
  ctx.save();ctx.translate(W/2,H*.5-bh*.15);ctx.scale(pulse,pulse);
  ctx.fillStyle='#ffd34d';ctx.font=`bold ${Math.max(16,Math.min(30,W/22))}px Trebuchet MS`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(a,0,0);ctx.restore();
  ctx.fillStyle='#fff';ctx.font=`${Math.max(11,Math.min(16,W/45))}px Trebuchet MS`;ctx.textAlign='center';ctx.fillText(b,W/2,H*.5+bh*.25);
}

function loop(now){
  const dt=Math.min(.05,(now-lastT)/1000);lastT=now;time+=dt;
  if(alive){acc+=dt*1000;while(acc>=tickMs&&alive){acc-=tickMs;step();}}
  else{deadTimer+=dt;if(demo&&deadTimer>1.5)reset();}
  for(const s of segs)if(s.pop!==undefined&&s.pop<1)s.pop=Math.min(1,s.pop+dt*4);
  for(const p of particles){p.x+=p.vx;p.y+=p.vy;if(p.type==='conf'){p.vy+=.15;p.life-=dt*1.2;}else p.life-=dt*1.1;}
  particles=particles.filter(p=>p.life>0);
  for(const f of floaters){f.y-=dt*30;f.life-=dt*.9;}
  floaters=floaters.filter(f=>f.life>0);
  const t=alive?Math.min(1,acc/tickMs):1;
  render(t);
  requestAnimationFrame(loop);
}

// ---------- input ----------
function startPlay(){demo=false;particles=[];floaters=[];reset();}
const KEYS={ArrowUp:{x:0,y:-1},ArrowDown:{x:0,y:1},ArrowLeft:{x:-1,y:0},ArrowRight:{x:1,y:0},w:{x:0,y:-1},s:{x:0,y:1},a:{x:-1,y:0},d:{x:1,y:0},W:{x:0,y:-1},S:{x:0,y:1},A:{x:-1,y:0},D:{x:1,y:0}};
window.addEventListener('keydown',e=>{
  if(e.key.startsWith('Arrow')||e.key===' ')e.preventDefault();
  if(demo||!alive){if(!alive&&deadTimer<.4)return;startPlay();const d=KEYS[e.key];if(d&&!(d.x===-1&&d.y===0))queue.push(d);return;}
  const d=KEYS[e.key];if(d&&queue.length<3)queue.push(d);
});
cv.addEventListener('click',()=>{if(demo||(!alive&&deadTimer>.4))startPlay();});
let tx=0,ty=0;
cv.addEventListener('touchstart',e=>{const t=e.touches[0];tx=t.clientX;ty=t.clientY;e.preventDefault();},{passive:false});
cv.addEventListener('touchend',e=>{
  const t=e.changedTouches[0],dx=t.clientX-tx,dy=t.clientY-ty;
  if(demo||!alive){if(alive||deadTimer>.4)startPlay();return;}
  if(Math.max(Math.abs(dx),Math.abs(dy))<20)return;
  queue.push(Math.abs(dx)>Math.abs(dy)?{x:Math.sign(dx),y:0}:{x:0,y:Math.sign(dy)});
  e.preventDefault();
},{passive:false});
window.addEventListener('resize',resize);

resize();reset();requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (2,979 tokens)
I'm planning a complete HTML snake-style train game with passengers on platforms, a demo AI mode, and click or key to start, aiming for something compact but polished. I'm sketching a grid canvas around 24 by 16 cells, with grass, rails under the train, and a locomotive with chimney smoke.

I'm working through the AI as well: a breadth-first search to the passenger that routes around the body, falling back to whichever move leaves the most open space. Movement will interpolate between ticks, and the canvas will scale to the window with touch swipe support added.

Still working out the track drawing: I'm stroking the path with dashed brown sleepers first, then layering gray rails on top so they read as two parallel lines.

I'm settling on the track look now: a wide light-brown ballast strip, then dashed brown sleepers over it, and I'm weighing whether gray rails drawn on top would hide the sleepers, which would look off even at this small size.

I'm drawing the track as a ballast strip with dashed sleepers, and I'll let the wagons cover most of it, so only the gaps between them show the path. For the passengers, I'm picturing a gray platform tile with a yellow edge, a bobbing figure in a colored shirt, and a small sign, possibly with a few waiting at once.

I'm settling on the passenger and wagon details now: each pickup adds a wagon in a cycling color, with a little passenger head showing through the window. I'm also planning confetti and "+10" floating text on pickup, plus chimney smoke puffs, and I'll speed the game up slightly from a 120ms tick. The controls take arrow keys or WASD with queued directions, and the demo overlay tells the player to click or press a key to start.

Still writing the game loop...

Still working out the snake update: I'm storing each segment's previous and current position so the body can slide smoothly between ticks, and I'm working through how the new tail segment gets added when the snake grows.

Still working out the segment motion: I'm storing each part's old and new position, and turning its angle smoothly toward the direction it moved. The locomotive will be drawn facing right, with a cab, a boiler, and a chimney up front.

Still working on the drawing details: the locomotive's front light and cowcatcher, then the wagons with their roof stripes and a passenger's head peeking from each window. I'm also planning a pickup's wagon colour, a grass background with a few flowers and a border fence, and a HUD bar above the canvas for score, passengers and best.

Next I'm setting the canvas size from the window, fitting the grid to the available width and height. Then I'm placing food on a random free cell and drawing the platform with a passenger standing on it, plus a smoke puff effect.

I'm adding smoke puffs that spawn from the chimney, drifting up while growing and fading out, and I'll add swipe support using touch start and end events.