python-course.eu

18. Snake in Python

By Bernd Klein. Last modified: 24 Nov 2021.

As a 90’s kid, I’ve witnessed many technological advancements, that rapidly changed the world I was born into. One of those changes was the “Mobile Phone”. Back in the day, when Nokia was the hype, some people would buy SMS packages and being frugal, they would make compromises to fit everything into one message, while others would go crazy with the new polyphonic ringtones and were always ready to pay more to show off when their phone rang. It also brought an interesting fashion, the phone belts, a nice accessory to the over-sized clothes of the time. However, for me and for many other kids and adults, the biggest thing about this invention was the games one could play, rather than all the other practicalities it offered. I would always look forward to playing the “Snake”.

Of course, Snake has actually been around since 1976, introduced with Blockade. However, one could say that the game’s popularity has significantly increased after being implemented in the Nokia phones. Programmed by Taneli Armanto in 1997, the first phone to offer Snake was the 6610.

In this article, you will find how we recreated the famous “Snake” using Python, how you could modify it according to your wishes and what kind of thinking strategy one should follow when making / recreating games. This article will encourage you to test and modify your code, rather than just explaining the end product we made.

You can download the complete code by using these links:

We used Pygames, a free library for creating video games, for recreating Snake.

Where do we begin?

Snake, borders, food, score… We all know that these are the main elements of the game, however, where should we really begin?

After opening your preferred code editor, the first step you need to take is:

<pre> import pygame as pg </pre>

After all, we are going to be using some of the modules from the Pygame library.

Afterwards, don’t forget to name your file “game.py”.

Prepare yourself to work with multiple files, and travel between the multiple tabs open on your code editor. Let’s begin!

The Game Display

Let’s construct the game display in game.py. For this, we first need to define a class, called “Game” . For now, it will only contain the initializer “init_()”, and “run()” methods.

Inside initializer we need to set our width and height, we preferred to make it 800 * 600 pixels, however, these are arbitrary values. We preferred to save it in self.width and self.height correspondingly to make our code more readable, and for the changes to be easier later on.

import pygame as pg

class Game:
    def ```__init__```(self):               
        self.width = 800
        self.height = 600       


        self.gameDisplay = pg.display.set_mode((self.width, self.height))

The Game Display is constructed with display.set_mode() function of pygames, and takes the width and height as arguments.

In order to be able to get the pygames modules running though, we need to initalize them first (i.e. before we set the game display), which is taken care of by the function pg.init(). Let’s call:

 pg.init()

Meanwhile, we shouldn’t forget to name our game, which of course will be “Snake”. For this, after deciding on our height and width and calling pg.init(), we use the display.set_caption() function of pygames.

pg.display.set_caption( ' Snake ')   

Wonderful. Now we need to set our clock, we will be using the time.clock() function of pygames. This will help us later on with the food spawning(i.e. how many seconds does it take for new food to appear), and as well as computing how many milliseconds have passed since the previous call. This function limits the runtime speed of a game, i.e. the program wouldn’t run more than the given number of frames per second.

        self.clock = pg.time.Clock()

We now need to define another method, run(), to get our game running and to stop it. Here, the pygames function event.get() helps us track the events. Currently, we are only tracking if the player is trying to quit the game. This is represented with pg.QUIT.

def run(self):
        running = True        
        while running: 
            events = pg.event.get()
            for event in events:
                if event.type == pg.QUIT:
                    running = False                    
            self.clock.tick(100) # limits the game to 100 frames a second

        pg.quit()

Now our game display is ready to run! Let’s call the run() on Game.

g = Game()
g.run()

At this stage, your code should look like the following image.

Creating the Snake

As we mentioned earlier, we’ll be traveling between files, and now it is time to open a new file and name it “snake.py”. It will contain everything about the Snake, like its name suggests.

Setting the Directions

What all of us can certainly remember about Snake was that, it could move towards 4 directions, up, down, left and right. As Pygames library suggests, we determine the directions as:

class Direction: 
    UP = 1
    DOWN = 2
    LEFT = 3
    RIGHT = 4

With this, we will be able to move the snake using our arrow keys.

Where are we?

It is important to determine where the snake is in the canvas. That’s why we will construct another class called “Point”.

We need to imagine the canvas like the coordinate system, so we will have x and y as locations. We need to make sure we add them as arguments in the initializer. However, this canvas has (0,0) on the top left, so we need to think accordingly.

In our code we decided to move the snake by one grid, which in our case is 10 pixels, so we increment or decrement accordingly when it goes up or down and left and right. For each movement, we need a specific method.

