Train snakeOne-line
Grok 4.7
by SpaceXAIreasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 17 min 31 s
- Cost
- $0.662
- Output tokens
- 110,124
- Spent thinking
- 89,526 tokens
- Speed
- 105 tok/s
- Console errors
- 0
- First token after
- 1.7 s
- First visible text after
- 15 min 2 s
- Prompt tokens
- 1,599
- Code size
- 48.2 KB
- Finish reason
- stop
- Served by
- xAI
- Model id
x-ai/grok-4.7- Reasoning sent
model default- Max output
- 450,000 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 · 1574 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#174832">
<title>Meadow Line — little train snake</title>
<style>
:root {
--ink: #1c2b24;
--cream: #fff8ec;
--gold: #e6c15a;
--gold-deep: #b0893e;
--pine: #174832;
--pine-deep: #123528;
--header: #1b5640;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100%;
overflow: hidden;
background: var(--pine-deep);
color: var(--cream);
font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.page {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
max-width: 100%;
}
header {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 14px;
background: linear-gradient(180deg, #22684c 0%, var(--header) 100%);
border-bottom: 4px solid var(--gold);
box-shadow: inset 0 -10px 0 rgba(0,0,0,0.05);
}
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.logo { width: 72px; height: 44px; flex: 0 0 auto; display: block; }
.logo .wheel { transform-box: fill-box; transform-origin: center; animation: spin 1.1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
h1 {
margin: 0;
font-family: Georgia, "Palatino Linotype", Palatino, serif;
font-size: clamp(18px, 2.6vw, 32px);
font-weight: 700;
letter-spacing: 0.01em;
line-height: 1;
color: #fff8ec;
}
.tag {
margin: 3px 0 0;
font-size: clamp(11px, 1.4vw, 13px);
opacity: 0.88;
letter-spacing: 0.01em;
}
.tag .dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
background: #ff5d45;
box-shadow: 0 0 8px #ff5d45;
margin-right: 6px;
animation: blink 1.5s infinite;
}
@keyframes blink { 50% { opacity: 0.35; } }
.stats { display: flex; gap: 6px; flex-shrink: 0; }
.stat {
background: var(--cream);
color: var(--ink);
border-radius: 12px;
padding: 4px 10px 5px;
min-width: 64px;
text-align: center;
box-shadow: 0 2px 0 rgba(0,0,0,0.18);
border: 1px dashed #e0c48a;
}
.stat span {
display: block;
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #8a6a3b;
font-weight: 700;
}
.stat strong {
display: block;
font-family: Georgia, serif;
font-size: clamp(16px, 2vw, 22px);
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.stat strong.pop { animation: pop 0.28s ease; }
@keyframes pop {
40% { transform: scale(1.18); }
100% { transform: scale(1); }
}
.stage-wrap {
flex: 1;
min-height: 0;
padding: 10px 12px;
display: flex;
}
.stage {
flex: 1;
min-width: 0;
position: relative;
border-radius: 18px;
overflow: hidden;
cursor: pointer;
background: linear-gradient(#7ec8e3, #f8e7c3 42%, #5ea84a);
box-shadow:
0 0 0 3px var(--gold),
0 0 0 6px #8a6230,
0 14px 30px rgba(0,0,0,0.28);
touch-action: none;
}
canvas { display: block; width: 100%; height: 100%; }
.overlay {
position: absolute;
left: 50%;
bottom: 10px;
transform: translateX(-50%);
z-index: 3;
max-width: calc(100% - 16px);
width: max-content;
text-align: center;
pointer-events: none;
background: rgba(22, 16, 12, 0.9);
color: #fff8ec;
border: 2px solid rgba(230, 193, 90, 0.85);
border-radius: 16px;
padding: 8px 16px 10px;
box-shadow: 0 10px 24px rgba(0,0,0,0.28);
}
.overlay.is-hidden { display: none; }
.chip {
display: inline-block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.16em;
background: var(--gold);
color: #2a2118;
border-radius: 999px;
padding: 2px 8px;
margin-bottom: 3px;
}
.chip.bad { background: #e15a4a; color: white; }
.msg {
font-family: Georgia, "Palatino Linotype", serif;
font-size: clamp(15px, 2.3vw, 22px);
font-weight: 700;
line-height: 1.15;
}
.submsg {
margin-top: 2px;
font-size: 12px;
opacity: 0.88;
line-height: 1.3;
}
footer {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 14px 10px;
background: #143c2c;
border-top: 4px solid var(--gold);
color: #f6edd9;
}
.hint {
margin: 0;
font-size: 13px;
line-height: 1.35;
min-width: 0;
}
.hint b { color: #ffe08a; }
.dpad {
display: grid;
grid-template-columns: 42px 42px 42px;
grid-template-areas: ". up ." "left down right";
gap: 4px;
touch-action: none;
flex: 0 0 auto;
}
.dpad button {
border: none;
border-radius: 10px;
background: #f0d48a;
color: #1e3a2f;
font-size: 16px;
font-weight: 800;
height: 36px;
cursor: pointer;
box-shadow: 0 3px 0 var(--gold-deep);
font-family: inherit;
}
.dpad button:nth-child(1) { grid-area: up; }
.dpad button:nth-child(2) { grid-area: left; }
.dpad button:nth-child(3) { grid-area: down; }
.dpad button:nth-child(4) { grid-area: right; }
.dpad button:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--gold-deep); }
.dpad button:focus { outline: none; }
.dpad button:focus-visible { outline: 2px solid #fff; }
@media (max-width: 520px) {
header { padding: 7px 8px; gap: 6px; }
.tag { display: none; }
.logo { width: 52px; height: 32px; }
.stat { min-width: 52px; padding: 3px 6px; }
.stat.best { display: none; }
.hint { font-size: 12px; }
footer { padding: 6px 8px 8px; }
.stage-wrap { padding: 8px; }
}
@media (max-height: 640px) {
.dpad {
grid-template-columns: repeat(4, 40px);
grid-template-areas: "left up down right";
}
header { padding-top: 6px; padding-bottom: 6px; }
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="brand">
<svg class="logo" viewBox="0 0 72 44" aria-hidden="true">
<rect x="2" y="12" width="20" height="18" rx="3" fill="#b4332a"/>
<rect x="5" y="15" width="9" height="7" rx="1" fill="#bfe9f8"/>
<rect x="18" y="16" width="30" height="14" rx="6" fill="#2c2c2c"/>
<rect x="36" y="7" width="6" height="11" rx="1" fill="#2c2c2c"/>
<rect x="34" y="5" width="10" height="4" rx="1" fill="#e6c15a"/>
<polygon points="48,20 62,30 48,30" fill="#8a5a34"/>
<circle cx="52" cy="20" r="3" fill="#ffe9a8" stroke="#e6c15a" stroke-width="1"/>
<g class="wheel">
<circle cx="28" cy="34" r="6" fill="#222" stroke="#e6c15a" stroke-width="2"/>
<circle cx="28" cy="34" r="2" fill="#e6c15a"/>
</g>
<g class="wheel">
<circle cx="42" cy="34" r="6" fill="#222" stroke="#e6c15a" stroke-width="2"/>
<circle cx="42" cy="34" r="2" fill="#e6c15a"/>
</g>
</svg>
<div>
<h1>Meadow Line</h1>
<p class="tag"><span class="dot"></span>A little train that picks up everyone</p>
</div>
</div>
<div class="stats">
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
<div class="stat"><span>Cars</span><strong id="aboard">4</strong></div>
<div class="stat best"><span>Best</span><strong id="best">0</strong></div>
</div>
</header>
<div class="stage-wrap">
<div class="stage" id="stage">
<canvas id="c" aria-label="Meadow Line train game. Steer the locomotive, collect passengers waiting on platforms, and don't derail."></canvas>
<div class="overlay" id="overlay">
<div class="chip" id="chip">DEMO RUN</div>
<div class="msg" id="msg">Click or press a key to start</div>
<div class="submsg" id="submsg">Steer the train · each passenger adds a colorful wagon</div>
</div>
</div>
</div>
<footer>
<p class="hint">Arrows, WASD, swipe, or the pad.<br>Waiting on the platform: <b id="pname">Mina</b></p>
<div class="dpad" aria-label="Direction controls">
<button type="button" data-x="0" data-y="-1" aria-label="Up">▲</button>
<button type="button" data-x="-1" data-y="0" aria-label="Left">◀</button>
<button type="button" data-x="0" data-y="1" aria-label="Down">▼</button>
<button type="button" data-x="1" data-y="0" aria-label="Right">▶</button>
</div>
</footer>
</div>
<script>
'use strict';
const COLS = 12;
const ROWS = 8;
const WAGONS = [
['#3b82f6', '#1d4ed8'],
['#f5c518', '#d39b0b'],
['#34c47a', '#1f8a52'],
['#f15b4a', '#c4362a'],
['#a855f7', '#7c3aed'],
['#fb923c', '#ea6a12'],
['#14b8a6', '#0f766e'],
['#f472b6', '#db2777']
];
const SKINS = ['#f6d7b0', '#e0ac69', '#c68642', '#8d5524', '#ffd8be'];
const HATS = ['#e6c15a', '#ef5d4a', '#2f6fed', '#2c2c2c', '#f4efe4', '#2faf6a'];
const NAMES = ['Ada','Otto','Mina','Leo','Nell','Hugo','Ivy','Sam','Bea','Gus','Noor','Felix','Hana','Jules','Pia','Theo','Willa','Omar','Luz','Ned'];
const CHIM_X = 0.12;
const CHIM_Y = -0.50;
const NOSE_X = 0.48;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const stage = document.getElementById('stage');
const overlay = document.getElementById('overlay');
const chipEl = document.getElementById('chip');
const msgEl = document.getElementById('msg');
const subEl = document.getElementById('submsg');
const scoreEl = document.getElementById('score');
const aboardEl = document.getElementById('aboard');
const bestEl = document.getElementById('best');
const nameEl = document.getElementById('pname');
let cssW = 0, cssH = 0, dpr = 1;
let ox = 0, oy = 0, cs = 0, horizon = 0, boardW = 0, boardH = 0;
let snake = [], prevSnake = [], wagonColors = [];
let dir = {x: 1, y: 0}, nextDir = {x: 1, y: 0};
let passenger = null;
let mode = 'demo';
let score = 0, best = 0, picked = 0;
let particles = [], floaters = [];
let acc = 0, shake = 0, flash = 0, glow = 0, travel = 0;
let steamBooted = false, wheelFreeze = 0;
let deathReason = '';
let restartTimer = null;
let lastNow = 0;
let swipe = null;
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function pathRound(c, x, y, w, h, r) {
const rr = Math.max(0, Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2));
c.beginPath();
c.moveTo(x + rr, y);
c.arcTo(x + w, y, x + w, y + h, rr);
c.arcTo(x + w, y + h, x, y + h, rr);
c.arcTo(x, y + h, x, y, rr);
c.arcTo(x, y, x + w, y, rr);
c.closePath();
}
function layoutBoard() {
const narrow = cssW < 520;
horizon = narrow ? clamp(cssH * 0.15, 46, 78) : clamp(cssH * 0.18, 72, 118);
const reserve = narrow ? 78 : 62;
const side = Math.max(6, cssW * 0.015);
const top = horizon + 4;
const availW = Math.max(40, cssW - side * 2);
const availH = Math.max(40, cssH - top - reserve);
cs = Math.floor(Math.min(availW / COLS, availH / ROWS));
cs = Math.max(8, cs);
boardW = cs * COLS;
boardH = cs * ROWS;
ox = Math.round((cssW - boardW) / 2);
oy = Math.round(top + Math.max(0, (availH - boardH) / 2));
}
function resize() {
const r = stage.getBoundingClientRect();
const w = Math.max(1, Math.floor(r.width));
const h = Math.max(1, Math.floor(r.height));
const next = Math.min(window.devicePixelRatio || 1, 2);
if (w === cssW && h === cssH && next === dpr && canvas.width === Math.floor(w * next)) return;
cssW = w;
cssH = h;
dpr = next;
canvas.width = Math.max(1, Math.floor(w * dpr));
canvas.height = Math.max(1, Math.floor(h * dpr));
layoutBoard();
}
function bump(el) {
if (!el) return;
el.classList.remove('pop');
void el.offsetWidth;
el.classList.add('pop');
}
function updateHUD() {
scoreEl.textContent = String(score);
aboardEl.textContent = String(Math.max(0, snake.length - 1));
bestEl.textContent = String(best);
nameEl.textContent = passenger ? passenger.name : '—';
}
function setOverlay(kind) {
if (kind === 'play') {
overlay.classList.add('is-hidden');
return;
}
overlay.classList.remove('is-hidden');
if (kind === 'dead') {
chipEl.textContent = 'DERAILED';
chipEl.classList.add('bad');
msgEl.textContent = 'Click or press a key to start';
subEl.textContent = deathReason + ' · Score ' + score;
} else {
chipEl.textContent = 'DEMO RUN';
chipEl.classList.remove('bad');
msgEl.textContent = 'Click or press a key to start';
subEl.textContent = 'Steer the train · each passenger adds a colorful wagon';
}
}
function makePassenger(x, y, ci, name) {
return {
x: x, y: y,
ci: ci,
name: name,
skin: Math.floor(Math.random() * SKINS.length),
seed: Math.random() * 6
};
}
function spawnPassenger(first) {
if (first) {
const x = Math.min(COLS - 2, snake[0].x + 5);
const y = snake[0].y;
passenger = makePassenger(x, y, 4, 'Mina');
nameEl.textContent = 'Mina';
return;
}
const occ = new Set(snake.map(s => s.x + ',' + s.y));
const free = [];
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (!occ.has(x + ',' + y)) free.push({x: x, y: y});
}
}
if (!free.length) {
passenger = null;
nameEl.textContent = '—';
return;
}
const head = snake[0];
let pool = free.filter(c => Math.abs(c.x - head.x) + Math.abs(c.y - head.y) >= 4);
if (!pool.length) pool = free;
const cell = pool[Math.floor(Math.random() * pool.length)];
const ci = Math.floor(Math.random() * WAGONS.length);
const name = NAMES[Math.floor(Math.random() * NAMES.length)];
passenger = makePassenger(cell.x, cell.y, ci, name);
nameEl.textContent = name;
}
function resetState() {
snake = [];
const y = 3;
const headX = 4;
for (let i = 0; i < 5; i++) snake.push({x: headX - i, y: y});
prevSnake = snake.map(s => ({x: s.x, y: s.y}));
wagonColors = [0, 1, 2, 3];
dir = {x: 1, y: 0};
nextDir = {x: 1, y: 0};
score = 0;
picked = 0;
particles = [];
floaters = [];
acc = 0;
shake = 0;
flash = 0;
glow = 0;
travel = 0;
steamBooted = false;
spawnPassenger(true);
updateHUD();
}
function startDemo() {
clearTimeout(restartTimer);
resetState();
mode = 'demo';
setOverlay('demo');
}
function startPlay() {
if (mode === 'play') return;
clearTimeout(restartTimer);
resetState();
mode = 'play';
setOverlay('play');
}
function cellPx(gx, gy) {
return { x: ox + gx * cs + cs / 2, y: oy + gy * cs + cs / 2 };
}
function burst(x, y, color) {
const colors = [color, '#fff8ec', '#e6c15a', '#ffffff'];
for (let i = 0; i < 22; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 40 + Math.random() * 160;
particles.push({
type: 'confetti',
x: x, y: y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 40,
life: 0.7 + Math.random() * 0.45,
max: 1,
rot: Math.random() * 6,
vr: (Math.random() - 0.5) * 10,
w: 4 + Math.random() * 4,
h: 5 + Math.random() * 5,
color: colors[i % colors.length]
});
}
}
function boom(x, y) {
for (let i = 0; i < 18; i++) {
const a = (i / 18) * Math.PI * 2;
particles.push({
type: 'confetti',
x: x, y: y,
vx: Math.cos(a) * 140,
vy: Math.sin(a) * 140,
life: 0.65,
max: 0.65,
rot: 0,
vr: 6,
w: 5, h: 5,
color: i % 2 ? '#5b5146' : '#e6c15a'
});
}
}
function die(reason) {
if (mode === 'dead') return;
const wasDemo = mode === 'demo';
mode = 'dead';
deathReason = reason;
prevSnake = snake.map(s => ({x: s.x, y: s.y}));
shake = 1;
flash = 1;
wheelFreeze = performance.now() / 150;
if (cs > 0 && snake.length) {
const hp = cellPx(snake[0].x, snake[0].y);
boom(hp.x, hp.y);
}
if (score > best) best = score;
updateHUD();
setOverlay('dead');
if (wasDemo) {
clearTimeout(restartTimer);
restartTimer = setTimeout(() => {
if (mode === 'dead') startDemo();
}, 900);
}
}
function dirFromKey(k) {
const key = k.toLowerCase();
if (key === 'arrowup' || key === 'w') return {x: 0, y: -1};
if (key === 'arrowdown' || key === 's') return {x: 0, y: 1};
if (key === 'arrowleft' || key === 'a') return {x: -1, y: 0};
if (key === 'arrowright' || key === 'd') return {x: 1, y: 0};
return null;
}
function queueDir(d) {
if (!d) return;
if (d.x === -dir.x && d.y === -dir.y) return;
nextDir = {x: d.x, y: d.y};
}
function bfs(head, target, blocked) {
const q = [{x: head.x, y: head.y, first: null}];
const seen = new Set([head.x + ',' + head.y]);
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
let qi = 0;
while (qi < q.length) {
const cur = q[qi++];
for (let i = 0; i < 4; i++) {
const dx = dirs[i][0], dy = dirs[i][1];
const nx = cur.x + dx, ny = cur.y + dy;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const key = nx + ',' + ny;
if (seen.has(key)) continue;
const first = cur.first || {x: dx, y: dy};
if (nx === target.x && ny === target.y) return first;
if (blocked.has(key)) continue;
seen.add(key);
q.push({x: nx, y: ny, first: first});
}
}
return null;
}
function freeSpace(x, y, blocked) {
const b = new Set(blocked);
b.delete(snake[0].x + ',' + snake[0].y);
b.add(x + ',' + y);
const q = [[x, y]];
const seen = new Set([x + ',' + y]);
let n = 0, qi = 0;
while (qi < q.length) {
const cur = q[qi++];
n++;
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (let i = 0; i < 4; i++) {
const nx = cur[0] + dirs[i][0], ny = cur[1] + dirs[i][1];
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const k = nx + ',' + ny;
if (seen.has(k) || b.has(k)) continue;
seen.add(k);
q.push([nx, ny]);
}
}
return n;
}
function aiMove() {
const head = snake[0];
const blocked = new Set();
for (let i = 0; i < snake.length - 1; i++) blocked.add(snake[i].x + ',' + snake[i].y);
if (passenger) {
const step = bfs(head, passenger, blocked);
if (step) return step;
}
let bestMove = null;
let bestScore = -1;
const opts = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (let i = 0; i < 4; i++) {
const dx = opts[i][0], dy = opts[i][1];
if (dx === -dir.x && dy === -dir.y) continue;
const nx = head.x + dx, ny = head.y + dy;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
if (blocked.has(nx + ',' + ny)) continue;
let sc = freeSpace(nx, ny, blocked);
if (dx === dir.x && dy === dir.y) sc += 0.6;
if (sc > bestScore) { bestScore = sc; bestMove = {x: dx, y: dy}; }
}
return bestMove || {x: dir.x, y: dir.y};
}
function currentInterval() {
if (mode === 'demo') return 160;
return Math.max(68, 150 - picked * 4);
}
function advance() {
if (mode === 'demo') {
const m = aiMove();
dir = {x: m.x, y: m.y};
nextDir = {x: m.x, y: m.y};
} else {
dir = {x: nextDir.x, y: nextDir.y};
}
const head = snake[0];
const nx = head.x + dir.x;
const ny = head.y + dir.y;
const eating = !!(passenger && passenger.x === nx && passenger.y === ny);
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) {
die('The train ran off the yard');
return;
}
for (let i = 0; i < snake.length; i++) {
if (!eating && i === snake.length - 1) continue;
if (snake[i].x === nx && snake[i].y === ny) {
die('The cars folded together');
return;
}
}
prevSnake = snake.map(s => ({x: s.x, y: s.y}));
snake.unshift({x: nx, y: ny});
if (eating) {
const ci = passenger.ci;
const pname = passenger.name;
wagonColors.push(ci);
picked += 1;
score += 10;
if (score > best) best = score;
const p = cellPx(nx, ny);
burst(p.x, p.y, WAGONS[ci % WAGONS.length][0]);
floaters.push({x: p.x, y: p.y - 10, text: pname + ' +10', life: 1.15, vy: 34});
glow = 1;
spawnPassenger(false);
updateHUD();
bump(scoreEl);
} else {
snake.pop();
}
travel += 1;
}
function makeSteam(x, y, life) {
const L = life == null ? 1 : life;
return {
type: 'steam',
x: x + (Math.random() - 0.5) * 8,
y: y - (1 - L) * 22,
vx: (Math.random() - 0.5) * 18,
vy: -26 - Math.random() * 18,
life: L,
max: 1,
r: 3 + Math.random() * 5
};
}
function updateParticles(dt) {
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
if (p.type === 'steam') {
p.vy -= 10 * dt;
p.r += 10 * dt;
} else {
p.vy += 320 * dt;
p.rot += (p.vr || 0) * dt;
}
}
particles = particles.filter(p => p.life > 0);
if (particles.length > 110) particles.splice(0, particles.length - 110);
}
function updateFloaters(dt) {
for (let i = 0; i < floaters.length; i++) {
floaters[i].life -= dt;
floaters[i].y -= (floaters[i].vy || 30) * dt;
}
floaters = floaters.filter(f => f.life > 0);
}
function faceFromVector(dx, dy) {
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) {
if (dir.x > 0) return 'right';
if (dir.x < 0) return 'left';
if (dir.y > 0) return 'down';
return 'up';
}
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? 'right' : 'left';
return dy >= 0 ? 'down' : 'up';
}
function segPos(i, t) {
const b = snake[i];
const a = prevSnake[i] || b;
return {
x: ox + (a.x + (b.x - a.x) * t) * cs + cs / 2,
y: oy + (a.y + (b.y - a.y) * t) * cs + cs / 2
};
}
function faceOf(i, t) {
let dx = 0, dy = 0;
if (i < snake.length - 1) {
const a = segPos(i, t);
const b = segPos(i + 1, t);
dx = a.x - b.x;
dy = a.y - b.y;
}
if (dx * dx + dy * dy < 0.25) {
const a = prevSnake[i] || snake[i];
const b = snake[i];
dx = (b.x - a.x) * cs;
dy = (b.y - a.y) * cs;
}
if (dx * dx + dy * dy < 0.25 && i > 0) {
const a = segPos(i - 1, t);
const b = segPos(i, t);
dx = a.x - b.x;
dy = a.y - b.y;
}
return faceFromVector(dx, dy);
}
function withFace(c, x, y, face, draw) {
c.save();
c.translate(x, y);
if (face === 'left') c.scale(-1, 1);
else if (face === 'down') c.rotate(Math.PI / 2);
else if (face === 'up') c.rotate(-Math.PI / 2);
draw(c);
c.restore();
}
function localToWorld(x, y, face, lx, ly) {
if (face === 'left') return {x: x - lx, y: y + ly};
if (face === 'down') return {x: x - ly, y: y + lx};
if (face === 'up') return {x: x + ly, y: y - lx};
return {x: x + lx, y: y + ly};
}
function bobY(i, now) {
if (mode === 'dead') return 0;
return Math.sin(now / 90 + i * 0.8) * Math.min(1.4, cs * 0.028);
}
function drawWheel(c, x, y, r, spin) {
c.save();
c.translate(x, y);
c.fillStyle = '#1a1a1a';
c.beginPath();
c.arc(0, 0, r, 0, Math.PI * 2);
c.fill();
c.strokeStyle = '#c9a24a';
c.lineWidth = Math.max(1, r * 0.18);
c.stroke();
c.rotate(spin);
c.strokeStyle = '#777';
c.lineWidth = Math.max(1, r * 0.12);
c.beginPath();
c.moveTo(-r * 0.65, 0);
c.lineTo(r * 0.65, 0);
c.moveTo(0, -r * 0.65);
c.lineTo(0, r * 0.65);
c.stroke();
c.fillStyle = '#e6c15a';
c.beginPath();
c.arc(0, 0, r * 0.28, 0, Math.PI * 2);
c.fill();
c.restore();
}
function drawTrack(c, u) {
c.fillStyle = '#7a5336';
for (let i = -1; i <= 1; i++) {
pathRound(c, -u * 0.30, i * u * 0.15 - u * 0.03, u * 0.60, u * 0.06, 1);
c.fill();
}
c.fillStyle = '#9aa3ad';
c.fillRect(-u * 0.36, -u * 0.11, u * 0.72, u * 0.03);
c.fillRect(-u * 0.36, u * 0.08, u * 0.72, u * 0.03);
c.fillStyle = '#c5ced6';
c.fillRect(-u * 0.36, -u * 0.11, u * 0.72, u * 0.012);
c.fillRect(-u * 0.36, u * 0.08, u * 0.72, u * 0.012);
}
function drawLoco(c, u, spin) {
drawWheel(c, -u * 0.18, u * 0.20, u * 0.11, spin);
drawWheel(c, u * 0.08, u * 0.20, u * 0.11, spin);
drawWheel(c, u * 0.26, u * 0.22, u * 0.065, spin);
c.fillStyle = '#7a4e2d';
c.beginPath();
c.moveTo(u * 0.24, u * 0.02);
c.lineTo(u * 0.50, u * 0.20);
c.lineTo(u * 0.22, u * 0.20);
c.closePath();
c.fill();
c.beginPath();
c.moveTo(u * 0.24, -u * 0.04);
c.lineTo(u * 0.44, -u * 0.16);
c.lineTo(u * 0.24, u * 0.02);
c.closePath();
c.fill();
c.fillStyle = '#a33b32';
pathRound(c, -u * 0.48, -u * 0.40, u * 0.34, u * 0.56, u * 0.06);
c.fill();
c.strokeStyle = 'rgba(0,0,0,0.25)';
c.lineWidth = Math.max(1, u * 0.02);
c.stroke();
c.fillStyle = '#2b2b2b';
pathRound(c, -u * 0.52, -u * 0.46, u * 0.40, u * 0.10, u * 0.04);
c.fill();
c.fillStyle = '#e6c15a';
c.beginPath();
c.arc(-u * 0.20, -u * 0.48, u * 0.04, 0, Math.PI * 2);
c.fill();
c.fillStyle = '#b9e6fb';
pathRound(c, -u * 0.40, -u * 0.32, u * 0.16, u * 0.15, u * 0.03);
c.fill();
c.fillStyle = 'rgba(255,255,255,0.45)';
c.fillRect(-u * 0.38, -u * 0.30, u * 0.05, u * 0.06);
c.fillStyle = '#f1c27d';
c.beginPath();
c.arc(-u * 0.32, -u * 0.24, u * 0.045, 0, Math.PI * 2);
c.fill();
c.fillStyle = '#1d3557';
c.fillRect(-u * 0.36, -u * 0.29, u * 0.08, u * 0.03);
c.fillStyle = '#2a2a2a';
pathRound(c, -u * 0.22, -u * 0.18, u * 0.52, u * 0.34, u * 0.12);
c.fill();
c.fillStyle = '#3a3a3a';
c.fillRect(-u * 0.22, -u * 0.18, u * 0.52, u * 0.06);
c.fillStyle = '#e6c15a';
c.fillRect(-u * 0.02, -u * 0.18, u * 0.035, u * 0.34);
c.fillStyle = '#1a1a1a';
pathRound(c, u * CHIM_X - u * 0.05, u * CHIM_Y, u * 0.10, u * 0.34, u * 0.03);
c.fill();
c.fillStyle = '#e6c15a';
pathRound(c, u * CHIM_X - u * 0.07, u * CHIM_Y - u * 0.02, u * 0.14, u * 0.06, u * 0.02);
c.fill();
c.fillStyle = '#a33b32';
c.fillRect(u * CHIM_X - u * 0.05, u * CHIM_Y + u * 0.10, u * 0.10, u * 0.04);
c.fillStyle = '#3a3a3a';
c.beginPath();
c.arc(-u * 0.06, -u * 0.18, u * 0.055, Math.PI, 0);
c.fill();
c.fillStyle = '#ffe9a8';
c.beginPath();
c.arc(u * 0.30, -u * 0.02, u * 0.065, 0, Math.PI * 2);
c.fill();
c.strokeStyle = '#e6c15a';
c.lineWidth = Math.max(1, u * 0.02);
c.stroke();
c.fillStyle = '#111';
pathRound(c, -u * 0.42, -u * 0.02, u * 0.14, u * 0.11, 2);
c.fill();
c.fillStyle = '#e6c15a';
c.beginPath();
c.arc(-u * 0.35, u * 0.035, u * 0.028, 0, Math.PI * 2);
c.fill();
}
function drawWagon(c, u, ci, spin) {
const pal = WAGONS[(ci == null ? 0 : ci) % WAGONS.length];
drawWheel(c, -u * 0.16, u * 0.20, u * 0.075, spin);
drawWheel(c, u * 0.16, u * 0.20, u * 0.075, spin);
const g = c.createLinearGradient(0, -u * 0.3, 0, u * 0.16);
g.addColorStop(0, pal[0]);
g.addColorStop(1, pal[1]);
c.fillStyle = g;
pathRound(c, -u * 0.32, -u * 0.28, u * 0.64, u * 0.44, u * 0.07);
c.fill();
c.strokeStyle = 'rgba(0,0,0,0.22)';
c.lineWidth = Math.max(1, u * 0.02);
c.stroke();
c.fillStyle = pal[1];
pathRound(c, -u * 0.36, -u * 0.36, u * 0.72, u * 0.12, u * 0.04);
c.fill();
c.fillStyle = 'rgba(255,255,255,0.28)';
c.fillRect(-u * 0.32, -u * 0.02, u * 0.64, u * 0.035);
const wins = [-u * 0.18, u * 0.04];
for (let i = 0; i < wins.length; i++) {
const wx = wins[i];
c.fillStyle = '#d7f1fb';
pathRound(c, wx, -u * 0.22, u * 0.14, u * 0.13, u * 0.02);
c.fill();
c.fillStyle = 'rgba(255,255,255,0.5)';
c.fillRect(wx + u * 0.015, -u * 0.21, u * 0.035, u * 0.05);
c.fillStyle = SKINS[(ci + i) % SKINS.length];
c.beginPath();
c.arc(wx + u * 0.07, -u * 0.15, u * 0.032, 0, Math.PI * 2);
c.fill();
}
c.fillStyle = pal[1];
pathRound(c, u * 0.20, -u * 0.16, u * 0.07, u * 0.26, 2);
c.fill();
}
function drawCoupling(a, b) {
cStrokeCoupling(a, b);
}
function cStrokeCoupling(a, b) {
ctx.strokeStyle = '#3e2c22';
ctx.lineWidth = Math.max(2, cs * 0.045);
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
ctx.fillStyle = '#e6c15a';
ctx.beginPath();
ctx.arc((a.x + b.x) / 2, (a.y + b.y) / 2, Math.max(1.6, cs * 0.032), 0, Math.PI * 2);
ctx.fill();
}
function drawGroundShadow(x, y, rx) {
ctx.save();
ctx.translate(x, y + cs * 0.22);
ctx.scale(1, 0.32);
ctx.fillStyle = 'rgba(40, 30, 10, 0.2)';
ctx.beginPath();
ctx.arc(0, 0, rx, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawPerson(c, x, feetY, h, outfit, now, waving) {
const bob = Math.sin(now / 180 + x * 0.02) * h * 0.03;
c.save();
c.translate(x, feetY + bob);
c.fillStyle = 'rgba(0,0,0,0.15)';
c.beginPath();
c.ellipse(0, 2, h * 0.22, h * 0.06, 0, 0, Math.PI * 2);
c.fill();
c.strokeStyle = outfit.pants;
c.lineWidth = Math.max(1.5, h * 0.07);
c.lineCap = 'round';
c.beginPath();
c.moveTo(-h * 0.06, -h * 0.28);
c.lineTo(-h * 0.08, -h * 0.02);
c.moveTo(h * 0.06, -h * 0.28);
c.lineTo(h * 0.09, -h * 0.02);
c.stroke();
c.fillStyle = outfit.shirt;
pathRound(c, -h * 0.16, -h * 0.58, h * 0.32, h * 0.32, h * 0.08);
c.fill();
const wave = waving ? Math.sin(now / 140) * 0.5 : 0.15;
c.strokeStyle = outfit.skin;
c.lineWidth = Math.max(1.4, h * 0.055);
c.beginPath();
c.moveTo(h * 0.12, -h * 0.48);
c.lineTo(h * 0.26, -h * 0.62 - wave * h * 0.18);
c.moveTo(-h * 0.12, -h * 0.46);
c.lineTo(-h * 0.24, -h * 0.32);
c.stroke();
c.fillStyle = outfit.bag || '#6d4c41';
pathRound(c, -h * 0.34, -h * 0.36, h * 0.12, h * 0.1, 2);
c.fill();
c.fillStyle = outfit.skin;
c.beginPath();
c.arc(0, -h * 0.68, h * 0.12, 0, Math.PI * 2);
c.fill();
c.fillStyle = '#2b2b2b';
c.beginPath();
c.arc(-h * 0.04, -h * 0.69, Math.max(0.6, h * 0.016), 0, Math.PI * 2);
c.arc(h * 0.04, -h * 0.69, Math.max(0.6, h * 0.016), 0, Math.PI * 2);
c.fill();
c.strokeStyle = '#2b2b2b';
c.lineWidth = 1;
c.beginPath();
c.arc(0, -h * 0.66, h * 0.04, 0.2, Math.PI - 0.2);
c.stroke();
if (outfit.hat) {
c.fillStyle = outfit.hat;
c.fillRect(-h * 0.13, -h * 0.82, h * 0.26, h * 0.045);
c.fillRect(-h * 0.08, -h * 0.92, h * 0.16, h * 0.11);
}
c.restore();
}
function drawPlatform(c, x, y, w) {
const h = Math.max(6, w * 0.22);
c.fillStyle = '#8d6a45';
pathRound(c, x - w / 2, y, w, h, 3);
c.fill();
c.strokeStyle = 'rgba(0,0,0,0.15)';
c.lineWidth = 1;
c.beginPath();
for (let i = 1; i < 4; i++) {
c.moveTo(x - w / 2, y + (h * i) / 4);
c.lineTo(x + w / 2, y + (h * i) / 4);
}
c.stroke();
c.fillStyle = '#f2d04b';
c.fillRect(x - w / 2 + 2, y + 2, w - 4, 2);
const px = x + w * 0.36;
c.fillStyle = '#5d4037';
c.fillRect(px, y - w * 0.55, Math.max(2, w * 0.045), w * 0.55);
c.fillStyle = '#c62828';
c.beginPath();
c.arc(px + 1.5, y - w * 0.58, Math.max(4, w * 0.09), 0, Math.PI * 2);
c.fill();
c.fillStyle = '#fff';
c.font = '700 ' + Math.max(7, w * 0.12) + 'px system-ui, sans-serif';
c.textAlign = 'center';
c.textBaseline = 'middle';
if (w > 28) c.fillText('1', px + 1.5, y - w * 0.57);
}
function drawLabel(text, x, y) {
ctx.font = '700 ' + Math.max(10, Math.min(16, cs * 0.24)) + 'px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const tw = ctx.measureText(text).width + 14;
const h = Math.max(16, cs * 0.30);
ctx.fillStyle = 'rgba(255,248,236,0.96)';
pathRound(ctx, x - tw / 2, y - h / 2, tw, h, h / 2);
ctx.fill();
ctx.strokeStyle = '#e6c15a';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#1c2b24';
ctx.fillText(text, x, y + 0.5);
}
function drawYardPassenger(now) {
if (!passenger) return;
const top = oy + passenger.y * cs;
const x = ox + passenger.x * cs + cs / 2;
const pulse = 0.5 + 0.5 * Math.sin(now / 200 + passenger.seed);
ctx.save();
ctx.strokeStyle = 'rgba(230, 168, 40,' + (0.35 + pulse * 0.5) + ')';
ctx.lineWidth = 2.5;
pathRound(ctx, ox + passenger.x * cs + 2, top + 2, cs - 4, cs - 4, 7);
ctx.stroke();
ctx.restore();
drawPlatform(ctx, x, top + cs * 0.68, cs * 0.92);
const outfit = {
shirt: WAGONS[passenger.ci % WAGONS.length][0],
pants: '#243140',
skin: SKINS[passenger.skin % SKINS.length],
hat: HATS[passenger.ci % HATS.length],
bag: '#6d4c41'
};
drawPerson(ctx, x - cs * 0.05, top + cs * 0.78, cs * 0.62, outfit, now, true);
if (cs >= 20) {
let ly = top - 6;
if (passenger.y === 0) ly = top + cs * 0.18;
ly = clamp(ly, 14, cssH - 14);
drawLabel(passenger.name, x, ly);
}
}
function drawCloud(c, x, y, s) {
c.fillStyle = 'rgba(255,255,255,0.9)';
c.beginPath();
c.arc(x, y, 16 * s, 0, Math.PI * 2);
c.arc(x + 18 * s, y + 4 * s, 12 * s, 0, Math.PI * 2);
c.arc(x - 16 * s, y + 5 * s, 11 * s, 0, Math.PI * 2);
c.arc(x + 6 * s, y - 8 * s, 10 * s, 0, Math.PI * 2);
c.fill();
}
function drawBird(c, x, y, flap) {
c.strokeStyle = 'rgba(45,55,75,0.75)';
c.lineWidth = 1.6;
c.beginPath();
c.moveTo(x - 7, y + flap);
c.quadraticCurveTo(x - 3, y - 5, x, y);
c.quadraticCurveTo(x + 3, y - 5, x + 7, y + flap);
c.stroke();
}
function drawSun(c, x, y, r) {
const g = c.createRadialGradient(x, y, r * 0.2, x, y, r * 2.4);
g.addColorStop(0, 'rgba(255,220,120,0.95)');
g.addColorStop(0.35, 'rgba(255,200,80,0.28)');
g.addColorStop(1, 'rgba(255,200,80,0)');
c.fillStyle = g;
c.beginPath();
c.arc(x, y, r * 2.4, 0, Math.PI * 2);
c.fill();
c.fillStyle = '#ffe08a';
c.beginPath();
c.arc(x, y, r, 0, Math.PI * 2);
c.fill();
}
function drawTree(c, x, ground, h) {
c.fillStyle = '#6b4428';
c.fillRect(x - 4, ground - h * 0.42, 8, h * 0.42);
c.fillStyle = '#2f7d46';
c.beginPath();
c.arc(x, ground - h * 0.55, h * 0.26, 0, Math.PI * 2);
c.fill();
c.fillStyle = '#3e9654';
c.beginPath();
c.arc(x - h * 0.12, ground - h * 0.42, h * 0.2, 0, Math.PI * 2);
c.fill();
c.fillStyle = '#267346';
c.beginPath();
c.arc(x + h * 0.12, ground - h * 0.68, h * 0.18, 0, Math.PI * 2);
c.fill();
}
function drawSignal(c, x, y, go) {
c.fillStyle = '#3d4a55';
c.fillRect(x, y - 54, 4, 54);
c.fillStyle = '#222';
pathRound(c, x - 8, y - 78, 20, 28, 4);
c.fill();
c.fillStyle = go ? '#5a2018' : '#ff5a4a';
c.beginPath();
c.arc(x + 2, y - 70, 4, 0, Math.PI * 2);
c.fill();
c.fillStyle = go ? '#7CFF6B' : '#1e4a28';
c.beginPath();
c.arc(x + 2, y - 58, 4, 0, Math.PI * 2);
c.fill();
if (go) {
c.fillStyle = 'rgba(124,255,107,0.28)';
c.beginPath();
c.arc(x + 2, y - 58, 8, 0, Math.PI * 2);
c.fill();
}
}
function drawWaterTower(c, x, ground) {
c.fillStyle = '#6b4428';
c.fillRect(x - 14, ground - 62, 5, 62);
c.fillRect(x + 12, ground - 62, 5, 62);
c.fillRect(x - 16, ground - 28, 36, 4);
c.fillStyle = '#a67c52';
c.fillRect(x - 20, ground - 78, 44, 20);
c.fillStyle = '#c4a574';
c.beginPath();
c.ellipse(x + 2, ground - 78, 22, 8, 0, 0, Math.PI * 2);
c.fill();
c.fillStyle = '#7a3030';
c.beginPath();
c.moveTo(x - 24, ground - 78);
c.lineTo(x + 2, ground - 104);
c.lineTo(x + 28, ground - 78);
c.closePath();
c.fill();
}
function drawSignpost(c, x, y) {
c.fillStyle = '#6b4428';
c.fillRect(x, y, 4, 46);
c.fillStyle = '#fff8ec';
pathRound(c, x - 36, y - 8, 76, 22, 4);
c.fill();
c.strokeStyle = '#6b3e26';
c.lineWidth = 1.5;
c.stroke();
c.fillStyle = '#1c2b24';
c.font = '700 10px system-ui, sans-serif';
c.textAlign = 'center';
c.textBaseline = 'middle';
c.fillText('ALL ABOARD', x + 2, y + 3);
}
function drawFlower(c, x, y, color) {
c.fillStyle = '#3e8f45';
c.fillRect(x, y, 2, 7);
c.fillStyle = color;
for (let a = 0; a < 5; a++) {
const ang = (a / 5) * Math.PI * 2;
c.beginPath();
c.arc(x + Math.cos(ang) * 3.2, y + Math.sin(ang) * 3.2, 2.2, 0, Math.PI * 2);
c.fill();
}
c.fillStyle = '#ffe08a';
c.beginPath();
c.arc(x, y, 1.6, 0, Math.PI * 2);
c.fill();
}
function drawClock(c, x, y, r) {
c.save();
c.fillStyle = '#fff8ec';
c.beginPath();
c.arc(x, y, r, 0, Math.PI * 2);
c.fill();
c.strokeStyle = '#1c2b24';
c.lineWidth = Math.max(1, r * 0.12);
c.stroke();
const d = new Date();
const min = d.getMinutes() + d.getSeconds() / 60;
const hr = (d.getHours() % 12) + min / 60;
const hand = (frac, len, w) => {
const a = frac * Math.PI * 2 - Math.PI / 2;
c.strokeStyle = '#1c2b24';
c.lineWidth = w;
c.lineCap = 'round';
c.beginPath();
c.moveTo(x, y);
c.lineTo(x + Math.cos(a) * len, y + Math.sin(a) * len);
c.stroke();
};
hand(hr / 12, r * 0.48, Math.max(1.4, r * 0.14));
hand(min / 60, r * 0.7, Math.max(1, r * 0.09));
c.fillStyle = '#b4332a';
c.beginPath();
c.arc(x, y, Math.max(1, r * 0.12), 0, Math.PI * 2);
c.fill();
c.restore();
}
function drawStation(now, base) {
const u = clamp(base / 100, 0.45, 1.2);
const bw = Math.min(cssW * 0.28, 210 * u);
const bh = 70 * u;
const bx = Math.max(10, cssW * 0.035);
const by = base - bh;
cShadowRect(bx + 4, base - 3, bw, 6);
ctx.fillStyle = '#2c3e50';
ctx.beginPath();
ctx.moveTo(bx - 8, by + 16 * u);
ctx.lineTo(bx + bw / 2, by - 6 * u);
ctx.lineTo(bx + bw + 8, by + 16 * u);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#c45c4a';
ctx.fillRect(bx, by + 14 * u, bw, bh - 14 * u);
ctx.strokeStyle = 'rgba(80,20,10,0.08)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let y = by + 22 * u; y < base - 2; y += 6 * u) {
ctx.moveTo(bx, y);
ctx.lineTo(bx + bw, y);
}
ctx.stroke();
ctx.fillStyle = '#5a3a2a';
const dw = 16 * u, dh = 26 * u;
ctx.fillRect(bx + bw * 0.42, base - dh, dw, dh);
ctx.fillStyle = '#f6e27a';
ctx.fillRect(bx + 12 * u, by + 28 * u, 14 * u, 16 * u);
ctx.strokeStyle = '#fff8ec';
ctx.lineWidth = 1.5;
ctx.strokeRect(bx + 12 * u, by + 28 * u, 14 * u, 16 * u);
drawClock(ctx, bx + bw - 18 * u, by + 34 * u, 9 * u);
ctx.fillStyle = '#8d6e55';
ctx.fillRect(0, base - 5 * u, cssW, 7 * u);
ctx.fillStyle = '#f0d56c';
ctx.fillRect(0, base - 5 * u, cssW, 2);
const ax = bx + bw + 6;
const awningRight = Math.min(cssW - 8, ax + Math.min(280, cssW * 0.42));
if (awningRight > ax + 24) {
const stripes = Math.max(4, Math.floor((awningRight - ax) / (12 * u)));
const sw = (awningRight - ax) / stripes;
for (let i = 0; i < stripes; i++) {
ctx.fillStyle = i % 2 === 0 ? '#b4332a' : '#f4efe4';
ctx.fillRect(ax + i * sw, base - 48 * u, sw + 0.6, 8 * u);
}
ctx.fillStyle = '#5c4030';
ctx.fillRect(ax + 2, base - 48 * u, 3, 48 * u);
ctx.fillRect(awningRight - 5, base - 48 * u, 3, 48 * u);
ctx.fillStyle = '#1c2b24';
const swid = Math.min(92 * u, awningRight - ax - 16);
pathRound(ctx, ax + 10, base - 44 * u, swid, 14 * u, 3);
ctx.fill();
ctx.fillStyle = '#fff8ec';
ctx.font = '700 ' + Math.max(8, 9 * u) + 'px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('PLATFORM 1', ax + 10 + swid / 2, base - 44 * u + 7 * u);
const count = Math.max(2, Math.min(5, Math.floor((awningRight - ax - 20) / (30 * u))));
for (let i = 0; i < count; i++) {
const span = awningRight - ax - 24;
const px = ax + 16 + (span * (i + 0.5)) / count;
drawPerson(ctx, px, base - 2, 32 * u, {
shirt: WAGONS[i % WAGONS.length][0],
pants: '#2c3e50',
skin: SKINS[i % SKINS.length],
hat: HATS[(i + 1) % HATS.length],
bag: '#6d4c41'
}, now, true);
}
}
}
function cShadowRect(x, y, w, h) {
ctx.fillStyle = 'rgba(0,0,0,0.08)';
ctx.fillRect(x, y, w, h);
}
function drawScenery() {
const ground = Math.min(cssH - 8, oy + boardH + 6);
if (ox > 56) {
drawTree(ctx, Math.max(24, ox * 0.32), ground - 8, Math.min(150, ox * 0.62));
if (ox > 130) drawTree(ctx, ox * 0.58, ground + 4, Math.min(100, ox * 0.4));
if (ox > 90) drawSignpost(ctx, Math.min(ox - 28, ox * 0.78), oy + Math.min(70, boardH * 0.25));
}
const right = cssW - (ox + boardW);
if (right > 48) {
drawSignal(ctx, ox + boardW + Math.min(28, right * 0.28), oy + Math.min(78, boardH * 0.35), mode !== 'dead');
if (right > 110) drawWaterTower(ctx, cssW - Math.min(64, right * 0.42), ground);
if (right > 80) drawTree(ctx, ox + boardW + right * 0.62, ground - 4, Math.min(120, right * 0.55));
}
const spots = [[0.05,0.74],[0.09,0.84],[0.14,0.92],[0.03,0.9],[0.93,0.72],[0.97,0.82],[0.9,0.9],[0.84,0.78],[0.28,0.96],[0.62,0.95]];
const cols = ['#ff6b8a', '#ffe066', '#fff', '#c084fc', '#ff9f43'];
for (let i = 0; i < spots.length; i++) {
const x = spots[i][0] * cssW;
const y = spots[i][1] * cssH;
if (x > ox - 8 && x < ox + boardW + 8 && y > oy - 8 && y < oy + boardH + 8) continue;
drawFlower(ctx, x, y, cols[i % cols.length]);
}
}
function drawBackdrop(now) {
const H = horizon > 8 ? horizon : Math.max(40, cssH * 0.2);
const sky = ctx.createLinearGradient(0, 0, 0, H + 30);
sky.addColorStop(0, '#7ec8e3');
sky.addColorStop(1, '#f8e7c3');
ctx.fillStyle = sky;
ctx.fillRect(0, 0, cssW, cssH);
drawSun(ctx, cssW * 0.84, Math.max(28, H * 0.32), Math.max(16, Math.min(28, cssW * 0.025)));
const clouds = [
{p: 0.05, y: 0.28, s: 1, v: 12},
{p: 0.32, y: 0.16, s: 0.72, v: 8},
{p: 0.58, y: 0.36, s: 1.1, v: 16},
{p: 0.8, y: 0.18, s: 0.6, v: 10}
];
for (let i = 0; i < clouds.length; i++) {
const cl = clouds[i];
const x = ((cl.p * cssW + now * 0.012 * cl.v) % (cssW + 160)) - 80;
drawCloud(ctx, x, H * cl.y, cl.s);
}
const flap = Math.sin(now / 180) * 3;
drawBird(ctx, cssW * 0.22 + (now * 0.02) % 80, H * 0.22, flap);
drawBird(ctx, cssW * 0.55, H * 0.14, -flap);
drawBird(ctx, cssW * 0.7, H * 0.3, flap * 0.6);
ctx.fillStyle = '#8fbf84';
ctx.beginPath();
ctx.moveTo(0, H - 8);
for (let x = 0; x <= cssW; x += 10) ctx.lineTo(x, H - 18 + Math.sin(x * 0.01) * 10);
ctx.lineTo(cssW, cssH);
ctx.lineTo(0, cssH);
ctx.fill();
ctx.fillStyle = '#5ea84a';
ctx.beginPath();
ctx.moveTo(0, H + 6);
for (let x = 0; x <= cssW; x += 12) ctx.lineTo(x, H + Math.sin(x * 0.015 + 1) * 6);
ctx.lineTo(cssW, cssH);
ctx.lineTo(0, cssH);
ctx.fill();
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, cssW, Math.max(8, oy - 2));
ctx.clip();
drawStation(now, H);
ctx.restore();
if (cs >= 6) drawScenery();
}
function drawYard() {
ctx.save();
pathRound(ctx, ox, oy, boardW, boardH, 8);
ctx.clip();
const g = ctx.createLinearGradient(ox, oy, ox, oy + boardH);
g.addColorStop(0, '#ead7ae');
g.addColorStop(1, '#d8c294');
ctx.fillStyle = g;
ctx.fillRect(ox, oy, boardW, boardH);
ctx.strokeStyle = 'rgba(90,70,40,0.08)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 1; x < COLS; x++) {
ctx.moveTo(ox + x * cs + 0.5, oy);
ctx.lineTo(ox + x * cs + 0.5, oy + boardH);
}
for (let y = 1; y < ROWS; y++) {
ctx.moveTo(ox, oy + y * cs + 0.5);
ctx.lineTo(ox + boardW, oy + y * cs + 0.5);
}
ctx.stroke();
ctx.fillStyle = 'rgba(90,60,20,0.035)';
for (let i = 0; i < 80; i++) {
const sx = ox + ((i * 97) % boardW);
const sy = oy + ((i * 57) % boardH);
ctx.fillRect(sx, sy, 2, 2);
}
ctx.restore();
ctx.lineWidth = Math.max(8, cs * 0.16);
ctx.strokeStyle = '#6b3e26';
ctx.strokeRect(ox - 1, oy - 1, boardW + 2, boardH + 2);
ctx.lineWidth = 3;
ctx.strokeStyle = '#e6c15a';
ctx.strokeRect(ox + 3, oy + 3, boardW - 6, boardH - 6);
if (oy > 18 && cs >= 18) {
ctx.font = '700 ' + Math.max(10, Math.min(13, cs * 0.24)) + 'px Georgia, serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const label = 'BOARDING YARD';
const tw = ctx.measureText(label).width;
ctx.fillStyle = '#6b3e26';
pathRound(ctx, ox + boardW / 2 - tw / 2 - 8, oy - 15, tw + 16, 16, 4);
ctx.fill();
ctx.fillStyle = '#fff8ec';
ctx.fillText(label, ox + boardW / 2, oy - 7);
}
}
function renderTrain(now, t) {
const spin = mode === 'dead' ? wheelFreeze : now / 150;
for (let i = 0; i < snake.length; i++) {
const p = segPos(i, t);
const face = faceOf(i, t);
withFace(ctx, p.x, p.y, face, c => drawTrack(c, cs));
}
drawYardPassenger(now);
for (let i = 0; i < snake.length - 1; i++) drawCoupling(segPos(i, t), segPos(i + 1, t));
for (let i = 0; i < snake.length; i++) {
const p = segPos(i, t);
drawGroundShadow(p.x, p.y + bobY(i, now), cs * (i === 0 ? 0.4 : 0.32));
}
for (let i = snake.length - 1; i >= 1; i--) {
const p = segPos(i, t);
const face = faceOf(i, t);
const by = bobY(i, now);
const ci = wagonColors[i - 1];
withFace(ctx, p.x, p.y + by, face, c => drawWagon(c, cs, ci, spin));
}
const hp = segPos(0, t);
const hf = faceOf(0, t);
const hbob = bobY(0, now);
withFace(ctx, hp.x, hp.y + hbob, hf, c => drawLoco(c, cs, spin));
const front = localToWorld(hp.x, hp.y + hbob, hf, cs * NOSE_X, 0);
const glowG = ctx.createRadialGradient(front.x, front.y, 0, front.x, front.y, cs * 0.4);
glowG.addColorStop(0, 'rgba(255,220,120,0.5)');
glowG.addColorStop(1, 'rgba(255,220,120,0)');
ctx.fillStyle = glowG;
ctx.beginPath();
ctx.arc(front.x, front.y, cs * 0.4, 0, Math.PI * 2);
ctx.fill();
const ch = localToWorld(hp.x, hp.y + hbob, hf, cs * CHIM_X, cs * CHIM_Y);
if (!steamBooted) {
steamBooted = true;
for (let i = 0; i < 14; i++) particles.push(makeSteam(ch.x, ch.y, 0.25 + Math.random() * 0.75));
}
if (mode !== 'dead' && Math.random() < 0.55) particles.push(makeSteam(ch.x, ch.y, 1));
}
function drawParticleLayer() {
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
const a = clamp(p.life / (p.max || 1), 0, 1);
if (p.type === 'steam') {
ctx.fillStyle = 'rgba(255,255,255,' + (a * 0.62) + ')';
ctx.beginPath();
ctx.arc(p.x, p.y, Math.max(0.5, p.r), 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(90,100,110,' + (a * 0.18) + ')';
ctx.lineWidth = 1;
ctx.stroke();
} else {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot || 0);
ctx.globalAlpha = a;
ctx.fillStyle = p.color || '#e6c15a';
ctx.fillRect(-(p.w || 4) / 2, -(p.h || 4) / 2, p.w || 4, p.h || 4);
ctx.restore();
}
}
}
function drawFloaterLayer() {
for (let i = 0; i < floaters.length; i++) {
const f = floaters[i];
const a = clamp(f.life, 0, 1);
ctx.save();
ctx.globalAlpha = a;
ctx.font = '700 ' + Math.max(13, Math.min(20, cs * 0.34)) + 'px Georgia, serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(28,22,16,0.75)';
ctx.strokeText(f.text, f.x, f.y);
ctx.fillStyle = '#fff4c8';
ctx.fillText(f.text, f.x, f.y);
ctx.restore();
}
}
function render(now, t) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cssW, cssH);
if (cssW < 8 || cssH < 8) return;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.globalAlpha = 1;
if (shake > 0.01) {
ctx.translate(Math.sin(now * 0.05) * shake * 6, Math.cos(now * 0.07) * shake * 4);
}
drawBackdrop(now);
if (cs >= 8 && snake.length) {
drawYard();
renderTrain(now, t);
}
drawParticleLayer();
drawFloaterLayer();
if (glow > 0 && cs >= 8) {
ctx.strokeStyle = 'rgba(230,193,90,' + glow + ')';
ctx.lineWidth = 6;
ctx.strokeRect(ox - 3, oy - 3, boardW + 6, boardH + 6);
}
if (flash > 0) {
ctx.fillStyle = 'rgba(170,32,24,' + (flash * 0.3) + ')';
ctx.fillRect(-30, -30, cssW + 60, cssH + 60);
}
}
function loop(now) {
const dtMs = Math.min(48, now - lastNow);
lastNow = now;
const dt = dtMs / 1000;
if (mode !== 'dead') {
acc += dtMs;
const interval = currentInterval();
if (acc >= interval) {
acc -= interval;
if (acc > interval) acc = interval * 0.4;
advance();
}
}
updateParticles(dt);
updateFloaters(dt);
if (shake > 0) shake = Math.max(0, shake - dt * 1.6);
if (flash > 0) flash = Math.max(0, flash - dt * 1.25);
if (glow > 0) glow = Math.max(0, glow - dt * 1.05);
const t = clamp(acc / Math.max(1, currentInterval()), 0, 1);
render(now, t);
requestAnimationFrame(loop);
}
function boot() {
resize();
startDemo();
lastNow = performance.now();
render(lastNow, 0);
requestAnimationFrame(loop);
}
window.addEventListener('resize', resize);
if (typeof ResizeObserver !== 'undefined') new ResizeObserver(resize).observe(stage);
window.addEventListener('keydown', e => {
if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Alt' || e.key === 'Meta') return;
const d = dirFromKey(e.key);
if (d || e.key === ' ' || e.key === 'Enter') e.preventDefault();
if (mode !== 'play') startPlay();
if (d) queueDir(d);
});
window.addEventListener('pointerdown', e => {
if (e.target.closest && e.target.closest('button')) return;
if (mode !== 'play') startPlay();
});
stage.addEventListener('pointerdown', e => {
if (e.target.closest && e.target.closest('button')) return;
e.preventDefault();
swipe = {x: e.clientX, y: e.clientY, id: e.pointerId};
}, {passive: false});
stage.addEventListener('pointerup', e => {
if (!swipe || swipe.id !== e.pointerId) return;
const dx = e.clientX - swipe.x;
const dy = e.clientY - swipe.y;
swipe = null;
if (Math.hypot(dx, dy) < 28) return;
if (mode !== 'play') startPlay();
if (Math.abs(dx) > Math.abs(dy)) queueDir({x: Math.sign(dx), y: 0});
else queueDir({x: 0, y: Math.sign(dy)});
});
stage.addEventListener('pointercancel', () => { swipe = null; });
document.querySelectorAll('.dpad button').forEach(btn => {
btn.addEventListener('pointerdown', e => {
e.preventDefault();
e.stopPropagation();
if (mode !== 'play') startPlay();
queueDir({x: Number(btn.dataset.x), y: Number(btn.dataset.y)});
}, {passive: false});
});
boot();
</script>
</body>
</html>Reasoning trace (89,526 tokens)
Planning a snake game where the snake is a little train with a locomotive and colorful wagons. Passengers will wait on platforms to be picked up.
Designing a single self-contained HTML file with inline canvas. The game will be fully self-playing with a start prompt. It must look good at 1280×800 and adapt to 375px. No external dependencies or localStorage. One fenced code block only.
The locomotive will have a smokestack, wheels, and cabin. Passengers are little people on platforms. Each pickup adds a new wagon and increases the score. The visual style is a warm railway illustration with tracks, grass, and platforms. A 20×15 grid will handle train movement.
Self-collision ends the game. Each passenger pickup grows the train and slightly increases speed. The locomotive will have a chimney and steam puffs. Wagons will connect with couplings and show windows and wheels. The demo AI will chase passengers while avoiding walls and the train body. The game will be visually polished for side-by-side comparison.
The background will feature a sky gradient, rolling hills, and a decorative station building. The play area will be a rounded grassy field with subtle grid lines. The locomotive will have a dark red body with gold trim, big wheels, a headlight, and a cowcatcher. Each wagon will be a different color and show passengers inside after pickup.
Passengers will be cute stick figures with colored clothing and a wave animation. Each platform will display a small "STOP" sign. The UI will show a title, score, collected count, and a high score stored in memory. The canvas will scale responsively with CSS flexbox and clamp-based fonts. The demo AI will use BFS pathfinding to reach passengers while avoiding the snake body. If no path exists, it will survive by following the tail or making a random safe move.
Game states are demo, playing, and game over. The demo AI will restart automatically if it dies. Controls will use arrow keys and WASD, plus an on-screen D-pad for mobile. The overlay handles keyboard focus. The HTML structure is being drafted.
The HTML structure is now fully sketched with a header, canvas stage, start overlay, game-over panel, and mobile D-pad. Cell size will be computed dynamically from the canvas dimensions. The drawing routines are being refined for a polished look.
The canvas will use a fixed logical resolution scaled to the display size for crispness. The board will be 18 columns by 12 rows. At 1280px the cell size will be about 50px. At 375px it will be about 20px. The locomotive will have a rounded front body. It will have a cabin. It will have a chimney. It will have two to three wheels. It will have a headlight glow.
Steam particles will animate in world space rather than on the grid. Each wagon will have a rounded body, a darker roof, two windows, two wheels, and a coupling bar to the next segment. Filled wagons will show passenger faces in their windows. The train will be drawn from tail to head so the locomotive stays on top. Each car will be oriented based on its movement direction relative to the segment behind it.
Passenger platforms will be wooden planks slightly larger than the grid cells. Each waiting figure will have a waving arm, a suitcase, and a unique outfit with different skin tones and hat colors. When a passenger is picked up, the game will burst hearts or tickets, play a steam whistle sound, and show a score popup. The entire canvas will serve as a countryside scene with the playfield framed by a dirt and grass border. A decorative station will be kept to the side to avoid cluttering the view.
The color palette is set: sky blue to pale cream, grass green, wood brown track borders, and cream paper with deep navy text and brass accents. The title will use Georgia or Palatino, while the UI stays in a system sans. Game ticks will start at 140 milliseconds and speed up to a 70 millisecond minimum as the score grows. Train segments will interpolate between grid cells for smooth movement. Steam and passenger wave animations will run on every frame.
The demo will begin with a five-segment train so it looks like a real locomotive immediately. A passenger will already be waiting on the platform. The AI will avoid collisions to keep the demo alive for the three-second screenshot. If the AI does die, the game will restart after a brief pause.
The game will use one active passenger on the track at a time, matching classic snake mechanics. A station platform along the edge will hold decorative waiting passengers. When a passenger is collected, a new one will appear at a random empty cell with a small platform beneath it.
The train will start with a locomotive and three wagons. Each new passenger pickup adds one segment, cycling through a fixed color palette for the wagons.
The layout will use a 70-pixel header and 50-pixel footer, with the canvas filling the remaining space. On a 375×800 portrait phone the canvas will be roughly square. The canvas will be sized in JavaScript on resize.
The game will have three states: demo, play, and dead. Any keypress or click on the stage will switch from demo to play or restart from dead. During active play, clicks will not restart the game. Direction changes will come from keyboard arrows, WASD, or the on-screen D-pad.
Swipe gestures will control the train during play. Arrow keys will be intercepted to prevent browser scrolling. Steam particles will be emitted from the locomotive’s chimney each frame. Score popups will float upward as "+1" labels. The whistle effect will be purely visual to avoid audio autoplay issues. The locomotive drawing is being detailed next.
The locomotive will be drawn at the interpolated center of its grid cell, rotated to face the train’s current direction. Each wagon will be slightly shorter and connected by a dark coupling bar. Shadows will be drawn beneath the train for depth. A platform will be drawn under each passenger.
The station will be drawn as a background element outside the play grid. It will feature a small building with a clock and decorative passengers standing on the platform. The background will also include slowly moving clouds and distant hills. This makes the waiting passengers visible before the player reads the mechanic.
The grid will be 16 columns by 11 rows. Cell size will be computed dynamically from the available canvas area. The scene will be drawn in layers: sky gradient, hills, clouds, a station banner, and the playfield. The station banner may be moved into the HTML page margins so the canvas only renders the game.
The HTML header will use a train emoji and a "Platform Express" title. It will also show live score and passenger count stats.
The snake will not wrap around the board. Reversing into the body is blocked, and the demo AI will never turn back on itself. Food will not spawn on top of the snake. When the train eats a passenger, the tail stays in place so the train grows. The demo AI will use a BFS pathfinder to find the shortest route to the next passenger.
The BFS pathfinder will treat every snake segment except the tail as an obstacle, since the tail moves away before the next step. It returns the first direction from the head to the food, or null if no path exists.
The demo AI will fall back to the safest available move if the BFS finds no path, preferring open space or following the tail. The game will run at roughly five to six moves per second for a cute pace, with the demo loop set to 160 milliseconds.
The player train will match the demo speed at first, then accelerate slightly as the score grows. Smooth interpolation at 160 millisecond intervals keeps the movement fluid. Horn puffs and track sparks will be added for extra polish. Five distinct passenger outfits with different skin tones, hats, and colors are defined. Each waiting figure will be drawn as a shadow ellipse, legs, body, and head.
Passenger figures will wave with a sine-wave arm and occasionally show a speech bubble. Each platform will be a wooden slab with a post and a round sign. When a passenger is picked up, confetti will burst in that passenger’s shirt color. On game over the train will stop and display a “Derailed!” message with the score; the demo will restart automatically after 1.2 seconds. The initial train will sit at the left-center of the grid facing right.
The locomotive will carry a nameplate reading "No. 1" or "PUFF". Each wagon will take a color from a fixed palette based on its segment index. The visual style is shifting toward a bright, cheerful vintage travel poster with a cream background, deep teal accents, coral, and mustard yellow.
The page background will be cream paper, header text deep navy, and locomotive red. Cards will be white with soft shadows. The canvas will stay illustrated rather than flat. Drawing functions will use relative units so they remain readable at small cell sizes. The grid is being reduced to 14 columns by 10 rows to keep cells large enough on narrow mobile screens.
The grid is now 15 columns by 10 rows. At a 1280×800 viewport the board will be 900×600 and centered, leaving margins for the station scene. On a 375-pixel phone the cell size will be about 24 pixels.
The canvas will reserve 16 percent of its height at the top for the sky and station, 4 percent at the bottom for the grass edge, and 3 percent on each side for margins. The board will be sized from that available space.
The station will include a wooden "PLATFORM" sign. Score labels will float upward in canvas. The entire game will be a single strict-mode script.
The D-pad buttons will be compact HTML overlays that always stay visible so the game starts immediately on first paint. A window click handler will ignore the D-pad and trigger play or restart from demo or dead states. Keyboard input will be captured via a window keydown listener.
Arrow keys and space will be intercepted to prevent browser scrolling. The keydown handler will start play and set the initial direction in the same event. When the train dies, the overlay will display "Derailed! Score 12 — Click or press a key to start". The demo overlay stays visible until the player interacts.
The overlay will sit at the bottom center of the canvas as a translucent dark card so the train remains fully visible behind it. The scrim will use a dark brown background with white text and a small padding to keep the start message clearly readable.
The overlay will use a blurred translucent background with a soft gold border so clicks pass through to the stage. The locomotive will be drawn with a front cowcatcher, boiler cylinder, cabin window, roof, chimney with red band, steam, driving wheels with a connecting rod that rotates over time, and a glowing headlamp.
The locomotive will carry a number plate. Each wagon will have cream window frames, two windows showing the sky or a passenger silhouette, a door, and rotating wheels. The wheels will spin based on distance traveled to look alive. The new tail wagon will adopt the color of the passenger just picked up. The snake will be stored as a position array with a parallel color array, and the locomotive head will have no color. The rendering order will be scene, grass, then grid marks.
The rendering order is now fixed: scene, grass, platform, couplings, wagons, locomotive, steam, particles, and score floaters. Track lines will be omitted to avoid confusion. A tick-based interpolation will blend the previous and current snake positions so the train moves smoothly between grid cells.
The interpolation formula blends the previous and current snake positions using a tick value between 0 and 1. When the train grows, the old tail position is duplicated in the previous array so the new segment does not snap from the origin. The step function checks for collisions by testing the head against the food and against every body segment except the tail, which vacates the cell before the next move.
The interpolation logic is being simplified: no extra point needs to be pushed into the previous array when the snake grows, because the fallback to the current position handles the new segment cleanly. Each car will face the direction of motion, pointing from the segment behind it toward its own interpolated position.
The heading function now uses the previous and current positions of each segment to determine direction. For the tail, it falls back to the previous and current positions of that segment, since no segment sits behind it.
The heading function now falls back to the raw movement vector when a segment is nearly stationary. Corner wagons may flip direction mid-interpolation, which is acceptable for an abrupt snake turn. A capsule-shaped train along the interpolated polyline would look smoother.
Each car will be a rounded rectangle slightly smaller than the cell, with a 15 percent gap between them to show the couplings. The wheels will rotate based on elapsed time multiplied by speed. A local-to-world coordinate helper is being drafted so the chimney’s steam can be positioned correctly after the locomotive rotates.
The locomotive will use a deep crimson boiler, red cabin, brass trim, and black wheels. Each car will cast a soft shadow beneath it. The wagon colors are now fixed as blue, yellow, and green.
The color palette now includes coral, purple, orange, teal, and pink. Food will spawn at least one cell away from the train head. The score will update in the DOM each tick and persist in a memory variable. The D-pad buttons will be 48 pixels wide for mobile touch targets. The footer will wrap on small screens. The title will use Iowan Old Style or Palatino.
UI text will use Avenir Next or Segoe UI. Clouds will animate in canvas. The station will be a brick building with arched windows, a clock, awning stripes, a platform with waving passengers, and an "ALL ABOARD" sign. If the top margin is too small, the station will scale down or be skipped. A few simple bird shapes may fly across the sky. Collecting a passenger will trigger a whistle steam ring.
The demo AI will be visible within three seconds. The game loop starts immediately so the first tick at 400 ms places the train before the screenshot. Steam and waving animations will begin on frame one. A path-based rounded rectangle helper will be used as a fallback for ctx.roundRect.
The roundRect helper is now fully implemented using arcTo. When the train dies, the locomotive will tilt slightly, a smoke puff will appear, and a "DERAILED" banner will display. If the player is in demo mode, the game will auto-restart; otherwise it will wait for input. Speed will increase as the score grows, with the interval dropping from 150 ms down to a minimum of 80 ms. The next passenger's name may also be shown.
A random passenger name from a short list will appear in the UI as the next target. A visual keyboard hint will sit on the board edge. The game will be written as a polished HTML file of roughly 600 to 900 lines. When the player starts, the demo AI state will be reset so the train does not inherit a doomed position.
The D-pad will start the game and set the direction when pressed during the demo. A queueDir helper prevents 180-degree reversals by ignoring conflicting inputs. The demo mode will ignore queued directions until the player explicitly starts playing. Touch swipes on the canvas will also be handled via pointer events.
A swipe on the canvas will start the game and set the direction based on the swipe angle. A short tap will also start the game. The startPlay function is idempotent so it will not reset the game if the player is already playing.
A visual "demo" chip will mark the corner so the demo state is obvious. The overlay text will use a large white-on-dark-brown font and read "Click or press a key to start." The locomotive drawing function is being drafted with a local coordinate system scaled to half the cell size.
The locomotive will be drawn in local coordinates facing right, with its shadow cast in world space before rotation so it stays vertical on the ground. The shadow will be drawn separately at a fixed offset below the locomotive.
The locomotive is being mapped to exact local coordinates: boiler from x=-0.42 to 0.28, cabin from x=-0.46 to -0.12, chimney at x=-0.02, cowcatcher at x=0.28 to 0.50, and wheels at y=0.20. The wagon body spans x=-0.40 to 0.40 and y=-0.32 to 0.18, with wheels at ±0.22 and windows at -0.18 and 0.08. Couplings will be drawn as lines connecting each car's rear to the next car's front.
A short bar at the rear of each car will visually connect the train segments. Passengers will be drawn at the cell center scaled to the cell size. A bell will swing on the locomotive. Particle effects will include steam, confetti, and spark, limited to eighty active particles. The AI will choose its next move by scanning the grid for open cells, treating the tail as vacated unless it is adjacent to the head.
The AI will first try to reach food using BFS. If that fails, it will pick a random safe direction, preferring the one that leaves the most open space ahead.
The AI will sort its safe directions by the amount of open space ahead, then pick the best one. A flood-fill check is fast enough for a small board. The AI runs every tick during the demo. A greedy BFS should keep the short snake alive for the full demo, and the game will restart if the train dies.
A wooden sleepers border with brass corners will frame the board. The inner yard will use pale gravel with subtle procedural noise, clipped to the board edges. The border will be dark wood with a gold inner line.
A soft vignette will be added to the background. The station clock hands will track real time using `new Date()`. The game will display a speed indicator and a line name such as "The Meadow Line". The layout will be locked to a single viewport height with `overflow-x: hidden` to prevent horizontal scrollbars.
The header will use a flex row with clamped font sizes so the title and stats stay readable on small screens. The footer will be 56 pixels tall, and the stage will fill the remaining viewport height. The D-pad will sit on the side of the footer, and instructions will truncate on narrow viewports.
The header will stay a single compact row to avoid clipping on narrow screens. The title will be "Meadow Line" with a small subtitle. Stats will appear as three compact pills. A CSS train icon made of divs is also being considered.
The header will use a 🚂 emoji for instant visual charm. The canvas will be sized and drawn synchronously during initialization so the first frame appears immediately, before the browser's next animation frame. The canvas will also scale to devicePixelRatio for crisp rendering.
The canvas transform will be reset each frame to apply devicePixelRatio scaling. A ResizeObserver will handle stage resizes, and window resize will be supported too. localStorage will not be used. A tutorial arrow on the canvas was considered but rejected to avoid clutter, since the passenger already waves and shows a "!" bubble.
If the board fills up, the game will declare a victory with the message "Full train!". Food will spawn only on empty cells, and each passenger will carry a suitcase and a raised hand. The passenger will bob up and down slightly as they wait on the platform.
The snake variable will be renamed from `curr` to `snake` for clarity. The game board will be 15 by 10 cells. Each passenger will carry a small ticket. The step timing logic is being drafted.
The game loop measures elapsed time with `performance.now()` and accumulates it into an accumulator. The demo mode advances every 170 ms, while the player mode speeds up from 158 ms down to 78 ms as the score rises. The accumulator is capped at two steps to prevent the snake from spiraling out of control. During the demo, the AI picks the next direction each tick and applies it immediately. When the train dies, the accumulator resets to one so the death animation plays at normal speed.
A `currentInterval` variable will keep the render timing in sync with the step interval. When the train dies, the tick will freeze at one so the locomotive stays on the grid, and a shake effect may be added. The `die()` function will show a death overlay and restart the demo after a short delay if the game was in demo mode. If the player clicks during that delay, the game will start playing immediately and cancel the timer.
The death overlay will read "Derailed! Score 8" with a restart prompt. The best score HUD updates on death. A visual whistle effect will be added. Cloud positions will be precomputed. Bird sprites are being drafted.
Bird sprites are placed in the sky region with varying sizes and speeds. The top band will show a station house with a clock and "MEADOW" sign, a platform with four people and luggage, and an awning. If the band is too short, the station will be simplified. The canvas background will extend under the board so the yard is part of the scene, while the sky stays confined to the top band.
Side margins will show a tree and a lamp post on the left, and a railway signal with a green light on the right. These decorations fill extra space when the board is centered. They will be skipped if the margin is narrower than thirty pixels. A `seedSteam` function will initialize steam particles so the locomotive shows smoke immediately on the first frame.
The passenger will be placed immediately in the reset function so it is visible from the first frame. The initial snake will be created before food spawns, and the first food will be biased toward the right side so it sits in front of the train for a better screenshot composition.
The initial snake will be five segments long, starting at x=6 so the locomotive sits just ahead of the passenger. The food spawn logic will bias toward cells directly ahead of the train when the `preferAhead` flag is set.
The snake will be five segments long, with the locomotive at the head and the first four wagons colored in a fixed palette. Each wagon will show a little face in its window to look occupied. When the train eats a passenger, it appends a new colored segment at the tail instead of popping the old tail.
The snake segments are being restructured into objects with x, y, and color fields. The locomotive head keeps a null color, while each wagon carries its own color. This cleanly separates the engine from the train cars and avoids shifting color indices when the tail is removed.
The snake will be stored as a flat array of segment objects. The locomotive head keeps a null color. Each wagon carries its own color. On a normal move the head unshifts into the new cell and the tail pops off. The remaining segments shift forward. Their colors stay locked to their original indices.
The flat-array snake model is proving awkward. When the train moves without eating, the tail pops and the neck shifts forward, but the color array stays fixed. When the train eats, the snake grows at the head and every color index shifts by one. This makes it hard to keep each wagon's color locked to the right segment.
The color array will be indexed by body segment order, with index 0 matching the first wagon. Because unshift and pop preserve the relative order of the remaining segments, each wagon keeps its color locked to the same position in the train.
The color array will be indexed by body segment order, with index 0 matching the first wagon. Because unshift and pop preserve the relative order of the remaining segments, each wagon keeps its color locked to the same position in the train.
The locomotive headlight will glow a soft yellow cone even in daytime. Each eaten passenger will spawn a score floater that rises from the food cell. The board will display a "YARD" nameplate. The UI will use a paper-textured CSS gradient and ticket-stub-style score stats.
Stat cards will use simple cream backgrounds with dashed borders and soft shadows to look like ticket stubs. The header will be a deep green railway carriage with cream text.
The footer will use a dark green gradient so the sky-blue canvas scene stands out like a framed illustration. The page background is a deep forest green. The stage has a ten-pixel margin, sixteen-pixel rounded corners, and a soft shadow. A gold inset border frames the canvas like a poster. The overlay sits inside this frame.
A "LIVE DEMO" pill will sit in the top-left corner during demo mode. The dpad buttons and canvas will block scrolling with `touch-action: none` and `preventDefault` on pointer events. A custom `roundRect` fallback will replace the browser API to avoid console errors. The AI pathfinding is being checked to prevent looping behavior.
BFS will route the train directly to food, and it will handle U-turns when the target is behind. A 300ms delay before the first move will let the initial composition settle. Between steps, the snake will interpolate from the previous positions toward the new ones.
When the train crashes, the interpolation will freeze at the collision cell rather than snapping to the wall. A screen shake effect will be added to the death sequence.
A growing ring will appear when a passenger boards the train. The overlay will use a max-width of calc(100% - 24px) and max-content width so the pill does not overflow on narrow screens. The entire response will be wrapped in a single HTML code block.
The start overlay will use clamp-based font sizing so the prompt stays readable on narrow screens. User selection will be disabled to prevent text highlighting during rapid clicks. The locomotive drawing code is being refined to look crisp at both small and large cell sizes.
The locomotive will use bold, thick shapes at small cell sizes so thin lines do not disappear. At 24 pixels the design keeps a large cabin, boiler, wheels, chimney, cowcatcher, window, and gold stripe. At 60 pixels finer details like rivets, a second window, bell, and number plate will appear. The drawing code will always render these extra elements but they will scale down to remain readable.
The locomotive drawing is being corrected to match a standard side-view steam engine. The cabin will sit at the rear, the boiler will extend forward, and the chimney, cowcatcher, and wheels will align with the front of the locomotive.
The locomotive is being mapped to exact local coordinates so it faces right. The cabin sits at the rear, the boiler extends forward, and the chimney, cowcatcher, headlight, and wheels are placed at precise offsets. A side view of the wagon is also being sketched.
The locomotive wheels will spin using a rotating angle based on elapsed time. A simple black hub with a gold rim and offset spoke lines will show motion without looking messy.
The passenger drawing function is being implemented. Each figure bobs gently in place and casts a soft shadow beneath it. The legs are drawn in the passenger's outfit color.
The passenger drawing is being refined with rounded body shapes and a waving arm that pulses when a passenger boards. The arm stroke uses the passenger's skin tone and a slight offset to show motion.
Passenger smiles and hats are being added to the drawing code. A platform function is being created with a dark brown rounded rectangle and subtle plank lines.
The platform now includes a yellow line and a pole with a red station sign.
Passenger name labels will float above each figure as small pill-shaped text, hidden when cells are too small. A permanent "PLATFORM 1" sign will sit in the top scene. The footer will display on-screen steering instructions. The full file is now being written with syntax checks.
The dpad will use arrow buttons with ARIA labels. The demo mode key handler was verified: startPlay sets the initial direction to right, then the key press queues the intended direction, so the train always begins moving right.
Space will be blocked from scrolling. The high score label will read "Best". Aboard count will display the number of wagons, since each one carries passengers. Score will increase by 10 points per passenger to feel more satisfying. Game speed will scale with the number of passengers collected rather than total points.
A wooden-tie track will be drawn beneath each train segment so the snake reads as a locomotive rather than a caterpillar. The HUD will show Score, Aboard count, and Best. The game will remain simple without combo multipliers.
The wooden-tie track will be drawn beneath each train segment so the snake reads as a locomotive rather than a caterpillar. The locomotive wheels will spin using a rotating angle based on elapsed time. A simple black hub with a gold rim and offset spoke lines will show motion without looking messy.
The passenger drawing function is being implemented. Each figure bobs gently in place and casts a soft shadow beneath it. The legs are drawn in the passenger's outfit color.
The passenger drawing is being refined with rounded body shapes and a waving arm that pulses when a passenger boards. The arm stroke uses the passenger's skin tone and a slight offset to show motion.
Passenger smiles and hats are being added to the drawing code. A platform function is being created with a dark brown rounded rectangle and subtle plank lines.
The platform now includes a yellow line and a pole with a red station sign.
Passenger name labels will float above each figure as small pill-shaped text, hidden when cells are too
The render order is set: yard, wooden ties, platforms, couplings, wagons, locomotive, then passengers. A subtle shadow pass will be added. A precomputed noise-dot texture will serve as a gravel background.
A warm sun will be added to the sky. The locomotive cabin will display a railway name and a number plate. Smoke will emit continuously from the locomotive. The demo overlay will sit at the bottom so it does not cover the train. The screenshot will show the station, train, passenger, and call-to-action in a single frame.
A gentle pulse animation will be added to the start overlay to make it feel inviting. A brief win state will be handled for rare completions. The code will stay clean with minimal comments. No external fonts or URLs are used. The canvas will fill the stage without overflowing.
The canvas will use exact pixel dimensions for its width and height attributes, set to the stage size multiplied by the device pixel ratio. This prevents blur when the canvas is scaled to fill the stage via CSS.
The resize function will guard against zero dimensions by retrying on the next animation frame if the stage is too small. The stage element will receive a tabindex so keyboard input works correctly after the user clicks to start the game.
Opposite-direction key presses during the same tick will be handled by letting the last key pressed win. An iterative flood-fill algorithm is being written to find open space for placing passengers, avoiding recursion to prevent stack overflow.
The AI will pick moves that leave the most open space. It will also prefer continuing straight to avoid zigzagging. If a tight path is found, the AI will still use it.
The BFS pathfinding will exclude the snake's tail from blocked cells, since the tail moves after each step. This may occasionally let the AI pass through cells the body will soon occupy, but it is acceptable for a demo. A slightly better chase strategy is being considered for improved quality.
The AI will prioritize moving directly toward food if it is an unoccupied neighbor. Otherwise it will use BFS to find a path, and if no path exists it will fall back to survival moves. The snake's tail will remain unblocked so the pathfinding does not need to account for its future position. Reversing direction will be avoided. The implementation is now ready to be written.
White diagonal gloss will be added to the wagons for a glassy look. A conductor face with a cap will sit in the locomotive cabin window. The overlay DOM will use a chip element for the demo label.
The overlay DOM will hold a chip label, a main message, and a submessage. The chip will read DEMO RUN during the demo and DERAIED during a crash. The main message will always prompt the user to start the game. A setOverlay function will toggle the overlay and update its text.
The overlay will use a `.show` CSS class instead of the `hidden` attribute to avoid layout conflicts. The initial HTML already contains the demo message so the screen is ready before JavaScript runs, preventing any flash of blank content.
A CSS pop animation will flash the score header whenever the score changes. Passengers will also be drawn on the sky band platforms as decorative figures that wave, so the waiting passengers are visible in the background rather than only during gameplay.
Passengers will be drawn smaller and placed above the yard fence in the station scene. A fence will separate the station from the yard. The canvas will be split into a sky and station band at the top, and a game board below. The band height will scale with stage height between 36 and 110 pixels. The station drawing will use this band height as its ground line.
The station drawing will scale to fit the band height using a factor `u = bandH / 110`. At full height the building is 70 pixels tall and 120 pixels wide, with a clock, awning, and platform ground near the baseline. At half height the figures remain recognizable blobs. Clouds will float above the building in the sky. The game board will sit below the band with a small gap.
The background is being simplified to avoid clutter. Instead of drawing trees and a full station scene, the canvas will use a sky gradient across the top and a clean horizon line. The yard will sit as a plain inset rectangle on the grass below. This keeps the visual focus on the train and passengers.
The background will feature a warm sky gradient, distant hills, and a small platform with waiting passengers above the yard. The game board will sit centered below with a thick wooden frame, inner grass, and corner lamp posts. A complete color palette has been chosen, covering sky tones, grass, yard gravel, wood, gold, station brick, and cream.
A subtle vignette overlay will be added at the end to give the finished scene a polished look without darkening the train. The directional angle constants are being defined for segment positioning.
Smooth rotation via atan2 will replace the four-way heading snap. This prevents sudden visual pops when the train turns corners.
Wheels and bodies will rotate using the smooth atan2 angle. Couplings will be drawn as dark lines between consecutive segment centers, with small rings at each joint. The cars will be slightly smaller than the gap so the couplings remain visible.
Car length will be clamped to the segment gap so wagons do not overlap during tight corners. A roof highlight will be added to the locomotive. The code is now being written.
The game title will be "Meadow Line" with the tagline "A little train snake". A passenger name will appear in the footer, such as "Waiting on the platform: Mina". Touch scrolling will be disabled on the body to keep the game controls isolated.
The footer will use a flex layout with a gap and padding. The dpad will be a 3×2 grid of 44×36 pixel buttons, arranged in a classic cross shape. The footer height will be 72 pixels plus text padding.
The footer will be about 100 pixels tall. The stage will be roughly 624 pixels high.
The footer will wrap its contents so the cross dpad fits beside the text on narrow screens. A keyboard hint will be added to the UI. The train will emit a few steam rings when the game starts. A score floater and a "toot!" text will appear when the snake collects a passenger.
The "toot!" text will appear only when the snake collects a passenger, not during the demo. The code will be organized into constants, DOM references, state, passenger data, layout, game logic, input, particles, drawing helpers, and the main loop. A seeded random will keep the background specks stable. The station clock will show real time. The overlay will be hidden with CSS rather than the hidden attribute to avoid pointer-events bugs.
The overlay will use `pointer-events: none` so clicks pass through to the stage. A stage pointer listener will start the game on tap and handle swipe direction on release. The dpad will sit in the footer outside the stage to avoid duplicate start events.
A running flag will guard the main loop. The draw routine will skip rendering if cell size is zero to avoid NaN.
A 50-millisecond cap on frame delta will prevent the game from freezing when the tab regains focus after being backgrounded. The step accumulator will be reset to zero only when it exceeds half the interval, ensuring the snake advances by exactly one step per frame.
The accumulator will be reset to zero after each step, letting the snake catch up on its own if the frame delta spikes. The yard border will flash gold briefly when a passenger is picked up. The initial passenger name will be set in the HTML as a placeholder until JavaScript runs.
The canvas will use a CSS gradient background so the frame is visible before JavaScript runs. The doStep function is being drafted now.
The `doStep` function is being drafted. It applies the AI move or the queued direction to the snake head. It checks for collisions with the board edges and with the snake body. It saves the previous snake state and prepends the new head position. It also sets up the logic for when the snake eats a passenger.
The `doStep` function now handles passenger collection. It adds the passenger's color to the train, updates the score and best record, spawns a burst of particles, and creates a floating score and name text. It also resets the yard border glow and spawns a new passenger. When the snake does not eat, it simply shifts the head and trims the tail. The previous snake array is kept one cell longer than the current snake so the segment positions can be interpolated correctly.
A bug was found in the segment interpolation logic. When the snake eats a passenger, the new head is prepended without popping the tail, so the previous snake array no longer aligns with the new one. This breaks the smooth movement of wagons behind the locomotive. The logic needs to be fixed so the first wagon correctly follows the old head position after a meal.
The interpolation logic is actually correct as written. Because unshift shifts every index by one and pop trims the tail, the previous snake array still aligns with the new one. Each segment moves to the position the previous segment occupied, so wagons follow the locomotive smoothly after a meal. No fix is needed.
The snake will freeze mid-interpolation when it dies, and a shake effect will accompany the crash. The accumulator will not reset on death so the train stays at its last valid position. The AI move function will return a direction vector. The wooden stage frame will serve as the wall, and the snake will die if it hits the edge.
A brief red flash overlay will appear when the train dies. A Web Audio whistle will play on pickup and game start, but only after the user clicks to satisfy autoplay policies; the demo mode will stay silent.
The `toot` function is being implemented with Web Audio. It creates a triangle-wave oscillator and a gain node. The frequency ramps up from 520 Hz to 728 Hz over 0.15 seconds. The volume rises from near silence to 0.08 and fades out over 0.35 seconds. The sound only plays in play mode.
The `toot` function now connects the oscillator to the gain node and schedules it to stop after 0.36 seconds. The AudioContext will be resumed on game start, and any promise rejection is caught so the game never crashes from blocked audio. The sound is purely optional; the visuals remain fully functional even without it.
Audio will be removed entirely to guarantee zero console errors. The script will be placed at the bottom of the page for an immediate draw. ARIA labels will be added for accessibility.
The train cars will be 0.82 times the cell size, leaving a 0.18 gap between them so couplings stay visible. The drawing order for rotated cars was checked to prevent overlap during turns.
During turns, consecutive train cars can overlap because the segment ahead moves while the segment behind is still moving in its old direction. The fix is to measure the actual distance between each car and the next one, then scale the car length to fit that gap so they never collide.
The locomotive will be slightly longer than the other cars, capped at 92% of the cell size or 85% of the gap to the next segment. The cowcatcher will sit inside the car length so it does not clip into the next cell. The layout is being planned for a 1280×800 viewport, with the header and footer fixed and the stage filling the remaining space.
The CSS layout is being finalized. The stage wrapper uses flex to fill the viewport with safe padding. The stage itself is rounded and clipped. The html and body elements are set to 100% height so the iframe scales correctly.
A meta viewport tag will be added for 375-pixel adaptation. The game will use a single life with restart instead of multiple lives. Particle and floating score caps will be added. A shared `drawPerson` function will render both the train passengers and the decorative station figures using the same color palette.
Station figures will be positioned relative to the scene rather than locked to the grid. A cute luggage cart will sit near the platform. The response will contain only the code block. The layout will initialize on the first animation frame to handle resize correctly. System fonts will be used instead of external ones. The visual design will include a gold frame, steam effects, waving passengers, and a ticket-style HUD to maximize the judging impact.
Confetti will burst on each passenger pickup. A demo AI will play automatically. The scene will drift with animated clouds that wrap around the edges. Birds will fly across the sky with a wing-flap animation.
A radial gradient will create a glowing headlight at the locomotive's front point, computed from the train's heading angle.
The locomotive headlight glow is being defined as a radial gradient from gold to transparent. The chimney offset is being calculated by rotating a local point around the train's heading angle to place the steam source at the correct world position.
The steam particles will originate at the rotated chimney point and rise upward with slight backward drift. To keep the effect smooth, the game will emit only one or two particles per frame and cap the total at sixty.
The locomotive will be drawn using canvas translate and rotate for cleaner local-space rotation. Steam particles will grow in size as they age. Small rivets will be added to the boiler. A number plate will be placed on the locomotive.
The locomotive number plate is being drawn as a dark rectangle with gold text in local coordinates. It will sit on the cabin side. The locomotive drawing will be wrapped in a dedicated function with coordinate comments. Visual positions will be kept conservative so the train still reads clearly even if shapes overlap slightly.
The locomotive and wagon dimensions were verified at a 50-pixel cell size. The boiler slightly overlaps the cabin, and the cowcatcher sits at the front. Wagons will have stripes in a darker shade, and color pairs will be created for the train cars.
Each wagon will display a unique passenger face drawn with skin-tone circles and small hats. The platform sign will read "WAIT". The demo AI will be given a safe start by spawning food ahead of it, and any food that lands on the snake will be respawned within a 100-iteration limit.
The `spawnPassenger` function now checks every grid cell for occupancy. It builds a free-cell pool and narrows it to positions ahead of the train when `preferAhead` is true. If no cells remain, the game ends in a win.
Passenger names are generated from a fixed list of safe names and displayed in a dedicated span element. The win overlay will read "The train is full!" when the board is completely occupied.
The best score will update whenever the score changes, not only on death. A `data-mode` class may be added to the body for CSS styling. Floating score labels will use world coordinates derived from grid positions, and confetti will burst at cell centers in world pixels. The code will be organized with a `DIRS` constant for movement directions.
The demo AI will use a BFS pathfinder to navigate the grid. It will queue cells by index instead of shifting an array, keeping the search efficient.
The BFS pathfinder returns the first move needed to reach the target. If the target is unreachable, the AI falls back to a survival mode that treats the snake's tail as a valid escape cell when no other path exists.
The AI survival mode scores each possible move by measuring open space around the head and adding a small bonus for continuing in the current direction.
The AI survival mode will use a flood-fill to measure open space around each candidate move. The head cell is vacated when the snake moves, so the blocked set must be updated to exclude it and include the new cell.
The flood-fill logic will be simplified by cloning the blocked set, removing the head cell, and adding the candidate move. Reversing the snake is impossible because the second segment blocks that direction.
The demo train will remain visible so the AI movement is clearly seen. A "MEADOW YARD" label will be drawn on the yard frame using canvas text.
The "MEADOW YARD" label will be inscribed directly onto the top wooden frame rather than floating above the scene, and it will be skipped if the viewport is too cramped. Corner bolts will be painted onto the frame. A small inline SVG locomotive icon will sit beside the title in the header.
A burst of steam particles will be emitted immediately after the first render so the locomotive headlight is visible right away.
Twelve steam puffs with varied lifespans will be emitted immediately after the first render, once the locomotive position is known. A subtle shadow will be drawn beneath the yard for depth. The grid cell size will be clamped to whole pixels so the snake and wagons stay crisp. A devicePixelRatio listener is also being considered.
A ResizeObserver will handle canvas resizing. The canvas will be styled with CSS width and height at 100% to avoid feedback loops. Only the bitmap width and height attributes will be set programmatically.
The station will include a "TICKETS" sign and a bench, kept inside the drawStation function to keep the main loop clean. The total demo is estimated at roughly one thousand lines of code. Arrow keys will be blocked from scrolling, and the on-screen dpad buttons will use type="button" so they cannot submit forms. They will be styled in gold on dark green with a pressed state handled by CSS :active.
The dpad buttons will be 46 by 40 pixels with a rounded gold background and a dark green shadow. Pressing them will push the button down slightly. The header logo will spin via CSS animation so the wheels are visible on first paint.
The HUD will refresh the score, passenger count, best score, and passenger name on every step. The passenger name element will show a dash when no passenger is present. A subtle animation for the passenger count was considered but skipped to keep the code simple. The train will remain visible during the game-over overlay. Whether to clear the steam particles when restarting is still undecided.
The reset function will clear all particles and floaters, then immediately bootstrap steam particles for the next frame. The snake will spawn at x=6 with five segments starting at y=4, and the initial direction will be set to the right.
The train will be five segments long, starting at x=6 and ending at x=2, with food placed ahead at x≥9. A pulsing yellow glow ring will surround the passenger cell to make it clearly visible.
The game will rely on shape and motion rather than color alone, making it easier for color-blind players to distinguish the train and passengers. A few small flowers will be scattered outside the yard for charm, using precomputed random positions and a limited palette of soft colors.
A railway signal will sit beside the yard, showing red and green lights that flip when the game ends. A tree will be placed in the margin if space allows. The sky will feature a slowly pulsing sun. The train remains the dominant visual element, kept large and high-contrast against the light background.
A subtle drop shadow will be drawn beneath each car to give depth. Thin darker strokes will outline the wagons for definition at small sizes. Wagon colors will be darkened so they remain readable against the light beige yard.
The overlay will not be hidden on startup. The demo AI should survive the three-second preview because a five-segment train can safely traverse the open board in about seventeen moves.
The demo AI will run at 150 ms per step so the train visibly picks up passengers during the three-second preview. The player interval will start at 160 ms and speed up.
Food will be placed four cells ahead so the first passenger is collected around 0.8 seconds. The AI will prefer moving straight when food is on the same row. By the three-second mark the train should be six or seven cars long with one or two passengers on board.
The AI will target cells three to six steps ahead on the same row or one row above or below. Extra scale animations for new wagons were skipped to keep the code simple. The final file will be a single HTML code block with no external assets.
Strict mode will be enabled and all variables declared explicitly to avoid implicit globals. Event listeners will be attached once rather than on every reset. The demo restart timer will be cleared and rescheduled properly so the preview only loops when the game is actually dead. setTimeout is allowed and will be used for the restart delay. The game-over overlay will also display the reason the train crashed.
The game-over overlay will display two distinct crash messages: "Crashed into the edge" or "The train folded". The player interval will be calculated by the same function used in the demo, so the train speeds up visibly as it grows longer. The demo will start at 150 ms per step and accelerate along with the player.
The player interval will be 150 ms in demo mode and 70 ms per step once the train collects its first passenger, dropping by 5 ms per additional passenger. WASD and arrow keys will both control the train, with a helper function mapping each key to a direction vector.
The `queueDir` helper will block reverse turns so the train cannot immediately turn around. It will also reject a direction that is the exact opposite of the currently queued turn.
The `queueDir` helper will block reverse turns by checking only against the currently committed direction vector. This prevents the train from immediately turning around while still allowing perpendicular turns to replace queued moves.
The demo AI overwrites the queued direction each step, so player input is ignored until the user presses startPlay. The queued turn is applied to the train's current direction at the start of each step. A slight delay between pressing a key and the visual turn is acceptable, so the interval will remain at 150 milliseconds.
A 24-pixel swipe threshold will be added for touch controls. Pointer events will ignore presses on the on-screen buttons, and the game will start automatically when the user first touches the stage.
The swipe handler will queue a horizontal or vertical turn based on whichever axis moved farther. The canvas will call preventDefault on pointerdown to block text selection and image drag ghosts, while the on-screen buttons remain outside the stage so they are not affected.
A single pointerdown listener on the stage will handle both mouse and touch, calling preventDefault only when the target is the canvas. The canvas will also receive a CSS touch-action: none rule. A visual direction indicator was considered but skipped. The yard will gain an inner shadow to create a recessed look.
The yard frame will be drawn as a thick stroke with a dark brown outer edge and a gold inner edge. Subtle wood grain lines will be added to the frame for texture.
The header will use a dark green gradient with a gold bottom border and a subtle inset shadow. The footer will be dark green with a matching gold top border. Stats cards will be cream on dark green. The stage frame will stay gold to keep the design cohesive.
Stat cards will use a 12-pixel border radius, 4–10px padding, and a 64px minimum width. A subtle shadow will sit 2px below each card. The label text will be small, uppercase, and brown. The value will scale with clamp and use tabular numerals. The title will use Georgia with slight letter spacing.
A small blinking red lantern dot will sit in the tagline area. The brand row will pair a logo with text. Tagline text will use a system font at a clamped size with reduced opacity. I am now writing the full code and carefully escaping strings.
A narrow-screen media query will hide the tagline and shrink the stats cards, logo, and header padding. The footer hint will wrap the passenger name into a second line.
The game-over overlay will use a solid dark background with high contrast instead of a backdrop-filter blur. It will be centered at the bottom of the stage with a semi-transparent dark brown fill, a gold border, and a soft shadow.
A red derailed chip class will be added for derailment messages. The overlay text will use Georgia for the main message and a system font for the subtitle.
Derailment chips will use a red background with white text. The train smoke will be white with a gray outline so it remains visible against both the sky and the yard. Confetti will be ticket-shaped rectangles in wagon colors plus gold, rotating as they fall.
Confetti particles will use a physics update with gravity, velocity, rotation, and lifetime. The animation loop will compute delta time in seconds from millisecond timestamps. A shake effect will be added for derailments.
The derailment shake will use a smooth sine-wave oscillation instead of random flicker. It will decay over time and be applied after the canvas transform reset. A win condition message will also be added.
A faint grid will be drawn on the yard so the snake-game structure reads clearly. The locomotive will carry a gold "PUFF" nameplate.
A self-review pass will verify all DOM IDs and check for unmatched braces, undefined variables, and typos. The keydown listener will be attached at the window level, and startPlay will guard against repeated key presses by checking the mode state.
Modifier keys will be ignored. The spacebar will start the game and will not pause it. preventDefault will only block arrow keys and space.
The locomotive drawing function is being written with relative units so the train scales cleanly with the cell size. It uses a save-and-restore context and rotates around the train's center point. The cowcatcher is drawn as two overlapping brown polygons, and the wheel placement behind the body is being set up next.
The locomotive body is being drawn with three wheels positioned behind the body. The cabin is a red rounded rectangle with a dark outline, a black roof, and a blue window. The conductor is next.
The locomotive drawing continues with the conductor, a brown face and blue tie, a dark boiler, a gold band, and a black chimney.
The locomotive drawing continues with the headlight, a gold number plate, and the locomotive nameplate.
The locomotive nameplate will display the number "01" centered on the gold plate. A `drawWheel` helper is being added to draw each wheel with a dark center, gold rim, and a spin rotation passed from the train's global animation state.
The `drawWheel` helper is now complete with a cross and gold center dot. A `drawWagon` function is being written to draw each passenger car with its own color from the palette and a wheel behind it.
The wagon body is drawn as a rounded rectangle filled with the current color and outlined in dark gray. A white stripe runs across the top of the body. The roof is filled with the second color from the palette. The windows are next.
The wagon windows are filled with blue and each has a small face. A door is drawn on the right side. The coupling hooks are omitted because the thick dark brown line between car centers is enough.
A `segAngle` helper is being written to compute each train segment's rotation from its neighbors. The render loop will pass the animation time `t` into `segPos` so positions can be interpolated. An empty snake is considered impossible.
The render function will clear the canvas and apply a device-pixel ratio transform. It will draw the sky, ground, station, and yard in sequence, then exit early if the cell size is zero. Flowers and trees will be added to the scene. The train, ties, passengers, particles, floaters, and flash effects will be drawn last. A layoutBoard helper is being added to prevent the station from overlapping the yard by ensuring the vertical offset stays above the band height.
The layout function now clamps the band height between 48 and 120 pixels and centers the board horizontally. It computes the cell size from the remaining vertical space after the top band and bottom margin, forcing a minimum of six pixels. The vertical offset is set to the top band plus eight pixels, and the board is vertically centered within the remaining space. A potential overlap between the board and the station is noted if the stage is very short.
The station will be clipped to the top band so it cannot overlap the yard. The horizon sits at the band height, with sky above and grass below. The layout is safe even if the stage is very short.
The horizon is set to the lower of the band height and the yard offset minus six pixels. The sky will draw to that horizon line, and the grass will fill the area below. The station and its passengers will sit on that baseline. A fence will be drawn along the horizon as a light gray picket line.
The fence will span the full width as a light picket line separating the station from the tracks. A pulsing ring will surround each passenger, and a name label will sit above them.
The passenger label is drawn as a rounded rectangle with a gold border and dark text, sized relative to the cell scale. It sits above each passenger, flipping below if the passenger is near the top of the yard.
Passenger labels will flip below the head when a passenger is at the top of the yard. Each passenger gets a unique color-coded suitcase. Outfit generation uses eight wagon colors and five skin tones.
Passenger hats are chosen from a fixed palette of six colors. Each passenger’s outfit is generated from their index, skin tone, and hat. When a passenger boards the train, the new wagon adopts the same color as their shirt, creating visual continuity. Initial wagons also carry faces. Passenger labels will appear as small name pills above each person.
A soft gold glow will pulse around the yard border when active. A red flash will cover the screen on death, fading over time. On death, the shake and flash will both trigger, and a smoke burst will spawn at the train head. Tilt effects were considered but skipped.
A brief "ALL ABOARD" floater will appear above each passenger when they board. It rises in world space and fades out. The floater's pixel position is stored at creation so it does not follow the grid.
A `cellToPx` helper will convert grid positions to pixel coordinates for the eat burst and floaters. The first render happens before the first step, so the train and passengers appear immediately with the overlay visible. Image smoothing will be enabled for crisp circles. The design is complete and ready to be written to file.
A null passenger check will prevent the AI from crashing when every seat is full. The win() function will switch the game to dead mode with a full-seats message. If no free cells remain, the game will simply stop spawning passengers rather than adding complex restart logic.
Board dimensions and initial snake placement were verified to stay within bounds. A colorFor helper maps each snake segment to its corresponding wagon color. The locomotive wheel rotation logic was checked and confirmed correct.
The `roundRect` helper clamps the corner radius to half the width and height to avoid invalid arcs. A global `ctx.lineJoin = 'round'` will be set each frame. Text baseline defaults are being managed carefully, and a potential bug where `measureText` runs before the font is set was noted.
The font will be set before any text measurement. Each frame will reset globalAlpha to 1 to prevent shadow and transparency leaks. Particles will use rgba values instead of changing globalAlpha. The four station passengers will be drawn at a small scale using a fake cell size.
The `drawPerson` function is being refactored to accept a `feetY` parameter instead of deriving position from the center point. This makes it easier to place station passengers at the correct height on the platform.
Passengers will be drawn at 72% of the cell size so they fit neatly above the platform. The platform sits at the bottom 22% of each cell, with feet placed at 78% from the top. At a 50-pixel cell size, each person is about 36 pixels tall, and at 22 pixels, they remain recognizable as blobs with heads.
A bench will be drawn at the station platform. The sun will be rendered as a radial gradient.
The sun will sit at the top right corner, drawn as a radial gradient with a bright yellow center and a soft gold halo. Three drifting clouds will be added to the sky, each with its own position, scale, and speed.
Birds will drift across the sky like the clouds. Extra decorative tracks outside the play area were considered but skipped to avoid confusion. The code will be written now in a mostly comment-free style. The user's request to make the snake a little train is being kept in mind.
The locomotive will be slightly larger than the wagons so it reads as the head of the train. Smoke trails were considered but skipped to avoid cluttering the yard. Steam will only come from the locomotive chimney.
A small bell will sit on the locomotive cabin roof. A whistle puff particle will spawn at the chimney when a passenger boards, so a `chimneyPos` helper is being added to map the locomotive's world position.
The chimney world position helper is now complete. Because the step function lacks direct pixel access, the steam burst on passenger pickup will instead spawn a confetti burst at the head cell center. Each burst emits eighteen particles with random horizontal velocity and upward motion.
The confetti burst will use the passenger’s own wagon color and include white and gold particles. The burst fires before the next passenger is assigned a color, so the train keeps the correct shade. The demo restart timer is cleared when the user clicks, preventing a race condition. A screenshot taken during a demo death might show a derailed train, but the AI should avoid that.
The demo AI will be kept safe and invincible so it never crashes. The snake will move at a fixed 160 ms speed and the board will stay open. A 90-second safety cap will restart the demo to keep the footage fresh.
The 90-second demo safety cap was dropped because it is not needed for judging. The initialization order was fixed so the canvas is drawn synchronously before the first animation frame, ensuring the scene appears immediately and the screenshot at three seconds is not blank.
ResizeObserver and window resize listeners will guard against zero-size canvas during boot. A title element will be added to the HTML. CSS will animate the logo SVG wheels. The footer layout was checked to prevent horizontal overflow from the dpad buttons.
The flex layout will use `min-height: 0` on the stage so it shrinks without overflowing the page. Header and footer will stay fixed. A `max-width` and `box-sizing` rule will cap the page width.
The overlay will use a CSS class to toggle visibility instead of the `hidden` attribute, which some browsers force to `display: none`. JavaScript will add an `is-hidden` class during play. The overlay will also carry `role="status"` for accessibility.
Meadow hills will be drawn as sine-wave curves behind the train. The far hill will render first, then the near hill, followed by flat grass. The horizon line will separate the sky from the ground. The station and yard will sit on top of the grass layer.
The horizon line will sit just above the station baseline, keeping the distant hills hidden behind the station. The rendering order is now fixed: sky, sun, clouds, birds, far hills, grass, station, yard, trees, then the game.
The horizon line will be clamped between 40 and bandH pixels. The grass gap between the station baseline and the yard will be calculated from the vertical centering offset.
A road connecting the station to the yard was considered but skipped. The stage will get a gold CSS border and a soft shadow, layered over the canvas wood frame. The grass will be dotted with 80 precomputed specks clipped to the grass region.
The stage will use a deep gold box-shadow with a dark brown outline and soft shadow for a premium look. The stage-wrap padding will be 12px 14px so the shadow does not get clipped. The title will be "Meadow Line" with a vintage tagline reading "Passenger express · est. 1894".
A CSS pop animation will make the score and best-score counters jump when they increase. The bump effect will be triggered only by the eat handler and the best-score update, not on every HUD refresh. The tagline will read "A little train that picks up everyone". The code is ready to be written.
A faint headlight cone will be drawn behind the locomotive in local space, using a low-opacity yellow triangle so it does not obscure the cowcatcher.
The headlight cone will extend into the next cell so it reads as a continuous glow. Fixed decorative steam puffs will also appear near the chimney, and the first fourteen particles will be spawned immediately when the game starts so the locomotive is not left smokeless.
The `makeSteam` helper now gives each particle a random initial age so some appear faded and already risen. Because the locomotive’s world position depends on the current time and layout, the render loop will spawn these particles instead of the update loop.
A 45 percent chance of spawning a steam particle per frame will keep the locomotive smoking, even after death. The particle array will be capped at ninety items, and floaters at eight.
A soft shadow will be drawn beneath the entire train. Track ties and rails will be rendered under each segment so the train looks like it is rolling on real rails.
Track ties and rails will rotate with each segment so they follow the train’s path. The render order per segment is: tracks, shadows, couplings, wagons back to front, locomotive, then steam.
Passengers will be drawn after the tracks but before the train so the locomotive can cover them as it collects them. The render order is now scene, yard, tracks, passenger, couplings, shadows, wagons from tail to head, locomotive, particles, floaters, and flash. Wagons will be drawn from tail to head so the locomotive sits on top.
Shadows will be drawn in a separate pass before the wagons, so each car does not obscure the shadow of the one behind it. A slight vertical offset will be added to the shadow. The implementation plan is now complete.
A "MEADOW STATION" sign will be drawn on the building, shrinking to "MEADOW" when the scale is small. A clock face will also be added to the station.
The station building will sit on the left third of the top edge so the sky and sun remain visible. The platform and people extend to the right. The game yard will sit at center-bottom with the train as the main focus. The building will scale to roughly 28 percent of the canvas width and 72 percent of the horizon height.
Platform passengers will be spaced 28 pixels apart and capped at five to avoid clutter. The collectible passenger will sit in the yard with a name label and pulsing ring, making the pickup target unmistakable.
The platform pole sign will be colored to match the passenger’s shirt. A speech bubble was considered but dropped to avoid overlapping the name label. The full HTML is now being written. All required browser APIs are confirmed to exist in the sandbox.
The canvas context will use the default alpha channel rather than forcing opaque mode. A null guard for the context is unnecessary because the canvas element will always exist. The canvas will also carry `role="img"` and an `aria-label` for accessibility.
The script will be placed at the end of the body so the DOM is ready before any updateHUD calls. The four HUD elements will be cached in variables. The dpad buttons will use type="button" to prevent accidental form submission. Arrow keys will still be intercepted to stop page scrolling.
The overlay, chip, message, submessage, stage, and canvas elements are now cached in variables. Buttons will hide the default focus outline and show a white one on focus-visible to avoid screenshot artifacts. The body will also receive `user-select: none` to prevent text selection.
The bottom overlay risks covering the yard and train. The train starts near the center of the 470-pixel board, so the 70-pixel overlay at the bottom could obscure it. The HTML overlay will hold the instructions instead of drawing them on the canvas.
The bottom overlay would cover the yard’s bottom row. A 62-pixel reserve at the canvas bottom will keep the yard clear of the overlay. The train and passengers will stay fully visible.
The bottom reserve will shrink to 52 pixels at short heights and 86 pixels on narrow screens, preventing the overlay from covering the train.
At 1280×800 the board would be only 615×410 with 41-pixel cells. Shrinking the header, footer, and top band to 68, 72, and 90 pixels yields 49-pixel cells and a 735×490 board. The train will be larger.
The grid is being narrowed to 12 by 8 cells. At a 640-pixel stage this yields 63-pixel squares, giving the train a chunkier, cuter look while keeping the snake game playable.
The grid is being narrowed to 13 columns and 9 rows. This yields about 55-pixel cells on desktop and roughly 24-pixel cells on mobile. The train will stay recognizable on small screens while the snake game remains playable.
The grid is finalized at 12 columns and 8 rows. Desktop cells will be about 63 pixels, and mobile cells about 31. The snake will start at x=4, y=3 with a length of 5, and food will spawn at x=8 or 9 so the initial path fits cleanly.
Food will spawn between columns 8 and 10 and rows 2 and 4 so the train can collect it without colliding with its own tail. The 12 by 8 grid is now locked in. Trees, signals, and flowers will fill the side margins to keep the scene from looking empty.
The train will start at y=3 so it does not sit too close to the station. A lamp post will be added in the margin. Side decorations will be drawn beside the yard from the top of the board to its bottom to avoid overlapping the station. The tree-drawing function is being written.
The tree-drawing function is now complete with a trunk, leaves, and two smaller leaf clusters. One or two trees will sit in each margin when space allows. A signal will be placed on the right side of the yard. Flowers will be added around the yard corners and along the bottom grass, but the scene will stay subtle to avoid overcrowding.
The overlay reserve and footer instructions are being reconciled so the bottom grass area reads as a caption bar. A wooden signpost reading "SLOW" or "ALL ABOARD" will be drawn in the yard margin when space allows.
The signpost will read "ALL ABOARD" in dark green. It will only appear when margin space allows. Small whistle arcs will appear near the locomotive chimney during movement. The game title is already in the header.
The mobile header is too wide for 375 pixels. Three stats plus the logo and title exceed the available space. A media query will hide the best-score stat and shrink the logo and title on screens under 420 pixels.
The tagline will also be hidden on screens under 480 pixels. The header will wrap to prevent horizontal scrolling, with the stats area set to `flex-shrink: 0`. The body will use `overflow-x: hidden` as a safety net, and the page container will be constrained to `100%` width with a `max-width` of `100vw`.
A `clamp` helper will keep values within bounds. Food detection will use the new head position before the snake body shifts. Self-collision logic is verified: the neck is blocked by the direction queue, the tail vacates when the snake moves, and food cannot spawn on the tail.
A 16-particle smoke burst will fire at the snake head on death. The head will also show a small crack or X mark. The death reason will be recorded.
The overlay text will use textContent exclusively to prevent XSS. A longer list of twenty passenger names was added for variety.
The reset function will clear the score but preserve the best score and game mode. The collected passenger count will be tracked as `picked`. The "Aboard" stat will show `snake.length - 1`, which already includes the four initial passengers.
The "Aboard" label was rejected because four initial wagons would make the count look like a full passenger load. The middle stat will instead read "Cars," showing the snake length minus the locomotive.
The footer will display the next passenger's name, for example "Waiting: Mina". Each pickup will award 10 points, and a "+10" floater will appear above the train. A large "ALL ABOARD" flash will also appear on the canvas when a passenger is picked up.
A large "ALL ABOARD" flash was rejected because it would obscure the train; a small floater is the better choice. The code will be written carefully in one pass, using modern JavaScript syntax and avoiding TypeScript. The game loop will be named `advance` to avoid shadowing a `step` variable, and several local variables will be declared with `let`.
The game state variables are being declared at the top with `let`, and the dpad buttons will use pointer events to start the game and queue direction changes.
The dpad buttons will queue direction changes via pointer events. A global pointer listener is being added so clicking anywhere on the page can start the game, including the header.
The window pointer listener will ignore clicks on the dpad buttons so the stage can still handle swipes. Because the window listener fires after the stage listener, it will not interfere with the swipe detection.
Swipe detection will ignore gestures shorter than 28 pixels. The stage will also clear the swipe state on pointer cancellation.
The locomotive will be drawn with a tall cabin, long body, chimney, wheels, and a rounded front so it reads clearly at small size. Smoke and colorful wagons will be the main visual cues for train identity. The file will be formatted cleanly and kept under roughly nine hundred lines.
The initial four wagons will use a distinct rainbow palette of blue, yellow, green, and red. Bold white window frames will be added so the wagons remain readable at small size. A red buffer beam will be drawn on the front of the locomotive to make the train instantly recognizable.
The final code is being prepared now. The judge compares side by side, so every core element must be unmistakably visible: a distinct locomotive, colorful wagons, passengers waiting on the platform, and the full snake mechanics. A demo mode will play automatically. The station platform will use only a slab and pole, with no roof to hide the passengers.
A friendly face with eyes and a smile will be drawn on the locomotive boiler so the train looks cute and identifiable at small size. The dashed safety line idea was dropped.
The locomotive face will be simplified to just a smile, omitting eyes to avoid cluttering the gold band. The combination of cabin, chimney, wheels, and face should still read as a cute little train.
The locomotive face is being placed on the boiler front at local coordinates x=0.06, y=-0.02. The cabin spans x=-0.46 to -0.14 and the boiler spans x=-0.20 to 0.28. The entire train silhouette is being kept within -0.48 to 0.48 so it does not overlap the next car.
The locomotive face is being moved to the cabin window as a conductor. A front headlight glow will mark the front. The boiler face is dropped to keep the train look clean and on-brief. The final coordinates are being locked in.
The locomotive roof will carry a bell at x=-0.20. Steam will rise from the chimney top. The conductor face will sit at the cabin window center (-0.32, -0.22). Wagon dimensions are now fixed: body spans -0.34 to 0.34, roof spans -0.38 to 0.38, windows are at -0.22 and 0.02, the door is at 0.18 to 0.28, and wheels are at y=0.18 with radius 0.08. A dark coupling line will connect the wagons beneath them.
The locomotive and wagons will sit 0.18 cell widths apart, leaving a visible gap between them. The locomotive front will extend slightly into the next cell so the cowcatcher can overlap arriving passengers during pickup. A smokebox door will be added to the front of the boiler for a classic look.
The smokebox door will sit at local x=0.18, just behind the headlight at x=0.30, so the two will not overlap. The developer is now moving from design to implementation and will not keep refining the visuals. A reduced-motion CSS fallback is unnecessary because the animations are central to the experience. The final check ensures that every distinctive element is clearly visible within the first few seconds of the demo.
The start overlay will use the exact required phrase and reserve a margin so the train remains visible behind it. A second decorative passenger will be added to the yard, with a new one appearing after each pickup.
Only one passenger will be active at a time. Two collectible passengers at once would confuse the AI and the player. Decorative passengers will remain on the platform. The footer will show a single waiting name. A queue of three names is overkill.
The yard platform will be drawn with wooden planks, a bench, a pole with a clock-like sign, and a passenger waving a suitcase. A "STOP" label will appear on the pole when the cell size is large enough.
The platform sign will be a small rectangular "1" instead of a red circle with white text. The developer is now moving from design to implementation. A pause when the tab is hidden was considered but skipped. Canvas image smoothing will be enabled, and the board offset will be rounded to integers to keep grid lines crisp.
The board offset will be rounded to integers and the cell size floored. A render safeguard will draw the sky if the canvas is too small or the cell size is below four pixels. The game loop is also protected against an empty snake by ensuring reset always initializes the snake before advance runs.
The overlay helper is being written so the exact start message always appears for both demo and dead states. The demo chip will read "DEMO RUN" and the dead chip will read "DERAILED" with the score and reason.
The demo overlay will include a subtitle explaining arrow keys, WASD, or swipe controls for picking up passengers. Two helper functions, startPlay and startDemo, will reset the game state and switch modes. They safely handle cases where the game is already running, dead, or in demo mode.
The dpad button listener will use `{passive: false}` so it can call `preventDefault` on touch. The stage pointer listener will also use `{passive: false}` to block native gesture navigation. Both the stage and the dpad will get `touch-action: none` to keep the game fully in control of touch input.
The best score will update live. A meta theme-color tag will be added. The gravel specks will be generated once at startup rather than per frame.
Gravel specks will be drawn clipped to the yard edges. A fixed list of ten flower positions will be placed procedurally in the world margins so they stay stable across frames.
A green pulse signal light will animate at the station. A `cellPx` helper will convert grid coordinates to screen pixels. The interpolation factor will be clamped between zero and one. The full source will be written as one continuous block.
When the snake dies, the interpolation factor will freeze at one. This keeps the train frozen at its last successful cell rather than snapping forward into the wall.
The developer traced the exact frame sequence before a fatal collision. If the accumulator is not reset when the snake dies, the interpolation factor clamps to one and the train stays frozen at its last successful cell. Subtracting the interval before the collision check would instead snap the train backward to the previous position, causing a visible jump.
The developer realized that subtracting the interval before confirming the move causes a backward visual jump when the snake dies. The fix is to freeze the interpolation factor at the end of the last successful pose instead of resetting the accumulator.
The developer is refining the death animation. Rather than freezing the interpolation factor, the plan is to set the previous snake state equal to the current snake state when the move fails. This makes the interpolation factor zero and keeps the train frozen at its last successful position without a backward jump.
The AI move function will use the current direction to avoid reversing and to reward straight paths. The direction object will be copied to prevent aliasing. The developer is ready to write the full source. A new wagon will appear in the passenger's color when picked up. The passenger's name will not be shown on the wagon; the float text is enough.
The pickup floater will display the passenger name, such as "+ Mina", so the screenshot clearly shows the mechanic. The demo speed is being reduced so the train is still collecting its first passenger by the three-second mark, ensuring the second passenger is visible and the train has five wagons. A five-cell head-to-food distance confirms the first food will be reached in about 800 milliseconds.
The first passenger will spawn at a fixed position so the demo layout is predictable. Later pickups will remain random. The developer is checking whether that cell is always empty.
The first passenger will be placed five cells ahead of the snake head, defaulting to a fixed coordinate that falls safely outside the train's occupied space. If that cell is somehow occupied, the game will fall back to a random spawn. Later passengers will also be placed at a distance from the head so the player can see them clearly.
Passenger spawns will be filtered to cells at least four grid steps away from the snake head. The first passenger, Mina, will be purple and waving from the start. The HTML placeholder name is being set to Mina so the initial paint matches the game state.
The developer is refining the demo layout. The train will sit at y=3 and the food at y=3 so the AI moves straight across the top row. The screenshot will show the rainbow train facing right with Mina waving on the platform beside it. Steam will rise from the locomotive. The demo interval is being tightened to 220 milliseconds so the train reaches Mina by the three-second mark.
The demo interval is set to 170 milliseconds for livelier movement. By the three-second mark the train will have picked up several passengers and grown to six or seven wagons. The developer confirmed that occupied-cell checks prevent the second passenger from spawning on the train. The design is complete and ready to be written.
A gentle vertical bob will animate the train wagons based on time. The bob is applied only to the cars, not the tracks, so the wheels stay grounded on the rails. This tiny bounce gives the train a subtle, alive feel.
The wheel spin will be driven by total travel distance plus the current move’s interpolation factor. This keeps the wheels rotating during movement and stops them instantly when the train dies.
The developer simplified the wheel spin logic. The wheels will rotate based on elapsed time while the game is active, and freeze at their current angle when the train dies. This avoids a jump from time-based to travel-based rotation at death.
The developer is now writing the full source. A frozen wheel angle will be captured on death so the wheels stop cleanly without a rotation jump. Wagons will receive a subtle vertical gradient for a 3D effect, and the locomotive boiler will get a darker-to-lighter gradient. A window shine will also be added.
The page background will be set to the same green as the header so any gaps do not show white. The developer is now mentally organizing the full source into thirteen sections, from constants and DOM setup through the main game loop.
The developer added a null guard so drawing functions skip the passenger when none is present. The spawn logic now includes a win path: if no free cell exists for a new passenger, the game clears the passenger and updates the name label to an em dash.
The train head will start at x=4 and y=3, with five wagons trailing leftward. The reset function initializes the snake array, copies the starting state for interpolation, and sets the initial direction to right.
The reset function initializes score, picked count, particles, floaters, and animation flags. A fallback color is added for wagons when the color array is shorter than the snake. The keyboard hint is placed in the overlay subtext. iOS Safari is not a concern since the judge uses the latest Chrome.
The CSS will be compact but complete. Overflow hidden is set on the html and body elements to prevent horizontal scrollbars from the stage's gold box-shadow.
The developer decided against using 100vw to avoid horizontal scrollbars. A train whistle visual was considered but skipped in favor of the steam effect. The station awning will display a passenger count caption such as "Platform 1". The awning will use alternating red and cream stripes.
The developer is now coding the station drawing function. It scales proportionally to the horizon and canvas width, with a subtle ground shadow beneath the building.
The station drawing function is being completed with a triangular roof, brick-walled facade, subtle brick lines, a dark door, and yellow windows.
The station drawing is being completed with window frames, a clock, and a dark sign with cream-colored Georgia text.
The station sign text will switch between "MEADOW STATION" and "MEADOW" depending on scale. A brown platform strip with a gold top edge is drawn beneath the building. The awning extends to the right of the station and is divided into alternating red and cream stripes.
The awning posts are drawn in dark brown. The developer calculates how many passengers can fit under the awning based on its width and draws them with alternating wagon colors and skin tones.
Passengers under the awning are drawn with alternating hats and bags. The clock is placed just below the roof peak to avoid overlapping the triangular roof. The wall and sign positions are tuned so the station reads clearly without over-engineering.
The clock is placed on the upper wall to avoid overlapping the door. The sign is moved to the awning front so it does not clash with the clock. A dark rectangle with cream-colored text reads "PLATFORM 1" on the awning.
A bench will sit under the awning if space allows. The clock hands function is being added. Birds will be drawn in the sky during rendering. Hills may be added to the background. The full HTML file is ready to be composed. A resize listener and ResizeObserver will be included. The layout function will be called on boot and on resize.
The resize handler will compare the current CSS width and height against a cached key before updating the canvas bitmap. This prevents the ResizeObserver from triggering redundant layout cycles when the canvas dimensions are already correct.
The resize handler will also detect changes in device pixel ratio. It compares the scaled width against the cached key and updates the canvas buffer whenever the ratio changes.
The resize handler now exits early when the CSS width, height, and device pixel ratio all match their cached values. This prevents the ResizeObserver from looping. The initial call may see zero width before layout completes, so the function still proceeds to set the canvas and run layout.
The developer confirmed that the snake grid uses cell coordinates, so resizing the canvas will not break the game logic. Pixel-based floaters and particles may drift slightly after a resize, but they expire quickly, so this is acceptable. The developer is now ready to write the full source code.
Georgia will fall back to a generic serif if the font is missing. Line cap and join are reset each frame. The locomotive chimney smoke is drawn before the bootstrap particles so it appears immediately on the first frame.
The developer added a bootstrap step that generates twelve steam particles from the locomotive chimney on the first frame the snake exists. A local chimney offset constant was defined so the smoke origin matches the drawing position exactly.
The developer is verifying the locomotive chimney offset math. They confirmed that a local offset of (0.15, -0.52) places the chimney above the center point when the train faces right, and that the rotation formula correctly maps this local position to world coordinates for any movement angle.
The developer confirmed that when the train faces down, the chimney offset rotates to the left side of the boiler. This matches the expected side-view behavior.
The developer weighed keeping the train in a top-down view for vertical movement to keep the chimney visually upward, but rejected it as too complex and inconsistent. They decided to stick with the standard side-view rotation and accept the chimney pointing left or right when the train faces down or up.
The developer realized that when the train faces left, the chimney rotates to point downward, which looks wrong. They are now weighing three fixes: mirroring the train vertically when facing left, using four separate sprites, or switching to a top-down train view.
The developer decided to abandon the side-view train entirely. A 3/4 top-down view will be used for all directions so the locomotive stays upright and the chimney never points downward. This avoids the awkward strafing effect that would occur if the train were forced to face left or right during vertical movement.
The top-down train will be drawn facing up and rotated to match movement direction. The locomotive chimney becomes a circle on the body, and wagons are rectangles with roofs. This view is symmetric, so the sprite never appears upside-down.
The top-down locomotive design is now detailed. It will face right with an elongated rounded body, a square cabin at the rear, a narrower boiler toward the front, a circular chimney, a headlight at the front tip, wheels on both sides, and a cowcatcher at the front.
Wagons will have wheels on the sides and colorful roofs with lighter window rectangles. The developer accepts mixing side-view passengers with the top-down train as a cute game convention.
The developer decided to draw passengers as top-down blobs with heads and bodies rather than side-view figures. This keeps the entire scene in a consistent 3/4 top-down perspective.
Front-view passengers are cuter and more readable than top-down blobs, so the developer is keeping them. The train will remain top-down for clean four-direction movement. The upside-down issue when the train faces left is being solved by mirroring the sprite horizontally instead of using a full side-view rotation.
The developer considered using a front-view locomotive sprite for vertical movement to keep the train upright, but rejected the idea. Switching between front and rear views would make the snake look discontinuous during turns.
The top-down locomotive is being drawn facing right. Its body, cabin, chimney, headlight, cowcatcher, wheels, and gold trim are all mapped to exact local coordinates. Wagons will have colorful rounded roof rectangles.
Wagons will have lighter inner roofs, darker window squares, and tiny passenger dots inside. A coupling will connect the cars. Steam smoke will always rise upward on screen, which fits the top-down view naturally.
Smoke particles will always rise upward in screen space, independent of the train's rotation. The locomotive sprite will be chunky and cute, with a slight 3/4 offset shadow to give it height.
The locomotive drawing function is being implemented. It translates and rotates the canvas context to match the train's position and angle. Six wheels are placed on the sides and front of the body. The cabin is drawn as a dark red rounded rectangle at the rear.
The locomotive sprite is being detailed with a dark red cabin roof, a light blue window, and a conductor dot with a hat. The boiler is drawn as a dark rounded rectangle, and a gold ring is being added to the front.
The locomotive sprite is being detailed with a gold vertical trim line, a dark circular chimney with a lighter inner highlight, and a yellow headlight.
The locomotive sprite is being completed with a gold headlight outline, a brown cowcatcher, and a large '1' number on the cabin. The canvas context is then restored.
The developer is reconsidering the pure top-down locomotive. A 3/4 perspective with a darker side strip beneath the roof would look more train-like, but rotating that view 180 degrees would make the side face point upward and look upside-down.
Pure top-down is the safer choice because it avoids upside-down rotation issues. The developer plans to add rail tracks beneath the train to reinforce the train theme. The wagon drawing function is being implemented with wheels positioned on the sides.
The wagon body and roof are being drawn as rounded rectangles in the car's color palette. Two white window squares are placed along the roof, and a colored passenger dot is being added inside one of them.
The wagon passenger dots are drawn as small circles with a door line. The locomotive will stand out from the wagons through its black boiler, red cabin, gold trim, and smoke. The bright-colored wagons will contrast against the darker locomotive.
Outlines will be added to all sprites for readability at small sizes. The developer briefly reconsidered a side-view train that mirrors horizontally and flips vertically during movement to keep the chimney always pointing upward, but then abandoned the idea.
The developer realized that a side-view locomotive cannot satisfy both constraints at once. If the front of the train points in the direction of movement, the chimney must rotate to point left or right. This confirms that top-down view is the only viable approach.
A side-view locomotive silhouette will be added to the header logo. This classic portrait will complement the top-down in-game train, which will include chimney smoke, wagons, and tracks.
A side-view train is being reconsidered. The locomotive would face right or left based on horizontal movement, and wagons would stack vertically beneath it. This would create a side-view train climbing upward during vertical movement.
The developer is now leaning toward a side-view locomotive that always faces right or left based on horizontal movement. The locomotive would remain the leading head of the snake, and vertical movement would stack side-view cars in a vertical column. This approach would never look upside-down and might read more like a real train than a top-down view.
The side-view train idea was abandoned. A rotated top-down locomotive with detailed art is now preferred for a more polished, game-like appearance.
Side-view locomotives are now the chosen approach. They will stay upright and mirror horizontally for left and right movement. The developer is working out how vertical movement will look in a still screenshot.
The developer is now using a rotation-with-flip technique for side-view sprites. When the train moves vertically, the sprite will rotate and then be flipped vertically so the chimney stays pointing upward and never looks upside-down.
The rotation math confirms the chimney points upward when the train faces right, but points downward when facing left. The flip trick is being applied to correct that leftward orientation.
Horizontal movement will use a simple horizontal mirror without any rotation. Vertical movement will rotate the sprite and then flip it vertically so the chimney stays pointing upward. When facing up, the chimney will sit on the right side of the train, which is acceptable.
The developer is now coding the orientation logic. Because the snake snaps to four cardinal directions, the rotation simplifies to a clean set of conditional transforms. Rightward movement needs no change, leftward movement mirrors horizontally, and vertical movement rotates the sprite by ninety degrees.
The developer is now coding the `drawOriented` helper. It will translate the canvas to the train's position and apply the rotation and flip transforms. Because the snake snaps to four cardinal directions, the sprite will pop at grid corners rather than rotate smoothly. This small visual jump is acceptable for a grid-based game.
The developer is refining the `drawOriented` helper to handle continuous angles. It wraps the angle to the -PI to PI range and mirrors the sprite instead of rotating past ninety degrees when facing left. The flip condition is being calculated by checking whether the sprite's local up vector, after rotation, points downward.
The continuous-angle rotation logic was abandoned. The developer will instead snap the train heading to the dominant cardinal axis and use simple four-direction transforms. This avoids interpolation bugs and upside-down sprites entirely. A `facing` helper is being drafted to return the current direction.
The `withFacing` helper is now fully implemented. It translates the canvas to the train's position and applies a horizontal mirror for leftward movement, plus ninety-degree rotations for up and down. The developer noticed that mirroring the locomotive would flip any text on it, such as the "1" number, so they will either avoid placing text on the locomotive or draw it after unflipping the context.
A symmetric diamond symbol will replace the locomotive number plate so it does not look mirrored. Spoked wheels will mirror correctly. The developer accepts a one-frame rotation pop at grid corners and will implement the snap-based facing logic.
The developer is now writing a `localToWorld` helper to map local coordinates to world space for the steam chimney. It applies the same facing transforms used for the train body, including mirroring for leftward movement and ninety-degree rotations for vertical headings.
The chimney position was verified against all four facing directions. It stays above and slightly forward when facing right or left, and shifts to the right or left when facing down or up, matching the rotated sprite. Smoke particles will emit upward from the chimney with slight random drift.
The developer is returning to the earlier detailed side-view locomotive design. They will implement `faceOf` and `withFace` helpers to orient the train instead of using raw continuous angles. Wheel spin will remain in local space.
Tracks will be oriented to match the train's facing direction. A side-view train that is always horizontal and simply stacks wagons vertically during movement was considered as an alternative to the rotated approach.
The vertical-stack approach was rejected because it would look like disconnected cars rather than a connected train. The developer will proceed with the rotation-and-mirror side-view locomotive. Each segment will use its movement vector snapped to four cardinal directions for facing, accepting a one-frame flip at corners. The final code is now being written.
The developer is now coding the complete implementation. They will add tracks, stations, passengers, a demo AI, overlays, and responsive UI. The side-view locomotive will include a darker underside for volume. The chimney will sit at local coordinates (0.14, -0.50) and emit steam from that exact point.
The developer considered adding two static smoke circles in local space above the chimney as a guaranteed fallback visual. They decided against this redundancy and will rely solely on the drifting smoke particles.
Twelve smoke particles will be pre-positioned at varied heights on the first frame so the train appears with a rising plume immediately. The locomotive will use a symmetric bell and rivets instead of a numbered plate to avoid mirrored text. A zero-movement fallback will be added to the facing logic so it defaults to the current direction when dx and dy are both zero.
The `faceFromVector` helper is now fully implemented. It snaps the train heading to the dominant cardinal axis and falls back to the current direction when movement is nearly zero. Coupling lines will be drawn between segment centers without any orientation. A flat shadow ellipse will be drawn under each car so shadows remain on the ground regardless of rotation. The developer is ready to write the complete implementation.
The game will use a 12 by 8 grid. At 375 pixels wide each cell is about 28 pixels, so the side-view train will be a bold silhouette. The developer will not add a reduced-motion preference because the demo requires animation. The HTML and CSS are being added now.
Wagon colors will be more saturated for visual impact. A light inner highlight will be added to the locomotive boiler. The developer is now writing the complete HTML document. A small arrow on the demo path was considered and rejected. The score will not be duplicated on the canvas because the header should remain visible.
The demo AI will target passengers. Track ties will appear only beneath the train. Audio is skipped. No alerts or localStorage. No eval or document.write. Null passenger fields will be guarded.
The developer is adding defensive checks to prevent division by zero, empty string measureText calls, and invalid chimney coordinates when cell size is zero. The render loop will exit early if cell size drops below four pixels after drawing the background. To keep the wide desktop layout from looking empty, flowers and trees will be added to the margins. A signal post drawing function is also being drafted.
The signal post will show a red light when the train is dead and a green glow when it is moving. It will be placed just outside the right edge of the board if space allows. A tree will be drawn on the left margin when the board offset is greater than sixty pixels. Clouds, a sun, and hills will also be added to complete the background scene.
The canvas CSS gradient will appear before JavaScript paints the richer scene. A score pop animation and passenger names will be added. Instructions will be visible in the footer and overlay. A dpad will support mobile and desktop input. The judge can start the game by clicking the overlay message. An automatic screenshot will capture the demo train.
The demo AI will start moving quickly so the train is clearly in motion by the three-second mark. It will be conservative to avoid looking dead in the screenshot. The BFS pathfinding will treat the tail as an obstacle so the train does not collide with itself. After eating food, the AI will plan a new path to the next random food location.
The developer is adding a "BOARDING YARD" name plate to the top of the yard frame. The label will scale with the cell size and sit inside a dark brown rounded rectangle.
Comments will be kept minimal to avoid text outside the code block. The overlay will display "DEMO RUN" and remain visible even if JavaScript fails. The stage will use relative positioning with an absolute overlay, and the canvas will fill the container with no inline gaps. The stage background will be set to a sky color so the area does not look empty before the first frame paints.
The developer is now encoding the side-view locomotive silhouette into exact local coordinates. The train will be about 95% of the cell width and 70% of the cell height. The planned segments are a tall red cabin with a roof and conductor, a long black boiler with a rounded front, a chimney, a headlight, a cowcatcher wedge, three wheels, and a gold band.
The developer will draw the locomotive in this order: wheels, cowcatcher, cabin, boiler, details, chimney, headlight. A small steam dome will sit on top of the boiler. Passenger drawing will proceed from feet upward, and platforms will be drawn inside each cell.
Passenger name labels will be clamped to keep them inside the yard bounds. A pulsing gold ring will surround each waiting passenger, and the platform will sit at the cell bottom.
Passenger names will sit just above the yard when the train is near the top of the board. The label may overlap the station if the yard is too close to the horizon.
Passenger labels will sit just above the cell when the train is near the top of the board. If the cell above is empty, the label may overlap into it. The train will cover the label as it picks up the passenger.
Passenger labels will be clamped to the canvas edges to prevent off-screen text. A small "toot" visual near the locomotive was considered but skipped as potentially annoying. A `drawFloaters` function is being added to render score pop-ups with fading opacity and a bold Georgia font scaled to the cell size.
The score pop-up will use a dark brown stroke outline and a cream fill. The canvas context will be saved and restored to keep the globalAlpha reset. Steam particles will rise and fade over time using delta-time physics.
The developer is now implementing the particle physics. Steam particles will rise and expand as they fade, while confetti particles will fall with rotation and color. The particle system will also cap its total count at one hundred to keep the frame rate smooth.
Confetti bursts will include gold pieces. Death smoke will be added. The locomotive and tracks both use the same horizontal flip for left-facing movement. The developer is ready to write the complete file.
The developer will draw coupling hooks at the rear of each car so the train’s connections remain visible even when the center line is hidden. The locomotive will be 78% of the cell width and each wagon 62%, leaving a small gap between them so the couplings show through.
The coupling bar will be drawn before the cars so it remains visible between them. It will be a thick rounded line between segment centers, with a pin added at the connection point.
The coupling pin will sit at the midpoint between two cars, and shorter cars keep it visible in the gap. A soft yellow headlight glow will be drawn in world space after the locomotive, using localToWorld to place it at the front point.
The developer will prevent default keyboard arrow behavior and make the start handler idempotent so rapid clicks or key presses do not spawn duplicate games.
The developer is adding a `restartTimer` variable and clearing it inside the `die` function so the demo does not restart while the player is dead. When the demo dies, it will wait briefly before spawning a new train.
The death overlay will display two reasons: the train hit the yard edge or the cars folded together. The best score will update live during play. The stage will use `touch-action: none` and body overflow hidden to prevent scrolling. The dpad will use pointer events.
The browser tab title will be "Meadow Line — little train snake". Layout math was adjusted so the train stays large on desktop and mobile. At 1280 pixels wide the board becomes 672 by 448 with 56-pixel cells. On mobile the stage reserves a 118-pixel bottom band and leaves 450 pixels for the board.
The developer is narrowing the board to ten columns so the train stays large on mobile. On a 375-pixel screen the cell size will be 34 pixels. On desktop it will remain 56 pixels. The snake will start at column three and the first food will appear at column eight.
The board will use twelve columns and eight rows. Desktop cells remain fifty-six pixels. Mobile cells shrink to twenty-eight pixels. The snake field is larger on desktop. Mobile still reads clearly.
The stage will be taller with a 90-pixel bottom band and a 56-pixel overlay reserve. At 1280 pixels wide, the board leaves 304 pixels on each side for trees and scenery. The left margin will hold two trees, flowers, and an "ALL ABOARD" signpost. The right margin will feature a signal, another tree, flowers, and a water tower to complete the railway atmosphere.
The developer is now writing the complete code. The water tower roof will be drawn as a conical shape. The scene hierarchy is set: the train is the central focus, followed by passengers, stations, side scenery, and UI chrome.
A soft vignette will be added at the end to keep the train from looking muddy. The yard will use warm gravel instead of rich green grass so the play area is clearly bounded. The rendering order will be sky to horizon, then sun, clouds, and birds, then hills, then grass.
Scenery will be drawn before the yard so the frame sits on top of trees and flowers beside the board. The station will be drawn before the yard to keep it visible when overlapping. Steam particles will render above the station for a natural effect. Floaters, flash, and the vignette will be drawn last.
Particles will float freely into the sky without clipping. The HTML will pre-load score, aboard count, and best score at zero so the first paint shows numbers before JavaScript runs. A tiny CSS animation will be added to the lantern. The developer will use a custom roundRect helper instead of the browser's ctx.roundRect for broader compatibility.
The custom rounded-rectangle helper will be named `pathRound`. The canvas context will use round line joins and caps for smoother curves. The yard frame will be drawn after the gravel so it borders the warm ground. A subtle inner shadow will be added with a faint brown stroke around the yard edges.
The first passenger is placed five cells ahead of the train head so the locomotive never covers her at the start. Her label reads "Mina" and she waves. Her purple shirt matches the purple wagon that joins the train when she is picked up. The initial snake cars are blue, yellow, green, and red. The developer is now defining the WAGONS color palette.
The full WAGONS palette is now defined with eight high-saturation colors, including purple, orange, teal, and pink. The locomotive will be black and red to stand out from the wagons. The developer decided against adding a help line inside the yard frame to avoid clutter. An aria-label will be added to the canvas for accessibility.
The developer is adding a subtle headlight beam that only renders when the locomotive faces a direction. The AI pathfinding now guards against a null passenger before attempting to reach one.
The developer is now implementing the BFS pathfinding function. It uses a queue to search the grid for the nearest passenger, tracking visited cells and valid directions to avoid walls and the snake's own body.
The BFS pathfinding now returns the first valid step toward the passenger rather than the final cell. It checks the target before blocked cells so the snake can still move toward a passenger even if that cell is occupied. The developer is now writing the survival fallback to find free space when no passenger is present.
The `freeSpace` function counts reachable cells from a candidate position by running a BFS that excludes the snake head and any blocked cells. This lets the AI choose the safest direction when no passenger is present.
The developer confirmed the AI cannot reverse direction because the snake's neck blocks that move. The player's turn queue also prevents reversing. A `currentInterval` helper is being added to set the demo speed to 160 milliseconds.
The game loop now limits frame time to 48 milliseconds. It accumulates elapsed time and advances the snake only when the accumulated interval is reached, clamping any excess to half an interval. The loop also updates particles, floaters, and the shake, flash, and glow effects.
The render function now receives a progress value between zero and one based on how far the accumulated interval has advanced toward the next step. This value is clamped to one when the train dies, so the death animation freezes on the final frame. The interpolation between the previous snake position and the current one remains smooth even if the snake dies during the step.
The advance function now checks for collisions with the yard edges and with the snake's own body. When the locomotive reaches a passenger, the train gains a new colored wagon, the score increases by ten, and a burst of that wagon's color particles appears at the pickup spot.
The advance function now adds a score floater above the pickup spot, spawns a new passenger, and bumps the score display. When the train does not eat, it simply removes its tail. The developer also verified that the wagon color array stays one entry behind the snake length so the new color always maps to the correct tail wagon.
The developer realized the previous assumption about the color array was wrong. After unshifting a new head without popping the tail, the old wagons shift down by one index, so the color array no longer aligns with the snake segments. The first wagon now sits at index two instead of one, which would map the wrong color to the wrong car.
The developer realized the color array does not need to stay aligned with the snake indices. After the locomotive moves, the first wagon naturally occupies the old head position, so the color array can simply grow by one entry to match the new tail.
The developer confirmed that normal moves keep the color array aligned because the snake length stays the same. They are now writing the code and adding an updateFloaters function.
The `updateFloaters` function now decrements each floater's lifetime and upward velocity, then removes expired entries. A `bump` helper will verify the score element exists before animating it. The developer is now writing the complete file in one pass, finalizing the draw functions and station variables. Four cloud positions with varying heights, sizes, and speeds have been defined.
Clouds are drawn as layered white circles with a fractional horizon height and time-wrapped horizontal position. Birds are sketched with a dark outline and a quadratic curve for the wing.
The sun will sit in the upper right corner, offset from the station to avoid overlap. The developer is now writing the complete file in one pass with consistent code style. They will add a four-button directional pad using a CSS grid.
The directional pad grid is now fully defined with CSS grid areas for each button. The developer is concerned the cross-shaped pad may crowd the canvas on short screens, so they are considering a media query to collapse it into a single row when viewport height drops below 640 pixels.
The directional pad will collapse into a single row on short screens. A gold pinstripe and railway aesthetic will run throughout the design. The logo will be a side-view locomotive. The stage will use a pointer cursor to invite clicks.
The developer is now writing the `withFace` helper to handle locomotive facing. It uses canvas transforms to mirror or rotate the drawing context so the train can face left or down. Text will not be drawn inside mirrored contexts to avoid clipping. Wheel spokes will rotate in local space, so the visual spin direction reverses when the train faces left. The directional pad will also get a subtle hover effect.
The `withFace` helper now supports facing up as well. A `localToWorld` helper was added to map local coordinates to world space for the chimney and headlight based on the train's facing direction.
A `faceOf` helper is being added to determine each snake segment's facing direction from its position vector. It compares the current segment against the next one, falls back to the previous position for the tail, and finally uses the raw movement vector. The developer notes that during interpolation, the tail segment may have zero grid delta if it is newly added.
The `faceOf` helper now falls back to the previous segment's direction when a segment has no movement vector, preventing the tail from pointing the wrong way during interpolation.
The `faceOf` helper now avoids infinite recursion by falling back to the previous segment's position when a segment has no movement vector. A `segPos` function was added to interpolate segment positions between the previous and current snake states. A `bobY` helper is being added to make wagons bob up and down during movement, but it will return zero when the train is dead.
The `bobY` helper now uses a sine wave to make each wagon bob slightly during movement. The locomotive, wagons, and tracks will all draw at the origin facing right, with the renderer translating and orienting each piece before drawing. The `renderTrain` function is being drafted to draw the track segments and apply the wheel spin animation.
The renderer now draws each track segment with its correct facing direction. Passenger sprites are drawn after the tracks so they remain visible. Couplings connect the wagons. Ground shadows are cast beneath each car, with the locomotive shadow scaled to 0.42 and the wagons to 0.34. The wagons are drawn from tail to head so the locomotive stays on top.
The locomotive headlight glow is being drawn as a radial gradient in world space, positioned at the front of the train using the local-to-world helper.
The locomotive headlight glow is now drawn as a filled circle at the front of the train. Steam particles are emitted from the locomotive's smokestack, with an initial burst of fourteen particles when the train starts and a continuous stream while the train is alive. The `makeSteam` helper is being simplified to only require a position and lifetime, since smoke always rises vertically.
The `makeSteam` helper now gives each smoke particle a random horizontal offset, a strong upward velocity, and a radius between 3 and 7 pixels. The particle loop updates existing particles before rendering, then adds new ones during the same pass so they appear immediately and move on the next frame.
The render function is being structured to clear the canvas, apply the device pixel ratio, and set rounded line joins before drawing. Steam will not be emitted when the grid cell size is too small.
The render function now applies a subtle screen shake using sine and cosine offsets. It draws the backdrop, yard, and train only when the grid cell size is at least six pixels and the snake exists. The particle and floater layers are drawn next. A red flash overlay and a golden glow border are rendered conditionally. The developer will store boardW as cs times COLS.
The developer will store board dimensions as `boardW` and `boardH` and set them during layout. The backdrop drawing function will include sky, hills, grass, station, and trees, and it will still render the sky even when the grid cell size is zero. The boarding yard will display a "BOARDING YARD" label, and each waiting passenger will have a small platform number on their pole.
The developer finalized the color palette with warm gravel, bright sky, and deep green grass. They decided on solid grass rather than soft variations and will add only a few flower dots for life. The file will be around a thousand lines. They also began adding the meta viewport tag.
The developer added an iOS bounce prevention rule using `overflow: hidden`. They also verified that `drawBackdrop` safely handles a zero horizon value, ensuring the station and sky render correctly before the initial layout sets the grid cell size.
The render function will exit early when the canvas is too small to avoid drawing NaN text. The best score will persist across deaths because the `best` variable is never reset. A CSS `pop` animation will be triggered on passenger elements when they are bumped.
The `updateHUD` function will refresh the score, passenger count, best score, and current passenger name. The overlay, demo, and play entry points are ready. Input handlers will intercept arrow keys and space, but WASD will not block typing.
The developer changed their mind about WASD: they will now call `preventDefault` on all arrow keys, space, and Enter to stop scrolling. The keydown listener will also trigger `startPlay` on any keypress, though that call does nothing while the game is already running.
The developer verified that key presses during play and dead states behave correctly, and that the demo entry flow works as intended. They are now considering whether to ignore modifier-only keys to prevent accidental game starts.
Modifier-only keys will be ignored to prevent accidental game starts. Key repeats are harmless because startPlay exits immediately once the game is already running.
The developer confirmed that the dpad button listener will call `stopPropagation` to prevent the window-level pointerdown handler from firing and accidentally starting the game. They also verified that stage swipe and tap events will not conflict with the window listener, since a tap simply starts the game and defaults to moving right.
The stage pointerdown listener will call `preventDefault` to block browser drag-select. A slow swipe might briefly default to moving right before the turn applies, but that is acceptable. The developer is now ready to write the complete file.
A click ripple effect was skipped. The overlay animation will pulse only the chip's brightness instead of using transforms, keeping the overlay position stable. Box-sizing will be applied consistently across all elements. The developer is now writing the complete HTML file.
The developer is adding a `drawScenery` function to place trees and a signpost beside the boarding yard. Tree positions and heights scale with the yard width, and the signpost sits at a fixed offset from the yard top.
The developer added a right-side signal post, a water tower, and a distant tree to the `drawScenery` function. The signal sits beside the yard at a fixed vertical offset, and the tower and tree scale with the remaining screen width.
The "ALL ABOARD" signpost will appear on the left side of the yard. It is only drawn when the screen margin is wide enough, so it stays hidden on small viewports. The developer confirmed that on a 1280-pixel screen, the yard sits with a large left margin, leaving room for the signpost and other scenery. They also began adding a fixed pattern of colored flowers to the margins.
The developer added a `drawFlower` helper that draws a green stem and five petals in a random color. The `drawFlowers` function places eight fixed spots around the screen edges and skips any that fall inside the board area.
The developer finalized the `drawFlower` helper with a yellow center and five petals. They then compiled a complete list of every function needed for the file, including layout, HUD, passenger spawning, AI movement, and particle effects, to ensure nothing is missing before writing.
The developer mapped out every helper function needed for the game, from particle and floater updates to train drawing, passenger sprites, and backdrop elements. They will keep the code compact but readable.
The developer decided to keep the clock drawing separate from the station function for clarity. They began writing a `drawClock` helper that draws a white clock face with a dark border and calculates the current time to position the hour, minute, and second hands.
The `drawClock` helper is now complete with hour, minute, and second hands, a red center dot, and a dark border. A roof flag was considered but skipped. The developer is ready to write the full code.
The locomotive will not display "ML" text because M and L are not symmetric. A gold star badge will sit on the cabin instead, and it will remain circular when mirrored. The chimney top position is fixed at local coordinates x = 0.10 * u and y = -0.48 * u, and these exact values will be used in both the drawing and smoke emission functions.
The developer defined `CHIM_X`, `CHIM_Y`, and `NOSE_X` constants for the locomotive's chimney and front light. These fractions of the cell size will be used consistently in both `drawLoco` and `localToWorld`. They also planned a "Meadow Line" title treatment with a small subtitle, plus a footer hint that wraps cleanly.
The footer hint will display the current passenger name and accept arrow keys, WASD, or swipe input. The overlay subtext was finalized to explain the core mechanic: the train grows a colorful wagon for every passenger picked up.
A "DEMO RUN" chip will mark the demo state. The overlay message will read "Click or press a key to start". The developer is now doing a final mobile layout pass to prevent horizontal scrolling. They will set stat elements to `min-width: 0`, cap fixed widths below 375 pixels, and shrink the logo on small screens.
The mobile layout now hides the best-score stat and shrinks the remaining stats to 16 pixels. Images and SVGs are capped at 100 percent width. The page wrapper uses flexbox with `min-height: 0` so the stage shrinks when the footer wraps. Horizontal overflow is hidden on the root elements to prevent scrollbars.
A decorative railway track will be drawn beneath the station platform using dark brown ties and rails. The developer is now writing the complete file.
The developer will clip the station drawing so the decorative railway track beneath the platform does not overlap the boarding yard.
Scenery will be drawn outside the station clip so trees and other decorations beside the yard are not hidden. The render order is sky, clipped station, margin scenery, yard, then train. The layout ensures the station clip never overlaps the yard.