Muse Ember
by MetaReasoningClean
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 <kbd>P</kbd> pause <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.