We used the magic method __eq__ check points from Points class.

At the end class Point should look like this:

class Point:
    def ```__init__```(self, x, y):
        self.x = x
        self.y = y
    def ```__eq__```(self, other):
        if self.x == other.x and self.y == other.y:
            return True
        return False

    def move_left(self):
        self.x -= 1
    def move_right(self):
        self.x += 1
    def move_up(self):
        self.y -= 1
    def move_down(self):
        self.y += 1

The Snake

Before we construct the snake, we need to think about everything that the snake entails. We need to know its position, length, direction and the borders of the canvas (if any). So these are going to be the arguments that the initializer will contain.

The snake consists of tiles, even though this was more apparent in the small Nokia screens than the computer screen you are going to be playing with. So, we need to make an empty list for the snake’s tiles, and append tiles as it gets longer.

The first thing we should construct is a method for getting the head position, which should be the tile at the position 0 i.e. the first tile. We also need to get the tiles, to see how big the snake is. After that we also need to get the directions, to see where we are heading.

   def get_head_pos(self):
        return self.tiles[0]

    def get_tiles(self):
        return self.tiles 

    def get_directions(self):
        return self.direction

The next method we should construct is a method for moving the snake. We set borders to up and down, however feel free to experiment on this. We thought that if the snake is above the ground, it’d be discovered, and if it was too deep in the ground it’d find nothing but rocks so it’d starve. After the move, we change its position to where its head is. After that, should be able to determine our location, that’s why we return self.check_position().

    def move(self):
        for i in range(len(self.tiles) - 1, 0, -1):

            x = self.tiles[i-1].x
            y = self.tiles[i-1].y
            self.tiles[i] = Point(x, y)

        if self.direction == Direction.UP:
            self.tiles[0].move_up()

        if self.direction == Direction.DOWN:
            self.tiles[0].move_down()

        if self.direction == Direction.LEFT:
            self.tiles[0].move_left()
            if self.tiles[0].x < 0:
                self.tiles[0].x = self.borders[0]

        if self.direction == Direction.RIGHT:
            self.tiles[0].move_right()
            if self.tiles[0].x > self.borders[0]:
                self.tiles[0].x = 0

        self.pos = self.tiles[0]
        return self.check_position()

Wait! We didn’t write a method about checking the position, what are we going to return? Let’s do that now. This method checks if the snake’s head touches its body, and if it is out of the borders. If everything is OK, i.e. True, the game continues. Otherwise, the game is over.

    def check_position(self):
        if self.tiles[0].y < 0:
            return False
        if self.tiles[0].y > self.borders[1]:
            return  False
        # check if the snake’s head touches the body
        for i in  range(1, len(self.tiles)):
            if self.tiles[0] == self.tiles[i]:
                return False 
        return True

The main goal of this game is to grow the snake as much as possible, so we need to construct a method called eat. In our code, each food has the value 2 and therefore increments the length of the snake by 2. Feel free to experiment on this in your recreation. This is also the reason why our score increments by 2.

def eat(self, value = 2):
        self.length += value
        x= self.tiles[-1].x
        y= self.tiles[-1].y
        self.tiles.append(Point(x,y))

Last but not least, the snake eats, but how about the food? We construct a class called Food with a position and a value. We are going to visualize it in our game file.

class Food: 
    def __init__(self, pos, value = 2):
        self.pos = pos  
        self.value = value 

Setting boundaries

So far so good, however, what if we press up when the snake is moving down or vice versa? This is very problematic, so we need to set some limits as to where the snake can move to. In the method below, you see an empty return statement, which is for doing nothing. In other words, by doing nothing we prevent the action from taking place.

    def change_direction(self, direction):
        if direction == Direction.UP and self.direction == Direction.DOWN:
            return 
        if direction == Direction.DOWN and self.direction == Direction.UP:
            return 
        if direction == Direction.LEFT and self.direction == Direction.RIGHT:
            return
        if direction == Direction.RIGHT and self.direction == Direction.LEFT:
            return 
        self.direction = direction

Updating the Game.py

We need to update our imports, we have our snake file ready so we need to connect it with the game file. Besides, we also need to import randint from random, since the food should appear at random places.

import pygame as pg
import snake
from random import randint 

One thing to consider is the scoreboard, so we determine an offset in our initializer and we set our initial score to 0. Doing these, we need a font and a font size, which we choose as freesansbold.ttf and 40. You can run print(pygame.font.get_fonts()) to see which fonts are available.

