Programming: Arduino

Check out my other articles:


Sudoku

The word Sudoku means single number in Japanese and reflects the objective of the game - to fill a 9 x 9 grid with digits so that each column, each row and each 3x3 sub-grid contains all of the digits from 1 to 9. This hugely popular game is pervasive - appearing in magazines, books, newspapers and on numerous phone and tablet platforms. I thought it was time to bring it to the Arduino!


Overview

After the disappointment of my first attempt at a classic video game, I decided to focus on something that did not require moving graphics. I decided to develop a Sudoku game that would take advantage of most of the SmartGPU2's features including its graphics library, the onboard EEPROM and SD card support. This version has a number of neat features :

  • squares can be marked in either pencil or pen with errors (optionally) highlighted
  • new games can be retrieved from a library of games stored on the SD card. These are organised on three separate tabs - easy, medium and hard - with paging over longer lists of games
  • ability to save a game at any stage and restore back to that point. This feature utilises the EEPROM allowing the saved game to be restored after the power has been restored to the Arduino.
  • automatic saving of the game after each pencil or pen mark change. This feature also uses the EEPROM and enables the Arduino to be turned off at any time and have the state restored exactly when power is resumed.

The screen is laid out with the main playing board on the left hand side and both a pncil and pen scratch pad on the right hand side. When no cell of the main board is selected, the pencil and pen pads are greyed out.


Selecting a blank or user-entered cell enables the pencil and pen pads. Any preselected pencil or pen markings are highlighted with errors optionally highlighted in red.


Confirmation and status messages use a function inspired by Visual Basic's MsgBox or C#'s MessageBox function with input parameters for the caption, message and buttons to display. The function simply returns the selected button to the calling routine .. I am pretty confident that I can re-use this in future projects!


Click the Menu button reveals options to save and restore the game or to load a new game from one of the puzzles stored on the SD card. These new games are categorised by complexity into separate tabs (easy, medium or hard) and paging controls at the bottom of each tab enables navigation through the list of puzzles.

Errors in pencil and pen markings can be automatically be highlighted in game play by checking the two checkboxes in the lower right of the screen. Click Play! to resume playing ..


It Sudoku, simply fill in all of the squares while making sure that each row, column and 3x3 grid use the numbers one to nine and ... voila! You're done.


Δ Top


Code Fragments

The following code highlights areas of interest in the application. It is not intended to be a tutorial or a complete solution. I have provided the complete code for download and you may use or modify it as required.

Maintaining the Puzzle Solution and Game Play

The solution and game play are stored in two multi-dimensional grids. To conserve space, both arrays are of type 'char' - a single byte element capable of storing a single ASCII character or a number between -127 and +127.

001
002
char solution[9][9];
char board[9][9][10];

As the name suggests, the solution array is used to store the solution with positive values used to indicate the numbers that are displayed during gameplay and negative numbers used to indicate the hidden, correct solution values. A sample puzzle and its corresponding solution array is shown below.

      -3-5-61-2-6974
-46-7-39852-1
-912-47-5-6-8-3
-2-9-6-743-1-58
1-75-2-8-94-36
8-4-356-1-2-9-7
5-2-1-83-476-9
-63495-7-81-2
789-612-3-4-5

The board array stores the game play and is a little more complex - the first two axes define a 9x9 board. The last dimension has ten elements with the 0th element used to store any value that has been penned in. Elements at index one to nine are used to store any values that have been pencilled in.

Δ Top

Rendering the Board

The board can be rendered by simply iterating through the board array and rendering each cell in turn. Optionally, a cell can be highlighted to indicate whether a user has selected it or not.

001
002
003
004
005
006
007
void drawMainBoard() { 
  for (byte x=0; x <= 8; x++){
    for (byte y=0; y <= 8; y++){
      drawCell(x, y, false);
    }
  }
}

Shading is used to differentiate the nine separate 3 x 3 sub grids of the board. Line 019 tests the X and Y coordinates of the cell being rendered is in the middle column or middle row of the 3x3 grids but not in the midlle column and the middle row to determine which 3 x 3 grid background colour to use.

If the cell to be rendered has been selected by the user, the cell is rendered with an a different background with a contrasting coloured outer border (lines 011 - 016).

