Search code examples
pythontkinterturtle-graphicspython-turtleevent-loop

Turtle-based Python game behaves unexpectedly when it is replayed for the second time


I have created a game using Python's turtle package where you use the Up arrow key to help the turtle cross the screen and avoid obstacles while doing so. It has a Replay button that you can click after you lose in order to reset the game and play again. However, if you lose twice and click the 'Replay' button, the obstacles suddenly start moving faster on the screen and once you lose, the obstacles keep getting generated and appearing over the Game Over message. This does not happen the first two times the game is played.

I believe this is due to some tkinter functionality that I am not aware of. I tried using a debugger to resolve this a few weeks ago and noticed the following in the begin_game function when the turtle hits an obstacle and the game ends:

  1. play_game is set to False because the if cosmo.distance(c) <= 20 evaluates to True

  2. The if cosmo.reach_finish_line condition evaluates to False and this is skipped over

  3. When the if play_game condition is reached, the debugger gets into some tkinter code and at the end of it, goes to the start of the begin_game function and starts executing it. This should not be happening because play_game is set to False, so begin_game should not be recursively called. I even double-checked the value of play_game before this if statement is evaluated, and it was still False.

Does anyone know why this might be happening? Why do the obstacles suddenly move faster when the game is replayed for the second time? And why do they continue to appear on the screen even when the player loses?

Below is the problematic begin_game function. Here is a link to my GitHub repository with the entire project code: https://github.com/karenbraganz/intergalactic-adventure-game if you need more context.

# Begin game when player hits 'Play' or 'Replay' buttons
def begin_game():
    cosmo.showturtle()
    level_display.show_level()
    play_game = True
    screen.update()

    # Call obstacle_generator methods to create obstacles and move them forward
    obstacle_generator.create_obstacles()
    obstacle_generator.obstacle_move()

    # Detect collision between obstacle and turtle
    for c in obstacle_generator.all_obstacles:
        if cosmo.distance(c) <= 20:
            time.sleep(0.3)
            screen.tracer(0)

            # Iterate over obstacle list, hide turtle, and append to recycle list since this game round is over,
            # and they should not be visible on the end game screen
            for num in range(len(obstacle_generator.all_obstacles)):
                obstacle_generator.all_obstacles[num].hideturtle()
                obstacle_generator.recycle.append(obstacle_generator.all_obstacles[num])
            obstacle_generator.all_obstacles = []

            cosmo.hideturtle()

            # Call function to display end game screen
            end_game()

            play_game = False
            break

    # Detect if player has completed current level
    if cosmo.reach_finish_line():
        level_display.refresh_level()
        obstacle_generator.level_up()

    # Recursively call begin_game function if game is still on
    if play_game:
        screen.ontimer(fun=begin_game, t=100)

