pythonturtle-graphicspython-turtleonkeypress

Dynamically passing Turtles to onkeypress() binds always only one of them


I'm working with Turtle, recreating the pong game as part of an intro course. I have 2 turtle objects, called paddles, stored in a dict with the relevant keys to move them, see below. I'm trying using a nested for loop for the binding with onkeypress(). As suggested elsewhere, I use the lambda function to pass the direction 'way' to the 'move' method, which I have in the Paddle class in another module.

Expected behavior in the MRE:

  1. on pressing the up or down arrows, "move method, side = right" is written
  2. on pressing "w" or "s", "move method, side = left" "left is recognized" is written

Behavior: only (1) works, (2) doesnt.

The main:

from turtle import Screen
from paddle import Paddle, PADDLE_KEY_MAP

screen = Screen()

paddles = PADDLE_KEY_MAP
# creates the paddles and adds it to the dictionary
for side in paddles:
    paddles[side]["paddle"] = Paddle(side=side)


print(paddles) # for debugging

screen.update()
screen.listen()

for side in paddles:
    for way in paddles[side]["way"]:
        key = paddles[side]["way"][way]["key"]
        print(side, way, key) # for debugging
        screen.onkeypress(lambda way=way, key=key:
                          paddles[side]["paddle"].move(way), paddles[side]["way"][way]["key"])

    screen.update()

screen.exitonclick()

and the paddles module. (Yes, the dict is ugly, I'm practicing only.)

from turtle import Turtle

PADDLE_KEY_MAP = {
    "left":
        {
            "way":
                {
                    "up":
                        {
                            "key": "w",
                         },
                    "down":
                        {
                            "key": "s",
                        },
                }
        },
    "right":
        {
            "way":
                {
                    "up":
                        {
                            "key": "Up",
                         },
                    "down":
                        {
                            "key": "Down",
                        },
                }
        }
}


class Paddle:

    def __init__(self, side):
        self.paddle = Turtle()
        self.side = side

    def move(self, way):
        print("move method, side = ", self.side)
        # showcasing the error: left is never recognized
        if self.side == "left":
             print("left is recognized")

If I explicitly use just one of the paddles in onkeypress(), then the binding works for either side, and the expected behavior occurs for that side. But it never works if both paddles are looped through. I also tried enforcing paddles within the for loop (if side == left then paddle = left paddle, else the right), but this failed too.

So I narrowed this down to the onkeypress statement and/or the lambda function. Hope this makes sense. Any suggestions? Thanks in advance


Solution

  • The issue is that those loop variables aren't bound to a closure, so they'll always have whatever the last values they had when the loop ended. Add a function closure to persist each iteration of the loop:

    from turtle import Screen, Turtle
    
    
    PADDLE_KEY_MAP = {
        "left": {
            "way": {
                "up": {
                    "key": "w",
                },
                "down": {
                    "key": "s",
                },
            }
        },
        "right": {
            "way": {
                "up": {
                    "key": "Up",
                },
                "down": {
                    "key": "Down",
                },
            }
        },
    }
    
    
    class Paddle:
        def __init__(self, side):
            self.paddle = Turtle()
            self.side = side
    
        def move(self, way):
            print(self.side, way)
    
    
    paddles = PADDLE_KEY_MAP
    for side in paddles:
        paddles[side]["paddle"] = Paddle(side)
    
    screen = Screen()
    screen.listen()
    
    
    def bind(side, way):
        screen.onkeypress(
            lambda: paddles[side]["paddle"].move(way),
            paddles[side]["way"][way]["key"]
        )
    
    
    for side in paddles:
        for way in paddles[side]["way"]:
            bind(side, way)
    
    screen.exitonclick()
    

    The syntax you had for onkeypress was also incorrect and fixed above.

    See How to bind several key presses together in turtle graphics? for a complete, working example of the bind() function.

    In general, you shouldn't need this large nested dict for such a simple keybinding operation--the approach feels overengineered. Try to avoid complicated nesting, boolean parameters (yes, you're using a string, but it's effectively a yes/no boolean), and premature generalization. There's only two paddles and two movements, so enumerate them explicitly:

    from turtle import Screen, Turtle
    
    
    class Paddle:
        def __init__(self):
            self.paddle = Turtle()
    
        def move_up(self):
            ...
    
        def move_down(self):
            ...
    
    
    p1 = Paddle()
    p2 = Paddle()
    screen = Screen()
    screen.listen()
    screen.onkeypress(p1.move_up, "w")
    screen.onkeypress(p2.move_down, "s")
    screen.onkeypress(p1.move_up, "Up")
    screen.onkeypress(p2.move_down, "Down")
    screen.exitonclick()
    

    For more complete versions of pong in turtle, showing the next steps in this design, see: