Snake Game with Pygame

This lesson will teach you how to make a snake game with the Pygame module

Snake & Pygame Basics



    The projects we have completed so far have involved a variety of different data science and AI methods.

    For this project, we're going to create a single-player game that can either be played by a person or by an AI.

    We will be using the pygame module, which has a lot of features that make it easier to create games, such as:

    1. An easy way to draw objects on the screen
    2. An event system to handle player inputs
    3. Methods to continually update the screen and handle framerates

    The game that we will be making is called Snake. You control a snake that moves around a board eating Fruit, which are placed on the game world randomly.

    Every time you eat a fruit, the snake grows longer, and you get a point. The objective of the game is to grow as long as possible without running into either the walls or yourself.

    The game starts off easy, since the snake is very short, but gets harder as you progress, since your tail grows larger and larger.

    This video displays the game in action.



    Before we go too in-depth making the game mechanics, we'll start by creating a simple menu in pygame to understand the basics of pygame.

Creating a Menu



    Start by creating a new project called Snake, and inside that project create a new file called game.py. You can remove the default text there.

    The project will be very important for creating this game, since we will be creating a lot of different classes, and using different objects to do so.

    Whenever we create a new file, we will try to keep the file name consistent with the major class that is in the file.

    To get started with using pygame, we will do the following:

    1. Create a class to hold all our game information, called Game. Right now it will take two inputs to initialize, the screen width and screen height.
    2. Initialize pygame using pygame.init() and add header text to our window using pygame.display.set_caption()
    3. Set the width and height of our pygame window using pygame.display.set_mode()
    4. Wait a little bit using pygame.time.wait()
    5. Close out of the pygame window using pygame.quit() and sys.exit()

    This will create a window, and then close it automatically afterwards. If you try to press the X button to close out of the game, that won't work yet, since we haven't yet enabled quitting out of the game through pygame's functionality.

    import pygame
    import sys
    
    class Game:
    
        pygame.init()
        pygame.display.set_caption("Snake Game")
    
        def __init__(self, screen_width, screen_height):
     
            self.screen = pygame.display.set_mode((screen_width, screen_height))
            pygame.time.wait(5000)
            self.quit_game()
    
        def quit_game(self):
            pygame.quit()
            sys.exit()      
            
    Game(300, 300)
          


    Right now if you run game.py, the window will open for five seconds (as specified by the argument for wait() being 5000 milliseconds), then it will close while calling the quit_game() method.

    We initialize the Game class at the bottom of the file, which will start running the game.

    The game, when run, should look like this:



    Next, we're going to add more interactivity to our pygame screen by adding buttons that will allow players to start and exit the game.

    1. We'll add a way to make the game continually update using a while True: loop, the pygame.display.update() function, and the pygame.time.Clock().tick() function.
    2. Then, we'll add button visuals by using the pygame.Rect() and pygame.draw.rect() functions to draw buttons on the screen
    3. To add text to the buttons, we'll use the pygame.font.SysFont() function, the font.render() function, and the screen.blit() functions
    4. Lastly, we'll set up actions to occur when the buttons get clicked using the pygame event system and the collidepoint() function

    There will be an additional input for our Game object, frames per second (fps). Most games run at 30-60 fps, but for our snake game we'll run it at 5 fps.

    By using a low fps, we will give players a lot of time to make their decisions. When we get to making the game, we will get to try different fps values to see how the game feels to play.

        pygame.display.set_caption("Snake Game")
    
        def __init__(self, fps, screen_width, screen_height):
     
            self.screen = pygame.display.set_mode((screen_width, screen_height))
            self.fps = fps
            
            while True:
                self.start_screen()
        
        def start_screen(self):
    
            font = pygame.font.SysFont("Arial", 25)
    
            button_start = pygame.Rect(60, 130, 80, 40)
            pygame.draw.rect(self.screen, [0, 255, 0], button_start)
            start_text = font.render('Start', True, (0, 0, 0))
            self.screen.blit(start_text, (60, 130)) 
    
            button_quit = pygame.Rect(160, 130, 80, 40)
            pygame.draw.rect(self.screen, [255, 0, 0], button_quit)
            quit_text = font.render('Quit', True, (0, 0, 0))
            self.screen.blit(quit_text, (160, 130))
            
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONDOWN:
                    mouse_position = event.pos
                    if button_start.collidepoint(mouse_position):
                        print("Start button was pressed")
                    if button_quit.collidepoint(mouse_position):
                        print("Quit button was pressed")
                        self.quit_game()
                if event.type == pygame.QUIT:
                    self.quit_game()
            
            pygame.display.update()
            pygame.time.Clock().tick(self.fps)
    
        def quit_game(self):
            pygame.quit()
            sys.exit()      
            
    Game(5, 300, 300)
    				




    Each of the different parts of the code has an important role in setting up the start screen, which is defined in the start_screen function. When we make the game, we will a different function for each different screen of the game.

    These are the lines of code in the

    button_start = pygame.Rect(60, 130, 80, 40)
    When creating a rectangle in pygame (rect), the four variables are the x position, the y position, the width, and the height.

    pygame.draw.rect(self.screen, [0, 255, 0], button_start)
    When drawing a rect, you need to specify the screen to draw it on, the color of the rect in rgb format (red, green, blue), and the position of the button, as defined in the rect we already made.

    font = pygame.font.SysFont("Arial", 25)
    In order to draw text on the screen, we need to specify the font we will use and the size of the text. SysFont takes in two arguments, the font name and font size.

    start_text = font.render('Start', True, (0, 0, 0))
    The three arguments for font.render() are the text to display as a string, whether to use anti-aliasing to make the text look smoother, and an rgb value for the font color.

    self.screen.blit(start_text, (60, 130))
    The blit method takes the rendered text and actually places it on the screen, at the x and y position designated by the tuple. You must call the blit function on a screen object.

    for event in pygame.event.get():
    pygame.event.get() returns all the ways the player could interact with the game, as well as some other events. If you need to check whether the player pressed a key or clicked on the screen, you will need to check for it inside this events list.

    if event.type == pygame.MOUSEBUTTONDOWN:
    The pygame.MOUSEBUTTONDOWN event indicates when the player clicks the screen

    if button_start.collidepoint(mouse_position):
    The collidepoint() method checks to see if the mouse position is inside of the rect's box. In this line of code, we check if it's inside the start button's box.

    if button_quit.collidepoint(mouse_position):
    When the player presses the quit button, they will exit the game.

    if event.type == pygame.QUIT:
    If the player presses the exit button at the top right, the game will quit

    pygame.display.update()
    We need to call update() at the end to update all the images we have drawn on the screen. During the running of this screen, they might have moved or change, and that change needs to be displayed to the player.

    pygame.time.Clock().tick(self.fps)
    Lastly, we will wait to perform the next update, rather than trying to update continually.



    Try playing your game now. When you press the Start button, a message will display to console, but nothing else will happen.

    When you click the Quit button, the game will exit. Note: in order to be able to use the exit button to quit our game, pygame requires you to code that functionality in specifically. If you do not add that functionality inside of your code, the player will not be able to close the game.



    Now, it's time to talk about how we're going to set up our game architecture.

