Search code examples
pythonturtle-graphicspython-turtle

Why is the snake increasing its speed when it grows?


I am working on the famous snake game in Python and I've stumbled upon a glitch where the snake starts doing when it eats food.

The snake should keep constant speed throughout the game and the segments should be of equal distance from each other. But what happens is every time the snake eats food it increases its speed and the segments start moving away from each other.

Here is part of the code if anybody can help.

import random
import time
from turtle import Screen, Turtle


MOVE_DISTANCE = 10
POSITIONS = [(0, 0), (0, -20), (0, -40)]
class Snake:
    def __init__(self):
        self.segments = []
        self.create_snake()
        self.head = self.segments[0] 

    def create_snake(self):
        for position in POSITIONS:
            self.add_segment(position) 

    def add_segment(self, position):
        new_segment = Turtle("square")
        new_segment.color("white")
        new_segment.penup()
        new_segment.goto(position)
        self.segments.append(new_segment) 

    def move(self):
        for seg_num in range(len(self.segments) - 1, 0, -1):
            self.head.forward(MOVE_DISTANCE) # Moving distance of head
            new_x = self.segments[seg_num - 1].xcor()
            new_y = self.segments[seg_num - 1].ycor()
            self.segments[seg_num].goto(new_x, new_y) 

    def grow(self):
        self.add_segment(self.segments[-1].position()) 

    def up(self):
        if self.head.heading() != 270:
            self.head.setheading(90) 

    def down(self):
        if self.head.heading() != 90:
            self.head.setheading(270) 

    def left(self):
        if self.head.heading() != 0:
            self.head.setheading(180) 

    def right(self):
        if self.head.heading() != 180:
            self.head.setheading(0)

class Food(Turtle):
    def __init__(self):
        super().__init__()
        self.shape("circle")
        self.penup()
        self.shapesize(stretch_len=0.5, stretch_wid=0.5)
        self.color("red")
        self.speed("fastest")
        self.refresh_food() 

    def refresh_food(self):
        rand_x_cor = random.randint(-480, 480)
        rand_y_cor = random.randint(-260, 260)
        self.goto(rand_x_cor, rand_y_cor)

my_screen = Screen()
my_screen.setup(width=1000, height=600)
my_screen.bgcolor("black")
my_screen.title("My Snake Game")
my_screen.tracer(0)

my_snake = Snake()

food = Food()

my_screen.listen()
my_screen.onkey(my_snake.up, "Up")
my_screen.onkey(my_snake.down, "Down")
my_screen.onkey(my_snake.left, "Left")
my_screen.onkey(my_snake.right, "Right")

game_over = False 
while not game_over:
    my_screen.update()
    time.sleep(0.1)
    my_snake.move()

    # Detect collision with the food
    if my_snake.head.distance(food) < 15:
        food.refresh_food()
        my_snake.grow()

    # Detect collision with wall
    if my_snake.head.xcor() < -480 or my_snake.head.xcor() > 480 or my_snake.head.ycor() < -280 or my_snake.head.ycor() > 280:
        game_over = True

    # Detect collision with tail
    for segment in my_snake.segments[2:]:
        if my_snake.head.distance(segment) < 10:
            game_over = True 

I tried reducing the moving distance of the snake head self.head.forward(MOVE_DISTANCE) # Moving distance of head

I also tried different ways of moving the snake but it also didn't work.


Solution

  • The move method looks incorrect:

    def move(self):
        for seg_num in range(len(self.segments) - 1, 0, -1):
            self.head.forward(MOVE_DISTANCE)  # Moving distance of head
            new_x = self.segments[seg_num - 1].xcor()
            new_y = self.segments[seg_num - 1].ycor()
            self.segments[seg_num].goto(new_x, new_y)
    

    I don't think you want to move the head for every single tail segment. Instead, you want to move the head once, and then move all the tail segments once:

    def move(self):
        self.head.forward(MOVE_DISTANCE)  # Moving distance of head
        for seg_num in range(len(self.segments) - 1, 0, -1):
            new_x = self.segments[seg_num - 1].xcor()
            new_y = self.segments[seg_num - 1].ycor()
            self.segments[seg_num].goto(new_x, new_y)
    

    Here's a runnable example with some adjustments:

    from random import randint
    from turtle import Screen, Turtle
    
    GRID_SIZE = 20
    
    
    class Snake:
        POSITIONS = (0, 0), (GRID_SIZE, 0), (GRID_SIZE * 2, 0)
    
        def __init__(self):
            self.segments = []
            self.create_snake()
            self.head = self.segments[0]
    
        def create_snake(self):
            for position in Snake.POSITIONS:
                self.add_segment(position)
    
        def add_segment(self, position):
            new_segment = Turtle("square")
            new_segment.color("white")
            new_segment.penup()
            new_segment.goto(position)
            self.segments.append(new_segment)
    
        def move(self):
            self.head.forward(GRID_SIZE)
    
            for seg_num in range(len(self.segments) - 1, 0, -1):
                new_x = self.segments[seg_num - 1].xcor()
                new_y = self.segments[seg_num - 1].ycor()
                self.segments[seg_num].goto(new_x, new_y)
    
        def grow(self):
            self.add_segment(self.segments[-1].position())
    
        def up(self):
            if self.head.heading() != 270:
                self.head.setheading(90)
    
        def down(self):
            if self.head.heading() != 90:
                self.head.setheading(270)
    
        def left(self):
            if self.head.heading() != 0:
                self.head.setheading(180)
    
        def right(self):
            if self.head.heading() != 180:
                self.head.setheading(0)
    
        def eats(self, food):
            return self.head.distance(food.position()) < GRID_SIZE / 2
    
        def collides_with_wall(self):
            return (
                self.head.xcor() < -w
                or self.head.xcor() > w
                or self.head.ycor() < -h
                or self.head.ycor() > h
            )
    
        def collides_with_itself(self):
            for segment in self.segments[3:]:
                if self.head.distance(segment) < GRID_SIZE:
                    return True
    
            return False
    
    
    class Food:
        def __init__(self):
            self.pen = pen = Turtle()
            pen.shape("circle")
            pen.penup()
            pen.shapesize(stretch_len=0.5, stretch_wid=0.5)
            pen.color("red")
            pen.speed("fastest")
            self.reposition()
    
        def reposition(self):
            self.pen.goto(
                randint(-w, w) // GRID_SIZE * GRID_SIZE,
                randint(-h, h) // GRID_SIZE * GRID_SIZE,
            )
    
        def position(self):
            return self.pen.pos()
    
    
    def tick():
        snake.move()
    
        if snake.eats(food):
            food.reposition()
            snake.grow()
    
        if snake.collides_with_wall() or snake.collides_with_itself():
            screen.update()
            return
    
        screen.update()
        screen.ontimer(tick, fps)
    
    
    screen = Screen()
    screen.setup(width=1000, height=600)
    fps = 1000 // 10
    w = screen.window_width() / 2 - GRID_SIZE
    h = screen.window_height() / 2 - GRID_SIZE
    screen.bgcolor("black")
    screen.title("My Snake Game")
    screen.tracer(0)
    snake = Snake()
    food = Food()
    screen.listen()
    screen.onkey(snake.up, "Up")
    screen.onkey(snake.down, "Down")
    screen.onkey(snake.left, "Left")
    screen.onkey(snake.right, "Right")
    tick()
    screen.exitonclick()
    

    There's still room for improvement, but overall this should be easier to maintain.