Sample Snake Game in Python 2.7

Update 21 Aug 2020: Fixed the link to the source code.


The codes are available here.

I was kind of idle once again in the office. For some reason we could not smoothly work in our client’s office yet and proceed with the system integration testing of our work. I had a lot of time to kill and I figured I could use it to continue my study in python programming. Yes I know most CS students start with python, but for us it was C so I’m still learning the language now. I’ve read through the manual for the basic stuff months ago but I never really worked on any exercise. And just last Tuesday (December 20) I thought, how else would you want to practice python programming than by creating a sample of the snake game? Yes I found it funny. I’m pretty shallow that way. But thinking it over, game programming is enjoyable and the snake game is pretty simple. It would be worth a shot. So here’s my work.

Note: If you want to try this on your own, please make sure that you have Python2.7 installed and the corresponding version of PyGame.

THE SPRITES

I put all the sprites in one file named sprites.py. The class names are pretty ridiculous for I was sort of doing things on the fly and I am sorry for that. I even used the name Apple instead of Food so for this game you are going to have an apple-eating snake.

To begin, we need to put the following line to import the pygame module:

import pygame

The Apple Class

class Apple(pygame.sprite.Sprite):
    #private constants
    _DEFAULT_COLOR = [255, 0, 0] #red
    _DEFAULT_SIZE = [10, 10]
    def __init__(self, color, size, position):
        #parameter validation
        if color == None:
            color = Apple._DEFAULT_COLOR
        if size == None:
            size = Apple._DEFAULT_SIZE
        if position == None:
            raise Exception('Invalid position.')

        #initialization
        self.color = color
        self.size = size
        self.image = pygame.Surface(size)
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.rect.topleft = position

This one’s pretty straight-forward. We have here an apple class accepting parameters for the color, size, and position upon creation of an instance. I provided default values for the color and size and a little parameter validation since it’s going to be a part of a class library.

Note: All sprites are just composed of square areas in the screen of about 10px X 10px filled by a certain color.

The Callable Class

class Callable:
    def __init__(self, anycallable):
        self.__call__ = anycallable

This class is only used to implement static functions in the sprite classes.

The Snake Class

The Snake class has with it three internal classes namely _SnakeTail, _SnakeHead, and SnakeMove with the first two as private. I figured, If I would like to use images rather than solid colors in the future, I would most certainly dedicate a different image to the head compared to the body. Hence, I decided to separate the snake’s head from its tail.

Like what I did in the Apple class, I also provided default values for the attributes.

    #private constants
    _DEFAULT_COLOR = [0, 255, 0] #green
    _DEFAULT_SIZE = [10, 10] #10px X 10px
    _DEFAULT_POSITION = [30, 30] #space given to tail of length 2

The _SnakeTail Class

    class _SnakeTail(pygame.sprite.Sprite):
        def __init__(self):
            #initialization
            self.tiles = []

        def add_tile(self, color, size, position):
            #creates a new tile
            tile = pygame.Surface(size)
            tile.fill(color)
            rect = tile.get_rect()
            rect.topleft = position

            self.tiles.append({'image':tile, 'rect':rect})

The snake’s tail is composed of an array of color-filled square areas that I call tiles for lack of a better term. It also has an add_tile function that will make the tail grow longer. The add_tile function creates another instance of a tile of a given color and size and adds it to the tail’s array of tiles. Once the tail is re-rendered on screen, it should appear longer.

The _SnakeHead Class

    class _SnakeHead(pygame.sprite.Sprite):
        def __init__(self, color, size, position):
            #initialization
            self.image = pygame.Surface(size)
            self.image.fill(color)
            self.rect = self.image.get_rect()
            self.rect.topleft = position

The _SnakeHead class is very similar to the Apple class.

The SnakeMove Class

    class SnakeMove():
        UP = '1Y'
        DOWN = '-1Y'
        RIGHT = '1X'
        LEFT = '-1X'

        #checks a direction's validity
        def SnakeMove_is_member(direction):
            if direction == Snake.SnakeMove.UP:
                return True
            elif direction == Snake.SnakeMove.DOWN:
                return True
            elif direction == Snake.SnakeMove.RIGHT:
                return True
            elif direction == Snake.SnakeMove.LEFT:
                return True

            return False

        #makes the function 'is_member' a static function
        SnakeMove_is_member = Callable(SnakeMove_is_member)