The Pixel-Based Environment



    In the game of snake, there are discrete squares where you will have one of a snake piece, a fruit, a wall, or a blank square. If we were to use individual pixels in pygame to represent each of the game elements, it would be much too small to see!

    This creates a problem: how do we scale up the individual squares so that players can see them? We'll solve this problem by using Normalized Points.

    A normalized point means that inside of our data structure, we'll talk about the snake being at the point (1,3) or (4,6) or a similar x and y value. But when it's time to draw the snake on the screen, we'll be drawing a large rect that covers many actual pixels.

    For now, we're going to focus on the following:

    1. Setting up a file to manage our constant values
    2. Showing our game play screen with a header bar
    2. Taking in basic keyboard input
    3. Creating necessary helper classes to set up our normalized points system

    First, we'll set up a class that will just hold various constants that we can use in different files.

    Create a new python file called constants.py in your project.

    This file will hold some basic values that will be helpful for us to keep track of that will be consistent for the entire time the player plays the game.
    class Constants:
        
        FPS = 5
        PIXEL_SIZE = 25
        SCREEN_WIDTH = 300
        SCREEN_HEIGHT = 300
        ENV_HEIGHT = SCREEN_HEIGHT/PIXEL_SIZE
        ENV_WIDTH = SCREEN_WIDTH/PIXEL_SIZE
        NAVIGATION_BAR_HEIGHT = 30
    				  




    By placing variables inside of constants, we can access them from any file that needs the information, and ensure that every file has the exact same value for information such as the pixel size for the game.

    FPS = 5
    The number of frames to display per second

    PIXEL_SIZE = 25
    How large each of our pixels of our game world are going to be

    SCREEN_WIDTH = 300
    SCREEN_HEIGHT = 300
    The width and height of the screen, in pixels

    ENV_HEIGHT = SCREEN_HEIGHT/PIXEL_SIZE
    ENV_WIDTH = SCREEN_WIDTH/PIXEL_SIZE
    How many squares high and across our game world is going to be (12 squares)

    NAVIGATION_BAR_HEIGHT = 30
    How large our top navigation bar will be, in pixels



    Now that we have a Constants class, we can import it into our game.py file and use those variables wherever we need them.

    At the top of the file, import the class.
    import pygame
    import sys
    from constants import Constants
    
    class Game:
    				  


    At the bottom of the file, where we initialize the Game class, use our constants instead of hard-coding the input values
        def quit_game(self):
            pygame.quit()
            sys.exit()      
            
    Game(Constants.FPS, Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT+Constants.NAVIGATION_BAR_HEIGHT)
    				  


    Having this constants file is going to be important when we start defining our drawing methods. Next, we're going to set up our play screen.

    To manage our play screen and our start screen, we're going to do the following:

    1. Set up a variable to track whether the game is playing or not, and some additional class variables related to the space of our screen
    2. Create a new method that will handle drawing to the screen while the game is playing and add the ability to quit the game when it is playing by pressing a key
    3. Draw the navigation bar on the screen

    First, we'll add our variables that are important for our new game play screen and set up the logic that will switch screens.
        def __init__(self, fps, screen_width, screen_height):
     
            self.screen = pygame.display.set_mode((screen_width, screen_height))
            self.fps = fps
            self.pixel_size = Constants.PIXEL_SIZE
            self.navigation_bar_height = Constants.NAVIGATION_BAR_HEIGHT
            self.horizontal_pixels = int(screen_width / self.pixel_size)
            self.vertical_pixels = int((screen_height-self.navigation_bar_height) / self.pixel_size)
            self.game_started = False
            
            while True:
                if self.game_started:
                    self.play_screen()
                else:
                    self.start_screen()
    				  

    Our while True loop will be pointing to different functions based on whether or not the game has started.

    Next, we'll set up the method for the screen while the game is playing. We'll also need to change the game_started boolean when the player clicks the Start button.
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONDOWN:
                    mouse_position = event.pos
                    if button_start.collidepoint(mouse_position):
                        print("Start button was pressed")
                        self.game_started = True
                    if button_quit.collidepoint(mouse_position):
                        print("Quit button was pressed")
                        self.quit_game()
                if event.type == pygame.QUIT:
                    self.quit_game()
            pygame.display.update()
            pygame.time.Clock().tick(self.fps)
    
        def quit_game(self):
            pygame.quit()
            sys.exit()      
            
        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.quit_game()
                if event.type == pygame.QUIT:
                    self.quit_game()
            pygame.time.Clock().tick(self.fps)
            self.draw_screen()
            pygame.display.update()
            
        def draw_screen(self):
            self.screen.fill((255, 255, 255))
    				  


    You can try to play the game now. It is pretty basic, as soon as you press the Start button, it changes to a completely white screen.

    When you press any key on the keyboard, or click the close button at the top right, the game will exit. Remember, for each screen you create, you have to specifically code the game to exit when the close button at the top-right is pressed.



    Next, we'll add the navigation bar to the start screen. This is where our pixel drawing method will be used, so we'll need to set up a special class used to handle points.

    Create a new file called point.py. We'll create a class that is very simple, it is a representation of 2 points. This is a very reusable piece of code.

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


    Next, to make our lives easier when it comes to colors, we'll set up another class to handle constants related to colors. Create a new file called color.py and create the Color class.

    class Color:
        black = (0, 0, 0)
        red = (255, 0, 0)
        green = (0, 150, 0)
        white = (255, 255, 255)
        gray = (211, 211, 211)
    				  


    With those classes out of the way, we can get to setting up our drawing helper methods. Don't forget to add the imports for the new classes in the game.py file.
    import pygame
    import sys
    from constants import Constants
    from point import Point
    from color import Color
    				  


    The draw_pixel() method we will set up will take in 3 arguments.

    1. The screen, since we need an object to draw the pixel on
    2. The color, since our screen will have pixels of different colors.
    3. The point, so that we know where to place our pixel inside of the screen

    The method will set up a rect, and have pygame draw that rect on the screen.

    Then, we'll add to the draw_screen method to draw pixels on the top of our screen for the navigation bar.

        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.quit_game()
                if event.type == pygame.QUIT:
                    self.quit_game()
            pygame.time.Clock().tick(self.fps)
            self.draw_screen()
            pygame.display.update()
            
        def draw_pixel(self, screen, color, point):
            rect = pygame.Rect(point.x, point.y, self.pixel_size, self.pixel_size)
            pygame.draw.rect(screen, color, rect)
            
        def draw_screen(self):
            self.screen.fill((255, 255, 255))
            
            for x in range(0, (self.horizontal_pixels*self.pixel_size)+1):
                self.draw_pixel(self.screen, Color.gray, Point(x, 0))
                for y in range(0, (self.navigation_bar_height-self.pixel_size)+1):
                    self.draw_pixel(self.screen, Color.gray, Point(x, y))
    				  


    When you click play, you'll now see the navigation bar. Now that we have that infrastructure out of the way, we can start to set up our game.



