Initial Snake Prototype: Part 2

Previously, we had gotten the snake to move around correctly and eat apples correctly. However, we still have some work to do in ordre to get a functioning snake. Let’s add the ability for the snake to die. First, let’s get a refresh on the code we had last time.

import pygame
					from enum import Enum
					from random import randint
					from math import floor
					
					
					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, 40)
					        self.food = Food(self, 40)
					
					    def play(self):
					        timer = 0
					        speed = 10
					        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)
					
					            if timer * speed > 1:
					                timer = 0
					                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)
					
					            for block in self.snake.tail:
					                pygame.draw.rect(self.screen, Block.color, block)
					
					            pygame.display.flip()
					            timer += self.clock.tick(self.fps) / 1000
					
					    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 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
					
					
					class Direction(Enum):
					    UP = [0, -40]
					    DOWN = [0, 40]
					    LEFT = [-40, 0]
					    RIGHT = [40, 0]
					
					
					class Snake(Block):
					    color = (124, 252, 0)
					
					    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.direction = Direction.UP
					        self.game = game
					        self.tail = []
					
					    def move(self):
					        if self.tail:
					            self.tail = self.tail[1:]
					            self.tail.append(Block(self.dim, self.x, self.y))
					
					        self.x += self.direction.value[0]
					        self.y += self.direction.value[1]
					
					        self.checkEat()
					        self.hitWall()
					
					    def changeDirection(self, newDirection):
					        self.direction = newDirection
					
					    def checkEat(self):
					        if self.colliderect(self.game.food):
					            self.game.food.relocate()
					            self.growTail()
					
					    def growTail(self):
					        self.tail.append(Block(
					            self.dim, self.x - self.direction.value[0], self.y - self.direction.value[1]))
					
					    def hitWall(self):
					        if (self.x < 0 or self.y < 0
					                    or self.x > pygame.display.get_window_size()[0] - self.dim
					                    or self.y > pygame.display.get_window_size()[1] - self.dim
					                ):
					            # Push back
					            self.x -= self.direction.value[0]
					            self.y -= self.direction.value[1]
					
					
					class Food(Block):
					    color = (255, 0, 100)
					
					    def __init__(self, game, size):
					        Block.__init__(self, size, 0, 0)
					        self.game = game
					        self.relocate()
					
					    def relocate(self):
					        cols = floor(pygame.display.get_window_size()[0]/self.dim - 1)
					        rows = floor(pygame.display.get_window_size()[1]/self.dim - 1)
					
					        self.x, self.y = randint(0, cols) * \
					            self.dim, randint(0, rows) * self.dim
					
					        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()

Since we want the game to end, let’s add a gameOver() method for the Game class.

# In Game
					def gameOver(self):
					    print(f"You have died! You ate {len(self.snake.tail)} pieces of food!")
					    self.done = True

As of right now, the only way for the snake to die is if he hits a wall, so let’s add a check after every movement.

if self.snake.hit_wall:
					    self.gameOver()

This means we also need to edit out Snake.hitWall() function to edit the instance variable hit_wall.

def hitWall(self):
					   if (self.x < 0 or self.y < 0
					           or self.x > pygame.display.get_window_size()[0] - self.dim
					           or self.y > pygame.display.get_window_size()[1] - self.dim
					       ):
					       self.hit_wall = True
					       # Push back
					       self.x -= self.direction.value[0]
					       self.y -= self.direction.value[1]
					   else:
					       self.hit_wall = False

Now if we run our code, the snake can die! You may not be able to tell from the video, but if you run it yourself the window will close right after the snake hits the wall.

The snake should also die when it hits one of it’s tail. Let’s add a hitSelf() function and hit_self instance variable.

def hitSelf(self):
					    for block in self.tail:
					        if block.colliderect(self):
					            self.hit_self = True
					            return
					    self.hit_self = False