The creation of this class would have been avoided if I settled on accepting just some set of values for directions in making the snake move but it was against to what I’m used to. Since I wanted sprites.py to function like a standard class library I wanted it to implement a uniform way of using the classes and their functions and sort of inform developers about it. The SnakeMove class functions like an enum class in C#, this way developers won’t have to guess valid direction values once they deal with making the snake move. All they have to do is pass a constant from this class as a parameter to the Snake class’ move function.

The class has a SnakeMove_is_member static function which is used for validating the direction parameter in the Snake class’ move function. The static class was created with the help of the previously shown Callable class.

The Snake Class Functions

Initialization

    def __init__(self, color, size, position):
        #parameter validation
        if color == None:
            color = Snake._DEFAULT_COLOR
        if size == None:
            size = Snake._DEFAULT_SIZE
        if size[0] != size[1]:
            raise Exception('Invalid tile size. Width and height must be equal.')
        if position == None:
            position = Snake._DEFAULT_POSITION

        self.color = color
        self.size = size
        self.head = Snake._SnakeHead(color, size, position)
        self.tail = Snake._SnakeTail()
        tailposition = [(position[0] - size[0]), position[1]]
        self.tail.add_tile(color, size, tailposition)
        tailposition = [(position[0] - 2*size[0]), position[1]]
        self.tail.add_tile(color, size, tailposition)

The initialization of the snake class simple involves validating the parameters and assigning default values for missing paramaters and creating the instance for the snake’s head and tail. As shown in the code, the initial length of the tail is 2 tiles. All in all, the Snake sprite has a color, size, head, and tail attributes.

Movement

    def move(self, direction, frame_width, frame_height):
        #parameter validation
        if Snake.SnakeMove.SnakeMove_is_member(direction) != True:
            raise Exception('Invalid movement direction.')

        #initializes new position
        stepsize = self.head.image.get_rect()[2] #gets the size of the head tile
        newheadposition = [self.head.rect.topleft[0], self.head.rect.topleft[1]]
        if direction == Snake.SnakeMove.UP:
            newheadposition[1] = (newheadposition[1]-stepsize)%frame_height
        if direction  == Snake.SnakeMove.DOWN:
            newheadposition[1] = (newheadposition[1]+stepsize)%frame_height
        if direction == Snake.SnakeMove.RIGHT:
            newheadposition[0] = (newheadposition[0]+stepsize)%frame_width
        if direction == Snake.SnakeMove.LEFT:
            newheadposition[0] = (newheadposition[0]-stepsize)%frame_width

        if self.occupies_position(newheadposition):
            return False

        #moves the head to its new position
        newtileposition = self.head.rect.topleft
        self.head.rect.topleft = newheadposition

        #moves the tail tiles to its respective new positions
        for count in range(len(self.tail.tiles)):
            prevtileposition = self.tail.tiles[count]['rect'].topleft
            self.tail.tiles[count]['rect'].topleft = newtileposition
            newtileposition = prevtileposition

        return True

As mentioned earlier, the move function first checks if the supplied direction is valid. If not, it will raise an exception. If it is, it will proceed to determing the next position of each tile starting with the head.

stepsize = self.head.image.get_rect()[2] #gets the size of the head tile

In this line, we are assuming that the size of the head is equal with the all the other tiles in the snake’s body.

if self.occupies_position(newheadposition):
    return False

Here we are making sure that the snake is not trying to move to a space that its body already occupies. If it is, then the snake won’t be able to move and the game will be over.

The remaining lines deal with moving each tile to the position of the one before it. If all goes well then the function will return True, signifying that the movement was successful.

Collision Detection

    #checks if this snake's body occupies a given position
    def occupies_position(self, position):
        #parameter validation
        if position[0] == None or position[1] == None:
            return True

        if self.head.rect.topleft[0] == position[0] \
            and self.head.rect.topleft[1] == position[1]:
                return True

        for count in range(len(self.tail.tiles)):
            if self.tail.tiles[count]['rect'].topleft[0] == position[0] \
            and self.tail.tiles[count]['rect'].topleft[1] == position[1]:
                return True

        return False

PyGame already has a built-in function for collision detection involving objects with rect attribute but I found it quite late. I did this one and it was pretty good enough for me. As the function name implies, this function checks whether the snake is occupying a given position.

