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
28.8 s
Cost
$0.0088
Output tokens
17,557
Spent thinking
9,391 tokens
Speed
659.5 tok/s
Console errors
0
First token after
2.2 s
First visible text after
20.7 s
Prompt tokens
510
Code size
23.5 KB
Finish reason
stop
Served by
Meta
Model id
meta/muse-ember
Reasoning sent
model default
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 · 448 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Snake — Smooth Arcade Snake</title>
<style>
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{width:100%}
  body{
    font-family:ui-sans-system,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    color:#e2e8f0;background:#07111f;min-height:100vh;overflow-x:hidden;
    background-image:radial-gradient(900px 500px at 15% -10%,rgba(34,197,94,.22),transparent 60%),radial-gradient(800px 500px at 90% 0%,rgba(59,130,246,.18),transparent 55%),linear-gradient(180deg,#0a1a2f 0%,#07111f 100%);
  }
  .app{width:100%;max-width:720px;margin:0 auto;padding:14px 14px 16px;display:flex;flex-direction:column;align-items:center;gap:12px}
  .topbar{width:100%;max-width:600px;display:flex;align-items:center;justify-content:space-between;gap:12px}
  .brand{display:flex;align-items:center;gap:11px}
  .brand-icon{width:42px;height:42px;border-radius:13px;background:linear-gradient(135deg,#22c55e,#15803d);display:grid;place-items:center;box-shadow:0 8px 24px rgba(34,197,94,.35),inset 0 1px 0 rgba(255,255,255,.25);position:relative;flex:0 0 auto}
  .brand-icon::before,.brand-icon::after{content:"";width:7px;height:7px;background:#fff;border-radius:50%;position:absolute;top:15px}
  .brand-icon::before{left:12px}.brand-icon::after{right:12px}
  .brand-icon i{width:22px;height:6px;background:rgba(255,255,255,.85);border-radius:99px;position:absolute;bottom:12px}
  h1{font-size:21px;letter-spacing:.14em;font-weight:900;line-height:1;color:#f8fafc}
  h1 span{color:#4ade80}
  .brand p{font-size:11px;color:#94a3b8;margin-top:4px;letter-spacing:.02em}
  .badge{font-size:10px;font-weight:800;letter-spacing:.16em;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);white-space:nowrap}
  .badge.demo{color:#facc15;border-color:rgba(250,204,21,.35);background:rgba(250,204,21,.08)}
  .badge.playing{color:#4ade80;border-color:rgba(74,222,128,.35);background:rgba(74,222,128,.08)}
  .badge.paused{color:#93c5fd;border-color:rgba(147,197,253,.35);background:rgba(147,197,253,.08)}
  .badge.gameover{color:#fca5a5;border-color:rgba(248,113,113,.4);background:rgba(248,113,113,.08)}
  .hud{width:100%;max-width:600px;display:grid;grid-template-columns:repeat(4,1fr);gap:9px}
  .card{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:9px 12px 10px;backdrop-filter:blur(8px)}
  .card label{display:block;font-size:9px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:#7d8aa0}
  .card strong{display:block;font-size:23px;line-height:1.15;font-weight:900;color:#f8fafc;font-variant-numeric:tabular-nums}
  .card:first-child strong{color:#4ade80}
  .card:nth-child(2) strong{color:#facc15}
  .board-wrap{position:relative;width:min(600px,94vw,62vh);aspect-ratio:1/1;flex:0 0 auto}
  canvas{width:100%;height:100%;display:block;border-radius:18px;background:#0c1a2b;box-shadow:0 24px 60px rgba(0,0,0,.55),0 0 0 1px rgba(148,163,184,.22),0 0 42px rgba(34,197,94,.10);touch-action:none;cursor:pointer}
  .demo-pill{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);z-index:4;pointer-events:none;display:flex;align-items:center;gap:8px;white-space:nowrap;background:rgba(2,6,18,.72);border:1px solid rgba(255,255,255,.14);color:#f1f5f9;font-size:13px;font-weight:700;padding:9px 13px;border-radius:999px;backdrop-filter:blur(8px);box-shadow:0 10px 30px rgba(0,0,0,.35);animation:pulsePill 1.8s ease-in-out infinite}
  .demo-pill em{font-style:normal;font-size:11px;font-weight:600;color:#facc15}
  .play-ico{color:#4ade80;font-size:11px}
  @keyframes pulsePill{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(-3px)}}
  .demo-pill.hidden,.overlay.hidden{display:none!important}
  .overlay{position:absolute;inset:0;z-index:5;display:flex;align-items:center;justify-content:center;background:rgba(3,7,18,.52);backdrop-filter:blur(3px);border-radius:18px;padding:18px}
  .panel{background:rgba(10,22,40,.94);border:1px solid rgba(255,255,255,.13);border-radius:18px;padding:22px 26px;text-align:center;box-shadow:0 22px 50px rgba(0,0,0,.5);animation:pop .22s ease;max-width:320px;width:100%}
  @keyframes pop{from{transform:scale(.92);opacity:0}to{transform:scale(1);opacity:1}}
  .panel .tag{font-size:10px;font-weight:900;letter-spacing:.22em;color:#94a3b8;text-transform:uppercase}
  .panel h2{font-size:32px;font-weight:900;letter-spacing:.03em;margin:6px 0 2px;color:#f8fafc}
  .panel h2.red{color:#f87171}.panel h2.blue{color:#93c5fd}
  .panel p{font-size:13px;color:#a8b5c8;line-height:1.5}
  .big-score{font-size:46px;font-weight:900;line-height:1;color:#4ade80;margin:8px 0 2px;font-variant-numeric:tabular-nums}
  .score-label{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:#7d8aa0;font-weight:800}
  .panel .stats{display:flex;justify-content:center;gap:18px;margin:14px 0 16px}
  .panel .stats div{text-align:center}
  .panel .stats strong{display:block;font-size:19px;color:#f8fafc}
  .panel .stats span{font-size:9px;letter-spacing:.15em;text-transform:uppercase;color:#7d8aa0;font-weight:800}
  .panel .cta{display:inline-block;background:#22c55e;color:#04150a;font-weight:900;font-size:13px;letter-spacing:.04em;border:0;border-radius:999px;padding:11px 18px;cursor:pointer;box-shadow:0 10px 24px rgba(34,197,94,.3)}
  .panel .cta:active{transform:scale(.97)}
  .panel .hint-line{margin-top:12px;font-size:11px;color:#94a3b8}
  .controls{width:100%;max-width:600px;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px}
  .keys{display:flex;align-items:center;gap:5px;flex-wrap:wrap}
  .hint{font-size:10px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#7d8aa0;margin-right:2px}
  .or{color:#64748b;font-size:11px}
  kbd{font-family:ui-monospace,Menlo,monospace;font-size:11px;font-weight:800;color:#e2e8f0;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);border-bottom-width:2px;border-radius:7px;padding:4px 7px;min-width:24px;text-align:center}
  .actions{display:flex;gap:7px}
  button{font:inherit;cursor:pointer}
  .btn{border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);color:#e2e8f0;font-size:12px;font-weight:800;border-radius:999px;padding:8px 13px}
  .btn.primary{background:#22c55e;color:#04150a;border-color:transparent}
  .btn:active{transform:scale(.97)}
  @media(max-width:560px){
    .app{padding:10px 10px 12px;gap:9px}
    h1{font-size:18px}.brand p{display:none}.brand-icon{width:36px;height:36px}
    .card{padding:7px 9px}.card strong{font-size:19px}.board-wrap{width:min(94vw,60vh)}
    .controls{justify-content:center}.demo-pill{font-size:12px;padding:8px 10px}.demo-pill em{display:none}
  }
</style>
</head>
<body>
<div class="app">
  <header class="topbar">
    <div class="brand">
      <div class="brand-icon" aria-hidden="true"><i></i></div>
      <div><h1>NEON <span>SNAKE</span></h1><p>Smooth arcade snake &bull; eat, grow, survive</p></div>
    </div>
    <div id="stateBadge" class="badge demo">&#9679; DEMO</div>
  </header>

  <section class="hud" aria-label="Scoreboard">
    <div class="card"><label>Score</label><strong id="scoreVal">0</strong></div>
    <div class="card"><label>Best</label><strong id="bestVal">0</strong></div>
    <div class="card"><label>Length</label><strong id="lenVal">4</strong></div>
    <div class="card"><label>Level</label><strong id="lvlVal">1</strong></div>
  </section>

  <main class="board-wrap" id="boardWrap">
    <canvas id="game" width="600" height="600" aria-label="Snake game board"></canvas>
    <div id="overlay" class="overlay hidden"></div>
    <div id="demoPill" class="demo-pill"><span class="play-ico">&#9654;</span> Click or press a key to start <em>AI demo playing</em></div>
  </main>

  <footer class="controls">
    <div class="keys">
      <span class="hint">Move</span><kbd>&#8592;</kbd><kbd>&#8593;</kbd><kbd>&#8595;</kbd><kbd>&#8594;</kbd><span class="or">/</span><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd>
    </div>
    <div class="keys"><kbd>P</kbd><span class="hint">Pause</span><kbd>Space</kbd><span class="hint">Restart</span></div>
    <div class="actions"><button id="pauseBtn" class="btn">Pause</button><button id="restartBtn" class="btn primary">Restart</button></div>
  </footer>
</div>

<script>
(() => {
  const COLS = 24, ROWS = 24;
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const boardWrap = document.getElementById('boardWrap');
  const overlay = document.getElementById('overlay');
  const demoPill = document.getElementById('demoPill');
  const badge = document.getElementById('stateBadge');
  const scoreEl = document.getElementById('scoreVal');
  const bestEl = document.getElementById('bestVal');
  const lenEl = document.getElementById('lenVal');
  const lvlEl = document.getElementById('lvlVal');

  let state = 'demo'; // demo | playing | paused | gameover
  let snake = [], prevSnake = [], dir = {x:1,y:0}, dirQueue = [], food = {x:16,y:12};
  let score = 0, best = 0, stepMs = 140, lastStep = performance.now(), lastTime = 0;
  let particles = [], floaters = [], shake = 0;

  const keyOf = (x,y) => x + ',' + y;
  const clamp = (v,a,b) => Math.max(a, Math.min(b, v));
  const lerp = (a,b,t) => a + (b-a)*t;
  const isOpposite = (a,b) => a.x === -b.x && a.y === -b.y;

  function resizeCanvas(){
    const size = canvas.clientWidth || 600;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(size * dpr);
    canvas.height = Math.round(size * dpr);
  }
  window.addEventListener('resize', resizeCanvas);

  function updateHUD(){
    scoreEl.textContent = score;
    bestEl.textContent = best;
    lenEl.textContent = snake.length || 4;
    lvlEl.textContent = 1 + Math.floor(score / 50);
  }

  function spawnFood(){
    const occupied = new Set(snake.map(s => keyOf(s.x,s.y)));
    const empty = [];
    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++) if(!occupied.has(keyOf(x,y))) empty.push({x,y});
    if(!empty.length){ gameOver(); return; }
    food = empty[Math.floor(Math.random()*empty.length)];
  }

  function resetGame(demo){
    snake = [{x:11,y:12},{x:10,y:12},{x:9,y:12},{x:8,y:12}];
    prevSnake = snake.map(s => ({...s}));
    dir = {x:1,y:0}; dirQueue = [];
    score = 0; stepMs = 140; particles = []; floaters = []; shake = 0;
    spawnFood();
    state = demo ? 'demo' : 'playing';
    lastStep = performance.now();
    updateHUD(); renderState();
  }

  function startPlaying(){ resetGame(false); }
  function togglePause(){
    if(state === 'playing'){ state = 'paused'; }
    else if(state === 'paused'){ state = 'playing'; lastStep = performance.now(); }
    else if(state === 'demo'){ startPlaying(); return; }
    else if(state === 'gameover'){ startPlaying(); return; }
    renderState();
  }

  function renderState(){
    badge.textContent = state === 'demo' ? '\u25CF DEMO' : state === 'playing' ? '\u25CF PLAYING' : state === 'paused' ? '\u25CF PAUSED' : '\u25CF GAME OVER';
    badge.className = 'badge ' + state;
    demoPill.classList.toggle('hidden', state !== 'demo');
    if(state === 'paused'){
      overlay.classList.remove('hidden');
      overlay.innerHTML = '<div class="panel"><div class="tag">Paused</div><h2 class="blue">Paused</h2><p>Take a breath. The snake is frozen.</p><div class="stats"><div><strong>'+score+'</strong><span>Score</span></div><div><strong>'+snake.length+'</strong><span>Length</span></div><div><strong>'+best+'</strong><span>Best</span></div></div><button class="cta" id="resumeBtn">Resume</button><div class="hint-line">Press <kbd>P</kbd> or <kbd>Space</kbd> to resume</div></div>';
    } else if(state === 'gameover'){
      overlay.classList.remove('hidden');
      const isBest = score > 0 && score >= best;
      overlay.innerHTML = '<div class="panel"><div class="tag">Game Over</div><h2 class="red">Game Over</h2><div class="big-score">'+score+'</div><div class="score-label">points '+(isBest ? '&bull; new best!' : '')+'</div><div class="stats"><div><strong>'+best+'</strong><span>Best</span></div><div><strong>'+snake.length+'</strong><span>Length</span></div><div><strong>'+(1+Math.floor(score/50))+'</strong><span>Level</span></div></div><button class="cta" id="againBtn">Play Again</button><div class="hint-line">Press <kbd>Space</kbd> or <kbd>Enter</kbd> to restart</div></div>';
    } else {
      overlay.classList.add('hidden'); overlay.innerHTML = '';
    }
  }

  function burst(x,y,colors){
    for(let i=0;i<18;i++){
      const a = Math.random()*Math.PI*2, sp = 1.2 + Math.random()*3.4;
      particles.push({x:x+0.5,y:y+0.5,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:420+Math.random()*420,max:840,size:0.06+Math.random()*0.11,color:(colors||['#facc15','#fb923c','#f87171','#ffffff'])[i%4]});
    }
  }

  function gameOver(){
    burst(snake[0].x, snake[0].y, ['#f87171','#ef4444','#ffffff','#fca5a5']);
    shake = 8;
    if(state === 'demo'){ resetGame(true); return; }
    best = Math.max(best, score);
    state = 'gameover'; updateHUD(); renderState();
  }

  function step(){
    prevSnake = snake.map(s => ({...s}));
    if(dirQueue.length){
      const nxt = dirQueue.shift();
      if(!isOpposite(nxt, dir)) dir = nxt;
    }
    const head = snake[0];
    const nh = {x: head.x + dir.x, y: head.y + dir.y};
    if(nh.x < 0 || nh.x >= COLS || nh.y < 0 || nh.y >= ROWS){ gameOver(); return; }
    const eating = nh.x === food.x && nh.y === food.y;
    const body = eating ? snake : snake.slice(0,-1);
    if(body.some(s => s.x === nh.x && s.y === nh.y)){ gameOver(); return; }
    snake.unshift(nh);
    if(eating){
      score += 10; best = Math.max(best, score);
      stepMs = Math.max(75, stepMs - 4);
      burst(food.x, food.y);
      floaters.push({x:food.x+0.5,y:food.y+0.2,text:'+10',life:750,max:750});
      shake = Math.max(shake, 4);
      spawnFood(); updateHUD();
    } else {
      snake.pop();
    }
    lenEl.textContent = snake.length;
  }

  /* ---------- Simple AI for demo mode ---------- */
  function findPathToFood(){
    const start = snake[0], goal = food;
    const obstacles = new Set(snake.slice(0,-1).map(s => keyOf(s.x,s.y)));
    const q = [start], parent = new Map(), seen = new Set([keyOf(start.x,start.y)]);
    const dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
    while(q.length){
      const cur = q.shift();
      if(cur.x === goal.x && cur.y === goal.y){
        const path = [cur]; let k = keyOf(cur.x,cur.y);
        while(parent.has(k)){ const p = parent.get(k); path.unshift(p); k = keyOf(p.x,p.y); }
        return path;
      }
      for(const d of dirs){
        const nx = cur.x+d.x, ny = cur.y+d.y, k = keyOf(nx,ny);
        if(nx<0||nx>=COLS||ny<0||ny>=ROWS||seen.has(k)||obstacles.has(k)) continue;
        seen.add(k); parent.set(k,cur); q.push({x:nx,y:ny});
      }
    }
    return null;
  }
  function spaceCount(start, obstacles){
    let count = 0; const seen = new Set([keyOf(start.x,start.y)]), q = [start];
    const dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
    while(q.length && count < 70){ const cur=q.shift(); count++;
      for(const d of dirs){ const nx=cur.x+d.x, ny=cur.y+d.y, k=keyOf(nx,ny);
        if(nx<0||nx>=COLS||ny<0||ny>=ROWS||seen.has(k)||obstacles.has(k)) continue;
        seen.add(k); q.push({x:nx,y:ny}); } }
    return count;
  }
  function aiStep(){
    const path = findPathToFood();
    if(path && path.length > 1){
      const n = path[1], d = {x:n.x-snake[0].x, y:n.y-snake[0].y};
      if(!isOpposite(d, dir)) dirQueue = [d];
      return;
    }
    const obstacles = new Set(snake.slice(0,-1).map(s => keyOf(s.x,s.y)));
    const cand = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}].filter(d => !isOpposite(d,dir));
    let bestD = null, bestScore = -1e9;
    for(const d of cand){
      const nx=snake[0].x+d.x, ny=snake[0].y+d.y;
      if(nx<0||nx>=COLS||ny<0||ny>=ROWS||obstacles.has(keyOf(nx,ny))) continue;
      const dist = Math.abs(nx-food.x)+Math.abs(ny-food.y);
      const sc = -dist*2 + spaceCount({x:nx,y:ny}, obstacles)*0.6;
      if(sc>bestScore){ bestScore=sc; bestD=d; }
    }
    if(bestD) dirQueue = [bestD];
  }

  /* ---------- Input: queue honours quick successive turns, never reverses ---------- */
  function queueDirection(nd){
    const last = dirQueue.length ? dirQueue[dirQueue.length-1] : dir;
    if(isOpposite(nd,last)) return;
    if(nd.x===last.x && nd.y===last.y) return;
    if(dirQueue.length < 3) dirQueue.push(nd);
  }
  const moves = {ArrowUp:{x:0,y:-1},KeyW:{x:0,y:-1},ArrowDown:{x:0,y:1},KeyS:{x:0,y:1},ArrowLeft:{x:-1,y:0},KeyA:{x:-1,y:0},ArrowRight:{x:1,y:0},KeyD:{x:1,y:0}};

  window.addEventListener('keydown', (e) => {
    const code = e.code;
    if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(code)) e.preventDefault();
    const isMove = !!moves[code];
    if(state === 'demo'){
      startPlaying();
      if(isMove) queueDirection(moves[code]);
      else if(code === 'KeyP') togglePause();
      return;
    }
    if(state === 'gameover'){
      if(code === 'Space' || code === 'Enter') startPlaying();
      return;
    }
    if(state === 'paused'){
      if(code === 'KeyP' || code === 'Space' || code === 'Enter') togglePause();
      else if(code === 'KeyR') startPlaying();
      return;
    }
    // playing
    if(code === 'KeyP') togglePause();
    else if(code === 'Space' || code === 'Enter') startPlaying();
    else if(isMove) queueDirection(moves[code]);
  });

  boardWrap.addEventListener('click', (e) => {
    if(e.target && e.target.id === 'againBtn'){ startPlaying(); return; }
    if(e.target && e.target.id === 'resumeBtn'){ togglePause(); return; }
    if(state === 'demo' || state === 'gameover') startPlaying();
    else if(state === 'paused') togglePause();
  });
  document.getElementById('pauseBtn').addEventListener('click', togglePause);
  document.getElementById('restartBtn').addEventListener('click', startPlaying);

  // touch swipe
  let ts = null;
  boardWrap.addEventListener('touchstart', e => { ts = {x:e.changedTouches[0].clientX, y:e.changedTouches[0].clientY}; }, {passive:true});
  boardWrap.addEventListener('touchend', e => {
    if(!ts) return;
    const dx = e.changedTouches[0].clientX - ts.x, dy = e.changedTouches[0].clientY - ts.y; ts = null;
    if(Math.max(Math.abs(dx),Math.abs(dy)) < 24){ if(state==='demo'||state==='gameover') startPlaying(); return; }
    const d = Math.abs(dx) > Math.abs(dy) ? {x:Math.sign(dx),y:0} : {x:0,y:Math.sign(dy)};
    if(state === 'demo'){ startPlaying(); queueDirection(d); }
    else if(state === 'playing') queueDirection(d);
  }, {passive:true});

  function rr(x,y,w,h,r){
    ctx.beginPath();
    if(ctx.roundRect) ctx.roundRect(x,y,w,h,r); else ctx.rect(x,y,w,h);
  }

  function draw(now, dt){
    const W = canvas.width, cs = W / COLS;
    ctx.save();
    if(shake > 0.4){ ctx.translate((Math.random()-0.5)*shake*(W/600), (Math.random()-0.5)*shake*(W/600)); shake *= 0.86; }
    // board
    ctx.fillStyle = '#0e2138'; ctx.fillRect(0,0,W,W);
    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*cs,y*cs,cs,cs); }
    }
    ctx.strokeStyle='rgba(148,163,184,0.07)'; ctx.lineWidth=1;
    for(let i=0;i<=COLS;i++){ ctx.beginPath(); ctx.moveTo(i*cs,0); ctx.lineTo(i*cs,W); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,i*cs); ctx.lineTo(W,i*cs); ctx.stroke(); }

    // food (pulsing apple-orb)
    const pulse = 1 + Math.sin(now/210)*0.14;
    const fx=(food.x+0.5)*cs, fy=(food.y+0.5)*cs, fr=cs*0.31*pulse;
    ctx.save(); ctx.shadowColor='rgba(239,68,68,0.85)'; ctx.shadowBlur=cs*0.55;
    const g=ctx.createRadialGradient(fx-fr*0.35,fy-fr*0.4,fr*0.15,fx,fy,fr);
    g.addColorStop(0,'#fca5a5'); g.addColorStop(0.45,'#ef4444'); g.addColorStop(1,'#991b1b');
    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(fx,fy,fr,0,Math.PI*2); ctx.fill();
    ctx.restore();
    ctx.fillStyle='rgba(255,255,255,0.75)'; ctx.beginPath(); ctx.arc(fx-fr*0.32,fy-fr*0.38,fr*0.22,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle='#92400e'; ctx.lineWidth=Math.max(2,cs*0.07); ctx.beginPath(); ctx.moveTo(fx,fy-fr*0.8); ctx.quadraticCurveTo(fx+cs*0.08,fy-fr*1.15,fx+cs*0.18,fy-fr*1.22); ctx.stroke();
    ctx.fillStyle='#4ade80'; ctx.beginPath(); ctx.ellipse(fx+cs*0.24,fy-fr*1.02,cs*0.16,cs*0.08,-0.5,0,Math.PI*2); ctx.fill();

    // particles
    for(const p of particles){
      const a = clamp(p.life/p.max,0,1);
      ctx.globalAlpha = a; ctx.fillStyle = p.color;
      ctx.beginPath(); ctx.arc(p.x*cs,p.y*cs,p.size*cs*(0.4+a),0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha = 1;

    // snake (tail -> head, interpolated for smooth motion)
    const interp = (state==='playing'||state==='demo') ? clamp((now-lastStep)/stepMs,0,1) : 1;
    for(let i=snake.length-1;i>=0;i--){
      const s=snake[i], p=prevSnake[i] || prevSnake[prevSnake.length-1] || s;
      const gx=lerp(p.x,s.x,interp), gy=lerp(p.y,s.y,interp);
      const cx=(gx+0.5)*cs, cy=(gy+0.5)*cs;
      const isHead = i===0;
      const size = cs*(isHead?0.94:0.88 - Math.min(i,12)*0.015);
      const light = isHead?64:Math.max(38,58 - i*0.7);
      ctx.save();
      if(isHead){ ctx.shadowColor='rgba(74,222,128,0.65)'; ctx.shadowBlur=cs*0.45; }
      ctx.fillStyle='hsl(145,82%,'+light+'%)';
      rr(cx-size/2,cy-size/2,size,size,cs*0.32); ctx.fill();
      ctx.restore();
      ctx.fillStyle='rgba(255,255,255,0.14)';
      rr(cx-size*0.28,cy-size*0.34,size*0.5,size*0.18,cs*0.1); ctx.fill();
      if(isHead){
        // tongue flick
        if(now % 2600 < 180){
          ctx.strokeStyle='#f87171'; ctx.lineWidth=Math.max(1.5,cs*0.05); ctx.lineCap='round';
          const tx=cx+dir.x*size*0.55, ty=cy+dir.y*size*0.55, px=-dir.y, py=dir.x;
          ctx.beginPath(); ctx.moveTo(tx,ty); ctx.lineTo(tx+dir.x*cs*0.22,ty+dir.y*cs*0.22);
          ctx.moveTo(tx+dir.x*cs*0.22,ty+dir.y*cs*0.22); ctx.lineTo(tx+dir.x*cs*0.34+px*cs*0.09,ty+dir.y*cs*0.34+py*cs*0.09);
          ctx.moveTo(tx+dir.x*cs*0.22,ty+dir.y*cs*0.22); ctx.lineTo(tx+dir.x*cs*0.34-px*cs*0.09,ty+dir.y*cs*0.34-py*cs*0.09); ctx.stroke();
        }
        // eyes facing direction
        const perp={x:-dir.y,y:dir.x};
        const bx=cx+dir.x*cs*0.13, by=cy+dir.y*cs*0.13;
        for(const sgn of [1,-1]){
          const ex=bx+perp.x*cs*0.20*sgn, ey=by+perp.y*cs*0.20*sgn;
          ctx.fillStyle='#ffffff'; ctx.beginPath(); ctx.arc(ex,ey,cs*0.175,0,Math.PI*2); ctx.fill();
          ctx.fillStyle='#0f172a'; ctx.beginPath(); ctx.arc(ex+dir.x*cs*0.055,ey+dir.y*cs*0.055,cs*0.09,0,Math.PI*2); ctx.fill();
          ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(ex+dir.x*cs*0.02,ey+dir.y*cs*0.02,cs*0.03,0,Math.PI*2); ctx.fill();
        }
      }
    }

    // floating +10
    ctx.textAlign='center';
    for(const f of floaters){
      const a=clamp(f.life/f.max,0,1);
      ctx.globalAlpha=a; ctx.font='900 '+(cs*0.55)+'px ui-sans-serif,system-ui,sans-serif';
      ctx.fillStyle='#facc15'; ctx.fillText(f.text,f.x*cs,f.y*cs);
    }
    ctx.globalAlpha=1;
    ctx.restore();
  }

  function frame(now){
    requestAnimationFrame(frame);
    const dt = Math.min(32, now - (lastTime || now)); lastTime = now;
    if(state==='playing' || state==='demo'){
      if(now - lastStep >= stepMs){
        if(state==='demo') aiStep();
        step();
        // demo gameOver() resets lastStep itself; only advance if still in a running state
        if(state==='playing' || state==='demo') lastStep = now;
      }
    }
    // particles & floaters
    for(const p of particles){ p.life-=dt; p.x+=p.vx*dt/1000; p.y+=p.vy*dt/1000; p.vx*=0.96; p.vy*=0.96; }
    particles = particles.filter(p=>p.life>0);
    for(const f of floaters){ f.life-=dt; f.y-=dt*0.0012; }
    floaters = floaters.filter(f=>f.life>0);
    draw(now, dt);
  }

  resizeCanvas();
  resetGame(true);
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (9,391 tokens)
Implementing a 20x20 centered grid snake game with input handling, scoring, collision, pause/restart, visual polish, and self-playing demo mode in a single HTML code block.Adding demo autoplay, input queueing, interpolated rendering, and responsive layout for first-paint visibility.Extending input handling with key mapping, queueing, and pause plus AI pathfinding and responsive canvas scaling.Detailing canvas DPR scaling, rAF loop, snake state, collision, interpolation, and particle rendering.Adding BFS pathfinding, flood-fill scoring, input queueing, and overlay state handling.Extending rendering with interpolation, screen shake, floating text, and mobile UI controls.Calculating responsive canvas sizing and overlay interaction handling for mobile layout.Implementing canvas sizing, drawing routines, and AI pathfinding with input and state handling.Defining keydown behavior for gameover, paused, and playing states with restart and resume handling.Adding a canvas polyfill fallback and smoothing snake motion with interpolation and particle updates.