2

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()

ggorlen
  • 44,755
  • 7
  • 76
  • 106
漣侑稀
  • 73
  • 7

2 Answers2

2

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 were 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.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Thank you a lot @ggorlen It was truly helpful. If you don't mind however, just so I am not mindlessly copying code and pasting and then trying to figure it out but end up failing. I legit care about understanding yk? I had a few questions about your code. 1 - What's the main purpose of the **grid_size** 2 - Why did you call the tick() inside of the main() 3 - Why did you use the **tick()** as a parameter for the **turtle.ontimer()** (I understand that it needs a high order function, my main question is, why did you call it and pass in the same function you're in as a parameter. – 漣侑稀 Jun 29 '22 at 21:00
  • 1
    `grid_size` is just a more general name for `movement_distance`. When I make a 2d grid game, I always set a `grid_size` variable and use that for everything that is related to the grid, including movement. I guess the parameter to the snake makes more sense as `movement_distance`--it's not too important. – ggorlen Jun 29 '22 at 21:03
  • 1
    I call `tick` inside of `main` to kick off rendering the first frame. I pass `tick` as the `ontimer` callback because it doesn't make sense to define a separate function that does the same thing. It's a loop: we kick off the first tick manually which runs all future ticks on its own. If we ever need to stop this loop, we simply put a condition around `turtle.ontimer` so that it doesn't call itself again. – ggorlen Jun 29 '22 at 21:04
  • oh, okay. yeah I think it's a better term for it I guess. I am a little new to coding so I am sorry if I am a little weird about details. started coding like a few months ago – 漣侑稀 Jun 29 '22 at 21:05
  • Yeah, I get it now. I was a little confused when I saw it at first but now it's very clear. thanks a lot. – 漣侑稀 Jun 29 '22 at 21:07
  • I am sorry I prolly asked you a lot of questions, but for a program like this do you think I should make a different file for every class? so like a file for classes Snake, ScoreBoard, Food. or make them all in the same file? – 漣侑稀 Jun 29 '22 at 21:50
  • 1
    Different files, generally. But if the code is small, a few hundred lines, it's probably not a big deal either way. The important thing is that a human can understand and follow your organization. – ggorlen Jun 29 '22 at 22:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246077/discussion-between--and-ggorlen). – 漣侑稀 Jun 30 '22 at 16:56
1

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()

Bruno Vermeulen
  • 2,970
  • 2
  • 15
  • 29
  • Thanks a lot for your assistance. I truly appreciate it as well. – 漣侑稀 Jun 29 '22 at 21:11
  • I was just reviewing your version of the code and it seemed really interesting. I have a question however, why did you use a lambda in **run_snake(snake, screen)** I am kinda curious about it. Thanks a lot. – 漣侑稀 Jun 29 '22 at 21:26
  • The use of lambda enables giving arguments to a callback function reference. This link explains the matter: https://stackoverflow.com/questions/30769851/commands-in-tkinter-when-to-use-lambda-and-callbacks – Bruno Vermeulen Jun 29 '22 at 23:55