SLBros
Published © CC BY-NC

Tic Tac Toe

Classic Tic Tac Toe powered by an ESP32, with fast rounds and fancy colours.

AdvancedFull instructions provided10 hours37
Tic Tac Toe

Things used in this project

Hardware components

Adafruit NeoPixel Digital RGB LED Strip 144 LED, 1m White
Adafruit NeoPixel Digital RGB LED Strip 144 LED, 1m White
×1
PTS 645 Series Switch
C&K Switches PTS 645 Series Switch
×9
spring set
you need the 3x10
×1
Female/Female Jumper Wires
Female/Female Jumper Wires
×1
PCBWay Custom PCB
PCBWay Custom PCB
we have providet the file
×3
Elegoo pcb
×1
Espressif ESP32 Development Board - Developer Edition
Espressif ESP32 Development Board - Developer Edition
×1
battery
×2
battery charging pcb TP4056
×1
push button self locking
×1
usb c port
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

top plate

Sketchfab still processing.

middle part

Sketchfab still processing.

Top plate

Sketchfab still processing.

field

If the bars don’t fit into the holes, sand them down.

Sketchfab still processing.

Guide

Schematics

curcit

Code

tic tac toe code

C/C++
You have to change a few variables in order for the code to work with you ESP32 board before you upload it. For more details check the guide.
/*
Hello :)
Thank you for downloading our Code for Tic Tac Toe and showing interest in our project!
Ill try to explain the Code the best I can and hopefully you can learn something new and create something cool aswell.
Hope its not to Convolluded or however you Spell that Word.
This code uses the Adafruit  Neo Pixel Library for the LEDs and varios ESP32 Libraries to make it work.
This code uses the esp Timer, which is only in chips similar or the same as the ESP32.
You would have to majorly change the code to make it work with other types of chips.
But if you want a challange, go for it! We believe in you ;)

Have fun!
Simon :)
*/



#include <Adafruit_NeoPixel.h>      // NeoPixel Library
#define timerDuration 40000         // How long the code waits before checking the Input./Timer Interrupt.
#define idleTimerDuration 10000000  // Time after a game before the Idle Animation Starts.
#define gridRows 3                  // The Size of the Grid in this case 3x3 so this is set to 3.
#define LEDPIN 4                    // The Pin The Data In from the NeoPixel LEDs is connected.
#define NUMLED 36                   // The Total amount of LEDs.
#define LEDPERFIELD 4               // The amount of LEDs per Field/Button.
#define REDRGB 150, 0, 0            // The RGB Codes for the Colours used.
#define BLUERGB 0, 0, 150
#define OFFRGB 0, 0, 0
#define YELLOWRGB 150, 70, 0




Adafruit_NeoPixel strip(NUMLED, LEDPIN, NEO_GRB + NEO_KHZ800);  // Sets up the Pin for the NeoPixel LEDs.

esp_timer_handle_t my_timer;  //The Esp Timers used for the Idle Animation and the Timer Interrupt for the Input.
esp_timer_handle_t idle_timer;
/*
This next Part need to be adjusted according to the board you are using.

pinY is responsible for the Columbs of the grid (Vertical)
pinX is responsible for the Rows of the grid (Horizontal)
The first pin for both starts at top left field.
*/



const byte pinY[gridRows] = { 3, 0, 1 };  //The Pins used for matrix Input pinY being Vertikal and pinX Horizontal.
const byte pinX[gridRows] = { 7, 6, 5 };  // these should be counted from     Up to Down / Left to Right starting from 0.



byte cordX;  // Most up to date Cords, which get updatet each Input.
byte cordY;
byte cordXZ;  // Cords that only get Saved at the Beginning of each Turn. These get used by the Code.
byte cordYZ;
byte inARow;       // How many are in a Row. Used for the Winner Check.
byte numTurn = 9;  // The Current Turn Which your on. Used to Check for a Draw.
byte winner = 0;   // Is the Number of the winning Player. 1 = Blue, 2 = Red.
byte fade = 0;     // Used for the fade in of the Idle Animation (Rainbow effect).

bool stateX[gridRows];  // The Coordinates in Bool for instance of stateX[0] and stateY[2] is true the top right field is pressed.
bool stateY[gridRows];