After rendering the cell container, the application determines whether to render a number supplied as part of the puzzle (the element at position X and Y of the solution array is greater than zero, line 031), a number penned in by the user (the element at the X, Y and 0th element of the board array is not zero, line 041) or one or more numbers have been pencilled in by the user (tested by iterating thir array dimension at position X and Y of the board array, line 061 onwards).

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
void drawCell(char x, char y, boolean highlight) {

  POINT p1, p2;
  COLOUR colour;
  
  p1.x = (x * CELL_SPACING);
  p1.y = (y * CELL_SPACING);
  p2.x = (x * CELL_SPACING) + CELL_WIDTH;
  p2.y = (y * CELL_SPACING) + CELL_WIDTH;
  
  if (highlight) {
    lcd.drawRoundRect(p1.x, p1.y, p2.x, p2.y, radius5,
                      COLOUR_DARKBLUE, solidFill); 
    lcd.drawRoundRect(p1.x, p1.y, p2.x, p2.y, radius5,
                      COLOUR_GRID_SELECTED, hollowFill); 
  }
  else {
  
    if (((x>2 && x<6) || (y>2 && y<6)) && !(x>2 && x<6 && y>2 && y<6)) {
      colour= COLOUR_GRID_HIGHLIGHT;
    }
    else {
      colour= COLOUR_GRID_LOWLIGHT;
    }
	
    lcd.drawRoundRect(p1.x,p1.y,p2.x,p2.y,radius5,colour,solidFill); 
	
  }
  
  if (solution[x][y] > 0) {
  
    lcd.setTextColour(WHITE);    
    lcd.setTextSize(FONT3);
    itoa(solution[x][y], buf, 10);
    lcd.string((x * CELL_SPACING) + 11, (y * CELL_SPACING) + 6, 
		MAX_X_LANDSCAPE, MAX_Y_LANDSCAPE, buf, 0);
		
  }
  else {
  
    if (board[x][y][CELL_SELECTED_VALUE] > 0) {
	
      lcd.setTextColour(YELLOW);    
      lcd.setTextSize(FONT3);
	  
      if (showPenInError && penInError(x, y, board[x][y][0])) {
        lcd.setTextColour(RED);    
        lcd.setTextSize(FONT3);
      }
	  
      itoa(board[x][y][0], buf, 10);
      lcd.string((x * CELL_SPACING) + 11, (y * CELL_SPACING) + 6, 
                  MAX_X_LANDSCAPE, MAX_Y_LANDSCAPE, buf, 0);
				  
    }
    else {
	
      lcd.setTextColour(YELLOW);    
      lcd.setTextSize(FONT0);
	  
      for (byte x1=1; x1<=3; x1++) {
	  
        for (byte y1=0; y1<=2; y1++) {
		
          if (board[x][y][(y1*3)+x1] > 0) {
		  
            lcd.setTextColour(YELLOW);    
            lcd.setTextSize(FONT0);
            if (showPencilInError && pencilInError(x, y, (y1*3)+x1)) {
                lcd.setTextColour(RED);    
                lcd.setTextSize(FONT0);
            }
            itoa(board[x][y][(y1*3)+x1],buf, 10);
            lcd.string((x * CELL_SPACING) - 4 + (x1 * 9), 
                       (y * CELL_SPACING) + 1 + (y1 * 10), 
                        MAX_X_LANDSCAPE, MAX_Y_LANDSCAPE, buf, 0);
				
          }
		  
        }
		
      }
	  
    }
	
  }
  
}
Δ Top

Saving the Game to the EEPROM

The SmartGPU2 comes equipped with 8 x 2K pages of EEPROM or flash memory which is ideal for storing both a saved version of the game - allowing the user to restore back to a previously saved point - and to automatically store the users progress as the change cell values - allowing the game to restore automatically after the power is turned off and resumed. The saveGsme function accepts a page number and uses this to determine which page to write to.

As I plan to use ny Arduino for multiple purposes, I cannot guarantee the contents of the EEPROM are either blank or contains a valid Sudoku puzzle. To ensure that any data found is valid, I prime the first six bytes of the output buffer with the characters 'SUDOKU' at line 006.

