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
15.1 s
Cost
$0.0045
Output tokens
8,968
Spent thinking
1,311 tokens
Speed
688.5 tok/s
Console errors
0
First token after
2.0 s
First visible text after
4.7 s
Prompt tokens
510
Code size
23.3 KB
Finish reason
stop
Served by
Meta
Model id
meta/muse-ember
Reasoning sent
{"effort":"low"}
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 · 535 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-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
    background:#070b12;
    background-image:radial-gradient(900px 500px at 20% -10%,rgba(34,197,94,.18),transparent 60%),radial-gradient(800px 500px at 90% 110%,rgba(14,165,233,.16),transparent 60%),radial-gradient(600px 400px at 80% 0%,rgba(244,114,182,.08),transparent 60%);
    color:#e6f0e9;min-height:100vh;display:flex;flex-direction:column;align-items:center;
    padding:16px 12px 20px;overflow-x:hidden;
  }
  header{width:min(620px,96vw);display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:12px}
  .logo{display:flex;align-items:center;gap:10px}
  .logo-icon{width:40px;height:40px;border-radius:11px;background:linear-gradient(135deg,#22c55e,#0ea5e9);display:grid;place-items:center;font-size:22px;box-shadow:0 8px 24px rgba(34,197,94,.35)}
  h1{font-size:26px;letter-spacing:.14em;font-weight:900;line-height:1}
  h1 span{color:#4ade80}
  .sub{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:#8aa096;margin-top:4px}
  .scores{display:flex;gap:8px}
  .card{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.09);backdrop-filter:blur(8px);border-radius:14px;padding:8px 14px;text-align:center;min-width:82px;box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
  .card label{display:block;font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#93a89e;font-weight:700}
  .card b{font-size:24px;line-height:1.1;font-variant-numeric:tabular-nums}
  .card.best b{color:#facc15}
  .board-wrap{position:relative;width:min(620px,96vw)}
  .frame{position:relative;border-radius:22px;padding:10px;background:linear-gradient(180deg,rgba(255,255,255,.10),rgba(255,255,255,.03));border:1px solid rgba(255,255,255,.12);box-shadow:0 24px 60px rgba(0,0,0,.55),0 0 0 1px rgba(0,0,0,.2),0 0 42px rgba(34,197,94,.10)}
  canvas{display:block;width:100%;aspect-ratio:1/1;border-radius:14px;background:#0c1a14;touch-action:none;outline:none}
  .hud{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:11px;flex-wrap:wrap}
  .hint{font-size:12.5px;color:#9db3a8}
  .hint kbd{background:rgba(255,255,255,.09);border:1px solid rgba(255,255,255,.14);border-bottom-width:2px;border-radius:6px;padding:1px 6px;font-family:inherit;font-size:11px;color:#e6f0e9;font-weight:700}
  .actions{display:flex;gap:8px}
  button{font:inherit;cursor:pointer;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.08);color:#e6f0e9;border-radius:999px;padding:8px 14px;font-size:13px;font-weight:800;letter-spacing:.02em;transition:.15s}
  button:hover{background:rgba(255,255,255,.15);transform:translateY(-1px)}
  button:active{transform:translateY(0)}
  button.primary{background:#22c55e;border-color:#22c55e;color:#052e16}
  button.primary:hover{background:#4ade80}
  /* Overlay */
  .overlay{position:absolute;inset:10px;border-radius:14px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:20px;background:rgba(5,10,8,.62);backdrop-filter:blur(6px);z-index:5;transition:opacity .25s}
  .overlay.hidden{opacity:0;pointer-events:none}
  .overlay h2{font-size:32px;letter-spacing:.08em;font-weight:900;text-shadow:0 4px 20px rgba(0,0,0,.5)}
  .overlay p{color:#b8d0c2;margin-top:8px;font-size:14px;line-height:1.5;max-width:360px}
  .overlay .big-score{font-size:52px;font-weight:900;color:#4ade80;line-height:1.05;margin-top:6px}
  .overlay .newbest{display:inline-block;margin-top:8px;background:#facc15;color:#422006;font-weight:900;font-size:12px;letter-spacing:.1em;padding:4px 10px;border-radius:999px}
  .overlay .cta{margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;justify-content:center}
  .demo-badge{position:absolute;top:18px;left:50%;transform:translateX(-50%);z-index:6;background:rgba(34,197,94,.92);color:#052e16;font-size:11px;font-weight:900;letter-spacing:.14em;text-transform:uppercase;padding:6px 12px;border-radius:999px;box-shadow:0 6px 18px rgba(0,0,0,.35);white-space:nowrap;animation:pulse 1.6s ease-in-out infinite}
  @keyframes pulse{0%,100%{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.04)}}
  .demo-badge.hidden{display:none}
  /* Controls */
  .controls{width:min(620px,96vw);margin-top:14px;display:flex;justify-content:space-between;align-items:center;gap:14px}
  .legend{display:flex;gap:14px;font-size:12px;color:#8aa096;flex-wrap:wrap}
  .legend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:5px;vertical-align:-1px}
  .dpad{display:grid;grid-template-columns:44px 44px 44px;grid-template-rows:44px 44px 44px;gap:4px;flex:0 0 auto}
  .dpad button{padding:0;border-radius:12px;font-size:17px;display:grid;place-items:center}
  .dpad .up{grid-column:2;grid-row:1}.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-width:560px){
    h1{font-size:20px}.card{min-width:68px;padding:6px 10px}.card b{font-size:20px}
    .controls{flex-direction:column}.legend{justify-content:center}
    .overlay h2{font-size:26px}.overlay .big-score{font-size:42px}
    header{align-items:flex-start}
  }
</style>
</head>
<body>

<header>
  <div class="logo">
    <div class="logo-icon">🐍</div>
    <div>
      <h1>NEON <span>SNAKE</span></h1>
      <div class="sub">Smooth • Fast • Classic</div>
    </div>
  </div>
  <div class="scores">
    <div class="card"><label>Score</label><b id="score">0</b></div>
    <div class="card best"><label>★ Best</label><b id="best">0</b></div>
  </div>
</header>

<div class="board-wrap">
  <div class="frame">
    <canvas id="game" width="660" height="660" aria-label="Snake game board" tabindex="0"></canvas>
    <div class="demo-badge" id="demoBadge">▶ Demo playing — Click / Key to start</div>

    <div class="overlay hidden" id="overlay">
      <h2 id="ovTitle">GAME OVER</h2>
      <div class="big-score" id="ovScore">0</div>
      <div id="ovBestWrap"><span class="newbest" id="ovBest" style="display:none">★ NEW BEST!</span> <p id="ovBestText" style="margin-top:6px"></p></div>
      <p id="ovText">You hit a wall.</p>
      <div class="cta">
        <button class="primary" id="btnRestart">↻ Play Again</button>
        <button id="btnPauseOv">Pause</button>
      </div>
      <p style="margin-top:12px;font-size:12px;color:#8aa096"><kbd style="background:rgba(255,255,255,.1);padding:2px 7px;border-radius:6px;border:1px solid rgba(255,255,255,.2)">Space</kbd> / <kbd style="background:rgba(255,255,255,.1);padding:2px 7px;border-radius:6px;border:1px solid rgba(255,255,255,.2)">Enter</kbd> to restart &nbsp;•&nbsp; <kbd style="background:rgba(255,255,255,.1);padding:2px 7px;border-radius:6px;border:1px solid rgba(255,255,255,.2)">P</kbd> pause</p>
    </div>
  </div>
</div>

<div class="controls">
  <div class="legend">
    <span><i style="background:#4ade80"></i>Snake</span>
    <span><i style="background:#fb7185"></i>Food +10</span>
    <span><i style="background:#facc15"></i>Best this session</span>
  </div>
  <div>
    <div class="hint" style="text-align:center;margin-bottom:8px"><kbd>←↑↓→</kbd> / <kbd>WASD</kbd> move &nbsp; <kbd>P</kbd> pause &nbsp; <kbd>Space</kbd> restart</div>
    <div class="dpad" aria-label="On-screen controls">
      <button class="up" data-dir="up" aria-label="Up">▲</button>
      <button class="left" data-dir="left" aria-label="Left">◀</button>
      <button class="down" data-dir="down" aria-label="Down">▼</button>
      <button class="right" data-dir="right" aria-label="Right">▶</button>
    </div>
  </div>
</div>

<script>
(() => {
  const COLS = 22, ROWS = 22;
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const scoreEl = document.getElementById('score');
  const bestEl = document.getElementById('best');
  const overlay = document.getElementById('overlay');
  const ovTitle = document.getElementById('ovTitle');
  const ovScore = document.getElementById('ovScore');
  const ovText = document.getElementById('ovText');
  const ovBest = document.getElementById('ovBest');
  const ovBestText = document.getElementById('ovBestText');
  const demoBadge = document.getElementById('demoBadge');

  const cell = canvas.width / COLS; // 30

  let snake, prevSnake, dir, queue, food, particles, score, best = 0;
  let state = 'demo'; // demo | playing | paused | over
  let lastStep = 0, interval = 135, deathReason = '';
  let wasNewBest = false;

  function reset(toDemo) {
    const cx = Math.floor(COLS/2), cy = Math.floor(ROWS/2);
    snake = [{x:cx,y:cy},{x:cx-1,y:cy},{x:cx-2,y:cy},{x:cx-3,y:cy}];
    prevSnake = snake.map(s=>({...s}));
    dir = {x:1,y:0};
    queue = [];
    particles = [];
    score = 0;
    interval = 135;
    wasNewBest = false;
    updateScoreUI();
    spawnFood();
    lastStep = performance.now();
    hideOverlay();
    state = toDemo ? 'demo' : 'playing';
    demoBadge.classList.toggle('hidden', !toDemo);
    canvas.focus({preventScroll:true});
  }

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

  function updateScoreUI(){ scoreEl.textContent = score; bestEl.textContent = best; }

  function showOverlay(title, text, showPauseBtn){
    ovTitle.textContent = title;
    ovScore.textContent = score;
    ovText.textContent = text;
    ovBest.style.display = wasNewBest ? 'inline-block' : 'none';
    ovBestText.textContent = 'Best: ' + best + (best>0 ? '  •  Length: ' + snake.length : '');
    document.getElementById('btnPauseOv').style.display = showPauseBtn ? '' : 'none';
    overlay.classList.remove('hidden');
  }
  function hideOverlay(){ overlay.classList.add('hidden'); }

  function queueDir(nx, ny){
    const last = queue.length ? queue[queue.length-1] : dir;
    if (last.x === -nx && last.y === -ny) return; // reverse
    if (last.x === nx && last.y === ny) return; // same
    if (queue.length < 3) queue.push({x:nx,y:ny});
  }

  function isLethal(x,y, body){
    if (x<0||x>=COLS||y<0||y>=ROWS) return true;
    return body.some(s=>s.x===x&&s.y===y);
  }

  // ---- Demo AI: BFS to food, fallback to safest move ----
  function demoDir(){
    const head = snake[0];
    const bodySet = new Set(snake.map(s=>s.x+','+s.y));
    // BFS
    const key = (x,y)=>x+','+y;
    const prevMap = new Map();
    const visited = new Set([key(head.x,head.y)]);
    const q = [{x:head.x,y:head.y}];
    let found=null;
    const dirs=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
    while(q.length && !found){
      const cur=q.shift();
      for(const d of dirs){
        const nx=cur.x+d.x, ny=cur.y+d.y;
        if(nx<0||nx>=COLS||ny<0||ny>=ROWS) continue;
        const k=key(nx,ny);
        if(visited.has(k)) continue;
        // allow tail cell (it will move)
        const isTail = snake[snake.length-1].x===nx && snake[snake.length-1].y===ny;
        if(bodySet.has(k) && !isTail) continue;
        visited.add(k);
        prevMap.set(k,{x:cur.x,y:cur.y});
        if(nx===food.x && ny===food.y){ found={x:nx,y:ny}; break; }
        q.push({x:nx,y:ny});
      }
    }
    if(found){
      let cur=found;
      while(prevMap.has(key(cur.x,cur.y))){
        const p=prevMap.get(key(cur.x,cur.y));
        if(p.x===head.x && p.y===head.y){
          return {x:cur.x-head.x, y:cur.y-head.y};
        }
        cur=p;
      }
    }
    // fallback: pick non-lethal move with most free neighbours / towards centre
    let bestMove=null, bestVal=-1e9;
    for(const d of dirs){
      if(d.x===-dir.x && d.y===-dir.y) continue;
      const nx=head.x+d.x, ny=head.y+d.y;
      if(isLethal(nx,ny,snake.slice(0,-1))) continue;
      let freeN=0;
      for(const dd of dirs){ const ax=nx+dd.x, ay=ny+dd.y; if(ax>=0&&ax<COLS&&ay>=0&&ay<ROWS&&!bodySet.has(key(ax,ay))) freeN++; }
      const dist = Math.abs(nx-food.x)+Math.abs(ny-food.y);
      const centre = Math.abs(nx-COLS/2)+Math.abs(ny-ROWS/2);
      const val = freeN*10 - dist*2 - centre*0.5;
      if(val>bestVal){bestVal=val;bestMove=d;}
    }
    return bestMove || dir;
  }

  function burst(x,y){
    const cx = (x+0.5)*cell, cy=(y+0.5)*cell;
    const colors=['#fb7185','#fda4af','#facc15','#ffffff','#4ade80'];
    for(let i=0;i<16;i++){
      const a = Math.random()*Math.PI*2;
      const sp = 1.2 + Math.random()*3.2;
      particles.push({x:cx,y:cy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-0.6,life:1,decay:0.025+Math.random()*0.02,r:2+Math.random()*3.5,color:colors[i%colors.length]});
    }
    // ring
    particles.push({x:cx,y:cy,vx:0,vy:0,life:1,decay:0.06,r:4,ring:true,color:'#fb7185'});
  }

  function step(now){
    prevSnake = snake.map(s=>({...s}));
    if(state==='demo'){
      const d = demoDir();
      if(!(d.x===-dir.x && d.y===-dir.y)) queueDir(d.x,d.y);
    }
    if(queue.length) dir = queue.shift();

    const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};
    const willEat = head.x===food.x && head.y===food.y;
    const bodyToCheck = willEat ? snake : snake.slice(0,-1);

    if(head.x<0||head.x>=COLS||head.y<0||head.y>=ROWS){ gameOver('You hit the wall.'); return; }
    if(bodyToCheck.some(s=>s.x===head.x&&s.y===head.y)){ gameOver('You bit yourself.'); return; }

    snake.unshift(head);
    if(willEat){
      score += 10;
      if(score>best){ if(!wasNewBest && best>0) wasNewBest=true; best=score; if(best===score) wasNewBest = best>0 && score>=best; }
      // simpler newbest flag:
      wasNewBest = (score===best && score>0);
      updateScoreUI();
      burst(food.x, food.y);
      interval = Math.max(78, 135 - Math.floor(score/40)*6);
      spawnFood();
    } else {
      snake.pop();
    }
    lastStep = now;
  }

  function gameOver(reason){
    deathReason = reason;
    state='over';
    if(score>best) best=score;
    updateScoreUI();
    burst(snake[0].x, snake[0].y);
    showOverlay('GAME OVER', reason + '  Score ' + score + ' points.', false);
    demoBadge.classList.add('hidden');
  }

  function startGame(firstDir){
    reset(false);
    if(firstDir) queueDir(firstDir.x, firstDir.y);
    hideOverlay();
  }

  // ---- Rendering ----
  function roundRect(x,y,w,h,r){
    ctx.beginPath();
    ctx.moveTo(x+r,y);
    ctx.arcTo(x+w,y,x+w,y+h,r);
    ctx.arcTo(x+w,y+h,x,y+h,r);
    ctx.arcTo(x,y+h,x,y,r);
    ctx.arcTo(x,y,x+w,y,r);
    ctx.closePath();
  }

  function draw(now){
    // background checker
    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
      ctx.fillStyle = (x+y)%2===0 ? '#0e2119' : '#0c1a14';
      ctx.fillRect(x*cell, y*cell, cell, cell);
    }
    // subtle vignette
    const grad = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 120, canvas.width/2, canvas.height/2, 520);
    grad.addColorStop(0,'rgba(255,255,255,0.03)');
    grad.addColorStop(1,'rgba(0,0,0,0.28)');
    ctx.fillStyle=grad; ctx.fillRect(0,0,canvas.width,canvas.height);

    // food (pulsing cherry)
    if(food){
      const fx=(food.x+0.5)*cell, fy=(food.y+0.5)*cell;
      const pulse = 1 + Math.sin(now/180)*0.12;
      ctx.save();
      ctx.shadowColor='#fb7185'; ctx.shadowBlur=18;
      ctx.fillStyle='#f43f5e';
      ctx.beginPath(); ctx.arc(fx, fy+2, 9.5*pulse, 0, Math.PI*2); ctx.fill();
      ctx.shadowBlur=0;
      ctx.fillStyle='rgba(255,255,255,0.85)';
      ctx.beginPath(); ctx.arc(fx-3, fy-1, 3, 0, Math.PI*2); ctx.fill();
      ctx.strokeStyle='#16a34a'; ctx.lineWidth=2.5; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(fx, fy-6); ctx.quadraticCurveTo(fx+4, fy-13, fx+8, fy-13); ctx.stroke();
      ctx.fillStyle='#22c55e';
      ctx.beginPath(); ctx.ellipse(fx+8, fy-13, 5, 2.6, -0.4, 0, Math.PI*2); ctx.fill();
      ctx.restore();
    }

    // interpolation
    let alpha = state==='paused'||state==='over' ? 1 : Math.min(1, Math.max(0,(now-lastStep)/interval));
    // when tab was idle, don't jump
    if(now-lastStep > interval*2) { alpha=1; }

    function interpPos(i){
      const cur = snake[i];
      const prev = prevSnake[Math.min(i, prevSnake.length-1)] || cur;
      return {x: prev.x + (cur.x-prev.x)*alpha, y: prev.y + (cur.y-prev.y)*alpha};
    }

    // snake body tail -> head
    ctx.save();
    for(let i=snake.length-1;i>=0;i--){
      const p = interpPos(i);
      const px = p.x*cell, py = p.y*cell;
      const t = snake.length>1 ? i/(snake.length-1) : 0; // 0 head ->1 tail
      // gradient head bright green -> tail teal
      const hue = 145 - t*35;
      const light = 58 - t*10;
      ctx.shadowColor = i===0 ? 'rgba(74,222,128,0.7)' : 'rgba(34,197,94,0.35)';
      ctx.shadowBlur = i===0 ? 14 : 6;
      ctx.fillStyle = `hsl(${hue} 85% ${light}%)`;
      const inset = i===0 ? 1.5 : 3 + t*1.5;
      const r = i===0 ? 9 : 7;
      roundRect(px+inset, py+inset, cell-inset*2, cell-inset*2, r);
      ctx.fill();
      ctx.shadowBlur=0;
      // belly highlight
      if(i>0){
        ctx.fillStyle='rgba(255,255,255,0.10)';
        roundRect(px+inset+3, py+inset+3, cell-inset*2-6, (cell-inset*2-6)*0.38, 5);
        ctx.fill();
      }
    }
    ctx.restore();

    // head eyes facing direction
    const hp = interpPos(0);
    const hx = (hp.x+0.5)*cell, hy=(hp.y+0.5)*cell;
    const ex = dir.x, ey = dir.y;
    const pxv = -ey, pyv = ex;
    ctx.save();
    for(const side of [-1,1]){
      const eyeX = hx + ex*4 + pxv*6*side;
      const eyeY = hy + ey*4 + pyv*6*side;
      ctx.fillStyle='#ffffff';
      ctx.beginPath(); ctx.arc(eyeX, eyeY, 5.2, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle='#0f172a';
      ctx.beginPath(); ctx.arc(eyeX+ex*2, eyeY+ey*2, 2.8, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle='#ffffff';
      ctx.beginPath(); ctx.arc(eyeX+ex*2-1, eyeY+ey*2-1, 1, 0, Math.PI*2); ctx.fill();
    }
    // tongue occasionally
    if(Math.sin(now/500)>0.85){
      ctx.strokeStyle='#fb7185'; ctx.lineWidth=2; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(hx+ex*13, hy+ey*13); ctx.lineTo(hx+ex*19, hy+ey*19); ctx.stroke();
    }
    ctx.restore();

    // particles
    for(let i=particles.length-1;i>=0;i--){
      const pt = particles[i];
      pt.x += pt.vx; pt.y += pt.vy; pt.vy += 0.06; pt.vx*=0.96; pt.vy*=0.96;
      pt.life -= pt.decay;
      if(pt.life<=0){ particles.splice(i,1); continue; }
      ctx.save();
      ctx.globalAlpha = Math.max(0, pt.life);
      ctx.fillStyle = pt.color;
      if(pt.ring){
        ctx.strokeStyle = pt.color; ctx.lineWidth=2.5;
        ctx.beginPath(); ctx.arc(pt.x, pt.y, pt.r + (1-pt.life)*26, 0, Math.PI*2); ctx.stroke();
      } else {
        ctx.beginPath(); ctx.arc(pt.x, pt.y, pt.r*pt.life+0.6, 0, Math.PI*2); ctx.fill();
      }
      ctx.restore();
    }

    // paused tint
    if(state==='paused'){
      ctx.fillStyle='rgba(5,10,8,0.45)'; ctx.fillRect(0,0,canvas.width,canvas.height);
      ctx.fillStyle='#e6f0e9'; ctx.font='900 34px system-ui, sans-serif'; ctx.textAlign='center';
      ctx.fillText('PAUSED', canvas.width/2, canvas.height/2 - 6);
      ctx.font='500 15px system-ui, sans-serif'; ctx.fillStyle='#9db3a8';
      ctx.fillText('Press P or tap Pause to resume', canvas.width/2, canvas.height/2+24);
    }
  }

  function loop(now){
    if(state==='demo' || state==='playing'){
      if(now - lastStep >= interval) step(now);
    } else if(state==='over'){
      // keep lastStep far so alpha=1, particles still animate in draw
    }
    draw(now);
    requestAnimationFrame(loop);
  }

  // ---- Input ----
  const keyMap = {
    ArrowUp:{x:0,y:-1}, ArrowDown:{x:0,y:1}, ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0},
    w:{x:0,y:-1}, s:{x:0,y:1}, a:{x:-1,y:0}, d:{x:1,y:0},
    W:{x:0,y:-1}, S:{x:0,y:1}, A:{x:-1,y:0}, D:{x:1,y:0},
  };

  window.addEventListener('keydown', (e)=>{
    const k = e.key;
    if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(k)) e.preventDefault();

    if(k===' ' || k==='Enter'){
      if(state==='over' || state==='demo'){ startGame(); return; }
      if(state==='paused'){ togglePause(); return; }
      return;
    }
    if(k==='p' || k==='P' || k==='Escape'){
      if(state==='playing' || state==='paused') togglePause();
      return;
    }
    const d = keyMap[k];
    if(d){
      if(state==='demo'){ startGame(d); return; }
      if(state==='over'){ startGame(d); return; }
      if(state==='paused'){ togglePause(); }
      if(state==='playing') queueDir(d.x, d.y);
    } else if(state==='demo' && k.length===1){
      startGame();
    }
  });

  function togglePause(){
    if(state==='playing'){
      state='paused';
      showOverlay('PAUSED', 'Take a breath. Your snake is waiting.', true);
      ovScore.textContent = score;
      ovBest.style.display='none';
    } else if(state==='paused'){
      state='playing';
      hideOverlay();
      lastStep = performance.now();
    }
  }

  // Buttons
  document.getElementById('btnRestart').addEventListener('click', ()=>startGame());
  document.getElementById('btnPauseOv').addEventListener('click', togglePause);
  // pause / restart in HUD via overlay? add keyboard hint buttons using overlay pause button handled.
  // Click canvas / overlay to start from demo
  canvas.addEventListener('pointerdown', ()=>{ if(state==='demo') startGame(); canvas.focus({preventScroll:true}); });
  overlay.addEventListener('pointerdown', (e)=>{ if(e.target===overlay && state==='demo') startGame(); });

  // D-pad
  document.querySelectorAll('.dpad button').forEach(btn=>{
    btn.addEventListener('pointerdown', (e)=>{
      e.preventDefault();
      const map={up:{x:0,y:-1},down:{x:0,y:1},left:{x:-1,y:0},right:{x:1,y:0}};
      const d=map[btn.dataset.dir];
      if(state==='demo'||state==='over'){ startGame(d); return; }
      if(state==='paused') togglePause();
      if(state==='playing') queueDir(d.x,d.y);
    });
  });

  // Swipe
  let touchStart=null;
  canvas.addEventListener('touchstart', e=>{ touchStart={x:e.touches[0].clientX,y:e.touches[0].clientY}; }, {passive:true});
  canvas.addEventListener('touchend', e=>{
    if(!touchStart) return;
    const dx=e.changedTouches[0].clientX-touchStart.x, dy=e.changedTouches[0].clientY-touchStart.y;
    touchStart=null;
    if(Math.max(Math.abs(dx),Math.abs(dy))<18) { if(state==='demo') startGame(); return; }
    const d = Math.abs(dx)>Math.abs(dy) ? {x:Math.sign(dx),y:0} : {x:0,y:Math.sign(dy)};
    if(state==='demo'||state==='over'){ startGame(d); return; }
    if(state==='paused') togglePause();
    if(state==='playing') queueDir(d.x,d.y);
  }, {passive:true});

  // Add pause/restart buttons into header actions area dynamically (keeps layout tidy)
  const hud = document.querySelector('.hud');
  // (hud not present; buttons are in overlay + keyboard) - create small floating controls inside frame
  const ctrl = document.createElement('div');
  ctrl.style.cssText='position:absolute;right:18px;bottom:18px;z-index:6;display:flex;gap:6px';
  ctrl.innerHTML='<button id="btnPause" style="background:rgba(7,11,18,.65);backdrop-filter:blur(6px)">❚❚ Pause</button><button id="btnRestart2" style="background:rgba(7,11,18,.65);backdrop-filter:blur(6px)">↻ Restart</button>';
  document.querySelector('.frame').appendChild(ctrl);
  document.getElementById('btnPause').addEventListener('click', ()=>{ if(state==='playing'||state==='paused') togglePause(); else if(state==='demo') startGame(); });
  document.getElementById('btnRestart2').addEventListener('click', ()=>startGame());

  reset(true);
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (1,311 tokens)
Balancing single-file HTML snake game requirements with demo mode overlay and smooth canvas rendering constraints.Adding CSS responsive layout, BFS demo AI, input queueing, interpolation, and particle effects.