We now have to call this function in the move() function. It can just be added to the end self.hitSelf(().

Let’s add a method to check if the snake has died.

def checkDead(self):
					    if self.hit_self or self.hit_wall:
					        return True
					    else:
					        return False

This changes the check after movment as follows:

#Old Method
					if self.snake.hit_wall:
					    self.gameOver()
					#New Method
					if self.snake.checkDead():
					    self.gameOver()

Now we can see that the snake will die if it hits itself or if it hits a wall. One issue is that the snake can reverse directions and go from Direction.LEFT to Direction.RIGHT, but that type of movement should be impossible. To fix this, we need to set the changeDirection() function to filter out those inputs.

def changeDirection(self, newDirection):
					    if newDirection.value == [-x for x in self.direction.value]:
					        return
					    self.direction = newDirection

Now, no matter how hard you try, you can’t go back into yourself.

One issue that is persistant is if you press two keys such as up and left at the same time while going right, the snake will appear to switch directions. This is because we check for user input every frame, but we only mnove once the timer has reached a certain time. The way I decided to fix this is to introduce a buffer that will hold any extra user input while also allowing a user to add input at any time.

while not self.done:
					    #For loop:
					        if event.type == pygame.KEYDOWN:
					            if moved:
					                self.userInput(event.key)
					                moved = False
					            else:
					                input_buffer.append(event.key)
					
					if input_buffer and moved:
					    self.userInput(input_buffer.pop(0))
					    moved = False

Now no matter how fast we press the buttons, we can’t flip directions.

Finally, let’s add a score to the game so we can see how we’re doing in game.

class Score():
					    def __init__(self, screen, location):
					        self.font = pygame.font.Font(None, 36)
					        self.value = 0
					        self.location = location
					        self.screen = screen
					
					    def draw(self):
					        text = self.font.render(f"Score: {self.value}", 1, (255, 255, 255))
					        self.screen.blit(text, self.location)
					
					    def changeScore(self, change):
					        self.value += change
					
					    def reset(self):
					        self.value = 0

Now we have to create an instance in Game

class Game:
					    #init:
					        #original code
					        self.score = Score(self.screen, (10, 10))

Now we have to draw it in play(). Since I want the score to be visible at all times and not get covered up by the snake, it should be drawn first.

#In Game.play():
					    #In while:
					        self.score.draw()
					        pygame.draw.rect(self.screen, Snake.color, self.snake)
					        pygame.draw.rect(self.screen, Food.color, self.food)

In order to get the score to update, we can add a line to the code that executes when the snake eats because that is when the score should change.

#In Snake.checkEat()
					    def checkEat(self):
					        if self.colliderect(self.game.food):
					            self.game.food.relocate()
					            self.growTail()
					            self.game.score.changeScore(1)

Now we have a functional snake game! The final code is posted below. The next step will be to refactor the code where applicable.

import pygame
					from enum import Enum
					from random import randint
					from math import floor
					
					
					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, 40)
					        self.food = Food(self, 40) 
					        self.score = Score(self.screen, (10, 10))
					
					    def play(self):
					        timer = 0
					        speed = 10
					        input_buffer = []
					        moved = False
					        while not self.done:
					            for event in pygame.event.get():
					                if event.type == pygame.QUIT:
					                    self.done = True
					
					                if event.type == pygame.KEYDOWN:
					                    if moved:
					                        self.userInput(event.key)
					                        moved = False
					                    else:
					                        input_buffer.append(event.key)
					
					            if input_buffer and moved:
					                self.userInput(input_buffer.pop(0))
					                moved = False
					
					            if timer * speed > 1:
					                timer = 0
					                self.snake.move()
					                if self.snake.checkDead():
					                    self.gameOver()
					                moved = True
					
					            self.snake.checkEat()
					            self.screen.fill((0, 0, 0))
					            self.score.draw()
					            pygame.draw.rect(self.screen, Snake.color, self.snake)
					            pygame.draw.rect(self.screen, Food.color, self.food)
					
					            for block in self.snake.tail:
					                pygame.draw.rect(self.screen, Block.color, block)
					
					            pygame.display.flip()
					            timer += self.clock.tick(self.fps) / 1000
					
					    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)
					        elif key == pygame.K_SPACE:
					            self.pause = ~self.pause
					
					    def gameOver(self):
					        print(f"You have died! You ate {len(self.snake.tail)} pieces of food!")
					        self.done = True
					
					
					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
					
					
					class Direction(Enum):
					    UP = [0, -40]
					    DOWN = [0, 40]
					    LEFT = [-40, 0]
					    RIGHT = [40, 0]
					
					class Snake(Block):
					    color = (124, 252, 0)
					
					    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.direction = Direction.NONE
					        self.game = game
					        self.tail = []
					
					    def move(self):
					        if self.tail:
					            self.tail = self.tail[1:]
					            self.tail.append(Block(self.dim, self.x, self.y))
					
					        self.x += self.direction.value[0]
					        self.y += self.direction.value[1]
					
					        self.checkEat()
					        self.hitWall()
					        self.hitSelf()
					
					    def changeDirection(self, newDirection):
					        if newDirection.value == [-x for x in self.direction.value]:
					            return
					        self.direction = newDirection
					
					    def checkEat(self):
					        if self.colliderect(self.game.food):
					            self.game.food.relocate()
					            self.growTail()
					            self.game.score.changeScore(1)
					
					    def growTail(self):
					        self.tail.append(Block(
					            self.dim, self.x - self.direction.value[0], self.y - self.direction.value[1]))
					
					    def hitWall(self):
					        if (self.x < 0 or self.y < 0
					                    or self.x > pygame.display.get_window_size()[0] - self.dim
					                    or self.y > pygame.display.get_window_size()[1] - self.dim
					                ):
					            self.hit_wall = True
					            # Push back
					            self.x -= self.direction.value[0]
					            self.y -= self.direction.value[1]
					        else:
					            self.hit_wall = False
					
					    def hitSelf(self):
					        for block in self.tail:
					            if block.colliderect(self):
					                self.hit_self = True
					                return
					        self.hit_self = False
					
					    def checkDead(self):
					        if self.hit_self or self.hit_wall:
					            return True
					        else:
					            return False
					
					
					class Food(Block):
					    color = (255, 0, 100)
					
					    def __init__(self, game, size):
					        Block.__init__(self, size, 0, 0)
					        self.game = game
					        self.relocate()
					
					    def relocate(self):
					        cols = floor(pygame.display.get_window_size()[0]/self.dim - 1)
					        rows = floor(pygame.display.get_window_size()[1]/self.dim - 1)
					
					        self.x, self.y = randint(0, cols) * \
					            self.dim, randint(0, rows) * self.dim
					
					        if not self.isSafe():
					            self.relocate()
					
					    def isSafe(self):
					        if self.colliderect(self.game.snake):
					            return False
					        else:
					            return True
					
					class Score():
					    def __init__(self, screen, location):
					        self.font = pygame.font.Font(None, 36)
					        self.value = 0
					        self.location = location
					        self.screen = screen
					
					    def draw(self):
					        text = self.font.render(f"Score: {self.value}", 1, (255, 255, 255))
					        self.screen.blit(text, self.location)
					
					    def changeScore(self, change):
					        self.value += change
					
					    def reset(self):
					        self.value = 0
					
					
					
					def main():
					    game = Game(60, 800, 600)
					    game.play()
					
					
					if __name__ == "__main__":
					    main()