Models 44
Train snakeOne-line

Qwen3.8 Flash

by QwenReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
2 min 48 s
Cost
$0.0063
Output tokens
13,255
Spent thinking
0 tokens
Speed
81.8 tok/s
Console errors
0
First token after
5.8 s
First visible text after
5.8 s
Prompt tokens
382
Code size
33.9 KB
Finish reason
stop
Served by
Alibaba
Model id
qwen/qwen3.8-flash
Reasoning sent
{"effort":"none"}
Max output
64,000 tokens
Generated
Nov 11, 2026

What it was asked

The prompt
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 789 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Choo-Choo Express — a little train snake</title>
<style>
  :root{
    --sky-top:#79c4f2; --sky-mid:#bfe6ff; --horizon:#eafaff;
    --grass:#5aa84a; --grass-dk:#4a903f; --grass-lt:#6fb85a;
    --ink:#12324a; --cream:#fffdf6; --red:#d8412f; --yellow:#ffd23f;
    --gold:#ffcf3f; --green:#2fae5b; --blue:#2b7fd4;
    --panel:rgba(255,253,246,.86);
    --shadow:0 10px 24px rgba(18,50,74,.28);
    --font:'Trebuchet MS','Segoe UI',system-ui,-apple-system,sans-serif;
    --mono:'Courier New',ui-monospace,monospace;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    font-family:var(--font); color:var(--ink);
    background:radial-gradient(120% 90% at 50% -10%, #d8f0ff 0%, #9fd0ef 45%, #6fb6dd 100%);
    overflow:hidden; -webkit-font-smoothing:antialiased;
  }

  .app{display:flex;flex-direction:column;height:100vh;height:100dvh;width:100%;overflow:hidden}

  /* ---------- Header / ticket board ---------- */
  header.board{
    flex:0 0 auto; display:flex;align-items:center;gap:clamp(10px,2.4vw,26px);
    padding:clamp(8px,1.4vw,14px) clamp(12px,3vw,28px);
    background:linear-gradient(#12324a,#0d2740);
    border-bottom:5px solid var(--gold);
    box-shadow:0 6px 18px rgba(0,0,0,.28); position:relative; z-index:5;
  }
  .brand{display:flex;align-items:center;gap:12px;min-width:0}
  .logo{
    width:clamp(40px,6vw,54px);height:clamp(40px,6vw,54px);flex:0 0 auto;
    display:grid;place-items:center;font-size:clamp(24px,3.6vw,34px);
    background:radial-gradient(circle at 35% 30%,#ffe9a8,var(--gold));
    border-radius:14px;box-shadow:inset 0 0 0 3px #fff,0 4px 10px rgba(0,0,0,.35);
    animation:bob 2.4s ease-in-out infinite;
  }
  @keyframes bob{50%{transform:translateY(-3px) rotate(-3deg)}}
  .name h1{
    font-size:clamp(17px,2.4vw,26px);letter-spacing:.5px;line-height:1;color:#fff;
    text-transform:uppercase;text-shadow:0 2px 0 #0a1f33;white-space:nowrap;
  }
  .name p{font-size:clamp(10px,1.3vw,13px);color:#9fd0ef;letter-spacing:.6px;margin-top:3px;white-space:nowrap}

  .board-stats{margin-left:auto;display:flex;align-items:stretch;gap:clamp(6px,1.4vw,14px)}
  .stat{
    display:flex;flex-direction:column;justify-content:center;align-items:center;
    min-width:clamp(58px,8vw,86px);padding:6px 10px;border-radius:10px;
    background:linear-gradient(#1b3f5c,#12324a);border:1px solid #2b5a80;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.12);
  }
  .stat .k{font-size:9px;letter-spacing:1.6px;color:#7fb0d6;text-transform:uppercase}
  .stat .v{font-family:var(--mono);font-weight:bold;color:var(--gold);font-size:clamp(16px,2.2vw,24px);line-height:1.1}
  .stat.best .v{color:#c9f2d4}
  .stat.passengers .v{color:#ffe08a}

  .controls{display:flex;gap:8px;align-items:center}
  .btn{
    font-family:var(--font);font-weight:bold;cursor:pointer;border:none;border-radius:10px;
    padding:9px 14px;font-size:clamp(12px,1.5vw,15px);color:#3a2400;
    background:linear-gradient(#ffe08a,#ffc93f);box-shadow:0 4px 0 #c99a12,0 6px 12px rgba(0,0,0,.3);
    transition:transform .08s,box-shadow .08s;letter-spacing:.3px;white-space:nowrap;
  }
  .btn:hover{filter:brightness(1.05)}
  .btn:active{transform:translateY(3px);box-shadow:0 1px 0 #c99a12}
  .btn.small{padding:9px 12px;background:linear-gradient(#cfe6ff,#9cc8f0);box-shadow:0 4px 0 #5a8bb5,0 6px 12px rgba(0,0,0,.25);color:#0d2740}
  .btn.ghost{background:transparent;color:#9fd0ef;box-shadow:inset 0 0 0 2px #35618a}
  .btn.ghost:hover{color:#fff}

  /* ---------- Stage ---------- */
  main.stage{flex:1 1 auto;position:relative;min-height:0;overflow:hidden}
  canvas#game{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none}

  /* ---------- Overlays ---------- */
  .overlay{
    position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;
    align-items:center;justify-content:center;gap:14px;padding:20px;text-align:center;
    background:radial-gradient(120% 120% at 50% 30%, rgba(18,50,74,.15), rgba(18,50,74,.55));
    backdrop-filter:blur(2px);transition:opacity .35s;
  }
  .overlay.hidden{opacity:0;pointer-events:none}
  .card{
    background:var(--panel);border-radius:22px;padding:clamp(20px,4vw,38px);
    box-shadow:var(--shadow);max-width:min(560px,92vw);position:relative;
    border:3px solid #fff;outline:3px dashed var(--gold);outline-offset:-14px;
    animation:pop .4s cubic-bezier(.2,1.3,.4,1);
  }
  @keyframes pop{from{transform:scale(.9) translateY(10px);opacity:0}}
  .card::before,.card::after{content:"";position:absolute;width:14px;height:14px;border-radius:50%;background:var(--red);top:50%;transform:translateY(-50%);box-shadow:inset 0 0 0 3px #fff}
  .card::before{left:-8px}.card::after{right:-8px}
  .card .big{font-size:clamp(28px,6vw,52px);font-weight:900;letter-spacing:1px;line-height:1;color:var(--red);text-transform:uppercase;text-shadow:2px 2px 0 #fff,3px 3px 0 rgba(0,0,0,.12)}
  .card .sub{font-size:clamp(13px,1.8vw,17px);color:#3a5a74;margin-top:10px;line-height:1.5}
  .card .score-line{font-family:var(--mono);font-weight:bold;font-size:clamp(18px,3vw,26px);color:var(--ink);margin-top:14px}
  .card .score-line b{color:var(--red)}
  .chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:16px}
  .chip{display:inline-flex;align-items:center;gap:6px;background:#eef6ff;border:2px solid #cfe0f2;border-radius:999px;padding:6px 12px;font-size:clamp(11px,1.4vw,13px);font-weight:bold;color:#274a66}
  .kbd{font-family:var(--mono);background:#12324a;color:#fff;border-radius:6px;padding:2px 7px;font-size:12px;box-shadow:0 2px 0 #0a1f33}
  .pulse{animation:pulse 1.1s ease-in-out infinite}
  @keyframes pulse{50%{transform:scale(1.05)}}
  .hint{font-size:clamp(12px,1.6vw,15px);color:#fff;font-weight:bold;text-shadow:0 2px 6px rgba(0,0,0,.5);letter-spacing:.4px}
  .hint .dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--gold);margin-right:6px;box-shadow:0 0 0 3px rgba(255,207,63,.35);animation:pulse 1s infinite}

  /* ---------- D-pad (touch) ---------- */
  .dpad{position:absolute;z-index:4;bottom:16px;right:16px;width:150px;height:150px;display:none;user-select:none}
  .dbtn{position:absolute;width:52px;height:52px;border:none;cursor:pointer;border-radius:14px;
    background:rgba(255,253,246,.82);box-shadow:0 4px 10px rgba(0,0,0,.3),inset 0 0 0 2px #fff;
    color:#12324a;font-size:22px;font-weight:900;display:grid;place-items:center;touch-action:none}
  .dbtn:active{background:var(--gold);transform:scale(.94)}
  .dbtn.up{top:0;left:49px}.dbtn.down{bottom:0;left:49px}.dbtn.left{top:49px;left:0}.dbtn.right{top:49px;right:0}

  /* ---------- Footer legend ---------- */
  footer.legend{
    flex:0 0 auto;display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:center;
    padding:8px 14px;background:linear-gradient(#0d2740,#0a1f33);color:#9fd0ef;
    font-size:clamp(11px,1.4vw,13px);letter-spacing:.3px;border-top:3px solid var(--gold)
  }
  footer.legend b{color:#fff}
  .swatch{display:inline-block;width:14px;height:14px;border-radius:4px;vertical-align:-2px;margin:0 3px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)}

  @media (max-width:640px){
    .name p{display:none}
    .board-stats .stat{min-width:52px;padding:5px 7px}
    .btn{padding:8px 10px}
    .controls .btn.hide-s{display:none}
  }
  @media (pointer:coarse){ .dpad{display:block} }
</style>
</head>
<body>
<div class="app">
  <header class="board">
    <div class="brand">
      <div class="logo">🚂</div>
      <div class="name">
        <h1>Choo-Choo Express</h1>
        <p>Pick up passengers · keep the wagons rolling · don't crash!</p>
      </div>
    </div>
    <div class="board-stats">
      <div class="stat"><span class="k">Score</span><span class="v" id="score">0</span></div>
      <div class="stat best"><span class="k">Best</span><span class="v" id="best">0</span></div>
      <div class="stat passengers"><span class="k">Pax</span><span class="v" id="pax">0</span></div>
    </div>
    <div class="controls">
      <button class="btn small hide-s" id="muteBtn" title="Toggle sound">🔊</button>
      <button class="btn" id="playBtn">Start ▸</button>
    </div>
  </header>

  <main class="stage">
    <canvas id="game"></canvas>

    <div class="dpad" id="dpad">
      <button class="dbtn up"    data-dir="up">▲</button>
      <button class="dbtn left"  data-dir="left">◀</button>
      <button class="dbtn right" data-dir="right">▶</button>
      <button class="dbtn down"  data-dir="down">▼</button>
    </div>

    <!-- Start / attract -->
    <div class="overlay" id="startOverlay">
      <div class="card">
        <div class="big">All aboard!</div>
        <p class="sub">You're the conductor of a tiny steam train. Steer it around the station and scoop up every passenger waiting on the platforms before you run out of track.</p>
        <div class="chips">
          <span class="chip">🕹️ Arrows / WASD to steer</span>
          <span class="chip">⌨️ Space = pause</span>
          <span class="chip">📱 Swipe or tap the pad</span>
        </div>
        <p class="score-line" style="margin-top:18px">Watch the demo roll for a moment… then hit start!</p>
        <button class="btn pulse" id="startBig" style="margin-top:14px;font-size:18px;padding:12px 26px">Click or press any key to start ▸</button>
      </div>
    </div>

    <!-- Game over -->
    <div class="overlay hidden" id="overOverlay">
      <div class="card">
        <div class="big" style="color:#12324a">💥 End of the line</div>
        <p class="sub" id="deathMsg">The train jumped the rails!</p>
        <p class="score-line">Final score <b id="finalScore">0</b> · Passengers carried <b id="finalPax">0</b></p>
        <p class="score-line" style="font-size:16px;color:#3a5a74">Personal best <b id="finalBest" style="color:var(--red)">0</b></p>
        <button class="btn pulse" id="againBtn" style="margin-top:16px;font-size:18px;padding:12px 26px">↻ Run again</button>
      </div>
    </div>
  </main>

  <footer class="legend">
    <span>🚂 Locomotive</span>
    <span><i class="swatch" style="background:#d8412f"></i><i class="swatch" style="background:#2b7fd4"></i><i class="swatch" style="background:#2fae5b"></i><i class="swatch" style="background:#ffcf3f"></i> Wagons grow as you pick up passengers</span>
    <span><b>TIP:</b> shorter gaps between stations = faster trains = bigger scores</span>
  </footer>
</div>

<script>
"use strict";
/* ============================ Setup ============================ */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let dpr = Math.min(window.devicePixelRatio || 1, 2);
let W = 0, H = 0;                 // css pixels
let COLS = 20, ROWS = 13, cell = 30;
let ox = 0, oy = 0;               // playfield origin in css px

function resize(){
  const stage = canvas.parentElement;
  W = stage.clientWidth; H = stage.clientHeight;
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.round(W*dpr); canvas.height = Math.round(H*dpr);
  canvas.style.width = W+'px'; canvas.style.height = H+'px';
  ctx.setTransform(dpr,0,0,dpr,0,0);
  computeGrid();
}
function computeGrid(){
  let cols = Math.max(12, Math.min(26, Math.floor(W/44)));
  let rows = Math.max(9,  Math.min(18, Math.floor(H/44)));
  if(cols !== COLS || rows !== ROWS){ COLS=cols; ROWS=rows; initFieldData(); }
  cell = Math.min((W-2)/COLS, (H-2)/ROWS);
  ox = (W - COLS*cell)/2;
  oy = (H - ROWS*cell)/2;
}

/* ============================ Config ============================ */
const BASE_TICK = 155, MIN_TICK = 72;
const DIRS = { up:[0,-1], down:[0,1], left:[-1,0], right:[1,0] };
const COLORS = ['#d8412f','#2b7fd4','#2fae5b','#ffcf3f','#8e5bd0','#ee7a1f','#17a3a3'];
const SKIN   = ['#eab676','#c98a52','#8a5a34','#f0c79a','#b07a4a'];
const SHIRT  = ['#e2493b','#3a7ec6','#37a95a','#e8b62f','#8a56c9','#ef7f2a','#16a5a5','#d94f8a','#5a6b7a'];
const HAIR   = ['#2a1a12','#4a2f1c','#111','#6b4a2a','#b07a3a','#3a2a2a'];
const DEATH_MSGS = [
  "The train jumped the rails!",
  "Too many wagons, not enough track!",
  "Signal failure — full stop!",
  "That platform was occupied by the engine!",
  "Derailment! Mind the gap."
];

/* ============================ State ============================ */
let mode = 'attract';            // attract | playing | paused | dead
let snake = [], dir=[1,0], nextDir=[1,0];
let passenger=null, spawnTimer=0;
let score=0, best=0, tick=BASE_TICK, acc=0, lastTime=0;
let particles=[], clouds=[], birds=[];
let shake=0, flash=0, deathReason='';
let field={};                    // precomputed scenery arrays
let muted=false;

/* ============================ Field scenery ============================ */
function rnd(a,b){return a+Math.random()*(b-a);}
function initFieldData(){
  field.clouds = [];
  for(let i=0;i<5;i++) field.clouds.push({x:rnd(0,W),y:rnd(oy-40,oy+ROWS*cell*0.5),s:rnd(0.7,1.4),v:rnd(6,16)});
  field.birds = [];
  for(let i=0;i<3;i++) field.birds.push({x:rnd(0,W),y:rnd(oy-30,oy+ROWS*cell*0.4),v:rnd(24,40),ph:rnd(0,6)});
  field.trees = [];
  const nTrees = Math.floor(rnd(3,6));
  for(let i=0;i<nTrees;i++){
    const gx=Math.floor(rnd(1,COLS-1)), gy=Math.floor(rnd(1,ROWS-1));
    field.trees.push({gx,gy});
  }
  field.flowers=[];
  const nF=Math.floor(rnd(10,18));
  for(let i=0;i<nF;i++) field.flowers.push({gx:Math.floor(rnd(0,COLS)),gy:Math.floor(rnd(0,ROWS)),c:COLORS[Math.floor(rnd(0,COLORS.length))]});
}

/* ============================ Game logic ============================ */
function resetGame(startMode){
  const cy = Math.floor(ROWS/2);
  snake = [{x:3,y:cy},{x:2,y:cy},{x:1,y:cy}];
  dir=[1,0]; nextDir=[1,0];
  score=0; tick=BASE_TICK; acc=0;
  passenger=null; spawnTimer=0;
  particles=[]; shake=0; flash=0;
  mode=startMode;
  updateHUD();
  spawnPassenger();
}

function blocked(x,y){
  if(x<0||y<0||x>=COLS||y>=ROWS) return true;
  for(const s of snake) if(s.x===x&&s.y===y) return true;
  if(field.trees.some(t=>t.gx===x&&t.gy===y)) return true;
  return false;
}

function spawnPassenger(){
  let tries=0;
  while(tries++<200){
    const x=Math.floor(rnd(0,COLS)), y=Math.floor(rnd(0,ROWS));
    if(!blocked(x,y)){
      passenger={x,y,t:0,skin:SKIN[Math.floor(rnd(0,SKIN.length))],shirt:SHIRT[Math.floor(rnd(0,SHIRT.length))],hair:HAIR[Math.floor(rnd(0,HAIR.length))]};
      return;
    }
  }
}

function step(){
  // commit direction (no reversing)
  if(!(nextDir[0]===-dir[0] && nextDir[1]===-dir[1])) dir=nextDir.slice();
  const head=snake[0];
  const nx=head.x+dir[0], ny=head.y+dir[1];

  if(nx<0||ny<0||nx>=COLS||ny>=ROWS||snake.some(s=>s.x===nx&&s.y===ny)||field.trees.some(t=>t.gx===nx&&t.gy===ny)){
    die(snake.some(s=>s.x===nx&&s.y===ny)?'self':'wall');
    return;
  }
  snake.unshift({x:nx,y:ny});
  if(passenger && nx===passenger.x && ny===passenger.y){
    collect(nx,ny);
  } else {
    snake.pop();
  }
}

function collect(x,y){
  const pts = 10 + Math.floor((BASE_TICK-tick)/6);
  score += pts;
  tick = Math.max(MIN_TICK, BASE_TICK - snake.length*3.2);
  burst(x,y,'#ffcf3f',pts);
  passenger=null;
  spawnTimer=0;
  spawnPassenger();
  updateHUD();
}

function die(reason){
  deathReason=reason;
  mode='dead';
  if(score>best){ best=score; updateHUD(); }
  const msgEl=document.getElementById('deathMsg');
  msgEl.textContent = reason==='wall' ? "Off the edge of the map!" :
                      reason==='tree' ? "Crashed into a tree!" :
                      DEATH_MSGS[Math.floor(Math.random()*DEATH_MSGS.length)];
  document.getElementById('finalScore').textContent=score;
  document.getElementById('finalPax').textContent=Math.max(0,snake.length-3);
  document.getElementById('finalBest').textContent=best;
  show('overOverlay');
  shake=16; flash=1;
  beep(160,0.3,'sawtooth'); setTimeout(()=>beep(110,0.3,'sawtooth'),120); setTimeout(()=>beep(70,0.4,'square'),260);
}

/* ============================ AI (attract mode) ============================ */
function aiStep(){
  const head=snake[0];
  let tgt = passenger ? {x:passenger.x,y:passenger.y} : null;
  if(!tgt){ // wander toward center-ish
    tgt={x:Math.floor(COLS/2),y:Math.floor(ROWS/2)};
  }
  const opts=[[1,0],[-1,0],[0,1],[0,-1]];
  let scored=[];
  for(const o of opts){
    if(o[0]===-dir[0]&&o[1]===-dir[1]) continue;
    const nx=head.x+o[0], ny=head.y+o[1];
    if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
    if(snake.some(s=>s.x===nx&&s.y===ny)) continue;
    if(field.trees.some(t=>t.gx===nx&&t.gy===ny)) continue;
    let free=0;
    for(const f of [[1,0],[-1,0],[0,1],[0,-1]]){
      const fx=nx+f[0],fy=ny+f[1];
      if(fx>=0&&fy>=0&&fx<COLS&&fy<ROWS&&!snake.some(s=>s.x===fx&&s.y===fy)&&!field.trees.some(t=>t.gx===fx&&t.gy===fy)) free++;
    }
    const dist=Math.abs(nx-tgt.x)+Math.abs(ny-tgt.y);
    scored.push({o,score:free*3-dist});
  }
  if(!scored.length){ // stuck -> try anything valid incl reverse? just die gracefully by moving forward
    return;
  }
  scored.sort((a,b)=>b.score-a.score);
  nextDir=scored[0].o;
}

/* ============================ Particles ============================ */
function burst(gx,gy,color,n){
  const cx=ox+(gx+0.5)*cell, cy=oy+(gy+0.5)*cell;
  for(let i=0;i<16;i++){
    const a=rnd(0,Math.PI*2), sp=rnd(40,190);
    particles.push({x:cx,y:cy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-rnd(20,90),life:rnd(.5,1.1),max:1,r:rnd(2,5),c:i%3?color:'#fff'});
  }
  for(let i=0;i<6;i++){
    particles.push({x:cx+rnd(-6,6),y:cy,vx:rnd(-20,20),vy:rnd(-40,-15),life:rnd(.6,1),max:1,r:rnd(3,6),c:Math.random()<.5?'#12324a':'#fff',star:true});
  }
}
function puff(x,y){
  particles.push({x:x+rnd(-3,3),y:y+rnd(-3,3),vx:rnd(-8,8),vy:rnd(-30,-14),life:rnd(.7,1.3),max:1.3,r:rnd(3,7),c:'rgba(255,255,255,0.9)',smoke:true});
}
function updateParticles(dt){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.life-=dt; if(p.life<=0){particles.splice(i,1);continue;}
    p.vy+=(p.smoke?-8:520)*dt; p.vx*=0.99;
    p.x+=p.vx*dt; p.y+=p.vy*dt;
  }
}

/* ============================ Rendering ============================ */
function rr(x,y,w,h,r){ r=Math.min(r,w/2,h/2); 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 drawBackground(){
  // sky
  const g=ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#79c4f2');g.addColorStop(.45,'#bfe6ff');g.addColorStop(.72,'#eafaff');g.addColorStop(1,'#dff2d8');
  ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
  // sun
  ctx.save();
  const sx=W*0.86, sy=oy*0.5+20;
  const sg=ctx.createRadialGradient(sx,sy,4,sx,sy,60);
  sg.addColorStop(0,'rgba(255,240,180,.95)');sg.addColorStop(1,'rgba(255,220,120,0)');
  ctx.fillStyle=sg;ctx.beginPath();ctx.arc(sx,sy,60,0,7);ctx.fill();
  ctx.fillStyle='#ffe98a';ctx.beginPath();ctx.arc(sx,sy,26,0,7);ctx.fill();
  ctx.restore();
  // distant hills
  ctx.fillStyle='#a9dca0';
  hill(oy-6, H*0.14, 1);
  ctx.fillStyle='#8fce8a';
  hill(oy+4, H*0.10, 1.7);
  // clouds
  for(const c of field.clouds) drawCloud(c.x,c.y,c.s);
  // birds
  for(const b of field.birds) drawBird(b.x,b.y,b.ph);
}
function hill(base,amp,freq){
  ctx.beginPath();ctx.moveTo(0,base+amp);
  for(let x=0;x<=W;x+=20){ ctx.lineTo(x, base - amp*Math.sin(x/W*Math.PI*freq)-amp*0.2); }
  ctx.lineTo(W,H);ctx.lineTo(0,H);ctx.closePath();ctx.fill();
}
function drawCloud(x,y,s){
  ctx.save();ctx.translate(x,y);ctx.scale(s,s);
  ctx.fillStyle='rgba(255,255,255,.92)';
  ctx.beginPath();ctx.arc(0,0,16,0,7);ctx.arc(18,-6,13,0,7);ctx.arc(34,2,15,0,7);ctx.arc(16,8,14,0,7);ctx.fill();
  ctx.restore();
}
function drawBird(x,y,ph){
  ctx.save();ctx.translate(x,y);ctx.strokeStyle='#2a4a63';ctx.lineWidth=2;ctx.lineCap='round';
  const w=Math.sin(ph)*4;
  ctx.beginPath();ctx.moveTo(-6,0);ctx.quadraticCurveTo(-3,-w,0,0);ctx.quadraticCurveTo(3,-w,6,0);ctx.stroke();
  ctx.restore();
}

function drawGround(){
  // grass tiles with subtle checker
  for(let y=0;y<ROWS;y++){
    for(let x=0;x<COLS;x++){
      ctx.fillStyle=((x+y)&1)?'#5aa84a':'#61b051';
      ctx.fillRect(ox+x*cell,oy+y*cell,cell+1,cell+1);
    }
  }
  // flowers
  for(const f of field.flowers){
    const cx=ox+(f.gx+.5)*cell, cy=oy+(f.gy+.5)*cell, r=cell*0.12;
    ctx.fillStyle=f.c;
    for(let i=0;i<5;i++){const a=i/5*Math.PI*2;ctx.beginPath();ctx.arc(cx+Math.cos(a)*r,cy+Math.sin(a)*r,r*.7,0,7);ctx.fill();}
    ctx.fillStyle='#ffe066';ctx.beginPath();ctx.arc(cx,cy,r*.7,0,7);ctx.fill();
  }
  // trees
  for(const t of field.trees){
    const cx=ox+(t.gx+.5)*cell, cy=oy+(t.gy+.5)*cell;
    ctx.fillStyle='rgba(0,0,0,.12)';ctx.beginPath();ctx.ellipse(cx,cy+cell*.35,cell*.4,cell*.16,0,0,7);ctx.fill();
    ctx.fillStyle='#7a4a24';rr(cx-cell*.06,cy-cell*.02,cell*.12,cell*.42,cell*.05);ctx.fill();
    ctx.fillStyle='#2f7d3a';ctx.beginPath();ctx.arc(cx,cy-cell*.15,cell*.34,0,7);ctx.fill();
    ctx.fillStyle='#3f9a4c';ctx.beginPath();ctx.arc(cx-cell*.14,cy-cell*.05,cell*.22,0,7);ctx.arc(cx+cell*.14,cy-cell*.08,cell*.2,0,7);ctx.fill();
  }
  // playfield frame
  ctx.strokeStyle='rgba(18,50,74,.25)';ctx.lineWidth=2;
  ctx.strokeRect(ox-1,oy-1,COLS*cell+2,ROWS*cell+2);
}

function drawPlatform(){
  if(!passenger) return;
  const cx=ox+(passenger.x+.5)*cell, cy=oy+(passenger.y+.5)*cell;
  const pw=cell*1.15, ph=cell*0.5;
  // shadow
  ctx.fillStyle='rgba(0,0,0,.15)';ctx.beginPath();ctx.ellipse(cx,cy+ph*0.5,pw*0.6,ph*0.4,0,0,7);ctx.fill();
  // slab
  ctx.fillStyle='#d9d2c4';rr(cx-pw/2,cy-ph/2,pw,ph,4);ctx.fill();
  ctx.fillStyle='#c3bcac';rr(cx-pw/2,cy+ph*0.1,pw,ph*0.4,3);ctx.fill();
  // yellow safety stripe
  ctx.fillStyle='#ffcf3f';for(let i=-2;i<=2;i++){rr(cx+i*pw/5-3,cy-ph/2-2,6,4,1);ctx.fill();}
  // sign post
  ctx.fillStyle='#8a8f98';ctx.fillRect(cx+pw*0.42,cy-ph*1.6,2,ph*1.4);
  ctx.fillStyle='#12324a';rr(cx+pw*0.30,cy-ph*2.1,pw*0.42,ph*0.6,3);ctx.fill();
  ctx.fillStyle='#fff';ctx.font=`bold ${Math.max(7,cell*0.22)}px var(--mono)`;ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.fillText("STOP",cx+pw*0.51,cy-ph*1.8);
  // waiting person
  drawPerson(cx,cy-passenger.t*0.001,passenger,true);
  // bobbing motion hint arrow
  const wob=Math.sin(performance.now()/200)*3;
  ctx.fillStyle='rgba(255,207,63,.9)';
  ctx.beginPath();ctx.moveTo(cx,cy-ph-8+wob);ctx.lineTo(cx-5,cy-ph-16+wob);ctx.lineTo(cx+5,cy-ph-16+wob);ctx.closePath();ctx.fill();
}

function drawPerson(cx,cy,p,isWaiting){
  const s=cell*0.9;
  ctx.save();ctx.translate(cx,cy);
  if(isWaiting){const b=Math.sin(performance.now()/220+p.x)*1.5;ctx.translate(0,b);}
  ctx.fillStyle='rgba(0,0,0,.15)';ctx.beginPath();ctx.ellipse(0,s*0.34,s*0.24,s*0.08,0,0,7);ctx.fill();
  // legs
  ctx.fillStyle='#2a3a4a';rr(-s*0.14,s*0.02,s*0.11,s*0.3,2);ctx.fill();rr(s*0.03,s*0.02,s*0.11,s*0.3,2);ctx.fill();
  // body
  ctx.fillStyle=p.shirt;rr(-s*0.19,-s*0.12,s*0.38,s*0.24,s*0.07);ctx.fill();
  // arms
  ctx.fillStyle=p.skin;rr(-s*0.27,-s*0.1,s*0.09,s*0.2,3);ctx.fill();rr(s*0.18,-s*0.1,s*0.09,s*0.2,3);ctx.fill();
  // suitcase for some
  if(p.x%2===0){ctx.fillStyle='#b5651d';rr(s*0.22,s*0.02,s*0.16,s*0.14,2);ctx.fill();ctx.strokeStyle='#7a4412';ctx.lineWidth=1;ctx.strokeRect(s*0.22,s*0.02,s*0.16,s*0.14);}
  // head
  ctx.fillStyle=p.skin;ctx.beginPath();ctx.arc(0,-s*0.26,s*0.15,0,7);ctx.fill();
  // hair
  ctx.fillStyle=p.hair;ctx.beginPath();ctx.arc(0,-s*0.3,s*0.15,Math.PI,0);ctx.fill();
  // face
  ctx.fillStyle='#1a1a1a';ctx.beginPath();ctx.arc(-s*0.05,-s*0.25,1.3,0,7);ctx.arc(s*0.05,-s*0.25,1.3,0,7);ctx.fill();
  ctx.strokeStyle='#1a1a1a';ctx.lineWidth=1;ctx.beginPath();ctx.arc(0,-s*0.2,s*0.06,0.2,Math.PI-0.2);ctx.stroke();
  ctx.restore();
}

/* ---- Train rendering (smooth interpolation) ---- */
let renderPos=[]; // interpolated positions per segment
function lerp(a,b,t){return a+(b-a)*t;}
function computeRenderPos(alpha){
  const n=snake.length;
  renderPos.length=n;
  if(n===0)return;
  // head moves from previous head pos to current along dir
  const prevHead={x:snake[0].x-dir[0],y:snake[0].y-dir[1]};
  renderPos[0]={x:lerp(prevHead.x,snake[0].x,alpha),y:lerp(prevHead.y,snake[0].y,alpha)};
  for(let i=1;i<n;i++){
    renderPos[i]={x:snake[i].x,y:snake[i].y};
  }
}
function segCenter(i){const p=renderPos[i];return{x:ox+(p.x+.5)*cell,y:oy+(p.y+.5)*cell};}

function drawTrain(){
  if(snake.length===0)return;
  const n=snake.length;
  // shadows first
  ctx.fillStyle='rgba(0,0,0,.16)';
  for(let i=n-1;i>=0;i--){const c=segCenter(i);ctx.beginPath();ctx.ellipse(c.x,c.y+cell*0.28,cell*0.36,cell*0.14,0,0,7);ctx.fill();}

  // wheels connecting segments (drawn under cars)
  ctx.fillStyle='#1a2530';
  for(let i=n-1;i>=1;i--){
    const a=segCenter(i), b=segCenter(i-1);
    const mx=(a.x+b.x)/2,my=(a.y+b.y)/2;
    ctx.beginPath();ctx.arc(mx-3,my,cell*0.12,0,7);ctx.arc(mx+3,my,cell*0.12,0,7);ctx.fill();
  }

  // carriages (back to front so head overlaps nicely)
  for(let i=n-1;i>=1;i--){
    drawWagon(segCenter(i),i);
  }
  // locomotive
  drawLocomotive(segCenter(0),dir);

  // steam puffs occasionally
  if(mode==='playing'||mode==='attract'){
    const c=segCenter(0);
    if(Math.random()<0.5) puff(c.x-dir[0]*cell*0.1, c.y-cell*0.28);
  }
}

function drawWagon(c,i){
  const col=COLORS[(i-1)%COLORS.length];
  const w=cell*0.86,h=cell*0.62;
  ctx.save();ctx.translate(c.x,c.y);
  // roof band
  ctx.fillStyle='rgba(0,0,0,.18)';rr(-w/2,-h/2,w,h,4);ctx.fill();
  // body
  ctx.fillStyle=col;rr(-w/2,-h/2,w,h,4);ctx.fill();
  // top highlight
  ctx.fillStyle='rgba(255,255,255,.28)';rr(-w/2,-h/2,w,h*0.28,4);ctx.fill();
  // windows with passengers inside
  ctx.fillStyle='rgba(255,255,255,.92)';
  const wx=w*0.24, wy=h*0.4;
  for(let k=-1;k<=1;k++){
    ctx.save();ctx.translate(k*w*0.3, -h*0.02);
    rr(-wx/2,-wy/2,wx,wy,3);ctx.fill();
    // little passenger head
    ctx.fillStyle=SHIRT[(i+k+9)%SHIRT.length];ctx.beginPath();ctx.arc(0,wy*0.05,wx*0.28,0,7);ctx.fill();
    ctx.fillStyle=SKIN[(i+k)%SKIN.length];ctx.beginPath();ctx.arc(0,-wy*0.1,wx*0.22,0,7);ctx.fill();
    ctx.restore();
  }
  // bottom trim + coupling dots
  ctx.fillStyle='rgba(0,0,0,.25)';rr(-w/2,h/2-3,w,3,1);ctx.fill();
  ctx.restore();
}

function drawLocomotive(c,d){
  const ang=Math.atan2(d[1],d[0]);
  ctx.save();ctx.translate(c.x,c.y);ctx.rotate(ang);
  const L=cell*1.05, B=cell*0.78; // length along travel, breadth across
  // chassis shadow strip
  ctx.fillStyle='#1a2530';rr(-L*0.5,-B*0.5,L,B*0.18,3);ctx.fill();
  // boiler (body)
  ctx.fillStyle='#12324a';rr(-L*0.42,-B*0.42,L*0.72,B*0.84,6);ctx.fill();
  ctx.fillStyle='#1b4a6b';rr(-L*0.42,-B*0.42,L*0.72,B*0.3,6);ctx.fill();
  // red cab
  ctx.fillStyle='#d8412f';rr(-L*0.5,-B*0.46,L*0.2,B*0.92,5);ctx.fill();
  ctx.fillStyle='#b5321f';rr(-L*0.5,-B*0.46,L*0.2,B*0.3,5);ctx.fill();
  // gold stripe
  ctx.fillStyle='#ffcf3f';rr(-L*0.42,B*0.02,L*0.72,B*0.1,2);ctx.fill();
  // front bumper
  ctx.fillStyle='#1a2530';rr(L*0.3,-B*0.4,L*0.08,B*0.8,2);ctx.fill();
  // cow-catcher triangle
  ctx.fillStyle='#c9c9c9';ctx.beginPath();ctx.moveTo(L*0.38,-B*0.34);ctx.lineTo(L*0.55,0);ctx.lineTo(L*0.38,B*0.34);ctx.closePath();ctx.fill();
  // chimney
  ctx.fillStyle='#1a2530';rr(L*0.06,-B*0.62,B*0.22,B*0.24,3);ctx.fill();
  ctx.fillStyle='#0d1b28';rr(L*0.02,-B*0.66,B*0.3,B*0.12,3);ctx.fill();
  // dome
  ctx.fillStyle='#ffcf3f';ctx.beginPath();ctx.arc(-L*0.12,-B*0.34,B*0.16,Math.PI,0);ctx.fill();
  // headlamp glow
  ctx.fillStyle='#fff6c8';ctx.beginPath();ctx.arc(L*0.34,0,B*0.14,0,7);ctx.fill();
  // window on cab
  ctx.fillStyle='#bfe6ff';rr(-L*0.47,-B*0.3,B*0.16,B*0.34,2);ctx.fill();
  // driver silhouette
  ctx.fillStyle='#12324a';ctx.beginPath();ctx.arc(-L*0.44,-B*0.12,B*0.09,0,7);ctx.fill();
  // wheels
  ctx.fillStyle='#0d1b28';
  for(const wx of [-L*0.34,-L*0.02,L*0.2]){ctx.beginPath();ctx.arc(wx,B*0.42,B*0.16,0,7);ctx.fill();
    ctx.fillStyle='#ffcf3f';ctx.beginPath();ctx.arc(wx,B*0.42,B*0.06,0,7);ctx.fill();ctx.fillStyle='#0d1b28';}
  ctx.restore();
}

function drawParticles(){
  for(const p of particles){
    const a=Math.max(0,p.life/p.max);
    if(p.star){
      ctx.globalAlpha=a;ctx.fillStyle=p.c;
      ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.life*8);
      ctx.beginPath();for(let i=0;i<5;i++){const an=i/5*Math.PI*2-Math.PI/2;ctx.lineTo(Math.cos(an)*p.r*1.8,Math.sin(an)*p.r*1.8);const an2=an+Math.PI/5;ctx.lineTo(Math.cos(an2)*p.r*0.7,Math.sin(an2)*p.r*0.7);}ctx.closePath();ctx.fill();ctx.restore();
    }else{
      ctx.globalAlpha=p.smoke?a*0.6:a;
      ctx.fillStyle=p.c;ctx.beginPath();ctx.arc(p.x,p.y,p.r*(p.smoke?(1.4-a):1),0,7);ctx.fill();
    }
  }
  ctx.globalAlpha=1;
}

/* ============================ HUD ============================ */
const el=id=>document.getElementById(id);
function updateHUD(){el('score').textContent=score;el('best').textContent=best;el('pax').textContent=Math.max(0,snake.length-3);}

/* ============================ Overlays ============================ */
function show(id){el(id).classList.remove('hidden');}
function hide(id){el(id).classList.add('hidden');}

/* ============================ Input ============================ */
function queueDir(name){
  const d=DIRS[name]; if(!d)return;
  if(d[0]===-dir[0]&&d[1]===-dir[1])return;
  nextDir=d.slice();
  if(mode==='attract') beginPlay();
}
window.addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  if(['arrowup','w'].includes(k)){queueDir('up');e.preventDefault();}
  else if(['arrowdown','s'].includes(k)){queueDir('down');e.preventDefault();}
  else if(['arrowleft','a'].includes(k)){queueDir('left');e.preventDefault();}
  else if(['arrowright','d'].includes(k)){queueDir('right');e.preventDefault();}
  else if(k===' '){e.preventDefault();togglePause();}
  else if(k==='enter'){if(mode==='dead'||mode==='attract')beginPlay();}
  else if(e.key==='Escape'){toggleMute();}
  else if(mode==='attract'&&e.key.length===1){beginPlay();}
});

// touch / swipe
let tStart=null;
canvas.addEventListener('touchstart',e=>{tStart={x:e.touches[0].clientX,y:e.touches[0].clientY};},{passive:true});
canvas.addEventListener('touchmove',e=>{
  if(!tStart)return;const t=e.touches[0];const dx=t.clientX-tStart.x,dy=t.clientY-tStart.y;
  if(Math.abs(dx)>28||Math.abs(dy)>28){
    if(Math.abs(dx)>Math.abs(dy))queueDir(dx>0?'right':'left');else queueDir(dy>0?'down':'up');
    tStart={x:t.clientX,y:t.clientY};
  }
},{passive:true});
canvas.addEventListener('touchend',()=>{tStart=null;if(mode==='attract')beginPlay();});

document.querySelectorAll('.dbtn').forEach(b=>{
  const go=e=>{e.preventDefault();queueDir(b.dataset.dir);};
  b.addEventListener('pointerdown',go);
});

// buttons
el('playBtn').addEventListener('click',()=>{ if(mode==='dead')beginPlay(); else togglePause(); });
el('startBig').addEventListener('click',beginPlay);
el('againBtn').addEventListener('click',beginPlay);
el('muteBtn').addEventListener('click',toggleMute);
canvas.addEventListener('click',()=>{ if(mode==='attract')beginPlay(); });

function beginPlay(){resetGame('playing');hide('startOverlay');hide('overOverlay');initAudio();updatePlayBtn();}
function togglePause(){
  if(mode==='playing'){mode='paused';show('pauseOverlay');}
  else if(mode==='paused'){mode='playing';hide('pauseOverlay');}
  updatePlayBtn();
}
function updatePlayBtn(){el('playBtn').textContent = mode==='playing'?'❚❚ Pause':mode==='paused'?'Resume ▸':mode==='dead'?'Run again ↻':'Start ▸';}

/* ============================ Audio (WebAudio, no storage) ============================ */
let AC=null;
function initAudio(){try{if(!AC)AC=new (window.AudioContext||window.webkitAudioContext)();if(AC.state==='suspended')AC.resume();}catch(e){}}
function beep(freq,dur,type='triangle',vol=0.15){
  if(muted||!AC)return;
  const o=AC.createOscillator(),g=AC.createGain();
  o.type=type;o.frequency.value=freq;
  g.gain.setValueAtTime(vol,AC.currentTime);
  g.gain.exponentialRampToValueAtTime(0.0001,AC.currentTime+dur);
  o.connect(g).connect(AC.destination);o.start();o.stop(AC.currentTime+dur);
}
function toggleMute(){muted=!muted;el('muteBtn').textContent=muted?'🔇':'🔊';}

/* ============================ Main loop ============================ */
function frame(now){
  requestAnimationFrame(frame);
  if(!lastTime)lastTime=now;
  let dt=(now-lastTime)/1000; lastTime=now;
  dt=Math.min(dt,0.05);

  // ambient
  for(const c of field.clouds){c.x+=c.v*dt;if(c.x>W+60)c.x=-60;}
  for(const b of field.birds){b.x+=b.v*dt;b.ph+=dt*6;if(b.x>W+30)b.x=-30;}
  if(passenger)passenger.t+=dt*1000;

  if(mode==='playing'||mode==='attract'){
    acc+=dt*1000;
    const effTick = mode==='attract'?Math.max(BASE_TICK*0.9,MIN_TICK+30):tick;
    let guard=0;
    while(acc>=effTick && guard<4){
      acc-=effTick;
      if(mode==='attract')aiStep();
      step();
      if(mode==='dead')break;
      guard++;
    }
    if(mode==='attract'){spawnTimer+=effTick;if(spawnTimer>1200&&!passenger){spawnPassenger();spawnTimer=0;}}
  } else if(mode==='paused'){
    acc=0;
  }

  updateParticles(dt);
  if(shake>0)shake=Math.max(0,shake-dt*40);
  if(flash>0)flash=Math.max(0,flash-dt*2.2);

  // render
  const alpha = (mode==='playing'||mode==='attract')?Math.min(1,acc/tick):1;
  computeRenderPos(alpha);

  ctx.clearRect(0,0,W,H);
  ctx.save();
  if(shake>0){ctx.translate(rnd(-shake,shake)*0.4,rnd(-shake,shake)*0.4);}
  drawBackground();
  drawGround();
  drawPlatform();
  drawTrain();
  drawParticles();
  ctx.restore();

  if(flash>0){ctx.fillStyle=`rgba(255,80,60,${flash*0.35})`;ctx.fillRect(0,0,W,H);}

  // attract watermark hint near top-center
  if(mode==='attract'){
    ctx.save();ctx.globalAlpha=.9;
    ctx.fillStyle='rgba(18,50,74,.75)';
    const txt='Auto-demo running…';ctx.font='bold 13px var(--font)';ctx.textAlign='center';
    const tw=ctx.measureText(txt).width+22;
    rr(W/2-tw/2,10,tw,24,12);ctx.fill();
    ctx.fillStyle='#ffcf3f';ctx.fillText(txt,W/2,26);
    ctx.restore();
  }
}

/* ============================ Boot ============================ */
window.addEventListener('resize',resize);
resize();
initFieldData();
resetGame('attract');
// add a pause overlay element dynamically (keeps HTML tidy)
(function addPauseOverlay(){
  const div=document.createElement('div');div.id='pauseOverlay';div.className='overlay hidden';
  div.innerHTML='<div class="card"><div class="big" style="color:#2b7fd4">Paused</div><p class="sub">Take a breather at the station.</p><button class="btn pulse" id="resumeBtn" style="margin-top:12px;font-size:16px;padding:10px 22px">Resume ▸</button></div>';
  document.querySelector('.stage').appendChild(div);
  div.addEventListener('click',()=>{if(mode==='paused')togglePause();});
})();
requestAnimationFrame(frame);
</script>
</body>
</html>