My First Raspberry Pi Game – Part 11 – Being less rude

Parts: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.

Writing your first ever computer program on the Raspberry Pi.

We’ve nearly finished our game. Next on our list is to fix that bug where you can’t exit some of the time, and make our code a bit tidier in the process.

The first thing I want to do is make the Esc key quit the game. This is fairly normal behaviour, and will help if we run in full screen mode, where there is no close button to click.

Open up redgreen.py in LeafPad as usual, and find the function shape_wait and the line if evt.type == pygame.QUIT:. Replace the whole line with this:

        if is_quit( evt ):

We’ve replaced the code asking whether the event was a quit event (i.e. the user closed the window) with a call to a function. Let’s write that function. Just above shape_wait type in this function:

def is_quit( evt ):
    return evt.type == pygame.QUIT

You’ve just done another bit of refactoring. Instead of writing the code directly in the if line, we’ve added a call to the function, and the function does exactly the same thing as we did before: it returns True if the event is a quit event, and False otherwise. If you try the program now (by saving in LeafPad, opening LXTerminal and typing ./redgreen.py) you should see it behaves exactly as it did before.

You may well ask why we did it. The answer is because now we can change the is_quit function to do something extra. Replace it with this:

def is_quit( evt ):
    return (
        evt.type == pygame.QUIT or
        (
            evt.type == pygame.KEYDOWN and
            evt.key == pygame.K_ESCAPE
        )
    )

This is a more complicated bit of logic, saying that we will return True if either of two things is true: EITHER the event is a quit event (as before), OR the event is a KEYDOWN, and the specific key that was pressed was the Escape key (“Esc”) on the keyboard. Notice that the brackets around the “and” part help us know which bits go together – we don’t want to quit for any keypress event, only one where the key is Escape.

If you try your program again, you should find you can press Escape to exit when you’re looking at a red or green shape.

The shape_wait function is quite a useful one, and the next thing we want to do is use it in a few more places. Before we can do that, we need to refactor it to make it a bit more flexible.

Make a new function called timed_wait further up, just before start, and cut the entire body of shape_wait and paste it into timed_wait. So it looks like this:

def timed_wait():
    event_types_that_cancel = pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN
    ... all the rest of shape_wait here ...
    pygame.time.set_timer( finished_waiting_event_id, 0 )

Now change shape_wait to look like this:

def shape_wait():
    """
    Wait while we display a shape.  Return True if a key was pressed,
    or false otherwise.
    """
    return timed_wait()

As usual, we’ve just replaced some code with a call to a function that contains the exact same code, so hopefully our program will work exactly as before.

Now we’re going to make timed_wait a bit more general, while still preserving all the same behaviour. We do this by changing some of the variables we use in timed_wait into arguments we pass in. Change shape_wait to look like this:

def shape_wait():
    """
    Wait while we display a shape.  Return True if a key was pressed,
    or false otherwise.
    """
    press_events = pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN 
    return timed_wait( 2000, press_events ) # 2 seconds

and modify timed_wait to accept those arguments (notice I also added a description of what it does):

def timed_wait( time_to_wait, event_types_that_cancel ):
    """
    Wait for time_to_wait, but cancel if a relevant event happens.
    Return True if cancelled, or False if we waited the full time.
    """

    finished_waiting_event_id = pygame.USEREVENT + 1
    pygame.time.set_timer( finished_waiting_event_id, time_to_wait )

    pygame.event.clear()

    pressed = False
    waiting = True
    while waiting:
        evt = pygame.event.wait()
        if is_quit( evt ):
            quit()
        elif evt.type in event_types_that_cancel:
            waiting = False
            pressed = True
        elif evt.type == finished_waiting_event_id:
            waiting = False

    pygame.time.set_timer( finished_waiting_event_id, 0 )

    return pressed

Again, after these changes our program should work exactly as before – we’re passing values in as arguments that are exactly what we used to make as variables. But now, timed_wait is a lot more flexible, and we’ll use that flexibility very soon.

But first, we need to make a change to cover the unexpected. Inside timed_wait we’ve made a timer using pygame.time.set_timer and at the end we’ve cancelled it by calling pygame.time.set_timer again. However, if something goes wrong in between where we create the timer, and where we cancel it, it’s possible that something called an “exception” will be “thrown”. When an exception is thrown, the program stops running normally, line by line, and jumps out to somewhere else.

I’m not going to explain any more about exceptions here, but I am going to show you how to make absolutely sure that something will happen, even if an exception is thrown. The way to do that is to use a try ... finally block. We want to make sure our timer is always cancelled, so as soon as we’ve made it, we start a try block, and at the end we say finally. Anything inside that finally block will be run, even if an exception was thrown in the code inside the try block. The changes look like this:

