Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ac-unefm/snake-game/llms.txt

Use this file to discover all available pages before exploring further.

Snake Classic Game drives its animation with a recursive setTimeout rather than requestAnimationFrame. This is a deliberate choice: because the tick interval must shrink as the player’s score grows, a fixed-rate requestAnimationFrame loop would require extra bookkeeping to throttle updates. Using setTimeout directly with a computed delay keeps the implementation simple — each tick schedules its own successor at exactly the right interval for the current speed.

Loop lifecycle

1

startGame()

startGame() calls init() to reset all per-attempt state, sets running = true, clears the status message, focuses the canvas, and calls scheduleNext() to kick off the first tick.
2

scheduleNext()

function scheduleNext() {
  if (!running) return;
  loopId = setTimeout(tick, getTick(score));
}
The delay passed to setTimeout is getTick(score) — a value that decreases as the score rises. The returned handle is stored in loopId so it can be cancelled by endGame().
3

tick()

function tick() {
  update();
  draw();
  scheduleNext();
}
Each tick executes the full game cycle: advance state, render the new frame, then re-queue itself. If update() calls endGame(), running is set to false and scheduleNext() returns immediately without queuing another tick.
4

endGame()

gameOver = true;
running  = false;
clearTimeout(loopId);
Setting running = false and cancelling the pending timeout stops the loop immediately. endGame() then records the attempt metrics and either prompts for the next attempt or shows the session results panel.

update() logic

update() is the game’s state machine — called once per tick, it advances the snake and checks every termination condition:
function update() {
  dir = { ...nextDir };
  const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };

  if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) { endGame(false); return; }
  if (snake.some(s => s.x === head.x && s.y === head.y))             { endGame(false); return; }

  snake.unshift(head);
  attemptSteps++;

  const currentTick = getTick(score);
  if (currentTick < attemptMinTick) attemptMinTick = currentTick;

  if (head.x === food.x && head.y === food.y) {
    score++;
    // level-up check, best-score update, placeFood()...
  } else {
    snake.pop();
  }
}
Step-by-step breakdown:
  1. Direction commitdir is updated from the buffered nextDir at the start of each tick, not at input time. This prevents mid-tick direction changes from causing inconsistencies.
  2. New head — the new head cell is computed by adding the direction vector to the current head position.
  3. Wall check — if the new head lies outside [0, COLS) × [0, ROWS), the attempt ends immediately.
  4. Self-collision checksnake.some(...) scans every existing segment. A hit on any cell triggers endGame(false).
  5. unshift (grow) — the new head is prepended to the snake array. At this point the snake is one cell longer than it should be.
  6. Step counterattemptSteps is incremented unconditionally; it counts every cell traversed regardless of food.
  7. Speed trackingattemptMinTick records the shortest tick interval seen this attempt (used to compute peak speed in session stats).
  8. Food collision — if the new head coincides with food, score is incremented, level and best-score checks run, and placeFood() is called. The tail is not removed, so the snake stays one cell longer — net growth of one segment.
  9. pop (no food) — when no food was eaten, the last tail segment is removed, keeping total length constant.

placeFood() algorithm

Rather than retrying random positions until a free cell is found (which would loop indefinitely on a nearly-full board), placeFood() builds an explicit list of all free cells and picks uniformly from it:
function placeFood() {
  const occupied = new Set(snake.map(s => `${s.x},${s.y}`));
  const free = [];
  for (let x = 0; x < COLS; x++)
    for (let y = 0; y < ROWS; y++)
      if (!occupied.has(`${x},${y}`)) free.push({ x, y });

  if (free.length === 0) { endGame(true); return; }
  food = free[Math.floor(Math.random() * free.length)];
}
  • A Set of "x,y" strings is built from the current snake body in O(n).
  • Every cell in the 20×20 grid is tested against the set. Free cells are collected into free[].
  • If free is empty the board is completely filled — endGame(true) is called, signalling a win condition rather than a loss.
  • Otherwise a uniformly random free cell is selected.

Direction buffering

The game maintains two direction variables:
  • dir — the direction that was actually applied in the last tick. Written only inside update().
  • nextDir — the direction requested by the player. Written by all input handlers.
Input handlers call applyDirection() before updating nextDir:
function applyDirection(direccion) {
  if (direccion.x !== -dir.x || direccion.y !== -dir.y) nextDir = direccion;
  if (!running && !gameOver) startGame();
}
The condition d.x !== -dir.x || d.y !== -dir.y rejects any direction that is the exact opposite of the current direction. Without this guard, the player could reverse 180° into the snake’s own neck on the very next tick, causing an instant self-collision. Because nextDir is only read at the start of update(), multiple key presses between ticks are collapsed into the last valid one — the buffer holds exactly one pending direction.

Speed functions

Two small pure functions encapsulate the entire speed schedule:
function getLevel(puntuacion) {
  return Math.floor(puntuacion / STEP_SCORE) + 1;
}

function getTick(puntuacion) {
  const lvl = getLevel(puntuacion);
  return Math.max(MIN_TICK, BASE_TICK - (lvl - 1) * STEP_MS);
}
getLevel maps score to level: every STEP_SCORE (5) points the level increments by 1, starting at level 1. getTick converts a level to a tick interval in milliseconds: it subtracts STEP_MS (12 ms) per level above 1 from the BASE_TICK (130 ms) base, floored at MIN_TICK (45 ms). At level 1 the interval is 130 ms (~7.7 ticks/s); at level 8 and above it is 45 ms (~22 ticks/s). The Math.max guard ensures the interval never drops below MIN_TICK regardless of score.

Build docs developers (and LLMs) love