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 19 s
Cost
$0.051
Output tokens
13,545
Spent thinking
696 tokens
Speed
178.1 tok/s
Console errors
0
First token after
2.5 s
First visible text after
8.9 s
Prompt tokens
367
Code size
36.4 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"medium"}
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 · 1199 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 Train Express - Passenger Snake</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    body {
      background: #1e293b;
      color: #f8fafc;
      font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      overflow: hidden;
      padding: 10px;
    }
    #game-container {
      position: relative;
      background: #2e5c38;
      border: 6px solid #472b1d;
      border-radius: 16px;
      box-shadow: 0 16px 40px rgba(0,0,0,0.5), inset 0 0 20px rgba(0,0,0,0.25);
      max-width: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    #header {
      width: 100%;
      background: #3a2215;
      color: #fef08a;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 8px 18px;
      border-top-left-radius: 10px;
      border-top-right-radius: 10px;
      font-weight: bold;
      font-size: 15px;
      letter-spacing: 0.5px;
      border-bottom: 2px solid #5c3a21;
    }
    .badge {
      background: #54311c;
      color: #fff;
      padding: 3px 10px;
      border-radius: 12px;
      border: 1px solid #784528;
      font-size: 13px;
    }
    .val {
      color: #38bdf8;
      font-weight: 800;
    }
    canvas {
      display: block;
      background: #417244;
      cursor: pointer;
    }
    #overlay {
      position: absolute;
      top: 40px;
      left: 0;
      right: 0;
      bottom: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      background: rgba(15, 23, 42, 0.65);
      backdrop-filter: blur(2px);
      pointer-events: none;
      transition: opacity 0.25s ease;
      border-bottom-left-radius: 10px;
      border-bottom-right-radius: 10px;
      z-index: 10;
    }
    #overlay.hidden {
      opacity: 0;
      visibility: hidden;
    }
    .card {
      background: #fffbeb;
      color: #451a03;
      padding: 20px 28px;
      border-radius: 14px;
      text-align: center;
      box-shadow: 0 12px 30px rgba(0,0,0,0.4), inset 0 0 0 3px #b45309;
      max-width: 88%;
      animation: floatPrompt 2.4s ease-in-out infinite alternate;
    }
    @keyframes floatPrompt {
      0% { transform: translateY(0); }
      100% { transform: translateY(-5px); }
    }
    .card h1 {
      font-size: 26px;
      color: #9a3412;
      margin-bottom: 6px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
    }
    .card p {
      font-size: 15px;
      margin-bottom: 12px;
      color: #78350f;
    }
    .btn-prompt {
      display: inline-block;
      background: #ea580c;
      color: #fff;
      font-weight: bold;
      padding: 8px 18px;
      border-radius: 20px;
      font-size: 14px;
      box-shadow: 0 4px 10px rgba(234, 88, 12, 0.4);
      margin-top: 4px;
    }
    #touch-controls {
      display: none;
      width: 100%;
      justify-content: center;
      gap: 12px;
      padding: 10px;
      background: #2b170c;
      border-bottom-left-radius: 10px;
      border-bottom-right-radius: 10px;
    }
    .t-btn {
      background: #e2e8f0;
      color: #0f172a;
      font-size: 20px;
      font-weight: bold;
      border: none;
      width: 48px;
      height: 48px;
      border-radius: 10px;
      box-shadow: 0 3px 0 #94a3b8;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
    }
    .t-btn:active {
      transform: translateY(2px);
      box-shadow: none;
    }
    @media (max-width: 700px) {
      #touch-controls { display: flex; }
      .card h1 { font-size: 21px; }
      .card p { font-size: 13px; }
    }
  </style>
