Search code examples
pythonpython-3.xturtle-graphicspython-turtle

Python Snake Game - Why does the food only gets eaten occasionally?


I'm currently taking a python course where we had to built a snake game. I did not follow the tutors instuctions and I tried to do it on my own. I could make it work for the most part, but I can't figure out why the purple food elements sometimes are not eaten by the snake, even though the snake directly strikes over the food. I would appreciate it, if someone with more experience than me could look over my code to find the bug, I certainly don't know where the mistake is. Here is the code:

from turtle import Turtle, Screen
import random
import time

class Snake():
    def __init__(self):
        screen.onkeypress(self.look_left, 'a')
        screen.onkeypress(self.look_right, 'd')
        screen.listen()

        self.snake_squares = {
            1: Turtle(),
            2: Turtle(),
            3: Turtle(),
        }

        for square in self.snake_squares:
            self.design_square(self.snake_squares[square])

        self.snake_head = self.snake_squares[1]
        self.look_left()

    def design_square(self, current_square):
        current_square.penup()
        current_square.shape('square')
        current_square.color('white')
        current_square.shapesize(1.00)
        current_square.pensize(1)

    def add_square(self, new_score):
        square_amount = new_score + 3
        self.snake_squares[square_amount] = Turtle()
        self.design_square(self.snake_squares[square_amount])
        self.snake_squares[square_amount].setposition(self.old_position)


    def look_left(self):
        self.snake_head.left(90)

    def look_right(self):
        self.snake_head.right(90)

    def move(self):
        self.old_position = self.snake_head.position()
        self.snake_head.forward(10.00)

        all_square_positions = []

        for square in self.snake_squares:
            if square != 1:
                next_square = self.snake_squares[square]
                new_position = self.old_position
                self.old_position = next_square.position()
                next_square.setposition(new_position)
                all_square_positions.append(new_position)
        return self.snake_head.position(), all_square_positions


    def check_self_hit(self, head_position, tail_positions):
        if head_position in tail_positions:
            return False
        else:
            return True



class Board():
    def __init__(self, height):
        self.score_board = Turtle()
        self.score_board.hideturtle()
        self.score_board.setposition(0, height + 10)
        self.score_board.color('white')
        self.score = 0

    def add_to_score(self, old_score):
        self.score_board.color('black')
        self.score_board.write(arg=f"Score: {self.score}", font=('Arial', 25, "normal"), align='center')
        self.score = old_score + 1
        self.score_board.color('white')
        self.score_board.write(arg=f"Score: {self.score}", font=('Arial', 25, "normal"), align='center')
        return self.score



class Border():
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.positive_borders = [self.width, self.height]
        self.negative_borders = [- self.width, - self.height]


    def check_borders(self, snake_position):
        no_hit = True
        for border in self.positive_borders:
            if snake_position[0] >= border:
                no_hit = False
            elif snake_position[1] >= border:
                no_hit = False
            else:
                pass
        for border in self.negative_borders:
            if snake_position[0] <= border:
                no_hit = False
            elif snake_position[1] <= border:
                no_hit = False
            else:
                pass
        return no_hit


    def print_border(self, width, height):
        line = Turtle()
        line.hideturtle()
        line.setposition(-float(width), -float(height))
        line.shape("circle")
        line.speed('fastest')
        line.color('white')
        for wall in range(4):
            line.forward(800)
            line.left(90)


class Food():
    def __init__(self, width, height):
        self.width = width
        self.height = height

        self.food = Turtle()
        self.food.hideturtle()
        self.food.speed('fastest')
        self.food.penup()
        self.food.pensize(3)
        self.food.shape('circle')
        self.food.color('purple')

    def create_food(self):
        self.food.showturtle()
        self.food.clear()
        return self.random_position()


    def random_position(self):
        x_min = -self.width + 10                #-390
        x_max = self.width - 10                 #390
        y_min = -self.height + 10               #-390
        y_max = self.height - 10                #390

        x = 11
        y = 11
        while x % 10.00 != 0.00:
            x = float(random.randint(x_min, x_max))

        while y % 10.00 != 0.00:
            y = float(random.randint(y_min, y_max))

        self.food.setposition((x, y))

        x += 10.00
        y += 10.00

        all_possible_hits = []
        for hits in range(3):
            all_possible_hits.append((x, y -10.00))
            x -= 10.00

        for hits in range(3):
            all_possible_hits.append((x + 10.00, y))
            y -= 10.00

        return all_possible_hits


    def erase_food(self):
        self.food.hideturtle()


