LearnAI ToolsCareerPractice BuildsPlayContact
C ProgrammingIntermediate~2.5 hours

Snake Game

Build the classic Snake game with score tracking, increasing difficulty, and collision detection.

ArraysLoopsConditional Logic

Overview

Snake is a great intermediate C project because the entire game state — the board, the snake's body, and the food — can be represented with plain arrays, and every rule of the game ("did the snake hit a wall?", "did it hit itself?", "did it eat the food?") is just conditional logic applied to those arrays inside a loop. There is no graphics library involved; the whole game runs in the terminal, one turn at a time.

This tutorial builds a turn-based version of Snake: instead of the snake moving continuously in real time, the board is redrawn after every move, and the player types a direction (`w`, `a`, `s`, or `d`) and presses Enter to make the snake take one step. This keeps the project entirely within standard C — no non-blocking keyboard input, no operating-system-specific headers — so the exact same code compiles and runs on Windows, Linux, and macOS.

What You'll Build
  • A fixed-size 2D character grid representing the game board.
  • A snake body tracked as an array of row/column positions that grows as it eats.
  • Random food placement that avoids spawning on top of the snake.
  • Turn-based movement driven by `scanf`, with wall and self-collision detection.
  • A score counter and a difficulty level that increases as the score grows.

Prerequisites

  • Arrays — one-dimensional arrays for the snake's body, and two-dimensional arrays for the grid.
  • Loops — `for` and `while` loops, including nested loops for iterating over a grid.
  • Conditional logic — `if`/`else if`/`else` chains and logical operators (`&&`, `||`).
  • Functions — passing arrays to functions and returning values that control the game loop.
  • Basic use of `rand()` and `srand()` from `<stdlib.h>` for randomness.

Project Structure

The board is a 10-row by 20-column grid, represented as a 2D `char` array that gets cleared and redrawn every turn — walls are drawn around the border, the snake's body is drawn as `O` characters with the head as `@`, and food is drawn as `*`. The snake's own position is stored separately, as an array of `{row, col}` pairs in `Segment snake[MAX_LEN]`, with `snakeLength` tracking how many segments are currently in use. Each turn we compute where the head would move next based on the player's input, check that position for collisions, and if it is safe, shift every segment forward by one position (unless the snake just ate food, in which case it grows instead of shifting).

Difficulty in a turn-based game cannot come from speeding up a timer, since there is no timer driving movement — instead, this version tracks a `level` that increases every 30 points scored, giving the player a visible sense of progression on top of the genuinely increasing challenge of a longer snake body, which becomes easier to run into as it grows.

Step 1: Model the Board and the Snake

We start with constants for the board size and maximum snake length, a struct for a single body segment, and global-ish state variables that every function will operate on. Keeping the state in a small number of well-named variables makes the rest of the game logic easy to follow.

#include <stdio.h> // printf/putchar for drawing the board and scanf for reading moves
#include <stdlib.h> // rand()/srand() for randomly placing food
#include <time.h> // time() used to seed the random number generator differently each run
#define ROWS 10 // Board height in characters, including the top and bottom border
#define COLS 20 // Board width in characters, including the left and right border
#define MAX_LEN 100 // Longest the snake can grow to, sizes the snake[] array
typedef struct {
int row; // Row position of one snake segment on the grid
int col; // Column position of one snake segment on the grid
} Segment;
Segment snake[MAX_LEN]; // Ordered list of body segments; index 0 is always the head
int snakeLength; // How many of snake[]'s slots currently hold real segments
int foodRow, foodCol; // Current position of the food on the board
int score; // Player's running score, increases each time food is eaten
int level; // Difficulty level, increases as score grows

`snake[0]` will always represent the head of the snake, and each following index represents the next segment of the body, ending at the tail at index `snakeLength - 1`. This ordering matters later when we shift the body forward on each move.

Step 2: Draw the Grid

Drawing the board means building a fresh 2D character grid every turn: fill the interior with spaces, draw `#` borders around the edge, place the food, then place every snake segment on top. Rebuilding the grid from scratch each turn (rather than trying to update it incrementally) keeps the drawing logic simple and bug-free.

