In this tutorial, we are creating a classic Nokia snake game in Python. It has features such as screen wrapping, progressive difficulty (speeds up as you score) & with clean, modular code structure. If you are interested in learning more about game development, you may find my Pong game tutorial, Side-scrolling shooter & racing game tutorial helpful (but they are for Godot engine and not for PyGame). Now lets begin with this tutorial!
Prerequisites: For this tutorial, you need to have basic Python knowledge. You will have to install PyGame. For this, type following in your terminal (or Command Prompt):
pip install pygame
Breakdown of Game Structure
Let’s break down our game into smaller pieces:
- Game window setup
- Snake movement
- Food generation
- Collision detection
- Scoring system
Now let’s start with the code:
import pygame
import random
from enum import Enum
from typing import List, Tuple
# Initialize Pygame
pygame.init()
# Constants
WINDOW_SIZE = 600 # Total window size in pixels
GRID_SIZE = 20 # Size of each grid square
GRID_COUNT = WINDOW_SIZE // GRID_SIZE # Number of grid squares in each row/column
# Colors (RGB format)
BLACK = (10, 10, 10) # Almost black background
RED = (255, 0, 0) # Snake color
DARK_RED = (200, 0, 0) # Snake body color
FOOD_COLOR = (255, 50, 50) # Food color
In above code, WINDOW_SIZE
determines how big our game window will be. GRID_SIZE
defines how big each “cell” in our game is. GRID_COUNT
calculates how many cells fit in our window. Colors are defined in RGB format (Red, Green, Blue) from 0-255.
Creating the Direction System
class Direction(Enum):
"""Enum for storing directional constants"""
UP = 0
RIGHT = 1
DOWN = 2
LEFT = 3
We use an Enum
to create a set of named constants. This makes our code more readable and prevents errors. Instead of using numbers, we can use meaningful names like Direction.UP
Creating the Snake Segment Class
class SnakeSegment:
"""Class representing a segment of the snake"""
def __init__(self, position: Tuple[int, int]):
self.position = position
def draw(self, surface: pygame.Surface):
"""Draw the snake segment"""
rect = pygame.Rect(
self.position[0] * GRID_SIZE, # X position
self.position[1] * GRID_SIZE, # Y position
GRID_SIZE - 1, # Width
GRID_SIZE - 1 # Height
)
pygame.draw.rect(surface, RED, rect)
Each segment of our snake is an object of this class. position
is a tuple of (x, y) coordinates on our grid. draw
method creates a rectangle for each segment. We subtract 1 from GRID_SIZE to create small gaps between segments.
Creating the Food Class
class Food:
"""Class representing the food"""
def __init__(self):
self.position = self.generate_position()
def generate_position(self) -> Tuple[int, int]:
"""Generate a random position for the food"""
return (
random.randint(0, GRID_COUNT - 1),
random.randint(0, GRID_COUNT - 1)
)
def draw(self, surface: pygame.Surface):
"""Draw the food"""
rect = pygame.Rect(
self.position[0] * GRID_SIZE,
self.position[1] * GRID_SIZE,
GRID_SIZE - 1,
GRID_SIZE - 1
)
pygame.draw.rect(surface, FOOD_COLOR, rect)
Food appears at random positions on the grid. generate_position
creates random coordinates within our game bounds. Food is drawn similarly to snake segments but with a different color.
Creating the Snake Class
class Snake:
"""Class representing the snake"""
def __init__(self):
self.reset()
def reset(self):
"""Reset the snake to initial state"""
self.direction = Direction.RIGHT
self.segments = [
SnakeSegment((GRID_COUNT // 2, GRID_COUNT // 2))
]
self.growing = False
The snake starts in the middle of the screen. Initially moves right. Contains a list of segments (starting with one). growing
flag determines if the snake should grow after eating.
Now let’s add movement and control methods:
def update(self) -> bool:
"""Update snake position and return True if game should continue"""
head = self.segments[0].position
# Calculate new head position
new_head = list(head)
if self.direction == Direction.UP:
new_head[1] -= 1
elif self.direction == Direction.DOWN:
new_head[1] += 1
elif self.direction == Direction.LEFT:
new_head[0] -= 1
elif self.direction == Direction.RIGHT:
new_head[0] += 1
# Wrap around screen
new_head[0] = new_head[0] % GRID_COUNT
new_head[1] = new_head[1] % GRID_COUNT
# Check self-collision
if tuple(new_head) in [segment.position for segment in self.segments]:
return False
# Add new head
self.segments.insert(0, SnakeSegment(tuple(new_head)))
# Remove tail if not growing
if not self.growing:
self.segments.pop()
else:
self.growing = False
return True
update
moves the snake by adding a new head and removing the tail. Screen wrapping is handled by using modulo (%
). Returns False
if snake collides with itself.
Creating the Main Game Class
class Game:
"""Main game class"""
def __init__(self):
self.screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE))
pygame.display.set_caption('Nokia Snake Game')
self.clock = pygame.time.Clock()
self.snake = Snake()
self.food = Food()
self.score = 0
self.game_speed = 10
Creates the game window. Initializes snake and food. Sets up game speed and scoring.
Add the game loop methods:
def handle_input(self):
"""Handle keyboard input"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
self.snake.change_direction(Direction.UP)
elif event.key == pygame.K_DOWN:
self.snake.change_direction(Direction.DOWN)
elif event.key == pygame.K_LEFT:
self.snake.change_direction(Direction.LEFT)
elif event.key == pygame.K_RIGHT:
self.snake.change_direction(Direction.RIGHT)
return True
def run(self):
"""Main game loop"""
running = True
while running:
running = self.handle_input()
self.update()
self.draw()
self.clock.tick(self.game_speed)
handle_input
processes keyboard events. run
is the main game loop that keeps everything running. clock.tick
controls game speed.
Running the Game
Finally, add this at the bottom of your file:
if __name__ == "__main__":
game = Game()
game.run()
pygame.quit()
Expanding the Game
For your own exercise, try adding following features on your own.
- High score system
- Different types of food
- Sound effects
- Menu system
Full Source Code
import pygame
import random
from enum import Enum
from typing import List, Tuple
# Initialize Pygame
pygame.init()
# Constants
WINDOW_SIZE = 600
GRID_SIZE = 20
GRID_COUNT = WINDOW_SIZE // GRID_SIZE
# Colors
BLACK = (10, 10, 10) # Very dark grey, almost black like Nokia screen
RED = (255, 0, 0)
DARK_RED = (200, 0, 0) # For snake body
FOOD_COLOR = (255, 50, 50)
class Direction(Enum):
"""Enum for storing directional constants"""
UP = 0
RIGHT = 1
DOWN = 2
LEFT = 3
class SnakeSegment:
"""Class representing a segment of the snake"""
def __init__(self, position: Tuple[int, int]):
self.position = position
def draw(self, surface: pygame.Surface):
"""Draw the snake segment"""
rect = pygame.Rect(
self.position[0] * GRID_SIZE,
self.position[1] * GRID_SIZE,
GRID_SIZE - 1,
GRID_SIZE - 1
)
pygame.draw.rect(surface, RED, rect)
class Food:
"""Class representing the food"""
def __init__(self):
self.position = self.generate_position()
def generate_position(self) -> Tuple[int, int]:
"""Generate a random position for the food"""
return (
random.randint(0, GRID_COUNT - 1),
random.randint(0, GRID_COUNT - 1)
)
def draw(self, surface: pygame.Surface):
"""Draw the food"""
rect = pygame.Rect(
self.position[0] * GRID_SIZE,
self.position[1] * GRID_SIZE,
GRID_SIZE - 1,
GRID_SIZE - 1
)
pygame.draw.rect(surface, FOOD_COLOR, rect)
class Snake:
"""Class representing the snake"""
def __init__(self):
self.reset()
def reset(self):
"""Reset the snake to initial state"""
self.direction = Direction.RIGHT
self.segments = [
SnakeSegment((GRID_COUNT // 2, GRID_COUNT // 2))
]
self.growing = False
def update(self) -> bool:
"""Update snake position and return True if game should continue"""
# Get the current head position
head = self.segments[0].position
# Calculate new head position based on direction
new_head = list(head)
if self.direction == Direction.UP:
new_head[1] -= 1
elif self.direction == Direction.DOWN:
new_head[1] += 1
elif self.direction == Direction.LEFT:
new_head[0] -= 1
elif self.direction == Direction.RIGHT:
new_head[0] += 1
# Wrap around screen
new_head[0] = new_head[0] % GRID_COUNT
new_head[1] = new_head[1] % GRID_COUNT
# Check for collision with self
if tuple(new_head) in [segment.position for segment in self.segments]:
return False
# Add new head
self.segments.insert(0, SnakeSegment(tuple(new_head)))
# Remove tail if not growing
if not self.growing:
self.segments.pop()
else:
self.growing = False
return True
def draw(self, surface: pygame.Surface):
"""Draw the snake"""
for segment in self.segments:
segment.draw(surface)
def grow(self):
"""Mark the snake to grow on next update"""
self.growing = True
def change_direction(self, new_direction: Direction):
"""Change the snake's direction if valid"""
opposite_directions = {
Direction.UP: Direction.DOWN,
Direction.DOWN: Direction.UP,
Direction.LEFT: Direction.RIGHT,
Direction.RIGHT: Direction.LEFT
}
if new_direction != opposite_directions.get(self.direction):
self.direction = new_direction
class Game:
"""Main game class"""
def __init__(self):
self.screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE))
pygame.display.set_caption('Nokia Snake Game')
self.clock = pygame.time.Clock()
self.snake = Snake()
self.food = Food()
self.score = 0
self.game_speed = 10
def handle_input(self):
"""Handle keyboard input"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
self.snake.change_direction(Direction.UP)
elif event.key == pygame.K_DOWN:
self.snake.change_direction(Direction.DOWN)
elif event.key == pygame.K_LEFT:
self.snake.change_direction(Direction.LEFT)
elif event.key == pygame.K_RIGHT:
self.snake.change_direction(Direction.RIGHT)
return True
def update(self):
"""Update game state"""
if not self.snake.update():
self.reset_game()
return
# Check for food collision
if self.snake.segments[0].position == self.food.position:
self.snake.grow()
self.food.position = self.food.generate_position()
self.score += 1
# Increase game speed every 5 points
if self.score % 5 == 0:
self.game_speed += 1
def draw(self):
"""Draw the game state"""
self.screen.fill(BLACK)
self.snake.draw(self.screen)
self.food.draw(self.screen)
pygame.display.flip()
def reset_game(self):
"""Reset the game state"""
self.snake.reset()
self.food = Food()
self.score = 0
self.game_speed = 10
def run(self):
"""Main game loop"""
running = True
while running:
running = self.handle_input()
self.update()
self.draw()
self.clock.tick(self.game_speed)
if __name__ == "__main__":
game = Game()
game.run()
pygame.quit()
Thank you for reading <3