The EEPROM is easy to read and write from as all activity is handled via a staging buffer. Lines 013 - 021 demonstrate the staging of all of the elements from the solution array into 81 consecutive bytes of the array before this being written to bytes 6 - 86 of the EEPROM's buffer at line 023. The possible values of -9 to +9 are converted to safe ASCII characters by adding 80 to arrive at values in the lower-case alphabet range.

The user's input stored in the board array is then stored to the EEPROM's buffer. As the array is 9 x 9 x 10 (or 810) characters in length this process is performed 81 characters at a time to prevent all of the memory being consumed (lines 028 - 042).

Finally the EEPROM page is cleared and the buffer committed at lines 04 and 045.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
void saveGame(EEPROMPAGE page) {

  lcd.initClearEEPROMBuff();


  // Write out SUDOKU at start of buffer to identify our data ..
  
  lcd.writeEEPROMBuff("SUDOKU", 0, 6, 0);
  

  //  Save solution first ..
  //
  for (byte x=0; x<=8; x=x+1) {   

    for (byte y=0; y<=8; y=y+1) {   

       buffer[(y * 9) + x] = solution[x][y] + 80;
       
    }
    
  }

  lcd.writeEEPROMBuff(buffer, 6, 81, 0);

  
  //  Save board next ..
  //
  for (byte z=0; z<=9; z=z+1) { // Z

    for (byte x=0; x<=8; x=x+1) {   

      for (byte y=0; y<=8; y=y+1) {   
  
       buffer[(y * 9) + x] = board[x][y][z] + 80;
       
      }
     
    }

    lcd.writeEEPROMBuff(buffer, ((z + 1) * 87), 81, 0);
  
  }
  
  lcd.eraseEEPROMPage(page);
  lcd.saveBuffToEEPROMPage(page);    
  
}
Δ Top

Restoring the Game from the EEPROM

Restoring the game from a version saved in the EEPROM is the reverse of the saving process. After determining that the data stored on the EEPROM in the supplied page (line 008 - 011), the application reads 81 characters into memory to populate the solution array - again subtracting 80 from the ASCII friendly characters to derive values between -9 and 9 (lines 016 - 026)

The user's input is then restored into the board array. As the array is 9 x 9 x 10 (or 810) characters in length this process is performed 81 characters at a time to prevent all of the memory being consumed (lines 031 - 045).

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
void restoreGame(EEPROMPAGE page) {

  lcd.initClearEEPROMBuff();
  lcd.fillBuffFromEEPROMPage(page);
  
  // Check to see if the saved data starts with SUDOKU ..

  lcd.readEEPROMBuff(buffer, 0, 6, 0);

  if (buffer[0] == 'S' && buffer[1] == 'U' && buffer[2] == 'D' && 
      buffer[3] == 'O' && buffer[4] == 'K' && buffer[5] == 'U') { 

     
    // Populate the solution with the first 81 chars ..
    
    lcd.readEEPROMBuff(buffer, 6, 87, 0);
  
    for (byte x=0; x<=8; x=x+1) {   
  
      for (byte y=0; y<=8; y=y+1) {   
  
         solution[x][y] = buffer[(y * 9) + x] - 80;
         
      }
      
    }
    
    
    // Populate the board with the remaining 810 chars ..
  
    for (byte z=0; z<=9; z=z+1) {   
  
      lcd.readEEPROMBuff(buffer, ((z + 1) * 87), 81, 0);
    
      for (byte x=0; x<=8; x=x+1) {   
    
        for (byte y=0; y<=8; y=y+1) {   
    
          board[x][y][z] = buffer[(y * 9) + x] - 80;
         
        }
       
      }
    
    }
    
    initBoard();
  
  }
  
}
Δ Top

Loading a Puzzle from the SD Card

The menu function of the game allows a user to load a puzzle from the SD card. These are organised on three separate tabs - easy, medium and hard - and the user can scroll through longer lists a page at a time. These entries are organised into three separate files on the SD card - sudoku_Easy.txt, sudoku_Medium.txt and sudoku_Hard.txt - with each line in the file representing a different puzzle. These lines are further broken into two sections - the first section of 20 characters is used to describe the puzzle and the raminding 81 characters to detail the puzzle itself and the solution.