void drawBoard(void) {
char grid[ROWS][COLS]; // Fresh scratch grid rebuilt from scratch every call, avoiding stale state
int i, r, c;
for (r = 0; r < ROWS; r++) {
for (c = 0; c < COLS; c++) {
if (r == 0 || r == ROWS - 1 || c == 0 || c == COLS - 1) { // Outermost ring of cells is the wall
grid[r][c] = '#';
} else {
grid[r][c] = ' '; // Interior starts empty before food/snake are drawn on top
}
}
}
grid[foodRow][foodCol] = '*'; // Mark the food's current cell
for (i = 0; i < snakeLength; i++) {
grid[snake[i].row][snake[i].col] = (i == 0) ? '@' : 'O'; // Head is '@', every other segment is 'O'
}
printf("\n");
for (r = 0; r < ROWS; r++) {
for (c = 0; c < COLS; c++) {
putchar(grid[r][c]); // Print the finished grid one character at a time
}
printf("\n"); // End of row
}
printf("Score: %d Level: %d\n", score, level); // Status line under the board
}

Step 3: Place Food Randomly

Placing food means picking a random interior cell (never on the border) and rejecting the choice if it happens to land on the snake's body, trying again until a free cell is found. Because the board is small and the snake starts short, this loop finishes almost instantly in practice.

int isOnSnake(int r, int c) {
int i;
for (i = 0; i < snakeLength; i++) {
if (snake[i].row == r && snake[i].col == c) { // Compare the target cell against every body segment
return 1;
}
}
return 0; // No segment occupied this cell
}
void placeFood(void) {
int r, c;
do {
r = 1 + rand() % (ROWS - 2); // Random row strictly inside the top/bottom border
c = 1 + rand() % (COLS - 2); // Random column strictly inside the left/right border
} while (isOnSnake(r, c)); // Reject and retry if the random cell landed on the snake
foodRow = r; // Commit the chosen position as the new food location
foodCol = c;
}

`1 + rand() % (ROWS - 2)` generates a random row strictly between the top and bottom borders; the same idea applies to the column. `isOnSnake` is a small helper we will reuse again in Step 5 for self-collision detection.

Step 4: Read a Move and Compute the Next Head

Each turn, the player types a single character direction. We read it with `scanf(" %c", &dir)` — the leading space skips any leftover whitespace/newline from the previous input — and translate it into a row/column offset that gets added to the current head position to compute where the head would move next.

void getNextHead(char dir, int *newRow, int *newCol) {
int dr = 0, dc = 0; // Row/column offset the head should move by this turn
switch (dir) {
case 'w': dr = -1; dc = 0; break; // Up: move one row toward the top of the screen
case 's': dr = 1; dc = 0; break; // Down: move one row toward the bottom
case 'a': dr = 0; dc = -1; break; // Left: move one column left
case 'd': dr = 0; dc = 1; break; // Right: move one column right
default: dr = 0; dc = 0; break; // Unrecognized key: head stays put this turn
}
*newRow = snake[0].row + dr; // Written through the pointer so the caller sees the computed value
*newCol = snake[0].col + dc;
}

Using output parameters (`int *newRow, int *newCol`) lets one function hand back two computed values to the caller, which is the standard C idiom for "returning" more than one value from a function.

Step 5: Detect Collisions

A move is illegal if the new head position lands on a wall (row or column 0 or the last index) or on the snake's own body. We check both conditions in one function and return `1` for "game over" or `0` for "still safe."

int isCollision(int r, int c) {
if (r <= 0 || r >= ROWS - 1 || c <= 0 || c >= COLS - 1) { // Cell falls on or past the wall ring
return 1;
}
if (isOnSnake(r, c)) { // Cell is occupied by the snake's own body
return 1;
}
return 0; // Neither wall nor body, so the move is safe
}

Note that checking `isOnSnake` here also correctly catches the case where the new head would land on the current tail segment, which is fine to treat as a collision in this simple version even though the real tail is about to move — it keeps the collision rule easy to reason about, at the minor cost of disallowing a technically-legal move into the tail's vacated cell.

Step 6: Move the Snake and Grow on Food

Moving the snake means shifting every segment one position toward the tail — but we must do this from the tail end backward, otherwise we would overwrite a segment before copying its old value forward. If the new head lands on the food, we skip removing the tail (so the snake grows by one segment) and immediately place new food.

void moveSnake(int newRow, int newCol) {
int i;
int ateFood = (newRow == foodRow && newCol == foodCol); // True if the new head lands exactly on the food
if (ateFood) {
snakeLength++; // Grow by one segment; this changes how far the shift loop below reaches
score += 10;
if (score % 30 == 0) { // Every 30 points marks a new difficulty milestone
level++;
}
}
for (i = snakeLength - 1; i > 0; i--) { // Shift from the tail backward so we never overwrite unread data
snake[i] = snake[i - 1]; // Each segment takes on the position of the one ahead of it
}
snake[0].row = newRow; // Head moves to the newly computed position
snake[0].col = newCol;
if (ateFood) {
placeFood(); // Only spawn new food after the old food has actually been consumed
}
}

