Initial Snake Prototype: Part 1

The main goal of the first part of this instruction is to get familiar with the Pygame environment and functions. After the first half there will be lots of code refactoring to get actual snake behavior, but the first half is to get used to coding with Pygame.

Now that we have our environment set up we can go ahead and start creating our Snake. If you look at the typical Pygame loop, it looks something like this:

import pygame
pygame.init()
done = False
clock = pygame.time.Clock()
screen = pygame.display.set_mode((800, 600))
while not done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done =  True

    pygame.display.flip() #update display
    clock.tick(60)
                  

At first, some variables are set from the Pygame library. We get a copy of the internal clock and the screen object with a width of 800px and a height of 600px. The while loop will check if the game is done then iterate x times every second until the end of the game. Here, the x represents the parameter passed to the clock.tick() function. In this case, every 60 seconds the game will check for a Pygame event, check if it is the user wanting to quit, then if not it will update the display.

This will be the basic outline from which I create the Snake game. Currently, only a blank screen will pop up.

Step one is to create a Game class to encapsulate the game. The game should have a done instance variable, and a main game method.

import pygame
class Game():
    def __init__(self, fps, width, height):
        pygame.init()
        self.done = False
        self.fps = fps
        self.clock = pygame.time.Clock()
        self.screen = pygame.display.set_mode((width, height))
        
    def play(self):
        while not self.done:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.done =  True

            pygame.display.flip() 
            self.clock.tick(self.fps)

def main():
    game = Game(60, 800, 600)
    game.play()

if __name__ == "__main__":
    main()
                  

Here is a basic outline of the class. It has one parameter passed into it, fps which is passed into the internal clock of the game, retrieved from pygame.time.clock and stored in self.clock. This class will continually be updated as we add members to the game.

We also need a main method in order to create our game and play it, that’s what the last two 6 lines are doing.

Now we can move on to the actual Snake. Let’s start with getting a square on the screen. All of these changed will take place in the play method.

while not self.done:
    #for loop
    pygame.draw.rect(self.screen, (124, 252, 0), pygame.Rect(400, 300, 30, 30))
    pygame.display.flip()
    self.clock.tick(self.fps)
#rest of the code
                  

Running this, you should end up with a green square on your screen like below.

What is happening is we are telling Pygame we want to draw a rectangle with the rgb values (124, 252, 0) onto the screen. The rectangle will be at coordinates x -- 400, x -- 300, size of 30x30 or square.

We don’t have any movement yet, it’s just the same square being redrawn over and over again. Let’s bring this out into a class for the snake and add some movement.

class Snake(pygame.rect):
    color = (124, 252, 0)

    def __init__(self):
        self.size = 30
        self.x = 400
        self.y = 300

    def move(self):
        self.x += 3
                  

Adding this into the main game loop we get a total script of

import pygame
    class Game():
        def __init__(self, fps, width, height):
            pygame.init()
            self.done = False
            self.fps = fps
            self.clock = pygame.time.Clock()
            self.screen = pygame.display.set_mode((width, height))
            self.snake = Snake()
            
        def play(self):
            while not self.done:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        self.done =  True
                        
                self.snake.move()
                pygame.draw.rect(self.screen, Snake.color, self.snake)
                pygame.display.flip() 
                self.clock.tick(self.fps)
    
    class Snake(pygame.Rect):
        color = (124, 252, 0)
    
        def __init__(self):
            self.size = 30
            self.x = 400
            self.y = 300
    
        def move(self):
            self.x += 3 #move across the x axis 3 pixels per frame
    
    def main():
        game = Game(60, 800, 600)
        game.play()
    
    if __name__ == "__main__":
        main()
                  

This code should lead to something like this.

You may notice that something isn’t quite right. This is because we aren’t overwriting the previous place of the square. The quick fix is to refill the screen as black every iteration. Adding the following line should fix this.
self.screen.fill((0, 0, 0)) #value for black
This just needs to be placed inside the loop BEFORE the other objects are drawn.

Perfect. Now let’s add the ability to control the direction of the snake.

First we should define direction. I decided on defining it as an enumeration

from enum import Enum
    class Direction(Enum):
        UP = [0, -3]
        DOWN = [0, 3]
        LEFT = [-3, 0]
        RIGHT = [3, 0]
                  

Next, we should define user input. Pygame has an event KEYDOWN that fires when a key is pressed. The key is held in the event variable from the for loop in the main game loop. First, let’s make a function for the snake that takes in a Direction and updates it’s current direction. We’ll also add a Direction instance variable.

#In class Snake
def changeDirection(self, newDirection):
    self.direction = newDirection

