Thursday, 9 June 2016

Arduino Platform - SIMON Game Implementation

Note: I originally published this a long time ago (October 2009) on my old website (accidentally killed by my ISP) and also on CodeProject.

Hardware Schematic

Introduction

Having been a CodeProject member for several years, and not having got round to ever publishing an article, left me feeling a bit disappointed in myself. I have thought many a time on what kind of article I can produce. Then after coming across some great articles by jeffb42 here on CodeProject, my problem was solved.

I started playing with the Arduino hardware platform about two months ago, after looking for something suitable for a couple of projects I had in mind for use in the house. In order to get to the stage of being able to make use of the device, I had to first get to know its capabilities.

This article will be looking at how to overcome the problem of running a SIMON game clone using the Arduino platform, as this demonstrates methods of dealing with the main application Loop constraint and code reuse on this hardware platform.

For those of you not familiar with SIMON, this was an early electronics game from the 70's, where the user basically had to repeat back a sequence to the console to progress to the next level. In my implementation of the game, the user can play the Progressive Mode, where the game re-uses the previous level as the start of the next level before adding a random step at the end, or Random Mode, where every level of the game produces a new random sequence, each step longer than the previous.

jeffb42's Introduction to Arduino and Interfacing with LCD articles provide more details on the Arduino platform specifics, and here on my website, I have various examples of some of the IO basics, so there is no need to repeat this here.

Background

The challenge to building this game on the Arduino is the way in which the platform works. Within the source code, there are three areas in which to place code: the initialisation area, the setup method, and the loop method. These are executed in that order.
//initialisation section
// Library references and variable/constant declarations contained here

void setup()
{
    //This method runs once
    //Hardware allocation done here e.g. input/output pin directions
}

void loop()
{
    //This method will run until power is removed or a new program is 
    //uploaded to the platform
}
The challenge is being able to make the code operate in such a way that a game can be played over and over again, in either of the two modes, without ever leaving a never ending loop.

Using the Code

The schematic of the hardware requirements is as you can see at the top of this post, and the full source code can be found at the end of the post.

The source can also be grabbed from GitHub Gist at: https://gist.github.com/DaveAuld/b32ee816e2aaef6a4ca5305ac4a5c99d

If you own the necessary components, you can build the game and upload the code to the Arduino to make it work. Alternatively, if you are just starting out with the platform or something similar, the code may provide you with ideas on how to achieve similar goals in your own projects.

There is also a video of the 1st version of this code/hardware running at the bottom of the post before the full source dump.

The Game Structure

We need to be able to keep the game code running within an endless loop, and one method to achieve this is by using flags. Boolean variables which are either True or False is one of the easiest ways to achieve this. Looking at how the game works, there are a few basic flags that are required. They hold the state of the game, i.e., has it started, has it been played, and if the game is now over. A couple of other variables are also required to keep track of what the sequence of steps generated is and what level the user is currently on. These requirements as well as the hardware pin allocations are identified in the initialisation section of the code:
#include <LiquidCrystal.h>


//initialise the library with the numbers of the interface pins
LiquidCrystal lcd(13,12,11,10,9,8);

int userInput = 0;      //Analog pin for user input from buttons
int led1 = 2;           //LED 1
int led2 = 3;           //LED 2
int led3 = 4;           //LED 3
int led4 = 5;           //LED 4
int speaker = 6;        //Speaker

//Game Stats
boolean started = false;    //Has the game started yet
boolean gameover = false;   //Has the game ended
int level = 0;              //What level is the user on (Score at end = level -1)

int gameMode = 0;           //Which game mode is being used
                            // 1 = Progressive
                            // 2 = Random
                            
boolean soundEnabled = true;  //Is the sound enabled or not
                            
int lastLevelSeq[50];        //The sequence for the steps used
                             //in progressive mode previous level
                             //Also used by game over to replay correct sequence back
                             //Nobody can get passed 50, surely!

