© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
C. BellBeginning MicroPython with the Raspberry Pi Picohttps://doi.org/10.1007/978-1-4842-8135-2_11

11. Project: Simon Game

Charles Bell1  
(1)
Warsaw, VA, USA
 

If you like vintage electronic games, you have played a game named Simon.1 It is a round tabletop game that has four large colored buttons on top. One or more players can play with the objective to repeat a sequence from memory. The game presents the player with a sequence of colored lights in a random pattern. The player’s goal is to press the buttons for each color in the sequence before time runs out. If the player repeats the sequence correctly, the game continues and adds another light to the sequence. The game starts with a single light, so early levels are pretty easy, but as the sequence gets longer, it becomes harder to play. Throw in several players and you’ve got a cool, Internet-free game party!

In this chapter, we will see how to create a version of the Simon game using Grove modules using analog, digital, and I2C protocols. Let’s get started.

Overview

The project for this chapter is designed to demonstrate how to use analog, digital, and I2C devices on the same Grove host adapter to build a Simon game. It works very much like the original game but with an LCD for displaying messages. We will use a Grove Buzzer for sound and two Grove Dual Button modules. For the lights, we will use one Grove RGB LED module.

While this seems like a simple project build, the number of modules in use and integrating all of the code for those modules makes this project the most ambitious of the book. If you haven’t read and worked on the other projects, you may want to work on the earlier chapters first and save this one until you’ve mastered a few of the others.

For those with access to a 3D printer, we will also see a simple mounting plate you can print and install the modules onto to protect them and make it easier to use in playing the game.

Let’s see what hardware we will need.

Required Components

The hardware needed for this project is listed in Table 11-1. We will use a Grove Dual Button, Grove Buzzer, Grove LCD RGB Backlight, Grove Dual Button modules, and a Grove Chainable RGB LED V2.0.
Table 11-1

Hardware Needed for the Mood Detector Project

Component

Qty

Description

Cost

Links

Grove Dual Button

3

Buttons

$2.20

www.seeedstudio.com/Grove-Dual-Button-p-4529.html

Grove Buzzer

1

Buzzer

$1.90

www.seeedstudio.com/Grove-Buzzer.html

Grove LCD RGB Backlight

1

LCD

$11.90

www.seeedstudio.com/Grove-LCD-RGB-Backlight.html

Grove Chainable RGB LED V2.0

1

LED

$6.60

www.seeedstudio.com/Grove-Chainable-RGB-Led-V2-0.html

Grove Cable

5

Cable

$0.95

www.sparkfun.com/products/14426

Grove Shield for Pi Pico V1.0

1

Host board

$4.30

www.seeedstudio.com/Grove-Shield-for-Pi-Pico-v1-0-p-4846.html

Let’s discuss these components briefly. We will discover how to work with the hardware in more detail later in the chapter. We saw the Grove Shield for Pico, Grove Buzzer, and the Grove Chainable RGB LED in the last chapter, so let’s look at the new Grove components for this chapter.

Grove Dual Button

The Grove Dual Button is a digital module that has two momentary buttons. While there are two buttons on the module, we need only a single Grove cable to connect to the host adapter. This is because digital modules use only three wires: ground, 3.3/5V, and one for signal. Since we have four cables available, we can use the extra wire for the second button.

The button comes with a set of colored button caps that you can use to help color code your button choices, which is a nice option.

You may have noticed we need three of these modules. Two modules are used for the four color buttons, and another is used for a mode and start option. We will see these functions later when we start the code for the project.

Figure 11-1 shows the Grove Dual Button.
Figure 11-1

Grove Dual Button (courtesy of seeedstudio.com)

Grove LCD RGB Backlight

If you’ve used monochrome LCD displays in the past, you may appreciate the interesting option on the Grove LCD RGB Backlight. While the text color remains dark gray, you can change the background using an RGB color similar to the Chainable RGB LED. Figure 11-2 shows the Grove LCD RGB Backlight.
Figure 11-2

Grove LCD RGB Backlight (courtesy of seeedstudio.com)

While this module does not offer the option, some Grove I2C modules support address changes by opening or closing jumpers on the bottom of the board. Now, let’s see how to connect the components together.

Set Up the Hardware

Once again, connecting the hardware for a Grove project is really easy. Since the cables are keyed, you don’t have to worry about incorrect connections. Rather, we have to consider which Grove connectors we need to use. For this project, we need six connections for the six modules we will be using. The connections and their types are shown in Table 11-2.
Table 11-2

Simon Game Connections

Module

Description

Pico Shield Connector

Dual Button 1

Start/mode

D16/D17

Dual Button 2

Red/green buttons

D18/D19

Dual Button 3

White/blue buttons

D20/D21

Buzzer

Sound

A0

RGB LCD

Display

I2C0

Chainable RGB LED

Color cue

I2C1

Thus, we will need six Grove cables, and each will plug into one spot on the Pico Shield. Figure 11-3 shows how the connections will look once all of the modules are connected to the Pico Shield.
Figure 11-3

Connections for Simon Game

Wow! That’s a lot of connections, right? Not really, though, considering we’re only using six cables. If we had wired everything together using breadboards, we would have used over 30 jumper wires!

Note