bool blink = false;   // This is used when the Winning colour Blinks Yellow (Gold).
bool change = false;  // This is set to true when the code detects an Input and changed to false when the code has run through. This is useful, so that the Code dosen't get run multiple times per Input.
bool turn = false;    // False = Red is at turn. True = Blue is at turn.
bool draw = false;    // True if the game has ended in a draw.
bool skip = false;    // This variable is used to skip the idle animation.

byte grid[2][gridRows][gridRows] = {
  { /* Layer 0 – LED-Address, because there are 4 LEDs per field the Address per field goes up 4 each Step. 
      The Code uses this Address as a base, to add the constant LEDPERFIELD to and reapeat the code as many times as there are LEDs per field.
      Change this according on how you have wired up the Neo Pixel LEDs*/
    { 0, 4, 8 },
    { 20, 16, 12 },
    { 24, 28, 32 } },
  { // Layer 1 – State. 0 = Empty, 1 = Blue, 2 = Red
    { 0, 0, 0 },
    { 0, 0, 0 },
    { 0, 0, 0 } }
};

void IRAM_ATTR idleWait(void* arg) {  //Sub-Programm to Trigger the Wait animation after Ten Seconds
  skip = false;
}

void IRAM_ATTR inputCheck(void* arg) {
  /* Programm for getting the Inputs.
  This program uses a Timer Interrupt to get triggerd, basically a timer runs parallel to the main code.
  Everytime it runs out it pauses the main code, runs this code and then returns to the main code.

  Long story short this uses the Same Princible as a matrix input in for instance a Numberpad.
  It checks if the Wires between the pins conduct, if the button is pressed they do conduct. 
  These are LOW active, so that if two pins are connected the digitalRead retuns LOW.
  It switches between the X and Y pins being the In and Outputs to get the cords of the Input. 

*/
  for (int i = 0; i < gridRows; i++) {
    pinMode(pinY[i], OUTPUT);
    pinMode(pinX[i], INPUT_PULLUP);
  }
  for (int i = 0; i < gridRows; i++) {
    if (digitalRead(pinX[i]) == LOW) {
      stateY[i] = true;
      change = true;
      cordY = i;

    } else {
      stateY[i] = false;
    }
  }
  for (int i = 0; i < gridRows; i++) {
    pinMode(pinX[i], OUTPUT);
    pinMode(pinY[i], INPUT_PULLUP);
  }
  for (int i = 0; i < gridRows; i++) {
    if (digitalRead(pinY[i]) == LOW) {
      stateX[i] = true;
      change = true;
      cordX = i;
    } else {
      stateX[i] = false;
    }
  }
}

void setup() {

  Serial.begin(115200);  // Serial connection so you can check the function of the code, before having it completly assembled. If you please you could remove all serial communicatiom, since its not required for the code to funktion
  
  // Timer-configuration
  const esp_timer_create_args_t my_timer_config = {
    // Timer for the Input detection.
    .callback = &inputCheck,            // The Sub-program that the Timer opens.
    .arg = NULL,                        // A value that the Timer gives to the Sub Program. Since its unused, its set to Null.
    .dispatch_method = ESP_TIMER_TASK,  // disides when the callback program gets triggerd, in this case when the timer ends.
    .name = "MyTimer"                   // Name of the Timer
  };

  const esp_timer_create_args_t idle_timer_config = {
    // Timer for the Idle animation.
    .callback = &idleWait,
    .arg = NULL,
    .dispatch_method = ESP_TIMER_TASK,
    .name = "IdleTimer"
  };

  // Actually creates the Timers using the configs
  esp_timer_create(&my_timer_config, &my_timer);
  esp_timer_create(&idle_timer_config, &idle_timer);
  esp_timer_start_periodic(my_timer, timerDuration);  // Starts the input timer. It gets restarted every time it runs out.
  strip.setBrightness(0);                             // Sets the LED brightness to 0.
}

void loop() {
  /*
The main Loop of the program,
Check each code for more in depth commantary but here is a basic overview:
*/
  waitAnimation();  //  plays the idle animation, which gets skipped, when there is an input.

  playTurn();  // checks for changes in the input and actually changes the states of the fields.

  showField();  // controlls the LEDs to display the colour of their state. 0 = off, 1 = blue, 2 = red.

  checkStatus();  // checks to see if there are 3 of the same color in a row to assign a winner.

  drawOrWinner();  // checks if there is a winner or if the turns have run out and plays a short animation.

  waitTillRelease();  // checks if any buttons are pressed and stalls the code till all buttons are released.



  delay(10);
}


void waitAnimation() {

  if (numTurn == 9) {  // if the current turn is not 9 it skips the whole code, else it plays the code.


    for (long firstPixelHue = 0; firstPixelHue < 5 * 65536; firstPixelHue += 256) {
      Serial.println(grid[0][1][2]);
      // This for loop is for the Rainbow animation, which is a sub programm inbuild in the Adafruit Neopixel Library
      if (skip == false) {     // if the animation is not suppose to be skipped, it gets played
        if (!(fade == 100)) {  // If its below 100 it adds one to the brightness, until it is 100. This makes for a nice little fade in effect
          fade++;
          strip.setBrightness(fade);
        }
        strip.rainbow(firstPixelHue);         // the inbuild Rainbow effect in the Adafruit Neopixel library
        strip.show();                         // Updates strip with new contents
        delay(10);                            // Pauses for a moment
        for (int i = 0; i < gridRows; i++) {  // checks if any button is pressed, if yes, then it changes the skip variable, so the code gets skipped next time around.
          if ((stateX[i] == true) || (stateY[i] == true)) {
            skip = true;
          }
        }

      } else {                      // If skip is true the loop is broken
        firstPixelHue = 5 * 65536;  // Sets the variable of the for loop to its max, so that the loop ends
      }
    }
  }
}




void playTurn() {
  if ((change == true) && !(numTurn == 0)) {  //Checks to see if there was an input.

    strip.setBrightness(255);  // changes the brightness of the LED strip, since it was changed in the rainbow animation.
    cordYZ = cordY;            // The Cordinates get saved into another variable, so that if there is a Timer interuppt during the programm it dosent screw it up.
    cordXZ = cordX;

    if (grid[1][cordYZ][cordXZ] == 0) {  // checks if the slected field is already assigned to another color, if yes it skips the code.
      if (turn == false) {               // assigns the selected field to the current player at turn and switches the turn over to the other player.
        grid[1][cordYZ][cordXZ] = 2;
        turn = true;
      } else {
        grid[1][cordYZ][cordXZ] = 1;
        turn = false;
      }
      numTurn--;
      Serial.println(numTurn);
    }
    Serial.print(cordYZ);
    Serial.print(" ");
    Serial.println(cordXZ);
    Serial.println(grid[1][cordYZ][cordXZ]);
  }
}
void showField() {
  if (!(numTurn == 9)) {
    // this code lights up the fields according to their assigned state

    for (int x = 0; x < gridRows; x++) {  // these for loops go through each fields and check their state
      for (int y = 0; y < gridRows; y++) {
        for (int i = 0; i < LEDPERFIELD; i++) {
          switch (grid[1][y][x]) {
            case 0:

              strip.setPixelColor(grid[0][y][x] + i, strip.Color(OFFRGB));


              break;
            case 1:
              if (winner == 1 && blink == true) {
                strip.setPixelColor(grid[0][y][x] + i, strip.Color(YELLOWRGB));  // in an normal case it just lights up in its colour but when the blink variable is active the winnering color turns yellow.
              } else {                                                           // since it switches between the variable being true and false the field will blink yellow (gold).
                strip.setPixelColor(grid[0][y][x] + i, strip.Color(BLUERGB));
              }
              break;
            case 2:
              if (winner == 2 && blink == true) {
                strip.setPixelColor(grid[0][y][x] + i, strip.Color(YELLOWRGB));
              } else {
                strip.setPixelColor(grid[0][y][x] + i, strip.Color(REDRGB));
              }
              break;
          }
        }
      }
    }
  }
  strip.show();
}

