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
- 2 min 10 s
- Cost
- $0.012
- Output tokens
- 23,243
- Spent thinking
- 12,299 tokens
- Speed
- 148440 tok/s
- Console errors
- 0
- First token after
- 2 min 10 s
- First visible text after
- 2 min 10 s
- Prompt tokens
- 380
- Code size
- 31.9 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 674 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Little Train Snake — Sunny Station</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
font-family: ui-rounded, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(180deg,#6ec3ff 0%, #aee3ff 32%, #e3f7ff 62%, #fff2a8 100%);
min-height:100vh; color:#1c2b3a; overflow-x:hidden;
}
.cloud{position:fixed; background:#fff; border-radius:999px; opacity:.75; z-index:0; pointer-events:none; box-shadow:0 8px 20px rgba(255,255,255,.6)}
.cloud::before,.cloud::after{content:""; position:absolute; background:#fff; border-radius:50%}
.cloud::before{width:52px;height:52px;top:-22px;left:18px}
.cloud::after{width:38px;height:38px;top:-14px;right:16px}
.c1{width:130px;height:38px;top:34px;left:-40px;animation:drift 55s linear infinite}
.c2{width:100px;height:30px;top:120px;left:-30px;animation:drift 75s linear infinite;animation-delay:-25s;opacity:.55}
@keyframes drift{from{transform:translateX(0)} to{transform:translateX(calc(100vw + 180px))}}
.app{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:14px 14px 22px}
header{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.logo{display:flex;align-items:center;gap:12px;background:rgba(255,255,255,.96);padding:10px 16px 10px 12px;border-radius:20px;box-shadow:0 10px 24px rgba(30,60,90,.18);border:3px solid #263238}
.logo-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(145deg,#e63946,#ff7b54);display:grid;place-items:center;font-size:30px;box-shadow:inset 0 -6px 0 rgba(0,0,0,.18)}
.logo h1{margin:0;font-size:26px;line-height:1;letter-spacing:.2px}
.logo p{margin:4px 0 0;font-size:13px;font-weight:700;color:#5a6d80}
.subtitle{margin-top:10px;background:rgba(255,255,255,.85);display:inline-block;padding:6px 12px;border-radius:999px;font-weight:800;font-size:13px;box-shadow:0 6px 16px rgba(0,0,0,.08)}
.stats{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.pill{background:#fff;border-radius:16px;padding:8px 12px;min-width:92px;text-align:center;box-shadow:0 8px 18px rgba(0,0,0,.10);border:2px solid #e8f1f8}
.pill span{display:block;font-size:10px;font-weight:900;letter-spacing:.12em;color:#7a8ea3;text-transform:uppercase}
.pill b{font-size:20px;line-height:1.1}
main{display:flex;gap:16px;align-items:flex-start;margin-top:14px}
.board-wrap{position:relative;flex:1 1 auto;max-width:812px;margin:0 auto;background:#fff;padding:10px;border-radius:24px;box-shadow:0 20px 42px rgba(20,40,60,.22);border:4px solid #263238}
canvas{display:block;width:100%;height:auto;border-radius:14px;background:#84c36d;touch-action:none;cursor:pointer}
.demo-banner{position:absolute;top:20px;left:50%;transform:translateX(-50%);background:linear-gradient(135deg,#ffe066,#ffb703);color:#402d00;font-weight:900;font-size:14px;padding:10px 16px;border-radius:999px;box-shadow:0 10px 22px rgba(0,0,0,.22);border:3px solid #fff;white-space:nowrap;z-index:5;animation:pulse 1.6s ease-in-out infinite;max-width:92%;overflow:hidden;text-overflow:ellipsis}
@keyframes pulse{0%,100%{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.04)}}
.toast{position:absolute;top:70px;left:50%;transform:translateX(-50%) translateY(-8px);background:#263238;color:#fff;font-weight:900;padding:9px 14px;border-radius:999px;opacity:0;pointer-events:none;transition:.25s;z-index:5;font-size:14px;box-shadow:0 10px 20px rgba(0,0,0,.25)}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.gameover{position:absolute;inset:10px;border-radius:14px;background:rgba(18,28,38,.48);display:flex;align-items:center;justify-content:center;z-index:6;padding:14px}
.gameover-card{background:#fff;border-radius:22px;padding:20px;text-align:center;max-width:340px;width:100%;box-shadow:0 18px 36px rgba(0,0,0,.3);border:4px solid #263238}
.gameover-card h2{margin:0 0 8px;font-size:26px}
.gameover-card p{margin:6px 0;font-weight:800;color:#546a7d}
.gameover-card .big{font-size:42px}
.hidden{display:none !important}
aside{width:302px;flex:0 0 302px;display:flex;flex-direction:column;gap:12px}
.card{background:rgba(255,255,255,.96);border-radius:20px;padding:14px;box-shadow:0 12px 26px rgba(20,40,60,.12);border:2px solid #eaf2f8}
.card h3{margin:0 0 10px;font-size:16px}
.card p,.card li{font-size:13.5px;line-height:1.45;color:#42576b;font-weight:650}
.card ul{margin:6px 0;padding-left:18px}
.btn{appearance:none;border:none;cursor:pointer;font-weight:900;font-family:inherit;border-radius:999px;padding:11px 14px;font-size:14px;box-shadow:0 6px 0 rgba(0,0,0,.16);transition:transform .08s, box-shadow .08s}
.btn:active{transform:translateY(3px);box-shadow:0 2px 0 rgba(0,0,0,.16)}
.btn-primary{background:linear-gradient(135deg,#ffd93d,#ff9f2e);color:#3a2400}
.btn-blue{background:linear-gradient(135deg,#4d96ff,#00c2a8);color:#fff}
.btn-dark{background:#263238;color:#fff}
.btn-ghost{background:#eef4f9;color:#263238;box-shadow:none;border:2px solid #d7e5ef}
.btn-row{display:flex;gap:8px;flex-wrap:wrap}
.legend{display:flex;flex-direction:column;gap:8px}
.legend-item{display:flex;align-items:center;gap:10px;font-weight:800;font-size:13.5px;color:#42576b}
.dot{width:26px;height:20px;border-radius:7px;border:2px solid rgba(0,0,0,.18);flex:0 0 auto}
.controls-row{display:flex;justify-content:center;gap:8px;margin-top:8px}
.dpad-btn{width:56px;height:56px;border-radius:16px;border:none;background:linear-gradient(145deg,#fff,#eaf2f8);font-size:22px;font-weight:900;box-shadow:0 6px 0 #c7d7e3;cursor:pointer;color:#263238}
.dpad-btn:active{transform:translateY(3px);box-shadow:0 2px 0 #c7d7e3}
.hint{margin-top:10px;text-align:center;font-size:13px;font-weight:800;color:#4a6076;background:rgba(255,255,255,.8);padding:7px 10px;border-radius:999px}
footer{margin-top:14px;text-align:center;font-size:12.5px;font-weight:800;color:#49627a}
@media (max-width:980px){
main{flex-direction:column}
aside{width:100%;flex:none;flex-direction:row;flex-wrap:wrap}
aside .card{flex:1 1 260px}
}
@media (max-width:560px){
.app{padding:10px}
.logo h1{font-size:21px}
.logo p{font-size:11.5px}
.logo-icon{width:44px;height:44px;font-size:24px}
.pill{min-width:78px;padding:6px 8px}
.pill b{font-size:17px}
.demo-banner{font-size:12px;padding:8px 11px;top:16px}
.board-wrap{padding:6px;border-radius:18px}
.dpad-btn{width:52px;height:52px}
}
</style>
</head>
<body>
<div class="cloud c1"></div><div class="cloud c2"></div>
<div class="app">
<header>
<div>
<div class="logo">
<div class="logo-icon">🚂</div>
<div>
<h1>Little Train Snake</h1>
<p>Sunny Station • Pick up passengers, grow your train!</p>
</div>
</div>
<div class="subtitle">🚉 Drive the locomotive to the platform passenger — every passenger adds a colorful wagon!</div>
</div>
<div class="stats" aria-label="Game stats">
<div class="pill"><span>Score</span><b id="scoreVal">0</b></div>
<div class="pill"><span>Passengers</span><b id="pickedVal">0</b></div>
<div class="pill"><span>Train</span><b id="lengthVal">4</b></div>
<div class="pill"><span>Speed</span><b id="speedVal">Lv 1</b></div>
<div class="pill"><span>Best</span><b id="bestVal">0</b></div>
</div>
</header>
<main>
<section>
<div class="board-wrap" id="boardWrap">
<canvas id="game" width="760" height="570" aria-label="Little train snake game board"></canvas>
<div class="demo-banner" id="demoBanner">🤖 DEMO — Click or press a key to drive!</div>
<div class="toast" id="toast">You're driving!</div>
<div class="gameover hidden" id="gameOver">
<div class="gameover-card">
<div class="big">💥</div>
<h2>Train derailed!</h2>
<p id="finalStats">Score 0 • Passengers 0</p>
<p style="font-size:13px">The little train bumped into a wall or its own wagons.</p>
<div class="btn-row" style="justify-content:center;margin-top:12px">
<button class="btn btn-primary" id="playAgainBtn">▶ Play again</button>
<button class="btn btn-ghost" id="watchDemoBtn">🤖 Watch demo</button>
</div>
</div>
</div>
</div>
<div class="hint">⬆️⬇️⬅️➡️ Arrow keys • W A S D • Swipe on board • Space = Pause • R = Restart</div>
</section>
<aside>
<div class="card">
<h3>🎮 Drive the train</h3>
<p>Take control any time — the demo train is already running!</p>
<div class="btn-row">
<button class="btn btn-primary" id="pauseBtn">⏸ Pause</button>
<button class="btn btn-blue" id="restartBtn">↻ Restart</button>
<button class="btn btn-dark" id="demoBtn">🤖 Demo</button>
</div>
<div class="controls-row" aria-label="On screen controls">
<button class="dpad-btn" data-dx="-1" data-dy="0" aria-label="Left">◀</button>
<button class="dpad-btn" data-dx="0" data-dy="-1" aria-label="Up">▲</button>
<button class="dpad-btn" data-dx="0" data-dy="1" aria-label="Down">▼</button>
<button class="dpad-btn" data-dx="1" data-dy="0" aria-label="Right">▶</button>
</div>
</div>
<div class="card">
<h3>🚉 How to play</h3>
<ul>
<li>Follow the pulsing <b>platform</b> where a passenger is waiting.</li>
<li>Roll up to the platform to pick them up — <b>+10 points</b>.</li>
<li>Every pickup adds a <b>colorful wagon</b> carrying a passenger.</li>
<li>Every 4th passenger is <b>golden ⭐ +30 points</b>!</li>
<li>Don't hit walls or your own train. Speed grows as you collect.</li>
</ul>
</div>
<div class="card">
<h3>🎨 Meet the train</h3>
<div class="legend">
<div class="legend-item"><span class="dot" style="background:#e63946"></span> Red locomotive — that's you!</div>
<div class="legend-item"><span class="dot" style="background:linear-gradient(90deg,#ff5a5a,#ffd93d,#4d96ff,#9b5de5)"></span> Colorful wagons — one per passenger</div>
<div class="legend-item"><span class="dot" style="background:#d9a05b"></span> Station platform — passenger waits here</div>
<div class="legend-item"><span class="dot" style="background:#ffd93d"></span> Golden passenger — bonus points!</div>
</div>
</div>
</aside>
</main>
<footer>Made with ❤️ • A snake game where you are a little train • Demo plays itself until you click or press a key</footer>
</div>
<script>
(() => {
const COLS = 20, ROWS = 15, CELL = 38, W = COLS*CELL, H = ROWS*CELL;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const boardWrap = document.getElementById('boardWrap');
const demoBanner = document.getElementById('demoBanner');
const toast = document.getElementById('toast');
const gameOverEl = document.getElementById('gameOver');
const scoreVal = document.getElementById('scoreVal');
const pickedVal = document.getElementById('pickedVal');
const lengthVal = document.getElementById('lengthVal');
const speedVal = document.getElementById('speedVal');
const bestVal = document.getElementById('bestVal');
const pauseBtn = document.getElementById('pauseBtn');
const restartBtn = document.getElementById('restartBtn');
const demoBtn = document.getElementById('demoBtn');
const playAgainBtn = document.getElementById('playAgainBtn');
const watchDemoBtn = document.getElementById('watchDemoBtn');
const finalStats = document.getElementById('finalStats');
const wagonColors = ['#ff5a5a','#ff9f2e','#ffd93d','#6bcb77','#4d96ff','#9b5de5','#ff6fb5','#00c2a8','#ff7b54','#3ec6e0'];
const shirtColors = ['#ff6fb5','#4d96ff','#ffd93d','#6bcb77','#9b5de5','#ff9f2e','#00c2a8','#e63946'];
const hairColors = ['#5a3a22','#222222','#a0522d','#e0ac69','#7b4b2a','#3d2b1f'];
let snake = [], prevSnake = [], dir = {x:1,y:0}, dirQueue = [];
let passenger = {x:14,y:6}, golden = false;
let score = 0, picked = 0, best = 0;
let stepMs = 155, demo = true, gameOver = false, paused = false, pausedT = 0;
let lastStep = performance.now(), lastSmoke = 0;
let particles = [], floaters = [], toastTimer = null, audioCtx = null;
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const lerp = (a,b,t)=>a+(b-a)*t;
const key = (x,y)=>x+','+y;
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 hash(x,y){ let h=(x*374761393 + y*668265263) ^ (x*y*1274126177); h=Math.abs(h); return h%100; }
function updateHUD(){
scoreVal.textContent = score;
pickedVal.textContent = picked;
lengthVal.textContent = snake.length;
speedVal.textContent = 'Lv ' + (1+Math.floor(picked/4));
bestVal.textContent = best;
}
function showToast(msg){
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(()=>toast.classList.remove('show'), 2200);
}
function tone(freq, dur=0.12, type='sine', vol=0.07){
if(demo) return;
try{
audioCtx = audioCtx || new (window.AudioContext||window.webkitAudioContext)();
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.frequency.value = freq; o.type = type; g.gain.value = vol;
o.connect(g); g.connect(audioCtx.destination);
o.start();
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
o.stop(audioCtx.currentTime + dur);
}catch(e){}
}
function spawnPassenger(first=false){
if(first){ passenger = {x:14,y:6}; golden=false; return; }
golden = (picked>0 && picked%4===0);
for(let tries=0; tries<600; tries++){
const x = 1 + Math.floor(Math.random()*(COLS-2));
const y = 1 + Math.floor(Math.random()*(ROWS-2));
if(snake.some(s=>s.x===x && s.y===y)) continue;
if(Math.abs(x-snake[0].x)+Math.abs(y-snake[0].y) < 4) continue;
passenger = {x,y};
return;
}
passenger = {x:COLS-2,y:2};
}
function resetGame(){
snake = [{x:5,y:7},{x:4,y:7},{x:3,y:7},{x:2,y:7}];
prevSnake = snake.map(s=>({...s}));
dir = {x:1,y:0}; dirQueue = [];
score = 0; picked = 0; stepMs = 155;
gameOver = false; paused = false; pausedT = 0;
particles = []; floaters = [];
pauseBtn.textContent = '⏸ Pause';
gameOverEl.classList.add('hidden');
demoBanner.classList.toggle('hidden', !demo);
spawnPassenger(true);
lastStep = performance.now();
updateHUD();
}
function takeControl(){
if(demo){
demo = false;
demoBanner.classList.add('hidden');
showToast("🚂 You're driving! Arrows / WASD / swipe");
}
}
function queueDir(d){
const last = dirQueue.length ? dirQueue[dirQueue.length-1] : dir;
if(d.x === -last.x && d.y === -last.y) return;
if(d.x === last.x && d.y === last.y) return;
if(dirQueue.length < 3) dirQueue.push(d);
}
function floodCount(sx,sy,blocked){
const seen = new Set([key(sx,sy)]);
const q = [{x:sx,y:sy}];
while(q.length){
const c = q.shift();
for(const d of [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]){
const nx=c.x+d.x, ny=c.y+d.y, k=key(nx,ny);
if(nx<0||nx>=COLS||ny<0||ny>=ROWS||seen.has(k)||blocked.has(k)) continue;
seen.add(k); q.push({x:nx,y:ny});
}
}
return seen.size;
}
function aiChoose(){
const head = snake[0];
const blocked = new Set(snake.slice(0,-1).map(s=>key(s.x,s.y)));
const visited = new Map();
visited.set(key(head.x,head.y), null);
const q = [head];
let found = false;
while(q.length){
const cur = q.shift();
if(cur.x===passenger.x && cur.y===passenger.y){ found=true; break; }
for(const d of [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]){
const nx=cur.x+d.x, ny=cur.y+d.y, k=key(nx,ny);
if(nx<0||nx>=COLS||ny<0||ny>=ROWS||visited.has(k)||blocked.has(k)) continue;
visited.set(k,cur); q.push({x:nx,y:ny});
}
}
if(found){
let cur = passenger; const path=[];
while(cur && !(cur.x===head.x && cur.y===head.y)){
path.push(cur); cur = visited.get(key(cur.x,cur.y));
}
path.reverse();
if(path.length){
const nxt = path[0];
dir = {x:nxt.x-head.x, y:nxt.y-head.y};
return;
}
}
let bestDir = dir, bestScore = -1;
for(const d of [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]){
if(d.x===-dir.x && d.y===-dir.y) continue;
const nx=head.x+d.x, ny=head.y+d.y;
if(nx<0||nx>=COLS||ny<0||ny>=ROWS||blocked.has(key(nx,ny))) continue;
const cnt = floodCount(nx,ny,blocked);
if(cnt>bestScore){ bestScore=cnt; bestDir=d; }
}
if(bestScore>=0) dir = bestDir;
}
function burst(cell){
const cx=(cell.x+0.5)*CELL, cy=(cell.y+0.5)*CELL;
for(let i=0;i<18;i++){
const a=Math.random()*Math.PI*2, sp=1+Math.random()*3.2;
particles.push({x:cx,y:cy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-1,life:28+Math.random()*22,max:50,r:2+Math.random()*3.4,color: golden ? '#ffd93d' : wagonColors[Math.floor(Math.random()*wagonColors.length)]});
}
}
function addFloater(cell,text){
floaters.push({x:(cell.x+0.5)*CELL, y:cell.y*CELL+6, text, life:52, gold:golden});
}
function step(){
prevSnake = snake.map(s=>({...s}));
while(dirQueue.length){
const nd = dirQueue.shift();
if(nd.x===-dir.x && nd.y===-dir.y) continue;
if(nd.x===dir.x && nd.y===dir.y) continue;
dir = nd; break;
}
const head = snake[0];
const nh = {x:head.x+dir.x, y:head.y+dir.y};
const willGrow = (nh.x===passenger.x && nh.y===passenger.y);
const body = willGrow ? snake : snake.slice(0,-1);
if(nh.x<0||nh.x>=COLS||nh.y<0||nh.y>=ROWS||body.some(s=>s.x===nh.x&&s.y===nh.y)){
doGameOver(); return;
}
snake.unshift(nh);
if(willGrow){
picked++;
const pts = golden ? 30 : 10;
score += pts;
if(score>best) best=score;
burst(nh); addFloater(nh, '+'+pts+(golden?' ⭐':''));
if(golden) showToast('⭐ Golden passenger! +30');
spawnPassenger(false);
stepMs = Math.max(78, 155 - picked*5);
tone(golden?880:660, .14, 'triangle', .08);
setTimeout(()=>tone(golden?1174:880,.12,'triangle',.06), 70);
updateHUD();
}else{
snake.pop();
}
}
function doGameOver(){
gameOver = true;
if(score>best) best=score;
updateHUD();
tone(160,.35,'sawtooth',.07);
if(demo){
setTimeout(()=>{ if(demo) resetGame(); }, 950);
}else{
finalStats.textContent = 'Score '+score+' • Passengers '+picked+' • Train length '+snake.length;
gameOverEl.classList.remove('hidden');
}
}
function togglePause(){
if(gameOver || demo) return;
paused = !paused;
if(paused){ pausedT = clamp((performance.now()-lastStep)/stepMs,0,1); pauseBtn.textContent='▶ Resume'; showToast('⏸ Paused'); }
else{ pauseBtn.textContent='⏸ Pause'; lastStep = performance.now() - pausedT*stepMs; showToast('▶ Go!'); }
}
function drawBoard(now){
const g = ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#a7d98a'); g.addColorStop(1,'#7fbf6a');
ctx.fillStyle = g; ctx.fillRect(0,0,W,H);
for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
if((x+y)%2===0){ ctx.fillStyle='rgba(255,255,255,0.055)'; ctx.fillRect(x*CELL,y*CELL,CELL,CELL); }
}
ctx.strokeStyle='rgba(0,0,0,0.07)'; ctx.lineWidth=1;
for(let x=0;x<=COLS;x++){ ctx.beginPath(); ctx.moveTo(x*CELL+.5,0); ctx.lineTo(x*CELL+.5,H); ctx.stroke(); }
for(let y=0;y<=ROWS;y++){ ctx.beginPath(); ctx.moveTo(0,y*CELL+.5); ctx.lineTo(W,y*CELL+.5); ctx.stroke(); }
// faint tracks
ctx.strokeStyle='rgba(60,40,20,0.08)'; ctx.lineWidth=5;
for(let y=0;y<ROWS;y++){ ctx.beginPath(); ctx.moveTo(0,(y+.5)*CELL); ctx.lineTo(W,(y+.5)*CELL); ctx.stroke(); }
// grass tufts & flowers (deterministic)
for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
const h=hash(x,y);
if(h<9){
const gx=x*CELL+8+h, gy=y*CELL+10+(h*3)%18;
ctx.strokeStyle='rgba(30,90,30,0.35)'; ctx.lineWidth=1.4;
ctx.beginPath(); ctx.moveTo(gx,gy); ctx.lineTo(gx-3,gy-6); ctx.moveTo(gx+3,gy); ctx.lineTo(gx+5,gy-6); ctx.stroke();
}else if(h>93){
const gx=x*CELL+12+(h*2)%14, gy=y*CELL+12+(h*5)%14;
ctx.fillStyle= h%2 ? '#ff8fab':'#fff3b0';
ctx.beginPath(); ctx.arc(gx,gy,2.6,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#ff9f2e'; ctx.beginPath(); ctx.arc(gx,gy,1.1,0,Math.PI*2); ctx.fill();
}
}
// wall
ctx.strokeStyle='#5d4037'; ctx.lineWidth=9; ctx.strokeRect(4,4,W-8,H-8);
ctx.strokeStyle='#a1887f'; ctx.lineWidth=2; ctx.strokeRect(9,9,W-18,H-18);
}
function drawPlatformAndPassenger(now){
const p = passenger, x=p.x*CELL, y=p.y*CELL;
const pulse = 0.5 + Math.sin(now/260)*0.5;
ctx.save();
ctx.strokeStyle = golden ? `rgba(255,200,0,${0.55+0.35*pulse})` : `rgba(255,220,90,${0.45+0.3*pulse})`;
ctx.lineWidth = 4;
ctx.beginPath(); ctx.ellipse(x+CELL/2,y+CELL/2,CELL*0.48,CELL*0.44,0,0,Math.PI*2); ctx.stroke();
ctx.restore();
const py = y + CELL*0.38, ph = CELL*0.56;
ctx.fillStyle='rgba(0,0,0,0.18)';
rr(x+3,py+5,CELL-6,ph,5); ctx.fill();
ctx.fillStyle='#d9a05b'; rr(x+2,py,CELL-4,ph,5); ctx.fill();
ctx.strokeStyle='#8d5a2b'; ctx.lineWidth=1.4;
for(let i=1;i<4;i++){ ctx.beginPath(); ctx.moveTo(x+2+i*((CELL-4)/4),py+3); ctx.lineTo(x+2+i*((CELL-4)/4),py+ph-3); ctx.stroke(); }
ctx.fillStyle='#ffe066'; ctx.fillRect(x+2,py+3,CELL-4,4);
ctx.strokeStyle='rgba(0,0,0,0.25)'; ctx.lineWidth=2; rr(x+2,py,CELL-4,ph,5); ctx.stroke();
// station sign
const sx=x+8, sy=py-2;
ctx.strokeStyle='#5d4037'; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(sx,py+4); ctx.lineTo(sx,sy-14); ctx.stroke();
ctx.fillStyle='#263238'; rr(sx-10,sy-26,20,15,4); ctx.fill();
ctx.fillStyle='#ffe066'; ctx.font='900 9px system-ui'; ctx.textAlign='center'; ctx.fillText('STN',sx,sy-15);
// passenger
const cx=x+CELL/2, feet=py+10;
const bob=Math.sin(now/280)*1.6;
const shirt=shirtColors[picked%shirtColors.length];
const hair=hairColors[picked%hairColors.length];
ctx.fillStyle='rgba(0,0,0,0.18)';
ctx.beginPath(); ctx.ellipse(cx,feet+3,11,4,0,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#37474f'; rr(cx-6,feet-7,5,8,2); ctx.fill(); rr(cx+1,feet-7,5,8,2); ctx.fill();
ctx.fillStyle= golden ? '#ffd93d' : shirt; rr(cx-7,feet-20+bob,14,14,5); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.2)'; ctx.lineWidth=1.5; rr(cx-7,feet-20+bob,14,14,5); ctx.stroke();
// waving arm
const wave=Math.sin(now/160)*0.5-0.4;
ctx.strokeStyle='#ffdbac'; ctx.lineWidth=4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(cx+6,feet-16+bob); ctx.lineTo(cx+13,feet-26+bob+Math.sin(now/150)*3); ctx.stroke();
// head
ctx.fillStyle='#ffdbac'; ctx.beginPath(); ctx.arc(cx,feet-27+bob,8,0,Math.PI*2); ctx.fill();
ctx.fillStyle=hair; ctx.beginPath(); ctx.arc(cx,feet-29+bob,8,Math.PI,Math.PI*2); ctx.fill();
if(golden){ ctx.fillStyle='#ff9f2e'; ctx.beginPath(); ctx.moveTo(cx-6,feet-36+bob); ctx.lineTo(cx,feet-42+bob); ctx.lineTo(cx+6,feet-36+bob); ctx.closePath(); ctx.fill(); }
ctx.fillStyle='#263238';
ctx.beginPath(); ctx.arc(cx-2.5,feet-26+bob,1.1,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(cx+2.5,feet-26+bob,1.1,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='#263238'; ctx.lineWidth=1.2;
ctx.beginPath(); ctx.arc(cx,feet-24+bob,3,0.2,Math.PI-0.2); ctx.stroke();
// speech bubble
const bx=cx+16, by=feet-48+bob;
ctx.fillStyle='#fff'; rr(bx-2,by-10,20,17,6); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.15)'; ctx.lineWidth=1.2; rr(bx-2,by-10,20,17,6); ctx.stroke();
ctx.fillStyle='#e63946'; ctx.font='900 11px system-ui'; ctx.textAlign='center'; ctx.fillText(golden?'★!':'Hey!',bx+8,by+3);
}
function drawWagon(cx,cy,angle,idx){
const color=wagonColors[idx%wagonColors.length];
ctx.save(); ctx.translate(cx,cy); ctx.rotate(angle);
ctx.fillStyle='rgba(0,0,0,0.18)';
ctx.beginPath(); ctx.ellipse(2,6,CELL*0.44,CELL*0.36,0,0,Math.PI*2); ctx.fill();
const w=CELL*0.94,h=CELL*0.80;
ctx.fillStyle='#263238';
ctx.beginPath(); ctx.arc(-11,-h/2-1,4,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(11,-h/2-1,4,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(-11,h/2+1,4,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(11,h/2+1,4,0,Math.PI*2); ctx.fill();
ctx.fillStyle=color; rr(-w/2,-h/2,w,h,7); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.28)'; ctx.lineWidth=2; rr(-w/2,-h/2,w,h,7); ctx.stroke();
ctx.fillStyle='rgba(255,255,255,0.24)'; rr(-w/2+4,-h/2+5,w-8,h-10,5); ctx.fill();
for(const wx of [-10,6]){
ctx.fillStyle='#cfefff'; rr(wx-6,-9,12,18,3); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.25)'; ctx.lineWidth=1.2; rr(wx-6,-9,12,18,3); ctx.stroke();
// tiny passenger face
ctx.fillStyle='#ffdbac'; ctx.beginPath(); ctx.arc(wx,0,3.6,0,Math.PI*2); ctx.fill();
ctx.fillStyle=hairColors[idx%hairColors.length]; ctx.beginPath(); ctx.arc(wx,-1,3.6,Math.PI,Math.PI*2); ctx.fill();
ctx.fillStyle='#263238'; ctx.fillRect(wx-1.4,-0.4,1,1); ctx.fillRect(wx+0.6,-0.4,1,1);
}
ctx.fillStyle='rgba(255,255,255,0.55)'; rr(-w/2+5,-2,w-10,4,2); ctx.fill();
ctx.restore();
}
function drawLoco(cx,cy,angle,now){
ctx.save(); ctx.translate(cx,cy); ctx.rotate(angle);
// headlight glow
const glow=ctx.createRadialGradient(20,0,2,20,0,22);
glow.addColorStop(0,'rgba(255,230,120,0.55)'); glow.addColorStop(1,'rgba(255,230,120,0)');
ctx.fillStyle=glow; ctx.beginPath(); ctx.arc(20,0,22,0,Math.PI*2); ctx.fill();
ctx.fillStyle='rgba(0,0,0,0.20)';
ctx.beginPath(); ctx.ellipse(2,7,CELL*0.5,CELL*0.4,0,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#263238';
ctx.beginPath(); ctx.arc(-12,-15,4.5,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(8,-15,4.5,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(-12,15,4.5,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(8,15,4.5,0,Math.PI*2); ctx.fill();
// chassis
ctx.fillStyle='#333a4d'; rr(-19,-14,38,28,8); ctx.fill();
ctx.strokeStyle='#141a24'; ctx.lineWidth=2; rr(-19,-14,38,28,8); ctx.stroke();
// boiler
ctx.fillStyle='#4a5568'; rr(-3,-12,21,24,6); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.14)'; rr(-1,-9,17,6,3); ctx.fill();
// chimney
ctx.fillStyle='#141a24'; ctx.beginPath(); ctx.arc(10,0,7,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#6c7a89'; ctx.beginPath(); ctx.arc(10,0,3.2,0,Math.PI*2); ctx.fill();
// cab
ctx.fillStyle='#e63946'; rr(-18,-13,15,26,5); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.25)'; rr(-18,-13,15,26,5); ctx.stroke();
ctx.fillStyle='#cfefff'; rr(-16,-8,10,16,3); ctx.fill();
ctx.fillStyle='#fff'; ctx.font='900 10px system-ui'; ctx.textAlign='center';
ctx.fillText('1',-10.5,3.5);
// stripe & light
ctx.fillStyle='#ffd93d'; rr(-3,-14,3,28,1); ctx.fill();
ctx.fillStyle='#ffe066'; ctx.beginPath(); ctx.arc(18,0,4,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='#b8860b'; ctx.lineWidth=1; ctx.beginPath(); ctx.arc(18,0,4,0,Math.PI*2); ctx.stroke();
ctx.restore();
}
function render(now){
ctx.clearRect(0,0,W,H);
drawBoard(now);
drawPlatformAndPassenger(now);
let t = paused ? pausedT : clamp((now-lastStep)/stepMs,0,1);
const interp = snake.map((curr,i)=>{
const prev = prevSnake[i] || prevSnake[prevSnake.length-1] || curr;
return {x:lerp(prev.x,curr.x,t), y:lerp(prev.y,curr.y,t), dx:curr.x-prev.x, dy:curr.y-prev.y};
});
let angles=[], lastAngle=Math.atan2(dir.y,dir.x);
for(let i=0;i<interp.length;i++){
if(interp[i].dx!==0 || interp[i].dy!==0) lastAngle=Math.atan2(interp[i].dy,interp[i].dx);
angles.push(lastAngle);
}
// connectors
ctx.strokeStyle='#263238'; ctx.lineWidth=5; ctx.lineCap='round';
for(let i=interp.length-1;i>0;i--){
const a=interp[i], b=interp[i-1];
ctx.beginPath(); ctx.moveTo((a.x+.5)*CELL,(a.y+.5)*CELL); ctx.lineTo((b.x+.5)*CELL,(b.y+.5)*CELL); ctx.stroke();
}
for(let i=interp.length-1;i>=0;i--){
const cx=(interp[i].x+.5)*CELL, cy=(interp[i].y+.5)*CELL;
if(i===0) drawLoco(cx,cy,angles[i],now);
else drawWagon(cx,cy,angles[i],i);
}
// smoke
if(!gameOver && !paused && now-lastSmoke>115 && interp.length){
lastSmoke=now;
const h0=interp[0], ang=angles[0];
const sx=(h0.x+.5)*CELL + Math.cos(ang)*10, sy=(h0.y+.5)*CELL + Math.sin(ang)*10;
particles.push({x:sx,y:sy,vx:(Math.random()-.5)*.4,vy:-.7-Math.random()*.5,life:42,max:46,r:4+Math.random()*3,color:'smoke'});
}
// particles
for(let i=particles.length-1;i>=0;i--){
const p=particles[i]; p.x+=p.vx; p.y+=p.vy; p.life--;
if(p.color==='smoke'){
ctx.fillStyle=`rgba(230,230,230,${clamp(p.life/p.max,0,1)*0.55})`;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r+(46-p.life)*0.22,0,Math.PI*2); ctx.fill();
}else{
ctx.globalAlpha=clamp(p.life/p.max,0,1);
ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
ctx.globalAlpha=1;
}
if(p.life<=0) particles.splice(i,1);
}
// floaters
for(let i=floaters.length-1;i>=0;i--){
const f=floaters[i]; f.y-=0.7; f.life--;
ctx.globalAlpha=clamp(f.life/38,0,1);
ctx.font='900 17px system-ui'; ctx.textAlign='center';
ctx.lineWidth=4; ctx.strokeStyle='#fff'; ctx.strokeText(f.text,f.x,f.y);
ctx.fillStyle=f.gold?'#e69a00':'#263238'; ctx.fillText(f.text,f.x,f.y);
ctx.globalAlpha=1;
if(f.life<=0) floaters.splice(i,1);
}
}
function frame(now){
requestAnimationFrame(frame);
if(!gameOver && !paused && now-lastStep>=stepMs){
if(demo) aiChoose();
step();
lastStep=now;
}
render(now);
}
// Events
window.addEventListener('keydown', e=>{
const k=e.key.toLowerCase();
if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
if(k===' '){ if(demo) takeControl(); else togglePause(); return; }
if(k==='p'){ togglePause(); return; }
if(k==='r'){ takeControl(); resetGame(); showToast('↻ New trip started!'); return; }
let d=null;
if(k==='arrowup'||k==='w') d={x:0,y:-1};
else if(k==='arrowdown'||k==='s') d={x:0,y:1};
else if(k==='arrowleft'||k==='a') d={x:-1,y:0};
else if(k==='arrowright'||k==='d') d={x:1,y:0};
if(d){ takeControl(); queueDir(d); }
});
boardWrap.addEventListener('pointerdown', ()=>takeControl());
canvas.addEventListener('pointerdown', ()=>takeControl());
let touchX=null, touchY=null;
canvas.addEventListener('touchstart', e=>{
takeControl();
const t=e.touches[0]; touchX=t.clientX; touchY=t.clientY;
}, {passive:true});
canvas.addEventListener('touchend', e=>{
if(touchX===null) return;
const t=e.changedTouches[0];
const dx=t.clientX-touchX, dy=t.clientY-touchY;
if(Math.abs(dx)<22 && Math.abs(dy)<22) return;
if(Math.abs(dx)>Math.abs(dy)) queueDir(dx>0?{x:1,y:0}:{x:-1,y:0});
else queueDir(dy>0?{x:0,y:1}:{x:0,y:-1});
touchX=touchY=null;
}, {passive:true});
document.querySelectorAll('.dpad-btn').forEach(btn=>{
btn.addEventListener('pointerdown', e=>{
e.preventDefault();
takeControl();
queueDir({x:parseInt(btn.dataset.dx,10), y:parseInt(btn.dataset.dy,10)});
});
});
pauseBtn.addEventListener('click', ()=>{ takeControl(); togglePause(); });
restartBtn.addEventListener('click', ()=>{ takeControl(); resetGame(); showToast('↻ New trip started!'); });
demoBtn.addEventListener('click', ()=>{ demo=true; resetGame(); showToast('🤖 Demo mode — watching the train'); });
playAgainBtn.addEventListener('click', ()=>{ demo=false; demoBanner.classList.add('hidden'); resetGame(); });
watchDemoBtn.addEventListener('click', ()=>{ demo=true; resetGame(); });
resetGame();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>