Playing a game with push buttons: Simon says

In this tutorial we learn how you can implement the game "Simon says" on the Dwengo board. The goal of the game is that the player can reproduce a demonstrated sequence. The sequence will be shown with 3 LEDs, while the player can input the sequence by using the push buttons of the Dwengo board. The different aspects of the previous tutorials come together now in a more complex program. Moreover, you also learn how to use the push buttons of the Dwengo board.

Requirements

  1. One Dwengo board
  2. One Dwengo programmer
  3. Enclosed USB cable

Simon says

In the game "Simon says" a player has to reproduce a demonstrated sequence. This sequence becomes increasingly longer and longer, making the game more difficult.

Of course you can specify a maximum length for the sequence. For showing the sequence we light up LED0 up to LED2 on the board. Correspondingly, the player has to push the button E(ast), C(entral) or W(est) respectively when LED0, LED1 or LED2 lit up.

Lets get started ...

Step 1: creating and initializing a project

As usual we start by creating a new project. Next, make a new C file and add it to the project (e.g. SimonSays.c). Also add the library for using the LCD: lcd.c and lcd.h. Make sure that the options of your project are setup correctly so the compiler knows where to look for lcd.h. More information is in the tutorial on the LCD.

On top of the C file for this project (SimonSays.c), add the code to load the required libraries. This time we are using four of them:

  1. #include <p18f4550.h>
  2. #include <delays.h>
  3. #include <stdlib.h>
  4. #include "lcd.h"

The first three contain the necessary C functions we will be using and the fourth one is used to operate the LCD. Similar to previous projects, also include the code to set the configuration bits correctly.

Because the code becomes more elaborate, we define several macros in order to make the code more readable.

  1. #define BYTE unsigned char
  2. #define TRUE 1
  3. #define FALSE 0
  4. #define PRESSED 0
  5. #define LEDS PORTD
  6. #define S_N PORTBbits.RB4
  7. #define S_E PORTBbits.RB1
  8. #define S_S PORTBbits.RB5
  9. #define S_W PORTBbits.RB0
  10. #define S_C PORTBbits.RB2
  11. #define MAX_LENGTH_SEQ 10
  12. #define ALFA 3

The code in main() starts with the definition of the variables we'll use and some initializations. Note that we try to make as many variables as possible of one byte in size. Only the variable seed needs a bigger size. To specify a one byte variable we can use unsigned char, but thanks to the macros we can write BYTE. It is a good habit to choose the smallest possible datatype. For more information about datatypes, we refer to the basic concepts of programming.

We also make an array of unsigned chars with the name sequence. In this variable the numbers that correspond to the LEDs are stored, the sequence the player has to reproduce. Next we initialize the LCD, configure the pins that are connected to the LEDs as outputs and set all port B-pins as inputs (because the push buttons of the Dwengo board are connected to these pins, as shown on the Dwengo board schematics).