def move(self):
    self.x += self.direction.value[0]
    self.y += self.direction.value[1]
                  

That was simple. Now let’s write a function that takes a key and adjusts the direction of the snake accordingly.

#In class Game
def userInput(self, key):
    if key == pygame.K_UP:
        self.snake.changeDirection(Direction.UP)
    elif key == pygame.K_DOWN:
        self.snake.changeDirection(Direction.DOWN)
    elif key == pygame.K_LEFT:
        self.snake.changeDirection(Direction.LEFT)
    elif key == pygame.K_RIGHT:
        self.snake.changeDirection(Direction.RIGHT)
    else:
        return
                  

Finally, we add the code in the game loop to call the functions we have created.

#In play()
while not self.done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            self.done = True

        if event.type == pygame.KEYDOWN:
            self.userInput(event.key)
    #Rest of the function
                  

We have movement! We’re starting to get a snake game.

Now we can get to work on the food that the snake will eat. The food will be similar to the snake, it will be a rectangle, and will be able to relocate once it is eaten, and it will have a different color – pink.

class Food(pygame.Rect):
    color = (255, 0, 100)

    def __init__(self, size, game):
        self.width = size
        self.height = size
        self.relocate()

    def relocate(self):
        self.x, self.y = randint(0, 800), randint(0, 600) #from random import randint
        if not self.isSafe():
            self.relocate()

    def isSafe(self):
        if self.colliderect(self.game.snake):
            return False
        else:
            return True
                  

Now we can define a basic eating method for the snake, when the snake hits the food, the food should relocate.

#In Snake
def checkEat(self):
    if self.colliderect(self.game.food):
        self.game.food.relocate()
                  

Now we have to call checkEat() in the main game loop, right after we move. We also have to draw the food on the screen.

So far we have

import pygame
from enum import Enum
from random import randint


class Game():
    def __init__(self, fps, width, height):
        pygame.init()
        self.done = False
        self.fps = fps
        self.clock = pygame.time.Clock()
        self.screen = pygame.display.set_mode((width, height))
        self.snake = Snake(self, 30)
        self.food = Food(self, 30)

    def play(self):
        while not self.done:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.done = True

                if event.type == pygame.KEYDOWN:
                    self.userInput(event.key)

            self.snake.move()
            self.snake.checkEat()
            self.screen.fill((0, 0, 0))
            pygame.draw.rect(self.screen, Snake.color, self.snake)
            pygame.draw.rect(self.screen, Food.color, self.food)
            pygame.display.flip()
            self.clock.tick(self.fps)

    def userInput(self, key):
        if key == pygame.K_UP:
            self.snake.changeDirection(Direction.UP)
        elif key == pygame.K_DOWN:
            self.snake.changeDirection(Direction.DOWN)
        elif key == pygame.K_LEFT:
            self.snake.changeDirection(Direction.LEFT)
        elif key == pygame.K_RIGHT:
            self.snake.changeDirection(Direction.RIGHT)
        else:
            return


class Direction(Enum):
    UP = [0, -3]
    DOWN = [0, 3]
    LEFT = [-3, 0]
    RIGHT = [3, 0]


class Snake(pygame.Rect):
    color = (124, 252, 0)

    def __init__(self, game, size):
        self.x = 400
        self.y = 300
        self.height = size
        self.width = size
        self.direction = Direction.UP
        self.game = game

    def move(self):
        self.x += self.direction.value[0]
        self.y += self.direction.value[1]

    def changeDirection(self, newDirection):
        self.direction = newDirection

    def checkEat(self):
        if self.colliderect(self.game.food):
            self.game.food.relocate()


class Food(pygame.Rect):
    color = (255, 0, 100)

    def __init__(self, game, size):
        self.width = size
        self.height = size
        self.game = game
        self.relocate()

    def relocate(self):
        self.x, self.y = randint(0, 800), randint(0, 600)
        if not self.isSafe():
            self.relocate()

    def isSafe(self):
        if self.colliderect(self.game.snake):
            return False
        else:
            return True


def main():
    game = Game(60, 800, 600)
    game.play()


if __name__ == "__main__":
    main()
                  

The above code leads to

However, we have a few issues with the design. The food spawns outside of the display sometimes, the snake can go outside of the board, the snake moves in continuous steps and not in blocks, and the snake doesn’t grow.

Let’s tackle one issue at a time, starting with moving in blocks. In order to split the window into a grid, we need to divide the width and height of the window into equal size blocks. Our width is 800 and our height is 600. If we divide each by 40 we will get our rows and columns of the grid. This will involve changes to the sizes of the squares, the Direction enum and the relocate method to align the food on the grid.