When selecting a puzzle, the selected index is passed to the loadGame and that entry is loaded from the relevant file on the SD card. The filename is determined from the global varaible selectedTab at lines 006 - 008.

The SD functions support forward, sequential reading only and records are retrieved and discarded until the correct entry is retrieved (line 018 - 020). Once the correct record is found, the solution array is populated in much the same way as that shown in the restoreGame function above. Finally, any previous user activity is cleared (line 040 -052) and the game is ready to play!

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
void loadGame(int fileIndex) {

  unsigned int bytesRead=0, i=0;       
  char* filename = "";         

  if (selectedTab == TAB_EASY)     { filename = "sudoku_Easy.txt     "; }
  if (selectedTab == TAB_MEDIUM)   { filename = "sudoku_Medium.txt   "; }
  if (selectedTab == TAB_HARD)     { filename = "sudoku_Hard.txt     "; }


  res = lcd.SDFopenFile(filename, READONLY, WORKSPACE0); 
 
  do {
 
    memset(buffer, 0, sizeof(buffer));
    res = lcd.SDFreadFile(buffer, 20, &bytesRead, WORKSPACE0);

    if (bytesRead == RECORD_DESC_LENGTH) {

      res = lcd.SDFreadFile(buffer, 81, &bytesRead, WORKSPACE0); 
 
      if (fileIndex == (selectedTab_Page * TAB_ENTRIES_PER_PAGE) +  i) {

         
        // Populate the solution with the first 81 chars ..
        
        for (byte x=0; x<=8; x=x+1) {   
      
          for (byte y=0; y<=8; y=y+1) {   
      
             solution[x][y] = buffer[(y * 9) + x] - 80;
             
          }
          
        }
        
        
        // Clear the board of user mark up ..
      
        for (byte z=0; z<=9; z=z+1) {   
        
          for (byte x=0; x<=8; x=x+1) {   
        
            for (byte y=0; y<=8; y=y+1) {   
        
              board[x][y][z] = 0;
             
            }
           
          }
        
        }
     
        initBoard();
        break;
     
      }
      
      res = lcd.SDFreadFile(buffer, 2, &bytesRead, WORKSPACE0);  
      i++;
 
    }
 
  }
  while (bytesRead == 2);
 
  lcd.SDFcloseFile(WORKSPACE0);   
  
}
Δ Top

Creating Puzzles

As detailed earlier, the SD card contains three separate text files - sudoku_Easy.txt, sudoku_Medium.txt and sudoku_Hard.txt - which contain multiple puzzles. Each puzzle is described on a single line with the first 20 characters representing the puzzle description followed by 81 characters that describes the solution. The solution values of -9 to +9 are converted into 'friendly' ASCII codes by adding 80 to them resulting in values that fall in the lower case alphabet range.

001
002
Puzzle number 1     sdfsdfsdfsdfsdfdsfdsdsfsfsfsdsddsfds ...
Puzzle number 2     sdfsdfsdfsdfsdfdsfdsdsfsfsfsdsddsfds ...

To assist in the generation of puzzles, I have built a simple Excel spreadsheet which allows the developer to enter a complete Sudoku puzzle into a 9 x 9 grid using positive numbers for values to be displayed to the user and negative numbers for those that are hidden. Rows and columns are totalled so that the validity of the puzzle can be verified.


Each row of the puzzle is converted into a sequence of nine ASCII characters using the 'trick' of adding 80 to convert the possible vales of -9 to +9 into 'friendly' ASCII characters. The user must also enter a description for the puzzle. This description and the converted row data is concatenated together to produce a row of data that can be pasted into the appropriate text file on the SD card.

Δ Top

Wiring Diagram

The diagram below show the wiring diagram when using a standard Arduino Uno (or compatible) and the SmartGPU2. Click on the image to see a detailed drawing.


Δ Top

Δ Top

Downloads

Feel free to download, modify and adapt this code to your needs. Please credit me if you use some or all of this code in your own project. If you generate more puzzles, please submit them back to me and I will add them to the download.

   Arduino Source Code
   Sudoku Generator Spreadsheet
Δ Top

Comments?

 Anything to say?
Name :
Email :
Comments :
Confirmation: