Qwen3.8 Flash
by QwenReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 43 s
- Cost
- $0.0037
- Output tokens
- 7,796
- Spent thinking
- 0 tokens
- Speed
- 76.2 tok/s
- Console errors
- 1
- First token after
- 687 ms
- First visible text after
- 687 ms
- Prompt tokens
- 520
- Code size
- 20.8 KB
- Finish reason
- stop
- Served by
- Alibaba
- Model id
qwen/qwen3.8-flash- Reasoning sent
{"effort":"none"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
Cannot read properties of undefined (reading 'x')
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 · 495 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Serpent</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#060a12;--panel:#0d1524;--line:#1b2740;
--ink:#eaf3ff;--muted:#7f93b5;--accent:#4fe3c1;--gold:#ffd257;--danger:#ff5d6c;
}
html,body{height:100%}
body{
font-family:"Segoe UI",system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;
color:var(--ink);background:var(--bg);overflow:hidden;
display:flex;align-items:center;justify-content:center;min-height:100vh;padding:18px;
-webkit-font-smoothing:antialiased;user-select:none;-webkit-user-select:none;touch-action:none;
}
.stage{position:absolute;inset:0;z-index:0;pointer-events:none}
#board{position:absolute;inset:0;width:100%;height:100%;display:block}
.wrap{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:14px;max-width:min(92vw,640px)}
header{text-align:center}
h1{font-size:clamp(22px,4.6vw,34px);font-weight:800;letter-spacing:.02em;
background:linear-gradient(90deg,#7effd6,#4fe3c1 40%,#5aa8ff);-webkit-background-clip:text;background-clip:text;color:transparent}
.sub{color:var(--muted);font-size:13px;margin-top:3px;letter-spacing:.02em}
.hud{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
.stat{background:rgba(13,21,36,.72);border:1px solid var(--line);border-radius:12px;padding:7px 15px;
min-width:78px;text-align:center;backdrop-filter:blur(6px)}
.stat .k{display:block;font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.stat .v{display:block;font-size:22px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.15}
.score .v{color:var(--accent)} .best .v{color:var(--gold)} .len .v{color:#bcd2ff}
.frame{position:relative;border-radius:16px;padding:10px;
background:linear-gradient(180deg,rgba(18,28,48,.6),rgba(10,16,28,.6));
border:1px solid var(--line);
box-shadow:0 24px 60px rgba(0,0,0,.55),0 0 0 1px rgba(79,227,193,.06) inset,0 0 40px rgba(79,227,193,.05)}
canvas#game{display:block;border-radius:9px;background:#0a1220;image-rendering:auto}
.overlay{position:absolute;inset:10px;border-radius:9px;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:10px;text-align:center;padding:22px;
background:radial-gradient(circle at 50% 38%,rgba(8,14,24,.55),rgba(5,9,16,.86));
backdrop-filter:blur(3px);opacity:0;visibility:hidden;transition:opacity .25s ease;z-index:5}
.overlay.show{opacity:1;visibility:visible}
.ov-kicker{font-size:12px;letter-spacing:.28em;text-transform:uppercase;color:var(--accent);font-weight:700}
.ov-title{font-size:clamp(26px,6vw,40px);font-weight:800;line-height:1.05}
.ov-sub{color:var(--muted);font-size:14px;max-width:34ch}
.ov-score{font-size:46px;font-weight:800;color:var(--gold);text-shadow:0 0 22px rgba(255,210,87,.35);font-variant-numeric:tabular-nums}
.ov-hint{margin-top:4px;font-size:13px;color:var(--ink);opacity:.85}
kbd{display:inline-block;background:#16223a;border:1px solid #2a3b5c;border-bottom-width:2px;border-radius:6px;
padding:1px 8px;font-family:inherit;font-size:12px;color:#cfe0ff;margin:0 2px}
.controls{display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:center}
.legend{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;color:var(--muted);font-size:12px}
.legend b{color:var(--ink);font-weight:600}
.dpad{display:grid;grid-template-columns:repeat(3,44px);grid-template-rows:repeat(3,44px);gap:5px}
.pad{border:1px solid var(--line);background:rgba(18,28,48,.7);color:var(--accent);border-radius:10px;
font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:transform .08s,background .15s}
.pad:hover{background:rgba(30,46,74,.9)} .pad:active{transform:scale(.9)}
.p-up{grid-column:2;grid-row:1}.p-left{grid-column:1;grid-row:2}.p-down{grid-column:2;grid-row:2}.p-right{grid-column:3;grid-row:2}
.btns{display:flex;gap:8px}
.btn{border:1px solid var(--line);background:rgba(18,28,48,.7);color:var(--ink);border-radius:10px;
padding:9px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:transform .08s,background .15s}
.btn:hover{background:rgba(30,46,74,.9)} .btn:active{transform:scale(.94)}
@media (max-width:520px){.dpad{display:none}}
</style>
</head>
<body>
<div class="stage"><canvas id="board"></canvas></div>
<main class="wrap">
<header>
<h1>NEON SERPENT</h1>
<div class="sub">Arrow keys or WASD to steer · P pauses · Space restarts</div>
</header>
<div class="hud">
<div class="stat score"><span class="k">Score</span><span class="v" id="score">0</span></div>
<div class="stat best"><span class="k">Best</span><span class="v" id="best">0</span></div>
<div class="stat len"><span class="k">Length</span><span class="v" id="len">3</span></div>
</div>
<div class="frame">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="ov-kicker" id="ovKicker">Demo Mode</div>
<div class="ov-title" id="ovTitle">Neon Serpent</div>
<div class="ov-sub" id="ovSub">Watch the AI glide, then take the wheel.</div>
<div class="ov-score" id="ovScore" style="display:none">0</div>
<div class="ov-hint" id="ovHint"><kbd>Click</kbd> or press a key to play</div>
</div>
</div>
<div class="controls">
<div class="legend">
<span><b>↑↓←→ / WASD</b> move</span>
<span><b>P</b> pause</span>
<span><b>Space</b> restart</span>
</div>
<div class="dpad">
<button class="pad p-up" data-dir="up" aria-label="Up">▲</button>
<button class="pad p-left" data-dir="left" aria-label="Left">◀</button>
<button class="pad p-down" data-dir="down" aria-label="Down">▼</button>
<button class="pad p-right" data-dir="right" aria-label="Right">▶</button>
</div>
<div class="btns">
<button class="btn" id="pauseBtn">Pause</button>
<button class="btn" id="restartBtn">Restart</button>
</div>
</div>
</main>
<script>
(function(){
"use strict";
/* ---------- constants ---------- */
const COLS=22, ROWS=22, CELL=24;
const BASE_INTERVAL=120, MIN_INTERVAL=68, SPEED_STEP=1.5;
const START_LEN=3, FOOD_VALUE=10;
const TAU=Math.PI*2;
const DIR={ up:{x:0,y:-1}, down:{x:0,y:1}, left:{x:-1,y:0}, right:{x:1,y:0} };
const ORDER=["up","right","down","left"];
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerpAngle=(a,b,t)=>{let d=((b-a+Math.PI)%TAU)-Math.PI;if(d<-Math.PI)d+=TAU;return a+d*t;};
/* ---------- canvases ---------- */
const gameCanvas=document.getElementById("game");
const gctx=gameCanvas.getContext("2d");
const boardCanvas=document.getElementById("board");
const bctx=boardCanvas.getContext("2d");
/* ---------- state ---------- */
let snake=[], prevSnake=[], headPos={x:0,y:0}, tailPos={x:0,y:0};
let food=null, particles=[], ripples=[];
let dir="right", queue=[], interval=BASE_INTERVAL, acc=0;
let score=0, best=0, eaten=0, growth=0, dead=false, paused=false, demo=true, overShown=false;
let lastTime=performance.now(), timeSec=0;
/* ---------- sizing ---------- */
let W,H,cellPx,ox,oy,dpr=1,bw=0,bh=0;
function layout(){
const availW=Math.min(window.innerWidth*0.92, 640);
const availH=window.innerHeight*0.56;
cellPx=Math.max(12, Math.floor(Math.min(availW/COLS, availH/ROWS)));
W=COLS*cellPx; H=ROWS*cellPx;
dpr=Math.min(window.devicePixelRatio||1, 2);
gameCanvas.width=W*dpr; gameCanvas.height=H*dpr;
gameCanvas.style.width=W+"px"; gameCanvas.style.height=H+"px";
ox=cellPx*0.5; oy=cellPx*0.5;
bw=window.innerWidth; bh=window.innerHeight;
boardCanvas.width=bw*dpr; boardCanvas.height=bh*dpr;
boardCanvas.style.width=bw+"px"; boardCanvas.style.height=bh+"px";
}
layout();
/* ---------- helpers ---------- */
function rr(x,y,w,h,r){r=Math.min(r,w/2,h/2);gctx.beginPath();gctx.moveTo(x+r,y);
gctx.arcTo(x+w,y,x+w,y+h,r);gctx.arcTo(x+w,y+h,x,y+h,r);gctx.arcTo(x,y+h,x,y,r);gctx.arcTo(x,y,x+w,y,r);gctx.closePath();}
function centerOf(i){const s=snake[i];return{x:(s.x+0.5)*cellPx+ox,y:(s.y+0.5)*cellPx+oy};}
function centerOfSeg(s){return{x:(s.x+0.5)*cellPx+ox,y:(s.y+0.5)*cellPx+oy};}
function interpHead(t){const n=centerOf(0),p=prevSnake[0]||n;return{x:p.x+(n.x-p.x)*t,y:p.y+(n.y-p.y)*t};}
function interpTail(t){const n=tailPos,p=prevSnake[prevSnake.length-1]||n;return{x:p.x+(n.x-p.x)*t,y:p.y+(n.y-p.y)*t};}
function occupied(x,y){for(let i=0;i<snake.length;i++){if(snake[i].x===x&&snake[i].y===y)return true;}return false;}
/* ---------- setup ---------- */
function resetGame(){
const sy=Math.floor(ROWS/2), sx=Math.floor(COLS/2)-1;
snake=[{x:sx+2,y:sy},{x:sx+1,y:sy},{x:sx,y:sy}];
prevSnake=snake.map(s=>({x:s.x,y:s.y}));
headPos=centerOf(0); tailPos=centerOf(snake[snake.length-1]);
dir="right"; queue=[]; interval=BASE_INTERVAL; acc=0;
score=0; eaten=0; growth=0; dead=false; paused=false; overShown=false;
particles=[]; ripples=[];
spawnFood(); updateHUD(); showOverlay(false);
}
function spawnFood(){
const free=[];
for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++)if(!occupied(x,y))free.push({x,y});
if(free.length===0){food=null;return;}
const c=free[(Math.random()*free.length)|0];
food={x:c.x,y:c.y,phase:Math.random()*TAU};
}
/* ---------- input ---------- */
function pushDir(name){
const ref=queue.length?queue[queue.length-1]:dir;
if(name===ref)return;
if(DIR[name].x===-DIR[ref].x && DIR[name].y===-DIR[ref].y)return; // no reverse
if(queue.length>=2)return;
queue.push(name);
}
function startFromIdle(){
if(dead){resetGame();return;}
if(paused){paused=false;showOverlay(false);return;}
if(demo){demo=false;showOverlay(false);}
}
const KEYMAP={ArrowUp:"up",KeyW:"up",ArrowDown:"down",KeyS:"down",ArrowLeft:"left",KeyA:"left",ArrowRight:"right",KeyD:"right"};
window.addEventListener("keydown",e=>{
const code=e.code;
if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Space"].includes(code))e.preventDefault();
if(code==="KeyP"){togglePause();return;}
if(code==="Enter"){startFromIdle();return;}
if(code==="Space"){if(demo||dead||paused){startFromIdle();}else{resetGame();}return;}
if(KEYMAP[code]){
if(demo||dead||paused){startFromIdle(); if(KEYMAP[code])pushDir(KEYMAP[code]);}
else pushDir(KEYMAP[code]);
}
});
document.querySelectorAll(".pad").forEach(b=>b.addEventListener("click",()=>{
if(demo||dead||paused)startFromIdle(); pushDir(b.dataset.dir);
}));
document.getElementById("pauseBtn").addEventListener("click",togglePause);
document.getElementById("restartBtn").addEventListener("click",()=>{resetGame();demo=false;showOverlay(false);});
document.querySelector(".frame").addEventListener("click",()=>{if(demo||dead||paused)startFromIdle();});
function togglePause(){
if(demo||dead)return;
paused=!paused;
document.getElementById("pauseBtn").textContent=paused?"Resume":"Pause";
if(paused)showOverlay(true,{kicker:"Paused",title:"Take a breath",sub:"Press P to resume.",hint:'<kbd>P</kbd> to continue'});
else showOverlay(false);
}
/* ---------- step ---------- */
function step(){
if(queue.length){dir=queue.shift();}
const nx=snake[0].x+DIR[dir].x, ny=snake[0].y+DIR[dir].y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS){endGame();return;}
const willGrow=growth>0;
const limit=willGrow?snake.length:snake.length-1;
for(let i=0;i<limit;i++){if(snake[i].x===nx&&snake[i].y===ny){endGame();return;}}
prevSnake=snake.map(s=>({x:s.x,y:s.y}));
snake.unshift({x:nx,y:ny});
if(food&&nx===food.x&&ny===food.y){
score+=FOOD_VALUE; eaten++; growth++;
burst(headPos);
interval=Math.max(MIN_INTERVAL,BASE_INTERVAL-eaten*SPEED_STEP);
spawnFood(); updateHUD();
}
if(growth>0)growth--; else snake.pop();
headPos=centerOf(0); tailPos=centerOf(snake[snake.length-1]);
}
function endGame(){
dead=true; if(score>best)best=score;
updateHUD();
showOverlay(true,{kicker:"Game Over",title:"You crashed!",sub:"The serpent bit its own tale — or met the wall.",
score:score,hint:'<kbd>Space</kbd> / <kbd>Enter</kbd> to restart'});
}
/* ---------- AI ---------- */
function aiDecide(){
const h=snake[0], f=food;
if(!f)return dir;
let bestDir=dir, bestScore=-Infinity;
for(const name of ORDER){
const d=DIR[name];
if(d.x===-DIR[dir].x&&d.y===-DIR[dir].y)continue;
const nx=h.x+d.x, ny=h.y+d.y;
if(nx<0||ny<0||nx>=COLS||ny>=ROWS)continue;
const occ=new Set();
const lim=snake.length-1;
for(let i=0;i<lim;i++)occ.add(snake[i].x+","+snake[i].y);
if(occ.has(nx+","+ny))continue;
let open=0;
for(const o of ORDER){const od=DIR[o];const ax=nx+od.x,ay=ny+od.y;
if(ax>=0&&ay>=0&&ax<COLS&&ay<ROWS&&!occ.has(ax+","+ay))open++;}
if(open<=1)continue;
const dist=Math.abs(nx-f.x)+Math.abs(ny-f.y);
let sc=dist*3 + open*2 - ((name===dir)?0:1);
if(sc>bestScore){bestScore=sc;bestDir=name;}
}
return bestDir;
}
/* ---------- effects ---------- */
function burst(p){
const cols=["#4fe3c1","#7effd6","#5aa8ff","#ffffff"];
for(let i=0;i<18;i++){
const a=Math.random()*TAU, sp=cellPx*(1.2+Math.random()*3.2);
particles.push({x:p.x,y:p.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:0,max:0.45+Math.random()*0.35,
size:cellPx*(0.06+Math.random()*0.12),col:cols[(Math.random()*cols.length)|0]});
}
ripples.push({x:p.x,y:p.y,age:0,max:0.5});
}
function updateEffects(dt){
for(const p of particles){p.life+=dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=0.92;p.vy=p.vy*0.92+cellPx*1.6*dt;}
particles=particles.filter(p=>p.life<p.max);
for(const r of ripples)r.age+=dt;
ripples=ripples.filter(r=>r.age<r.max);
}
/* ---------- HUD & overlay ---------- */
function updateHUD(){
document.getElementById("score").textContent=score;
document.getElementById("best").textContent=best;
document.getElementById("len").textContent=snake.length;
}
function showOverlay(show,cfg){
const ov=document.getElementById("overlay");
if(!show){ov.classList.remove("show");return;}
cfg=cfg||{};
document.getElementById("ovKicker").textContent=cfg.kicker||"";
document.getElementById("ovTitle").textContent=cfg.title||"";
document.getElementById("ovSub").textContent=cfg.sub||"";
const sc=document.getElementById("ovScore");
if(cfg.score!=null){sc.textContent=cfg.score;sc.style.display="block";}else sc.style.display="none";
document.getElementById("ovHint").innerHTML=cfg.hint||"";
ov.classList.add("show");
}
/* ---------- draw: ambient ---------- */
function drawAmbient(){
const t=timeSec;
const g=bctx.createLinearGradient(0,0,0,bh);
g.addColorStop(0,"#081020");g.addColorStop(.55,"#0a1526");g.addColorStop(1,"#060a12");
bctx.setTransform(dpr,0,0,dpr,0,0);
bctx.fillStyle=g;bctx.fillRect(0,0,bw,bh);
bctx.globalCompositeOperation="lighter";
const orbs=[[bw*.2,bh*.32,260,"rgba(79,227,193,0.10)",.05,.0006],[bw*.82,bh*.24,300,"rgba(90,168,255,0.09)",.04,.0009],
[bw*.5,bh*.85,340,"rgba(126,255,214,0.06)",.03,.0007]];
for(let i=0;i<orbs.length;i++){const o=orbs[i];
const cx=o[0]+Math.sin(t*o[4]+i)*40, cy=o[1]+Math.cos(t*o[4]*1.3+i)*30;
const rg=bctx.createRadialGradient(cx,cy,0,cx,cy,o[2]);
rg.addColorStop(0,o[3]);rg.addColorStop(1,"rgba(0,0,0,0)");
bctx.fillStyle=rg;bctx.beginPath();bctx.arc(cx,cy,o[2],0,TAU);bctx.fill();
}
bctx.globalCompositeOperation="source-over";
}
/* ---------- draw: grid ---------- */
function drawGrid(){
gctx.save();gctx.translate(ox,oy);
const gw=COLS*cellPx, gh=ROWS*cellPx;
const bg=gctx.createLinearGradient(0,0,gw,gh);
bg.addColorStop(0,"#0b1424");bg.addColorStop(1,"#08101c");
gctx.fillStyle=bg;gctx.fillRect(-ox,-oy,W,H);
const gl=0.16+0.05*Math.sin(timeSec*1.5);
gctx.lineWidth=1;gctx.strokeStyle="rgba(90,168,255,"+gl+")";
for(let x=0;x<=COLS;x++){gctx.beginPath();gctx.moveTo(x*cellPx,0);gctx.lineTo(x*cellPx,gh);gctx.stroke();}
for(let y=0;y<=ROWS;y++){gctx.beginPath();gctx.moveTo(0,y*cellPx);gctx.lineTo(gw,y*cellPx);gctx.stroke();}
gctx.restore();
}
/* ---------- draw: food ---------- */
function drawFood(){
if(!food)return;
const cx=(food.x+0.5)*cellPx+ox, cy=(food.y+0.5)*cellPx+oy;
const pulse=0.5+0.5*Math.sin(timeSec*4+food.phase);
const R=cellPx*0.32+pulse*cellPx*0.06;
gctx.save();
gctx.shadowColor="rgba(255,120,140,0.9)";gctx.shadowBlur=cellPx*(0.7+pulse*0.6);
const rg=gctx.createRadialGradient(cx-R*0.3,cy-R*0.3,R*0.1,cx,cy,R);
rg.addColorStop(0,"#ffe3ea");rg.addColorStop(.4,"#ff8fa3");rg.addColorStop(1,"#ff4d63");
gctx.fillStyle=rg;gctx.beginPath();gctx.arc(cx,cy,R,0,TAU);gctx.fill();
gctx.shadowBlur=0;
gctx.fillStyle="rgba(255,255,255,0.85)";gctx.beginPath();gctx.arc(cx-R*0.32,cy-R*0.34,R*0.22,0,TAU);gctx.fill();
gctx.fillStyle="#5fd68a";gctx.beginPath();gctx.ellipse(cx+R*0.1,cy-R*0.95,R*0.28,R*0.16,-0.6,0,TAU);gctx.fill();
gctx.restore();
}
/* ---------- draw: snake ---------- */
function buildPath(t){
const pts=[];
pts.push(interpHead(t));
for(let i=1;i<snake.length-1;i++)pts.push(centerOf(i));
pts.push(interpTail(t));
return pts;
}
function strokePolyline(pts,width,color,alpha){
gctx.lineJoin="round";gctx.lineCap="round";gctx.strokeStyle=color;gctx.globalAlpha=alpha;gctx.lineWidth=width;
gctx.beginPath();gctx.moveTo(pts[0].x,pts[0].y);
for(let i=1;i<pts.length;i++)gctx.lineTo(pts[i].x,pts[i].y);
gctx.stroke();gctx.globalAlpha=1;
}
function drawSnakeBody(t){
const pts=buildPath(t);
if(pts.length<2){ // single segment fallback
const w=cellPx*0.78;rr(pts[0].x-w/2,pts[0].y-w/2,w,w,w*0.3);
gctx.fillStyle="#37d0b0";gctx.fill();return;
}
const w=cellPx*0.78;
gctx.save();
gctx.shadowColor="rgba(79,227,193,0.55)";gctx.shadowBlur=cellPx*0.9;
strokePolyline(pts,w,"#2fc7a6",1);
gctx.shadowBlur=0;
strokePolyline(pts,w*0.86,"#4fe3c1",1);
strokePolyline(pts,w*0.5,"#8ff2dd",0.5);
gctx.restore();
}
function drawHead(t){
const h=interpHead(t);
const ang=lerpAngle(angleFromDir(dir),angleFromDir(queue[0]||dir),clamp(t,0,1));
const s=cellPx*0.9;
gctx.save();gctx.translate(h.x,h.y);gctx.rotate(ang);
gctx.shadowColor="rgba(126,255,214,0.8)";gctx.shadowBlur=cellPx*0.7;
const hg=gctx.createLinearGradient(-s*0.5,0,s*0.5,0);
hg.addColorStop(0,"#2fbfa0");hg.addColorStop(1,"#7dffd6");
gctx.fillStyle=hg;
rr(-s*0.5,-s*0.46,s,s*0.92,s*0.4);gctx.fill();
gctx.shadowBlur=0;
// eyes
const ex=s*0.22, ey=s*0.26, er=s*0.16;
gctx.fillStyle="#fff";
gctx.beginPath();gctx.arc(ex,-ey,er,0,TAU);gctx.fill();
gctx.beginPath();gctx.arc(ex,ey,er,0,TAU);gctx.fill();
gctx.fillStyle="#0a1420";
const pr=er*0.55;
gctx.beginPath();gctx.arc(ex+er*0.35,-ey,pr,0,TAU);gctx.fill();
gctx.beginPath();gctx.arc(ex+er*0.35,ey,pr,0,TAU);gctx.fill();
// tongue
const tp=0.5+0.5*Math.sin(timeSec*9);
if(tp>0.4){
gctx.strokeStyle="#ff5d6c";gctx.lineWidth=s*0.06;gctx.lineCap="round";
const bx=s*0.48, by=s*0.12*(tp-0.5);
gctx.beginPath();gctx.moveTo(bx,by);gctx.lineTo(bx+s*0.28,by-s*0.12);gctx.stroke();
gctx.beginPath();gctx.moveTo(bx,by);gctx.lineTo(bx+s*0.28,by+s*0.12);gctx.stroke();
}
gctx.restore();
}
function angleFromDir(name){
switch(name){case "up":return -Math.PI/2;case "down":return Math.PI/2;case "left":return Math.PI;default:return 0;}
}
/* ---------- draw: fx ---------- */
function drawParticles(){
gctx.save();gctx.globalCompositeOperation="lighter";
for(const p of particles){
const a=1-p.life/p.max;
gctx.globalAlpha=a;gctx.fillStyle=p.col;
gctx.beginPath();gctx.arc(p.x,p.y,p.size*a+1,0,TAU);gctx.fill();
}
gctx.restore();
}
function drawRipples(){
gctx.save();gctx.globalCompositeOperation="lighter";
for(const r of ripples){
const k=r.age/r.max;
gctx.globalAlpha=(1-k)*0.6;gctx.strokeStyle="#7effd6";gctx.lineWidth=cellPx*0.12*(1-k);
gctx.beginPath();gctx.arc(r.x,r.y,k*cellPx*1.6,0,TAU);gctx.stroke();
}
gctx.restore();
}
/* ---------- main loop ---------- */
function frame(now){
const dt=Math.min((now-lastTime)/1000,0.05);lastTime=now;timeSec+=dt;
if(!dead&&!paused){
if(demo){
const want=aiDecide();
if(want!==dir&&(queue.length===0||want!==queue[queue.length-1])){
const ref=queue.length?queue[queue.length-1]:dir;
if(!(DIR[want].x===-DIR[ref].x&&DIR[want].y===-DIR[ref].y)){
if(queue.length>=2)queue.length=0;
queue.push(want);
}
}
}
acc+=dt*1000;
let guard=0;
while(acc>=interval&&guard<5){acc-=interval;step();guard++;if(dead)break;}
if(guard>=5)acc=0;
}
updateEffects(dt);
// render
gctx.setTransform(dpr,0,0,dpr,0,0);
gctx.clearRect(0,0,W,H);
drawGrid();
drawFood();
const t=clamp(acc/interval,0,1);
drawSnakeBody(t);
drawHead(t);
drawParticles();
drawRipples();
requestAnimationFrame(frame);
}
/* ---------- boot ---------- */
window.addEventListener("resize",layout);
resetGame();
showOverlay(true,{kicker:"Demo Mode",title:"Neon Serpent",
sub:"An AI is already slithering across the grid. Grab the wheel whenever you like.",
hint:'<kbd>Click</kbd> or press a key to play'});
updateHUD();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>