Solution

  • You are wondering how is it possible that the obstacles speed up ... The cause of the issue is not rooted in tkinter but in your code where the function replay_game() is called multiple times leading to multiple play_game() timer event loops running next to each other. And if there are two or more play_game() loops running, the obstacles are moved by each of these loops and this results then in faster moving obstacles.

    There are two main reasons in your code for the issue you have observed:

    • first reason is that the play_game variable is not global, but it should be global to fit into the control logic of the game code controlled by separate functions.
    • second reason is that the function replay_game() is called multiple times

    How does it come that the function replay_game() is called multiple times? The reason is usage of the add=True option in the onclick() method. This option if set to True results in adding an additional callback function which will be run on click. In case of your code it is the replay_game() function which is added to the list of functions to call when clicking the replay button. With each game end one more call to replay_game() is added and results then in multiple starts of the begin_game() function. Setting add=False does not add another one function and solves this way the observed issue.

    Based on this above the solution to your problem is:

    • make in all functions using play_game this variable global
    • remove the unnecessary play_game = True from begin_game() and set it everywhere in the code before the code calling begin_game()
    • change the add option for registering callback functions to False to prevent multiple runs of same function on one mouse click

    Below the code of "main.py" with the minimal set of changes implementing the above mentioned modifications. This code solves the issue with speeding up the obstacles with each new game restart:

    import time
    from turtle import Screen, Turtle
    
    from obstacle_generator import ObstacleGenerator
    from player import Player
    from scoreboard import Scoreboard
    from welcome_screen import WelcomeScreen
    
    
    # Complete initial setup of game
    def setup(*args):
        screen.setup(width=600, height=600)
        screen.bgcolor("black")
    
        # Display source of turtle icons
        icon_credit.goto(-240, -285)
        icon_credit.color("white")
        icon_credit.write("Icons by Icon8", move=False, align="center", font=FONT)
    
        # Add images used in welcome screen turtle objects to screen's available shapes
        for num in range(1, 5):
            screen.addshape(f"images/welcome_screen{num}.gif")
        screen.addshape("images/next.gif")
        screen.addshape("images/back.gif")
        screen.addshape("images/play.gif")
    
        # Call welcome_screen object methods to display welcome message and instructions
        welcome_screen.welcome1()
    
        screen.addshape("images/turtle.gif")
        cosmo.shape("images/turtle.gif")
        cosmo.hideturtle()
    
        screen.listen()
        screen.onkeypress(fun=cosmo.move_up, key="Up")
    
        # Add images used in obstacle turtle objects to screen's available shapes
        for obs_type in obstacle_generator.types:
            screen.addshape(f"images/{obs_type}.gif")
        check_game_trigger()
    
    
    # Use Turtle's ontimer function to recursively check whether player has pressed 'Play' button for game to begin
    def check_game_trigger():
        if welcome_screen.trigger:
            welcome_screen.goto(-1000, -1000)
            welcome_screen.next_button.goto(-1000, -1000)
            welcome_screen.back_button.goto(-1000, -1000)
            begin_game()
        else:
            screen.update()
            screen.ontimer(check_game_trigger, 100)
    
    
    # Begin game when player hits 'Play' or 'Replay' buttons
    def begin_game():
        global play_game ###<<<###
        cosmo.showturtle()
    
        level_display.show_level()
        # play_game = True ###<<<###
        screen.update()
    
        # Call obstacle_generator methods to create obstacles and move them forward
        obstacle_generator.create_obstacles()
        obstacle_generator.obstacle_move()
    
        # Detect collision between obstacle and turtle
        for c in obstacle_generator.all_obstacles:
            if cosmo.distance(c) <= 20:
                time.sleep(0.3)
                screen.tracer(0)
    
                # Iterate over obstacle list, hide turtle, and append to recycle list since this game round is over,
                # and they should not be visible on the end game screen
                for num in range(len(obstacle_generator.all_obstacles)):
                    obstacle_generator.all_obstacles[num].hideturtle()
                    obstacle_generator.recycle.append(obstacle_generator.all_obstacles[num])
                obstacle_generator.all_obstacles = []
    
                cosmo.hideturtle()
    
                # Call function to display end game screen
                end_game()
    
                play_game = False
                break
    
        # Detect if player has completed current level
        if cosmo.reach_finish_line():
            level_display.refresh_level()
            obstacle_generator.level_up()
    
        # Recursively call begin_game function if game is still on
        if play_game:
            screen.ontimer(fun=begin_game, t=100)
    
    
    def end_game():
        screen.bgcolor("black")
    
        # Add end game screen turtle image shapes to screen's available shapes
        screen.addshape("images/game_over_message.gif")
        screen.addshape("images/alien_monster.gif")
        screen.addshape("images/replay.gif")
        screen.addshape("images/exit.gif")
    
        # Call lose_screen method to display game over message and ask player to replay or exit
        lose_screen.lose_screen()
        lose_screen.exit_button.onclick(fun=exit_screen, add=False) ###<<<###
        lose_screen.replay_button.onclick(fun=replay_game, add=False) ###<<<###
        screen.update()
    
    
    # Reset screen if player chooses to replay and call the begin_game function
    def replay_game(*args):
        global play_game ###<<<###
    
        lose_screen.goto(-1000, -1000)
        lose_screen.exit_button.goto(-1000, -1000)
        lose_screen.replay_button.goto(-1000, -1000)
        lose_screen.game_over_message.goto(-1000, -1000)
        obstacle_generator.level = 0
        level_display.clear()
        level_display.level = 0
        cosmo.goto(cosmo.starting_pos)
    
        play_game = True
        begin_game()
    
    
    # Exit game screen if player clicks on the 'Exit' button
    def exit_screen(*args):
        screen.clearscreen()
        screen.bye()
    
    
    # Initialize objects used in game and call setup function
    if __name__ == "__main__":
        screen = Screen()
        screen.tracer(0)
        cosmo = Player()
        welcome_screen = WelcomeScreen()
        obstacle_generator = ObstacleGenerator()
        lose_screen = WelcomeScreen()
        level_display = Scoreboard()
        icon_credit = Turtle()
        icon_credit.hideturtle()
        FONT = ("Courier", 8, "normal")
        setup()
        play_game = True
        screen.mainloop()