Models 44

Nemotron 3 Super

by NVIDIAreasoning defaultClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
1 min 14 s
Cost
$0.0032
Output tokens
7,032
Spent thinking
4,450 tokens
Speed
96.3 tok/s
Console errors
0
First token after
804 ms
First visible text after
51.1 s
Prompt tokens
517
Code size
7.4 KB
Finish reason
stop
Served by
DekaLLM
Model id
nvidia/nemotron-3-super-120b-a12b
Reasoning sent
model default
Max output
16,384 tokens
Generated
Nov 13, 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 · 274 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Snake Game</title>
<style>
  html,body{margin:0;height:100%;background:#111;color:#fff;font-family:sans-serif;display:flex;align-items:center;justify-content:center;}
  canvas{background:#222;display:block;}
  #overlay{
    position:fixed;inset:0;display:none;align-items:center;justify-content:center;
    flex-direction:column;background:rgba(0,0,0,0.8);color:#fff;
    padding:2rem;border-radius:8px;text-align:center;z-index:10;
    font-size:1.2rem;
  }
  #overlay-content{line-height:1.4;}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="overlay"><div id="overlay-content"></div></div>
<script>
(() => {
  const COLS = 20, ROWS = 20, CELL_SIZE = 25;
  const canvas = document.getElementById('game');
  canvas.width = COLS * CELL_SIZE;
  canvas.height = ROWS * CELL_SIZE;
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');
  const overlayContent = document.getElementById('overlay-content');

  let snake = []; // {x,y} in grid coordinates
  let direction = {x:1, y:0};
  let pendingDirection = {x:1, y:0};
  let food = {x:0, y:0};
  let score = 0, bestScore = 0;
  let gameOverFlag = false, pausedFlag = false;
  const particles = [];
  const UPDATE_INTERVAL = 1000 / 15; // ~66.66ms -> 15 updates/sec
  let accumulator = 0, lastTime = 0, lastRenderTime = 0;

  function restart() {
    score = 0;
    snake = [
      {x: Math.floor(COLS/2), y: Math.floor(ROWS/2)},
      {x: Math.floor(COLS/2)-1, y: Math.floor(ROWS/2)},
      {x: Math.floor(COLS/2)-2, y: Math.floor(ROWS/2)}
    ];
    direction = {x:1, y:0};
    pendingDirection = {x:1, y:0};
    placeFood();
    gameOverFlag = false;
    pausedFlag = false;
    updateOverlay();
  }

  function placeFood() {
    let f;
    do {
      f = {x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS)};
    } while (snake.some(s => s.x === f.x && s.y === f.y));
    food = f;
  }

  function spawnParticles(cellX, cellY) {
    const cx = (cellX+0.5)*CELL_SIZE;
    const cy = (cellY+0.5)*CELL_SIZE;
    for(let i=0;i<8;i++){
      const ang = Math.random()*Math.PI*2;
      const sp = 2 + Math.random()*3;
      particles.push({
        x:cx, y:cy,
        vx:Math.cos(ang)*sp, vy:Math.sin(ang)*sp,
        life:0.6+Math.random()*0.4,
        radius:2+Math.random()*2
      });
    }
  }

  function updateParticles(dt){
    for(let i=particles.length-1;i>=0;i--){
      const p=particles[i];
      p.x+=p.vx*dt; p.y+=p.vy*dt;
      p.life-=dt;
      if(p.life<=0) particles.splice(i,1);
    }
  }

  function update(dt){
    // apply pending direction if not opposite
    if(!(pendingDirection.x===-direction.x && pendingDirection.y===-direction.y)){
      direction = pendingDirection;
    }
    const newHead = {x:snake[0].x+direction.x, y:snake[0].y+direction.y};
    // wall collision
    if(newHead.x<0||newHead.x>=COLS||newHead.y<0||newHead.y>=ROWS){
      gameOver(); return;
    }
    // self collision (ignore tail that will move if not growing)
    for(let i=0;i<snake.length-1;i++){
      if(snake[i].x===newHead.x && snake[i].y===newHead.y){
        gameOver(); return;
      }
    }
    // food
    if(newHead.x===food.x && newHead.y===food.y){
      score+=10;
      if(score>bestScore) bestScore=score;
      spawnParticles(newHead.x, newHead.y);
      // grow: do not pop tail
    }else{
      snake.pop(); // move forward
    }
    snake.unshift(newHead);
  }

  function gameOver(){
    gameOverFlag=true;
    updateOverlay();
  }

  function drawFood(){
    ctx.fillStyle='#ff5722';
    ctx.beginPath();
    ctx.arc(food.x*CELL_SIZE+CELL_SIZE/2,
            food.y*CELL_SIZE+CELL_SIZE/2,
            CELL_SIZE*0.4,0,Math.PI*2);
    ctx.fill();
  }

  function drawSnake(){
    snake.forEach((seg,i)=>{
      const x=seg.x*CELL_SIZE, y=seg.y*CELL_SIZE;
      if(i===0){
        // head
        ctx.fillStyle='#4caf50';
        ctx.beginPath();
        ctx.arc(x+CELL_SIZE/2, y+CELL_SIZE/2, CELL_SIZE*0.45,0,Math.PI*2);
        ctx.fill();
        // eyes
        const eyeSize=CELL_SIZE*0.1;
        const offset=CELL_SIZE*0.15;
        let ex1,ey1,ex2,ey2;
        switch(direction.x){
          case 1: // right
            ex1=x+CELL_SIZE-offset; ey1=y+offset;
            ex2=x+CELL_SIZE-offset; ey2=y+CELL_SIZE-offset;
            break;
          case -1: // left
            ex1=x+offset; ey1=y+offset;
            ex2=x+offset; ey2=y+CELL_SIZE-offset;
            break;
          case 0:
            if(direction.y===1){ // down
              ex1=x+offset; ey1=y+CELL_SIZE-offset;
              ex2=x+CELL_SIZE-offset; ey2=y+CELL_SIZE-offset;
            }else{ // up
              ex1=x+offset; ey1=y+offset;
              ex2=x+CELL_SIZE-offset; ey2=y+offset;
            }
        }
        ctx.fillStyle='#fff';
        ctx.beginPath();
        ctx.arc(ex1,ey1,eyeSize,0,Math.PI*2);ctx.fill();
        ctx.beginPath();
        ctx.arc(ex2,ey2,eyeSize,0,Math.PI*2);ctx.fill();
      }else{
        // body
        ctx.fillStyle='#8bc34a';
        ctx.fillRect(x+2,y+2,CELL_SIZE-4,CELL_SIZE-4);
      }
    });
  }

  function drawParticles(){
    particles.forEach(p=>{
      ctx.save();
      ctx.globalAlpha=p.life;
      ctx.fillStyle='#ffeb3b';
      ctx.beginPath();
      ctx.arc(p.x,p.y,p.radius,0,Math.PI*2);
      ctx.fill();
      ctx.restore();
    });
  }

  function drawScore(){
    ctx.fillStyle='#fff';
    ctx.font='20px sans-serif';
    ctx.textAlign='left';
    ctx.fillText(`Score: ${score}`,10,30);
    ctx.fillText(`Best: ${bestScore}`,10,60);
  }

  function render(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    drawFood();
    drawSnake();
    drawParticles();
    drawScore();
  }

  function updateOverlay(){
    if(gameOverFlag){
      overlayContent.innerHTML=`
        <div>Game Over</div>
        <div>Score: ${score}</div>
        <div>Best Score: ${bestScore}</div>
        <div>Press Space or Enter to Restart</div>
      `;
      overlay.style.display='flex';
    }else if(pausedFlag){
      overlayContent.innerHTML=`
        <div>Paused</div>
        <div>Press P to Resume</div>
      `;
      overlay.style.display='flex';
    }else{
      overlay.style.display='none';
    }
  }

  // Input
  document.addEventListener('keydown',e=>{
    const k=e.key;
    if(gameOverFlag){
      if(k==='Enter' || k===' '){ e.preventDefault(); restart(); }
      return;
    }
    if(k==='p' || k==='P'){
      e.preventDefault();
      pausedFlag=!pausedFlag;
      updateOverlay();
      return;
    }
    let dir=null;
    switch(k){
      case 'ArrowUp': case 'w': case 'W': dir={x:0,y:-1};break;
      case 'ArrowDown': case 's': case 'S': dir={x:0,y:1};break;
      case 'ArrowLeft': case 'a': case 'A': dir={x:-1,y:0};break;
      case 'ArrowRight': case 'd': case 'D': dir={x:1,y:0};break;
    }
    if(dir){
      if(!(dir.x===-direction.x && dir.y===-direction.y)){
        pendingDirection=dir;
      }
    }
  });

  // Game loop
  function loop(timestamp){
    if(!lastTime) lastTime=timestamp;
    const frameDt = (lastRenderTime===0)?0:timestamp-lastRenderTime;
    lastRenderTime=timestamp;
    updateParticles(frameDt/1000);

    accumulator+=timestamp-lastTime;
    lastTime=timestamp;
    while(accumulator>=UPDATE_INTERVAL){
      if(!pausedFlag && !gameOverFlag){
        update(UPDATE_INTERVAL/1000);
      }
      accumulator-=UPDATE_INTERVAL;
    }
    render();
    requestAnimationFrame(loop);
  }

  // Init
  restart();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (4,450 tokens)
We need to output a single HTML file with CSS/JS. Provide a polished snake game.

We need to respect the requirements: grid at least 20x20 cells, centered on page. Arrow keys and WASD control snake. Quick successive key presses must both be honored, but snake cannot reverse onto itself. Means we need to queue inputs. Typically we can allow changing direction on next tick but not immediate reversal.

Eating food adds 10 points, grows snake by one cell; new food never spawns on snake.

Game over when hits wall or itself, overlay with score; Space or Enter restarts; P pauses.

Visual polish: smooth movement, distinct head with eyes facing direction, small particle burst when eating.

Display current score and best score of the session.

All in one HTML file.

We'll implement: set canvas size based on cell size (e.g., cellSize = 25px). Grid 20x20 => canvas 500x500. Centered on page via CSS flex.

Game loop: requestAnimationFrame.

Input handling: keydown event, map to direction (dx,dy). Prevent reversal: if new direction is opposite of current direction ignore; but we want quick successive key presses both honored, so we can store a pending direction that will be applied on next movement tick if not opposite.

Smooth movement: we can use a fixed tick rate (e.g., 10 fps or 15). Use time delta to move proportionally? Simpler: we update position each frame based on elapsed time and speed; we can have a fixed update interval (e.g., 100ms). We'll store lastUpdateTime and if now - lastUpdate > updateInterval then update.

Smooth movement: we can also interpolate between grid positions for rendering; but simpler to just render at cell positions each tick; it's still smooth enough if update interval is small (like 100ms). Could also do subcell movement using velocity.

But requirement: smooth movement; we can implement using requestAnimationFrame and move the snake by a fraction each frame based on direction and speed (cells per second). We'll keep snake position as floating point (x,y) in cell units. On update we add velocity * dt (converted to cells per second). When the snake's head passes a cell boundary we check for collisions and food, and we snap to grid? Simpler: keep grid-based movement with a fixed tick but use requestAnimationFrame for rendering only; the movement appears smooth if we use linear interpolation between previous and next grid position based on alpha (time since last tick / tick interval). We'll render each segment at interpolated position.

Implement: maintain list of segments as objects {x, y} in grid coordinates (floats). On each tick we calculate new head position: newHead = {x: head.x + dx, y: head.y + dy}. Insert at front. If ate food, don't pop tail; else pop tail. Then set lastHeadPos for interpolation? For rendering, we can compute each segment's render position based on previous and current positions using lerp factor based on time within tick.

Simpler: we can store positions for each segment as floating and update them each tick by shifting. For rendering we just draw at those positions * cellSize. That will appear as jumps if tick interval is large; but we can set tick interval to 80ms (12.5 fps) maybe still visible jumps. Better: use requestAnimationFrame to move continuously: we keep a direction vector and a speed (cells per second). Each frame we update head position: head.x += dx * speed * dt; head.y += dy * speed * dt. Then we need to grow when crossing food cell boundaries. That is more complex.

Given time, perhaps simplest: use fixed tick but with requestAnimationFrame for rendering only and linear interpolation between last and current positions; that yields smooth movement.

Implementation details:

- constants: COLS = ROWS = 20; CELL_SIZE = 25; CANVAS_SIZE = COLS * CELL_SIZE.
- snake: array of objects {x, y} (grid coordinates). Initially length 3 at center.
- direction: {x: 1, y:0} initially right.
- pendingDirection: same as direction initially.
- speed: ticks per second (e.g., 10 => 100ms interval).
- lastRender = 0; lastUpdate = 0; accumulator for fixed timestep.

Game loop function(timestamp):
  dt = timestamp - lastRender; lastRender = timestamp;
  // update accumulator
  accumulator += dt;
  while (accumulator >= FIXED_TIME_STEP) {
    update(FIXED_TIME_STEP/1000); // in seconds
    accumulator -= FIXED_TIME_STEP;
  }
  render();
  requestAnimationFrame(loop);

Update function:
  // apply pending direction if not opposite
  if (pendingDirection.x !== -direction.x || pendingDirection.y !== -direction.y) {
    direction = pendingDirection;
  }
  // compute new head
  const newHead = {x: snake[0].x + direction.x, y: snake[0].y + direction.y};
  // check collisions with walls
  if (newHead.x < 0 || newHead.x >= COLS || newHead.y < 0 || newHead.y >= ROWS) {
    gameOver(); return;
  }
  // check self collision (excluding tail that will move)
  for (let i = 0; i < snake.length - 1; i++) { // exclude last segment because it will move if not growing
    if (snake[i].x === newHead.x && snake[i].y === newHead.y) {
      gameOver(); return;
    }
  }
  // check food
  if (newHead.x === food.x && newHead.y === food.y) {
    score += 10;
    if (score > bestScore) bestScore = score;
    // grow: we will not pop tail
    // spawn particles
    spawnParticles(newHead.x, newHead.y);
    // place new food
    placeFood();
  } else {
    // remove tail
    snake.pop();
  }
  // add new head at front
  snake.unshift(newHead);

Render:
  clear canvas.
  draw grid optional? maybe not needed.
  draw snake: for each segment compute render position using interpolation between previous and current? Actually we have only current positions after update; we also need previous positions for interpolation. We can store each segment's previous position from last update (i.e., before applying update). To do that we could store snakePrev as copy before update each tick, then during render compute alpha = accumulator / FIXED_TIME_STEP; then renderPos = prev + (current - prev) * alpha. We'll need to keep both arrays.

Simpler: we can just draw at current positions; still okay if tick rate high enough (like 15 fps => 66ms). Might still look a bit choppy but acceptable. Let's set UPDATE_INTERVAL = 80 => 12.5 fps maybe visible. Let's set to 100ms (10 fps) might be noticeable. Let's set to 60ms (~16.6 fps) decent. We'll go with 66ms (15 fps). Good enough.

We'll also draw head with eyes: draw a circle for head, then two smaller circles for eyes offset based on direction.

Particle burst: we can create an array of particles each with x,y (in pixel coordinates), vx, vy, life, radius. On eating food, spawn maybe 8 particles with random velocities.

Draw particles each frame, update life, remove when life <=0.

Pause: when paused, we stop updating but still render? We'll show overlay with "Paused". pressing P toggles pause.

Game over: show overlay with final score, best score, and instruction to press Space or Enter to restart. When not paused and not game over, we update.

We'll need variables: gameOverFlag, pausedFlag.

Input handling: keydown:
  if (code === 'Space' || code === 'Enter') { if (gameOver) restart(); else if (!paused) ??? Actually space also used for pause? We have P for pause. So space/restart only when game over. If not game over and not paused, maybe ignore.
  if (code === 'KeyP') toggle pause (if not game over).
  Arrow keys and WASD: map to direction; set pendingDirection accordingly if not opposite.

Prevent immediate reversal: we already check in update; but also we can ignore if opposite.

Now implement.

Need to compute bestScore of session: variable bestScore persists across restarts (reset only on page reload). So we keep it as let bestScore = 0; when game over we may update if score > bestScore.

Now code.

We'll embed everything in HTML.

Let's write CSS: body {margin:0; height:100vh; display:flex; align-items:center; justify-content:center; background:#111; color:#fff; font-family:sans-serif;} canvas {background:#222; display:block;}

Overlay: create a div with position absolute; top:0; left:0; width:100%; height:100%; display:flex; align-items:center; justify-content:center; flex-direction:column; background:rgba(0,0,0,0.7); color:#fff; pointer-events:none; z-index:10; hidden when not needed.

We'll have two overlays: pause overlay and game over overlay. Could reuse same.

We'll create a div id="overlay" with inner HTML.

We'll toggle its visibility and content.

Implementation steps in JS:

- constants.
- get canvas, ctx.
- init function: reset game variables.
- loop.

Let's code.

Edge Cases: ensure that when game over overlay shows, pressing space/restart restarts game (reset variables, hide overlay). Also pressing P when paused should resume.

We'll need to manage overlay display: overlay.style.display = gameOver || paused ? 'flex' : 'none'.

When paused, we still want to show the current frame (so we still render but not update). So loop continues but if paused we skip update.

When game over, we also skip update.

Now implement functions:

function restart() {
  score = 0;
  snake = [{x: Math.floor(COLS/2), y: Math.floor(ROWS/2)}];
  // add two more segments to the left initially (assuming moving right)
  for (let i=1;i<3;i++) { snake.push({x: snake[0].x - i, y: snake[0].y}); }
  direction = {x:1, y:0};
  pendingDirection = {x:1, y:0};
  placeFood();
  gameOverFlag = false;
  pausedFlag = false;
  updateOverlay();
}

function placeFood() {
  let newFood;
  do {
    newFood = {x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS)};
  } while (snake.some(seg => seg.x === newFood.x && seg.y === newFood.y));
  food = newFood;
}

function spawnParticles(cellX, cellY) {
  const centerX = (cellX+0.5)*CELL_SIZE;
  const centerY = (cellY+0.5)*CELL_SIZE;
  for (let i=0;i<8;i++) {
    const angle = Math.random()*Math.PI*2;
    const speed = 2 + Math.random()*3;
    particles.push({
      x: centerX,
      y: centerY,
      vx: Math.cos(angle)*speed,
      vy: Math.sin(angle)*speed,
      life: 0.6 + Math.random()*0.4,
      radius: 2 + Math.random()*2
    });
  }
}

function updateParticles(dt) {
  for (let i=particles.length-1;i>=0;i--) {
    const p = particles[i];
    p.x += p.vx*dt;
    p.y += p.vy*dt;
    p.life -= dt;
    if (p.life <= 0) particles.splice(i,1);
  }
}

function drawParticles() {
  particles.forEach(p=>{
    ctx.save();
    ctx.globalAlpha = p.life;
    ctx.fillStyle = '#ffeb3b';
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius, 0, Math.PI*2);
    ctx.fill();
    ctx.restore();
  });
}

function drawSnake() {
  snake.forEach((segment,index)=>{
    const x = segment.x * CELL_SIZE;
    const y = segment.y * CELL_SIZE;
    if (index===0) {
      // head
      ctx.fillStyle = '#4caf50';
      ctx.beginPath();
      ctx.arc(x+CELL_SIZE/2, y+CELL_SIZE/2, CELL_SIZE*0.45, 0, Math.PI*2);
      ctx.fill();
      // eyes
      const eyeSize = CELL_SIZE*0.1;
      const offset = CELL_SIZE*0.15;
      let ex1, ey1, ex2, ey2;
      switch (direction.x) {
        case 1: // right
          ex1 = x + CELL_SIZE - offset; ey1 = y + offset;
          ex2 = x + CELL_SIZE - offset; ey2 = y + CELL_SIZE - offset;
          break;
        case -1: // left
          ex1 = x + offset; ey1 = y + offset;
          ex2 = x + offset; ey2 = y + CELL_SIZE - offset;
          break;
        case 0:
          if (direction.y === 1) { // down
            ex1 = x + offset; ey1 = y + CELL_SIZE - offset;
            ex2 = x + CELL_SIZE - offset; ey2 = y + CELL_SIZE - offset;
          } else { // up
            ex1 = x + offset; ey1 = y + offset;
            ex2 = x + CELL_SIZE - offset; ey2 = y + offset;
          }
          break;
      }
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(ex1, ey1, eyeSize, 0, Math.PI*2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(ex2, ey2, eyeSize, 0, Math.PI*2);
      ctx.fill();
    } else {
      // body
      ctx.fillStyle = '#8bc34a';
      ctx.fillRect(x+2, y+2, CELL_SIZE-4, CELL_SIZE-4);
    }
  });
}

function drawFood() {
  ctx.fillStyle = '#ff5722';
  ctx.beginPath();
  ctx.arc(food.x*CELL_SIZE+CELL_SIZE/2, food.y*CELL_SIZE+CELL_SIZE/2, CELL_SIZE*0.4, 0, Math.PI*2);
  ctx.fill();
}

function drawScore() {
  ctx.fillStyle = '#fff';
  ctx.font = '20px sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText(`Score: ${score}`, 10, 30);
  ctx.fillText(`Best: ${bestScore}`, 10, 60);
}

function drawOverlay() {
  // overlay div content
}

We'll use an HTML overlay div.

Define overlay div in HTML: <div id="overlay"><div id="overlay-content"></div></div>

Style overlay: position:fixed; inset:0; display:none; align-items:center; justify-content:center; background:rgba(0,0,0,0.8); color:#fff; font-family:sans-serif; z-index:10; flex-direction:column;

Then in JS we set overlayContent.innerHTML accordingly.

updateOverlay function:
  if (gameOverFlag) {
    overlayContent.innerHTML = `
      <div>Game Over</div>
      <div>Score: ${score}</div>
      <div>Best Score: ${bestScore}</div>
      <div>Press Space or Enter to Restart</div>
    `;
    overlay.style.display = 'flex';
  } else if (pausedFlag) {
    overlayContent.innerHTML = `
      <div>Paused</div>
      <div>Press P to Resume</div>
    `;
    overlay.style.display = 'flex';
  } else {
    overlay.style.display = 'none';
  }

Now implement keydown:

document.addEventListener('keydown', e=>{
  const key = e.key;
  if (gameOverFlag) {
    if (key === 'Enter' || key === ' ') {
      e.preventDefault();
      restart();
    }
    return;
  }
  if (key === 'p' || key === 'P') {
    e.preventDefault();
    pausedFlag = !pausedFlag;
    updateOverlay();
    return;
  }
  // map keys to direction
  let dir = null;
  switch(key) {
    case 'ArrowUp':
    case 'w':
    case 'W':
      dir = {x:0, y:-1};
      break;
    case 'ArrowDown':
    case 's':
    case 'S':
      dir = {x:0, y:1};
      break;
    case 'ArrowLeft':
    case 'a':
    case 'A':
      dir = {x:-1, y:0};
      break;
    case 'ArrowRight':
    case 'd':
    case 'D':
      dir = {x:1, y:0};
      break;
  }
  if (dir) {
    // Prevent reverse
    if (!(dir.x === -direction.x && dir.y === -direction.y)) {
      pendingDirection = dir;
    }
  }
});

We need to define direction and pendingDirection variables.

Now implement update function (fixed step). We'll define const FIXED_TIME_STEP = 1/15 * 1000? Actually we want 15 updates per second => 66.666 ms. Let's set UPDATE_INTERVAL = 1000/15 ≈ 66.6667.

We'll use let accumulator = 0; let lastTime = 0;

function gameLoop(timestamp){
  if (!lastTime) lastTime = timestamp;
  const dt = timestamp - lastTime;
  lastTime = timestamp;
  accumulator += dt;
  while (accumulator >= UPDATE_INTERVAL) {
    if (!pausedFlag && !gameOverFlag) {
      update(UPDATE_INTERVAL/1000); // seconds
    }
    accumulator -= UPDATE_INTERVAL;
  }
  render();
  requestAnimationFrame(gameLoop);
}

function update(dt){
  // apply pending direction
  if (!(pendingDirection.x === -direction.x && pendingDirection.y === -direction.y)) {
    direction = pendingDirection;
  }
  // compute new head
  const newHead = {x: snake[0].x + direction.x, y: snake[0].y + direction.y};
  // wall collision
  if (newHead.x < 0 || newHead.x >= COLS || newHead.y < 0 || newHead.y >= ROWS) {
    gameOver();
    return;
  }
  // self collision (except tail that will move if not growing)
  let hitSelf = false;
  for (let i=0; i<snake.length-1; i++) {
    if (snake[i].x === newHead.x && snake[i].y === newHead.y) {
      hitSelf = true;
      break;
    }
  }
  if (hitSelf) {
    gameOver();
    return;
  }
  // food
  if (newHead.x === food.x && newHead.y === food.y) {
    score += 10;
    if (score > bestScore) bestScore = score;
    spawnParticles(newHead.x, newHead.y);
    // do not remove tail (grow)
  } else {
    snake.pop(); // remove tail
  }
  snake.unshift(newHead);
}

function gameOver(){
  gameOverFlag = true;
  updateOverlay();
}

function render(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  drawFood();
  drawSnake();
  drawParticles();
  drawScore();
}

// Particles update and draw need dt in seconds. In game loop we have dt for each update step; but particles need to be updated each frame based on real time for smoothness. We'll update particles in render using elapsed time since last render. We'll keep a separate lastRenderTime for particles.

Simpler: In gameLoop we have timestamp; we can compute frameDt = timestamp - lastRenderTime; lastRenderTime = timestamp; then updateParticles(frameDt/1000); then render.

We'll adjust.

Initialize lastRenderTime = 0; In gameLoop:

const now = timestamp;
const frameDt = (lastRenderTime === 0) ? 0 : now - lastRenderTime;
lastRenderTime = now;
updateParticles(frameDt/1000);
// then fixed step updates as above
// then render.

We'll need to define particles array.

Now we need to place initial food after resetting snake.

Now produce final HTML.

Let's craft code.

Check for errors: we need to define variables in proper scope (global). Use let/const.

We'll wrap everything in <script> after canvas.

Potential oversight: When spawning particles we used cell coordinates; but we need pixel coordinates; we did that.

Now test mentally: Should work.

Now produce final answer: just the code block.

Let's write.