def timed_wait( time_to_wait, event_types_that_cancel ):
    """
    Wait for time_to_wait, but cancel if a relevant event happens.
    Return True if cancelled, or False if we waited the full time.
    """

    finished_waiting_event_id = pygame.USEREVENT + 1
    pygame.time.set_timer( finished_waiting_event_id, time_to_wait )

    try:
        pygame.event.clear()

        pressed = False
        waiting = True
        while waiting:
            evt = pygame.event.wait()
            if is_quit( evt ):
                quit()
            elif evt.type in event_types_that_cancel:
                waiting = False
                pressed = True
            elif evt.type == finished_waiting_event_id:
                waiting = False
    finally:
        pygame.time.set_timer( finished_waiting_event_id, 0 )

    return pressed

The lines in green are new, and the ones in blue are just indented by four more spaces to make them part of the try and finally blocks. Now, we know that even if something goes wrong while we’re waiting, we will always cancel the timer we set up. Yet more good manners!

Now, after all that work, we finally have a timed_wait function that is flexible enough to be used everywhere we want to wait for something. Let’s start with the wait function. Change it to look like this:

def wait():
    time_to_wait = random.randint( 1500, 3000 ) # Between 1.5 and 3 seconds
    timed_wait( time_to_wait, () )

By using our clever timed_wait function instead of the built-in pygame.time.wait we gain some extra politeness: we can now quit the program on the “Ready?” screen by closing the window or pressing the Escape key. Try it!

Notice that we passed in () as the second argument to timed_wait. This argument is called event_types_that_cancel and is normally a list of types of event that will stop us waiting. () is Python’s way of saying an empty list, so we’re saying we don’t want to stop for any normal events (such as key presses) – only for quit events, or when the time is up.

Before we change lots more code to use timed_wait, we are going to make a new variable that we can use in lots of places in the code. Quite a few times, we want to wait for either a key press or a mouse click. We want this when we’re showing a red or green square, and when we’re at the end saying goodbye, and ideally we also want it when we’re telling the user how they did, so they can skip it if they’re impatient. So, right near the top, add a new line just below where we create screen_size:

screen_width = 640
screen_height = 480
screen_size = screen_width, screen_height
press_events = pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN

This variable press_events will be our list of normal event types that we consider to be a “press” – essentially, the player doing something. Now that we’ve defined this at the top, we can take out the variable with the same name from shape_wait – it will use the global one instead:

def shape_wait():
    """
    Wait while we display a shape.  Return True if a key was pressed,
    or false otherwise.
    """
    return timed_wait( 2000, press_events ) # 2 seconds

We can also re-use press_events in the end function, and at the same time call our new timed_wait function:

def end():
    screen.fill( pygame.Color( "black" ) )
    white = pygame.Color( "white" )
    write_text( screen, "Thanks for playing!", white, True )
    write_text( screen, "Press a key to exit", white, False )
    pygame.display.flip()
    pygame.event.clear()
    timed_wait( 0, press_events )

Notice that this time we pass zero as the time to wait – this just means we will never time out on this screen – the zero gets passed in and used in the pygame.time.set_timer call, but passing in zero for the time there means “cancel this event”, and is harmless if the event doesn’t actually exist, so no timer will be set up – we will only stop waiting when the player presses something, which is what we want here.

Now we can make our success and failure functions more polite. Change them all to look like this:

def green_success():
    tick()
    green = pygame.Color( "green" )
    white = pygame.Color( "white" )
    write_text( screen, "Well done!", green, True )
    write_text( screen, "You pressed on green!", white, False )
    pygame.display.flip()
    timed_wait( 2000, press_events ) # 2 seconds

def green_failure():
    cross()
    red   = pygame.Color( "red" )
    white = pygame.Color( "white" )
    write_text( screen, "Bad Luck!", red, True )
    write_text( screen, "Green means press something!", white, False )
    pygame.display.flip()
    timed_wait( 2000, press_events ) # 2 seconds

def red_success():
    tick()
    green = pygame.Color( "green" )
    white = pygame.Color( "white" )
    write_text( screen, "Well done!", green, True )
    write_text( screen, "You didn't press on red!", white, False )
    pygame.display.flip()
    timed_wait( 2000, press_events ) # 2 seconds

def red_failure():
    cross()
    red   = pygame.Color( "red" )
    white = pygame.Color( "white" )
    write_text( screen, "Bad Luck!", red, True )
    write_text( screen, "Red means don't press anything!", white, False )
    pygame.display.flip()
    timed_wait( 2000, press_events ) # 2 seconds

They all call timed_wait saying wait for 2 seconds, but skip if a key is pressed because the player is impatient to get on to the next round. This change means not only can you skip past these success and failure screens, but also you can quit while they are visible, and the last vestige of rudeness has been wiped out from our game.

Well done – just one job left, which is to allow several rounds, and count the player’s score as they play. We’ll do that next time.

In the meantime, you can fix a bug I made – I typed get_width instead of get_height, which made my circles too big. Change the line inside green_shape that looks like radius = screen.get_width() / 3 to look like this:

    radius = screen.get_height() / 3

There we are – much better

You can check your verson against mine here: redgreen.py

See you next time, when hopefully we’ll finish the game!