Lengthening the Tail

    def lengthen_tail(self, number, current_direction):
        #parameter validation
        if number is None:
            number = 1
        if Snake.SnakeMove.SnakeMove_is_member(current_direction) != True:
            raise Exception('Invalid movement direction.')

        size = self.size[0]
        color = self.color

        for count in range(number):
            lastindex = len(self.tail.tiles) - 1
            X = self.tail.tiles[lastindex]['rect'].topleft[0]
            Y = self.tail.tiles[lastindex]['rect'].topleft[1]

            #determines position of new tile
            if current_direction == Snake.SnakeMove.UP:
                Y = Y - size + (count*size)
            elif current_direction == Snake.SnakeMove.DOWN:
                Y = Y + size + (count*size)
            elif current_direction == Snake.SnakeMove.RIGHT:
                X = X - size + (count*size)
            elif current_direction == Snake.SnakeMove.LEFT:
                X = X + size + (count*size)

            self.tail.add_tile(color, self.size, [X, Y])

Since the _SnakeTail class is supposed to be private, I provided this function on the Snake class. This should be the one that developers using my sprites library use when making the Snake grow longer. I included an number parameter just in case someone wants to make the snake grow longer by more than one tile. Determining the position for the new tile involves checking the snake’s current direction to ensure that addition of tiles will be done on the right end and following the right direction of movement.

THE GAME

The main file for this snake game is game.py. It handles the display and game flow.

Initialization

import pygame
import pygame._view
from pygame import *
from sprites import Snake
from sprites import Apple
import random

The lines above shows the modules imported by the game file. Notice that only the Apple and Snake classes were imported from sprites.py.

pygame.init()

Initialize the game.

DEFAULT_SCREEN_SIZE = [640, 480]
INITIAL_DIRECTION = Snake.SnakeMove.RIGHT
DEFAULT_UPDATE_SPEED = 100

updatetime = pygame.time.get_ticks() + DEFAULT_UPDATE_SPEED

Some default values and update time initialization.

#screen initialization
screen = pygame.display.set_mode(DEFAULT_SCREEN_SIZE)
display.set_caption('Snake')

#sprite initialization
snake = Snake(None, None, None)
apple = None

Screen and sprites initialization.

is_done = False #signifies escape from game
is_over = False #signifies end of game by game rules
direction = None
score = 0
create_apple()

Initialization of variables that will be used in the game flow. Notice that we have is_done and is_over. create_apple was also called to create first instance of the apple.

Rendering

def render_snake():
    screen.blit(snake.head.image, snake.head.rect)
    for count in range(len(snake.tail.tiles)):
        screen.blit(snake.tail.tiles[count]['image']
            , snake.tail.tiles[count]['rect'])

This method renders the snake on screen. This simply displays all the tiles forming the image of the snake.

#renders the apple on screen
def render_apple():
    global apple
    screen.blit(apple.image, apple.rect)

Like the render_snake method, this method displays the apple on screen.

#creates a new apple
def create_apple():
    global apple
    global snake

    hlimit = (DEFAULT_SCREEN_SIZE[0]/Apple._DEFAULT_SIZE[0])-1
    vlimit = (DEFAULT_SCREEN_SIZE[1]/Apple._DEFAULT_SIZE[1])-1
    X, Y = None, None

    while snake.occupies_position([X, Y]) == True:
        X = random.randint(0, hlimit)*Apple._DEFAULT_SIZE[0]
        Y = random.randint(0, vlimit)*Apple._DEFAULT_SIZE[1]
    apple = Apple(None, None, [X, Y])

Notice that horiontal and vertical limits were computed first before generating a random position. This ensures that the position generated will be inside the area of the screen. Default values are used here ‘though, maybe we can make it configurable on a better version. We also keep on generating random positions if the current position is occupied by the snake.

The Game Flow

while is_done == False:
    screen.fill(0) #color screen black
    global direction
    if direction == None:
        direction = INITIAL_DIRECTION

    #checks for changes in direction and validates it
    for e in event.get():
        if e.type == KEYUP:
            if e.key == K_ESCAPE:
                is_done = True
            elif e.key == K_UP:
                if direction != Snake.SnakeMove.DOWN:
                    direction = Snake.SnakeMove.UP
            elif e.key == K_DOWN:
                if direction != Snake.SnakeMove.UP:
                    direction = Snake.SnakeMove.DOWN
            elif e.key == K_RIGHT:
                if direction != Snake.SnakeMove.LEFT:
                    direction = Snake.SnakeMove.RIGHT
            elif e.key == K_LEFT:
                if direction != Snake.SnakeMove.RIGHT:
                    direction = Snake.SnakeMove.LEFT

    #updates the display
    currenttime = pygame.time.get_ticks()
    global updatetime
    global is_over
    if is_over == False:
        if currenttime >= updatetime:
            moved = snake.move(direction, DEFAULT_SCREEN_SIZE[0], DEFAULT_SCREEN_SIZE[1])
            if moved == False:
                is_over = True
            if snake.occupies_position(apple.rect.topleft) == True:
                create_apple()
                snake.lengthen_tail(1, direction)
                global score
                score += 1
                display.set_caption('Snake: ' + str(score))

            render_apple()
            render_snake()
            pygame.display.update()
            updatetime += DEFAULT_UPDATE_SPEED
    else:
        display.set_caption('Snake: ' + str(score) + ' GAME OVER')

