Build a simple reaction timing game using an LED and Push-buttons, for one or two players
Microcontrollers aren’t only found in industrial devices: they power plenty of electronics around the home, including toys and games. In this tutorial you’re going to build a simple reaction timing game, seeing who among your friends will be the first to press a button when a light goes off.
The study of reaction time is known as mental chronometry and while it forms a hard science, it is also the basis of plenty of skill-based games –including the one you’re about to build. Your reaction time – the time it takes your brain to process the need to do something and send the signals to make that something happen – is measured in milliseconds: the average human reaction time is around 200–250 milliseconds, though some people enjoy considerably faster reaction times that will give them a real edge in the game!
For this project you’ll need your Pico, a breadboard, an LED of any colour, a single 330 Ω resistor, two push-button switches, and a selection of male-to-male (M2M) jumper wires. You’ll also need a micro USB cable, and to connect your Pico to your Raspberry Pi or other computer running the Thonny MicroPython IDE.
A single-player game
Start by placing your LED into your breadboard so that it straddles the centre divide. Remember that LEDs only work when they’re the right way around: make sure you know which is the longer leg, or the anode, and which is the shorter leg, the cathode. Using a 330 Ω current-limiting resistor, to protect both the LED and your Pico, wire the longer leg of the LED to pin GP15 at the bottom-left of your Pico as seen from the top with the micro USB cable uppermost. If you’re using a numbered breadboard and have your Pico inserted at the very top, this will be breadboard row 20.
Take a jumper wire and connect the shorter leg of the LED to your breadboard’s ground rail. Take another, and connect the ground rail to one of your Pico’s ground (GND) pins – in Figure 1, we’ve used the ground pin on row three of the breadboard. Next, add the push-button switch as shown in Figure 1. Take a jumper wire and connect one of the push-button’s switches to pin GP14, right next to the pin you used for your LED. Use another jumper wire to connect the other leg – the one diagonally opposite the first, if you’re using a four-leg pushbutton switch – to your breadboard’s power rail. Finally, take a last jumper wire and connect the power rail to your Pico’s 3V3 pin.
Your circuit now has everything it needs to act as a simple single-player game: the LED is the output device, taking the place of the TV you would normally use with a games console; the pushbutton switch is the controller; and your Pico is the games console, albeit one considerably smaller than you’d usually see! Now you need to actually write the game. As always, connect your Pico to your Raspberry Pi or other computer and load Thonny. Create a new program, and start it by importing the machine library so you can control your Pico’s GPIO pins:
import machine
You’re also going to need the utime library:
import utime
In addition, you’ll need a new library: urandom, which handles creating random numbers – a key part of making a game fun, and used in this game to prevent a player who has played it before from simply counting down a fixed number of seconds from clicking the Run button.
Next, set a pressed variable to False (more on this later) and set up the two pins you’re using: GP15 for the LED, and GP14 for the push-button switch.
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN,
machine.Pin.PULL_DOWN)
In previous Pico tutorials, you’ve handled pushbutton switches in either the main program or in a separate thread. This time, though, you’re going to take a different and more flexible approach: interrupt requests, or IRQs. The name sounds complex, but it’s really simple: imagine you’re reading a book, page by page, and someone comes up to you and asks you a question. That person is performing an interrupt request: asking you to stop what you’re doing, answer their question, then letting you go back to reading your book.
A MicroPython interrupt request works in exactly the same way: it allows something, in this case the press of a push-button switch, to interrupt the main program. In some ways it’s similar to a thread, in that there’s a chunk of code which sits outside the main program. Unlike a thread, though, the code isn’t constantly running: it only runs when the interrupt is triggered.
Start by defining a handler for the interrupt. This, known as a callback function, is the code which runs when the interrupt is triggered. As with any kind of nested code, the handler’s code – everything after the first line – needs to be indented by four spaces for each level; Thonny will do this for you automatically.
def button_handler(pin):
global pressed
if not pressed:
pressed=True
print(pin)
This handler starts by checking the status of the pressed variable and then setting it to True to ignore further button presses (thus ending the game). It then prints out information about the pin responsible for triggering the interrupt. That’s not too important at the moment – you only have one pin configured as an input, GP14, so the interrupt will always come from that pin – but lets you test your interrupt easily.
You’ll need a new library: urandom, which handles creating random numbers
Continue your program below, remembering to delete the indent that Thonny has automatically created – the following code is not part of the handler:
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
(https://github.com/themagpimag/magpi-issue105)
This code will be immediately familiar to you: the first line turns the LED, connected to pin GP15, on; the next line pauses the program; the last line turns the LED off again – the player’s signal to push the button. Rather than using a fixed delay, however, it makes use of the urandom library to pause the program for between five and ten seconds – the ‘uniform’ part referring to a uniform distribution between those two numbers.
At the moment, though, there’s nothing watching for the button being pushed. You need to set up the interrupt for that, by typing in the following line at the bottom of your program:
button.irq(trigger=machine.Pin.IRQ_RISING,
handler=button_handler)
Setting up an interrupt requires two things: a trigger and a handler. The trigger tells your Pico what it should be looking for as a valid signal to interrupt what it’s doing; the handler, which you defined earlier in your program, is the code which runs after the interrupt is triggered.
In this program your trigger is IRQ_RISING: this means the interrupt is triggered when the pin’s value rises from low, its default state thanks to the built-in pull-down resistor, to high, when the button connected to 3V3 is pushed. A trigger of IRQ_FALLING would do the opposite: trigger the interrupt when the pin goes from high to low. In the case of your circuit, IRQ_RISING will trigger as soon as the button is pushed; IRQ_FALLING would trigger only when the button is released. Your program should look like this:
import machine
import utime
import urandom
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN,
machine.Pin.PULL_DOWN)
def button_handler(pin):
global pressed
if not pressed:
pressed=True
print(pin)
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
button.irq(trigger=machine.Pin.IRQ_RISING,
handler=button_handler)
Click the Run button and save the program to your Pico as Reaction_Game.py. You’ll see the LED light up: that’s your signal to get ready with your finger on the button. When the LED goes out, press the button as quickly as you can.
When you press the button, it triggers the handler code you wrote earlier. Look at the Shell area: you’ll see your Pico has printed a message, confirming that the interrupt was triggered by pin GP14. You’ll also see another detail: mode=IN tells you the pin was configured as an input.
That message doesn’t make for much of a game, though: for that, you need a way to time the player’s reaction speed. Start by deleting the line
print(pin) from your button handler – you don’t need it any more. Go to the bottom of your program and add a new line, just above where you set up the interrupt:
timer_start = utime.ticks_ms()
This creates a new variable called timer_start and fills it with the output of the utime.ticks_ms() function, which counts the number of milliseconds that have elapsed since the utime library began counting. This provides a reference point: the time just after the LED went out and just before the interrupt trigger became ready to read the button press.
Next, go back to your button handler and add the following two lines, remembering that they’ll need to be indented by four spaces so MicroPython knows they form part of the nested code:
timer_reaction = utime.ticks_diff(utime.
ticks_ms(), timer_start)
print("Your reaction time was " +
str(timer_reaction) + " milliseconds!")
The first line creates another variable, this time for when the interrupt was actually triggered – in other words, when you pressed the button. Rather than simply taking a reading from utime.ticks_ms() as before, though, it uses utime.ticks_diff() – a function which provides the difference between when this line of code is triggered and the reference point held in the variable timer_start. The second line prints the result, but uses concatenation to format it nicely. The first bit of text, or string, tells the user what the number that follows means; the + means that whatever comes next should be printed alongside that string. In this case, what comes next is the contents of the timer_reaction variable – the difference, in milliseconds, between when you took the reference point for the timer and when the button was pushed and the interrupt triggered.
Finally, the last line concatenates one more string so the user knows the number is measured in milliseconds, and not some other unit like seconds
or microseconds. Pay attention to spacing: you’ll see that there’s a trailing space after ‘was’ and before the end quote of the first string, and a leading
space after the open quote of the second string and before the word ‘milliseconds’. Without those, the concatenated string will print something like ‘Your reaction time was323milliseconds’.
Your program should now look like this:
import machine
import utime
import urandom
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN,
machine.Pin.PULL_DOWN)
def button_handler(pin):
global pressed
if not pressed:
pressed=True
timer_reaction = utime.ticks_
diff(utime.ticks_ms(), timer_start)
print("Your reaction time was " +
str(timer_reaction) + " milliseconds!")
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
timer_start = utime.ticks_ms()
button.irq(trigger=machine.Pin.IRQ_RISING,
handler=button_handler)
Click the Run button again, wait for the LED to go out, and push the button. This time, instead of a report on the pin which triggered the interrupt,
you’ll see a line telling you how quickly you pushed the button – a measurement of your reaction time. Click the Run button again and see if you can push the button more quickly this time – in this game, you’re trying for as low a score as possible!
A two-player game
Single-player games are fun, but getting your friends involved is even better. You can start by inviting them to play your game and comparing
your high – or, rather, low – scores to see who has the quickest reaction time. Then, you can modify your game to let you go head-to-head!
Start by adding a second button to your circuit. Wire it up the same as the first button, with one leg going to the power rail of your breadboard but with the other going to pin GP16 – the pin across the board from GP14 where the LED is connected, at the opposite corner of your Pico. Make sure the two buttons are spaced far enough apart that each player has room to put their finger on their button. Your finished circuit should look like Figure 2.
Although your second button is now connected to your Pico, it doesn’t know what to do with it yet. Go back to your program in Thonny and find where you set up the first button. Directly beneath this line, add:
right_button = machine.Pin(16, machine.Pin.
IN, machine.Pin.PULL_DOWN)
You’ll notice that the name now specifies which button you’re working with: the right-hand button on the breadboard. To avoid confusion, edit the line above so that you make it clear what was the only button on the board is now the lefthand button:
left_button = machine.Pin(14, machine.Pin.IN,
machine.Pin.PULL_DOWN)
You’ll need to make the same change elsewhere in your program, too. Scroll to the bottom of your code and change the line that sets up the interrupt trigger to:
left_button.irq(trigger=machine.Pin.IRQ_
RISING, handler=button_handler)
Add another line beneath it to set up an interrupt trigger on your new button as well:
right_button.irq(trigger=machine.Pin.IRQ_
RISING, handler=button_handler)
Your program should now look like this:
import machine
import utime
import urandom
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
left_button = machine.Pin(14, machine.Pin.IN,
machine.Pin.PULL_DOWN)
right_button = machine.Pin(16, machine.Pin.IN,
machine.Pin.PULL_DOWN)
def button_handler(pin):
global pressed
if not pressed:
pressed=True
timer_reaction = utime.ticks_
diff(utime.ticks_ms(), timer_start)
print("Your reaction time was " +
str(timer_reaction) + " milliseconds!")
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
timer_start = utime.ticks_ms()
right_button.irq(trigger=machine.Pin.IRQ_RISING,
handler=button_handler)
left_button.irq(trigger=machine.Pin.IRQ_RISING,
handler=button_handler)
Click the Run icon, wait for the LED to go out, then press the left-hand push-button switch: you’ll see that the game works the same as before, printing your reaction time to the Shell area. Click the Run icon again, but this time when the LED goes out, press the right-hand button: the game will work just the same, printing your reaction time as normal.
To make the game a little more exciting, you can have it report on which of the two players was the first to press the button. Go back to the top of your program, just below where you set up the LED and the two buttons, and add the following:
fastest_button = None
This sets up a new variable, fastest_button, and sets its initial value to None – because no button has yet been pressed. Next, go to the bottom of your button handler and delete the two lines which handle the timer and printing – then replace them with:
global fastest_button
fastest_button = pin
Remember that these lines will need to be indented by four spaces so that MicroPython knows they’re part of the function. These two lines allow your function to change, rather than just read, the fastest_button variable, and set it to contain the details of the pin which triggered the interrupt – the same details your game printed to the Shell area earlier in the tutorial, including the number of the triggering pin.
Now go right to the bottom of your program, and add these two new lines:
lhile fastest_button is None:
utime.sleep(1)
This creates a loop, but it’s not an infinite loop: here, you’ve told MicroPython to run the code in the loop only when the fastest_button variable is still zero – the value it was initialised with at the start of the program. In effect, this pauses your program’s main thread until the interrupt handler changes the value of the variable. If neither player presses a button, the program will simply pause.
Finally, you need a way to determine which player won – and to congratulate them. Type the following at the bottom of the program, making sure to delete the four-space indent Thonny will have created for you on the first line – these lines do not form part of the loop:
if fastest_button is left_button:
print("Left Player wins!")
elif fastest_button is right_button:
print("Right Player wins!")
The first line sets up an ‘if’ conditional which looks to see if the fastest_button variable is left_button – meaning the IRQ was triggered by the left-hand button. If so, it will print a message – with the line below indented by four spaces so that MicroPython knows it should run it only if the conditional is true – congratulating the left-hand player, whose button is connected to GP14.
The next line, which should not be indented, extends the conditional as an ‘elif’ – short for ‘else if’, a way of saying ‘if the first conditional wasn’t true, check this conditional next’. This time it looks to see if the fastest_button variable is right_button – and, if so, prints a message congratulating the right-hand player, whose button is connected to GP16.
Your finished program should look like this:
import machine
import utime
import urandom
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
left_button = machine.Pin(14, machine.Pin.
IN, machine.Pin.PULL_DOWN)
right_button = machine.Pin(16, machine.Pin.
IN, machine.Pin.PULL_DOWN)
fastest_button = None
def button_handler(pin):
global pressed
if not pressed:
pressed=True
global fastest_button
fastest_button = pin
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
timer_start = utime.ticks_ms()
left_button.irq(trigger=machine.Pin.IRQ_
RISING, handler=button_handler)
right_button.irq(trigger=machine.Pin.IRQ_
RISING, handler=button_handler)
while fastest_button is None:
utime.sleep(1)
if fastest_button is left_button:
print("Left Player wins!")
elif fastest_button is right_button:
print("Right Player wins!")
Press the Run button and wait for the LED to go out – but don’t press either of the push-button switches just yet. You’ll see that the Shell area remains blank, and doesn’t bring back the >>> prompt; that’s because the main thread is still running, sitting in the loop you created.
Now push the left-hand button, connected to pin GP14. You’ll see a message congratulating you printed to the Shell – your left hand was the winner! Click Run again and try pushing the righthand button after the LED goes out: you’ll see another message printed, this time congratulating your right hand. Click Run again, this time with one finger on each button: push them both at the same time and see whether your right hand or left hand is faster!
Now that you’ve created a two-player game, you can invite your friends to play along and see which of you has the fastest reaction times!
Credit: Gareth Halfacree
Origional Post: The MagPi — Issue 105