Models 44
Train snakeOne-line

Gemini 4 Argon

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
3 min 3 s
Cost
$0.138
Output tokens
13,734
Spent thinking
0 tokens
Speed
77.2 tok/s
Console errors
0
First token after
4.8 s
First visible text after
4.8 s
Prompt tokens
406
Code size
35.0 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-4-argon
Reasoning sent
model default
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 · 1286 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Choo-Choo Snake: All Aboard!</title>
<style>
  :root {
    --bg-dark: #263238;
    --brass: #d4af37;
    --brass-dark: #997a15;
    --wood: #4e342e;
    --cream: #fff8e7;
    --red: #e53935;
  }
  * {
    box-sizing: border-box;
    user-select: none;
    -webkit-user-select: none;
  }
  body {
    margin: 0;
    padding: 0;
    background-color: var(--bg-dark);
    background-image: radial-gradient(#37474f 15%, transparent 16%), radial-gradient(#37474f 15%, transparent 16%);
    background-size: 30px 30px;
    background-position: 0 0, 15px 15px;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: var(--cream);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    overflow: hidden;
  }

  .station-shell {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: var(--wood);
    padding: 14px;
    border-radius: 18px;
    border: 4px solid var(--brass);
    box-shadow: 0 16px 40px rgba(0,0,0,0.6), inset 0 2px 6px rgba(255,255,255,0.2);
    max-width: 96vw;
  }

  /* Header / Station Signboard */
  .header-board {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    background: #1b2326;
    border: 3px solid var(--brass);
    border-radius: 10px;
    padding: 8px 16px;
    margin-bottom: 12px;
    box-shadow: inset 0 2px 8px rgba(0,0,0,0.8);
    gap: 12px;
    flex-wrap: wrap;
  }

  .title-badge {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .title-badge h1 {
    margin: 0;
    font-size: 1.35rem;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--brass);
    text-shadow: 0 2px 0 #000;
  }
  .train-icon {
    font-size: 1.5rem;
  }

  .stats-group {
    display: flex;
    gap: 14px;
    align-items: center;
  }

  .flap-display {
    background: #111;
    border: 2px solid #444;
    border-radius: 6px;
    padding: 4px 10px;
    text-align: center;
    min-width: 82px;
  }
  .flap-label {
    font-size: 0.65rem;
    color: #aaa;
    text-transform: uppercase;
    letter-spacing: 1px;
    display: block;
  }
  .flap-value {
    font-family: 'Courier New', Courier, monospace;
    font-size: 1.25rem;
    font-weight: bold;
    color: #ffd54f;
  }

  .btn-group {
    display: flex;
    gap: 8px;
  }
  .rail-btn {
    background: linear-gradient(to bottom, #ffd54f, #ffb300);
    border: 2px solid #fff3e0;
    color: #3e2723;
    font-weight: 800;
    padding: 6px 12px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.85rem;
    box-shadow: 0 3px 0 #b26a00;
    transition: transform 0.1s, box-shadow 0.1s;
  }
  .rail-btn:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 #b26a00;
  }

  /* Canvas Container */
  .viewport-frame {
    position: relative;
    border: 4px solid #2b1b17;
    border-radius: 10px;
    overflow: hidden;
    box-shadow: 0 6px 20px rgba(0,0,0,0.5);
    background: #558b2f;
    width: min(640px, 90vw);
    height: min(640px, 90vw);
    max-height: 70vh;
    max-width: 70vh;
  }

  canvas {
    width: 100%;
    height: 100%;
    display: block;
  }

  /* Overlay Banner */
  .overlay-banner {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
    padding: 22px;
    pointer-events: none;
    transition: opacity 0.25s ease;
  }
  .overlay-banner.hidden {
    opacity: 0;
  }

  .demo-pill {
    background: rgba(27, 35, 38, 0.88);
    border: 2px solid var(--brass);
    color: #fff;
    padding: 6px 16px;
    border-radius: 999px;
    font-size: 0.85rem;
    font-weight: 700;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    box-shadow: 0 4px 12px rgba(0,0,0,0.4);
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .pulse-dot {
    width: 10px;
    height: 10px;
    background: #4caf50;
    border-radius: 50%;
    animation: pulse 1.2s infinite;
  }
  @keyframes pulse {
    0%, 100% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.4); opacity: 0.6; }
  }

  .cta-ticket {
    background: rgba(255, 248, 231, 0.95);
    color: #2b1b17;
    border: 3px dashed #b26a00;
    border-radius: 12px;
    padding: 14px 26px;
    text-align: center;
    box-shadow: 0 10px 30px rgba(0,0,0,0.55);
    pointer-events: auto;
    cursor: pointer;
    transform: translateY(0);
    animation: floatTicket 2.4s ease-in-out infinite;
  }
  @keyframes floatTicket {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-5px); }
  }
  .cta-ticket h2 {
    margin: 0 0 4px 0;
    font-size: 1.35rem;
    color: #c62828;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }
  .cta-ticket p {
    margin: 0;
    font-size: 0.9rem;
    font-weight: 600;
    color: #4e342e;
  }

  /* Touch D-Pad for smaller viewports */
  .dpad {
    display: none;
    margin-top: 10px;
    gap: 6px;
    grid-template-columns: repeat(3, 52px);
    grid-template-rows: repeat(2, 44px);
    justify-content: center;
  }
  .dpad-btn {
    background: #37474f;
    border: 2px solid var(--brass);
    color: var(--cream);
    border-radius: 8px;
    font-size: 1.2rem;
    font-weight: bold;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .dpad-btn:active {
    background: var(--brass);
    color: #000;
  }

  @media (max-width: 680px) {
    .title-badge h1 { font-size: 1.05rem; }
    .dpad { display: grid; }
    .viewport-frame {
      max-height: 56vh;
      max-width: 56vh;
    }
  }
</style>
</head>
<body>

<div class="station-shell">
  <div class="header-board">
    <div class="title-badge">
      <span class="train-icon">🚂</span>
      <h1>Grand Central Express</h1>
    </div>

    <div class="stats-group">
      <div class="flap-display">
        <span class="flap-label">Passengers</span>
        <span class="flap-value" id="scoreVal">00</span>
      </div>
      <div class="flap-display">
        <span class="flap-label">Wagons</span>
        <span class="flap-value" id="wagonVal">04</span>
      </div>
      <div class="flap-display">
        <span class="flap-label">Best Run</span>
        <span class="flap-value" id="bestVal">00</span>
      </div>
    </div>

    <div class="btn-group">
      <button class="rail-btn" id="whistleBtn" title="Blow Steam Whistle (Space)">🎺 TOOT!</button>
      <button class="rail-btn" id="muteBtn" title="Toggle Sound">🔊</button>
    </div>
  </div>

  <div class="viewport-frame" id="viewportFrame">
    <canvas id="gameCanvas" width="700" height="700"></canvas>

    <div class="overlay-banner" id="overlayBanner">
      <div class="demo-pill" id="demoPill">
        <span class="pulse-dot"></span>
        <span id="pillText">Auto-Conductor Demo Active</span>
      </div>

      <div class="cta-ticket" id="ctaTicket">
        <h2 id="ctaTitle">Click or Press Any Key to Drive</h2>
        <p id="ctaSub">Use Arrow Keys / WASD to steer the locomotive &amp; board passengers!</p>
      </div>
    </div>
  </div>

  <div class="dpad">
    <div></div>
    <button class="dpad-btn" data-dir="UP">▲</button>
    <div></div>
    <button class="dpad-btn" data-dir="LEFT">◀</button>
    <button class="dpad-btn" data-dir="DOWN">▼</button>
    <button class="dpad-btn" data-dir="RIGHT">▶</button>
  </div>
</div>

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

  const scoreEl = document.getElementById('scoreVal');
  const wagonEl = document.getElementById('wagonVal');
  const bestEl = document.getElementById('bestVal');
  const overlayBanner = document.getElementById('overlayBanner');
  const ctaTicket = document.getElementById('ctaTicket');
  const ctaTitle = document.getElementById('ctaTitle');
  const ctaSub = document.getElementById('ctaSub');
  const pillText = document.getElementById('pillText');
  const whistleBtn = document.getElementById('whistleBtn');
  const muteBtn = document.getElementById('muteBtn');

  // Grid & timing constants
  const GRID = 14;
  const CELL = canvas.width / GRID; // 50px per cell
  const STEP_DURATION = 155; // ms per grid tile

  // Colorful wagon palette
  const WAGON_PALETTE = [
    { body: '#e53935', roof: '#ffcdd2', trim: '#b71c1c', name: 'Crimson Express' },
    { body: '#1e88e5', roof: '#bbdefb', trim: '#0d47a1', name: 'Sapphire Coach' },
    { body: '#fdd835', roof: '#fff9c4', trim: '#f57f17', name: 'Sunbeam Diner' },
    { body: '#43a047', roof: '#c8e6c9', trim: '#1b5e20', name: 'Emerald Sleeper' },
    { body: '#8e24aa', roof: '#e1bee7', trim: '#4a148c', name: 'Royal Parlour' },
    { body: '#fb8c00', roof: '#ffe0b2', trim: '#e65100', name: 'Amber Cargo' },
    { body: '#00acc1', roof: '#b2ebf2', trim: '#006064', name: 'Lagoon Lounge' }
  ];

  // Sound synthesis (Web Audio API)
  let audioCtx = null;
  let muted = false;

  function initAudio() {
    if (!audioCtx) {
      const AudioContext = window.AudioContext || window.webkitAudioContext;
      if (AudioContext) audioCtx = new AudioContext();
    }
    if (audioCtx && audioCtx.state === 'suspended') {
      audioCtx.resume();
    }
  }

  function playWhistle() {
    if (muted || !audioCtx) return;
    const now = audioCtx.currentTime;
    const freqs = [587.33, 739.99, 880]; // D major steam chord
    freqs.forEach(f => {
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(f, now);
      osc.frequency.exponentialRampToValueAtTime(f * 1.02, now + 0.18);
      gain.gain.setValueAtTime(0.001, now);
      gain.gain.linearRampToValueAtTime(0.07, now + 0.03);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.start(now);
      osc.stop(now + 0.36);
    });
  }

  function playBoardSound() {
    if (muted || !audioCtx) return;
    const now = audioCtx.currentTime;
    [523.25, 659.25, 783.99, 1046.5].forEach((f, idx) => {
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(f, now + idx * 0.045);
      gain.gain.setValueAtTime(0.08, now + idx * 0.045);
      gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.045 + 0.16);
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.start(now + idx * 0.045);
      osc.stop(now + idx * 0.045 + 0.17);
    });
  }

  function playCrashSound() {
    if (muted || !audioCtx) return;
    const now = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(160, now);
    osc.frequency.exponentialRampToValueAtTime(45, now + 0.4);
    gain.gain.setValueAtTime(0.12, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.42);
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start(now);
    osc.stop(now + 0.45);
  }

  // Scenery decorations (trees, flower beds, station clock)
  const trees = [
    { x: 0.6, y: 0.6, r: 16 }, { x: 13.4, y: 0.6, r: 17 },
    { x: 0.5, y: 13.4, r: 18 }, { x: 13.4, y: 13.4, r: 16 },
    { x: 3.5, y: 0.4, r: 12 }, { x: 10.5, y: 0.4, r: 13 },
    { x: 3.5, y: 13.6, r: 13 }, { x: 10.5, y: 13.6, r: 12 }
  ];

  // Game State
  let mode = 'DEMO'; // 'DEMO', 'PLAYING', 'GAMEOVER'
  let score = 0;
  let bestScore = 0;
  let train = [];
  let prevTrain = [];
  let dir = { x: 1, y: 0 };
  let inputQueue = [];
  let passenger = null;
  let smokePuffs = [];
  let floatingTexts = [];
  let boardingJump = null;
  let stepAccumulator = 0;
  let lastFrameTime = performance.now();
  let crashShake = 0;

  function pad2(n) {
    return String(n).padStart(2, '0');
  }

  function updateHUD() {
    scoreEl.textContent = pad2(score);
    wagonEl.textContent = pad2(Math.max(0, train.length - 1));
    bestEl.textContent = pad2(bestScore);
  }

  function createSegment(x, y, index) {
    const style = WAGON_PALETTE[(index - 1 + WAGON_PALETTE.length) % WAGON_PALETTE.length];
    return {
      x,
      y,
      style,
      hasPassenger: index > 0 // initial demo wagons look lively with passengers
    };
  }

  function resetTrain(isDemoMode) {
    mode = isDemoMode ? 'DEMO' : 'PLAYING';
    score = isDemoMode ? 5 : 0;
    dir = { x: 1, y: 0 };
    inputQueue = [];
    stepAccumulator = 0;

    const startX = 6;
    const startY = 7;
    const initialLen = 5; // 1 locomotive + 4 colorful wagons right from first paint
    train = [];
    for (let i = 0; i < initialLen; i++) {
      train.push(createSegment(startX - i, startY, i));
    }
    prevTrain = train.map(seg => ({ ...seg }));

    spawnPassenger();
    updateHUD();

    if (mode === 'DEMO') {
      overlayBanner.classList.remove('hidden');
      pillText.textContent = 'Auto-Conductor Demo Active';
      ctaTitle.textContent = 'Click or Press Any Key to Drive';
      ctaSub.textContent = 'Steer with Arrow Keys / WASD to pick up waiting passengers!';
    } else {
      overlayBanner.classList.add('hidden');
    }
  }

  function spawnPassenger() {
    const freeCells = [];
    for (let y = 1; y < GRID - 1; y++) {
      for (let x = 1; x < GRID - 1; x++) {
        const occupied = train.some(seg => seg.x === x && seg.y === y);
        if (!occupied) freeCells.push({ x, y });
      }
    }
    // Fallback to any free cell if inner ring is full
    if (freeCells.length === 0) {
      for (let y = 0; y < GRID; y++) {
        for (let x = 0; x < GRID; x++) {
          if (!train.some(seg => seg.x === x && seg.y === y)) {
            freeCells.push({ x, y });
          }
        }
      }
    }
    if (freeCells.length === 0) return;

    const pick = freeCells[Math.floor(Math.random() * freeCells.length)];
    const nextWagonIndex = train.length;
    const style = WAGON_PALETTE[(nextWagonIndex - 1) % WAGON_PALETTE.length];

    // Pick a cute platform offset side that isn't directly on top of the rail
    passenger = {
      x: pick.x,
      y: pick.y,
      style,
      hatColor: style.body,
      coatColor: style.roof,
      suitcaseColor: '#6d4c41',
      spawnTime: performance.now()
    };
  }

  // AI Pathfinder for Demo Mode (BFS shortest path + safe survival fallback)
  function computeAIDirection() {
    const head = train[0];
    const possibleDirs = [
      { x: 1, y: 0 },
      { x: -1, y: 0 },
      { x: 0, y: 1 },
      { x: 0, y: -1 }
    ].filter(d => !(d.x === -dir.x && d.y === -dir.y));

    const isSafeCell = (nx, ny) => {
      if (nx < 0 || nx >= GRID || ny < 0 || ny >= GRID) return false;
      // Tail cell will vacate unless boarding
      for (let i = 0; i < train.length - 1; i++) {
        if (train[i].x === nx && train[i].y === ny) return false;
      }
      return true;
    };

    if (!passenger) return dir;

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

    for (const d of possibleDirs) {
      const nx = head.x + d.x;
      const ny = head.y + d.y;
      if (isSafeCell(nx, ny)) {
        queue.push({ x: nx, y: ny, firstDir: d });
        visited.add(`${nx},${ny}`);
      }
    }

    while (queue.length > 0) {
      const curr = queue.shift();
      if (curr.x === passenger.x && curr.y === passenger.y) {
        return curr.firstDir;
      }
      for (const d of [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]) {
        const nx = curr.x + d.x;
        const ny = curr.y + d.y;
        const key = `${nx},${ny}`;
        if (!visited.has(key) && isSafeCell(nx, ny)) {
          visited.add(key);
          queue.push({ x: nx, y: ny, firstDir: curr.firstDir });
        }
      }
    }

    // Fallback: pick safe neighbor with most open neighbors
    const safeOptions = possibleDirs.filter(d => isSafeCell(head.x + d.x, head.y + d.y));
    if (safeOptions.length > 0) {
      safeOptions.sort((a, b) => {
        const countOpen = (d) => {
          let free = 0;
          const tx = head.x + d.x;
          const ty = head.y + d.y;
          for (const nd of [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]) {
            if (isSafeCell(tx + nd.x, ty + nd.y)) free++;
          }
          return free;
        };
        return countOpen(b) - countOpen(a);
      });
      return safeOptions[0];
    }

    return dir;
  }

  function spawnSmoke(px, py, count = 2, big = false) {
    for (let i = 0; i < count; i++) {
      smokePuffs.push({
        x: px + (Math.random() - 0.5) * 6,
        y: py + (Math.random() - 0.5) * 6,
        vx: (Math.random() - 0.5) * 18,
        vy: -15 - Math.random() * 20,
        r: big ? 7 + Math.random() * 5 : 4 + Math.random() * 4,
        alpha: 0.85,
        grow: big ? 18 : 12
      });
    }
  }

  function stepTrain() {
    if (mode === 'GAMEOVER') return;

    if (mode === 'DEMO') {
      dir = computeAIDirection();
    } else if (inputQueue.length > 0) {
      dir = inputQueue.shift();
    }

    prevTrain = train.map(seg => ({ ...seg }));

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

    const willPickUp = passenger && nextX === passenger.x && nextY === passenger.y;

    // Check collisions with station walls or wagons
    const hitWall = nextX < 0 || nextX >= GRID || nextY < 0 || nextY >= GRID;
    const checkLimit = willPickUp ? train.length : train.length - 1;
    let hitSelf = false;
    for (let i = 0; i < checkLimit; i++) {
      if (train[i].x === nextX && train[i].y === nextY) {
        hitSelf = true;
        break;
      }
    }

    if (hitWall || hitSelf) {
      if (mode === 'DEMO') {
        resetTrain(true);
        return;
      }
      mode = 'GAMEOVER';
      crashShake = 12;
      playCrashSound();
      overlayBanner.classList.remove('hidden');
      pillText.textContent = 'Track Derailment!';
      ctaTitle.textContent = `End of the Line! (${score} Boarded)`;
      ctaSub.textContent = 'Click or Press Any Key to Dispatch a New Train';
      return;
    }

    // Advance segments
    for (let i = train.length - 1; i >= 1; i--) {
      train[i].x = train[i - 1].x;
      train[i].y = train[i - 1].y;
    }
    head.x = nextX;
    head.y = nextY;

    if (willPickUp) {
      const tailOld = prevTrain[prevTrain.length - 1];
      const newSeg = createSegment(tailOld.x, tailOld.y, train.length);
      train.push(newSeg);
      prevTrain.push({ ...newSeg });

      score++;
      if (score > bestScore) bestScore = score;
      updateHUD();

      playBoardSound();

      const px = (nextX + 0.5) * CELL;
      const py = (nextY + 0.5) * CELL;
      spawnSmoke(px, py, 7, true);

      floatingTexts.push({
        x: px,
        y: py - 10,
        text: 'ALL ABOARD! +1',
        color: passenger.style.roof,
        stroke: passenger.style.trim,
        alpha: 1,
        vy: -32
      });

      boardingJump = {
        startX: px,
        startY: py - 18,
        color: passenger.hatColor,
        progress: 0
      };

      spawnPassenger();
    }
  }

  // Smooth interpolation helper
  function getInterpolatedPos(index, t) {
    const curr = train[index];
    const prev = prevTrain[index] || curr;
    return {
      x: (prev.x + (curr.x - prev.x) * t + 0.5) * CELL,
      y: (prev.y + (curr.y - prev.y) * t + 0.5) * CELL,
      angle: Math.atan2(curr.y - prev.y, curr.x - prev.x)
    };
  }

  // Drawing routines
  function drawBackgroundTracks() {
    // Lush lawn base
    ctx.fillStyle = '#4c8c2b';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Subtle checkerboard turf
    for (let y = 0; y < GRID; y++) {
      for (let x = 0; x < GRID; x++) {
        if ((x + y) % 2 === 0) {
          ctx.fillStyle = 'rgba(255, 255, 255, 0.04)';
          ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
        }
      }
    }

    // Crushed gravel ballast grid
    ctx.strokeStyle = 'rgba(62, 39, 35, 0.22)';
    ctx.lineWidth = 18;
    ctx.beginPath();
    for (let i = 0; i < GRID; i++) {
      const p = (i + 0.5) * CELL;
      ctx.moveTo(0, p);
      ctx.lineTo(canvas.width, p);
      ctx.moveTo(p, 0);
      ctx.lineTo(p, canvas.height);
    }
    ctx.stroke();

    // Wooden sleepers (ties) along every cell
    ctx.fillStyle = '#5d4037';
    for (let y = 0; y < GRID; y++) {
      for (let x = 0; x < GRID; x++) {
        const cx = (x + 0.5) * CELL;
        const cy = (y + 0.5) * CELL;
        ctx.fillRect(cx - 12, cy - 3, 24, 6);
        ctx.fillRect(cx - 3, cy - 12, 6, 24);
      }
    }

    // Steel rail lines across grid
    ctx.strokeStyle = 'rgba(207, 216, 220, 0.38)';
    ctx.lineWidth = 2;
    const railOffset = 7;
    ctx.beginPath();
    for (let i = 0; i < GRID; i++) {
      const p = (i + 0.5) * CELL;
      ctx.moveTo(0, p - railOffset);
      ctx.lineTo(canvas.width, p - railOffset);
      ctx.moveTo(0, p + railOffset);
      ctx.lineTo(canvas.width, p + railOffset);

      ctx.moveTo(p - railOffset, 0);
      ctx.lineTo(p - railOffset, canvas.height);
      ctx.moveTo(p + railOffset, 0);
      ctx.lineTo(p + railOffset, canvas.height);
    }
    ctx.stroke();

    // Perimeter Station Bumper Fence
    ctx.strokeStyle = '#3e2723';
    ctx.lineWidth = 8;
    ctx.strokeRect(4, 4, canvas.width - 8, canvas.height - 8);

    // Decorative corner trees
    trees.forEach(t => {
      const tx = t.x * CELL;
      const ty = t.y * CELL;
      ctx.fillStyle = 'rgba(0,0,0,0.22)';
      ctx.beginPath();
      ctx.arc(tx + 3, ty + 4, t.r, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = '#2e7d32';
      ctx.beginPath();
      ctx.arc(tx, ty, t.r, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = '#66bb6a';
      ctx.beginPath();
      ctx.arc(tx - 3, ty - 3, t.r * 0.6, 0, Math.PI * 2);
      ctx.fill();
    });
  }

  function drawWaitingPlatform(now) {
    if (!passenger) return;

    const cx = (passenger.x + 0.5) * CELL;
    const cy = (passenger.y + 0.5) * CELL;
    const pulse = Math.sin(now * 0.008);

    // Glowing arrival target zone on the track
    ctx.save();
    ctx.strokeStyle = passenger.style.roof;
    ctx.lineWidth = 2.5;
    ctx.setLineDash([5, 4]);
    ctx.beginPath();
    ctx.arc(cx, cy, 19 + pulse * 2, 0, Math.PI * 2);
    ctx.stroke();
    ctx.restore();

    // Wooden Boarding Platform Base just above/beside the tile
    const platW = 46;
    const platH = 20;
    const platX = cx - platW / 2;
    const platY = cy - 34;

    // Platform shadow
    ctx.fillStyle = 'rgba(0,0,0,0.35)';
    ctx.beginPath();
    ctx.roundRect(platX + 2, platY + 3, platW, platH, 5);
    ctx.fill();

    // Concrete/wood platform deck
    ctx.fillStyle = '#d7ccc8';
    ctx.strokeStyle = '#5d4037';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.roundRect(platX, platY, platW, platH, 5);
    ctx.fill();
    ctx.stroke();

    // Yellow safety line along platform edge
    ctx.fillStyle = '#fbc02d';
    ctx.fillRect(platX + 3, platY + platH - 4, platW - 6, 3);

    // Striped canopy / awning post on left of platform
    ctx.fillStyle = '#37474f';
    ctx.fillRect(platX + 4, platY - 10, 3, 14);
    ctx.fillStyle = passenger.style.body;
    ctx.beginPath();
    ctx.arc(platX + 5.5, platY - 12, 5, 0, Math.PI * 2);
    ctx.fill();

    // Waiting Passenger Character (bouncing and waving)
    const bounce = Math.abs(Math.sin(now * 0.009)) * 4;
    const px = cx + 2;
    const py = platY + 10 - bounce;

    // Little leather suitcase beside passenger
    ctx.fillStyle = '#5d4037';
    ctx.fillRect(px - 14, platY + 5, 8, 7);
    ctx.strokeStyle = '#ffca28';
    ctx.lineWidth = 1;
    ctx.strokeRect(px - 14, platY + 5, 8, 7);

    // Passenger coat/body
    ctx.fillStyle = passenger.style.body;
    ctx.beginPath();
    ctx.roundRect(px - 6, py - 4, 12, 11, 4);
    ctx.fill();

    // Waving arm
    const waveAngle = Math.sin(now * 0.018) * 0.6 - 0.8;
    ctx.save();
    ctx.translate(px + 5, py - 2);
    ctx.rotate(waveAngle);
    ctx.fillStyle = passenger.style.body;
    ctx.fillRect(0, -2, 8, 3.5);
    // White glove/ticket
    ctx.fillStyle = '#fff9c4';
    ctx.fillRect(6, -3, 4, 5);
    ctx.restore();

    // Passenger Head
    ctx.fillStyle = '#ffccbc';
    ctx.beginPath();
    ctx.arc(px, py - 8, 5.5, 0, Math.PI * 2);
    ctx.fill();

    // Cute Bowler / Conductor Hat matching wagon color
    ctx.fillStyle = passenger.style.trim;
    ctx.fillRect(px - 7, py - 12, 14, 2.5);
    ctx.fillStyle = passenger.style.body;
    ctx.beginPath();
    ctx.arc(px, py - 12, 4.5, Math.PI, 0);
    ctx.fill();
  }

  function drawWagon(pos, seg, index, now) {
    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.rotate(pos.angle);

    const w = 38;
    const h = 26;

    // Drop Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.32)';
    ctx.beginPath();
    ctx.roundRect(-w / 2 + 2, -h / 2 + 4, w, h, 6);
    ctx.fill();

    // Animated iron wheels (4 wheels peeking out)
    const wheelWobble = Math.sin(now * 0.03 + index) * 1.2;
    ctx.fillStyle = '#263238';
    const wheelPositions = [
      [-11, -h / 2 - 2], [11, -h / 2 - 2],
      [-11, h / 2 - 2],  [11, h / 2 - 2]
    ];
    wheelPositions.forEach(([wx, wy]) => {
      ctx.fillRect(wx - 4 + wheelWobble * 0.3, wy, 8, 4);
    });

    // Main Coach Body
    ctx.fillStyle = seg.style.body;
    ctx.strokeStyle = seg.style.trim;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.roundRect(-w / 2, -h / 2, w, h, 6);
    ctx.fill();
    ctx.stroke();

    // Cream Window Stripe & Passenger Silhouettes
    ctx.fillStyle = '#fff8e1';
    ctx.fillRect(-w / 2 + 5, -h / 2 + 2, 8, h - 4);
    ctx.fillRect(-4, -h / 2 + 2, 8, h - 4);
    ctx.fillRect(w / 2 - 13, -h / 2 + 2, 8, h - 4);

    // Center Clerestory Roof
    ctx.fillStyle = seg.style.roof;
    ctx.beginPath();
    ctx.roundRect(-w / 2 + 4, -h / 2 + 5, w - 8, h - 10, 4);
    ctx.fill();

    // Seated Passenger Hat/Head badge on roof showing wagon is occupied
    if (seg.hasPassenger) {
      ctx.fillStyle = seg.style.trim;
      ctx.beginPath();
      ctx.arc(-6, 0, 3.2, 0, Math.PI * 2);
      ctx.arc(6, 0, 3.2, 0, Math.PI * 2);
      ctx.fill();
    }

    ctx.restore();
  }

  function drawLocomotive(pos, now) {
    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.rotate(pos.angle);

    const w = 44;
    const h = 30;

    // Warm Headlight Beam
    const beamGrad = ctx.createRadialGradient(w / 2, 0, 2, w / 2 + 45, 0, 58);
    beamGrad.addColorStop(0, 'rgba(255, 241, 118, 0.55)');
    beamGrad.addColorStop(1, 'rgba(255, 241, 118, 0)');
    ctx.fillStyle = beamGrad;
    ctx.beginPath();
    ctx.moveTo(w / 2, -6);
    ctx.lineTo(w / 2 + 75, -34);
    ctx.lineTo(w / 2 + 75, 34);
    ctx.lineTo(w / 2, 6);
    ctx.closePath();
    ctx.fill();

    // Locomotive Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.4)';
    ctx.beginPath();
    ctx.roundRect(-w / 2 + 2, -h / 2 + 4, w, h, 6);
    ctx.fill();

    // Red Cowcatcher (Pilot) at front
    ctx.fillStyle = '#c62828';
    ctx.strokeStyle = '#ffca28';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(w / 2 - 2, -h / 2 + 3);
    ctx.lineTo(w / 2 + 9, 0);
    ctx.lineTo(w / 2 - 2, h / 2 - 3);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Side Piston Rods (animated)
    const rodShift = Math.sin(now * 0.025) * 4;
    ctx.fillStyle = '#cfd8dc';
    ctx.fillRect(-10 + rodShift, -h / 2 - 3, 20, 3);
    ctx.fillRect(-10 - rodShift, h / 2, 20, 3);

    // Rear Engineer Cab (Crimson & Gold)
    ctx.fillStyle = '#d32f2f';
    ctx.strokeStyle = '#ffb300';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.roundRect(-w / 2, -h / 2, 20, h, 5);
    ctx.fill();
    ctx.stroke();

    // Cab Roof Window
    ctx.fillStyle = '#fff9c4';
    ctx.fillRect(-w / 2 + 4, -h / 2 + 4, 11, h - 8);
    ctx.fillStyle = '#b71c1c';
    ctx.fillRect(-w / 2 + 3, -h / 2 + 6, 13, h - 12);

    // Cast-Iron Cylindrical Boiler
    ctx.fillStyle = '#263238';
    ctx.strokeStyle = '#455a64';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.roundRect(-4, -h / 2 + 3, 25, h - 6, 6);
    ctx.fill();
    ctx.stroke();

    // Brass Boiler Bands
    ctx.fillStyle = '#ffd54f';
    ctx.fillRect(2, -h / 2 + 3, 3, h - 6);
    ctx.fillRect(11, -h / 2 + 3, 3, h - 6);

    // Brass Steam Dome
    ctx.fillStyle = '#ffb300';
    ctx.beginPath();
    ctx.arc(4, 0, 5, 0, Math.PI * 2);
    ctx.fill();

    // Smokestack (Chimney) at front
    ctx.fillStyle = '#111';
    ctx.beginPath();
    ctx.arc(15, 0, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#ffd54f';
    ctx.lineWidth = 2;
    ctx.stroke();

    // Glowing Brass Lantern on nose
    ctx.fillStyle = '#ffee58';
    ctx.fillRect(w / 2 - 2, -4, 4, 8);

    ctx.restore();
  }

  function render(now) {
    const dt = Math.min(50, now - lastFrameTime);
    lastFrameTime = now;

    if (mode !== 'GAMEOVER') {
      stepAccumulator += dt;
      while (stepAccumulator >= STEP_DURATION) {
        stepAccumulator -= STEP_DURATION;
        stepTrain();
      }
    }

    const interpT = mode === 'GAMEOVER' ? 1 : Math.min(1, stepAccumulator / STEP_DURATION);

    ctx.save();
    if (crashShake > 0) {
      const dx = (Math.random() - 0.5) * crashShake;
      const dy = (Math.random() - 0.5) * crashShake;
      ctx.translate(dx, dy);
      crashShake = Math.max(0, crashShake - dt * 0.04);
    }

    drawBackgroundTracks();
    drawWaitingPlatform(now);

    // Compute smooth world coordinates for all segments
    const positions = [];
    for (let i = 0; i < train.length; i++) {
      let pos = getInterpolatedPos(i, interpT);
      // Keep locomotive orientation aligned with current travel direction if stationary
      if (i === 0 && pos.angle === 0 && (dir.x !== 1 || dir.y !== 0)) {
        pos.angle = Math.atan2(dir.y, dir.x);
      }
      positions.push(pos);
    }

    // Draw dark iron couplings linking every wagon
    ctx.strokeStyle = '#263238';
    ctx.lineWidth = 7;
    ctx.lineCap = 'round';
    ctx.beginPath();
    for (let i = 0; i < positions.length - 1; i++) {
      ctx.moveTo(positions[i].x, positions[i].y);
      ctx.lineTo(positions[i + 1].x, positions[i + 1].y);
    }
    ctx.stroke();

    // Brass coupling pins
    ctx.fillStyle = '#ffd54f';
    for (let i = 0; i < positions.length - 1; i++) {
      const mx = (positions[i].x + positions[i + 1].x) / 2;
      const my = (positions[i].y + positions[i + 1].y) / 2;
      ctx.beginPath();
      ctx.arc(mx, my, 3, 0, Math.PI * 2);
      ctx.fill();
    }

    // Draw colorful wagons from tail to head
    for (let i = positions.length - 1; i >= 1; i--) {
      drawWagon(positions[i], train[i], i, now);
    }

    // Draw steam locomotive at head
    if (positions.length > 0) {
      const headPos = positions[0];
      drawLocomotive(headPos, now);

      // Emit continuous chug-chug steam from the smokestack
      if (Math.random() < 0.38 && mode !== 'GAMEOVER') {
        const stackX = headPos.x + Math.cos(headPos.angle) * 14;
        const stackY = headPos.y + Math.sin(headPos.angle) * 14;
        spawnSmoke(stackX, stackY, 1, false);
      }
    }

    // Animate boarding hop arc if a passenger just hopped on
    if (boardingJump && positions.length > 1) {
      boardingJump.progress += dt * 0.0045;
      if (boardingJump.progress >= 1) {
        boardingJump = null;
      } else {
        const p = boardingJump.progress;
        const target = positions[positions.length - 1];
        const bx = boardingJump.startX + (target.x - boardingJump.startX) * p;
        const by = boardingJump.startY + (target.y - boardingJump.startY) * p - Math.sin(p * Math.PI) * 32;
        ctx.fillStyle = boardingJump.color;
        ctx.beginPath();
        ctx.arc(bx, by, 7, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 2;
        ctx.stroke();
      }
    }

    // Update & render steam puffs
    for (let i = smokePuffs.length - 1; i >= 0; i--) {
      const s = smokePuffs[i];
      s.x += (s.vx * dt) / 1000;
      s.y += (s.vy * dt) / 1000;
      s.r += (s.grow * dt) / 1000;
      s.alpha -= (dt / 1000) * 0.75;
      if (s.alpha <= 0) {
        smokePuffs.splice(i, 1);
        continue;
      }
      ctx.fillStyle = `rgba(245, 245, 245, ${s.alpha})`;
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }

    // Update & render floating boarding callouts
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
      const ft = floatingTexts[i];
      ft.y += (ft.vy * dt) / 1000;
      ft.alpha -= (dt / 1000) * 0.85;
      if (ft.alpha <= 0) {
        floatingTexts.splice(i, 1);
        continue;
      }
      ctx.save();
      ctx.font = '900 15px "Segoe UI", sans-serif';
      ctx.textAlign = 'center';
      ctx.globalAlpha = Math.max(0, ft.alpha);
      ctx.strokeStyle = '#263238';
      ctx.lineWidth = 4;
      ctx.strokeText(ft.text, ft.x, ft.y);
      ctx.fillStyle = '#fff59d';
      ctx.fillText(ft.text, ft.x, ft.y);
      ctx.restore();
    }

    ctx.restore();
    requestAnimationFrame(render);
  }

  // Input Handling
  function queueDirection(newDir) {
    const lastDir = inputQueue.length > 0 ? inputQueue[inputQueue.length - 1] : dir;
    // Prevent 180-degree self-reversal or duplicate directions
    if ((newDir.x === -lastDir.x && newDir.y === -lastDir.y) ||
        (newDir.x === lastDir.x && newDir.y === lastDir.y)) {
      return;
    }
    if (inputQueue.length < 2) {
      inputQueue.push(newDir);
    }
  }

  function startHumanGame(initialDir = null) {
    initAudio();
    playWhistle();
    resetTrain(false);
    if (initialDir) {
      queueDirection(initialDir);
    }
  }

  window.addEventListener('keydown', (e) => {
    const keyMap = {
      ArrowUp: { x: 0, y: -1 }, KeyW: { x: 0, y: -1 },
      ArrowDown: { x: 0, y: 1 }, KeyS: { x: 0, y: 1 },
      ArrowLeft: { x: -1, y: 0 }, KeyA: { x: -1, y: 0 },
      ArrowRight: { x: 1, y: 0 }, KeyD: { x: 1, y: 0 }
    };

    if (e.code === 'Space') {
      e.preventDefault();
      initAudio();
      playWhistle();
      if (train.length > 0) {
        spawnSmoke((train[0].x + 0.5) * CELL, (train[0].y + 0.5) * CELL, 8, true);
      }
      if (mode !== 'PLAYING') startHumanGame();
      return;
    }

    const mapped = keyMap[e.code];
    if (mapped) {
      e.preventDefault();
    }

    if (mode !== 'PLAYING') {
      startHumanGame(mapped || null);
      return;
    }

    if (mapped) {
      queueDirection(mapped);
    }
  });

  document.getElementById('viewportFrame').addEventListener('pointerdown', () => {
    if (mode !== 'PLAYING') {
      startHumanGame();
    }
  });

  ctaTicket.addEventListener('click', (e) => {
    e.stopPropagation();
    startHumanGame();
  });

  whistleBtn.addEventListener('click', () => {
    initAudio();
    playWhistle();
    if (train.length > 0) {
      spawnSmoke((train[0].x + 0.5) * CELL, (train[0].y + 0.5) * CELL, 8, true);
    }
  });

  muteBtn.addEventListener('click', () => {
    muted = !muted;
    muteBtn.textContent = muted ? '🔇' : '🔊';
  });

  document.querySelectorAll('.dpad-btn').forEach(btn => {
    btn.addEventListener('pointerdown', (e) => {
      e.preventDefault();
      const d = btn.dataset.dir;
      const map = {
        UP: { x: 0, y: -1 },
        DOWN: { x: 0, y: 1 },
        LEFT: { x: -1, y: 0 },
        RIGHT: { x: 1, y: 0 }
      };
      if (mode !== 'PLAYING') {
        startHumanGame(map[d]);
      } else {
        queueDirection(map[d]);
      }
    });
  });

  // Start immediately in self-playing Demo Mode
  resetTrain(true);
  requestAnimationFrame(render);
})();
</script>
</body>
</html>