Models 44
Train snakeOne-line

Gemini 3.8 Flash

by GoogleReasoningClean
Open in new window

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

How it went

Total time
1 min 51 s
Cost
$0.056
Output tokens
14,914
Spent thinking
364 tokens
Speed
152.4 tok/s
Console errors
0
First token after
12.8 s
First visible text after
16.0 s
Prompt tokens
369
Code size
37.3 KB
Finish reason
stop
Served by
Google AI Studio
Model id
google/gemini-3.8-flash
Reasoning sent
model default
Max output
65,536 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 · 1254 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 Express - Train Snake</title>
  <style>
    * {
      box-sizing: border-box;
      user-select: none;
      -webkit-user-select: none;
      margin: 0;
      padding: 0;
    }
    body {
      background: radial-gradient(circle at 50% 30%, #30583b, #182e1e 80%);
      color: #fff;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      overflow: hidden;
    }
    #game-container {
      position: relative;
      display: flex;
      flex-direction: column;
      align-items: center;
      box-shadow: 0 15px 35px rgba(0,0,0,0.5), 0 0 0 6px #785331, 0 0 0 10px #4d331c;
      border-radius: 16px;
      overflow: hidden;
      background: #739d4e;
    }
    header {
      width: 100%;
      background: linear-gradient(180deg, #99281e 0%, #6d1b14 100%);
      padding: 10px 18px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      box-shadow: 0 4px 10px rgba(0,0,0,0.3);
      border-bottom: 3px solid #f6c85f;
      z-index: 10;
    }
    .brand {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .brand h1 {
      font-size: 20px;
      letter-spacing: 1px;
      color: #fff8e7;
      text-shadow: 2px 2px 0px #3d0d09;
      font-weight: 800;
    }
    .badge {
      background: #f6c85f;
      color: #5c1811;
      padding: 2px 8px;
      border-radius: 12px;
      font-size: 11px;
      font-weight: bold;
      text-transform: uppercase;
      box-shadow: inset 0 -1px 2px rgba(0,0,0,0.2);
    }
    .stats {
      display: flex;
      gap: 16px;
      align-items: center;
    }
    .stat-pill {
      background: rgba(0, 0, 0, 0.35);
      border: 1px solid rgba(255, 255, 255, 0.2);
      border-radius: 20px;
      padding: 4px 12px;
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 13px;
      font-weight: 600;
    }
    .stat-pill span.val {
      color: #fdd835;
      font-size: 15px;
      font-weight: 800;
    }
    .horn-btn {
      background: #f59e0b;
      border: 2px solid #fbbf24;
      border-radius: 18px;
      color: #451a03;
      padding: 5px 12px;
      font-size: 12px;
      font-weight: 800;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 4px;
      transition: transform 0.1s, background 0.2s;
    }
    .horn-btn:hover { background: #fbbf24; transform: scale(1.05); }
    .horn-btn:active { transform: scale(0.95); }

    canvas {
      display: block;
      background: #7ca853;
      image-rendering: auto;
    }

    #overlay {
      position: absolute;
      top: 52px;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(18, 30, 20, 0.72);
      backdrop-filter: blur(3px);
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      color: white;
      text-align: center;
      padding: 20px;
      z-index: 20;
      transition: opacity 0.25s ease;
      cursor: pointer;
    }
    #overlay.hidden {
      opacity: 0;
      pointer-events: none;
    }
    .card {
      background: linear-gradient(135deg, #fffaf0, #f7ecd5);
      color: #3e2723;
      padding: 24px 34px;
      border-radius: 18px;
      border: 4px solid #b27a3c;
      box-shadow: 0 15px 30px rgba(0,0,0,0.5), inset 0 0 0 2px #fff;
      max-width: 440px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 12px;
    }
    .card h2 {
      font-size: 26px;
      color: #8c251d;
      margin: 0;
      text-transform: uppercase;
      letter-spacing: 1px;
    }
    .card p {
      font-size: 14px;
      line-height: 1.45;
      color: #5d4037;
      margin: 0;
    }
    .cta-button {
      margin-top: 8px;
      background: linear-gradient(180deg, #22c55e, #15803d);
      color: white;
      border: none;
      padding: 12px 28px;
      border-radius: 30px;
      font-size: 16px;
      font-weight: 800;
      letter-spacing: 0.5px;
      cursor: pointer;
      box-shadow: 0 6px 14px rgba(21, 128, 61, 0.4), inset 0 1px 0 rgba(255,255,255,0.4);
      animation: pulse 1.6s infinite ease-in-out;
    }
    @keyframes pulse {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.05); }
    }
    .controls-hint {
      display: flex;
      gap: 14px;
      margin-top: 4px;
      font-size: 12px;
      color: #795548;
    }
    .key {
      background: #e2d3be;
      border: 1px solid #bba58d;
      border-bottom: 2px solid #8d7054;
      padding: 2px 7px;
      border-radius: 4px;
      font-weight: bold;
      color: #3e2723;
    }

    #mobile-controls {
      display: none;
      grid-template-columns: repeat(3, 56px);
      grid-template-rows: repeat(2, 52px);
      gap: 6px;
      padding: 8px;
      background: rgba(0,0,0,0.25);
      width: 100%;
      justify-content: center;
    }
    .dpad-btn {
      background: #475569;
      color: white;
      border: none;
      border-radius: 10px;
      font-size: 20px;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 3px 0 #1e293b;
      touch-action: manipulation;
    }
    .dpad-btn:active {
      transform: translateY(2px);
      box-shadow: 0 1px 0 #1e293b;
    }
    .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: 680px) {
      #mobile-controls { display: grid; }
      header { padding: 8px 12px; }
      .brand h1 { font-size: 16px; }
      .card { padding: 18px 20px; }
      .card h2 { font-size: 22px; }
    }
  </style>
