Models 44
Train snakeOne-line

GPT-6.1 Terra

by OpenAIReasoningClean
Open in new window

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

How it went

Total time
1 min 21 s
Cost
$0.035
Output tokens
6,839
Spent thinking
0 tokens
Speed
85.6 tok/s
Console errors
0
First token after
1.2 s
First visible text after
1.2 s
Prompt tokens
361
Code size
19.2 KB
Finish reason
stop
Served by
OpenAI
Model id
openai/gpt-6.1-terra
Reasoning sent
{"effort":"none"}
Max output
128,000 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 · 419 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Platform Express</title>
<style>
  :root {
    --ink: #233f3a;
    --muted: #758780;
    --cream: #f7f5ec;
    --green: #287a5c;
    --gold: #edb957;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; min-height: 100%; }
  body {
    color: var(--ink);
    background: #e6eee5;
    font-family: "Trebuchet MS", Arial, sans-serif;
    padding: 24px;
  }
  .shell {
    max-width: 1180px;
    margin: auto;
    background: var(--cream);
    border: 1px solid rgba(53, 88, 69, .12);
    border-radius: 24px;
    overflow: hidden;
    box-shadow: 0 20px 60px rgba(44, 75, 55, .12);
  }
  header {
    height: 84px;
    padding: 0 32px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    background: #fffef8;
    border-bottom: 1px solid #e7eadf;
  }
  .brand { display: flex; align-items: center; gap: 13px; }
  .brand-icon {
    width: 42px; height: 42px; border-radius: 13px;
    background: #dceee0; display: grid; place-items: center;
  }
  .brand-icon svg { width: 25px; height: 25px; }
  h1 { margin: 0; font-size: 21px; letter-spacing: -.8px; line-height: 1.05; }
  .brand small { display: block; margin-top: 5px; color: #93a39a; font-size: 10px; font-weight: 800; letter-spacing: 1.8px; }
  .header-right { display: flex; align-items: center; gap: 12px; }
  .live { color: #4c8569; font-size: 11px; font-weight: bold; letter-spacing: .8px; }
  .live:before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #77b987; margin-right: 7px; vertical-align: 1px; }
  .sound {
    border: 1px solid #e5eae0; color: #668176; background: #fff; border-radius: 11px;
    width: 38px; height: 38px; font-size: 17px; cursor: pointer;
  }
  main { padding: 27px 32px 25px; }
  .intro { display: flex; justify-content: space-between; align-items: end; gap: 20px; margin-bottom: 20px; }
  .eyebrow { color: #d49a46; font-size: 10px; font-weight: 900; letter-spacing: 2px; margin-bottom: 7px; }
  h2 { margin: 0; font-size: 27px; letter-spacing: -1px; }
  .intro p { margin: 6px 0 0; color: #87958b; font-size: 13px; }
  .scores { display: flex; gap: 9px; }
  .score {
    min-width: 112px; padding: 10px 15px; background: #fffef9; border: 1px solid #e9ece2;
    border-radius: 12px; display: flex; align-items: center; gap: 10px;
  }
  .score-icon { font-size: 18px; }
  .score-label { color: #a0ada3; font-size: 9px; font-weight: 900; letter-spacing: 1px; white-space: nowrap; }
  .score-value { color: #325f4b; font-size: 19px; font-weight: 900; line-height: 1.1; margin-top: 2px; }
  .game-wrap { position: relative; overflow: hidden; border-radius: 16px; background: #dbe8d3; border: 1px solid #cfddca; }
  canvas { display: block; width: 100%; height: auto; aspect-ratio: 22 / 11; }
  .game-overlay {
    position: absolute; left: 50%; bottom: 20px; transform: translateX(-50%);
    display: flex; align-items: center; gap: 11px; padding: 10px 15px;
    background: rgba(255, 254, 247, .95); border: 1px solid rgba(255,255,255,.9);
    border-radius: 13px; box-shadow: 0 5px 18px rgba(48, 80, 52, .15);
    white-space: nowrap; pointer-events: none;
  }
  .play-circle { width: 30px; height: 30px; background: #317e5e; color: white; display: grid; place-items: center; border-radius: 9px; font-size: 11px; padding-left: 2px; }
  .overlay-title { font-size: 12px; font-weight: 900; color: #345d4b; }
  .overlay-sub { margin-top: 3px; font-size: 10px; color: #90a095; }
  .footer { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-top: 18px; }
  .instructions { display: flex; align-items: center; gap: 12px; color: #7e8f83; font-size: 12px; }
  .keys { display: flex; gap: 3px; }
  kbd { width: 25px; height: 25px; display: grid; place-items: center; border: 1px solid #e1e7dc; border-bottom-width: 2px; border-radius: 6px; background: #fffef9; font: bold 12px Arial; color: #728b7b; }
  .instructions strong { color: #52715f; }
  .legend { display: flex; align-items: center; gap: 8px; color: #95a298; font-size: 11px; }
  .legend-dot { width: 9px; height: 9px; border-radius: 50%; background: #ecb257; }
  .restart { border: 0; background: transparent; color: #4a8468; font: bold 12px "Trebuchet MS", sans-serif; cursor: pointer; padding: 8px 0 8px 8px; }
  .mobile-controls { display: none; justify-content: center; gap: 6px; margin-top: 15px; }
  .mobile-controls button { width: 48px; height: 39px; border: 1px solid #dce6d8; border-radius: 9px; background: #fffef9; color: #4c7961; font-size: 19px; cursor: pointer; touch-action: manipulation; }
  @media (max-width: 700px) {
    body { padding: 10px; }
    .shell { border-radius: 18px; }
    header { height: 68px; padding: 0 16px; }
    h1 { font-size: 18px; }
    .live { display: none; }
    main { padding: 20px 14px; }
    .intro { align-items: start; }
    h2 { font-size: 23px; }
    .intro p { font-size: 12px; max-width: 220px; line-height: 1.4; }
    .scores { gap: 5px; }
    .score { min-width: 65px; padding: 8px; gap: 5px; }
    .score-icon { font-size: 14px; }
    .score-label { font-size: 8px; letter-spacing: .4px; }
    .score-value { font-size: 16px; }
    .game-wrap { border-radius: 12px; }
    .game-overlay { bottom: 8px; padding: 7px 10px; gap: 7px; }
    .play-circle { width: 25px; height: 25px; }
    .overlay-title { font-size: 10px; }
    .overlay-sub { font-size: 9px; }
    .footer { margin-top: 12px; }
    .instructions { font-size: 11px; gap: 7px; }
    .keys { display: none; }
    .legend { display: none; }
    .mobile-controls { display: flex; }
  }
  @media (max-width: 430px) {
    .intro { gap: 5px; }
    h2 { font-size: 21px; }
    .intro p { max-width: 175px; }
    .score { min-width: 58px; }
    .score-icon { display: none; }
    .brand small { font-size: 8px; }
  }
</style>
</head>
<body>
<div class="shell">
  <header>
    <div class="brand">
      <div class="brand-icon">
        <svg viewBox="0 0 32 32" fill="none" aria-hidden="true"><rect x="6" y="4" width="20" height="22" rx="6" fill="#398263"/><rect x="9" y="8" width="14" height="8" rx="2" fill="#E5F2E5"/><circle cx="11" cy="21" r="1.7" fill="#F4C36C"/><circle cx="21" cy="21" r="1.7" fill="#F4C36C"/><path d="M10 26l-2 3M22 26l2 3M10 28h12" stroke="#398263" stroke-width="2" stroke-linecap="round"/></svg>
      </div>
      <div><h1>Platform Express</h1><small>A LITTLE JOURNEY</small></div>
    </div>
    <div class="header-right">
      <span class="live">ALL ABOARD</span>
      <button class="sound" id="sound" aria-label="Toggle sound" title="Toggle sound">♪</button>
    </div>
  </header>
  <main>
    <div class="intro">
      <div>
        <div class="eyebrow">WELCOME TO THE STATION</div>
        <h2>All aboard!</h2>
        <p>Pick up passengers and watch your little train grow.</p>
      </div>
      <div class="scores">
        <div class="score"><span class="score-icon">🎟️</span><div><div class="score-label">PASSENGERS</div><div class="score-value" id="score">0</div></div></div>
        <div class="score"><span class="score-icon">✦</span><div><div class="score-label">BEST</div><div class="score-value" id="best">0</div></div></div>
      </div>
    </div>
    <div class="game-wrap" id="game">
      <canvas id="canvas" width="1100" height="550" aria-label="Snake game with a train picking up passengers"></canvas>
      <div class="game-overlay" id="overlay">
        <div class="play-circle">▶</div>
        <div><div class="overlay-title" id="overlayTitle">Click or press a key to start</div><div class="overlay-sub" id="overlaySub">Enjoy the ride in demo mode</div></div>
      </div>
    </div>
    <div class="footer">
      <div class="instructions">
        <div class="keys"><kbd>←</kbd><kbd>↑</kbd><kbd>↓</kbd><kbd>→</kbd></div>
        <span><strong>Steer your train</strong> with arrow keys or WASD</span>
      </div>
      <div class="legend"><span class="legend-dot"></span> Pick up passengers <span style="padding:0 3px;color:#ccd5c9">•</span> Avoid your wagons</div>
      <button class="restart" id="restart">↻ &nbsp;Restart</button>
    </div>
    <div class="mobile-controls" aria-label="Touch controls">
      <button data-dir="left" aria-label="Left">←</button><button data-dir="up" aria-label="Up">↑</button><button data-dir="down" aria-label="Down">↓</button><button data-dir="right" aria-label="Right">→</button>
    </div>
  </main>
</div>
<script>
(() => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');
  const title = document.getElementById('overlayTitle');
  const sub = document.getElementById('overlaySub');
  const scoreEl = document.getElementById('score');
  const bestEl = document.getElementById('best');
  const COLS = 22, ROWS = 11, S = 50;
  const colors = ['#e9ac58', '#dc7770', '#70a9a2', '#aa8db2', '#e4b35e', '#7da979', '#d5856d'];
  const dirs = {right:{x:1,y:0}, left:{x:-1,y:0}, up:{x:0,y:-1}, down:{x:0,y:1}};
  let snake, direction, queued, passenger, score = 0, best = 0, mode = 'demo', last = 0, tickCount = 0, soundOn = false, audioCtx;
  let demoTargets = [{x:15,y:4},{x:18,y:7},{x:10,y:8},{x:5,y:5},{x:8,y:2},{x:16,y:3},{x:19,y:8},{x:8,y:8}];
  let targetIndex = 0;

  function reset(demo) {
    snake = [{x:8,y:4},{x:7,y:4},{x:6,y:4},{x:5,y:4},{x:4,y:4}];
    direction = dirs.right; queued = dirs.right;
    score = 0; tickCount = 0; targetIndex = 0;
    passenger = demo ? {x:15,y:4} : {x:15,y:4};
    scoreEl.textContent = score;
    mode = demo ? 'demo' : 'playing';
    overlay.style.display = demo ? 'flex' : 'none';
    title.textContent = 'Click or press a key to start';
    sub.textContent = 'Enjoy the ride in demo mode';
    last = performance.now();
  }
  function roundRect(x,y,w,h,r,fill,stroke) {
    ctx.beginPath(); ctx.roundRect(x,y,w,h,r);
    if(fill){ctx.fillStyle=fill;ctx.fill();}
    if(stroke){ctx.strokeStyle=stroke;ctx.stroke();}
  }
  function line(x1,y1,x2,y2,color,width=1) {
    ctx.strokeStyle=color; ctx.lineWidth=width; ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke();
  }
  function ellipse(x,y,rx,ry,color) {
    ctx.fillStyle=color;ctx.beginPath();ctx.ellipse(x,y,rx,ry,0,0,Math.PI*2);ctx.fill();
  }
  function tree(x,y,scale=1) {
    ctx.save(); ctx.translate(x,y); ctx.scale(scale,scale);
    ellipse(0,14,19,6,'rgba(77,115,74,.10)');
    roundRect(-3,0,6,17,2,'#a98d69');
    ellipse(-8,-5,13,12,'#a5c49a'); ellipse(7,-7,14,13,'#91b789'); ellipse(0,-14,13,12,'#a9cba0');
    ctx.restore();
  }
  function drawBackground() {
    ctx.fillStyle='#e4eddc';ctx.fillRect(0,0,1100,550);
    // Checkerboard lawn gives a gentle sense of the game grid.
    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++) {
      ctx.fillStyle=(x+y)%2===0?'#e1ebda':'#e6efdf';
      ctx.fillRect(x*S,y*S,S,S);
    }
    // Station platform across the top.
    ctx.fillStyle='#d4d7c1'; ctx.fillRect(0,0,1100,55);
    ctx.fillStyle='#eee9d8'; ctx.fillRect(0,0,1100,46);
    ctx.fillStyle='#d7ccb0';ctx.fillRect(0,46,1100,4);
    ctx.fillStyle='#fff5cf';ctx.fillRect(0,51,1100,4);
    for(let x=0;x<1100;x+=74) {
      line(x,0,x,46,'rgba(176,171,150,.22)');
      line(x+37,0,x+37,22,'rgba(176,171,150,.12)');
    }
    // A small station sign and platform details.
    roundRect(33,8,126,28,5,'#4e806a');
    ctx.fillStyle='#f8f8e9';ctx.font='bold 11px Trebuchet MS, sans-serif';ctx.textAlign='center';ctx.letterSpacing='2px';
    ctx.fillText('GREENFIELD',96,26);ctx.letterSpacing='0px';
    line(760,13,760,38,'#a5ad98',3); line(824,13,824,38,'#a5ad98',3);
    roundRect(754,12,76,7,3,'#ba9d77'); roundRect(754,28,76,7,3,'#ba9d77');
    roundRect(1002,9,48,29,4,'#d7e3cf');
    ctx.fillStyle='#648b70';ctx.font='bold 9px Trebuchet MS';ctx.fillText('PLATFORM',1026,21);ctx.font='bold 11px Trebuchet MS';ctx.fillText('01',1026,33);
    // Decorative greenery at the corners.
    tree(35,112,.85); tree(1072,122,.8); tree(35,500,.85); tree(1064,495,1);
    // little tufts and flowers
    const plants=[[115,115],[235,95],[360,493],[933,111],[984,451],[160,446],[871,488],[518,97],[104,295],[1020,310]];
    plants.forEach((p,i)=>{
      ctx.strokeStyle='#bad1b2';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(p[0],p[1]+5);ctx.lineTo(p[0]-3,p[1]);ctx.moveTo(p[0],p[1]+5);ctx.lineTo(p[0]+4,p[1]-2);ctx.stroke();
      if(i%3===0){ellipse(p[0]+6,p[1]-3,2,2,'#f5d39a');ellipse(p[0]+1,p[1]-5,2,2,'#f5d39a');}
    });
    // Small dashed boundary, like a garden railway.
    ctx.setLineDash([4,7]);ctx.strokeStyle='rgba(130,167,126,.32)';ctx.lineWidth=1;
    ctx.strokeRect(25,75,1050,450);ctx.setLineDash([]);
  }
  function drawPassenger(p, time) {
    const x=p.x*S+25,y=p.y*S+25;
    const bob=Math.sin(time/350)*1.7;
    // A platform tile beneath the waiting passenger.
    ellipse(x,y+19,22,8,'rgba(81,115,75,.12)');
    roundRect(x-23,y-20,46,41,9,'#f8f1d8','#e6d5ae');
    ctx.fillStyle='#e9bc61';
    for(let i=0;i<4;i++)ctx.fillRect(x-19+i*10,y+15,5,3);
    ctx.save();ctx.translate(x,y+bob);
    // A tiny suitcase
    roundRect(10,3,8,11,2,'#d79859');line(12,2,16,2,'#b97d48',1.5);
    // legs, body, head, hair
    line(-4,8,-5,14,'#5a6c62',3); line(4,8,5,14,'#5a6c62',3);
    roundRect(-8,-3,16,13,5,'#d87364');
    line(-7,0,-11,6,'#d68f71',3);line(7,0,10,5,'#d68f71',3);
    ellipse(0,-9,7,7,'#efbc92');
    ctx.fillStyle='#614e44';ctx.beginPath();ctx.arc(0,-10,7,Math.PI,Math.PI*2);ctx.fill();
    ctx.restore();
    // Floating attention mark.
    ellipse(x+18,y-19,9,9,'#f5bd61');
    ctx.fillStyle='#fffdf0';ctx.font='bold 13px Arial';ctx.textAlign='center';ctx.fillText('!',x+18,y-14);
  }
  function angleFor(i) {
    let d;
    if(i===0) d=direction;
    else {
      const prev=snake[i-1], cur=snake[i];
      let dx=prev.x-cur.x,dy=prev.y-cur.y;
      if(Math.abs(dx)>1)dx=-Math.sign(dx);
      if(Math.abs(dy)>1)dy=-Math.sign(dy);
      d={x:dx,y:dy};
    }
    return Math.atan2(d.y,d.x);
  }
  function drawCar(seg,i) {
    const x=seg.x*S+25,y=seg.y*S+25;
    ctx.save();ctx.translate(x,y);ctx.rotate(angleFor(i));
    ellipse(0,16,23,6,'rgba(67,100,65,.15)');
    // couplers
    roundRect(-27,-3,9,6,2,'#6f7770');
    if(i>0) roundRect(18,-3,9,6,2,'#6f7770');
    // wheels
    roundRect(-16,-18,9,5,2,'#53675b');roundRect(7,-18,9,5,2,'#53675b');
    roundRect(-16,13,9,5,2,'#53675b');roundRect(7,13,9,5,2,'#53675b');
    if(i===0) {
      roundRect(-21,-14,42,28,7,'#387e60');
      roundRect(-19,-12,37,24,6,'#43916d');
      roundRect(-19,-11,15,22,4,'#2f7155');
      roundRect(-15,-8,9,16,2,'#bfe0d0');
      roundRect(-1,-10,15,20,5,'#5ca17a');
      roundRect(6,-7,9,14,3,'#477b60');
      roundRect(16,-12,6,24,3,'#315f4d');
      roundRect(19,-9,4,5,2,'#f5d580');roundRect(19,4,4,5,2,'#f5d580');
      roundRect(-21,-12,3,24,1,'#e5b860');
      ellipse(3,0,4,4,'#315f4d');
    } else {
      const col=colors[(i-1)%colors.length];
      roundRect(-21,-14,42,28,6,col);
      roundRect(-18,-11,36,22,4,'rgba(255,255,255,.13)');
      roundRect(-14,-8,10,16,3,'#dcefe5');
      roundRect(4,-8,10,16,3,'#dcefe5');
      line(-18,10,18,10,'rgba(106,83,64,.18)',1);
      // passengers peeking through the windows
      if(i%2===1){ellipse(-9,-1,3,3,'#e7b59a');ellipse(9,1,3,3,'#d8a881');}
      else {ellipse(-9,1,3,3,'#d5a37d');ellipse(9,-1,3,3,'#edc5a0');}
    }
    ctx.restore();
  }
  function render(time) {
    drawBackground();
    drawPassenger(passenger,time);
    for(let i=snake.length-1;i>=0;i--) drawCar(snake[i],i);
    if(mode==='demo') {
      roundRect(20,74,75,24,12,'rgba(255,255,249,.83)');
      ellipse(33,86,3.5,3.5,'#85b98b');
      ctx.fillStyle='#77947f';ctx.textAlign='left';ctx.font='bold 9px Trebuchet MS';ctx.fillText('DEMO RIDE',42,89);
    }
  }
  function occupied(x,y,ignoreTail=false) {
    return snake.slice(0,ignoreTail?-1:undefined).some(s=>s.x===x&&s.y===y);
  }
  function spawnPassenger() {
    if(mode==='demo') {
      for(let n=0;n<demoTargets.length;n++) {
        targetIndex=(targetIndex+1)%demoTargets.length;
        const p=demoTargets[targetIndex];
        if(!occupied(p.x,p.y)){passenger={...p};return;}
      }
    }
    const options=[];
    for(let y=1;y<ROWS;y++) for(let x=1;x<COLS-1;x++) if(!occupied(x,y)) options.push({x,y});
    if(options.length) passenger=options[Math.floor(Math.random()*options.length)];
  }
  function playTone() {
    if(!soundOn)return;
    try {
      audioCtx=audioCtx||new (window.AudioContext||window.webkitAudioContext)();
      const osc=audioCtx.createOscillator(), gain=audioCtx.createGain();
      osc.type='sine';osc.frequency.setValueAtTime(660,audioCtx.currentTime);
      osc.frequency.exponentialRampToValueAtTime(880,audioCtx.currentTime+.1);
      gain.gain.setValueAtTime(.045,audioCtx.currentTime);gain.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+.18);
      osc.connect(gain);gain.connect(audioCtx.destination);osc.start();osc.stop(audioCtx.currentTime+.18);
    } catch(e) {}
  }
  function autoDirection() {
    const head=snake[0];
    const candidates=Object.values(dirs).filter(d=>!(d.x===-direction.x&&d.y===-direction.y));
    candidates.sort((a,b)=>{
      const dist=d=>Math.abs(head.x+d.x-passenger.x)+Math.abs(head.y+d.y-passenger.y);
      return dist(a)-dist(b);
    });
    for(const d of candidates) {
      const nx=head.x+d.x,ny=head.y+d.y;
      if(nx>=0&&nx<COLS&&ny>=1&&ny<ROWS&&!occupied(nx,ny,true)) return d;
    }
    return direction;
  }
  function step() {
    if(mode==='over') return;
    direction = mode==='demo' ? autoDirection() : queued;
    const head={x:snake[0].x+direction.x,y:snake[0].y+direction.y};
    const eating=head.x===passenger.x&&head.y===passenger.y;
    if(head.x<0||head.x>=COLS||head.y<1||head.y>=ROWS||occupied(head.x,head.y,!eating)) {
      if(mode==='demo'){reset(true);return;}
      mode='over'; overlay.style.display='flex';title.textContent='End of the line!';
      sub.textContent='Click or press a key to ride again';return;
    }
    snake.unshift(head);
    if(eating){
      score++;scoreEl.textContent=score;
      if(mode==='playing'&&score>best){best=score;bestEl.textContent=best;}
      playTone();spawnPassenger();
    } else snake.pop();
    tickCount++;
  }
  function frame(time) {
    if(time-last>(mode==='demo'?220:180)){step();last=time;}
    render(time);requestAnimationFrame(frame);
  }
  function start() { if(mode!=='playing') reset(false); }
  function setDirection(d) {
    if(mode!=='playing') start();
    if(d.x!==-direction.x||d.y!==-direction.y) queued=d;
  }
  document.addEventListener('keydown',e=>{
    const key=e.key.toLowerCase();
    const map={arrowleft:'left',a:'left',arrowright:'right',d:'right',arrowup:'up',w:'up',arrowdown:'down',s:'down'};
    if(map[key]){e.preventDefault();setDirection(dirs[map[key]]);}
    else if(mode!=='playing') start();
  });
  document.getElementById('game').addEventListener('click',()=>{if(mode!=='playing') start();});
  document.getElementById('restart').addEventListener('click',()=>reset(false));
  document.querySelectorAll('[data-dir]').forEach(btn=>btn.addEventListener('click',()=>setDirection(dirs[btn.dataset.dir])));
  document.getElementById('sound').addEventListener('click',e=>{
    soundOn=!soundOn;e.currentTarget.textContent=soundOn?'♫':'♪';
    e.currentTarget.style.color=soundOn?'#2e805b':'#668176';
    e.currentTarget.setAttribute('aria-label',soundOn?'Mute sound':'Enable sound');
    if(soundOn)playTone();
  });
  reset(true); requestAnimationFrame(frame);
})();
</script>
</body>
</html>