Train snakeOne-line
Muse Ember
by MetaReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 47.3 s
- Cost
- $0.012
- Output tokens
- 24,432
- Spent thinking
- 10,979 tokens
- Speed
- 554.1 tok/s
- Console errors
- 0
- First token after
- 3.2 s
- First visible text after
- 29.9 s
- Prompt tokens
- 380
- Code size
- 39.1 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
{"effort":"max"}- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 792 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passenger Express — Little Train Snake</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;padding:0;overflow-x:hidden}
body{
font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
background:linear-gradient(180deg,#bfe8ff 0%,#e6f7d9 55%,#fff3c4 100%);
color:#1e2a22;min-height:100vh;
}
header{
max-width:1240px;margin:0 auto;padding:14px 14px 8px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;
}
.logo{
width:62px;height:62px;border-radius:18px;background:linear-gradient(145deg,#d32f2f,#ff7043);
display:flex;align-items:center;justify-content:center;font-size:36px;box-shadow:0 8px 20px rgba(0,0,0,.2);
border:3px solid #fff;flex:0 0 auto;
}
header h1{margin:0;font-size:30px;line-height:1;letter-spacing:.3px;color:#17365d}
header h1 span{color:#d32f2f}
header p{margin:4px 0 0;font-size:14px;color:#40513f;max-width:560px}
.badge{
margin-left:auto;background:#fff;border:2px solid #ffca28;color:#7a5200;font-weight:800;font-size:12px;
padding:8px 12px;border-radius:999px;display:flex;align-items:center;gap:8px;box-shadow:0 4px 12px rgba(0,0,0,.12)
}
.dot{width:9px;height:9px;background:#43a047;border-radius:50%;animation:pulse 1s infinite alternate}
@keyframes pulse{from{transform:scale(1);opacity:1}to{transform:scale(1.5);opacity:.55}}
main{max-width:1240px;margin:0 auto;padding:8px 14px 18px;display:flex;gap:16px;align-items:flex-start;justify-content:center;flex-wrap:wrap}
.canvas-wrap{position:relative;flex:1 1 640px;max-width:880px;min-width:0}
canvas{
width:100%;height:auto;display:block;border-radius:18px;border:5px solid #2f3e2e;
box-shadow:0 18px 40px rgba(20,40,20,.28);background:#7fb069;touch-action:none;
}
aside{flex:0 1 310px;min-width:0;display:flex;flex-direction:column;gap:12px}
.card{background:rgba(255,255,255,.94);border-radius:18px;padding:14px;box-shadow:0 10px 24px rgba(0,0,0,.14);border:2px solid rgba(255,255,255,.9)}
.card h2{margin:0 0 10px;font-size:16px;color:#17365d}
.stats{display:grid;grid-template-columns:repeat(2,1fr);gap:9px}
.stat{background:#eef6ff;border-radius:13px;padding:10px 11px;border:2px solid #d6e8ff}
.stat b{display:block;font-size:24px;line-height:1;color:#17365d}
.stat small{font-size:11px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:#6a7d6b}
.stat.pink{background:#fff0f3;border-color:#ffd1dc}.stat.yellow{background:#fff8db;border-color:#ffe58a}
.stat.green{background:#eaffed;border-color:#b9f0c2}.stat.orange{background:#fff1e6;border-color:#ffd9b3}
#statusText{margin:10px 0 0;font-size:13px;font-weight:700;color:#40513f;background:#fffbe6;border:1px dashed #e6c84a;padding:8px 10px;border-radius:10px}
.waiting-line{font-size:14px;line-height:1.5}
.wait-people{font-size:26px;letter-spacing:4px;margin:6px 0 4px}
.wait-person{display:inline-block;animation:bob .8s infinite alternate ease-in-out}
.wait-person:nth-child(2){animation-delay:.15s}.wait-person:nth-child(3){animation-delay:.3s}
@keyframes bob{from{transform:translateY(0)}to{transform:translateY(-5px)}}
.btns{display:grid;grid-template-columns:1fr 1fr;gap:9px}
button{font-family:inherit;cursor:pointer;border:none;border-radius:13px;padding:12px 10px;font-weight:900;font-size:14px;box-shadow:0 5px 0 rgba(0,0,0,.18);transition:transform .08s, box-shadow .08s}
button:active{transform:translateY(3px);box-shadow:0 2px 0 rgba(0,0,0,.18)}
.primary{background:#43a047;color:#fff;grid-column:span 2;font-size:16px}
.secondary{background:#ff9800;color:#fff}
.blue{background:#1e88e5;color:#fff}
.purple{background:#8e24aa;color:#fff}
.light{background:#eef2f7;color:#17365d}
.dpad{display:grid;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(3,48px);gap:6px;justify-content:center;margin-top:6px}
.dpad button{background:#17365d;color:#fff;font-size:20px;padding:0;border-radius:12px}
.dpad .up{grid-column:2;grid-row:1}.dpad .left{grid-column:1;grid-row:2}.dpad .right{grid-column:3;grid-row:2}.dpad .down{grid-column:2;grid-row:2}
.how{margin:0;padding-left:18px;font-size:13.5px;line-height:1.55;color:#334433}
.legend{font-size:13.5px;line-height:1.7}
.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(14,26,38,.42);border-radius:18px;padding:12px;z-index:5}
.overlay.hidden{display:none}
.overlay-card{background:#fff;border-radius:22px;padding:22px 22px 18px;max-width:440px;width:100%;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,.35);border:4px solid #ffca28}
.overlay-card .train-emoji{font-size:54px;line-height:1}
.overlay-card h3{margin:6px 0 6px;font-size:28px;color:#17365d}
.overlay-card p{margin:6px 0;font-size:14.5px;line-height:1.5;color:#40513f}
.keys{display:inline-block;background:#eef6ff;border:2px solid #bcd8ff;border-radius:8px;padding:2px 8px;font-weight:900;color:#17365d}
.big-start{background:#d32f2f;color:#fff;font-size:18px;padding:14px 18px;width:100%;margin-top:12px}
.demo-note{background:#fff8db;border-radius:10px;padding:8px;font-size:12.5px;font-weight:800;color:#7a5200;margin-top:10px}
#pauseBadge{position:absolute;top:14px;left:50%;transform:translateX(-50%);background:#17365d;color:#fff;font-weight:900;letter-spacing:2px;padding:8px 16px;border-radius:999px;z-index:4;box-shadow:0 8px 20px rgba(0,0,0,.3)}
#pauseBadge.hidden{display:none}
footer{max-width:1240px;margin:0 auto;padding:0 14px 18px;font-size:12.5px;color:#5a6b5a;text-align:center}
.hidden{display:none!important}
@media(max-width:900px){
aside{flex:1 1 100%;max-width:880px}
.stats{grid-template-columns:repeat(4,1fr)}
}
@media(max-width:560px){
header h1{font-size:23px}.logo{width:50px;height:50px;font-size:28px}
.badge{margin-left:0}
.stats{grid-template-columns:repeat(2,1fr)}
.overlay-card{padding:16px}
.overlay-card h3{font-size:23px}
main{padding:6px 8px 12px}
}
</style>
</head>
<body>
<header>
<div class="logo">🚂</div>
<div>
<h1>Passenger <span>Express</span></h1>
<p>The snake is a little train! Drive the locomotive, collect passengers from the yellow platform, and grow your colorful wagons — don't crash into walls or your own tail.</p>
</div>
<div class="badge" id="demoBadge"><span class="dot"></span><span id="demoBadgeText">🤖 DEMO MODE — AI is driving</span></div>
</header>
<main>
<section class="canvas-wrap" id="canvasWrap">
<canvas id="game" width="880" height="600"></canvas>
<div id="pauseBadge" class="hidden">⏸ PAUSED</div>
<div class="overlay" id="startOverlay">
<div class="overlay-card">
<div class="train-emoji">🚂💨</div>
<h3>Passenger Express</h3>
<p>Drive the little train! Follow the pulsing ring to the <b>yellow platform</b> and pick up the waving passenger. Each passenger adds a colorful wagon!</p>
<p><span class="keys">← ↑ ↓ →</span> or <span class="keys">W A S D</span> to steer • <span class="keys">P</span> pause • <span class="keys">SPACE</span> whistle</p>
<p style="font-size:13px">📱 Swipe on the track or use the D-pad below.</p>
<button class="big-start" id="overlayStartBtn">▶ Click or press a key to start</button>
<div class="demo-note">🤖 A demo train is already driving behind this card — watch it collect passengers!</div>
</div>
</div>
<div class="overlay hidden" id="overOverlay">
<div class="overlay-card">
<div class="train-emoji">💥🚃</div>
<h3>Oh no! Derailment!</h3>
<p>Your train bumped into a wall or its own wagons.</p>
<p>Passengers delivered: <b id="finalPassengers">0</b><br>Score: <b id="finalScore">0</b> • Best: <b id="finalBest">0</b></p>
<button class="big-start" id="playAgainBtn">↻ Play Again</button>
<div class="demo-note">Tip: Press R or Enter to restart quickly.</div>
</div>
</div>
</section>
<aside>
<div class="card">
<h2>📊 Journey Stats</h2>
<div class="stats">
<div class="stat pink"><small>Passengers</small><b id="passengersVal">0</b></div>
<div class="stat yellow"><small>Score</small><b id="scoreVal">0</b></div>
<div class="stat green"><small>Wagons</small><b id="wagonsVal">5</b></div>
<div class="stat orange"><small>Best Score</small><b id="bestVal">0</b></div>
<div class="stat"><small>Level</small><b id="levelVal">1</b></div>
<div class="stat pink"><small>Speed</small><b id="speedVal">Normal</b></div>
</div>
<div id="statusText">🤖 Demo running — click the track or press any arrow key to take the controls!</div>
</div>
<div class="card">
<h2>🧍 Waiting on the Platform</h2>
<div class="wait-people"><span class="wait-person">🧍♀️</span><span class="wait-person">🧍♂️</span><span class="wait-person">👧</span><span class="wait-person">👦</span></div>
<div class="waiting-line" id="waitingInfo">A passenger is waving at the yellow platform…</div>
</div>
<div class="card">
<h2>🎛 Controls</h2>
<div class="btns">
<button class="primary" id="startBtn">▶ Start / Pause</button>
<button class="secondary" id="restartBtn">↻ Restart</button>
<button class="blue" id="soundBtn">🔊 Sound</button>
<button class="purple" id="speedBtn">⚡ Speed: Normal</button>
<button class="light" id="whistleBtn">💨 Whistle!</button>
</div>
<div class="dpad" aria-label="Direction pad">
<button class="up" data-dx="0" data-dy="-1" aria-label="Up">▲</button>
<button class="left" data-dx="-1" data-dy="0" aria-label="Left">◀</button>
<button class="down" data-dx="0" data-dy="1" aria-label="Down">▼</button>
<button class="right" data-dx="1" data-dy="0" aria-label="Right">▶</button>
</div>
</div>
<div class="card">
<h2>📖 How to play</h2>
<ul class="how">
<li>Steer with <b>arrows / WASD</b>, swipe, or the D-pad.</li>
<li>Drive onto the <b>yellow platform</b> to pick up the waving passenger.</li>
<li>Every passenger adds one colorful wagon and +100 points.</li>
<li>You get faster as you travel — watch your tail!</li>
<li>Don't hit the fence or your own train. <b>P</b> pauses, <b>Space</b> whistles.</li>
</ul>
<div class="legend" style="margin-top:8px">🔴 Locomotive 🟦 Wagons 🟨 Platform ⭐ Pickup target</div>
</div>
</aside>
</main>
<footer>Little Train Snake • Pick up passengers, grow your wagons, and try not to derail! 🚂</footer>
<script>
(function(){
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const COLS = 22, ROWS = 15, CELL = 40;
canvas.width = COLS*CELL; canvas.height = ROWS*CELL;
const passengersVal = document.getElementById('passengersVal');
const scoreVal = document.getElementById('scoreVal');
const wagonsVal = document.getElementById('wagonsVal');
const bestVal = document.getElementById('bestVal');
const levelVal = document.getElementById('levelVal');
const speedVal = document.getElementById('speedVal');
const statusText = document.getElementById('statusText');
const waitingInfo = document.getElementById('waitingInfo');
const startOverlay = document.getElementById('startOverlay');
const overOverlay = document.getElementById('overOverlay');
const pauseBadge = document.getElementById('pauseBadge');
const demoBadgeText = document.getElementById('demoBadgeText');
const names = ["Emma","Liam","Olivia","Noah","Ava","Mia","Lucas","Zoe","Leo","Ruby","Max","Nina","Ollie","Ivy","Felix","Poppy","Sam","Luna"];
const shirts = ["#e53935","#1e88e5","#43a047","#fb8c00","#8e24aa","#00acc1","#d81b60","#fdd835","#3949ab","#00bfa5"];
const skins = ["#ffdbac","#f1c27d","#e0ac69","#8d5524","#ffdbac","#c68642"];
const hairs = ["#3e2723","#6d4c41","#ffd54f","#212121","#a1887f","#ff7043","#5d4037"];
const stations = ["Central","Riverside","Hilltop","Sunnyvale","Seaside","Market","Garden"];
const wagonColors = ["#1e88e5","#43a047","#fb8c00","#8e24aa","#00acc1","#d81b60","#fdd835","#3949ab","#7cb342","#fb8c00"];
let snake, prevSnake, dir, dirQueue, food, passengers, score, best=0, level, lastTick, running=true, paused=false, demo=true, gameOver=false, userStarted=false;
let particles=[], floats=[], smokes=[], decor=[];
let soundOn=true, audioCtx=null, speedFactor=1, speedIndex=0;
const speedModes=[{f:1,label:"Normal"},{f:0.72,label:"Fast"},{f:0.55,label:"Turbo"},{f:1.35,label:"Slow"}];
let shake=0, lastSmoke=0, lastHeadInfo={x:100,y:100,angle:0};
function rand(a,b){return Math.floor(Math.random()*(b-a+1))+a}
function pick(arr){return arr[Math.floor(Math.random()*arr.length)]}
function rr(x,y,w,h,r){
ctx.beginPath(); ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
}
function makePerson(){
return {name:pick(names), shirt:pick(shirts), skin:pick(skins), hair:pick(hairs), station:pick(stations), platform:rand(1,4)};
}
function ensureAudio(){
try{
if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)();
if(audioCtx.state==='suspended') audioCtx.resume();
}catch(e){}
}
function tone(freq,dur,type,vol,delay){
if(!soundOn||!audioCtx) return;
try{
const t=audioCtx.currentTime+(delay||0);
const o=audioCtx.createOscillator(), g=audioCtx.createGain();
o.type=type||'sine'; o.frequency.setValueAtTime(freq,t);
g.gain.setValueAtTime(vol||0.12,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur);
o.connect(g); g.connect(audioCtx.destination); o.start(t); o.stop(t+dur+0.02);
}catch(e){}
}
function playPickup(){ tone(660,.12,'sine',.14,0); tone(880,.16,'sine',.14,.09); tone(1320,.12,'triangle',.08,.16); }
function playCrash(){ tone(180,.35,'sawtooth',.16,0); tone(90,.5,'sawtooth',.14,.08); }
function playWhistleSound(){ tone(740,.18,'square',.07,0); tone(988,.22,'square',.07,.14); tone(740,.28,'square',.06,.38); }
function playClick(){ tone(520,.07,'triangle',.08,0); }
function initDecor(){
decor=[];
for(let i=0;i<90;i++) decor.push({x:Math.random()*COLS, y:Math.random()*ROWS, c:pick(["#ffeb3b","#ff8a80","#ffffff","#ffccbc"]), s:2+Math.random()*2});
}
initDecor();
function platformCells(){
if(!food) return [];
if(food.horizontal) return [{x:food.x-1,y:food.y},{x:food.x,y:food.y},{x:food.x+1,y:food.y}];
return [{x:food.x,y:food.y-1},{x:food.x,y:food.y},{x:food.x,y:food.y+1}];
}
function spawnFood(){
let pos, attempts=0;
while(attempts<600){
pos={x:rand(0,COLS-1), y:rand(0,ROWS-1)}; attempts++;
const onSnake = snake.some(s=>s.x===pos.x&&s.y===pos.y);
const dist = Math.abs(pos.x-snake[0].x)+Math.abs(pos.y-snake[0].y);
if(!onSnake && (dist>=4 || attempts>300)) break;
}
const canH = pos.x>=1 && pos.x<=COLS-2;
const canV = pos.y>=1 && pos.y<=ROWS-2;
let horizontal=true;
if(canH && canV) horizontal = Math.random()<0.65;
else if(canV) horizontal=false; else horizontal=true;
food={x:pos.x,y:pos.y,horizontal:horizontal, person:makePerson(), queue:[makePerson(),makePerson()]};
waitingInfo.innerHTML = "👤 <b>"+food.person.name+"</b> is waiting at <b>"+food.person.station+" Station</b>, Platform "+food.person.platform+"!<br>Next: "+food.queue[0].name+" & "+food.queue[1].name+" are in the queue. Drive to the <b>yellow platform ⭐</b>!";
updateHUD();
}
function calcTick(){ return Math.max(68, 158 - passengers*4) * speedFactor; }
function resetGame(isDemo){
demo=isDemo;
snake=[{x:5,y:7},{x:4,y:7},{x:3,y:7},{x:2,y:7},{x:1,y:7}];
prevSnake=snake.map(s=>({x:s.x,y:s.y}));
dir={x:1,y:0}; dirQueue=[];
passengers=0; score=0; level=1; particles=[]; floats=[]; smokes=[]; shake=0;
gameOver=false; paused=false; running=true; lastTick=performance.now(); lastSmoke=0;
spawnFood();
overOverlay.classList.add('hidden');
pauseBadge.classList.add('hidden');
if(isDemo){
startOverlay.classList.remove('hidden');
demoBadgeText.textContent="🤖 DEMO MODE — AI is driving";
statusText.textContent="🤖 Demo running — click the track or press any arrow key to take the controls!";
}else{
startOverlay.classList.add('hidden');
demoBadgeText.textContent="🎮 YOU ARE DRIVING!";
statusText.textContent="🎮 You're driving! Head to the yellow platform to pick up the passenger. Good luck!";
}
updateHUD();
}
function updateHUD(){
passengersVal.textContent=passengers;
scoreVal.textContent=score;
wagonsVal.textContent=snake?snake.length:5;
bestVal.textContent=best;
levelVal.textContent=level;
speedVal.textContent=speedModes[speedIndex].label;
}
function queueDir(dx,dy){
const last = dirQueue.length?dirQueue[dirQueue.length-1]:dir;
if(dx===-last.x && dy===-last.y) return;
if(dx===last.x && dy===last.y) return;
if(dirQueue.length<3) dirQueue.push({x:dx,y:dy});
}
function startFromDemo(){
if(userStarted) return;
userStarted=true; ensureAudio(); playClick();
resetGame(false);
}
// AI BFS
function aiTurn(){
const head=snake[0];
const blocked=new Set();
for(let i=1;i<snake.length-1;i++) blocked.add(snake[i].x+','+snake[i].y);
const visited=new Set([head.x+','+head.y]);
const q=[{x:head.x,y:head.y,first:null}];
let found=null;
const dirs=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
while(q.length && !found){
const cur=q.shift();
for(const d of dirs){
const nx=cur.x+d.x, ny=cur.y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
const k=nx+','+ny;
if(visited.has(k)||blocked.has(k)) continue;
visited.add(k);
const first = cur.first || {x:nx,y:ny};
if(nx===food.x && ny===food.y){ found=first; break; }
q.push({x:nx,y:ny,first:first});
}
}
if(found){
const dx=found.x-head.x, dy=found.y-head.y;
if(!(dx===-dir.x && dy===-dir.y)){ dirQueue=[]; queueDir(dx,dy); }
return;
}
// safe wander
const shuffled=dirs.slice().sort(()=>Math.random()-0.5);
for(const d of shuffled){
const nx=head.x+d.x, ny=head.y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
if(blocked.has(nx+','+ny)) continue;
if(d.x===-dir.x && d.y===-dir.y) continue;
dirQueue=[]; queueDir(d.x,d.y); return;
}
}
function step(){
prevSnake=snake.map(s=>({x:s.x,y:s.y}));
if(dirQueue.length) dir=dirQueue.shift();
const head=snake[0];
const nh={x:head.x+dir.x, y:head.y+dir.y};
// wall
if(nh.x<0||nh.y<0||nh.x>=COLS||nh.y>=ROWS){ doCrash(); return; }
// self (exclude tail that will move, unless eating)
const willEat = (nh.x===food.x && nh.y===food.y);
const bodyToCheck = willEat ? snake : snake.slice(0,-1);
if(bodyToCheck.some(s=>s.x===nh.x&&s.y===nh.y)){ doCrash(); return; }
snake.unshift(nh);
if(willEat){
passengers++;
const gained=100+(level-1)*25;
score+=gained;
level=1+Math.floor(passengers/4);
playPickup();
// particles
const cx=nh.x*CELL+CELL/2, cy=nh.y*CELL+CELL/2;
for(let i=0;i<18;i++){
particles.push({x:cx,y:cy,vx:(Math.random()-0.5)*5,vy:(Math.random()-0.7)*5,life:1,sz:3+Math.random()*5,c:pick(["#ffeb3b","#ff4081","#ffffff","#69f0ae","#ff9800"])});
}
floats.push({x:cx,y:cy-18,text:"+1 Passenger aboard! +"+gained,life:1});
floats.push({x:cx,y:cy+6,text:food.person.name+" says Choo! 🚂",life:1});
spawnFood();
}else{
snake.pop();
}
updateHUD();
}
function doCrash(){
gameOver=true; shake=14; playCrash();
const cx=snake[0].x*CELL+CELL/2, cy=snake[0].y*CELL+CELL/2;
for(let i=0;i<26;i++) smokes.push({x:cx,y:cy,r:8+Math.random()*14,life:1,vy:-0.6-Math.random(),vx:(Math.random()-0.5)*1.4});
for(let i=0;i<20;i++) particles.push({x:cx,y:cy,vx:(Math.random()-0.5)*6,vy:(Math.random()-0.5)*6,life:1,sz:3+Math.random()*5,c:"#ff7043"});
if(demo){
setTimeout(()=>{ if(demo) resetGame(true); },1400);
}else{
if(score>best) best=score;
document.getElementById('finalPassengers').textContent=passengers;
document.getElementById('finalScore').textContent=score;
document.getElementById('finalBest').textContent=best;
overOverlay.classList.remove('hidden');
statusText.textContent="💥 Derailment! Press Play Again or R to try another journey.";
updateHUD();
}
}
function whistle(){
ensureAudio(); playWhistleSound();
const h=lastHeadInfo;
floats.push({x:h.x,y:h.y-24,text:"♪ Choo-choo! ♪",life:1.2});
for(let i=0;i<10;i++) smokes.push({x:h.x,y:h.y,r:6+Math.random()*10,life:1,vy:-1-Math.random(),vx:(Math.random()-0.5)*2});
}
// Drawing helpers
function interp(i,alpha){
const cur=snake[i];
const prev=prevSnake[Math.min(i,prevSnake.length-1)] || cur;
return {x:prev.x+(cur.x-prev.x)*alpha, y:prev.y+(cur.y-prev.y)*alpha};
}
function drawBackground(){
const g=ctx.createLinearGradient(0,0,0,canvas.height);
g.addColorStop(0,"#8bc07a"); g.addColorStop(1,"#6da35c");
ctx.fillStyle=g; ctx.fillRect(0,0,canvas.width,canvas.height);
// track bed cells
for(let y=0;y<ROWS;y++){
for(let x=0;x<COLS;x++){
ctx.fillStyle=((x+y)%2===0)?"#c9bda0":"#bcb090";
ctx.fillRect(x*CELL+1,y*CELL+1,CELL-2,CELL-2);
// sleepers (horizontal)
ctx.fillStyle="rgba(101,67,33,0.55)";
for(let s=0;s<3;s++) ctx.fillRect(x*CELL+3, y*CELL+5+s*13, CELL-6, 4);
// horizontal rails
ctx.strokeStyle="rgba(230,230,230,0.9)"; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x*CELL, y*CELL+13); ctx.lineTo(x*CELL+CELL, y*CELL+13); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x*CELL, y*CELL+27); ctx.lineTo(x*CELL+CELL, y*CELL+27); ctx.stroke();
// vertical rails faint
ctx.strokeStyle="rgba(230,230,230,0.35)"; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.moveTo(x*CELL+13, y*CELL); ctx.lineTo(x*CELL+13, y*CELL+CELL); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x*CELL+27, y*CELL); ctx.lineTo(x*CELL+27, y*CELL+CELL); ctx.stroke();
}
}
// decor flowers / grass tufts at intersections
for(const d of decor){
ctx.fillStyle=d.c; ctx.globalAlpha=0.65;
ctx.beginPath(); ctx.arc(d.x*CELL, d.y*CELL, d.s, 0, Math.PI*2); ctx.fill();
ctx.globalAlpha=1;
}
// corner trees
const trees=[[18,18],[canvas.width-18,18],[18,canvas.height-18],[canvas.width-18,canvas.height-18]];
for(const t of trees){
ctx.fillStyle="rgba(0,0,0,0.18)"; ctx.beginPath(); ctx.ellipse(t[0]+3,t[1]+4,16,8,0,0,Math.PI*2); ctx.fill();
ctx.fillStyle="#2e7d32"; ctx.beginPath(); ctx.arc(t[0],t[1],15,0,Math.PI*2); ctx.fill();
ctx.fillStyle="#43a047"; ctx.beginPath(); ctx.arc(t[0]-4,t[1]-4,9,0,Math.PI*2); ctx.fill();
ctx.fillStyle="#6d4c41"; ctx.fillRect(t[0]-2,t[1]+8,4,8);
}
// fence border
ctx.strokeStyle="#5d4037"; ctx.lineWidth=6; ctx.strokeRect(3,3,canvas.width-6,canvas.height-6);
ctx.fillStyle="#8d6e63";
for(let x=0;x<=COLS;x++) for(let y=0;y<=ROWS;y++){
if(x===0||y===0||x===COLS||y===ROWS){
ctx.fillRect(x*CELL-3, y*CELL-3, 6, 6);
}
}
}
function drawPerson(cx,cy,scale,person,waving,now,highlight){
ctx.save(); ctx.translate(cx,cy);
const bob = waving ? Math.sin(now/180)*2.2 : 0;
ctx.translate(0,bob);
// shadow
ctx.fillStyle="rgba(0,0,0,0.22)"; ctx.beginPath(); ctx.ellipse(0,16*scale,11*scale,4*scale,0,0,Math.PI*2); ctx.fill();
// legs
ctx.fillStyle="#37474f"; ctx.fillRect(-6*scale,6*scale,5*scale,10*scale); ctx.fillRect(1*scale,6*scale,5*scale,10*scale);
ctx.fillStyle="#263238"; ctx.fillRect(-7*scale,15*scale,7*scale,3*scale); ctx.fillRect(0,15*scale,7*scale,3*scale);
// body shirt
ctx.fillStyle=person.shirt; rr(-8*scale,-8*scale,16*scale,16*scale,5*scale); ctx.fill();
// arms
ctx.strokeStyle=person.skin; ctx.lineWidth=4*scale; ctx.lineCap="round";
ctx.beginPath(); ctx.moveTo(-8*scale,-4*scale); ctx.lineTo(-13*scale,3*scale); ctx.stroke();
if(waving){
const a=-0.6+Math.sin(now/140)*0.5;
const hx=8*scale+Math.cos(a)*10*scale, hy=-4*scale+Math.sin(a)*10*scale;
ctx.beginPath(); ctx.moveTo(8*scale,-4*scale); ctx.lineTo(hx,hy); ctx.stroke();
ctx.fillStyle=person.skin; ctx.beginPath(); ctx.arc(hx,hy,3.5*scale,0,Math.PI*2); ctx.fill();
}else{
ctx.beginPath(); ctx.moveTo(8*scale,-4*scale); ctx.lineTo(13*scale,3*scale); ctx.stroke();
}
// head
ctx.fillStyle=person.skin; ctx.beginPath(); ctx.arc(0,-14*scale,8*scale,0,Math.PI*2); ctx.fill();
// hair
ctx.fillStyle=person.hair; ctx.beginPath(); ctx.arc(0,-17*scale,8*scale,Math.PI,Math.PI*2); ctx.fill();
ctx.fillRect(-8*scale,-18*scale,16*scale,4*scale);
// face
ctx.fillStyle="#212121"; ctx.beginPath(); ctx.arc(-3*scale,-13*scale,1.1*scale,0,Math.PI*2); ctx.arc(3*scale,-13*scale,1.1*scale,0,Math.PI*2); ctx.fill();
ctx.strokeStyle="#212121"; ctx.lineWidth=1.2; ctx.beginPath(); ctx.arc(0,-11*scale,3*scale,0.15*Math.PI,0.85*Math.PI); ctx.stroke();
// bag for target
if(highlight){
ctx.fillStyle="#ffca28"; rr(9*scale,2*scale,8*scale,9*scale,2*scale); ctx.fill();
ctx.strokeStyle="#8d6e63"; ctx.lineWidth=1; ctx.strokeRect(11*scale,-2*scale,4*scale,5*scale);
}
ctx.restore();
}
function drawPlatform(now){
if(!food) return;
const cells=platformCells();
const minX=Math.min(...cells.map(c=>c.x)), maxX=Math.max(...cells.map(c=>c.x));
const minY=Math.min(...cells.map(c=>c.y)), maxY=Math.max(...cells.map(c=>c.y));
const px=minX*CELL+3, py=minY*CELL+3, pw=(maxX-minX+1)*CELL-6, ph=(maxY-minY+1)*CELL-6;
// pulse ring around target
const cx=food.x*CELL+CELL/2, cy=food.y*CELL+CELL/2;
const pulse=1+Math.sin(now/220)*0.12;
ctx.save(); ctx.globalAlpha=0.55+Math.sin(now/200)*0.2;
ctx.strokeStyle="#ffeb3b"; ctx.lineWidth=4; ctx.beginPath(); ctx.arc(cx,cy,26*pulse,0,Math.PI*2); ctx.stroke();
ctx.restore();
// platform base
ctx.fillStyle="rgba(0,0,0,0.2)"; rr(px+3,py+5,pw,ph,10); ctx.fill();
const grad=ctx.createLinearGradient(px,py,px,py+ph);
grad.addColorStop(0,"#fff3a6"); grad.addColorStop(1,"#f9d976");
ctx.fillStyle=grad; rr(px,py,pw,ph,10); ctx.fill();
ctx.strokeStyle="#d4a017"; ctx.lineWidth=3; rr(px,py,pw,ph,10); ctx.stroke();
// tactile dots along edge
ctx.fillStyle="#d4a017";
if(food.horizontal){
for(let i=0;i<pw;i+=12){ ctx.beginPath(); ctx.arc(px+8+i, py+ph-7, 2,0,Math.PI*2); ctx.fill(); }
}else{
for(let i=0;i<ph;i+=12){ ctx.beginPath(); ctx.arc(px+pw-7, py+8+i, 2,0,Math.PI*2); ctx.fill(); }
}
// bench on a non-target cell
const benchCell = cells.find(c=>!(c.x===food.x&&c.y===food.y)) || cells[0];
const bx=benchCell.x*CELL+CELL/2, by=benchCell.y*CELL+CELL/2;
ctx.fillStyle="#795548"; rr(bx-14,by-4,28,8,3); ctx.fill();
ctx.fillStyle="#5d4037"; ctx.fillRect(bx-11,by+4,3,7); ctx.fillRect(bx+8,by+4,3,7);
// extra waiting passengers (small)
const others=cells.filter(c=>!(c.x===food.x&&c.y===food.y));
others.forEach((c,idx)=>{
const p=food.queue[idx%food.queue.length];
drawPerson(c.x*CELL+CELL/2, c.y*CELL+CELL/2+2, 0.72, p, true, now+idx*220, false);
});
// sign
const signCell=cells[0];
const sx=signCell.x*CELL+CELL/2, sy=signCell.y*CELL+CELL/2-18;
ctx.fillStyle="#546e7a"; ctx.fillRect(sx-1,sy+10,2,16);
ctx.fillStyle="#ffffff"; rr(sx-30,sy-6,60,17,4); ctx.fill(); ctx.strokeStyle="#17365d"; ctx.lineWidth=1.5; rr(sx-30,sy-6,60,17,4); ctx.stroke();
ctx.fillStyle="#17365d"; ctx.font="bold 8px system-ui,Arial"; ctx.textAlign="center";
ctx.fillText(food.person.station.toUpperCase(), sx, sy+4);
ctx.font="bold 7px system-ui,Arial"; ctx.fillStyle="#d32f2f"; ctx.fillText("PLATFORM "+food.person.platform, sx, sy+11);
// main passenger
drawPerson(cx,cy+2,1.15,food.person,true,now,true);
// bouncing arrow
const ay=cy-38+Math.sin(now/200)*4;
ctx.fillStyle="#d32f2f"; ctx.beginPath(); ctx.moveTo(cx-9,ay); ctx.lineTo(cx+9,ay); ctx.lineTo(cx,ay+11); ctx.closePath(); ctx.fill();
ctx.strokeStyle="#fff"; ctx.lineWidth=2; ctx.stroke();
ctx.fillStyle="#fff"; ctx.font="bold 11px system-ui,Arial"; ctx.textAlign="center"; ctx.fillText("⭐", cx, ay-4);
// speech bubble
const bubbleText=food.person.name+" • Choo! 🚂";
ctx.font="bold 11px system-ui,Arial";
const bw=ctx.measureText(bubbleText).width+16;
let bx2=cx-bw/2; if(bx2<6) bx2=6; if(bx2+bw>canvas.width-6) bx2=canvas.width-6-bw;
const by2=Math.max(6,cy-72+Math.sin(now/300)*2);
ctx.fillStyle="#ffffff"; rr(bx2,by2,bw,22,9); ctx.fill(); ctx.strokeStyle="#17365d"; ctx.lineWidth=1.5; rr(bx2,by2,bw,22,9); ctx.stroke();
ctx.fillStyle="#17365d"; ctx.textAlign="center"; ctx.fillText(bubbleText,bx2+bw/2,by2+15);
ctx.beginPath(); ctx.moveTo(cx-5,by2+22); ctx.lineTo(cx+5,by2+22); ctx.lineTo(cx,by2+29); ctx.closePath(); ctx.fillStyle="#fff"; ctx.fill();
}
function drawCar(cx,cy,angle,isLoco,index,now){
const L=36,W=27;
ctx.save(); ctx.translate(cx,cy); ctx.rotate(angle);
// shadow
ctx.fillStyle="rgba(0,0,0,0.22)"; ctx.beginPath(); ctx.ellipse(1,3,L/2,W/2+2,0,0,Math.PI*2); ctx.fill();
// coupling
if(!isLoco){ ctx.fillStyle="#263238"; ctx.fillRect(-L/2-4,-3,5,6); }
// wheels
const wheelR=isLoco?6:5;
const wheelPos=[[-12,-W/2+2],[12,-W/2+2],[-12,W/2-2],[12,W/2-2]];
for(const w of wheelPos){
ctx.fillStyle="#212121"; ctx.beginPath(); ctx.arc(w[0],w[1],wheelR,0,Math.PI*2); ctx.fill();
ctx.fillStyle="#9e9e9e"; ctx.beginPath(); ctx.arc(w[0],w[1],2.2,0,Math.PI*2); ctx.fill();
// spokes rotating
const wa=now/90;
ctx.strokeStyle="#616161"; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(w[0]-Math.cos(wa)*wheelR,w[1]-Math.sin(wa)*wheelR); ctx.lineTo(w[0]+Math.cos(wa)*wheelR,w[1]+Math.sin(wa)*wheelR); ctx.stroke();
}
if(isLoco){
// body
const g=ctx.createLinearGradient(0,-W/2,0,W/2); g.addColorStop(0,"#ef5350"); g.addColorStop(0.5,"#c62828"); g.addColorStop(1,"#8e0000");
ctx.fillStyle=g; rr(-L/2,-W/2,L,W,6); ctx.fill(); ctx.strokeStyle="#5f0000"; ctx.lineWidth=2; rr(-L/2,-W/2,L,W,6); ctx.stroke();
// boiler front
ctx.fillStyle="#7f0000"; rr(2,-W/2+3,L/2-5,W-6,5); ctx.fill();
// cab window
ctx.fillStyle="#b3e5fc"; rr(-L/2+4,-W/2+4,14,8,2); ctx.fill(); rr(-L/2+4,W/2-12,14,8,2); ctx.fill();
ctx.fillStyle="#fff"; ctx.font="bold 10px system-ui,Arial"; ctx.textAlign="center"; ctx.fillText("1",-L/2+11,3);
// chimney
ctx.fillStyle="#212121"; ctx.beginPath(); ctx.arc(12,0,7.5,0,Math.PI*2); ctx.fill();
ctx.fillStyle="#616161"; ctx.beginPath(); ctx.arc(12,0,4,0,Math.PI*2); ctx.fill();
// dome
ctx.fillStyle="#ffca28"; ctx.beginPath(); ctx.arc(-2,0,5.5,0,Math.PI*2); ctx.fill(); ctx.strokeStyle="#c17900"; ctx.lineWidth=1; ctx.stroke();
// headlight
ctx.save(); ctx.shadowColor="#ffeb3b"; ctx.shadowBlur=10;
ctx.fillStyle="#ffeb3b"; ctx.beginPath(); ctx.arc(L/2-1,0,4.5,0,Math.PI*2); ctx.fill(); ctx.restore();
// cowcatcher
ctx.fillStyle="#9e9e9e"; ctx.beginPath(); ctx.moveTo(L/2-2,-9); ctx.lineTo(L/2+7,0); ctx.lineTo(L/2-2,9); ctx.closePath(); ctx.fill(); ctx.strokeStyle="#616161"; ctx.stroke();
// stripe
ctx.fillStyle="#ffca28"; ctx.fillRect(-L/2+2,-2,L-4,4);
}else{
const col=wagonColors[(index-1)%wagonColors.length];
const g=ctx.createLinearGradient(0,-W/2,0,W/2); g.addColorStop(0,col); g.addColorStop(1,"#26323833"); g.addColorStop(1,col);
ctx.fillStyle=col; rr(-L/2,-W/2,L,W,6); ctx.fill(); ctx.strokeStyle="rgba(0,0,0,0.35)"; ctx.lineWidth=2; rr(-L/2,-W/2,L,W,6); ctx.stroke();
// roof spine
ctx.fillStyle="rgba(255,255,255,0.85)"; rr(-10,-4,20,8,3); ctx.fill();
// windows strips top/bottom
ctx.fillStyle="#e1f5fe"; rr(-13,-W/2+3,26,6,2); ctx.fill(); rr(-13,W/2-9,26,6,2); ctx.fill();
ctx.strokeStyle="#0277bd"; ctx.lineWidth=1; rr(-13,-W/2+3,26,6,2); ctx.stroke(); rr(-13,W/2-9,26,6,2); ctx.stroke();
// passenger heads in windows if delivered
if(index <= passengers+2){
ctx.fillStyle=skins[index%skins.length];
ctx.beginPath(); ctx.arc(-6,-W/2+6,3,0,Math.PI*2); ctx.arc(6,-W/2+6,3,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(-6,W/2-6,3,0,Math.PI*2); ctx.arc(6,W/2-6,3,0,Math.PI*2); ctx.fill();
}
// bottom stripe
ctx.fillStyle="#ffffff"; ctx.globalAlpha=0.85; ctx.fillRect(-L/2+3,-1.5,L-6,3); ctx.globalAlpha=1;
// wagon number
ctx.fillStyle="rgba(255,255,255,0.95)"; ctx.font="bold 9px system-ui,Arial"; ctx.textAlign="center"; ctx.fillText(String(index),0,3);
}
ctx.restore();
}
function drawTrain(now,alpha){
if(!snake) return;
// compute head info for smoke
const headP=interp(0,alpha);
let headAngle=Math.atan2(dir.y,dir.x);
if(snake.length>1){
const p1=interp(1,alpha);
// head points away from second car
const dx=headP.x-p1.x, dy=headP.y-p1.y;
if(Math.abs(dx)+Math.abs(dy)>0.01) headAngle=Math.atan2(dy,dx);
}
lastHeadInfo={x:(headP.x*CELL+CELL/2)+Math.cos(headAngle)*12, y:(headP.y*CELL+CELL/2)+Math.sin(headAngle)*12, angle:headAngle};
// emit smoke
if(!gameOver && now-lastSmoke>150){
lastSmoke=now;
smokes.push({x:lastHeadInfo.x,y:lastHeadInfo.y,r:5+Math.random()*4,life:1,vy:-0.7-Math.random()*0.5,vx:-Math.cos(headAngle)*0.3+(Math.random()-0.5)*0.4});
}
// draw from tail to head
for(let i=snake.length-1;i>=0;i--){
const p=interp(i,alpha);
let angle;
if(i===0) angle=headAngle;
else{
const front=interp(i-1,alpha);
angle=Math.atan2(front.y-p.y, front.x-p.x);
if(Math.abs(front.x-p.x)+Math.abs(front.y-p.y)<0.01) angle=headAngle;
}
drawCar(p.x*CELL+CELL/2, p.y*CELL+CELL/2, angle, i===0, i, now);
}
}
function draw(now){
ctx.save();
if(shake>0){ ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake); shake*=0.88; if(shake<0.4) shake=0; }
let alpha=1;
if(running && !paused && !gameOver) alpha=Math.min(1,(now-lastTick)/calcTick());
else alpha=1;
drawBackground();
drawPlatform(now);
// smoke behind train
for(const s of smokes){
ctx.globalAlpha=Math.max(0,s.life)*0.55; ctx.fillStyle="#eeeeee";
ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
drawTrain(now,alpha);
// particles
for(const p of particles){
ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.c;
ctx.beginPath(); ctx.arc(p.x,p.y,p.sz,0,Math.PI*2); ctx.fill();
// star sparkle
ctx.globalAlpha=Math.max(0,p.life)*0.8; ctx.fillRect(p.x-1,p.y-p.sz*1.6,2,p.sz*3.2); ctx.fillRect(p.x-p.sz*1.6,p.y-1,p.sz*3.2,2);
}
ctx.globalAlpha=1;
// floats
for(const f of floats){
ctx.globalAlpha=Math.max(0,f.life);
ctx.font="bold 15px system-ui,Arial"; ctx.textAlign="center";
ctx.strokeStyle="rgba(0,0,0,0.6)"; ctx.lineWidth=4; ctx.strokeText(f.text,f.x,f.y);
ctx.fillStyle="#fff"; ctx.fillText(f.text,f.x,f.y);
}
ctx.globalAlpha=1;
ctx.restore();
// update particles/smoke/floats
for(const s of smokes){ s.y+=s.vy; s.x+=s.vx; s.r+=0.14; s.life-=0.012; }
smokes=smokes.filter(s=>s.life>0);
for(const p of particles){ p.x+=p.vx; p.y+=p.vy; p.vy+=0.08; p.life-=0.022; }
particles=particles.filter(p=>p.life>0);
for(const f of floats){ f.y-=0.6; f.life-=0.014; }
floats=floats.filter(f=>f.life>0);
}
function loop(now){
requestAnimationFrame(loop);
if(running && !paused && !gameOver && now-lastTick>=calcTick()){
if(demo) aiTurn();
step();
lastTick=now;
}
draw(now);
}
// Events
function handleDir(dx,dy){
if(!userStarted) startFromDemo();
ensureAudio();
if(gameOver) return;
if(paused){ paused=false; pauseBadge.classList.add('hidden'); lastTick=performance.now(); statusText.textContent="🎮 You're driving! Head to the yellow platform!"; }
queueDir(dx,dy);
}
document.addEventListener('keydown',(e)=>{
const k=e.key;
const isArrow=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(k);
if(isArrow) e.preventDefault();
const lower=k.toLowerCase();
if(!userStarted && (isArrow || ["w","a","s","d","enter","p","r","b"].includes(lower))){
startFromDemo();
// fall through to handle this key as action below (except Enter/P/R)
if(k==="Enter"||lower==="p"||lower==="r") return;
}
if(gameOver && (k==="Enter"||lower==="r"||k===" ")){
userStarted=true; resetGame(false); ensureAudio(); return;
}
if(k==="ArrowUp"||lower==="w") handleDir(0,-1);
else if(k==="ArrowDown"||lower==="s") handleDir(0,1);
else if(k==="ArrowLeft"||lower==="a") handleDir(-1,0);
else if(k==="ArrowRight"||lower==="d") handleDir(1,0);
else if(k===" "||lower==="b"){ ensureAudio(); whistle(); }
else if(lower==="p"){ togglePause(); }
else if(lower==="r"){ userStarted=true; ensureAudio(); resetGame(false); }
else if(k==="Enter" && !userStarted){ startFromDemo(); }
});
function togglePause(){
if(!userStarted){ startFromDemo(); return; }
if(gameOver) return;
paused=!paused;
pauseBadge.classList.toggle('hidden',!paused);
statusText.textContent=paused?"⏸ Paused — press P or Start/Pause to resume.":"🎮 You're driving! Head to the yellow platform!";
if(!paused) lastTick=performance.now();
ensureAudio(); playClick();
}
document.getElementById('overlayStartBtn').addEventListener('click',(e)=>{ e.stopPropagation(); startFromDemo(); });
startOverlay.addEventListener('click',()=>{ startFromDemo(); });
document.getElementById('playAgainBtn').addEventListener('click',()=>{ userStarted=true; ensureAudio(); resetGame(false); });
document.getElementById('startBtn').addEventListener('click',togglePause);
document.getElementById('restartBtn').addEventListener('click',()=>{ userStarted=true; ensureAudio(); playClick(); resetGame(false); });
document.getElementById('soundBtn').addEventListener('click',(e)=>{ soundOn=!soundOn; e.target.textContent=soundOn?"🔊 Sound":"🔇 Muted"; if(soundOn){ensureAudio(); playClick();} });
document.getElementById('speedBtn').addEventListener('click',(e)=>{
speedIndex=(speedIndex+1)%speedModes.length; speedFactor=speedModes[speedIndex].f;
e.target.textContent="⚡ Speed: "+speedModes[speedIndex].label; updateHUD(); ensureAudio(); playClick();
});
document.getElementById('whistleBtn').addEventListener('click',()=>{ if(!userStarted) startFromDemo(); whistle(); });
document.querySelectorAll('.dpad button').forEach(btn=>{
btn.addEventListener('click',()=>{
const dx=parseInt(btn.dataset.dx,10), dy=parseInt(btn.dataset.dy,10);
handleDir(dx,dy);
});
});
// swipe
let touchX=0,touchY=0;
canvas.addEventListener('touchstart',(e)=>{ const t=e.touches[0]; touchX=t.clientX; touchY=t.clientY; },{passive:true});
canvas.addEventListener('touchend',(e)=>{
const t=e.changedTouches[0]; const dx=t.clientX-touchX, dy=t.clientY-touchY;
if(Math.abs(dx)<18 && Math.abs(dy)<18) return;
if(Math.abs(dx)>Math.abs(dy)) handleDir(dx>0?1:-1,0); else handleDir(0,dy>0?1:-1);
},{passive:true});
canvas.addEventListener('click',()=>{ if(!userStarted) startFromDemo(); });
resetGame(true);
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (10,979 tokens)
Mapping snake game to train theme with grid layout, viewport scaling, demo mode, and platform passenger spawning.Refining the platform layout by placing stations as special tiles or border strips within the grid.Expanding the grid design with train rendering, UI layout, and passenger pickup mechanics.Adding interpolation for smooth train movement, AI pathfinding, demo mode overlay, and control bindings.Extending rendering with track tiles, oriented locomotive drawing, passenger platforms, and UI panel elements.Adding game state variables, BFS pathfinding, interpolation, input handling, and audio for demo mode.Extending rendering with overlay UI, responsive CSS layout, and detailed background and passenger drawing logic.Adding locomotive and wagon rendering with colors, wheels, and passenger name tags.Adding DOM updates for stats and controls, plus keyboard handling and canvas state initialization.Adding smoke, particles, and shake effects plus speed toggle, whistle action, and HUD updates.