</head>
<body>

  <div id="game-container">
    <div id="header">
      <div>🚂 <span style="color:#fdba74">Tiny Train</span> Express</div>
      <div style="display:flex; gap:10px; align-items:center;">
        <div class="badge">Passengers: <span class="val" id="score-val">0</span></div>
        <div class="badge">Next Station: <span class="val" id="station-val" style="color:#fde047">Meadowbrook</span></div>
        <button id="mute-btn" style="background:#54311c; border:1px solid #784528; color:#fff; border-radius:8px; padding:2px 8px; cursor:pointer; font-size:12px;">🔊 Sound</button>
      </div>
    </div>

    <canvas id="gameCanvas" width="800" height="520"></canvas>

    <div id="overlay">
      <div class="card" id="overlay-card">
        <h1><span>🚂</span> All Aboard! <span>🎟️</span></h1>
        <p>Pick up cheerful passengers from station platforms.<br>Extend your train with colorful carriages!</p>
        <div style="font-size:12px; color:#92400e; margin-bottom:10px;">
          🎮 Controls: <b>Arrow Keys</b> or <b>W A S D</b>
        </div>
        <div class="btn-prompt" id="start-btn-text">Click or Press Any Key to Take the Controls</div>
        <div style="margin-top: 8px; font-size: 11px; opacity: 0.85;">(Autopilot demo running in background)</div>
      </div>
    </div>

    <div id="touch-controls">
      <button class="t-btn" id="btn-up">▲</button>
      <div style="display:flex; gap:14px;">
        <button class="t-btn" id="btn-left">◀</button>
        <button class="t-btn" id="btn-down">▼</button>
        <button class="t-btn" id="btn-right">▶</button>
      </div>
      <button class="t-btn" id="btn-whistle" style="background:#f59e0b; color:#fff;">📢</button>
    </div>
  </div>

  <script>
    // --- Audio Synthesis (Web Audio API) ---
    let audioCtx = null;
    let isMuted = false;

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

    function playWhistle() {
      if (isMuted || !audioCtx) return;
      try {
        const now = audioCtx.currentTime;
        // Dual tone train whistle
        [587.33, 739.99].forEach(freq => {
          const osc = audioCtx.createOscillator();
          const gain = audioCtx.createGain();
          osc.type = 'triangle';
          osc.frequency.setValueAtTime(freq * 0.96, now);
          osc.frequency.exponentialRampToValueAtTime(freq, now + 0.1);
          osc.frequency.exponentialRampToValueAtTime(freq * 0.98, now + 0.45);

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

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

    function playChug() {
      if (isMuted || !audioCtx) return;
      try {
        const now = audioCtx.currentTime;
        // White noise puff
        const bufferSize = audioCtx.sampleRate * 0.06;
        const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) {
          data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.4));
        }
        const noise = audioCtx.createBufferSource();
        noise.buffer = buffer;

        const filter = audioCtx.createBiquadFilter();
        filter.type = 'bandpass';
        filter.frequency.value = 450 + Math.random() * 100;
        filter.Q.value = 2.0;

        const gain = audioCtx.createGain();
        gain.gain.setValueAtTime(0.06, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.06);

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

    function playBoardingDing() {
      if (isMuted || !audioCtx) return;
      try {
        const now = audioCtx.currentTime;
        const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
        notes.forEach((freq, idx) => {
          const osc = audioCtx.createOscillator();
          const gain = audioCtx.createGain();
          osc.type = 'sine';
          osc.frequency.setValueAtTime(freq, now + idx * 0.05);

          gain.gain.setValueAtTime(0, now + idx * 0.05);
          gain.gain.linearRampToValueAtTime(0.12, now + idx * 0.05 + 0.02);
          gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.05 + 0.35);

          osc.connect(gain);
          gain.connect(audioCtx.destination);
          osc.start(now + idx * 0.05);
          osc.stop(now + idx * 0.05 + 0.38);
        });
      } catch(e) {}
    }

    function playCrashSound() {
      if (isMuted || !audioCtx) return;
      try {
        const now = audioCtx.currentTime;
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(140, now);
        osc.frequency.exponentialRampToValueAtTime(30, now + 0.4);

        gain.gain.setValueAtTime(0.2, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);

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

    document.getElementById('mute-btn').addEventListener('click', (e) => {
      e.stopPropagation();
      initAudio();
      isMuted = !isMuted;
      document.getElementById('mute-btn').innerText = isMuted ? '🔇 Muted' : '🔊 Sound';
    });

    // --- Grid & Game Setup ---
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');

    const COLS = 20;
    const ROWS = 13;
    const CELL_SIZE = 40; // 20 * 40 = 800, 13 * 40 = 520

    const STATIONS = [
      "Meadowbrook", "Sunnyvale", "Pine Creek", "Daisy Hill",
      "Willow Glen", "Buttercup Ridge", "Maple Junction", "Oakwood Station"
    ];

    const WAGON_PALETTES = [
      { body: '#dc2626', roof: '#991b1b', trim: '#fecaca' }, // Cherry Red
      { body: '#2563eb', roof: '#1e40af', trim: '#bfdbfe' }, // Royal Blue
      { body: '#d97706', roof: '#b45309', trim: '#fef3c7' }, // Amber Yellow
      { body: '#059669', roof: '#047857', trim: '#a7f3d0' }, // Emerald Green
      { body: '#7c3aed', roof: '#5b21b6', trim: '#ddd6fe' }, // Purple
      { body: '#db2777', roof: '#9d174d', trim: '#fbcfe8' }, // Pink
      { body: '#0891b2', roof: '#0e7490', trim: '#cffafe' }  // Teal
    ];

    const PASSENGER_TYPES = [
      { name: 'Gentleman', hat: '#1e293b', coat: '#3b82f6', skin: '#fcd34d', bag: '#78350f' },
      { name: 'Traveler', hat: '#dc2626', coat: '#10b981', skin: '#fed7aa', bag: '#92400e' },
      { name: 'Grandma', hat: '#f43f5e', coat: '#a855f7', skin: '#ffedd5', bag: '#b45309' },
      { name: 'Student', hat: '#eab308', coat: '#f97316', skin: '#fde047', bag: '#1e3a8a' },
      { name: 'Tourist', hat: '#06b6d4', coat: '#e11d48', skin: '#fbcfe8', bag: '#3f6212' }
    ];

    // Scenery: decorative trees, flowers, rocks fixed on grass
    const sceneryItems = [];
    function generateScenery() {
      sceneryItems.length = 0;
      for (let r = 0; r < ROWS; r++) {
        for (let c = 0; c < COLS; c++) {
          const rand = Math.random();
          if (rand < 0.12) {
            sceneryItems.push({
              x: c * CELL_SIZE + 8 + Math.random() * 24,
              y: r * CELL_SIZE + 8 + Math.random() * 24,
              type: rand < 0.08 ? 'tree' : 'flowers',
              variant: Math.floor(Math.random() * 3)
            });
          }
        }
      }
    }
    generateScenery();

    // Game state
    let isDemo = true;
    let gameOver = false;
    let score = 0;
    let currentStationIdx = 0;

    // Snake Representation
    let train = []; // Array of grid coordinates {c, r}
    let direction = { x: 1, y: 0 };
    let nextDirection = { x: 1, y: 0 };
    let wagonsColorList = [];

    // Passenger representation (Food)
    let passenger = { c: 14, r: 6, type: PASSENGER_TYPES[0], anim: 0, speechTimer: 0 };

    // Particles (Steam, hearts, confetti)
    let particles = [];

    // Animation / Move timers
    let lastTickTime = 0;
    let tickInterval = 175; // ms per grid move (smooth & readable)
    let moveProgress = 0;   // 0.0 to 1.0 interpolation between ticks
    let prevTrainPositions = [];

    function resetGame(startAsDemo = false) {
      isDemo = startAsDemo;
      gameOver = false;
      score = 0;
      currentStationIdx = 0;
      tickInterval = 175;
      direction = { x: 1, y: 0 };
      nextDirection = { x: 1, y: 0 };

      // Initialize train in center-left moving right
      const startC = 6;
      const startR = 6;
      train = [
        { c: startC, r: startR },
        { c: startC - 1, r: startR },
        { c: startC - 2, r: startR }
      ];
      prevTrainPositions = train.map(p => ({ ...p }));

      wagonsColorList = [
        WAGON_PALETTES[0],
        WAGON_PALETTES[1]
      ];

      particles = [];
      spawnPassenger();
      updateUI();

      if (isDemo) {
        document.getElementById('overlay').classList.remove('hidden');
        document.getElementById('overlay-card').querySelector('h1').innerHTML = '<span>🚂</span> All Aboard! <span>🎟️</span>';
        document.getElementById('start-btn-text').innerText = 'Click or Press Any Key to Play';
      } else {
        document.getElementById('overlay').classList.add('hidden');
        playWhistle();
      }
    }

    function spawnPassenger() {
      let valid = false;
      let c, r;
      let attempts = 0;
      while (!valid && attempts < 200) {
        attempts++;
        c = Math.floor(Math.random() * (COLS - 2)) + 1;
        r = Math.floor(Math.random() * (ROWS - 2)) + 1;
        // Don't spawn on train
        const onTrain = train.some(seg => seg.c === c && seg.r === r);
        if (!onTrain) valid = true;
      }
      passenger.c = c;
      passenger.r = r;
      passenger.type = PASSENGER_TYPES[Math.floor(Math.random() * PASSENGER_TYPES.length)];
      passenger.anim = 0;
      passenger.speechTimer = 180; // display welcome speech bubble
    }

    function updateUI() {
      document.getElementById('score-val').innerText = score;
      document.getElementById('station-val').innerText = STATIONS[currentStationIdx % STATIONS.length];
    }

    // --- AI / Autopilot for Demo Mode (Safe BFS to find path to passenger) ---
    function computeDemoNextDirection() {
      const head = train[0];
      const target = { c: passenger.c, r: passenger.r };

      // BFS to passenger
      const queue = [ [head] ];
      const visited = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
      visited[head.r][head.c] = true;

      // Mark train body as blocked (except the tail, which will move away)
      for (let i = 0; i < train.length - 1; i++) {
        const seg = train[i];
        if (seg.r >= 0 && seg.r < ROWS && seg.c >= 0 && seg.c < COLS) {
          visited[seg.r][seg.c] = true;
        }
      }

      const dirs = [
        { x: 1, y: 0 },
        { x: -1, y: 0 },
        { x: 0, y: 1 },
        { x: 0, y: -1 }
      ];

      let bestPath = null;

      while (queue.length > 0) {
        const path = queue.shift();
        const curr = path[path.length - 1];

        if (curr.c === target.c && curr.r === target.r) {
          bestPath = path;
          break;
        }

        for (const d of dirs) {
          const nc = curr.c + d.x;
          const nr = curr.r + d.y;

          if (nc >= 0 && nc < COLS && nr >= 0 && nr < ROWS && !visited[nr][nc]) {
            visited[nr][nc] = true;
            queue.push([...path, { c: nc, r: nr }]);
          }
        }
      }

      // If BFS found path, take first step
      if (bestPath && bestPath.length > 1) {
        const next = bestPath[1];
        return { x: next.c - head.c, y: next.r - head.r };
      }

      // Otherwise survive: pick any valid neighboring cell that doesn't immediately crash
      for (const d of dirs) {
        if (d.x === -direction.x && d.y === -direction.y) continue; // no 180 flip
        const nc = head.c + d.x;
        const nr = head.r + d.y;
        if (nc >= 0 && nc < COLS && nr >= 0 && nr < ROWS) {
          const hitsBody = train.slice(0, -1).some(s => s.c === nc && s.r === nr);
          if (!hitsBody) {
            return d;
          }
        }
      }

      return direction; // fallback
    }

    // --- Game Logic Tick ---
    function tick() {
      if (gameOver) return;

      if (isDemo) {
        nextDirection = computeDemoNextDirection();
      }

      direction = { ...nextDirection };

      // Save previous position snapshot for smooth rendering interpolation
      prevTrainPositions = train.map(p => ({ ...p }));

      const head = train[0];
      const newHead = {
        c: head.c + direction.x,
        r: head.r + direction.y
      };

      // Collision checks: Wall
      if (newHead.c < 0 || newHead.c >= COLS || newHead.r < 0 || newHead.r >= ROWS) {
        handleGameOver();
        return;
      }

      // Collision check: Self
      for (let i = 0; i < train.length; i++) {
        if (train[i].c === newHead.c && train[i].r === newHead.r) {
          handleGameOver();
          return;
        }
      }

      // Add puff of smoke at old engine position
      createSmokeParticle(head.c * CELL_SIZE + CELL_SIZE / 2, head.r * CELL_SIZE + CELL_SIZE / 2);
      playChug();

      // Check if boarded passenger
      if (newHead.c === passenger.c && newHead.r === passenger.r) {
        // Collect!
        score++;
        currentStationIdx++;
        updateUI();
        playBoardingDing();

        // Spawn boarding celebration particles
        createHeartBurst(passenger.c * CELL_SIZE + CELL_SIZE / 2, passenger.r * CELL_SIZE + CELL_SIZE / 2);

        // Train grows: append new wagon palette
        const randomPalette = WAGON_PALETTES[Math.floor(Math.random() * WAGON_PALETTES.length)];
        wagonsColorList.push(randomPalette);

        // Advance head without removing tail -> train grows
        train.unshift(newHead);
        prevTrainPositions.unshift({ ...head });

        // Speed up slightly as train gets longer
        tickInterval = Math.max(105, 175 - Math.min(score * 3, 70));

        spawnPassenger();
      } else {
        // Normal movement
        train.unshift(newHead);
        train.pop();
      }
    }

    function handleGameOver() {
      gameOver = true;
      playCrashSound();

      // Derailed sparks particles
      const head = train[0];
      for (let i = 0; i < 24; i++) {
        particles.push({
          x: head.c * CELL_SIZE + CELL_SIZE / 2,
          y: head.r * CELL_SIZE + CELL_SIZE / 2,
          vx: (Math.random() - 0.5) * 6,
          vy: (Math.random() - 0.5) * 6,
          color: Math.random() > 0.5 ? '#f59e0b' : '#ef4444',
          size: Math.random() * 5 + 3,
          life: 1.0,
          decay: 0.02 + Math.random() * 0.03,
          type: 'spark'
        });
      }

      if (isDemo) {
        // In demo mode, automatically respawn after brief pause
        setTimeout(() => {
          resetGame(true);
        }, 1200);
      } else {
        // Prompt player to restart
        document.getElementById('overlay').classList.remove('hidden');
        document.getElementById('overlay-card').querySelector('h1').innerHTML = '<span>💥</span> Derailed! <span>🛠️</span>';
        document.getElementById('overlay-card').querySelector('p').innerHTML = `You transported <b>${score}</b> happy passengers to their destinations!`;
        document.getElementById('start-btn-text').innerText = 'Click or Press Any Key to Play Again';
      }
    }

    // --- Particles & FX ---
    function createSmokeParticle(x, y) {
      particles.push({
        x: x + (Math.random() - 0.5) * 6,
        y: y + (Math.random() - 0.5) * 6,
        vx: -direction.x * 0.4 + (Math.random() - 0.5) * 0.8,
        vy: -direction.y * 0.4 - (0.4 + Math.random() * 0.6),
        size: 7 + Math.random() * 5,
        growth: 0.18,
        life: 1.0,
        decay: 0.022,
        color: '#ffffff',
        type: 'steam'
      });
    }

    function createHeartBurst(x, y) {
      for (let i = 0; i < 12; i++) {
        const angle = (Math.PI * 2 * i) / 12;
        const spd = 1.2 + Math.random() * 2.2;
        particles.push({
          x: x,
          y: y,
          vx: Math.cos(angle) * spd,
          vy: Math.sin(angle) * spd - 1.2,
          size: 6 + Math.random() * 4,
          life: 1.0,
          decay: 0.02,
          color: ['#f43f5e', '#38bdf8', '#fbbf24', '#34d399'][i % 4],
          type: 'heart'
        });
      }
    }

    function updateParticles() {
      for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        p.x += p.vx;
        p.y += p.vy;
        p.life -= p.decay;

        if (p.type === 'steam') {
          p.size += p.growth;
          p.vy -= 0.02; // rise
        } else if (p.type === 'spark') {
          p.vy += 0.18; // gravity
        } else if (p.type === 'heart') {
          p.vy += 0.04;
        }

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

    function drawParticles() {
      particles.forEach(p => {
        ctx.save();
        ctx.globalAlpha = Math.max(0, p.life);
        if (p.type === 'steam') {
          ctx.fillStyle = p.color;
          ctx.beginPath();
          ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
          ctx.fill();
        } else if (p.type === 'heart') {
          ctx.fillStyle = p.color;
          // Small sparkling diamond / ticket star
          ctx.beginPath();
          ctx.rect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
          ctx.fill();
        } else {
          ctx.fillStyle = p.color;
          ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
        }
        ctx.restore();
      });
    }

    // --- Drawing the World ---
    function drawGrassAndScenery() {
      // Background base
      ctx.fillStyle = '#417244';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // Subtle checkered lawn tiles
      for (let r = 0; r < ROWS; r++) {
        for (let c = 0; c < COLS; c++) {
          if ((c + r) % 2 === 0) {
            ctx.fillStyle = 'rgba(255, 255, 255, 0.035)';
            ctx.fillRect(c * CELL_SIZE, r * CELL_SIZE, CELL_SIZE, CELL_SIZE);
          }
        }
      }

      // Draw cute scenery items (trees, wildflowers)
      sceneryItems.forEach(item => {
        if (item.type === 'tree') {
          // Pine tree / round tree
          ctx.fillStyle = 'rgba(0,0,0,0.18)';
          ctx.beginPath();
          ctx.ellipse(item.x + 2, item.y + 10, 8, 4, 0, 0, Math.PI * 2);
          ctx.fill();

          ctx.fillStyle = '#2d532a';
          ctx.beginPath();
          ctx.arc(item.x, item.y, 10, 0, Math.PI * 2);
          ctx.fill();

          ctx.fillStyle = '#386c35';
          ctx.beginPath();
          ctx.arc(item.x - 2, item.y - 2, 7, 0, Math.PI * 2);
          ctx.fill();
        } else {
          // Wildflowers
          ctx.fillStyle = item.variant === 0 ? '#fde047' : (item.variant === 1 ? '#f472b6' : '#ffffff');
          ctx.beginPath();
          ctx.arc(item.x, item.y, 2.5, 0, Math.PI * 2);
          ctx.fill();
        }
      });
    }

    // Draw railway tracks under the train path
    function drawRailGrid() {
      // Draw standard gravel / ties across the active grid lines or under the train
      // To give a lovely model train feel, draw wooden sleepers and silver steel rails along the train route
      ctx.save();
      for (let i = 0; i < train.length; i++) {
        const seg = train[i];
        const cx = seg.c * CELL_SIZE + CELL_SIZE / 2;
        const cy = seg.r * CELL_SIZE + CELL_SIZE / 2;

        // Ballast gravel bed
        ctx.fillStyle = '#78716c';
        ctx.fillRect(seg.c * CELL_SIZE + 4, seg.r * CELL_SIZE + 4, CELL_SIZE - 8, CELL_SIZE - 8);

        // Wooden ties
        ctx.fillStyle = '#451a03';
        ctx.fillRect(seg.c * CELL_SIZE + 8, cy - 3, CELL_SIZE - 16, 6);
        ctx.fillRect(seg.c * CELL_SIZE + 8, cy - 14, CELL_SIZE - 16, 6);
        ctx.fillRect(seg.c * CELL_SIZE + 8, cy + 8, CELL_SIZE - 16, 6);

        // Steel rails
        ctx.fillStyle = '#cbd5e1';
        ctx.fillRect(seg.c * CELL_SIZE + 10, seg.r * CELL_SIZE + 2, 3, CELL_SIZE - 4);
        ctx.fillRect(seg.c * CELL_SIZE + CELL_SIZE - 13, seg.r * CELL_SIZE + 2, 3, CELL_SIZE - 4);
      }
      ctx.restore();
    }

    // --- Draw Passenger & Station Platform ---
    function drawPlatformAndPassenger(time) {
      const px = passenger.c * CELL_SIZE;
      const py = passenger.r * CELL_SIZE;

      ctx.save();

      // Platform Shadow
      ctx.fillStyle = 'rgba(0,0,0,0.22)';
      ctx.beginPath();
      ctx.roundRect(px + 2, py + 4, CELL_SIZE - 4, CELL_SIZE - 4, 6);
      ctx.fill();

      // Wooden / brick station platform
      ctx.fillStyle = '#a16207';
      ctx.beginPath();
      ctx.roundRect(px + 3, py + 3, CELL_SIZE - 6, CELL_SIZE - 6, 6);
      ctx.fill();

      // Platform planks
      ctx.fillStyle = '#ca8a04';
      ctx.fillRect(px + 5, py + 8, CELL_SIZE - 10, 4);
      ctx.fillRect(px + 5, py + 18, CELL_SIZE - 10, 4);
      ctx.fillRect(px + 5, py + 28, CELL_SIZE - 10, 4);

      // Yellow safety edge
      ctx.fillStyle = '#fde047';
      ctx.fillRect(px + 4, py + 4, CELL_SIZE - 8, 3);
      ctx.fillStyle = '#1e293b';
      for (let i = 8; i < CELL_SIZE - 8; i += 8) {
        ctx.fillRect(px + i, py + 4, 4, 3);
      }

      // Draw waiting passenger
      const p = passenger.type;
      const bounce = Math.sin(time * 0.008) * 2;
      const centerX = px + CELL_SIZE / 2;
      const centerY = py + CELL_SIZE / 2 - 2 + bounce;

      // Suitcase on platform
      ctx.fillStyle = p.bag;
      ctx.beginPath();
      ctx.roundRect(centerX + 6, centerY + 2, 7, 9, 2);
      ctx.fill();
      ctx.fillStyle = '#f59e0b';
      ctx.fillRect(centerX + 8, centerY, 3, 2); // Handle

      // Body (Coat)
      ctx.fillStyle = p.coat;
      ctx.beginPath();
      ctx.roundRect(centerX - 6, centerY - 2, 12, 12, 4);
      ctx.fill();

      // Head
      ctx.fillStyle = p.skin;
      ctx.beginPath();
      ctx.arc(centerX, centerY - 6, 5.5, 0, Math.PI * 2);
      ctx.fill();

      // Cute eyes
      ctx.fillStyle = '#0f172a';
      ctx.fillRect(centerX - 3, centerY - 7, 2, 2);
      ctx.fillRect(centerX + 1, centerY - 7, 2, 2);

      // Hat
      ctx.fillStyle = p.hat;
      ctx.beginPath();
      ctx.arc(centerX, centerY - 9, 5, Math.PI, 0);
      ctx.fill();
      ctx.fillRect(centerX - 7, centerY - 9, 14, 2); // Hat brim

      // Waving hand
      const waveAngle = Math.sin(time * 0.012) * 0.5;
      ctx.strokeStyle = p.skin;
      ctx.lineWidth = 2.5;
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(centerX - 5, centerY + 1);
      ctx.lineTo(centerX - 10, centerY - 3 + Math.sin(time * 0.012) * 4);
      ctx.stroke();

      // Speech / Waiting Bubble ("Choo Choo!" or Ticket icon)
      if (passenger.speechTimer > 0) {
        passenger.speechTimer--;
        ctx.fillStyle = '#ffffff';
        ctx.strokeStyle = '#334155';
        ctx.lineWidth = 1.5;
        const bx = centerX - 18;
        const by = centerY - 28;
        ctx.beginPath();
        ctx.roundRect(bx, by, 36, 16, 6);
        ctx.fill();
        ctx.stroke();

        // Pointer
        ctx.beginPath();
        ctx.moveTo(centerX - 3, by + 16);
        ctx.lineTo(centerX, by + 20);
        ctx.lineTo(centerX + 3, by + 16);
        ctx.fill();

        ctx.fillStyle = '#ea580c';
        ctx.font = 'bold 9px system-ui';
        ctx.textAlign = 'center';
        ctx.fillText('WAITING', centerX, by + 11);
      }

      ctx.restore();
    }

    // --- Draw The Train (Locomotive + Colorful Wagons) ---
    function drawTrain(time) {
      if (train.length === 0) return;

      // Render from tail to head so head sits on top
      for (let i = train.length - 1; i >= 0; i--) {
        const cur = train[i];
        const prev = prevTrainPositions[i] || cur;

        // Smooth sub-tick interpolated coordinates
        const interpX = (prev.c + (cur.c - prev.c) * moveProgress) * CELL_SIZE;
        const interpY = (prev.r + (cur.r - prev.r) * moveProgress) * CELL_SIZE;

        // Determine orientation
        let heading = { x: 1, y: 0 };
        if (i === 0) {
          heading = direction;
        } else {
          const lead = train[i - 1];
          heading = { x: lead.c - cur.c, y: lead.r - cur.r };
          if (heading.x === 0 && heading.y === 0) heading = direction;
        }

        const angle = Math.atan2(heading.y, heading.x);
        const cx = interpX + CELL_SIZE / 2;
        const cy = interpY + CELL_SIZE / 2;

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

        if (i === 0) {
          drawLocomotive(time);
        } else {
          const wagonColor = wagonsColorList[i - 1] || WAGON_PALETTES[0];
          drawWagon(wagonColor, i === train.length - 1);
        }

        ctx.restore();
      }
    }

    // Draw the Locomotive Engine
    function drawLocomotive(time) {
      // Cast shadow
      ctx.fillStyle = 'rgba(0,0,0,0.28)';
      ctx.beginPath();
      ctx.roundRect(-15, -12, 34, 26, 4);
      ctx.fill();

      // Front Cowcatcher / Pilot grill
      ctx.fillStyle = '#1e293b';
      ctx.beginPath();
      ctx.moveTo(14, -10);
      ctx.lineTo(19, 0);
      ctx.lineTo(14, 10);
      ctx.closePath();
      ctx.fill();

      // Main boiler (Steam Engine Body) - Rich Navy / Black
      ctx.fillStyle = '#0f172a';
      ctx.beginPath();
      ctx.roundRect(-14, -11, 28, 22, 4);
      ctx.fill();

      // Brass / Gold bands on boiler
      ctx.fillStyle = '#f59e0b';
      ctx.fillRect(0, -11, 2.5, 22);
      ctx.fillRect(7, -11, 2.5, 22);

      // Cab cabin (rear)
      ctx.fillStyle = '#b91c1c'; // Red driver's cab
      ctx.beginPath();
      ctx.roundRect(-14, -12, 13, 24, 3);
      ctx.fill();

      // Cab windows
      ctx.fillStyle = '#7dd3fc';
      ctx.fillRect(-10, -10, 6, 4);
      ctx.fillRect(-10, 6, 6, 4);

      // Gold dome / bell
      ctx.fillStyle = '#fbbf24';
      ctx.beginPath();
      ctx.arc(2, 0, 3.5, 0, Math.PI * 2);
      ctx.fill();

      // Smokestack (chimney)
      ctx.fillStyle = '#334155';
      ctx.beginPath();
      ctx.arc(10, 0, 4.5, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#f59e0b';
      ctx.beginPath();
      ctx.arc(10, 0, 2.5, 0, Math.PI * 2);
      ctx.fill();

      // Golden Headlight with active glowing light beam
      ctx.fillStyle = '#fef08a';
      ctx.beginPath();
      ctx.arc(16, 0, 3.5, 0, Math.PI * 2);
      ctx.fill();

      // Headlight glow cone
      ctx.save();
      const grad = ctx.createRadialGradient(16, 0, 2, 45, 0, 35);
      grad.addColorStop(0, 'rgba(254, 240, 138, 0.45)');
      grad.addColorStop(1, 'rgba(254, 240, 138, 0.0)');
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.moveTo(16, -2);
      ctx.lineTo(55, -22);
      ctx.lineTo(55, 22);
      ctx.lineTo(16, 2);
      ctx.closePath();
      ctx.fill();
      ctx.restore();

      // Wheels on sides
      ctx.fillStyle = '#475569';
      [-8, 4].forEach(wx => {
        ctx.fillRect(wx - 3, -13, 6, 2.5);
        ctx.fillRect(wx - 3, 10.5, 6, 2.5);
      });
    }

    // Draw Passenger Wagons / Carriages
    function drawWagon(palette, isCaboose) {
      // Cast shadow
      ctx.fillStyle = 'rgba(0,0,0,0.22)';
      ctx.beginPath();
      ctx.roundRect(-14, -11, 28, 24, 3);
      ctx.fill();

      // Coupler hitch link
      ctx.fillStyle = '#334155';
      ctx.fillRect(-17, -2, 5, 4);

      // Wagon Body
      ctx.fillStyle = palette.body;
      ctx.beginPath();
      ctx.roundRect(-13, -11, 26, 22, 4);
      ctx.fill();

      // Roof strip
      ctx.fillStyle = palette.roof;
      ctx.beginPath();
      ctx.roundRect(-11, -9, 22, 18, 3);
      ctx.fill();

      // Windows with warm light & passenger silhouettes
      ctx.fillStyle = '#fef08a';
      const winPositions = [-8, -1, 6];
      winPositions.forEach((wx, idx) => {
        // Top window
        ctx.fillRect(wx, -10, 4.5, 3.5);
        // Bottom window
        ctx.fillRect(wx, 6.5, 4.5, 3.5);

        // Tiny passenger silhouette in some windows
        if ((idx + 1) % 2 === 0) {
          ctx.fillStyle = '#1e293b';
          ctx.fillRect(wx + 1, -9, 2.5, 2.5);
          ctx.fillRect(wx + 1, 7.5, 2.5, 2.5);
          ctx.fillStyle = '#fef08a';
        }
      });

      // Decorative central line / trim
      ctx.fillStyle = palette.trim;
      ctx.fillRect(-12, -1, 24, 2);

      // If caboose, add lantern / balcony railings
      if (isCaboose) {
        ctx.fillStyle = '#ef4444'; // Red tail lantern
        ctx.beginPath();
        ctx.arc(-14, 0, 2.5, 0, Math.PI * 2);
        ctx.fill();
      }

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

    // --- Main Game Loop ---
    function gameLoop(timestamp) {
      if (!lastTickTime) lastTickTime = timestamp;
      const elapsed = timestamp - lastTickTime;

      // Calculate interpolation fraction for smooth rendering
      moveProgress = Math.min(1.0, elapsed / tickInterval);

      if (elapsed >= tickInterval) {
        lastTickTime = timestamp;
        tick();
        moveProgress = 0.0;
      }

      updateParticles();

      // Render Scene
      drawGrassAndScenery();
      drawRailGrid();
      drawPlatformAndPassenger(timestamp);
      drawTrain(timestamp);
      drawParticles();

      requestAnimationFrame(gameLoop);
    }

    // --- Input & Controls ---
    function handleDirectionInput(dx, dy) {
      initAudio();
      if (isDemo) {
        // User takes control!
        resetGame(false);
      }

      // Prevent 180-degree instant reversal into own neck
      if (train.length > 1) {
        if (dx === -direction.x && dy === -direction.y) return;
      }
      nextDirection = { x: dx, y: dy };
    }

    window.addEventListener('keydown', (e) => {
      initAudio();
      if (['ArrowUp', 'KeyW'].includes(e.code)) {
        e.preventDefault();
        handleDirectionInput(0, -1);
      } else if (['ArrowDown', 'KeyS'].includes(e.code)) {
        e.preventDefault();
        handleDirectionInput(0, 1);
      } else if (['ArrowLeft', 'KeyA'].includes(e.code)) {
        e.preventDefault();
        handleDirectionInput(-1, 0);
      } else if (['ArrowRight', 'KeyD'].includes(e.code)) {
        e.preventDefault();
        handleDirectionInput(1, 0);
      } else if (e.code === 'Space') {
        e.preventDefault();
        if (isDemo || gameOver) {
          resetGame(false);
        } else {
          playWhistle();
        }
      } else {
        if (isDemo || gameOver) {
          resetGame(false);
        }
      }
    });

    // Touch and on-screen button handlers
    canvas.addEventListener('click', () => {
      initAudio();
      if (isDemo || gameOver) {
        resetGame(false);
      } else {
        playWhistle();
      }
    });

    document.getElementById('overlay').addEventListener('click', () => {
      initAudio();
      resetGame(false);
    });

    document.getElementById('btn-up').addEventListener('click', (e) => { e.stopPropagation(); handleDirectionInput(0, -1); });
    document.getElementById('btn-down').addEventListener('click', (e) => { e.stopPropagation(); handleDirectionInput(0, 1); });
    document.getElementById('btn-left').addEventListener('click', (e) => { e.stopPropagation(); handleDirectionInput(-1, 0); });
    document.getElementById('btn-right').addEventListener('click', (e) => { e.stopPropagation(); handleDirectionInput(1, 0); });
    document.getElementById('btn-whistle').addEventListener('click', (e) => { e.stopPropagation(); initAudio(); playWhistle(); });

    // Touch Swipe handling on canvas
    let touchStartX = 0;
    let touchStartY = 0;
    canvas.addEventListener('touchstart', (e) => {
      if (e.touches.length > 0) {
        touchStartX = e.touches[0].clientX;
        touchStartY = e.touches[0].clientY;
      }
    }, { passive: true });

    canvas.addEventListener('touchend', (e) => {
      if (e.changedTouches.length > 0) {
        const dx = e.changedTouches[0].clientX - touchStartX;
        const dy = e.changedTouches[0].clientY - touchStartY;
        if (Math.abs(dx) > 20 || Math.abs(dy) > 20) {
          if (Math.abs(dx) > Math.abs(dy)) {
            handleDirectionInput(dx > 0 ? 1 : -1, 0);
          } else {
            handleDirectionInput(0, dy > 0 ? 1 : -1);
          }
        }
      }
    }, { passive: true });

    // Start with demo mode immediately active so screenshots & initial preview look lively!
    resetGame(true);
    requestAnimationFrame(gameLoop);
  </script>
</body>
</html>
Reasoning trace (696 tokens)
**Developing Visual Metaphor**

I'm visualizing the classic snake game mechanics as a charming little train. My focus is on how to represent the train's body as colorful carriages and the growing length as picked-up passengers at various station platforms. The goal is a self-contained HTML file for ease of use.

**Refining Visual Style**

I'm refining the visual details, aiming for a charming model train aesthetic. I'm leaning towards canvas for smooth particle effects like steam and dynamic track elements. The engine will feature a puffing chimney and a subtle glow beam, while colorful carriages will be added as passengers are collected.

**Enhancing Train Details**

I'm currently exploring how to implement vibrant, distinctly colored carriages with miniature passengers and luggage, and thinking about rendering charming railway tracks that dynamically lay beneath the train's path for a visually engaging experience. I'm also considering animated passenger sprites with their own little station details.