Models 44
Train snakeOne-line

Muse Ember

Open in new window

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

How it went

Total time
37.0 s
Cost
$0.0077
Output tokens
15,235
Spent thinking
5,428 tokens
Speed
436.7 tok/s
Console errors
0
First token after
2.1 s
First visible text after
20.0 s
Prompt tokens
380
Code size
27.8 KB
Finish reason
stop
Served by
Meta
Model id
meta/muse-ember
Reasoning sent
{"effort":"xhigh"}
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 · 589 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Passenger Express — Little Train Snake</title>
<style>
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{width:100%;min-height:100%}
  body{
    font-family:ui-rounded,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
    background:linear-gradient(180deg,#7ed6ff 0%,#c8f0ff 22%,#fff7d6 78%,#ffe9a8 100%);
    color:#1e2b3a;overflow-x:hidden;
  }
  .wrap{max-width:1160px;margin:0 auto;padding:14px 14px 22px}
  header{
    display:flex;align-items:center;gap:14px;background:#fff;border-radius:22px;
    padding:12px 16px;box-shadow:0 10px 28px rgba(30,80,120,.18);border:3px solid #fff3b0;
  }
  .logo{
    width:62px;height:62px;flex:0 0 62px;border-radius:18px;display:grid;place-items:center;font-size:38px;
    background:linear-gradient(145deg,#ff5a5f,#e63946);box-shadow:inset 0 -6px 0 rgba(0,0,0,.18),0 6px 14px rgba(230,57,70,.35);
    transform:rotate(-3deg)
  }
  h1{font-size:28px;line-height:1;letter-spacing:.3px;color:#173a5e}
  h1 span{color:#e63946}
  header p{margin-top:6px;font-size:14px;color:#5b7186;font-weight:600}
  .mode{
    margin-left:auto;background:#173a5e;color:#fff;font-weight:800;font-size:12px;letter-spacing:.8px;
    padding:9px 12px;border-radius:999px;white-space:nowrap;box-shadow:0 6px 14px rgba(23,58,94,.3)
  }
  .mode.player{background:#06a377}
  main{display:flex;gap:16px;margin-top:16px;align-items:flex-start}
  .board-card{
    flex:1 1 auto;min-width:0;background:#fff;border-radius:24px;padding:10px;
    box-shadow:0 16px 36px rgba(30,80,120,.22);border:3px solid #ffffff
  }
  .canvas-holder{position:relative;border-radius:18px;overflow:hidden;background:#7ec850;line-height:0}
  canvas{width:100%;height:auto;display:block;aspect-ratio:4/3;touch-action:none;cursor:pointer}
  #overlay{
    position:absolute;inset:0;line-height:1.3;display:flex;align-items:center;justify-content:center;
    background:linear-gradient(180deg,rgba(10,30,55,.32),rgba(10,30,55,.58));backdrop-filter:blur(1.5px);
    padding:16px;transition:opacity .25s ease;z-index:5
  }
  #overlay.hidden{opacity:0;pointer-events:none}
  .overlay-card{
    background:#fff;border-radius:22px;padding:18px 20px;text-align:center;max-width:430px;width:100%;
    box-shadow:0 18px 40px rgba(0,0,0,.35);border:4px solid #ffd166
  }
  .overlay-card .train-emoji{font-size:46px;line-height:1}
  .overlay-card h2{font-size:26px;color:#173a5e;margin:6px 0 6px}
  .overlay-card p{font-size:14px;color:#5b7186;font-weight:600}
  .keys{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin:12px 0}
  .keys span{background:#eef6ff;border:2px solid #cfe8ff;color:#173a5e;font-weight:800;font-size:12px;padding:6px 9px;border-radius:999px}
  #playBtn{
    border:0;cursor:pointer;background:linear-gradient(180deg,#ff6b6b,#e63946);color:#fff;font-weight:900;letter-spacing:.4px;
    font-size:17px;padding:13px 24px;border-radius:999px;box-shadow:0 8px 0 #b81f2d,0 14px 22px rgba(230,57,70,.35);width:100%
  }
  #playBtn:active{transform:translateY(4px);box-shadow:0 4px 0 #b81f2d}
  .controls-bar{display:flex;gap:8px;padding-top:10px}
  .controls-bar button{
    flex:1;border:0;cursor:pointer;font-weight:900;font-size:13px;padding:11px 8px;border-radius:14px;color:#173a5e;background:#eef6ff;border:2px solid #d7ecff
  }
  .controls-bar button:active{transform:scale(.97)}
  aside{flex:0 0 300px;width:300px;display:flex;flex-direction:column;gap:12px}
  .card{background:#fff;border-radius:20px;padding:14px;box-shadow:0 10px 24px rgba(30,80,120,.16);border:3px solid rgba(255,255,255,.9)}
  .card h3{font-size:13px;letter-spacing:1px;text-transform:uppercase;color:#7a93aa;margin-bottom:10px}
  .stats{display:grid;grid-template-columns:1fr 1fr;gap:10px}
  .stat{background:linear-gradient(180deg,#f4fbff,#e8f4ff);border-radius:16px;padding:11px;border:2px solid #d7ecff;text-align:center}
  .stat .label{font-size:11px;font-weight:800;letter-spacing:.6px;color:#6c87a0;text-transform:uppercase}
  .stat .value{font-size:26px;font-weight:900;color:#173a5e;line-height:1.1;margin-top:2px}
  .stat .sub{font-size:11px;font-weight:700;color:#8aa0b5}
  .how p{font-size:13.5px;line-height:1.55;color:#435b73;font-weight:600}
  .how b{color:#173a5e}
  .legend{display:flex;flex-direction:column;gap:8px;font-size:13px;font-weight:700;color:#435b73}
  .legend div{display:flex;align-items:center;gap:9px}
  .dot{width:22px;height:22px;border-radius:7px;flex:0 0 22px;display:grid;place-items:center;font-size:13px;border:2px solid rgba(0,0,0,.08)}
  .dots{display:flex;gap:3px}
  .dots i{width:14px;height:14px;border-radius:4px;display:inline-block;border:2px solid rgba(0,0,0,.08)}
  .dpad{display:grid;grid-template-columns:repeat(3,62px);grid-template-rows:repeat(3,52px);gap:7px;justify-content:center}
  .dpad button{
    border:0;cursor:pointer;border-radius:15px;background:linear-gradient(180deg,#ffffff,#eef6ff);color:#173a5e;font-size:21px;font-weight:900;
    box-shadow:0 5px 0 #c8dff5,0 8px 14px rgba(30,80,120,.18);border:2px solid #d7ecff
  }
  .dpad button:active{transform:translateY(3px);box-shadow:0 2px 0 #c8dff5}
  .dpad .empty{visibility:hidden}
  .dpad .up{grid-column:2;grid-row:1}.dpad .left{grid-column:1;grid-row:2}.dpad .down{grid-column:2;grid-row:2}.dpad .right{grid-column:3;grid-row:2}
  footer{margin-top:14px;text-align:center;font-size:12.5px;font-weight:700;color:#4d6a86;background:rgba(255,255,255,.72);padding:9px 12px;border-radius:999px}
  @media(max-width:900px){
    main{flex-direction:column}
    aside{width:100%;flex:none}
    .stats{grid-template-columns:repeat(4,1fr)}
    h1{font-size:23px}
    .mode{font-size:10px;padding:7px 9px}
    .logo{width:52px;height:52px;flex-basis:52px;font-size:30px}
  }
  @media(max-width:520px){
    .wrap{padding:10px 10px 16px}
    header{padding:10px 12px;gap:10px}
    header p{font-size:12px}
    .stats{grid-template-columns:1fr 1fr}
    .overlay-card{padding:14px}
    .overlay-card h2{font-size:22px}
    .controls-bar button{font-size:12px}
  }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="logo">🚂</div>
    <div>
      <h1>Passenger <span>Express</span></h1>
      <p>Drive the little train! Pick up passengers waiting on the platform.</p>
    </div>
    <div class="mode" id="modeBadge">● DEMO MODE</div>
  </header>

  <main>
    <section class="board-card">
      <div class="canvas-holder" id="holder">
        <canvas id="game" width="1000" height="750"></canvas>
        <div id="overlay">
          <div class="overlay-card">
            <div class="train-emoji">🚃💨</div>
            <h2 id="overlayTitle">All Aboard!</h2>
            <p id="overlayText">The little train is already running in demo mode. Click or press a key to take the controls and pick up passengers!</p>
            <div class="keys"><span>⬆️ ⬇️ ⬅️ ➡️</span><span>W A S D</span><span>Swipe</span><span>🎫 +10 pts</span></div>
            <button id="playBtn">▶ &nbsp;Start Driving</button>
          </div>
        </div>
      </div>
      <div class="controls-bar">
        <button id="pauseBtn">⏸ Pause</button>
        <button id="restartBtn">↻ Restart</button>
        <button id="soundBtn">🔊 Sound: On</button>
      </div>
    </section>

    <aside>
      <div class="card">
        <h3>🚉 Station Board</h3>
        <div class="stats">
          <div class="stat"><div class="label">Score</div><div class="value" id="scoreVal">0</div><div class="sub">points</div></div>
          <div class="stat"><div class="label">Passengers</div><div class="value" id="pickVal">0</div><div class="sub">on board</div></div>
          <div class="stat"><div class="label">Wagons</div><div class="value" id="wagonVal">4</div><div class="sub">colorful</div></div>
          <div class="stat"><div class="label">Best</div><div class="value" id="bestVal">0</div><div class="sub" id="speedVal">0 km/h</div></div>
        </div>
      </div>

      <div class="card how">
        <h3>How to play</h3>
        <p>Your snake is a <b>little train</b>. The red <b>locomotive</b> leads and colorful <b>wagons</b> follow behind. Drive onto a waiting <b>passenger on the platform</b> (top or bottom) to pick them up — the train grows by one wagon! ⭐ Golden VIP passengers are worth <b>50 points</b>. Don't crash into walls or your own wagons!</p>
      </div>

      <div class="card">
        <h3>Legend</h3>
        <div class="legend">
          <div><span class="dot" style="background:#e63946">🚂</span> Locomotive — you drive this!</div>
          <div><span class="dot" style="background:#fff;padding:0"><span class="dots"><i style="background:#ef476f"></i><i style="background:#ffd166"></i><i style="background:#06d6a0"></i></span></span> Colorful wagons — grow as you ride</div>
          <div><span class="dot" style="background:#ffe8a3">🙋</span> Passenger waiting on platform</div>
          <div><span class="dot" style="background:#d8d3c3">🏫</span> Platform — pickups happen here</div>
        </div>
      </div>

      <div class="card">
        <h3>Controls</h3>
        <div class="dpad">
          <button class="up" data-dir="up" aria-label="Up">▲</button>
          <button class="left" data-dir="left" aria-label="Left">◀</button>
          <button class="down" data-dir="down" aria-label="Down">▼</button>
          <button class="right" data-dir="right" aria-label="Right">▶</button>
        </div>
      </div>
    </aside>
  </main>
  <footer>💡 Tip: Passengers wait on the top &amp; bottom platforms — plan your route early, the train gets faster as it grows!</footer>
</div>

<script>
'use strict';
const COLS=20, ROWS=15, CELL=50;
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const overlay=document.getElementById('overlay');
const overlayTitle=document.getElementById('overlayTitle');
const overlayText=document.getElementById('overlayText');
const playBtn=document.getElementById('playBtn');
const modeBadge=document.getElementById('modeBadge');
const scoreVal=document.getElementById('scoreVal'), pickVal=document.getElementById('pickVal'),
      wagonVal=document.getElementById('wagonVal'), bestVal=document.getElementById('bestVal'), speedVal=document.getElementById('speedVal');

const WAGON_COLORS=['#ef476f','#ffd166','#06d6a0','#118ab2','#9b5de5','#f15bb5','#ff9f1c','#4cc9f0','#80ed99','#ff8fa3'];
const SHIRTS=['#ef476f','#118ab2','#06d6a0','#ff9f1c','#9b5de5','#f15bb5','#4cc9f0','#e63946','#2a9d8f','#f4a261'];
const SKINS=['#ffdbac','#f1c27d','#e0ac69','#c68642','#8d5524','#ffe0bd'];
const HAIRS=['#2b2b2b','#6b4423','#f2c14e','#c0c0c0','#8b0000','#3a3a3a','#a0522d'];

let snake, prevSnake, dir, pendingDir, passengers, particles, score, picked, best=0, growing, tickMs, lastTick;
let demo=true, paused=false, gameOver=false, soundOn=true, nextVip=false;
let audioCtx=null, lastSmoke=0, lastFrame=performance.now();

const tufts=[]; for(let i=0;i<90;i++) tufts.push({x:Math.random()*COLS,y:2+Math.random()*(ROWS-4),r:2+Math.random()*3});

function rnd(a){return a[Math.floor(Math.random()*a.length)]}
function key(p){return p.x+','+p.y}
function lerp(a,b,t){return a+(b-a)*t}

function resetGame(){
  snake=[{x:9,y:7},{x:8,y:7},{x:7,y:7},{x:6,y:7},{x:5,y:7}];
  prevSnake=snake.map(s=>({...s}));
  dir={x:1,y:0}; pendingDir=null;
  passengers=[]; particles=[]; score=0; picked=0; growing=0; nextVip=false;
  tickMs=165; gameOver=false; paused=false;
  document.getElementById('pauseBtn').textContent='⏸ Pause';
  for(let i=0;i<3;i++) spawnPassenger(false);
  lastTick=performance.now();
  updateHUD();
}
function spawnPassenger(vip){
  for(let tries=0;tries<40;tries++){
    const y=Math.random()<0.5?1:ROWS-2;
    const x=1+Math.floor(Math.random()*(COLS-2));
    const p={x,y};
    if(snake.some(s=>s.x===x&&s.y===y)) continue;
    if(passengers.some(o=>o.x===x&&o.y===y)) continue;
    passengers.push({x,y,vip:!!vip,shirt:vip?'#ffd700':rnd(SHIRTS),skin:rnd(SKINS),hair:rnd(HAIRS),off:Math.random()*6.28});
    return;
  }
}
function updateHUD(){
  scoreVal.textContent=score; pickVal.textContent=picked;
  wagonVal.textContent=Math.max(0,snake.length-1); bestVal.textContent=best;
  speedVal.textContent=Math.round(1150/tickMs)+' km/h';
}
function tone(f,d,type,vol,delay){
  if(!soundOn) return;
  try{
    if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
    if(audioCtx.state==='suspended') audioCtx.resume();
    const o=audioCtx.createOscillator(), g=audioCtx.createGain(), t=audioCtx.currentTime+(delay||0);
    o.type=type||'sine'; o.frequency.value=f;
    g.gain.setValueAtTime(vol||0.12,t); g.gain.exponentialRampToValueAtTime(0.001,t+d);
    o.connect(g); g.connect(audioCtx.destination); o.start(t); o.stop(t+d+0.02);
  }catch(e){}
}
function whistle(){ tone(740,.18,'triangle',.14,0); tone(988,.28,'triangle',.14,.12); }
function pickupSound(vip){ tone(vip?990:660,.12,'sine',.14,0); tone(vip?1320:880,.18,'sine',.14,.08); }
function crashSound(){ tone(220,.25,'sawtooth',.12,0); tone(140,.4,'sawtooth',.12,.12); }

/* ---------- AI (demo) ---------- */
function aiDirection(){
  if(!passengers.length) return dir;
  const obstacles=new Set();
  const tailExcl=(growing===0)?snake.length-1:snake.length;
  for(let i=1;i<tailExcl;i++) obstacles.add(snake[i].x+','+snake[i].y);
  const targets=new Set(passengers.map(p=>p.x+','+p.y));
  const head=snake[0], startK=head.x+','+head.y;
  const q=[head], seen=new Map([[startK,null]]);
  let found=null;
  const dirs=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
  while(q.length){
    const cur=q.shift(), ck=cur.x+','+cur.y;
    if(targets.has(ck)&&!(cur.x===head.x&&cur.y===head.y)){found=ck;break;}
    for(const d of dirs){
      const nx=cur.x+d.x, ny=cur.y+d.y, nk=nx+','+ny;
      if(nx<0||ny<0||nx>=COLS||ny>=ROWS||seen.has(nk)||obstacles.has(nk)) continue;
      seen.set(nk,ck); q.push({x:nx,y:ny});
    }
  }
  if(found){
    let cur=found, prev=seen.get(cur);
    while(prev!==null && prev!==undefined && prev!==startK){cur=prev; prev=seen.get(cur);}
    if(prev===startK){
      const [cx,cy]=cur.split(',').map(Number);
      const d={x:cx-head.x,y:cy-head.y};
      if(!(d.x===-dir.x&&d.y===-dir.y)) return d;
    }
  }
  // safe fallback: pick move with most open space
  let bestD=dir,bestSpace=-1;
  for(const d of dirs){
    if(d.x===-dir.x&&d.y===-dir.y) continue;
    const nx=head.x+d.x, ny=head.y+d.y;
    if(nx<0||ny<0||nx>=COLS||ny>=ROWS||obstacles.has(nx+','+ny)) continue;
    let space=0; const st=[[nx,ny]], vis=new Set([nx+','+ny]);
    while(st.length){const [x,y]=st.pop(); space++; for(const dd of dirs){const ax=x+dd.x,ay=y+dd.y,ak=ax+','+ay; if(ax<0||ay<0||ax>=COLS||ay>=ROWS||vis.has(ak)||obstacles.has(ak))continue; vis.add(ak); st.push([ax,ay]);}}
    if(space>bestSpace){bestSpace=space;bestD=d;}
  }
  return bestD;
}

/* ---------- Game step ---------- */
function step(){
  prevSnake=snake.map(s=>({...s}));
  let nd=demo?aiDirection():(pendingDir||dir);
  if(nd.x===-dir.x&&nd.y===-dir.y) nd=dir;
  dir=nd; pendingDir=null;
  const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y};

  // wall collision
  if(head.x<0||head.y<0||head.x>=COLS||head.y>=ROWS){crash();return;}
  // self collision
  const limit=(growing===0)?snake.length-1:snake.length;
  for(let i=0;i<limit;i++){ if(snake[i].x===head.x&&snake[i].y===head.y){crash();return;} }

  snake.unshift(head);

  const idx=passengers.findIndex(p=>p.x===head.x&&p.y===head.y);
  if(idx>=0){
    const p=passengers.splice(idx,1)[0];
    picked++; growing++;
    const pts=p.vip?50:10; score+=pts;
    if(score>best) best=score;
    pickupSound(p.vip);
    burst((head.x+0.5)*CELL,(head.y+0.5)*CELL,p.vip?'#ffd700':'#ffffff');
    // boarding sparkle toward wagons + "on board" popup
    particles.push({x:(head.x+0.5)*CELL,y:(head.y+0.5)*CELL,vx:0,vy:-26,life:1,max:1,size:16,color:'#fff',type:'text',text:'+'+pts});
    if(picked%5===0) nextVip=true;
    spawnPassenger(nextVip); nextVip=false;
  }
  if(growing>0) growing--; else snake.pop();
  tickMs=Math.max(85,165-picked*4);
  updateHUD();
}
function burst(x,y,color){
  for(let i=0;i<14;i++){
    const a=Math.random()*6.28, sp=40+Math.random()*90;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-30,life:.7+Math.random()*.5,max:1.1,size:3+Math.random()*5,color:Math.random()<.4?'#ffd700':color,type:'star'});
  }
}
function crash(){
  gameOver=true; crashSound();
  const h=snake[0]; burst((h.x+0.5)*CELL,(h.y+0.5)*CELL,'#ff5a5f');
  if(demo){
    setTimeout(()=>{ if(demo&&gameOver) resetGame(); },1600);
  }else{
    setTimeout(()=>{
      overlayTitle.textContent='💥 Oh no! Crash!';
      overlayText.textContent='Score: '+score+' • Passengers: '+picked+' • Best: '+best+'. The little train will run again — press play!';
      playBtn.textContent='↻  Play Again';
      overlay.classList.remove('hidden');
    },650);
  }
}

/* ---------- Drawing ---------- */
function draw(now,t){
  // ground
  const g=ctx.createLinearGradient(0,0,0,750); g.addColorStop(0,'#9be184'); g.addColorStop(1,'#6fbf4a');
  ctx.fillStyle=g; ctx.fillRect(0,0,1000,750);
  // checker grass subtle
  for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++){
    if((x+y)%2===0){ctx.fillStyle='rgba(255,255,255,0.05)';ctx.fillRect(x*CELL,y*CELL,CELL,CELL);}
  }
  // platform pavement (top & bottom strips)
  ctx.fillStyle='#ddd6c2'; ctx.fillRect(0,0,1000,CELL*2); ctx.fillRect(0,(ROWS-2)*CELL,1000,CELL*2);
  ctx.fillStyle='rgba(0,0,0,0.05)';
  for(let x=0;x<COLS;x+=2){ctx.fillRect(x*CELL+6,6,CELL-12,3);ctx.fillRect(x*CELL+6,(ROWS-2)*CELL+6,CELL-12,3);}
  // tracks on middle rows
  ctx.strokeStyle='rgba(90,70,50,0.35)'; ctx.lineWidth=3;
  for(let y=2;y<ROWS-2;y++){
    const cy=(y+0.5)*CELL;
    ctx.beginPath(); ctx.moveTo(0,cy-9); ctx.lineTo(1000,cy-9); ctx.moveTo(0,cy+9); ctx.lineTo(1000,cy+9); ctx.stroke();
    ctx.fillStyle='rgba(120,90,55,0.30)';
    for(let x=0;x<COLS;x++) ctx.fillRect(x*CELL+4,cy-3,CELL-8,6);
  }
  // platform edge lines
  ctx.fillStyle='#ffd166'; ctx.fillRect(0,CELL*2-5,1000,5); ctx.fillRect(0,(ROWS-2)*CELL,1000,5);
  ctx.fillStyle='#173a5e'; ctx.fillRect(0,CELL*2,1000,2); ctx.fillRect(0,(ROWS-2)*CELL-2,1000,2);
  // platform details: benches, lamps, sign
  drawPlatformProps(now);
  // grass tufts
  ctx.fillStyle='rgba(40,110,40,0.45)';
  tufts.forEach(tf=>{ctx.beginPath();ctx.arc(tf.x*CELL+25,tf.y*CELL+25,tf.r,0,6.28);ctx.fill();});
  // passengers
  passengers.forEach(p=>drawPassenger(p,now));
  // particles behind train for smoke handled later; draw pickup particles
  drawParticles(false);
  // train
  drawTrain(now,t);
  drawParticles(true);
  // border fence
  ctx.strokeStyle='#8b5a2b'; ctx.lineWidth=8; ctx.strokeRect(4,4,992,742);
  ctx.strokeStyle='#fff'; ctx.lineWidth=3; ctx.strokeRect(10,10,980,730);
  // corner bushes
  [[40,120],[960,120],[40,630],[960,630]].forEach(b=>{ctx.fillStyle='#2f9e44';ctx.beginPath();ctx.arc(b[0],b[1],16,0,6.28);ctx.fill();ctx.fillStyle='#37b24d';ctx.beginPath();ctx.arc(b[0]-6,b[1]-6,10,0,6.28);ctx.fill();});
}
function drawPlatformProps(now){
  // station sign top center
  ctx.fillStyle='#173a5e'; roundRect(340,8,320,34,10); ctx.fill();
  ctx.fillStyle='#ffd166'; ctx.font='900 17px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle';
  ctx.fillText('★ SUNNY STATION ★',500,26);
  ctx.fillStyle='#173a5e'; roundRect(340,(ROWS-1)*CELL+8,320,34,10); ctx.fill();
  ctx.fillStyle='#fff'; ctx.fillText('SUNNY STATION',500,(ROWS-1)*CELL+25);
  // benches
  [110,500,890].forEach(x=>{ ctx.fillStyle='#8b5a2b'; ctx.fillRect(x,38,80,8); ctx.fillRect(x+6,46,10,14); ctx.fillRect(x+64,46,10,14);
                             ctx.fillRect(x,(ROWS-1)*CELL+44,80,8); ctx.fillRect(x+6,(ROWS-1)*CELL+52,10,14); ctx.fillRect(x+64,(ROWS-1)*CELL+52,10,14); });
  // lamps
  [60,950].forEach(x=>{ [30,(ROWS-1)*CELL+18].forEach(y=>{ ctx.fillStyle='#555'; ctx.fillRect(x-2,y,4,28); ctx.fillStyle='#ffe066'; ctx.beginPath(); ctx.arc(x,y-4,8,0,6.28); ctx.fill(); }); });
}
function drawPassenger(p,now){
  const cx=(p.x+0.5)*CELL, cy=(p.y+0.5)*CELL+Math.sin(now/280+p.off)*2.5;
  ctx.fillStyle='rgba(0,0,0,0.22)'; ctx.beginPath(); ctx.ellipse(cx,cy+16,13,5,0,0,6.28); ctx.fill();
  if(p.vip){ // sparkles
    ctx.fillStyle='#ffd700';
    for(let i=0;i<3;i++){const a=now/400+i*2.1; ctx.beginPath(); ctx.arc(cx+Math.cos(a)*18,cy-14+Math.sin(a)*8,2.5,0,6.28); ctx.fill();}
  }
  // body
  ctx.fillStyle=p.shirt; roundRect(cx-10,cy-2,20,20,7); ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.35)'; roundRect(cx-10,cy-2,20,7,7); ctx.fill();
  // head
  ctx.fillStyle=p.skin; ctx.beginPath(); ctx.arc(cx,cy-10,10,0,6.28); ctx.fill();
  ctx.fillStyle=p.hair; ctx.beginPath(); ctx.arc(cx,cy-13,10,Math.PI,0); ctx.fill();
  // eyes + smile
  ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(cx-3.5,cy-9,1.5,0,6.28); ctx.arc(cx+3.5,cy-9,1.5,0,6.28); ctx.fill();
  ctx.strokeStyle='#222'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(cx,cy-6,4,0.15*Math.PI,0.85*Math.PI); ctx.stroke();
  // waving arm
  const wave=Math.sin(now/160+p.off)*0.5;
  ctx.strokeStyle=p.skin; ctx.lineWidth=5; ctx.lineCap='round';
  ctx.beginPath(); ctx.moveTo(cx+9,cy+4); ctx.lineTo(cx+17,cy-8+wave*5); ctx.stroke();
  if(p.vip){ ctx.fillStyle='#ffd700'; ctx.beginPath(); ctx.moveTo(cx-7,cy-20); ctx.lineTo(cx-3,cy-27); ctx.lineTo(cx,cy-20); ctx.lineTo(cx+3,cy-27); ctx.lineTo(cx+7,cy-20); ctx.closePath(); ctx.fill(); }
  // waiting bubble
  const by=cy-30+Math.sin(now/300+p.off)*2;
  ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(cx+16,by,8,0,6.28); ctx.fill();
  ctx.fillStyle='#e63946'; ctx.font='900 11px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('!',cx+16,by+0.5);
}
function roundRect(x,y,w,h,r){
  ctx.beginPath();
  if(ctx.roundRect) ctx.roundRect(x,y,w,h,r); else ctx.rect(x,y,w,h);
}
function drawTrain(now,t){
  const pts=snake.map((s,i)=>{
    const prev=prevSnake[Math.min(i,prevSnake.length-1)]||s;
    const gx=lerp(prev.x,s.x,t), gy=lerp(prev.y,s.y,t);
    return {x:(gx+0.5)*CELL,y:(gy+0.5)*CELL};
  });
  // connectors
  ctx.strokeStyle='#333'; ctx.lineWidth=7; ctx.lineCap='round';
  for(let i=1;i<pts.length;i++){ctx.beginPath();ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[i-1].x,pts[i-1].y);ctx.stroke();}
  // wagons first (tail to front)
  for(let i=pts.length-1;i>=1;i--){
    const p=pts[i], ahead=pts[i-1];
    let ang=Math.atan2(ahead.y-p.y,ahead.x-p.x); if(!isFinite(ang)) ang=0;
    drawWagon(p.x,p.y,ang,WAGON_COLORS[(i-1)%WAGON_COLORS.length]);
  }
  // locomotive
  const hp=pts[0];
  let ang=Math.atan2(dir.y,dir.x);
  // refine angle from movement
  if(prevSnake[0]){const dx=hp.x-((prevSnake[0].x+0.5)*CELL), dy=hp.y-((prevSnake[0].y+0.5)*CELL); if(dx||dy) ang=Math.atan2(dy,dx);}
  drawLoco(hp.x,hp.y,ang);
  // smoke emission
  if(now-lastSmoke>200&&!gameOver){
    lastSmoke=now;
    const sx=hp.x+Math.cos(ang)*16, sy=hp.y+Math.sin(ang)*16;
    particles.push({x:sx,y:sy,vx:(Math.random()-.5)*12-6,vy:-34-Math.random()*18,life:1,max:1.3,size:7+Math.random()*6,color:'rgba(255,255,255,0.85)',type:'smoke'});
  }
}
function drawWagon(x,y,ang,color){
  ctx.save(); ctx.translate(x,y); ctx.rotate(ang);
  ctx.fillStyle='rgba(0,0,0,0.22)'; roundRect(-22,-14,44,30,8); ctx.fill();
  ctx.fillStyle=color; roundRect(-23,-17,46,34,8); ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.45)'; roundRect(-23,-17,46,10,8); ctx.fill();
  ctx.fillStyle='#dff4ff';
  [-14,-4,6].forEach(wx=>{roundRect(wx,-9,8,10,2);ctx.fill();});
  ctx.fillStyle='#222';
  [-15,9].forEach(wx=>{ctx.fillRect(wx,14,10,6);ctx.fillRect(wx,-20,10,6);});
  ctx.fillStyle='#fff'; ctx.font='900 11px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('★',0,9);
  ctx.restore();
}
function drawLoco(x,y,ang){
  ctx.save(); ctx.translate(x,y); ctx.rotate(ang);
  ctx.fillStyle='rgba(0,0,0,0.25)'; roundRect(-23,-15,47,32,9); ctx.fill();
  // body
  ctx.fillStyle='#e63946'; roundRect(-24,-18,48,36,9); ctx.fill();
  ctx.fillStyle='#c1121f'; roundRect(-24,-18,20,36,9); ctx.fill(); // cabin end
  ctx.fillStyle='#ffd166'; roundRect(18,-18,6,36,3); ctx.fill(); // front plate
  // boiler stripe
  ctx.fillStyle='#ff8fa3'; roundRect(-6,-10,24,20,6); ctx.fill();
  // chimney (front)
  ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(12,0,8,0,6.28); ctx.fill();
  ctx.fillStyle='#333'; ctx.beginPath(); ctx.arc(12,0,5,0,6.28); ctx.fill();
  // cabin window
  ctx.fillStyle='#bde0fe'; roundRect(-19,-11,12,22,3); ctx.fill();
  // headlight
  ctx.fillStyle='#fff9c4'; ctx.beginPath(); ctx.arc(22,0,5,0,6.28); ctx.fill();
  ctx.strokeStyle='#f59f00'; ctx.lineWidth=2; ctx.stroke();
  // wheels
  ctx.fillStyle='#111';
  [-16,2,14].forEach(wx=>{ctx.fillRect(wx,15,11,6);ctx.fillRect(wx,-21,11,6);});
  // whistle
  ctx.fillStyle='#ffd700'; ctx.beginPath(); ctx.arc(-2,-6,3,0,6.28); ctx.fill();
  ctx.restore();
}
function drawParticles(front){
  particles=particles.filter(p=>p.life>0);
  particles.forEach(p=>{
    if(front===(p.type==='smoke')) return;
    const a=Math.max(0,p.life/p.max);
    ctx.globalAlpha=a;
    if(p.type==='text'){ctx.fillStyle='#fff';ctx.font='900 18px system-ui';ctx.textAlign='center';ctx.fillText(p.text,p.x,p.y);}
    else if(p.type==='star'){ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.size*a+1,0,6.28);ctx.fill();}
    ctx.globalAlpha=1;
  });
}
function updateParticles(dt){
  particles.forEach(p=>{
    p.life-=dt;
    p.x+=p.vx*dt; p.y+=p.vy*dt;
    if(p.type==='smoke'){p.size+=10*dt; p.vy-=12*dt;}
    if(p.type==='star'){p.vy+=90*dt;}
  });
}

/* ---------- Loop ---------- */
function loop(now){
  requestAnimationFrame(loop);
  const dt=Math.min(0.05,(now-lastFrame)/1000); lastFrame=now;
  let t=1;
  if(!paused&&!gameOver){
    if(now-lastTick>=tickMs){step(); lastTick=now;}
    t=Math.min(1,(now-lastTick)/tickMs);
    updateParticles(dt);
  }else{
    updateParticles(dt); t=1;
  }
  draw(now,t);
}
requestAnimationFrame(loop);

/* ---------- Interaction / controls ---------- */
function hideOverlay(){overlay.classList.add('hidden');}
function startPlayer(){
  if(demo){
    demo=false; modeBadge.textContent='● PLAYER MODE'; modeBadge.classList.add('player');
    hideOverlay(); resetGame(); whistle();
  }else if(gameOver){
    hideOverlay(); resetGame(); whistle();
  }else{
    hideOverlay();
  }
}
function setDir(name){
  const map={up:{x:0,y:-1},down:{x:0,y:1},left:{x:-1,y:0},right:{x:1,y:0}};
  const d=map[name]; if(!d) return;
  // prevent reversing vs current direction (or queued)
  const ref=pendingDir||dir;
  if(!(d.x===-ref.x&&d.y===-ref.y)) pendingDir=d;
}
playBtn.addEventListener('click',e=>{e.stopPropagation();startPlayer();});
overlay.addEventListener('click',startPlayer);
canvas.addEventListener('click',()=>{ if(gameOver&&!demo) startPlayer(); else if(demo) startPlayer(); });

window.addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  const map={arrowup:'up',w:'up',arrowdown:'down',s:'down',arrowleft:'left',a:'left',arrowright:'right',d:'right'};
  if(map[k]){ e.preventDefault(); }
  if(demo||gameOver){ startPlayer(); }
  if(map[k]) setDir(map[k]);
  if(k===' '||k==='enter'){ e.preventDefault(); if(paused) togglePause(); }
});
function togglePause(){
  if(gameOver) return;
  paused=!paused;
  document.getElementById('pauseBtn').textContent=paused?'▶ Resume':'⏸ Pause';
  if(paused) lastTick=performance.now();
}
document.getElementById('pauseBtn').addEventListener('click',togglePause);
document.getElementById('restartBtn').addEventListener('click',()=>{ if(demo){resetGame();} else { hideOverlay(); resetGame(); } });
document.getElementById('soundBtn').addEventListener('click',function(){ soundOn=!soundOn; this.textContent=soundOn?'🔊 Sound: On':'🔇 Sound: Off'; if(soundOn) tone(880,.1,'sine',.1,0); });

document.querySelectorAll('.dpad button').forEach(b=>{
  b.addEventListener('pointerdown',e=>{e.preventDefault(); if(demo||gameOver) startPlayer(); setDir(b.dataset.dir);});
});

// swipe
let tsx=0,tsy=0;
canvas.addEventListener('touchstart',e=>{const t=e.touches[0];tsx=t.clientX;tsy=t.clientY;},{passive:true});
canvas.addEventListener('touchend',e=>{
  const t=e.changedTouches[0], dx=t.clientX-tsx, dy=t.clientY-tsy;
  if(Math.hypot(dx,dy)<24) return;
  if(demo||gameOver) startPlayer();
  setDir(Math.abs(dx)>Math.abs(dy)?(dx>0?'right':'left'):(dy>0?'down':'up'));
},{passive:true});

resetGame();
</script>
</body>
</html>
Reasoning trace (5,428 tokens)
Designing a self-contained canvas snake game with a locomotive and wagons theme, adapting layout for desktop and mobile with an initial demo mode.Refining the grid layout with platform zones and passenger spawn rules for pickup mechanics.Extending the grid with train movement, BFS AI, passenger pickup effects, and responsive canvas UI.Adding top-down train rendering, smoke and boarding animations, WebAudio sounds, and mobile D-pad controls.Implementing interpolation, BFS AI, input handling, HUD updates, and responsive layout for the train game.Detailing overlay layout, BFS collision handling, and rendering for the train game.Finalizing train rendering with orientation, particles, and UI controls.