Introduction
Are you interested in testing out your python knowledge with Object-Oriented Programming with a twist of game development? If yes, then you are in the right place. In this tutorial, we are going to create a classic snake game using Object-Oriented Programming in Python. We will also be using the turtle module to implement the GUI of the game.
Firstly, let's create the required files for the game. We will create a total of 5 files, 4 python files containing the logic of the game and 1 text file to implement the high-score functionality.
main.py: Contains the main logic of the game.
snake.py: Contains the logic to implement the snake.
food.py: Contains the logic to generate food at random locations on the screen.
scoreboard.py: To keep track of the current score.
high_score.txt: To keep track of the high score.
Now that we have created the required files, let's start writing the code.
Setting up the Screen
The first step is to set up the screen for the game. In the main.py file, we will import the necessary classes and modules, including Screen, Snake, Food, Scoreboard, and time.
from turtle import Screen
from snake import Snake
from food import Food
from scoreboard import Scoreboard
import time
# Set up the screen
screen = Screen()
screen.setup(width=600, height=600)
screen.bgcolor("black")
screen.title("Snake Game")
Here, we create an object of the Screen class available in the turtle module. We can access the Screen class methods and attributes using the dot notation. The screen should be 600 x 600, but you can tweak the dimensions according to your needs. I've also imported the time module to make the gaming experience smooth otherwise, our snake moves so fast that it's not easy to track.
Your screen will look like this after this:
Creating the Snake
The next step is to create the Snake class in the snake.py file. Here's the code for the Snake class:
from turtle import Turtle
class Snake:
def __init__(self):
self.segments = []
self.create_snake()
def create_snake(self):
starting_positions = [(0, 0), (-20, 0), (-40, 0)]
for position in starting_positions:
self.add_segment(position)
def add_segment(self, position):
new_segment = Turtle("square")
new_segment.color("white")
new_segment.penup()
new_segment.goto(position)
self.segments.append(new_segment)
def extend(self):
self.add_segment(self.segments[-1].position())
def move(self):
for seg_num in range(len(self.segments) - 1, 0, -1):
new_x = self.segments[seg_num - 1].xcor()
new_y = self.segments[seg_num - 1].ycor()
self.segments[seg_num].goto(new_x, new_y)
self.segments[0].forward(20)
Here, we create the Snake class with the __init__
method to initialize the segments
list and create the snake. We use the create_snake
method to generate the starting positions of the snake. The add_segment
method is used to add a new segment to the snake when it eats food. The extend
method is called when the snake eats food to add a new segment to the end of the snake. The move
method moves the snake forward by changing the position of each segment.
Creating the Food
The Food class will be responsible for generating a piece of food at a random location on the screen. Here's the code for the Food class:
from turtle import Turtle
import random
class Food(Turtle):
def __init__(self):
super().__init__()
self.shape("circle")
self.color("red")
self.penup()
self.shapesize(stretch_wid=0.5, stretch_len=0.5)
self.speed("fastest")
self.refresh()
def refresh(self):
x = random.randint(-280, 280)
y = random.randint(-280, 280)
self.goto(x, y)
The Food
class inherits from the Turtle
class, which allows us to use all the methods and attributes of the Turtle
class. In the constructor method __init__()
, we first call the constructor of the parent class using super().__init__()
. We then set the shape and color of the food using the shape()
and color()
methods. We use penup()
to not leave a trace while moving the food. We also stretch the food using shapesize()
method. Finally, we call the refresh()
method to place the food at a random location on the screen.
The refresh()
method generates random x and y coordinates using the random.randint()
function and moves the food to that location using the goto()
method.
Next, we need to detect when the snake eats the food, which we will implement in the Snake class.
Detecting Snake-Food Collision
In the snake
class, we need to check if the head of the snake collides with the food. If it does, we need to update the score, increase the size of the snake, and generate a new piece of food.
Here's the updated Snake
class code:
pythonCopy codefrom turtle import Turtle
STARTING_POSITIONS = [(0, 0), (-20, 0), (-40, 0)]
MOVE_DISTANCE = 20
class Snake:
def __init__(self):
self.segments = []
self.create_snake()
self.head = self.segments[0]
def create_snake(self):
for position in STARTING_POSITIONS:
new_segment = Turtle("square")
new_segment.color("white")
new_segment.penup()
new_segment.goto(position)
self.segments.append(new_segment)
def move(self):
for seg_num in range(len(self.segments) - 1, 0, -1):
new_x = self.segments[seg_num - 1].xcor()
new_y = self.segments[seg_num - 1].ycor()
self.segments[seg_num].goto(new_x, new_y)
self.head.forward(MOVE_DISTANCE)
def up(self):
if self.head.heading() != 270:
self.head.setheading(90)
def down(self):
if self.head.heading() != 90:
self.head.setheading(270)
def left(self):
if self.head.heading() != 0:
self.head.setheading(180)
def right(self):
if self.head.heading() != 180:
self.head.setheading(0)
def extend(self):
new_segment = Turtle("square")
new_segment.color("white")
new_segment.penup()
self.segments.append(new_segment)
def reset(self):
for seg in self.segments:
seg.goto(1000, 1000)
self.segments.clear()
self.create_snake()
self.head = self.segments[0]
We added a new method called eat_food
in the Snake class to detect when the snake eats the food. Here's the updated code:
pythonCopy codeclass Snake:
def __init__(self):
self.segments = [Segment((0, 0)), Segment((-20, 0)), Segment((-40, 0))]
self.head = self.segments[0]
self.color = "white"
def move(self):
for seg_num in range(len(self.segments) - 1, 0, -1):
new_x = self.segments[seg_num - 1].xcor()
new_y = self.segments[seg_num - 1].ycor()
self.segments[seg_num].goto(new_x, new_y)
self.head.fd(20)
def turn_up(self):
if self.head.heading() != DOWN:
self.head.setheading(UP)
def turn_down(self):
if self.head.heading() != UP:
self.head.setheading(DOWN)
def turn_left(self):
if self.head.heading() != RIGHT:
self.head.setheading(LEFT)
def turn_right(self):
if self.head.heading() != LEFT:
self.head.setheading(RIGHT)
def add_segment(self, position):
new_segment = Segment(position)
new_segment.color(self.color)
self.segments.append(new_segment)
def extend(self):
self.add_segment(self.segments[-1].position())
def eat_food(self, food):
if self.head.distance(food) < 15:
self.extend()
return True
else:
return False
We passed the Food
object as a parameter to the eat_food
method and checked if the distance between the head of the snake and the food is less than 15 (the size of the food). If it is, then we call the extend
method to add a new segment to the snake's body and return True
. Otherwise, we return False
.
Now, let's update the main.py
file to make the snake eat the food when it collides with it. We will also generate new food at a random location when the snake eats the food. Here's the updated code:
pythonCopy codeimport time
from turtle import Screen
from snake import Snake
from food import Food
from scoreboard import Scoreboard
screen = Screen()
screen.setup(width=600, height=600)
screen.bgcolor("black")
screen.title("Snake Game")
screen.tracer(0)
snake = Snake()
food = Food()
scoreboard = Scoreboard()
screen.listen()
screen.onkey(snake.turn_up, "Up")
screen.onkey(snake.turn_down, "Down")
screen.onkey(snake.turn_left, "Left")
screen.onkey(snake.turn_right, "Right")
game_is_on = True
while game_is_on:
screen.update()
time.sleep(0.1)
snake.move()
# Detect collision with food
if snake.eat_food(food):
food.refresh()
scoreboard.increase_score()
# Detect collision with wall
if snake.head.xcor() > 280 or snake.head.xcor() < -280 or snake.head.ycor() > 280 or snake.head.ycor() < -280:
game_is_on = False
scoreboard.game_over()
# Detect collision with tail
for segment in snake.segments[1:]:
if snake.head.distance(segment) < 10:
game_is_on = False
scoreboard.game_over()
screen.exitonclick()
We added a condition to check if the snake eats the food, and if it does, we refresh the food's position using the refresh
method and increase the score by 1 using the scoreboard's increase_score method. We also increase the snake's length using the snake's extend method.
# in Snake class
def extend(self):
"""Add a new segment to the snake"""
self.add_segment(self.segments[-1].position())
# in Food class
def eat_food(self):
"""Refresh the food and increase score if the snake eats it"""
if self.distance(self.snake.head()) < 15:
self.refresh()
self.scoreboard.increase_score()
self.snake.extend()
Now that we have the basic game logic implemented, it's time to add some more features to make the game more interesting.
Detecting Collisions with the Wall
Currently, our snake can move off the screen and disappear. We want to detect when the snake hits the wall and end the game. We can do this by checking if the snake's head position is outside the screen's boundaries.
# in Snake class
def check_wall_collision(self):
"""Detect collision with the wall"""
x = self.head().xcor()
y = self.head().ycor()
if abs(x) > 280 or abs(y) > 280:
return True
return False
We added a new method called check_wall_collision
that checks if the snake's head is outside the screen's boundaries. We return True if it is and False otherwise.
Next, we need to check for wall collisions in the main game loop in the main.py
file.
while game_is_on:
screen.update()
snake.move()
# Detect collision with food
if snake.head().distance(food) < 15:
food.eat_food()
# Detect collision with wall
if snake.check_wall_collision():
scoreboard.game_over()
game_is_on = False
time.sleep(0.1)
We added a new if statement to check for wall collisions. If the snake hits the wall, we end the game by calling the scoreboard's game_over method.
Detecting Collisions with the Snake's Own Tail
Another thing we need to check for is if the snake collides with its tail. We can do this by checking if the snake's head position is the same as any of its segments' positions.
# in Snake class
def check_tail_collision(self):
"""Detect collision with the snake's tail"""
for segment in self.segments[1:]:
if self.head().distance(segment) < 10:
return True
return False
We added a new method called check_tail_collision
that checks if the snake's head collides with any of its segments. We return True if it does and False otherwise.
Next, we need to check for tail collisions in the main game loop in the main.py
file.
while game_is_on:
screen.update()
snake.move()
# Detect collision with food
if snake.head().distance(food) < 15:
food.eat_food()
# Detect collision with wall
if snake.check_wall_collision():
scoreboard.game_over()
game_is_on = False
# Detect collision with tail
if snake.check_tail_collision():
scoreboard.game_over()
game_is_on = False
time.sleep(0.1)
We added a new if statement to check for tail collisions. If the snake's head collides with any of its segments, we end the game by calling the scoreboard's game_over method.
Keeping Track of Current Score & High Score
Finally, we want to keep track of the player's high score. We can do this by storing the high score in a file called high_score.txt
. If the current score is higher than the high score, we update the file with the new high score.
Here's the code for the Scoreboard class:
from turtle import Turtle
ALIGNMENT = "center"
FONT = ("Courier", 24, "normal")
class Scoreboard(Turtle):
def __init__(self):
super().__init__()
self.score = 0
with open("high_score.txt") as file:
self.high_score = int(file.read())
self.color("white")
self.penup()
self.goto(0, 260)
self.hideturtle()
self.update_scoreboard()
def update_scoreboard(self):
self.write(f"Score: {self.score} High Score: {self.high_score}", align=ALIGNMENT, font=FONT)
def reset(self):
if self.score > self.high_score:
self.high_score = self.score
with open("high_score.txt", mode="w") as file:
file.write(f"{self.high_score}")
self.score = 0
self.update_scoreboard()
def increase_score(self):
self.score += 1
self.clear()
self.update_scoreboard()
In the __init__
method, we set the initial score to 0 and read the high score from the high_score.txt
file. We then position the scoreboard at the top center of the screen and call update_scoreboard
to display the score and high score on the screen.
The update_scoreboard
method writes the current score and high score to the screen.
The reset
method is called when the game is over, and it resets the score to 0. If the current score is higher than the high score, we update the high score in the high_score.txt
file and display the new high score on the screen.
The increase_score
method is called when the snake eats a piece of food, and it increases the score by 1. We then clear the screen and call update_scoreboard
to update the score and high score on the screen.
With these four classes, we have all the necessary logic to create the snake game. Now, let's put it all together in the main.py
file.
Here's the complete code for the main.py
file:
from turtle import Screen
from snake import Snake
from food import Food
from scoreboard import Scoreboard
import time
# Setup the screen
screen = Screen()
screen.setup(width=600, height=600)
screen.bgcolor("black")
screen.title("Snake Game")
screen.tracer(0)
# Create the snake
snake = Snake()
# Create the food
food = Food()
# Create the scoreboard
scoreboard = Scoreboard()
# Listen for key presses
screen.listen()
screen.onkey(snake.up, "Up")
screen.onkey(snake.down, "Down")
screen.onkey(snake.left, "Left")
screen.onkey(snake.right, "Right")
# Game loop
game_is_on = True
while game_is_on:
screen.update()
time.sleep(0.1)
# Move the snake
snake.move()
# Detect collision with food
if snake.head.distance(food) < 15:
food.refresh()
snake.extend()
scoreboard.increase_score()
# Detect collision with wall
if snake.head.xcor() > 280 or snake.head.xcor() < -280 or snake.head.ycor() > 280 or snake.head.ycor() < -280:
scoreboard.reset()
snake.reset()
# Detect collision with tail
for segment in snake.segments[1:]:
if snake.head.distance(segment) < 10:
scoreboard.reset()
snake.reset()
# Update the high score
if scoreboard.score > scoreboard.high_score:
scoreboard.update_high_score()
# Exit the screen
screen.exitonclick()
Let's take a moment to go through this code.
We start by setting up the screen and creating the snake, the food, and the scoreboard. We also listen for key presses and set up the game loop.
In the game loop, we update the screen, move the snake, and detect collisions with the food, the wall, and the snake's tail. We also update the high score if necessary.
Finally, we exit the screen when the game is over.
Wrap Up
That's it! You now have a fully functional snake game built using object-oriented programming in Python.
Congratulations on completing this project! You can now further enhance this game by adding sound effects, changing the color scheme, or even implementing a two-player mode. The possibilities are endless.
If you're interested in checking out the code for this game, you can find it on my GitHub. The repository includes all the code files we've discussed in this tutorial, as well as some additional resources you might find helpful.
If you enjoyed this article and want to be a part of this amazing journey, don't forget to follow me on Twitter for more content like this!