Additional System Setup



    Here, we'll focus on adding three new classes for our game:

    1. The Environment class will handle the structure of our game and where the objects are in the game world.
    2. The ScreenObject class will give us a way to create different objects that will be displayed on the screen
    3. The Tile class will represent the different types of objects in our world within a 2D array.

    We'll start with the Tile class, which is the simplest. Create a new file called tile.py and add the below class.

    class Tile():
        empty = " "
        snake = "x"
        fruit = "$"
        wall = "#"
    				  


    If we were creating a text-based game rather than a graphics-based game, we could use this to represent the world. But in our game, this format will be translated into the visuals that the game will display through other objects.

    Next, we're going to create the
    ScreenObject class, so that we have a way to translate the idea of tiles into the graphics.

    Create a file called objects.py, and add the following code to the file.
    class ScreenObject:
    
        points = []
    
        def __init__(self, game, color):
            self.game = game
            self.color = color
    
        def draw(self, screen):
            for point in self.points:
                self.game.draw_pixel(screen, self.color, point)
    				  


    Every ScreenObject will be a collection of points, and when it draws on the screen, it will draw pixels on the screen by using the game object.

    Now it's time for the Environment class. Create a new file called environment.py

    from constants import Constants
    from tile import Tile
    
    class Environment:
        
        def __init__(self, width=Constants.ENV_WIDTH, height=Constants.ENV_HEIGHT):
            self.width = width
            self.height = height
            self.tiles = []
            
            for y in range(0, self.height):
                self.tiles.append([])
                for x in range(0, self.width):
                    self.tiles[y].append(Tile.empty)
    				  


    On the setup of the environment, we will be populating the world with a list of tiles. Right now, all of the tiles are empty.

    We're done with our basic system setup. It's time to add the actual objects to our world.

    We don't have anything to see when we click play yet, but that will come in the next step.

