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.
A few things here:
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.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.
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:
.