Train snakeOne-line
MiniMax M3.1 Flash
by MiniMaxReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 6 min 58 s
- Cost
- $0.018
- Output tokens
- 30,027
- Spent thinking
- 15,773 tokens
- Speed
- 72.2 tok/s
- Console errors
- 0
- First token after
- 1.9 s
- First visible text after
- 5 min 18 s
- Prompt tokens
- 532
- Code size
- 41.2 KB
- Finish reason
- stop
- Served by
- Together
- Model id
minimax/minimax-m3.1-flash- Reasoning sent
{"effort":"xhigh"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 1407 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rail Pickup — The Little Train Game</title>
<style>
:root {
--ink: #f5f7f2;
--muted: #91a6aa;
--navy: #08141d;
--panel: #10232d;
--panel-2: #142c35;
--line: rgba(180, 219, 214, 0.13);
--mint: #65dfbd;
--gold: #ffc85c;
--coral: #ff786b;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at 15% 8%, rgba(39, 115, 108, 0.28), transparent 34%),
radial-gradient(circle at 88% 90%, rgba(237, 123, 75, 0.12), transparent 32%),
#07131b;
color: var(--ink);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
min-height: 100vh;
overflow-x: hidden;
}
button { font: inherit; }
.app {
width: min(1120px, calc(100vw - 24px));
margin: 0 auto;
padding: 10px 0 14px;
}
.topbar {
min-height: 62px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 3px 4px 9px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.logo {
width: 46px;
height: 46px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 15px;
background: linear-gradient(145deg, #1c464a, #102730);
box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 8px 20px rgba(0,0,0,.2);
}
.logo svg { width: 36px; height: 36px; }
.brand-copy { min-width: 0; }
.eyebrow {
margin: 0 0 2px;
color: var(--gold);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.19em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(19px, 2.2vw, 27px);
line-height: 1;
letter-spacing: -0.035em;
}
.tagline {
margin: 5px 0 0;
color: var(--muted);
font-size: 12px;
}
.header-note {
display: flex;
align-items: center;
gap: 9px;
color: #b7c9c9;
font-size: 11px;
white-space: nowrap;
}
.header-note span {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--mint);
box-shadow: 0 0 12px rgba(101, 223, 189, .75);
}
.game-card {
position: relative;
overflow: hidden;
border: 1px solid rgba(165, 216, 211, .16);
border-radius: 23px;
background: #0b1b23;
box-shadow:
0 30px 70px rgba(0, 0, 0, .35),
0 2px 0 rgba(255, 255, 255, .035) inset;
}
.gamebar {
height: 57px;
padding: 0 17px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
background: linear-gradient(90deg, #122832, #0e2029);
border-bottom: 1px solid var(--line);
}
.station-id {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.station-dot {
width: 10px;
height: 10px;
flex: 0 0 auto;
border-radius: 50%;
background: var(--mint);
box-shadow: 0 0 0 5px rgba(101, 223, 189, .09), 0 0 15px rgba(101,223,189,.35);
}
.station-id strong {
display: block;
font-size: 11px;
letter-spacing: .08em;
}
.station-id small {
display: block;
margin-top: 2px;
color: var(--muted);
font-size: 9px;
}
.stats {
display: flex;
align-items: center;
gap: 9px;
}
.stat {
min-width: 75px;
padding: 6px 10px 5px;
border: 1px solid rgba(255,255,255,.065);
border-radius: 10px;
background: rgba(4, 13, 18, .22);
}
.stat span {
display: block;
margin-bottom: 1px;
color: #819ba0;
font-size: 8px;
font-weight: 800;
letter-spacing: .12em;
}
.stat strong {
font-size: 16px;
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.stat.score strong { color: var(--gold); }
.stat.boarded strong { color: var(--mint); }
.mode-pill {
display: flex;
align-items: center;
gap: 7px;
height: 29px;
padding: 0 10px;
border: 1px solid rgba(255, 200, 92, .22);
border-radius: 999px;
color: #ffe2a1;
background: rgba(255, 200, 92, .08);
font-size: 9px;
font-weight: 900;
letter-spacing: .1em;
white-space: nowrap;
}
.mode-pill i {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--gold);
box-shadow: 0 0 8px rgba(255, 200, 92, .7);
animation: blink 1.4s ease-in-out infinite;
}
.mode-pill.manual {
color: #a5f1db;
border-color: rgba(101,223,189,.22);
background: rgba(101,223,189,.08);
}
.mode-pill.manual i { background: var(--mint); box-shadow: 0 0 8px rgba(101,223,189,.7); }
@keyframes blink { 50% { opacity: .35; } }
.canvas-wrap {
position: relative;
background: #08151d;
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
touch-action: none;
outline: none;
}
.demo-banner {
position: absolute;
top: 11px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 9px;
max-width: calc(100% - 24px);
padding: 8px 13px;
border: 1px solid rgba(255, 255, 255, .13);
border-radius: 999px;
background: rgba(7, 18, 25, .91);
box-shadow: 0 7px 24px rgba(0,0,0,.25);
backdrop-filter: blur(10px);
pointer-events: none;
transition: opacity .2s, transform .2s;
white-space: nowrap;
}
.demo-banner strong {
color: #fff6d8;
font-size: 10px;
letter-spacing: .065em;
}
.demo-banner span {
color: #9db2b5;
font-size: 10px;
}
.demo-banner i {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--gold);
box-shadow: 0 0 10px rgba(255,200,92,.7);
}
.demo-banner.hidden {
opacity: 0;
transform: translate(-50%, -8px);
}
.game-footer {
min-height: 67px;
padding: 10px 15px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background: linear-gradient(90deg, #10252e, #0d1f27);
border-top: 1px solid var(--line);
}
.instructions {
display: flex;
align-items: center;
gap: 11px;
min-width: 0;
}
.instruction-icon {
width: 35px;
height: 35px;
flex: 0 0 auto;
display: grid;
place-items: center;
border-radius: 11px;
color: var(--gold);
background: rgba(255,200,92,.08);
border: 1px solid rgba(255,200,92,.12);
}
.instructions strong {
display: block;
font-size: 10px;
letter-spacing: .08em;
}
.instructions small {
display: block;
margin-top: 3px;
color: var(--muted);
font-size: 10px;
}
.control-cluster {
display: flex;
align-items: center;
gap: 13px;
}
.desktop-hint {
color: #8fa5a9;
font-size: 10px;
white-space: nowrap;
}
kbd {
display: inline-grid;
place-items: center;
min-width: 23px;
height: 23px;
padding: 0 5px;
margin: 0 1px;
border: 1px solid rgba(255,255,255,.12);
border-bottom-width: 2px;
border-radius: 6px;
color: #dbe8e7;
background: #172e38;
font: 700 10px ui-monospace, SFMono-Regular, Menlo, monospace;
}
.actions {
display: flex;
align-items: center;
gap: 7px;
}
.btn {
height: 34px;
padding: 0 12px;
border: 1px solid rgba(255,255,255,.12);
border-radius: 10px;
color: #dce9e8;
background: #19333d;
cursor: pointer;
font-size: 10px;
font-weight: 800;
letter-spacing: .04em;
transition: transform .15s, background .15s, border-color .15s;
}
.btn:hover { background: #23434d; border-color: rgba(255,255,255,.22); }
.btn:active { transform: translateY(1px); }
.btn.primary {
color: #10231f;
border-color: #7ce9c7;
background: var(--mint);
}
.btn.primary:hover { background: #83efd0; }
.touch-controls { display: none; gap: 5px; }
.touch-controls button {
width: 37px;
height: 37px;
display: grid;
place-items: center;
padding: 0;
border: 1px solid rgba(255,255,255,.13);
border-radius: 10px;
color: #e8f4f1;
background: #19333d;
font-size: 18px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.touch-controls button:active { background: #2b5159; transform: translateY(1px); }
.modal {
position: absolute;
inset: 0;
z-index: 5;
display: grid;
place-items: center;
padding: 20px;
background: rgba(4, 13, 18, .72);
backdrop-filter: blur(5px);
}
[hidden] { display: none !important; }
.modal-card {
width: min(360px, 100%);
padding: 26px;
border: 1px solid rgba(255,255,255,.14);
border-radius: 20px;
text-align: center;
background: linear-gradient(145deg, #17323b, #0e222b);
box-shadow: 0 24px 60px rgba(0,0,0,.42);
}
.modal-card .train-icon { font-size: 36px; margin-bottom: 8px; }
.modal-card h2 { margin: 0; font-size: 23px; letter-spacing: -.03em; }
.modal-card p { margin: 9px 0 18px; color: #a7bbbd; font-size: 12px; line-height: 1.55; }
.modal-card .btn { min-width: 145px; }
@media (max-width: 700px) {
.app { width: calc(100vw - 14px); padding-top: 6px; }
.topbar { min-height: 54px; padding-bottom: 7px; }
.logo { width: 40px; height: 40px; border-radius: 12px; }
.logo svg { width: 31px; height: 31px; }
.tagline, .header-note { display: none; }
.eyebrow { font-size: 8px; }
.game-card { border-radius: 18px; }
.gamebar { height: 54px; padding: 0 9px; gap: 8px; }
.station-id { display: none; }
.stats { flex: 1; justify-content: center; gap: 5px; }
.stat { min-width: 59px; padding: 5px 6px; }
.stat span { font-size: 7px; }
.stat strong { font-size: 14px; }
.mode-pill { width: 28px; padding: 0; justify-content: center; }
.mode-pill span { display: none; }
.game-footer { min-height: 57px; padding: 9px; justify-content: center; }
.instructions, .desktop-hint { display: none; }
.control-cluster { width: 100%; justify-content: space-between; gap: 8px; }
.touch-controls { display: flex; }
.demo-banner { top: 8px; gap: 6px; padding: 7px 9px; }
.demo-banner strong { font-size: 8px; }
.demo-banner span { display: none; }
}
@media (max-width: 390px) {
.actions .btn:not(.primary) { display: none; }
.btn.primary { padding: 0 10px; }
.touch-controls button { width: 34px; height: 35px; }
}
</style>
</head>
<body>
<main class="app">
<header class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true">
<svg viewBox="0 0 48 48" fill="none">
<path d="M9 35.5h29" stroke="#E9C26E" stroke-width="3" stroke-linecap="round"/>
<path d="M13 32V19c0-3 2.4-5 5.5-5h13C35 14 37 17 37 21v11" fill="#1BA58C" stroke="#071A20" stroke-width="2"/>
<path d="M13 21h12v11H13z" fill="#F4C75C"/>
<path d="M25 18h8.5c1.8 0 3.5 2 3.5 4v10H25z" fill="#EB665A"/>
<rect x="17" y="16" width="6" height="7" rx="1.5" fill="#C5F1E9"/>
<rect x="29" y="21" width="5" height="5" rx="1.5" fill="#C5F1E9"/>
<circle cx="18" cy="34" r="3.5" fill="#071A20" stroke="#A4B8B8" stroke-width="1.5"/>
<circle cx="31" cy="34" r="3.5" fill="#071A20" stroke="#A4B8B8" stroke-width="1.5"/>
<path d="M37 15v-4" stroke="#071A20" stroke-width="3" stroke-linecap="round"/>
<circle cx="39.5" cy="27" r="2" fill="#FFF4B4"/>
</svg>
</div>
<div class="brand-copy">
<p class="eyebrow">The friendly commuter express</p>
<h1>Rail Pickup</h1>
<p class="tagline">A little engine. A lot of happy passengers.</p>
</div>
</div>
<div class="header-note"><span></span> Night service is in full steam</div>
</header>
<section class="game-card" aria-label="Rail Pickup game">
<div class="gamebar">
<div class="station-id">
<span class="station-dot"></span>
<div>
<strong>GARDEN CENTRAL</strong>
<small>All platforms · Southbound line</small>
</div>
</div>
<div class="stats">
<div class="stat boarded"><span>BOARDED</span><strong id="boardedStat">0</strong></div>
<div class="stat score"><span>SCORE</span><strong id="scoreStat">0000</strong></div>
<div class="stat"><span>WAGONS</span><strong id="wagonStat">04</strong></div>
</div>
<div class="mode-pill" id="modePill"><i></i><span id="modeText">AI DEMO</span></div>
</div>
<div class="canvas-wrap">
<canvas id="game" width="960" height="540" aria-label="A colorful train collecting passengers at Garden Central station"></canvas>
<div class="demo-banner" id="demoBanner">
<i></i>
<strong>CLICK OR PRESS A KEY TO START</strong>
<span>AI demo is already on the move</span>
</div>
<div class="modal" id="modal" hidden>
<div class="modal-card">
<div class="train-icon" id="modalIcon">🚂</div>
<h2 id="modalTitle">All aboard!</h2>
<p id="modalText">Your route has been paused.</p>
<button class="btn primary" id="modalButton">Back on the rails</button>
</div>
</div>
</div>
<footer class="game-footer">
<div class="instructions">
<div class="instruction-icon" aria-hidden="true">◎</div>
<div>
<strong>COLLECT COMMUTERS</strong>
<small>Roll through the platform · Avoid the caboose</small>
</div>
</div>
<div class="control-cluster">
<div class="desktop-hint">
<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> or <kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd>
</div>
<div class="touch-controls" aria-label="Train controls">
<button data-dir="up" aria-label="Move up">↑</button>
<button data-dir="left" aria-label="Move left">←</button>
<button data-dir="down" aria-label="Move down">↓</button>
<button data-dir="right" aria-label="Move right">→</button>
</div>
<div class="actions">
<button class="btn" id="pauseButton">Pause</button>
<button class="btn" id="restartButton">New route</button>
<button class="btn primary" id="driveButton">Take the wheel</button>
</div>
</div>
</footer>
</section>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = 960, H = 540;
const COLS = 18, ROWS = 9, CELL = 50;
const GX = 30, GY = 62;
const GW = COLS * CELL, GH = ROWS * CELL;
const TICK = 138;
const boardedStat = document.getElementById("boardedStat");
const scoreStat = document.getElementById("scoreStat");
const wagonStat = document.getElementById("wagonStat");
const modePill = document.getElementById("modePill");
const modeText = document.getElementById("modeText");
const demoBanner = document.getElementById("demoBanner");
const pauseButton = document.getElementById("pauseButton");
const restartButton = document.getElementById("restartButton");
const driveButton = document.getElementById("driveButton");
const modal = document.getElementById("modal");
const modalTitle = document.getElementById("modalTitle");
const modalText = document.getElementById("modalText");
const modalButton = document.getElementById("modalButton");
let DPR = Math.min(window.devicePixelRatio || 1, 2);
const staticCanvas = document.createElement("canvas");
const staticCtx = staticCanvas.getContext("2d");
let segments = [];
let direction = { x: 0, y: -1 };
let queuedDirections = [];
let passengers = [];
let particles = [];
let floaters = [];
let score = 0;
let boarded = 0;
let pendingGrowth = 0;
let mode = "demo";
let gameState = "playing";
let paused = false;
let tickAccumulator = 0;
let lastFrame = performance.now();
let progress = 0;
let passengerId = 0;
const passengerColors = ["#ff796b", "#5da9ff", "#ffd166", "#b68cff", "#5de0b7"];
const skinTones = ["#f1b98c", "#9a6045", "#d99065", "#f4c7a1", "#754936"];
const wagonColors = ["#f16f61", "#4f91d9", "#f1bd46", "#9a73d2", "#48bfa1", "#ed8eb4"];
const cellCenter = (x, y) => ({ x: GX + x * CELL + CELL / 2, y: GY + y * CELL + CELL / 2 });
function roundedRect(c, x, y, w, h, r, fill, stroke, lineWidth = 1) {
c.beginPath();
c.roundRect(x, y, w, h, r);
if (fill) { c.fillStyle = fill; c.fill(); }
if (stroke) {
c.strokeStyle = stroke;
c.lineWidth = lineWidth;
c.stroke();
}
}
function configureCanvas() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
staticCanvas.width = Math.round(W * DPR);
staticCanvas.height = Math.round(H * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
staticCtx.setTransform(DPR, 0, 0, DPR, 0, 0);
buildStaticBoard();
}
function buildStaticBoard() {
const c = staticCtx;
c.clearRect(0, 0, W, H);
c.fillStyle = "#08151d";
c.fillRect(0, 0, W, H);
const glow = c.createRadialGradient(470, 235, 30, 470, 235, 540);
glow.addColorStop(0, "rgba(36, 93, 91, .18)");
glow.addColorStop(1, "rgba(8, 21, 29, 0)");
c.fillStyle = glow;
c.fillRect(0, 0, W, H);
c.fillStyle = "#e9f0e9";
c.font = "800 12px system-ui, sans-serif";
c.textBaseline = "middle";
c.fillText("GARDEN CENTRAL", 30, 25);
c.fillStyle = "#6f8c91";
c.font = "600 8px system-ui, sans-serif";
c.letterSpacing = "1px";
c.fillText("PLATFORM 1 • NIGHT LOCAL", 31, 42);
c.letterSpacing = "0px";
// Station clock.
c.save();
c.translate(897, 29);
c.beginPath();
c.arc(0, 0, 15, 0, Math.PI * 2);
c.fillStyle = "#17313a";
c.fill();
c.strokeStyle = "#789397";
c.lineWidth = 1.5;
c.stroke();
for (let i = 0; i < 12; i++) {
const a = i * Math.PI / 6;
c.beginPath();
c.moveTo(Math.sin(a) * 10, -Math.cos(a) * 10);
c.lineTo(Math.sin(a) * 12, -Math.cos(a) * 12);
c.strokeStyle = "#9bb0af";
c.lineWidth = 1;
c.stroke();
}
c.beginPath();
c.moveTo(0, 0); c.lineTo(-5, -7);
c.moveTo(0, 0); c.lineTo(8, 3);
c.strokeStyle = "#ffd166";
c.lineWidth = 2;
c.lineCap = "round";
c.stroke();
c.restore();
// Main yard.
roundedRect(c, 24, 56, 912, 458, 17, "#10282c", "rgba(165,216,211,.13)", 1);
c.save();
roundedRect(c, 24, 56, 912, 458, 17, null);
c.clip();
for (let r = 0; r < ROWS; r++) {
for (let col = 0; col < COLS; col++) {
const x = GX + col * CELL;
const y = GY + r * CELL;
c.fillStyle = (r + col) % 2 === 0 ? "#142f31" : "#122c2f";
c.fillRect(x, y, CELL, CELL);
}
}
// Raised platforms along the north and south edges.
c.fillStyle = "rgba(222, 204, 164, .16)";
c.fillRect(GX, GY, GW, CELL);
c.fillRect(GX, GY + (ROWS - 1) * CELL, GW, CELL);
c.fillStyle = "rgba(247, 222, 167, .62)";
c.fillRect(GX, GY + CELL - 3, GW, 3);
c.fillRect(GX, GY + (ROWS - 1) * CELL, GW, 3);
// Tiny garden details between the tracks.
for (let r = 1; r < ROWS - 1; r++) {
for (let col = 0; col < COLS; col++) {
if ((r * 7 + col * 3) % 13 === 0) {
const x = GX + col * CELL + 10;
const y = GY + r * CELL + 39;
c.fillStyle = "#20483d";
c.beginPath(); c.arc(x, y, 5, 0, Math.PI * 2); c.fill();
c.fillStyle = "#3a6950";
c.beginPath(); c.arc(x - 2, y - 2, 3, 0, Math.PI * 2); c.fill();
}
if ((r + col * 5) % 17 === 0) {
const x = GX + col * CELL + 40;
const y = GY + r * CELL + 10;
c.fillStyle = "#315044";
c.fillRect(x - 1, y, 2, 7);
c.fillStyle = "#56826a";
c.beginPath(); c.arc(x, y - 2, 4, 0, Math.PI * 2); c.fill();
}
}
}
// Ballast and sleepers.
c.lineCap = "butt";
for (let r = 0; r < ROWS; r++) {
const y = GY + r * CELL + CELL / 2;
for (let x = GX + 5; x < GX + GW; x += 13) {
c.beginPath(); c.moveTo(x, y - 8); c.lineTo(x, y + 8);
c.strokeStyle = "rgba(104,77,57,.35)"; c.lineWidth = 2; c.stroke();
}
for (const dy of [-4.2, 4.2]) {
c.beginPath(); c.moveTo(GX, y + dy); c.lineTo(GX + GW, y + dy);
c.strokeStyle = "rgba(161,175,165,.37)"; c.lineWidth = 1.25; c.stroke();
}
}
for (let col = 0; col < COLS; col++) {
const x = GX + col * CELL + CELL / 2;
for (let y = GY + 5; y < GY + GH; y += 13) {
c.beginPath(); c.moveTo(x - 8, y); c.lineTo(x + 8, y);
c.strokeStyle = "rgba(104,77,57,.28)"; c.lineWidth = 2; c.stroke();
}
for (const dx of [-4.2, 4.2]) {
c.beginPath(); c.moveTo(x + dx, GY); c.lineTo(x + dx, GY + GH);
c.strokeStyle = "rgba(161,175,165,.28)"; c.lineWidth = 1.15; c.stroke();
}
}
// Switch points make the yard feel like a real railway.
for (let r = 0; r < ROWS; r++) {
for (let col = 0; col < COLS; col++) {
const x = GX + col * CELL + CELL / 2;
const y = GY + r * CELL + CELL / 2;
c.beginPath(); c.arc(x, y, 3.2, 0, Math.PI * 2);
c.fillStyle = "#5a6861"; c.fill();
c.beginPath(); c.arc(x, y, 1.2, 0, Math.PI * 2);
c.fillStyle = "#b3a06b"; c.fill();
}
}
// Platform lamps and little destination signs.
[1, 5, 10, 15].forEach(col => {
const x = GX + col * CELL + 12;
const y = GY + CELL - 8;
c.fillStyle = "#263a3c"; c.fillRect(x - 1, y - 12, 2, 12);
c.beginPath(); c.arc(x, y - 13, 3.5, 0, Math.PI * 2);
c.fillStyle = "#ffe19a"; c.fill();
c.beginPath(); c.arc(x, y - 13, 7, 0, Math.PI * 2);
c.fillStyle = "rgba(255,222,139,.09)"; c.fill();
});
roundedRect(c, GX + 16, GY + 7, 57, 11, 3, "rgba(10,29,31,.7)", "rgba(255,255,255,.08)");
c.fillStyle = "#e5d8b5";
c.font = "800 6px system-ui, sans-serif";
c.fillText("BOARD HERE", GX + 22, GY + 12.5);
roundedRect(c, GX + GW - 73, GY + GH - 19, 57, 11, 3, "rgba(10,29,31,.7)", "rgba(255,255,255,.08)");
c.fillText("PLATFORM 1", GX + GW - 67, GY + GH - 13.5);
c.restore();
c.fillStyle = "#55757a";
c.font = "700 8px system-ui, sans-serif";
c.textAlign = "right";
c.fillText("THE FRIENDLY COMMUTER EXPRESS", 930, 29);
c.textAlign = "left";
c.fillStyle = "#3c6065";
c.font = "600 7px system-ui, sans-serif";
c.fillText("COLLECT • CONNECT • DON'T DERAIL", 931, 42);
}
function occupiedCells() {
const set = new Set();
for (const s of segments) set.add(s.y * COLS + s.x);
return set;
}
function platformCells() {
const cells = [];
for (let x = 0; x < COLS; x++) {
cells.push({ x, y: 0 });
cells.push({ x, y: ROWS - 1 });
}
return cells;
}
function randomPassenger(avoid = []) {
const occupied = occupiedCells();
const blocked = new Set(avoid.map(p => p.y * COLS + p.x));
let options = platformCells().filter(p =>
!occupied.has(p.y * COLS + p.x) && !blocked.has(p.y * COLS + p.x)
);
if (!options.length) {
options = platformCells().filter(p => !occupied.has(p.y * COLS + p.x));
}
if (!options.length) return null;
const p = options[Math.floor(Math.random() * options.length)];
return {
id: ++passengerId,
x: p.x,
y: p.y,
color: passengerColors[Math.floor(Math.random() * passengerColors.length)],
skin: skinTones[Math.floor(Math.random() * skinTones.length)],
phase: Math.random() * Math.PI * 2,
hair: Math.floor(Math.random() * 3)
};
}
function resetGame(startMode = "demo") {
segments = [];
const start = [{ x: 8, y: 4 }, { x: 8, y: 5 }, { x: 8, y: 6 }, { x: 8, y: 7 }, { x: 8, y: 8 }];
for (const p of start) segments.push({ x: p.x, y: p.y, px: p.x, py: p.y });
direction = { x: 0, y: -1 };
queuedDirections = [];
passengers = [];
particles = [];
floaters = [];
score = 0;
boarded = 0;
pendingGrowth = 0;
mode = startMode;
gameState = "playing";
paused = false;
tickAccumulator = 0;
progress = 0;
const initial = [
{ x: 8, y: 0, color: "#ff796b", skin: "#f1b98c", phase: .2, hair: 0, id: ++passengerId },
{ x: 4, y: 8, color: "#5da9ff", skin: "#754936", phase: 1.8, hair: 1, id: ++passengerId },
{ x: 14, y: 0, color: "#ffd166", skin: "#d99065", phase: 3.1, hair: 2, id: ++passengerId }
];
passengers = initial;
modal.hidden = true;
pauseButton.textContent = "Pause";
demoBanner.classList.toggle("hidden", mode !== "demo");
updateUI();
}
function updateUI() {
boardedStat.textContent = boarded;
scoreStat.textContent = String(score).padStart(4, "0");
wagonStat.textContent = String(Math.max(0, segments.length - 1 + pendingGrowth)).padStart(2, "0");
const manual = mode === "manual";
modePill.classList.toggle("manual", manual);
modeText.textContent = manual ? "YOU'RE DRIVING" : "AI DEMO";
}
function startManual() {
if (gameState !== "playing") return;
if (mode !== "manual") {
mode = "manual";
demoBanner.classList.add("hidden");
updateUI();
}
}
function enqueueDirection(x, y) {
startManual();
if (gameState !== "playing") return;
const base = queuedDirections.length ? queuedDirections[queuedDirections.length - 1] : direction;
if ((x === -base.x && y === -base.y) || (x === base.x && y === base.y)) return;
if (queuedDirections.length < 2) queuedDirections.push({ x, y });
}
function neighbors(x, y) {
const result = [];
if (x > 0) result.push({ x: x - 1, y });
if (x < COLS - 1) result.push({ x: x + 1, y });
if (y > 0) result.push({ x, y: y - 1 });
if (y < ROWS - 1) result.push({ x, y: y + 1 });
return result;
}
function bfsDirection(target) {
const head = segments[0];
const blocked = new Set();
const bodyLimit = pendingGrowth > 0 ? segments.length : segments.length - 1;
for (let i = 0; i < bodyLimit; i++) blocked.add(segments[i].y * COLS + segments[i].x);
const start = head.y * COLS + head.x;
const goal = target.y * COLS + target.x;
const prev = new Int16Array(COLS * ROWS).fill(-1);
const queue = [start];
prev[start] = start;
for (let qi = 0; qi < queue.length; qi++) {
const current = queue[qi];
const cx = current % COLS;
const cy = Math.floor(current / COLS);
for (const n of neighbors(cx, cy)) {
const ni = n.y * COLS + n.x;
if (prev[ni] !== -1 || blocked.has(ni)) continue;
prev[ni] = current;
if (ni === goal) {
let node = goal;
while (prev[node] !== start) node = prev[node];
return { x: node % COLS - cx, y: Math.floor(node / COLS) - cy };
}
queue.push(ni);
}
}
return null;
}
function safeScore(x, y, blocked) {
if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return -10000;
if (blocked.has(y * COLS + x)) return -10000;
let score = 0;
for (const n of neighbors(x, y)) {
if (!blocked.has(n.y * COLS + n.x)) score++;
}
return score;
}
function chooseAIDirection() {
const blocked = new Set();
const bodyLimit = pendingGrowth > 0 ? segments.length : segments.length - 1;
for (let i = 0; i < bodyLimit; i++) blocked.add(segments[i].y * COLS + segments[i].x);
const targets = [...passengers].sort((a, b) => {
const h = segments[0];
return (Math.abs(h.x - a.x) + Math.abs(h.y - a.y)) - (Math.abs(h.x - b.x) + Math.abs(h.y - b.y));
});
for (const target of targets) {
const d = bfsDirection(target);
if (d) return d;
}
const options = [
{ x: 0, y: -1 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }
].filter(d => !(d.x === -direction.x && d.y === -direction.y));
let best = null, bestScore = -Infinity;
for (const d of options) {
const nx = segments[0].x + d.x, ny = segments[0].y + d.y;
const score = safeScore(nx, ny, blocked) + Math.random() * .2;
if (score > bestScore) { bestScore = score; best = d; }
}
if (!best || bestScore < -1000) {
best = { x: -direction.x, y: -direction.y };
}
return best;
}
function stepGame() {
if (gameState !== "playing" || paused) return;
if (mode === "manual") {
if (queuedDirections.length) {
const next = queuedDirections.shift();
if (!(next.x === -direction.x && next.y === -direction.y)) direction = next;
}
} else {
direction = chooseAIDirection();
}
const head = segments[0];
const nx = head.x + direction.x;
const ny = head.y + direction.y;
const outside = nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS;
const tail = segments[segments.length - 1];
const hitsSelf = segments.some((s, i) => {
if (i === segments.length - 1 && pendingGrowth === 0) return false;
return s.x === nx && s.y === ny;
});
if (outside || hitsSelf) {
gameState = "gameover";
showModal("Off the tracks!", `You collected ${boarded} passenger${boarded === 1 ? "" : "s"} and scored ${score} points.`);
updateUI();
return;
}
segments.unshift({ x: nx, y: ny, px: head.x, py: head.y });
if (pendingGrowth > 0) {
pendingGrowth--;
} else {
segments.pop();
}
const picked = passengers.find(p => p.x === nx && p.y === ny);
if (picked) {
passengers = passengers.filter(p => p.id !== picked.id);
boarded++;
score += 100 + Math.min(boarded - 1, 8) * 5;
if (segments.length + pendingGrowth < 11) pendingGrowth++;
const center = cellCenter(nx, ny);
for (let i = 0; i < 14; i++) {
const a = Math.random() * Math.PI * 2;
const speed = 28 + Math.random() * 65;
particles.push({
x: center.x, y: center.y,
vx: Math.cos(a) * speed,
vy: Math.sin(a) * speed - 18,
life: .65 + Math.random() * .35,
max: 1,
color: picked.color,
size: 2 + Math.random() * 3
});
}
floaters.push({
x: center.x, y: center.y - 15,
text: "+" + (100 + Math.min(boarded - 1, 8) * 5) + " BOARDED",
life: 1.25, max: 1.25,
color: "#ffe28b"
});
const replacement = randomPassenger(passengers);
if (replacement) passengers.push(replacement);
}
updateUI();
}
function showModal(title, text, buttonText = "Back on the rails", icon = "🚂") {
modalTitle.textContent = title;
modalText.textContent = text;
modalButton.textContent = buttonText;
document.getElementById("modalIcon").textContent = icon;
modal.hidden = false;
}
function drawPassenger(p, time) {
const center = cellCenter(p.x, p.y);
const offset = p.y === 0 ? 14 : -14;
const bob = Math.sin(time * 2.4 + p.phase) * 1.2;
const x = center.x;
const y = center.y + offset + bob;
ctx.save();
ctx.translate(x, y);
const halo = ctx.createRadialGradient(0, 0, 2, 0, 0, 18);
halo.addColorStop(0, "rgba(255,218,116,.18)");
halo.addColorStop(1, "rgba(255,218,116,0)");
ctx.fillStyle = halo;
ctx.beginPath(); ctx.arc(0, 0, 18, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(0, 10, 9, 3, 0, 0, Math.PI * 2);
ctx.fillStyle = "rgba(0,0,0,.24)"; ctx.fill();
// Legs and shoes.
ctx.strokeStyle = "#243641"; ctx.lineWidth = 3; ctx.lineCap = "round";
ctx.beginPath(); ctx.moveTo(-3, 3); ctx.lineTo(-4, 9); ctx.moveTo(3, 3); ctx.lineTo(4, 9); ctx.stroke();
// Coat.
roundedRect(ctx, -6, -4, 12, 10, 4, p.color, "rgba(0,0,0,.25)");
ctx.fillStyle = "rgba(255,255,255,.25)";
ctx.fillRect(-2, -3, 4, 8);
// Arms.
ctx.strokeStyle = p.skin; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(-5, -2); ctx.lineTo(-8, 3); ctx.moveTo(5, -2); ctx.lineTo(8, 2); ctx.stroke();
// Head and hair.
ctx.beginPath(); ctx.arc(0, -9, 5.2, 0, Math.PI * 2);
ctx.fillStyle = p.skin; ctx.fill();
ctx.fillStyle = ["#3b2a24", "#8b4e2f", "#d5b05b"][p.hair];
ctx.beginPath();
if (p.hair === 0) ctx.arc(0, -10.3, 5.2, Math.PI, Math.PI * 2);
else if (p.hair === 1) { ctx.ellipse(0, -11, 5.6, 3.2, 0, 0, Math.PI * 2); }
else { ctx.arc(-1, -10.5, 5.1, Math.PI * 1.05, Math.PI * 1.95); }
ctx.fill();
// Tiny ticket.
roundedRect(ctx, 6, -4, 4, 5, 1, "#fff0b9", null);
ctx.restore();
}
function drawWagon(x, y, angle, index, isLast) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillStyle = "rgba(0,0,0,.27)";
ctx.beginPath(); ctx.ellipse(0, 2, 20, 15, 0, 0, Math.PI * 2); ctx.fill();
// Couplings.
ctx.strokeStyle = "#26383b"; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(-23, 0); ctx.lineTo(-17, 0); ctx.moveTo(17, 0); ctx.lineTo(23, 0); ctx.stroke();
// Wheels, visible along both sides.
for (const wx of [-10, 9]) {
for (const wy of [-15, 15]) {
roundedRect(ctx, wx - 4, wy - 2, 8, 4, 2, "#081217", "#526466");
}
}
const color = isLast ? "#d95754" : wagonColors[(index - 1) % wagonColors.length];
roundedRect(ctx, -19, -13, 38, 26, 6, color, "rgba(4,15,19,.65)", 1.5);
roundedRect(ctx, -16, -11, 32, 3, 2, "rgba(255,255,255,.33)");
// Windows and riders.
const faceCount = Math.max(0, Math.min(2, boarded - (index - 1) * 2));
for (let w = 0; w < 2; w++) {
const wx = w === 0 ? -8 : 6;
roundedRect(ctx, wx - 4, -7, 8, 9, 2, "#d9eee8", "rgba(20,44,48,.7)");
if (w < faceCount) {
ctx.beginPath(); ctx.arc(wx, -3.7, 2.4, 0, Math.PI * 2);
ctx.fillStyle = ["#f2b98d", "#925c42", "#d99569"][(index + w) % 3]; ctx.fill();
ctx.fillStyle = "#372c28";
ctx.beginPath(); ctx.arc(wx, -4.5, 2.4, Math.PI, Math.PI * 2); ctx.fill();
}
}
// Lower trim and lamp.
roundedRect(ctx, -16, 6, 32, 4, 2, "rgba(20,43,47,.28)");
ctx.beginPath(); ctx.arc(16, 8, 1.6, 0, Math.PI * 2);
ctx.fillStyle = "#ffe49b"; ctx.fill();
if (isLast) {
roundedRect(ctx, -21, -9, 5, 18, 2, "#f3c85d", "rgba(0,0,0,.3)");
ctx.fillStyle = "#ffdf83"; ctx.fillRect(-23, -6, 2, 12);
}
ctx.restore();
}
function drawEngine(x, y, angle, time) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillStyle = "rgba(0,0,0,.3)";
ctx.beginPath(); ctx.ellipse(0, 2, 26, 17, 0, 0, Math.PI * 2); ctx.fill();
// Coupling.
ctx.strokeStyle = "#2a3a3d"; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(20, 0); ctx.lineTo(28, 0); ctx.stroke();
// Wheels.
for (const wx of [-15, 9]) {
for (const wy of [-16, 16]) {
roundedRect(ctx, wx - 4.5, wy - 2.5, 9, 5, 2.5, "#071217", "#59696a");
}
}
// Chassis.
roundedRect(ctx, -25, -14, 49, 28, 7, "#1a3034", "rgba(0,0,0,.65)", 1.5);
// Golden boiler.
const boiler = ctx.createLinearGradient(0, -13, 0, 13);
boiler.addColorStop(0, "#ffe29a");
boiler.addColorStop(.48, "#efbb50");
boiler.addColorStop(1, "#c88935");
roundedRect(ctx, -7, -12, 29, 24, 10, boiler, "rgba(66,44,22,.55)", 1.2);
ctx.strokeStyle = "rgba(255,255,255,.42)"; ctx.lineWidth = 1.4;
ctx.beginPath(); ctx.moveTo(0, -8); ctx.lineTo(14, -8); ctx.stroke();
// Teal cab.
roundedRect(ctx, -25, -17, 20, 34, 5, "#159486", "rgba(3,29,29,.8)", 1.5);
roundedRect(ctx, -21, -13, 12, 13, 2.5, "#c9eee5", "rgba(255,255,255,.4)");
roundedRect(ctx, -21, 4, 12, 5, 2, "#087267");
ctx.fillStyle = "#f2c25d";
ctx.fillRect(-25, -2, 20, 3);
// Chimney and steam.
roundedRect(ctx, 7, -21, 6, 12, 2, "#203438", "rgba(0,0,0,.4)");
roundedRect(ctx, 5, -23, 10, 4, 2, "#34494a");
for (let i = 0; i < 3; i++) {
const phase = (time * .7 + i * .31) % 1;
ctx.beginPath();
ctx.arc(-1 - phase * 18, -24 - phase * 11, 3 + phase * 4, 0, Math.PI * 2);
ctx.fillStyle = `rgba(225,240,234,${.22 * (1 - phase)})`;
ctx.fill();
}
// Cowcatcher and lamp.
ctx.beginPath();
ctx.moveTo(22, -10); ctx.lineTo(31, -5); ctx.lineTo(31, 5); ctx.lineTo(22, 10);
ctx.closePath(); ctx.fillStyle = "#f1c75d"; ctx.fill();
ctx.strokeStyle = "#9f7331"; ctx.lineWidth = 1; ctx.stroke();
ctx.beginPath(); ctx.arc(22, 0, 4, 0, Math.PI * 2);
ctx.fillStyle = "#fff0a9"; ctx.fill();
ctx.strokeStyle = "#705127"; ctx.lineWidth = 1; ctx.stroke();
ctx.restore();
}
function drawTrain(time) {
const positions = segments.map(s => ({
x: GX + (s.px + (s.x - s.px) * progress) * CELL + CELL / 2,
y: GY + (s.py + (s.y - s.py) * progress) * CELL + CELL / 2
}));
// Couplings beneath the cars.
ctx.strokeStyle = "#26383a";
ctx.lineWidth = 4;
ctx.lineCap = "round";
for (let i = 1; i < positions.length; i++) {
ctx.beginPath();
ctx.moveTo(positions[i - 1].x, positions[i - 1].y);
ctx.lineTo(positions[i].x, positions[i].y);
ctx.stroke();
}
for (let i = segments.length - 1; i >= 1; i--) {
const s = segments[i];
const angle = Math.atan2(segments[i - 1].y - s.y, segments[i - 1].x - s.x);
drawWagon(positions[i].x, positions[i].y, angle, i, i === segments.length - 1);
}
const head = segments[0];
const headAngle = Math.atan2(head.y - head.py, head.x - head.px);
drawEngine(positions[0].x, positions[0].y, headAngle, time);
}
function updateEffects(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 90 * dt;
p.vx *= .985;
if (p.life <= 0) particles.splice(i, 1);
}
for (let i = floaters.length - 1; i >= 0; i--) {
const f = floaters[i];
f.life -= dt;
f.y -= 25 * dt;
if (f.life <= 0) floaters.splice(i, 1);
}
}
function drawEffects() {
for (const p of particles) {
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
}
ctx.globalAlpha = 1;
for (const f of floaters) {
ctx.save();
ctx.globalAlpha = Math.min(1, f.life * 2);
ctx.fillStyle = f.color;
ctx.strokeStyle = "rgba(5,17,22,.9)";
ctx.lineWidth = 3;
ctx.font = "900 11px system-ui, sans-serif";
ctx.textAlign = "center";
ctx.strokeText(f.text, f.x, f.y);
ctx.fillText(f.text, f.x, f.y);
ctx.restore();
}
}
function render(time) {
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.clearRect(0, 0, W, H);
ctx.drawImage(staticCanvas, 0, 0, W, H);
// Dynamic waiting count.
ctx.fillStyle = "#7d999d";
ctx.font = "700 8px system-ui, sans-serif";
ctx.textAlign = "right";
ctx.fillText(`${passengers.length} WAITING`, 790, 43);
ctx.textAlign = "left";
for (const p of passengers) drawPassenger(p, time);
drawTrain(time);
drawEffects();
}
function frame(now) {
const dt = Math.min(.05, (now - lastFrame) / 1000);
lastFrame = now;
if (gameState === "playing" && !paused) {
tickAccumulator += dt * 1000;
while (tickAccumulator >= TICK) {
tickAccumulator -= TICK;
stepGame();
}
progress = Math.min(1, tickAccumulator / TICK);
updateEffects(dt);
} else {
progress = 1;
}
render(now / 1000);
requestAnimationFrame(frame);
}
function togglePause() {
if (gameState !== "playing") return;
paused = !paused;
pauseButton.textContent = paused ? "Resume" : "Pause";
if (paused) {
showModal("A brief stop", "Your train is waiting safely at the station.", "Keep rolling", "⏸");
} else {
modal.hidden = true;
}
}
document.addEventListener("keydown", e => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
const key = e.key.toLowerCase();
const directions = {
arrowup: [0, -1], w: [0, -1],
arrowdown: [0, 1], s: [0, 1],
arrowleft: [-1, 0], a: [-1, 0],
arrowright: [1, 0], d: [1, 0]
};
if (directions[key]) {
e.preventDefault();
enqueueDirection(directions[key][0], directions[key][1]);
} else {
if (mode === "demo") startManual();
if (key === "enter" && gameState === "gameover") {
e.preventDefault();
resetGame("manual");
}
}
});
canvas.addEventListener("pointerdown", () => {
startManual();
canvas.focus();
});
let touchStart = null;
canvas.addEventListener("pointerdown", e => {
touchStart = { x: e.clientX, y: e.clientY };
});
canvas.addEventListener("pointerup", e => {
if (!touchStart) return;
const dx = e.clientX - touchStart.x;
const dy = e.clientY - touchStart.y;
if (Math.max(Math.abs(dx), Math.abs(dy)) > 24) {
if (Math.abs(dx) > Math.abs(dy)) enqueueDirection(dx > 0 ? 1 : -1, 0);
else enqueueDirection(0, dy > 0 ? 1 : -1);
}
touchStart = null;
});
document.querySelectorAll("[data-dir]").forEach(button => {
button.addEventListener("pointerdown", e => {
e.preventDefault();
const d = button.dataset.dir;
if (d === "up") enqueueDirection(0, -1);
if (d === "down") enqueueDirection(0, 1);
if (d === "left") enqueueDirection(-1, 0);
if (d === "right") enqueueDirection(1, 0);
});
});
driveButton.addEventListener("click", startManual);
pauseButton.addEventListener("click", togglePause);
restartButton.addEventListener("click", () => resetGame("demo"));
modalButton.addEventListener("click", () => {
if (gameState === "gameover") resetGame("manual");
else {
paused = false;
pauseButton.textContent = "Pause";
modal.hidden = true;
}
});
window.addEventListener("resize", configureCanvas);
configureCanvas();
resetGame("demo");
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (15,773 tokens)
We need create HTML self contained. Need output exact code block. Need plan polished snake train game, visible first paint, autoplay demo with click/key start message per dev. Need perhaps canvas plus UI. No external assets. Need playable via keyboard arrows WASD, maybe touch controls. Train snake: locomotive head, colorful wagons body; track maybe railway path around garden station, platforms with passengers waiting. Picking passengers by train passing platform? Need gameplay snake collect passengers. We can make grid movement train segments along winding rail path, perhaps orthogonal grid with round corners. Snake moves continuous grid, locomotives orientation. Collect passengers at platform cells. Use canvas 960x560? View 1280x800. Design full page dark/navy station aesthetic.
We need ensure train shape looks locomotive and colorful wagons. Grid movement. Canvas can render smooth rails track based fixed path? Classic snake grid but themed to rails. Use board cells. Better movement on a grid of railway track path where train moves tile to tile; rails connect full horizontal/vertical lines, allowing turns at intersections. Passengers stand on platform tiles (near track perhaps above). Pickup when head moves to passenger cell. But "waiting on platform" visually. Could place passenger icons on track adjacent maybe platform tile exactly train can collect when moving over. Each passenger located on a tile, render platform background with waiting people, so picking up. Could draw platform along selected stations.
Could use grid 18x11? Canvas 960x560, cell 48/50. Use W=960 H=560 cols=20 rows=11 cell 48. Border. Train length maybe 5 wagons, head locomotive. Controls changes direction, self play AI. Passengers with point value. Goal maybe collect 12 passengers, avoid trees/closed station doors? Need meaningful. We can have passengers as collectibles on board and passengers in wagon visible? Mechanic: pass platform and passengers board train. Collection count, score, train length increases per collected? Snake mechanics - train itself snake, collisions hit end; pickups make train longer. Good.
AI mode. Need user click overlay to begin? Requirement games start self-playing demo mode behind "Click or press a key to start" message. Could have overlay visible centered "READY TO DEPART" and subtext "Click or press a key to start • Watching demo" but AI must play behind. After 3 sec screenshot, perhaps overlay remains with animated train behind. We can auto start AI immediately but overlay message says Click or press a key to start and "Demo departs automatically". They can click/key to take control. Ensure initial meaningful. Overlay semi transparent maybe card in center but don't obscure all. "Click or press a key to start" exactly. AI moving. Could let click anywhere canvas or page starts manual? Then no direction specified, starts moving based AI current direction. Key click. We can display top header. Layout 1280x800 maybe game fills. Use 1120x700? Side panel and main canvas. But canvas CSS scales. Responsive. Need no external fonts. Use system fonts. Rich visual aesthetic.
Maybe render 2D vector canvas, no images. Board with dark blue/green station at dusk, tracks. Top UI and bottom controls. Canvas 1000x600. Fit within. 1280x800: header 80, game panel canvas 1000x600 = 680 plus footer = too high. Need at 1280x800 content maybe 760. Use canvas logical 960x560 and CSS width calc; main wrapper max 1180. Header 64, game canvas ~600, footer 70 = 734. Good.
Canvas dimensions 960x540. Grid 18x10 cell 50 =>900x500, margins 30, track. Let's make canvas 960x560, board area x=30 y=30 width=900 height=500, 18 cols,10 rows cell50. Render rails perhaps entire grid, lots of crossing confusing. Instead generate fixed railway "snake board" with path graph. Could draw track on every cell as connected horizontal/vertical but train can move any direction. For rails at every cell, crossings. Use rounded rails dark gray lines; ties. Draw path style like a continuous rectangular serpentine route: rows alternating; but snake can turn any adjacent direction and paths may not exist. Need graph. We can construct board as tile garden with all tile routes (station yard). Draw rails on cells based route adjacency dynamically? Since train can travel any grid direction, draw every cell connected orthogonally as rails crossing. Visual looks like rail map, possible. Passengers on tile appear "platform" with side platform around.
Could render train center positions with body wagons oriented direction. Each segment should connect visually to previous, with couplings. If turn, train wagons each oriented toward previous segment. Engine front points direction. Snake body train: head plus wagons. Need add locomotive and colorful wagons. Head cell center. Engine oriented direction. Draw undercarriage, wheels, body, chimney, cab, pilot. Wagons rotate according to vector from head? For segment i, direction from segment to previous (toward head) perhaps orientation. Body linked via couplers. Draw wagons rear-to-front so head on top. Each wagon colorful, passengers heads in windows when collect. Engine teal/cream with red? Colorful.
Movement tile-based every ~110ms, interpolation. AI every tick chooses direction to target passenger or food, avoid collisions. Manual mode with arrows. Game starts AI. Passengers spawn on random free cells, ideally enough space. Collect when head tile equals passenger tile. Each pickup: train extends by one wagon, score +100, passengers count. Passenger wait platform art: tile platform strip and 2-3 tiny people. Could have several passenger groups at once? Snake game usually one food. "passengers waiting on the platform" plural. We can spawn 3 passenger tiles simultaneously, pickup any. AI target nearest. Increase length each collectible, maybe each group is one passenger. Count passengers. Maybe wagons max 8, points. On collision game over. AI will likely keep playing 3 sec. Need AI not immediately collide. Board obstacles? Train collision with own tail only. At 18x10 and length 5, AI path with grid could self-collide quickly. Implement AI with BFS or simple heuristic and no reversal. Self-collision predicts if next occupied except tail. Could be okay with length 5 and lots space. But snake body growth length. Random food.
Alternatively board with rails path designed large loops but still grid.
Train movement and direction. Need smooth interpolation: maintain prev positions and current. On each tick, update body array: [newHead, ...body], pop if not growing. Draw each segment interpolated from prev center to current center over tick duration. But body list segments reorder every tick and prev aligns each index. Use state positions arrays objects x,y, prevX,prevY. For each tick:
newHead = head+dir
segments.unshift({x,y,prevX:head.x,...}); if length > max pop
Interpolate from prev to current.
Direction visual from current position minus previous? head to prev vector points back; front opposite. Better orientation movement vector = new x - old x for head. For wagons, vector current - previous segment? Since body order front to rear, segment i following segment i-1, direction from prev segment to current rear? Wagon's long axis along line connecting to previous (forward). use angle atan2(prev.y-current.y, prev.x-current.x) for i>0; head uses movement angle (new-old). At turns, wagon orientation follow path.
Input buffer. Update direction on key. Prevent reverse. Could queue up to 2. For touch arrows.
Passenger rendering. Need platform visible. Pickup cell maybe render platform along bottom of tile? Train overlaps them. Passenger(s) wait "on platform" at cell but train moves on adjacent track? Could logically board train if head arrives. Place platform on tile itself. Draw a light beige strip maybe full tile and sign, people at back. Engine will overlap. Fine. Could perhaps offset passenger above track in same cell, and collectible halo center.
Game info. Header: logo icon train, title "Platformbound" maybe "RAIL PICKUP". Subtitle "The little train that collects happy commuters". top right badges Demo Live, passengers, score, best? No local storage. "Best" current session memory. Could use difficulty toggle? Keep.
Main layout maybe canvas and right side 220 station board. At 1280 fits. But responsive at 375. A single card canvas with top status. Since canvas aspect 12:7. On 375, scaled width 343 => height200, too small but acceptable? Need adapt gracefully. Could use canvas CSS width 100%, aspect. Touch controls below. On mobile, board small but no overflow. Maybe at <=600, header stack, hide side. Controls.
Could build canvas responsive with logical 960x560 and CSS width:100%. Fine.
Top app within max-width 1180. header 78. game-shell includes top status strip 54, canvas 560 aspect (actual at width 1180 maybe height688 due ratio 1.714; 1180/1.714=688 too tall). Need canvas logical ratio 16:9 maybe W=960 H=540. At 1180 =>663, header 70 + info 60 + total793 + footer controls no. Better max-width 1120 and canvas ratio 16:9 =>630. header 70 + strip50 + canvas630 + footer maybe60 =810. At 1280x800 slight scroll but okay? "look right in viewport" likely screenshot maybe full page? Prefer fit: max-width 1080, canvas 607, header64, game top52, bottom50 =773. Good. At 1280 margins100. We can set game area width min(1080px, calc(100vw - 32px)). Canvas width 100%, height auto ratio 16/9. total 64+52+607+70=793 exactly. Body padding maybe 0. Header 72. status bar 56. canvas 607. footer 58. total793. Great. On 1280x800 fits. At width smaller, scroll vertically fine.
Need implement decorative background no external. CSS body background #091420 with radial gradients. App header.
Canvas drawing detailed. Let's design coordinate board:
W=960 H=540.
marginX=30, marginY=30. cols=18 rows=9? 18*50=900, 9*50=450, total height 510 leaving. H540. Board x30,y45 perhaps. Better grid 18x9, cell50, board 900x450, y=70? header within canvas top 28 "CENTRAL GARDEN STATION", subtitle. Canvas board area y=70 to520. 9 rows. At 1080 scaled x1.125. Nice. Use W960 H540, gridX=30 gridY=64, cell50, boardW900 boardH450. Outer.
Cell 50. Track lines along centers x=55 etc. Entire grid. Draw board tiles with alternating #102c2c, etc. rails horizontal/vertical all cells. Actually drawing every row and column creates 9 horizontal and 18 vertical tracks, like rail yard. Trains can navigate. Could draw a "network" graph all cells. To reduce visual clutter, draw tracks in a snake path? But movement unrestricted can leave rails. Better define track graph that covers all cells maybe all horizontal lines plus vertical connectors at selected cols, but AI moves through. Could have railway switch yard. Yet user expects snake, grid lines acceptable if styled.
Maybe instead draw track under train dynamically only along its segments? But snake game train can move anywhere. Could draw faint grid garden and a winding highlighted route through entire board; train movement on grid. Passenger on station platforms next to highlighted route, pickup only on route. More work but pretty. Let's create fixed closed loop route that fills board like a serpentine? A train can travel only along route, with branches? Snake mechanics less flexible. Path coordinates sequence for snake board:
Start maybe (3,7) moving right. Use rows serpentine: row7 x1..16, up at16 to6, row6 right? To preserve continuous route:
Path a Hamiltonian snake:
row7 x1..16 (16 points)
row6 x16
row5 x16 down to x1
row4 x1
row3 x1 to16
row2 x16
row1 x16 to1
Then row0? This path forms serpentine with only one-way turns. Train can reverse only at ends, but snake collision unlikely. Passengers along. Could support reverse direction along path and train follows rail route, but snake grows and catches? If train tail follows path and passenger growth. UI arrows should choose branches? only reverse. Not ideal.
Use graph with track path and intersections. Grid all tracks okay.
Could render rails as faint graph with switch highlights. The whole board is "railyard" and tracks can turn. Tiny sleepers. To avoid grid clutter, draw horizontal tracks every row and vertical lines at each 3 columns. But train can go vertical any column. no.
Could draw only rail routes connecting adjacent possible moves dynamically, perhaps any adjacent cell (all tracks) but sleepers and rails low contrast. At intersections, looks railroad yard. Good. Add foliage blocks between tracks? tile backgrounds. Board appears a large yard. Maybe station platforms along top row and right side. Passenger spawn on designated "platform tiles": top row cells and side col. But only 25 possible, enough. Train starts center. Passengers visibly wait on raised platform strip. Use platform cells: rows0 (x=2..15) and col17 maybe. Passenger randomly among platform cells, 3 at once. But if snake collects one, train enters platform. Rail there. Render top row as raised cream platform and signs. Each passenger person. There can be groups. AI goes to passenger.
Train length 6, max 9 maybe. Collect passenger => +1 wagon up to 10, then body remains? Snake if max can still grow? maybe max 12. Keep max 10. Passengers count. Board enough. Score. "Next stop" maybe status.
Better put platform cells along bottom and top, and left/right sides, visually. Spawn from all platform cells except occupied. Need platform render behind train; each platform tile draw sidewalk with benches, shelter. But top row across gives nice station platform. Vertical side rows.
Use logical canvas:
- Background outside board with dark teal.
- Board rectangle with rounded rect.
- grid cell dark #10272a.
- draw grid subtle lines and rail routes perhaps track network.
- Platforms: top row y=64..114 and bottom row y=464..514, maybe. Draw cream sidewalk and canopy near top? But train tracks along center of cells; okay.
- Interior decorative shrubs/lamp posts in cells corners, but avoid collision confusion. Could mark "trees" as obstacles? Maybe not. Add scenery only in cell corners.
- Each platform tile has waiting passenger art at center. But train on same center overlaps. Offset person to y+15? pickup when train reaches. Fine.
- Spawn 3 people on distinct platform tiles. Render little person above track center; train center catches.
Wait passengers are on platform tile but train route also same cell. Could set pickup condition train head distance to platform cell. It will overlap person, then disappear with boarding animation. That's acceptable.
Visual:
Canvas draw order:
1 bg, board.
2 platform strips + rail network.
3 passengers (except board effect?)
4 train segments maybe.
5 particles and floating score.
Need board passengers behind train.
Rail draw: for each cell center, horizontal track from x to x+50? Vertical? Draw all horizontal across board and all vertical. Rails color #33484d and sleepers #3c3330. But lots.
Alternative draw only track center lines per rows/cols with low opacity. Since train body covers.
Could instead create a pattern of connected route edges all grid edges. Draw rails edge segments: each cell's outgoing right/down edges. This same lattice. Sleepers perpendicular. For horizontal edge: two horizontal parallel lines y±4, ties vertical every 10. Vertical edge similar. Junction can be messy but okay.
Set rails dark gold-ish #5e625e and ties #2b3131. Use globalAlpha 0.55.
Could avoid full grid by generate connected maze route graph using DFS Hamiltonian path, then train can only move along graph. But manual control less intuitive and collisions self. Yet snake games grid route not strict. Keep grid.
Perhaps use aesthetic isometric? no.
Need AI robust:
State mode 'demo' initially, but movement starts. Overlay banner in canvas? Could HTML overlay absolute over canvas with pill "Click or press a key to take the wheel" at bottom, not central, so action visible. Requirement behind message. Could place top center card translucent small. Better canvas itself draws message? HTML overlay with pointer-events:none, position absolute top:18px left50. It can hide station title but okay. Add "DEMO RIDE • Click or press a key to take control". This persists until user interaction. AI moves.
Upon user arrow/WASD click, mode='manual', overlay hidden. Ensure click starts manual maybe if click canvas: mode manual and direction unchanged. Any key starts and controls if recognized; Space/Enter maybe manual but no direction change. Requirement key start. We can treat any key from set. Touch arrow controls buttons.
AI choose target: nearest passenger. For each candidate direction nonreverse. Avoid occupied body cells (except tail if no growth). Choose candidate that decreases Manhattan distance to target, with tie randomized; if blocked choose safe with BFS maybe. With full grid and length <=10, should safe. But AI direct pursuit can hit itself around turns. Implement BFS on grid avoiding body to target. At each tick find shortest path from head to target via cells not body (except tail perhaps), choose first direction. If no path, safe direction maximizing distance. This ensures. But if target platform and path crosses body, choose another passenger target with BFS. Score. 3 passengers spawn, choose reachable nearest.
BFS treats board boundaries. Need body occupancy set excluding tail if not growing. Return path from head. Choose direction path[1]-head. Good.
If target unreachable all, pick safe direction maximizing distance to body? Use candidates and random among safe, prefer space count. For demo indefinite.
Manual game over if collision. Could then auto restart demo? In demo collision can happen and game over screenshot undesirable. AI should avoid. If manual game over show overlay "Back on the rails?" no confirm. Could include Restart button. Need controls. We'll implement game over overlay HTML hidden, with score and button "Run another route". But button no prompt okay.
Maybe not game over in 3 sec.
Demo status top badge "AI DEMO". passenger count 0 score 000. After click manual. There should maybe initial score and train in action. AI targets passenger in platform row, takes around. Start head at (7,4) dir left? Passenger positions. If AI moves 100ms per tick, in 3 sec 30 cells, reaches target. Good. First passenger at x=9,row0, distance5. Start head (7,4), direction up maybe AI can choose up. Place passenger (8,0) dist5, collected ~0.6 sec. Then spawn new.
Need set initial body extends down from head if moving up. body [7,4 head, 7,5,7,6,7,7,7,8] length6, direction up. AI BFS target (8,0) chooses up then right etc no collision.
AI can collect passenger and growth. Render engine. Need draw after animation. tick interval maybe 135ms. At 3 sec 22 ticks.
Interpolation: tick starts at performance.now. draw progress. State positions update. At tick function called via setInterval? Use requestAnimationFrame and accumulator robust. let lastTick; in loop dt; if >= tickMs updateTick and reset. At each tick, positions prev=current and update. Use 135ms.
Manual input changes desiredDirection; actual direction can change only at next tick. Need queue. We can set pendingDir if valid relative to current/pending. We use dir current; key event if mode demo switch manual; call queue direction. Reverse relative to current disallowed. On first key ArrowUp etc. queue array. Maybe if turn input sequence. Simpler desiredDir variable. If not opposite current, set. Update tick uses desired. Then current=desired. If user quickly presses up then left before tick, left relative current up is valid but then direction left from current up is allowed diagonal? In grid, turning left okay, but up intended not performed. Could queue max2. Implement inputQueue array; enqueue if dir not opposite last queued/current and not same. On tick shift queue if valid against current.
Body growth: pendingGrowth increments on collect. In tick after unshift if pendingGrowth>0 decrement else pop. When collect at tile head (new segment), set pendingGrowth +=1; passenger removed. spawn after delay? Immediately spawn replacement. Keep 3. When pickup occurs, board passenger tile maybe no passenger. Score and particles.
Need avoid spawn on body and platform cells. list platformCells. Since 3 passengers always. Pick platform cells not occupied. render maybe passenger groups only one each.
Passenger art:
function drawPassenger(x,y, color, index):
platform tile y maybe platform strips. person at center y+12? Little head circle, body colored, suitcase. waiting sign maybe circle ring/pulsing. Add "BOARD" icon? For screenshot obvious.
Use bob animation sin. Above head a speech bubble with tiny icon? Not too much.
Passenger objects {x,y,color,skin,phase,id}. color palette.
Train:
Cell size 50. Engine length ~40, width 28. Draw in local coords x along forward.
Coordinates forward vector. ctx.translate(cx,cy); rotate(angle). For engine:
- coupler front? draw track shadow.
- chassis rect -22,-15,42,30 rounded dark.
- boiler cylinder from x=-12 to18, y=-10 height20 in #e8c56a maybe green.
- cab at rear x=-25..-8, y=-17..17, red body? locomotive green/teal, yellow trim. Use #1aa58e.
- front nose triangle / cowcatcher.
- chimney x=11, y=-17 small dark top.
- headlamp circle at x=19 y=0 yellow glow.
- wheels at -14, 8 etc circles dark with spokes.
- steam puffs maybe animated at front top.
Need "little train with a locomotive and colorful wagons". Wagons draw behind engine:
orientation to front segment. Coupler line.
- body rectangle -18..18 width36 height26, rounded, color.
- roof perhaps cream strip.
- windows little cream squares, wheels.
- passengers after collected: visible heads in windows? Each pickup adds one passenger into train, so show little heads in each wagon, capped count. Could store passengersRiding count. draw passenger faces in wagon windows based count. Great.
Colors alternate coral #ef6b61, blue #4b8fea, yellow #f4c95d, purple #9c79de. Each wagon has distinct color.
Tail wagon perhaps small caboose red.
Inter-segment connections: draw coupler lines between current center points under all segments. Since rotated segments. Draw line from segment i center to previous center maybe width4 dark then coupler. render body order rear to head so head top. for i=length-1 downto0.
Orientation:
segments order head index0. For segment0 forward = current - previous (movement). angle atan2(vy,vx).
For wagon i orientation forward toward previous segment: previous = segments[i-1], current=segments[i], vector prev-current. angle. At turn, line connecting centers. Smooth interpolation but okay.
Wagons might overlap around turns. fine.
Engine front direction. Starting up angle -pi/2.
Particles pickup: array {x,y,vx,vy,life,color}; update. text floats "+100" above. Draw in canvas.
Score and count top HTML update maybe every pickup. Score maybe 100 per passenger + combo. Train length display. Status. Status right cards:
Passengers 0
Score 0000
Cars 5
Mode AI Demo
Need maybe next stop "Platform 3" etc.
Header logo. Use inline SVG in HTML allowed self contained. Can create station logo.
Canvas responsive crisp: set width=960 height=540, CSS width100%. DPR? To ensure sharp on scaling, can set canvas.width=960*dpr but then coordinate scaling. But CSS display 1080, slight blur. We can handle DPR up to2: canvas.width=W*dpr, height=H*dpr; ctx.setTransform(dpr,0,0,dpr,0,0). CSS aspect ratio. But HTML width attribute could. Implement resizeDPR on init and window resize maybe canvas fixed. ctx.setTransform at draw start after clear. Canvas coordinate logical. canvas style aspect-ratio:16/9.
CSS:
* box-sizing border-box
body margin0 min-height:100vh overflow-x:hidden background...
.app max-width:1120 margin auto padding 14px 16px maybe. At 1280 width max1120 incl. Header flex height56 margin-bottom10.
.logo 48.
title 22.
subtitle 12.
.header-stats? Stats in game bar.
.game-card border radius24 overflow hidden, box-shadow.
.game-top 56 flex, background #101f2b.
canvas display:block width:100%.
.game-footer 64 flex controls and tip.
.demo-banner absolute top 14 center pointer events none, backdrop blur.
Maybe canvas board top has "GARDEN CENTRAL" sign; overlay banner covers. Place overlay bottom 16 perhaps. "DEMO IN MOTION • Click or press a key to take the wheel" pill bottom 18; train action in board middle visible. Use white.
At mobile, game top wrap and footer.
touch controls visible only <=700 maybe also desktop small arrows? Keyboard hint. Could always show a controls group with keys boxes. For mobile arrows buttons.
Overlay start:
<div class=demo-banner id=...><span class=dot></span><strong>DEMO RIDE</strong><span>Click or press a key to take the wheel</span></div>. requirement clear. If any key start, we only should take control for recognized movement? It says click or press key to start. We'll let any key except modifiers starts manual; if arrow update direction; other key starts manual. But if press Tab could. Use document keydown and prevent for arrows/space. user can type nothing.
Maybe exact "Click or press a key to start" must display. Use text exactly. "CLICK OR PRESS A KEY TO START • DEMO IS PLAYING". Good.
On click canvas, start manual. But user may click demo banner pointer-events none through to canvas. entire document pointerdown maybe start but clicks buttons could inadvertently. Use canvas pointerdown and keydown. We can include footer button "Take the wheel" starts. On mobile, arrow buttons.
Game over modal HTML absolute over canvas, card. show.
Footer:
left "HOW TO RIDE" arrow key caps "WASD / ARROWS" + "Collect everyone on the platform. Don't bump the caboose."
right buttons Pause and Restart. Pause button useful. In demo maybe pause.
Need no confirmation. Restart resets to demo or manual? Button "New route" reset demo. In game over, restart demo.
Pause toggles. During pause show overlay "PAUSED". HTML.
Can have speed selection? no.
Manual game logic and game over. In demo if manual? after start. If AI collision auto reset? Maybe resetGame('demo') preserving? implement if demo collision, wait 1 sec reset. But no need.
Board boundaries: Train can't leave grid. collision => gameover.
Need maybe no obstacles, only self/border. Could add "buffer stops" at corners? Not necessary.
Scoring and passengers. "passengers waiting on platform" plural.
Could include route sign in canvas top left and decorative skyline behind. Since board itself. Draw outside:
- board bg.
- station building maybe top? Canvas y0-64 header area with station name. Grid begins y=62. title "GARDEN CENTRAL" and clock. But demo banner maybe at bottom.
- board y=62 H=450 ends512; bottom strip 28 for controls? "PLATFORM 1 • NORTHBOUND". We'll draw.
Actually W=960 H=540, grid x=30,y=62,w=900,h=450 (rows9). Footer y=520. Draw rounded board.
Draw top row platform at cell y=62-112. bottom row platform y=462-512.
Interior scenery: tiles. Add little bushes at random fixed coordinates maybe. Could designate non-track? Visual only. Maybe buildings at top border.
Rails lattice:
For each row y center: draw horizontal lines from x=gridX to gridX+gridW. For each col: vertical lines. This creates dense. Use line width1.4 and rails #50615e alpha .32. Ties every 12. To make platform areas look tracks on platform, okay.
Could instead draw a grid of only horizontal rails and vertical at columns 1,6,11,16. But train may move vertical any col across grass, visually weird. Full grid.
Can create edges line style: horizontal rails y±5, sleepers vertical every 14. Vertical rails x±5, sleepers. At crossings overwhelming. Use low alpha.
Maybe draw only center route lines in very faint gold. Train overlays enough. Board cells have subtle checker. Intersections represented by switch dots at cell centers. Fine.
Actually whole lattice with 18x9 = 27 intersections; draw all. Looks like rail yard, on theme.
Scenery between cells corners can be small grass tufts, but tie lines occupy. Use random deterministic array for shrubs on some interior cells drawn at corner offset -18,-18, radius4. Avoid platform. Station lamps on platforms every 4 cells.
Passengers spawn platform cells. Top and bottom row plus side columns? Add platform top/bottom only 36 cells. train initial at row4. Distances 4. Fine.
Top platform tile has cream floor at y+34..50? Person stands above rail? Let's position person at x+25, y+35 maybe. But pickup train at center won't exactly overlap. Use tile platform strip near top of cell and person at center. Maybe all platform cells draw beige floor full. People centered. Train covers them as it arrives. Good.
When pickup, draw passenger disappearing and +100.
Station signs:
At top left canvas outside board y38 "GARDEN CENTRAL STATION" etc, clock.
At bottom inside "PLATFORM 1" etc.
Could add small clock using real time? fixed analog maybe.
Canvas header dark.
HTML details.
Need maybe support pointer swipe on canvas. Implement pointerdown records, pointerup detects delta >24 -> direction, and starts manual. Touch buttons better. Add four buttons shown mobile. Could include buttons all times compact.
Let's write full code.
Potential canvas issue with dpr and rounded rect. Use helper roundRect via ctx.roundRect supported Chrome. Latest yes. Use path fallback? Use ctx.roundRect. Zero errors.
Animation:
const W=960,H=540, COLS=18,ROWS=9,CELL=50,GX=30,GY=62.
State:
let segments, direction, desiredQueue, passengers, particles, score, collected, mode='demo', gameState='playing', paused=false, tickAccumulator=0,lastTime=performance.now(),tickMs=140;
const colors.
resetGame(startMode='demo'):
segments = [];
let path = [[8,4],[8,5],[8,6],[8,7],[8,8],[8,8?]] Wait tail at row8 bottom platform if body. Keep body straight down 5: [8,4],[8,5],[8,6],[8,7],[8,8], length5. Head moving up. Last at bottom platform maybe okay. Could length5 with [8,4],[8,5],[8,6],[7,6]? orientation. Straight.
dir={x:0,y:-1}. queue.
passengers=[]; spawnPassengers(3)
selected colors random.
score0 collected0 car target5.
mode startMode.
updateUI.
For demo, status "AI DEMO"; manual "YOU'RE DRIVING".
If reset from button, start demo. If gameover restart maybe manual? Button "New demo route".
Spawn platform cells: row0 and row8 all x 0..17, maybe avoid too near? Platform cells array. Choose candidates not occupied and not existing. Could ensure not top row x8 where? Fine.
At reset spawn maybe predetermined [8,0], [3,8], [14,0]. Then random later.
But platform person should be visible first paint. Colors.
AI target:
function chooseAIDirection:
for each passenger sorted dist, bfs.
BFS from head; occupancy set segments.slice(0, segments.length - (pendingGrowth>0?0:1)). Note tail can move if no growth. Current head start.
Use queue arrays and prev map keyed y*COLS+x. Grid boundaries. For each neighbor if not body (unless tail) and not visited. Goal. path.
If path found direction. But BFS can route through all open.
If passenger target unreachable due body, next.
If no target path: candidates directions nonreverse; safe if next in bounds and not body except tail. Score candidates by empty-neighbor count + random; choose max.
Need never reverse unless dead end maybe. If no safe nonreverse but reverse safe, allow reverse. Could deadlock but okay.
When direction turns, body occupancy and tail.
Tick:
if state playing and !paused:
timer += dt; while >= tickMs:
timer -= tickMs
step()
Need first update positions.
step:
determine nextDir. For manual queue shift. For demo choose AI.
if gameState no.
new head.
if collision (boundary or body segment excluding tail if no growth): if mode demo? maybe set game over. For AI safe.
segments.unshift({x,y,px:head.x,py:head.y});
if pendingGrowth>0 pending-- else pop.
Check passenger at new coordinates: remove, score +=100, collected++, pendingGrowth++ (careful sequence: body already popped because pendingGrowth was 0 at tick before collection. We want growth on next tick? Standard: grow when eat after move. We popped, then increment pending. This results length next tick increases? Next tick unshift and pending>0 no pop, so yes one extra. good). spawn one.
Particles.
But engine center collection perhaps at next tick exactly new cell. good.
Combo maybe score 100 + (collected-1)*10.
Update UI.
Train length display segments.length? After growth pending, next length. display.
Manual input queue and desired. current direction updated. function enqueueDirection dx dy:
if mode demo start manual.
base = queue last or direction. if opposite skip. if same skip. push max2. mode manual.
On keydown any:
if key in WASD/Arrows then startManual and enqueue.
else if mode demo, startManual without direction.
Need exact click. canvas pointerdown start manual. Footer take button.
Buttons touch pointerdown prevent default; enqueue. Prevent click after maybe.
Button click may bubble document? We only canvas pointerdown, no global. Fine.
Take wheel button click.
Pause button toggles; if mode demo and pause, banner maybe. UI.
Restart button resetGame('demo') always.
Could let arrow key switch manual. Overlay hidden and status updated.
Banner HTML:
<div id="demoBanner" class="demo-banner"><span class="live-dot"></span><strong>CLICK OR PRESS A KEY TO START</strong><span class="banner-sub">AI demo is already riding the rails</span></div>
At 375, strong size 10, sub maybe hide? But must exact visible. Keep.
When manual class hidden.
Game over modal and pause overlay:
<div id="modal" class=modal hidden>...
button id=modalRestart.
Use [hidden]{display:none!important}.
Canvas itself click if gameover maybe no.
UI update score text padStart(4). passengers count. length segments.length + pending? show segments.length maybe. Goal 12? progress? Could have "Tickets 0 / 12" but game indefinite. Use passengers count and score, train length.
Could create passenger collection target and celebratory when 10 "Express service unlocked" maybe not needed. Maybe route level. More interesting to have milestones every 8 passengers: "Silver service", "Gold conductor". Header status maybe station sign updates. On 10, confetti. But screenshot early only.
The requirement "everything interesting visible first seconds: nothing hidden behind later level". We have train and waiting passengers visible, good.
Could include collecting passengers in wagons. Count riding, draw faces. Each wagon after first has passengers. Maybe when 5 collected, five visible in train. Good.
Passenger ride visual: draw window heads based collected count distributed across cars. Each wagon windows at x -8 and 8. draw head in up to windows. For first N. Ensure colorful.
Render train:
function drawSegment(seg,index,alpha):
cx = lerp(seg.px,seg.x,progress). For each segment.
We need progress global 0-1 since last tick. Draw couplings first between current interpolated centers. Need compute rendered positions array.
For each i from len-1 down to0, angle:
if i0: vector (seg.x-seg.px). If zero use direction.
else vector segments[i-1]-segments[i].
At turns, wagon orientation to front. For head current movement.
At interpolation, couplings should connect centers. Draw lines between pos i and pos i-1. Because i>0.
Then draw each wagon.
Need orientation for wagon should be toward prev, but coupler maybe okay.
Engine draw at pos0 angle head.
Wagons index i correspond body segment i. engine index0, car number i. Colors[i %]. Last car index length-1 may caboose red.
Draw wheels.
Canvas rounding helper:
function rr(x,y,w,h,r,fill,stroke) using ctx.beginPath();ctx.roundRect(...).
Need ctx state save/restore.
Background:
ctx.setTransform(DPR,..); clear.
draw fill #08161f.
draw top info.
board rounded x=24,y=56,w=912,h=458 fill #10262b.
Grid cells each x=GX=30,y=GY=62 etc. cell backgrounds alternating. Board clipped maybe.
Platform rows:
draw top strip at GY width GW height CELL fill #c9b993? Could be too bright; style warm #d8c8a4 at alpha. bottom.
We need rails over platform. Add platform edge line at y=GY+CELL-8 and y=GY+GW? top row person background.
Draw interior cells with slight checker #122d2d and #153131. Maybe whole station "garden" with park.
Draw tiny flower beds in top.
Rail lattice:
We can draw route lines in all cells but high clutter. Let's calculate: horizontal lines each row at center. vertical lines each col. Board width 900, track lines across. With low alpha. Visual.
Draw sleepers as short perpendicular segments along horizontal and vertical at every 13 px. Could too many 18*900/13*2 ~2500, okay. But intersections. We'll draw lines only:
For each row:
for x=GX; x<=GX+GW; x+=12 draw line (x,y-8 to x,y+8) dark brown alpha .28, then two horizontal rails y±4 line from left to right.
For each col similarly. This is 27 long lines. At intersections rails cross. Maybe use switches circles.
Could use only grid lines, and train path highlighted with gold? But train covers. Fine.
Passengers draw after rails. Their platform should be visible. Persons at center y maybe. Render shadow, legs, body, head, hair, arm, suitcase. bob.
Halo behind as soft circle and boarding pin maybe. Add tiny sign with plus.
Scenery: use deterministic shrubs at interior cells coordinates (3,3), etc in corners. But draw behind rails? draw before rails.
Station lamps on platform row x every5 at y+8 maybe.
Draw little canopy rectangles on top row x1-4 etc but might cover passengers. Could draw canopy at far edges.
Draw small station building at top? Canvas header sign enough.
Board footer with route dots.
Maybe body board rounded clipping. Use ctx.save clip rr then draw grid and rails. Clip helper path no fill. Good.
We can draw rails with actual network perhaps path is all grid. No concern.
Particles update dt and draw.
Floating text update life.
Overlay in canvas not needed.
CSS responsive:
@media(max-width:700):
.app padding 8
header height auto; title 18; subtitle hidden maybe
game-top padding8; .stats gap etc; hide station-label
canvas maybe width calc; aspect ratio.
.footer flex direction column; controls.
touch display grid always? We'll include .touch-controls in footer display none; at max 700 flex.
At 375 game canvas 343x193, enough to see. Header ~52, gamebar maybe84 wrap, canvas193, footer100 =429.
No horizontal.
Top status game bar:
<div class=station-id><span class=station-dot></span><div><b>GARDEN CENTRAL</b><small>All platforms • Evening service</small></div></div>
<div class=stats><div><span>PASSENGERS</span><strong id>0</strong></div>...
Mode badge.
On mobile station-id hidden, stats flex.
Header logo + title + pill "A SNAKE GAME IN FULL STEAM". Right maybe line "Collect • Connect • Don't derail". Hide small screens.
Need make code self contained, no external network.
Potential issue no explicit game start click? yes.
Could maybe provide audio? No external, could WebAudio chimes triggered by user but demo can't audio without gesture. Avoid.
Game over "That’s a wrap!" etc.
Let's think visual impact first paint. Canvas board dark with train and 3 colorful tiny people, but persons tiny at scaled board. 50 cell, person 24 px -> at 1080 27px, visible. Banner bottom maybe.
Header vector logo. Good.
Could make locomotive bigger 44x32. Wagons 36x28. Connect. Fine.
Need maybe draw station clock in canvas top right; visible.
Canvas header text:
drawText "GARDEN CENTRAL" x30 y30; small "THE FRIENDLY COMMUTER EXPRESS". right little route badges.
But board starts62, enough.
Draw direction markers maybe at board edges? rails.
Station background header maybe board x24. Fine.
Canvas header top dark.
Implementation detail clipping rounded:
function roundedPath. use ctx.roundRect. Then ctx.clip.
Draw cells:
for rows cols fillRect cell with alpha. But platforms rows should overlay.
Board background.
Shrubs deterministic positions based array.
Rails lattice maybe line colors. Let's style with warm brown sleepers and slate rails. At row center y, draw ties vertical every12 in #5d4d40 alpha. rails y±4 in #8a928b alpha .45. Vertical ties horizontal and rails x±4. This creates grid resembling rails. At intersections, weird but okay.
Could instead use each cell's four track edges, but same.
Station platforms should overlay tiles but rails remain. Draw platforms before rails, so tracks run through platform. People on platform beside track? Person at y + 15? If track horizontal along y center and person at y+15, train overlaps half. Better platform row has track at y center; people at cell x+35, y+18, but pickup head center won't directly overlap. Yet collision condition by tile. That's acceptable. Draw people offset +12 y. Train center covers as it arrives.
Maybe platform is a narrow strip y=GY+CELL-18..GY+CELL, person lower. But bottom row platform strip at bottom of tile? Top row person at bottom, train above? Head at cell center, near. Fine. Make platform band top row bottom 22 px, bottom row top22. Passenger stand in band, visible. But cell pickup no overlap. visually passes platform. Great:
Top platform: whole row tinted, person y=GY+CELL-13 (e.g. 99), tracks center y87. person below track.
Bottom platform: band at y=GY+8*50 .. +? person y=GY+8*50+14. Track center487, person474? Could band top half. We'll draw person offset based row0 +14, row8 -14. Train center overlaps some.
Canopy etc.
Maybe rails inside platform should be visible.
Passengers list includes row0/row8. render person at y center + (row==0?14:-14). This could draw suitcase. Collected when head tile. Fine.
Train initial tail at row8 col8 on bottom platform person maybe spawn there could overlap. Spawn avoid body.
Manual directional controls. Touch button layout.
Game over and pause. If pause clicked, overlay class.
Modal hidden initial. Canvas continues draw no movement but particles maybe. render.
Demo banner should hide on pause? show pause overlay.
Could have toast message DOM for pickup? no.
Need update status mode if manual: mode label "MANUAL", badge orange. CSS.
Route stop number maybe passenger count. Status station label changes "GARDEN CENTRAL". no.
Let's maybe add a small "route progress" line in gamebar: Passengers, Score, Train length. Three stats enough.
Initial HTML mode badge "AI DEMO". stats passenger count 0, score 0000, length 5.
Title "RAIL PICKUP" and subtitle "A little engine. A lot of passengers."
Let's code exact. Need ensure no external assets.
CSS include keyframes pulse and float. Since canvas handles animation.
Potential issue touch button pointerdown and click causes focus, but okay.
Take wheel button id. On click startManual.
Header right maybe "HOW TO PLAY" not.
Maybe add a button in header "Play now" as clear. But banner sufficient.
Need start message visible over canvas and not obscure action. Position absolute bottom 14. At board bottom, perhaps passengers bottom row obscured by banner. Better position top:14; canvas top info obscured. Board action center unaffected. At 375 top banner may cover board top row (passengers) partly. Alternative place banner as narrow full-width strip just above footer within game-card, not overlay canvas: in game footer? But requirement message visible. We can have footer top? Could place banner absolute at bottom:72? Hmm.
We can make overlay centered at bottom of canvas, top row visible. It will cover bottom platform some. Train center. okay.
At 1280 canvas scale, banner bottom 14, board bottom 512 scaled; covers bottom 35 logical. bottom row passengers may be covered. Top row still. Use banner top 16 maybe covers title only, board top row starts62 and banner height42 ends58. Perfect. Place top:12, so banner covers canvas header info, but okay. It might overlap board? height46 +12 =58, board starts56 slight. Fine.
Banner white/dark card. exact phrase.
Could put at top of game card above canvas as strip 38, reducing canvas? The gamebar already. But banner overlay best.
Canvas header text maybe redundant but fine.
AI in demo. The banner says start but demo runs. Good.
One subtle requirement "The iframe only receives keyboard events after user clicks it: if demo uses keyboard, display clear..." We do.
Let's design engine drawing in detail:
function drawEngine(x,y,angle,t):
ctx.save translate rotate.
shadow ellipse.
chassis rr(-24,-15,46,30,7,#182029)
coupler front line 21..28.
Boiler rr(-7,-12,30,24,10,#f0b84e) maybe locomotive green body? Choose boiler #f3c85b, cab teal #168f87.
cab rr(-25,-18,21,36,5,#178e83); window rr(-21,-13,11,12,2,#bfe9e5); stripe.
boiler horizontal gradient? Canvas createLinearGradient. use.
chimney at x10 y-21.
headlamp at x22.
cowcatcher front triangle gold.
wheels at -14,10 etc. Need wheel circles under body but drawn before body ideally. draw wheels before chassis. Engine front right by angle.
Steam puffs at front around x29,y-16 maybe.
Wagons:
body rounded -18,-14 36x28 color. roof cream. windows 2.
wheels at -10,10.
coupler.
Use black outlines 1.5.
Draw order couplings, wagon rear to front, engine.
Passenger faces in wagons: we know collected count and car index. For each window, if faceBudget >0 draw head circle skin #f2b78d radius3 at y=-4, hair arc. There are 2 windows per car. Passengers allocated sequential from car1? engine no. For each wagon if budget. Need state passengersRiding = collected, but some passenger maybe still. Draw for i from1. budget=collected. In drawWagon accept rideOffset. Use global rideFacesRemaining decrement in rendering rear-to-front might show rear cars first. We want front cars maybe. Draw car order rear to front but allocate based index. function facesForCar(i) distribute collected across cars: for each car i>0, max 0 min(2, collected - (i-1)*2). This fills front wagons first. Good.
Wagon colors based i.
Maybe engine smoke animated: draw 3 translucent circles above chimney based time, but orientation means local coordinates. puffs at x=14+..., y=-23-i* etc; with rotation, weird but steam behind? Engine moves forward, chimney at x8 and puffs opposite -x. local puffs x=0,-10,-20 at y=-23. Fine.
Draw passenger in platform:
ctx.save translate person x y+bob.
shadow ellipse
legs lines
body rr etc
head circle
hair semicircle
arm
suitcase
halo dashed ring maybe a golden circle radius15.
Above maybe a boarding icon? A small luminous diamond.
Need people not too cluttered.
Board sign top row: "BOARD HERE" small at some.
Background scenic:
- shrubs array inside cells corners.
- warm lights along platform.
Let's ensure rails don't cover people because draw rails first then people. train then over people.
Particles world coords: x pixel, y pixel. On collect at cell center.
Text.
Render board clipping around x=24 y56 w912 h458. Grid extends to930,512 within.
Grid top 62, bottom512. Board clip y56 to516. good.
Canvas footer y525.
Draw rails across.
Need board labels maybe "PLATFORM 1" top left at x30 y57? but rails etc. Draw over board at top? Tiny.
Maybe top row platform passengers on y99, track y87. top border board y56. Fine.
At bottom y487 track, person y473, board bottom516. good.
Board scenery top and bottom.
Interaction and game:
function startManual:
if mode !== manual {mode='manual'; banner.classList.add('hidden'); updateMode; toast?}
Should we reset direction? AI may have current direction. Fine. message.
keydown event:
const key lower.
if movement:
e.preventDefault();
startManual();
enqueue.
else if gameover and Enter -> resetGame('manual' maybe demo). requirement.
if Space prevent? start manual, maybe pause? no.
Pause button.
When click canvas start manual. But if user clicks to aim? no.
Status update mode.
Modal button restart sets resetGame('manual') perhaps manual starts; but banner hidden. New route. Header restart button resetGame('demo') then banner appears, to give option. Could button says "New ride" and resets manual preserving. maybe start manual.
Game over modal text "The conductor had to step off." Button "Try this route again" resets manual.
Top restart button "Restart" resets manual? Let's call "New route", reset demo maybe banner appears. Fine.
AI collision maybe gameover overlay. AI should safe.
Could the BFS path direction choose immediately reverse? BFS from head can include previous body cell as tail only. no reverse likely except tail if no growth and target behind, but tail cell excluded as available. We treat tail safe but path may go through tail, direction could be reverse if target behind through tail. Since target platform and tail often behind, BFS could choose reverse into tail; tail moves away same tick, allowed, okay but train may reverse direction. In snake, moving into tail is allowed if not growing. Fine. For AI visual direction can reverse.
Occupancy: Set segments 0..length-2 (exclude tail always) except when pendingGrowth>0, tail will stay so include tail. If no pending growth tail moves. Actually unshift then pop old tail, so new head into old tail is safe. Exclude tail. Good.
BFS excludes body set excludes tail.
No growth if pendingGrowth >0. Collection happens after movement and increments pending for next tick; at next tick body keeps tail. AI occupancy during next includes tail because pending >0. Great.
spawn passenger can be on tail? no body exclude.
Start passengers at row0 col8 maybe distance? initial body includes tail row8 col8. Spawn [8,0] valid. AI target path: head row4 to row0 direct. no body. collects. Good.
Second [3,8], third[15,0]. At t3, AI may collect first and start second, maybe travel. active.
When collect first at row0 col8, spawn random maybe row0 col2. count remains3.
Score update.
Could add ticket/confetti after every pickup, particle burst.
Train length grows to6 after next tick. eventually up to10. Need max? Keep growth indefinite but body length could 14; still okay. Snake growth and board 18x9 enough. AI BFS might unreachable. Limit max10; after 10 pickups, still collect but no more growth maybe passenger count all fit? 10 wagons. passengers riding 10. Keep growth to max10. If segments.length<10 pendingGrowth++ else score still and confetti. Then all passengers can board but train capacity only10. Maybe count can exceed10, display "10/10". Better max12 and only 18x9. But if passengers count >12, passengers can stay? requirement picking up. Keep max10 and cap passenger total? When full, no new spawn? Could display "Train full!" but game not interesting. Better max length12 and passengers count unlimited, passengers visible in windows only first12. AI path. Board huge enough.
Length at screenshot maybe6.
Manual can hit self. Fine.
Do we need high score? no localStorage prohibited but can session memory variable. Could track best in JS and show "BEST 0000" perhaps. But not needed.
Add best as variable max score, update. gamebar 4 stats maybe. At 1080 okay. Could show passengers, score, length. no best.
Need no horizontal at 375. CSS min-width0.
Potential code length maybe 30k. Fine.
Let's write careful.
Canvas dpr:
const DPR = Math.min(window.devicePixelRatio||1,2);
canvas.width=W*DPR; canvas.height=H*DPR; canvas.style.width='100%'; style height auto? Since attribute ratio remains. set css aspect-ratio:16/9;height:auto. width attr 1920 etc and height1080 ratio same.
ctx.setTransform(DPR,0,0,DPR,0,0). At each draw setTransform then clearRect(0,0,W,H). save etc.
Resize DPR can trigger; keep if changed set dimensions and transform. draw sets.
roundRect function:
function roundRect(x,y,w,h,r,fill,stroke,lineWidth=1){
ctx.beginPath(); ctx.roundRect(x,y,w,h,r);
if(fill){ctx.fillStyle=fill;ctx.fill();}
...
}
Need if r maybe > half. okay.
Canvas text baseline.
Draw clock at x886,y31 maybe radius14.
Board title header at x30 y29.
Draw station environment: small mountain/tree silhouettes behind board? Board fills.
Grid rail drawing is many ctx operations every frame, okay 18*9 ~162 cells plus lines. Sleepers maybe ~4000 each frame at 60fps could be 240k ops, fine but canvas. Need optimize by drawing static board to offscreen canvas then blit each frame. Great. Build static canvas document.createElement('canvas'), size logical*DPR? Could create at W,H and scale by DPR maybe. simpler staticCanvas width=W*2? But screenshot high. Use staticCtx setTransform(DPR). Draw background and board once; main drawImage(staticCanvas,0,0,W,H) with ctx transformed? If main ctx has DPR transform, drawImage(staticCanvas,0,0,W,H) uses dest logical W,H, source pixel 1920, good. static canvas 1920x1080, context scaled DPR and draws logical. Then main drawImage with main transform scale -> output 1920, okay maybe double. Good.
But dynamic canvas clear then drawImage. static includes background/header. Need resize DPR rebuild. define buildStatic().
No external. use Offscreen? document canvas fine.
Static draw scenery and rails. Dynamic draw passengers, train, particles. Good.
drawStatic:
sc = staticCanvas.getContext('2d'); sc.setTransform(DPR,...).
fill whole bg.
header.
board rounded.
clip.
cells.
platform bands.
scenery.
rails.
platform lamps.
restore clip.
maybe bottom labels.
Need grid cell rectangle within clip.
Scenery array.
Draw rails loops with alpha.
Interior cell backgrounds subtle:
for r,c fillRect, alternating. Since cell dimensions and board. Then platforms:
top row fill from GY+18 to +50? Actually whole top row fill tan alpha .14; bottom.
draw inner landscape in interior cells maybe tiny shrubs.
Rail lattice with intersections. Could create switches circles at intersections of cells? Let's draw all horizontal and vertical, but due every row/col there are grid. okay.
Maybe use two layers:
sleepers every13 along each horizontal/vertical. But both directions. We'll limit vertical rails only at every second column? Train movement vertical any column would traverse grass. Not ideal. Full lattice.
We could render "ballast" under tracks. Fine.
Add some station buildings between board edges? no.
Station labels top of board:
At x42 y=76 small "PLATFORM 1 • NORTHBOUND". But top row track begins y87, label may overlap. Put y76.
Bottom row label y508 maybe.
Canvas dynamic passengers draw.
drawStationPassenger:
ring pulse with ctx.globalAlpha etc; no shadowBlur expensive but okay.
ctx.translate(cx,cy+offset+bob)
scale? cell 50.
circle halo radius14 fill gold alpha.
legs dark
body.
head.
hair
suitcase.
Use tiny white boarding ticket rectangle.
Passengers platform row offset. Some passengers might overlap platform edge but okay.
Train dynamic draw:
positions compute from segments with lerp progress.
Need current position p0 uses seg.px,py previous tile center. On initial reset, px=x. Good.
For head angle vector x-px. During interpolation after tick, x-px gives direction. At next tick px set old x so same.
For wagon angle vector segments[i-1].x - segments[i].x (tile coords) not interpolated. Good.
Couplings lines between rendered positions. Draw all.
For wagon colors global array by i.
Engine draws.
Need if segments tail at boundary, wagon extends outside board maybe slight. Start tail row8, body y462? cell center=487, wagon extends14 ->501 within board512. good.
Engine smoke local behind. Use time.
Wheel draw circles.
drawEngine:
wheels first at x=-13,8 y ±? Wheel positions along x, y=-13? Since train longitudinal x, wheels sit on sides? In top-down, wheels appear as rectangles along sides. Traditional top-down train: boiler, cab, wheels as small black rectangles on side edges. Better draw top-down:
- chassis rect -25,-15 47x30
- boiler from -8 to18, width?
- cab rear -26..-9 full
- boiler green/gold.
- wheels at x -15,10, y±14 as small rounded rectangles.
- coupler front x22.
- headlamp.
This reads top down.
Wagons body -18..18 x 28, windows on top. Coupler.
Passenger faces in windows. windows squares at x=-8 and5, y=-9 maybe.
Engine cab window, roof stripe.
Station platform person side/top.
Particles use pixel positions and draw. text "+100 BOARDED".
Maybe ticket counter top "Passengers aboard" count equals collected, but passengers on platform still waiting. Stats "BOARDED". User asked picking. use "BOARDED". Other "SCORE", "TRAIN". Good.
Header subtitle says collect waiting commuters.
Add route labels in footer.
CSS polished:
:root colors.
body font-family ui-sans-serif, system-ui.
background radial.
app.
topbar.
.logo mark gradient orange.
.title.
.tagline.
.game-card.
.gamebar.
.stat.
.value.
.mode-pill.
canvas.
.demo-banner.
button.
.touch.
kbd.
footer.
At 375 status text sizes.
Footer text maybe "COLLECT COMMUTERS" and desc.
Controls.
Use aria labels.
HTML complete with <!doctype html>.
No JS errors:
query selectors all exist.
Use `ctx.roundRect` latest Chrome yes.
e.key.toLowerCase.
Pointer events.
Pause:
let paused false; pause overlay. If paused, no step but draw.
button toggles and text.
Restart. Modal.
Start message any key: document.addEventListener('keydown', (e)=> { if e.ctrlKey... return; if movement...; else if e.key===' ' prevent start manual maybe. });
Requirement "press a key to start" any key. implement:
if gameState==='gameover' and e.key==='Enter' restart; else.
if mode==='demo' { startManual(); } for any key except Tab? even Tab. Fine. But if user presses key after manual no effect.
Arrow key after startManual.
If mode demo and key 'r' then start manual not restart. okay.
No use prompt etc.
Click canvas start manual, but initial canvas click starts.
Touch arrow pointerdown should start manual.
Take button.
AI BFS:
function bfsDirection(target):
const blocked=new Set(); for i=0;i<segments.length-1;i++ blocked. If pendingGrowth>0 include last tail too. Wait at current tick pendingGrowth indicates tail won't be popped this tick; but movement current occupancy all segments including tail. BFS next target could enter tail only if tail vacates? if pending>0 tail doesn't vacate, blocked. If no pending tail vacates, exclude. So `for i=0;i<segments.length - (pendingGrowth>0?0:1);i++` => if pending>0 all, else length-1.
Start head coordinate is in blocked set because segments[0], but BFS starts there and neighbor. We should block body excluding head? body includes head but visited start so no issue; when checking neighbor don't return to start due visited. Fine.
Neighbors grid.
Use prev Int16Array(COLS*ROWS).fill(-1), queue arrays. prev[start]=start. For each. Return coords path.
No target path, safe candidates:
base direction current; candidates all directions except reverse first. next not blocked. score count free neighbors around next, random.
If none allow reverse. If all blocked gameover.
Because blocked excludes tail when vacating, next tail safe.
But AI target passenger might be at same as head? not spawn body. fine.
Need if passengers array empty choose random safe.
Manual queue:
let direction={x:0,y:-1}, queuedDirections=[];
enqueue:
const base=queuedDirections.length?last:direction;
if (dx===-base.x &&dy===-base.y) return;
if same return;
if queued length<2 push.
At tick, shift q; if q opposite current? queued already relative; check.
When mode demo starts manual and direction current maybe AI changed.
If clicking only, train continues current direction. good.
Could maybe arrow key while mode demo choose direction and enqueue relative to current.
Space maybe pause? no.
Collect passengers:
newHead x,y. passengers find. If collect:
const p = ...
score +=100+Math.min(collected,8)*5
collected++
pendingGrowth++ (if segments length + pending <10 maybe; if cap no pending)
spawnReplacement excluding occupied.
But segments.length after current tick maybe4; pending 1. condition `segments.length + pendingGrowth < 10`; okay.
Particles.
Maybe all passengers spawn on platform. use random.
spawnPassenger initial predetermined. function getPlatformCells returns.
At reset spawn first three manually with colors.
For new random choose not occupied; list. use rand.
Need ensure no duplicate.
Colors.
What does train length display? segments.length + pendingGrowth maybe. At initial5. display carCount = segments.length; engine plus 4 wagons. label "CARS" maybe includes engine? Use "WAGONS" and value segments.length-1. initial4. User can see colorful. Stats: BOARDED 0, SCORE 0000, WAGONS 04. Better. update train length as segments.length-1+pending.
At collect, pending after update UI, display5 before body growth. fine.
Update mode badge.
Need status on AI: badge text "AI DEMO"; dot.
Passenger count initial3 but stat boarded0. okay.
Add board "WAITING 3" dynamic maybe canvas static can't. Dynamic at header x? draw in render dynamic to show waiting count top right below clock. static can not. Draw at x760 y28 "WAITING: 3". update each frame. maybe.
Clock static decorative analog hands based current time? buildStatic only once, fine. Use fixed 5:42.
Potential conflict static header overwritten dynamic waiting. fine.
Need use CSS `color-mix` maybe latest Chrome supported but avoid.
Buttons style.
Modal overlay over canvas card. `position:absolute; inset:0; display:grid; place-items:center; background rgba...`.
Card responsive.
Pause overlay same modal content.
When gameover mode perhaps still manual. update UI.
Restart reset and modal hide.
Game state values 'playing','gameover'. paused boolean.
startManual if gameover maybe no.
Game over step:
gameState='gameover'; showModal('ROUND COMPLETE', ...).
But AI collision maybe.
No long play hidden.
Let's consider if canvas static board drawn with rails so train movement is visible but direction. yes.
Maybe top board row platform and bottom row with people. We might want 3 waiting always. Spawn random positions. At initial [8,0], [4,8], [14,0]. They are visible. Good.
AI path to [8,0] from head row4 goes up rows3,2,1,0. Body below, no collision. collect at ~0.7 sec. Then likely target bottom [4,8], path down around body? body now vertical row0-? head at0, body row1..4. Need go down blocked by body. It can go around columns. BFS finds. Good.
Need score screenshot after3 sec maybe count2.
Let's estimate AI path after collect at t0.6: head(8,0), body includes(8,1),(8,2),(8,3),(8,4),(8,5). target(4,8). BFS around, maybe path left/down, takes8 cells 1.1s, collect by1.7. third. score visible.
Let's write code. Ensure output only fenced block.
We need maybe no svg code requirement, HTML.
Let's draft full CSS and JS.
CSS body height. `main.app`.
Header `.brand-copy .eyebrow`.
Logo SVG inline.
Gamebar maybe stats.
`canvas-wrap {position:relative; background:#08151d;}`.
canvas.
demo banner.
modal.
Footer `.instructions`, `.control-cluster`.
Touch controls.
Keyboard hint.
At max700 `.instructions` hidden? But need help maybe desc. `.control-cluster` flex. touch display flex. Desktop touch hidden.
Take wheel button in footer maybe visible desktop and hidden mobile? Could always. footer:
left instruction
right controls includes touch arrows, kbd group, take button.
At 375 right width100%, touch plus take. Footer height maybe. We'll style wrap.
Better:
.footer-left title+desc.
.footer-right:
.touch-controls
.desktop-hint
button.
Mobile: footer-left none; footer-right width100; touch flex; desktop hidden.
At 700, touch display flex. Button text "Take wheel". No horizontal; controls four 38 buttons = 164 + button100 + gaps ~276 fits343. good.
Gamebar at mobile:
station identity hide.
stats width 100%; each.
mode pill maybe order.
height56.
`.stat label` 9.
score 16.
Header at mobile right tagline hide.
Canvas banner at top 10; strong 10. Banner sub hide at max500 but phrase remains.
`banner-sub` maybe "The AI is driving • click or press any key" but exact strong. Keep phrase exact. At desktop strong "CLICK OR PRESS A KEY TO START"; sub "AI demo is already on the move." Great.
Maybe initial demo banner pointer-events:none, clicking passes to canvas.
Modal overlay's button.
Let's ensure no use localstorage.
One concern staticCanvas drawImage on every frame with high DPR:
staticCanvas.width=W*DPR, staticCtx.setTransform(DPR,...). main ctx transform DPR; `ctx.drawImage(staticCanvas,0,0,W,H)` source full 1920x1080, dest logical 960x540, then transform -> 1920. correct.
If DPR changes build.
Text fonts in canvas use system.
`ctx.setLineDash` reset after passenger rings.
Board rail drawing:
For each row center:
for x GX+6 to end every14 draw tie vertical y-8 to y+8 color #57473c width2
rails line y-4 and y+4 across.
For cols similarly.
This means at every cell center rails cross. use line width1.2.
Track ballast maybe background.
Could draw line of route through body? no.
Static scenery: cells interior corners. Use deterministic array from formula, draw little shrubs at x+8,y+40 maybe between tracks. But tracks cross. Maybe lamps on platform edges.
Interior board label "GARDEN YARD • 9 CARRIAGE LIMIT" not.
Draw tiny signs at left board vertical "UP" maybe.
No need.
One subtle visual: grid board huge and lines all over could look spreadsheet, but station theme. We can color tie lines brown and rails grey. Passengers on platforms stand out.
Could maybe generate network rails only along edges and omit center lines? same.
Train visible.
Let's code.
Need consider border collision and movement tail. At boundaries. AI won't. Manual.
draw positions:
function renderTrain(t){
const positions=segments.map((s,i)=>{
const p=Math.min(1,progress);
x=cx(s.px,s.x,p)
...
})
Couplings for i=1 to len-1 line positions[i-1] to positions[i]. But order positions.
Wagon angle uses segments[i-1]-segments[i]. At head segment current tile to prev tile. fine.
draw wagons i len-1 downto1; then engine.
Couplings drawn under.
Maybe line connector from head center to first wagon emerges.
Engine smoke drawn as part local at back? puffs local x=-5? Chimney at x=10; smoke should x=7+? along x behind negative. draw.
drawWagon at position with angle. Faces distribution.
For i car index, `faces = Math.max(0, Math.min(2, collected - (i-1)*2))`; if i=1 gets2, i=2 next2. fill front cars. If collected1 one.
Windows draw dark first, then face.
Wagon body color array[(i-1)%4]? i=1 color coral, etc.
Caboose last if i=segments.length-1 use deep red and small rear balcony. But color sequence maybe. Use color = i===last ? '#d84d4d' : palette[(i-1)%...].
draw windows on both maybe.
Engine body gold boiler and teal cab. Colorful wagons.
Train shadow.
drawPassengers before train. Halo visible.
Particles after train.
Draw route top dynamic waiting.
Static clock maybe draw in static.
In buildStatic:
header text and board.
ctx board clip.
Inside cell fill.
top platform row fill #d8c89d with alpha .17, bottom.
platform edge line tan.
rails.
lamps.
Maybe each top/bottom row has white dashed safety line.
Interior scenery shrubs e.g. coordinates derived: for rows1..7, cols, if (r*7+c*3)%11===0 draw shrub at x+11,y+39, etc. But rails maybe. draw before rails.
Buildings maybe rectangles in interior would look obstacles but no collision. Avoid.
Lamps at top row x=2,7,12,16 y=GY+CELL-5? small poles and yellow glow. Person offset+14 may overlap. Use x positions 1,5,10,15. bottom maybe.
Station canopy line.
Header static board title. Main dynamic draw waiting text maybe.
Let's ensure canvas render loop starts immediately; initial frame under banner. `requestAnimationFrame(frame)`.
At load script at end body.
Resize event debounce? build static if dpr changes.
No console errors.
Potential `Math.hypot`.
Colors.
Let's create.
We can also add route destination list footer maybe not.
Let's write final code with code block.