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 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.