Arduino Application Development Procedure Part-4

In the previous part: Arduino Application Development Procedure Part-3 I briefly discussed testing the inputs and outputs to avoid troubleshooting issues. In this step, I will provide code and commentary on issues I had making sure the appropriate sequence happened between players. The concept works like this:

  1. Step one: no lights are on. Wait for the first player to press a button that corresponds to the position in the LED matrix.
  2. Step two: player one makes a move and one light (red) is on. Wait for the second player to press a button that corresponds to a different position in the LED matrix.
  3. Step three: player two makes a move and one light (blue) is on. Repeat steps one and two until one person wins or both players draw.
I will be providing sections of the code in this post to discuss any issues I had and make it a little easier to read. In the end, I will provide a zipped folder of the entire code for convenience.

Section One: Setup and Global Variables

/*
   This stage of the design I figure out the basic methodology to switch between players and change color per turn, set no winning actions, just a phase to
   figure out sequencing of player control. This stage took a lot of troubleshooting and method testing. In post I'll explain I had issues with certain areas, but wont
   include code because I forgot the old code that broke everything.
*/
//define red output pins and name them with short hand for Row number and Column number
#define RR3C3 53
#define RR3C2 52
#define RR3C1 51
#define RR2C3 50
#define RR2C2 49
#define RR2C1 48
#define RR1C3 47
#define RR1C2 46
#define RR1C1 45
//define blue output pins and name them with short hand for Row number and Column number
#define BR1C1 22
#define BR1C2 23
#define BR1C3 24
#define BR2C1 25
#define BR2C2 26
#define BR2C3 27
#define BR3C1 28
#define BR3C2 29
#define BR3C3 30
//define input pins and name them with shorthand for Row number and Column number
#define R3C1IN 34
#define R3C2IN 3
#define R3C3IN 4
#define R2C1IN 35
#define R2C2IN 6
#define R2C3IN 7
#define R1C1IN 8
#define R1C2IN 9
#define R1C3IN 10
//initialize two 3 by 3 matrices to keep track of the board, easy for logic; make the matrix empty to start with since nobody has made a move yet
const int rows = 3;
const int columns = 3;
boolean ttt1[ rows ][ columns ] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; //matrix for 3x3 tracking for player 1
boolean ttt2[ rows ][ columns ] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; //matrix for 3x3 tracking for player 2
boolean playerSw = 0; //variable for tracking which player is taking a turn
int lightCount = 0; //variable for counting how many lights are currently on
boolean flagTurn1P1 = 0; //variable for turn1 player1, will help with logic so I don't override past turns
boolean flagTurn2P2 = 0; //you can only ever have 9 turns max in a game of tic-tac-toe, need to keep track of who took a turn and where it took place; aslo specific turns exist for specific players in paper tic-tac-toe, so I named them appropriately
boolean flagTurn3P1 = 0;
boolean flagTurn4P2 = 0;
boolean flagTurn5P1 = 0;
boolean flagTurn6P2 = 0;
boolean flagTurn7P1 = 0;
boolean flagTurn8P2 = 0;
boolean flagTurn9P1 = 0;
void setup() {
  //set all the appropriate OUTPUT pins for the LED matrix, used R for Red and B for blue, then referred to short hand to indicate position in matrix for ease of use
  pinMode(RR1C1, OUTPUT);
  pinMode(RR1C2, OUTPUT);
  pinMode(RR1C3, OUTPUT);
  pinMode(RR2C1, OUTPUT);
  pinMode(RR2C2, OUTPUT);
  pinMode(RR2C3, OUTPUT);
  pinMode(RR3C1, OUTPUT);
  pinMode(RR3C2, OUTPUT);
  pinMode(RR3C3, OUTPUT);
  pinMode(BR1C1, OUTPUT);
  pinMode(BR1C2, OUTPUT);
  pinMode(BR1C3, OUTPUT);
  pinMode(BR2C1, OUTPUT);
  pinMode(BR2C2, OUTPUT);
  pinMode(BR2C3, OUTPUT);
  pinMode(BR3C1, OUTPUT);
  pinMode(BR3C2, OUTPUT);
  pinMode(BR3C3, OUTPUT);
  //set all the inputs for appropriate matrix positions, only need 9 of them because I don't need separate buttons for different players
  pinMode(R3C1IN, INPUT);
  pinMode(R3C2IN, INPUT);
  pinMode(R3C3IN, INPUT);
  pinMode(R2C1IN, INPUT);
  pinMode(R2C2IN, INPUT);
  pinMode(R2C3IN, INPUT);
  pinMode(R1C1IN, INPUT);
  pinMode(R1C2IN, INPUT);
  pinMode(R1C3IN, INPUT);
  //use serial for potential debugging issues
  Serial.begin(9600);
}

