Build a Snake Game with PyGame

Build a classic Snake game in Python using PyGame with collision detection.

Introduction

30 Second Summary

Remember playing Snake on an old phone, steering a growing line around the screen while desperately avoiding the walls? That simple concept has taught millions of people how games actually work under the hood.

In this project, you will build a fully playable Snake game from scratch using Python and PyGame. You will control a snake with your arrow keys, eat food to grow longer, and implement the collision logic that makes the game challenging.

What You'll Build

By the end, you will have a Snake game running in its own window that you can play, restart, and show off to anyone watching over your shoulder.

By the end of this project, you'll have:

  • A playable snake you control with arrow keys, gliding smoothly across a grid-based game window.
  • A food and growth system where eating red squares makes your snake longer and your score climbs higher.
  • Real collision detection that ends the game instantly when you hit a wall or your own tail, creating genuine tension as the snake grows.
  • A game over screen with restart so you can jump right back in and try to beat your high score.
  • Secret Mission: Add increasing difficulty so the snake speeds up as your score climbs.

Are there any prerequisites?

None. This guide walks you through setting up everything you need, including Python and PyGame. No prior game development experience is required.

Before We Start

You are about to build a Snake game that runs in its own window, complete with arrow-key controls, growing mechanics, and collision detection. All of this is powered by PyGame, a game development library for Python.

Before you can open any game windows or draw anything to screen, you need PyGame installed and verified. This step gets your environment ready so you can jump straight into building.

In this step, get ready to:
  • Confirm Python 3.14.6 is ready to use.
  • Install the pygame-ce game library.
  • Verify everything works by playing a test game.
Open VS Code and verify Python
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open a terminal inside VS Code by clicking Terminal in the top menu bar, then selecting New Terminal.
  • Confirm Python is available by running this command:
py --version

You should see Python 3.14.6 printed in the terminal.

Don't see Python 3.14.6?

If the command is not recognized, Python may not be on your system PATH. Reinstall Python from python.org and check the box that says Add Python to PATH during installation.

help me fix Python not being recognized in my terminal

Install pygame-ce

pygame-ce is the community edition of PyGame, maintained by the original core developers. It supports Python 3.14 and installs as the regular pygame package, so all standard PyGame code works unchanged.

  • Install pygame-ce by running this command:
py -m pip install pygame-ce==2.5.7

Wait for the installation to finish. You should see a Successfully installed message at the end of the output.

Why pygame-ce instead of pygame?

The original pygame package does not support Python 3.14. Installation fails because it relies on a module that was removed in newer Python versions.

pygame-ce is the actively maintained fork built by the same core developers. It has the same API, so every PyGame tutorial and example works exactly the same way.

Installation failed?

Make sure you typed pygame-ce (with the hyphen and ce) and not just pygame. The original pygame package will fail to install on Python 3.14.

help me troubleshoot my pygame-ce installation

Verify PyGame works

pygame-ce ships with built-in example games you can run to confirm everything installed correctly.

  • Before you run the next command, what do you think will happen?
  • Verify your installation by running this command:
py -m pygame.examples.aliens

A game window opens with a playable Aliens game. You can move your ship and shoot. This proves PyGame is installed and working correctly.

  • Play for a moment, then close the Aliens window to return to your terminal.

No game window appeared?

If you see an error about pygame not being found, the install may not have completed. Re-run the install command from the previous substep and check for error messages in the output.

help me get the pygame aliens example running

Your environment is locked and loaded. Next up, you will create the game window and get a snake moving across the screen with arrow-key controls.

Set Up PyGame and Verify Installation

PyGame is installed and working on your machine. Now it is time to put it to use.

Every game needs three things: a window to draw on, a loop that keeps it alive, and something moving on screen. In this step, you will create a snake that you control with your arrow keys.

In this step, get ready to:
  • Create a PyGame window with color constants and a game loop.
  • Add a snake that moves continuously across the screen.
  • Wire up arrow key controls with direction reversal prevention.
Create the game window

Before you can draw a snake, you need a window and a game loop that refreshes the screen many times per second. The game loop is the heartbeat of every game.

  • In VS Code, create a new file called snake.py in your project folder on the Desktop.
  • Add the following setup code to snake.py:
import pygame
import random

pygame.init()

SCREEN_WIDTH = 600
SCREEN_HEIGHT = 400
CELL_SIZE = 20