Growing the snake before shifting is what makes the tail "stay behind" for one extra turn: since `snakeLength` increased first, the loop now shifts one more segment than before, and the old tail segment (still holding its previous value) becomes the new last segment instead of being dropped.

Step 7: Increase Difficulty and Run the Game Loop

The main loop initializes the snake with one segment in the middle of the board, seeds the random number generator, places the first food, and then repeatedly draws the board, reads a direction, checks for collisions, and either ends the game or moves the snake. Every 30 points, `level` increases inside `moveSnake`, giving the player a visible sense of progression even though the board itself does not change size.

int main(void) {
char dir; // Direction character read from the player each turn
int newRow, newCol; // Computed position the head would move to this turn
srand((unsigned int)time(NULL)); // Seed the RNG so food placement differs between runs
snakeLength = 1; // Snake starts as a single segment
snake[0].row = ROWS / 2; // Spawn roughly in the vertical middle of the board
snake[0].col = COLS / 2; // Spawn roughly in the horizontal middle of the board
score = 0;
level = 1;
placeFood(); // Place the first piece of food before the game loop starts
printf("Snake Game (turn-based)\n");
printf("Controls: w = up, s = down, a = left, d = right, then press Enter\n");
while (1) { // Runs until an explicit break on collision; this is a turn-based game, not timed
drawBoard();
printf("Move (w/a/s/d): ");
scanf(" %c", &dir); // Leading space skips leftover whitespace/newline from prior input
getNextHead(dir, &newRow, &newCol); // Compute where the head would go, without moving yet
if (isCollision(newRow, newCol)) { // Check before committing to the move
printf("\nGame Over! You hit %s.\n",
(newRow <= 0 || newRow >= ROWS - 1 || newCol <= 0 || newCol >= COLS - 1)
? "a wall" : "yourself"); // Distinguish the two collision causes for the message
printf("Final Score: %d Final Level: %d\n", score, level);
break; // End the game loop
}
moveSnake(newRow, newCol); // Safe to commit the move
}
return 0;
}
Extend This Project Idea: Real-Time Input

This tutorial deliberately reads one move per Enter key press so the code stays 100% portable standard C. On Windows, you can experiment with `conio.h`'s `getch()` (and a manual timing loop) to read a key the instant it is pressed, without waiting for Enter, turning this into a real-time game. That approach is Windows-only and not part of the core tutorial — see "Extend This Project" below for other platforms.

Complete Code

Here is the full, portable, turn-based Snake game, ready to compile with `gcc snake.c -o snake`.