I have a few things to say about the setup, none of the code caused problems, yet deciding what the final global variables would be was a process. Originally, I only had one matrix (two dimensional array) to keep track of the entire tic-tac-toe board. I found out that this wasn’t a good method to keep track of specific player moves. Having a second unique matrix for player two makes it way easier to code. Originally, I hadn’t thought of keeping track of how many lights were already on (lightCount wasn’t my original plan). It turns out knowing how many lights are already on helps sequencing quite a bit. It will also allow me to program when to check if a player has won at the right time to reduce code. The flags for each turn also end up being part of my programming process. When programs become complex and long, having redundant systems or code makes sure “bugs” don’t occur. Figuring out global variables takes experimentation by repetition.

Section Two: Main Loop

void loop() {
  //blank slate game state (beginning of game check); this checks if player 1's array is empty (other player's is empty anyway, don't need to check) and constantly reads each input
  //as soon as one of the buttons is pressed it will still read the pin, but now one of the values will be 1, therefor it will not allow the if statement to continue which is desired behavior
  if (ttt1[0][0] == 0 && ttt1[0][1] == 0 && ttt1[0][2] == 0 && ttt1[1][0] == 0 && ttt1[1][1] == 0 && ttt1[1][2] == 0 && ttt1[2][0] ==  0 && ttt1[2][1] == 0 && ttt1[2][2] == 0 && lightCount == 0 && playerSw == 0) {
    updateMatrix(); //update the appropriate 3x3 matrix by reading the input for buttons for all positions, see updateMatrix function below
  } else { //as soon as one of the values changes the game has begun
    if (lightCount == 0) { //run the update light FCN once because player one will have made a move, check if no lights are lit yet to run the first function, see updateFirstMove below
      updateFirstMove();
    } else if (lightCount == 1 && flagTurn2P2 != 1 && playerSw == 1) { //check if the light count is 1, make sure the flag for Player 2 for this turn is not set yet and make sure the right player is playing
      buttonPressAndMatrixUp(playerSw, lightCount); //check if buttons are pressed, update appropriate matrices, uses current player and lightCount as arguments, see function below
    } else if (lightCount == 2 && flagTurn3P1 != 1 && playerSw == 0) { //each turn has a pattern, check current light count, make sure appropriate turn flag is not set yet, and make sure right player is playing
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 3 && flagTurn4P2 != 1 && playerSw == 1) {
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 4 && flagTurn5P1 != 1 && playerSw == 0){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 5 && flagTurn6P2 != 1 && playerSw == 1){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 6 && flagTurn7P1 != 1 && playerSw == 0){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 7 && flagTurn8P2 != 1 && playerSw == 1){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 8 && flagTurn9P1 != 1 && playerSw == 0){
      buttonPressAndMatrixUp(playerSw, lightCount);
      Serial.println("TTT1:"); //print the matrix after 8 lights are lit, once the last button is pressed the output to the monitor will stop, the last TTT1: and TTT2: should reflect the same pattern seen on the lights with 1's and 0's
      printMat(ttt1);
      Serial.println("TTT2:");
      printMat(ttt2);
    }
  }
}

The main loop is relatively simple because I spent more time developing the functions used. I like to make the main loop as simple as possible. The first turn is unique because no lights are on, so I made two functions unique to the first turn. If player one’s matrix is empty (every position still equals zero), set the matrix position equivalent to the location equal to reading the digital input that corresponds to that position. R1C1IN corresponds with ttt1[0][0], R1C2IN corresponds with ttt1[0][1], R1C3IN corresponds with ttt1[0][2], R2C1IN corresponds with ttt1[1][0], and so on. I advise reading the comments in the code to understand the sequence, sections below will cover the different functions I wrote. The most common way I end up developing the main code is as follows: start writing code in the main loop and test it (no functions because writing functions from scratch is difficult at times), find any errors in the logic through troubleshooting the result on the output, try a few iterations of the code you want by changing things around if failing, re-write the code with different logic if it isn’t working anyway you rewrite it, and finally turn repeated code into functions by using the same general logic.

Section Three: Turn One Functions