Adding Walls



    The first object we're going to make is the walls that appear around the border of the play area.

    When the player's snake runs into these walls, they will lose the game.

    To add the walls, we're going to need to make adjustments to a few classes.

    1. In objects.py, we're going to add a child class using inheritance that will create the specific color that the walls will be.
    2. The Environment class will handle setting up the objects as tiles in the game world.
    3. The Game class will handle initial setup and drawing the objects on screen.

    Starting with objects.py, we'll add a new class that will inherit from the ScreenObject class.

    from color import Color
    
    class ScreenObject:
    
        points = []
    
        def __init__(self, game, color):
            self.game = game
            self.color = color
    
        def draw(self, screen):
            for point in self.points:
                self.game.draw_pixel(screen, self.color, point)
                
    class WallScreenObject(ScreenObject):
        def __init__(self, game):
            ScreenObject.__init__(self, game, Color.black)
    				  


    The WallScreenObject will initialize itself using the ScreenObject's initialization method, and set its color to black.

    If you want, you can change the walls to a different color other than black.

    Next, the Environment class initially set up a blank world. We're going to add two methods, one to set the edge tiles of the world to walls, and another to find out all the points we're going to need to draw the walls at.

    from constants import Constants
    from tile import Tile
    from point import Point
    
    class Environment:
    	
        def __init__(self, width=Constants.ENV_WIDTH, height=Constants.ENV_HEIGHT):
            self.width = width
            self.height = height
            self.tiles = []
            
            for y in range(0, self.height):
                self.tiles.append([])
                for x in range(0, self.width):
                    self.tiles[y].append(Tile.empty)
                    
        def set_wall(self):
            for y in range(0, self.height):
                for x in range(0, self.width):
                    if x == 0 or x == self.width-1 or y == 0 or y == self.height-1:
                        self.tiles[y][x] = Tile.wall
            self.wall = self.points_of(Tile.wall)
            return self.wall
        
        def points_of(self, environment_object):
            points = []
            for y in range(0, self.height):
                for x in range(0, self.width):
                    tile = self.tiles[y][x]
                    if tile == environment_object:
                        points.append(Point(x, y))
            return points
    				  


    The set_wall function will set the tiles on the edges of the screen to walls, and the points_of will return the points of all tiles of a particular type.

    Lastly, the Game class will need to keep track of the screen objects after they are set up, normalize the points it receives from the Environment class so we'll draw them in the correct place, and then draw those points on the screen.

    To start, we will make sure that we import the Environment class, as well as the WallScreenObject class we created. In the game.py file, add the following imports.

    import pygame
    import sys
    from constants import Constants
    from point import Point
    from color import Color
    from environment import Environment
    from objects import WallScreenObject
    
    class Game:
    				  


    Next, we'll create the normalization method to ensure that we draw our points on the correct spot on our screen.

    We need the X pixels to be spread across the x axis, and the y pixels to be below the navigation bar and spread across the y axis.

    For example, a tile point of (2, 5) would be normalized to (50, 155), and a tile point of (4,0) would be normalized to (100, 30). The y value has to take into account the height of the navigation bar.
        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.quit_game()
                if event.type == pygame.QUIT:
                    self.quit_game()
            pygame.time.Clock().tick(self.fps)
            self.draw_screen()
            pygame.display.update()
        
        def screen_normalized_point(self, point):
            return Point(point.x * self.pixel_size, self.navigation_bar_height + (point.y * self.pixel_size))
            
        def draw_pixel(self, screen, color, point):
            rect = pygame.Rect(point.x, point.y, self.pixel_size, self.pixel_size)
            pygame.draw.rect(screen, color, rect)
    				  


    Time to have the game set up the world when it starts. We'll add a new method that will specifically call the Environment class to set up the walls for the game.

    This method will be called as soon as the start button for the game is clicked.
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONDOWN:
                    mouse_position = event.pos
                    if button_start.collidepoint(mouse_position):
                        print("Start button was pressed")
                        self.game_started = True
                        self.setup_world()
                    if button_quit.collidepoint(mouse_position):
                        print("Quit button was pressed")
                        self.quit_game()
                if event.type == pygame.QUIT:
                    self.quit_game()
            
            pygame.display.update()
            pygame.time.Clock().tick(self.fps)
    
        def setup_world(self):
            self.screen_objects = []
            self.environment = Environment(width=self.horizontal_pixels,
                                           height=self.vertical_pixels)
    
            self.wall = WallScreenObject(self)
            self.wall.points = list([self.screen_normalized_point(x) for x in self.environment.set_wall()])
            self.screen_objects.append(self.wall)
            
    				  


    When the world is set up, a new Environment object is created, and all the places for the wall are added to the list of screen objects.

    Lastly, we need to actually draw the walls on the screen. We'll do this inside of the draw_screen() method.

        def draw_screen(self):
            self.screen.fill(Color.white)
            
            for game_object in self.screen_objects:
                game_object.draw(self.screen)
            
            for x in range(0, (self.horizontal_pixels*self.pixel_size)+1):
                self.draw_pixel(self.screen, Color.gray, Point(x, 0))
                for y in range(0, (self.navigation_bar_height-self.pixel_size)+1):
                    self.draw_pixel(self.screen, Color.gray, Point(x, y))
            
            
    Game(Constants.FPS, Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT+Constants.NAVIGATION_BAR_HEIGHT)
    				  


    We also set up the height of the screen to include the navigation bar.

    Now you can click play, start the game, and see the walls around your game space. Next, we're going to add the other objects to our world.