The next section of the code is the setup() method. This is where the hardware pins are initialised.
void setup()
{
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  pinMode(led4, OUTPUT);
  pinMode(speaker, OUTPUT);
  
  //Set up the LCD Col and Row count
  lcd.begin(16,2);
  delay(100);
  lcd.clear();
  lcd.print("Welcome to");
  lcd.setCursor(0,1);
  lcd.print("Arduino SIMON");
  
  //...........rest of setup code snipped. see the full code at bottom.

From here, the next thing that is executed is the loop() method, which is run endlessly, hence the requirement to keep track of the game status. Also, to keep the code shorter and more efficient, a number of helper methods are used to meet the reuse requirements mentioned at the start of the article.
//used to determine which button the user is pressing
getButtonPressed()
//Play a given sound
playTone(tone)
//This is the actual game level, it returns boolean true
//is user success or false if user failed
doLevel(level)
//Turn on a given led, or 0 to turn them all off
lightLed(led)
//This will play a sound and light the appropriate 
//led using the other helper methods
playStep(step)

The main loop() structure looks like the following (all the details have been stripped to leave the structure):
void loop()
{        
    while (started == false)
    {
      if (getButtonPressed() > 0)
      {
        //This part waits for the user to press a button to start the game
      }
    }
     
    while (gameMode == 0)
    {     
      switch (getButtonPressed())
      {
      case 1:
            //The user pressed button 1 for Progressive Mode
        break;
       
      case 2:
            //The user pressed button 2 for Random Mode
        break;
      
      case 4:
        soundEnabled = (!soundEnabled);
        //The user user is pressing Button 4 toggling the sound on and off
        break;  
      }
    }    
     
    while (gameover == false)
    {
      if (doLevel(level)==true)
        {
            //The user played a level and got it correct
        } 
       else
        {
            //Level Wrong, set game over
        }  
    }
    
    if (gameover == true)
    {
      //The game is now over, playback the correct sequence
    }
      
    //Wait for user
    while (gameover == true)
    {
       if (getButtonPressed() > 0)
        {
            //Wait for the user to press a button to go back to the start menu
        }        
    }     
}

The easiest way of coming up with a structure is to play the game in your head and write down in blocks each step that is required; from this, you can build a flow diagram and then the code structure.

Points of Interest

Given the limited number of digital IO pins available, the four buttons were actually linked through a resistor network onto an analog input pin. Each button pressed will change the value of the analog input; from this value, it is then possible to determine which button is pressed, using the getButtonPressed() method. Also, some variance may occur through fluctuations of the analog signal, and suitable resistor sizes must be used to give sufficient spacing of the input values:

/*
Read the analog input and determine which button is pressed
*/  
int getButtonPressed()
{
    // What is the pushbutton resistor matrix value
    int userValue = 0;
    userValue = analogRead(userInput);
    
    int buttonPressed = 0;
      
    if (userValue > 850)
    {
      buttonPressed = 0;
      //No Button Pressed
    }
    if (userValue < 850)
    { 
      buttonPressed = 4;
      // Maybe Button 4 still to check others
    }
    if (userValue < 800)
    {
      buttonPressed = 3;
      // Maybe Button 3 still to check others
    }
    if (userValue < 700)
    {
      buttonPressed = 2;
      // Maybe Button 2 still to check last one
    }
    if (userValue < 600)
    {
      buttonPressed = 1;
      // Done Checking buttons
    }
    return buttonPressed;  
}

Overall, as a first project on the Arduino for myself, this gave me a good challenge into coming up with a suitably structured code that met the game requirements.

Video of 1st Version Running



Full Code Dump:

  1  /*
  2    Arduino Simon Game
  3  
  4    Author: David M. Auld
  5    Date: 19th October 2009
  6    Version: 2.0
  7    
  8    This is the simple game of Simon built on the Arduino Platform.
  9    
 10    It uses the following Hardware;
 11    4 x LEDs for Visual Feedback of step
 12    4 x Pushbuttons for Input of step
 13    1 x Speaker for Audio Feedback of Step
 14    1 x LCD for Game Info and Scoring.
 15    1 x Arduino
 16    +various Resistors/pots etc.
 17   
 18    Built and tested on Arduino Duemilanove. 
 19    
 20    Change Log
 21    **********
 22    V2: 19th October 2009
 23    ---------------------
 24    NEW: Added progressive game mode, V1 was random only
 25    NEW: Correct sequence playback on Game Over
 26    NEW: Sound On/Off Mode
 27    FIX: Removed surplus variables, and code tidy
 28    IMP: Random Seed Generator now reads and adds all analog inputs to generate seed value
 29    
 30    V1: 17th October 2009
 31    ---------------------
 32    Initial Release
 33    
 34  */
 35  
 36  #include <LiquidCrystal.h>
 37  
 38  //initialise the library with the numbers of the interface pins
 39  LiquidCrystal lcd(13,12,11,10,9,8);
 40  
 41  int userInput = 0;      //Analog pin for user input from buttons
 42  int led1 = 2;           //LED 1
 43  int led2 = 3;           //LED 2
 44  int led3 = 4;           //LED 3
 45  int led4 = 5;           //LED 4
 46  int speaker = 6;        //Speaker
 47  
 48  //Game Stats
 49  boolean started = false;    //Has the game started yet
 50  boolean gameover = false;   //Has the game ended
 51  int level = 0;              //What level is the user on (Score at end = level -1)
 52  
 53  int gameMode = 0;           //Which game mode is being used
 54                              // 1 = Progressive
 55                              // 2 = Random
 56                              
 57  boolean soundEnabled = true;  //Is the sound enabled or not
 58                              
 59  int lastLevelSeq[50];        //The sequence for the steps used in progressive mode previous level
 60                               //Also used by game over to replay correct sequence back
 61                               //Nobody can get passed 50, surely!
 62  
 63  //Setup Routine
 64  void setup()
 65  {
 66    pinMode(led1, OUTPUT);
 67    pinMode(led2, OUTPUT);
 68    pinMode(led3, OUTPUT);
 69    pinMode(led4, OUTPUT);
 70    pinMode(speaker, OUTPUT);
 71    
 72    //Set up the LCD Col and Row count
 73    lcd.begin(16,2);
 74    delay(100);
 75    lcd.clear();
 76    lcd.print("Welcome to");
 77    lcd.setCursor(0,1);
 78    lcd.print("Arduino SIMON");
 79    delay(3000);
 80    for (int i = 0; i < 14; i++)
 81      {
 82      lcd.scrollDisplayLeft();
 83      delay(300);
 84      }
 85    lcd.clear();
 86    lcd.print("initialising...");
 87    for (int i = 1; i<7; i++)
 88    {
 89      if (i<5)
 90      {
 91        lightLed(i);    //Light each LED
 92      }
 93        playTone(i);    //Play each tone used
 94        lightLed(0);    //Clear the LED's
 95    }
 96  }
 97    
 98    void loop()
 99    {
100    lcd.clear();
101    lcd.print("To Start");
102    lcd.setCursor(0,1);
103    lcd.print("PRESS ANY BUTTON");
104      while (started == false)
105      {
106        if (getButtonPressed() > 0)
107        {
108          playTone(5);
109          started=true;
110        }
111      }
112      
113      //Clear the previous game Last Level Steps
114      for (int s=0; s < 50; s++)
115      {
116        lastLevelSeq[s] = 0;
117      }
118      
119       delay(500);
120       
121       // Select Game Mode Here
122       lcd.clear();
123       lcd.print("Select Game Mode");
124       
125       while (gameMode == 0)
126      {
127       lcd.setCursor(0,1);
128       lcd.print("1=P 2=R 4=TGLSND");
129       
130        switch (getButtonPressed())
131        {
132        case 1:
133          if (soundEnabled)
134          {
135            playTone(5);
136          }
137          gameMode = 1;
138          lcd.clear();
139          lcd.print("Progressive");
140          lcd.setCursor(0,1);
141          lcd.print("Mode Selected");
142          break;
143         
144        case 2:
145          if (soundEnabled)
146          {
147            playTone(5);
148          }
149          
150          gameMode = 2;
151          lcd.clear();
152          lcd.print("Random");
153          lcd.setCursor(0,1);
154          lcd.print("Mode Selected");
155          break;
156        
157        case 4:
158          soundEnabled = (!soundEnabled);
159          lcd.setCursor(0,1);
160          
161          if (soundEnabled)
162          {
163            lcd.print("Sound Mode: ON  "); 
164            playTone(5);          
165          }
166          else
167          {
168           lcd.print("Sound Mode: OFF "); 
169          }
170          delay(1000);
171          break;  
172        }
173      }    
174      
175      delay(1000);
176      
177       //Set start level
178       level =1;
179       
180      while (gameover == false)
181      {
182        if (doLevel(level)==true)
183          {
184            //Level Correct, increment Level and repeat
185            lcd.clear();
186            lcd.print("LEVEL: "); lcd.print(level);
187            lcd.setCursor(0,1);
188            lcd.print("Completed......"); 
189            
190            if (soundEnabled)
191            {
192              //play level correct sound
193              for (int i =0; i < 3; i++)
194              {
195                playTone(5);
196                delay(50);
197              }
198            }
199            else
200            {
201              delay(125);
202            }
203            delay(1000);    //Delay so user can see level completed message
204            level++;        //Set next level
205          } 
206         else
207        {
208         //Level Wrong, set game over
209         gameover = true;
210        }  
211      }
212      
213      if (gameover == true)
214      {
215        lcd.clear();
216        lcd.print("WRONG! GAME OVER");
217        lcd.setCursor(0,1);
218        lcd.print("SCORE: "); lcd.print(level-1);
219        
220        if (soundEnabled)
221        {
222          for (int i = 0; i < 5; i++)
223          {
224            playTone(6);
225            delay(50);
226          }
227        }
228        else
229        {
230          delay(125);
231        }
232      }
233          
234        //Playback the correct sequence
235        lcd.clear();
236        lcd.print("Sequence");
237        lcd.setCursor(0,1);
238        lcd.print("should have been");
239        delay(500);
240        for(int s=0; s < level; s++)
241        {
242          playStep(lastLevelSeq[s]);
243          //lightLed(lastLevelSeq[s]);
244          //playTone(lastLevelSeq[s]);
245          //lightLed(0);
246          //delay(250);
247        }
248        
249        lcd.clear();
250        lcd.print("Your Score: "); lcd.print(level-1);
251        lcd.setCursor(0,1);
252        lcd.print("PRESS ANY BUTTON");
253        
254        //Wait for user
255        while (gameover == true)
256        {
257         if (getButtonPressed() > 0)
258          {
259          level = 0;
260          
261          if (soundEnabled)
262          {
263            playTone(5);
264          }
265          gameover = false;
266          started = false;
267          gameMode = 0;
268          }        
269        }     
270      }
271    
272     /*
273    Read the analog input and determine which button is pressed
274    */  
275    int getButtonPressed()
276    {
277      int userValue = 0;      // What is the pushbutton resistor matrix value
278      userValue = analogRead(userInput);
279      
280      int buttonPressed = 0;
281        
282      if (userValue > 850)
283      {
284        buttonPressed = 0;      //No Button Pressed
285      }
286      if (userValue < 850)
287      { 
288        buttonPressed = 4;      // Maybe Button 4 still to check others
289      }
290      if (userValue < 800)
291      {
292        buttonPressed = 3;      // Maybe Button 3 still to check others
293      }
294      if (userValue < 700)
295      {
296        buttonPressed = 2;      // Maybe Button 2 still to check last one
297      }
298      if (userValue < 600)
299      {
300        buttonPressed = 1;      // Done Checking buttons
301      }
302      return buttonPressed;  
303    }
304    
305    /*
306    This will light the led, play the tone, switch off the led 
307    */
308    void playStep(int number)
309    {
310      lightLed(number);
311      if (soundEnabled)
312      {
313        playTone(number);
314      }
315      else
316      {
317        delay(100);
318      }
319      delay(250);
320      lightLed(0);
321    }
322    
323    
324    /*
325    Light the Relevant LED
326    param led = which LED to light
327    */
328    void lightLed(int led)
329    {
330      switch (led)
331      {
332      case 0:
333        digitalWrite(led1,LOW);          //Set all the LEDs off
334        digitalWrite(led2,LOW);
335        digitalWrite(led3,LOW);
336        digitalWrite(led4,LOW);
337        delay(50);                      //Give time for the LED to go
338        break;
339      case 1:
340        digitalWrite(led1,HIGH);        //LED 1 on
341        break;
342      case 2:
343        digitalWrite(led2,HIGH);        //LED 2 on
344        break;
345      case 3:
346        digitalWrite(led3,HIGH);        //LED 3 on
347        break;
348      case 4:
349        digitalWrite(led4,HIGH);        //LED 4 on
350        break;
351      }
352    }
353   
354    /*
355    Play the tones for the game
356    param tone = which tone to play
357    tone 1 to 4 are the button tones
358    tone 5 is level correct tone / start tone (any button pressed)
359    tone 6 is level wrong and game over tone
360    */
361    void playTone(int tone)
362    {
363        int tones[6] = { 1000, 1250, 1500, 1750, 500, 3000}; 
364        for (long i = 0; i < 125; i++)
365        {
366          //generate a square wave
367          digitalWrite(speaker, HIGH);
368          delayMicroseconds(tones[tone-1]);
369          digitalWrite(speaker, LOW);
370          delayMicroseconds(tones[tone-1]);
371        }
372    }
373  
374  /* 
375  This is the actual game level.
376  Return TRUE if success or FALSE if fail
377  */
378  boolean doLevel(int level)
379  {
380    int steps[level];                //Which steps the user must match
381    int userStep = 0;                //Which button has the user pressed
382    int userLength =0;               //How many steps has the user entered
383    boolean levelPass = false;       //Level was won or lost  
384    boolean inProgress = true;       //Level currently running
385      
386    //Display a message to the user that the Arduino is doing its bit
387    lcd.clear();
388    lcd.print("Arduino Turn");
389    lcd.setCursor(0,1);
390    lcd.print("Pay Attention");
391    delay(1000);                    //Add a delay
392    
393    //Seed the random number generator
394    randomSeed(analogRead(0) + analogRead(1) + analogRead(2)+ analogRead(3)+analogRead(4)+analogRead(5));
395    
396    //The steps to be played are dependent on gameMode
397    //Setup the steps based on which mode we are playing
398    
399    if (gameMode == 1)
400    {
401       //Progressive 
402       //Get last level
403       for (int s=0; s < level; s++)
404       {
405         if (level > 1)
406         {
407           // Can only copy if higher than level 1
408           steps[s] = lastLevelSeq[s];
409         }
410         //Add new step
411         steps[level-1] = (int)random(1,5);
412       }
413    }
414    if (gameMode == 2)
415    {
416       //Random
417      for (int s=0; s < level; s++)
418      {
419        steps[s] = (int)random(1,5);
420      }
421    }
422    
423       //Copy back to last level for next time and gameOver
424       for (int s=0; s < level; s++)
425       {
426         lastLevelSeq[s] = steps[s];
427       }
428    
429    //Play the steps
430    for (int s=0; s < level; s++)
431    {
432      playStep(steps[s]);
433      //lightLed(steps[s]);        //Light the led for the step from the sequence
434      //playTone(steps[s]);        //Play the step tone from the sequence
435      //lightLed(0);               //Turn off the LEDs
436    }
437    
438    //Display user turn message
439    lcd.clear();
440    lcd.print("LEVEL: "); lcd.print(level);
441    lcd.setCursor(0,1);
442    lcd.print("Your Turn.....");
443    
444    while (inProgress)
445    {
446      //Get the user input  
447     userStep = getButtonPressed();
448     if (userStep >0)
449     {
450       userLength++;
451       playStep(userStep);
452       
453       //lightLed(userStep);
454       //playTone(userStep);
455       //lightLed(0);
456     
457       //Check the user input against the sequence
458       if (steps[userLength-1] == userStep)
459       {
460         //Correct button pressed, check if sequence still to complete
461         if (userLength ==level)
462         {
463           //finished the sequence
464           inProgress = false;
465           levelPass = true;
466         }
467       }
468       else
469       {
470         //wrong button pressed
471         inProgress = false;
472         levelPass = false;
473       }
474      }
475    }
476    return levelPass;
477  }