//generic function to call for updating any lights after a move has been made, loops through the matrix and checks which player had played with playerSw so the appropriate color is showing, fine for turn 0 since no lights are on anyway
void updateFirstMove() {
  for (int i = 0; i < rows; i++) { //loop through rows
    for (int j = 0; j < columns; j++) { //loop through columns
      if (i == 0 && j == 0 && ttt1[i][j] == 1) { //check each position in the matrix individually by checking index values and if any of the matrix positions has been updated to 1
        digitalWrite(RR1C1, HIGH); //set appropriate light to be on (Red for player 1)
        flagTurn1P1 = 1; //update the flag for Turn1
      } else if (i == 0 && j == 1 && ttt1[i][j] == 1) { //same process happens for each index position
        digitalWrite(RR1C2, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 0 && j == 2 && ttt1[i][j] == 1) {
        digitalWrite(RR1C3, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 1 && j == 0 && ttt1[i][j] == 1) {
        digitalWrite(RR2C1, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 1 && j == 1 && ttt1[i][j] == 1) {
        digitalWrite(RR2C2, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 1 && j == 2 && ttt1[i][j] == 1) {
        digitalWrite(RR2C3, HIGH);
      } else if (i == 2 && j == 0 && ttt1[i][j] == 1) {
        digitalWrite(RR3C1, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 2 && j == 1 && ttt1[i][j] == 1) {
        digitalWrite(RR3C2, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 2 && j == 2 && ttt1[i][j] == 1) {
        digitalWrite(RR3C3, HIGH);
        flagTurn1P1 = 1;
      }
    }
    if (lightCount == 0) { //check current lightCount, this will only allow lightCount to increment once (important for sequencing)
      lightCount++;
    }
    playerSw = 1; //switch player 
  }
}
//update the matrix with no conditions, good for turn 0, this just literally sets any player 1 matrix position to reading the appropriate input
void updateMatrix() {
  ttt1[0][0] = digitalRead(R1C1IN);
  ttt1[0][1] = digitalRead(R1C2IN);
  ttt1[0][2] = digitalRead(R1C3IN);
  ttt1[1][0] = digitalRead(R2C1IN);
  ttt1[1][1] = digitalRead(R2C2IN);
  ttt1[1][2] = digitalRead(R2C3IN);
  ttt1[2][0] = digitalRead(R3C1IN);
  ttt1[2][1] = digitalRead(R3C2IN);
  ttt1[2][2] = digitalRead(R3C3IN);
}

This section was easy to work with and was the first thing I wrote to get an idea of how I wanted to program the sequence for the first turn.

Section Four: Turns Two Through Nine Sequence

//use following code to check if a button is pressed and update the appropriate matrix beyond the first turn, accepts the player and light count
void buttonPressAndMatrixUp(boolean player, int lc) {
  int reps = 0; //set a repetition variable for the function, important for sequencing
  for (int i2 = 0; i2 < rows; i2++) { //loop through rows
    for (int j2 = 0; j2 < columns; j2++) { //loop through columns
      if (player == 0) { //first check which player is making a move
        if (ttt1[i2][j2] == 1 || ttt2[i2][j2] == 1) { //check if any position in either matrix is already equal to 1, do nothing if this occurs (prevents cheating and good for sequencing)

        } else if (ttt1[i2][j2] == 0 && ttt2[i2][j2] == 0) { //check if both positions are 0 in both matrixes (just as a redundancy check)
          if (i2 == 0 && j2 == 0) { //again we need to check each index of the appropriate matrix (ttt1 belongs to player 1, ttt2 belongs to player 2
            reps = 0; //reset the reps to 0 at the beginning of this step, makes sure it can run again the next time the loop runs
            turnLightUp(i2, j2, R1C1IN, lc, RR1C1, player, reps); //update the appropriate light based on index values, correct input pin, correct light count, which light is being turned on, which player, and what the current repition value is; see fcn below
          } else if (i2 == 0 && j2 == 1) { //the same process is repeated for all the other index values for the matrix ttt1; appropriate inputs, light count, output lights, players, and reps are referenced
            reps = 0;
            turnLightUp(i2, j2, R1C2IN, lc, RR1C2, player, reps);
          } else if (i2 == 0 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R1C3IN, lc, RR1C3, player, reps);
          } else if (i2 == 1 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R2C1IN, lc, RR2C1, player, reps);
          } else if (i2 == 1 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R2C2IN, lc, RR2C2, player, reps);
          } else if (i2 == 1 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R2C3IN, lc, RR2C3, player, reps);
          } else if (i2 == 2 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R3C1IN, lc, RR3C1, player, reps);
          } else if (i2 == 2 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R3C2IN, lc, RR3C2, player, reps);
          } else if (i2 == 2 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R3C3IN, lc, RR3C3, player, reps);
          }
        }
      } else if (player == 1) { //do the same process for player 1, just make sure the right matrix is being updated and checked, light count being checked, correct color and R and column being referenced; altered same code as above
        if (ttt1[i2][j2] == 1 || ttt2[i2][j2] == 1) {

        } else if (ttt2[i2][j2] == 0 && ttt1[i2][j2] == 0) {
          if (i2 == 0 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R1C1IN, lc, BR1C1, player, reps);
          } else if (i2 == 0 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R1C2IN, lc, BR1C2, player, reps);
          } else if (i2 == 0 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R1C3IN, lc, BR1C3, player, reps);
          } else if (i2 == 1 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R2C1IN, lc, BR2C1, player, reps);
          } else if (i2 == 1 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R2C2IN, lc, BR2C2, player, reps);
          } else if (i2 == 1 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R2C3IN, lc, BR2C3, player, reps);
          } else if (i2 == 2 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R3C1IN, lc, BR3C1, player, reps);
          } else if (i2 == 2 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R3C2IN, lc, BR3C2, player, reps);
          } else if (i2 == 2 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R3C3IN, lc, BR3C3, player, reps);
          }
        }
      }
    }
  }
}
//following function accepts the row and column being checked, which input is being read, what the light count is, output pin being modified, player, and repetition amount to update the appropriate matrix, turn the right light on, and set flags for next player move
void turnLightUp(int row, int col, int inPin, int lc, int outPin, int player, int reps) {
  while (reps < 1) { //set a repetition loop so all the code below is repeated once each time (more than once causes errors sometimes where a light is lit both blue and red)
    if (player == 0) { //check player again for redundancy
      ttt1[row][col] = digitalRead(inPin); //update the ttt1 matrix with digitalread of the appropriate pin (remember row, col, and inPin are local to this function, so whatever value is passed in will be referenced)
      if (ttt1[row][col] == 1 && lc == 2) { //if the value is updated to one after previous step and the appropriate light count is equal to specific value based on turn, do the following 
        digitalWrite(outPin, HIGH); //update the referenced outPin to HIGH
        if (lightCount == 2) { //update the light count once, important for sequencing
          lightCount++;
        }
        flagTurn3P1 = 1; //set the appropriate flag to 1
        playerSw = 1; //switch player
      } else if (ttt1[row][col] == 1 && lc == 4) { //we always check if the value updated is 1, but the light count changes per turn and certain light counts only happen with certain players, this repeated action is done for unique turns with the same code below
        digitalWrite(outPin, HIGH);
        if (lightCount == 4) {
          lightCount++;
        }
        flagTurn5P1 = 1;
        playerSw = 1;
      } else if (ttt1[row][col] == 1 && lc == 6) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 6) {
          lightCount++;
        }
        flagTurn7P1 = 1;
        playerSw = 1;
      } else if (ttt1[row][col] == 1 && lc == 8) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 8) {
          lightCount++;
        }
        flagTurn9P1 = 1;
        playerSw = 1;
      }
    } else if (player == 1) { //have to do the same process for the other player of course, just refer to different light counts
      ttt2[row][col] = digitalRead(inPin);
      if (ttt2[row][col] == 1 && lc == 1) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 1) {
          lightCount++;
        }
        flagTurn2P2 = 1;
        playerSw = 0;
      } else if (ttt2[row][col] == 1 && lc == 3) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 3) {
          lightCount++;
        }
        flagTurn4P2 = 1;
        playerSw = 0;
      } else if (ttt2[row][col] == 1 && lc == 5) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 5) {
          lightCount++;
        }
        flagTurn6P2 = 1;
        playerSw = 0;
      } else if (ttt2[row][col] == 1 && lc == 7) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 7) {
          lightCount++;
        }
        flagTurn8P2 = 1;
        playerSw = 0;
      }
    } //finally after the while loop finishes update reps by incrementing by one, this will allow it to reach the next while loop in the next call of the function above, it's also important that incrementing does not matter which player is playing, so it is outside of everything above
    reps++;
  }
}
//print the matrix input into the function, I just used this to make sure the values match what the lights show for all 9 lights being on, won't be in final product
void printMat(boolean a[rows][columns]){
  for (int i = 0; i < rows; i++){
    for (int j = 0; j < columns; j++){
      Serial.print(a[i][j]);
      Serial.print("\n");
    }
  }
}

Figuring out how to write the sequence after turn one was a very difficult process and had the most amount of debugging/troubleshooting. There was a time in writing the “buttonPressAndMatrixUp” function where the for() loop for searching through either player matrix was breaking for no apparent reason. It would not continue adding one to either i1 or j1 at some point (I could not pin down what was going on). If there is code that breaks what you think should happen to a point you don’t know what is causing the problem, I don’t recommend making small edits until it works. I tried this and almost gave up. I tried a different approach to the logic which worked. After I tested the final version of buttonPressAndMatrixUp, I made it shorter to reduce the total lines by writing a second function called turnLightUp() that is used when each element is checked in the appropriate matrix.

The whole code doesn’t have any “winning” conditions at this point. I needed to first figure out how to code the behavior of taking turns first. Tackling small parts of a longer code is feasible rather than trying to figure out the whole process at once. The next post will provide the final version where I include winning and draw conditions as well as some features I’d like to have like light sequences for winning.

Here is the zip file of the entire code, remember to have the Arduino code in a folder named the same as the program if you rename it (already is in the zip): testTTTBasic_Up.zip (4.1 KB)