So far, my snake game is doing somewhat okay. One issue is that for some reason the "turtle" isn't responding to the keystrokes, and I really don't know why. I tried a lot of different stuff but it was all useless. The main problem is that I am not entirely sure where the main issue is. What I know for sure is that the problem is most likely from my code, but I can't seem to find it. If you could assist me in solving this issue that would great.
import time
from turtle import Screen, Turtle
STARTING_X_POSITIONS = [0, -20, -40]
MOVEMENT_DISTANCE = 20
class Snake:
def __init__(self):
self.segments = []
self.create_snake()
self.head = self.segments[0]
def create_snake(self):
for i in range(3):
new_snake = Turtle('square')
new_snake.color('RoyalBlue')
new_snake.penup()
new_snake.goto(STARTING_X_POSITIONS[i], 0)
self.segments.append(new_snake)
def move(self):
# We Want The Loop To Start At Index (2) And Decrease Itself Till It Reaches Zero (Excluded)
for snake_index in range(len(self.segments) - 1, 0, -1):
x_pos = self.segments[snake_index - 1].xcor()
y_pos = self.segments[snake_index - 1].ycor()
self.segments[snake_index].goto(x_pos, y_pos)
self.segments[0].forward(MOVEMENT_DISTANCE)
def up(self):
self.head.setheading(90)
def down(self):
self.head.setheading(270)
def left(self):
self.head.setheading(180)
def right(self):
self.head.setheading(0)
def setup_screen(screen):
screen.bgcolor('black')
screen.title('Snake Game')
screen.setup(width=600, height=600)
screen.tracer(0)
def start_game(screen, snake):
setup_screen(screen)
game_on = True
while game_on:
screen.update()
time.sleep(0.1)
snake.move()
def control_snake(screen, snake):
screen.listen()
screen.onkey(key='Up', fun=snake.up)
screen.onkey(key='Down', fun=snake.down)
screen.onkey(key='Left', fun=snake.left)
screen.onkey(key='Right', fun=snake.right)
screen.exitonclick()
def main():
screen = Screen()
snake = Snake()
start_game(screen, snake)
control_snake(screen, snake)
if __name__ == '__main__':
main()
CodePudding user response:
This is a good example of the importance of minimizing the code when you debug. Consider the code here:
def start_game(screen, snake):
game_on = True
while game_on: # infinite loop
screen.update()
time.sleep(0.1)
snake.move()
def control_snake(screen, snake):
# add key listeners, the failing behavior
def main():
# ...
start_game(screen, snake)
control_snake(screen, snake)
main
calls start_game
, but start_game
has an infinite while
loop in it. game_on
is never set to False
, and so control_snake
will never be reached.
Try adding key listeners before you go into your infinite rendering loop, not after.
Moving control_snake
ahead of start_game
introduces a new problem, which is that screen.exitonclick()
is part of control_snake
, but if control_snake
is called before start_game
, then the screen.exitonclick()
blocks and prevents start_game
from running. So we need to remove screen.exitonclick()
.
But there's a better way to trigger repeated events than while
/sleep
, which is screen.ontimer
. This lets you defer control back to your main loop and block on a screen.exitonclick()
call. This post shows an example.
Taking a step back, here are a few other tips that address underlying misconceptions and root causes of your bugs.
It's a bit odd that setup_screen
is called from start_game
. I'd call setup_screen
from main
to decouple these. I can imagine a case where we want to set up the screen once, but restart the game multiple times, for example, after the snake dies.
In general, I'd worry less about breaking things out into functions until you have the basic code working. Don't write abstractions just because you've heard that functions longer than 5 or 6 lines are bad. The functions should have a clear, singular purpose foremost and avoid odd dependencies.
For example, control_snake
should really be called add_snake_controls_then_block_until_exit
or something like that, because not only does it add snake controls (it doesn't really "control the snake" exactly, it registers the controls that do so), it also blocks the whole script and runs turtle's internal update loop until the user clicks the window. This might sound pedantic, but if you'd named this function to state exactly what it does, the bug would be much more obvious, with the side benefit of clearer code in general.
Your game loop code:
while game_on:
screen.update()
time.sleep(0.1)
snake.move()
is a bit confusing to follow. The usual rendering sequence is:
- update positions
- rerender
- sleep/defer control until the next update cycle
I suggest the clearer
while game_on:
snake.move() # update positions
screen.update() # render the frame
time.sleep(0.1) # defer/pause until the next tick
Another tip/rule of thumb is to work in small chunks, running your code often. It looks like you wrote a huge amount of code, then ran it for the first time and weren't sure where to begin debugging. If I was writing a snake game, I wouldn't worry about the tail logic until I've set up the head and established that it works, for example.
If you do wind up with a lot of code and a bug in spite of your best efforts, systematically add prints to see where control is reached. If you added a print in control_snake
, you'd see it never gets called, which pretty much gives away the problem (and therefore its solution).
Another debugging strategy is to remove code until the problem goes away, then bring back the last chunk to see exactly what the problem was.
All that said, your Snake
class seems purposeful and well-written.
Here's my rewrite suggestion:
import turtle
class Snake:
def __init__(self, grid_size, initial_x_positions):
self.grid_size = grid_size
self.create_snake(initial_x_positions)
def create_snake(self, initial_x_positions):
self.segments = []
for x in initial_x_positions:
segment = turtle.Turtle("square")
segment.color("RoyalBlue")
segment.penup()
segment.goto(x, 0)
self.segments.append(segment)
self.head = self.segments[0]
def move(self):
for i in range(len(self.segments) - 1, 0, -1):
x_pos = self.segments[i - 1].xcor()
y_pos = self.segments[i - 1].ycor()
self.segments[i].goto(x_pos, y_pos)
self.head.forward(self.grid_size)
def up(self):
self.head.setheading(90)
def down(self):
self.head.setheading(270)
def left(self):
self.head.setheading(180)
def right(self):
self.head.setheading(0)
def create_screen():
screen = turtle.Screen()
screen.tracer(0)
screen.bgcolor("black")
screen.title("Snake Game")
screen.setup(width=600, height=600)
screen.listen()
return screen
def main():
initial_x_positions = 0, -20, -40
frame_delay_ms = 80
grid_size = 20
screen = create_screen()
snake = Snake(grid_size, initial_x_positions)
screen.onkey(key="Up", fun=snake.up)
screen.onkey(key="Down", fun=snake.down)
screen.onkey(key="Left", fun=snake.left)
screen.onkey(key="Right", fun=snake.right)
def tick():
snake.move()
screen.update()
turtle.ontimer(tick, frame_delay_ms)
tick()
screen.exitonclick()
if __name__ == "__main__":
main()
Since there's no restarting condition or accompanying logic, this will probably need to be refactored to allow for a "game over" screen and resetting the snake or something like that, but at least it's solid and there aren't a lot of premature abstractions to have to reason about.
CodePudding user response:
I got it working as follows
import turtle
STARTING_X_POSITIONS = [0, -20, -40]
MOVEMENT_DISTANCE = 20
frame_delay_ms = 80
class Snake:
def __init__(self, screen):
self.screen = screen
self.control_snake()
self.segments = []
self.create_snake()
self.head = self.segments[0]
def create_snake(self):
for i in range(3):
new_snake = turtle.Turtle('square')
new_snake.color('RoyalBlue')
new_snake.penup()
new_snake.goto(STARTING_X_POSITIONS[i], 0)
self.segments.append(new_snake)
def control_snake(self):
self.screen.onkey(key='Up', fun=self.up)
self.screen.onkey(key='Down', fun=self.down)
self.screen.onkey(key='Left', fun=self.left)
self.screen.onkey(key='Right', fun=self.right)
self.screen.listen()
def move(self):
# We Want The Loop To Start At Index (2) And Decrease Itself Till It Reaches Zero (Excluded)
for snake_index in range(len(self.segments) - 1, 0, -1):
x_pos = self.segments[snake_index - 1].xcor()
y_pos = self.segments[snake_index - 1].ycor()
self.segments[snake_index].goto(x_pos, y_pos)
self.segments[0].forward(MOVEMENT_DISTANCE)
def up(self):
self.head.setheading(90)
def down(self):
self.head.setheading(270)
def left(self):
self.head.setheading(180)
def right(self):
self.head.setheading(0)
class ScreenSetup:
def __init__(self):
self._screen = turtle.Screen()
self.setup_screen()
def setup_screen(self):
self._screen.bgcolor('black')
self._screen.title('Snake Game')
self._screen.setup(width=600, height=600)
self._screen.tracer(0)
@property
def screen(self):
return self._screen
def run_snake(snake, screen):
snake.move()
screen.update()
turtle.ontimer(lambda: run_snake(snake, screen), frame_delay_ms)
def main():
screen = ScreenSetup().screen
snake = Snake(screen)
run_snake(snake, screen)
screen.exitonclick()
if __name__ == '__main__':
main()