Be sure to switch the Pico Shield to the 5V setting. This is because the Grove RGB LED works best with 5V power.

You can play the game with the modules connected loosely, but for best results, you may want to consider using a small board about 4” wide and 8” long to attach the modules using small wood screws. This will make gameplay much better.

Or, better, you could build yourself a mounting plate!

Using a Mounting Plate

Since we have so many components and a bunch of cables connecting them all together, using the project can take a little bit of space, and with all of those modules dangling by their cables tethered only to the Grove host adapter, you run the risk of accidentally unplugging a module, or, worse, the electronics on the module may come into contact with conductive material. Even so, using them to play a game like Simon can become a lesson in patience.

You can mitigate this by using a double-sided tape to tape them to your desk, but a better solution is to create a mounting plate. We could create a full enclosure, but as you will see, leaving the modules exposed gives the project a genuine cool factor.

If you have your own 3D printer or have access to a 3D printer, you can print a mounting plate. The source code for this chapter includes the 3D printing files you need to create a simple enclosure to mount the modules arranged in a manner that enables gameplay. Figure 11-4 shows the mounting plate.

Figure 11-4 shows an example mounting plate for the game. There are places to bolt all of the modules as well as the Pico Shield.
Figure 11-4

3D mounting plate design for the Simon Says project

While this looks like nothing more than a coaster, there are feet on the bottom of the plate and places for M2 nuts. In fact, you will need to print this plate upside down.

There is also a set of spacers you will need to print as shown in Figure 11-5.
Figure 11-5

3D spacers design for the Simon Says project

Notice there are (2) short M2 spacers for the buzzer module, (12) medium spacers for the Dual Button modules and the Chainable RGB LED (each take 3), and (4) long spacers for the LCD RGB Backlight.

To mount the modules, you will need the following hardware. You may use longer bolts if you cannot find the exact sizes, but be sure to adjust the constant FOOT_HEIGHT in the simon.scad file to allow for the extra length:
  • (22) M2 nuts

  • (6) M2x10mm bolts

  • (12) M2x12mm bolts

  • (4) M2x19mm bolts

To assemble the enclosure, begin by mounting the Dual Button and Chainable RGB LED modules. Arrange the mounting plate with the Pico section (the square section) facing away from you (call it the top). Find the four mounting positions that match the holes in the modules.

Mount one Dual Button module on the far left of the plate (oriented vertically) and the two along the bottom edge (oriented horizontally). The last position where the button module fits is in the center. This spot is for the Chainable RGB LED. Mount the Chainable RGB LED in the remaining spot in the center.

You will need three bolts and three medium spacers for each. Place the M2 nuts in the nut traps on the bottom of the mounting plate and tighten.2

Next, mount the Buzzer to the spot on the right side of the mounting plate using two small spacers and two bolts and nuts.

Next, mount the LCD using the four long spacers and long bolts. Be sure to orient the LCD so the Grove connector is on the same side as the RGB LED.

Finally, mount the Pico Shield. You will need two bolts and nuts (no spacers). You will need to mount the Pico Shield before you mount the Pico to the shield. When you have the Pico Shield mounted, insert the Pico.

Now you are ready to run the Grove cables. It is recommended you make the connections in the following order, routing the cables under the LCD to keep them away from the buttons:
  1. 1.

    Connect the leftmost Dual Button to D16/D17 on the Pico Shield. The topmost button will be the mode and the bottom the start button.

     
  2. 2.

    Connect the next Dual Button module to D18/D19 on the Pico Shield. This is the left module on the bottom of the plate.

     
  3. 3.

    Connect the last Dual Button module to D20/D21 on the Pico Shield.

     
  4. 4.

    Connect the Chainable RGB LED to I2C0 on the Pico Shield.

     
  5. 5.

    Connect the RGB LCD to I2C1 on the Pico Shield.

     
  6. 6.

    Connect the Buzzer to A0 on the Pico Shield.

     
Tip

If you plan to partially disassemble the game to use parts for other projects temporarily, be sure to use a small piece of painter’s or masking tape to note where each cable is used. That way, you can replace the module and reconnect it without guessing the connection.

The Dual Button modules come with colored caps for the buttons. I used a white cap for the mode button and a blue cap for the start button (but any color will do for these). The game button caps should be, from left to right, red, green, white, and blue.

You should also make all of the cable connections as well since we will route all wiring under the LCD RGB Backlight. You can use a small zip tie to bundle the cables, but be sure to avoid kinking or putting strain on the Grove connectors. Figure 11-6 shows the completed Simon game.
Figure 11-6

Mounting the modules to the 3D printed plate

If you have experience creating 3D models for printing, feel free to experiment with creating your own enclosure – one that also includes a battery so you can make the project a handheld game.

Now that we know more about the hardware for this chapter, let’s write the code!

Write the Code

The code for this project uses analog and digital modules as well as two I2C devices. The Dual Buttons are digital, the Buzzer is an analog module, and the RGB LCD and Chainable RGB LED are I2C devices. As you will see, the code isn’t overly complicated, but there is a lot of code to work through as well as some new modules for working with the hardware.

Like the previous projects, we will use classes to wrap our functionality. We’ll focus on making the Simon game its own class and general control of the game system in the main code. We will also be making a number of class modules, which will be presented before we look at the main code. Briefly, we will need three class modules. We will write class modules for the buzzer, buttons, and the gameplay.

