Models 44

LFM3-1.7B (free)

by LiquidAIreasoning defaultRan out of tokens
Ran out of tokensThe model hit its 8,192 output-token limit before writing any code.

How it went

Total time
39.2 s
Cost
$0
Output tokens
8,192
Spent thinking
8,192 tokens
Speed
— tok/s
Console errors
—
First token after
265 ms
First visible text after
—
Prompt tokens
514
Code size
0 B
Finish reason
length
Served by
Liquid
Model id
liquid/lfm-3-1.7b:free
Reasoning sent
model default
Max output
8,192 tokens
Generated
Nov 9, 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.
Reasoning trace (8,192 tokens)
We need to create a single HTML file with embedded CSS and JS for a Snake game. Requirements:

- Grid at least 20x20 cells, centered on the page.
- Control: Arrow keys and WASD.
- Quick successive key presses honored (i.e., if you press multiple times quickly, snake moves accordingly). But cannot reverse onto itself.
- Eating food adds 10 points and grows snake by one cell; new food never spawns on snake.
- Game over when hits wall or self -> overlay showing score; Space or Enter restarts, P pauses.
- Visual polish: smooth movement, distinct head and eyes facing direction, small particle burst when eating.
- Display current score and best score.

Implementation details:
- Use canvas element.
- Use requestAnimationFrame? Or simple loop with setInterval? Since we need immediate visible after load ~3 seconds screenshot taken automatically. We can just use requestAnimationFrame for smoothness.
- Need to handle keyboard events only after user clicks the canvas? The requirement says: "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." Wait that's about iframes but we are not using an iframe. So maybe they refer to typical pattern where you hide controls until clicked. However we can simply show instructions before starting. Usually in these demos we put a message like "Click or press a key to start" and then the game starts. Let's implement that: On page load, show a div with text "Click or press a key to start". Then after clicking or pressing any key, the game begins. But we also want to allow immediate control via space/enter etc. Hmm.

Better approach: At the top of page, have a button or link that triggers start. But simpler: Show a message "Click or press a key to start" and wait for first interaction. In many implementations, they listen for keydown globally and start moving the snake immediately. But we must ensure that the game is ready. I think easiest: Add a hidden flag `gameStarted = false`. When page loads, set up event listeners for keydown and mouse? Actually we don't need mouse. For arrow keys and WASD, we can capture them globally. But we should only start moving once started. Could do: If no start yet, ignore input until user clicks the canvas area. But easier: Just start listening from the beginning; the initial state will be snake in center, no food. Pressing arrow keys right away would move snake. That might be okay. But requirement: "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." This seems like a generic instruction for iframes. Not needed here. I'll include a visible prompt "Click or press a key to start" near the title.

Let's design layout: Canvas centered, size maybe 600x400? With grid 20 columns x 20 rows => each cell maybe 30px width and height? 20*30=600. So canvas width 600, height 600? But we need 1280x800 viewport. So we can make canvas 600x600 and center it. Or we could make larger e.g., 700x500. Let's compute: 20 cells per side, spacing between cells maybe 2px gap. So total width = 20 * cellSize + (20-1)*gap. Choose cellSize=30, gap=2 => total = 600+38=638. Height similar. Center on 1280x800: left = (1280 - 638)/2 ≈ 321. So x=321, y=(800-638)/2≈81. So canvas placed at (321,81) with width 638, height 638. That fits within viewport without scrollbars.

But we can also choose smaller cell size like 25, making canvas 500x500. Let's pick 20x20 grid, cell size 35, gap 2 => total = 20*35 + 19*2 = 700 + 38 = 738 > 1280? Actually 738 < 1280 so fine. Let's decide 30x30 cells gives 600 wide, plus gaps 2*(19)=38 => 638. Good enough.

I'll go with cellSize=30, gap=2. So canvas width = 20*30 + 19*2 = 600 + 38 = 638. Height same.

Now we need to draw snake as series of rectangles. Head has eyes: two small circles or dots oriented based on direction. Particle burst: small colored particles radiating outward upon eating.

Game states: idle, playing, paused, game over.

