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
47.3 s
Cost
$0.012
Output tokens
24,432
Spent thinking
10,979 tokens
Speed
554.1 tok/s
Console errors
0
First token after
3.2 s
First visible text after
29.9 s
Prompt tokens
380
Code size
39.1 KB
Finish reason
stop
Served by
Meta
Model id
meta/muse-ember
Reasoning sent
{"effort":"max"}
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 · 792 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passenger Express — Little Train Snake</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;overflow-x:hidden}
  body{
    font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
    background:linear-gradient(180deg,#bfe8ff 0%,#e6f7d9 55%,#fff3c4 100%);
    color:#1e2a22;min-height:100vh;
  }
  header{
    max-width:1240px;margin:0 auto;padding:14px 14px 8px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;
  }
  .logo{
    width:62px;height:62px;border-radius:18px;background:linear-gradient(145deg,#d32f2f,#ff7043);
    display:flex;align-items:center;justify-content:center;font-size:36px;box-shadow:0 8px 20px rgba(0,0,0,.2);
    border:3px solid #fff;flex:0 0 auto;
  }
  header h1{margin:0;font-size:30px;line-height:1;letter-spacing:.3px;color:#17365d}
  header h1 span{color:#d32f2f}
  header p{margin:4px 0 0;font-size:14px;color:#40513f;max-width:560px}
  .badge{
    margin-left:auto;background:#fff;border:2px solid #ffca28;color:#7a5200;font-weight:800;font-size:12px;
    padding:8px 12px;border-radius:999px;display:flex;align-items:center;gap:8px;box-shadow:0 4px 12px rgba(0,0,0,.12)
  }
  .dot{width:9px;height:9px;background:#43a047;border-radius:50%;animation:pulse 1s infinite alternate}
  @keyframes pulse{from{transform:scale(1);opacity:1}to{transform:scale(1.5);opacity:.55}}
  main{max-width:1240px;margin:0 auto;padding:8px 14px 18px;display:flex;gap:16px;align-items:flex-start;justify-content:center;flex-wrap:wrap}
  .canvas-wrap{position:relative;flex:1 1 640px;max-width:880px;min-width:0}
  canvas{
    width:100%;height:auto;display:block;border-radius:18px;border:5px solid #2f3e2e;
    box-shadow:0 18px 40px rgba(20,40,20,.28);background:#7fb069;touch-action:none;
  }
  aside{flex:0 1 310px;min-width:0;display:flex;flex-direction:column;gap:12px}
  .card{background:rgba(255,255,255,.94);border-radius:18px;padding:14px;box-shadow:0 10px 24px rgba(0,0,0,.14);border:2px solid rgba(255,255,255,.9)}
  .card h2{margin:0 0 10px;font-size:16px;color:#17365d}
  .stats{display:grid;grid-template-columns:repeat(2,1fr);gap:9px}
  .stat{background:#eef6ff;border-radius:13px;padding:10px 11px;border:2px solid #d6e8ff}
  .stat b{display:block;font-size:24px;line-height:1;color:#17365d}
  .stat small{font-size:11px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:#6a7d6b}
  .stat.pink{background:#fff0f3;border-color:#ffd1dc}.stat.yellow{background:#fff8db;border-color:#ffe58a}
  .stat.green{background:#eaffed;border-color:#b9f0c2}.stat.orange{background:#fff1e6;border-color:#ffd9b3}
  #statusText{margin:10px 0 0;font-size:13px;font-weight:700;color:#40513f;background:#fffbe6;border:1px dashed #e6c84a;padding:8px 10px;border-radius:10px}
  .waiting-line{font-size:14px;line-height:1.5}
  .wait-people{font-size:26px;letter-spacing:4px;margin:6px 0 4px}
  .wait-person{display:inline-block;animation:bob .8s infinite alternate ease-in-out}
  .wait-person:nth-child(2){animation-delay:.15s}.wait-person:nth-child(3){animation-delay:.3s}
  @keyframes bob{from{transform:translateY(0)}to{transform:translateY(-5px)}}
  .btns{display:grid;grid-template-columns:1fr 1fr;gap:9px}
  button{font-family:inherit;cursor:pointer;border:none;border-radius:13px;padding:12px 10px;font-weight:900;font-size:14px;box-shadow:0 5px 0 rgba(0,0,0,.18);transition:transform .08s, box-shadow .08s}
  button:active{transform:translateY(3px);box-shadow:0 2px 0 rgba(0,0,0,.18)}
  .primary{background:#43a047;color:#fff;grid-column:span 2;font-size:16px}
  .secondary{background:#ff9800;color:#fff}
  .blue{background:#1e88e5;color:#fff}
  .purple{background:#8e24aa;color:#fff}
  .light{background:#eef2f7;color:#17365d}
  .dpad{display:grid;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(3,48px);gap:6px;justify-content:center;margin-top:6px}
  .dpad button{background:#17365d;color:#fff;font-size:20px;padding:0;border-radius:12px}
  .dpad .up{grid-column:2;grid-row:1}.dpad .left{grid-column:1;grid-row:2}.dpad .right{grid-column:3;grid-row:2}.dpad .down{grid-column:2;grid-row:2}
  .how{margin:0;padding-left:18px;font-size:13.5px;line-height:1.55;color:#334433}
  .legend{font-size:13.5px;line-height:1.7}
  .overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(14,26,38,.42);border-radius:18px;padding:12px;z-index:5}
  .overlay.hidden{display:none}
  .overlay-card{background:#fff;border-radius:22px;padding:22px 22px 18px;max-width:440px;width:100%;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,.35);border:4px solid #ffca28}
  .overlay-card .train-emoji{font-size:54px;line-height:1}
  .overlay-card h3{margin:6px 0 6px;font-size:28px;color:#17365d}
  .overlay-card p{margin:6px 0;font-size:14.5px;line-height:1.5;color:#40513f}
  .keys{display:inline-block;background:#eef6ff;border:2px solid #bcd8ff;border-radius:8px;padding:2px 8px;font-weight:900;color:#17365d}
  .big-start{background:#d32f2f;color:#fff;font-size:18px;padding:14px 18px;width:100%;margin-top:12px}
  .demo-note{background:#fff8db;border-radius:10px;padding:8px;font-size:12.5px;font-weight:800;color:#7a5200;margin-top:10px}
  #pauseBadge{position:absolute;top:14px;left:50%;transform:translateX(-50%);background:#17365d;color:#fff;font-weight:900;letter-spacing:2px;padding:8px 16px;border-radius:999px;z-index:4;box-shadow:0 8px 20px rgba(0,0,0,.3)}
  #pauseBadge.hidden{display:none}
  footer{max-width:1240px;margin:0 auto;padding:0 14px 18px;font-size:12.5px;color:#5a6b5a;text-align:center}
  .hidden{display:none!important}
  @media(max-width:900px){
    aside{flex:1 1 100%;max-width:880px}
    .stats{grid-template-columns:repeat(4,1fr)}
  }
  @media(max-width:560px){
    header h1{font-size:23px}.logo{width:50px;height:50px;font-size:28px}
    .badge{margin-left:0}
    .stats{grid-template-columns:repeat(2,1fr)}
    .overlay-card{padding:16px}
    .overlay-card h3{font-size:23px}
    main{padding:6px 8px 12px}
  }
</style>
</head>
<body>
<header>
  <div class="logo">🚂</div>
  <div>
    <h1>Passenger <span>Express</span></h1>
    <p>The snake is a little train! Drive the locomotive, collect passengers from the yellow platform, and grow your colorful wagons — don't crash into walls or your own tail.</p>
  </div>
  <div class="badge" id="demoBadge"><span class="dot"></span><span id="demoBadgeText">🤖 DEMO MODE — AI is driving</span></div>
</header>

<main>
  <section class="canvas-wrap" id="canvasWrap">
    <canvas id="game" width="880" height="600"></canvas>
    <div id="pauseBadge" class="hidden">⏸ PAUSED</div>

    <div class="overlay" id="startOverlay">
      <div class="overlay-card">
        <div class="train-emoji">🚂💨</div>
        <h3>Passenger Express</h3>
        <p>Drive the little train! Follow the pulsing ring to the <b>yellow platform</b> and pick up the waving passenger. Each passenger adds a colorful wagon!</p>
        <p><span class="keys">← ↑ ↓ →</span> or <span class="keys">W A S D</span> to steer &nbsp;•&nbsp; <span class="keys">P</span> pause &nbsp;•&nbsp; <span class="keys">SPACE</span> whistle</p>
        <p style="font-size:13px">📱 Swipe on the track or use the D-pad below.</p>
        <button class="big-start" id="overlayStartBtn">▶ Click or press a key to start</button>
        <div class="demo-note">🤖 A demo train is already driving behind this card — watch it collect passengers!</div>
      </div>
    </div>

    <div class="overlay hidden" id="overOverlay">
      <div class="overlay-card">
        <div class="train-emoji">💥🚃</div>
        <h3>Oh no! Derailment!</h3>
        <p>Your train bumped into a wall or its own wagons.</p>
        <p>Passengers delivered: <b id="finalPassengers">0</b><br>Score: <b id="finalScore">0</b> &nbsp;•&nbsp; Best: <b id="finalBest">0</b></p>
        <button class="big-start" id="playAgainBtn">↻ Play Again</button>
        <div class="demo-note">Tip: Press R or Enter to restart quickly.</div>
      </div>
    </div>
  </section>

  <aside>
    <div class="card">
      <h2>📊 Journey Stats</h2>
      <div class="stats">
        <div class="stat pink"><small>Passengers</small><b id="passengersVal">0</b></div>
        <div class="stat yellow"><small>Score</small><b id="scoreVal">0</b></div>
        <div class="stat green"><small>Wagons</small><b id="wagonsVal">5</b></div>
        <div class="stat orange"><small>Best Score</small><b id="bestVal">0</b></div>
        <div class="stat"><small>Level</small><b id="levelVal">1</b></div>
        <div class="stat pink"><small>Speed</small><b id="speedVal">Normal</b></div>
      </div>
      <div id="statusText">🤖 Demo running — click the track or press any arrow key to take the controls!</div>
    </div>

    <div class="card">
      <h2>🧍 Waiting on the Platform</h2>
      <div class="wait-people"><span class="wait-person">🧍‍♀️</span><span class="wait-person">🧍‍♂️</span><span class="wait-person">👧</span><span class="wait-person">👦</span></div>
      <div class="waiting-line" id="waitingInfo">A passenger is waving at the yellow platform…</div>
    </div>

    <div class="card">
      <h2>🎛 Controls</h2>
      <div class="btns">
        <button class="primary" id="startBtn">▶ Start / Pause</button>
        <button class="secondary" id="restartBtn">↻ Restart</button>
        <button class="blue" id="soundBtn">🔊 Sound</button>
        <button class="purple" id="speedBtn">⚡ Speed: Normal</button>
        <button class="light" id="whistleBtn">💨 Whistle!</button>
      </div>
      <div class="dpad" aria-label="Direction pad">
        <button class="up" data-dx="0" data-dy="-1" aria-label="Up">▲</button>
        <button class="left" data-dx="-1" data-dy="0" aria-label="Left">◀</button>
        <button class="down" data-dx="0" data-dy="1" aria-label="Down">▼</button>
        <button class="right" data-dx="1" data-dy="0" aria-label="Right">▶</button>
      </div>
    </div>

    <div class="card">
      <h2>📖 How to play</h2>
      <ul class="how">
        <li>Steer with <b>arrows / WASD</b>, swipe, or the D-pad.</li>
        <li>Drive onto the <b>yellow platform</b> to pick up the waving passenger.</li>
        <li>Every passenger adds one colorful wagon and +100 points.</li>
        <li>You get faster as you travel — watch your tail!</li>
        <li>Don't hit the fence or your own train. <b>P</b> pauses, <b>Space</b> whistles.</li>
      </ul>
      <div class="legend" style="margin-top:8px">🔴 Locomotive &nbsp; 🟦 Wagons &nbsp; 🟨 Platform &nbsp; ⭐ Pickup target</div>
    </div>
  </aside>
</main>
<footer>Little Train Snake • Pick up passengers, grow your wagons, and try not to derail! 🚂</footer>

<script>
(function(){
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const COLS = 22, ROWS = 15, CELL = 40;
  canvas.width = COLS*CELL; canvas.height = ROWS*CELL;

  const passengersVal = document.getElementById('passengersVal');
  const scoreVal = document.getElementById('scoreVal');
  const wagonsVal = document.getElementById('wagonsVal');
  const bestVal = document.getElementById('bestVal');
  const levelVal = document.getElementById('levelVal');
  const speedVal = document.getElementById('speedVal');
  const statusText = document.getElementById('statusText');
  const waitingInfo = document.getElementById('waitingInfo');
  const startOverlay = document.getElementById('startOverlay');
  const overOverlay = document.getElementById('overOverlay');
  const pauseBadge = document.getElementById('pauseBadge');
  const demoBadgeText = document.getElementById('demoBadgeText');

  const names = ["Emma","Liam","Olivia","Noah","Ava","Mia","Lucas","Zoe","Leo","Ruby","Max","Nina","Ollie","Ivy","Felix","Poppy","Sam","Luna"];
  const shirts = ["#e53935","#1e88e5","#43a047","#fb8c00","#8e24aa","#00acc1","#d81b60","#fdd835","#3949ab","#00bfa5"];
  const skins = ["#ffdbac","#f1c27d","#e0ac69","#8d5524","#ffdbac","#c68642"];
  const hairs = ["#3e2723","#6d4c41","#ffd54f","#212121","#a1887f","#ff7043","#5d4037"];
  const stations = ["Central","Riverside","Hilltop","Sunnyvale","Seaside","Market","Garden"];
  const wagonColors = ["#1e88e5","#43a047","#fb8c00","#8e24aa","#00acc1","#d81b60","#fdd835","#3949ab","#7cb342","#fb8c00"];

  let snake, prevSnake, dir, dirQueue, food, passengers, score, best=0, level, lastTick, running=true, paused=false, demo=true, gameOver=false, userStarted=false;
  let particles=[], floats=[], smokes=[], decor=[];
  let soundOn=true, audioCtx=null, speedFactor=1, speedIndex=0;
  const speedModes=[{f:1,label:"Normal"},{f:0.72,label:"Fast"},{f:0.55,label:"Turbo"},{f:1.35,label:"Slow"}];
  let shake=0, lastSmoke=0, lastHeadInfo={x:100,y:100,angle:0};

  function rand(a,b){return Math.floor(Math.random()*(b-a+1))+a}
  function pick(arr){return arr[Math.floor(Math.random()*arr.length)]}
  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 makePerson(){
    return {name:pick(names), shirt:pick(shirts), skin:pick(skins), hair:pick(hairs), station:pick(stations), platform:rand(1,4)};
  }
  function ensureAudio(){
    try{
      if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)();
      if(audioCtx.state==='suspended') audioCtx.resume();
    }catch(e){}
  }
  function tone(freq,dur,type,vol,delay){
    if(!soundOn||!audioCtx) return;
    try{
      const t=audioCtx.currentTime+(delay||0);
      const o=audioCtx.createOscillator(), g=audioCtx.createGain();
      o.type=type||'sine'; o.frequency.setValueAtTime(freq,t);
      g.gain.setValueAtTime(vol||0.12,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur);
      o.connect(g); g.connect(audioCtx.destination); o.start(t); o.stop(t+dur+0.02);
    }catch(e){}
  }
  function playPickup(){ tone(660,.12,'sine',.14,0); tone(880,.16,'sine',.14,.09); tone(1320,.12,'triangle',.08,.16); }
  function playCrash(){ tone(180,.35,'sawtooth',.16,0); tone(90,.5,'sawtooth',.14,.08); }
  function playWhistleSound(){ tone(740,.18,'square',.07,0); tone(988,.22,'square',.07,.14); tone(740,.28,'square',.06,.38); }
  function playClick(){ tone(520,.07,'triangle',.08,0); }

  function initDecor(){
    decor=[];
    for(let i=0;i<90;i++) decor.push({x:Math.random()*COLS, y:Math.random()*ROWS, c:pick(["#ffeb3b","#ff8a80","#ffffff","#ffccbc"]), s:2+Math.random()*2});
  }
  initDecor();

  function platformCells(){
    if(!food) return [];
    if(food.horizontal) return [{x:food.x-1,y:food.y},{x:food.x,y:food.y},{x:food.x+1,y:food.y}];
    return [{x:food.x,y:food.y-1},{x:food.x,y:food.y},{x:food.x,y:food.y+1}];
  }

  function spawnFood(){
    let pos, attempts=0;
    while(attempts<600){
      pos={x:rand(0,COLS-1), y:rand(0,ROWS-1)}; attempts++;
      const onSnake = snake.some(s=>s.x===pos.x&&s.y===pos.y);
      const dist = Math.abs(pos.x-snake[0].x)+Math.abs(pos.y-snake[0].y);
      if(!onSnake && (dist>=4 || attempts>300)) break;
    }
    const canH = pos.x>=1 && pos.x<=COLS-2;
    const canV = pos.y>=1 && pos.y<=ROWS-2;
    let horizontal=true;
    if(canH && canV) horizontal = Math.random()<0.65;
    else if(canV) horizontal=false; else horizontal=true;
    food={x:pos.x,y:pos.y,horizontal:horizontal, person:makePerson(), queue:[makePerson(),makePerson()]};
    waitingInfo.innerHTML = "👤 <b>"+food.person.name+"</b> is waiting at <b>"+food.person.station+" Station</b>, Platform "+food.person.platform+"!<br>Next: "+food.queue[0].name+" &amp; "+food.queue[1].name+" are in the queue. Drive to the <b>yellow platform ⭐</b>!";
    updateHUD();
  }

  function calcTick(){ return Math.max(68, 158 - passengers*4) * speedFactor; }

  function resetGame(isDemo){
    demo=isDemo;
    snake=[{x:5,y:7},{x:4,y:7},{x:3,y:7},{x:2,y:7},{x:1,y:7}];
    prevSnake=snake.map(s=>({x:s.x,y:s.y}));
    dir={x:1,y:0}; dirQueue=[];
    passengers=0; score=0; level=1; particles=[]; floats=[]; smokes=[]; shake=0;
    gameOver=false; paused=false; running=true; lastTick=performance.now(); lastSmoke=0;
    spawnFood();
    overOverlay.classList.add('hidden');
    pauseBadge.classList.add('hidden');
    if(isDemo){
      startOverlay.classList.remove('hidden');
      demoBadgeText.textContent="🤖 DEMO MODE — AI is driving";
      statusText.textContent="🤖 Demo running — click the track or press any arrow key to take the controls!";
    }else{
      startOverlay.classList.add('hidden');
      demoBadgeText.textContent="🎮 YOU ARE DRIVING!";
      statusText.textContent="🎮 You're driving! Head to the yellow platform to pick up the passenger. Good luck!";
    }
    updateHUD();
  }

  function updateHUD(){
    passengersVal.textContent=passengers;
    scoreVal.textContent=score;
    wagonsVal.textContent=snake?snake.length:5;
    bestVal.textContent=best;
    levelVal.textContent=level;
    speedVal.textContent=speedModes[speedIndex].label;
  }

  function queueDir(dx,dy){
    const last = dirQueue.length?dirQueue[dirQueue.length-1]:dir;
    if(dx===-last.x && dy===-last.y) return;
    if(dx===last.x && dy===last.y) return;
    if(dirQueue.length<3) dirQueue.push({x:dx,y:dy});
  }

  function startFromDemo(){
    if(userStarted) return;
    userStarted=true; ensureAudio(); playClick();
    resetGame(false);
  }

  // AI BFS
  function aiTurn(){
    const head=snake[0];
    const blocked=new Set();
    for(let i=1;i<snake.length-1;i++) blocked.add(snake[i].x+','+snake[i].y);
    const visited=new Set([head.x+','+head.y]);
    const q=[{x:head.x,y:head.y,first: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 && !found){
      const cur=q.shift();
      for(const d of dirs){
        const nx=cur.x+d.x, ny=cur.y+d.y;
        if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
        const k=nx+','+ny;
        if(visited.has(k)||blocked.has(k)) continue;
        visited.add(k);
        const first = cur.first || {x:nx,y:ny};
        if(nx===food.x && ny===food.y){ found=first; break; }
        q.push({x:nx,y:ny,first:first});
      }
    }
    if(found){
      const dx=found.x-head.x, dy=found.y-head.y;
      if(!(dx===-dir.x && dy===-dir.y)){ dirQueue=[]; queueDir(dx,dy); }
      return;
    }
    // safe wander
    const shuffled=dirs.slice().sort(()=>Math.random()-0.5);
    for(const d of shuffled){
      const nx=head.x+d.x, ny=head.y+d.y;
      if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
      if(blocked.has(nx+','+ny)) continue;
      if(d.x===-dir.x && d.y===-dir.y) continue;
      dirQueue=[]; queueDir(d.x,d.y); return;
    }
  }

  function step(){
    prevSnake=snake.map(s=>({x:s.x,y:s.y}));
    if(dirQueue.length) dir=dirQueue.shift();
    const head=snake[0];
    const nh={x:head.x+dir.x, y:head.y+dir.y};
    // wall
    if(nh.x<0||nh.y<0||nh.x>=COLS||nh.y>=ROWS){ doCrash(); return; }
    // self (exclude tail that will move, unless eating)
    const willEat = (nh.x===food.x && nh.y===food.y);
    const bodyToCheck = willEat ? snake : snake.slice(0,-1);
    if(bodyToCheck.some(s=>s.x===nh.x&&s.y===nh.y)){ doCrash(); return; }

    snake.unshift(nh);
    if(willEat){
      passengers++;
      const gained=100+(level-1)*25;
      score+=gained;
      level=1+Math.floor(passengers/4);
      playPickup();
      // particles
      const cx=nh.x*CELL+CELL/2, cy=nh.y*CELL+CELL/2;
      for(let i=0;i<18;i++){
        particles.push({x:cx,y:cy,vx:(Math.random()-0.5)*5,vy:(Math.random()-0.7)*5,life:1,sz:3+Math.random()*5,c:pick(["#ffeb3b","#ff4081","#ffffff","#69f0ae","#ff9800"])});
      }
      floats.push({x:cx,y:cy-18,text:"+1 Passenger aboard! +"+gained,life:1});
      floats.push({x:cx,y:cy+6,text:food.person.name+" says Choo! 🚂",life:1});
      spawnFood();
    }else{
      snake.pop();
    }
    updateHUD();
  }

  function doCrash(){
    gameOver=true; shake=14; playCrash();
    const cx=snake[0].x*CELL+CELL/2, cy=snake[0].y*CELL+CELL/2;
    for(let i=0;i<26;i++) smokes.push({x:cx,y:cy,r:8+Math.random()*14,life:1,vy:-0.6-Math.random(),vx:(Math.random()-0.5)*1.4});
    for(let i=0;i<20;i++) particles.push({x:cx,y:cy,vx:(Math.random()-0.5)*6,vy:(Math.random()-0.5)*6,life:1,sz:3+Math.random()*5,c:"#ff7043"});
    if(demo){
      setTimeout(()=>{ if(demo) resetGame(true); },1400);
    }else{
      if(score>best) best=score;
      document.getElementById('finalPassengers').textContent=passengers;
      document.getElementById('finalScore').textContent=score;
      document.getElementById('finalBest').textContent=best;
      overOverlay.classList.remove('hidden');
      statusText.textContent="💥 Derailment! Press Play Again or R to try another journey.";
      updateHUD();
    }
  }

  function whistle(){
    ensureAudio(); playWhistleSound();
    const h=lastHeadInfo;
    floats.push({x:h.x,y:h.y-24,text:"♪ Choo-choo! ♪",life:1.2});
    for(let i=0;i<10;i++) smokes.push({x:h.x,y:h.y,r:6+Math.random()*10,life:1,vy:-1-Math.random(),vx:(Math.random()-0.5)*2});
  }

  // Drawing helpers
  function interp(i,alpha){
    const cur=snake[i];
    const prev=prevSnake[Math.min(i,prevSnake.length-1)] || cur;
    return {x:prev.x+(cur.x-prev.x)*alpha, y:prev.y+(cur.y-prev.y)*alpha};
  }

  function drawBackground(){
    const g=ctx.createLinearGradient(0,0,0,canvas.height);
    g.addColorStop(0,"#8bc07a"); g.addColorStop(1,"#6da35c");
    ctx.fillStyle=g; ctx.fillRect(0,0,canvas.width,canvas.height);
    // track bed cells
    for(let y=0;y<ROWS;y++){
      for(let x=0;x<COLS;x++){
        ctx.fillStyle=((x+y)%2===0)?"#c9bda0":"#bcb090";
        ctx.fillRect(x*CELL+1,y*CELL+1,CELL-2,CELL-2);
        // sleepers (horizontal)
        ctx.fillStyle="rgba(101,67,33,0.55)";
        for(let s=0;s<3;s++) ctx.fillRect(x*CELL+3, y*CELL+5+s*13, CELL-6, 4);
        // horizontal rails
        ctx.strokeStyle="rgba(230,230,230,0.9)"; ctx.lineWidth=2;
        ctx.beginPath(); ctx.moveTo(x*CELL, y*CELL+13); ctx.lineTo(x*CELL+CELL, y*CELL+13); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(x*CELL, y*CELL+27); ctx.lineTo(x*CELL+CELL, y*CELL+27); ctx.stroke();
        // vertical rails faint
        ctx.strokeStyle="rgba(230,230,230,0.35)"; ctx.lineWidth=1.5;
        ctx.beginPath(); ctx.moveTo(x*CELL+13, y*CELL); ctx.lineTo(x*CELL+13, y*CELL+CELL); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(x*CELL+27, y*CELL); ctx.lineTo(x*CELL+27, y*CELL+CELL); ctx.stroke();
      }
    }
    // decor flowers / grass tufts at intersections
    for(const d of decor){
      ctx.fillStyle=d.c; ctx.globalAlpha=0.65;
      ctx.beginPath(); ctx.arc(d.x*CELL, d.y*CELL, d.s, 0, Math.PI*2); ctx.fill();
      ctx.globalAlpha=1;
    }
    // corner trees
    const trees=[[18,18],[canvas.width-18,18],[18,canvas.height-18],[canvas.width-18,canvas.height-18]];
    for(const t of trees){
      ctx.fillStyle="rgba(0,0,0,0.18)"; ctx.beginPath(); ctx.ellipse(t[0]+3,t[1]+4,16,8,0,0,Math.PI*2); ctx.fill();
      ctx.fillStyle="#2e7d32"; ctx.beginPath(); ctx.arc(t[0],t[1],15,0,Math.PI*2); ctx.fill();
      ctx.fillStyle="#43a047"; ctx.beginPath(); ctx.arc(t[0]-4,t[1]-4,9,0,Math.PI*2); ctx.fill();
      ctx.fillStyle="#6d4c41"; ctx.fillRect(t[0]-2,t[1]+8,4,8);
    }
    // fence border
    ctx.strokeStyle="#5d4037"; ctx.lineWidth=6; ctx.strokeRect(3,3,canvas.width-6,canvas.height-6);
    ctx.fillStyle="#8d6e63";
    for(let x=0;x<=COLS;x++) for(let y=0;y<=ROWS;y++){
      if(x===0||y===0||x===COLS||y===ROWS){
        ctx.fillRect(x*CELL-3, y*CELL-3, 6, 6);
      }
    }
  }

  function drawPerson(cx,cy,scale,person,waving,now,highlight){
    ctx.save(); ctx.translate(cx,cy);
    const bob = waving ? Math.sin(now/180)*2.2 : 0;
    ctx.translate(0,bob);
    // shadow
    ctx.fillStyle="rgba(0,0,0,0.22)"; ctx.beginPath(); ctx.ellipse(0,16*scale,11*scale,4*scale,0,0,Math.PI*2); ctx.fill();
    // legs
    ctx.fillStyle="#37474f"; ctx.fillRect(-6*scale,6*scale,5*scale,10*scale); ctx.fillRect(1*scale,6*scale,5*scale,10*scale);
    ctx.fillStyle="#263238"; ctx.fillRect(-7*scale,15*scale,7*scale,3*scale); ctx.fillRect(0,15*scale,7*scale,3*scale);
    // body shirt
    ctx.fillStyle=person.shirt; rr(-8*scale,-8*scale,16*scale,16*scale,5*scale); ctx.fill();
    // arms
    ctx.strokeStyle=person.skin; ctx.lineWidth=4*scale; ctx.lineCap="round";
    ctx.beginPath(); ctx.moveTo(-8*scale,-4*scale); ctx.lineTo(-13*scale,3*scale); ctx.stroke();
    if(waving){
      const a=-0.6+Math.sin(now/140)*0.5;
      const hx=8*scale+Math.cos(a)*10*scale, hy=-4*scale+Math.sin(a)*10*scale;
      ctx.beginPath(); ctx.moveTo(8*scale,-4*scale); ctx.lineTo(hx,hy); ctx.stroke();
      ctx.fillStyle=person.skin; ctx.beginPath(); ctx.arc(hx,hy,3.5*scale,0,Math.PI*2); ctx.fill();
    }else{
      ctx.beginPath(); ctx.moveTo(8*scale,-4*scale); ctx.lineTo(13*scale,3*scale); ctx.stroke();
    }
    // head
    ctx.fillStyle=person.skin; ctx.beginPath(); ctx.arc(0,-14*scale,8*scale,0,Math.PI*2); ctx.fill();
    // hair
    ctx.fillStyle=person.hair; ctx.beginPath(); ctx.arc(0,-17*scale,8*scale,Math.PI,Math.PI*2); ctx.fill();
    ctx.fillRect(-8*scale,-18*scale,16*scale,4*scale);
    // face
    ctx.fillStyle="#212121"; ctx.beginPath(); ctx.arc(-3*scale,-13*scale,1.1*scale,0,Math.PI*2); ctx.arc(3*scale,-13*scale,1.1*scale,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle="#212121"; ctx.lineWidth=1.2; ctx.beginPath(); ctx.arc(0,-11*scale,3*scale,0.15*Math.PI,0.85*Math.PI); ctx.stroke();
    // bag for target
    if(highlight){
      ctx.fillStyle="#ffca28"; rr(9*scale,2*scale,8*scale,9*scale,2*scale); ctx.fill();
      ctx.strokeStyle="#8d6e63"; ctx.lineWidth=1; ctx.strokeRect(11*scale,-2*scale,4*scale,5*scale);
    }
    ctx.restore();
  }

  function drawPlatform(now){
    if(!food) return;
    const cells=platformCells();
    const minX=Math.min(...cells.map(c=>c.x)), maxX=Math.max(...cells.map(c=>c.x));
    const minY=Math.min(...cells.map(c=>c.y)), maxY=Math.max(...cells.map(c=>c.y));
    const px=minX*CELL+3, py=minY*CELL+3, pw=(maxX-minX+1)*CELL-6, ph=(maxY-minY+1)*CELL-6;
    // pulse ring around target
    const cx=food.x*CELL+CELL/2, cy=food.y*CELL+CELL/2;
    const pulse=1+Math.sin(now/220)*0.12;
    ctx.save(); ctx.globalAlpha=0.55+Math.sin(now/200)*0.2;
    ctx.strokeStyle="#ffeb3b"; ctx.lineWidth=4; ctx.beginPath(); ctx.arc(cx,cy,26*pulse,0,Math.PI*2); ctx.stroke();
    ctx.restore();
    // platform base
    ctx.fillStyle="rgba(0,0,0,0.2)"; rr(px+3,py+5,pw,ph,10); ctx.fill();
    const grad=ctx.createLinearGradient(px,py,px,py+ph);
    grad.addColorStop(0,"#fff3a6"); grad.addColorStop(1,"#f9d976");
    ctx.fillStyle=grad; rr(px,py,pw,ph,10); ctx.fill();
    ctx.strokeStyle="#d4a017"; ctx.lineWidth=3; rr(px,py,pw,ph,10); ctx.stroke();
    // tactile dots along edge
    ctx.fillStyle="#d4a017";
    if(food.horizontal){
      for(let i=0;i<pw;i+=12){ ctx.beginPath(); ctx.arc(px+8+i, py+ph-7, 2,0,Math.PI*2); ctx.fill(); }
    }else{
      for(let i=0;i<ph;i+=12){ ctx.beginPath(); ctx.arc(px+pw-7, py+8+i, 2,0,Math.PI*2); ctx.fill(); }
    }
    // bench on a non-target cell
    const benchCell = cells.find(c=>!(c.x===food.x&&c.y===food.y)) || cells[0];
    const bx=benchCell.x*CELL+CELL/2, by=benchCell.y*CELL+CELL/2;
    ctx.fillStyle="#795548"; rr(bx-14,by-4,28,8,3); ctx.fill();
    ctx.fillStyle="#5d4037"; ctx.fillRect(bx-11,by+4,3,7); ctx.fillRect(bx+8,by+4,3,7);
    // extra waiting passengers (small)
    const others=cells.filter(c=>!(c.x===food.x&&c.y===food.y));
    others.forEach((c,idx)=>{
      const p=food.queue[idx%food.queue.length];
      drawPerson(c.x*CELL+CELL/2, c.y*CELL+CELL/2+2, 0.72, p, true, now+idx*220, false);
    });
    // sign
    const signCell=cells[0];
    const sx=signCell.x*CELL+CELL/2, sy=signCell.y*CELL+CELL/2-18;
    ctx.fillStyle="#546e7a"; ctx.fillRect(sx-1,sy+10,2,16);
    ctx.fillStyle="#ffffff"; rr(sx-30,sy-6,60,17,4); ctx.fill(); ctx.strokeStyle="#17365d"; ctx.lineWidth=1.5; rr(sx-30,sy-6,60,17,4); ctx.stroke();
    ctx.fillStyle="#17365d"; ctx.font="bold 8px system-ui,Arial"; ctx.textAlign="center";
    ctx.fillText(food.person.station.toUpperCase(), sx, sy+4);
    ctx.font="bold 7px system-ui,Arial"; ctx.fillStyle="#d32f2f"; ctx.fillText("PLATFORM "+food.person.platform, sx, sy+11);
    // main passenger
    drawPerson(cx,cy+2,1.15,food.person,true,now,true);
    // bouncing arrow
    const ay=cy-38+Math.sin(now/200)*4;
    ctx.fillStyle="#d32f2f"; ctx.beginPath(); ctx.moveTo(cx-9,ay); ctx.lineTo(cx+9,ay); ctx.lineTo(cx,ay+11); ctx.closePath(); ctx.fill();
    ctx.strokeStyle="#fff"; ctx.lineWidth=2; ctx.stroke();
    ctx.fillStyle="#fff"; ctx.font="bold 11px system-ui,Arial"; ctx.textAlign="center"; ctx.fillText("⭐", cx, ay-4);
    // speech bubble
    const bubbleText=food.person.name+" • Choo! 🚂";
    ctx.font="bold 11px system-ui,Arial";
    const bw=ctx.measureText(bubbleText).width+16;
    let bx2=cx-bw/2; if(bx2<6) bx2=6; if(bx2+bw>canvas.width-6) bx2=canvas.width-6-bw;
    const by2=Math.max(6,cy-72+Math.sin(now/300)*2);
    ctx.fillStyle="#ffffff"; rr(bx2,by2,bw,22,9); ctx.fill(); ctx.strokeStyle="#17365d"; ctx.lineWidth=1.5; rr(bx2,by2,bw,22,9); ctx.stroke();
    ctx.fillStyle="#17365d"; ctx.textAlign="center"; ctx.fillText(bubbleText,bx2+bw/2,by2+15);
    ctx.beginPath(); ctx.moveTo(cx-5,by2+22); ctx.lineTo(cx+5,by2+22); ctx.lineTo(cx,by2+29); ctx.closePath(); ctx.fillStyle="#fff"; ctx.fill();
  }

  function drawCar(cx,cy,angle,isLoco,index,now){
    const L=36,W=27;
    ctx.save(); ctx.translate(cx,cy); ctx.rotate(angle);
    // shadow
    ctx.fillStyle="rgba(0,0,0,0.22)"; ctx.beginPath(); ctx.ellipse(1,3,L/2,W/2+2,0,0,Math.PI*2); ctx.fill();
    // coupling
    if(!isLoco){ ctx.fillStyle="#263238"; ctx.fillRect(-L/2-4,-3,5,6); }
    // wheels
    const wheelR=isLoco?6:5;
    const wheelPos=[[-12,-W/2+2],[12,-W/2+2],[-12,W/2-2],[12,W/2-2]];
    for(const w of wheelPos){
      ctx.fillStyle="#212121"; ctx.beginPath(); ctx.arc(w[0],w[1],wheelR,0,Math.PI*2); ctx.fill();
      ctx.fillStyle="#9e9e9e"; ctx.beginPath(); ctx.arc(w[0],w[1],2.2,0,Math.PI*2); ctx.fill();
      // spokes rotating
      const wa=now/90;
      ctx.strokeStyle="#616161"; ctx.lineWidth=1;
      ctx.beginPath(); ctx.moveTo(w[0]-Math.cos(wa)*wheelR,w[1]-Math.sin(wa)*wheelR); ctx.lineTo(w[0]+Math.cos(wa)*wheelR,w[1]+Math.sin(wa)*wheelR); ctx.stroke();
    }
    if(isLoco){
      // body
      const g=ctx.createLinearGradient(0,-W/2,0,W/2); g.addColorStop(0,"#ef5350"); g.addColorStop(0.5,"#c62828"); g.addColorStop(1,"#8e0000");
      ctx.fillStyle=g; rr(-L/2,-W/2,L,W,6); ctx.fill(); ctx.strokeStyle="#5f0000"; ctx.lineWidth=2; rr(-L/2,-W/2,L,W,6); ctx.stroke();
      // boiler front
      ctx.fillStyle="#7f0000"; rr(2,-W/2+3,L/2-5,W-6,5); ctx.fill();
      // cab window
      ctx.fillStyle="#b3e5fc"; rr(-L/2+4,-W/2+4,14,8,2); ctx.fill(); rr(-L/2+4,W/2-12,14,8,2); ctx.fill();
      ctx.fillStyle="#fff"; ctx.font="bold 10px system-ui,Arial"; ctx.textAlign="center"; ctx.fillText("1",-L/2+11,3);
      // chimney
      ctx.fillStyle="#212121"; ctx.beginPath(); ctx.arc(12,0,7.5,0,Math.PI*2); ctx.fill();
      ctx.fillStyle="#616161"; ctx.beginPath(); ctx.arc(12,0,4,0,Math.PI*2); ctx.fill();
      // dome
      ctx.fillStyle="#ffca28"; ctx.beginPath(); ctx.arc(-2,0,5.5,0,Math.PI*2); ctx.fill(); ctx.strokeStyle="#c17900"; ctx.lineWidth=1; ctx.stroke();
      // headlight
      ctx.save(); ctx.shadowColor="#ffeb3b"; ctx.shadowBlur=10;
      ctx.fillStyle="#ffeb3b"; ctx.beginPath(); ctx.arc(L/2-1,0,4.5,0,Math.PI*2); ctx.fill(); ctx.restore();
      // cowcatcher
      ctx.fillStyle="#9e9e9e"; ctx.beginPath(); ctx.moveTo(L/2-2,-9); ctx.lineTo(L/2+7,0); ctx.lineTo(L/2-2,9); ctx.closePath(); ctx.fill(); ctx.strokeStyle="#616161"; ctx.stroke();
      // stripe
      ctx.fillStyle="#ffca28"; ctx.fillRect(-L/2+2,-2,L-4,4);
    }else{
      const col=wagonColors[(index-1)%wagonColors.length];
      const g=ctx.createLinearGradient(0,-W/2,0,W/2); g.addColorStop(0,col); g.addColorStop(1,"#26323833"); g.addColorStop(1,col);
      ctx.fillStyle=col; rr(-L/2,-W/2,L,W,6); ctx.fill(); ctx.strokeStyle="rgba(0,0,0,0.35)"; ctx.lineWidth=2; rr(-L/2,-W/2,L,W,6); ctx.stroke();
      // roof spine
      ctx.fillStyle="rgba(255,255,255,0.85)"; rr(-10,-4,20,8,3); ctx.fill();
      // windows strips top/bottom
      ctx.fillStyle="#e1f5fe"; rr(-13,-W/2+3,26,6,2); ctx.fill(); rr(-13,W/2-9,26,6,2); ctx.fill();
      ctx.strokeStyle="#0277bd"; ctx.lineWidth=1; rr(-13,-W/2+3,26,6,2); ctx.stroke(); rr(-13,W/2-9,26,6,2); ctx.stroke();
      // passenger heads in windows if delivered
      if(index <= passengers+2){
        ctx.fillStyle=skins[index%skins.length];
        ctx.beginPath(); ctx.arc(-6,-W/2+6,3,0,Math.PI*2); ctx.arc(6,-W/2+6,3,0,Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(-6,W/2-6,3,0,Math.PI*2); ctx.arc(6,W/2-6,3,0,Math.PI*2); ctx.fill();
      }
      // bottom stripe
      ctx.fillStyle="#ffffff"; ctx.globalAlpha=0.85; ctx.fillRect(-L/2+3,-1.5,L-6,3); ctx.globalAlpha=1;
      // wagon number
      ctx.fillStyle="rgba(255,255,255,0.95)"; ctx.font="bold 9px system-ui,Arial"; ctx.textAlign="center"; ctx.fillText(String(index),0,3);
    }
    ctx.restore();
  }

  function drawTrain(now,alpha){
    if(!snake) return;
    // compute head info for smoke
    const headP=interp(0,alpha);
    let headAngle=Math.atan2(dir.y,dir.x);
    if(snake.length>1){
      const p1=interp(1,alpha);
      // head points away from second car
      const dx=headP.x-p1.x, dy=headP.y-p1.y;
      if(Math.abs(dx)+Math.abs(dy)>0.01) headAngle=Math.atan2(dy,dx);
    }
    lastHeadInfo={x:(headP.x*CELL+CELL/2)+Math.cos(headAngle)*12, y:(headP.y*CELL+CELL/2)+Math.sin(headAngle)*12, angle:headAngle};
    // emit smoke
    if(!gameOver && now-lastSmoke>150){
      lastSmoke=now;
      smokes.push({x:lastHeadInfo.x,y:lastHeadInfo.y,r:5+Math.random()*4,life:1,vy:-0.7-Math.random()*0.5,vx:-Math.cos(headAngle)*0.3+(Math.random()-0.5)*0.4});
    }
    // draw from tail to head
    for(let i=snake.length-1;i>=0;i--){
      const p=interp(i,alpha);
      let angle;
      if(i===0) angle=headAngle;
      else{
        const front=interp(i-1,alpha);
        angle=Math.atan2(front.y-p.y, front.x-p.x);
        if(Math.abs(front.x-p.x)+Math.abs(front.y-p.y)<0.01) angle=headAngle;
      }
      drawCar(p.x*CELL+CELL/2, p.y*CELL+CELL/2, angle, i===0, i, now);
    }
  }

  function draw(now){
    ctx.save();
    if(shake>0){ ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake); shake*=0.88; if(shake<0.4) shake=0; }
    let alpha=1;
    if(running && !paused && !gameOver) alpha=Math.min(1,(now-lastTick)/calcTick());
    else alpha=1;
    drawBackground();
    drawPlatform(now);
    // smoke behind train
    for(const s of smokes){
      ctx.globalAlpha=Math.max(0,s.life)*0.55; ctx.fillStyle="#eeeeee";
      ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha=1;
    drawTrain(now,alpha);
    // particles
    for(const p of particles){
      ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.c;
      ctx.beginPath(); ctx.arc(p.x,p.y,p.sz,0,Math.PI*2); ctx.fill();
      // star sparkle
      ctx.globalAlpha=Math.max(0,p.life)*0.8; ctx.fillRect(p.x-1,p.y-p.sz*1.6,2,p.sz*3.2); ctx.fillRect(p.x-p.sz*1.6,p.y-1,p.sz*3.2,2);
    }
    ctx.globalAlpha=1;
    // floats
    for(const f of floats){
      ctx.globalAlpha=Math.max(0,f.life);
      ctx.font="bold 15px system-ui,Arial"; ctx.textAlign="center";
      ctx.strokeStyle="rgba(0,0,0,0.6)"; ctx.lineWidth=4; ctx.strokeText(f.text,f.x,f.y);
      ctx.fillStyle="#fff"; ctx.fillText(f.text,f.x,f.y);
    }
    ctx.globalAlpha=1;
    ctx.restore();
    // update particles/smoke/floats
    for(const s of smokes){ s.y+=s.vy; s.x+=s.vx; s.r+=0.14; s.life-=0.012; }
    smokes=smokes.filter(s=>s.life>0);
    for(const p of particles){ p.x+=p.vx; p.y+=p.vy; p.vy+=0.08; p.life-=0.022; }
    particles=particles.filter(p=>p.life>0);
    for(const f of floats){ f.y-=0.6; f.life-=0.014; }
    floats=floats.filter(f=>f.life>0);
  }

  function loop(now){
    requestAnimationFrame(loop);
    if(running && !paused && !gameOver && now-lastTick>=calcTick()){
      if(demo) aiTurn();
      step();
      lastTick=now;
    }
    draw(now);
  }

  // Events
  function handleDir(dx,dy){
    if(!userStarted) startFromDemo();
    ensureAudio();
    if(gameOver) return;
    if(paused){ paused=false; pauseBadge.classList.add('hidden'); lastTick=performance.now(); statusText.textContent="🎮 You're driving! Head to the yellow platform!"; }
    queueDir(dx,dy);
  }

  document.addEventListener('keydown',(e)=>{
    const k=e.key;
    const isArrow=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(k);
    if(isArrow) e.preventDefault();
    const lower=k.toLowerCase();
    if(!userStarted && (isArrow || ["w","a","s","d","enter","p","r","b"].includes(lower))){
      startFromDemo();
      // fall through to handle this key as action below (except Enter/P/R)
      if(k==="Enter"||lower==="p"||lower==="r") return;
    }
    if(gameOver && (k==="Enter"||lower==="r"||k===" ")){
      userStarted=true; resetGame(false); ensureAudio(); return;
    }
    if(k==="ArrowUp"||lower==="w") handleDir(0,-1);
    else if(k==="ArrowDown"||lower==="s") handleDir(0,1);
    else if(k==="ArrowLeft"||lower==="a") handleDir(-1,0);
    else if(k==="ArrowRight"||lower==="d") handleDir(1,0);
    else if(k===" "||lower==="b"){ ensureAudio(); whistle(); }
    else if(lower==="p"){ togglePause(); }
    else if(lower==="r"){ userStarted=true; ensureAudio(); resetGame(false); }
    else if(k==="Enter" && !userStarted){ startFromDemo(); }
  });

  function togglePause(){
    if(!userStarted){ startFromDemo(); return; }
    if(gameOver) return;
    paused=!paused;
    pauseBadge.classList.toggle('hidden',!paused);
    statusText.textContent=paused?"⏸ Paused — press P or Start/Pause to resume.":"🎮 You're driving! Head to the yellow platform!";
    if(!paused) lastTick=performance.now();
    ensureAudio(); playClick();
  }

  document.getElementById('overlayStartBtn').addEventListener('click',(e)=>{ e.stopPropagation(); startFromDemo(); });
  startOverlay.addEventListener('click',()=>{ startFromDemo(); });
  document.getElementById('playAgainBtn').addEventListener('click',()=>{ userStarted=true; ensureAudio(); resetGame(false); });
  document.getElementById('startBtn').addEventListener('click',togglePause);
  document.getElementById('restartBtn').addEventListener('click',()=>{ userStarted=true; ensureAudio(); playClick(); resetGame(false); });
  document.getElementById('soundBtn').addEventListener('click',(e)=>{ soundOn=!soundOn; e.target.textContent=soundOn?"🔊 Sound":"🔇 Muted"; if(soundOn){ensureAudio(); playClick();} });
  document.getElementById('speedBtn').addEventListener('click',(e)=>{
    speedIndex=(speedIndex+1)%speedModes.length; speedFactor=speedModes[speedIndex].f;
    e.target.textContent="⚡ Speed: "+speedModes[speedIndex].label; updateHUD(); ensureAudio(); playClick();
  });
  document.getElementById('whistleBtn').addEventListener('click',()=>{ if(!userStarted) startFromDemo(); whistle(); });
  document.querySelectorAll('.dpad button').forEach(btn=>{
    btn.addEventListener('click',()=>{
      const dx=parseInt(btn.dataset.dx,10), dy=parseInt(btn.dataset.dy,10);
      handleDir(dx,dy);
    });
  });

  // swipe
  let touchX=0,touchY=0;
  canvas.addEventListener('touchstart',(e)=>{ const t=e.touches[0]; touchX=t.clientX; touchY=t.clientY; },{passive:true});
  canvas.addEventListener('touchend',(e)=>{
    const t=e.changedTouches[0]; const dx=t.clientX-touchX, dy=t.clientY-touchY;
    if(Math.abs(dx)<18 && Math.abs(dy)<18) return;
    if(Math.abs(dx)>Math.abs(dy)) handleDir(dx>0?1:-1,0); else handleDir(0,dy>0?1:-1);
  },{passive:true});
  canvas.addEventListener('click',()=>{ if(!userStarted) startFromDemo(); });

  resetGame(true);
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (10,979 tokens)
Mapping snake game to train theme with grid layout, viewport scaling, demo mode, and platform passenger spawning.Refining the platform layout by placing stations as special tiles or border strips within the grid.Expanding the grid design with train rendering, UI layout, and passenger pickup mechanics.Adding interpolation for smooth train movement, AI pathfinding, demo mode overlay, and control bindings.Extending rendering with track tiles, oriented locomotive drawing, passenger platforms, and UI panel elements.Adding game state variables, BFS pathfinding, interpolation, input handling, and audio for demo mode.Extending rendering with overlay UI, responsive CSS layout, and detailed background and passenger drawing logic.Adding locomotive and wagon rendering with colors, wheels, and passenger name tags.Adding DOM updates for stats and controls, plus keyboard handling and canvas state initialization.Adding smoke, particles, and shake effects plus speed toggle, whistle action, and HUD updates.