Muse Ember
by MetaReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 27.2 s
- Cost
- $0.0081
- Output tokens
- 16,136
- Spent thinking
- 6,705 tokens
- Speed
- 645.3 tok/s
- Console errors
- 0
- First token after
- 2.2 s
- First visible text after
- 16.5 s
- Prompt tokens
- 510
- Code size
- 28.0 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
{"effort":"high"}- 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 · 668 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 — Polished Arcade</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#070b14; --panel:rgba(255,255,255,.06); --border:rgba(255,255,255,.12);
--green:#22c55e; --green2:#4ade80; --cyan:#38bdf8; --red:#ef4444;
--text:#f1f5f9; --muted:#94a3b8;
}
html,body{height:100%}
body{
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
background:
radial-gradient(700px 420px at 15% -5%, rgba(34,197,94,.22), transparent 60%),
radial-gradient(700px 420px at 90% 10%, rgba(56,189,248,.16), transparent 60%),
radial-gradient(600px 500px at 50% 110%, rgba(239,68,68,.10), transparent 60%),
var(--bg);
color:var(--text); display:flex; justify-content:center; padding:14px 12px 18px; overflow-x:hidden;
}
.app{width:100%; max-width:620px; display:flex; flex-direction:column; align-items:center; gap:12px}
/* header */
.top{width:100%; display:flex; align-items:center; justify-content:space-between; gap:12px}
.brand{display:flex; align-items:center; gap:12px}
.logo{
width:48px;height:48px;border-radius:14px;display:grid;place-items:center;font-size:26px;
background:linear-gradient(145deg,#14532d,#052e16); border:1px solid rgba(74,222,128,.45);
box-shadow:0 10px 26px rgba(34,197,94,.25), inset 0 1px 0 rgba(255,255,255,.18);
}
h1{font-size:24px; letter-spacing:.14em; line-height:1}
h1 em{font-style:normal; color:var(--green2)}
.brand p{font-size:12px; color:var(--muted); margin-top:5px; letter-spacing:.02em}
.badge{
font-size:11px; font-weight:800; letter-spacing:.16em; padding:8px 12px; border-radius:999px;
background:rgba(56,189,248,.14); color:#7dd3fc; border:1px solid rgba(56,189,248,.35); white-space:nowrap;
}
.badge.playing{background:rgba(34,197,94,.16); color:#86efac; border-color:rgba(34,197,94,.4)}
.badge.paused{background:rgba(251,191,36,.16); color:#fcd34d; border-color:rgba(251,191,36,.4)}
.badge.over{background:rgba(239,68,68,.16); color:#fca5a5; border-color:rgba(239,68,68,.4)}
/* stats */
.stats{width:100%; display:grid; grid-template-columns:repeat(4,1fr); gap:10px}
.stat{
background:var(--panel); border:1px solid var(--border); border-radius:16px; padding:10px 12px;
backdrop-filter:blur(8px); box-shadow:0 8px 24px rgba(0,0,0,.22);
}
.stat span{display:block; font-size:10px; letter-spacing:.18em; color:var(--muted); font-weight:700}
.stat strong{display:block; font-size:24px; margin-top:3px; line-height:1; font-variant-numeric:tabular-nums}
.stat:first-child strong{color:#86efac}
.stat:nth-child(2) strong{color:#fcd34d}
/* board */
.board-wrap{width:100%; display:flex; justify-content:center}
.board-frame{
position:relative; width:min(520px,94vw,54vh); aspect-ratio:1/1;
border-radius:22px; padding:10px;
background:linear-gradient(160deg, rgba(255,255,255,.10), rgba(255,255,255,.02) 45%, rgba(34,197,94,.08));
border:1px solid rgba(255,255,255,.16);
box-shadow:0 24px 60px rgba(0,0,0,.5), 0 0 0 1px rgba(0,0,0,.35), 0 0 42px rgba(34,197,94,.12);
}
.board-frame.shake{animation:shake .38s ease}
@keyframes shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-7px)}40%{transform:translateX(7px)}60%{transform:translateX(-4px)}80%{transform:translateX(4px)}}
canvas{width:100%; height:100%; display:block; border-radius:14px; background:#0b1220; cursor:pointer; touch-action:none}
.banner{
position:absolute; left:50%; top:22px; transform:translateX(-50%);
display:flex; align-items:center; gap:8px; white-space:nowrap;
font-size:12px; font-weight:800; letter-spacing:.06em;
padding:9px 14px; border-radius:999px; color:#ecfdf5;
background:rgba(5,20,12,.78); border:1px solid rgba(74,222,128,.45);
box-shadow:0 10px 24px rgba(0,0,0,.35); backdrop-filter:blur(6px); pointer-events:none; z-index:3;
}
.pulse-dot{width:9px;height:9px;border-radius:50%;background:#4ade80;box-shadow:0 0 0 0 rgba(74,222,128,.6);animation:pulse 1.4s infinite}
@keyframes pulse{70%{box-shadow:0 0 0 8px rgba(74,222,128,0)}100%{box-shadow:0 0 0 0 rgba(74,222,128,0)}}
.hidden{display:none !important}
.overlay{
position:absolute; inset:10px; border-radius:14px; z-index:4;
display:flex; align-items:center; justify-content:center; padding:18px;
background:rgba(4,8,16,.66); backdrop-filter:blur(5px);
}
.ov-card{
width:min(360px,92%); text-align:center; padding:24px 20px 20px; border-radius:20px;
background:linear-gradient(180deg, rgba(20,30,48,.96), rgba(9,14,26,.96));
border:1px solid rgba(255,255,255,.14); box-shadow:0 22px 48px rgba(0,0,0,.5);
}
.ov-icon{font-size:36px; line-height:1}
.ov-card h2{font-size:28px; letter-spacing:.04em; margin:10px 0 6px}
.ov-card p{color:var(--muted); font-size:13px}
.ov-stats{display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin:16px 0}
.ov-stats div{background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.09); border-radius:12px; padding:10px 4px}
.ov-stats span{display:block; font-size:9px; letter-spacing:.16em; color:var(--muted); font-weight:700}
.ov-stats strong{display:block; font-size:20px; margin-top:4px}
button{font-family:inherit; cursor:pointer; border:none; color:var(--text)}
.primary-btn{
width:100%; padding:13px 16px; border-radius:13px; font-weight:800; letter-spacing:.04em; font-size:15px;
background:linear-gradient(180deg,#4ade80,#16a34a); color:#052e16;
box-shadow:0 12px 24px rgba(34,197,94,.30), inset 0 1px 0 rgba(255,255,255,.45);
}
.primary-btn:hover{filter:brightness(1.07)} .primary-btn:active{transform:translateY(1px)}
.ov-hint{margin-top:12px; font-size:11px !important}
kbd{
display:inline-block; padding:2px 6px; border-radius:6px; font-size:10px; font-family:inherit;
background:rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.18); border-bottom-width:2px; color:#e2e8f0;
}
/* controls */
.controls{display:flex; gap:10px; width:100%; justify-content:center; flex-wrap:wrap}
.controls button{
padding:10px 16px; border-radius:12px; background:var(--panel); border:1px solid var(--border);
font-size:13px; font-weight:700; display:flex; align-items:center; gap:8px;
}
.controls button:hover{background:rgba(255,255,255,.10)}
.hint{font-size:12px; color:var(--muted); text-align:center; line-height:1.7}
/* dpad only on small / touch */
.dpad{display:none; width:168px; grid-template-columns:repeat(3,52px); grid-template-rows:repeat(3,52px); gap:6px; justify-content:center}
.dpad button{border-radius:14px; background:var(--panel); border:1px solid var(--border); font-size:18px}
.dpad button:active{background:rgba(74,222,128,.22)}
.dpad .up{grid-column:2;grid-row:1}.dpad .left{grid-column:1;grid-row:2}.dpad .right{grid-column:3;grid-row:2}.dpad .down{grid-column:2;grid-row:3}
@media (max-width:640px){
.dpad{display:grid}
h1{font-size:20px}.logo{width:42px;height:42px}
.stat strong{font-size:20px}.stat{padding:8px 10px}
.board-frame{width:min(94vw,52vh)}
.banner{font-size:10.5px; padding:7px 10px; top:18px}
.brand p{display:none}
}
@media (max-width:375px){ .stats{gap:7px} .stat span{font-size:8.5px} }
</style>
</head>
<body>
<main class="app">
<header class="top">
<div class="brand">
<div class="logo">🐍</div>
<div><h1>NEON <em>SNAKE</em></h1><p>Classic arcade · smooth movement · particle bursts</p></div>
</div>
<div class="badge" id="stateBadge">● DEMO</div>
</header>
<section class="stats">
<div class="stat"><span>SCORE</span><strong id="scoreEl">0</strong></div>
<div class="stat"><span>BEST</span><strong id="bestEl">0</strong></div>
<div class="stat"><span>LENGTH</span><strong id="lenEl">4</strong></div>
<div class="stat"><span>SPEED</span><strong id="speedEl">×1.0</strong></div>
</section>
<section class="board-wrap">
<div class="board-frame" id="boardFrame">
<canvas id="game" aria-label="Snake game board"></canvas>
<div class="banner" id="demoBanner"><span class="pulse-dot"></span> DEMO — Click or press a key to start</div>
<div class="overlay hidden" id="overlay">
<div class="ov-card">
<div class="ov-icon" id="ovIcon">🏆</div>
<h2 id="ovTitle">Game Over</h2>
<p id="ovDesc">You hit a wall!</p>
<div class="ov-stats">
<div><span>SCORE</span><strong id="ovScore">0</strong></div>
<div><span>BEST</span><strong id="ovBest">0</strong></div>
<div><span>LENGTH</span><strong id="ovLen">0</strong></div>
</div>
<button class="primary-btn" id="ovBtn">Play Again</button>
<p class="ov-hint">Press <kbd>Space</kbd> or <kbd>Enter</kbd> to restart</p>
</div>
</div>
</div>
</section>
<section class="controls">
<button id="pauseBtn">⏸ Pause <kbd>P</kbd></button>
<button id="restartBtn">↻ Restart</button>
<button id="soundBtn">🔊 Sound On</button>
</section>
<p class="hint">Move with <kbd>←</kbd><kbd>↑</kbd><kbd>↓</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> • <kbd>P</kbd> Pause • <kbd>Space</kbd> Restart<br>Quick turns are queued — you can never reverse onto yourself.</p>
<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="right" data-dir="right" aria-label="Right">▶</button>
<button class="down" data-dir="down" aria-label="Down">▼</button>
</div>
</main>
<script>
(() => {
const COLS = 20, ROWS = 20;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const frame = document.getElementById('boardFrame');
const scoreEl = document.getElementById('scoreEl');
const bestEl = document.getElementById('bestEl');
const lenEl = document.getElementById('lenEl');
const speedEl = document.getElementById('speedEl');
const badge = document.getElementById('stateBadge');
const banner = document.getElementById('demoBanner');
const overlay = document.getElementById('overlay');
const ovIcon = document.getElementById('ovIcon');
const ovTitle = document.getElementById('ovTitle');
const ovDesc = document.getElementById('ovDesc');
const ovScore = document.getElementById('ovScore');
const ovBest = document.getElementById('ovBest');
const ovLen = document.getElementById('ovLen');
const ovBtn = document.getElementById('ovBtn');
let boardSize = 400, cell = 20, dpr = 1;
let snake = [], prevSnake = [], food = {x:15,y:10};
let dir = {x:1,y:0};
let queue = [];
let score = 0, best = 0;
let state = 'demo'; // demo | playing | paused | over
let modeDemo = true;
let interval = 145, lastTick = performance.now(), lastFrame = performance.now();
let particles = [], floaters = [];
let demoTimer = null;
let soundOn = true, audioCtx = null;
const DIRS = {
up:{x:0,y:-1}, down:{x:0,y:1}, left:{x:-1,y:0}, right:{x:1,y:0}
};
const keyMap = {
arrowup:'up', w:'up', arrowdown:'down', s:'down',
arrowleft:'left', a:'left', arrowright:'right', d:'right'
};
function resizeCanvas(){
const w = canvas.clientWidth || frame.clientWidth - 20 || 400;
boardSize = w; dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(w * dpr);
cell = w / COLS;
}
window.addEventListener('resize', resizeCanvas);
function intervalForScore(){
const lvl = Math.floor(score / 60);
return Math.max(78, 145 - lvl * 9);
}
function updateHUD(){
scoreEl.textContent = score;
bestEl.textContent = best;
lenEl.textContent = snake.length;
const lvl = Math.floor(score / 60);
speedEl.textContent = '×' + (1 + lvl * 0.25).toFixed(1).replace('.0','');
}
function updateStateUI(){
badge.className = 'badge ' + (state==='playing'?'playing':state==='paused'?'paused':state==='over'?'over':'');
badge.textContent = state==='demo' ? '● DEMO' : state==='playing' ? '● PLAYING' : state==='paused' ? '⏸ PAUSED' : '✕ GAME OVER';
banner.classList.toggle('hidden', state !== 'demo');
if(state !== 'paused' && state !== 'over') overlay.classList.add('hidden');
}
function spawnFood(){
const occupied = new Set(snake.map(s => s.x + s.y*COLS));
const empty = [];
for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++) if(!occupied.has(x+y*COLS)) empty.push({x,y});
food = empty.length ? empty[(Math.random()*empty.length)|0] : {x:0,y:0};
}
function resetRun(demo){
modeDemo = demo;
snake = [{x:10,y:10},{x:9,y:10},{x:8,y:10},{x:7,y:10}];
dir = {x:1,y:0};
queue = [];
score = 0;
particles = []; floaters = [];
interval = intervalForScore();
spawnFood();
prevSnake = snake.map(s=>({...s}));
lastTick = performance.now();
updateHUD();
}
function enqueueDir(d){
const last = queue.length ? queue[queue.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(queue.length < 3) queue.push(d);
}
/* ---------- AI for demo ---------- */
function keyOf(p){ return p.x + p.y*COLS; }
function bfsToFood(){
const head = snake[0];
const body = new Set(snake.slice(0,-1).map(keyOf));
const q = [head];
const seen = new Map(); seen.set(keyOf(head), null);
const dirs = [DIRS.up, DIRS.right, DIRS.down, DIRS.left];
while(q.length){
const cur = q.shift();
if(cur.x===food.x && cur.y===food.y){
// reconstruct
let k = keyOf(cur), path = [];
while(k !== null && k !== keyOf(head)){ path.push(k); k = seen.get(k); }
path.reverse();
if(!path.length) return null;
const firstKey = path[0];
return {x: firstKey % COLS, y: (firstKey / COLS)|0};
}
for(const d of dirs){
if(cur===head && d.x===-dir.x && d.y===-dir.y) continue;
const nx = cur.x+d.x, ny = cur.y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
const k = nx+ny*COLS;
if(seen.has(k) || body.has(k)) continue;
seen.set(k, keyOf(cur));
q.push({x:nx,y:ny});
}
}
return null;
}
function floodCount(start, body){
let count=0; const q=[start]; const seen=new Set([keyOf(start)]);
while(q.length){
const cur=q.shift(); count++;
for(const d of [DIRS.up,DIRS.right,DIRS.down,DIRS.left]){
const nx=cur.x+d.x, ny=cur.y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
const k=nx+ny*COLS;
if(seen.has(k)||body.has(k)) continue;
seen.add(k); q.push({x:nx,y:ny});
}
}
return count;
}
function aiDir(){
const target = bfsToFood();
if(target){
const h = snake[0];
const d = {x: target.x-h.x, y: target.y-h.y};
if(!(d.x===-dir.x && d.y===-dir.y)) return d;
}
const body = new Set(snake.slice(0,-1).map(keyOf));
let bestDir = dir, bestScore = -1;
for(const d of [DIRS.up,DIRS.right,DIRS.down,DIRS.left]){
if(d.x===-dir.x && d.y===-dir.y) continue;
const nx=snake[0].x+d.x, ny=snake[0].y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
if(body.has(nx+ny*COLS)) continue;
const space = floodCount({x:nx,y:ny}, body);
const dist = Math.abs(nx-food.x)+Math.abs(ny-food.y);
const s = space*3 - dist;
if(s>bestScore){ bestScore=s; bestDir=d; }
}
return bestDir;
}
/* ---------- Sound ---------- */
function tone(f, dur=0.09, type='sine', vol=0.12, delay=0){
if(!soundOn || modeDemo) return;
try{
if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if(audioCtx.state==='suspended') audioCtx.resume();
const t = audioCtx.currentTime + delay;
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.frequency.value=f; o.type=type;
g.gain.setValueAtTime(vol, t);
g.gain.exponentialRampToValueAtTime(0.001, t+dur);
o.connect(g); g.connect(audioCtx.destination);
o.start(t); o.stop(t+dur);
}catch(e){}
}
/* ---------- Particles / floaters ---------- */
function burst(cx, cy){
const colors = ['#f87171','#fbbf24','#fb923c','#ffffff','#4ade80'];
for(let i=0;i<22;i++){
const a = Math.random()*Math.PI*2, 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:0, max:0.55+Math.random()*0.45,
size:0.06+Math.random()*0.10, color:colors[(Math.random()*colors.length)|0]
});
}
floaters.push({x:cx, y:cy-0.6, text:'+10', life:0, max:0.8});
}
/* ---------- Game step ---------- */
function step(){
prevSnake = snake.map(s=>({...s}));
if(modeDemo){ dir = aiDir(); }
else 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);
const hitWall = head.x<0||head.y<0||head.x>=COLS||head.y>=ROWS;
const hitSelf = bodyToCheck.some(s=>s.x===head.x && s.y===head.y);
if(hitWall || hitSelf){ gameOver(hitWall?'wall':'self'); return; }
snake.unshift(head);
if(willEat){
score += 10;
if(!modeDemo && score>best) best = score;
interval = intervalForScore();
burst(food.x+0.5, food.y+0.5);
tone(660,0.09,'sine',0.14); tone(990,0.10,'sine',0.10,0.06);
spawnFood();
} else {
snake.pop();
}
updateHUD();
}
function gameOver(cause){
frame.classList.remove('shake'); void frame.offsetWidth; frame.classList.add('shake');
if(modeDemo){
state = 'over';
updateStateUI();
clearTimeout(demoTimer);
demoTimer = setTimeout(()=>{
if(state==='over' && modeDemo){ resetRun(true); state='demo'; updateStateUI(); }
}, 1500);
return;
}
state = 'over';
if(score>best) best = score;
updateHUD(); updateStateUI();
tone(220,0.25,'sawtooth',0.10); tone(140,0.35,'sawtooth',0.08,0.12);
ovIcon.textContent = '💥';
ovTitle.textContent = 'Game Over';
ovDesc.textContent = cause==='wall' ? 'You crashed into a wall!' : 'You bit your own tail!';
ovScore.textContent = score; ovBest.textContent = best; ovLen.textContent = snake.length;
ovBtn.textContent = '▶ Play Again';
overlay.classList.remove('hidden');
}
function startReal(initialDir){
clearTimeout(demoTimer);
resetRun(false);
state = 'playing';
overlay.classList.add('hidden');
if(initialDir) enqueueDir(initialDir);
lastTick = performance.now();
updateStateUI();
tone(520,0.07,'sine',0.08);
}
function togglePause(){
if(state==='playing'){
state='paused';
ovIcon.textContent='⏸'; ovTitle.textContent='Paused';
ovDesc.textContent='Take a breath. The snake is waiting.';
ovScore.textContent=score; ovBest.textContent=best; ovLen.textContent=snake.length;
ovBtn.textContent='▶ Resume';
overlay.classList.remove('hidden');
} else if(state==='paused'){
state='playing'; overlay.classList.add('hidden'); lastTick=performance.now();
} else if(state==='demo'){
startReal(null); togglePause();
}
updateStateUI();
}
/* ---------- Drawing ---------- */
function lerp(a,b,t){ return a+(b-a)*t; }
function drawBoard(){
ctx.fillStyle = '#0b1220';
ctx.fillRect(0,0,boardSize,boardSize);
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*cell,y*cell,cell,cell); }
}
ctx.strokeStyle='rgba(255,255,255,0.045)'; ctx.lineWidth=1;
ctx.beginPath();
for(let i=1;i<COLS;i++){ ctx.moveTo(i*cell,0); ctx.lineTo(i*cell,boardSize); }
for(let i=1;i<ROWS;i++){ ctx.moveTo(0,i*cell); ctx.lineTo(boardSize,i*cell); }
ctx.stroke();
const g = ctx.createRadialGradient(boardSize/2,boardSize/2,boardSize*0.3,boardSize/2,boardSize/2,boardSize*0.75);
g.addColorStop(0,'rgba(0,0,0,0)'); g.addColorStop(1,'rgba(0,0,0,0.28)');
ctx.fillStyle=g; ctx.fillRect(0,0,boardSize,boardSize);
}
function drawFood(now){
const cx=(food.x+0.5)*cell, cy=(food.y+0.5)*cell;
const pulse = 1 + Math.sin(now/280)*0.09;
const r = cell*0.34*pulse;
// glow
const glow = ctx.createRadialGradient(cx,cy,r*0.2,cx,cy,r*2.2);
glow.addColorStop(0,'rgba(239,68,68,0.35)'); glow.addColorStop(1,'rgba(239,68,68,0)');
ctx.fillStyle=glow; ctx.beginPath(); ctx.arc(cx,cy,r*2.2,0,Math.PI*2); ctx.fill();
// apple body
const grad = ctx.createRadialGradient(cx-r*0.35,cy-r*0.4,r*0.2,cx,cy,r);
grad.addColorStop(0,'#fca5a5'); grad.addColorStop(0.45,'#ef4444'); grad.addColorStop(1,'#991b1b');
ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fill();
// shine
ctx.fillStyle='rgba(255,255,255,0.75)';
ctx.beginPath(); ctx.ellipse(cx-r*0.35,cy-r*0.42,r*0.22,r*0.13,-0.6,0,Math.PI*2); ctx.fill();
// stem + leaf
ctx.strokeStyle='#92400e'; ctx.lineWidth=Math.max(2,cell*0.07); ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(cx,cy-r*0.85); ctx.quadraticCurveTo(cx+r*0.12,cy-r*1.25,cx+r*0.3,cy-r*1.35); ctx.stroke();
ctx.fillStyle='#22c55e';
ctx.beginPath(); ctx.ellipse(cx+r*0.42,cy-r*1.18,r*0.34,r*0.16,-0.5,0,Math.PI*2); ctx.fill();
}
function drawSnake(now){
if(!snake.length) return;
const alpha = (state==='playing'||state==='demo') ? Math.min(1, Math.max(0,(now-lastTick)/interval)) : 1;
const pts = snake.map((s,i)=>{
const p = prevSnake[Math.min(i, prevSnake.length-1)] || s;
return {x:(lerp(p.x,s.x,alpha)+0.5)*cell, y:(lerp(p.y,s.y,alpha)+0.5)*cell};
});
ctx.lineCap='round'; ctx.lineJoin='round';
// body as tapered segments (tail -> head)
for(let i=pts.length-1;i>=1;i--){
const t = i/Math.max(1,pts.length-1);
const w = cell*(0.66*(1-0.45*t));
const hue = 145 + t*42; // green -> teal
ctx.strokeStyle = `hsl(${hue} 78% ${52 - t*8}%)`;
ctx.lineWidth = w;
ctx.beginPath(); ctx.moveTo(pts[i].x,pts[i].y); ctx.lineTo(pts[i-1].x,pts[i-1].y); ctx.stroke();
}
// highlight stripe
ctx.strokeStyle='rgba(255,255,255,0.16)'; ctx.lineWidth=cell*0.12;
for(let i=pts.length-1;i>=1;i--){
const t=i/Math.max(1,pts.length-1);
ctx.beginPath(); ctx.moveTo(pts[i].x,pts[i].y-cell*0.10); ctx.lineTo(pts[i-1].x,pts[i-1].y-cell*0.10); ctx.stroke();
}
// head
const h = pts[0];
const hr = cell*0.44;
const hg = ctx.createRadialGradient(h.x-hr*0.3,h.y-hr*0.35,hr*0.2,h.x,h.y,hr);
hg.addColorStop(0,'#a7f3d0'); hg.addColorStop(0.5,'#4ade80'); hg.addColorStop(1,'#16a34a');
ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(h.x,h.y,hr,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.22)'; ctx.lineWidth=1.5; ctx.stroke();
// tongue flick occasionally
if(Math.floor(now/900)%3===0 && (state==='playing'||state==='demo')){
const tx=h.x+dir.x*hr, ty=h.y+dir.y*hr;
ctx.strokeStyle='#f87171'; ctx.lineWidth=Math.max(1.5,cell*0.05);
ctx.beginPath(); ctx.moveTo(tx,ty);
ctx.lineTo(tx+dir.x*cell*0.28, ty+dir.y*cell*0.28);
ctx.moveTo(tx+dir.x*cell*0.28, ty+dir.y*cell*0.28);
ctx.lineTo(tx+dir.x*cell*0.38 - dir.y*cell*0.08, ty+dir.y*cell*0.38 + dir.x*cell*0.08);
ctx.moveTo(tx+dir.x*cell*0.28, ty+dir.y*cell*0.28);
ctx.lineTo(tx+dir.x*cell*0.38 + dir.y*cell*0.08, ty+dir.y*cell*0.38 - dir.x*cell*0.08);
ctx.stroke();
}
// eyes facing direction
const perp = {x:-dir.y, y:dir.x};
for(const side of [-1,1]){
const ex = h.x + dir.x*cell*0.14 + perp.x*cell*0.19*side;
const ey = h.y + dir.y*cell*0.14 + perp.y*cell*0.19*side;
ctx.fillStyle='#ffffff'; ctx.beginPath(); ctx.arc(ex,ey,cell*0.125,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#0f172a';
ctx.beginPath(); ctx.arc(ex+dir.x*cell*0.045, ey+dir.y*cell*0.045, cell*0.065,0,Math.PI*2); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.9)';
ctx.beginPath(); ctx.arc(ex+dir.x*cell*0.045-cell*0.02, ey+dir.y*cell*0.045-cell*0.02, cell*0.02,0,Math.PI*2); ctx.fill();
}
}
function drawParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i]; p.life+=dt;
if(p.life>=p.max){ particles.splice(i,1); continue; }
p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=2.2*dt; p.vx*=0.96;
const a = 1-p.life/p.max;
ctx.globalAlpha=a; ctx.fillStyle=p.color;
ctx.beginPath(); ctx.arc(p.x*cell,p.y*cell,p.size*cell*(0.6+a*0.6),0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
for(let i=floaters.length-1;i>=0;i--){
const f=floaters[i]; f.life+=dt;
if(f.life>=f.max){ floaters.splice(i,1); continue; }
const a=1-f.life/f.max;
ctx.globalAlpha=a; ctx.fillStyle='#fef08a';
ctx.font=`800 ${Math.round(cell*0.55)}px ui-sans-serif,system-ui`;
ctx.textAlign='center';
ctx.fillText(f.text, f.x*cell, (f.y - f.life*1.2)*cell);
}
ctx.globalAlpha=1;
}
function draw(now){
ctx.setTransform(dpr,0,0,dpr,0,0);
drawBoard();
drawFood(now);
drawSnake(now);
}
function loop(now){
const dt = Math.min(0.05,(now-lastFrame)/1000); lastFrame=now;
if(state==='playing'||state==='demo'){
if(now-lastTick >= interval){ step(); lastTick = now; }
}
draw(now);
drawParticles(dt);
requestAnimationFrame(loop);
}
/* ---------- Input ---------- */
function handleDirName(name){
const d = DIRS[name]; if(!d) return;
if(state==='demo'){ startReal(d); return; }
if(state==='over'){ startReal(d); return; }
if(state==='paused'){ state='playing'; overlay.classList.add('hidden'); lastTick=performance.now(); updateStateUI(); enqueueDir(d); return; }
enqueueDir(d);
}
window.addEventListener('keydown', (e)=>{
const k = e.key.toLowerCase();
const isDir = !!keyMap[k];
const isSpace = e.key===' ' || e.code==='Space';
const isEnter = e.key==='Enter';
const isP = k==='p';
if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k) || isSpace) e.preventDefault();
if(isDir){ handleDirName(keyMap[k]); return; }
if(isP){ togglePause(); return; }
if(isSpace || isEnter){
if(state==='demo') startReal(null);
else if(state==='over') startReal(null);
else if(state==='paused') togglePause();
return;
}
});
frame.addEventListener('click', (e)=>{
if(e.target===ovBtn) return;
if(state==='demo') startReal(null);
});
canvas.addEventListener('pointerdown', ()=>{
if(state==='demo') startReal(null);
});
ovBtn.addEventListener('click', (e)=>{
e.stopPropagation();
if(state==='paused') togglePause();
else startReal(null);
});
document.getElementById('pauseBtn').addEventListener('click', togglePause);
document.getElementById('restartBtn').addEventListener('click', ()=>{
if(state==='demo' || state==='over' || state==='paused') startReal(null);
else { clearTimeout(demoTimer); resetRun(false); state='playing'; lastTick=performance.now(); overlay.classList.add('hidden'); updateStateUI(); }
});
document.getElementById('soundBtn').addEventListener('click', (e)=>{
soundOn=!soundOn; e.currentTarget.textContent = soundOn ? '🔊 Sound On' : '🔇 Sound Off';
});
document.querySelectorAll('.dpad button').forEach(btn=>{
btn.addEventListener('pointerdown', (e)=>{ e.preventDefault(); handleDirName(btn.dataset.dir); });
});
// 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.abs(dx)<24 && Math.abs(dy)<24) return;
handleDirName(Math.abs(dx)>Math.abs(dy) ? (dx>0?'right':'left') : (dy>0?'down':'up'));
}, {passive:true});
/* ---------- Boot: self-playing demo ---------- */
resizeCanvas();
resetRun(true);
state='demo';
updateStateUI();
requestAnimationFrame(loop);
// Re-measure after layout settles (fonts / vh)
setTimeout(resizeCanvas, 60);
})();
</script>
</body>
</html>Reasoning trace (6,705 tokens)
Implementing vanilla JS canvas Snake with grid, input handling, scoring, visuals, and demo start mode for 1280x800.Refining canvas sizing, grid choice, interpolation, input queueing, tick timing, and particle visuals.Extending the delta to cover state handling, AI logic, controls, rendering, and mobile layout.Designing the tapered body rendering, head eyes, and overlay states for demo, pause, and game over.Adding input handling, AI pathfinding, timing, resize, and audio for gameplay states.Extending input handling to touch swipes, keyboard focus, and on-screen D-pad controls.Adding pause/resume state handling, sound initialization, input queues, and resize loop logic.Implementing demo-over state with timeout cancellation and mode-aware overlay logic.