Score: increment by 10 each time food eaten. Best score persists across sessions? Without localStorage we can't store best score permanently. But we can keep a variable `bestScore` updated locally and displayed. It resets on restart? Typically best stays. We'll just track bestScore in memory and update when score exceeds bestScore. Since no persistence, it's fine.

Implement:

- Variables:
  - const COLS = 20;
  - const ROWS = 20;
  - const CELL = 30;
  - let snake = []; // array of {x,y} positions
  - let direction = { dx: 1, dy: 0 } // current movement direction
  - let nextDirection = { dx: 1, dy: 0 } // to prevent reversal?
  - Let's define functions: moveSnake(), eatFood(), spawnFood(), checkCollision().

- Initial snake: place 5 segments? Starting length maybe 3? Usually start with 3 segments. Let's start with 5 segments in center.

- Food: random position not occupied by snake.

- Particles: array of objects {x,y,dx,dy,color}. On eating, generate few particles around head location.

- Game Over detection: after moving, check if new head collides with wall (outside grid) or with any body segment (excluding tail? Actually collision includes all body parts except maybe tail? Standard: if head collides with its own body (any segment other than last?), we treat as death. Simpler: check if new head coordinates equal any existing segment coordinate (including tail). Because if tail moves off, it's okay. But we can just check against all segments.

- Restart: pressing Space or Enter clears snake and resets position and direction, places food again.

- Pause: pressing 'p' toggles pause. While paused, movement stops but still processing events? We'll handle.

- Keyboard handling: arrow keys (up, down, left, right), W, S, A, D. Also space and enter for restart, p for pause.

Implementation steps:

HTML structure:

```
<div id="prompt">Click or press a key to start</div>
<canvas id="gameCanvas" width="638" height="638"></canvas>
<div id="overlay" style="display:none;">...score...</div>
```

CSS: center canvas, add some styling, fonts.

JavaScript:

- Get canvas context.

- Define constants: cols=20, rows=20, cell=30, gap=2.

- Initialize snake: start at center. Compute midX = Math.floor(cols/2); midY = Math.floor(rows/2). Place snake = [{x:midX,gap?}, ...]. Actually we need coordinates relative to canvas origin (0,0) top-left. So center at (cx,cy). Let's compute cx = Math.floor((canvasWidth)/cols/2)? Actually canvas width = cols*cell + (cols-1)*gap. So half width = (cols*cell + (cols-1)*gap)/2. Similarly for height.

Simplify: Set canvas width = 638, height = 638. Then compute cellSize = 30, gap = 2. So number of cells = 20. So we can just fill grid loops.

Initialize snake as array of positions: for i=0..4 (length 5): {x: cx+i*gap+1?, y: cy+...}. Eh.

Simpler: Start snake at (cx, cy) and extend in direction (right). So positions: [{x:cx, y:cy}, {x:cx+cell, y:cy}, {x:cx+2*cell, y:cy}, ...] but that goes out-of-bounds initially. Better to start short: length 3: [{x:cx, y:cy}, {x:cx+cell, y:cy}, {x:cx+2*cell, y:cy}]? That extends beyond canvas? No, because canvas width is 638, center at 319.5? Let's compute: cx = (638/2) = 319. So positions: 319, 349, 379. All within 0-637. Yes.

So we can set initial snake length = 3. Direction = {dx:1, dy:0} (moving right). So we add one more segment behind? Actually we need to maintain order: head at front? Usually snake list contains head at index 0. So we can set snake[0] = {x: cx, y: cy}; snake[1] = {x: cx+cell, y: cy}; snake[2] = {x: cx+2*cell, y: cy}. That's three segments extending rightwards. That matches direction.

Alternatively we can randomize orientation later.

Anyway.

- Spawn food: while true find random cell not in snake positions.

- Particle system: function spawnParticles(x,y,count,color) pushes into array.

- Game loop: use requestAnimationFrame.

Pseudo-code:

```
let lastTime = 0;
function gameLoop(timestamp){
   // update logic
   requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```

Inside:

- Move snake according to direction, but first apply pending direction change? We'll handle by updating direction at end of frame.

Simplify: Each frame:

1. Apply current direction to head: newHead = {x: head.x + direction.dx, y: head.y + direction.dy}.

2. Check collisions: if newHead.x < 0 or >= canvasWidth or newHead.y < 0 or >= canvasHeight OR newHead equals any body segment (except maybe tail?) => game over.

3. If not over, remove tail (if length>1) to keep length constant unless eating.

4. If newHead matches food position => eat: increase score, grow snake (add new head), remove old tail? Actually growth: we add new head and keep old tail? Usually when eating, snake grows by one cell: you add new head and keep tail. Implementation: push new head onto array, and do NOT pop tail. So length increases by 1.

5. Else: keep same length (pop tail).

6. Spawn new food if not already? Usually after each move we spawn new food regardless of whether ate or not? Requirement: "new food never spawns on the snake." So we need to spawn fresh food each turn, but avoid snake cells. After eating, we also need to spawn new food for next turn, but not on the snake. So we can call spawnFood() after each move.

7. Update display: draw canvas.

8. Handle events: keypresses.

Given complexity, we can implement a simplified version but meeting requirements.

Also need to handle pause: toggle pause state. During pause, movement doesn't happen but we still process key events? Probably not needed.

Space or Enter: restart. Pressing Space or Enter resets snake to initial positions, direction reset, and place new food.

Press 'p': toggle pause.

We also need to show overlay with score and best score when game over.

Let's outline code sections:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Snake Game</title>
<style>
  body {
    margin: 0;
    background: #222;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    font-family: Arial, sans-serif;
    color: #fff;
  }
  #prompt {
    position: absolute;
    top: 10%;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(0,0,0,0.5);
    padding: 20px 40px;
    border-radius: 8px;
    font-size: 18px;
    pointer-events: none;
  }
  canvas {
    border: 2px solid #fff;
    background: #000;
    display: block;
  }
  #overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0,0,0,0.75);
    z-index: 10;
    font-size: 24px;
    pointer-events: none;
  }
  .score {
    font-weight: bold;
  }
