Python Snake Game
Let's make a Snake game in Python (in less than 100 lines code)!
For those who don't know, the white thing is the snake. It can be controlled by the player to go up, down, left and right. Every time the snake eats one of those blue things (let's call it food), it gets bigger.
Most snake games are a bit more complex though. There are walls that kill the snake when it runs into it, there is food that kills it if the snake eats it and there are different levels and speeds. However, to keep everything nice and simple, we will only focus on the snake and its food.
Preparations
We will make this game with Python and OpenGL. Please take a look at our Default Python IDE and Python OpenGL tutorials to learn how to set it up properly.
We will start with the code from the Python OpenGL Introduction tutorial with just a few modifications:
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
window = 0 # glut window number
width, height = 500, 500 # window size
field_width, field_height = 50, 50 # internal resolution
def refresh2d_custom(width, height, internal_width, internal_height):
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0.0, internal_width, 0.0, internal_height, 0.0, 1.0)
glMatrixMode (GL_MODELVIEW)
glLoadIdentity()
def draw_rect(x, y, width, height):
glBegin(GL_QUADS) # start drawing a rectangle
glVertex2f(x, y) # bottom left point
glVertex2f(x + width, y) # bottom right point
glVertex2f(x + width, y + height) # top right point
glVertex2f(x, y + height) # top left point
glEnd() # done drawing a rectangle
def draw(): # draw is called all the time
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen
glLoadIdentity() # reset position
refresh2d_custom(width, height, field_width, field_height)
# TODO draw things
glutSwapBuffers() # important for double buffering
# initialization
glutInit() # initialize glut
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH)
glutInitWindowSize(width, height) # set window size
glutInitWindowPosition(0, 0) # set window position
window = glutCreateWindow("noobtuts.com") # create window with title
glutDisplayFunc(draw) # set draw function callback
glutIdleFunc(draw) # draw all the time
glutMainLoop() # start everything
Note: don't worry, we don't see anything yet if we run the program (except a black screen).
The interesting thing about this code is the refresh2d_custom function. As a reminder, the refresh2d function in our Python OpenGL tutorial was used to tell OpenGL that we want to draw things in 2D.
The custom refresh2d function basically does the same. The difference is that it takes two more parameters: internal_width and internal_height for the internal resolution.
Please note that the internal resolution is completely independent from the window resolution.
To avoid confusion we will take a look at a few chess field pictures. They all have a window resolution of 500 x 500 pixels, but they have a different internal resolution:
50x50 internal resolution (sorry for making your eyes go crazy)
10x10 internal resolution
2x2 internal resolution
So why all this? Well the reason is that we want to make our lives easier. We want our food to be just one pixel and our snake to be one (or more depending on the length) pixels. If we would do this with the default internal resolution, our snake and our food would look like this:
No this is not just a black window, there are some tiny little pixels in there. They are so tiny because our internal resolution is 500 by 500 pixels, so obviously everything looks really small.
Now if we choose a much smaller internal resolution like 50 by 50 pixels, we get this:
Every pixel that we draw is nicely visible without the need for a magnifying glass. Much better, right?
Note: our game's internal resolution is hold in the field_width and field_height variables.
Okay, now that we talked about internal and external resolutions, we can focus on the actual game.
Creating the Snake
Let's create the main part of our game: the Snake.
Snake Variable
The snake is just a list of pixels at different positions. In the beginning it's only one pixel, after the snake eats something it's two pixels, then three and so on. The snake's head (the first element) will be in the first position in the list.
We will use two variables in order to represent our snake: the just mentioned list, and the current movement direction (as x,y coordinates). As usual, they will be placed at the top of our program (where we stored our window size):
snake = [] # snake list of (x, y) positions
snake_dir = (1, 0) # snake movement direction
Note: snake dir (1, 0) means that its current movement direction is x=1 and y=0, which means it moves to the right.
But wait, at the beginning the snake already has one element (its head). So let's remove the previous definition and add the snake list with one initial element in form of a (x, y) position:
snake = [(20, 20)] # snake list of (x, y) positions
snake_dir = (1, 0) # snake movement direction
This means that the snake head is at the position (x=20, y=20) in the beginning. To make this more clear: if the snake would eat something, our snake list might look like this:
[(20, 20), (21, 20)] # snake after eating
Drawing the Snake
We really want to see something now. Let's create a draw_snake() function that throws every pixel in our snake list onto the screen:
def draw_snake():
glColor3f(1.0, 1.0, 1.0) # set color to white
for x, y in snake: # go through each (x, y) entry
draw_rect(x, y, 1, 1) # draw it at (x, y) with width=1 and height=1
Now that we have a function that draws the snake, we also have to use it in our draw() function:
def draw(): # draw is called all the time
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen
glLoadIdentity() # reset position
refresh2d_custom(width, height, field_width, field_height)
draw_snake() # draw the snake
glutSwapBuffers() # important for double buffering
Let's press the run button and see what happens:
We can see our snake's head, awesome!
Moving the Snake
We will use the W, S, A and D keys to move the snake up, down, left and right. Things like movement should be done in a update function. We don't have one yet, but we can easily add one. At first we create it:
def update(value):
# TODO update things...
glutTimerFunc(interval, update, 0) # trigger next update
The interval is defined at the top (where we defined the window size) again like this:
interval = 200 # update interval in milliseconds
And finally we have to tell OpenGL that we have a update function now. We will do this with glutTimerFunc in our OpenGL initialization. The new initialization part looks like this:
# initialization
glutInit() # initialize glut
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH)
glutInitWindowSize(width, height) # set window size
glutInitWindowPosition(0, 0) # set window position
window = glutCreateWindow("noobtuts.com") # create window with title
glutDisplayFunc(draw) # set draw function callback
glutIdleFunc(draw) # draw all the time
glutTimerFunc(interval, update, 0) # trigger next update
glutMainLoop() # start everything
Explanation: in our OpenGL initialization we use glutTimerFunc to tell OpenGL that it should call our update function in 200 milliseconds (specified in interval).
After we start the program, OpenGL will call our update function after 200ms. Our update function itself doesn't do anything yet, except tell OpenGL to call it again in 200ms. Hence we created our update loop.
Okay, back to the snake's movement...
Let's assume the snake already ate something three times. Hence it looks like this (as text version):
o
o
o
o
If the user would move it to the right, it would then look like this the next time:
oo
o
o
So the obvious way would be to create an algorithm that first moves the snake's head to the new position and then let's every other entry in our snake list follow the snake's head by one step.
Since this sounds kinda complicated, we will use a little trick:
Instead of moving every single element by one step, we will just remove the last element and put it to the new position.
Here is our wonderful text snake again, this time "x" is the last element:
o
o
o
x
Now if the player wants to move it to the right, instead of moving the first one to the right and letting everything else follow, we will simply remove the x from the end and put it to the new position:
ox
o
o
This way it appears that the whole snake moved, even though we just removed the last element and made it the new head.
Note: from a performance side, this is just beautiful. It means that in each update call, instead of doing "n" calculations ("n" is the snake length) we only have to do 2 calculations. This kind of trick can make the difference between 60 fps and 30 fps in bigger games.
Anyway, let's create an algorithm that does that for us. At first we need a function that adds two positions. Example:
(3, 4) + (1, 2) = (4, 6).
We will name the function vec_add which stands for "Add Vectors":
def vec_add((x1, y1), (x2, y2)):
return (x1 + x2, y1 + y2)
Note: more about Vectors in Python can be learned in our Python Vector tutorial.
We need this function in order to move the snake. In each update function we want to move it a bit towards the snake_dir variable that we defined before. The math will look like this:
new_pos = vec_add(snake[0], snake_dir)
Note: snake[0] is the first entry in our snake list, which is the snake head.
Enough talking about math, let's implement our snake movement in our update function:
def update(value):
snake.insert(0, vec_add(snake[0], snake_dir)) # insert new position in the beginning of the snake list
snake.pop() # remove the last element
glutTimerFunc(interval, update, 0) # trigger next update
One call to insert and one call to pop (which removes ("pops") the last element) does all the magic.
If we run the game, we can now see the snake moving to the right all the time.
Note: obviously we don't see all the magic yet since the snake only consists of one element.
Since we want to be able to move it with the W, S, A and D keys, we will have to check if those were pressed by the player. We have to do two things to check the keys. At first, we create a keys() function:
def keyboard(*args):
global snake_dir # important if we want to set it to a new value
if args[0] == 'w':
snake_dir = (0, 1) # up
if args[0] == 's':
snake_dir = (0, -1) # down
if args[0] == 'a':
snake_dir = (-1, 0) # left
if args[0] == 'd':
snake_dir = (1, 0) # right
Note: this is just the way it's done, we won't worry about it too much.
Again we have to tell OpenGL that we have a key checking function. We will go down to our initialization code and add glutKeyboardFunc(keyboard):
# initialization
glutInit() # initialize glut
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH)
glutInitWindowSize(width, height) # set window size
glutInitWindowPosition(0, 0) # set window position
window = glutCreateWindow("noobtuts.com") # create window with title
glutDisplayFunc(draw) # set draw function callback
glutIdleFunc(draw) # draw all the time
glutTimerFunc(interval, update, 0) # trigger next update
glutKeyboardFunc(keyboard) # tell opengl that we want to check keys
glutMainLoop() # start everything
That's it. If we run the game, we can now move the snake with the W, S, A and D keys. Kinda looks like Snake already...
Spawning the Food
A snake has to eat, we will have to throw some food into the game every now and then. Again we will just use a simple list to store the food. The list will be defined at the top again (where we stored our window size):
food = [] # food list of type (x, y)
We will use randomness in order to spawn food at some random position in a random interval. If we want to use Python's random function, we will have to import something first (at the very top of our program):
from random import randint
Alright, let's spawn the food in our update function:
def update(value):
# move snake
snake.insert(0, vec_add(snake[0], snake_dir)) # insert new position in the beginning of the snake list
snake.pop() # remove the last element
# spawn food
r = randint(0, 20) # spawn food with 5% chance
if r == 0:
x, y = randint(0, field_width), randint(0, field_height) # random spawn pos
food.append((x, y))
glutTimerFunc(interval, update, 0) # trigger next update
So what happens is that we create a random value and store it in our r variable. This value is between 0 and 20. Then we check if the value is 0 (which happens with a 5% chance because 100 divided by 20 is 5). If this happens we use the random function again to create a random x and y value and then we append it to our food list.
In order to actually see something, we have to draw our food in the same way that we used to draw our snake (just with a different color):
def draw_food():
glColor3f(0.5, 0.5, 1.0) # set color to blue
for x, y in food: # go through each (x, y) entry
draw_rect(x, y, 1, 1) # draw it at (x, y) with width=1 and height=1
As usual, we then use the draw_food() function in our draw function:
def draw(): # draw is called all the time
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen
glLoadIdentity() # reset position
refresh2d_custom(width, height, field_width, field_height)
draw_food() # draw the food
draw_snake() # draw the snake
glutSwapBuffers() # important for double buffering
If we run the game and wait a while, we should see food spawning all over our screen:
Eating the Food
So we created our snake, we added some food, but sadly the snake still doesn't know how to eat it.
The good news is, this is really easy to do. In each update call, we simply find out if the snake's head (at snake[0]) is at the same position as any of the food in the food list. If so, the snake will eat it (which means that the snake gets longer and that the food is removed).
The code (to be placed in the update function):
# let the snake eat the food
(hx, hy) = snake[0] # get the snake's head x and y position
for x, y in food: # go through the food list
if hx == x and hy == y: # is the head where the food is?
snake.append((x, y)) # make the snake longer
food.remove((x, y)) # remove the food
If we run the game and move our snake to the food, it now gets longer and longer:
Summary
There we go, a lightweight Snake game written in less than a hundred lines of Python code. As usual, now it's your turn to make the game fun. Add different kinds of food, let the snake die if it hits the wall, add different levels and think about how to make the snake die if it collides with itself. Maybe even add some background music and a few textures. It's your game world, you can do whatever you like!
Download Source Code & Project Files
The Python Snake Game source code & project files can be downloaded by Premium members.All Tutorials. All Source Codes & Project Files. One time Payment.
Get Premium today!