python – Snake Game: How to make the snake’s body gets trimmed when its head touches its body?

I have an assignment that ask me to make a snake game with Python. The assignment asks me to make the snake’s body gets trimmed when its head touches the body.

The snake body is a queue, so here is the code for forming a queue: The snake’s head is defined as the rear node and the snake’s tail is defined as the front node, as shown in the picture below.

#The code of forming a queue, it cannot be modified

class Node:
    def __init__(self, x, y):
        self.x = x        
        self.y = y        
        self.pre = None   
        self.next = None  

class Queue:
    def __init__(self):
        self.front = None 
        self.rear = None  

    def len(self): 
        length = 0
        cur = self.front
        while cur:
            cur = cur.next
            length += 1
        return length

    def enQueue(self, x, y): 
        new = Node(x, y)
        if self.len() == 0: 
            self.front = new
            self.rear = new
        else:
            new.pre = self.rear
            self.rear.next = new
            self.rear = new

    def deQueue(self): 
        if self.len() <= 1: 
            self.front = None
            self.rear = None
        else:
            self.front = self.front.next
            self.front.pre = None
    
    def reverse(self): 
        cur = self.front
        self.rear, self.front = self.front , self.rear
        while cur:
            cur.next, cur.pre = cur.pre, cur.next
            cur = cur.pre

    def printQueue(self): 
        cur = self.front
        print("front", end="  ")
        while cur:
            print(f"[{cur.x}, {cur.y}]", end="  ")
            cur = cur.next
        print("rear")

And this is the main code for the game:

import pygame, sys, time, random
from pygame.locals import *
from pathlib import Path, os
from coor_queue import Node, Queue
from item_stack import Stack
class SnakeGame:
    def __init__(self):
        self.g = 30 #The width of each grid
        self.dir = "right" #Initial Direction
        self.snake = Queue() #Queue of the snake
        for i in range(9):
            self.snake.enQueue(i*self.g,9*self.g)
        self.init_params()
        self.init_pygame()
        self.init_objects()
        self.init_images()
#The code above cannot be modified
# =========================== Movement ===========================
    def move(self): 
        headx = self.snake.rear.x
        heady = self.snake.rear.y
        if self.dir == "down":
            heady += self.g
            self.snake.enQueue(headx, heady)
            self.snake.deQueue()
        if self.dir == "up":
            heady -= self.g
            self.snake.enQueue(headx, heady)
            self.snake.deQueue()
        if self.dir == "left":
            headx -= self.g
            self.snake.enQueue(headx, heady)
            self.snake.deQueue()
        if self.dir == "right":
            headx += self.g
            self.snake.enQueue(headx, heady)
            self.snake.deQueue()
# =========================== Add Tails ===========================
    def add_tail(self): 
        tailx = self.snake.front.x
        taily = self.snake.front.y
        newtail = Node(tailx, taily)
        newtail.next = self.snake.front
        self.snake.front.pre = newtail
        self.snake.front = newtail
# =========================== Trim the snake's body ===========================
    def eat_body(self): 
        # If the snake's head touches a grid (node) of the body
        # Then the part from original tail to the grid which the snake's head touches
        # Hint: Use self.snake