#include <stdio.h> // printf/putchar for drawing the board and scanf for reading moves
#include <stdlib.h> // rand()/srand() for randomly placing food
#include <time.h> // time() used to seed the random number generator differently each run
#define ROWS 10 // Board height in characters, including the top and bottom border
#define COLS 20 // Board width in characters, including the left and right border
#define MAX_LEN 100 // Longest the snake can grow to, sizes the snake[] array
typedef struct {
int row; // Row position of one snake segment on the grid
int col; // Column position of one snake segment on the grid
} Segment;
Segment snake[MAX_LEN]; // Ordered list of body segments; index 0 is always the head
int snakeLength; // How many of snake[]'s slots currently hold real segments
int foodRow, foodCol; // Current position of the food on the board
int score; // Player's running score, increases each time food is eaten
int level; // Difficulty level, increases as score grows
void drawBoard(void) {
char grid[ROWS][COLS]; // Fresh scratch grid rebuilt from scratch every call, avoiding stale state
int i, r, c;
for (r = 0; r < ROWS; r++) {
for (c = 0; c < COLS; c++) {
if (r == 0 || r == ROWS - 1 || c == 0 || c == COLS - 1) { // Outermost ring of cells is the wall
grid[r][c] = '#';
} else {
grid[r][c] = ' '; // Interior starts empty before food/snake are drawn on top
}
}
}
grid[foodRow][foodCol] = '*'; // Mark the food's current cell
for (i = 0; i < snakeLength; i++) {
grid[snake[i].row][snake[i].col] = (i == 0) ? '@' : 'O'; // Head is '@', every other segment is 'O'
}
printf("\n");
for (r = 0; r < ROWS; r++) {
for (c = 0; c < COLS; c++) {
putchar(grid[r][c]); // Print the finished grid one character at a time
}
printf("\n"); // End of row
}
printf("Score: %d Level: %d\n", score, level); // Status line under the board
}
int isOnSnake(int r, int c) {
int i;
for (i = 0; i < snakeLength; i++) {
if (snake[i].row == r && snake[i].col == c) { // Compare the target cell against every body segment
return 1;
}
}
return 0; // No segment occupied this cell
}
void placeFood(void) {
int r, c;
do {
r = 1 + rand() % (ROWS - 2); // Random row strictly inside the top/bottom border
c = 1 + rand() % (COLS - 2); // Random column strictly inside the left/right border
} while (isOnSnake(r, c)); // Reject and retry if the random cell landed on the snake
foodRow = r; // Commit the chosen position as the new food location
foodCol = c;
}
void getNextHead(char dir, int *newRow, int *newCol) {
int dr = 0, dc = 0; // Row/column offset the head should move by this turn
switch (dir) {
case 'w': dr = -1; dc = 0; break; // Up: move one row toward the top of the screen
case 's': dr = 1; dc = 0; break; // Down: move one row toward the bottom
case 'a': dr = 0; dc = -1; break; // Left: move one column left
case 'd': dr = 0; dc = 1; break; // Right: move one column right
default: dr = 0; dc = 0; break; // Unrecognized key: head stays put this turn
}
*newRow = snake[0].row + dr; // Written through the pointer so the caller sees the computed value
*newCol = snake[0].col + dc;
}
int isCollision(int r, int c) {
if (r <= 0 || r >= ROWS - 1 || c <= 0 || c >= COLS - 1) { // Cell falls on or past the wall ring
return 1;
}
if (isOnSnake(r, c)) { // Cell is occupied by the snake's own body
return 1;
}
return 0; // Neither wall nor body, so the move is safe
}
void moveSnake(int newRow, int newCol) {
int i;
int ateFood = (newRow == foodRow && newCol == foodCol); // True if the new head lands exactly on the food
if (ateFood) {
snakeLength++; // Grow by one segment; this changes how far the shift loop below reaches
score += 10;
if (score % 30 == 0) { // Every 30 points marks a new difficulty milestone
level++;
}
}
for (i = snakeLength - 1; i > 0; i--) { // Shift from the tail backward so we never overwrite unread data
snake[i] = snake[i - 1]; // Each segment takes on the position of the one ahead of it
}
snake[0].row = newRow; // Head moves to the newly computed position
snake[0].col = newCol;
if (ateFood) {
placeFood(); // Only spawn new food after the old food has actually been consumed
}
}
int main(void) {
char dir; // Direction character read from the player each turn
int newRow, newCol; // Computed position the head would move to this turn
srand((unsigned int)time(NULL)); // Seed the RNG so food placement differs between runs
snakeLength = 1; // Snake starts as a single segment
snake[0].row = ROWS / 2; // Spawn roughly in the vertical middle of the board
snake[0].col = COLS / 2; // Spawn roughly in the horizontal middle of the board
score = 0;
level = 1;
placeFood(); // Place the first piece of food before the game loop starts
printf("Snake Game (turn-based)\n");
printf("Controls: w = up, s = down, a = left, d = right, then press Enter\n");
while (1) { // Runs until an explicit break on collision; this is a turn-based game, not timed
drawBoard();
printf("Move (w/a/s/d): ");
scanf(" %c", &dir); // Leading space skips leftover whitespace/newline from prior input
getNextHead(dir, &newRow, &newCol); // Compute where the head would go, without moving yet
if (isCollision(newRow, newCol)) { // Check before committing to the move
printf("\nGame Over! You hit %s.\n",
(newRow <= 0 || newRow >= ROWS - 1 || newCol <= 0 || newCol >= COLS - 1)
? "a wall" : "yourself"); // Distinguish the two collision causes for the message
printf("Final Score: %d Final Level: %d\n", score, level);
break; // End the game loop
}
moveSnake(newRow, newCol); // Safe to commit the move
}
return 0;
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add real-time input using `conio.h`'s `getch()` on Windows so the snake keeps moving on a timer instead of waiting for Enter each turn.
  • Introduce obstacle tiles that appear on the board at higher levels and count as collisions, forcing the player to navigate around them.
  • Save the highest score reached to a text file and display it as a "High Score" alongside the current score at the top of the board.
  • Add a second, slower-moving "rival" snake controlled by simple AI logic that the player must also avoid.
  • Wrap the snake around the edges of the board instead of treating the border as a wall, and see how that changes the difficulty balance.

Summary

You built a fully playable, portable Snake game using nothing but arrays, loops, and conditional logic — no graphics library and no platform-specific input handling required. The core techniques here, especially representing moving game objects as arrays of positions and redrawing a grid from scratch every frame, are the same techniques used in far more complex terminal-based games.