BLACK = (0, 0, 0)
GREEN = (0, 200, 0)
DARK_GREEN = (0, 150, 0)
RED = (200, 0, 0)
WHITE = (255, 255, 255)

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Snake Game")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 35)

What does this code do?

This block initialises PyGame and defines the constants your game needs.

  • SCREEN_WIDTH and SCREEN_HEIGHT set the window to 600x400 pixels.
  • CELL_SIZE of 20 means the snake moves in a grid of 20-pixel squares.
  • pygame.display.set_mode() opens the actual game window.
  • clock controls how fast the game runs (frames per second).
  • Now add the game loop below your setup code. This keeps the window open and refreshes it every frame:
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    screen.fill(BLACK)
    pygame.display.flip()
    clock.tick(10)

pygame.quit()

What does the game loop do?

The while running loop repeats every frame. Each cycle does three things:

  • Checks for events (like clicking the X button to close the window).
  • Fills the screen with black to clear the previous frame.
  • clock.tick(10) limits the loop to 10 frames per second, so the game runs at a playable speed.
  • Save snake.py.
  • Run the game by entering this in your terminal:
py snake.py

A black 600x400 window titled "Snake Game" appears. Close it by clicking the X button.

Window not appearing?

  • Make sure you saved snake.py before running.
  • Check your terminal is in the same folder as snake.py. Run ls (macOS) or dir (Windows) to confirm snake.py is listed.
  • If you see a ModuleNotFoundError for pygame, re-run py -m pip install pygame-ce==2.5.7 and try again.

Help me debug why my PyGame window is not appearing.

Add the snake with movement

The snake is a list of (x, y) positions. Each frame, you add a new head position in the current direction and remove the tail. This creates the illusion of smooth movement.

  • In snake.py, find the line running = True. Add these two lines directly above it:
snake = [(100, 100), (80, 100), (60, 100)]
direction = (CELL_SIZE, 0)

What is the snake made of?

  • snake is a list of three (x, y) tuples. Each tuple is a body segment. The first tuple is the head.
  • direction is set to (CELL_SIZE, 0) which means "move 20 pixels to the right each frame."
  • Now find the single line screen.fill(BLACK) inside your game loop. Replace it with the following block that adds movement and drawing:
    # Move the snake by adding a new head and removing the tail
    new_head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
    snake.insert(0, new_head)
    snake.pop()

    # Clear the screen and draw each snake segment
    screen.fill(BLACK)
    for i, segment in enumerate(snake):
        color = DARK_GREEN if i == 0 else GREEN
        pygame.draw.rect(screen, color, (segment[0], segment[1], CELL_SIZE, CELL_SIZE))

How does the snake move?

Each frame, a new head is calculated by adding the direction to the current head position. Inserting the new head at index 0 grows the front. Calling snake.pop() removes the last tail segment, keeping the length constant.

The head draws in DARK_GREEN and the body in GREEN so you can tell them apart.

  • Save snake.py and run it again:
py snake.py

You should see a green rectangle moving steadily to the right across the black window. It will pass through the right edge and disappear.

Snake not moving?

  • Make sure the movement code (new_head, snake.insert, snake.pop) is inside the while running loop, indented with 4 spaces.
  • Confirm the movement lines come BEFORE screen.fill(BLACK). If they come after, the snake moves but you draw its old position.
  • Check that snake and direction are defined ABOVE running = True, not inside the loop.

Help me figure out why my snake is not moving on screen.

Add arrow key controls

Right now the snake only moves right. You need to listen for arrow key presses and change direction. You also need to prevent the snake from reversing directly into itself, because that would look like an instant self-collision.

  • In your event loop, find the if event.type == pygame.QUIT: block. Add the following elif block directly below the running = False line (at the same indentation level as if event.type == pygame.QUIT):
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP and direction != (0, CELL_SIZE):
                direction = (0, -CELL_SIZE)
            elif event.key == pygame.K_DOWN and direction != (0, -CELL_SIZE):
                direction = (0, CELL_SIZE)
            elif event.key == pygame.K_LEFT and direction != (CELL_SIZE, 0):
                direction = (-CELL_SIZE, 0)
            elif event.key == pygame.K_RIGHT and direction != (-CELL_SIZE, 0):
                direction = (CELL_SIZE, 0)

Why check the current direction?

