Train snakeOne-line
Gemini 3.8 Flash
by GoogleReasoningClean
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
- 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.