Skip to Content
🎮 GameplayGame Mechanics

Last Updated: 3/18/2026


Understanding how Snake Game implements core mechanics like collision detection, food spawning, grid expansion, and score tracking. This guide explains the internal logic that drives gameplay.

The Game Loop

The game runs on a fixed interval timer controlled by gameSpeed, which starts at 200 milliseconds (base speed). Every interval, the update() method executes:

  1. Process the next direction from user input or bot AI
  2. Calculate the new head position by moving one cell in the current direction
  3. Check for collision with walls or the snake’s body
  4. Add the new head to the front of the snake array
  5. Check if the head position matches the food position
  6. If food eaten: increment score, spawn new food, check grid expansion threshold
  7. If food not eaten: remove the tail segment (maintaining constant length)
  8. Redraw the canvas with updated positions

This loop continues until a collision occurs, triggering the game-over sequence.

Movement and Direction

The snake’s direction property determines movement on each update. Direction changes come from keyboard input (arrow keys or WASD) or bot AI decisions.

Direction Validation — You cannot reverse directly into the opposite direction. The isValidDirection() check prevents:

  • UP → DOWN (or DOWN → UP)
  • LEFT → RIGHT (or RIGHT → LEFT)

If the snake is moving RIGHT and you press LEFT, the input is ignored. This prevents instant self-collision, which would make the game unplayable.

Buffered Input — User input sets nextDirection, which applies on the next game loop iteration. This allows direction changes to queue while the current move completes, making controls more responsive at high speeds.

Collision Detection

The game ends when the snake’s head collides with:

Grid Boundaries — The inBounds() check verifies the head position falls within 0 ≤ x < gridSize and 0 ≤ y < gridSize. Moving outside this range causes instant game-over.

Snake Body — The game maintains a snakeSet (a JavaScript Set) containing string keys for every snake segment in the format "x,y". On each move, the game checks if the new head position’s key exists in snakeSet. If it does, the snake collided with itself.

Using a Set for body collision detection runs in O(1) constant time regardless of snake length. A naive array search would be O(n), creating noticeable lag when the snake fills 50%+ of a large grid.

Food Spawning

When the snake eats food or the grid expands, spawnFood() selects a random empty cell.

Low Fill Ratio (<50%) — Random sampling: generate random x,y coordinates until landing on an empty cell. This typically succeeds in 1–3 attempts when the grid is mostly empty.

High Fill Ratio (≥50%) — Enumeration: collect all empty cells into an array, then pick one at random. At high fill ratios, random sampling could take dozens or hundreds of attempts. Direct enumeration is faster despite checking every cell.

The food position is stored as { x, y } and compared against the head position on each update using simple coordinate equality: head.x === food.x && head.y === food.y.

Grid Expansion Mechanics

The game calculates fill percentage after every food eaten:

fillPercentage = snakeLength / (gridSize × gridSize)

When fillPercentage ≥ 0.25 (25%), the grid doubles:

  1. gridSize *= 2 — Double both width and height
  2. level++ — Increment the level counter
  3. gameSpeed /= 2 — Cut game loop interval in half (doubling effective speed)
  4. Restart the game loop with the new speed
  5. Spawn new food (existing food position remains valid, as the grid expanded around it)

The snake’s absolute position doesn’t change during expansion. If your snake occupied cells (5,5) through (5,10) on a 10×10 grid, those same cells remain occupied on the new 20×20 grid.

Speed Scaling — Each grid expansion halves the interval between moves:

  • Level 1: 200ms per move (5 moves/second)
  • Level 2: 100ms per move (10 moves/second)
  • Level 3: 50ms per move (20 moves/second)
  • Level 4: 25ms per move (40 moves/second)

By level 4, the game demands frame-perfect reactions. Few players reach level 5 manually.

Score and High Score

Score — The score property increments by 1 each time food is eaten. It starts at 0 on each new game and has no upper limit.

High Score Persistence — When a game ends, the current score is compared against the stored high score from localStorage. If the current score exceeds the stored value, it’s saved using localStorage.setItem('snake-high-score', score).

High scores persist per browser. Clearing browser data or playing in incognito mode resets the high score to 0.

Pause and Resume

Pressing P toggles the isPaused boolean. When paused, the update() method exits early without processing movement or collision:

if (this.isPaused || this.isGameOver) return

The game loop timer continues running, but each interval does nothing. Resuming simply sets isPaused = false, allowing the next interval to process normally.

This approach keeps the timer alive, avoiding the complexity of stopping and restarting intervals. The game loop interval (setInterval) only changes when the grid expands and speed increases.

Game Over Sequence

When collision is detected:

  1. Set isGameOver = true to stop the update loop
  2. Stop auto-rotation (if bot mode was active)
  3. Clear the game loop interval with clearInterval(this.gameLoop)
  4. Compare current score to stored high score, update if beaten
  5. Display the game-over screen with final score and high score
  6. Stop all game logic (bot decisions, rendering updates, input processing)

The game-over state persists until the player clicks “Play Again” or presses R, which calls startNewGame() to reset all state.

State Initialization

Starting a new game via startNewGame():

  1. Reset gridSize to initialGridSize (user’s preferred starting size)
  2. Position snake at grid center: { x: center, y: center }
  3. Set direction = 'RIGHT' and nextDirection = 'RIGHT'
  4. Reset score, level, and speed to defaults
  5. Clear pause and game-over flags
  6. Spawn initial food
  7. Render the initial game state
  8. Wait for directional input to start the game loop

The snake doesn’t move until the player presses a direction key (or bot mode is enabled). This gives players time to see the initial board state and plan their approach.

Grid Size Customization

Before starting a game, pressing + or - adjusts initialGridSize in increments of 5, clamped between 5×5 and 50×50.

Changing this setting:

  • Updates gridSize immediately (visual feedback)
  • Recalculates canvas dimensions with updateCanvasSize()
  • Repositions the snake at the new grid’s center
  • Spawns food in the new grid
  • Redraws the canvas

The setting only applies when gameStarted === false. Once the game loop begins, grid size changes occur only through the automatic expansion mechanic.

What’s Next

  • Bot Automation: How the AI bots use these mechanics to make decisions and survive longer
  • Configuration: Customizing game speed, grid limits, and rendering parameters