Let’s first look at the software libraries we will need to download.

Install Software Libraries

We will need to download two libraries, one for the RGB LCD and another for the Chainable RGB LED.

The library we will need for the RGB LCD is found on a web page to a Chinese GitLab that shows the contents of the code module (http://47.106.166.129/Embeded/pico-micropython-grove/blob/master/I2C/lcd1602.py). Rather than download the file, you can open a new file in Thonny, copy the code from the web page, and paste it in the new file and save it as lcd1602.py. We will use the code library unchanged once we upload it to the Pico.

We will be using the same library for the Chainable RGB LCD that we used in Chapter 10 (https://github.com/mcauser/micropython-p9813). Recall, to download the library, we use the command git clone https://github.com/mcauser/micropython-p9813 to make a copy (clone) of the repository as follows. This copies all of the files including examples and documentation to your PC:
$ git clone https://github.com/mcauser/micropython-p9813
Cloning into 'micropython-p9813'...
remote: Enumerating objects: 36, done.
remote: Total 36 (delta 0), reused 0 (delta 0), pack-reused 36
Unpacking objects: 100% (36/36), done.

Once you clone the repository, you can locate the p9813.py file in the <root of clone>/micropython-p9813 folder. You can then download that to your Pico.

Now, let’s take a look at the class modules for the project.

Create the Class Modules

While we will not dive into every line of code, we will see some of the more complex code and those areas discussed that differ significantly from what you may have experienced thus far in your MicroPython journey. You can read the code and learn more about how it works at your leisure.

Since there are several code modules (files) for this project, it is recommended that you create a project folder (e.g., named project5) and save all of the files there. It will make copying them to your Pico easier later.

Let’s start with writing the classes starting with the Buzzer class.

Buzzer Class

The Buzzer class provides a mechanism to play tones. Specifically, we will create functions for each of the sounds that the Simon game uses. In this case, we need the following tones (or tone sequences):
  • play_theme_song( ): An introductory song played when the game starts

  • play_ready_set_go( ): A tone to indicate the player can begin entering the sequence of buttons

  • play_success( ): A tone to indicate the sequence entered matches the challenge sequence

  • play_failure( ): A set of tones to indicate the sequence is not correct and the player’s turn ends

  • play_color( ): Play a unique tone for each of the four LED buttons

Aside from those functions, we will also create a constructor so we can set up the class and additional helper functions: one for playing a set of tones (song), another to get the frequency for the tone, and another to play a single tone (note).

We will also use a scale of notes and their frequencies stored in private variables. In this way, we can record the notes in variables for each of the tones/sounds earlier and then use the frequency function to retrieve the frequency of the note. The frequency defines how long the buzzer will sound. By varying the frequency, we can get different notes.

We can also define how long to hold (play) each tone, which will help us determine a cadence or primitive rhythm. We will call these “beats” where each beat is a quarter note (so we’ll be using 4/4 time). Thus, a 1 is one quarter, 2 is half, etc. We also use a tempo that we can use to determine the speed, which we will set globally, but you could easily modify the code to allow different tones played at different speeds. This way, we can make the song faster or slower depending on our aesthetic requirements.

Rather than use a tuple, we will use one of the more powerful Python data storage called a dictionary. A dictionary allows us to create a structure where we can store one or more key-value pairs where we can store all of the parts of the song, the number of notes, notes, and beats. We will also see how to store the tempo for each song.

The following shows the layout of the dictionary we will use for each song. Here, we use the keys tempo, num_notes, notes, and beats which will be used in the code to reference the value for each:
# Success tones dictionary
self.success = {
    'tempo': DEFAULT_TEMPO,
    'num_notes': 3,
    'notes': "CCC",
    'beats': [1, 1, 1]
}

Let’s get started writing the code. Open the Thonny Python IDE and create a new file named buzzer.py in the project folder. As usual, we begin with the imports and constants followed by the class definition. For this class, we will define in the constructor the songs we will be using for the Simon game. Along with the default tempo, we also define the Pico GPIO pin in the code module. Recall, we are using the analog (A0) connector on the Pico Shield, but we will be using the pin as a digital output. This is perfectly fine since that pin can be used as either an analog or a digital pin. We refer to the pin by its pin number (26). In fact, we will be using a technique called pulse-width modulation3 (PWM) to rapidly turn the pin on and off over a period of time to change the sound produced.

Listing 11-1 shows the first part of the code with documentation removed for brevity.
from machine import Pin
from utime import sleep
# CONSTANTS
DEFAULT_TEMPO = 0.095
BUZZER_PIN = 26
NOTES_IN_SCALE = 8
HIGH = 1
LOW = 0
def tone(buzzer_pin, frequency, duration):
    """Generate a tone on the buzzer."""
    half_wave = 1 / (frequency * 2)
    waves = int(duration * frequency)
    # pylint: disable=unused-variable
    for i in range(waves):
        buzzer_pin.on()
        sleep(half_wave)
        buzzer_pin.off()
        sleep(half_wave)
class Buzzer:
    """Buzzer Class"""
    note_names = ['c', 'd', 'e', 'f', 'g', 'a', 'b', 'C']
    frequencies = [131, 147, 165, 175, 196, 220, 247, 262]
    failure = {}
    success = {}
    theme_song = {}
    ready_set_go = {}
    colors = [{}, {}, {}, {}]
    def __init__(self):
        """Constructor"""
        # Failure tones dictionary
        self.failure = {
            'tempo': DEFAULT_TEMPO,
            'num_notes': 5,
            'notes': "g c",
            'beats': [4, 1, 4, 1, 10]
        }
        # Success tones dictionary
        self.success = {
            'tempo': DEFAULT_TEMPO,
            'num_notes': 3,
            'notes': "CCC",
            'beats': [1, 1, 1]
        }
        # Theme song dictionary
        self.theme_song = {
            'tempo': DEFAULT_TEMPO,
            'num_notes': 18,
            'notes': "cdfda ag cdfdg gf ",
            'beats': [1, 1, 1, 1, 1, 1, 4, 4, 2,
                      1, 1, 1, 1, 1, 1, 4, 4, 2]
        }
        # Start signal dictionary
        self.ready_set_go = {
            'tempo': DEFAULT_TEMPO,
            'num_notes': 1,
            'notes': "e",
            'beats': [2]
        }
        # Tones for the colors
        for i in range(0, 4):
            self.colors[i]['tempo'] = DEFAULT_TEMPO
            self.colors[i]['num_notes'] = 1
            self.colors[i]['beats'] = [1]
        self.colors[0]['notes'] = "a"
        self.colors[1]['notes'] = "g"
        self.colors[2]['notes'] = "C"
        self.colors[3]['notes'] = "f"
        # Setup the buzzer
        self.buzzer_pin = Pin(BUZZER_PIN, Pin.OUT)
...
Listing 11-1

Buzzer Class (Part 1)

Notice the tone() function. This function is used by the play_song() function to play the note on the buzzer. Notice here we do some math first where we get the half wave of the frequency. We are getting one half of the sine wave so that we can turn the buzzer for half the wave and off for half the wave, which is the frequency times the duration and, hence, a pulse. Again, there are other ways to generate a PWM, but this works well for the buzzer.

Next are the various functions defined to play the specific songs. We won’t go into too much detail as the code is not complicated.

Listing 11-2 shows the rest of the code for the class with documentation removed for brevity.
...
    def play_theme_song(self):
        """Play theme_song tones."""
        self.play_song(self.theme_song)
    def play_success(self):
        """Play success tones."""
        self.play_song(self.success)
    def play_failure(self):
        """Play failure tones."""
        self.play_song(self.failure)
    def play_color(self, color):
        """Play button_color tones."""
        self.play_song(self.colors[color])
    def play_ready_set_go(self):
        """Play ready_set_go tones."""
        self.play_song(self.ready_set_go)
    def frequency(self, note):
        """Get frequency for a note."""
        # Search through the letters in the array, and
        # return the frequency for that note.
        for i in range(0, NOTES_IN_SCALE):
            if self.note_names[i] == note:
                return self.frequencies[i]
        return 0
    def play_song(self, song):
        """Play a song."""
        for i in range(0, song['num_notes']):
            duration = song['beats'][i] * song['tempo']
            if song['notes'][i] == ' ':
                sleep(duration)
            else:
                freq = self.frequency(song['notes'][i])
                tone(self.buzzer_pin, freq, duration)
            sleep(duration)
            sleep(song['tempo']/10)
Listing 11-2

Buzzer Class (Part 2)

Now, let’s look at the Buttons class file.

Buttons Class

The Buttons class is designed to manage the six buttons in the game. Since the code for reading each button is the same, we can combine the code to make it easier to use. We also use a digital pin on the GPIO for each button. We use an array to define the buttons and the index of the button in the array to refer to a specific button.

There are two functions for the class. We will use the get_button_pressed() function to return the number of the button that is being pressed (or –1 if no button is pressed) and the get_button_value() function to return the current state for a specific button.

We will also need two helper functions to make using the buttons easier. First, we will create a function named button_name(), which simply returns a string to match the button number. This is for diagnostic purposes since the game doesn’t need it for gameplay. But it does make it easier to debug!

Second, we create a function named debounce(), which has a very unique and key role. When mechanical switches (buttons) are pressed, the mechanics can produce a lot of noise initially and thus can vary in value rapidly. We call this “bouncing,” which can make reading buttons problematic.4 One way to reduce the noise is to use a loop to sample the value of the button over a brief period of time to stabilize the fluctuations. This function is one way we can achieve that goal.

Let’s look at the completed code for this class. It is not complicated and does not need a lot of explanation. However, you should examine the debounce() code to see how it works so that you can use it in other projects where buttons are employed. Listing 11-3 shows the code for the class with documentation removed for brevity.
from machine import Pin
from utime import sleep
def button_name(button_num):
    """Return the name of the button for diagnostics."""
    name = ""
    if button_num == 0:
        name = "START_BUTTON"
    elif button_num == 1:
        name = "MODE_BUTTON"
    elif button_num == 2:
        name = "RED_BUTTON"
    elif button_num == 3:
        name = "GREEN_BUTTON"
    elif button_num == 4:
        name = "WHITE_BUTTON"
    else:
        name = "BLUE_BUTTON"
    return name
def debounce(pin):
    """Debounce button presses."""
    # wait for pin to change value
    # it needs to be stable for a continuous 20ms
    cur_value = pin.value()
    active = 0
    while active < 20:
        if pin.value() != cur_value:
            active += 1
        else:
            active = 0
        sleep(0.01)
class Buttons:
    """Class to manage buttons for Simon game."""
    START_BUTTON = 0
    MODE_BUTTON = 1
    RED_BUTTON = 2
    GREEN_BUTTON = 3
    WHITE_BUTTON = 4
    BLUE_BUTTON = 5
    def __init__(self):
        self.button_list = []
        self.button_list.append(Pin(17, Pin.IN, Pin.PULL_UP))  # START
        self.button_list.append(Pin(16, Pin.IN, Pin.PULL_UP))  # MODE
        self.button_list.append(Pin(19, Pin.IN, Pin.PULL_UP))  # RED
        self.button_list.append(Pin(18, Pin.IN, Pin.PULL_UP))  # GREEN
        self.button_list.append(Pin(20, Pin.IN, Pin.PULL_UP))  # WHITE
        self.button_list.append(Pin(21, Pin.IN, Pin.PULL_UP))  # BLUE
    def get_button_pressed(self):
        """Return the button (index) pressed."""
        for button_num in range(0,6):
            if self.button_list[button_num].value() == 0:
                debounce(self.button_list[button_num])
                return button_num
        return -1
    def get_button_value(self, button_num):
        """Check a button for status."""
        return self.button_list[button_num].value()
Listing 11-3

Buttons Class

At this point, you might be wondering how one could write classes like this and expect them to work when put with the main code. Indeed, it is often unlikely this will happen smoothly unless you do some testing and debugging.

If you recall from earlier projects, we used a main() function and a condition at the end of the module to call it if the module were executed. Well, we can do that with class modules! The following shows how to write a short test for this class module. Simply place it at the end of the file and then execute the code. This code simply runs a loop that reports the button pressed. You can simply wire up the three Dual Button modules and test them. It is also a terrific way to figure out which buttons correspond to the button functions.
...
if __name__ == '__main__':
    try:
        buttons = Buttons()
        while True:
            index = buttons.get_button_pressed()
            if index >= 0:
                print("{} = {} pressed".format(index, button_name(index)))
    except (KeyboardInterrupt, SystemExit) as err:
        print(" bye! ")

To create the class file, open a new file in Thonny and save it as buttons.py in the project folder.

When you execute this code in Thonny, you will see something similar to the following (buttons were pressed randomly for this example). Now we see where that button_name() function comes in handy!
>>> 0 = START_BUTTON pressed
>>> 4 = WHITE_BUTTON pressed
>>> 2 = RED_BUTTON pressed
>>> 1 = MODE_BUTTON pressed
>>> 3 = GREEN_BUTTON pressed
>>> 5 = BLUE_BUTTON pressed
...

Now, let’s look at the Simon class file.

Simon Class

This class is responsible for running the Simon gameplay. That is, it generates the random sequences for the player to press and then checks the results to see if there is a match. If the sequence matches, gameplay continues.

The mode and start buttons discussed previously are not part of this class. Rather, this class only contains code to detect the four color buttons, the Chainable RGB LED, and the RGB LCD. The main code will manage the mode and start buttons.

We will use functions for the setup routine where we can change the number of players (setup_mode()), start the game (start_game()), show the number of players (show_players()), and play the game (play()).

Aside from that, we will also need a number of helper functions that are a bit more complicated. We need functions to control the LCD, play a challenge sequence, read a sequence of buttons from the player, generate the challenge sequence using the randint() function to generate a random integer from zero to three to correspond to the button array index, and even determine a winner for the multiplayer mode. The following lists the private functions and their uses:
  • num_alive(): Determine the number of players still active (alive)

  • reset_screen(): Reset the LCD and display a new message

  • show_winner(): Show the winner on the LCD

  • generate_sequence(): Generate a challenge sequence

  • play_sequence(): Play a challenge sequence by turning on the corresponding LED and playing the tone for the button

  • read_sequence(): Read a sequence from the player

Finally, we will need a number of variables to store information including an instance of the LCD class and the buttons (stored as an array). The constructor will also need to be added to set up the hardware.

Since there are a lot of functions in the class, we will first discuss each function in overview and then highlight some of the more complex ones in more detail, but none are overly difficult. You can discover how the other functions work as an exercise. We will begin with the public functions.

The constructor is where we set up the hardware for the class, which includes the new Buttons class, Buzzer class, RGB LCD, and the Chainable RGB LED.

We also initialize the random number generator using a read from an analog pin as the seed. This will simulate using a different seed each time because reading an uninitialized pin will generate an unpredictable value. We then place the game in setup mode with the setup_mode() function, which simply resets the RGB LCD to indicate we are in the setup mode.

The start_game() function takes an integer for the number of players and simply zeros out the player scores, plays the theme song, and sets the RGB LCD for the start of gameplay.

The play() function is a bit more complicated. Here is where the gameplay is coded. At the highest level, the function loops generating a challenge sequence, playing it to the user, then reading the player’s response. If the challenge is met, the loop continues with an extra button added and a new random sequence generated.5

When there are more than one player, the loop cycles through each player in turn. If a player misses the sequence, that player is removed from the cycle (considered no longer playing or “alive”). Play continues until there are no more players alive and a winner is determined and the game ends. When the game ends, the code pauses and then resets the game class for the next game.

Next, let’s look at the private functions. Recall, private functions are used internally to the class and not visible to the caller.

The num_alive() function loops through the player scores to determine how many players are still playing. It is used in the play() function to determine when the game ends.

The reset_screen() takes as a parameter a message to be displayed on the LCD. The function clears the display and then adds the message. It is used to control the LCD during gameplay.

The show_winner() function loops through the player scores to determine which player has the highest score. Since the play() function is designed to keep going until all players have failed to complete a sequence, it is possible for two or more players to have the same score. This is an intentional omission that you are encouraged to solve as an exercise. Hint: You can simply declare a tie.

The generate_sequence() function takes as a parameter the number of buttons and returns an integer array allocated from memory that includes a set of random integers in the range 0–3 to represent the buttons in the sequence. To create a random integer in that range, we call randint(0, 3), which returns the correct range.

The play_sequence() function uses two parameters, one for the button (challenge) sequence and another for the number of notes. It simply loops through the array turning on the Chainable RGB LED with the appropriate color and playing the tone for each button using a delay between each. This is used by the play() function to present the challenge sequence to the player.

The read_sequence() function also uses two parameters, one for the button (challenge) sequence and another for the number of notes. It simply loops through the array reading the button presses from the player. If the correct button is pressed, the next button is read and so forth. If all buttons were pressed in the correct order (the sequence pressed equals the challenge sequence), the function returns true, or false is returned on the first incorrect button press in the sequence. This is used by the play() function to read the player’s response.

OK, that’s a lot of functions! Let’s now look at the complete code for the class. Take a few moments to read it (there’s a lot of code) to ensure you understand how it all works.

Listing 11-4 shows the complete code for the class with documentation removed for brevity. Take a few moments to read the code so that you understand all of the parts of the code. As you will see, it is not complicated, but there is a lot of code to sift through.
import time
import urandom
from machine import ADC, I2C, Pin
from project5.buttons import Buttons
from project5.buzzer import Buzzer
from project5.lcd1602 import LCD1602_RGB, LCD1602
from project5.p9813 import P9813
# Constants
MIN_BEATS = 2        # Starting number of beats
MAX_PLAYERS = 4      # Max number of players
MAX_TIMEOUT = 5.0    # Seconds to wait to abort read
KEY_INTERVAL = 0.500 # Interval between button playback
# RGB Values
RED_LED = (255, 0, 0)
GREEN_LED = (0, 255, 0)
WHITE_LED = (200, 200, 200)
BLUE_LED = (0, 0, 255)
RGB_COLORS = (RED_LED, GREEN_LED, WHITE_LED, BLUE_LED)
def generate_sequence(num_notes):
    """Generate a new button sequence."""
    if num_notes == 0:
        return []
    # Create a new sequence adding a new beat
    challenge_sequence = []
    i = 0
    while i < num_notes:
        challenge_sequence.append(urandom.randint(0, 3))
        i = i + 1
    return challenge_sequence
class Simon:
    """Simon Class"""
    i2c = I2C(0,scl=Pin(9), sda=Pin(8), freq=400000)
    lcd = LCD1602(i2c, 2, 16)           # LCD
    lcd_rgb = LCD1602_RGB(i2c, 2, 16)   # LCD RGB control
    buzzer = Buzzer()                   # Buzzer
    buttons = Buttons()                 # Buttons
    num_players = 1
    player_scores = []
    # Setup the RGB module
    scl = Pin(7, Pin.OUT)
    sda = Pin(6, Pin.OUT)
    rgb_chain = P9813(scl, sda, 1)
    rgb_chain[0] = (0, 0, 0) # turn RGB off
    rgb_chain.write()
    def __init__(self):
        """Constructor"""
        # Setup the LCD
        self.lcd.clear()
        # Set background color?
        self.lcd_rgb.set_rgb(127, 127, 127)
        # if analog input pin 0 is unconnected, random analog
        # noise will cause the call to randomSeed() to generate
        # different seed numbers each time the sketch runs.
        # random.seed() will then shuffle the random function.
        urandom.seed(ADC(0).read_u16())
        print("Playing theme...")
        self.buzzer.play_theme_song()
        print("done.")
        # Put game in setup mode
        self.setup_mode()
    def start_game(self, players):
        """Start a new game."""
        self.num_players = players
        for player in range(0, players):
            player_score = {
                'number': player,
                'is_alive': True,
                'high_score': 0
            }
            self.player_scores.append(player_score)
        self.reset_screen("Get ready!")
    def play(self):
        """Play game."""
        game_over = False
        num_notes = 1
        # Main game loop
        while not game_over:
            # For each player, generate a new sequence and test skills
            for player in range(0, self.num_players):
                if self.player_scores[player]['is_alive']:
                    num_notes = self.player_scores[player]['high_score'] + 1
                    self.reset_screen("Player {0}".format(player + 1))
                    challenge_sequence = generate_sequence(num_notes)
                    self.play_sequence(challenge_sequence, num_notes)
                    self.buzzer.play_ready_set_go()
                    self.reset_screen("Go!")
                    print("Go!")
                    if self.read_sequence(challenge_sequence, num_notes):
                        self.buzzer.play_success()
                        self.reset_screen("Success!")
                        print("Success!")
                        time.sleep(0.500)
                        self.player_scores[player]['high_score'] = num_notes
                    else:
                        self.reset_screen("FAILED")
                        print("Fail")
                        self.player_scores[player]['is_alive'] = False
            # Check to see if any players remain alive
            # and show winner if multiple players
            players_remaining = self.num_alive()
            if players_remaining == 0:
                self.reset_screen("GAME OVER")
                if self.num_players > 1:
                    self.show_winner()
                game_over = True
                print("Game over...")
                time.sleep(2)
        self.player_scores = []
    def setup_mode(self):
        """Enter setup mode."""
        self.lcd.clear()
        self.lcd.setCursor(0, 0)
        self.lcd.print("Simon Says!")
        self.lcd.setCursor(0, 1) # column 1, row 2
        self.lcd.print("Setup Mode")
    def show_players(self, num_players):
        """Show players."""
        self.lcd.clear()
        self.lcd.setCursor(0, 0)
        self.lcd.print("Simon Says!")
        self.lcd.setCursor(0, 1) # column 1, row 2
        if num_players == 1:
            self.lcd.print("single player")
        else:
            self.lcd.print(chr(num_players + 0x30))
            self.lcd.print(" players")
    def show_winner(self):
        """Show the winner."""
        winner = -1
        score = 0
        for player in range(0, self.num_players):
            if self.player_scores[player]['high_score'] > score:
                winner = player
                score = self.player_scores[player]['high_score']
        self.lcd.setCursor(0, 0)
        self.lcd.print("Player ")
        self.lcd.print(winner + 1)
        self.lcd.print("WON!")
        self.lcd.setCursor(0, 1) # column 1, row 2
        self.lcd.print("Score = ")
        self.lcd.print(score)
    def num_alive(self):
        """Number of players still playing."""
        count = 0
        for player in range(0, self.num_players):
            if self.player_scores[player]['is_alive']:
                count = count + 1
        return count
    def reset_screen(self, message):
        """Reset the LCD."""
        self.lcd.clear()
        self.lcd.setCursor(0, 0)
        self.lcd.print("Simon Says! (")
        self.lcd.print("{0}".format(self.num_players))
        self.lcd.print(")")
        self.lcd.setCursor(0, 1) # column 1, row 2
        self.lcd.print(message)
    def read_sequence(self, challenge_sequence, num_notes):
        """Read button sequence from the player."""
        def show_challenge_sequence():
            colors = ""
            for color in challenge_sequence:
                if color == 0:
                    colors += "R "
                elif color == 1:
                    colors += "G "
                elif color == 2:
                    colors += "W "
                elif color == 3:
                    colors += "B "
            return colors
        button_read = -1
        index = 0
        start_time = time.time()
        # Loop reading buttons and compare to stored sequence
        while index < num_notes:
            button_read = self.buttons.get_button_pressed() - 2
            # if a color button is pressed, check the sequence
            if button_read >= 0:
                # print(">", button_read, show_challenge_sequence())
                if challenge_sequence[index] != button_read:
                    self.buzzer.play_failure()
                    self.reset_screen("FAIL SEQUENCE")
                    time.sleep(5)
                    return False
                print("MATCH!")
                start_time = time.time()
                index = index + 1
                button_read = -1
            if (time.time() - start_time) > MAX_TIMEOUT:
                print("ERROR: Timeout!")
                self.buzzer.play_failure()
                self.reset_screen("FAIL TIMEOUT")
                time.sleep(5)
                return False
            time.sleep(0.050)
        return True
    def play_sequence(self, challenge_sequence, num_notes):
        """Play the tones and illuminate the buttons in the sequence."""
        for beat in range(0, num_notes):
            button_index = challenge_sequence[beat]
            self.rgb_chain[0] = RGB_COLORS[button_index]
            self.rgb_chain.write()
            self.buzzer.play_color(button_index)
            time.sleep(KEY_INTERVAL)
            self.rgb_chain[0] = (0, 0, 0) # turn RGB off
            self.rgb_chain.write()
            time.sleep(KEY_INTERVAL)
Listing 11-4

Simon Class

To create the class file, open a new file in Thonny and save it as simon_says.py in the project folder.

Notice one interesting feature of this code. Look at the read_sequence() function. Notice there is another function declared inside that one named show_challenge_sequence(). This is a common technique for removing duplicate code. The inner function simply encapsulates a few lines of code that are called repeatedly from the outer function. What is this function used for, you may wonder? It is placed there so you can cheat while debugging your code. It simply prints to the console the sequence of buttons, making it easy for you to evaluate the game. I’ll leave it up to you as to where you can place a call to this function. Hint: Look for a line of code commented out. Cool, eh?

Now we can write our main code.

Main Code Module

Now we can write the main code. Open a new file and name it main.py. Since we are placing most of the hardware work in the Simon game class, all we need to do here is write code to interact with the Simon game class.

We use a simple loop for controlling the mode button to set the number of players and the start button to start the game. We will make the code allow the use of the mode button so long as a game is not in process.

Note

The mode button is closest to the Grove connector. If you orient the module with the Grove connector on top, the mode button is the top button and start is the bottom button.

Recording the number of players is done using a variable that we allow up to four players. So, pressing the mode button continually will cycle through the options (e.g., 1, 2, 3, 4, 1, 2, 3, 4…). We will use this value when the player presses the start button.

When the start button is pressed, we use the Simon class to start a new game with the start_game() method passing in the number of players selected. Then we call the play() function turning control over to the Simon class. Once the game ends, we place the Simon instance back to the setup mode with the setup_mode() function. A few short delays are added to make the game flow better.6

By placing all of the game control in its own class, we’ve simplified the main code. Listing 11-5 shows the complete code for the main script for this project. You can read it to see how all of the code works.
from time import sleep
from project5.simon_says import Simon, MAX_PLAYERS
from project5.buttons import Buttons
def main():
    """Main"""
    print("Welcome to the Simon Says game!")
    simon = Simon()
    game_started = False
    start_button = False
    mode_button = False
    num_players = 1
    buttons = Buttons()
    while True:
        if not game_started:
            # Show number of players
            start_button = buttons.get_button_value(Buttons.START_BUTTON) == 0
            mode_button = buttons.get_button_value(Buttons.MODE_BUTTON) == 0
            if start_button:
                print("Start button pressed.")
                simon.start_game(num_players)
                sleep(1)
                simon.play()
                sleep(1)
                simon.setup_mode()
            elif mode_button:
                num_players = num_players + 1
                if num_players > MAX_PLAYERS:
                    num_players = 1
                print("Mode button pressed - {0} players."
                      "".format(num_players))
                sleep(0.050)
                simon.show_players(num_players)
                sleep(2)
                simon.setup_mode()
if __name__ == '__main__':
    try:
        main()
    except (KeyboardInterrupt, SystemExit) as err:
        print(" bye! ")
Listing 11-5

Main Code Module

OK, that’s it! We’ve written the code. We’re now ready to execute the project!

Execute

Now that we’ve spent many pages exploring the Grove modules and writing the code to interact with them, it is time to test the project by executing (running) it. Recall, we can copy all of our code to our Pico. If you haven’t already done so, you should create a folder named project5 on your Pico and then upload the buttons.py, buzzer.py, lcd1602.py, p9813.py, and simon_sys.py files to the project5 folder. Next, upload the main.py file to the root folder of your Pico.

Recall, there are two ways to test or execute the code. We could use Thonny to connect to the Pico and simply run the main.py script. Or we can reboot the Pico by unplugging it and plugging it back in to the USB port on your PC.

The difference is if you run the main.py file manually, you will see the debug statements show in the output at the bottom of Thonny. Running the script automatically may not show those statements if you do not use Thonny or a similar application to connect to the Pico.

Once the program starts, you will see some diagnostic messages written to the terminal. You will also see a welcome message appear on the LCD. You can then press the mode button to set the number of players, and when you’re ready, press the start button to start the game. Figure 11-7 shows examples of the LCD when in setup mode.
Figure 11-7

Executing the Simon Says project

When you run the code from Thonny, you will see output similar to the following:
Welcome to the Simon Says game!
Mode button pressed - 2 players.
Mode button pressed - 3 players.
Mode button pressed - 4 players.
Mode button pressed - 1 players.
Start button pressed.
Playing theme...
...

If everything worked as executed, congratulations! You’ve just built your second Grove project. If something isn’t working, check your connections to ensure you’ve connected everything correctly.

Since we named the main code file main.py, you can restart the Pico and run the game on boot. If you connect a power supply to the Pico, or a 5V battery pack, you can play the game as a handheld game. Cool!

Taking It Further

While we didn’t discuss them in this chapter, there are some ideas where you could make this project into an IoT project. Here are just a few suggestions you can try once we have learned how to take our projects to the cloud. Put your skills to work!
  • Complete the enclosure: Use the sample base plate and create a cover for the game.

  • Handheld version: Find someone with a 3D printer and print out the mounting plate. Once assembled, purchase a portable 5V battery and attach it to the bottom of the mounting plate. This will allow you to run the game without the need of a PC or USB power from a wall wart.

  • Increase the difficulty: One of the ways you can enhance gameplay is to make the timeout time for a player to enter a sequence shorter as gameplay continues. For example, for the first n sequences, use the default timeout; for the next n sequences, reduce the timeout by a portion; and so on until the timeout gets to a minimum timeout. If you do the same thing for the delay used in playing the challenge sequence, it will ensure the game will become much more difficult and more fun to play.

Summary

In this chapter, we completed a more complex project to explore Grove modules. Along the way, we learned more about how to work with Grove modules including how to write our own classes for managing multiple modules and sensors (buttons are sensors after all).

Rather than build something that has an “OK, that’s cool” factor, we built a working Simon game that we can play with our friends. Since we wrote all of the code for the game, we can also expand it however we want, including making it more difficult to play as the game progresses.

Using the examples in this chapter, you will discover other uses for the code to build other games and replicate another vintage handheld electronic game.

In the next chapter, we will look at one more project using Grove modules, returning to building projects that use sensors. We will build an environmental project that allows you to monitor your environment.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.200.27.215