Snake

1- Instal Required Libraries

Option 1) System level installation

pip install pygame

Option 2) Install Pygame in a virtual environment

# 1. Install Miniconda (or Anaconda)
# The official page has very clear instructions 
# https://docs.conda.io/en/latest/miniconda.html
# 2. Create virtual environment
conda create -n env_name # I called it `snake_env`
# 3. Activate the virtual environment
conda activate env_name
# 4. Install needed libraries
conda install pip
pip install pygame 
2- Pygame preliminary

Before starting the design process, let us familiarize ourselves with basic Pygame functionalities. In the following code we will

import pygame
# Initialize pygame modules 
pygame.init() 
# define a window (or screen) to draw on it
#                       window's  width,  height
screen = pygame.display.set_mode((400,    100))
# define colors in RGB
BLACK = (0,0,0) 
GREEN = (0, 255, 0)

# set a flag to terminate exection when it is set to False
running = True
# this while loop will keep executing until the user click the close button
while running == True:
    # get user events
    for event in pygame.event.get():
        # pygame.QUIT happens when the user click the close button of the window
        if event.type == pygame.QUIT: 
            running = False

    # make the background color of the window back
    screen.fill(BLACK)
    # Draw a rectangle        color   position   width,  height
    pygame.draw.rect(screen,  GREEN,  (100, 40,   20,    40))
    # Update the screen
    pygame.display.flip()

# quit pygame
pygame.quit()

Output:


Let us draw a grid :

To do so we create a function draw_grid that has a nested loop to draw rectangles with a small gap in between to make a grid.
import pygame
pygame.init()
# define a cell size
CELL = 20
GAP = 1
# To have a nice grid, define the width and height to be multiple of the cell size 
width = 20 
height = 5 
BLACK = (0,0,0) 
GRAY = (40, 40, 40)
screen = pygame.display.set_mode((width * CELL, height * CELL))

def draw_grid(cell=CELL, color=BLACK, w=width, h=height):
    """This function draw a gird.
    Note: the background color must not be the same as the cell color"""
    # w//cell : allow us to progress on a cell level instead of a pixel level
    for i in range(w):
        for j in range(h):
            # cell-GAP: to make a grid we draw a rectangle that is bit smaller than CELL
            rect = (i * cell, j * cell, cell-GAP, cell-GAP)
            pygame.draw.rect(screen, color, rect)

running = True
while running == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: 
            running = False
    screen.fill(GRAY)
    draw_grid()
    pygame.display.flip()

pygame.quit()

Output:

3- Requirements
  1. We shall make the environment a grid (or virtual grid)
  2. The snake must move across a complete cell of the grid. For example, the snake must not move half a cell.
  3. The food shall be generated randomly, but it must occupy a complete single cell.
  4. If the snake finds food, it becomes longer by one cell.
  5. If the snake hit the border or itself, it dies.
