MiniMax M3.1 Flash
by MiniMaxReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 50.6 s
- Cost
- $0.0077
- Output tokens
- 12,726
- Spent thinking
- 5,986 tokens
- Speed
- 254.5 tok/s
- Console errors
- 0
- First token after
- 637 ms
- First visible text after
- 30.8 s
- Prompt tokens
- 677
- Code size
- 19.6 KB
- Finish reason
- stop
- Served by
- Nebius
- Model id
minimax/minimax-m3.1-flash- Reasoning sent
{"effort":"medium"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 599 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Neon Snake</title>
<style>
:root{
--bg:#070a14;
--panel:#0e1426;
--line:rgba(255,255,255,.08);
--green:#8dff6b;
--teal:#25d3c4;
--red:#ff4d6d;
--txt:#e8eeff;
--muted:#8c99bd;
}
*{box-sizing:border-box;}
html,body{height:100%;}
body{
margin:0;
background:
radial-gradient(1200px 700px at 20% -10%, #16204a 0%, transparent 60%),
radial-gradient(900px 600px at 100% 110%, #12313a 0%, transparent 55%),
var(--bg);
color:var(--txt);
font-family:"Trebuchet MS", "Segoe UI", system-ui, -apple-system, sans-serif;
display:flex; align-items:center; justify-content:center;
padding:16px;
-webkit-font-smoothing:antialiased;
user-select:none;
-webkit-tap-highlight-color:transparent;
}
.app{width:100%; display:flex; flex-direction:column; align-items:center; gap:12px;}
header{
width:min(94vw,520px,66vh);
display:flex; align-items:flex-end; justify-content:space-between; gap:12px;
flex-wrap:wrap;
}
.title{
margin:0; font-size:clamp(20px,4.4vw,30px); letter-spacing:.32em; font-weight:800;
background:linear-gradient(90deg,#9dff7a,#38e0d6 60%,#6aa8ff);
-webkit-background-clip:text; background-clip:text; color:transparent;
text-shadow:0 0 22px rgba(80,255,190,.15);
}
.stats{display:flex; gap:10px;}
.stat{
background:linear-gradient(180deg,rgba(255,255,255,.07),rgba(255,255,255,.02));
border:1px solid var(--line);
border-radius:12px; padding:6px 12px; min-width:74px; text-align:center;
box-shadow:inset 0 1px 0 rgba(255,255,255,.07);
}
.stat .k{font-size:10px; letter-spacing:.18em; color:var(--muted); text-transform:uppercase;}
.stat .v{font-size:20px; font-weight:800; font-variant-numeric:tabular-nums; line-height:1.15;}
.stat.score .v{color:var(--green);}
.stat.best .v{color:#ffd166;}
.board{
position:relative;
width:min(94vw,520px,66vh);
aspect-ratio:1/1;
border-radius:20px;
padding:8px;
background:linear-gradient(180deg,rgba(255,255,255,.08),rgba(255,255,255,.02));
border:1px solid rgba(255,255,255,.10);
box-shadow:0 30px 70px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.03) inset, 0 0 60px rgba(60,255,200,.06);
}
canvas{
display:block; width:100%; height:100%;
border-radius:13px; background:#05070f;
touch-action:none;
}
.overlay{
position:absolute; inset:8px;
border-radius:13px;
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px;
text-align:center; padding:20px;
background:radial-gradient(60% 60% at 50% 45%, rgba(8,12,26,.82), rgba(4,6,14,.92));
backdrop-filter:blur(3px);
-webkit-backdrop-filter:blur(3px);
transition:opacity .22s ease;
}
.overlay.hidden{opacity:0; pointer-events:none;}
.ov-title{
font-size:clamp(22px,5.5vw,36px); font-weight:800; letter-spacing:.06em;
color:#fff; text-shadow:0 0 26px rgba(120,255,200,.35);
}
.ov-title.bad{color:#ff8095; text-shadow:0 0 26px rgba(255,80,110,.4);}
.ov-sub{font-size:15px; color:#cfd8f5;}
.ov-keys{
margin-top:6px; font-size:12px; color:var(--muted); letter-spacing:.06em;
border-top:1px solid var(--line); padding-top:10px; max-width:340px;
}
kbd{
background:rgba(255,255,255,.1); border:1px solid rgba(255,255,255,.18);
border-bottom-width:2px; border-radius:6px; padding:2px 7px; font-family:inherit;
font-size:11px; color:#e9eefc; margin:0 2px;
}
.hint{
margin:0; font-size:12px; color:var(--muted); letter-spacing:.05em; text-align:center;
}
.hint b{color:#a9b6dd;}
@media (max-width:420px){
header{width:min(94vw,520px,66vh);}
.title{font-size:20px;}
.stat{padding:5px 9px; min-width:64px;}
.stat .v{font-size:17px;}
}
</style>
</head>
<body>
<div class="app">
<header>
<h1 class="title">SNAKE</h1>
<div class="stats">
<div class="stat score"><div class="k">Score</div><div class="v" id="score">0</div></div>
<div class="stat best"><div class="k">Best</div><div class="v" id="best">0</div></div>
<div class="stat"><div class="k">Length</div><div class="v" id="len">3</div></div>
</div>
</header>
<div class="board" id="board">
<canvas id="cv" width="520" height="520"></canvas>
<div class="overlay" id="ov">
<div class="ov-title" id="ovTitle">Ready?</div>
<div class="ov-sub" id="ovSub">Click or press a key to start</div>
<div class="ov-keys" id="ovKeys">A demo snake is playing in the background — take control at any time.<br><br>
<kbd>←</kbd><kbd>↑</kbd><kbd>↓</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> to steer · <kbd>P</kbd> pause · <kbd>Space</kbd> restart</div>
</div>
</div>
<p class="hint">Eat <b style="color:#ff8095">●</b> to grow — <b>+10</b> points each · walls and your own tail are fatal</p>
</div>
<script>
(function(){
"use strict";
// ---------- setup ----------
const COLS = 24, ROWS = 24;
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const ov = document.getElementById('ov');
const ovTitle = document.getElementById('ovTitle');
const ovSub = document.getElementById('ovSub');
const ovKeys = document.getElementById('ovKeys');
const elScore = document.getElementById('score');
const elBest = document.getElementById('best');
const elLen = document.getElementById('len');
let size = 520, cell = size / COLS, dpr = 1;
const DIRS = [
{x:0,y:-1}, // up
{x:1,y:0}, // right
{x:0,y:1}, // down
{x:-1,y:0} // left
];
const KEYMAP = {
ArrowUp:{x:0,y:-1}, w:{x:0,y:-1}, W:{x:0,y:-1},
ArrowDown:{x:0,y:1}, s:{x:0,y:1}, S:{x:0,y:1},
ArrowLeft:{x:-1,y:0}, a:{x:-1,y:0}, A:{x:-1,y:0},
ArrowRight:{x:1,y:0}, d:{x:1,y:0}, D:{x:1,y:0}
};
// ---------- state ----------
let snake, prev, dir, queue, food, score, best, state, demo, overTimer;
let particles = [], floaters = [];
let acc = 0, last = 0, started = false;
const BASE_STEP = 132, MIN_STEP = 72;
function stepDuration(){
return Math.max(MIN_STEP, BASE_STEP - (score/10) * 4.2);
}
function resetGame(asDemo){
demo = !!asDemo;
snake = [{x:8,y:12},{x:7,y:12},{x:6,y:12}];
prev = snake.map(c=>({x:c.x,y:c.y}));
dir = {x:1,y:0};
queue = [];
score = 0; overTimer = 0;
particles = []; floaters = [];
placeFood();
updateHUD();
}
function placeFood(){
const taken = new Set();
for(const c of snake) taken.add(c.y*COLS+c.x);
const free = [];
for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++) if(!taken.has(y*COLS+x)) free.push({x,y});
food = free.length ? free[(Math.random()*free.length)|0] : null;
}
function updateHUD(){
elScore.textContent = score;
elBest.textContent = best;
elLen.textContent = snake.length;
elScore.animate(
[{transform:'scale(1.25)',color:'#fff'},{transform:'scale(1)',color:'#8dff6b'}],
{duration:260, easing:'ease-out'}
);
}
// ---------- input ----------
function isOpposite(a,b){ return a.x === -b.x && a.y === -b.y; }
function enqueue(d){
if(!d) return;
const ref = queue.length ? queue[queue.length-1] : dir;
if(isOpposite(d, ref)) return; // never reverse into yourself
if(queue.length === 0 && d.x === dir.x && d.y === dir.y) return; // redundant
if(queue.length > 2) return;
queue.push({x:d.x,y:d.y});
}
function setOverlay(title, sub, keys, bad){
ovTitle.textContent = title;
ovTitle.className = 'ov-title' + (bad ? ' bad' : '');
ovSub.innerHTML = sub;
ovKeys.innerHTML = keys;
ov.classList.remove('hidden');
}
function hideOverlay(){ ov.classList.add('hidden'); }
const KEYS_HELP = '<kbd>←</kbd><kbd>↑</kbd><kbd>↓</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> to steer · <kbd>P</kbd> pause · <kbd>Space</kbd> restart';
const OVER_KEYS = 'Press <kbd>Space</kbd> or <kbd>Enter</kbd> to play again · <kbd>P</kbd> pause';
function beginPlay(){
if(state === 'ready' || state === 'over'){
resetGame(false);
state = 'playing';
hideOverlay();
}
}
function togglePause(){
if(state === 'playing'){
state = 'paused';
setOverlay('Paused','Take a breath.', KEYS_HELP);
} else if(state === 'paused'){
state = 'playing';
hideOverlay();
}
}
window.addEventListener('keydown', function(e){
const k = e.key;
if(k === ' ' || k === 'Spacebar') e.preventDefault();
if(k === 'p' || k === 'P'){
if(state === 'playing' || state === 'paused'){ togglePause(); }
return;
}
if(k === ' ' || k === 'Enter'){
if(state === 'ready'){ beginPlay(); }
else if(state === 'over'){ beginPlay(); }
else if(state === 'paused'){ togglePause(); }
return;
}
const d = KEYMAP[k];
if(d){
if(state === 'ready'){ beginPlay(); }
if(state === 'playing'){ enqueue(d); }
e.preventDefault();
}
}, {passive:false});
cv.addEventListener('pointerdown', function(){
if(state === 'ready' || state === 'over') beginPlay();
else if(state === 'paused') togglePause();
});
ov.addEventListener('pointerdown', function(){ if(state!=='paused') beginPlay(); });
document.addEventListener('pointerdown', function(){
if(state === 'ready' || state === 'over') beginPlay();
});
// swipe support
let tsx=0, tsy=0, swiped=false;
cv.addEventListener('touchstart', function(e){
const t = e.changedTouches[0]; tsx = t.clientX; tsy = t.clientY; swiped = true;
}, {passive:true});
cv.addEventListener('touchend', function(e){
if(!swiped) return; swiped = false;
const t = e.changedTouches[0];
const dx = t.clientX - tsx, dy = t.clientY - tsy;
if(Math.abs(dx) < 18 && Math.abs(dy) < 18) return;
if(Math.abs(dx) > Math.abs(dy)) enqueue(dx>0?DIRS[1]:DIRS[3]);
else enqueue(dy>0?DIRS[2]:DIRS[0]);
if(state === 'ready') beginPlay();
}, {passive:true});
// ---------- AI (demo mode) ----------
function bodyBlockedSet(skipTail){
const s = new Set();
const n = skipTail ? snake.length-1 : snake.length;
for(let i=0;i<n;i++) s.add(snake[i].y*COLS+snake[i].x);
return s;
}
function pathExists(sx, sy){
if(!food) return false;
if(sx === food.x && sy === food.y) return true;
const blocked = bodyBlockedSet(true);
const seen = new Set([sy*COLS+sx]);
const q = [[sx,sy]];
let head = 0;
while(head < q.length){
const cur = q[head++];
const x = cur[0], y = cur[1];
for(let i=0;i<4;i++){
const nx = x + DIRS[i].x, ny = y + DIRS[i].y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
const k = ny*COLS+nx;
if(seen.has(k) || blocked.has(k)) continue;
if(nx === food.x && ny === food.y) return true;
seen.add(k); q.push([nx,ny]);
}
}
return false;
}
function aiDir(){
if(!food) return dir;
const h = snake[0];
const blocked = bodyBlockedSet(true);
let bestD = null, bestS = -1e9;
for(let i=0;i<4;i++){
const d = DIRS[i];
if(isOpposite(d, dir)) continue;
const nx = h.x + d.x, ny = h.y + d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
if(blocked.has(ny*COLS+nx)) continue;
let s = 0;
const dist = Math.abs(nx-food.x) + Math.abs(ny-food.y);
s += pathExists(nx,ny) ? 1000 : -600;
s -= dist * 4;
let free = 0;
for(let j=0;j<4;j++){
const ax = nx+DIRS[j].x, ay = ny+DIRS[j].y;
if(ax>=0&&ay>=0&&ax<COLS&&ay<ROWS && !blocked.has(ay*COLS+ax)) free++;
}
s += free * 10;
if(d.x === dir.x && d.y === dir.y) s += 8;
if(s > bestS){ bestS = s; bestD = d; }
}
return bestD || dir;
}
// ---------- game logic ----------
function doStep(){
if(state === 'over') return;
if(demo) enqueue(aiDir());
let nd = dir;
if(queue.length) nd = queue.shift();
dir = nd;
const h = snake[0];
const nx = h.x + dir.x, ny = h.y + dir.y;
const eating = !!(food && nx === food.x && ny === food.y);
prev = snake.map(c=>({x:c.x,y:c.y}));
if(nx<0||ny<0||nx>=COLS||ny>=ROWS){ die(); return; }
const body = eating ? snake : snake.slice(0, snake.length-1);
for(let i=0;i<body.length;i++){
if(body[i].x === nx && body[i].y === ny){ die(); return; }
}
snake.unshift({x:nx,y:ny});
if(eating){
score += 10;
if(score > best) best = score;
burst(food);
floaters.push({x:(food.x+0.5)*cell, y:(food.y+0.3)*cell, t:'+10', life:1});
placeFood();
updateHUD();
} else {
snake.pop();
}
}
function die(){
state = 'over';
overTimer = 0;
setOverlay('Game Over',
'Score <b style="color:#8dff6b">'+score+'</b> · Best <b style="color:#ffd166">'+best+'</b> · Length '+snake.length,
OVER_KEYS, true);
}
function burst(f){
const cx = (f.x+0.5)*cell, cy = (f.y+0.5)*cell;
for(let i=0;i<16;i++){
const a = Math.random()*Math.PI*2;
const sp = 40 + Math.random()*170;
particles.push({
x:cx, y:cy,
vx:Math.cos(a)*sp, vy:Math.sin(a)*sp - 30,
size: 1.6 + Math.random()*3.2,
life: 0.45 + Math.random()*0.45, max: 0.9,
c: Math.random() < .5 ? '#ff5d7a' : (Math.random()<.5 ? '#ffd166' : '#ffffff')
});
}
}
// ---------- rendering ----------
function resize(){
const rect = cv.getBoundingClientRect();
const w = Math.max(100, rect.width);
dpr = Math.min(2, window.devicePixelRatio || 1);
size = w; cell = w / COLS;
cv.width = Math.round(w*dpr); cv.height = Math.round(w*dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
}
window.addEventListener('resize', resize);
if(window.ResizeObserver) new ResizeObserver(resize).observe(cv);
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 render(t){
const S = size, C = cell;
ctx.clearRect(0,0,S,S);
// background
const bg = ctx.createLinearGradient(0,0,S,S);
bg.addColorStop(0,'#0a0f20'); bg.addColorStop(1,'#060a13');
ctx.fillStyle = bg; ctx.fillRect(0,0,S,S);
// grid
ctx.strokeStyle = 'rgba(255,255,255,.045)';
ctx.lineWidth = 1;
ctx.beginPath();
for(let i=1;i<COLS;i++){
const p = Math.round(i*C)+0.5;
ctx.moveTo(p,0); ctx.lineTo(p,S);
ctx.moveTo(0,p); ctx.lineTo(S,p);
}
ctx.stroke();
// border glow
ctx.strokeStyle = 'rgba(120,255,200,.14)'; ctx.lineWidth = 2;
ctx.strokeRect(1,1,S-2,S-2);
// food
if(food){
const fx = (food.x+0.5)*C, fy = (food.y+0.5)*C;
const pulse = 1 + Math.sin(performance.now()/260)*0.09;
const r = C*0.30*pulse;
ctx.save();
ctx.shadowColor = 'rgba(255,77,109,.9)'; ctx.shadowBlur = C*0.9;
const g = ctx.createRadialGradient(fx-r*0.35, fy-r*0.4, r*0.15, fx, fy, r);
g.addColorStop(0,'#ffd0d8'); g.addColorStop(0.45,'#ff5d7a'); g.addColorStop(1,'#b6123a');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(fx,fy,r,0,Math.PI*2); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle='rgba(255,255,255,.75)';
ctx.beginPath(); ctx.arc(fx-r*0.34, fy-r*0.38, r*0.22, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
// snake
const pts = [];
for(let i=0;i<snake.length;i++){
const c = snake[i], p = prev[i] || c;
const tt = (i === 0) ? Math.min(1, t) : Math.min(1, t);
const x = (p.x + (c.x-p.x)*tt + 0.5) * C;
const y = (p.y + (c.y-p.y)*tt + 0.5) * C;
pts.push({x:x,y:y});
}
if(pts.length > 1){
const head = pts[0], tail = pts[pts.length-1];
const g = ctx.createLinearGradient(head.x, head.y, tail.x, tail.y);
g.addColorStop(0,'#b6ff7a');
g.addColorStop(0.35,'#5fe88f');
g.addColorStop(1,'#13a6b8');
ctx.save();
ctx.lineJoin='round'; ctx.lineCap='round';
ctx.shadowColor='rgba(70,240,180,.35)'; ctx.shadowBlur=C*0.5;
ctx.strokeStyle=g; ctx.lineWidth=C*0.74;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for(let i=1;i<pts.length;i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.stroke();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,.14)'; ctx.lineWidth=C*0.24;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for(let i=1;i<pts.length;i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.stroke();
ctx.restore();
}
// head
const h = pts[0];
const hr = C*0.44;
ctx.save();
ctx.shadowColor='rgba(160,255,120,.75)'; ctx.shadowBlur=C*0.8;
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,'#eaffd0'); hg.addColorStop(0.5,'#a8ff77'); hg.addColorStop(1,'#57d98a');
ctx.fillStyle=hg;
ctx.beginPath(); ctx.arc(h.x,h.y,hr,0,Math.PI*2); ctx.fill();
ctx.shadowBlur=0;
// eyes
const fx = dir.x, fy = dir.y;
const px = -fy, py = fx;
for(let s=-1;s<=1;s+=2){
const ex = h.x + fx*C*0.16 + px*s*C*0.20;
const ey = h.y + fy*C*0.16 + py*s*C*0.20;
ctx.fillStyle='#0b1020';
ctx.beginPath(); ctx.arc(ex,ey,C*0.135,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#ffffff';
ctx.beginPath(); ctx.arc(ex - fx*C*0.03 - px*s*C*0.02, ey - fy*C*0.03 - py*s*C*0.02, C*0.085,0,Math.PI*2); ctx.fill();
}
ctx.restore();
// particles
for(let i=0;i<particles.length;i++){
const p = particles[i];
const a = Math.max(0, p.life/p.max);
ctx.globalAlpha = a;
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x,p.y,p.size*(0.4+a*0.9),0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
// floating text
for(let i=0;i<floaters.length;i++){
const f = floaters[i];
ctx.globalAlpha = Math.max(0, f.life);
ctx.fillStyle = '#ffd166';
ctx.font = 'bold ' + Math.round(C*0.62) + 'px Trebuchet MS, sans-serif';
ctx.textAlign='center';
ctx.fillText(f.t, f.x, f.y);
}
ctx.globalAlpha = 1;
}
// ---------- main loop ----------
function frame(ts){
requestAnimationFrame(frame);
if(!last) last = ts;
let dt = ts - last; last = ts;
if(dt > 60) dt = 60;
// update particles / floaters always
for(let i=particles.length-1;i>=0;i--){
const p = particles[i];
p.life -= dt/1000;
p.x += p.vx*dt/1000; p.y += p.vy*dt/1000; p.vy += 520*dt/1000;
if(p.life <= 0) particles.splice(i,1);
}
for(let i=floaters.length-1;i>=0;i--){
const f = floaters[i];
f.life -= dt/900; f.y -= dt*0.06;
if(f.life <= 0) floaters.splice(i,1);
}
if(state === 'playing' || state === 'ready'){
const sd = stepDuration();
acc += dt;
let guard = 0;
while(acc >= sd && guard < 4){
acc -= sd; guard++;
doStep();
if(state === 'over') break;
}
} else if(state === 'over' && demo){
overTimer += dt;
if(overTimer > 1100){
resetGame(true);
state = 'ready';
setOverlay('Ready?', 'Click or press a key to start',
'A demo snake is playing in the background — take control at any time.<br><br>'+KEYS_HELP);
}
}
const t = stepDuration() > 0 ? Math.min(1, acc/stepDuration()) : 1;
render(t);
}
// ---------- boot ----------
resize();
resetGame(true);
state = 'ready';
setOverlay('Ready?', 'Click or press a key to start',
'A demo snake is playing in the background — take control at any time.<br><br>'+KEYS_HELP);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (5,986 tokens)
Let me build a polished Snake game.
Key features:
- Canvas grid 24x24 cells
- Smooth movement (interpolation between grid steps)
- Input queue for quick successive presses
- Food, particles, score, best score
- Overlay for game over, pause
- Responsive down to 375px
Let me write it carefully.
Design: dark theme, neon green snake, glowing eyes, rounded segments, gradient food, particle burst.
Implementation details:
- Grid size 24, cell size computed from canvas size.
- Canvas CSS size responsive; internal resolution = css size * dpr.
- Use requestAnimationFrame with fixed step logic and interpolation.
Snake represented as array of {x,y} grid cells, head at index 0. Move: compute new head, unshift, pop.
Smooth movement: keep prevPositions and a progress t between steps. Render with interpolated positions.
Let me do: snake array in grid coords. Each frame, stepTimer advances by dt. When stepTimer >= stepDuration, perform a grid step. For rendering, compute `t = stepTimer/stepDuration` and lerp between previous snake cells and current cells. Simpler: keep `renderSnake` array where each segment's position is lerp from previous cell to current cell. i.e., for index i, current cell = snake[i], previous cell = snake[i+1] (the cell it came from). Interpolate from snake[i+1] to snake[i] by t? Actually when snake moves, head goes to new cell, and body segment i takes cell that was segment i-1's cell. So segment i moves from snake[i+1] to snake[i]. Yes: before step, segments at positions p0..pn. After step: new positions p0'=newHead, p1'=p0, p2'=p1... So segment i moves from old p_i to new p_{i+1}... hmm.
Let me define after a step, snake[i] is current grid position of segment i. Before that step, snake_old[i]. Segment i's previous position = snake_old[i]. Since snake_new[i] = snake_old[i-1] for i>=1, segment i travels from snake_old[i] to snake_old[i-1]. So if I store prev array copy before step, then lerp(prev[i], cur[i]) with t gives smooth movement. Segment i: from prev[i] to cur[i] where cur[i]=prev[i-1]. Good.
For rendering, at t=0 render prev positions, at t=1 render cur. t = stepTimer/stepDuration.
Input queue: queue of directions pressed. On key press, validate against the last queued direction (or current direction if queue empty) — not reversing. Also prevent 180 relative to current dir. Use queue length cap.
Actually to honor quick presses: if queue has pending, compare to queue's last element; else compare to current direction. Also disallow pressing same as current direction? Allow but useless; better skip duplicates to avoid wasting steps. Hmm, if user presses Up then Left quickly, queue = [up, left]. Good.
But careful: after queue processing, direction changes. Direction validity check against queued last.
Also one issue: if direction turns, snake body segments follow the old path; that's fine, classic snake uses grid movement.
Walls: game over on hitting wall. Optionally wrap. Requirement says game over when hitting wall, so solid walls.
Particles: on eat, burst of small circles with velocity, gravity, fade.
Food: never spawn on snake; use free cells list.
Best score session: variable bestScore in memory (no localStorage allowed).
Overlay: "Click or press a key to start" initially, game over overlay with score, pause overlay.
Keyboard events require focus/click: add click listener on document to start, and show message. The requirement says "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". So before start, an AI plays automatically. Let's implement AI autopilot: picks direction that avoids collision, preferring toward food. Runs in demo mode.
So: state = 'demo' | 'playing' | 'paused' | 'over'. On load, demo mode with AI controlling. Click or key press → starts playing (real user control), and shows instructions. If game over in demo, maybe restart demo automatically? Better: in demo, if game over, restart demo after short delay so it keeps playing. Then user click takes over.
Let me implement AI: for each candidate direction (not reverse), simulate moving head, check collision with body (body excluding tail? tail moves away, so tail cell is safe if not growing). Compute path safety via a simple flood/lookup; pick the direction that moves toward food with a safety check. Use BFS from head to food avoiding body to ensure reachable. Simpler heuristic: choose direction that minimizes distance to food, but verify the resulting cell is safe and that there's an escape. To be robust, do BFS from candidate next cell to food treating body as obstacles (tail as free). If found, prefer that direction; else pick any safe direction.
I'll implement BFS on grid with obstacles = snake cells except tail.
Let's also add speed increase slightly as score grows (nice polish), keep step duration base 130ms decreasing to 70ms.
Rendering:
- background grid lines subtle
- food: pulsing glowing circle with gradient
- snake: rounded rects, gradient head brighter, eyes with pupils facing direction, tongue? Keep simple.
- Connect segments: draw thick line through interpolated points for continuity plus rounded rects. I'll draw a rounded polyline path with lineJoin round, lineCap round for the body, then head as circle/rounded square with eyes.
Interpolated points: for segment i, center = lerp(prev[i], cur[i], t). Draw stroke path through centers with lineWidth = cell*0.78, round joins. Then head = first center, draw circle radius cell*0.44 with eyes.
Eyes: two white circles offset perpendicular to direction, plus dark pupils offset toward direction.
Score display in HTML above canvas.
Responsive: canvas CSS width = min(560px, 90vw, calc(100vh - 220px)). Use JS to set size. Grid 24x24 => cell size = size/24.
Let's compute: container centered. Use CSS: .wrap { display:flex; flex-direction:column; align-items:center; } canvas { width: min(560px, 92vw, 62vh); aspect-ratio: 1; }
But internal canvas resolution needs to match for crispness. I'll set canvas.width = size*dpr each resize and scale context.
Let me write code.
Also mobile touch? optional swipe support — add for polish.
Let me write.
Colors: background #0b1020, grid lines rgba(255,255,255,0.03), snake body gradient teal-green, head #b8ff6a, food #ff4d6d pulsing.
HUD: score, best, level/speed.
Game over overlay: HTML div overlay on top of canvas with backdrop blur.
Let me code now.
Details for step:
```
function step(){
const dir = queue.length ? queue.shift() : dir;
...
}
```
Wait: direction should be applied from queue at the time of the step. Current direction `dir`. If queue non-empty, take queue.shift() as new dir (already validated). But validation should have been done at press time against last queued/ current. However the queued direction was validated relative to dir at press time, which could differ by the time step happens. E.g., dir=right, queue=[up], then next step dir becomes up; then user presses left — validated against queue last (up) → left vs up is not a reverse, allowed. Then next step dir=left. Fine. But pressing left when dir=right and queue empty → rejected (reverse). Then user presses up: validated against current dir right → allowed, queue=[up]. ok.
Edge: dir=right, queue=[up] pending, user presses right again? validated vs queue last 'up' → 'right' vs 'up' ok, allowed → queue=[up,right]. After step dir=up, next step dir=right. Fine.
Problem: press 'up' then 'down' quickly when dir=right: 'down' vs queue last 'up' → reverse → rejected. Good.
So validation function: reference = queue.length ? queue[queue.length-1] : dir; if newDir is opposite of reference → reject; if newDir equals reference → could skip (redundant) — but if equal to current dir and queue empty, skip is fine. But careful: dir=right, queue empty, press right → skip fine. But dir=right, queue=[up], press right: keep it, since after up, right is needed. So only skip if equals reference AND queue empty. Actually if newDir equals reference, pushing it is harmless but consumes a step. Let's: if queue.length===0 && newDir===dir → ignore. Else push (after reverse check).
Hmm but pushing a dir equal to queue last with queue non-empty: dir=right, queue=[up], press up → queue=[up,up]. Fine, redundant but harmless.
Reverse check: opp(a,b) i.e. a.x === -b.x && a.y === -b.y.
Also limit queue length to 3.
Direction handling: snake head at cell; movement: newHead = head + dir. Note: my snake array — I'll store as array with head first (unshift new head). Fine.
Eating: if newHead equals food → don't pop tail (grow), score+10, particles.
Collision: newHead out of bounds → game over. newHead in body (excluding tail when not growing) → game over. With array unshift approach: after unshift, body = snake.slice(0, len- (growing?0:1)). Check against that.
Implementation:
```
const eating = food && newHead.x===food.x && newHead.y===food.y;
snake.unshift(newHead);
if(!eating) snake.pop();
else { score+=10; particles...; placeFood(); }
```
Collision check before unshift: check newHead against snake cells that will remain: snake.slice(0, eating?snake.length:snake.length-1).
Wait if eating, tail stays, so remaining body = snake (all old). If not eating, body = snake minus last.
Let's check newHead against snake.slice(0, eating ? snake.length : snake.length-1) — for eating case tail included. ok.
Render interpolation: keep prev = copy of snake before step.
Let me write the whole file.
AI function:
```
function aiDir(){
const head = snake[0];
const bodySet = new Set(snake.slice(0, snake.length-1).map(c=>c.y*COLS+c.x)); // tail excluded
const cands = DIRS.filter(d=> !(d.x===-dir.x && d.y===-dir.y));
// score each
let best=null, bestScore=-Infinity;
for(const d of cands){
const nx=head.x+d.x, ny=head.y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
if(bodySet.has(ny*COLS+nx)) continue;
const dist = Math.abs(nx-food.x)+Math.abs(ny-food.y);
// safety: can we still find path? use reachability check
let s = -dist;
// bonus for path existence
if (pathExists(nx,ny)) s += 100;
if(s>bestScore){bestScore=s;best=d;}
}
if(!best) return dir; // will die
return best;
}
```
pathExists: BFS from (nx,ny) to food avoiding bodySet. If food adjacent-ish. Implement BFS.
Also AI should prefer to continue if no improvement.
Careful: the AI's chosen dir is validated as not reverse, but also must not be reverse of current dir (which it filters). Good.
But the AI direction must also be queued/valid: push into queue? In demo, set dir directly: use queue mechanism: queue = [aiDir()] each step? But the reverse-check inside aiDir uses current dir. Fine — I'll set queue.push(d) if d !== dir and not opposite... Actually simplest: in demo mode, before each step compute d = aiDir(); if d differs from dir, set queue=[d]? But queue validation happens at push. Let's just call enqueue(d) which validates against queue last or dir. Since queue empty each step, validates against dir. AI already excludes reverse. Good.
Hmm, but if AI dir equals dir, enqueue ignores (queue empty && equal) → dir stays. fine.
In demo mode, the food might be unreachable etc. Fine.
Also in demo mode, when snake length grows, AI needs to handle. BFS approach handles.
Game over in demo: after 1s, restart demo (reset score? keep best). Let's restart demo automatically so the demo keeps playing — but requirement says game over overlay shows score and Space/Enter restarts. In demo, auto-restart after ~1.2s, showing overlay briefly. When user takes control (click), game over → overlay with Space/Enter restart.
Let me structure:
states: 'ready' (showing start message, demo playing behind), 'playing', 'paused', 'over'.
On load: state='ready', demo=true, overlay message "Click or press a key to start". Demo plays behind the overlay (overlay semi-transparent so you can see). Actually "behind a 'Click or press a key to start' message" — yes, show overlay with translucent backdrop so demo visible.
On key/click: if state==='ready' → start playing (reset? keep current snake? Probably restart fresh with score 0). Let's reset the game fresh and set demo=false, state='playing'. If state==='over' → Space/Enter restarts. Any key? spec: Space or Enter restarts. I'll make Space/Enter restart; also allow click to restart.
Pause: P key toggles. In ready state, P shouldn't matter.
Let me write HUD: SCORE, BEST, and maybe LENGTH.
Now write the code.
Particle system: array of {x,y (pixel), vx,vy,life,size,color}.
Rendering snake with gradient along body: create linear gradient from head to tail? Use a simple per-segment color interpolate by index.
Let me draw:
- path through centers: stroke with lineWidth cell*0.72, lineJoin/lineCap round, strokeStyle gradient (green at head → teal at tail).
- Then draw a highlight: thinner lighter stroke offset? Keep simple: draw second stroke width cell*0.3 with rgba(255,255,255,0.12) for shine.
- Head: circle at head center radius cell*0.44 filled with brighter color + glow shadow.
- Eyes: white ellipses at offset perpendicular ± cell*0.16 from head center, forward offset cell*0.12; pupils dark circles radius cell*0.09 offset forward.
Direction for eyes = current dir (the direction of travel). Use dir variable.
Food: circle radius cell*0.3 with radial gradient red/orange, pulsing scale, glow, plus small shine.
Grid: subtle lines.
Also draw a faint border.
Let's produce code.
For canvas sizing: use a ResizeObserver or window resize; compute size = canvas.clientWidth; set canvas.width = size*dpr; canvas.height=size*dpr; ctx.setTransform(dpr,0,0,dpr,0,0). cell = size/COLS.
Wrap canvas in a div with position relative for overlay.
CSS for overlay: absolute inset 0, flex center, backdrop-filter blur(4px), background rgba(8,10,25,.72), border-radius.
Fonts: use system monospace / "Segoe UI". No external fonts.
Write final HTML.
Also touch: swipe on canvas.
Let me write the code fully.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Neon Snake</title>
<style>...</style>
</head>
<body>
<div class="app">
<header>
<h1>SNAKE</h1>
<div class="stats">...</div>
</header>
<div class="board" id="board">
<canvas id="cv"></canvas>
<div class="overlay" id="ov"> ... </div>
</div>
<p class="hint">Arrows / WASD to move · P pause · Space restart</p>
</div>
<script>...</script>
</body>
```
Now JS.
I'll write carefully.
Best score session variable best = 0.
Time handling: last timestamp; dt clamp.
Loop:
```
function frame(ts){
requestAnimationFrame(frame);
const dt = Math.min(50, ts-last); last=ts;
if(state==='playing'||state==='ready'){
if(state==='ready'){ // demo ai
// choose dir each step
}
acc += dt;
stepDur = current;
while(acc>=stepDur){ acc-=stepDur; doStep(); }
}
t = acc/stepDur; render(t);
}
```
But doStep may trigger game over → state 'over'. Handle.
Interpolation t should use min(1, acc/stepDur).
For AI: compute the desired dir just before doStep (when acc >= stepDur). I'll compute AI dir at the moment of step.
doStep:
```
function doStep(){
if(state==='over') return;
if(demo){
const d = aiDir();
enqueue(d);
}
let nd = dir;
if(queue.length) nd = queue.shift();
dir = nd;
const head = snake[0];
const nx = head.x+dir.x, ny = head.y+dir.y;
const eating = food && nx===food.x && ny===food.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS){ prev = snake.slice(); die(); return;}
const body = eating ? snake : snake.slice(0,-1);
for(const c of body){ if(c.x===nx&&c.y===ny){ prev=snake.slice(); die(); return; } }
prev = snake.slice();
snake.unshift({x:nx,y:ny});
if(eating){ score+=10; best=Math.max(best,score); spawnParticles(...); placeFood(); updateHUD(); }
else snake.pop();
}
```
Note prev must be copy of snake BEFORE unshift for interpolation: segment i's previous pos = prev[i], current = snake[i]. Yes prev = old array copy, snake after unshift/pop. Then lerp(prev[i], snake[i], t). For segment i, snake[i] = old[i-1] for i>=1 → so segment i moves from old[i] to old[i-1]. Correct direction of travel (toward head). Good.
At t=0 it renders prev positions = old positions, which is exactly previous frame's end. Continuous.
But careful when growing: new tail segment index = old length, prev doesn't have that index → prev[i] undefined → use snake[i] (no interpolation, appears at its cell). Handle: if(!prev[i]) use snake[i].
die(): state='over', show overlay.
placeFood(): collect free cells; random pick.
Particles: burst at food cell center in pixels. Need cell size: compute at spawn time using current canvas size.
Also add a floating "+10" text animation? Nice polish: floating score text. I'll add texts array.
Let me write render:
```
function render(t){
const S=size, C=cell;
ctx.clearRect(0,0,S,S);
// bg
ctx.fillStyle='#080b18'; fillRect
// grid
...
// food
// particles behind? draw particles on top.
// snake
}
```
For glow use ctx.shadowBlur.
Let me now finalize code text. Also handle window blur → pause? Optional; skip or auto-pause when state playing and hidden. I'll auto-pause on visibilitychange hidden.
Overlay content: title, subtitle, hint. Update via function showOverlay(title, sub, hint).
Ready overlay: "Click or press a key to start" + "Demo playing — take control".
Game over: "Game Over" + "Score: X" + "Best: Y" + "Press Space or Enter to restart".
Paused: "Paused" + "Press P to resume".
Now, "The iframe only receives keyboard events after the user clicks it" — so we add document click listener to start playing; also keydown listener.
Write the enqueue with reverse check.
DIRS = [{x:0,y:-1},{x:1,y:0},{x:0,y:1},{x:-1,y:0}].
opposite check.
Key mapping: ArrowUp/w/W → {0,-1}, etc.
Now write AI BFS:
```
function pathExists(sx,sy){
if(sx===food.x&&sy===food.y) return true;
const blocked = new Set();
for(let i=0;i<snake.length-1;i++) blocked.add(snake[i].y*COLS+snake[i].x);
// start cell may be blocked? it's the candidate next cell which we already know is free
const q=[[sx,sy]], seen=new Set([sy*COLS+sx]);
while(q.length){
const [x,y]=q.shift();
for(const d of DIRS){
const nx=x+d.x, ny=y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
const k=ny*COLS+nx;
if(seen.has(k)||blocked.has(k)) continue;
if(nx===food.x&&ny===food.y) return true;
seen.add(k); q.push([nx,ny]);
}
}
return false;
}
```
BFS over 576 cells fine.
AI scoring: prefer pathExists, then min distance, then prefer current dir as tiebreak.
Also avoid tight spots: count free neighbors.
Let me write aiDir:
```
function aiDir(){
const head=snake[0];
const blocked=new Set();
for(let i=0;i<snake.length-1;i++) blocked.add(snake[i].y*COLS+snake[i].x);
let best=null,bestScore=-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(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
if(blocked.has(ny*COLS+nx)) continue;
let s = 0;
const dist = Math.abs(nx-food.x)+Math.abs(ny-food.y);
if(pathExists(nx,ny)) s += 1000; else s -= 500;
s -= dist*3;
// free space count
let free=0;
for(const dd of DIRS){const ax=nx+dd.x,ay=ny+dd.y; if(ax>=0&&ay>=0&&ax<COLS&&ay<ROWS&&!blocked.has(ay*COLS+ax))free++;}
s += free*8;
if(d.x===dir.x&&d.y===dir.y) s+=6;
if(s>bestScore){bestScore=s;best=d;}
}
return best||dir;
}
```
Note blocked includes head? snake[0..len-2] includes head (index 0) — head cell is blocked, that's fine since we move away. But pathExists start cell (nx,ny) not blocked.
Careful: pathExists blocks snake[0..len-2], which includes head; that's ok.
Edge: The AI may die anyway; demo restarts.
Demo auto-restart: in die(), if demo → schedule restart after 1000ms. I'll handle in frame: if(state==='over' && demo) { overTimer += dt; if(overTimer>1000) resetGame(true) }.
resetGame(demo): snake reset, score 0, food placed, prev=snake copy, particles cleared, dir right, queue empty.
Also in 'over' state show overlay with score.
Now updateHUD on score change.
Also when user takes control from ready: resetGame(false) and state='playing'. But then the score resets — fine. Show small "You control the snake" flash? The overlay hides.
Let's code the sizes:
```
const cv=document.getElementById('cv');
function resize(){
const rect = cv.getBoundingClientRect();
const dpr = Math.min(2, window.devicePixelRatio||1);
size = rect.width;
cv.width = Math.round(size*dpr); cv.height=Math.round(size*dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
cell = size/COLS;
}
```
Use ResizeObserver on canvas or window resize. Canvas CSS width set by CSS: width:100%; aspect-ratio 1. The .board has width min(560px, 94vw, 60vh?) Let's set .board{width:min(92vw, 560px)} and canvas width:100%; height:auto; aspect-ratio:1/1; display:block.
But height limit for 800px viewport: header ~ 90px + hint 30 + padding. 560 board + 90 + 40 = 690 fits in 800. Good. Maybe use min(92vw, 520px, 62vh) to be safe. Use `width: min(92vw, 520px, 58vh)`. At 800 height, 58vh=464 → board 464, plus header fits nicely. Hmm maybe a bit small. Let's use min(92vw,520px,66vh) → 528 at 800 → cap 520. Total ~ 520+120=640 fine. Use that.
Canvas must have attribute width/height initially to avoid layout shift — set width=520 height=520 in HTML.
Let me write everything now.
Eyes direction: use dir (current). Head may be mid-move; interpolate head center. Eye positions based on dir.
I'll draw head as rounded rect? A circle is simpler and distinct. Let's draw head circle radius cell*0.46 with gradient and glow, and the body stroke ends under it.
Body stroke color gradient: create linear gradient from head pos to tail pos with colors from '#c6ff6b' to '#12b8a6'? Let's do per-segment stroke: draw path as a single path but stroke with gradient — gradient computed head→tail direction. Good.
Shine: stroke again narrower with rgba(255,255,255,.15).
Eyes: for perpendicular p = (-dir.y, dir.x). eye offsets: center + forward*cell*0.13 ± perp*cell*0.19. White circle radius cell*0.13, pupil radius cell*0.07 at forward extra cell*0.06.
Let's finalize.
Particles: {x,y,vx,vy,life,maxLife,size,color}. Update: life-=dt/1000; x+=vx*dt/1000 etc; gravity 400. Draw alpha=life/maxLife.
Floating text: {x,y,text,life} rising, alpha fading, fill '#ffd166'.
HUD: score with padding animation? Add class pulse on eat: element.animate? Use simple CSS class toggle with setTimeout.
Write final code.
Also need to prevent scrolling on arrow keys: e.preventDefault().
Let's write it.
One more: "Quick successive key presses must both be honored" — handled by queue.
Now output.