Adding the Snake



    The snake of our world is going to be another object like our walls. But we're going to need to do some additional steps to create it.

    1. First, we'll add a new ScreenObject class for our snake to make them the color green.

    2. Then, we'll set up a method in Environment to start the snake in a random position on the screen.
    3. Next, we'll set up the initial creation of the snake and make sure we can play again after the game is over.
    4. Finally, we'll track the snake in the Game class and draw it on the screen.

    First, let's open the objects.py file and add a new class for our snake.

    class WallScreenObject(ScreenObject):
        def __init__(self, game):
            ScreenObject.__init__(self, game, Color.black)
            
    class SnakeScreenObject(ScreenObject):
        def __init__(self, game):
            ScreenObject.__init__(self, game, Color.green)
    				  


    With a snake object set up, go to the Environment class.

    To set up a random position, make sure to import the random module at the top of the environment.py file.

    from constants import Constants
    from tile import Tile
    from point import Point
    import random
    
    class Environment:
    				  


    The method that we will create to get a random position will take in one argument, the distance from the edge. This is important because we don't want to spawn our snake on the edge of the level, that might be hard for the player to react to.

    To find out where to spawn the snake, this method will loop through the tiles randomly until it finds a tile that is not occupied already.

    Once it finds that tile, it will return that point. This method will be helpful later when we make our fruit spawn, too.

        def points_of(self, environment_object):
            points = []
            for y in range(0, self.height):
                for x in range(0, self.width):
                    tile = self.tiles[y][x]
                    if tile == environment_object:
                        points.append(Point(x, y))
            return points
        
        def random_available_position(self, distance_from_edge = 0):
            tile = None
            while tile is None or tile is not Tile.empty:
                random_x = random.randint(distance_from_edge, self.width-(distance_from_edge+1))
                random_y = random.randint(distance_from_edge, self.height-(distance_from_edge+1))
                tile = self.tiles[random_x][random_y]
            return Point(random_x, random_y)
    				  


    It's time to put the snake on the board. After getting a random empty position, we'll create the snake on that position inside of our set_snake() method.

        def random_available_position(self, distance_from_edge = 0):
            tile = None
            while tile is None or tile is not Tile.empty:
                random_x = random.randint(distance_from_edge, self.width-(distance_from_edge+1))
                random_y = random.randint(distance_from_edge, self.height-(distance_from_edge+1))
                tile = self.tiles[random_x][random_y]
            return Point(random_x, random_y)
        
        def set_snake(self):
            random_position = self.random_available_position(distance_from_edge = 3)
            self.tiles[random_position.x][random_position.y] = Tile.snake
            self.snake = self.points_of(Tile.snake)
            return self.snake
    				  


    In order to ensure that we can restart the game, we need to clear the board of all snake tiles every time the game restarts. We'll add a method to clear the board and run that method before we set up the snake.

        def set_snake(self):
            self.clear_environment_for(Tile.snake)
            random_position = self.random_available_position(distance_from_edge = 3)
            self.tiles[random_position.x][random_position.y] = Tile.snake
            self.snake = self.points_of(Tile.snake)
            return self.snake
        
        def clear_environment_for(self, environment_object):
            points_to_clear = self.points_of(environment_object)
            for point in points_to_clear:
                self.tiles[point.y][point.x] = Tile.empty
    				  


    Finally, it's time to add the setup inside of the Game class.

    Make sure to import the new SnakeScreenObject class we created.
    import pygame
    import sys
    from constants import Constants
    from point import Point
    from color import Color
    from environment import Environment
    from objects import WallScreenObject, SnakeScreenObject
    
    class Game:
    				  


    We will create the snake the exact same way that we created the wall inside the setup_world() method.

        def setup_world(self):
            self.screen_objects = []
            self.environment = Environment(width=self.horizontal_pixels,
                                           height=self.vertical_pixels)
    
            self.wall = WallScreenObject(self)
            self.wall.points = list([self.screen_normalized_point(x) for x in self.environment.set_wall()])
            self.screen_objects.append(self.wall)
            
            self.snake = SnakeScreenObject(self)
            self.snake.points = list([self.screen_normalized_point(x) for x in self.environment.set_snake()])
            self.screen_objects.append(self.snake)
    				  


    Try to play the game now. You'll see the snake spawn on the screen. If you exit and restart, every time you start, the snake will spawn in a different position.