The flow as expected is implemented using a loop.

screen.fill(0) #color screen black

First we fill the screen with black.

    global direction
    if direction == None:
        direction = INITIAL_DIRECTION

    #checks for changes in direction and validates it
    for e in event.get():
        if e.type == KEYUP:
            if e.key == K_ESCAPE:
                is_done = True
            elif e.key == K_UP:
                if direction != Snake.SnakeMove.DOWN:
                    direction = Snake.SnakeMove.UP
            elif e.key == K_DOWN:
                if direction != Snake.SnakeMove.UP:
                    direction = Snake.SnakeMove.DOWN
            elif e.key == K_RIGHT:
                if direction != Snake.SnakeMove.LEFT:
                    direction = Snake.SnakeMove.RIGHT
            elif e.key == K_LEFT:
                if direction != Snake.SnakeMove.RIGHT:
                    direction = Snake.SnakeMove.LEFT

Then we check for changes in direction based on keyboard input. Notice that global direction was used to make things less confusing. After all only one direction is needed. Additional check was also placed to deem direction opposite the current one as invalid.

    #updates the display
    currenttime = pygame.time.get_ticks()
    global updatetime
    global is_over
    if is_over == False:
        if currenttime >= updatetime:
            moved = snake.move(direction, DEFAULT_SCREEN_SIZE[0], DEFAULT_SCREEN_SIZE[1])
            if moved == False:
                is_over = True
            if snake.occupies_position(apple.rect.topleft) == True:
                create_apple()
                snake.lengthen_tail(1, direction)
                global score
                score += 1
                display.set_caption('Snake: ' + str(score))

            render_apple()
            render_snake()
            pygame.display.update()
            updatetime += DEFAULT_UPDATE_SPEED
    else:
        display.set_caption('Snake: ' + str(score) + ' GAME OVER')

Updating the display depends on two things, one is if the game is not over yet and the other is if the update time was already reached.

    moved = snake.move(direction, DEFAULT_SCREEN_SIZE[0], DEFAULT_SCREEN_SIZE[1])
    if moved == False:
        is_over = True

If the game is not yet over and the update time was already reached, an attempt to move the snake is done. In case movement failed which is probably becaused the snake hit itself, the game will be over.

    if snake.occupies_position(apple.rect.topleft) == True:
        create_apple()
        snake.lengthen_tail(1, direction)
        global score
        score += 1
        display.set_caption('Snake: ' + str(score))

If movement was successful, it is determined if the snake passes through the position of the apple. If it does, then a new apple is created, the snake’s tail will be lengthened, the score will be incremented, and the score display will be updated.

    render_apple()
    render_snake()
    pygame.display.update()
    updatetime += DEFAULT_UPDATE_SPEED

To cap off the updates, all the elements will be re-rendered and the update time will be set to a future time.

On the other hand, if the game is already over, the updates will simply stop and the display will flash GAME OVER together with the score.

Wow this post is pretty lengthy but I hope someone might find it useful at least for comparisons haha. Feel free to comment ‘though I’d be moderating them. Have a nice day everyone!

8 thoughts on “Sample Snake Game in Python 2.7

  1. thank you! how can i change the color of the apples and the snake? i want a snake htat changes to the color of the apple it just ate.

    1. it has been a very long time; i cannot perfectly recall everything. but you can do color randomization for the apple then fill the snake head and snake tail’s image with that color once the snake gets to eat the apple. the rendering function should render the snake with the new color.

      thanks for asking. it’s nice to get a message from readers once in a while.

      1. Thank you so much! I Will try that when I get to know a little bit more about programming.I am starting a python game programming course in coursera.org that you may want to reccomend to your readers/followers,
        because (as your site), its fun and free! 😀

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s