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:
play_game is set to False because the if cosmo.distance(c) <= 20 evaluates to True
The if cosmo.reach_finish_line condition evaluates to False and this is skipped over
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.
# 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)
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:
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.replay_game()
is called multiple timesHow 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:
play_game
this variable globalplay_game = True
from begin_game()
and set it everywhere in the code before the code calling begin_game()
add
option for registering callback functions to False
to prevent multiple runs of same function on one mouse clickBelow 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()