screen = Screen()
screen.tracer(0)
canvas_width = 400
canvas_height = 400
screen.bgcolor("black")
border = Border(canvas_width, canvas_height)
border.print_border(canvas_width, canvas_height)
snake = Snake()
food = Food(canvas_width, canvas_height)

board = Board(canvas_height)
current_score = board.add_to_score(-1)
current_food_positions = food.create_food()
snake_alive = True
screen.update()

snake_path = []
while snake_alive:
    time.sleep(0.03)
    screen.update()
    snake_current_position, all_positions = snake.move()
    snake_path.append(snake_current_position)
    if snake.check_self_hit(snake_current_position, all_positions):
        if border.check_borders(snake_current_position):
            if snake_current_position in current_food_positions:
                current_score = board.add_to_score(current_score)
                snake.add_square(current_score)
                food.erase_food()
                current_food_positions = food.create_food()
                screen.update()
        else:
            snake_alive = False
    else:
        snake_alive = False

screen.exitonclick()

I don't really know why that happens, especially because it only happens sometimes and at different score amounts.


Solution

  • A few things here:

    1. As the first comment pointed out, generally, you would not come to stackoverflow for debugging advice like this, at least without trying to debug on your own and posting the results with your question
    2. Something you will need to keep in mind always is that directly comparing floating point values for equality is a big no-no in any programming (that is only one of the hundreds of articles explaining why, for a much more in depth and possibly over your head look, check out this). I believe that is directly where your problem lies in this case as well. By using snake_current_position in current_food_positions, you are likely asking for that comparison to be made between a bunch of floats as your have it written, and therefor failing the equality check when it looks like it should be true.
    3. To find this out and debug your next program on your own, think about how debugging could be done. There are many tools but the simplest is sometimes just to print things. When taking a look myself, the first thing I did was add
    print("snake:")
    print(snake_pos_int)
    print("food")
    print(current_food_positions)
    

    as well as turned the game speed down (sleep time up) so I could watch the output. This allowed me to get an idea if the values were sane, and also notice that the values were printing as floating point numbers with different amounts of rounding (10.0 vs 10.00) which is another indicator you definitely should not be directly comparing them.

    1. This brings me to the "solution"
    from turtle import Turtle, Screen
    import random
    import time
    
    class Snake():
        def __init__(self):
            screen.onkeypress(self.look_left, 'a')
            screen.onkeypress(self.look_right, 'd')
            screen.listen()
    
            self.snake_squares = {
                1: Turtle(),
                2: Turtle(),
                3: Turtle(),
            }
    
            for square in self.snake_squares:
                self.design_square(self.snake_squares[square])
    
            self.snake_head = self.snake_squares[1]
            self.look_left()
    
        def design_square(self, current_square):
            current_square.penup()
            current_square.shape('square')
            current_square.color('white')
            current_square.shapesize(1)
            current_square.pensize(1)
    
        def add_square(self, new_score):
            square_amount = new_score + 3
            self.snake_squares[square_amount] = Turtle()
            self.design_square(self.snake_squares[square_amount])
            self.snake_squares[square_amount].setposition(self.old_position)
    
    
        def look_left(self):
            self.snake_head.left(90)
    
        def look_right(self):
            self.snake_head.right(90)
    
        def move(self):
            self.old_position = self.snake_head.position()
            self.snake_head.forward(10)
    
            all_square_positions = []
    
            for square in self.snake_squares:
                if square != 1:
                    next_square = self.snake_squares[square]
                    new_position = self.old_position
                    self.old_position = next_square.position()
                    next_square.setposition(new_position)
                    all_square_positions.append(new_position)
            return self.snake_head.position(), all_square_positions
    
    
        def check_self_hit(self, head_position, tail_positions):
            if head_position in tail_positions:
                return False
            else:
                return True
    
    
    
    class Board():
        def __init__(self, height):
            self.score_board = Turtle()
            self.score_board.hideturtle()
            self.score_board.setposition(0, height + 10)
            self.score_board.color('white')
            self.score = 0
    
        def add_to_score(self, old_score):
            self.score_board.color('black')
            self.score_board.write(arg=f"Score: {self.score}", font=('Arial', 25, "normal"), align='center')
            self.score = old_score + 1
            self.score_board.color('white')
            self.score_board.write(arg=f"Score: {self.score}", font=('Arial', 25, "normal"), align='center')
            return self.score
    
    
    
    class Border():
        def __init__(self, width, height):
            self.width = width
            self.height = height
            self.positive_borders = [self.width, self.height]
            self.negative_borders = [- self.width, - self.height]
    
    
        def check_borders(self, snake_position):
            no_hit = True
            for border in self.positive_borders:
                if snake_position[0] >= border:
                    no_hit = False
                elif snake_position[1] >= border:
                    no_hit = False
                else:
                    pass
            for border in self.negative_borders:
                if snake_position[0] <= border:
                    no_hit = False
                elif snake_position[1] <= border:
                    no_hit = False
                else:
                    pass
            return no_hit
    
    
        def print_border(self, width, height):
            line = Turtle()
            line.hideturtle()
            line.setposition(-float(width), -float(height))
            line.shape("circle")
            line.speed('fastest')
            line.color('white')
            for wall in range(4):
                line.forward(800)
                line.left(90)
    
    
    class Food():
        def __init__(self, width, height):
            self.width = width
            self.height = height
    
            self.food = Turtle()
            self.food.hideturtle()
            self.food.speed('fastest')
            self.food.penup()
            self.food.pensize(3)
            self.food.shape('circle')
            self.food.color('purple')
    
        def create_food(self):
            self.food.showturtle()
            self.food.clear()
            return self.random_position()
    
    
        def random_position(self):
            x_min = -self.width + 10                #-390
            x_max = self.width - 10                 #390
            y_min = -self.height + 10               #-390
            y_max = self.height - 10                #390
    
            x = 11
            y = 11
            while x % 10 != 0:
                x = random.randint(x_min, x_max)
    
            while y % 10 != 0:
                y = random.randint(y_min, y_max)
    
            self.food.setposition((x, y))
    
            x += 10
            y += 10
    
            all_possible_hits = []
            for hits in range(3):
                all_possible_hits.append((x, y -10))
                x -= 10
    
            for hits in range(3):
                all_possible_hits.append((x+10, y))
                y -= 10
    
            return all_possible_hits
    
    
        def erase_food(self):
            self.food.hideturtle()
    
    
    screen = Screen()
    screen.tracer(0)
    canvas_width = 400
    canvas_height = 400
    screen.bgcolor("black")
    border = Border(canvas_width, canvas_height)
    border.print_border(canvas_width, canvas_height)
    snake = Snake()
    food = Food(canvas_width, canvas_height)
    
    board = Board(canvas_height)
    current_score = board.add_to_score(-1)
    current_food_positions = food.create_food()
    snake_alive = True
    screen.update()
    
    snake_path = []
    while snake_alive:
        time.sleep(0.1)
        screen.update()
        snake_current_position, all_positions = snake.move()
        snake_path.append(snake_current_position)
        if snake.check_self_hit(snake_current_position, all_positions):
            if border.check_borders(snake_current_position):
                snake_pos_int = (round(int(snake_current_position[0])/10)*10, round(int(snake_current_position[1])/10)*10)
                if snake_pos_int in current_food_positions:
                    current_score = board.add_to_score(current_score)
                    snake.add_square(current_score)
                    food.erase_food()
                    current_food_positions = food.create_food()
                    screen.update()
            else:
                snake_alive = False
        else:
            snake_alive = False
    
    screen.exitonclick()
    

    You can note the main change: The previously floating point values are integers (I notice that you even cast the random ints for food to floats, do not do this if you do not need to, in your case this only hurt) and the snake head values are somewhat hacked to be integers (simply casting them to integers was not enough, as in another demonstration of why not to use floats here, at one point one got rounded down to 9, breaking the game, so I apply a rounding to the nearest 10 as well). THIS IS NOT AN IDEAL SOLUTION. It is meant to demonstrate that the problem is the use of floats and that you should be using integers for this all the way through.

    With this hacky solution, I was able to run multiple games out to and past 25 score, so I believe that mostly fixes your main problem. I will leave it to you to fix the secondary but related issue that if your snake grows long enough to run into itself, the collision detection may not work consistently because of the same issue with comparing floats for equality as you do with if head_position in tail_positions:.