Search code examples
pythonpygame

Pygame - Snake shrinks randonmly


I'm creating my own clone of the Snake game with help of Python3 and Pygame. During coding, I've encountered a strange problem.

A snake moves and grows as expected, however sometimes randomly, it decreases to its initial length (two units). I have no the slightest idea what's going on. My code is here:

#main.py

import sys
import random
import pygame
 
from apple import Apple
from settings import Settings
from snake import Snake
 
settings = Settings()
snake = Snake(settings)
apple = Apple(settings)
pygame.init()
screen = pygame.display.set_mode((settings.SW, settings.SH))
pygame.display.set_caption(settings.title)
clock = pygame.time.Clock()
 
 
def draw_grid():
    """Draw a gird on the screen."""
    for x in range(0, settings.SW, settings.BLOCK_SIZE):
        for y in range(0, settings.SH, settings.BLOCK_SIZE):
            rect = pygame.Rect(x, y, settings.BLOCK_SIZE, 
            settings.BLOCK_SIZE)
            pygame.draw.rect(screen, (255, 255, 255), rect, 1)
 
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            snake.change_direction(event)
    snake.update()
    screen.fill(settings.BLACK)
    draw_grid()
    apple.draw_apple(screen)
    snake.draw_snake(screen)
    pygame.display.update()
    snake.eat(apple)
    clock.tick(10) 
 
#snake.py

import pygame
 
class Snake:
    """A simple moveable snake"""
    def __init__(self, settings):
        self.settings = settings
        self.x = self.settings.BLOCK_SIZE
        self.y =self.settings.BLOCK_SIZE
        self.xdir = 1
        self.ydir = 0
        self.head = pygame.Rect(self.x, self.y, 
        self.settings.BLOCK_SIZE, self.settings.BLOCK_SIZE)
        self.body = [pygame.Rect(self.x-self.settings.BLOCK_SIZE,
        self.y, self.settings.BLOCK_SIZE, self.settings.BLOCK_SIZE)]
        self.collided = False
        self.excess_border = False
        
    def draw_snake(self, screen):
        """Draw a snake on the screen."""
        pygame.draw.rect(screen, (30, 180,15), self.head)
        for square in self.body:
            pygame.draw.rect(screen, (30, 180, 15), square)
 
    def update(self):
        """Make a snake move"""
        self.body.append(self.head)
        for i in range(len(self.body)-1):
            self.body[i].x = self.body[i+1].x
            self.body[i].y = self.body[i+1].y
        self.head.x += self.xdir * self.settings.BLOCK_SIZE
        self.head.y += self.ydir * self.settings.BLOCK_SIZE
        self.body.remove(self.head) 
        
    def change_direction(self, event):
        """React to an user's input."""
        if event.key == pygame.K_DOWN:
            self.xdir = 0
            self.ydir = 1
        if event.key == pygame.K_UP:
            self.xdir = 0
            self.ydir = -1
        if event.key == pygame.K_LEFT:
            self.xdir = -1
            self.ydir = 0
        if event.key == pygame.K_RIGHT:
            self.xdir = 1
            self.ydir = 0
                
    def grow(self):
        """Make the snake grow."""
        self.body.append(pygame.Rect(self.head.x, self.head.y,
        self.settings.BLOCK_SIZE, self.settings.BLOCK_SIZE))
        
    def eat(self, apple):
        """"Make a snake eat."""
        if (self.head.x == apple.x) and (self.head.y == apple.y):
         self.grow()
         apple.create_apple()
#settings.py

class Settings:
    """A class storing the game's essential settings."""
    def __init__(self):
        self.title = 'Snake!'
        self.SW = 800
        self.SH = 800
        self.BLACK = (0, 0, 0)
        self.RED = (170, 20, 30)
        self.BLOCK_SIZE = 50 
#apple.py

import random
import pygame
 
class Apple:
    """A class to represent an apple."""
    def __init__(self, settings):
        self.settings = settings
        self.create_apple()
        
    def create_apple(self):
        """Create a new apple."""
        self.x = (int(random.randint(0, self.settings.SW)/
        self.settings.BLOCK_SIZE) 
        * self.settings.BLOCK_SIZE)
        self.y =  (int(random.randint(0, self.settings.SW)
        / self.settings.BLOCK_SIZE) 
        * self.settings.BLOCK_SIZE)
        self.apple = pygame.Rect(self.x, self.y, 
        self.settings.BLOCK_SIZE,
        self.settings.BLOCK_SIZE)
        
    def draw_apple(self, screen):
        """"Draw an apple on the screen."""
        pygame.draw.rect(screen, self.settings.RED, self.apple)

Thanks in advance for any help


Solution

  • The bug arises when the head of the snake collides with its body.

    This issue is rooted in the use of the statement self.body.remove(self.head) in Snake.update().
    In the game's logic, the head is temporarily added to the body list to facilitate the snake's movement processing.
    However, the core of the problem lies in how equality is determined between two pygame.Rect objects; they are deemed equal if they share identical positions and sizes.
    Consequently, if the head occupies the same space as any segment of the snake's body, they are considered identical.
    The remove function's behavior, which is to eliminate the first instance of a matching value from a list, exacerbates the issue.
    Since the head is positioned at the end of the array, the function inadvertently removes a segment of the snake's body instead of the head, leading to the bug.

    You can instead use self.body.pop() to remove the last element from self.body, which is self.head.