Adding the Fruit



    The last object we need to add to the game is the fruit. Here's the steps we're going to follow; some of them may look familiar to you.

    1. Add a new ScreenObject for the fruit.
    2. Enable Environment to pick a place to set the position of the fruit.
    3. Set up the fruit inside of the Game class.

    We'll start with the objects.py file. We'll create a new class, but the color for this class will be red.
    class SnakeScreenObject(ScreenObject):
        def __init__(self, game):
            ScreenObject.__init__(self, game, Color.green)
            
    class FruitScreenObject(ScreenObject):
        def __init__(self, game):
            ScreenObject.__init__(self, game, Color.red)
    				  


    Next, inside of the Environment class file, we'll copy and paste most of the code related to our set_snake() to create our set_fruit() method. We won't specify a minimum distance from the edge. Fruit can spawn on the edge of the map.

        def set_snake(self):
            self.clear_environment_for(Tile.snake)
            random_position = self.random_available_position(distance_from_edge = 3)
            self.tiles[random_position.x][random_position.y] = Tile.snake
            self.snake = self.points_of(Tile.snake)
            return self.snake
    		
        def set_fruit(self):
            self.clear_environment_for(Tile.fruit)
            random_position = self.random_available_position()
            self.tiles[random_position.x][random_position.y] = Tile.fruit
            self.fruit = self.points_of(Tile.fruit)
            return self.fruit
    				  


    Next, import the FruitScreenObject class in the game.py file.

    import pygame
    import sys
    from constants import Constants
    from point import Point
    from color import Color
    from environment import Environment
    from objects import WallScreenObject, SnakeScreenObject, FruitScreenObject
    
    class Game:
    
    				  


    Lastly, add to the setup_world() method to spawn the fruit in the world.

        def setup_world(self):
            self.screen_objects = []
            self.environment = Environment(width=self.horizontal_pixels,
                                           height=self.vertical_pixels)
    
            self.wall = WallScreenObject(self)
            self.wall.points = list([self.screen_normalized_point(x) for x in self.environment.set_wall()])
            self.screen_objects.append(self.wall)
            
            self.snake = SnakeScreenObject(self)
            self.snake.points = list([self.screen_normalized_point(x) for x in self.environment.set_snake()])
            self.screen_objects.append(self.snake)
            
            self.fruit = FruitScreenObject(self)
            self.fruit.points = list([self.screen_normalized_point(x) for x in self.environment.set_fruit()])
            self.screen_objects.append(self.fruit)
    				  


    When you click play, you will now see the fruit and the snake spawn on the screen. Every time you restart, they will spawn in a different place.



