Game Logic

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:

Download: file
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:

Download: file
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:

Download: file
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:

Download: file
>>> 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:

Download: file
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:

Download: file
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.

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']
        elif cpx.touch_A2:
            val = PAD_REGION['A2']
        elif cpx.touch_A3:
            val = PAD_REGION['A3']
        elif cpx.touch_A4:
            val = PAD_REGION['A4']
        elif cpx.touch_A5:
            val = PAD_REGION['A5']
        elif cpx.touch_A6:
            val = PAD_REGION['A6']
        elif cpx.touch_A7:
            val = PAD_REGION['A7']
    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!

This guide was first published on Jul 24, 2019. It was last updated on Jul 24, 2019. This page (Game Logic) was last updated on Sep 11, 2019.