All of the core pieces of functionality of the game are now implemented, so it is time to start looking at the higher level logic that actually implements the game itself.
First, I know I'll be playing sequences of lights and tones. A good way to represent the growing sequence is to use a list, so I can create a play_sequence()
function that takes a list of region numbers and reproduces the sequence by invoking the light_region()
function for each region:
def play_sequence(sequence): duration = 1 - len(sequence) * 0.05 if duration < 0.1: duration = 0.1 for region in sequence: light_region(region, duration)
The key part of this function is in the last two lines, where a for-loop runs through the regions given in the sequence
list one by one. But to make the game a bit more interesting I decided to make the duration of each light shorter as the sequence gets longer. I start from a duration of one second, and substract 0.05 of a second for each item that is in the sequence. The longer the sequence, the shorter the duration, so it gets harder to play. Since a very long sequence could make the duration
variable zero or even negative, I make sure that it never goes below 0.1.
The game is going to start by playing a sequence, which initially will have a length of one. Then it will expect the player to repeat the sequence by touching the pads. The next function I'm going to add is going to be for this purpose, and I'm going to call it read_sequence()
. This function will also take the sequence
list, but instead of playing the color regions, it will read them from the player:
def read_sequence(sequence): for region in sequence: if read_region() != region: # the player made a mistake! return False light_region(region, 0.25) return True
You can see that here each time the player touches a region I light up that region with a short duration of 0.25 of a second. This is important, as it gives the player feedback that the input was recognized. If the user touches the wrong region, or if the read function times out and returns None
, then the conditional will cause the function to return False
, which will indicate that the player has lost the game. If the entire sequence is entered correctly, then the return value is True
, and this will trigger the game logic to add one more element to the sequence and repeat the cycle.
In the event of the player making a mistake, the game needs to indicate that the game has been lost. This can be achieved with a low frequency tone. The function play_error()
does this:
def play_error(): cpx.start_tone(160) time.sleep(1) cpx.stop_tone()
And now, finally all the parts are in place to implement the main game logic!
To select a new color when adding to the sequence, the random.randint()
function from CircuitPython can be used. Here is how this function works in a REPL session:
>>> import random >>> random.randint(0, 3) 3 >>> random.randint(0, 3) 1 >>> random.randint(0, 3) 2 >>>
Each time random.randint()
is called, a random number that is between the two arguments is generated. Each time the player reproduces the sequence correctly, a new element will be generated randomly and appended at the end of the sequence.
Here is the function play_game()
, which implements the complete game logic:
import random def play_game(): sequence = [] while True: sequence.append(random.randint(0, 3)) play_sequence(sequence) if not read_sequence(sequence): # game over play_error() break time.sleep(1)
Isn't it amazing that the game is now so simple to write? This function starts by creating an empty sequence
list. This is where the color regions are going to be added as the player makes progress through the game. The rest of the game is implemented inside a while True
loop, which will run until the player makes a mistake.
The loop starts by adding a random color to the sequence. The sequence is then played by calling the play_sequence()
function. Next the player needs to repeat the sequence, which is something that I already implemented in the read_sequence()
function. If this function returns False
it means that the player lost the game, so the error tone is played by calling play_error()
and then the break
statement causes the while-loop to exit, which in turn makes the play_game()
function exit as well. If the player enters the entire sequence correctly, then I have a short delay of one second to give the player a short break, and then the loop will start again from the top, adding another element to the sequence and repeating the cycle.
The last snippet of code that I need is to call play_game()
in the global scope, so that the board automatically runs the game when it is powered. Instead of just calling the function, I'm going to put it inside a while True
loop, so that each time the player loses the game a new game is started:
while True: play_game()
And with this, the game is now complete!
Here is the complete code for the game. Download the code and save onto your CIRCUITPY drive as code.py.
# SPDX-FileCopyrightText: 2019 Anne Barela for Adafruit Industries # # SPDX-License-Identifier: MIT import time import random from adafruit_circuitplayground.express import cpx cpx.pixels.brightness = 0.1 # adjust NeoPixel brightness to your liking REGION_LEDS = ( (5, 6, 7), # yellow region (2, 3, 4), # blue region (7, 8, 9), # red region (0, 1, 2), # green region ) REGION_COLOR = ( (255, 255, 0), # yellow region (0, 0, 255), # blue region (255, 0, 0), # red region (0, 255, 0), # green region ) REGION_TONE = ( 252, # yellow region 209, # blue region 310, # red region 415, # green region ) PAD_REGION = { 'A1': 0, # yellow region 'A2': 2, # red region 'A3': 2, # red region 'A4': 3, # green region 'A5': 3, # green region 'A6': 1, # blue region 'A7': 1, # blue region } def light_region(region, duration=1): # turn the LEDs for the selected region on for led in REGION_LEDS[region]: cpx.pixels[led] = REGION_COLOR[region] # play a tone for the selected region cpx.start_tone(REGION_TONE[region]) # wait the requested amount of time time.sleep(duration) # stop the tone cpx.stop_tone() # turn the LEDs for the selected region off for led in REGION_LEDS[region]: cpx.pixels[led] = (0, 0, 0) def read_region(timeout=30): val = 0 start_time = time.time() while time.time() - start_time < timeout: if cpx.touch_A1: val = PAD_REGION['A1'] time.sleep(.3) break elif cpx.touch_A2: val = PAD_REGION['A2'] time.sleep(.3) break elif cpx.touch_A3: val = PAD_REGION['A3'] time.sleep(.3) break elif cpx.touch_A4: val = PAD_REGION['A4'] time.sleep(.3) break elif cpx.touch_A5: val = PAD_REGION['A5'] time.sleep(.3) break elif cpx.touch_A6: val = PAD_REGION['A6'] time.sleep(.3) break elif cpx.touch_A7: val = PAD_REGION['A7'] time.sleep(.3) break return val def play_sequence(sequence): duration = 1 - len(sequence) * 0.05 if duration < 0.1: duration = 0.1 for region in sequence: light_region(region, duration) def read_sequence(sequence): for region in sequence: if read_region() != region: # the player made a mistake! return False light_region(region, 0.25) return True def play_error(): cpx.start_tone(160) time.sleep(1) cpx.stop_tone() def play_game(): sequence = [] while True: sequence.append(random.randint(0, 3)) play_sequence(sequence) if not read_sequence(sequence): # game over play_error() break time.sleep(1) while True: play_game()
I hope you found this guide useful, not only to learn how to write this specific game, but also to give you some ideas on how you can use similar techniques to implement other games!
Page last edited January 21, 2025
Text editor powered by tinymce.