Snake Movement



    We're finally getting into the action, letting the snake move around. Here's what we're going to do next:

    1. To allow our snake to move, we're going to create another class called Action related to handling the potential movement directions of our snake.
    2. We'll add an initial movement direction to our snake randomly.
    3. Every frame, we'll update the snake's position on the screen in the Environment class.
    4. In the Game class, we'll make sure that actions represent valid movements, and update the graphics for our game.

    Create a new file called action.py, and set up the class. This class has 4 tuples, which represent movement in the four different directions that the snake could move.

    In this class, we also create a static method by writing @staticmethod above the all() function. This allows us to use the all() method without creating an instance of the class.

    Being able to get a list of all the action directions will be helpful for picking random directions.

    class Action():
        left = (-1, 0)
        up = (0, -1)
        right = (1, 0)
        down = (0, 1)
        
        @staticmethod
        def all():
            return [Action.left, Action.up, Action.right, Action.down]
    				  


    Now that we have our action class, we'll update the environment.py file to import this new class. We'll also add a variable at the top of our class to track the snake's action.

    from constants import Constants
    from tile import Tile
    from point import Point
    import random
    from action import Action
    
    class Environment:
        
        snake_action = None
    	
    	def __init__(self, width=Constants.ENV_WIDTH, height=Constants.ENV_HEIGHT):
    				  


    Next, we'll update the set_snake() method to give our snake an initial action, based on a random choice from all available actions.

        def set_snake(self):
            self.clear_environment_for(Tile.snake)
            random_position = self.random_available_position(distance_from_edge = 3)
            self.tiles[random_position.x][random_position.y] = Tile.snake
            self.snake = self.points_of(Tile.snake)
            if self.snake_action is None:
                self.snake_action = random.choice(Action.all())
            return self.snake
    				  


    To change the snake's position, we're going to implement a method inside of Environment called step(). This method will run every single frame to update the game world.

    This method will have a boolean return value. If the method returns True, then the snake has moved to a valid location on this step and the game keeps playing.

    If the method returns False, then the game ends, because the snake hit either a wall or itself.

        def set_snake(self):
            self.clear_environment_for(Tile.snake)
            random_position = self.random_available_position(distance_from_edge = 3)
            self.tiles[random_position.x][random_position.y] = Tile.snake
            self.snake = self.points_of(Tile.snake)
            if self.snake_action is None:
                self.snake_action = random.choice(Action.all())
            return self.snake
        
        def step(self, action):
            self.snake_action = action
            head = self.snake[0]
            x, y = self.snake_action
            new = Point(x=(head.x + x),
                        y=(head.y + y))
            if new in self.snake:
                print("Hit Snake, game over!")
                return False
            elif new in self.wall:
                print("Hit Wall, game over!")
                return False
            else:
                self.snake.insert(0, new)
                self.tiles[new.y][new.x] = Tile.snake
                last = self.snake.pop()
                self.tiles[last.y][last.x] = Tile.empty
                return True
    				  


    The logic for moving the snake works like this: a snake is a list of points, and every time the snake moves, we create a new point at the snake's new location and put it at the front of the list. Then, we remove the last point of the snake from its list. We know where to add the point for the new head of the snake from the action object.

    Lastly, we need to update the Game class to display our changed world. To do this, we will add the following:

    1. We will import our new Action class into the Game class.
    2. We will call the Environment step method to update the game world.
    3. We will create a new method that will ensure the list of objects in Game stays in sync with the Environment

    Firstly, import the Action class into the game.py file
    import pygame
    import sys
    from constants import Constants
    from point import Point
    from color import Color
    from environment import Environment
    from objects import WallScreenObject, SnakeScreenObject, FruitScreenObject
    from action import Action
    
    class Game:
    				  


    Next, we're going to take the move action that we randomly started with, and apply that to the step() method. If that method ever returns false, we will set the game_started variable to False so we know the game is over.

        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.quit_game()
            pygame.time.Clock().tick(self.fps)
            move_action = self.environment.snake_action
            if (self.environment.step(move_action) == False):
                self.game_started = False
            self.draw_screen()
            pygame.display.update()
    				  


    To make sure that as the snake moves the screen updates, we'll need to sync with our environment on every update. The sync_screen_with_environment() method lets us do that, transforming the points from Environment into normalized points that can be displayed.

        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.quit_game()
            pygame.time.Clock().tick(self.fps)
            move_action = self.environment.snake_action
            if (self.environment.step(move_action) == False):
                self.game_started = False
            self.sync_screen_with_environment()
            self.draw_screen()
            pygame.display.update()
            
        def sync_screen_with_environment(self):
            self.fruit.points = list([self.screen_normalized_point(x) for x in self.environment.fruit])
            self.snake.points = list([self.screen_normalized_point(x) for x in self.environment.snake])
    				  


    Try to play the game, and you'll see that the snake travels until it hits a wall, then the Start and Quit buttons are displayed. You can now retry again and again, but without any control over the snake, you will only be able to watch it meet its demise. It's time to give power to the player to control the snake's destiny.



Player Controls



    To control the snake, we're going to use the arrow keys on the keyboard. Fortunately, all of these updates will be inside the Game class.

    1. We'll set up a separate method to handle user input
    2. We'll set up a move() method to prevent the player from trying to move backwards (they can only move left, right, or forwards)

    Before we set up user input, we'll need to import all of the keys we want to use to track user input. We're going to import the four arrow keys, as well as q, to let players quit while a game is still running.

    In addition, we'll set the initial action for the game to None. We'll overwrite this variable every time the player presses an arrow key.

    import pygame
    import sys
    from constants import Constants
    from point import Point
    from color import Color
    from environment import Environment
    from objects import WallScreenObject, SnakeScreenObject, FruitScreenObject
    from action import Action
    from pygame.locals import K_UP, K_DOWN, K_LEFT, K_RIGHT, K_q
    
    class Game:
    
        pygame.init()
        pygame.display.set_caption("Snake Game")
        action = None
    				  


    To take in user input, any time the player presses a key, we will call a method to handle the input.

    Each direction key press will correspond with a specific action. We'll set our stored action to that action, so that it can be processed as part of the step.

        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.user_input(event)
            pygame.time.Clock().tick(self.fps)
            move_action = self.environment.snake_action
            if (self.environment.step(move_action) == False):
                self.game_started = False
            self.sync_screen_with_environment()
            self.draw_screen()
            pygame.display.update()
            
        def user_input(self, event):
            if event.key == K_UP:
                self.action = Action.up
            elif event.key == K_DOWN:
                self.action = Action.down
            elif event.key == K_LEFT:
                self.action = Action.left
            elif event.key == K_RIGHT:
                self.action = Action.right
            elif event.key == K_q:
                self.quit_game()
    				  


    Before we pass the input into the step, we will put it through the move() method to ensure that it is a valid move.

        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.user_input(event)
            pygame.time.Clock().tick(self.fps)
            move_action = self.move(self.environment)
            if (self.environment.step(move_action) == False):
                self.game_started = False
            self.sync_screen_with_environment()
            self.draw_screen()
            pygame.display.update()
            
        def move(self, environment):
            if (self.action == None):
                return environment.snake_action
            backward_action = (self.action[0] == environment.snake_action[0] * -1) or (self.action[1] == environment.snake_action[1] * -1)
            return environment.snake_action if backward_action else self.action
    				  


    If the player tries to make a backwards action, we ignore that input from the player.

    When you play the game, you will now be able to use the arrow keys to move around. You can travel through the fruit, but not eat it yet.



