Models 44

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 11 s
Cost
$0.032
Output tokens
6,377
Spent thinking
0 tokens
Speed
90.8 tok/s
Console errors
0
First token after
1.2 s
First visible text after
1.2 s
Prompt tokens
493
Code size
19.1 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
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
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 · 413 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake Garden</title>
<style>
  :root {
    color-scheme: dark;
    --bg: #0c1412;
    --panel: #111f1b;
    --line: #263b32;
    --cream: #edf3dc;
    --muted: #8ba397;
    --lime: #c8f76a;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; min-height: 100%; }
  body {
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 24px;
    background:
      radial-gradient(ellipse at 50% 20%, rgba(61, 104, 63, .17), transparent 48%),
      radial-gradient(ellipse at 8% 100%, rgba(46, 87, 65, .12), transparent 40%),
      var(--bg);
    color: var(--cream);
    font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  }
  .app { width: min(100%, 1050px); }
  .topbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 20px;
    margin-bottom: 22px;
  }
  .brand { display: flex; align-items: center; gap: 11px; }
  .brand-icon {
    width: 35px; height: 35px; border: 1px solid #52734b;
    border-radius: 11px; display: grid; place-items: center;
    background: #203a28; color: var(--lime);
  }
  .brand-icon svg { width: 20px; height: 20px; }
  .brand-name { font-size: 13px; font-weight: 800; letter-spacing: .16em; }
  .top-note {
    color: #839c8c; font-size: 10px; font-weight: 750;
    letter-spacing: .15em; text-transform: uppercase;
    display: flex; align-items: center; gap: 8px;
  }
  .top-note::before {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: #aee56a; box-shadow: 0 0 10px #9be65c;
  }
  .layout { display: grid; grid-template-columns: minmax(0, 1fr) 270px; gap: 22px; align-items: stretch; }
  .game-card {
    border: 1px solid #31473a;
    background: #14241c;
    padding: 12px;
    border-radius: 20px;
    box-shadow: 0 24px 70px #0005, inset 0 1px #ffffff08;
  }
  .board-wrap { position: relative; width: 100%; aspect-ratio: 1; overflow: hidden; border-radius: 12px; background: #1b3022; }
  canvas { display: block; width: 100%; height: 100%; touch-action: none; }
  .board-label {
    position: absolute; left: 18px; top: 15px;
    color: #8da98a; font-size: 9px; font-weight: 800; letter-spacing: .15em;
    pointer-events: none; opacity: .8;
  }
  .board-label.right { left: auto; right: 18px; }
  .overlay {
    position: absolute; inset: 0; display: flex; align-items: flex-end; justify-content: center;
    padding: 24px; pointer-events: none;
  }
  .overlay.center { align-items: center; background: rgba(7, 16, 11, .68); backdrop-filter: blur(3px); }
  .message {
    max-width: 100%; text-align: center; padding: 14px 20px;
    border: 1px solid #6c885b; border-radius: 12px;
    background: rgba(17, 32, 22, .91); box-shadow: 0 10px 35px #0005;
  }
  .message strong { display: block; color: #e9f6d7; font-size: 14px; letter-spacing: .01em; }
  .message span { display: block; color: #a6bd9e; font-size: 11px; margin-top: 5px; }
  .center .message { padding: 26px 38px; min-width: 220px; }
  .center .message strong { font-size: 24px; }
  .center .message span { margin-top: 10px; font-size: 12px; }
  .overlay.hidden { display: none; }
  .sidebar { display: flex; flex-direction: column; gap: 14px; }
  .intro { padding: 9px 2px 8px; }
  .eyebrow { color: #a4c783; font-size: 10px; font-weight: 800; letter-spacing: .18em; text-transform: uppercase; }
  h1 { margin: 11px 0 8px; font-size: clamp(31px, 3.2vw, 43px); line-height: 1; letter-spacing: -.055em; font-weight: 750; }
  .intro p { color: #97ab9d; font-size: 13px; line-height: 1.6; margin: 0; max-width: 240px; }
  .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  .stat, .info {
    border: 1px solid var(--line); background: rgba(19, 34, 27, .85);
    border-radius: 14px;
  }
  .stat { padding: 16px 14px 14px; min-width: 0; }
  .stat-label { color: #90a79a; font-size: 9px; letter-spacing: .13em; font-weight: 800; }
  .stat-value { color: #f0f7e5; font-size: 29px; font-weight: 750; line-height: 1; margin-top: 12px; font-variant-numeric: tabular-nums; letter-spacing: -.04em; }
  .stat.best .stat-value { color: var(--lime); }
  .info { padding: 18px; }
  .info-title { font-size: 10px; font-weight: 800; letter-spacing: .13em; color: #b5c9b7; text-transform: uppercase; margin-bottom: 15px; }
  .control-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; color: #a5b8aa; font-size: 12px; }
  .control-row + .control-row { margin-top: 12px; }
  .keys { display: flex; align-items: center; gap: 4px; }
  kbd {
    min-width: 24px; height: 23px; display: inline-flex; justify-content: center; align-items: center;
    background: #253a2d; border: 1px solid #3b5140; border-bottom-width: 2px;
    border-radius: 5px; font: 700 10px ui-sans-serif, system-ui, sans-serif; color: #d2e1ce;
  }
  .or { font-size: 10px; color: #738d7b; padding: 0 2px; }
  .tip { display: flex; gap: 10px; align-items: flex-start; padding: 2px 3px; color: #819a88; font-size: 11px; line-height: 1.55; }
  .tip-dot { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: #f2ab6b; margin-top: 4px; box-shadow: 0 0 10px #f2ab6b66; }
  .footer { display: flex; justify-content: space-between; color: #667f6d; font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; margin-top: 18px; }
  .touch-controls { display: none; justify-content: center; gap: 8px; margin-top: 12px; }
  .touch-controls button {
    width: 46px; height: 40px; border: 1px solid #49634c; background: #20382a;
    border-radius: 9px; color: #e3f2d8; font-size: 18px; cursor: pointer; touch-action: manipulation;
  }
  @media (max-width: 760px) {
    body { align-items: flex-start; padding: 20px 16px 28px; }
    .app { max-width: 560px; }
    .topbar { margin-bottom: 16px; }
    .layout { grid-template-columns: 1fr; gap: 14px; }
    .sidebar { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
    .intro { grid-column: 1 / -1; padding: 3px 1px; }
    h1 { font-size: 30px; margin: 7px 0 5px; }
    .intro p { max-width: none; }
    .stats { grid-column: 1 / -1; }
    .info { padding: 14px; }
    .tip { align-self: center; }
    .game-card { padding: 8px; border-radius: 16px; }
    .board-wrap { border-radius: 10px; }
    .footer { margin-top: 14px; }
    .touch-controls { display: flex; }
  }
  @media (max-width: 430px) {
    .top-note { font-size: 9px; }
    .board-label { font-size: 8px; top: 11px; left: 12px; }
    .board-label.right { right: 12px; }
    .overlay { padding: 13px; }
    .message { padding: 10px 13px; }
    .message strong { font-size: 12px; }
    .message span { font-size: 10px; }
  }
</style>
</head>
<body>
<main class="app">
  <header class="topbar">
    <div class="brand">
      <div class="brand-icon" aria-hidden="true">
        <svg viewBox="0 0 24 24" fill="none"><path d="M6 17.5h7a4 4 0 0 0 0-8H9a3 3 0 0 1 0-6h7" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/><circle cx="17.5" cy="3.5" r="2" fill="currentColor"/><circle cx="6" cy="17.5" r="2" fill="currentColor"/></svg>
      </div>
      <span class="brand-name">SNAKE GARDEN</span>
    </div>
    <div class="top-note">A little arcade escape</div>
  </header>

  <div class="layout">
    <section class="game-card" aria-label="Snake game">
      <div class="board-wrap" id="boardWrap">
        <canvas id="game" width="672" height="672" aria-label="Snake game board"></canvas>
        <div class="board-label">THE GARDEN</div>
        <div class="board-label right">22 × 22</div>
        <div class="overlay" id="overlay">
          <div class="message"><strong>Click or press a key to start</strong><span>Demo is playing · your turn awaits</span></div>
        </div>
      </div>
      <div class="touch-controls" aria-label="Touch controls">
        <button type="button" data-dir="left" aria-label="Move left">←</button>
        <button type="button" data-dir="up" aria-label="Move up">↑</button>
        <button type="button" data-dir="down" aria-label="Move down">↓</button>
        <button type="button" data-dir="right" aria-label="Move right">→</button>
      </div>
    </section>

    <aside class="sidebar">
      <div class="intro">
        <div class="eyebrow">A classic, reimagined</div>
        <h1>Eat. Grow.<br>Keep going.</h1>
        <p>Collect fruit, find your rhythm, and see how long you can last.</p>
      </div>
      <div class="stats">
        <div class="stat"><div class="stat-label">SCORE</div><div class="stat-value" id="score">0</div></div>
        <div class="stat best"><div class="stat-label">SESSION BEST</div><div class="stat-value" id="best">0</div></div>
      </div>
      <div class="info">
        <div class="info-title">How to play</div>
        <div class="control-row"><span>Move</span><div class="keys"><kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd><span class="or">or</span><kbd>WASD</kbd></div></div>
        <div class="control-row"><span>Pause</span><div class="keys"><kbd>P</kbd></div></div>
        <div class="control-row"><span>Restart</span><div class="keys"><kbd>SPACE</kbd></div></div>
      </div>
      <div class="tip"><span class="tip-dot"></span><span>Every fruit is worth 10 points. Careful not to bump into yourself or the edge!</span></div>
    </aside>
  </div>
  <div class="footer"><span>Take your time, find your flow</span><span>Made for a little break ✳</span></div>
</main>
<script>
(() => {
  'use strict';
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');
  const scoreEl = document.getElementById('score');
  const bestEl = document.getElementById('best');
  const N = 22, SIZE = canvas.width, CELL = SIZE / N, STEP = 125;
  const dirs = {up:{x:0,y:-1}, down:{x:0,y:1}, left:{x:-1,y:0}, right:{x:1,y:0}};
  let snake, previous, direction, queue, food, score, best = 0, particles = [];
  let state = 'demo', lastStep = 0, lastFrame = 0, clock = 0, demoTarget = 0;

  function reset() {
    snake = [{x:10,y:11},{x:9,y:11},{x:8,y:11},{x:7,y:11},{x:6,y:11}];
    previous = snake.map(p => ({...p}));
    direction = {...dirs.right}; queue = []; score = 0; particles = [];
    food = {x:15,y:11}; lastStep = performance.now(); demoTarget = 0;
    updateScores();
  }
  function updateScores() {
    scoreEl.textContent = score;
    bestEl.textContent = best;
  }
  function setOverlay(kind) {
    overlay.className = 'overlay' + (kind === 'none' ? ' hidden' : '') + (kind === 'pause' || kind === 'over' || kind === 'win' ? ' center' : '');
    if (kind === 'demo') overlay.innerHTML = '<div class="message"><strong>Click or press a key to start</strong><span>Demo is playing · your turn awaits</span></div>';
    if (kind === 'pause') overlay.innerHTML = '<div class="message"><strong>Taking a breather</strong><span>Press P to resume</span></div>';
    if (kind === 'over') overlay.innerHTML = '<div class="message"><strong>Game over</strong><span>Score: ' + score + ' · Press Space or Enter to restart</span></div>';
    if (kind === 'win') overlay.innerHTML = '<div class="message"><strong>Garden complete!</strong><span>Score: ' + score + ' · Press Space or Enter to play again</span></div>';
  }
  function start() {
    state = 'playing'; reset(); setOverlay('none');
  }
  function placeFood() {
    const free = [];
    for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
      if (!snake.some(p => p.x === x && p.y === y)) free.push({x,y});
    }
    if (!free.length) {
      if (state === 'demo') { reset(); return; }
      state = 'win'; setOverlay('win'); return;
    }
    food = free[Math.floor(Math.random() * free.length)];
  }
  function enqueue(d) {
    const last = queue.length ? queue[queue.length - 1] : direction;
    if (d.x === -last.x && d.y === -last.y) return;
    if (d.x === last.x && d.y === last.y) return;
    if (queue.length < 4) queue.push(d);
  }
  function burst(x, y) {
    for (let i = 0; i < 15; i++) {
      const a = Math.random() * Math.PI * 2, speed = 35 + Math.random() * 105;
      particles.push({x:(x+.5)*CELL, y:(y+.5)*CELL, vx:Math.cos(a)*speed, vy:Math.sin(a)*speed,
        life:.35+Math.random()*.35, max:.7, r:2+Math.random()*3, color:Math.random()<.5?'#ffd18a':'#f69867'});
    }
  }
  function demoDirection() {
    // A breadth-first path to the fruit keeps the attract mode moving purposefully.
    const start = snake[0], blocked = new Set(snake.slice(1, -1).map(p => p.x + ',' + p.y));
    const options = [dirs.up, dirs.right, dirs.down, dirs.left];
    const q = [{x:start.x,y:start.y,first:null}], seen = new Set([start.x+','+start.y]);
    for (let i=0; i<q.length; i++) {
      const p=q[i];
      if (p.x===food.x && p.y===food.y && p.first) return p.first;
      for (const d of options) {
        if (!p.first && d.x === -direction.x && d.y === -direction.y) continue;
        const x=p.x+d.x,y=p.y+d.y,key=x+','+y;
        if(x<0||x>=N||y<0||y>=N||blocked.has(key)||seen.has(key)) continue;
        seen.add(key); q.push({x,y,first:p.first||d});
      }
    }
    const safe = options.filter(d => {
      const x=start.x+d.x,y=start.y+d.y;
      return !(d.x===-direction.x && d.y===-direction.y) && x>=0 && x<N && y>=0 && y<N && !blocked.has(x+','+y);
    });
    return safe[demoTarget++ % Math.max(1,safe.length)] || direction;
  }
  function step() {
    if (state === 'demo') direction = demoDirection();
    else if (queue.length) direction = queue.shift();
    const head = {x:snake[0].x+direction.x, y:snake[0].y+direction.y};
    const eating = head.x === food.x && head.y === food.y;
    const hit = head.x < 0 || head.x >= N || head.y < 0 || head.y >= N ||
      snake.slice(0, eating ? snake.length : -1).some(p => p.x === head.x && p.y === head.y);
    if (hit) {
      if (state === 'demo') { reset(); return; }
      state = 'over'; previous = snake.map(p => ({...p})); setOverlay('over'); return;
    }
    previous = snake.map(p => ({...p}));
    snake.unshift(head);
    if (eating) {
      burst(food.x,food.y);
      if (state === 'playing') {
        score += 10; best = Math.max(best,score); updateScores();
      }
      placeFood();
    } else snake.pop();
  }
  function rounded(x,y,w,h,r) {
    ctx.beginPath(); ctx.roundRect(x,y,w,h,r); ctx.fill();
  }
  function drawBackground() {
    ctx.fillStyle = '#1a3021'; ctx.fillRect(0,0,SIZE,SIZE);
    for(let y=0;y<N;y++) for(let x=0;x<N;x++) {
      ctx.fillStyle = (x+y)%2===0 ? '#203b28' : '#1d3525';
      ctx.fillRect(x*CELL,y*CELL,CELL+.5,CELL+.5);
    }
    ctx.strokeStyle = 'rgba(192,226,166,.035)'; ctx.lineWidth = 1;
    ctx.strokeRect(.5,.5,SIZE-1,SIZE-1);
  }
  function drawFood() {
    const x=(food.x+.5)*CELL, y=(food.y+.5)*CELL;
    const pulse=1+Math.sin(clock*3.4)*.05;
    const glow=ctx.createRadialGradient(x,y,1,x,y,CELL*.8);
    glow.addColorStop(0,'rgba(255,178,105,.25)'); glow.addColorStop(1,'rgba(255,178,105,0)');
    ctx.fillStyle=glow; ctx.beginPath(); ctx.arc(x,y,CELL*.8,0,Math.PI*2); ctx.fill();
    ctx.save(); ctx.translate(x,y); ctx.scale(pulse,pulse);
    ctx.fillStyle='#f6a467'; ctx.beginPath(); ctx.arc(0,2,CELL*.29,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#ffd39d'; ctx.beginPath(); ctx.arc(-CELL*.08,-CELL*.02,CELL*.075,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle='#a5d975'; ctx.lineWidth=2; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(0,-CELL*.2); ctx.quadraticCurveTo(1,-CELL*.36,CELL*.16,-CELL*.38); ctx.stroke();
    ctx.fillStyle='#b8e984'; ctx.beginPath(); ctx.ellipse(CELL*.16,-CELL*.32,CELL*.12,CELL*.06,-.55,0,Math.PI*2); ctx.fill();
    ctx.restore();
  }
  function drawSnake(alpha) {
    const points=snake.map((p,i) => {
      const from=previous[i] || previous[previous.length-1] || p;
      return {x:(from.x+(p.x-from.x)*alpha+.5)*CELL, y:(from.y+(p.y-from.y)*alpha+.5)*CELL};
    });
    // Thick connected strokes produce a soft, continuous body while the cells interpolate.
    ctx.lineCap='round'; ctx.lineJoin='round';
    if(points.length>1) {
      ctx.strokeStyle='rgba(4,15,8,.22)'; ctx.lineWidth=CELL*.78;
      ctx.beginPath(); ctx.moveTo(points[0].x,points[0].y+2);
      for(let i=1;i<points.length;i++) ctx.lineTo(points[i].x,points[i].y+2);
      ctx.stroke();
      ctx.strokeStyle='#a7dd67'; ctx.lineWidth=CELL*.70;
      ctx.beginPath(); ctx.moveTo(points[0].x,points[0].y);
      for(let i=1;i<points.length;i++) ctx.lineTo(points[i].x,points[i].y);
      ctx.stroke();
    }
    for(let i=points.length-1;i>=1;i--) {
      const p=points[i]; ctx.fillStyle= i%2 ? '#b4e876' : '#a9e06d';
      ctx.beginPath(); ctx.arc(p.x,p.y,CELL*.325,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='rgba(232,255,198,.16)'; ctx.beginPath(); ctx.arc(p.x-CELL*.07,p.y-CELL*.09,CELL*.09,0,Math.PI*2); ctx.fill();
    }
    const h=points[0], dx=direction.x, dy=direction.y, px=-dy, py=dx;
    ctx.fillStyle='#d2f58a'; ctx.beginPath(); ctx.arc(h.x,h.y,CELL*.40,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#223b29';
    for(const side of [-1,1]) {
      const ex=h.x+dx*CELL*.15+px*side*CELL*.16, ey=h.y+dy*CELL*.15+py*side*CELL*.16;
      ctx.beginPath(); ctx.arc(ex,ey,CELL*.060,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#e9ffe0'; ctx.beginPath(); ctx.arc(ex-CELL*.015,ey-CELL*.015,CELL*.018,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#223b29';
    }
  }
  function drawParticles(dt) {
    for (const p of particles) {
      p.life-=dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=95*dt;
      ctx.globalAlpha=Math.max(0,p.life/p.max); ctx.fillStyle=p.color;
      ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha=1; particles=particles.filter(p=>p.life>0);
  }
  function frame(now) {
    const dt=Math.min((now-lastFrame)/1000 || 0,.05); lastFrame=now; clock+=dt;
    if (state==='playing'||state==='demo') {
      if(now-lastStep>=STEP) {
        step(); lastStep=now;
      }
    }
    drawBackground(); drawFood();
    const alpha=(state==='playing'||state==='demo') ? Math.min(1,(now-lastStep)/STEP) : 1;
    drawSnake(alpha); drawParticles(dt);
    requestAnimationFrame(frame);
  }
  const keyDirs = {ArrowUp:dirs.up,ArrowDown:dirs.down,ArrowLeft:dirs.left,ArrowRight:dirs.right,
    w:dirs.up,s:dirs.down,a:dirs.left,d:dirs.right};
  document.addEventListener('keydown', e => {
    const key=e.key.length===1 ? e.key.toLowerCase() : e.key;
    if (keyDirs[key] || [' ','Enter','p'].includes(key)) e.preventDefault();
    if (state==='demo') {
      start();
      if(keyDirs[key]) enqueue(keyDirs[key]);
      return;
    }
    if (state==='over'||state==='win') {
      if(key===' '||key==='Enter') start();
      return;
    }
    if(key==='p') {
      if(state==='playing') { state='paused'; setOverlay('pause'); }
      else if(state==='paused') { state='playing'; lastStep=performance.now(); previous=snake.map(p=>({...p})); setOverlay('none'); }
      return;
    }
    if(state==='playing' && keyDirs[key]) enqueue(keyDirs[key]);
  });
  document.getElementById('boardWrap').addEventListener('click', () => {
    if(state==='demo') start();
  });
  document.querySelectorAll('[data-dir]').forEach(button => {
    button.addEventListener('click', () => {
      if(state==='demo'||state==='over'||state==='win') start();
      if(state==='playing') enqueue(dirs[button.dataset.dir]);
    });
  });
  reset(); setOverlay('demo'); requestAnimationFrame(frame);
})();
</script>
</body>
</html>