Models 44

Muse Ember

Open in new window

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

How it went

Total time
16.0 s
Cost
$0.0052
Output tokens
10,295
Spent thinking
2,249 tokens
Speed
712.7 tok/s
Console errors
0
First token after
1.6 s
First visible text after
6.3 s
Prompt tokens
510
Code size
24.3 KB
Finish reason
stop
Served by
Meta
Model id
meta/muse-ember
Reasoning sent
{"effort":"medium"}
Max output
943,718 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 · 509 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>Neon Snake</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
  html,body{height:100%}
  body{
    font-family: ui-rounded, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background:
      radial-gradient(900px 500px at 20% -10%, rgba(52,211,153,.22), transparent 60%),
      radial-gradient(800px 500px at 90% 110%, rgba(244,63,94,.18), transparent 60%),
      radial-gradient(600px 400px at 80% 10%, rgba(56,189,248,.14), transparent 60%),
      linear-gradient(160deg,#070b18 0%,#0b1226 55%,#0a0f1f 100%);
    color:#e8eefc;
    min-height:100vh;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    padding:16px 12px 14px;
    overflow-x:hidden;
  }
  .wrap{width:min(94vw,560px); display:flex; flex-direction:column; align-items:center; gap:12px}
  .top{width:100%; display:flex; align-items:end; justify-content:space-between; gap:12px}
  .title{line-height:1}
  .title h1{font-size:clamp(24px,5vw,34px); letter-spacing:.14em; font-weight:900; color:#fff}
  .title h1 span{color:#34d399; text-shadow:0 0 18px rgba(52,211,153,.8)}
  .title p{margin-top:5px; font-size:11px; letter-spacing:.22em; text-transform:uppercase; color:#8ea0c2; font-weight:700}
  .btns{display:flex; gap:8px}
  button{
    border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.07); color:#e8eefc;
    padding:9px 13px; border-radius:999px; font-weight:800; font-size:13px; cursor:pointer;
    transition:.18s; letter-spacing:.04em; backdrop-filter:blur(6px)
  }
  button:hover{background:rgba(52,211,153,.22); border-color:rgba(52,211,153,.55); transform:translateY(-1px)}
  button:active{transform:translateY(0) scale(.97)}
  .hud{width:100%; display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px}
  .card{
    background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.09);
    border-radius:16px; padding:10px 14px; text-align:center; backdrop-filter:blur(8px);
    box-shadow:0 10px 24px rgba(0,0,0,.25)
  }
  .card label{display:block; font-size:10px; letter-spacing:.22em; text-transform:uppercase; color:#93a5c8; font-weight:800}
  .card b{display:block; font-size:26px; line-height:1.2; color:#fff; font-variant-numeric:tabular-nums}
  .card.score b{color:#34d399; text-shadow:0 0 14px rgba(52,211,153,.55)}
  .card.best b{color:#fbbf24; text-shadow:0 0 14px rgba(251,191,36,.45)}
  .stage{
    position:relative; width:100%; aspect-ratio:1/1; border-radius:22px;
    padding:10px; background:linear-gradient(160deg, rgba(52,211,153,.35), rgba(56,189,248,.18) 45%, rgba(244,63,94,.28));
    box-shadow:0 24px 60px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.08), 0 0 42px rgba(52,211,153,.18);
  }
  .inner{position:relative; width:100%; height:100%; border-radius:15px; overflow:hidden; background:#0d1526}
  canvas{display:block; width:100%; height:100%; touch-action:none}
  .badge{
    position:absolute; top:12px; left:12px; z-index:5; pointer-events:none;
    background:rgba(0,0,0,.5); border:1px solid rgba(255,255,255,.16); color:#a7f3d0;
    font-size:10px; letter-spacing:.18em; font-weight:900; padding:6px 9px; border-radius:999px;
    display:none; backdrop-filter:blur(6px)
  }
  .badge.on{display:block}
  .overlay{
    position:absolute; inset:0; z-index:6; display:flex; flex-direction:column; align-items:center; justify-content:center;
    text-align:center; padding:22px; background:radial-gradient(circle at center, rgba(7,11,24,.52), rgba(7,11,24,.82));
    backdrop-filter:blur(3px); transition:opacity .25s; cursor:pointer
  }
  .overlay.hidden{opacity:0; pointer-events:none}
  .overlay .emoji{font-size:52px; line-height:1; filter:drop-shadow(0 8px 18px rgba(0,0,0,.5)); animation:bob 1.6s ease-in-out infinite}
  @keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
  .overlay h2{margin-top:12px; font-size:clamp(24px,6vw,34px); letter-spacing:.08em; color:#fff}
  .overlay h2 em{color:#34d399; font-style:normal; text-shadow:0 0 16px rgba(52,211,153,.7)}
  .overlay p{margin-top:8px; color:#b9c7e2; font-size:14px; line-height:1.5; max-width:340px}
  .overlay .bigscore{margin-top:12px; font-size:42px; font-weight:900; color:#fbbf24; text-shadow:0 0 18px rgba(251,191,36,.5)}
  .overlay .newbest{display:none; margin-top:8px; background:#fbbf24; color:#1a1200; font-weight:900; font-size:11px; letter-spacing:.16em; padding:6px 10px; border-radius:999px}
  .overlay .newbest.on{display:inline-block}
  .cta{margin-top:18px; background:#34d399; color:#052e1d; border:none; font-size:14px; padding:12px 20px; border-radius:999px; font-weight:900; letter-spacing:.06em; box-shadow:0 10px 26px rgba(52,211,153,.38); animation:pulse 1.8s infinite}
  @keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
  .hint{margin-top:10px; font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:#7f93b8; font-weight:800}
  .hint kbd{background:rgba(255,255,255,.1); border:1px solid rgba(255,255,255,.18); border-bottom-width:2px; padding:2px 6px; border-radius:6px; color:#fff; font-family:inherit}
  .footer{width:100%; display:flex; justify-content:center; gap:14px; flex-wrap:wrap; color:#8ea0c2; font-size:12px; font-weight:700}
  .footer span{display:flex; align-items:center; gap:6px}
  .dpad{display:none; gap:8px; margin-top:2px}
  .dpad button{width:54px;height:54px;border-radius:16px;font-size:20px;padding:0}
  @media (pointer:coarse), (max-width:560px){ .dpad{display:grid; grid-template-columns:repeat(3,54px); grid-template-rows:repeat(2,54px)} .dpad .up{grid-column:2} .dpad .left{grid-column:1;grid-row:2} .dpad .down{grid-column:2;grid-row:2} .dpad .right{grid-column:3;grid-row:2} }
  @media (max-height:760px){ body{justify-content:flex-start} .card b{font-size:22px} .card{padding:7px 10px} }
</style>
</head>
<body>
<div class="wrap">
  <div class="top">
    <div class="title"><h1>NEON <span>SNAKE</span></h1><p>Vanilla JS • Canvas</p></div>
    <div class="btns"><button id="pauseBtn" type="button">⏸ Pause</button><button id="restartBtn" type="button">↻ Restart</button></div>
  </div>

  <div class="hud">
    <div class="card score"><label>Score</label><b id="scoreEl">0</b></div>
    <div class="card best"><label>Best</label><b id="bestEl">0</b></div>
    <div class="card"><label>Length</label><b id="lenEl">4</b></div>
  </div>

  <div class="stage">
    <div class="inner" id="inner">
      <canvas id="game" width="560" height="560" aria-label="Snake game board"></canvas>
      <div class="badge" id="demoBadge">● DEMO — AI PLAYING</div>

      <div class="overlay" id="overlay">
        <div class="emoji" id="ovEmoji">🐍</div>
        <h2 id="ovTitle">NEON <em>SNAKE</em></h2>
        <p id="ovText">Eat the glowing apples to grow. Don't hit walls or yourself!</p>
        <div class="bigscore" id="ovScore" style="display:none">0</div>
        <div class="newbest" id="newBest">★ NEW BEST! ★</div>
        <button class="cta" id="ctaBtn" type="button">▶ Click or press a key to start</button>
        <div class="hint"><kbd>←↑↓→</kbd> / <kbd>WASD</kbd> move &nbsp; <kbd>P</kbd> pause &nbsp; <kbd>Space</kbd> restart</div>
      </div>
    </div>
  </div>

  <div class="dpad" aria-label="On-screen controls">
    <button class="up" data-dir="up" type="button">▲</button>
    <button class="left" data-dir="left" type="button">◀</button>
    <button class="down" data-dir="down" type="button">▼</button>
    <button class="right" data-dir="right" type="button">▶</button>
  </div>

  <div class="footer">
    <span>🍎 +10 points</span><span>⚡ Speeds up as you grow</span><span>✨ Particle burst on eat</span>
  </div>
</div>

<script>
(() => {
  const COLS = 20, ROWS = 20;
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const scoreEl = document.getElementById('scoreEl');
  const bestEl = document.getElementById('bestEl');
  const lenEl = document.getElementById('lenEl');
  const overlay = document.getElementById('overlay');
  const ovEmoji = document.getElementById('ovEmoji');
  const ovTitle = document.getElementById('ovTitle');
  const ovText = document.getElementById('ovText');
  const ovScore = document.getElementById('ovScore');
  const newBest = document.getElementById('newBest');
  const ctaBtn = document.getElementById('ctaBtn');
  const demoBadge = document.getElementById('demoBadge');
  const pauseBtn = document.getElementById('pauseBtn');

  const DIRS = { up:{x:0,y:-1}, down:{x:0,y:1}, left:{x:-1,y:0}, right:{x:1,y:0} };
  const DIR_LIST = [DIRS.up, DIRS.right, DIRS.down, DIRS.left];

  let snake, prevSnake, dir, dirQueue, food, score, best = 0, isNewBest = false;
  let state = 'demo'; // demo | playing | paused | over
  let particles = [], floaters = [];
  let lastTick = performance.now(), lastFrame = performance.now();
  let audioCtx = null;

  function resizeCanvas(){
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const rect = canvas.getBoundingClientRect();
    const size = Math.max(280, Math.round(rect.width));
    canvas.width = size * dpr; canvas.height = size * dpr;
    ctx.setTransform(dpr,0,0,dpr,0,0);
    canvas._size = size; canvas._tile = size / COLS;
  }
  window.addEventListener('resize', resizeCanvas);

  function interval(){ return Math.max(72, 145 - Math.floor(score/50)*8 - Math.floor(snake.length/10)*2); }
  function updateHUD(){ scoreEl.textContent = score; bestEl.textContent = best; lenEl.textContent = snake ? snake.length : 4; }

  function spawnFood(){
    const occ = new Set(snake.map(s=>s.x+','+s.y));
    const empty=[];
    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++) if(!occ.has(x+','+y)) empty.push({x,y});
    food = empty.length ? empty[(Math.random()*empty.length)|0] : null;
  }

  function reset(toState){
    const cy = Math.floor(ROWS/2);
    snake = [{x:7,y:cy},{x:6,y:cy},{x:5,y:cy},{x:4,y:cy}];
    prevSnake = snake.map(s=>({...s}));
    dir = DIRS.right; dirQueue = [];
    score = 0; isNewBest = false; particles=[]; floaters=[];
    spawnFood(); updateHUD();
    state = toState;
    lastTick = performance.now();
  }

  function showOverlay(mode){
    if(mode==='hidden'){ overlay.classList.add('hidden'); demoBadge.classList.toggle('on', state==='demo'); return; }
    overlay.classList.remove('hidden');
    demoBadge.classList.toggle('on', state==='demo');
    ovScore.style.display='none'; newBest.classList.remove('on');
    if(mode==='demo'){
      ovEmoji.textContent='🐍'; ovTitle.innerHTML='NEON <em>SNAKE</em>';
      ovText.textContent='Watch the AI play — eat apples, avoid walls and your own tail!';
      ctaBtn.textContent='▶ Click or press a key to play';
    } else if(mode==='paused'){
      ovEmoji.textContent='⏸️'; ovTitle.innerHTML='PAUSED';
      ovText.textContent='Take a breath. Your snake is waiting.';
      ctaBtn.textContent='▶ Resume  (P / Space)';
    } else if(mode==='over'){
      ovEmoji.textContent='💥'; ovTitle.innerHTML='GAME <em>OVER</em>';
      ovText.textContent='You hit a wall or yourself. Great run!';
      ovScore.style.display='block'; ovScore.textContent=score+' pts';
      if(isNewBest && score>0) newBest.classList.add('on');
      ctaBtn.textContent='↻ Play again  (Space / Enter)';
    }
  }

  function startPlaying(initialDir){
    reset('playing');
    if(initialDir) queueDir(initialDir, true);
    showOverlay('hidden'); updateHUD(); ensureAudio();
  }

  function ensureAudio(){
    try{ if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); if(audioCtx.state==='suspended') audioCtx.resume(); }catch(e){}
  }
  function beep(freq, dur=0.07, type='sine', vol=0.06){
    if(!audioCtx || state!=='playing') return;
    try{
      const o=audioCtx.createOscillator(), g=audioCtx.createGain();
      o.type=type; o.frequency.value=freq; g.gain.setValueAtTime(vol, audioCtx.currentTime);
      g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+dur);
      o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime+dur);
    }catch(e){}
  }

  function queueDir(d, force=false){
    if(!d) return;
    const last = dirQueue.length ? dirQueue[dirQueue.length-1] : dir;
    if(d.x === -last.x && d.y === -last.y) return; // never reverse
    if(d.x === last.x && d.y === last.y) return;
    if(dirQueue.length < 3) dirQueue.push(d);
    if(force && dirQueue.length===0) dirQueue.push(d);
  }

  function burst(cx, cy){
    const tile = canvas._tile || 28;
    for(let i=0;i<18;i++){
      const a = Math.random()*Math.PI*2, sp = 40 + Math.random()*150;
      particles.push({x:cx, y:cy, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp - 30, life:1, decay:1.4+Math.random()*1.6, size:2+Math.random()*4, color: Math.random()<0.5 ? '#fbbf24' : (Math.random()<0.5?'#34d399':'#fb7185')});
    }
    floaters.push({x:cx, y:cy-8, text:'+10', life:1});
  }

  function step(){
    // consume queued direction (validated against current dir again)
    while(dirQueue.length){
      const nd = dirQueue.shift();
      if(!(nd.x === -dir.x && nd.y === -dir.y)){ dir = nd; break; }
    }
    prevSnake = snake.map(s=>({...s}));
    const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};

    if(head.x<0||head.x>=COLS||head.y<0||head.y>=ROWS|| snake.some(s=>s.x===head.x&&s.y===head.y)){
      gameOver(); return;
    }
    snake.unshift(head);
    if(food && head.x===food.x && head.y===food.y){
      score += 10;
      if(score>best){ best=score; isNewBest=true; }
      prevSnake.push({...prevSnake[prevSnake.length-1]}); // grow: tail stays
      const tile = canvas._tile||28;
      burst((head.x+0.5)*tile, (head.y+0.5)*tile);
      beep(660,0.08,'sine',0.07); setTimeout(()=>beep(880,0.09,'sine',0.06),55);
      spawnFood(); updateHUD();
    } else {
      snake.pop();
    }
  }

  function gameOver(){
    if(state==='demo'){ reset('demo'); showOverlay('demo'); return; }
    state='over'; if(score>best) best=score;
    updateHUD(); beep(160,0.25,'sawtooth',0.06);
    showOverlay('over');
  }

  // ---------- Demo AI (BFS to food + flood-fill safety) ----------
  function key(x,y){return x+','+y}
  function findPath(start, goal, blocked){
    if(start.x===goal.x && start.y===goal.y) return [];
    const q=[start], seen=new Set([key(start.x,start.y)]), parent=new Map();
    while(q.length){
      const cur=q.shift();
      for(const d of DIR_LIST){
        const nx=cur.x+d.x, ny=cur.y+d.y;
        if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
        const k=key(nx,ny);
        if(seen.has(k) || blocked.has(k)) continue;
        seen.add(k); parent.set(k, cur); 
        if(nx===goal.x&&ny===goal.y){
          const path=[]; let c={x:nx,y:ny};
          while(!(c.x===start.x&&c.y===start.y)){ path.unshift(c); c=parent.get(key(c.x,c.y)); }
          return path;
        }
        q.push({x:nx,y:ny});
      }
    }
    return null;
  }
  function flood(start, blocked){
    const q=[start], seen=new Set([key(start.x,start.y)]); let n=0;
    while(q.length){ const cur=q.shift(); n++;
      for(const d of DIR_LIST){ const nx=cur.x+d.x, ny=cur.y+d.y, k=key(nx,ny);
        if(nx<0||ny<0||nx>=COLS||ny>=ROWS||seen.has(k)||blocked.has(k)) continue;
        seen.add(k); q.push({x:nx,y:ny}); } }
    return n;
  }
  function aiStep(){
    if(!food) return;
    const head=snake[0];
    const blocked=new Set(snake.slice(0,-1).map(s=>key(s.x,s.y)));
    const path=findPath(head, food, blocked);
    let chosen=null;
    if(path && path.length){
      const nxt=path[0];
      const candDir={x:nxt.x-head.x, y:nxt.y-head.y};
      if(!(candDir.x===-dir.x && candDir.y===-dir.y)){
        const simBlocked=new Set(snake.map(s=>key(s.x,s.y))); simBlocked.delete(key(snake[snake.length-1].x,snake[snake.length-1].y)); simBlocked.add(key(head.x,head.y));
        // after move, new head is nxt; flood from there
        const space=flood(nxt, new Set([...snake.slice(1,-1).map(s=>key(s.x,s.y)), key(head.x,head.y)]));
        const eating = nxt.x===food.x && nxt.y===food.y;
        if(space > snake.length || eating) chosen=candDir;
      }
    }
    if(!chosen){
      let bestSpace=-1;
      for(const d of DIR_LIST){
        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||blocked.has(key(nx,ny))) continue;
        const space=flood({x:nx,y:ny}, new Set([...snake.slice(1,-1).map(s=>key(s.x,s.y)), key(head.x,head.y)]));
        if(space>bestSpace){bestSpace=space; chosen=d;}
      }
    }
    if(chosen) queueDir(chosen);
  }

  // ---------- Drawing ----------
  function draw(now){
    const size=canvas._size||560, tile=canvas._tile||28;
    ctx.clearRect(0,0,size,size);
    // board
    const g=ctx.createLinearGradient(0,0,size,size); g.addColorStop(0,'#111c33'); g.addColorStop(1,'#0d1526');
    ctx.fillStyle=g; ctx.fillRect(0,0,size,size);
    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.028)'; ctx.fillRect(x*tile,y*tile,tile,tile); }
    }
    ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.lineWidth=1;
    for(let i=1;i<COLS;i++){ ctx.beginPath(); ctx.moveTo(i*tile,0); ctx.lineTo(i*tile,size); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,i*tile); ctx.lineTo(size,i*tile); ctx.stroke(); }
    ctx.strokeStyle='rgba(52,211,153,0.35)'; ctx.lineWidth=3; ctx.strokeRect(1.5,1.5,size-3,size-3);

    const alpha = state==='paused' ? 0 : Math.min(1, Math.max(0,(now-lastTick)/interval()));
    const interp = snake.map((s,i)=>{
      const p = prevSnake[i] || s;
      return {x: p.x + (s.x-p.x)*alpha, y: p.y + (s.y-p.y)*alpha};
    });

    // food
    if(food){
      const fx=(food.x+0.5)*tile, fy=(food.y+0.5)*tile;
      const pulse=1+Math.sin(now/220)*0.12, r=tile*0.32*pulse;
      ctx.save(); ctx.shadowColor='#f43f5e'; ctx.shadowBlur=18;
      ctx.fillStyle='#f43f5e'; ctx.beginPath(); ctx.arc(fx,fy+r*0.15,r,0,Math.PI*2); ctx.fill();
      ctx.shadowBlur=0;
      ctx.fillStyle='rgba(255,255,255,0.35)'; ctx.beginPath(); ctx.arc(fx-r*0.35,fy-r*0.2,r*0.35,0,Math.PI*2); ctx.fill();
      ctx.strokeStyle='#92400e'; ctx.lineWidth=2.5; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(fx,fy-r*0.7); ctx.quadraticCurveTo(fx+3,fy-r*1.25,fx+8,fy-r*1.3); ctx.stroke();
      ctx.fillStyle='#34d399'; ctx.beginPath(); ctx.ellipse(fx+9,fy-r*1.25,6,3,-0.5,0,Math.PI*2); ctx.fill();
      ctx.restore();
    }

    // particles
    for(const p of particles){
      ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.color;
      ctx.beginPath(); ctx.arc(p.x,p.y,p.size*p.life,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha=1;
    for(const f of floaters){
      ctx.globalAlpha=Math.max(0,f.life); ctx.fillStyle='#fde68a'; ctx.font=`900 ${Math.round(tile*0.62)}px system-ui`;
      ctx.textAlign='center'; ctx.shadowColor='rgba(0,0,0,.6)'; ctx.shadowBlur=6;
      ctx.fillText(f.text,f.x,f.y); ctx.shadowBlur=0;
    }
    ctx.globalAlpha=1;

    // snake body (tail -> head)
    ctx.lineCap='round'; ctx.lineJoin='round';
    for(let i=interp.length-1;i>=1;i--){
      const a=interp[i], b=interp[i-1];
      const t=i/(interp.length-1||1);
      const w=tile*(0.62 - 0.28*t) + 2;
      ctx.strokeStyle = i%3===0 ? '#16a34a' : '#22c55e';
      ctx.lineWidth=w; ctx.beginPath(); ctx.moveTo((a.x+0.5)*tile,(a.y+0.5)*tile); ctx.lineTo((b.x+0.5)*tile,(b.y+0.5)*tile); ctx.stroke();
      ctx.strokeStyle='rgba(255,255,255,0.14)'; ctx.lineWidth=w*0.35;
      ctx.beginPath(); ctx.moveTo((a.x+0.42)*tile,(a.y+0.38)*tile); ctx.lineTo((b.x+0.42)*tile,(b.y+0.38)*tile); ctx.stroke();
    }
    // head
    const h=interp[0], hx=(h.x+0.5)*tile, hy=(h.y+0.5)*tile;
    ctx.save(); ctx.shadowColor='rgba(34,197,94,.7)'; ctx.shadowBlur=16;
    ctx.fillStyle='#4ade80'; ctx.beginPath(); ctx.arc(hx,hy,tile*0.42,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
    ctx.fillStyle='rgba(255,255,255,0.28)'; ctx.beginPath(); ctx.arc(hx-tile*0.12,hy-tile*0.16,tile*0.16,0,Math.PI*2); ctx.fill();
    // tongue flick
    if(Math.sin(now/380)>0.82){
      ctx.strokeStyle='#f472b6'; ctx.lineWidth=2; ctx.beginPath();
      const tx=hx+dir.x*tile*0.45, ty=hy+dir.y*tile*0.45;
      ctx.moveTo(hx+dir.x*tile*0.35,hy+dir.y*tile*0.35); ctx.lineTo(tx+dir.x*6,ty+dir.y*6);
      ctx.moveTo(tx+dir.x*6,ty+dir.y*6); ctx.lineTo(tx+dir.x*10 - dir.y*4, ty+dir.y*10 + dir.x*4);
      ctx.moveTo(tx+dir.x*6,ty+dir.y*6); ctx.lineTo(tx+dir.x*10 + dir.y*4, ty+dir.y*10 - dir.x*4); ctx.stroke();
    }
    // eyes facing direction
    const px=-dir.y, py=dir.x;
    for(const side of [-1,1]){
      const ex=hx + dir.x*tile*0.13 + px*side*tile*0.2;
      const ey=hy + dir.y*tile*0.13 + py*side*tile*0.2;
      ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(ex,ey,tile*0.15,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#0f172a'; ctx.beginPath(); ctx.arc(ex+dir.x*3,ey+dir.y*3,tile*0.08,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(ex+dir.x*3-1.5,ey+dir.y*3-1.5,1.6,0,Math.PI*2); ctx.fill();
    }
    ctx.restore();
  }

  function loop(now){
    const dt=Math.min(50, now-lastFrame); lastFrame=now;
    if(state==='demo'){ aiStep(); }
    if(state==='playing'||state==='demo'){
      if(now-lastTick >= interval()){
        step();
        lastTick = now;
        // catch up at most one extra step if tab lagged
        if(now-lastTick >= interval()) lastTick = now;
      }
    }
    // particles / floaters
    for(const p of particles){ p.x+=p.vx*dt/1000; p.y+=p.vy*dt/1000; p.vy+=220*dt/1000; p.life-=p.decay*dt/1000; }
    particles=particles.filter(p=>p.life>0);
    for(const f of floaters){ f.y-=28*dt/1000; f.life-=dt/900; }
    floaters=floaters.filter(f=>f.life>0);
    draw(now);
    requestAnimationFrame(loop);
  }

  // ---------- Input ----------
  const KEYMAP={arrowup:'up',w:'up',arrowdown:'down',s:'down',arrowleft:'left',a:'left',arrowright:'right',d:'right'};
  window.addEventListener('keydown', e=>{
    const k=e.key.toLowerCase();
    if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
    if(k==='p'){
      if(state==='playing'){ state='paused'; showOverlay('paused'); pauseBtn.textContent='▶ Resume'; }
      else if(state==='paused'){ state='playing'; showOverlay('hidden'); lastTick=performance.now(); pauseBtn.textContent='⏸ Pause'; }
      return;
    }
    if(k===' '||k==='enter'){
      if(state==='demo'||state==='over') startPlaying();
      else if(state==='paused'){ state='playing'; showOverlay('hidden'); lastTick=performance.now(); pauseBtn.textContent='⏸ Pause'; }
      return;
    }
    const dName=KEYMAP[k];
    if(dName){
      if(state==='demo'){ startPlaying(DIRS[dName]); return; }
      if(state==='over'){ startPlaying(DIRS[dName]); return; }
      if(state==='playing') queueDir(DIRS[dName]);
    }
  });

  overlay.addEventListener('click', e=>{
    if(state==='demo'||state==='over') startPlaying();
    else if(state==='paused'){ state='playing'; showOverlay('hidden'); lastTick=performance.now(); pauseBtn.textContent='⏸ Pause'; }
  });
  ctaBtn.addEventListener('click', e=>{ e.stopPropagation(); overlay.dispatchEvent(new Event('click')); });

  pauseBtn.addEventListener('click', e=>{
    e.stopPropagation();
    if(state==='playing'){ state='paused'; showOverlay('paused'); pauseBtn.textContent='▶ Resume'; }
    else if(state==='paused'){ state='playing'; showOverlay('hidden'); lastTick=performance.now(); pauseBtn.textContent='⏸ Pause'; }
    else if(state==='demo'){ startPlaying(); }
  });
  document.getElementById('restartBtn').addEventListener('click', e=>{ e.stopPropagation(); startPlaying(); pauseBtn.textContent='⏸ Pause'; });

  document.querySelectorAll('.dpad button').forEach(b=>{
    b.addEventListener('click', ()=>{
      const d=DIRS[b.dataset.dir];
      if(state==='demo'||state==='over') startPlaying(d);
      else if(state==='playing') queueDir(d);
    });
  });

  // swipe
  let touchStart=null;
  canvas.addEventListener('touchstart', e=>{ touchStart={x:e.touches[0].clientX,y:e.touches[0].clientY}; }, {passive:true});
  canvas.addEventListener('touchmove', e=>{
    if(!touchStart) return; e.preventDefault();
    const dx=e.touches[0].clientX-touchStart.x, dy=e.touches[0].clientY-touchStart.y;
    if(Math.abs(dx)<24 && Math.abs(dy)<24) return;
    const d = Math.abs(dx)>Math.abs(dy) ? (dx>0?DIRS.right:DIRS.left) : (dy>0?DIRS.down:DIRS.up);
    if(state==='demo'||state==='over') startPlaying(d); else if(state==='playing') queueDir(d);
    touchStart={x:e.touches[0].clientX,y:e.touches[0].clientY};
  }, {passive:false});

  // init
  resizeCanvas();
  reset('demo');
  showOverlay('demo');
  updateHUD();
  requestAnimationFrame(t=>{ lastFrame=t; lastTick=t; requestAnimationFrame(loop); });
})();
</script>
</body>
</html>
Reasoning trace (2,249 tokens)
Implementing Snake game with canvas, input handling, collision, scoring, and visual polish under demo self-play requirement.Adding responsive canvas layout, DPR scaling, input queue, BFS AI, and overlay states.Defining state transitions, input queueing, BFS autopilot, and interpolation for smooth rendering.