← Back to Blog Apr 23, 2026 · 02:00 PM · 10 min read

How to Make a Snake Game: Fundamentals for Beginners

By Wuidi · Tutorial · Part 1

Snake is one of the best first games to build. The rules are simple, the logic is clear, and you will learn core game development concepts that apply to every game you build after this. In this first part of a 6-part series, we cover the fundamentals: grid system, snake movement, food spawning, collision detection, and the game loop.

What You Will Learn

Step 1: The Grid System

Snake games work on a grid. Each cell can be empty, part of the snake, or food. We define the grid size and cell size, then use them to calculate canvas dimensions.

const GRID_SIZE = 20;  // 20x20 grid
const CELL_SIZE = 20;  // each cell is 20px
const WIDTH = GRID_SIZE * CELL_SIZE;   // 400px
const HEIGHT = GRID_SIZE * CELL_SIZE;  // 400px

const canvas = document.getElementById('game');
canvas.width = WIDTH;
canvas.height = HEIGHT;
const ctx = canvas.getContext('2d');
Tip: Keep grid size and cell size as constants. This makes it easy to change the game board size later — just change one number and everything scales.

Step 2: Representing the Snake

The snake is an array of {x, y} objects. The first element is the head, the rest is the body. The snake starts with 3 segments.

let snake = [
  { x: 10, y: 10 },  // head
  { x: 9,  y: 10 },  // body
  { x: 8,  y: 10 },  // tail
];

let direction = { x: 1, y: 0 }; // moving right
let nextDirection = { x: 1, y: 0 };

We use nextDirection as a buffer. This prevents the snake from reversing into itself when the player presses keys quickly. The actual direction only updates at the start of each game tick.

Step 3: Moving the Snake

Each tick, we calculate a new head position by adding the direction to the current head. Then we add the new head to the front of the array and remove the tail. This creates the illusion of movement.

function moveSnake() {
  direction = { ...nextDirection };

  const head = snake[0];
  const newHead = {
    x: head.x + direction.x,
    y: head.y + direction.y
  };

  snake.unshift(newHead);  // add new head
  snake.pop();             // remove tail
}
Key insight: The snake does not actually "move" — we add a new segment at the front and remove one from the back. When the snake eats food, we skip the pop() so it grows by one segment.

Step 4: Handling Input

Listen for arrow keys and update nextDirection. The critical rule: the snake cannot reverse direction. If it is moving right, pressing left should do nothing.

document.addEventListener('keydown', (e) => {
  switch (e.key) {
    case 'ArrowUp':
      if (direction.y === 0)
        nextDirection = { x: 0, y: -1 };
      break;
    case 'ArrowDown':
      if (direction.y === 0)
        nextDirection = { x: 0, y: 1 };
      break;
    case 'ArrowLeft':
      if (direction.x === 0)
        nextDirection = { x: -1, y: 0 };
      break;
    case 'ArrowRight':
      if (direction.x === 0)
        nextDirection = { x: 1, y: 0 };
      break;
  }
});

The if (direction.y === 0) check ensures you can only change to up/down when currently moving horizontally, and vice versa. This is the simplest way to prevent 180-degree turns.

Step 5: Spawning Food

Food appears at a random grid position that is not occupied by the snake. We keep generating random positions until we find an empty cell.

let food = spawnFood();

function spawnFood() {
  let pos;
  do {
    pos = {
      x: Math.floor(Math.random() * GRID_SIZE),
      y: Math.floor(Math.random() * GRID_SIZE)
    };
  } while (snake.some(s => s.x === pos.x && s.y === pos.y));
  return pos;
}

function checkFood() {
  const head = snake[0];
  if (head.x === food.x && head.y === food.y) {
    // Grow: add segment back (undo the pop)
    snake.push({ ...snake[snake.length - 1] });
    food = spawnFood();
    score++;
  }
}
Tip: The do-while loop guarantees food never spawns on the snake. For small grids with a long snake, this could be slow — but for typical Snake games (20x20 grid), it is fast enough.

Step 6: Collision Detection

Two types of collision end the game: hitting a wall and hitting yourself.

function checkCollision() {
  const head = snake[0];

  // Wall collision
  if (head.x < 0 || head.x >= GRID_SIZE ||
      head.y < 0 || head.y >= GRID_SIZE) {
    return true;
  }

  // Self collision (skip head at index 0)
  for (let i = 1; i < snake.length; i++) {
    if (head.x === snake[i].x &&
        head.y === snake[i].y) {
      return true;
    }
  }

  return false;
}

Wall collision checks if the head is outside the grid bounds. Self collision checks if the head overlaps any body segment. Both are simple coordinate comparisons.

Step 7: Drawing Everything