We also need to determine the tile size, the maximum number of food that can appear on the canvas (we unconventionally chose 3 since the canvas is pretty big and therefore the food is contained in a list),

Although it is not the most essential aspect of making a game with pygames, we can add a caption to our game, by setting the caption to “ Snake “.

class Game:
    def __init__(self):
        self.world_dimensions = (80, 60)
        self.offset = 60 # offset at the top to print score and top wall
        self.width = 800
        self.height = 600 + self.offset + 10 # extra 10 pixels to print the bottom wall
        self.tile_size = 10
        self.max_food = 3
        self.snake = snake.Snake(Point(4,4), 2, snake.Direction.RIGHT, 
                                 self.world_dimensions)
        self.food = list()
        self.score = 0

        # Initialize pygame stuff
        pg.init()
        pg.display.set_caption( ' Snake ') # setting the caption
        # text and font related stuff
        self.font = pg.font.Font("freesansbold.ttf", 40)
        self.gameDisplay = pg.display.set_mode((self.width, self.height))
        # Clock for all timing related stuff
        self.clock = pg.time.Clock()

Drawing

We need to start by drawing the background, and then snake, walls, food, score and the updated display. We fit them all into the method called draw_all().

The background should not only be drawn, but also filled. So we have a separate method for it, draw_background(). We drew a blue rectangle above to represent the sky, but this is only a trivial detail.

Although the nostalgia brought us to recreate this game, we wanted to add some colors. We used the RGB values to determine the colors and we gave them names to make it easier for us.

class Colors:
    black = (0, 0, 0)
    white = (255, 255, 255)
    pink = (254, 127, 156) 
    blue = (135, 206, 235)
    green = (0, 255, 0)
    slate_blue = (106, 90, 205)
    light_blue = (173, 216, 230)
    maroon = (128, 0, 0)
    firebrick = (178, 34, 34)
    dark_orange = (255, 140, 0)

The score also has to be drawn, we chose dark orange, to represent the sun :-).