# ============The code below cannot be modified======================
    def main(self):
        while True:
            self.keyboard_input()
            self.check_input_valid()
            self.move()
            self.eat()
            self.display()
            if self.is_dead():
                self.game_over()
            self.fps.tick(8 + self.speed//5)
    def keyboard_input(self): # Keyboard input
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN:
                if event.key == ord("d") or event.key == K_RIGHT: self.input_dir = "right"
                if event.key == ord("a") or event.key == K_LEFT:  self.input_dir = "left"
                if event.key == ord("w") or event.key == K_UP:    self.input_dir = "up"
                if event.key == ord("s") or event.key == K_DOWN:  self.input_dir = "down"
                if event.key == K_ESCAPE: pygame.event.post(pygame.event.Event(QUIT))
    def check_input_valid(self): #If the input direction is the opposite of the original direction, the input will be invalid
        if (self.input_dir == "right" and self.dir != "left") or (self.input_dir == "left" and self.dir != "right") or 
           (self.input_dir == "up"    and self.dir != "down") or (self.input_dir == "down" and self.dir != "up"):
            self.dir = self.input_dir
    def eat(self): #Eat body or food
        self.eat_food()
        self.eat_body()
    def display(self):
        #Background
        self.blit_map()
        #Snake body
        cur = self.snake.rear.pre
        while cur:
            self.blit_image("body", cur.x, cur.y, self.g, self.g)
            cur = cur.pre
        #Snake head
        if self.snake.rear.x < self.width:
            self.blit_image("head", self.snake.rear.x, self.snake.rear.y, self.g, self.g)
        #Food
        self.blit_image(self.food, self.foodPos.x, self.foodPos.y, self.g, self.g)
        #Status Bar
        self.blit_status_bar()

        pygame.display.flip()
    def is_dead(self): 
        return (self.snake.rear.x == self.width or self.snake.rear.x < 0) or 
               (self.snake.rear.y == self.height or self.snake.rear.y < 0)
    def game_over(self): # Game Over 
        self.play_theme("game_over1")
        time.sleep(3)
        pygame.quit()
        sys.exit()
    def init_params(self): #Basic parameters
        self.screen_width  = 1140  
        self.screen_height = 540   
        self.width  = 960  
        self.height = 540  
        self.speed = 0     
        self.prob = 25     
        self.score = 0     
        self.spf = 10      
        self.satiety = 0   
        self.input_dir = None 
    def init_pygame(self): #Initialize pygame
        pygame.init()
        self.fps = pygame.time.Clock() #
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height)) #
        pygame.display.set_caption("Snake Game") #
        pygame.mixer.init() 
    def init_objects(self): 
        self.food_list =  [food.split(".")[0] for food in os.listdir(Path("src/image/food"))]
        self.food = None
        self.select_food()
        self.foodPos = Node(self.width//2, self.height//2)
    def init_images(self): 
        self.img_StatusBar = pygame.image.load(Path("src/image/other/StatusBar.jpg")).convert_alpha()
        self.img_head = pygame.image.load(Path("src/image/snake/SnakeHead.jpg")).convert_alpha()
        self.img_body = pygame.image.load(Path("src/image/snake/SnakeBody.jpg")).convert_alpha()
        self.img_map = pygame.image.load(Path("src/image/map/Map1.jpg")).convert_alpha()
        for food in self.food_list:
            exec(f"self.img_{food} = pygame.image.load(Path('src/image/food/{food}.jpg')).convert_alpha()")
    def eat_food(self):
        if self.snake.rear.x == self.foodPos.x and self.snake.rear.y == self.foodPos.y: 
            self.satiety += 1
            self.speed += 1
            self.spf = 10 + ((self.satiety-1)//10)*10
            self.score += self.spf
            self.play_effect("eat_food")
            x = random.randrange(1, self.width//self.g)
            y = random.randrange(1, self.height//self.g)
            self.foodPos.x = x*self.g
            self.foodPos.y = y*self.g
            self.select_food() 
            self.add_tail() 
    def select_food(self): 
        while True:
            next_food = self.food_list[random.randrange(len(self.food_list))]
            if next_food != self.food:
                self.food = next_food
                break 
    def play_theme(self, theme): 
        pygame.mixer.music.load(Path(f"src/sound/theme/{theme}.mp3"))
        pygame.mixer.music.set_volume(0.5)
        pygame.mixer.music.play()
    def play_effect(self, effect): 
        sound = pygame.mixer.Sound(Path(f"src/sound/effect/{effect}.mp3"))
        sound.set_volume(0.7)
        sound.play()
    def blit_image(self, name, coor_x = 0, coor_y = 0, size_x = 0, size_y = 0): 
        exec(f"self.img_{name} = pygame.transform.scale(self.img_{name}, size=(size_x, size_y))")
        exec(f"self.screen.blit(self.img_{name}, (coor_x, coor_y))")
    def blit_rect(self, data, coor_x = 0, coor_y = 0, font_size = 80, color = (0, 0, 0)): 
        self.Font = pygame.font.SysFont("", font_size)
        self.Surf = self.Font.render(str(data), True, color)
        self.Rect = self.Surf.get_rect()
        self.Rect.center = (coor_x, coor_y)
        self.screen.blit(self.Surf, self.Rect)
    def blit_map(self): 
        self.blit_image("map", 0, 0, self.width, self.height) 
    def blit_status_bar(self): 
        
        self.blit_image("StatusBar", 960, 0, 180, self.screen_height)
        
        self.blit_rect(self.score, 35*self.g, 1.87*self.g, font_size = 50, color = (238, 0, 0))
        
        self.blit_rect(int(8 + self.speed)-7, 35*self.g, 4.13*self.g, font_size = 50, color = (178, 58, 238))
        
        self.blit_rect(self.snake.len(), 35*self.g, 6.28*self.g, font_size = 50, color = (50, 205, 50))
# ==================================================================

game = SnakeGame()
game.main()

The picture below explains how trimming snake body works, the Python logo is the head.

I understand that the condition of trimming the body is when the head reaches the same grid as a part of the body. But I have no idea how to write this condition because I’m not sure if there is a way to get the coordinate from the nodes between front node and rear node.

Some further explanation is appreciated, thanks in advance.

Leave a Comment