Clear the canvas, draw the grid background, the snake, and the food. The snake head gets a slightly different color so the player can see which end is which.

function draw() {
  // Background
  ctx.fillStyle = '#0f0f23';
  ctx.fillRect(0, 0, WIDTH, HEIGHT);

  // Grid lines (optional, subtle)
  ctx.strokeStyle = 'rgba(255,255,255,0.05)';
  for (let i = 0; i <= GRID_SIZE; i++) {
    ctx.beginPath();
    ctx.moveTo(i * CELL_SIZE, 0);
    ctx.lineTo(i * CELL_SIZE, HEIGHT);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(0, i * CELL_SIZE);
    ctx.lineTo(WIDTH, i * CELL_SIZE);
    ctx.stroke();
  }

  // Snake
  snake.forEach((seg, i) => {
    ctx.fillStyle = i === 0 ? '#4cff72' : '#2ad060';
    ctx.fillRect(
      seg.x * CELL_SIZE + 1,
      seg.y * CELL_SIZE + 1,
      CELL_SIZE - 2,
      CELL_SIZE - 2
    );
  });

  // Food
  ctx.fillStyle = '#ff4040';
  ctx.beginPath();
  ctx.arc(
    food.x * CELL_SIZE + CELL_SIZE / 2,
    food.y * CELL_SIZE + CELL_SIZE / 2,
    CELL_SIZE / 2 - 2,
    0, Math.PI * 2
  );
  ctx.fill();
}

Step 8: The Game Loop

The game loop runs at a fixed interval. Each tick: move the snake, check for food, check for collisions, then draw. We use setInterval for simplicity — the interval controls the game speed.

let score = 0;
let gameOver = false;
const SPEED = 150; // milliseconds per tick

function gameLoop() {
  if (gameOver) return;

  moveSnake();
  checkFood();

  if (checkCollision()) {
    gameOver = true;
    drawGameOver();
    return;
  }

  draw();
  drawScore();
}

function drawScore() {
  ctx.fillStyle = '#fff';
  ctx.font = '14px "Segoe UI", sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('Score: ' + score, 8, 18);
}

function drawGameOver() {
  draw(); // draw final state
  ctx.fillStyle = 'rgba(0,0,0,0.7)';
  ctx.fillRect(0, 0, WIDTH, HEIGHT);
  ctx.fillStyle = '#ff4040';
  ctx.font = 'bold 24px "Segoe UI", sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('GAME OVER', WIDTH/2, HEIGHT/2 - 10);
  ctx.fillStyle = '#fff';
  ctx.font = '14px "Segoe UI", sans-serif';
  ctx.fillText('Score: ' + score, WIDTH/2, HEIGHT/2 + 20);
  ctx.fillStyle = '#888';
  ctx.font = '12px "Segoe UI", sans-serif';
  ctx.fillText('Press R to restart', WIDTH/2, HEIGHT/2 + 45);
}

setInterval(gameLoop, SPEED);

Step 9: Restart

Let the player restart by pressing R. Reset the snake, direction, score, and food.

document.addEventListener('keydown', (e) => {
  if ((e.key === 'r' || e.key === 'R') && gameOver) {
    snake = [
      { x: 10, y: 10 },
      { x: 9,  y: 10 },
      { x: 8,  y: 10 },
    ];
    direction = { x: 1, y: 0 };
    nextDirection = { x: 1, y: 0 };
    score = 0;
    gameOver = false;
    food = spawnFood();
  }
});

The Complete Picture

Here is how all the pieces fit together in the game loop:

  1. Input — player presses arrow key → nextDirection updates
  2. Move — new head calculated, added to front, tail removed
  3. Food check — if head is on food, grow snake and spawn new food
  4. Collision check — if head hits wall or body, game over
  5. Draw — clear canvas, draw grid, snake, food, score
  6. Repeat — every 150ms
Why Snake is a great first game: It teaches arrays (snake body), game loops (fixed interval), input handling (keyboard events), collision detection (coordinate comparison), and state management (score, game over). These concepts appear in every game you will ever build.

What's Next?

This gives you a fully playable Snake game. From here you can add:

Continue to Part 2: High Score where we add localStorage persistence, floating score animations, and a game over screen.

More Articles

👾
Tutorial How to Make a Pac-Man Game for Beginners Tutorial · Apr 23, 2026 · 08:00 AM
🏆
Tutorial Pac-Man Score Tracking: Count Dots and Display the Score Tutorial · Apr 23, 2026 · 09:00 AM
👻
Tutorial Pac-Man Ghost Chase AI: Make Ghosts Hunt the Player Tutorial · Apr 23, 2026 · 10:00 AM
← Back to Blog