The food is pink and circle shaped, it has coordinates (x,y) to determine its position.

    ################################################
    # Drawing methods                              #
    ################################################
    def draw_all(self):
        """ Draw all game related stuff on the display"""
        # Start with background then each layer from back to front
        self.draw_background()
        self.draw_snake()
        self.draw_walls()            
        self.draw_food()
        self.draw_score()
        pg.display.update()


    def draw_background(self):
        self.gameDisplay.fill(Colors.black)
        my_rect= pg.Rect((0, 0) , [self.width, 50])
        pg.draw.rect(self.gameDisplay, Colors.light_blue, my_rect)


    def draw_score(self):
        score_str = "Score: " + str(self.score) 
        text = self.font.render(score_str, True, Colors.dark_orange)
        self.gameDisplay.blit(text, (0,0))


    def draw_food(self):
        for food in self.food: 
            x= food.pos.x
            y= food.pos.y 
            x *= self.tile_size
            y *= self.tile_size
            pg.draw.circle(self.gameDisplay, Colors.pink, (x+5, y+5 + self.offset)

    def draw_walls(self):
        for i in range(self.world_dimensions[0]):
            # top wall
            my_rect= pg.Rect((i*self.tile_size, 50) , [self.tile_size,self.tile_size] )
            pg.draw.rect(self.gameDisplay, Colors.blue, my_rect)
            pg.draw.line(self.gameDisplay, Colors.slate_blue,
                         (i*self.tile_size, 53), (i*self.tile_size+3, 50))       
            pg.draw.line(self.gameDisplay, Colors.slate_blue,
                         (i*self.tile_size, 60), (i*self.tile_size+10, 50)) 
            pg.draw.line(self.gameDisplay, Colors.slate_blue,
                         (i*self.tile_size+7, 60), (i*self.tile_size+10, 57))     
            # bottom wall
            my_rect= pg.Rect((i*self.tile_size, 600 + self.offset) , [self.tile_size,self.tile_size] )
            pg.draw.rect(self.gameDisplay, Colors.maroon, my_rect)
            pg.draw.line(self.gameDisplay, Colors.firebrick, (i*self.tile_size, 600 + self.offset + 3), (i*10+3, 600 + self.offset))       
            pg.draw.line(self.gameDisplay, Colors.firebrick, (i*self.tile_size, 600 + self.offset +
                10), (i*self.tile_size+10, 600 + self.offset)) 
            pg.draw.line(self.gameDisplay, Colors.firebrick, (i*self.tile_size+7, 600 + self.offset
                + 10), (i*self.tile_size+10, 600 + self.offset + 7))

Now it is time to draw the snake! In this method, we will take the directions into account when drawing the snake. The integers stand for the pixels.

    def draw_snake(self):

        t= self.snake.get_tiles()
        d= self.snake.get_directions()
        head_tile = [ t[0].x *10, t[0].y *10 + self.offset]
        if d == snake.Direction.UP:
            head = [ [head_tile[0], head_tile[1] +10] ]
            head.append( [head_tile[0] + 10, head_tile[1] +10]  )
            head.append( [head_tile[0]+ 5, head_tile[1] ] )
            pg.draw.polygon( self.gameDisplay, Colors.white, head)
        if d == snake.Direction.DOWN:
            head = [ [head_tile[0], head_tile[1]] ]
            head.append( [head_tile[0] + 10, head_tile[1]]  )
            head.append( [head_tile[0]+ 5, head_tile[1]+ 10])
            pg.draw.polygon( self.gameDisplay, Colors.white, head)
        if d == snake.Direction.LEFT:
            head = [ [head_tile[0] +10, head_tile[1]] ]
            head.append( [head_tile[0] + 10, head_tile[1] +10]  )
            head.append( [head_tile[0], head_tile[1]+5]  )
            pg.draw.polygon( self.gameDisplay, Colors.white, head)
        if d == snake.Direction.RIGHT:
            head = [ [head_tile[0], head_tile[1]] ]
            head.append( [head_tile[0], head_tile[1] +10]  )
            head.append( [head_tile[0]+ 10, head_tile[1]+5])
            pg.draw.polygon( self.gameDisplay, Colors.white, head)

        for i in range(1, len(t)):
            my_rect= pg.Rect((t[i].x *10, t[i].y *10 + self.offset), [10,10] )
            pg.draw.rect(self.gameDisplay, Colors.white, my_rect)

The next method generates the food randomly.

    ################################
    #  Game Logic                  #
    ################################
    def generate_food(self):
        """
        Generates a new piece of food randomly on the map.
        Food will not appear on the snake.
        Will only produce food if there are less than max_food in the map.
        """
        if len(self.food) >= self.max_food:
            return 
        while 1:
            x = randint(0, 79)
            y = randint(0, 59)
            p = Point(x, y)
            if not self.snake.point_in_snake(p):
                self.food.append(snake.Food(p))
                return True 

The next method checks if the snake is on a food tile. If so, the score increases by the food value and the food eaten is popped from the list of food. we use the enumerate function to check if we landed on one of the multiple food.

    def check_food(self):
        """ Check if the snake landed on a food tile and can eat!"""
        head_pos = self.snake.get_head_pos()
        for i, food in  enumerate(self.food): 
            if head_pos == food.pos: 
                self.snake.eat()
                self.score += food.value
                self.food.pop(i)
                return 

The next method handles the keyboard events. Here the directions we set in the beginning are helpful.

    def evaluate_key(self, event):
        """
        Handle all keyboard events.
        Currently only doing things for the arrow keys
        """
        if event.key == pg.K_DOWN: 
            self.snake.change_direction(snake.Direction.DOWN)
        if event.key == pg.K_UP: 
            self.snake.change_direction(snake.Direction.UP)
        if event.key == pg.K_LEFT: 
            self.snake.change_direction(snake.Direction.LEFT)
        if event.key == pg.K_RIGHT: 
            self.snake.change_direction(snake.Direction.RIGHT)

We need the update function to keep the game running. We set the ticks to 100 to reach a middle speed, but feel free to experiment with it. Consider increasing it in case you want to have a more advanced version.

    ###############################################
    # Update function                             #
    ###############################################

    def run(self):
        running = True
        # intialise timer for food spawning and movement
        food_timer = pg.time.get_ticks()
        snake_timer = pg.time.get_ticks()

        while running: 
            events = pg.event.get()
            for event in events:
                if event.type == pg.KEYDOWN: 
                    self.evaluate_key(event)
                if event.type == pg.QUIT:
                    running = False

            timer = pg.time.get_ticks()
            if timer - snake_timer > 40: # check if 40 ms since last move passed
                running = self.snake.move()
                self.check_food()
                snake_timer = timer

            if timer - food_timer > 2000: # generating  food every 2000ms
                self.generate_food()
                food_timer = timer

            self.clock.tick(100) # limits the game to 100 frames a second

            self.draw_all()
        pg.quit()

The Last Stage

Create a new file named “main.py”. This is the file which is actually going to run your game. You can delete the last 2 lines from the game.py. The file should include only the following:

import game

g = game.Game()
g.run()