class Direction(enum):
    UP = [0, -40]
    DOWN = [0, 40]
    LEFT = [-40, 0]
    RIGHT = [40, 0]

#Later in Food class...
def relocate(self):
    cols = floor(self.game.screen.get_size()[0]/self.width - 1) #from math import floor
    rows = floor(self.game.screen.get_size()[1]/self.width - 1)
    #Note, the -1 is because we don't want the food to be drawn at the edge column because 
    #shapes are drawn from the top left meaning if the top left was at the edge of the screen
    #it would be drawn off the screen

    self.x, self.y = randint(0, cols) * self.width, randint(0, rows) * self.width

    if not self.isSafe():
        relocate()

#In __init__ of Snake, to start the snake in the middle of the screen
self.x = floor((pygame.display.get_window_size()[0]/size -1)/2) * size
self.y = floor((pygame.display.get_window_size()[1]/size -1)/2) * size

#In __init__ of Game
self.snake = Snake(self, 40)
self.food = Food(self, 40)
        

Now we have a new problem.

The snake is moving entirely too fast. That is because every frame of the game (60 times per second) the snake is moving by 40 pixels. The snake starts at y = 300 and is going up, meaning after 15 iterations the snake will be at the top of the screen. That should take 1/4 of a second at 60 fps. In reality, we would rather the snake only move once per second. In order to control this we can add in a variable and a check in our main game loop.

#in play()
timer = 0
speed = 10
while not self.done:
    #for loop

    if timer * speed > 1:
        timer = 0
        self.snake.move()

    #draw
    
    timer += self.clock.tick(self.fps) / 1000 #returns number of miliseconds since last call
                  

Now we’re looking good. We just need to add walls and have the snake grow. The walls are going to be a simple check if the snake is trying to go past 0 on either axis or past the window width on the x axis or height on the y axis.

#In Snake class
    def hitWall(self):
        if (self.x < 0 or self.y < 0 
        or self.x > pygame.display.get_window_size()[0] - self.width 
        or self.y > pygame.display.get_window_size()[1] - self.height
        ):
            #Push back
            self.x -= self.direction.value[0]
            self.y -= self.direction.value[1]
    
    #Editing move
    def move(self):
        self.x +=  self.direction.value[0]
        self.y +=  self.direction.value[1]
        self.hitWall()
                  

The only thing left for our basic snake is for the tail to grow. For this, we will create a new class for our rectangles. We could refactor all of our current squares into this class as well.

class Block(pygame.Rect):
                      color = (0, 128, 255) #blue
                      def __init__(self, size, x, y):
                          self.width = size
                          self.height = size
                          self.x = x
                          self.y = y
                          self.dim = size #faster way to look up without a confusing name
                  

The init of our Snake and Food classes would change

class Snake(Block): #Was class Snake(pygame.Rect)
                      def __init__(self, game, size):
                          Block.__init__(self, size,
                              floor((pygame.display.get_window_size()
                                  [0]/size -  1)/2) * size,
                              floor((pygame.display.get_window_size()
                                  [1]/size -  1)/2) * size)
                          self.tail = [] #Init our tail, will be a list of Blocks
                          #rest of the code
                  
                    #Later...
                  
class Food(Block): #Was class Food(pygame.Rect)
    def __init__(self):
        Block.__init__(self, size, 0, 0)
        #rest of the code
                  

Don’t forget to replace any code mentioning self.width or self.height, because size is clearer.

Now we need an eat method. We already check if the squares collide, now we can add a method that grows the snake.

#In Snake
                  def growTail(self):
                      self.tail.append(Block(self.dim, self.x, self.y))
                  
                  #Calling the above from the checkEat() method
                  def checkEat(self):
                      if  self.colliderect(self.game.food):
                          self.game.food.relocate()
                          self.growTail()
                  

We also can’t forget to draw the tail, we just go back to our main game loop where we draw everything else

#in while loop
                  for block in self.snake.tail:
                      pygame.draw.rect(self.screen, Block.color, block)
                  

Almost, but the tail doesn’t follow the snake. There are a few ways to deal with this issue. I decided to shift the list of the tail to the left and let the first element fall off. Then add a new Block to the tail that has the previous spot of the head.

#In Snake
                  def move(self):
                  
                      if self.tail:
                          self.tail.append(Block(self.dim, self.x, self.y))
                          self.tail = self.tail[1:]
                  
                      self.x += self.direction.value[0]
                      self.y += self.direction.value[1]
                  
                      self.checkEat()
                      self.hitWall()
                  

Looks pretty good, but there are a couple more issues that we’ll focus on tackling next time. These include the snake being able to go back into itself, and it can’t die.