Each and direction != check prevents a 180-degree reversal. If the snake is moving right ((CELL_SIZE, 0)), pressing Left would snap the head backwards into the body. The check blocks that input.

Notice that UP checks against (0, CELL_SIZE) which is DOWN, and vice versa. Each key is blocked only by its opposite.

  • Save snake.py.

Before you run this, what do you think will happen when you press the Up arrow key while the snake is moving right?

  • Run the game:
py snake.py

The snake starts moving right. Press the arrow keys to change its direction. The snake responds to Up, Down, Left, and Right. It cannot reverse direction (pressing Left while going Right does nothing).

Arrow keys not changing direction?

  • Make sure elif event.type == pygame.KEYDOWN is at the same indent level as if event.type == pygame.QUIT. Both should be inside the for event loop.
  • Check that you used elif (not if) so the key check only runs for events that are not QUIT.
  • If the snake moves but ignores all keys, make sure the KEYDOWN block is inside the for event in pygame.event.get(): loop, not outside it.

Help me debug why my arrow keys are not controlling the snake.

✔️ Awesome, I've got everything!

Great. Double check you have saved snake.py and that the snake moves and responds to arrow keys when you run it.

ⓧ I'd like to double check the full code

Here is the complete snake.py file at this point. Compare your code against it:

import pygame
import random

pygame.init()

SCREEN_WIDTH = 600
SCREEN_HEIGHT = 400
CELL_SIZE = 20

BLACK = (0, 0, 0)
GREEN = (0, 200, 0)
DARK_GREEN = (0, 150, 0)
RED = (200, 0, 0)
WHITE = (255, 255, 255)

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Snake Game")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 35)
snake = [(100, 100), (80, 100), (60, 100)]
direction = (CELL_SIZE, 0)

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP and direction != (0, CELL_SIZE):
                direction = (0, -CELL_SIZE)
            elif event.key == pygame.K_DOWN and direction != (0, -CELL_SIZE):
                direction = (0, CELL_SIZE)
            elif event.key == pygame.K_LEFT and direction != (CELL_SIZE, 0):
                direction = (-CELL_SIZE, 0)
            elif event.key == pygame.K_RIGHT and direction != (-CELL_SIZE, 0):
                direction = (CELL_SIZE, 0)
    new_head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
    snake.insert(0, new_head)
    snake.pop()

    screen.fill(BLACK)
    for i, segment in enumerate(snake):
        color = DARK_GREEN if i == 0 else GREEN
        pygame.draw.rect(screen, color, (segment[0], segment[1], CELL_SIZE, CELL_SIZE))

    pygame.display.flip()
    clock.tick(10)

pygame.quit()

You have a snake that moves and responds to your arrow keys. Next up, you will give it something to eat.

Add Food and Snake Growth

Your snake slithers across the screen, responding to your arrow keys. That's satisfying, but it's not a game yet.

A game needs a goal. Food gives the player something to chase, and growth creates tension as the snake fills up more of the screen. In this step, you'll spawn food at random positions and make the snake grow when it eats.

In this step, get ready to:
  • Create a food spawning function that places food at random grid-aligned positions.
  • Add eating logic so the snake grows when it reaches the food.
Create the food spawning and drawing functions

The food needs to appear at a position that lines up with the grid and never overlaps the snake body. You'll also extract the snake drawing code into its own function to keep the game loop clean.

  • In snake.py, find the line direction = (CELL_SIZE, 0). Copy the following code directly below it:
food = (300, 200)


