pythonturtle-graphicspython-turtle

Coding whack-a-mole and my keypress won't register for the current mole


I'm trying to get a whack-a-mole game running for a homework assignment. The program executes fine and it generates a random mole that hops between squares. The mole is supposed to be hit using the numberpad at the reference, so 7 on the top-left and so on.

However, whenever it plays, it tells me I miss every time. With experimenting, I found that if I predict where the mole goes, I'll hit it, which means it's not doing the comparison until the next loop. What needs to happen here?

import turtle
import random
import time

t = turtle.Turtle()
t.hideturtle()

mole_x, mole_y = 0, 0

# Set up screen
wn = turtle.Screen()
wn.title("Whack-A-Mole")
wn.bgcolor("green")
wn.setup(width=600, height=600)
wn.tracer(0)


# Draws a square with top-left position (x,y) and side length size
def drawsq(x, y, size):
    t.penup()
    t.goto(x, y)
    t.pendown()
    for i in range(4):
        t.forward(size)
        t.right(90)
    t.penup()


# Draw a circle at center (x,y) with radius r
def drawcr(x, y, r):
    t.penup()
    t.goto(x, y - r)
    t.pendown()
    t.circle(r)

def molecoords():
    coords = [-150, 0, 150]
    x = random.choice(coords)
    y = random.choice(coords)
    return x, y

#Draws the mole
def draw_mole(x, y): 
    drawcr(x, y, 50)  # Body
    drawcr(x - 40, y - 40, 7)  # Left foot
    drawcr(x + 40, y - 40, 7)  # Right foot
    drawcr(x - 55, y + 15, 7)  # Left hand
    drawcr(x + 55, y + 15, 7)  # Right hand
    t.penup()  # Head
    t.goto(x - 45, y + 20)
    t.setheading(-50)
    t.pendown()
    t.circle(60, 100)
    t.setheading(0)
    drawcr(x - 10, y + 35, 2)  # Left eye
    drawcr(x + 10, y + 35, 2)  # Right eye
    drawgrid(x - 7, y + 20, 2, 1, 7)  # Teeth
    t.goto(x, y + 22)  # Nose
    t.fillcolor("black")  # Set the fill color
    t.begin_fill()  # Begin filling
    t.pendown()
    t.left(60)
    t.forward(5)
    t.left(120)
    t.forward(5)
    t.left(120)
    t.forward(5)
    t.end_fill()
    t.setheading(0)

# Draw a grid with x rows and y columns with squares of side length size starting at (tlx,tly)
def drawgrid(tlx, tly, x, y, size):
    for i in range(x):
        for j in range(y):
            drawsq(tlx + (i * size), tly - j * size, size)

def check_hit(key):
    target_positions = {
        '1': (-150, -150),
        '2': (0, -150),
        '3': (150, -150),
        '4': (-150, 0),
        '5': (0, 0),
        '6': (150, 0),
        '7': (-150, 150),
        '8': (0, 150),
        '9': (150, 150)
    }
    target_x, target_y = target_positions.get(key)
    if (mole_x, mole_y) == (target_x, target_y):
        print("Hit!")
    else:
        print("Miss!")

def on_key_press(key):
    check_hit(key)

def game_loop():
    global mole_x, mole_y
    start = time.time()
    duration = 30
    while time.time() - start < duration:
        t.clear()
        drawgrid(-225, 225, 3, 3, 150)
        mole_x, mole_y = molecoords()
        draw_mole(mole_x, mole_y)
        wn.update()
        time.sleep(2)

# Bind key press events
wn.listen()
wn.onkeypress(lambda: on_key_press('1'), '1')
wn.onkeypress(lambda: on_key_press('2'), '2')
wn.onkeypress(lambda: on_key_press('3'), '3')
wn.onkeypress(lambda: on_key_press('4'), '4')
wn.onkeypress(lambda: on_key_press('5'), '5')
wn.onkeypress(lambda: on_key_press('6'), '6')
wn.onkeypress(lambda: on_key_press('7'), '7')
wn.onkeypress(lambda: on_key_press('8'), '8')
wn.onkeypress(lambda: on_key_press('9'), '9')

game_loop()

def gameover():
    t.penup()
    wn.clear()
    wn.bgcolor("black")
    t.goto(0, 100)
    t.pencolor("White")
    t.write("Time's Up!", align="center", font=("Arial", 80, "bold"))

gameover()

turtle.done()

It's reading the inputs properly, just not applying them at the right time.


