Models 44

Qwen3.8 Flash

by QwenReasoning1 console error
Open in new window

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&nbsp;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>