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
- 37.0 s
- Cost
- $0.0077
- Output tokens
- 15,235
- Spent thinking
- 5,428 tokens
- Speed
- 436.7 tok/s
- Console errors
- 0
- First token after
- 2.1 s
- First visible text after
- 20.0 s
- Prompt tokens
- 380
- Code size
- 27.8 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
{"effort":"xhigh"}- 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 · 589 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>Passenger Express — Little Train Snake</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;min-height:100%}
body{
font-family:ui-rounded,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
background:linear-gradient(180deg,#7ed6ff 0%,#c8f0ff 22%,#fff7d6 78%,#ffe9a8 100%);
color:#1e2b3a;overflow-x:hidden;
}
.wrap{max-width:1160px;margin:0 auto;padding:14px 14px 22px}
header{
display:flex;align-items:center;gap:14px;background:#fff;border-radius:22px;
padding:12px 16px;box-shadow:0 10px 28px rgba(30,80,120,.18);border:3px solid #fff3b0;
}
.logo{
width:62px;height:62px;flex:0 0 62px;border-radius:18px;display:grid;place-items:center;font-size:38px;
background:linear-gradient(145deg,#ff5a5f,#e63946);box-shadow:inset 0 -6px 0 rgba(0,0,0,.18),0 6px 14px rgba(230,57,70,.35);
transform:rotate(-3deg)
}
h1{font-size:28px;line-height:1;letter-spacing:.3px;color:#173a5e}
h1 span{color:#e63946}
header p{margin-top:6px;font-size:14px;color:#5b7186;font-weight:600}
.mode{
margin-left:auto;background:#173a5e;color:#fff;font-weight:800;font-size:12px;letter-spacing:.8px;
padding:9px 12px;border-radius:999px;white-space:nowrap;box-shadow:0 6px 14px rgba(23,58,94,.3)
}
.mode.player{background:#06a377}
main{display:flex;gap:16px;margin-top:16px;align-items:flex-start}
.board-card{
flex:1 1 auto;min-width:0;background:#fff;border-radius:24px;padding:10px;
box-shadow:0 16px 36px rgba(30,80,120,.22);border:3px solid #ffffff
}
.canvas-holder{position:relative;border-radius:18px;overflow:hidden;background:#7ec850;line-height:0}
canvas{width:100%;height:auto;display:block;aspect-ratio:4/3;touch-action:none;cursor:pointer}
#overlay{
position:absolute;inset:0;line-height:1.3;display:flex;align-items:center;justify-content:center;
background:linear-gradient(180deg,rgba(10,30,55,.32),rgba(10,30,55,.58));backdrop-filter:blur(1.5px);
padding:16px;transition:opacity .25s ease;z-index:5
}
#overlay.hidden{opacity:0;pointer-events:none}
.overlay-card{
background:#fff;border-radius:22px;padding:18px 20px;text-align:center;max-width:430px;width:100%;
box-shadow:0 18px 40px rgba(0,0,0,.35);border:4px solid #ffd166
}
.overlay-card .train-emoji{font-size:46px;line-height:1}
.overlay-card h2{font-size:26px;color:#173a5e;margin:6px 0 6px}
.overlay-card p{font-size:14px;color:#5b7186;font-weight:600}
.keys{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin:12px 0}
.keys span{background:#eef6ff;border:2px solid #cfe8ff;color:#173a5e;font-weight:800;font-size:12px;padding:6px 9px;border-radius:999px}
#playBtn{
border:0;cursor:pointer;background:linear-gradient(180deg,#ff6b6b,#e63946);color:#fff;font-weight:900;letter-spacing:.4px;
font-size:17px;padding:13px 24px;border-radius:999px;box-shadow:0 8px 0 #b81f2d,0 14px 22px rgba(230,57,70,.35);width:100%
}
#playBtn:active{transform:translateY(4px);box-shadow:0 4px 0 #b81f2d}
.controls-bar{display:flex;gap:8px;padding-top:10px}
.controls-bar button{
flex:1;border:0;cursor:pointer;font-weight:900;font-size:13px;padding:11px 8px;border-radius:14px;color:#173a5e;background:#eef6ff;border:2px solid #d7ecff
}
.controls-bar button:active{transform:scale(.97)}
aside{flex:0 0 300px;width:300px;display:flex;flex-direction:column;gap:12px}
.card{background:#fff;border-radius:20px;padding:14px;box-shadow:0 10px 24px rgba(30,80,120,.16);border:3px solid rgba(255,255,255,.9)}
.card h3{font-size:13px;letter-spacing:1px;text-transform:uppercase;color:#7a93aa;margin-bottom:10px}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.stat{background:linear-gradient(180deg,#f4fbff,#e8f4ff);border-radius:16px;padding:11px;border:2px solid #d7ecff;text-align:center}
.stat .label{font-size:11px;font-weight:800;letter-spacing:.6px;color:#6c87a0;text-transform:uppercase}
.stat .value{font-size:26px;font-weight:900;color:#173a5e;line-height:1.1;margin-top:2px}
.stat .sub{font-size:11px;font-weight:700;color:#8aa0b5}
.how p{font-size:13.5px;line-height:1.55;color:#435b73;font-weight:600}
.how b{color:#173a5e}
.legend{display:flex;flex-direction:column;gap:8px;font-size:13px;font-weight:700;color:#435b73}
.legend div{display:flex;align-items:center;gap:9px}
.dot{width:22px;height:22px;border-radius:7px;flex:0 0 22px;display:grid;place-items:center;font-size:13px;border:2px solid rgba(0,0,0,.08)}
.dots{display:flex;gap:3px}
.dots i{width:14px;height:14px;border-radius:4px;display:inline-block;border:2px solid rgba(0,0,0,.08)}
.dpad{display:grid;grid-template-columns:repeat(3,62px);grid-template-rows:repeat(3,52px);gap:7px;justify-content:center}
.dpad button{
border:0;cursor:pointer;border-radius:15px;background:linear-gradient(180deg,#ffffff,#eef6ff);color:#173a5e;font-size:21px;font-weight:900;
box-shadow:0 5px 0 #c8dff5,0 8px 14px rgba(30,80,120,.18);border:2px solid #d7ecff
}
.dpad button:active{transform:translateY(3px);box-shadow:0 2px 0 #c8dff5}
.dpad .empty{visibility:hidden}
.dpad .up{grid-column:2;grid-row:1}.dpad .left{grid-column:1;grid-row:2}.dpad .down{grid-column:2;grid-row:2}.dpad .right{grid-column:3;grid-row:2}
footer{margin-top:14px;text-align:center;font-size:12.5px;font-weight:700;color:#4d6a86;background:rgba(255,255,255,.72);padding:9px 12px;border-radius:999px}
@media(max-width:900px){
main{flex-direction:column}
aside{width:100%;flex:none}
.stats{grid-template-columns:repeat(4,1fr)}
h1{font-size:23px}
.mode{font-size:10px;padding:7px 9px}
.logo{width:52px;height:52px;flex-basis:52px;font-size:30px}
}
@media(max-width:520px){
.wrap{padding:10px 10px 16px}
header{padding:10px 12px;gap:10px}
header p{font-size:12px}
.stats{grid-template-columns:1fr 1fr}
.overlay-card{padding:14px}
.overlay-card h2{font-size:22px}
.controls-bar button{font-size:12px}
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="logo">🚂</div>
<div>
<h1>Passenger <span>Express</span></h1>
<p>Drive the little train! Pick up passengers waiting on the platform.</p>
</div>
<div class="mode" id="modeBadge">● DEMO MODE</div>
</header>
<main>
<section class="board-card">
<div class="canvas-holder" id="holder">
<canvas id="game" width="1000" height="750"></canvas>
<div id="overlay">
<div class="overlay-card">
<div class="train-emoji">🚃💨</div>
<h2 id="overlayTitle">All Aboard!</h2>
<p id="overlayText">The little train is already running in demo mode. Click or press a key to take the controls and pick up passengers!</p>
<div class="keys"><span>⬆️ ⬇️ ⬅️ ➡️</span><span>W A S D</span><span>Swipe</span><span>🎫 +10 pts</span></div>
<button id="playBtn">▶ Start Driving</button>
</div>
</div>
</div>
<div class="controls-bar">
<button id="pauseBtn">⏸ Pause</button>
<button id="restartBtn">↻ Restart</button>
<button id="soundBtn">🔊 Sound: On</button>
</div>
</section>
<aside>
<div class="card">
<h3>🚉 Station Board</h3>
<div class="stats">
<div class="stat"><div class="label">Score</div><div class="value" id="scoreVal">0</div><div class="sub">points</div></div>
<div class="stat"><div class="label">Passengers</div><div class="value" id="pickVal">0</div><div class="sub">on board</div></div>
<div class="stat"><div class="label">Wagons</div><div class="value" id="wagonVal">4</div><div class="sub">colorful</div></div>
<div class="stat"><div class="label">Best</div><div class="value" id="bestVal">0</div><div class="sub" id="speedVal">0 km/h</div></div>
</div>
</div>
<div class="card how">
<h3>How to play</h3>
<p>Your snake is a <b>little train</b>. The red <b>locomotive</b> leads and colorful <b>wagons</b> follow behind. Drive onto a waiting <b>passenger on the platform</b> (top or bottom) to pick them up — the train grows by one wagon! ⭐ Golden VIP passengers are worth <b>50 points</b>. Don't crash into walls or your own wagons!</p>
</div>
<div class="card">
<h3>Legend</h3>
<div class="legend">
<div><span class="dot" style="background:#e63946">🚂</span> Locomotive — you drive this!</div>
<div><span class="dot" style="background:#fff;padding:0"><span class="dots"><i style="background:#ef476f"></i><i style="background:#ffd166"></i><i style="background:#06d6a0"></i></span></span> Colorful wagons — grow as you ride</div>
<div><span class="dot" style="background:#ffe8a3">🙋</span> Passenger waiting on platform</div>
<div><span class="dot" style="background:#d8d3c3">🏫</span> Platform — pickups happen here</div>
</div>
</div>
<div class="card">
<h3>Controls</h3>
<div class="dpad">
<button class="up" data-dir="up" aria-label="Up">▲</button>
<button class="left" data-dir="left" aria-label="Left">◀</button>
<button class="down" data-dir="down" aria-label="Down">▼</button>
<button class="right" data-dir="right" aria-label="Right">▶</button>
</div>
</div>
</aside>
</main>
<footer>💡 Tip: Passengers wait on the top & bottom platforms — plan your route early, the train gets faster as it grows!</footer>
</div>
<script>
'use strict';
const COLS=20, ROWS=15, CELL=50;
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const overlay=document.getElementById('overlay');
const overlayTitle=document.getElementById('overlayTitle');
const overlayText=document.getElementById('overlayText');
const playBtn=document.getElementById('playBtn');
const modeBadge=document.getElementById('modeBadge');
const scoreVal=document.getElementById('scoreVal'), pickVal=document.getElementById('pickVal'),
wagonVal=document.getElementById('wagonVal'), bestVal=document.getElementById('bestVal'), speedVal=document.getElementById('speedVal');
const WAGON_COLORS=['#ef476f','#ffd166','#06d6a0','#118ab2','#9b5de5','#f15bb5','#ff9f1c','#4cc9f0','#80ed99','#ff8fa3'];
const SHIRTS=['#ef476f','#118ab2','#06d6a0','#ff9f1c','#9b5de5','#f15bb5','#4cc9f0','#e63946','#2a9d8f','#f4a261'];
const SKINS=['#ffdbac','#f1c27d','#e0ac69','#c68642','#8d5524','#ffe0bd'];
const HAIRS=['#2b2b2b','#6b4423','#f2c14e','#c0c0c0','#8b0000','#3a3a3a','#a0522d'];
let snake, prevSnake, dir, pendingDir, passengers, particles, score, picked, best=0, growing, tickMs, lastTick;
let demo=true, paused=false, gameOver=false, soundOn=true, nextVip=false;
let audioCtx=null, lastSmoke=0, lastFrame=performance.now();
const tufts=[]; for(let i=0;i<90;i++) tufts.push({x:Math.random()*COLS,y:2+Math.random()*(ROWS-4),r:2+Math.random()*3});
function rnd(a){return a[Math.floor(Math.random()*a.length)]}
function key(p){return p.x+','+p.y}
function lerp(a,b,t){return a+(b-a)*t}
function resetGame(){
snake=[{x:9,y:7},{x:8,y:7},{x:7,y:7},{x:6,y:7},{x:5,y:7}];
prevSnake=snake.map(s=>({...s}));
dir={x:1,y:0}; pendingDir=null;
passengers=[]; particles=[]; score=0; picked=0; growing=0; nextVip=false;
tickMs=165; gameOver=false; paused=false;
document.getElementById('pauseBtn').textContent='⏸ Pause';
for(let i=0;i<3;i++) spawnPassenger(false);
lastTick=performance.now();
updateHUD();
}
function spawnPassenger(vip){
for(let tries=0;tries<40;tries++){
const y=Math.random()<0.5?1:ROWS-2;
const x=1+Math.floor(Math.random()*(COLS-2));
const p={x,y};
if(snake.some(s=>s.x===x&&s.y===y)) continue;
if(passengers.some(o=>o.x===x&&o.y===y)) continue;
passengers.push({x,y,vip:!!vip,shirt:vip?'#ffd700':rnd(SHIRTS),skin:rnd(SKINS),hair:rnd(HAIRS),off:Math.random()*6.28});
return;
}
}
function updateHUD(){
scoreVal.textContent=score; pickVal.textContent=picked;
wagonVal.textContent=Math.max(0,snake.length-1); bestVal.textContent=best;
speedVal.textContent=Math.round(1150/tickMs)+' km/h';
}
function tone(f,d,type,vol,delay){
if(!soundOn) return;
try{
if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
if(audioCtx.state==='suspended') audioCtx.resume();
const o=audioCtx.createOscillator(), g=audioCtx.createGain(), t=audioCtx.currentTime+(delay||0);
o.type=type||'sine'; o.frequency.value=f;
g.gain.setValueAtTime(vol||0.12,t); g.gain.exponentialRampToValueAtTime(0.001,t+d);
o.connect(g); g.connect(audioCtx.destination); o.start(t); o.stop(t+d+0.02);
}catch(e){}
}
function whistle(){ tone(740,.18,'triangle',.14,0); tone(988,.28,'triangle',.14,.12); }
function pickupSound(vip){ tone(vip?990:660,.12,'sine',.14,0); tone(vip?1320:880,.18,'sine',.14,.08); }
function crashSound(){ tone(220,.25,'sawtooth',.12,0); tone(140,.4,'sawtooth',.12,.12); }
/* ---------- AI (demo) ---------- */
function aiDirection(){
if(!passengers.length) return dir;
const obstacles=new Set();
const tailExcl=(growing===0)?snake.length-1:snake.length;
for(let i=1;i<tailExcl;i++) obstacles.add(snake[i].x+','+snake[i].y);
const targets=new Set(passengers.map(p=>p.x+','+p.y));
const head=snake[0], startK=head.x+','+head.y;
const q=[head], seen=new Map([[startK,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){
const cur=q.shift(), ck=cur.x+','+cur.y;
if(targets.has(ck)&&!(cur.x===head.x&&cur.y===head.y)){found=ck;break;}
for(const d of dirs){
const nx=cur.x+d.x, ny=cur.y+d.y, nk=nx+','+ny;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS||seen.has(nk)||obstacles.has(nk)) continue;
seen.set(nk,ck); q.push({x:nx,y:ny});
}
}
if(found){
let cur=found, prev=seen.get(cur);
while(prev!==null && prev!==undefined && prev!==startK){cur=prev; prev=seen.get(cur);}
if(prev===startK){
const [cx,cy]=cur.split(',').map(Number);
const d={x:cx-head.x,y:cy-head.y};
if(!(d.x===-dir.x&&d.y===-dir.y)) return d;
}
}
// safe fallback: pick move with most open space
let bestD=dir,bestSpace=-1;
for(const d of dirs){
if(d.x===-dir.x&&d.y===-dir.y) continue;
const nx=head.x+d.x, ny=head.y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS||obstacles.has(nx+','+ny)) continue;
let space=0; const st=[[nx,ny]], vis=new Set([nx+','+ny]);
while(st.length){const [x,y]=st.pop(); space++; for(const dd of dirs){const ax=x+dd.x,ay=y+dd.y,ak=ax+','+ay; if(ax<0||ay<0||ax>=COLS||ay>=ROWS||vis.has(ak)||obstacles.has(ak))continue; vis.add(ak); st.push([ax,ay]);}}
if(space>bestSpace){bestSpace=space;bestD=d;}
}
return bestD;
}
/* ---------- Game step ---------- */
function step(){
prevSnake=snake.map(s=>({...s}));
let nd=demo?aiDirection():(pendingDir||dir);
if(nd.x===-dir.x&&nd.y===-dir.y) nd=dir;
dir=nd; pendingDir=null;
const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y};
// wall collision
if(head.x<0||head.y<0||head.x>=COLS||head.y>=ROWS){crash();return;}
// self collision
const limit=(growing===0)?snake.length-1:snake.length;
for(let i=0;i<limit;i++){ if(snake[i].x===head.x&&snake[i].y===head.y){crash();return;} }
snake.unshift(head);
const idx=passengers.findIndex(p=>p.x===head.x&&p.y===head.y);
if(idx>=0){
const p=passengers.splice(idx,1)[0];
picked++; growing++;
const pts=p.vip?50:10; score+=pts;
if(score>best) best=score;
pickupSound(p.vip);
burst((head.x+0.5)*CELL,(head.y+0.5)*CELL,p.vip?'#ffd700':'#ffffff');
// boarding sparkle toward wagons + "on board" popup
particles.push({x:(head.x+0.5)*CELL,y:(head.y+0.5)*CELL,vx:0,vy:-26,life:1,max:1,size:16,color:'#fff',type:'text',text:'+'+pts});
if(picked%5===0) nextVip=true;
spawnPassenger(nextVip); nextVip=false;
}
if(growing>0) growing--; else snake.pop();
tickMs=Math.max(85,165-picked*4);
updateHUD();
}
function burst(x,y,color){
for(let i=0;i<14;i++){
const a=Math.random()*6.28, sp=40+Math.random()*90;
particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-30,life:.7+Math.random()*.5,max:1.1,size:3+Math.random()*5,color:Math.random()<.4?'#ffd700':color,type:'star'});
}
}
function crash(){
gameOver=true; crashSound();
const h=snake[0]; burst((h.x+0.5)*CELL,(h.y+0.5)*CELL,'#ff5a5f');
if(demo){
setTimeout(()=>{ if(demo&&gameOver) resetGame(); },1600);
}else{
setTimeout(()=>{
overlayTitle.textContent='💥 Oh no! Crash!';
overlayText.textContent='Score: '+score+' • Passengers: '+picked+' • Best: '+best+'. The little train will run again — press play!';
playBtn.textContent='↻ Play Again';
overlay.classList.remove('hidden');
},650);
}
}
/* ---------- Drawing ---------- */
function draw(now,t){
// ground
const g=ctx.createLinearGradient(0,0,0,750); g.addColorStop(0,'#9be184'); g.addColorStop(1,'#6fbf4a');
ctx.fillStyle=g; ctx.fillRect(0,0,1000,750);
// checker grass subtle
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.05)';ctx.fillRect(x*CELL,y*CELL,CELL,CELL);}
}
// platform pavement (top & bottom strips)
ctx.fillStyle='#ddd6c2'; ctx.fillRect(0,0,1000,CELL*2); ctx.fillRect(0,(ROWS-2)*CELL,1000,CELL*2);
ctx.fillStyle='rgba(0,0,0,0.05)';
for(let x=0;x<COLS;x+=2){ctx.fillRect(x*CELL+6,6,CELL-12,3);ctx.fillRect(x*CELL+6,(ROWS-2)*CELL+6,CELL-12,3);}
// tracks on middle rows
ctx.strokeStyle='rgba(90,70,50,0.35)'; ctx.lineWidth=3;
for(let y=2;y<ROWS-2;y++){
const cy=(y+0.5)*CELL;
ctx.beginPath(); ctx.moveTo(0,cy-9); ctx.lineTo(1000,cy-9); ctx.moveTo(0,cy+9); ctx.lineTo(1000,cy+9); ctx.stroke();
ctx.fillStyle='rgba(120,90,55,0.30)';
for(let x=0;x<COLS;x++) ctx.fillRect(x*CELL+4,cy-3,CELL-8,6);
}
// platform edge lines
ctx.fillStyle='#ffd166'; ctx.fillRect(0,CELL*2-5,1000,5); ctx.fillRect(0,(ROWS-2)*CELL,1000,5);
ctx.fillStyle='#173a5e'; ctx.fillRect(0,CELL*2,1000,2); ctx.fillRect(0,(ROWS-2)*CELL-2,1000,2);
// platform details: benches, lamps, sign
drawPlatformProps(now);
// grass tufts
ctx.fillStyle='rgba(40,110,40,0.45)';
tufts.forEach(tf=>{ctx.beginPath();ctx.arc(tf.x*CELL+25,tf.y*CELL+25,tf.r,0,6.28);ctx.fill();});
// passengers
passengers.forEach(p=>drawPassenger(p,now));
// particles behind train for smoke handled later; draw pickup particles
drawParticles(false);
// train
drawTrain(now,t);
drawParticles(true);
// border fence
ctx.strokeStyle='#8b5a2b'; ctx.lineWidth=8; ctx.strokeRect(4,4,992,742);
ctx.strokeStyle='#fff'; ctx.lineWidth=3; ctx.strokeRect(10,10,980,730);
// corner bushes
[[40,120],[960,120],[40,630],[960,630]].forEach(b=>{ctx.fillStyle='#2f9e44';ctx.beginPath();ctx.arc(b[0],b[1],16,0,6.28);ctx.fill();ctx.fillStyle='#37b24d';ctx.beginPath();ctx.arc(b[0]-6,b[1]-6,10,0,6.28);ctx.fill();});
}
function drawPlatformProps(now){
// station sign top center
ctx.fillStyle='#173a5e'; roundRect(340,8,320,34,10); ctx.fill();
ctx.fillStyle='#ffd166'; ctx.font='900 17px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('★ SUNNY STATION ★',500,26);
ctx.fillStyle='#173a5e'; roundRect(340,(ROWS-1)*CELL+8,320,34,10); ctx.fill();
ctx.fillStyle='#fff'; ctx.fillText('SUNNY STATION',500,(ROWS-1)*CELL+25);
// benches
[110,500,890].forEach(x=>{ ctx.fillStyle='#8b5a2b'; ctx.fillRect(x,38,80,8); ctx.fillRect(x+6,46,10,14); ctx.fillRect(x+64,46,10,14);
ctx.fillRect(x,(ROWS-1)*CELL+44,80,8); ctx.fillRect(x+6,(ROWS-1)*CELL+52,10,14); ctx.fillRect(x+64,(ROWS-1)*CELL+52,10,14); });
// lamps
[60,950].forEach(x=>{ [30,(ROWS-1)*CELL+18].forEach(y=>{ ctx.fillStyle='#555'; ctx.fillRect(x-2,y,4,28); ctx.fillStyle='#ffe066'; ctx.beginPath(); ctx.arc(x,y-4,8,0,6.28); ctx.fill(); }); });
}
function drawPassenger(p,now){
const cx=(p.x+0.5)*CELL, cy=(p.y+0.5)*CELL+Math.sin(now/280+p.off)*2.5;
ctx.fillStyle='rgba(0,0,0,0.22)'; ctx.beginPath(); ctx.ellipse(cx,cy+16,13,5,0,0,6.28); ctx.fill();
if(p.vip){ // sparkles
ctx.fillStyle='#ffd700';
for(let i=0;i<3;i++){const a=now/400+i*2.1; ctx.beginPath(); ctx.arc(cx+Math.cos(a)*18,cy-14+Math.sin(a)*8,2.5,0,6.28); ctx.fill();}
}
// body
ctx.fillStyle=p.shirt; roundRect(cx-10,cy-2,20,20,7); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.35)'; roundRect(cx-10,cy-2,20,7,7); ctx.fill();
// head
ctx.fillStyle=p.skin; ctx.beginPath(); ctx.arc(cx,cy-10,10,0,6.28); ctx.fill();
ctx.fillStyle=p.hair; ctx.beginPath(); ctx.arc(cx,cy-13,10,Math.PI,0); ctx.fill();
// eyes + smile
ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(cx-3.5,cy-9,1.5,0,6.28); ctx.arc(cx+3.5,cy-9,1.5,0,6.28); ctx.fill();
ctx.strokeStyle='#222'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(cx,cy-6,4,0.15*Math.PI,0.85*Math.PI); ctx.stroke();
// waving arm
const wave=Math.sin(now/160+p.off)*0.5;
ctx.strokeStyle=p.skin; ctx.lineWidth=5; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(cx+9,cy+4); ctx.lineTo(cx+17,cy-8+wave*5); ctx.stroke();
if(p.vip){ ctx.fillStyle='#ffd700'; ctx.beginPath(); ctx.moveTo(cx-7,cy-20); ctx.lineTo(cx-3,cy-27); ctx.lineTo(cx,cy-20); ctx.lineTo(cx+3,cy-27); ctx.lineTo(cx+7,cy-20); ctx.closePath(); ctx.fill(); }
// waiting bubble
const by=cy-30+Math.sin(now/300+p.off)*2;
ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(cx+16,by,8,0,6.28); ctx.fill();
ctx.fillStyle='#e63946'; ctx.font='900 11px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('!',cx+16,by+0.5);
}
function roundRect(x,y,w,h,r){
ctx.beginPath();
if(ctx.roundRect) ctx.roundRect(x,y,w,h,r); else ctx.rect(x,y,w,h);
}
function drawTrain(now,t){
const pts=snake.map((s,i)=>{
const prev=prevSnake[Math.min(i,prevSnake.length-1)]||s;
const gx=lerp(prev.x,s.x,t), gy=lerp(prev.y,s.y,t);
return {x:(gx+0.5)*CELL,y:(gy+0.5)*CELL};
});
// connectors
ctx.strokeStyle='#333'; ctx.lineWidth=7; ctx.lineCap='round';
for(let i=1;i<pts.length;i++){ctx.beginPath();ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[i-1].x,pts[i-1].y);ctx.stroke();}
// wagons first (tail to front)
for(let i=pts.length-1;i>=1;i--){
const p=pts[i], ahead=pts[i-1];
let ang=Math.atan2(ahead.y-p.y,ahead.x-p.x); if(!isFinite(ang)) ang=0;
drawWagon(p.x,p.y,ang,WAGON_COLORS[(i-1)%WAGON_COLORS.length]);
}
// locomotive
const hp=pts[0];
let ang=Math.atan2(dir.y,dir.x);
// refine angle from movement
if(prevSnake[0]){const dx=hp.x-((prevSnake[0].x+0.5)*CELL), dy=hp.y-((prevSnake[0].y+0.5)*CELL); if(dx||dy) ang=Math.atan2(dy,dx);}
drawLoco(hp.x,hp.y,ang);
// smoke emission
if(now-lastSmoke>200&&!gameOver){
lastSmoke=now;
const sx=hp.x+Math.cos(ang)*16, sy=hp.y+Math.sin(ang)*16;
particles.push({x:sx,y:sy,vx:(Math.random()-.5)*12-6,vy:-34-Math.random()*18,life:1,max:1.3,size:7+Math.random()*6,color:'rgba(255,255,255,0.85)',type:'smoke'});
}
}
function drawWagon(x,y,ang,color){
ctx.save(); ctx.translate(x,y); ctx.rotate(ang);
ctx.fillStyle='rgba(0,0,0,0.22)'; roundRect(-22,-14,44,30,8); ctx.fill();
ctx.fillStyle=color; roundRect(-23,-17,46,34,8); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.45)'; roundRect(-23,-17,46,10,8); ctx.fill();
ctx.fillStyle='#dff4ff';
[-14,-4,6].forEach(wx=>{roundRect(wx,-9,8,10,2);ctx.fill();});
ctx.fillStyle='#222';
[-15,9].forEach(wx=>{ctx.fillRect(wx,14,10,6);ctx.fillRect(wx,-20,10,6);});
ctx.fillStyle='#fff'; ctx.font='900 11px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('★',0,9);
ctx.restore();
}
function drawLoco(x,y,ang){
ctx.save(); ctx.translate(x,y); ctx.rotate(ang);
ctx.fillStyle='rgba(0,0,0,0.25)'; roundRect(-23,-15,47,32,9); ctx.fill();
// body
ctx.fillStyle='#e63946'; roundRect(-24,-18,48,36,9); ctx.fill();
ctx.fillStyle='#c1121f'; roundRect(-24,-18,20,36,9); ctx.fill(); // cabin end
ctx.fillStyle='#ffd166'; roundRect(18,-18,6,36,3); ctx.fill(); // front plate
// boiler stripe
ctx.fillStyle='#ff8fa3'; roundRect(-6,-10,24,20,6); ctx.fill();
// chimney (front)
ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(12,0,8,0,6.28); ctx.fill();
ctx.fillStyle='#333'; ctx.beginPath(); ctx.arc(12,0,5,0,6.28); ctx.fill();
// cabin window
ctx.fillStyle='#bde0fe'; roundRect(-19,-11,12,22,3); ctx.fill();
// headlight
ctx.fillStyle='#fff9c4'; ctx.beginPath(); ctx.arc(22,0,5,0,6.28); ctx.fill();
ctx.strokeStyle='#f59f00'; ctx.lineWidth=2; ctx.stroke();
// wheels
ctx.fillStyle='#111';
[-16,2,14].forEach(wx=>{ctx.fillRect(wx,15,11,6);ctx.fillRect(wx,-21,11,6);});
// whistle
ctx.fillStyle='#ffd700'; ctx.beginPath(); ctx.arc(-2,-6,3,0,6.28); ctx.fill();
ctx.restore();
}
function drawParticles(front){
particles=particles.filter(p=>p.life>0);
particles.forEach(p=>{
if(front===(p.type==='smoke')) return;
const a=Math.max(0,p.life/p.max);
ctx.globalAlpha=a;
if(p.type==='text'){ctx.fillStyle='#fff';ctx.font='900 18px system-ui';ctx.textAlign='center';ctx.fillText(p.text,p.x,p.y);}
else if(p.type==='star'){ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.size*a+1,0,6.28);ctx.fill();}
ctx.globalAlpha=1;
});
}
function updateParticles(dt){
particles.forEach(p=>{
p.life-=dt;
p.x+=p.vx*dt; p.y+=p.vy*dt;
if(p.type==='smoke'){p.size+=10*dt; p.vy-=12*dt;}
if(p.type==='star'){p.vy+=90*dt;}
});
}
/* ---------- Loop ---------- */
function loop(now){
requestAnimationFrame(loop);
const dt=Math.min(0.05,(now-lastFrame)/1000); lastFrame=now;
let t=1;
if(!paused&&!gameOver){
if(now-lastTick>=tickMs){step(); lastTick=now;}
t=Math.min(1,(now-lastTick)/tickMs);
updateParticles(dt);
}else{
updateParticles(dt); t=1;
}
draw(now,t);
}
requestAnimationFrame(loop);
/* ---------- Interaction / controls ---------- */
function hideOverlay(){overlay.classList.add('hidden');}
function startPlayer(){
if(demo){
demo=false; modeBadge.textContent='● PLAYER MODE'; modeBadge.classList.add('player');
hideOverlay(); resetGame(); whistle();
}else if(gameOver){
hideOverlay(); resetGame(); whistle();
}else{
hideOverlay();
}
}
function setDir(name){
const map={up:{x:0,y:-1},down:{x:0,y:1},left:{x:-1,y:0},right:{x:1,y:0}};
const d=map[name]; if(!d) return;
// prevent reversing vs current direction (or queued)
const ref=pendingDir||dir;
if(!(d.x===-ref.x&&d.y===-ref.y)) pendingDir=d;
}
playBtn.addEventListener('click',e=>{e.stopPropagation();startPlayer();});
overlay.addEventListener('click',startPlayer);
canvas.addEventListener('click',()=>{ if(gameOver&&!demo) startPlayer(); else if(demo) startPlayer(); });
window.addEventListener('keydown',e=>{
const k=e.key.toLowerCase();
const map={arrowup:'up',w:'up',arrowdown:'down',s:'down',arrowleft:'left',a:'left',arrowright:'right',d:'right'};
if(map[k]){ e.preventDefault(); }
if(demo||gameOver){ startPlayer(); }
if(map[k]) setDir(map[k]);
if(k===' '||k==='enter'){ e.preventDefault(); if(paused) togglePause(); }
});
function togglePause(){
if(gameOver) return;
paused=!paused;
document.getElementById('pauseBtn').textContent=paused?'▶ Resume':'⏸ Pause';
if(paused) lastTick=performance.now();
}
document.getElementById('pauseBtn').addEventListener('click',togglePause);
document.getElementById('restartBtn').addEventListener('click',()=>{ if(demo){resetGame();} else { hideOverlay(); resetGame(); } });
document.getElementById('soundBtn').addEventListener('click',function(){ soundOn=!soundOn; this.textContent=soundOn?'🔊 Sound: On':'🔇 Sound: Off'; if(soundOn) tone(880,.1,'sine',.1,0); });
document.querySelectorAll('.dpad button').forEach(b=>{
b.addEventListener('pointerdown',e=>{e.preventDefault(); if(demo||gameOver) startPlayer(); setDir(b.dataset.dir);});
});
// swipe
let tsx=0,tsy=0;
canvas.addEventListener('touchstart',e=>{const t=e.touches[0];tsx=t.clientX;tsy=t.clientY;},{passive:true});
canvas.addEventListener('touchend',e=>{
const t=e.changedTouches[0], dx=t.clientX-tsx, dy=t.clientY-tsy;
if(Math.hypot(dx,dy)<24) return;
if(demo||gameOver) startPlayer();
setDir(Math.abs(dx)>Math.abs(dy)?(dx>0?'right':'left'):(dy>0?'down':'up'));
},{passive:true});
resetGame();
</script>
</body>
</html>Reasoning trace (5,428 tokens)
Designing a self-contained canvas snake game with a locomotive and wagons theme, adapting layout for desktop and mobile with an initial demo mode.Refining the grid layout with platform zones and passenger spawn rules for pickup mechanics.Extending the grid with train movement, BFS AI, passenger pickup effects, and responsive canvas UI.Adding top-down train rendering, smoke and boarding animations, WebAudio sounds, and mobile D-pad controls.Implementing interpolation, BFS AI, input handling, HUD updates, and responsive layout for the train game.Detailing overlay layout, BFS collision handling, and rendering for the train game.Finalizing train rendering with orientation, particles, and UI controls.