Eating the Fruit



    We're almost done with the game, just a little bit more to go! Whenever the snake passes over a fruit square, it should eat the fruit and grow longer. We'll make two small changes to make this happen:

    1. Track the snake's length
    2. Check to see if the snake's head is over a fruit

    First, we'll initialize a variable in the Environment class checking to track the length of the snake.
    from constants import Constants
    from tile import Tile
    from point import Point
    import random
    from action import Action
    
    class Environment:
        
        snake_action = None
        snake_length = 1
    				  


    Next, we'll create a method called reward to get the value of the snake's length. This will be used to keep track of the player's score.

        def clear_environment_for(self, environment_object):
            points_to_clear = self.points_of(environment_object)
            for point in points_to_clear:
                self.tiles[point.y][point.x] = Tile.empty
                
        def reward(self):
            return self.snake_length
    				  


    To eat the fruit, we'll create the eat_fruit_if_possible() method. If the head of the snake is on the same tile as a fruit, then we increase the length of the snake and put the fruit in a new place.

        def reward(self):
            return self.snake_length
        
        def eat_fruit_if_possible(self):
            if self.fruit[0] == self.snake[0]:
                self.snake_length += 1
                self.set_fruit()
                return True
            return False
    				  


    In the step() method, we need to update our logic around removing the tail of the snake. We have a simple check here, if the length of the snake's points is greater than the length we're tracking through the snake_length variable, we remove the tail.

    Otherwise, we simply leave the tail where it is. Since we are inserting a new point every frame, the snake will automatically grow longer.

        def step(self, action):
            self.snake_action = action
            head = self.snake[0]
            x, y = self.snake_action
            new = Point(x=(head.x + x),
                        y=(head.y + y))
            if new in self.snake:
                print("Hit Snake, game over!")
                return False
            elif new in self.wall:
                print("Hit Wall, game over!")
                return False
            else:
                self.snake.insert(0, new)
                self.tiles[new.y][new.x] = Tile.snake
                if len(self.snake) > self.reward():
                    last = self.snake.pop()
                    self.tiles[last.y][last.x] = Tile.empty
                return True
    				  


    Lastly, we'll call this new method from the Game class in the play_screen() method before the next move action occurs.

        def play_screen(self):
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    self.user_input(event)
            pygame.time.Clock().tick(self.fps)
            self.environment.eat_fruit_if_possible()
            move_action = self.move(self.environment)
            if (self.environment.step(move_action) == False):
                self.game_started = False
            self.sync_screen_with_environment()
            self.draw_screen()
            pygame.display.update()
    				  


    Play the game now, and it's a fully playable snake game!



    You can still make additional tweaks to the gameplay and visuals, of course. But it will be a good idea to save those until after the AI lesson.

    The last thing we're going to do is show the current score of the player. That way, it will be easy to see how long of a snake you got while playing.

Showing Player Score



    To show the player's length on screen, we're going to add some text to the navigation bar. We'll add this inside of the draw_screen() method.

    We'll set the text on the left side of the screen in the top navigation bar.

        def draw_screen(self):
            self.screen.fill(Color.white)
            
            for game_object in self.screen_objects:
                game_object.draw(self.screen)
            
            for x in range(0, (self.horizontal_pixels*self.pixel_size)+1):
                self.draw_pixel(self.screen, Color.gray, Point(x, 0))
                for y in range(0, (self.navigation_bar_height-self.pixel_size)+1):
                    self.draw_pixel(self.screen, Color.gray, Point(x, y))
            
            font = pygame.font.SysFont("Arial", int(self.navigation_bar_height / 1.3))
            score_text = font.render(str(self.environment.reward()), True, Color.green)
            score_text_rect = score_text.get_rect()
            score_text_rect.center = (self.navigation_bar_height/2, self.navigation_bar_height/2)
            self.screen.blit(score_text, score_text_rect)
    				  


    Play the game now, and you'll be able to see how long of a snake you were able to get while you played.



Next: AI



    So you have a working game. There's a lot you can play around with, from the speed of the game to the size of the map, but ultimately you'd still be making a game for humans to play.

    Games are a great way to test out different AI strategies, and in the next lesson we'll change some of the code around to allow us to write our own AIs.

    Who knows? Maybe if they're good enough, they'll be able to beat your high score...