Models 44
Train snakeOne-line

Gemini 4 Hydrogen

by Googlereasoning defaultClean
Open in new window

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

How it went

Total time
1 min 59 s
Cost
$0.142
Output tokens
18,847
Spent thinking
1,877 tokens
Speed
160.4 tok/s
Console errors
0
First token after
1.8 s
First visible text after
12.5 s
Prompt tokens
406
Code size
43.5 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-4-hydrogen
Reasoning sent
model default
Max output
40,340 tokens
Generated
Nov 13, 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 · 1609 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>Tiny Express — Train Snake</title>
<style>
  :root {
    --bg: #1e2a24;
    --panel: #27372f;
    --panel-border: #3c5347;
    --text: #f5f2e8;
    --muted: #b6c4ba;
    --accent: #f4b942;
    --crimson: #e04f47;
    --mint: #5ec492;
  }

  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    -webkit-tap-highlight-color: transparent;
  }

  html, body {
    width: 100%;
    min-height: 100vh;
    background: radial-gradient(circle at 50% 20%, #2c3f34 0%, #18221d 100%);
    color: var(--text);
    font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    user-select: none;
  }

  .app {
    width: 100%;
    max-width: min(95vw, calc((100vh - 150px) * 20 / 14), 920px);
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px 6px;
  }

  .hud {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px;
    background: rgba(31, 44, 37, 0.9);
    border: 1.5px solid var(--panel-border);
    border-radius: 14px;
    padding: 8px 14px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
  }

  .brand {
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 800;
    letter-spacing: 0.3px;
    font-size: clamp(14px, 2vw, 18px);
    color: #fff8e7;
  }

  .brand-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 8px;
    background: linear-gradient(135deg, #e04f47, #b9322c);
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.3);
    font-size: 16px;
  }

  .stats {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
  }

  .stat-pill {
    display: flex;
    align-items: center;
    gap: 6px;
    background: rgba(20, 29, 24, 0.65);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
    padding: 4px 10px;
    font-size: clamp(12px, 1.5vw, 14px);
    font-weight: 600;
    color: var(--muted);
  }

  .stat-pill strong {
    color: var(--text);
    font-variant-numeric: tabular-nums;
    font-size: clamp(13px, 1.6vw, 15px);
  }

  .controls-bar {
    display: flex;
    align-items: center;
    gap: 6px;
  }

  .btn-icon {
    appearance: none;
    border: 1px solid var(--panel-border);
    background: #2f4238;
    color: var(--text);
    border-radius: 9px;
    padding: 5px 10px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, transform 0.1s;
    display: inline-flex;
    align-items: center;
    gap: 5px;
  }

  .btn-icon:hover {
    background: #3b5347;
  }

  .btn-icon:active {
    transform: scale(0.96);
  }

  .stage-wrap {
    position: relative;
    width: 100%;
    aspect-ratio: 20 / 14;
    border-radius: 16px;
    overflow: hidden;
    border: 3px solid #41584b;
    box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45), inset 0 0 0 1px rgba(255,255,255,0.08);
    background: #5b8c48;
  }

  canvas {
    display: block;
    width: 100%;
    height: 100%;
    touch-action: none;
  }

  /* Demo mode non-blocking banner */
  .demo-banner {
    position: absolute;
    left: 50%;
    bottom: 14px;
    transform: translateX(-50%);
    background: rgba(21, 30, 25, 0.86);
    backdrop-filter: blur(6px);
    border: 1.5px solid rgba(244, 185, 66, 0.65);
    border-radius: 999px;
    padding: 8px 18px;
    display: flex;
    align-items: center;
    gap: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    pointer-events: none;
    z-index: 5;
    white-space: nowrap;
    max-width: 92%;
    transition: opacity 0.25s ease, transform 0.25s ease;
  }

  .demo-banner.hidden {
    opacity: 0;
    transform: translateX(-50%) translateY(10px);
  }

  .demo-dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent);
    animation: pulse 1.3s infinite ease-in-out;
    flex-shrink: 0;
  }

  .demo-text {
    font-size: clamp(12px, 1.7vw, 14px);
    font-weight: 700;
    color: #fff8e7;
    letter-spacing: 0.2px;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .demo-sub {
    color: var(--accent);
    font-weight: 800;
  }

  @keyframes pulse {
    0%, 100% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.35); opacity: 0.65; }
  }

  /* Overlay for Pause / Game Over */
  .overlay {
    position: absolute;
    inset: 0;
    background: rgba(17, 24, 20, 0.72);
    backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s ease;
  }

  .overlay.visible {
    opacity: 1;
    pointer-events: auto;
  }

  .modal {
    background: #23322a;
    border: 2px solid #486355;
    border-radius: 18px;
    padding: 22px 26px;
    text-align: center;
    max-width: 340px;
    width: 88%;
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
  }

  .modal h2 {
    font-size: 24px;
    margin-bottom: 6px;
    color: #fff8e7;
  }

  .modal p {
    color: var(--muted);
    font-size: 14px;
    margin-bottom: 16px;
    line-height: 1.4;
  }

  .modal-stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 18px;
  }

  .modal-stat {
    background: rgba(16, 23, 19, 0.6);
    border-radius: 10px;
    padding: 10px 8px;
    border: 1px solid rgba(255,255,255,0.08);
  }

  .modal-stat span {
    display: block;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--muted);
    margin-bottom: 2px;
  }

  .modal-stat strong {
    font-size: 20px;
    color: var(--accent);
  }

  .btn-primary {
    appearance: none;
    border: none;
    background: linear-gradient(180deg, #f6c453 0%, #e09c27 100%);
    color: #241906;
    font-weight: 800;
    font-size: 15px;
    padding: 11px 22px;
    border-radius: 11px;
    cursor: pointer;
    box-shadow: 0 4px 14px rgba(244, 185, 66, 0.35);
    transition: transform 0.1s, filter 0.15s;
    width: 100%;
  }

  .btn-primary:hover {
    filter: brightness(1.06);
  }

  .btn-primary:active {
    transform: scale(0.97);
  }

  .footer-hint {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 12px;
    color: var(--muted);
    padding: 0 4px;
    gap: 8px;
    flex-wrap: wrap;
  }

  /* Touch D-pad for narrow / touch devices */
  .touch-pad {
    display: none;
    grid-template-columns: repeat(3, 52px);
    grid-template-rows: repeat(2, 46px);
    gap: 6px;
    justify-content: center;
    margin-top: 4px;
  }

  .d-btn {
    appearance: none;
    border: 1.5px solid var(--panel-border);
    background: rgba(41, 58, 49, 0.92);
    color: #fff8e7;
    border-radius: 12px;
    font-size: 20px;
    font-weight: 700;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
  }

  .d-btn:active {
    background: #486757;
    transform: scale(0.94);
  }

  .d-up { grid-column: 2; grid-row: 1; }
  .d-left { grid-column: 1; grid-row: 2; }
  .d-down { grid-column: 2; grid-row: 2; }
  .d-right { grid-column: 3; grid-row: 2; }

  @media (max-width: 720px), (pointer: coarse) {
    .app {
      max-width: 97vw;
    }
    .touch-pad {
      display: grid;
    }
    .footer-hint {
      justify-content: center;
      text-align: center;
    }
  }

  @media (max-width: 480px) {
    .hud {
      padding: 7px 10px;
    }
    .brand-text {
      display: none;
    }
    .demo-banner {
      bottom: 8px;
      padding: 6px 12px;
    }
  }
</style>
</head>
<body>
  <div class="app">
    <header class="hud">
      <div class="brand">
        <span class="brand-badge" aria-hidden="true">🚂</span>
        <span class="brand-text">Tiny Express</span>
      </div>
      <div class="stats">
        <div class="stat-pill">Passengers <strong id="scoreVal">0</strong></div>
        <div class="stat-pill">Wagons <strong id="wagonsVal">3</strong></div>
        <div class="stat-pill">Best <strong id="bestVal">0</strong></div>
      </div>
      <div class="controls-bar">
        <button class="btn-icon" id="soundBtn" title="Toggle Sound">🔊 <span id="soundLabel">On</span></button>
        <button class="btn-icon" id="pauseBtn" title="Pause (Space)">⏸</button>
      </div>
    </header>

    <main class="stage-wrap">
      <canvas id="gameCanvas" width="960" height="672"></canvas>

      <div class="demo-banner" id="demoBanner">
        <span class="demo-dot"></span>
        <span class="demo-text"><span class="demo-sub">AUTO-PILOT DEMO</span> — Click or press a key to start</span>
      </div>

      <div class="overlay" id="overlay">
        <div class="modal">
          <h2 id="modalTitle">End of the Line!</h2>
          <p id="modalDesc">Your locomotive bumped off the route.</p>
          <div class="modal-stats">
            <div class="modal-stat">
              <span>Passengers</span>
              <strong id="finalScore">0</strong>
            </div>
            <div class="modal-stat">
              <span>Train Length</span>
              <strong id="finalLength">4</strong>
            </div>
          </div>
          <button class="btn-primary" id="restartBtn">All Aboard! (Play Again)</button>
        </div>
      </div>
    </main>

    <div class="touch-pad" aria-label="Train directional controls">
      <button class="d-btn d-up" data-dir="0,-1" aria-label="Up">▲</button>
      <button class="d-btn d-left" data-dir="-1,0" aria-label="Left">◀</button>
      <button class="d-btn d-down" data-dir="0,1" aria-label="Down">▼</button>
      <button class="d-btn d-right" data-dir="1,0" aria-label="Right">▶</button>
    </div>

    <footer class="footer-hint">
      <span>Steer with <strong>Arrow Keys</strong> or <strong>WASD</strong> (or swipe) • <strong>Space</strong> to pause</span>
      <span id="modeTag">Mode: Demo Auto-Pilot</span>
    </footer>
  </div>

<script>
(() => {
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');

  // Grid & logical dimensions (20 x 14 tiles of 48px = 960 x 672)
  const COLS = 20;
  const ROWS = 14;
  const CELL = 48;
  const LOGICAL_W = COLS * CELL;
  const LOGICAL_H = ROWS * CELL;

  // Handle crisp HiDPI rendering
  function resizeCanvasDPR() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = LOGICAL_W * dpr;
    canvas.height = LOGICAL_H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  resizeCanvasDPR();
  window.addEventListener('resize', resizeCanvasDPR);

  // UI elements
  const scoreVal = document.getElementById('scoreVal');
  const wagonsVal = document.getElementById('wagonsVal');
  const bestVal = document.getElementById('bestVal');
  const demoBanner = document.getElementById('demoBanner');
  const overlay = document.getElementById('overlay');
  const modalTitle = document.getElementById('modalTitle');
  const modalDesc = document.getElementById('modalDesc');
  const finalScore = document.getElementById('finalScore');
  const finalLength = document.getElementById('finalLength');
  const restartBtn = document.getElementById('restartBtn');
  const pauseBtn = document.getElementById('pauseBtn');
  const soundBtn = document.getElementById('soundBtn');
  const soundLabel = document.getElementById('soundLabel');
  const modeTag = document.getElementById('modeTag');

  // Wagon color palettes
  const WAGON_PALETTES = [
    { body: '#d94842', trim: '#a62c27', roof: '#f4ece1', stripe: '#f4b942' }, // Crimson Express
    { body: '#2c7da0', trim: '#1d5670', roof: '#f4ece1', stripe: '#90e0ef' }, // Cobalt Coach
    { body: '#e9a825', trim: '#b87d14', roof: '#fff8ea', stripe: '#6b3e26' }, // Sunburst Car
    { body: '#3b9b6f', trim: '#256b4b', roof: '#f4ece1', stripe: '#f2e8cf' }, // Emerald Sleeper
    { body: '#8b5fbf', trim: '#603b8c', roof: '#f5effa', stripe: '#ffd166' }, // Royal Plum
    { body: '#e76f51', trim: '#b5492e', roof: '#fff5ee', stripe: '#264653' }  // Tangerine Dining
  ];

  const COAT_COLORS = ['#e63946', '#457b9d', '#e9c46a', '#2a9d8f', '#f4a261', '#a855f7'];
  const STATION_NAMES = ['Oakwood', 'Maple Stop', 'Meadow', 'Pinehill', 'Clover', 'Willow', 'Sunnyvale'];

  // Web Audio sound effects (zero external assets, safe if locked)
  let audioCtx = null;
  let soundEnabled = true;

  function ensureAudio() {
    if (!soundEnabled) return null;
    if (!audioCtx) {
      const AudioContext = window.AudioContext || window.webkitAudioContext;
      if (AudioContext) {
        audioCtx = new AudioContext();
      }
    }
    if (audioCtx && audioCtx.state === 'suspended') {
      audioCtx.resume().catch(() => {});
    }
    return audioCtx;
  }

  function playPickupSound() {
    if (!soundEnabled || isDemo) return;
    const ac = ensureAudio();
    if (!ac) return;
    const now = ac.currentTime;
    // Cheerful two-tone train whistle + bell chime
    [587.33, 880].forEach((freq, i) => {
      const osc = ac.createOscillator();
      const gain = ac.createGain();
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(freq, now + i * 0.08);
      gain.gain.setValueAtTime(0.001, now + i * 0.08);
      gain.gain.exponentialRampToValueAtTime(0.12, now + i * 0.08 + 0.02);
      gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.08 + 0.24);
      osc.connect(gain);
      gain.connect(ac.destination);
      osc.start(now + i * 0.08);
      osc.stop(now + i * 0.08 + 0.25);
    });
  }

  function playCrashSound() {
    if (!soundEnabled || isDemo) return;
    const ac = ensureAudio();
    if (!ac) return;
    const now = ac.currentTime;
    const osc = ac.createOscillator();
    const gain = ac.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(160, now);
    osc.frequency.exponentialRampToValueAtTime(48, now + 0.32);
    gain.gain.setValueAtTime(0.14, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.34);
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.start(now);
    osc.stop(now + 0.35);
  }

  soundBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    soundEnabled = !soundEnabled;
    soundLabel.textContent = soundEnabled ? 'On' : 'Off';
    soundBtn.firstChild.textContent = soundEnabled ? '🔊 ' : '🔇 ';
    if (soundEnabled) ensureAudio();
  });

  // Decorative background meadow elements (generated deterministically so no flicker)
  const meadowProps = [];
  function generateMeadow() {
    meadowProps.length = 0;
    let seed = 1337;
    const rand = () => {
      seed = (seed * 16807) % 2147483647;
      return (seed - 1) / 2147483646;
    };
    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        const n = rand();
        if (n < 0.16) {
          meadowProps.push({
            c, r,
            type: n < 0.09 ? 'flower' : 'tuft',
            ox: (rand() - 0.5) * 26,
            oy: (rand() - 0.5) * 26,
            color: n < 0.045 ? '#fff3b0' : (n < 0.07 ? '#ffb5a7' : '#a8dadc'),
            size: 2.5 + rand() * 2
          });
        }
      }
    }
  }
  generateMeadow();

  // Game State
  let train = [];       // array of {x, y, prevX, prevY, paletteIdx, passengersCount}
  let dir = { x: 1, y: 0 };
  let dirQueue = [];
  let platforms = [];   // array of waiting platforms {x, y, name, waiters: [{coat, phase}], spawnTime}
  let trackHistory = []; // subtle fading rail sleepers behind train
  let particles = [];   // steam & confetti particles
  let popups = [];      // floating "+1" text

  let score = 0;
  let bestScore = 0;
  let isDemo = true;
  let isPaused = false;
  let isGameOver = false;

  let stepInterval = 148; // ms per grid step
  let stepAccumulator = 0;
  let lastFrameTime = performance.now();
  let steamTimer = 0;
  let globalTime = 0;

  function initGame(demoMode = true) {
    isDemo = demoMode;
    isPaused = false;
    isGameOver = false;
    score = 0;
    stepInterval = isDemo ? 155 : 152;
    stepAccumulator = 0;
    dir = { x: 1, y: 0 };
    dirQueue = [];
    particles = [];
    popups = [];
    trackHistory = [];

    // Start train in the middle-left with locomotive + 3 colorful wagons
    const startX = 7;
    const startY = 7;
    train = [];
    for (let i = 0; i < 4; i++) {
      const segX = startX - i;
      const segY = startY;
      train.push({
        x: segX,
        y: segY,
        prevX: segX - 1,
        prevY: segY,
        paletteIdx: (i - 1) % WAGON_PALETTES.length,
        passengersCount: i === 0 ? 0 : (1 + (i % 2))
      });
    }

    // Seed initial sleepers behind starting train
    for (let i = 1; i <= 6; i++) {
      trackHistory.push({ x: startX - i, y: startY, alpha: Math.max(0.15, 0.7 - i * 0.08) });
    }

    platforms = [];
    spawnPlatform(true);
    spawnPlatform(false);

    updateHUD();
    overlay.classList.remove('visible');
    demoBanner.classList.toggle('hidden', !isDemo);
    modeTag.textContent = isDemo ? 'Mode: Demo Auto-Pilot' : 'Mode: Playing';
    pauseBtn.textContent = '⏸';
  }

  function updateHUD() {
    scoreVal.textContent = score;
    wagonsVal.textContent = Math.max(0, train.length - 1);
    if (!isDemo && score > bestScore) {
      bestScore = score;
    }
    bestVal.textContent = bestScore;
  }

  function isCellOccupied(x, y) {
    for (const seg of train) {
      if (seg.x === x && seg.y === y) return true;
    }
    for (const p of platforms) {
      if (p.x === x && p.y === y) return true;
    }
    return false;
  }

  function spawnPlatform(preferAhead = false) {
    const freeCells = [];
    const head = train[0];
    for (let y = 1; y < ROWS - 1; y++) {
      for (let x = 1; x < COLS - 1; x++) {
        if (!isCellOccupied(x, y)) {
          const dist = Math.abs(x - head.x) + Math.abs(y - head.y);
          freeCells.push({ x, y, dist });
        }
      }
    }
    // Fallback to edges if center mostly full
    if (freeCells.length === 0) {
      for (let y = 0; y < ROWS; y++) {
        for (let x = 0; x < COLS; x++) {
          if (!isCellOccupied(x, y)) freeCells.push({ x, y, dist: 5 });
        }
      }
    }
    if (freeCells.length === 0) return;

    let chosen;
    if (preferAhead) {
      const nearby = freeCells.filter(c => c.x > head.x + 1 && c.dist >= 3 && c.dist <= 8);
      if (nearby.length > 0) {
        chosen = nearby[Math.floor(Math.random() * nearby.length)];
      }
    }
    if (!chosen) {
      chosen = freeCells[Math.floor(Math.random() * freeCells.length)];
    }

    const waiterCount = 1 + Math.floor(Math.random() * 2); // 1 or 2 passengers waiting
    const waiters = [];
    for (let i = 0; i < waiterCount; i++) {
      waiters.push({
        coat: COAT_COLORS[Math.floor(Math.random() * COAT_COLORS.length)],
        phase: Math.random() * Math.PI * 2
      });
    }

    platforms.push({
      x: chosen.x,
      y: chosen.y,
      name: STATION_NAMES[Math.floor(Math.random() * STATION_NAMES.length)],
      waiters,
      spawnTime: globalTime
    });
  }

  // AI Pathfinding for Demo Auto-Pilot
  function computeDemoDirection() {
    const head = train[0];
    const dirs = [
      { x: 1, y: 0 },
      { x: -1, y: 0 },
      { x: 0, y: 1 },
      { x: 0, y: -1 }
    ];

    // Prevent 180 reversal
    const validDirs = dirs.filter(d => !(d.x === -dir.x && d.y === -dir.y));

    const blocked = new Set();
    // Tail tip vacates unless we just grew, keep tail-1 safe
    for (let i = 0; i < train.length - 1; i++) {
      blocked.add(`${train[i].x},${train[i].y}`);
    }

    // BFS to nearest platform
    const queue = [];
    const visited = new Set([`${head.x},${head.y}`]);

    for (const d of validDirs) {
      const nx = head.x + d.x;
      const ny = head.y + d.y;
      const key = `${nx},${ny}`;
      if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && !blocked.has(key)) {
        visited.add(key);
        queue.push({ x: nx, y: ny, firstDir: d });
      }
    }

    const targetKeys = new Set(platforms.map(p => `${p.x},${p.y}`));
    let foundDir = null;

    while (queue.length > 0) {
      const cur = queue.shift();
      if (targetKeys.has(`${cur.x},${cur.y}`)) {
        foundDir = cur.firstDir;
        break;
      }
      for (const d of dirs) {
        const nx = cur.x + d.x;
        const ny = cur.y + d.y;
        const key = `${nx},${ny}`;
        if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && !blocked.has(key) && !visited.has(key)) {
          visited.add(key);
          queue.push({ x: nx, y: ny, firstDir: cur.firstDir });
        }
      }
    }

    if (foundDir) {
      // Verify the chosen step still has decent reachable space (avoid self-trap)
      const space = floodFillCount(head.x + foundDir.x, head.y + foundDir.y, blocked);
      if (space >= Math.min(train.length, 12)) {
        return foundDir;
      }
    }

    // Fallback: pick safe direction with maximum reachable open tiles
    let bestD = null;
    let bestSpace = -1;
    for (const d of validDirs) {
      const nx = head.x + d.x;
      const ny = head.y + d.y;
      const key = `${nx},${ny}`;
      if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && !blocked.has(key)) {
        const space = floodFillCount(nx, ny, blocked);
        if (space > bestSpace) {
          bestSpace = space;
          bestD = d;
        }
      }
    }
    return bestD || dir;
  }

  function floodFillCount(startX, startY, blocked) {
    const seen = new Set([`${startX},${startY}`]);
    const q = [{ x: startX, y: startY }];
    let count = 0;
    const dirs = [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }];
    while (q.length > 0 && count < 80) {
      const cur = q.shift();
      count++;
      for (const d of dirs) {
        const nx = cur.x + d.x;
        const ny = cur.y + d.y;
        const k = `${nx},${ny}`;
        if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && !blocked.has(k) && !seen.has(k)) {
          seen.add(k);
          q.push({ x: nx, y: ny });
        }
      }
    }
    return count;
  }

  // Particles
  function emitSteam(x, y, count = 1) {
    for (let i = 0; i < count; i++) {
      particles.push({
        type: 'steam',
        x: x + (Math.random() - 0.5) * 6,
        y: y + (Math.random() - 0.5) * 6,
        vx: (Math.random() - 0.5) * 14 - dir.x * 12,
        vy: -18 - Math.random() * 14,
        r: 4 + Math.random() * 4,
        life: 0,
        maxLife: 0.75 + Math.random() * 0.45
      });
    }
  }

  function emitBoardingBurst(x, y) {
    const colors = ['#f4b942', '#e04f47', '#5ec492', '#90e0ef', '#ffffff'];
    for (let i = 0; i < 16; i++) {
      const ang = (Math.PI * 2 * i) / 16 + Math.random() * 0.3;
      const spd = 40 + Math.random() * 70;
      particles.push({
        type: 'confetti',
        x, y,
        vx: Math.cos(ang) * spd,
        vy: Math.sin(ang) * spd - 15,
        r: 2.5 + Math.random() * 2.5,
        color: colors[i % colors.length],
        life: 0,
        maxLife: 0.55 + Math.random() * 0.25
      });
    }
  }

  // Step logic
  function stepGame() {
    if (isDemo) {
      dir = computeDemoDirection();
    } else if (dirQueue.length > 0) {
      dir = dirQueue.shift();
    }

    const head = train[0];
    const nextX = head.x + dir.x;
    const nextY = head.y + dir.y;

    // Check platform hit first to know if growing
    const platformIdx = platforms.findIndex(p => p.x === nextX && p.y === nextY);
    const willGrow = platformIdx !== -1;

    // Wall collision
    if (nextX < 0 || nextX >= COLS || nextY < 0 || nextY >= ROWS) {
      handleCrash();
      return;
    }

    // Self collision (exclude last tail segment if not growing)
    const checkLimit = willGrow ? train.length : train.length - 1;
    for (let i = 0; i < checkLimit; i++) {
      if (train[i].x === nextX && train[i].y === nextY) {
        handleCrash();
        return;
      }
    }

    // Record track sleeper under locomotive's departing tile
    trackHistory.push({ x: head.x, y: head.y, alpha: 0.72 });
    if (trackHistory.length > 36) trackHistory.shift();

    // Save previous positions & move segments
    const oldTail = { ...train[train.length - 1] };
    for (let i = train.length - 1; i >= 1; i--) {
      train[i].prevX = train[i].x;
      train[i].prevY = train[i].y;
      train[i].x = train[i - 1].x;
      train[i].y = train[i - 1].y;
    }
    head.prevX = head.x;
    head.prevY = head.y;
    head.x = nextX;
    head.y = nextY;

    if (willGrow) {
      const plat = platforms[platformIdx];
      const passengersBoarded = plat.waiters.length;
      const newPaletteIdx = (train.length - 1) % WAGON_PALETTES.length;
      train.push({
        x: oldTail.x,
        y: oldTail.y,
        prevX: oldTail.prevX,
        prevY: oldTail.prevY,
        paletteIdx: newPaletteIdx,
        passengersCount: passengersBoarded
      });

      platforms.splice(platformIdx, 1);
      score += passengersBoarded;
      spawnPlatform(false);

      // Gentle speed-up as train gets longer (capped comfortably)
      const minStep = isDemo ? 128 : 96;
      stepInterval = Math.max(minStep, (isDemo ? 155 : 152) - Math.floor(score * 1.1));

      const px = (nextX + 0.5) * CELL;
      const py = (nextY + 0.5) * CELL;
      emitBoardingBurst(px, py);
      popups.push({
        x: px,
        y: py - 10,
        text: `+${passengersBoarded} All Aboard!`,
        life: 0,
        maxLife: 1.0
      });

      playPickupSound();
      updateHUD();

      // In demo mode, keep train a fun manageable length so it loops gracefully forever
      if (isDemo && train.length > 18) {
        initGame(true);
      }
    }
  }

  function handleCrash() {
    if (isDemo) {
      // Seamless reset in demo mode
      initGame(true);
      return;
    }
    isGameOver = true;
    playCrashSound();
    const head = train[0];
    emitSteam((head.x + 0.5) * CELL, (head.y + 0.5) * CELL, 14);
    modalTitle.textContent = 'End of the Line!';
    modalDesc.textContent = `Your Tiny Express carried ${score} passenger${score === 1 ? '' : 's'} across the countryside.`;
    finalScore.textContent = score;
    finalLength.textContent = train.length;
    overlay.classList.add('visible');
  }

  // Input handling
  function takePlayerControl(initialDir = null) {
    ensureAudio();
    if (isDemo) {
      initGame(false);
      if (initialDir && !(initialDir.x === -dir.x && initialDir.y === -dir.y)) {
        dirQueue.push(initialDir);
      }
    }
  }

  function queueDirection(nx, ny) {
    if (isGameOver) return;
    if (isDemo) {
      takePlayerControl({ x: nx, y: ny });
      return;
    }
    if (isPaused) togglePause();

    const lastDir = dirQueue.length > 0 ? dirQueue[dirQueue.length - 1] : dir;
    if ((nx === -lastDir.x && ny === -lastDir.y) || (nx === lastDir.x && ny === lastDir.y)) {
      return;
    }
    if (dirQueue.length < 2) {
      dirQueue.push({ x: nx, y: ny });
    }
  }

  function togglePause() {
    if (isDemo || isGameOver) return;
    isPaused = !isPaused;
    pauseBtn.textContent = isPaused ? '▶' : '⏸';
    if (isPaused) {
      modalTitle.textContent = 'Station Stop';
      modalDesc.textContent = 'Train paused. Press Space or click below to continue.';
      finalScore.textContent = score;
      finalLength.textContent = train.length;
      restartBtn.textContent = 'Resume Journey';
      overlay.classList.add('visible');
    } else {
      restartBtn.textContent = 'All Aboard! (Play Again)';
      overlay.classList.remove('visible');
    }
  }

  window.addEventListener('keydown', (e) => {
    const key = e.key.toLowerCase();
    if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright', ' '].includes(key)) {
      e.preventDefault();
    }

    if (isGameOver && (key === 'enter' || key === ' ' || key === 'r')) {
      initGame(false);
      return;
    }

    if (key === ' ' || key === 'p') {
      if (isDemo) {
        takePlayerControl();
      } else {
        togglePause();
      }
      return;
    }

    if (key === 'r' && !isDemo) {
      initGame(false);
      return;
    }

    if (key === 'arrowup' || key === 'w') queueDirection(0, -1);
    else if (key === 'arrowdown' || key === 's') queueDirection(0, 1);
    else if (key === 'arrowleft' || key === 'a') queueDirection(-1, 0);
    else if (key === 'arrowright' || key === 'd') queueDirection(1, 0);
    else if (isDemo) {
      takePlayerControl();
    }
  });

  // Click canvas to start playing from demo
  canvas.addEventListener('pointerdown', (e) => {
    if (isDemo) {
      takePlayerControl();
    }
    touchStart = { x: e.clientX, y: e.clientY };
  });

  let touchStart = null;
  canvas.addEventListener('pointerup', (e) => {
    if (!touchStart || isDemo) return;
    const dx = e.clientX - touchStart.x;
    const dy = e.clientY - touchStart.y;
    touchStart = null;
    if (Math.hypot(dx, dy) < 18) return;
    if (Math.abs(dx) > Math.abs(dy)) {
      queueDirection(dx > 0 ? 1 : -1, 0);
    } else {
      queueDirection(0, dy > 0 ? 1 : -1);
    }
  });

  document.querySelectorAll('.d-btn').forEach(btn => {
    btn.addEventListener('pointerdown', (e) => {
      e.preventDefault();
      e.stopPropagation();
      const [dx, dy] = btn.dataset.dir.split(',').map(Number);
      queueDirection(dx, dy);
    });
  });

  pauseBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    if (isDemo) takePlayerControl();
    else togglePause();
  });

  restartBtn.addEventListener('click', () => {
    if (isPaused && !isGameOver) {
      togglePause();
    } else {
      initGame(false);
    }
  });

  // Rendering helpers
  function lerp(a, b, t) {
    return a + (b - a) * t;
  }

  function drawRoundedRect(x, y, w, h, r) {
    ctx.beginPath();
    ctx.roundRect(x, y, w, h, r);
  }

  function drawMeadowBackground() {
    // Subtle checkerboard pasture
    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        const even = (r + c) % 2 === 0;
        ctx.fillStyle = even ? '#699f4f' : '#619548';
        ctx.fillRect(c * CELL, r * CELL, CELL, CELL);
      }
    }

    // Soft border edge shading (countryside hedge frame)
    ctx.strokeStyle = 'rgba(35, 62, 28, 0.35)';
    ctx.lineWidth = 8;
    ctx.strokeRect(4, 4, LOGICAL_W - 8, LOGICAL_H - 8);

    // Wildflowers & grass tufts
    for (const p of meadowProps) {
      // Skip props under active platforms so platforms look neat
      if (platforms.some(pl => pl.x === p.c && pl.y === p.r)) continue;
      const cx = (p.c + 0.5) * CELL + p.ox;
      const cy = (p.r + 0.5) * CELL + p.oy;
      if (p.type === 'tuft') {
        ctx.strokeStyle = '#4e7d39';
        ctx.lineWidth = 1.6;
        ctx.beginPath();
        ctx.moveTo(cx - 3, cy + 2);
        ctx.lineTo(cx - 5, cy - 3);
        ctx.moveTo(cx, cy + 2);
        ctx.lineTo(cx, cy - 4);
        ctx.moveTo(cx + 3, cy + 2);
        ctx.lineTo(cx + 5, cy - 3);
        ctx.stroke();
      } else {
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(cx, cy, p.size, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#f4b942';
        ctx.beginPath();
        ctx.arc(cx, cy, p.size * 0.42, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }

  function drawTracks(interpT) {
    // Fading gravel & wooden sleepers behind train
    for (const tr of trackHistory) {
      const cx = (tr.x + 0.5) * CELL;
      const cy = (tr.y + 0.5) * CELL;
      ctx.save();
      ctx.globalAlpha = tr.alpha * 0.45;
      ctx.fillStyle = '#6f5a43';
      ctx.fillRect(cx - 11, cy - 11, 22, 22);
      ctx.fillStyle = '#4b3827';
      ctx.fillRect(cx - 13, cy - 7, 26, 3);
      ctx.fillRect(cx - 13, cy + 4, 26, 3);
      ctx.restore();
    }

    // Continuous smooth rail bed connecting interpolated train segments
    if (train.length > 1) {
      const pts = train.map(seg => ({
        x: (lerp(seg.prevX, seg.x, interpT) + 0.5) * CELL,
        y: (lerp(seg.prevY, seg.y, interpT) + 0.5) * CELL
      }));

      // Gravel ballast
      ctx.save();
      ctx.lineCap = 'round';
      ctx.lineJoin = 'round';
      ctx.strokeStyle = 'rgba(110, 92, 70, 0.55)';
      ctx.lineWidth = 22;
      ctx.beginPath();
      pts.forEach((pt, idx) => idx === 0 ? ctx.moveTo(pt.x, pt.y) : ctx.lineTo(pt.x, pt.y));
      ctx.stroke();

      // Iron rails
      ctx.strokeStyle = 'rgba(218, 226, 232, 0.72)';
      ctx.lineWidth = 12;
      ctx.beginPath();
      pts.forEach((pt, idx) => idx === 0 ? ctx.moveTo(pt.x, pt.y) : ctx.lineTo(pt.x, pt.y));
      ctx.stroke();

      ctx.strokeStyle = 'rgba(84, 70, 54, 0.9)';
      ctx.lineWidth = 7;
      ctx.beginPath();
      pts.forEach((pt, idx) => idx === 0 ? ctx.moveTo(pt.x, pt.y) : ctx.lineTo(pt.x, pt.y));
      ctx.stroke();
      ctx.restore();
    }
  }

  function drawPlatforms() {
    for (const p of platforms) {
      const x = p.x * CELL;
      const y = p.y * CELL;
      const cx = x + CELL / 2;
      const cy = y + CELL / 2;
      const age = globalTime - p.spawnTime;
      const popScale = Math.min(1, age * 4.5);

      ctx.save();
      ctx.translate(cx, cy);
      ctx.scale(popScale, popScale);

      // Platform shadow
      ctx.fillStyle = 'rgba(0, 0, 0, 0.22)';
      drawRoundedRect(-20, -15, 42, 36, 6);
      ctx.fill();

      // Wooden deck
      ctx.fillStyle = '#b98550';
      ctx.strokeStyle = '#6c4724';
      ctx.lineWidth = 2;
      drawRoundedRect(-21, -18, 42, 36, 6);
      ctx.fill();
      ctx.stroke();

      // Wood plank lines
      ctx.strokeStyle = 'rgba(108, 71, 36, 0.45)';
      ctx.lineWidth = 1.2;
      for (let py = -10; py <= 10; py += 7) {
        ctx.beginPath();
        ctx.moveTo(-19, py);
        ctx.lineTo(19, py);
        ctx.stroke();
      }

      // Yellow safety edge stripe
      ctx.fillStyle = '#f4b942';
      ctx.fillRect(-19, 13, 38, 3);

      // Little station shelter awning on top-left corner
      ctx.fillStyle = '#d94842';
      drawRoundedRect(-20, -21, 18, 10, 3);
      ctx.fill();
      ctx.fillStyle = '#fff5e4';
      ctx.fillRect(-17, -21, 3, 10);
      ctx.fillRect(-11, -21, 3, 10);

      // Waiting passengers (bouncing & waving)
      const count = p.waiters.length;
      p.waiters.forEach((w, idx) => {
        const px = count === 1 ? 4 : (idx === 0 ? -5 : 10);
        const bounce = Math.abs(Math.sin(globalTime * 4.5 + w.phase)) * 3.5;
        const wave = Math.sin(globalTime * 8 + w.phase) * 0.45;
        const py = 2 - bounce;

        // Shadow under passenger
        ctx.fillStyle = 'rgba(0,0,0,0.2)';
        ctx.beginPath();
        ctx.ellipse(px, 9, 5, 2.2, 0, 0, Math.PI * 2);
        ctx.fill();

        // Waving arm
        ctx.save();
        ctx.translate(px + 4, py - 1);
        ctx.rotate(-0.8 + wave);
        ctx.strokeStyle = w.coat;
        ctx.lineWidth = 2.6;
        ctx.lineCap = 'round';
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(4, -5);
        ctx.stroke();
        ctx.restore();

        // Coat / body
        ctx.fillStyle = w.coat;
        ctx.strokeStyle = '#1f2937';
        ctx.lineWidth = 1.3;
        drawRoundedRect(px - 5, py - 3, 10, 11, 4);
        ctx.fill();
        ctx.stroke();

        // Head
        ctx.fillStyle = '#ffd6b0';
        ctx.beginPath();
        ctx.arc(px, py - 6, 4.4, 0, Math.PI * 2);
        ctx.fill();
        ctx.stroke();

        // Tiny hat / hair
        ctx.fillStyle = '#3d2c20';
        ctx.beginPath();
        ctx.arc(px, py - 7.5, 4.2, Math.PI, 0);
        ctx.fill();
      });

      ctx.restore();
    }
  }

  function getSegmentAngle(seg, aheadSeg, interpT) {
    const sx = lerp(seg.prevX, seg.x, interpT);
    const sy = lerp(seg.prevY, seg.y, interpT);
    if (aheadSeg) {
      const ax = lerp(aheadSeg.prevX, aheadSeg.x, interpT);
      const ay = lerp(aheadSeg.prevY, aheadSeg.y, interpT);
      const dx = ax - sx;
      const dy = ay - sy;
      if (Math.hypot(dx, dy) > 0.01) {
        return Math.atan2(dy, dx);
      }
    }
    const dx = seg.x - seg.prevX;
    const dy = seg.y - seg.prevY;
    if (dx !== 0 || dy !== 0) return Math.atan2(dy, dx);
    return Math.atan2(dir.y, dir.x);
  }

  function drawCouplers(interpT) {
    ctx.strokeStyle = '#2b2d42';
    ctx.lineWidth = 4.5;
    ctx.lineCap = 'round';
    for (let i = 1; i < train.length; i++) {
      const a = train[i - 1];
      const b = train[i];
      const ax = (lerp(a.prevX, a.x, interpT) + 0.5) * CELL;
      const ay = (lerp(a.prevY, a.y, interpT) + 0.5) * CELL;
      const bx = (lerp(b.prevX, b.x, interpT) + 0.5) * CELL;
      const by = (lerp(b.prevY, b.y, interpT) + 0.5) * CELL;
      ctx.beginPath();
      ctx.moveTo(ax, ay);
      ctx.lineTo(bx, by);
      ctx.stroke();
    }
  }

  function drawWagon(seg, angle, interpT) {
    const cx = (lerp(seg.prevX, seg.x, interpT) + 0.5) * CELL;
    const cy = (lerp(seg.prevY, seg.y, interpT) + 0.5) * CELL;
    const pal = WAGON_PALETTES[seg.paletteIdx % WAGON_PALETTES.length];

    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(angle);

    // Drop shadow
    ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
    drawRoundedRect(-18, -12, 36, 26, 6);
    ctx.fill();

    // Wheels peeking out on sides
    ctx.fillStyle = '#1f2421';
    [-11, 7].forEach(wx => {
      ctx.fillRect(wx, -15, 6, 3);
      ctx.fillRect(wx, 12, 6, 3);
    });

    // Main wagon body
    ctx.fillStyle = pal.body;
    ctx.strokeStyle = '#1d1e22';
    ctx.lineWidth = 1.8;
    drawRoundedRect(-18, -13, 36, 26, 6);
    ctx.fill();
    ctx.stroke();

    // Trim band
    ctx.fillStyle = pal.trim;
    ctx.fillRect(-16, -12, 32, 4);
    ctx.fillRect(-16, 8, 32, 4);

    // Side windows with tiny passenger silhouettes
    ctx.fillStyle = '#d9f2ff';
    [-11, -1, 9].forEach((wx, idx) => {
      drawRoundedRect(wx - 3.5, -10.5, 7, 3.2, 1);
      ctx.fill();
      drawRoundedRect(wx - 3.5, 7.3, 7, 3.2, 1);
      ctx.fill();
      if (idx < seg.passengersCount) {
        ctx.fillStyle = '#2d3748';
        ctx.fillRect(wx - 1.5, -9.8, 3, 2.2);
        ctx.fillRect(wx - 1.5, 7.6, 3, 2.2);
        ctx.fillStyle = '#d9f2ff';
      }
    });

    // Cream roof
    ctx.fillStyle = pal.roof;
    ctx.strokeStyle = 'rgba(0,0,0,0.22)';
    ctx.lineWidth = 1;
    drawRoundedRect(-14, -7.5, 28, 15, 4);
    ctx.fill();
    ctx.stroke();

    // Accent stripe down roof
    ctx.fillStyle = pal.stripe;
    ctx.fillRect(-12, -1.5, 24, 3);

    // Roof vents
    ctx.fillStyle = '#6c757d';
    ctx.beginPath();
    ctx.arc(-6, 0, 2.1, 0, Math.PI * 2);
    ctx.arc(6, 0, 2.1, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();
  }

  function drawLocomotive(seg, angle, interpT) {
    const cx = (lerp(seg.prevX, seg.x, interpT) + 0.5) * CELL;
    const cy = (lerp(seg.prevY, seg.y, interpT) + 0.5) * CELL;

    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(angle);

    // Warm glowing headlight cone
    const grad = ctx.createRadialGradient(20, 0, 2, 48, 0, 44);
    grad.addColorStop(0, 'rgba(255, 243, 176, 0.55)');
    grad.addColorStop(1, 'rgba(255, 243, 176, 0)');
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.moveTo(18, -5);
    ctx.lineTo(68, -24);
    ctx.lineTo(68, 24);
    ctx.lineTo(18, 5);
    ctx.closePath();
    ctx.fill();

    // Shadow
    ctx.fillStyle = 'rgba(0, 0, 0, 0.32)';
    drawRoundedRect(-19, -13, 40, 28, 7);
    ctx.fill();

    // Cowcatcher / front bumper
    ctx.fillStyle = '#c93b35';
    ctx.strokeStyle = '#1b1d20';
    ctx.lineWidth = 1.6;
    ctx.beginPath();
    ctx.moveTo(16, -11);
    ctx.lineTo(24, -6);
    ctx.lineTo(24, 6);
    ctx.lineTo(16, 11);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Wheels
    ctx.fillStyle = '#1a1c1e';
    [-12, -1, 10].forEach(wx => {
      ctx.fillRect(wx - 3, -15.5, 6, 3);
      ctx.fillRect(wx - 3, 12.5, 6, 3);
    });

    // Main boiler body
    ctx.fillStyle = '#2b2f36';
    ctx.strokeStyle = '#141619';
    ctx.lineWidth = 1.8;
    drawRoundedRect(-18, -12.5, 36, 25, 6);
    ctx.fill();
    ctx.stroke();

    // Brass boiler bands
    ctx.fillStyle = '#e9b44c';
    ctx.fillRect(0, -12, 2.6, 24);
    ctx.fillRect(8, -12, 2.6, 24);

    // Driver's Cab (rear, crimson roof)
    ctx.fillStyle = '#c93b35';
    ctx.strokeStyle = '#141619';
    ctx.lineWidth = 1.6;
    drawRoundedRect(-19, -13.5, 16, 27, 4);
    ctx.fill();
    ctx.stroke();

    // Cab roof highlight
    ctx.fillStyle = '#e45852';
    drawRoundedRect(-17, -10.5, 12, 21, 3);
    ctx.fill();

    // Brass steam dome
    ctx.fillStyle = '#f4b942';
    ctx.strokeStyle = '#8b5e11';
    ctx.lineWidth = 1.2;
    ctx.beginPath();
    ctx.arc(3, 0, 4.3, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Smokestack chimney (front)
    ctx.fillStyle = '#15171a';
    ctx.strokeStyle = '#495057';
    ctx.lineWidth = 1.8;
    ctx.beginPath();
    ctx.arc(13, 0, 5.2, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Glowing brass headlight lamp
    ctx.fillStyle = '#fff3b0';
    ctx.beginPath();
    ctx.arc(20, 0, 2.8, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();
  }

  function drawParticlesAndPopups() {
    for (const p of particles) {
      const t = p.life / p.maxLife;
      ctx.save();
      if (p.type === 'steam') {
        ctx.globalAlpha = (1 - t) * 0.72;
        ctx.fillStyle = '#ffffff';
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r + t * 9, 0, Math.PI * 2);
        ctx.fill();
      } else {
        ctx.globalAlpha = 1 - t;
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.restore();
    }

    for (const pop of popups) {
      const t = pop.life / pop.maxLife;
      ctx.save();
      ctx.globalAlpha = Math.max(0, 1 - t * t);
      ctx.font = '800 15px "Segoe UI", system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.lineWidth = 4;
      ctx.strokeStyle = 'rgba(20, 29, 24, 0.85)';
      ctx.strokeText(pop.text, pop.x, pop.y);
      ctx.fillStyle = '#ffe17d';
      ctx.fillText(pop.text, pop.x, pop.y);
      ctx.restore();
    }
  }

  // Main animation loop
  function frame(now) {
    const dt = Math.min(0.05, (now - lastFrameTime) / 1000);
    lastFrameTime = now;
    globalTime += dt;

    if (!isPaused && !isGameOver) {
      stepAccumulator += dt * 1000;
      while (stepAccumulator >= stepInterval) {
        stepAccumulator -= stepInterval;
        stepGame();
        if (isGameOver) break;
      }

      // Emit chimney steam periodically
      steamTimer += dt;
      if (steamTimer >= 0.085 && train.length > 0) {
        steamTimer = 0;
        const interp = isGameOver ? 1 : stepAccumulator / stepInterval;
        const head = train[0];
        const angle = getSegmentAngle(head, null, interp);
        const hx = (lerp(head.prevX, head.x, interp) + 0.5) * CELL + Math.cos(angle) * 13;
        const hy = (lerp(head.prevY, head.y, interp) + 0.5) * CELL + Math.sin(angle) * 13;
        emitSteam(hx, hy, 1);
      }
    }

    // Update particles & popups
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life += dt;
      if (p.life >= p.maxLife) {
        particles.splice(i, 1);
      } else {
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        if (p.type === 'confetti') p.vy += 90 * dt;
      }
    }

    for (let i = popups.length - 1; i >= 0; i--) {
      const pop = popups[i];
      pop.life += dt;
      if (pop.life >= pop.maxLife) {
        popups.splice(i, 1);
      } else {
        pop.y -= 28 * dt;
      }
    }

    // Smooth interpolation factor between grid steps
    const interpT = (isGameOver || isPaused) ? 1 : Math.min(1, stepAccumulator / stepInterval);

    // Draw frame
    drawMeadowBackground();
    drawTracks(interpT);
    drawPlatforms();
    drawCouplers(interpT);

    // Draw wagons from tail to front, then locomotive on top
    for (let i = train.length - 1; i >= 1; i--) {
      const angle = getSegmentAngle(train[i], train[i - 1], interpT);
      drawWagon(train[i], angle, interpT);
    }
    if (train.length > 0) {
      const headAngle = getSegmentAngle(train[0], null, interpT);
      drawLocomotive(train[0], headAngle, interpT);
    }

    drawParticlesAndPopups();

    requestAnimationFrame(frame);
  }

  // Start immediately in Demo Auto-Pilot mode
  initGame(true);
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (1,877 tokens)
**Designing a Train-Themed Snake**

I'm currently envisioning a delightful HTML5 canvas Snake game. The core idea is a charming, responsive, and fluidly animated experience where the snake is reimagined as a steam train. I'm focusing on the visual appeal of a little locomotive with colorful, coupled wagons, and the gameplay element of picking up passengers from wooden platforms.

**Refining Train Mechanics**

I'm zeroing in on the core mechanics, ensuring the train moves smoothly across the grid, picking up passengers. My focus is on creating a seamless, self-contained HTML file that looks great on various screen sizes, with an AI demo running from the start.

**Adding Railway Details**

I'm now considering how to enhance the visual immersion by adding elements like railway ballast and tracks behind the playfield. I plan to implement smooth interpolation for the train's movement, making it glide realistically, and I'm also detailing the locomotive with a glowing headlight and puffing steam, along with adding colorful wagons that connect smoothly.

**Expanding Game Elements**

I'm currently focused on bringing the railway environment to life. My next steps involve introducing charming wooden platforms with passengers, along with responsive controls for both desktop and mobile. I'm also detailing game over sequences and a clear HUD for crucial information.

**Implementing Interaction Enhancements**

I'm now integrating interactive elements like sound cues for collecting items and an immediate AI demo start with initial wagons and platforms. I'm also refining responsive sizing to ensure a consistent experience across devices.

**Adjusting Canvas Dimensions**

I'm finalizing the canvas dimensions, aiming for a `960x672` logical resolution at `48px` per cell to ensure crisp rendering across various screen sizes. The goal is to maintain an optimal aspect ratio, allowing it to fit beautifully within different viewport heights while accommodating the HUD and a touch D-pad when needed.