Home > Software engineering >  Python turtle module is not responding to the keystroke that is being issued
Python turtle module is not responding to the keystroke that is being issued

Time:06-30

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:

  1. update positions
  2. rerender
  3. 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()

  • Related