</style>
</head>
<body>
<div id="prompt">Click or press a key to start</div>
<canvas id="gameCanvas" width="638" height="638"></canvas>
<div id="overlay" style="display:none;">Score: <span class="score">0</span> | Best: <span class="score">0</span></div>

<script>
// code here
</script>
</body>
</html>
```

Now JavaScript inside script tag.

Define variables:

```
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const COLUMNS = 20;
const ROWS = 20;
const CELL = 30;
const GAP = 2;

const CANVAS_W = COLUMNS * CELL + (COLUMNS - 1) * GAP; // 638
const CANVAS_H = ROWS * CELL + (ROWS - 1) * GAP; // 638

let snake = [];
let direction = { dx: 1, dy: 0 };
let nextDirection = { dx: 1, dy: 0 };
let food = { x: 0, y: 0 };
let score = 0;
let bestScore = 0;
let gameOver = false;
let paused = false;
let animationId;
let particles = [];

// Helper to get position from (col,row)
function getPos(cx, cy) {
  return { x: cx * CELL + GAP, y: cy * CELL + GAP };
}

// Initialize snake at center
let startX = Math.floor(CANVAS_W / (2 * COLUMNS)); // approximate center column
let startY = Math.floor(CANVAS_H / (2 * ROWS));
snake = [];
for (let i = 0; i < 3; i++) {
  snake.push({ x: startX + i * CELL, y: startY });
}
direction = { dx: 1, dy: 0 };
nextDirection = { dx: 1, dy: 0 };

// Spawn initial food
spawnFood();

// Function to spawn food
function spawnFood() {
  let pos;
  do {
    pos = {
      x: Math.floor(Math.random() * COLUMNS),
      y: Math.floor(Math.random() * ROWS)
    };
  } while (snake.some(s => s.x === pos.x && s.y === pos.y));
  food = pos;
}

