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 49 s
- Cost
- $0.067
- Output tokens
- 17,671
- Spent thinking
- 3,319 tokens
- Speed
- 166.3 tok/s
- Console errors
- 0
- First token after
- 2.6 s
- First visible text after
- 34.0 s
- Prompt tokens
- 367
- Code size
- 41.8 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-3.8-flash- Reasoning sent
{"effort":"high"}- 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 · 1470 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>The Little Steam Train - Railway Snake</title>
<style>
:root {
--bg-wood: #2c1d11;
--brass: #e5b158;
--brass-dark: #9a6f27;
--cream: #fbf5e6;
--rail-green: #2e5339;
--panel-bg: rgba(28, 18, 12, 0.88);
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
body {
background: radial-gradient(circle at center, #3d291a 0%, #19100a 100%);
color: var(--cream);
font-family: 'Trebuchet MS', 'Segoe UI', system-ui, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
padding: 10px;
}
#game-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 960px;
background: var(--panel-bg);
border: 4px solid var(--brass);
border-radius: 12px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.8), inset 0 0 15px rgba(0, 0, 0, 0.6);
padding: 12px;
}
/* Header & HUD */
header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 12px 10px 12px;
border-bottom: 2px solid var(--brass-dark);
margin-bottom: 8px;
}
.title-group {
display: flex;
align-items: center;
gap: 10px;
}
.title-group h1 {
font-size: 1.4rem;
letter-spacing: 2px;
color: var(--brass);
text-transform: uppercase;
text-shadow: 1px 2px 4px rgba(0,0,0,0.8);
}
.station-badge {
font-size: 0.75rem;
background: #142a1b;
color: #9cd4a7;
padding: 2px 8px;
border-radius: 10px;
border: 1px solid #3c704a;
letter-spacing: 1px;
display: inline-block;
}
.hud-stats {
display: flex;
align-items: center;
gap: 16px;
}
.stat-box {
background: #120b07;
border: 1px solid var(--brass-dark);
border-radius: 6px;
padding: 4px 10px;
text-align: center;
min-width: 80px;
}
.stat-label {
font-size: 0.65rem;
text-transform: uppercase;
color: #b59f84;
display: block;
letter-spacing: 1px;
}
.stat-value {
font-size: 1.2rem;
font-weight: bold;
color: var(--brass);
font-variant-numeric: tabular-nums;
}
.btn-sound {
background: #22140c;
border: 1px solid var(--brass);
color: var(--brass);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
}
.btn-sound:hover {
background: var(--brass);
color: #22140c;
}
/* Canvas Area */
.canvas-wrap {
position: relative;
width: 100%;
aspect-ratio: 22 / 14;
border-radius: 6px;
overflow: hidden;
border: 3px solid #1c120a;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.7);
background-color: #3b5a32;
}
canvas {
width: 100%;
height: 100%;
display: block;
image-rendering: auto;
}
/* Overlays */
.overlay {
position: absolute;
inset: 0;
background: rgba(14, 8, 4, 0.75);
backdrop-filter: blur(3px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px;
z-index: 10;
transition: opacity 0.3s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.demo-banner {
position: absolute;
top: 14px;
background: linear-gradient(90deg, transparent, rgba(229, 177, 88, 0.95), transparent);
color: #201206;
width: 100%;
padding: 6px 0;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 2px;
font-size: 0.9rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
animation: pulse 2s infinite ease-in-out;
pointer-events: none;
z-index: 5;
}
@keyframes pulse {
0%, 100% { opacity: 0.85; transform: scale(1); }
50% { opacity: 1; transform: scale(1.02); }
}
.overlay-card {
background: #1f140c;
border: 2px solid var(--brass);
border-radius: 12px;
padding: 24px 32px;
box-shadow: 0 10px 30px rgba(0,0,0,0.9);
max-width: 440px;
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.overlay-card h2 {
color: var(--brass);
font-size: 1.8rem;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 2px;
}
.overlay-card p {
color: #dfcdb7;
font-size: 0.95rem;
line-height: 1.4;
margin-bottom: 16px;
}
.start-btn {
background: linear-gradient(180deg, #f3ca7e 0%, #c48c2c 100%);
color: #241407;
border: none;
border-radius: 30px;
padding: 12px 28px;
font-size: 1.05rem;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 1.5px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.4);
transition: transform 0.1s, filter 0.2s;
}
.start-btn:hover {
filter: brightness(1.1);
transform: translateY(-2px);
}
.start-btn:active {
transform: translateY(1px);
}
/* Mobile Controls */
.controls-bar {
margin-top: 10px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: #a48c74;
}
.keyboard-legend {
display: flex;
gap: 12px;
}
.key-badge {
background: #1c1109;
border: 1px solid #4a3424;
padding: 2px 6px;
border-radius: 4px;
color: var(--cream);
font-family: monospace;
}
.touch-dpad {
display: none;
grid-template-columns: repeat(3, 48px);
grid-template-rows: repeat(2, 44px);
gap: 4px;
justify-content: center;
margin-top: 8px;
}
.touch-btn {
background: #2a1b12;
border: 1px solid var(--brass-dark);
border-radius: 8px;
color: var(--brass);
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
}
.touch-btn:active {
background: var(--brass-dark);
color: var(--cream);
transform: translateY(2px);
}
@media (max-width: 680px) {
.touch-dpad { display: grid; }
.keyboard-legend { display: none; }
.title-group h1 { font-size: 1.1rem; }
.station-badge { display: none; }
.hud-stats { gap: 8px; }
.stat-box { min-width: 64px; padding: 2px 6px; }
.stat-value { font-size: 1rem; }
}
</style>
</head>
<body>
<div id="game-container">
<header>
<div class="title-group">
<h1>Grand Valley Express</h1>
<span class="station-badge" id="station-name">Station: Sunny Meadow</span>
</div>
<div class="hud-stats">
<div class="stat-box">
<span class="stat-label">Passengers</span>
<span class="stat-value" id="score-val">0</span>
</div>
<div class="stat-box">
<span class="stat-label">Best Route</span>
<span class="stat-value" id="best-val">0</span>
</div>
<button class="btn-sound" id="btn-sound" aria-label="Toggle Sound">🔊 Whistle</button>
</div>
</header>
<div class="canvas-wrap" id="canvas-wrap">
<div class="demo-banner" id="demo-banner">🚂 DEMO AUTOPILOT — CLICK TO TAKE CONTROLS</div>
<canvas id="railway-canvas" width="924" height="588"></canvas>
<div class="overlay" id="start-overlay">
<div class="overlay-card">
<h2>All Aboard!</h2>
<p>Guide the miniature locomotive along the railway. Stop at platforms to collect waiting passengers and couple colorful wagons to your train!</p>
<button class="start-btn" id="start-btn">Take The Controls</button>
</div>
</div>
<div class="overlay hidden" id="gameover-overlay">
<div class="overlay-card">
<h2>Derailed!</h2>
<p>The train collided with the track perimeter or wagons! All passengers made it out safely with their tea intact.</p>
<p style="font-weight: bold; color: var(--brass);">Final Passenger Count: <span id="final-score">0</span></p>
<button class="start-btn" id="restart-btn">New Departure</button>
</div>
</div>
</div>
<div class="controls-bar">
<div class="keyboard-legend">
<span>Steer: <span class="key-badge">W A S D</span> / <span class="key-badge">Arrows</span></span>
<span>Sound: <span class="key-badge">M</span></span>
<span>Whistle: <span class="key-badge">H</span></span>
</div>
<div>Railway Dispatch: Keep wheels on iron!</div>
</div>
<!-- Onscreen D-pad for mobile / touch devices -->
<div class="touch-dpad">
<div style="grid-column: 2" class="touch-btn" id="btn-up">▲</div>
<div style="grid-column: 1; grid-row: 2;" class="touch-btn" id="btn-left">◀</div>
<div style="grid-column: 2; grid-row: 2;" class="touch-btn" id="btn-down">▼</div>
<div style="grid-column: 3; grid-row: 2;" class="touch-btn" id="btn-right">▶</div>
</div>
</div>
<script>
/* =========================================================================
AUDIO SYNTHESIZER (Pure Web Audio API)
========================================================================= */
class TrainAudio {
constructor() {
this.ctx = null;
this.muted = false;
this.chuffTimer = 0;
}
init() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
this.ctx = new AudioContext();
}
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
playWhistle() {
if (this.muted || !this.ctx) return;
this.init();
const now = this.ctx.currentTime;
// Classic dual chime harmonic whistle (e.g., ~520Hz and ~650Hz)
[520, 655].forEach(freq => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(freq, now);
osc.frequency.exponentialRampToValueAtTime(freq * 1.03, now + 0.15);
osc.frequency.exponentialRampToValueAtTime(freq, now + 0.4);
// Low pass filter for soft steam resonance
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1400, now);
gain.gain.setValueAtTime(0.001, now);
gain.gain.linearRampToValueAtTime(0.12, now + 0.08);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.46);
});
}
playChuff() {
if (this.muted || !this.ctx) return;
this.init();
const now = this.ctx.currentTime;
// White noise burst shaped like steam exhaust: "tshhh"
const bufferSize = this.ctx.sampleRate * 0.08;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(800, now);
filter.Q.setValueAtTime(2.0, now);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.08);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start(now);
}
playBoardingChime() {
if (this.muted || !this.ctx) return;
this.init();
const now = this.ctx.currentTime;
// Two-tone bell: High ding-dong
[880, 1174].forEach((freq, idx) => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + idx * 0.1);
gain.gain.setValueAtTime(0.001, now + idx * 0.1);
gain.gain.linearRampToValueAtTime(0.2, now + idx * 0.1 + 0.02);
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.1 + 0.35);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now + idx * 0.1);
osc.stop(now + idx * 0.1 + 0.36);
});
}
playCrash() {
if (this.muted || !this.ctx) return;
this.init();
const now = this.ctx.currentTime;
// Heavy derail clatter
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(140, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.5);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.51);
}
}
/* =========================================================================
CONSTANTS & THEMES
========================================================================= */
const COLS = 22;
const ROWS = 14;
const TILE = 42; // Canvas is 924x588
const STATIONS = [
"Sunny Meadow", "Pine Hill Junction", "Brambleside Yard",
"Willow Creek", "Copper Kettle Depot", "Rosewood Halt", "Silver Falls"
];
// Palette for distinct train wagons
const WAGON_COLORS = [
{ body: '#b83227', trim: '#e8a838', roof: '#3d1c19', name: 'Ruby Coach' }, // Red
{ body: '#1f6f8b', trim: '#99f3bd', roof: '#113340', name: 'Azure Sleeper' }, // Blue
{ body: '#2e7d32', trim: '#fdd835', roof: '#133e16', name: 'Emerald Express' }, // Green
{ body: '#d48818', trim: '#fff176', roof: '#543105', name: 'Amber Dining Car' },// Yellow/Gold
{ body: '#6a1b9a', trim: '#ea80fc', roof: '#2d0a42', name: 'Royal Pullman' }, // Purple
{ body: '#c25929', trim: '#ffe082', roof: '#481d09', name: 'Copper Mail Van' } // Orange
];
/* =========================================================================
GAME STATE
========================================================================= */
const canvas = document.getElementById('railway-canvas');
const ctx = canvas.getContext('2d');
const audio = new TrainAudio();
let score = 0;
let highScore = 0;
let isDemo = true;
let isGameOver = false;
// Movement & timing
let baseTickRate = 180; // ms per grid tile step
let tickInterval = baseTickRate;
let lastTickTime = performance.now();
let moveProgress = 0; // 0.0 to 1.0 between ticks for buttery smooth rendering
// Direction vector
let dir = { x: 1, y: 0 };
let nextDir = { x: 1, y: 0 };
// Train segments: Array of { x, y, prevX, prevY, angle }
let train = [];
let pendingWagons = 0;
// Passenger waiting at platform
let passenger = { x: 16, y: 7, variant: 0, bobTimer: 0 };
let floatingTexts = [];
let steamParticles = [];
// Decorative static station accessories
let decorations = [];
/* =========================================================================
INITIALIZATION & LEVEL SETUP
========================================================================= */
function initDecorations() {
decorations = [];
// Generate some pretty static model trees and platform sheds
const occupied = new Set();
// Add corners
const corners = [
{ x: 1, y: 1 }, { x: 2, y: 1 }, { x: 1, y: 2 },
{ x: COLS - 2, y: 1 }, { x: COLS - 3, y: 1 },
{ x: 1, y: ROWS - 2 }, { x: COLS - 2, y: ROWS - 2 }
];
corners.forEach(c => {
decorations.push({
x: c.x,
y: c.y,
type: 'tree',
seed: Math.random()
});
});
}
function resetTrain(startX, startY, length = 4) {
train = [];
dir = { x: 1, y: 0 };
nextDir = { x: 1, y: 0 };
for (let i = 0; i < length; i++) {
train.push({
x: startX - i,
y: startY,
prevX: startX - i - 1,
prevY: startY,
angle: 0
});
}
}
function spawnPassenger() {
let valid = false;
let rx = 0, ry = 0;
let attempts = 0;
while (!valid && attempts < 100) {
attempts++;
rx = Math.floor(Math.random() * (COLS - 4)) + 2;
ry = Math.floor(Math.random() * (ROWS - 4)) + 2;
valid = true;
// Don't spawn on train
for (let seg of train) {
if (seg.x === rx && seg.y === ry) { valid = false; break; }
}
// Don't spawn on decor
for (let d of decorations) {
if (d.x === rx && d.y === ry) { valid = false; break; }
}
}
passenger.x = rx;
passenger.y = ry;
passenger.variant = Math.floor(Math.random() * 4);
passenger.bobTimer = 0;
// Update station badge name
const stationEl = document.getElementById('station-name');
const stName = STATIONS[Math.floor(Math.random() * STATIONS.length)];
stationEl.textContent = "Platform: " + stName;
}
function startGame(asPlayer = true) {
isDemo = !asPlayer;
isGameOver = false;
score = 0;
pendingWagons = 0;
tickInterval = baseTickRate;
updateScoreUI();
document.getElementById('start-overlay').classList.add('hidden');
document.getElementById('gameover-overlay').classList.add('hidden');
document.getElementById('demo-banner').style.display = isDemo ? 'block' : 'none';
initDecorations();
resetTrain(6, 7, isDemo ? 5 : 4);
spawnPassenger();
floatingTexts = [];
steamParticles = [];
lastTickTime = performance.now();
if (!isDemo) {
audio.playWhistle();
}
}
function updateScoreUI() {
document.getElementById('score-val').textContent = score;
if (score > highScore) {
highScore = score;
document.getElementById('best-val').textContent = highScore;
}
}
/* =========================================================================
AUTOPILOT (DEMO MODE AI - BFS SEARCH)
========================================================================= */
function getDemoNextDirection() {
const head = train[0];
const target = { x: passenger.x, y: passenger.y };
// BFS to passenger
const queue = [{ x: head.x, y: head.y, path: [] }];
const visited = new Set();
visited.add(`${head.x},${head.y}`);
// Obstacles: train body (except very tip of tail which might move)
const obstacles = new Set();
for (let i = 0; i < train.length - 1; i++) {
obstacles.add(`${train[i].x},${train[i].y}`);
}
const neighbors = [
{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }
];
let foundPath = null;
while (queue.length > 0) {
const curr = queue.shift();
if (curr.x === target.x && curr.y === target.y) {
foundPath = curr.path;
break;
}
for (let n of neighbors) {
const nx = curr.x + n.x;
const ny = curr.y + n.y;
const key = `${nx},${ny}`;
if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS) {
if (!obstacles.has(key) && !visited.has(key)) {
visited.add(key);
queue.push({
x: nx,
y: ny,
path: curr.path.length === 0 ? [n] : curr.path
});
}
}
}
}
if (foundPath && foundPath.length > 0) {
return foundPath[0];
}
// If trapped or no direct path, pick open neighbor with most space
let bestDir = null;
let maxOpen = -1;
for (let n of neighbors) {
const nx = head.x + n.x;
const ny = head.y + n.y;
const key = `${nx},${ny}`;
if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && !obstacles.has(key)) {
// Count open cells (1-step lookahead)
let openCount = 0;
for (let sub of neighbors) {
const sx = nx + sub.x;
const sy = ny + sub.y;
if (sx >= 0 && sx < COLS && sy >= 0 && sy < ROWS && !obstacles.has(`${sx},${sy}`)) {
openCount++;
}
}
if (openCount > maxOpen) {
maxOpen = openCount;
bestDir = n;
}
}
}
return bestDir || dir;
}
/* =========================================================================
TICK UPDATE (GRID LOGIC)
========================================================================= */
function gameTick() {
if (isGameOver) return;
if (isDemo) {
nextDir = getDemoNextDirection();
}
// Prevent 180° instant reverse
if (nextDir.x !== -dir.x || nextDir.y !== -dir.y) {
dir = { ...nextDir };
}
const head = train[0];
const newX = head.x + dir.x;
const newY = head.y + dir.y;
// Check wall collision
if (newX < 0 || newX >= COLS || newY < 0 || newY >= ROWS) {
triggerDerail();
return;
}
// Check self collision (wagons)
for (let i = 0; i < train.length; i++) {
if (train[i].x === newX && train[i].y === newY) {
triggerDerail();
return;
}
}
// Move segments
// Each segment stores its previous spot for smooth interpolation
let prevX = head.x;
let prevY = head.y;
// Calculate head angle
const headAngle = Math.atan2(dir.y, dir.x);
train[0] = {
x: newX,
y: newY,
prevX: head.x,
prevY: head.y,
angle: headAngle
};
for (let i = 1; i < train.length; i++) {
const seg = train[i];
const segAngle = Math.atan2(prevY - seg.y, prevX - seg.x);
const tempX = seg.x;
const tempY = seg.y;
train[i] = {
x: prevX,
y: prevY,
prevX: tempX,
prevY: tempY,
angle: segAngle
};
prevX = tempX;
prevY = tempY;
}
// Attach new wagon if passenger was picked up
if (pendingWagons > 0) {
train.push({
x: prevX,
y: prevY,
prevX: prevX,
prevY: prevY,
angle: train[train.length - 1].angle
});
pendingWagons--;
}
// Sound: chuff chuff rhythm
audio.playChuff();
// Emit smoke puffs from engine smokestack
emitSteam(train[0].prevX, train[0].prevY, dir);
// Check Passenger collection
if (newX === passenger.x && newY === passenger.y) {
onPassengerBoarded();
}
}
function onPassengerBoarded() {
score++;
pendingWagons++;
updateScoreUI();
audio.playBoardingChime();
// Faster speed slightly as train grows
tickInterval = Math.max(90, baseTickRate - score * 3);
// Floating celebratory text
const messages = ["All Aboard!", "Tickets Checked!", "+1 Passenger", "Full Steam!", "Next Station!"];
const msg = messages[Math.floor(Math.random() * messages.length)];
floatingTexts.push({
text: msg,
x: (passenger.x + 0.5) * TILE,
y: (passenger.y + 0.2) * TILE,
alpha: 1.0,
vy: -1.2
});
// Special whistle on milestones
if (score % 5 === 0) {
audio.playWhistle();
}
spawnPassenger();
}
function triggerDerail() {
if (isDemo) {
// In demo, smoothly restart after a tiny pause
setTimeout(() => {
if (isDemo) startGame(false);
}, 1200);
} else {
isGameOver = true;
audio.playCrash();
document.getElementById('final-score').textContent = score;
document.getElementById('gameover-overlay').classList.remove('hidden');
}
}
/* =========================================================================
PARTICLES & VISUAL FX
========================================================================= */
function emitSteam(gridX, gridY, direction) {
const px = (gridX + 0.5) * TILE + direction.x * 12;
const py = (gridY + 0.5) * TILE + direction.y * 12;
for (let i = 0; i < 2; i++) {
steamParticles.push({
x: px + (Math.random() - 0.5) * 6,
y: py + (Math.random() - 0.5) * 6,
vx: -direction.x * 0.8 + (Math.random() - 0.5) * 0.8,
vy: -direction.y * 0.8 - Math.random() * 0.8,
radius: 5 + Math.random() * 4,
maxRadius: 18 + Math.random() * 10,
alpha: 0.8,
decay: 0.02 + Math.random() * 0.015
});
}
}
/* =========================================================================
RENDERING: DIORAMA, TRACKS, TRAIN, PASSENGER
========================================================================= */
function drawModelTracks() {
// 1. Grassy Field / Diorama Table
ctx.fillStyle = '#2d4a23';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Subtle grass texture / checkered lawn squares
ctx.fillStyle = 'rgba(255, 255, 255, 0.025)';
for (let x = 0; x < COLS; x++) {
for (let y = 0; y < ROWS; y++) {
if ((x + y) % 2 === 0) {
ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
}
}
}
// 2. Full Railway Grid Layout (Ties & Rails on all tiles so train is always on track)
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
const cx = x * TILE;
const cy = y * TILE;
// Gravel Ballast path
ctx.fillStyle = 'rgba(40, 32, 22, 0.45)';
ctx.fillRect(cx + 3, cy + 3, TILE - 6, TILE - 6);
// Wooden sleepers / cross-ties (two per cell in each direction)
ctx.fillStyle = '#442d18';
ctx.fillRect(cx + 8, cy + 18, TILE - 16, 5);
ctx.fillRect(cx + 18, cy + 8, 5, TILE - 16);
// Subtle iron rail guides
ctx.strokeStyle = '#6d5e52';
ctx.lineWidth = 1;
ctx.strokeRect(cx + 10, cy + 10, TILE - 20, TILE - 20);
}
}
// Diorama perimeter fencing/boardwalk
ctx.strokeStyle = '#181008';
ctx.lineWidth = 6;
ctx.strokeRect(3, 3, canvas.width - 6, canvas.height - 6);
// Brass corner nails
ctx.fillStyle = '#e5b158';
const nails = [[10, 10], [canvas.width - 10, 10], [10, canvas.height - 10], [canvas.width - 10, canvas.height - 10]];
nails.forEach(([nx, ny]) => {
ctx.beginPath();
ctx.arc(nx, ny, 3.5, 0, Math.PI * 2);
ctx.fill();
});
}
function drawDecorations() {
decorations.forEach(d => {
const cx = (d.x + 0.5) * TILE;
const cy = (d.y + 0.5) * TILE;
// Model pine tree
ctx.save();
ctx.translate(cx, cy);
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.beginPath();
ctx.ellipse(3, 6, 12, 8, 0, 0, Math.PI * 2);
ctx.fill();
// Tree layers
const greens = ['#1d3b1e', '#2c532d', '#3d6c3e'];
for (let i = 0; i < 3; i++) {
ctx.fillStyle = greens[i];
ctx.beginPath();
ctx.arc(0, -i * 5, 14 - i * 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
});
}
function drawPlatformAndPassenger() {
const px = passenger.x * TILE;
const py = passenger.y * TILE;
const time = performance.now() * 0.005;
// Platform Wooden Deck
ctx.save();
ctx.fillStyle = '#1e140d';
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
// Deck planks
ctx.fillStyle = '#7a5431';
ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8);
ctx.strokeStyle = '#523820';
ctx.lineWidth = 1;
for (let i = 8; i < TILE - 8; i += 6) {
ctx.beginPath();
ctx.moveTo(px + 4, py + i);
ctx.lineTo(px + TILE - 4, py + i);
ctx.stroke();
}
// Yellow safety edge
ctx.fillStyle = '#e5b158';
ctx.fillRect(px + 4, py + TILE - 8, TILE - 8, 3);
// Lantern post at corner of platform
ctx.fillStyle = '#222';
ctx.fillRect(px + 6, py + 6, 3, 14);
ctx.fillStyle = '#ffdf78';
ctx.beginPath();
ctx.arc(px + 7.5, py + 6, 3.5, 0, Math.PI * 2);
ctx.fill();
// Passenger Figure (bobbing with excitement)
const bob = Math.sin(time * 3) * 1.5;
const figX = px + TILE / 2 + 3;
const figY = py + TILE / 2 + bob;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.ellipse(figX, py + TILE / 2 + 8, 6, 3.5, 0, 0, Math.PI * 2);
ctx.fill();
// Suitcase
ctx.fillStyle = '#8d4b1a';
ctx.fillRect(figX - 11, figY + 1, 6, 8);
ctx.fillStyle = '#e2a35d';
ctx.fillRect(figX - 10, figY - 1, 4, 2); // Handle
// Coat/Dress (colored by variant)
const coatColors = ['#c0392b', '#2980b9', '#8e44ad', '#d35400'];
ctx.fillStyle = coatColors[passenger.variant % coatColors.length];
ctx.beginPath();
ctx.roundRect(figX - 4.5, figY - 2, 9, 12, 3);
ctx.fill();
// Head
ctx.fillStyle = '#f5cba7';
ctx.beginPath();
ctx.arc(figX, figY - 6, 4, 0, Math.PI * 2);
ctx.fill();
// Hat / Cap
ctx.fillStyle = '#2c3e50';
ctx.beginPath();
ctx.ellipse(figX, figY - 8.5, 5.5, 2.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillRect(figX - 3, figY - 12, 6, 4);
// Waving hand
const wave = Math.sin(time * 8) * 3;
ctx.fillStyle = '#f5cba7';
ctx.beginPath();
ctx.arc(figX + 6, figY - 4 + wave, 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function getInterpolatedSegmentPos(seg, progress) {
// Smooth linear interpolation between prev grid cell and current
const ix = seg.prevX + (seg.x - seg.prevX) * progress;
const iy = seg.prevY + (seg.y - seg.prevY) * progress;
return {
x: (ix + 0.5) * TILE,
y: (iy + 0.5) * TILE,
angle: seg.angle
};
}
function drawLocomotive(pos) {
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(pos.angle);
// Headlight Beam Cone
const grad = ctx.createRadialGradient(28, 0, 4, 75, 0, 40);
grad.addColorStop(0, 'rgba(255, 240, 160, 0.45)');
grad.addColorStop(1, 'rgba(255, 240, 160, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(18, -6);
ctx.lineTo(80, -32);
ctx.lineTo(80, 32);
ctx.lineTo(18, 6);
ctx.closePath();
ctx.fill();
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.roundRect(-16, -11, 35, 22, 5);
ctx.fill();
// Cowcatcher (Wedge at front)
ctx.fillStyle = '#1c1b1a';
ctx.beginPath();
ctx.moveTo(16, -9);
ctx.lineTo(21, 0);
ctx.lineTo(16, 9);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#c48c2c';
ctx.lineWidth = 1.5;
ctx.stroke();
// Boiler (Black/Dark Green cylinder)
ctx.fillStyle = '#1e2d24';
ctx.beginPath();
ctx.roundRect(-8, -8.5, 24, 17, 4);
ctx.fill();
// Brass Boiler Bands
ctx.strokeStyle = '#e5b158';
ctx.lineWidth = 1.8;
ctx.beginPath();
ctx.moveTo(4, -8); ctx.lineTo(4, 8);
ctx.moveTo(11, -8); ctx.lineTo(11, 8);
ctx.stroke();
// Smokestack
ctx.fillStyle = '#0f0f0f';
ctx.beginPath();
ctx.arc(10, 0, 4.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#c48c2c';
ctx.beginPath();
ctx.arc(10, 0, 2.5, 0, Math.PI * 2);
ctx.fill();
// Cabin (Driver's cab at rear)
ctx.fillStyle = '#2b1a13';
ctx.beginPath();
ctx.roundRect(-16, -10, 12, 20, 3);
ctx.fill();
// Cab Roof
ctx.fillStyle = '#110b08';
ctx.beginPath();
ctx.roundRect(-17, -11, 14, 22, 3);
ctx.fill();
// Cab Warm Lit Windows
ctx.fillStyle = '#ffea88';
ctx.fillRect(-14, -8, 3.5, 4.5);
ctx.fillRect(-14, 3.5, 3.5, 4.5);
// Brass Headlight Lantern
ctx.fillStyle = '#e5b158';
ctx.fillRect(16, -3, 3, 6);
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(18, 0, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawWagon(pos, index, totalWagons) {
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(pos.angle);
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath();
ctx.roundRect(-14, -10, 28, 20, 4);
ctx.fill();
// Coupler linking bar
ctx.fillStyle = '#222';
ctx.fillRect(-18, -2, 6, 4);
if (index === 0) {
// TENDER (Coal Car directly behind engine)
ctx.fillStyle = '#251b14';
ctx.beginPath();
ctx.roundRect(-13, -8, 25, 16, 2);
ctx.fill();
// Coal mound
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.ellipse(-1, 0, 8, 5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#2c2c2c';
ctx.beginPath();
ctx.arc(-3, -2, 2, 0, Math.PI * 2);
ctx.arc(2, 2, 2.5, 0, Math.PI * 2);
ctx.fill();
} else if (index === totalWagons - 1 && totalWagons > 2) {
// CABOOSE (Last carriage: red with lantern)
ctx.fillStyle = '#a82a20';
ctx.beginPath();
ctx.roundRect(-13, -8.5, 25, 17, 3);
ctx.fill();
// Cupola observation roof
ctx.fillStyle = '#68150f';
ctx.fillRect(-4, -5.5, 9, 11);
// Windows
ctx.fillStyle = '#ffecb3';
ctx.fillRect(-2, -4, 5, 8);
// Rear glowing red lantern
ctx.fillStyle = '#ff3333';
ctx.beginPath();
ctx.arc(-14, 0, 2.5, 0, Math.PI * 2);
ctx.fill();
} else {
// PASSENGER CARRIAGE (Colorful toy style)
const style = WAGON_COLORS[(index - 1) % WAGON_COLORS.length];
// Main body
ctx.fillStyle = style.body;
ctx.beginPath();
ctx.roundRect(-13, -8.5, 25, 17, 3);
ctx.fill();
// Roof Trim
ctx.fillStyle = style.roof;
ctx.beginPath();
ctx.roundRect(-11, -6, 22, 12, 2);
ctx.fill();
// Side windows with silhouettes
ctx.fillStyle = '#ffea9f';
ctx.fillRect(-9, -7.5, 4, 3);
ctx.fillRect(-2, -7.5, 4, 3);
ctx.fillRect(5, -7.5, 4, 3);
ctx.fillRect(-9, 4.5, 4, 3);
ctx.fillRect(-2, 4.5, 4, 3);
ctx.fillRect(5, 4.5, 4, 3);
// Brass handrail / trim stripe
ctx.fillStyle = style.trim;
ctx.fillRect(-12, -0.75, 23, 1.5);
}
ctx.restore();
}
function drawSteamAndEffects() {
// Steam particles
for (let i = steamParticles.length - 1; i >= 0; i--) {
const p = steamParticles[i];
p.x += p.vx;
p.y += p.vy;
p.radius += (p.maxRadius - p.radius) * 0.08;
p.alpha -= p.decay;
if (p.alpha <= 0) {
steamParticles.splice(i, 1);
continue;
}
ctx.fillStyle = `rgba(240, 240, 245, ${p.alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
}
// Floating passenger cheers
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const ft = floatingTexts[i];
ft.y += ft.vy;
ft.alpha -= 0.015;
if (ft.alpha <= 0) {
floatingTexts.splice(i, 1);
continue;
}
ctx.save();
ctx.font = 'bold 15px "Trebuchet MS", sans-serif';
ctx.fillStyle = `rgba(255, 240, 160, ${ft.alpha})`;
ctx.strokeStyle = `rgba(40, 20, 10, ${ft.alpha})`;
ctx.lineWidth = 3;
ctx.textAlign = 'center';
ctx.strokeText(ft.text, ft.x, ft.y);
ctx.fillText(ft.text, ft.x, ft.y);
ctx.restore();
}
}
/* =========================================================================
MAIN LOOP (RAF + INTERPOLATION)
========================================================================= */
function gameLoop(now) {
requestAnimationFrame(gameLoop);
// Compute grid step progress for smooth 60fps interpolation
const elapsed = now - lastTickTime;
moveProgress = Math.min(1.0, elapsed / tickInterval);
if (elapsed >= tickInterval) {
lastTickTime = now;
moveProgress = 0;
gameTick();
}
// Draw everything
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawModelTracks();
drawDecorations();
drawPlatformAndPassenger();
// Draw Train from tail to head so locomotive sits on top
if (train.length > 0) {
for (let i = train.length - 1; i >= 1; i--) {
const pos = getInterpolatedSegmentPos(train[i], moveProgress);
drawWagon(pos, i - 1, train.length - 1);
}
const headPos = getInterpolatedSegmentPos(train[0], moveProgress);
drawLocomotive(headPos);
}
drawSteamAndEffects();
}
/* =========================================================================
USER INPUT (KEYBOARD, TOUCH, SOUND)
========================================================================= */
function handleDirectionInput(dx, dy) {
if (isDemo) {
// Any real key click/press transitions out of demo
startGame(true);
return;
}
if (isGameOver) return;
// Don't allow reversing into previous segment
if (dx !== -dir.x || dy !== -dir.y) {
nextDir = { x: dx, y: dy };
}
}
window.addEventListener('keydown', (e) => {
audio.init();
if (isDemo && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','KeyW','KeyA','KeyS','KeyD','Space'].includes(e.code)) {
startGame(true);
return;
}
switch (e.code) {
case 'ArrowUp':
case 'KeyW':
handleDirectionInput(0, -1);
e.preventDefault();
break;
case 'ArrowDown':
case 'KeyS':
handleDirectionInput(0, 1);
e.preventDefault();
break;
case 'ArrowLeft':
case 'KeyA':
handleDirectionInput(-1, 0);
e.preventDefault();
break;
case 'ArrowRight':
case 'KeyD':
handleDirectionInput(1, 0);
e.preventDefault();
break;
case 'KeyM':
toggleMute();
break;
case 'KeyH':
audio.playWhistle();
break;
case 'Space':
if (isGameOver) {
startGame(true);
}
break;
}
});
function toggleMute() {
audio.init();
audio.muted = !audio.muted;
document.getElementById('btn-sound').textContent = audio.muted ? '🔇 Muted' : '🔊 Whistle';
}
document.getElementById('btn-sound').addEventListener('click', (e) => {
e.stopPropagation();
audio.init();
if (!audio.muted) {
audio.playWhistle();
}
toggleMute();
});
// Start / Restart buttons
document.getElementById('start-btn').addEventListener('click', () => {
audio.init();
startGame(true);
});
document.getElementById('restart-btn').addEventListener('click', () => {
audio.init();
startGame(true);
});
// Canvas click directly starts player game if in demo mode
canvas.addEventListener('pointerdown', () => {
audio.init();
if (isDemo) {
startGame(true);
} else {
audio.playWhistle();
}
});
// Touch D-Pad bindings
document.getElementById('btn-up').addEventListener('pointerdown', (e) => { e.preventDefault(); handleDirectionInput(0, -1); });
document.getElementById('btn-down').addEventListener('pointerdown', (e) => { e.preventDefault(); handleDirectionInput(0, 1); });
document.getElementById('btn-left').addEventListener('pointerdown', (e) => { e.preventDefault(); handleDirectionInput(-1, 0); });
document.getElementById('btn-right').addEventListener('pointerdown', (e) => { e.preventDefault(); handleDirectionInput(1, 0); });
// Swipe support on canvas
let touchStartX = 0;
let touchStartY = 0;
canvas.addEventListener('touchstart', (e) => {
const t = e.touches[0];
touchStartX = t.clientX;
touchStartY = t.clientY;
}, { passive: true });
canvas.addEventListener('touchend', (e) => {
const t = e.changedTouches[0];
const dx = t.clientX - touchStartX;
const dy = t.clientY - touchStartY;
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (Math.max(absDx, absDy) > 20) {
if (absDx > absDy) {
handleDirectionInput(dx > 0 ? 1 : -1, 0);
} else {
handleDirectionInput(0, dy > 0 ? 1 : -1);
}
}
}, { passive: true });
/* =========================================================================
BOOTSTRAP: FIRST PAINT IMMEDIATELY ACTIVE IN DEMO MODE
========================================================================= */
startGame(false); // Starts in self-playing demo mode
requestAnimationFrame(gameLoop);
</script>
</body>
</html>Reasoning trace (3,319 tokens)
**Developing Thought Patterns** I'm currently mapping out the core mechanics for this locomotive-themed arcade game. My focus is on conceptualizing how the "passengers" will function as the "food" the train collects, and how the train's growth will represent picking them up. **Refining Visual Elements** I'm now diving into the visual aspects, aiming for a charming, miniature toy train aesthetic. My priority is ensuring responsiveness across various viewports and crafting immediate, engaging first paint. I envision a diorama-style presentation with soft shadows and vibrant, vintage passenger cars. **Visualizing Track Mechanics** I'm solidifying how the railway tracks will manifest. I'm exploring rendering them dynamically under the train's movement or pre-laying them in a diorama-like fashion, aiming for a visually stunning model train layout aesthetic. **Synthesizing Soundscapes** I'm currently exploring the Web Audio API to create a satisfyingly rhythmic "chug-chug" sound effect that dynamically syncs with the train's speed. This will involve synthesizing noise bursts for an engaging, rhythmic audio experience, aiming for a cute and immersive feel. **Adding Sonic Textures** I'm refining the audio, focusing on a delightful train whistle and distinct passenger sounds. I'm also ensuring the mute button is readily available and audio only starts after user interaction. **Enhancing Locomotive Visuals** I'm detailing the locomotive's appearance, giving it a warm, golden headlight glow and puffing smoke particles. I'm also visualizing the track background as a charming diorama, complete with elegant ballast and wooden ties. **Painting Wagon Details** I'm focusing on the vibrant toy colors for each wagon – crimson, emerald, royal blue, mustard, violet, and orange. I'm also detailing roof vents, tiny passenger windows, visible wheels, and couplers. The caboose is getting its classic red look with a lantern. **Charting Navigation Logic** I'm now exploring navigation, specifically how the train will find passengers using BFS. My focus is on refining the pathfinding algorithm to account for avoiding the train's own wagons. **Implementing Control Schemes** I'm developing the control inputs. On mobile, I'm implementing a virtual D-pad. For desktop, I'm planning for keyboard and mouse interaction, alongside an introductory prompt to start the game. **Defining Grid Dimensions** I'm establishing the game's grid and canvas dimensions, aiming for a 22x14 grid with a cell size of 42 pixels, resulting in a 924x588 pixel canvas. I'll also add borders to create a scenic station effect. **Smooth Movement Engineering** I'm detailing the interpolated movement logic. This involves calculating progress based on elapsed time and tick interval, allowing for smooth turns by interpolating between current and previous segment positions. **Improving Navigation Pathfinding** I'm now refining the AI's navigation to ensure it prioritizes finding passengers while intelligently avoiding self-collision. My focus is on implementing a robust pathfinding algorithm that accounts for the train's growing length and available space, ensuring dynamic and engaging gameplay. I'm also beginning to integrate the varied wagon designs and passenger boarding animations. **Refining AI Pathfinding** I'm now enhancing the AI's pathfinding to dynamically avoid self-collision, prioritizing passenger acquisition. I'm also starting to integrate distinct wagon designs and the passenger boarding animations into the logic. **Refining Visual Assets** I'm continuing to flesh out the visual details, focusing on how to render the game elements with pure HTML5 Canvas. This involves defining the aesthetic for the diorama elements, including the felt-like grass, ballast, and detailed track components. I'm also conceptualizing the specific design of the miniature passengers. **Defining Locomotive Features** I'm currently detailing the locomotive's features, specifying its distinct components like the cowcatcher, boiler, and cab. My focus is on defining the golden headlight glow and smoke particle effects, alongside the visual style of the wagons and their couplers. **Improving Navigation Logic** I'm currently refining the AI's pathfinding to intelligently avoid self-collision while prioritizing passenger acquisition. I'm also beginning to integrate distinct wagon designs and the passenger boarding animations into the core logic. **Polishing Interface Elements** I'm now meticulously crafting the UI, aiming for a rich, vintage railway board aesthetic with wood grain, brass accents, and a mechanical flip-clock score display. I'm also finalizing the steam-punk themed touch controls and ensuring the demo/game over screens are clear and engaging.