void checkStatus() {

  for (int i = 0; i < 2; i++) {  // Runs through each row horizontaly to check if there are 3 in a Row. It does it twice for each player.
    for (int x = 0; x < gridRows; x++) {
      inARow = 0;
      for (int y = 0; y < gridRows; y++) {
        if (grid[1][y][x] == (1 + i)) {
          inARow++;
        }
      }
      if (inARow == gridRows) {
        winner = 1 + i;
      }
    }
  }
  for (int i = 0; i < 2; i++) {  // Runs through each row verticaly to check if there are 3 in a Row. again for each player.
    for (int y = 0; y < gridRows; y++) {
      inARow = 0;
      for (int x = 0; x < gridRows; x++) {
        if (grid[1][y][x] == (1 + i)) {
          inARow++;
        }
      }
      if (inARow == gridRows) {
        winner = 1 + i;
      }
    }
  }
  // Runs through each row diagonaly in both directions to check if there are 3 in a Row for each player.
  for (int i = 0; i < 2; i++) {

    inARow = 0;

    for (int xy[2] = { 0, 0 }; xy[0] < gridRows; xy[0]++) {
      if (grid[1][xy[0]][xy[1]] == (1 + i)) {
        inARow++;
      }
      xy[1]++;
    }
    if (inARow == gridRows) {
      winner = 1 + i;
    }
  }
  for (int i = 0; i < 2; i++) {
    inARow = 0;

    for (int xy[2] = { 0, (gridRows - 1) }; xy[0] < gridRows; xy[0]++) {
      if (grid[1][xy[0]][xy[1]] == (1 + i)) {
        inARow++;
      }
      xy[1]--;
    }
    if (inARow == gridRows) {
      winner = 1 + i;
    }
  }
}

void drawOrWinner() {
  // checks if a winner has been assigned or if the turns have run out.
  if (winner >= 1) {
    if (winner == 1) {
      Serial.println("Blue Wins");
    } else {
      Serial.println("Red Wins");
    }
    animationAndReset();
  } else if (numTurn == 0) {
    Serial.println("Draw");
    draw = true;
    animationAndReset();
  }
}

void waitTillRelease() {
  while (change == true) {  // As long as there is an input it repeats this code
    change = false;
    delay((timerDuration / 1000) + 20);  // Waits to see if during a timer interupt change switches back to true.
  }
}

void animationAndReset() {
  if (draw == true) {  // If all turns run out plays the quick animation of the fields getting turned off one after another.
    delay(200);
    for (int x = 0; x < gridRows; x++) {
      for (int y = 0; y < gridRows; y++) {
        for (int i = 0; i < LEDPERFIELD; i++) {
          strip.setPixelColor(grid[0][x][y] + i, strip.Color(OFFRGB));
          strip.show();
        }
        delay(100);
      }
    }


  } else {
    for (int i = 0; i < 5; i++) {  // Repeats the showFields code while switching around the blink varialbe to make the winning colour blink.
      blink = true;
      showField();
      delay(300);
      blink = false;
      showField();
      delay(300);
    }

    for (int y = 0; y < gridRows; y++) {  // Runs through each field and turns it to the winning colour.
      for (int x = 0; x < gridRows; x++) {
        for (int i = 0; i < LEDPERFIELD; i++) {
          switch (winner) {
            case 1:

              strip.setPixelColor(grid[0][y][x] + i, strip.Color(BLUERGB));
              break;
            case 2:
              strip.setPixelColor(grid[0][y][x] + i, strip.Color(REDRGB));
              break;
          }
        }
      }
    }
    strip.show();
    delay(800);
    for (int y = 0; y < gridRows; y++) {  // Turns off each row one after another.
      for (int x = 0; x < gridRows; x++) {
        for (int i = 0; i < LEDPERFIELD; i++) {
          strip.setPixelColor(grid[0][y][x] + i, strip.Color(OFFRGB));
        }
      }
      strip.show();
      delay(300);
    }
  }

  for (int y = 0; y < gridRows; y++) {  // Resets the state if each field.
    for (int x = 0; x < gridRows; x++) {
      grid[1][y][x] = 0;
    }
  }

  draw = false;  // Resets all variables to begin a new game.
  winner = 0;
  numTurn = 9;
  turn = false;
  skip = true;
  fade = 0;
  showField();
  strip.setBrightness(0);
  esp_timer_start_once(idle_timer, idleTimerDuration);  // Starts the timer for the idle animation
}

Credits

SLBros
1 project • 1 follower
Two Germans building cool projects together

Comments