The Board: a 2D Grid

Every Snake implementation starts with the same question: how do I represent the playing field? The answer is a 2D coordinate system. Each cell is addressed by (x, y) where x increases to the right and y increases downward.

const COLS = 40   // columns
const ROWS = 20   // rows
const CELL = 20   // pixels per cell
The board is COLS × ROWS cells. Every game object — snake segments, food — is stored as a list of (x, y) points. (0, 0) is always the top-left corner.

Movement: Directions and the Game Loop

The snake jumps one full cell per tick. Each tick the game reads the queued direction, computes a new head position, prepends it to the snake array, and removes the last segment unless food was eaten.

const delta = {
  RIGHT: { x:  1, y:  0 },
  LEFT:  { x: -1, y:  0 },
  DOWN:  { x:  0, y:  1 },
  UP:    { x:  0, y: -1 },
}

snake.unshift(newHead)   // grow head
snake.pop()              // shrink tail (unless food eaten)
You cannot reverse direction in a single step — the input queue rejects inputs that are the direct opposite of the current heading.

Collision Detection

The game ends in two cases, both checked after computing the new head but before drawing.

// Wall collision
if (newHead.x < 0 || newHead.x >= COLS ||
    newHead.y < 0 || newHead.y >= ROWS) gameOver()

// Self collision (tail excluded — it moves away this tick)
if (snake.slice(0, -1).some(s =>
    s.x === newHead.x && s.y === newHead.y)) gameOver()

Food and Growing

Food is placed at a random cell not occupied by the snake. When the head lands on food, the tail is not popped — the snake stays one cell longer. Each food adds one point.

function randomFood(snake) {
  let p
  do {
    p = { x: rand(COLS), y: rand(ROWS) }
  } while (snake.some(s => s.x === p.x && s.y === p.y))
  return p
}

Play

Arrow keys or WASD on desktop. D-pad on mobile. Hit Start to play.

score: 0 speed:
↑↓←→ or WASD · eat food to grow

Concepts at a Glance

ConceptImplementation
BoardCOLS × ROWS grid, each cell addressed by (x, y)
Game loopsetInterval at configurable ms, cleared on game over
MovementUnshift new head, pop tail each tick
Inputkeydown listener + queued nextDir per tick
Renderingcanvas.getContext("2d") draw calls each tick
CollisionArithmetic bounds check + Array.some overlap check
Fooddo-while random placement avoiding snake cells

[go back to /cool stuff]