Search code examples
pythonevent-handlinglistenerturtle-graphicspython-turtle

Python Turtle : Handling two successive screens for two successive event-listener driven game Functions


I'm new to Python and trying out Turtle through Tutorials. This question turned out to be verbose, my bad.

I have designed 2 Games handled by 2 Functions (in a separate module) which are called from MAIN:

F1 : Plays an Etch a Sketch Game where user inputs are used to move the Turtle to draw
F2 : Plays a Turtle Race where 5 Turtles (I have used a list to handle 5 Objects) move at randomized paces to see who wins.

The ISSUE :

  • The Games work fine individually on the first iteration but I have a loop in MAIN where user can choose games multiple times. On the second iteration, the screen pops up but I don't see the Turtles and when I click I get Turtle Terminator error.
  • I have initialized a create_screen() method in a separate config.py file which I import in each FN to create a screen. I then use exitonclick() to end the display. Mainloop() didn't work here. (Should I use Global - Just wanted to see how to make it work WITHOUT Global)
  • I need FN1 to stop listening and the ask user if he wants FN2, then move to FN2 and I can't seem to get that done without exitonclick() on FN1. How can I keep the screen alive, displayed, ready for FN2 ? It's the only reason I am using Screen on config file and creating with FNs. I'm sure there is a way, I just don't know it yet.

Solutions I have checked indicate the Turtle._RUNNING = True Flag which can be changed, but what is a better way to implement this without changing a Class Variable (or using Global) ? If someone can point me to the some sources (other than TURTLE DOCS) for Screen and Event Listeners to solve this, I'll do the learning myself !

Code Skeleton : I'm loading config.py into FN Module and the Game Functions from MAIN

# Module : config.py

from turtle import Turtle, Screen

squad = []
for i in range(0, 5):  # List of Turtle Objects available to all Modules
    raphael = Turtle()
    raphael.speed(0)
    squad.append(raphael)

def create_screen():
    """Creates and Returns a screen Object"""
    screen = Screen()
    screen.screensize(400, 400)
    screen.colormode(255)
    return screen
# Module : gamefunction.py

from config import squad, create_screen

def etch_sketch_game():
    """Plays the Etch a Sketch Game"""

    screen_fn = create_screen()

    def forwards():  # Other FNs designed but not included for this Question
        squad[0].fd(30)

    screen_fn.listen()
    screen_fn.onkey(key="w", fun=forwards)
    screen_fn.exitonclick()


def turtle_race(): # Only adde
    """Plays the Turtle Race Game"""

    screen_fn = create_screen()

    squad[0].setpos(-200, 30)
    squad[1].setpos(-200, 20)
    squad[2].setpos(-200, 0)
    squad[3].setpos(-200, -20)
    squad[4].setpos(-200, -30)

    def forwards():  # Other FNs designed but not included for this Question
        squad[0].fd(30)

    screen_fn.onkey(key="w", fun=forwards)
    screen.exitonclick()

Solution

  • Your question is somewhat answered by Python Turtle.Terminator even after using exitonclick() in that as of CPython 3.12, the only way to use exitonclick() more than once is to mess with an internal variable _RUNNING. If you want to work within turtle's public API, short of running subprocesses, you're sort of stuck with one screen for your whole application. This means that you'll need to move the exitonclick() outside each game, to be a one-time call. All games will reuse the same screen.

    With that in mind, Screen() is basically a singleton factory getter for the one screen turtle gives you, so your create_screen is a bit misleadingly named.

    In your application, it seems you want to have the user click to return to the main menu, so we'll use a normal click handler for that rather than exitonclick(). That handler is responsible for clearing out state from the previous game and prompting the user for the next game. For simplicity's sake, I'm using textinput, but in a fancier app this would probably be a clickable turtle button for better UX. Consider this a proof of concept which will require adaptation to your use case.

    I've also moved everything into one file, but you can move it back out to multiple files easily if you want.

    from turtle import Screen, Turtle
    
    
    def new_game():
        """Resets state and lets the user choose a new game"""
        screen.onkey(key="w", fun=None)
        screen.onkey(key="a", fun=None)
        screen.onkey(key="d", fun=None)
    
        for t in turtles:
            t.reset()
            t.hideturtle()
            t.penup()
    
        while True:
            game = screen.textinput(
                "Choose game",
                "Choose game ('sketch', 'race' or 'quit'):"
            )
    
            if not game:
                continue
            elif game.lower().strip() in ("sketch", "race"):
                break
            elif game.lower().strip() == "quit":
                return screen.bye()
    
        if game == "sketch":
            etch_sketch_game()
        else:
            turtle_race()
    
    
    def etch_sketch_game():
        """Runs an etch a sketch game"""
        screen.onkey(key="w", fun=lambda: t.forward(30))
        screen.onkey(key="a", fun=lambda: t.left(30))
        screen.onkey(key="d", fun=lambda: t.right(30))
        screen.listen()
        t = turtles[0]
        t.showturtle()
        t.pendown()
    
    
    def turtle_race():
        """Runs a turtle race game"""
        def forward():
            turtles[0].forward(30)
    
        screen.onkey(key="w", fun=forward)
        screen.listen()
        turtles[0].setpos(-200, 30)
        turtles[1].setpos(-200, 20)
        turtles[2].setpos(-200, 0)
        turtles[3].setpos(-200, -20)
        turtles[4].setpos(-200, -30)
    
        for t in turtles:
            t.showturtle()
            t.pendown()
    
    
    def initialize():
        """Performs one-time setup for entire application"""
        for i in range(5):
            t = Turtle()
            t.speed(0)
            t.hideturtle()
            turtles.append(t)
    
        screen.screensize(400, 400)
        screen.colormode(255)
        screen.onclick(lambda *_: new_game())
        new_game()
        screen.mainloop()
    
    
    # global state; can encapsulate in a module or class as you see fit
    turtles = []
    screen = Screen()
    initialize()
    

    By the way, it's good that you've allocated turtles up front. There's currently no real way to get rid of turtles once they're created, so keeping them in a global object pool is the best approach.

    As another aside, you may not actually need any global state of your own since turtle manages all of the state used in the above program. screen = Screen() can be done per file. Screen().turtles() can be used in place of the turtles = [] list. This might allow you to remove the config file.

    See this answer if you want real-time turtle control, avoiding those pesky keyboard retriggers.