def spawn_food():
    # Pick a random grid-aligned position that doesn't overlap the snake
    while True:
        x = random.randint(0, (SCREEN_WIDTH - CELL_SIZE) // CELL_SIZE) * CELL_SIZE
        y = random.randint(0, (SCREEN_HEIGHT - CELL_SIZE) // CELL_SIZE) * CELL_SIZE
        if (x, y) not in snake:
            return (x, y)


def draw_snake():
    # Draw each segment as a filled rectangle
    for i, segment in enumerate(snake):
        color = DARK_GREEN if i == 0 else GREEN
        pygame.draw.rect(screen, color, (segment[0], segment[1], CELL_SIZE, CELL_SIZE))


def draw_food():
    # Draw the food as a red square
    pygame.draw.rect(screen, RED, (food[0], food[1], CELL_SIZE, CELL_SIZE))

What does this code do?

  • food = (300, 200) places the first food square at a fixed position so you see it immediately when the game starts.
  • spawn_food() loops until it finds a random grid position that the snake isn't occupying. This prevents food from spawning inside the snake body.
  • draw_snake() and draw_food() extract the rendering logic into reusable functions you'll call every frame.

Now you need to update the game loop to actually call these new functions instead of the old inline drawing code.

  • In the game loop, find this drawing code:
    screen.fill(BLACK)
    for i, segment in enumerate(snake):
        color = DARK_GREEN if i == 0 else GREEN
        pygame.draw.rect(screen, color, (segment[0], segment[1], CELL_SIZE, CELL_SIZE))
  • Replace those four lines with the following:
    screen.fill(BLACK)
    draw_snake()
    draw_food()
  • Save snake.py and run the game by entering this command in your terminal:
py snake.py

You should see a red square sitting at the center-right of the screen alongside your moving green snake.

Don't see the red square?

  • Make sure draw_food() is called after screen.fill(BLACK). If you call it before the fill, the black background covers it.
  • Check that you removed the old inline for i, segment in enumerate(snake): loop from the game loop. Having both the old code and the new function calls can cause visual glitches.

Help me debug why my food isn't showing up on screen.

Add the eating and growth logic

Right now the food just sits there. You need the snake to detect when its head lands on the food, grow by one segment, and spawn a new food elsewhere.

  • In the game loop, find these two movement lines:
    new_head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
    snake.insert(0, new_head)
    snake.pop()
  • Replace those three lines with the following code that checks for food before deciding whether to remove the tail:
    new_head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
    snake.insert(0, new_head)

    # If the head lands on food, grow (skip pop) and spawn new food
    if new_head == food:
        food = spawn_food()
    else:
        snake.pop()

What does this code do?

Every frame, the snake gets a new head inserted at the front. The question is whether to remove the tail.

  • If the new head matches the food position, the snake "eats" it. By skipping snake.pop(), the tail stays, making the snake one segment longer.
  • If the head doesn't match food, snake.pop() removes the last segment so the snake stays the same length and appears to slide forward.

Before you run this, what do you think will happen when you steer the snake into the red square?

  • Save snake.py and run the game:
py snake.py

Steer the snake into the red square. The snake grows longer and a new food square spawns at a random position. Each time you eat, the snake gains one segment.

Now try steering the snake off the edge of the window, or looping it back into its own body. Nothing stops you. The snake phases right through walls and itself.

Snake not growing when it hits the food?

  • Make sure the if new_head == food: check comes after snake.insert(0, new_head). The head must already be in the list for the skip-pop trick to work.
  • Check that you deleted the old standalone snake.pop() line. If it's still there alongside the new else: snake.pop(), the tail always gets removed and the snake never grows.

Help me debug why my snake isn't growing when it eats food.

✔️ Awesome, I've got everything!

Great. Double check you've saved snake.py before moving on.

ⓧ I'd like to double check the full code

Here's the complete snake.py at this point. Scroll through and confirm each section is present in order:

  • Imports, PyGame init, constants, and color definitions at the top.
  • Screen, clock, font setup, then snake and direction variables.
  • The food variable followed by spawn_food(), draw_snake(), and draw_food() function definitions.
  • The game loop with event handling, movement with food check, and rendering using draw_snake() and draw_food().

Your snake can eat and grow, but there's no way to lose. It passes through walls and its own body like a ghost. Next up, you'll add collision detection to give the game real stakes.

Add Collision Detection

You have a snake that eats food and grows longer. But right now, you can drive the snake straight through walls or back into its own body with no consequence.

That means there is no way to lose. Collision detection is what turns your moving-snake toy into an actual game with stakes. In this step, you will add boundary checks and self-intersection checks so the game freezes the moment the snake hits something it should not.

In this step, get ready to:
  • Add wall collision detection that stops the game when the snake leaves the screen.
  • Add self-collision detection that stops the game when the snake runs into its own body.
Add wall collision detection

The snake moves on a grid bounded by the game window. If the head moves past any edge, the game should immediately stop.

  • In snake.py, find the line that says food = (300, 200). Add this new variable directly below it:
game_over = False

What does this variable do?

The game_over flag tracks whether the player has lost. When it flips to True, the game loop still runs (so the window stays open) but all movement logic is skipped.

Now you need to replace the existing movement logic with a version that checks for wall collisions before moving the snake.

  • Inside the while running: loop, find this block of code that handles movement:
    new_head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
    snake.insert(0, new_head)

    if new_head == food:
        food = spawn_food()
    else:
        snake.pop()
  • Replace that entire block with the following collision-aware movement logic:
    if not game_over:
        head_x = snake[0][0] + direction[0]
        head_y = snake[0][1] + direction[1]
        new_head = (head_x, head_y)

        if (head_x < 0 or head_x >= SCREEN_WIDTH or
                head_y < 0 or head_y >= SCREEN_HEIGHT):
            game_over = True

        if not game_over:
            snake.insert(0, new_head)

            if new_head == food:
                food = spawn_food()
            else:
                snake.pop()

What does this code do?

The outer if not game_over: guard prevents any movement after the player dies. The window stays open, but the snake stops moving.

The boundary check compares the new head position against all four edges. If head_x is less than 0 (left wall) or greater than or equal to SCREEN_WIDTH (right wall), the game ends. Same for head_y against the top and bottom.

The inner if not game_over: prevents the snake from inserting into an illegal position after a collision is detected.

  • Save snake.py.
  • Run your game to test wall collision:
py snake.py

Steer the snake towards any edge of the window. When it hits the wall, the snake should freeze in place and stop responding to arrow keys.

Snake still passes through walls?

  • Make sure you deleted the old movement code completely. If both the old and new blocks exist, the old code runs after the new code and overrides it.
  • Check indentation. The if not game_over: line should be indented one level inside the while running: loop, at the same level as the for event loop above it.
  • Confirm game_over = False is defined outside the loop, near the top of the file.

Help me debug my wall collision

Add self-collision detection

Wall detection handles the edges, but the snake can still drive straight through its own body. A real Snake game ends when the head overlaps any existing body segment.

  • In snake.py, find the wall collision check you just added. Locate the line game_over = True inside the boundary check. Add this self-collision check directly below that if block, at the same indentation level:
        if new_head in snake:
            game_over = True

What does this check do?

The snake list contains every segment position. If the new head position already exists somewhere in that list, the snake is about to overlap itself. Setting game_over = True stops the game immediately before the overlap happens.

  • Save snake.py.

Before you run the game, think about this: what do you need to do to actually trigger self-collision? How long does your snake need to be?

  • Run your game to test both collision types:
py snake.py

First, steer into a wall to confirm that still works. Then close the window, run the game again, eat several food squares to grow long, and steer the snake back into its own body. Both collisions should freeze the game.

Wall collision works but self-collision does not?

  • Make sure the if new_head in snake: line is inside the outer if not game_over: block, indented with two levels (8 spaces).
  • Your snake needs to be at least 4 segments long to physically collide with itself. Eat a few food squares first before attempting the test.
  • Verify the self-collision check comes AFTER new_head is calculated but BEFORE snake.insert(0, new_head) adds it to the list.

Help me debug my self-collision

✔️ Awesome, I've got everything!

Great. Double check you have saved snake.py and both collision types are working.

ⓧ I'd like to double check the full code

Scroll up and confirm your snake.py contains each of these pieces in order:

  • The game_over = False variable, defined right below food = (300, 200).
  • The if not game_over: guard wrapping all movement logic inside the game loop.
  • The wall boundary check comparing head_x and head_y against screen edges.
  • The self-collision check if new_head in snake: placed after the wall check.
  • The inner if not game_over: guard around snake.insert() and the food logic.

Your snake game now has real stakes. One wrong turn and the game is over. Next up, you will add a score counter and a game over screen so the player gets feedback and can play again without restarting the script.

Add Score Display and Game Over Screen

Your snake game has a real challenge now. Hitting a wall or your own body ends the game. But the player has no idea how well they did, and the only way to play again is to close the window and rerun the script.

In this step, you will add a live score that counts every piece of food eaten, a game over screen that appears on death, and restart functionality so the player can jump back in instantly.

In this step, get ready to:
  • Display a live score counter that increments when the snake eats food.
  • Show a game over screen with restart instructions when the snake dies.
  • Handle keyboard input to restart the game or quit from the game over screen.
Add score tracking and display

The player needs visible feedback every time they eat food. You will add a score variable and a function that renders it to the screen every frame.

  • In snake.py, find this line near the top of your file:
food = (300, 200)
game_over = False
  • Add a score variable between those two lines, so the section now looks like this:
food = (300, 200)
score = 0
game_over = False
  • Now add the draw_score() function below the existing draw_food() function by pasting this code:
def draw_score():
    # Render the current score as white text
    score_text = font.render(f"Score: {score}", True, WHITE)
    screen.blit(score_text, (10, 10))

What does this code do?

font.render() turns a string into an image (a Surface) that PyGame can draw. The True argument enables anti-aliasing for smoother text edges.

screen.blit() pastes that text image onto the game window at position (10, 10), which is the top-left corner.

  • In the render section at the bottom of the loop, find these lines:
    screen.fill(BLACK)
    draw_snake()
    draw_food()

    pygame.display.flip()
  • Add draw_score() after draw_food(), so the section now looks like this:
    screen.fill(BLACK)
    draw_snake()
    draw_food()
    draw_score()

    pygame.display.flip()
  • Save snake.py and run the game by entering this command in your terminal:
py snake.py

You should see "Score: 0" in white text at the top-left corner of the window. The score stays at 0 because you have not wired up the increment yet.

  • Close the game window.
  • In the game loop, find this existing code inside the food-eating check:
            if new_head == food:
                food = spawn_food()
            else:
                snake.pop()
  • Add score += 1 above food = spawn_food(), so the block now looks like this:
            if new_head == food:
                score += 1
                food = spawn_food()
            else:
                snake.pop()
  • Save snake.py and run the game again:
py snake.py

Steer the snake into the red food square. The score increments to 1, then 2, then 3 as you keep eating.

Score not showing up?

  • Make sure draw_score() is called after screen.fill(BLACK). If it is called before the fill, the black background paints over the text.
  • Check that score += 1 is inside the if new_head == food: block, not outside it. If it is at the wrong indentation level, the score will increment every frame.

Help me debug my score display

Add the game over screen and restart

Right now when the snake dies, the game simply freezes. The player has no feedback about what happened and no way to play again without restarting the script.

  • Close the game window if it is still running.
  • Below the draw_score() function, add the show_game_over() function by pasting this code:
def show_game_over():
    # Display game over text in red, centered on screen
    game_over_text = font.render("GAME OVER", True, RED)
    restart_text = font.render("Press R to Restart or Q to Quit", True, WHITE)
    screen.blit(game_over_text, (SCREEN_WIDTH // 2 - 80, SCREEN_HEIGHT // 2 - 30))
    screen.blit(restart_text, (SCREEN_WIDTH // 2 - 180, SCREEN_HEIGHT // 2 + 10))

What does this code do?

This renders two lines of text to the center of the screen. The x-offset values (-80 and -180) shift the text left from the center point so it appears roughly centered horizontally.

  • In the render section, add the show_game_over() call so the full render block looks like this:
    screen.fill(BLACK)
    draw_snake()
    draw_food()
    draw_score()

    if game_over:
        show_game_over()

    pygame.display.flip()
    clock.tick(10)
  • Save snake.py and run the game:
py snake.py

Steer the snake into a wall. You should see "GAME OVER" in red and "Press R to Restart or Q to Quit" in white, centered on screen. The keys do not work yet because you have not added the restart logic.

  • Close the game window.
  • Below show_game_over(), add the reset_game() function by pasting this code:
def reset_game():
    # Reset all game state to starting values
    global snake, direction, food, score, game_over
    snake = [(100, 100), (80, 100), (60, 100)]
    direction = (CELL_SIZE, 0)
    food = spawn_food()
    score = 0
    game_over = False

Why use the global keyword?

Python functions can read global variables freely, but cannot reassign them without the global keyword. Since reset_game() needs to overwrite snake, direction, food, score, and game_over with fresh values, it declares them as global first.

Now you need to restructure the event loop so it handles R and Q keys when the game is over, and only handles arrow keys when the game is running.

  • In the game loop, find the existing elif event.type == pygame.KEYDOWN: block. Replace the entire KEYDOWN section with this updated version:
        elif event.type == pygame.KEYDOWN:
            if game_over:
                if event.key == pygame.K_r:
                    reset_game()
                elif event.key == pygame.K_q:
                    running = False
            else:
                if event.key == pygame.K_UP and direction != (0, CELL_SIZE):
                    direction = (0, -CELL_SIZE)
                elif event.key == pygame.K_DOWN and direction != (0, -CELL_SIZE):
                    direction = (0, CELL_SIZE)
                elif event.key == pygame.K_LEFT and direction != (CELL_SIZE, 0):
                    direction = (-CELL_SIZE, 0)
                elif event.key == pygame.K_RIGHT and direction != (-CELL_SIZE, 0):
                    direction = (CELL_SIZE, 0)

What changed here?

The arrow key handling you already had is now inside an else: block. This means arrow keys only work while the game is running.

The new if game_over: block at the top intercepts key presses when the player has died. R calls reset_game() to start fresh. Q sets running = False to exit the game loop cleanly.

Before you run this, what do you think will happen when you press R after dying? Will the score reset to 0?

  • Save snake.py and run the game:
py snake.py

The score appears in the top-left corner and increments each time the snake eats food. Steer the snake into a wall or its own body. The "GAME OVER" message appears with restart instructions. Press R and the game resets completely. Press Q to quit.

R key not restarting the game?

  • Make sure the if game_over: check is inside the elif event.type == pygame.KEYDOWN: block, not outside it.
  • Check that the arrow key handling is inside the else: branch. If it is at the same indentation as if game_over:, both blocks will execute and the arrow keys will override the restart logic.
  • Verify reset_game() uses global for all five variables. Missing one means that variable will not reset.

Help me debug my restart logic

✔️ Awesome, I've got everything!

Great. Make sure you have saved snake.py and that the score increments, game over screen appears, and restart works.

ⓧ I'd like to double check the full code

Your snake.py should contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports (pygame and random).
  • pygame.init() and constants (SCREEN_WIDTH, SCREEN_HEIGHT, CELL_SIZE, colors).
  • Screen, clock, and font setup.
  • Game state variables: snake, direction, food, score = 0, game_over = False.
  • spawn_food() function.
  • draw_snake() function.
  • draw_food() function.
  • draw_score() function (renders f"Score: {score}" to (10, 10)).
  • show_game_over() function (renders two centered text lines).
  • reset_game() function (uses global and resets all five state variables).
  • Game loop with event handling: QUIT, KEYDOWN with if game_over: (K_r, K_q) and else: (arrow keys).
  • Movement, collision, and growth logic inside if not game_over: with score += 1 when food is eaten.
  • Render section: screen.fill(), draw_snake(), draw_food(), draw_score(), conditional show_game_over(), pygame.display.flip(), clock.tick(10).
  • pygame.quit() at the very end.

Your Snake game is fully playable. You have a score, a game over screen, and the ability to restart. Next up, tackle the Secret Mission to make the game speed up as your score climbs.

Secret mission

Add Increasing Difficulty

Your Snake game plays at a constant speed no matter how long your snake gets. Real Snake games ramp up the pressure as you grow. Can you make the game speed up every few points?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • snake.py file on your Desktop.
  • pygame-ce Python package installed via pip.

Keep everything running

No action needed. Choose this if you want to keep playing your Snake game or continue tweaking the code.

  • Your snake.py file stays on your Desktop, ready to run anytime with py snake.py.
  • The pygame-ce package remains installed and available for future Python game projects.

Pause - I'll come back to this later

Since this is a local project with no running processes or servers, there is nothing to pause. Your file and package sit idle until you use them again.

  • Your snake.py file uses no resources when it is not running.
  • The pygame-ce package takes up a small amount of disk space but does not run in the background.

Delete - I don't want to use this again

Remove all project files and the pygame-ce package to start fresh.

  • Delete the snake.py file by running this command in your terminal:

macOS

rm ~/Desktop/snake.py

Windows

del $HOME\Desktop\snake.py
  • Uninstall the pygame-ce package by running:
py -m pip uninstall pygame-ce
  • When prompted to confirm, type y and press Enter.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully playable Snake game from scratch using PyGame.

You've learned how to:

  • Built a complete game loop using the input-update-render pattern that powers every 2D game, with grid-based movement and arrow key controls.
  • Implemented collision detection for walls and self-intersection that gives the game real stakes and a failure state.
  • Created a full game experience with food spawning, snake growth, a live score counter, a game over screen, and restart functionality.
  • Secret Mission: Made the game progressively harder by increasing the snake's speed based on your score.

Ready to quiz yourself?