</head>
<body>

<div id="game-container">
  <header>
    <div class="brand">
      <span style="font-size:22px;">🚂</span>
      <div>
        <h1>Choo-Choo Express</h1>
      </div>
      <span class="badge" id="mode-badge">Auto Demo</span>
    </div>

    <div class="stats">
      <div class="stat-pill">
        <span>👥 Passengers:</span>
        <span class="val" id="score-val">0</span>
      </div>
      <div class="stat-pill">
        <span>🚉 Station:</span>
        <span class="val" id="station-val">Meadow</span>
      </div>
      <button class="horn-btn" id="horn-btn" title="Sound the Whistle!">🎺 Toot!</button>
    </div>
  </header>

  <canvas id="canvas" width="800" height="560"></canvas>

  <div id="overlay">
    <div class="card">
      <div style="font-size:42px; margin-top:-6px;">🚂💨</div>
      <h2 id="modal-title">All Aboard!</h2>
      <p id="modal-desc">Guide the steam engine, build your colorful train, and pick up happy passengers waiting at platforms across the countryside!</p>
      <button class="cta-button" id="start-btn">Click or Press Any Key to Play</button>
      <div class="controls-hint">
        <span>Use <span class="key">▲</span><span class="key">▼</span><span class="key">◄</span><span class="key">►</span> or <span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> to steer</span>
      </div>
    </div>
  </div>

  <div id="mobile-controls">
    <button class="dpad-btn d-up" data-dir="UP">▲</button>
    <button class="dpad-btn d-left" data-dir="LEFT">◄</button>
    <button class="dpad-btn d-down" data-dir="DOWN">▼</button>
    <button class="dpad-btn d-right" data-dir="RIGHT">►</button>
  </div>
</div>