// Function to draw
function draw() {
  ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
  // Draw food
  ctx.fillStyle = '#ff3333';
  ctx.fillRect(food.x * CELL, food.y * CELL, CELL, CELL);
  // Draw snake
  snake.forEach((segment, idx) => {
    // head gets different style
    if (idx === 0) {
      // head
      ctx.fillStyle = '#00ffcc'; // green head
      // eyes: two small circles offset based on direction
      const eyeOffset = 3;
      let ex1 = segment.x * CELL + GAP + eyeOffset;
      let ey1 = segment.y * CELL + GAP + eyeOffset;
      let ex2 = segment.x * CELL + GAP + eyeOffset;
      let ey2 = segment.y * CELL + GAP - eyeOffset;
      // draw eyes
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(ex1, ey1, 2, 0, Math.PI*2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(ex2, ey2, 2, 0, Math.PI*2);
      ctx.fill();
      // body
      ctx.fillStyle = '#00ffcc';
      ctx.fillRect(segment.x * CELL + 1, segment.y * CELL + 1, CELL-2, CELL-2);
    } else {
      // body segments (not head)
      ctx.fillStyle = '#00ffcc';
      ctx.fillRect(segment.x * CELL + 1, segment.y * CELL + 1, CELL-2, CELL-2);
    }
  });

  // Particles
  if (eatFood()) {
    // spawn particles at head position
    for(let i=0;i<8;i++){
      const angle = (Math.PI * 2 / 8) * i;
      const speed = 2 + Math.random()*2;
      particles.push({
        x: food.x * CELL,
        y: food.y * CELL,
        dx: Math.cos(angle) * speed,
        dy: Math.sin(angle) * speed,
        life: 1,
        color: '#ffdd00'
      });
    }
  }

  // Update particles
  particles = particles.filter(p => {
    p.x += p.dx;
    p.y += p.dy;
    p.life -= 1;
    return p.life > 0;
  });

  // Draw particles
  ctx.fillStyle = '#ffdd00';
  for (const p of particles) {
    ctx.globalAlpha = p.life;
    ctx.fillRect(p.x, p.y, 3, 3);
  }
  ctx.globalAlpha = 1;
}

// Detect eating
function eatFood() {
  // Check if head position matches food
  const head = snake[0];
  const newHead = { x: head.x + direction.dx, y: head.y + direction.dy };
  // Actually we haven't moved yet. Let's handle movement first.
  // We'll compute new head after moving.
  // Return true if matched.
  return false; // placeholder
}

```

Actually we need to integrate movement and checking.

Let's restructure:

In each frame:

- Determine new head position based on direction.
- Check collision with walls or self.
- If collision -> game over.
- Else, move snake: add new head, optionally remove tail depending on eating.
- If new head == food position -> eat: increase score, add new head, do not remove tail (grow).
- Else: remove tail (pop last segment) to keep length.

Thus we need to know current head before moving. So we can store `head = snake[0]`.

Algorithm:

```
let head = snake[0];
let newHead = { x: head.x + direction.dx, y: head.y + direction.dy };

// Wall collision
if (newHead.x < 0 || newHead.x >= CANVAS_W || newHead.y < 0 || newHead.y >= CANVAS_H) {
  gameOver = true;
  // break
}

// Self collision: check if newHead matches any segment in snake (excluding maybe tail? but including all)
let selfCollide = false;
for (const seg of snake) {
  if (seg.x === newHead.x && seg.y === newHead.y) {
    selfCollide = true;
    break;
  }
}
if (selfCollide) {
  gameOver = true;
}

if (!gameOver) {
  // Remove tail if not eating
  if (!eaten) { // we need to know if we ate
    // Actually we can decide after checking if newHead == food
    // We'll handle separately.
  }
}
```

Better: First check if newHead equals food position. If yes, then eat.

So:

```
let ate = false;
if (newHead.x === food.x && newHead.y === food.y) {
  ate = true;
  // remove tail? No, we keep tail.
  // grow snake
  snake.push(newHead);
  // best score update
  score += 10;
  if (score > bestScore) bestScore = score;
} else {
  // move normally: add new head, keep tail
  snake.unshift(newHead);
  // do not pop tail
  // but we need to keep length constant? Actually standard: if not eating, tail moves forward, effectively removing previous tail.
  // To simulate, we can pop the last element (the tail) after adding new head? Wait.
  // Let's think: original snake is a list of segments. When moving, we shift everything forward. 
  // If we add new head at front, we need to drop the tail (last element) to keep same length.
  // So we unshift new head, then pop last element (tail) if not eating.
  snake.pop(); // remove tail
}
```

But careful: If we unshift new head and then pop tail, net effect is shifting. However, if we didn't eat, we want the snake to move one step forward, which means the tail moves out of board. So we should keep length same. So we can do:

```
snake.unshift(newHead);
if (!ate) {
  snake.pop(); // remove tail
}
```

That works.

But note: When eating, we added new head and kept tail, thus length increased by 1. So we shouldn't pop tail.

Thus algorithm:

```
if (newHead.x === food.x && newHead.y === food.y) {
  ate = true;
  snake.push(newHead); // add new head
  score += 10;
  if (score > bestScore) bestScore = score;
} else {
  snake.unshift(newHead); // add new head
  snake.pop(); // remove tail
}
```

Note: For non-eating case, we popped tail, meaning the tail moves forward. That's correct.

Now we need to update direction handling: quick successive key presses must both be honored. Means if we receive two arrows in quick succession, we should move twice? Usually in snake games, you can change direction instantly, but you cannot reverse directly. Our implementation updates direction each frame based on latest key pressed. If we capture key events continuously, we may cause rapid changes. To honor quick successive key presses, we can allow direction to be changed even if it results in reversal? But rule: "the snake can never reverse onto itself." So we must prevent reversing direction such that it would cause immediate collision. Usually you can't go back to previous direction. So we can implement that we only accept new direction if it's not opposite of current direction. Eg: if current direction is right (dx=1, dy=0), and new key is left (dx=-1, dy=0), that would reverse. So we can ignore that or clamp. We'll implement: if newKey direction is opposite of current direction, ignore.

Simplify: In our event listener, we can set `nextDirection` to the new direction, but before applying, check if it's opposite of current direction; if so, keep current direction.

Implementation: Keep `currentDir = {dx, dy}` and `prevDir`? Actually we can just set direction each frame based on latest key. But to avoid reversal, we can check: if new direction is exactly opposite of current direction, then ignore.

We'll implement event listener for arrow keys and WASD. For simplicity, we can map key codes: ArrowUp: keyCode 37, ArrowDown: 38, ArrowLeft: 39, ArrowRight: 40. WASD: w=87, a=65, s=83, d=68.

When key pressed, determine dx, dy:

- Up: (-1,0)
- Down: (1,0)
- Left: (0,-1)
- Right: (0,1)

WASD mapping: w->up, a->left? Actually typical: W=up, A=left, S=down, D=right. But we can assign arbitrarily.

We'll implement:

```
const keyMap = {
  'ArrowUp': {dx:-1, dy:0},
  'ArrowDown': {dx:1, dy:0},
  'ArrowLeft': {dx:0, dy:-1},
  'ArrowRight': {dx:0, dy:1},
  'w': {dx:-1, dy:0},
  'W': {dx:-1, dy:0},
  'a': {dx:-1, dy:0},
  'A': {dx:-1, dy:0},
  's': {dx:1, dy:0},
  'S': {dx:1, dy:0},
  'd': {dx:0, dy:1},
  'D': {dx:0, dy:1},
};
```

But better to just use arrow keys and WASD letters.

For each key press, we set `nextDir = {dx, dy}` but also check if it's opposite of current direction. If opposite, we keep current direction. Additionally, we can allow same direction or orthogonal.

We'll implement a simple approach: In event handler, read key, compute candidate direction. If candidate is opposite of current direction (i.e., dx==-current.dx && dy==-current.dy), then ignore. Otherwise, set direction = candidate.

But this prevents immediate reversal. However, sometimes you might want to reverse after a pause? Not needed.

Now, we also need to handle pause: pressing 'p' toggles paused state. When paused, we skip movement updates but still process key events? Might be okay.

Now, we need to manage particles: They appear only when eating. We'll have a function `spawnParticles(headX, headY)` that creates array of particles.

We also need to limit screen refresh: we can just rely on requestAnimationFrame.

Now, overlay: When game over, show overlay with score and best. We'll set `document.getElementById('overlay').style.display = 'flex'` or something. Initially hidden.

Space or Enter: restart. We'll detect key press of space or Enter (keyCode 32 or 13). Reset snake to initial positions, direction reset, place new food, score=0, bestScore=0? Actually bestScore remains unchanged. We'll set score=0 and bestScore=0? Usually bestScore persists across restarts. But we can't persist. We'll just set bestScore to stored value? Hard. We'll just keep bestScore as max ever reached. On restart, we can keep bestScore unchanged. So we can store bestScore in variable and not reset.

Implementation of restart:

```
function restartGame() {
  snake = [];
  // re-initialize snake positions
  for (let i=0; i<3; i++) {
    snake.push({x: startX + i*CELL, y: startY});
  }
  direction = {dx:1, dy:0};
  nextDirection = {dx:1, dy:0};
  food = {x:0, y:0};
  spawnFood();
  score = 0;
  bestScore = Math.max(bestScore, score); // maybe update bestScore
  gameOver = false;
  paused = false;
  particles = [];
  // reset particles
  particles = [];
  // draw
  draw();
}
```

But we need to reinitialize properly.

Simplify: Instead of complex, we can just set snake = initial array and then call spawnFood() and reset variables.

Now, we need to handle the initial state before any key presses: The prompt "Click or press a key to start" appears. Maybe we can hide canvas until user interacts. But we can just start drawing and waiting for input. The first frame will draw empty canvas. That's okay.

Potential issues: The game loop runs constantly; we need to stop when game over. We can break loop and show overlay.

Implementation plan:

- Global variables defined.

- Event listeners:

```
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp); // optional
```

But we can just handle keydown and update direction accordingly. For space and enter, we need to trigger restart. So in keydown, if keyCode is 32 (space) or 13 (Enter) or ' ' or 'Enter', call restartGame().

- For pause: if keyCode is 'p' or 'P', toggle paused.

- For game over detection: after moving, if gameOver becomes true, set overlay display and stop further updates.

We'll implement a main loop:

```
let lastTime = 0;
function gameLoop(timestamp) {
  if (gameOver || paused) {
    // just render static? maybe break
    requestAnimationFrame(gameLoop);
    return;
  }
  // update
  // move snake
  ...
  // draw
  draw();
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```

But we need to handle pause: during pause, we still need to run loop but not update. So we can conditionally update.

Simplify: Use a variable `update()` called each frame. Inside, if not gameOver and !paused, perform movement and checks.

Now, we need to consider performance: 20x20 grid, small.

Let's write detailed code now.

### Constants

```
const COLS = 20;
const ROWS = 20;
const CELL = 30;
const GAP = 2;
const CANVAS_W = COLS * CELL + (COLS - 1) * GAP; // 638
const CANVAS_H = ROWS * CELL + (ROWS - 1) * GAP; // 638
```

### Coordinates

Positions: x = col * CELL + GAP, y = row * CELL + GAP.

### Initial snake

Compute center:

```
const centerX = Math.floor(CANVAS_W / (2 * COLS)); // approx middle column
const centerY = Math.floor(CANVAS_H / (2 * ROWS));
```

But since canvas width is 638, dividing by 2*20 = 40 yields 15.95 -> floor 15. So centerX = 15*30+2 = 452? Actually 15*30=450 +2 = 452. That's near left side? Let's compute precisely: 638/2 = 319. So centerX = 319. Using formula above gave 15*30+2=452, which is wrong. Let's recalc: Number of columns = 20. The leftmost column index 0 corresponds to x=0*30+GAP=2. Rightmost column index 19 corresponds to x=19*30+2=572. So center column index = (19/2) = 9.5? Actually there are 20 columns, indices 0..19. Middle is between 9 and 10. So centerX = 9*30+2 = 272? Wait: 9*30=270 +2 = 272. That's too far left. Something off. Let's compute correctly: The canvas width is 638 pixels. The grid occupies from x=2 to x=572 inclusive. The midpoint is (2+572)/2 = 287. So centerX = 287. Indeed 287 = 9*30 + 17? Actually 9*30=270, plus 17 = 287. So my earlier formula was wrong. Let's derive: For N columns, total width = N*cell + (N-1)*gap. The center pixel index = (total_width/2). So we can compute centerX = Math.floor(total_width/2). That's easier.

Similarly centerY = Math.floor(total_height/2).

So:

```
const centerX = Math.floor(CANVAS_W / 2);
const centerY = Math.floor(CANVAS_H / 2);
```

Then initial snake positions: start at (centerX, centerY) and extend in direction (dx, dy). For example, if direction is right (dx=1, dy=0), then positions: [{x=centerX, y=centerY}, {x=centerX+CELL, y=centerY}, {x=centerX+2*CELL, y=centerY}].

But we need to ensure those positions fit within canvas. Since centerX is roughly middle, adding 2*CELL (60) will stay within bounds (since canvas width 638, centerX~319, +60 = 379 < 638). So ok.

Thus we can set:

```
snake = [];
for (let i=0; i<3; i++) {
  snake.push({ x: centerX + i * CELL, y: centerY });
}
```

And direction = {dx:1, dy:0}.

Now spawn food: random cell not overlapping snake.

### Functions

**spawnFood**:

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

**draw**: As described earlier.

**handleKeyDown(e)**:

Check if e.key === 'ArrowUp' etc. Or e.code.

Simplify: Use key.charCodeAt(0) or e.key.

We'll map:

```
const KEY_MAP = {
  ArrowUp: [-1, 0],
  ArrowDown: [1, 0],
  ArrowLeft: [0, -1],
  ArrowRight: [0, 1],
  w: [-1, 0],
  W: [-1, 0],
  a: [-1, 0],
  A: [-1, 0],
  s: [1, 0],
  S: [1, 0],
  d: [0, 1],
  D: [0, 1]
};
```

But we can just handle arrow keys and WASD letters.

Implementation:

```
function handleKeyDown(e) {
  if (gameOver || paused) return;
  switch(e.key) {
    case 'ArrowUp':
    case 'w':
      dir = {dx:-1, dy:0};
      break;
    case 'ArrowDown':
    case 's':
      dir = {dx:1, dy:0};
      break;
    case 'ArrowLeft':
    case 'a':
      dir = {dx:0, dy:-1};
      break;
    case 'ArrowRight':
    case 'd':
      dir = {dx:0, dy:1};
      break;
    case 'p':
      paused = !paused;
      break;
    case ' ':
    case '\t':
      // space or enter restart
      restartGame();
      break;
    default:
      // ignore
  }
}
```

But we also need to enforce no reversal: If new direction is opposite of current direction, we ignore. So we need to keep track of current direction (`curDir`). Let's have variable `curDir = {dx, dy}`. Initially curDir = {dx:1, dy:0}. In handleKeyDown, compute candidate = {dx:..., dy:...}. If candidate.dx === -curDir.dx && candidate.dy === -curDir.dy, then ignore (reversal). Otherwise, set curDir = candidate.

Thus modify:

```
let curDir = {dx:1, dy:0};

function handleKeyDown(e) {
  if (gameOver || paused) return;
  switch(e.key) {
    case 'ArrowUp':
    case 'w':
      cand = {dx:-1, dy:0};
      if (!(cand.dx === -curDir.dx && cand.dy === -curDir.dy)) {
        curDir = cand;
      }
      break;
    case 'ArrowDown':
    case 's':
      cand = {dx:1, dy:0};
      if (!(cand.dx === -curDir.dx && cand.dy === -curDir.dy)) {
        curDir = cand;
      }
      break;
    case 'ArrowLeft':
    case 'a':
      cand = {dx:0, dy:-1};
      if (!(cand.dx === -curDir.dx && cand.dy === -curDir.dy)) {
        curDir = cand;
      }
      break;
    case 'ArrowRight':
    case 'd':
      cand = {dx:0, dy:1};
      if (!(cand.dx === -curDir.dx && cand.dy === -curDir.dy)) {
        curDir = cand;
      }
      break;
    case 'p':
      paused = !paused;
      break;
    case ' ':
    case '\t':
      restartGame();
      break;
  }
}
```

Note: For WASD mapping, we used w/s/d/a but we need to match arrow equivalents. Fine.

**restartGame()**:

```
function restartGame() {
  // reset snake
  snake = [];
  for (let i=0; i<3; i++) {
    snake.push({x: centerX + i * CELL, y: centerY});
  }
  curDir = {dx:1, dy:0};
  // reset food
  food = {x:0, y:0};
  spawnFood();
  score