We also have to activate the pull-up resistors of the B-pins. The moment the buttons are pushed, the corresponding pin is tied to 0 V. The pull-up resistor makes sure that the pin is at a defined level of 5 V while the button is not pressed, rather than letting it float which can result in a random value being read by the PIC.

  1. void main(void) {
  2. unsigned int seed;
  3. BYTE sequence[MAX_LENGTH_SEQ];
  4. BYTE error, n, i, j, is_button_pressed, button_pressed;
  5.  
  6. initLCD();
  7. clearLCD();
  8.  
  9. TRISD = 0; // port D as outputs
  10. LEDS = 0; // switch leds off
  11.  
  12. TRISB = 0xFF; // port B as inputs (buttons)
  13. INTCON2bits.NOT_RBPU = 0; // enable port B pull-up (buttons)

Step 2: let the games begin

The game only starts when the player has pressed the C button. So we start by printing a message to the screen.

To check whether the C button is pressed, we have to check if port RB2 is low (0 V). We used the macros to define S_C as port RB2 and PRESSED as 0. The while loop is repeated as long as S_C is not pressed. When the button is pressed, we leave the loop.

Meanwhile we count how long it takes before the player presses the button. This happens by increasing the variable seed by one every iteration. We will use seed to generate a random sequence for the game. Note that computers do not generate real random sequences, but actually pseudo-random sequences. By using srand(seed) we actually define which (fixed) sequence we want. In the end we get a random number with the function rand(), due to the fact that srand(seed) more or less depends on random events (the time needed by the player to press the button).

  1. LCDBacklightOn();
  2. appendStringToLCD("Press button C to play");
  3.  
  4. seed = 0;
  5. while (S_C != PRESSED) {
  6. seed++;
  7. }
  8. srand(seed); // seed of pseudo random sequence

Step 3: the actual game

Now we can start with the actual game. If we convert the game to an algorithm, we get the following steps:

  1. We start with an empty sequence (no elements in the sequence)
  2. Extend the sequence (add one element)
  3. Show the complete sequence
  4. Wait until the player pushes a button
  5. Check if the pushed button corresponds with the correct LED in the sequence
  6. If a mistake of the player is detected: stop the program, the player has lost
  7. Repeat step 4, 5 en 6 until the complete sequence is entered by the player
  8. Go back to step 2 if the maximum length of the sequence is not yet reached
  9. The maximum length is reached: the player has won

If we convert this to code, we first initialize the variable error to FALSE to indicate that there have been no mistakes made so far by the player. We set the counter n to 0 because the sequence is empty for the time being.

  1. error = FALSE; // no errors made yet
  2. n = 0;

Next we build a big while-loop that runs as long as we have not reached the maximum sequence length and the player has made no mistakes. We show on screen which round we are in.

  1. while (n < MAX_LENGTH_SEQ && error != TRUE) {
  2. clearLCD();
  3. appendStringToLCD("Round ");
  4. appendIntToLCD(n+1);
  5. appendStringToLCD(": showingsequence ");

Because the LEDs are connected to the data lines of the LCD, the LEDs will briefly light up while the text is written to screen. Once the text is written to the screen, we will have to explicitly turn off all LEDs, to make sure that that they are all turned off (the LCD library may have left some data lines in the high state).

Next we wait for 250ms to give the player the time to look at the LEDs. Since the argument of the function Delay10KTCYX(byte) only accepts a number between 0 and 255, we use a for loop to call the function several times, so we wait a longer time.

  1. LEDS = 0;
  2. for (j=0;j<5;j++) {
  3. Delay10KTCYx(60); // wait 250 ms
  4. }

Next we add a new element to the sequence at position n. The value is calculated by the pseudo random generator. rand() generates a random natural number. We apply the modulo 3 (ALFA) operation to this number, which results in a number between 0 and 2. After all we only use 3 LEDs. Number 0 corresponds to LED0 and push button W, number 1 with LED1 and push button C, and number 2 with LED2 and push button E.

Note that the built-in pseudo random generator, in combination with the seed, is random enough for this game, although the numbers are not really random.

  1. sequence[n] = rand() % ALFA; // pick random number from 0 to 2

Now we go through the whole sequence and show each element separately. The first time $n$ will be 0, so we show the first element in the array. Note that each LED is lit up for precisely 500 ms and we wait 250 ms between two elements of the sequence. You can adjust each of these times, allowing the sequence to be shown faster of slower.

  1. for (i=0;i<=n;i++) { // show sequence
  2. if (sequence[i] == 0)
  3. LEDS = 1;
  4. else if (sequence[i] == 1)
  5. LEDS = 2;
  6. else
  7. LEDS = 4;
  8. for (j=0;j<10;j++)
  9. Delay10KTCYx(60); // wait 500 ms
  10. LEDS = 0;
  11. for (j=0;j<5;j++)
  12. Delay10KTCYx(60); // wait 250 ms
  13. }

After the sequence is shown, we display a different text to the LCD in which the player is asked to press the correct push buttons.

  1. clearLCD();
  2. appendStringToLCD("Round ");
  3. appendIntToLCD(n+1);
  4. appendStringToLCD(": press correct buttons ");

Now we wait for the reproduction of the sequence by the player.
For this we iterate over the sequence again with a for loop. In this for loop we first wait for the player to press one of the allowed push buttons (E, C or W). For this we check all the push buttons using a while loop in which all buttons are checked for not being pushed. Waiting until a certain state (in this case: one of the buttons being pressed) is achieved, is called polling.

When the player has pressed a button, the number of the button is stored in the variable button_pressed. On the LCD we show a star to show the player that they can release the button. After a button was pressed, we wait for 250 ms, to avoid counting the same event multiple times. The player can try to press the button as short as possible, but for the fast microcontroller of the Dwengo board it will always seems as if the button was pressed for a longer time (= multiple instruction cycles). Now, the player can press the button for up to 250 ms which corresponds to the needed time for pressing a button for a human.

Finally the program has to check whether the pressed button, stored in the variable button_pressed, corresponds to the lit up LED in the sequence. This happens by means of an if instruction. When the pressed button is wrong, we set the variable error to TRUE which results in the termination of the loop while(n MAX_LENGTH_SEQ && error != TRUE). When the player makes a mistake in the middle of the sequence, the rest of the sequence will not be checked anymore. By assigning a value larger than n to i, we will jump out the loop for(i=0; i<=n;i++). The player has lost.

  1. for (i=0;i<=n;i++) {
  2. is_button_pressed = FALSE;
  3. while (!is_button_pressed) {
  4. is_button_pressed = TRUE;
  5. if (S_E == PRESSED)
  6. button_pressed = 0;
  7. else if (S_C == PRESSED)
  8. button_pressed = 1;
  9. else if (S_W == PRESSED)
  10. button_pressed = 2;
  11. else
  12. is_button_pressed = FALSE;
  13. }
  14. appendStringToLCD("*"); // give feedback about pressed buttons
  15. for (j=0;j<5;j++) {
  16. Delay10KTCYx(60); // wait 250 ms, avoids users press too long
  17. }
  18. if (button_pressed != sequence[i]) {
  19. error = TRUE;
  20. i = n + 1; // jump out for-loop if error
  21. }
  22. }

When the sequence is entered correctly, the sequence can be extended using n++. We then return to the beginning of the while loop.

  1. n++;
  2. } // ending large while-loop

When the loop while(n < MAX_LENGTH_SEQ && error != TRUE) is finished, this can mean two things:

  1. The player has made a mistake and the game finished prematurely. The variable error has the value TRUE.
  2. The games is ended because the sequence has reached the maximum length. The variable error is equal to FALSE.

Checking this is done by means of an if test and we display the correct message to the screen.

  1. if (error == TRUE) {
  2. clearLCD();
  3. appendStringToLCD(" :( You LOSE :( ");
  4. }
  5. else {
  6. clearLCD();
  7. appendStringToLCD(" :) You WIN :) ");
  8. }

The game is now finished, but this does not mean the end of our code. When we would end the bracket of the main function here, then the compiler would make sure (by adding a few instructions) that the main function would be restarted from the beginning. This would result in unwanted behavior, because the message whether the player won or lost would be overwritten immediately by the message asking the player to press the C push button to start the game anew.

This is avoided by adding an infinite loop in the form of a while(TRUE) loop. The program stays in this loop until the player presses the reset button.

  1. while (TRUE);
  2. } // end of main

The complete source code of the game and the required LCD library can be downloaded below. Have a lot of fun programming and playing this game!

Source codeSource code files are only available to Dwengo customers. If you have bought with us before, please log in to download source files.