<script>
(() => {
  // --- Audio Synthesis (Web Audio API) ---
  const AudioCtx = window.AudioContext || window.webkitAudioContext;
  let audioCtx = null;
  function initAudio() {
    if (!audioCtx) audioCtx = new AudioCtx();
    if (audioCtx.state === 'suspended') audioCtx.resume();
  }

  function playChug() {
    if (!audioCtx) return;
    try {
      const bufSize = audioCtx.sampleRate * 0.07;
      const buffer = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate);
      const output = buffer.getChannelData(0);
      for (let i = 0; i < bufSize; i++) {
        output[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufSize * 0.3));
      }
      const whiteNoise = audioCtx.createBufferSource();
      whiteNoise.buffer = buffer;
      const filter = audioCtx.createBiquadFilter();
      filter.type = 'bandpass';
      filter.frequency.value = 650;
      filter.Q.value = 2.0;

      const gain = audioCtx.createGain();
      gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.07);

      whiteNoise.connect(filter);
      filter.connect(gain);
      gain.connect(audioCtx.destination);
      whiteNoise.start();
    } catch(e) {}
  }

  function playWhistle() {
    initAudio();
    if (!audioCtx) return;
    try {
      const now = audioCtx.currentTime;
      [587.33, 739.99, 880.00].forEach((freq) => {
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(freq, now);
        osc.frequency.linearRampToValueAtTime(freq * 1.04, now + 0.35);
        osc.frequency.linearRampToValueAtTime(freq, now + 0.7);

        gain.gain.setValueAtTime(0, now);
        gain.gain.linearRampToValueAtTime(0.12, now + 0.08);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.7);

        const filter = audioCtx.createBiquadFilter();
        filter.type = 'lowpass';
        filter.frequency.value = 1600;

        osc.connect(filter);
        filter.connect(gain);
        gain.connect(audioCtx.destination);
        osc.start(now);
        osc.stop(now + 0.72);
      });
    } catch(e) {}
  }

  function playBoardingDing() {
    if (!audioCtx) return;
    try {
      const now = audioCtx.currentTime;
      [1046.50, 1318.51, 1567.98].forEach((f, idx) => {
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(f, now + idx * 0.08);
        gain.gain.setValueAtTime(0.18, now + idx * 0.08);
        gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.08 + 0.3);
        osc.connect(gain);
        gain.connect(audioCtx.destination);
        osc.start(now + idx * 0.08);
        osc.stop(now + idx * 0.08 + 0.32);
      });
    } catch(e) {}
  }

  function playCrash() {
    if (!audioCtx) return;
    try {
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(140, audioCtx.currentTime);
      osc.frequency.exponentialRampToValueAtTime(30, audioCtx.currentTime + 0.5);
      gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.start();
      osc.stop(audioCtx.currentTime + 0.55);
    } catch(e) {}
  }

  // --- Game Configuration & State ---
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const scoreValEl = document.getElementById('score-val');
  const stationValEl = document.getElementById('station-val');
  const overlayEl = document.getElementById('overlay');
  const modalTitle = document.getElementById('modal-title');
  const modalDesc = document.getElementById('modal-desc');
  const startBtn = document.getElementById('start-btn');
  const modeBadge = document.getElementById('mode-badge');
  const hornBtn = document.getElementById('horn-btn');

  const COLS = 25;
  const ROWS = 17;
  const TILE = 32; // 25 * 32 = 800, 17 * 32 = 544 (+16 margin = 560)
  const OFFSET_X = 0;
  const OFFSET_Y = 8;

  const STATIONS = ["Meadowbrook", "Pine Valley", "Sunnyside", "Apple Orchard", "Windmill Hill", "Golden Lake", "Grand Central"];
  const WAGON_COLORS = [
    { main: '#e11d48', trim: '#9f1239', roof: '#881337', label: '1st Class' }, // Red
    { main: '#2563eb', trim: '#1d4ed8', roof: '#1e3a8a', label: 'Standard' },  // Royal Blue
    { main: '#f59e0b', trim: '#d97706', roof: '#78350f', label: 'Dining' },    // Gold/Yellow
    { main: '#10b981', trim: '#059669', roof: '#064e3b', label: 'Sleeper' },   // Green
    { main: '#8b5cf6', trim: '#7c3aed', roof: '#4c1d95', label: 'Parlor' },    // Purple
    { main: '#06b6d4', trim: '#0891b2', roof: '#164e63', label: 'Observation' }, // Cyan
    { main: '#ea580c', trim: '#c2410c', roof: '#7c2d12', label: 'Express' },   // Orange
    { main: '#db2777', trim: '#be185d', roof: '#831843', label: 'Club' }       // Pink
  ];

  const PASSENGER_PALETTES = [
    { coat: '#dc2626', skin: '#ffd1a4', hat: '#1e293b' },
    { coat: '#2563eb', skin: '#f8c291', hat: '#f59e0b' },
    { coat: '#7c3aed', skin: '#cf8e5c', hat: '#374151' },
    { coat: '#16a34a', skin: '#ffd1a4', hat: '#991b1b' },
    { coat: '#ea580c', skin: '#8d5524', hat: '#1e3a8a' }
  ];

  // Scenery items (decorative trees, pond, rocks)
  const scenery = [];
  function generateScenery() {
    scenery.length = 0;
    // Pre-place cute decorations that avoid the starting snake path
    const spots = [
      {x: 2, y: 2, type: 'tree'}, {x: 2, y: 14, type: 'tree'},
      {x: 22, y: 2, type: 'tree'}, {x: 22, y: 14, type: 'tree'},
      {x: 4, y: 6, type: 'pond'},  {x: 20, y: 10, type: 'pond'},
      {x: 12, y: 2, type: 'tree'}, {x: 12, y: 14, type: 'tree'},
      {x: 6, y: 13, type: 'flower'}, {x: 18, y: 3, type: 'flower'},
      {x: 7, y: 3, type: 'flower'}, {x: 17, y: 13, type: 'flower'},
      {x: 1, y: 8, type: 'rock'},   {x: 23, y: 8, type: 'rock'}
    ];
    scenery.push(...spots);
  }
  generateScenery();

  let isAutoDemo = true;
  let isGameOver = false;
  let score = 0;
  let stationIndex = 0;

  // Directions
  const DIR = {
    UP:    { x: 0, y: -1, angle: -Math.PI / 2 },
    DOWN:  { x: 0, y: 1,  angle: Math.PI / 2 },
    LEFT:  { x: -1, y: 0, angle: Math.PI },
    RIGHT: { x: 1, y: 0,  angle: 0 }
  };

  let train = [];
  let currentDir = DIR.RIGHT;
  let nextDir = DIR.RIGHT;
  let passenger = { x: 15, y: 8, palette: PASSENGER_PALETTES[0], luggage: '#92400e', anim: 0 };
  let particles = [];
  let smokePuffs = [];
  let floatingTexts = [];

  // Track the history of visited cells so we can render cute wooden train tracks under the train!
  let trackHistory = [];
  const MAX_TRACK_HISTORY = 120;

  function initGame(auto = false) {
    isAutoDemo = auto;
    isGameOver = false;
    score = 0;
    stationIndex = 0;
    scoreValEl.textContent = '0';
    stationValEl.textContent = STATIONS[0];
    modeBadge.textContent = isAutoDemo ? 'Auto Demo' : 'Manual Drive';
    modeBadge.style.background = isAutoDemo ? '#f6c85f' : '#22c55e';
    modeBadge.style.color = isAutoDemo ? '#5c1811' : '#ffffff';

    currentDir = DIR.RIGHT;
    nextDir = DIR.RIGHT;

    // Start with locomotive + 3 initial wagons
    train = [
      { x: 8, y: 8, color: null }, // head = locomotive
      { x: 7, y: 8, color: WAGON_COLORS[0] },
      { x: 6, y: 8, color: WAGON_COLORS[1] },
      { x: 5, y: 8, color: WAGON_COLORS[2] }
    ];

    trackHistory = [];
    for (let i = 0; i < train.length; i++) {
      trackHistory.push({ x: train[i].x, y: train[i].y });
    }

    spawnPassenger();
    particles = [];
    smokePuffs = [];
    floatingTexts = [];
  }

  function spawnPassenger() {
    let valid = false;
    let attempts = 0;
    let newX = 0, newY = 0;
    while (!valid && attempts < 200) {
      attempts++;
      newX = Math.floor(Math.random() * (COLS - 2)) + 1;
      newY = Math.floor(Math.random() * (ROWS - 2)) + 1;
      valid = !train.some(seg => seg.x === newX && seg.y === newY);
      if (valid && scenery.some(s => s.x === newX && s.y === newY && s.type === 'pond')) {
        valid = false;
      }
    }
    const pal = PASSENGER_PALETTES[Math.floor(Math.random() * PASSENGER_PALETTES.length)];
    const luggageColors = ['#854d0e', '#7c2d12', '#1e3a8a', '#374151', '#065f46'];
    passenger = {
      x: newX,
      y: newY,
      palette: pal,
      luggage: luggageColors[Math.floor(Math.random() * luggageColors.length)],
      anim: 0
    };
  }

  function handleStart() {
    initAudio();
    if (isAutoDemo || isGameOver) {
      initGame(false);
      overlayEl.classList.add('hidden');
      playWhistle();
    }
  }

  overlayEl.addEventListener('click', handleStart);
  startBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    handleStart();
  });

  hornBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    playWhistle();
    spawnPuff(train[0].x * TILE + TILE/2, train[0].y * TILE + TILE/2, true);
    spawnFloatingText(train[0].x * TILE + TILE/2, train[0].y * TILE, "TOOT TOOT! 📢");
  });

  // Keyboard navigation
  window.addEventListener('keydown', (e) => {
    const key = e.key.toLowerCase();
    if (["arrowup", "arrowdown", "arrowleft", "arrowright", " ", "w", "a", "s", "d"].includes(key)) {
      e.preventDefault();
    }

    if (isAutoDemo || isGameOver) {
      handleStart();
      return;
    }

    if ((key === 'arrowup' || key === 'w') && currentDir !== DIR.DOWN) {
      nextDir = DIR.UP;
    } else if ((key === 'arrowdown' || key === 's') && currentDir !== DIR.UP) {
      nextDir = DIR.DOWN;
    } else if ((key === 'arrowleft' || key === 'a') && currentDir !== DIR.RIGHT) {
      nextDir = DIR.LEFT;
    } else if ((key === 'arrowright' || key === 'd') && currentDir !== DIR.LEFT) {
      nextDir = DIR.RIGHT;
    } else if (key === ' ' || key === 'h') {
      playWhistle();
      spawnFloatingText(train[0].x * TILE + TILE/2, train[0].y * TILE, "CHO-CHOO! 💨");
    }
  });

  // Mobile d-pad buttons
  document.querySelectorAll('.dpad-btn').forEach(btn => {
    btn.addEventListener('touchstart', (e) => {
      e.preventDefault();
      if (isAutoDemo || isGameOver) {
        handleStart();
        return;
      }
      const dirName = btn.dataset.dir;
      if (dirName === 'UP' && currentDir !== DIR.DOWN) nextDir = DIR.UP;
      if (dirName === 'DOWN' && currentDir !== DIR.UP) nextDir = DIR.DOWN;
      if (dirName === 'LEFT' && currentDir !== DIR.RIGHT) nextDir = DIR.LEFT;
      if (dirName === 'RIGHT' && currentDir !== DIR.LEFT) nextDir = DIR.RIGHT;
    });
    btn.addEventListener('click', (e) => {
      if (isAutoDemo || isGameOver) {
        handleStart();
        return;
      }
      const dirName = btn.dataset.dir;
      if (dirName === 'UP' && currentDir !== DIR.DOWN) nextDir = DIR.UP;
      if (dirName === 'DOWN' && currentDir !== DIR.UP) nextDir = DIR.DOWN;
      if (dirName === 'LEFT' && currentDir !== DIR.RIGHT) nextDir = DIR.LEFT;
      if (dirName === 'RIGHT' && currentDir !== DIR.LEFT) nextDir = DIR.RIGHT;
    });
  });

  // --- Auto-play AI (Breadth-First Search to find path to passenger) ---
  function getNextAiDirection() {
    const head = train[0];
    const target = { x: passenger.x, y: passenger.y };

    // BFS to find shortest path without crashing into self
    const queue = [[head.x, head.y, []]];
    const visited = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
    visited[head.y][head.x] = true;

    // Mark body segments as obstacles
    for (let i = 0; i < train.length - 1; i++) {
      const seg = train[i];
      if (seg.y >= 0 && seg.y < ROWS && seg.x >= 0 && seg.x < COLS) {
        visited[seg.y][seg.x] = true;
      }
    }

    let foundPath = null;
    const dirs = [DIR.UP, DIR.RIGHT, DIR.DOWN, DIR.LEFT];

    while (queue.length > 0) {
      const [cx, cy, path] = queue.shift();

      if (cx === target.x && cy === target.y) {
        foundPath = path;
        break;
      }

      for (const d of dirs) {
        const nx = cx + d.x;
        const ny = cy + d.y;
        if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && !visited[ny][nx]) {
          visited[ny][nx] = true;
          queue.push([nx, ny, [...path, d]]);
        }
      }
    }

    if (foundPath && foundPath.length > 0) {
      return foundPath[0];
    }

    // Fallback: pick any safe adjacent cell
    for (const d of dirs) {
      const nx = head.x + d.x;
      const ny = head.y + d.y;
      // Disallow 180 flip
      if (d.x === -currentDir.x && d.y === -currentDir.y) continue;
      if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS) {
        const collides = train.some(seg => seg.x === nx && seg.y === ny);
        if (!collides) return d;
      }
    }
    return currentDir;
  }

  // --- Particles & Effects ---
  function spawnPuff(x, y, big = false) {
    smokePuffs.push({
      x: x + (Math.random() - 0.5) * 6,
      y: y + (Math.random() - 0.5) * 6,
      vx: (Math.random() - 0.5) * 0.5,
      vy: -0.8 - Math.random() * 0.8,
      size: big ? 14 : 7 + Math.random() * 4,
      maxSize: big ? 32 : 18,
      alpha: 0.8,
      decay: big ? 0.015 : 0.025
    });
  }

  function spawnBoardingCelebration(px, py) {
    const colors = ['#f59e0b', '#ec4899', '#3b82f6', '#10b981', '#ffffff', '#fbbf24'];
    for (let i = 0; i < 22; i++) {
      const angle = (Math.PI * 2 * i) / 22 + Math.random() * 0.3;
      const speed = 1.5 + Math.random() * 3.5;
      particles.push({
        x: px,
        y: py,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed - 1.2,
        size: 3 + Math.random() * 3,
        color: colors[Math.floor(Math.random() * colors.length)],
        life: 1,
        decay: 0.02 + Math.random() * 0.02
      });
    }
    const boardingPhrases = ["All Aboard! 🎫", "Ticket Punched! ⭐", "Next Stop! 🔔", "Happy Journey! 🌻", "Ding-Dong! 🛎️"];
    const phrase = boardingPhrases[Math.floor(Math.random() * boardingPhrases.length)];
    spawnFloatingText(px, py - 10, phrase);
  }

  function spawnFloatingText(x, y, text) {
    floatingTexts.push({
      x, y, text, life: 1, vy: -1.2
    });
  }

  // --- Game Loop Timing ---
  let lastTick = 0;
  let tickInterval = 145; // ms per grid move (crisp, pleasant train pace)
  let lastPuffTime = 0;

  function update(time) {
    if (isAutoDemo) {
      nextDir = getNextAiDirection();
    }

    currentDir = nextDir;
    const newHead = {
      x: train[0].x + currentDir.x,
      y: train[0].y + currentDir.y,
      color: null
    };

    // Check Wall Collisions
    if (newHead.x < 0 || newHead.x >= COLS || newHead.y < 0 || newHead.y >= ROWS) {
      triggerGameOver("The train derailed at the boundary!");
      return;
    }

    // Check Self Collision
    for (let i = 0; i < train.length; i++) {
      if (train[i].x === newHead.x && train[i].y === newHead.y) {
        triggerGameOver("The engine collided with its own carriages!");
        return;
      }
    }

    // Advance Train
    train.unshift(newHead);
    trackHistory.push({ x: newHead.x, y: newHead.y });
    if (trackHistory.length > MAX_TRACK_HISTORY) trackHistory.shift();

    playChug();

    // Check Passenger Pickup
    if (newHead.x === passenger.x && newHead.y === passenger.y) {
      score++;
      scoreValEl.textContent = score;

      // Update station milestone every 4 passengers
      if (score % 4 === 0) {
        stationIndex = (stationIndex + 1) % STATIONS.length;
        stationValEl.textContent = STATIONS[stationIndex];
        spawnFloatingText(newHead.x * TILE + TILE/2, newHead.y * TILE - 15, `Arrived: ${STATIONS[stationIndex]}!`);
      }

      // Assign wagon color to new segment
      const chosenColor = WAGON_COLORS[(train.length - 1) % WAGON_COLORS.length];
      train[1].color = chosenColor;

      playBoardingDing();
      spawnBoardingCelebration(passenger.x * TILE + TILE/2, passenger.y * TILE + TILE/2);
      spawnPassenger();
    } else {
      // Normal move: remove tail
      train.pop();
    }

    // Smoke from smokestack
    const headAngle = currentDir.angle;
    const stackX = train[0].x * TILE + TILE/2 + Math.cos(headAngle) * 8;
    const stackY = train[0].y * TILE + TILE/2 + Math.sin(headAngle) * 8;
    spawnPuff(stackX, stackY, false);
  }

  function triggerGameOver(reason) {
    isGameOver = true;
    playCrash();
    modalTitle.textContent = "Derailment!";
    modalDesc.textContent = `${reason} You safely transported ${score} passenger${score === 1 ? '' : 's'} to ${STATIONS[stationIndex]}!`;
    startBtn.textContent = "Play Again (Click or Press Key)";
    overlayEl.classList.remove('hidden');
    modeBadge.textContent = 'Game Over';
    modeBadge.style.background = '#ef4444';
    modeBadge.style.color = '#fff';
  }

  // --- Drawing Routines ---

  function drawField(time) {
    // Grass checkerboard pattern
    ctx.fillStyle = '#6fa44b';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = '#679b43';
    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        if ((r + c) % 2 === 0) {
          ctx.fillRect(c * TILE, r * TILE + OFFSET_Y, TILE, TILE);
        }
      }
    }

    // Grid dots / subtle gravel
    ctx.fillStyle = 'rgba(0,0,0,0.035)';
    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        ctx.beginPath();
        ctx.arc(c * TILE + 4, r * TILE + OFFSET_Y + 4, 1.5, 0, Math.PI * 2);
        ctx.fill();
      }
    }

    // Scenery: ponds, flowers, trees
    scenery.forEach(item => {
      const sx = item.x * TILE;
      const sy = item.y * TILE + OFFSET_Y;
      if (item.type === 'tree') {
        // Shadow
        ctx.fillStyle = 'rgba(0,0,0,0.18)';
        ctx.beginPath();
        ctx.ellipse(sx + 16, sy + 25, 12, 6, 0, 0, Math.PI * 2);
        ctx.fill();
        // Trunk
        ctx.fillStyle = '#78350f';
        ctx.fillRect(sx + 13, sy + 16, 6, 10);
        // Foliage
        ctx.fillStyle = '#15803d';
        ctx.beginPath();
        ctx.arc(sx + 16, sy + 13, 11, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#22c55e';
        ctx.beginPath();
        ctx.arc(sx + 14, sy + 10, 8, 0, Math.PI * 2);
        ctx.fill();
      } else if (item.type === 'pond') {
        // Cute blue water tile
        ctx.fillStyle = '#38bdf8';
        ctx.beginPath();
        ctx.ellipse(sx + 16, sy + 16, 15, 12, 0.2, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#7dd3fc';
        ctx.beginPath();
        ctx.ellipse(sx + 14, sy + 14, 8, 5, 0.2, 0, Math.PI * 2);
        ctx.fill();
        // Lily pad
        ctx.fillStyle = '#16a34a';
        ctx.beginPath();
        ctx.arc(sx + 20, sy + 18, 3, 0, Math.PI * 1.7);
        ctx.fill();
      } else if (item.type === 'flower') {
        ctx.fillStyle = '#f43f5e';
        ctx.beginPath();
        ctx.arc(sx + 10, sy + 12, 2.5, 0, Math.PI * 2);
        ctx.arc(sx + 22, sy + 20, 2.5, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#fef08a';
        ctx.beginPath();
        ctx.arc(sx + 10, sy + 12, 1, 0, Math.PI * 2);
        ctx.arc(sx + 22, sy + 20, 1, 0, Math.PI * 2);
        ctx.fill();
      } else if (item.type === 'rock') {
        ctx.fillStyle = '#94a3b8';
        ctx.beginPath();
        ctx.ellipse(sx + 16, sy + 18, 7, 5, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#cbd5e1';
        ctx.beginPath();
        ctx.ellipse(sx + 15, sy + 16, 4, 3, -0.2, 0, Math.PI * 2);
        ctx.fill();
      }
    });

    // Outer fence/hedge border
    ctx.strokeStyle = '#476e2d';
    ctx.lineWidth = 4;
    ctx.strokeRect(2, OFFSET_Y + 2, canvas.width - 4, ROWS * TILE - 4);
  }

  function drawRailTracks() {
    // Draw railway sleepers and twin steel rails under the train's body segments
    ctx.save();
    for (let i = 0; i < train.length; i++) {
      const seg = train[i];
      const cx = seg.x * TILE + TILE / 2;
      const cy = seg.y * TILE + OFFSET_Y + TILE / 2;

      // Wooden cross-ties / sleepers
      ctx.fillStyle = '#785331';
      ctx.fillRect(cx - 13, cy - 13, 26, 4);
      ctx.fillRect(cx - 13, cy - 2, 26, 4);
      ctx.fillRect(cx - 13, cy + 9, 26, 4);

      // Steel rails (dual metallic lines)
      ctx.fillStyle = '#94a3b8';
      ctx.fillRect(cx - 9, cy - 16, 2.5, 32);
      ctx.fillRect(cx + 6.5, cy - 16, 2.5, 32);
      // Rail shine
      ctx.fillStyle = '#e2e8f0';
      ctx.fillRect(cx - 8.5, cy - 16, 1, 32);
      ctx.fillRect(cx + 7, cy - 16, 1, 32);
    }
    ctx.restore();
  }

  function drawPlatformAndPassenger(time) {
    const px = passenger.x * TILE;
    const py = passenger.y * TILE + OFFSET_Y;

    // Platform wooden deck
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.fillRect(px + 1, py + 3, TILE - 2, TILE - 2); // shadow

    ctx.fillStyle = '#b45309'; // Platform timber
    ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);

    // Platform planks
    ctx.strokeStyle = '#78350f';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(px + 2, py + 10); ctx.lineTo(px + TILE - 2, py + 10);
    ctx.moveTo(px + 2, py + 18); ctx.lineTo(px + TILE - 2, py + 18);
    ctx.moveTo(px + 2, py + 26); ctx.lineTo(px + TILE - 2, py + 26);
    ctx.stroke();

    // Platform tactile / yellow hazard warning edge
    ctx.fillStyle = '#facc15';
    ctx.fillRect(px + 2, py + 2, TILE - 4, 3);
    ctx.fillStyle = '#1e293b';
    for (let h = 0; h < 6; h++) {
      ctx.fillRect(px + 4 + h * 4, py + 2, 2, 3);
    }

    // Mini Station Post / Sign
    ctx.fillStyle = '#e2e8f0';
    ctx.fillRect(px + 24, py + 4, 5, 8);
    ctx.fillStyle = '#0284c7';
    ctx.fillRect(px + 22, py + 4, 8, 4);

    // Cute Luggage / Suitcase
    ctx.fillStyle = passenger.luggage;
    ctx.fillRect(px + 6, py + 18, 8, 6);
    // Luggage strap & handle
    ctx.fillStyle = '#fef08a';
    ctx.fillRect(px + 8, py + 18, 1, 6);
    ctx.fillRect(px + 11, py + 18, 1, 6);
    ctx.strokeStyle = '#374151';
    ctx.lineWidth = 1;
    ctx.strokeRect(px + 9, py + 16, 2, 2);

    // Passenger Character
    const bounce = Math.sin(time * 0.008) * 1.5;
    const charX = px + 17;
    const charY = py + 15 + bounce;

    // Body / Coat
    ctx.fillStyle = passenger.palette.coat;
    ctx.beginPath();
    ctx.roundRect(charX - 4.5, charY, 9, 10, [2, 2, 1, 1]);
    ctx.fill();

    // Head
    ctx.fillStyle = passenger.palette.skin;
    ctx.beginPath();
    ctx.arc(charX, charY - 4, 4.5, 0, Math.PI * 2);
    ctx.fill();

    // Waving Arm (waiting passenger waves hand to train!)
    const waveAngle = Math.sin(time * 0.012) * 0.6;
    ctx.save();
    ctx.translate(charX + 4, charY + 2);
    ctx.rotate(-0.5 + waveAngle);
    ctx.fillStyle = passenger.palette.coat;
    ctx.fillRect(0, -2, 7, 3);
    ctx.fillStyle = passenger.palette.skin;
    ctx.beginPath();
    ctx.arc(7, -0.5, 1.8, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    // Hat
    ctx.fillStyle = passenger.palette.hat;
    ctx.beginPath();
    ctx.ellipse(charX, charY - 7.5, 5.5, 1.8, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillRect(charX - 3.5, charY - 11, 7, 4);

    // Animated "Wait for me!" / sparkles above passenger
    const pSparkle = (time * 0.005) % 1;
    ctx.fillStyle = `rgba(255, 230, 100, ${1 - pSparkle})`;
    ctx.font = 'bold 9px sans-serif';
    ctx.fillText('👋', px + 13, py - 2 - pSparkle * 6);
  }

  function drawLocomotive(x, y, dir, time) {
    ctx.save();
    ctx.translate(x + TILE / 2, y + TILE / 2);
    ctx.rotate(dir.angle);

    // Cast soft shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.beginPath();
    ctx.roundRect(-15, -12, 30, 24, 6);
    ctx.fill();

    // Headlamp Glow Cone on tracks ahead
    const gradient = ctx.createRadialGradient(14, 0, 2, 40, 0, 36);
    gradient.addColorStop(0, 'rgba(254, 240, 138, 0.7)');
    gradient.addColorStop(0.4, 'rgba(254, 240, 138, 0.25)');
    gradient.addColorStop(1, 'rgba(254, 240, 138, 0)');
    ctx.fillStyle = gradient;
    ctx.beginPath();
    ctx.moveTo(14, -4);
    ctx.lineTo(48, -24);
    ctx.lineTo(48, 24);
    ctx.lineTo(14, 4);
    ctx.closePath();
    ctx.fill();

    // Locomotive main boiler chassis (rich emerald/black steam style with gold bands)
    ctx.fillStyle = '#1e293b'; // Dark steel boiler
    ctx.beginPath();
    ctx.roundRect(-14, -10, 28, 20, 5);
    ctx.fill();

    // Cowcatcher at very front
    ctx.fillStyle = '#dc2626';
    ctx.beginPath();
    ctx.moveTo(14, -9);
    ctx.lineTo(17, 0);
    ctx.lineTo(14, 9);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = '#facc15';
    ctx.lineWidth = 1;
    ctx.stroke();

    // Cabin at rear
    ctx.fillStyle = '#b91c1c'; // Red cabin
    ctx.fillRect(-14, -9, 10, 18);
    // Cabin window
    ctx.fillStyle = '#bae6fd';
    ctx.fillRect(-11, -7, 6, 5);
    ctx.fillRect(-11, 2, 6, 5);

    // Brass boiler bands
    ctx.fillStyle = '#eab308';
    ctx.fillRect(0, -9.5, 2.5, 19);
    ctx.fillRect(6, -9.5, 2.5, 19);

    // Smokestack (chimney)
    ctx.fillStyle = '#0f172a';
    ctx.beginPath();
    ctx.arc(8, 0, 3.8, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#eab308';
    ctx.beginPath();
    ctx.arc(8, 0, 2.2, 0, Math.PI * 2);
    ctx.fill();

    // Steam dome
    ctx.fillStyle = '#f59e0b';
    ctx.beginPath();
    ctx.arc(-1, 0, 3, 0, Math.PI * 2);
    ctx.fill();

    // Headlamp fixture
    ctx.fillStyle = '#eab308';
    ctx.fillRect(12, -3, 3, 6);
    ctx.fillStyle = '#fef08a';
    ctx.fillRect(14, -2, 2, 4);

    // Wheels / side rods
    ctx.fillStyle = '#0f172a';
    [-10, -2, 6].forEach(wx => {
      ctx.beginPath();
      ctx.arc(wx, -10.5, 2, 0, Math.PI * 2);
      ctx.arc(wx, 10.5, 2, 0, Math.PI * 2);
      ctx.fill();
    });

    ctx.restore();
  }

  function drawWagon(curr, prev, next, index, time) {
    const cx = curr.x * TILE + TILE / 2;
    const cy = curr.y * TILE + OFFSET_Y + TILE / 2;

    // Determine direction from previous segment to orient properly
    const dx = prev.x - curr.x;
    const dy = prev.y - curr.y;
    const angle = Math.atan2(dy, dx);

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

    // Wagon coupling link to front
    ctx.fillStyle = '#334155';
    ctx.fillRect(10, -2, 7, 4);

    // Wagon soft shadow
    ctx.fillStyle = 'rgba(0,0,0,0.26)';
    ctx.beginPath();
    ctx.roundRect(-13, -11, 26, 22, 4);
    ctx.fill();

    // Wagon body
    const col = curr.color || WAGON_COLORS[index % WAGON_COLORS.length];
    ctx.fillStyle = col.main;
    ctx.beginPath();
    ctx.roundRect(-12, -9, 24, 18, 4);
    ctx.fill();

    // Wagon decorative trim line
    ctx.fillStyle = col.trim;
    ctx.fillRect(-12, -2, 24, 4);

    // Wagon roof curved
    ctx.fillStyle = col.roof;
    ctx.beginPath();
    ctx.roundRect(-10, -7.5, 20, 15, 3);
    ctx.fill();

    // Passenger Windows showing silhouette inside
    ctx.fillStyle = '#e0f2fe';
    [-6, 1].forEach((wx, wIdx) => {
      // Twin windows on top & bottom side
      ctx.fillRect(wx, -7, 4.5, 3.5);
      ctx.fillRect(wx, 3.5, 4.5, 3.5);

      // Cute passenger head silhouettes in some windows!
      if ((index + wIdx) % 2 === 0) {
        ctx.fillStyle = '#1e293b';
        ctx.beginPath();
        ctx.arc(wx + 2.2, -5.2, 1.2, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#e0f2fe';
      }
    });

    // Wheels
    ctx.fillStyle = '#0f172a';
    [-8, 6].forEach(wx => {
      ctx.fillRect(wx - 2, -10.5, 4, 2);
      ctx.fillRect(wx - 2, 8.5, 4, 2);
    });

    ctx.restore();
  }

  function drawSmokeAndParticles() {
    // Render puffing steam
    for (let i = smokePuffs.length - 1; i >= 0; i--) {
      const p = smokePuffs[i];
      p.x += p.vx;
      p.y += p.vy;
      p.size = Math.min(p.maxSize, p.size + 0.35);
      p.alpha -= p.decay;

      if (p.alpha <= 0) {
        smokePuffs.splice(i, 1);
        continue;
      }

      ctx.save();
      ctx.fillStyle = `rgba(245, 245, 250, ${p.alpha})`;
      ctx.beginPath();
      ctx.arc(p.x, p.y + OFFSET_Y, p.size, 0, Math.PI * 2);
      ctx.fill();

      // Soft inner highlight
      ctx.fillStyle = `rgba(255, 255, 255, ${p.alpha * 0.7})`;
      ctx.beginPath();
      ctx.arc(p.x - p.size * 0.25, p.y + OFFSET_Y - p.size * 0.25, p.size * 0.5, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    // Render confetti / sparks
    for (let i = particles.length - 1; i >= 0; i--) {
      const pt = particles[i];
      pt.x += pt.vx;
      pt.y += pt.vy;
      pt.vy += 0.08; // gravity
      pt.life -= pt.decay;

      if (pt.life <= 0) {
        particles.splice(i, 1);
        continue;
      }

      ctx.fillStyle = pt.color;
      ctx.globalAlpha = Math.max(0, pt.life);
      ctx.beginPath();
      ctx.arc(pt.x, pt.y + OFFSET_Y, pt.size, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 1.0;
    }

    // Floating text banners
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
      const ft = floatingTexts[i];
      ft.y += ft.vy;
      ft.life -= 0.018;

      if (ft.life <= 0) {
        floatingTexts.splice(i, 1);
        continue;
      }

      ctx.save();
      ctx.font = 'bold 13px system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillStyle = `rgba(0, 0, 0, ${ft.life * 0.5})`;
      ctx.fillText(ft.text, ft.x + 1, ft.y + OFFSET_Y + 1);
      ctx.fillStyle = `rgba(255, 255, 255, ${ft.life})`;
      ctx.fillText(ft.text, ft.x, ft.y + OFFSET_Y);
      ctx.restore();
    }
  }

  // --- Main Animation Frame Loop ---
  function render(timestamp) {
    if (!lastTick) lastTick = timestamp;
    const delta = timestamp - lastTick;

    if (!isGameOver && delta >= tickInterval) {
      update(timestamp);
      lastTick = timestamp;
    }

    // Draw everything
    drawField(timestamp);
    drawRailTracks();
    drawPlatformAndPassenger(timestamp);

    // Draw Train Wagons (from caboose to front)
    for (let i = train.length - 1; i >= 1; i--) {
      const curr = train[i];
      const prev = train[i - 1];
      const next = train[i + 1] || null;
      drawWagon(curr, prev, next, i, timestamp);
    }

    // Draw Locomotive Engine (head)
    if (train.length > 0) {
      drawLocomotive(train[0].x * TILE, train[0].y * TILE + OFFSET_Y, currentDir, timestamp);
    }

    drawSmokeAndParticles();

    requestAnimationFrame(render);
  }

  // Start in Self-Playing Demo mode on load
  initGame(true);
  requestAnimationFrame(render);
})();
</script>
</body>
</html>
Reasoning trace (364 tokens)
**Conceptualizing the Snake Game**

I'm imagining a train-themed Snake game. The snake is now a locomotive, and its body is a series of colorful train carriages. Instead of simple food, players collect passengers waiting on platforms. These platforms could be tiny stations scattered around the grid, adding a bit of charm.


**Refining the Aesthetics**

I'm now picturing the game's visuals in greater detail. The train now has a puffing smokestack and glowing headlamp. The carriages are varied in color, with passengers holding luggage and waving. I'm exploring a countryside board aesthetic with rail sleepers, trees, and cute platforms.