
Building a snake game
Who doesn't remember the classic game called Snake, which involves a snake chasing a morsel of food? It is probably the very first game that you played as a child. The basic premise of the game is that you control a snake and lead it to a morsel of food. Every time the snake consumes that food, it grows by one unit length, and if the snake hits a boundary wall or itself, it dies. Now as you can imagine, the more you play the game, the longer the snake grows, which, consequently, makes it more difficult to control the snake. In some versions of the game, the speed of the snake also increases, making it even more difficult to control. There comes a point where you simply run out of screen space and the snake inevitably hits a wall or itself, and the game is over.
Here, we will learn how to build such a game. The basic logic of playing the game will be to have a moving rectangle, of which we know the leading point coordinates. This will be our snake. It will be controlled by the four arrow keys. The piece of food is initialized randomly on the screen. At each point of time, we will check whether the rectangle has hit the boundary wall or itself since we know the position of the snake at every point of time. If it has, then the program will exit. Let's now look at the code sectionwise; the code will be explained after each section:
from pygame.locals import * import pygame import random import sys import time pygame.init() fpsClock = pygame.time.Clock() gameSurface = pygame.display.set_mode((800, 600)) pygame.display.set_caption('Pi Snake') foodcolor = pygame.Color(0, 255, 0) backgroundcolor = pygame.Color(255, 255, 255) snakecolor = pygame.Color(0, 0, 0) textcolor = pygame.Color(255, 0, 0) snakePos = [120,240] snakeSeg = [[120,240],[120,220]] foodPosition = [400,300] foodSpawned = 1 Dir = 'D' changeDir = Dir Score = 0 Speed = 5 SpeedCount = 0
Now, we will learn what each block of code does, but for brevity the very basics are skipped as we have already learned about them in the previous sections.
The first few lines before the finish()
function initialize PyGame and set the game parameters. The pygame.time.Clock()
function is used to track time within the game, and this is mostly used for frames per second, or FPS. While it seems somewhat trivial, FPS is very important and can be tweaked. We can increase or decrease the FPS to control the speed of the game. Going further into the code, we can choose options such as the screen size, the color of the snake, the starting position, the starting speed, and so on. The snakePos
list variable has the head of the snake, and snakeSeg
will contain the initial coordinates of the segment of the snake in a nested list. The first element contains the coordinates of the head, and the second element contains the coordinates of the tail. This block of code also defines the initial food position, the state of the food, the initial direction, the initial speed, and the initial player score:
def finish(): finishFont = pygame.font.Font(None, 56) msg = "Game Over! Score = " + str(Score) finishSurf = finishFont.render(msg, True, textcolor) finishRect = finishSurf.get_rect() finishRect.midtop = (400, 10) gameSurface.blit(finishSurf, finishRect) pygame.display.flip() time.sleep(5) pygame.quit() exit(0)
The preceding block of code defines the finishing procedure for the game. As we will see in the following code, finish()
is called when the snake either hits the walls or itself. In this, we first specify the message that we want to display and its properties, such as the font, and then we add the final score to it. Then, we render the message via the render()
function, which operates on the finishFont
variable. Then, we get a rectangle via get_rect()
, and finally we draw those via the blit()
function. When using PyGame, blit()
is a very important function that allows us to draw one image on top of the other. In our case, this is very useful because it allows us to draw a bounding rectangle over the message that we show as a part of the ending of the game. Finally, we render our message on screen via the display.flip()
function and after a delay of 5
seconds, we quit the game:
while 1: for event in pygame.event.get(): if event.type == QUIT: pygame.quit() exit(0) elif event.type == KEYDOWN: if event.key == ord('d') or event.key == K_RIGHT: changeDir = 'R' if event.key == ord('a') or event.key == K_LEFT: changeDir = 'L' if event.key == ord('w') or event.key == K_UP: changeDir = 'U' if event.key == ord('s') or event.key == K_DOWN: changeDir = 'D' if event.key == K_ESCAPE: pygame.event.post(pygame.event.Event(QUIT)) pygame.quit() exit(0) if changeDir == 'R' and not Dir == 'L': Dir = changeDir if changeDir == 'L' and not Dir == 'R': Dir = changeDir if changeDir == 'U' and not Dir == 'D': Dir = changeDir if changeDir == 'D' and not Dir == 'U': Dir = changeDir if Dir == 'R': snakePos[0] += 20 if Dir == 'L': snakePos[0] -= 20 if Dir == 'U': snakePos[1] -= 20 if Dir == 'D': snakePos[1] += 20
Then, we move on to the infinite while
loop, which contains the bulk of the game's logic. Also, as mentioned earlier, the pygame.event.get()
function gets the type of event; according to what is pressed, it changes the state of some parameters. For example, pressing the Esc key causes the game to quit, and pressing the arrow keys changes the direction of the snake. After that, we check whether the new direction is directly opposite to the old direction and change the direction only if it isn't. In this case, changedDir
is only an intermediate variable. We then change the position of the snake according to the direction that's selected. Each shift in position signifies a shift of 20 pixels on screen:
snakeSeg.insert(0,list(snakePos)) if snakePos[0] == foodPosition[0] and snakePos[1] == foodPosition[1]: foodSpawned = 0 Score = Score + 1 SpeedCount = SpeedCount + 1 if SpeedCount == 5 : SpeedCount = 0 Speed = Speed + 1 else: snakeSeg.pop() if foodSpawned == 0: x = random.randrange(1,40) y = random.randrange(1,30) foodPosition = [int(x*20),int(y*20)] foodSpawned = 1 gameSurface.fill(backgroundcolor) for position in snakeSeg: pygame.draw.rect(gameSurface,snakecolor,Rect(position[0], position[1], 20, 20)) pygame.draw.circle(gameSurface,foodcolor,(foodPosition[0]+10, foodPosition[1]+10), 10, 0) pygame.display.flip() if snakePos[0] > 780 or snakePos[0] < 0: finish() if snakePos[1] > 580 or snakePos[1] < 0: finish() for snakeBody in snakeSeg[1:]: if snakePos[0] == snakeBody[0] and snakePos[1] == snakeBody[1]: finish() fpsClock.tick(Speed)
It is important to keep in mind that at this point, nothing is rendered on screen. We are just implementing the logic for the game, and only after we are done with that will anything be rendered on the screen. This will be done with the pygame.display.flip()
function. Another important thing is that there is another function named pygame.display.update()
. The difference between these two is that the update()
function only updates specific areas of the surface, whereas the flip()
function updates the entire surface. However, if we don't give any arguments to the update()
function, then it will also update the entire surface.
Now, since we changed the position of the head of the snake, we have to update the snakeSeg
variable to reflect this change in the snake body. For this, we use the insert()
method and give the position of object we want to append and the new coordinates. This adds the new coordinates of the snake head into the snakeSeg
variable. Then comes the interesting part, where we check whether the snake has reached the food. If it has, we increment the score, set the foodSpawned
state to False
, and increase SpeedCount
by one. So, once the speed count reaches five, the speed is increased by one unit. If not, then we remove the last coordinate with the pop()
method. This is interesting because if the snake has eaten the food, then its length will increase; consequently, the pop()
method in the else
statement will not be executed, and the length of the snake will be increased by one unit.
In the next block of code, we check whether the food is spawned; if not, we randomly spawn it, keeping in mind the dimensions of the screen. The randrange()
function from the random package allows us to do exactly that.
Finally, we get to the part where the actual rendering takes place. Rendering is nothing but a term for the process that is required to generate and display something on screen. The first statement fills the screen with our selected background color so that everything else on the screen is easily visible:
for position in snakeSeg: pygame.draw.rect(gameSurface,snakecolor,Rect(position[0], position[1], 20, 20))
The preceding block of code loops through all the coordinates present in the snakeSeg
variable and fills the space between them with the color specified for our snake in the initialization code. The rect
function takes three inputs: the window name on which the game will be played, the color of the rectangle, and the coordinates of the rectangle that are given by the Rect
function. The Rect
function itself takes four arguments: the x
and y
position and height
and width
of the rectangle. This means that for every coordinate contained in the snakeSeg
variable, we draw on a rectangle that has dimensions of 20 x 20
pixels. So, we can see that we do not have to keep track of the snake as a whole; we only have to keep track of the coordinates that describe the snake.
Next, we draw our food using the circle()
method from the draw
module in the PyGame package. This method takes five arguments: the window name
, the color
, the centre
of the circle, the radius
, and the width
of the circle. The centre of the circle is given by a tuple that contains the x
and y
coordinates that we selected previously. The next statement, pygame.display.flip()
, actually displays what we have just drawn.
Then, we check for the conditions in which the game can end: hitting the wall or itself. When it hits an exit condition, the finish()
function is called. The first two lines of the function are self-explanatory. In the third line, render()
basically makes the text displayable. But it is not displayed yet. It will only be displayed once we call the pygame.display.flip()
function. The next two lines set the position of the textbox that will be displayed on the window. And, finally, we quit the PyGame window and the program after a delay of 5
seconds.
Save this program in a file named prog2.py
and run it using the following command:
python prog2.py
This is what you will be greeted with:

We can play the game for as long as we want (and we should because it's our creation) and when we exit, the game will be greeted by the same message that was defined in the finish()
function!

As you may recognize, in the preceding screenshot, the black shape is the snake and the green circle is its food. You can play around with it and try to get an idea of the logic behind this game as to how it might be programmed.
With this, we complete the implementation of the snake game, and you should try out the program for yourself. An even better way to fully understand how the program works is to change some parameters and see how that affects the playing experience.