Solution

  • There's a good deal going on here (I generally suggest minimizing your question code to isolate the issue), but the fundamental problem is using while and sleep in a turtle program. Generally, neither belong. The usual tool to implement loops and events is Screen().ontimer().

    Here's a partial rewrite that cleans up a few things (uses basic docstrings, removes some repetition) and reorganizes the code a bit to accommodate an ontimer design. There's plenty of room for improvement, left as an exercise.

    import random
    from turtle import Screen, Turtle
    
    
    def draw_square(x, y, size):
        """Draws a square with top-left position (x,y) and side length size"""
        t.penup()
        t.goto(x, y)
        t.pendown()
        for i in range(4):
            t.forward(size)
            t.right(90)
        t.penup()
    
    
    def draw_circle(x, y, r):
        """Draw a circle at center (x,y) with radius r"""
        t.penup()
        t.goto(x, y - r)
        t.pendown()
        t.circle(r)
    
    
    def new_mole_position():
        """Chooses random mole coordinates"""
        coords = [-150, 0, 150]
        x = random.choice(coords)
        y = random.choice(coords)
        return x, y
    
    
    def draw_mole(x, y): 
        """Draws the mole at (x, y)"""
        draw_circle(x, y, 50)  # Body
        draw_circle(x - 40, y - 40, 7)  # Left foot
        draw_circle(x + 40, y - 40, 7)  # Right foot
        draw_circle(x - 55, y + 15, 7)  # Left hand
        draw_circle(x + 55, y + 15, 7)  # Right hand
        t.penup()  # Head
        t.goto(x - 45, y + 20)
        t.setheading(-50)
        t.pendown()
        t.circle(60, 100)
        t.setheading(0)
        draw_circle(x - 10, y + 35, 2)  # Left eye
        draw_circle(x + 10, y + 35, 2)  # Right eye
        draw_grid(x - 7, y + 20, 2, 1, 7)  # Teeth
        t.goto(x, y + 22)  # Nose
        t.fillcolor("black")  # Set the fill color
        t.begin_fill()  # Begin filling
        t.pendown()
        t.left(60)
        t.forward(5)
        t.left(120)
        t.forward(5)
        t.left(120)
        t.forward(5)
        t.end_fill()
        t.setheading(0)
    
    
    def draw_grid(tlx, tly, x, y, size):
        """
        Draws a grid with x rows and y columns with
        squares of side length size starting at (tlx,tly)
        """
        for i in range(x):
            for j in range(y):
                draw_square(tlx + (i * size), tly - j * size, size)
    
    
    def check_hit(key):
        """Determines whether a key corresponds to the mole"s location"""
        target_positions = {
            "1": (-150, -150),
            "2": (0, -150),
            "3": (150, -150),
            "4": (-150, 0),
            "5": (0, 0),
            "6": (150, 0),
            "7": (-150, 150),
            "8": (0, 150),
            "9": (150, 150)
        }
        target_x, target_y = target_positions.get(key)
        if (mole_x, mole_y) == (target_x, target_y):
            print("Hit!")
        else:
            print("Miss!")
    
    
    def move_mole():
        """
        Moves the mole and triggers the next mole move
        if the timer hasn"t expired
        """
        global mole_x, mole_y
    
        if game_over:
            return
    
        mole_x, mole_y = new_mole_position()
        t.clear()
        draw_grid(-225, 225, 3, 3, 150)
        draw_mole(mole_x, mole_y)
        wn.update()
        wn.ontimer(move_mole, 2000)
    
    
    def end_game():
        """Ends the game"""
        global game_over
        game_over = True
        t.penup()
        wn.clear()
        wn.bgcolor("black")
        t.goto(0, 100)
        t.pencolor("White")
        t.write("Time's Up!", align="center", font=("Arial", 20, "bold"))
    
    
    def bind_key(k):
        """Bind key k to check hit on the corresponding square"""
        wn.onkeypress(lambda: check_hit(k), k)
    
    
    t = Turtle()
    t.hideturtle()
    mole_x, mole_y = 0, 0
    
    # Set up screen
    wn = Screen()
    wn.tracer(0)
    wn.title("Whack-A-Mole")
    wn.bgcolor("green")
    wn.setup(width=600, height=600)
    
    # Bind key press events
    wn.listen()
    for i in range(1, 10):
        bind_key(str(i))
    
    duration = 30
    game_over = False
    wn.ontimer(end_game, duration * 1000)
    move_mole()
    wn.exitonclick()