4- Define the Grid class
This class defines the grid where the snake moves and the food is placed. In it we also compute the x and y coordinates of the grid vertices.
class Grid:
    def __init__(s, w=640, h=480, cell=CELL):
        s.cell_size = cell
        s.width = (w // cell) * cell  # make the width multiple of the cell size
        s.height = (h // cell) * cell
        s.x_vertices = range(0, s.width, s.cell_size) 
        s.y_vertices = range(0, s.height, s.cell_size)
Next we define the Vertex class.
5- Define the Vertex and Direction class
A vertex is an intersection point on the grid. Our math considers the top left vertex of a cell. A few vertices are highlighted in the following figure.

To move the snake and place the food according to the requirements, we need to add, subtract, and compare vertices. These functionalities are enabled by defining a few special python methods. Namely
class Vertex:
    def __init__(s, x, y ):
        s.x = x 
        s.y = y  
        # print(s)
    def __add__(s, p):
        return Vertex( s.x  + p.x , s.y + p.y)
    def __sub__(s, p):
        return Vertex( s.x  - p.x, s.y - p.y) 
    def __eq__(s, p):
        if not isinstance(p, Vertex):
            # don't compare againt unrelated types
            return NotImplemented
        return s.x == p.x and s.y == p.y
    def __repr__(s):
        return f'Vertex{s.x, s.y}'
To make our main code more readable, let us define the Direction class.
class Direction():
    def __init__(s, cell_size):
        s.RIGHT = Vertex(cell_size, 0)
        s.LEFT  = Vertex(-cell_size, 0)
        s.DOWN  = Vertex(0, cell_size)
        s.UP    = Vertex(0, -cell_size)

6- Define the Snake class

5.1 Initialization

class Snake:
    def __init__(s, grid):
        s.w = grid.width
        s.h = grid.height 
        s.cz = grid.cell_size
        s.grid = grid
        s.direction = Direction(s.cz)

        # set initial game state
        s.head =  Vertex(s.w//2, s.h//2)
        s.snake = [s.head, s.head - Vertex(s.cz, 0) , s.head - Vertex(2*s.cz, 0)]
        s.step = s.direction.RIGHT
        s.score = 0 
        s.food = s._gen_food()

5.2 Good Generation

Randomly place food but not on the snake.
def _gen_food(s):
    food_v = Vertex( random.choice(s.grid.x_vertices),  random.choice(s.grid.y_vertices))
    print(food_v)
    if food_v in s.snake:
        # if food_v is on the snake generate a new food_v
        s._gen_food()
    return food_v

5.3 User input handler

The following method checks if the user presses an arrow on the keyboard to change direction. The direction is saved in s.step to maintain the selected direction after the user releases the button. Also, we check if the user closes the window, and If the user did, we terminate the game.
def _handle_user_input(s):
    '''This method handles user input'''
    for event in pygame.event.get():
        # pygame.QUIT event happens when the user click on the window closing button 
        if event.type == pygame.QUIT:
            pygame.quit()   # quit pygame

        # check if a key is pressed 
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                s.step = Direction.LEFT
            elif event.key == pygame.K_RIGHT:
                s.step = Direction.RIGHT
            elif event.key == pygame.K_DOWN:
                s.step = Direction.DOWN
            elif event.key == pygame.K_UP:
                s.step = Direction.UP

5.4 Move the snake

After selecting the desired direction, we update the head position and insert it in the first place in the snake data structure (i.e. the snake list). If there is no food in the new head position, we drop (or pop) the trail to make the snake move without getting longer.
def move_snake(s):
    s._handle_user_input()
    s.head += s.step
    s.snake.insert(0, snake.head)
    if not snake.there_is_food():
        snake.snake.pop()  

However, if there is food, we increase the score, generate a new food cell, and skip dropping the last cell in the snake.
def there_is_food(s):
    if s.head == s.food:
        s.score +=1
        print(s.score)
        s.food = s._gen_food()
        return True
    return False 

5.5 Snake status

Check if the snake has died. The snake dies if it hits a border or itself. Notice, when we check if the head has moved on the snake itself we exclude the head position for obvious reasons.
    def snake_died(s):
    died =False
    if s.head.x > s.grid.x_vertices[-1]  or s.head.y > s.grid.y_vertices[-1]  or s.head.x < 0 or s.head.y < 0 :
        died  = True
    elif s.head in s.snake[1:]:
        died  = True
    return died  
    
7- Define the Window class
This class takes in a Grid and Snake class. This class is responsible for drawing the grid, snake, and food on the window. It also set the speed at which the screen is refreshed. Finally, it writes on the window to show the current score.
    def draw_grid(s, vertex=False):
        for i in s.grid.x_vertices:
            for j in s.grid.y_vertices:
                rect = (i, j, s.cz-1, s.cz-1)
                pygame.draw.rect(s.display, (BLACK), rect)
                if vertex:
                    vertex_point = (i, j, 2, 2)
                    pygame.draw.rect(s.display, RED, vertex_point)

    def update(s):
        s.display.fill(GRAY)
        s.draw_grid(True)
        for pt in s.snake.snake:
            pygame.draw.rect(s.display, WHITE, pygame.Rect(pt.x, pt.y, s.cz-GAP, s.cz-GAP))
        text = font.render(f"Score: {s.snake.score}", True, WHITE)
        s.display.blit(text, [0,0])
        pygame.draw.rect(s.display, GREEN, pygame.Rect(s.snake.food.x, s.snake.food.y, s.cz, s.cz))
        s.clock.tick(SPEED)
        pygame.display.flip()
8- The main loop of the game.
    if __name__ == '__main__':
        num_lives = 9
        for _ in range(num_lives):
            grid = Grid()
            snake = Snake(grid)
            window = Window(grid, snake)
            while not snake.snake_died():  
                snake.move_snake()
                window.update()
    
            print('Snake died!')
        print('Game is over!')
        pygame.quit()
        quit()