Train snakeOne-line
Gemini 4 Argon
by Googlereasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 3 min 3 s
- Cost
- $0.138
- Output tokens
- 13,734
- Spent thinking
- 0 tokens
- Speed
- 77.2 tok/s
- Console errors
- 0
- First token after
- 4.8 s
- First visible text after
- 4.8 s
- Prompt tokens
- 406
- Code size
- 35.0 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-4-argon- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1286 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Choo-Choo Snake: All Aboard!</title>
<style>
:root {
--bg-dark: #263238;
--brass: #d4af37;
--brass-dark: #997a15;
--wood: #4e342e;
--cream: #fff8e7;
--red: #e53935;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-dark);
background-image: radial-gradient(#37474f 15%, transparent 16%), radial-gradient(#37474f 15%, transparent 16%);
background-size: 30px 30px;
background-position: 0 0, 15px 15px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: var(--cream);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow: hidden;
}
.station-shell {
display: flex;
flex-direction: column;
align-items: center;
background: var(--wood);
padding: 14px;
border-radius: 18px;
border: 4px solid var(--brass);
box-shadow: 0 16px 40px rgba(0,0,0,0.6), inset 0 2px 6px rgba(255,255,255,0.2);
max-width: 96vw;
}
/* Header / Station Signboard */
.header-board {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: #1b2326;
border: 3px solid var(--brass);
border-radius: 10px;
padding: 8px 16px;
margin-bottom: 12px;
box-shadow: inset 0 2px 8px rgba(0,0,0,0.8);
gap: 12px;
flex-wrap: wrap;
}
.title-badge {
display: flex;
align-items: center;
gap: 10px;
}
.title-badge h1 {
margin: 0;
font-size: 1.35rem;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--brass);
text-shadow: 0 2px 0 #000;
}
.train-icon {
font-size: 1.5rem;
}
.stats-group {
display: flex;
gap: 14px;
align-items: center;
}
.flap-display {
background: #111;
border: 2px solid #444;
border-radius: 6px;
padding: 4px 10px;
text-align: center;
min-width: 82px;
}
.flap-label {
font-size: 0.65rem;
color: #aaa;
text-transform: uppercase;
letter-spacing: 1px;
display: block;
}
.flap-value {
font-family: 'Courier New', Courier, monospace;
font-size: 1.25rem;
font-weight: bold;
color: #ffd54f;
}
.btn-group {
display: flex;
gap: 8px;
}
.rail-btn {
background: linear-gradient(to bottom, #ffd54f, #ffb300);
border: 2px solid #fff3e0;
color: #3e2723;
font-weight: 800;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
box-shadow: 0 3px 0 #b26a00;
transition: transform 0.1s, box-shadow 0.1s;
}
.rail-btn:active {
transform: translateY(2px);
box-shadow: 0 1px 0 #b26a00;
}
/* Canvas Container */
.viewport-frame {
position: relative;
border: 4px solid #2b1b17;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.5);
background: #558b2f;
width: min(640px, 90vw);
height: min(640px, 90vw);
max-height: 70vh;
max-width: 70vh;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
/* Overlay Banner */
.overlay-banner {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 22px;
pointer-events: none;
transition: opacity 0.25s ease;
}
.overlay-banner.hidden {
opacity: 0;
}
.demo-pill {
background: rgba(27, 35, 38, 0.88);
border: 2px solid var(--brass);
color: #fff;
padding: 6px 16px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 1.2px;
text-transform: uppercase;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
display: flex;
align-items: center;
gap: 8px;
}
.pulse-dot {
width: 10px;
height: 10px;
background: #4caf50;
border-radius: 50%;
animation: pulse 1.2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.6; }
}
.cta-ticket {
background: rgba(255, 248, 231, 0.95);
color: #2b1b17;
border: 3px dashed #b26a00;
border-radius: 12px;
padding: 14px 26px;
text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.55);
pointer-events: auto;
cursor: pointer;
transform: translateY(0);
animation: floatTicket 2.4s ease-in-out infinite;
}
@keyframes floatTicket {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.cta-ticket h2 {
margin: 0 0 4px 0;
font-size: 1.35rem;
color: #c62828;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.cta-ticket p {
margin: 0;
font-size: 0.9rem;
font-weight: 600;
color: #4e342e;
}
/* Touch D-Pad for smaller viewports */
.dpad {
display: none;
margin-top: 10px;
gap: 6px;
grid-template-columns: repeat(3, 52px);
grid-template-rows: repeat(2, 44px);
justify-content: center;
}
.dpad-btn {
background: #37474f;
border: 2px solid var(--brass);
color: var(--cream);
border-radius: 8px;
font-size: 1.2rem;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.dpad-btn:active {
background: var(--brass);
color: #000;
}
@media (max-width: 680px) {
.title-badge h1 { font-size: 1.05rem; }
.dpad { display: grid; }
.viewport-frame {
max-height: 56vh;
max-width: 56vh;
}
}
</style>
</head>
<body>
<div class="station-shell">
<div class="header-board">
<div class="title-badge">
<span class="train-icon">🚂</span>
<h1>Grand Central Express</h1>
</div>
<div class="stats-group">
<div class="flap-display">
<span class="flap-label">Passengers</span>
<span class="flap-value" id="scoreVal">00</span>
</div>
<div class="flap-display">
<span class="flap-label">Wagons</span>
<span class="flap-value" id="wagonVal">04</span>
</div>
<div class="flap-display">
<span class="flap-label">Best Run</span>
<span class="flap-value" id="bestVal">00</span>
</div>
</div>
<div class="btn-group">
<button class="rail-btn" id="whistleBtn" title="Blow Steam Whistle (Space)">🎺 TOOT!</button>
<button class="rail-btn" id="muteBtn" title="Toggle Sound">🔊</button>
</div>
</div>
<div class="viewport-frame" id="viewportFrame">
<canvas id="gameCanvas" width="700" height="700"></canvas>
<div class="overlay-banner" id="overlayBanner">
<div class="demo-pill" id="demoPill">
<span class="pulse-dot"></span>
<span id="pillText">Auto-Conductor Demo Active</span>
</div>
<div class="cta-ticket" id="ctaTicket">
<h2 id="ctaTitle">Click or Press Any Key to Drive</h2>
<p id="ctaSub">Use Arrow Keys / WASD to steer the locomotive & board passengers!</p>
</div>
</div>
</div>
<div class="dpad">
<div></div>
<button class="dpad-btn" data-dir="UP">▲</button>
<div></div>
<button class="dpad-btn" data-dir="LEFT">◀</button>
<button class="dpad-btn" data-dir="DOWN">▼</button>
<button class="dpad-btn" data-dir="RIGHT">▶</button>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('scoreVal');
const wagonEl = document.getElementById('wagonVal');
const bestEl = document.getElementById('bestVal');
const overlayBanner = document.getElementById('overlayBanner');
const ctaTicket = document.getElementById('ctaTicket');
const ctaTitle = document.getElementById('ctaTitle');
const ctaSub = document.getElementById('ctaSub');
const pillText = document.getElementById('pillText');
const whistleBtn = document.getElementById('whistleBtn');
const muteBtn = document.getElementById('muteBtn');
// Grid & timing constants
const GRID = 14;
const CELL = canvas.width / GRID; // 50px per cell
const STEP_DURATION = 155; // ms per grid tile
// Colorful wagon palette
const WAGON_PALETTE = [
{ body: '#e53935', roof: '#ffcdd2', trim: '#b71c1c', name: 'Crimson Express' },
{ body: '#1e88e5', roof: '#bbdefb', trim: '#0d47a1', name: 'Sapphire Coach' },
{ body: '#fdd835', roof: '#fff9c4', trim: '#f57f17', name: 'Sunbeam Diner' },
{ body: '#43a047', roof: '#c8e6c9', trim: '#1b5e20', name: 'Emerald Sleeper' },
{ body: '#8e24aa', roof: '#e1bee7', trim: '#4a148c', name: 'Royal Parlour' },
{ body: '#fb8c00', roof: '#ffe0b2', trim: '#e65100', name: 'Amber Cargo' },
{ body: '#00acc1', roof: '#b2ebf2', trim: '#006064', name: 'Lagoon Lounge' }
];
// Sound synthesis (Web Audio API)
let audioCtx = null;
let muted = false;
function initAudio() {
if (!audioCtx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) audioCtx = new AudioContext();
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playWhistle() {
if (muted || !audioCtx) return;
const now = audioCtx.currentTime;
const freqs = [587.33, 739.99, 880]; // D major steam chord
freqs.forEach(f => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(f, now);
osc.frequency.exponentialRampToValueAtTime(f * 1.02, now + 0.18);
gain.gain.setValueAtTime(0.001, now);
gain.gain.linearRampToValueAtTime(0.07, now + 0.03);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.36);
});
}
function playBoardSound() {
if (muted || !audioCtx) return;
const now = audioCtx.currentTime;
[523.25, 659.25, 783.99, 1046.5].forEach((f, idx) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(f, now + idx * 0.045);
gain.gain.setValueAtTime(0.08, now + idx * 0.045);
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.045 + 0.16);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now + idx * 0.045);
osc.stop(now + idx * 0.045 + 0.17);
});
}
function playCrashSound() {
if (muted || !audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(160, now);
osc.frequency.exponentialRampToValueAtTime(45, now + 0.4);
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.42);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.45);
}
// Scenery decorations (trees, flower beds, station clock)
const trees = [
{ x: 0.6, y: 0.6, r: 16 }, { x: 13.4, y: 0.6, r: 17 },
{ x: 0.5, y: 13.4, r: 18 }, { x: 13.4, y: 13.4, r: 16 },
{ x: 3.5, y: 0.4, r: 12 }, { x: 10.5, y: 0.4, r: 13 },
{ x: 3.5, y: 13.6, r: 13 }, { x: 10.5, y: 13.6, r: 12 }
];
// Game State
let mode = 'DEMO'; // 'DEMO', 'PLAYING', 'GAMEOVER'
let score = 0;
let bestScore = 0;
let train = [];
let prevTrain = [];
let dir = { x: 1, y: 0 };
let inputQueue = [];
let passenger = null;
let smokePuffs = [];
let floatingTexts = [];
let boardingJump = null;
let stepAccumulator = 0;
let lastFrameTime = performance.now();
let crashShake = 0;
function pad2(n) {
return String(n).padStart(2, '0');
}
function updateHUD() {
scoreEl.textContent = pad2(score);
wagonEl.textContent = pad2(Math.max(0, train.length - 1));
bestEl.textContent = pad2(bestScore);
}
function createSegment(x, y, index) {
const style = WAGON_PALETTE[(index - 1 + WAGON_PALETTE.length) % WAGON_PALETTE.length];
return {
x,
y,
style,
hasPassenger: index > 0 // initial demo wagons look lively with passengers
};
}
function resetTrain(isDemoMode) {
mode = isDemoMode ? 'DEMO' : 'PLAYING';
score = isDemoMode ? 5 : 0;
dir = { x: 1, y: 0 };
inputQueue = [];
stepAccumulator = 0;
const startX = 6;
const startY = 7;
const initialLen = 5; // 1 locomotive + 4 colorful wagons right from first paint
train = [];
for (let i = 0; i < initialLen; i++) {
train.push(createSegment(startX - i, startY, i));
}
prevTrain = train.map(seg => ({ ...seg }));
spawnPassenger();
updateHUD();
if (mode === 'DEMO') {
overlayBanner.classList.remove('hidden');
pillText.textContent = 'Auto-Conductor Demo Active';
ctaTitle.textContent = 'Click or Press Any Key to Drive';
ctaSub.textContent = 'Steer with Arrow Keys / WASD to pick up waiting passengers!';
} else {
overlayBanner.classList.add('hidden');
}
}
function spawnPassenger() {
const freeCells = [];
for (let y = 1; y < GRID - 1; y++) {
for (let x = 1; x < GRID - 1; x++) {
const occupied = train.some(seg => seg.x === x && seg.y === y);
if (!occupied) freeCells.push({ x, y });
}
}
// Fallback to any free cell if inner ring is full
if (freeCells.length === 0) {
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if (!train.some(seg => seg.x === x && seg.y === y)) {
freeCells.push({ x, y });
}
}
}
}
if (freeCells.length === 0) return;
const pick = freeCells[Math.floor(Math.random() * freeCells.length)];
const nextWagonIndex = train.length;
const style = WAGON_PALETTE[(nextWagonIndex - 1) % WAGON_PALETTE.length];
// Pick a cute platform offset side that isn't directly on top of the rail
passenger = {
x: pick.x,
y: pick.y,
style,
hatColor: style.body,
coatColor: style.roof,
suitcaseColor: '#6d4c41',
spawnTime: performance.now()
};
}
// AI Pathfinder for Demo Mode (BFS shortest path + safe survival fallback)
function computeAIDirection() {
const head = train[0];
const possibleDirs = [
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
].filter(d => !(d.x === -dir.x && d.y === -dir.y));
const isSafeCell = (nx, ny) => {
if (nx < 0 || nx >= GRID || ny < 0 || ny >= GRID) return false;
// Tail cell will vacate unless boarding
for (let i = 0; i < train.length - 1; i++) {
if (train[i].x === nx && train[i].y === ny) return false;
}
return true;
};
if (!passenger) return dir;
// BFS to passenger
const queue = [];
const visited = new Set();
visited.add(`${head.x},${head.y}`);
for (const d of possibleDirs) {
const nx = head.x + d.x;
const ny = head.y + d.y;
if (isSafeCell(nx, ny)) {
queue.push({ x: nx, y: ny, firstDir: d });
visited.add(`${nx},${ny}`);
}
}
while (queue.length > 0) {
const curr = queue.shift();
if (curr.x === passenger.x && curr.y === passenger.y) {
return curr.firstDir;
}
for (const d of [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]) {
const nx = curr.x + d.x;
const ny = curr.y + d.y;
const key = `${nx},${ny}`;
if (!visited.has(key) && isSafeCell(nx, ny)) {
visited.add(key);
queue.push({ x: nx, y: ny, firstDir: curr.firstDir });
}
}
}
// Fallback: pick safe neighbor with most open neighbors
const safeOptions = possibleDirs.filter(d => isSafeCell(head.x + d.x, head.y + d.y));
if (safeOptions.length > 0) {
safeOptions.sort((a, b) => {
const countOpen = (d) => {
let free = 0;
const tx = head.x + d.x;
const ty = head.y + d.y;
for (const nd of [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]) {
if (isSafeCell(tx + nd.x, ty + nd.y)) free++;
}
return free;
};
return countOpen(b) - countOpen(a);
});
return safeOptions[0];
}
return dir;
}
function spawnSmoke(px, py, count = 2, big = false) {
for (let i = 0; i < count; i++) {
smokePuffs.push({
x: px + (Math.random() - 0.5) * 6,
y: py + (Math.random() - 0.5) * 6,
vx: (Math.random() - 0.5) * 18,
vy: -15 - Math.random() * 20,
r: big ? 7 + Math.random() * 5 : 4 + Math.random() * 4,
alpha: 0.85,
grow: big ? 18 : 12
});
}
}
function stepTrain() {
if (mode === 'GAMEOVER') return;
if (mode === 'DEMO') {
dir = computeAIDirection();
} else if (inputQueue.length > 0) {
dir = inputQueue.shift();
}
prevTrain = train.map(seg => ({ ...seg }));
const head = train[0];
const nextX = head.x + dir.x;
const nextY = head.y + dir.y;
const willPickUp = passenger && nextX === passenger.x && nextY === passenger.y;
// Check collisions with station walls or wagons
const hitWall = nextX < 0 || nextX >= GRID || nextY < 0 || nextY >= GRID;
const checkLimit = willPickUp ? train.length : train.length - 1;
let hitSelf = false;
for (let i = 0; i < checkLimit; i++) {
if (train[i].x === nextX && train[i].y === nextY) {
hitSelf = true;
break;
}
}
if (hitWall || hitSelf) {
if (mode === 'DEMO') {
resetTrain(true);
return;
}
mode = 'GAMEOVER';
crashShake = 12;
playCrashSound();
overlayBanner.classList.remove('hidden');
pillText.textContent = 'Track Derailment!';
ctaTitle.textContent = `End of the Line! (${score} Boarded)`;
ctaSub.textContent = 'Click or Press Any Key to Dispatch a New Train';
return;
}
// Advance segments
for (let i = train.length - 1; i >= 1; i--) {
train[i].x = train[i - 1].x;
train[i].y = train[i - 1].y;
}
head.x = nextX;
head.y = nextY;
if (willPickUp) {
const tailOld = prevTrain[prevTrain.length - 1];
const newSeg = createSegment(tailOld.x, tailOld.y, train.length);
train.push(newSeg);
prevTrain.push({ ...newSeg });
score++;
if (score > bestScore) bestScore = score;
updateHUD();
playBoardSound();
const px = (nextX + 0.5) * CELL;
const py = (nextY + 0.5) * CELL;
spawnSmoke(px, py, 7, true);
floatingTexts.push({
x: px,
y: py - 10,
text: 'ALL ABOARD! +1',
color: passenger.style.roof,
stroke: passenger.style.trim,
alpha: 1,
vy: -32
});
boardingJump = {
startX: px,
startY: py - 18,
color: passenger.hatColor,
progress: 0
};
spawnPassenger();
}
}
// Smooth interpolation helper
function getInterpolatedPos(index, t) {
const curr = train[index];
const prev = prevTrain[index] || curr;
return {
x: (prev.x + (curr.x - prev.x) * t + 0.5) * CELL,
y: (prev.y + (curr.y - prev.y) * t + 0.5) * CELL,
angle: Math.atan2(curr.y - prev.y, curr.x - prev.x)
};
}
// Drawing routines
function drawBackgroundTracks() {
// Lush lawn base
ctx.fillStyle = '#4c8c2b';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Subtle checkerboard turf
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if ((x + y) % 2 === 0) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)';
ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
}
}
}
// Crushed gravel ballast grid
ctx.strokeStyle = 'rgba(62, 39, 35, 0.22)';
ctx.lineWidth = 18;
ctx.beginPath();
for (let i = 0; i < GRID; i++) {
const p = (i + 0.5) * CELL;
ctx.moveTo(0, p);
ctx.lineTo(canvas.width, p);
ctx.moveTo(p, 0);
ctx.lineTo(p, canvas.height);
}
ctx.stroke();
// Wooden sleepers (ties) along every cell
ctx.fillStyle = '#5d4037';
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
const cx = (x + 0.5) * CELL;
const cy = (y + 0.5) * CELL;
ctx.fillRect(cx - 12, cy - 3, 24, 6);
ctx.fillRect(cx - 3, cy - 12, 6, 24);
}
}
// Steel rail lines across grid
ctx.strokeStyle = 'rgba(207, 216, 220, 0.38)';
ctx.lineWidth = 2;
const railOffset = 7;
ctx.beginPath();
for (let i = 0; i < GRID; i++) {
const p = (i + 0.5) * CELL;
ctx.moveTo(0, p - railOffset);
ctx.lineTo(canvas.width, p - railOffset);
ctx.moveTo(0, p + railOffset);
ctx.lineTo(canvas.width, p + railOffset);
ctx.moveTo(p - railOffset, 0);
ctx.lineTo(p - railOffset, canvas.height);
ctx.moveTo(p + railOffset, 0);
ctx.lineTo(p + railOffset, canvas.height);
}
ctx.stroke();
// Perimeter Station Bumper Fence
ctx.strokeStyle = '#3e2723';
ctx.lineWidth = 8;
ctx.strokeRect(4, 4, canvas.width - 8, canvas.height - 8);
// Decorative corner trees
trees.forEach(t => {
const tx = t.x * CELL;
const ty = t.y * CELL;
ctx.fillStyle = 'rgba(0,0,0,0.22)';
ctx.beginPath();
ctx.arc(tx + 3, ty + 4, t.r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.arc(tx, ty, t.r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#66bb6a';
ctx.beginPath();
ctx.arc(tx - 3, ty - 3, t.r * 0.6, 0, Math.PI * 2);
ctx.fill();
});
}
function drawWaitingPlatform(now) {
if (!passenger) return;
const cx = (passenger.x + 0.5) * CELL;
const cy = (passenger.y + 0.5) * CELL;
const pulse = Math.sin(now * 0.008);
// Glowing arrival target zone on the track
ctx.save();
ctx.strokeStyle = passenger.style.roof;
ctx.lineWidth = 2.5;
ctx.setLineDash([5, 4]);
ctx.beginPath();
ctx.arc(cx, cy, 19 + pulse * 2, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
// Wooden Boarding Platform Base just above/beside the tile
const platW = 46;
const platH = 20;
const platX = cx - platW / 2;
const platY = cy - 34;
// Platform shadow
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.roundRect(platX + 2, platY + 3, platW, platH, 5);
ctx.fill();
// Concrete/wood platform deck
ctx.fillStyle = '#d7ccc8';
ctx.strokeStyle = '#5d4037';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(platX, platY, platW, platH, 5);
ctx.fill();
ctx.stroke();
// Yellow safety line along platform edge
ctx.fillStyle = '#fbc02d';
ctx.fillRect(platX + 3, platY + platH - 4, platW - 6, 3);
// Striped canopy / awning post on left of platform
ctx.fillStyle = '#37474f';
ctx.fillRect(platX + 4, platY - 10, 3, 14);
ctx.fillStyle = passenger.style.body;
ctx.beginPath();
ctx.arc(platX + 5.5, platY - 12, 5, 0, Math.PI * 2);
ctx.fill();
// Waiting Passenger Character (bouncing and waving)
const bounce = Math.abs(Math.sin(now * 0.009)) * 4;
const px = cx + 2;
const py = platY + 10 - bounce;
// Little leather suitcase beside passenger
ctx.fillStyle = '#5d4037';
ctx.fillRect(px - 14, platY + 5, 8, 7);
ctx.strokeStyle = '#ffca28';
ctx.lineWidth = 1;
ctx.strokeRect(px - 14, platY + 5, 8, 7);
// Passenger coat/body
ctx.fillStyle = passenger.style.body;
ctx.beginPath();
ctx.roundRect(px - 6, py - 4, 12, 11, 4);
ctx.fill();
// Waving arm
const waveAngle = Math.sin(now * 0.018) * 0.6 - 0.8;
ctx.save();
ctx.translate(px + 5, py - 2);
ctx.rotate(waveAngle);
ctx.fillStyle = passenger.style.body;
ctx.fillRect(0, -2, 8, 3.5);
// White glove/ticket
ctx.fillStyle = '#fff9c4';
ctx.fillRect(6, -3, 4, 5);
ctx.restore();
// Passenger Head
ctx.fillStyle = '#ffccbc';
ctx.beginPath();
ctx.arc(px, py - 8, 5.5, 0, Math.PI * 2);
ctx.fill();
// Cute Bowler / Conductor Hat matching wagon color
ctx.fillStyle = passenger.style.trim;
ctx.fillRect(px - 7, py - 12, 14, 2.5);
ctx.fillStyle = passenger.style.body;
ctx.beginPath();
ctx.arc(px, py - 12, 4.5, Math.PI, 0);
ctx.fill();
}
function drawWagon(pos, seg, index, now) {
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(pos.angle);
const w = 38;
const h = 26;
// Drop Shadow
ctx.fillStyle = 'rgba(0,0,0,0.32)';
ctx.beginPath();
ctx.roundRect(-w / 2 + 2, -h / 2 + 4, w, h, 6);
ctx.fill();
// Animated iron wheels (4 wheels peeking out)
const wheelWobble = Math.sin(now * 0.03 + index) * 1.2;
ctx.fillStyle = '#263238';
const wheelPositions = [
[-11, -h / 2 - 2], [11, -h / 2 - 2],
[-11, h / 2 - 2], [11, h / 2 - 2]
];
wheelPositions.forEach(([wx, wy]) => {
ctx.fillRect(wx - 4 + wheelWobble * 0.3, wy, 8, 4);
});
// Main Coach Body
ctx.fillStyle = seg.style.body;
ctx.strokeStyle = seg.style.trim;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(-w / 2, -h / 2, w, h, 6);
ctx.fill();
ctx.stroke();
// Cream Window Stripe & Passenger Silhouettes
ctx.fillStyle = '#fff8e1';
ctx.fillRect(-w / 2 + 5, -h / 2 + 2, 8, h - 4);
ctx.fillRect(-4, -h / 2 + 2, 8, h - 4);
ctx.fillRect(w / 2 - 13, -h / 2 + 2, 8, h - 4);
// Center Clerestory Roof
ctx.fillStyle = seg.style.roof;
ctx.beginPath();
ctx.roundRect(-w / 2 + 4, -h / 2 + 5, w - 8, h - 10, 4);
ctx.fill();
// Seated Passenger Hat/Head badge on roof showing wagon is occupied
if (seg.hasPassenger) {
ctx.fillStyle = seg.style.trim;
ctx.beginPath();
ctx.arc(-6, 0, 3.2, 0, Math.PI * 2);
ctx.arc(6, 0, 3.2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawLocomotive(pos, now) {
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(pos.angle);
const w = 44;
const h = 30;
// Warm Headlight Beam
const beamGrad = ctx.createRadialGradient(w / 2, 0, 2, w / 2 + 45, 0, 58);
beamGrad.addColorStop(0, 'rgba(255, 241, 118, 0.55)');
beamGrad.addColorStop(1, 'rgba(255, 241, 118, 0)');
ctx.fillStyle = beamGrad;
ctx.beginPath();
ctx.moveTo(w / 2, -6);
ctx.lineTo(w / 2 + 75, -34);
ctx.lineTo(w / 2 + 75, 34);
ctx.lineTo(w / 2, 6);
ctx.closePath();
ctx.fill();
// Locomotive Shadow
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.beginPath();
ctx.roundRect(-w / 2 + 2, -h / 2 + 4, w, h, 6);
ctx.fill();
// Red Cowcatcher (Pilot) at front
ctx.fillStyle = '#c62828';
ctx.strokeStyle = '#ffca28';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(w / 2 - 2, -h / 2 + 3);
ctx.lineTo(w / 2 + 9, 0);
ctx.lineTo(w / 2 - 2, h / 2 - 3);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Side Piston Rods (animated)
const rodShift = Math.sin(now * 0.025) * 4;
ctx.fillStyle = '#cfd8dc';
ctx.fillRect(-10 + rodShift, -h / 2 - 3, 20, 3);
ctx.fillRect(-10 - rodShift, h / 2, 20, 3);
// Rear Engineer Cab (Crimson & Gold)
ctx.fillStyle = '#d32f2f';
ctx.strokeStyle = '#ffb300';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(-w / 2, -h / 2, 20, h, 5);
ctx.fill();
ctx.stroke();
// Cab Roof Window
ctx.fillStyle = '#fff9c4';
ctx.fillRect(-w / 2 + 4, -h / 2 + 4, 11, h - 8);
ctx.fillStyle = '#b71c1c';
ctx.fillRect(-w / 2 + 3, -h / 2 + 6, 13, h - 12);
// Cast-Iron Cylindrical Boiler
ctx.fillStyle = '#263238';
ctx.strokeStyle = '#455a64';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(-4, -h / 2 + 3, 25, h - 6, 6);
ctx.fill();
ctx.stroke();
// Brass Boiler Bands
ctx.fillStyle = '#ffd54f';
ctx.fillRect(2, -h / 2 + 3, 3, h - 6);
ctx.fillRect(11, -h / 2 + 3, 3, h - 6);
// Brass Steam Dome
ctx.fillStyle = '#ffb300';
ctx.beginPath();
ctx.arc(4, 0, 5, 0, Math.PI * 2);
ctx.fill();
// Smokestack (Chimney) at front
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(15, 0, 6, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffd54f';
ctx.lineWidth = 2;
ctx.stroke();
// Glowing Brass Lantern on nose
ctx.fillStyle = '#ffee58';
ctx.fillRect(w / 2 - 2, -4, 4, 8);
ctx.restore();
}
function render(now) {
const dt = Math.min(50, now - lastFrameTime);
lastFrameTime = now;
if (mode !== 'GAMEOVER') {
stepAccumulator += dt;
while (stepAccumulator >= STEP_DURATION) {
stepAccumulator -= STEP_DURATION;
stepTrain();
}
}
const interpT = mode === 'GAMEOVER' ? 1 : Math.min(1, stepAccumulator / STEP_DURATION);
ctx.save();
if (crashShake > 0) {
const dx = (Math.random() - 0.5) * crashShake;
const dy = (Math.random() - 0.5) * crashShake;
ctx.translate(dx, dy);
crashShake = Math.max(0, crashShake - dt * 0.04);
}
drawBackgroundTracks();
drawWaitingPlatform(now);
// Compute smooth world coordinates for all segments
const positions = [];
for (let i = 0; i < train.length; i++) {
let pos = getInterpolatedPos(i, interpT);
// Keep locomotive orientation aligned with current travel direction if stationary
if (i === 0 && pos.angle === 0 && (dir.x !== 1 || dir.y !== 0)) {
pos.angle = Math.atan2(dir.y, dir.x);
}
positions.push(pos);
}
// Draw dark iron couplings linking every wagon
ctx.strokeStyle = '#263238';
ctx.lineWidth = 7;
ctx.lineCap = 'round';
ctx.beginPath();
for (let i = 0; i < positions.length - 1; i++) {
ctx.moveTo(positions[i].x, positions[i].y);
ctx.lineTo(positions[i + 1].x, positions[i + 1].y);
}
ctx.stroke();
// Brass coupling pins
ctx.fillStyle = '#ffd54f';
for (let i = 0; i < positions.length - 1; i++) {
const mx = (positions[i].x + positions[i + 1].x) / 2;
const my = (positions[i].y + positions[i + 1].y) / 2;
ctx.beginPath();
ctx.arc(mx, my, 3, 0, Math.PI * 2);
ctx.fill();
}
// Draw colorful wagons from tail to head
for (let i = positions.length - 1; i >= 1; i--) {
drawWagon(positions[i], train[i], i, now);
}
// Draw steam locomotive at head
if (positions.length > 0) {
const headPos = positions[0];
drawLocomotive(headPos, now);
// Emit continuous chug-chug steam from the smokestack
if (Math.random() < 0.38 && mode !== 'GAMEOVER') {
const stackX = headPos.x + Math.cos(headPos.angle) * 14;
const stackY = headPos.y + Math.sin(headPos.angle) * 14;
spawnSmoke(stackX, stackY, 1, false);
}
}
// Animate boarding hop arc if a passenger just hopped on
if (boardingJump && positions.length > 1) {
boardingJump.progress += dt * 0.0045;
if (boardingJump.progress >= 1) {
boardingJump = null;
} else {
const p = boardingJump.progress;
const target = positions[positions.length - 1];
const bx = boardingJump.startX + (target.x - boardingJump.startX) * p;
const by = boardingJump.startY + (target.y - boardingJump.startY) * p - Math.sin(p * Math.PI) * 32;
ctx.fillStyle = boardingJump.color;
ctx.beginPath();
ctx.arc(bx, by, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
// Update & render steam puffs
for (let i = smokePuffs.length - 1; i >= 0; i--) {
const s = smokePuffs[i];
s.x += (s.vx * dt) / 1000;
s.y += (s.vy * dt) / 1000;
s.r += (s.grow * dt) / 1000;
s.alpha -= (dt / 1000) * 0.75;
if (s.alpha <= 0) {
smokePuffs.splice(i, 1);
continue;
}
ctx.fillStyle = `rgba(245, 245, 245, ${s.alpha})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Update & render floating boarding callouts
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const ft = floatingTexts[i];
ft.y += (ft.vy * dt) / 1000;
ft.alpha -= (dt / 1000) * 0.85;
if (ft.alpha <= 0) {
floatingTexts.splice(i, 1);
continue;
}
ctx.save();
ctx.font = '900 15px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.globalAlpha = Math.max(0, ft.alpha);
ctx.strokeStyle = '#263238';
ctx.lineWidth = 4;
ctx.strokeText(ft.text, ft.x, ft.y);
ctx.fillStyle = '#fff59d';
ctx.fillText(ft.text, ft.x, ft.y);
ctx.restore();
}
ctx.restore();
requestAnimationFrame(render);
}
// Input Handling
function queueDirection(newDir) {
const lastDir = inputQueue.length > 0 ? inputQueue[inputQueue.length - 1] : dir;
// Prevent 180-degree self-reversal or duplicate directions
if ((newDir.x === -lastDir.x && newDir.y === -lastDir.y) ||
(newDir.x === lastDir.x && newDir.y === lastDir.y)) {
return;
}
if (inputQueue.length < 2) {
inputQueue.push(newDir);
}
}
function startHumanGame(initialDir = null) {
initAudio();
playWhistle();
resetTrain(false);
if (initialDir) {
queueDirection(initialDir);
}
}
window.addEventListener('keydown', (e) => {
const keyMap = {
ArrowUp: { x: 0, y: -1 }, KeyW: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 }, KeyS: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 }, KeyA: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 }, KeyD: { x: 1, y: 0 }
};
if (e.code === 'Space') {
e.preventDefault();
initAudio();
playWhistle();
if (train.length > 0) {
spawnSmoke((train[0].x + 0.5) * CELL, (train[0].y + 0.5) * CELL, 8, true);
}
if (mode !== 'PLAYING') startHumanGame();
return;
}
const mapped = keyMap[e.code];
if (mapped) {
e.preventDefault();
}
if (mode !== 'PLAYING') {
startHumanGame(mapped || null);
return;
}
if (mapped) {
queueDirection(mapped);
}
});
document.getElementById('viewportFrame').addEventListener('pointerdown', () => {
if (mode !== 'PLAYING') {
startHumanGame();
}
});
ctaTicket.addEventListener('click', (e) => {
e.stopPropagation();
startHumanGame();
});
whistleBtn.addEventListener('click', () => {
initAudio();
playWhistle();
if (train.length > 0) {
spawnSmoke((train[0].x + 0.5) * CELL, (train[0].y + 0.5) * CELL, 8, true);
}
});
muteBtn.addEventListener('click', () => {
muted = !muted;
muteBtn.textContent = muted ? '🔇' : '🔊';
});
document.querySelectorAll('.dpad-btn').forEach(btn => {
btn.addEventListener('pointerdown', (e) => {
e.preventDefault();
const d = btn.dataset.dir;
const map = {
UP: { x: 0, y: -1 },
DOWN: { x: 0, y: 1 },
LEFT: { x: -1, y: 0 },
RIGHT: { x: 1, y: 0 }
};
if (mode !== 'PLAYING') {
startHumanGame(map[d]);
} else {
queueDirection(map[d]);
}
});
});
// Start immediately in self-playing Demo Mode
resetTrain(true);
requestAnimationFrame(render);
})();
</script>
</body>
</html>