Build a Smart Digital Game Board Using Multicolour PCB

Published  January 2, 2025   0
Smart Multicolor Custom PCB for Ludo Board Game

Ludo, one of our cherished childhood board games, holds a special place in our hearts. Its simple yet engaging gameplay that provided countless hours of fun and bonding. However, as times have changed, so have our ways of enjoying these nostalgic pastimes. Today, children often experience classic games like Ludo through smartphones and gaming consoles, losing the physical gameplay and interpersonal connection of traditional board games.

What if we could bridge the gap between the old and the new by creating a digital version of the Ludo board? This article shows how to build a Digital Ludo Board, a modern take on this classic game that combines the nostalgia of the physical game with the convenience and interactivity of the digital world. Designed and built from scratch, this is not only a fun project but also an opportunity to try out the new multicolor PCB.

Talking about multicolor PCB, a huge thanks to PCBWAY for sponsoring this project. The multicolor PCB shown in this project was fabricated using the UV printing technology from PCBWAY and as you can see from the images and videos, the game board looks astonishing with vibrant colors printed on the PCB. If you are looking to build your own multicolor PCB do check out PCBWAY. That being said lets get into the project. 

 

 

Features

  • Custom Multicolor PCB: Manufactured by PCBWay, for a clean and minimalistic look.

  • Powerful ESP32-S3 SoC: The main controller, equipped with 16MB flash and 8MB PSRAM, ensures smooth gameplay and easy programming.

  • 88 Addressable RGB LEDs: Reverse-mounted SK6812MINI-E LEDs provide vibrant light effects to indicate token positions.

  • Touch-Based Input System: Users can use the onboard touch pads to play the game.

  • IPS LCD Display: A 1.28-inch round IPS LCD in the centre of the board displays game status and virtual dice.

  • Cheat proof: Random number generation used for the virtual dice ensures that the dice rolls are really random

  • Open Source: It is easy for you to modify it according to your needs.

Components Required to Build Electronic Game Board

  • Here is the list of components we would require to build our digital Ludo board. The exact value of each component can be found in the schematics or the BOM.

  • ESP32-S3-WROOM-1-N16R8 x1

  • WaveShare 1.28” Round IPS display with 240x240 pixels resolution x1.

  • SK6812MINI-E addressable RGB LEDs x88

  • IP5306 power management SoC x1

  • 74HC595 Shift registers x2

  • MIC5219-3.3 LDO x1

  • Other passive components

  • Switches and connectors

  • Custom multicoulor PCB

  • 3D printed parts.

  • Other tools and consumables.

Digital Ludo Board Schematic Diagram

The complete circuit diagram for the Digital Ludo Board is shown below. It can also be downloaded in PDF format from the GitHub repo linked at the end.

Schematics of Digital Ludo Board

Let’s discuss the Schematics section by section for easier understanding. First, we have the power section. As you can see in the image below the power section is fairly simple and only uses a very few components. The USB type C port is not only used for the power but also to program the onboard ESP32-S3 SoC. The 5V input from the USB port is then connected to the IP5306 power management chip. The IP5306 power management controller will not only charge the internal battery but also act as a power path controller and will provide stable 5V output when either powered from the USB port or when it is powered directly from the battery it can supply up to 3A of current to the load, which will be handy since we have a lot of LED to control. There are 4 LEDs there to indicate the charge state of the internal battery. The IP5306 also features a low current shutdown, in which when the load current drops below the set level(approximately 45mA) for 32 seconds continuously the chip will disable the output and go into low power mode. To turn the chip back on we can use the provided tactile button which is connected to the fifth pin of the IP5306. We have used this feature to turn the Ludo board off automatically to save power if there is no user interaction for 15 minutes. The 5V output from the PMC is then converted to 3.3V for the ESP32 using a MIC5219-3.3V ultra-low noise LDO from Microchip.

Digital Ludo Board input and Power management

In the next section, we have the ESP32-S3 SoC itself. We have chosen the 16MB variant with 8MB or PSRAM since we are dealing with a lot of graphics graphics-intensive tasks and the extra storage and RAM are necessary to accommodate all of that. We have used a 1.28” round IPS display module from Waveshare since it is a perfect fit for our design aesthetically. The display is interfaced with the ESP32 through the HSPI interface.  You can also spot two 74HC595 shift registers in the schematics. We are using them along with the 7 ADC input pins of the ESP32 to detect the touch inputs. The touch detection uses the same logic as a matrix keyboard. We will continuously enable one output of the shift registers at a time, which will be connected to a row of touch pads, and then scan each adjacent pad near them to detect whether there is a touch detected in there or not. Then we will activate the next shift register output and will continue to scan. This process will be repeated in a loop to detect the touch whenever there is one.

Digital Ludo Board SoC

Next, we have the addressable RGB LEDs and touchpads. For making the schematics drawing and parts placement easier we have created custom schematic symbols and PCB footprints for the LEDs. In the new custom symbol, pins 1-4 represent the RGB LES’s pins while pins 5 and 6 indicate the touch pads. Since the LEDs are reverse mountable type the LEDs will be populated on the backside of the board with precise cutouts in the PCB to accommodate them. The touchpad will be on the top side of the PCB. The LEDs are connected in series with the data in the pin of the first LED connected to the GPIO16 of the ESP32.

Digital Ludo Board LEDs Schematics

Ludo Game Board PCB Design

For this project, we have decided to make a custom multi-colour PCB. This will ensure that the final product is as compact as possible as well as easy to assemble and use. The PCB is designed with KiCad. All the design files are available to download from the GitHub repo linked below this article. The PCB has a dimension of each face is approximately 260mm x 260mm. 

Here are the top and bottom layers of the PCB.

Ludo Board PCB Top Layer PCB

And here is the top and bottom 3D view of the PCB.

Ludo Board PCB Bottom Layer PCB

Here is one more 3D view of the PCB with the display mounted.

Ordering Multi Colour PCB from PCBWAY

Now after finalizing the design, you can proceed with ordering the PCB:

Step 1: Get into https://www.pcbway.com/   and sign up if this is your first time. Then, click on the PCB instant Quote to go to the PCBWAY’s PCB ordering page. On that page, you can click on the Quick-order PCB to go to the Quick Order page.

PCBWAY Quick Order

On the Quick Order page upload your PCB Gerber file, so that the PCBWAY  tool will automatically populate the PCB dimensions.

PCBWAY Upload Geber File

Now make any changes you need such as PCB colour, Silkscreen, PCB thickness, and the number of PCBs etc. For the multicolour PCB select the solder mask as white and then select the appropriate option for the UV printing Multi-colour. PCBWAY does support UV multi-colour printing on both sides if needed.

PCBWAY Select UV Multi Colour

Step 2: Once all the required parameters are set PCBWAY  will show the build time, Cost and shipping costs of different shipping methods. Choose the build time and shipping methods, then click on Save to Cart.

PCBWAY Save to Cart

Step 3: In the cart, once the Gerber file passes the DFM checks, select the PCB we have just added and click on proceed. Give the shipping and billing details and then proceed with the payment. Once the payment is done the PCBWAY will start manufacturing your order. Within a week, you will receive the finished PCB.

PCBWAY Ludo Board Package

Apart from PCB prototype PCBWAY also provides PCB stencil manufacturing, PCB assembly services, CNC machining, 3D printing service, Sheet metal fabrication, Injection molding, and even Electronic design and OEM services. You can check them out if you are interested.

Custom Made PCB from PCBWAY

Assembling the Ludo board PCB

For assembling the PCBs, the first step we have done is to sort all the required components as per the BOM. Once it's done we have placed them on the PCB and soldered them one by one. If you want to make this procedure easier you can use an SMD stencil to apply the solder paste and then place the components on it prior to reflowing the PCB with a SMD rework station or a reflow oven. Here are the images of a fully assembled Ludo Board PCB.

Assembling The Ludo Board PCB

3D Printed Parts

We have designed a 3D-printed enclosure for the Ludo board so that it would be secure and easier to handle during game plays. The files for all the 3D printed parts can be downloaded from the GitHub link provided at the end of the article along with the Arduino sketch and bitmap file. Learn more about 3D printing and how to get started with it by following the link. You can download the 3D files from the project GitHub repo.

The 3D View of Ludo Board PCB

Since the enclosure is a bit bigger and wouldn’t fit within my 3D printer build volume. I have split the enclosure into two parts and then printed them. The two parts will be slid over the PCB edges and will secured with some screws. Here are the 3D renderings of the two enclosure parts.

LUDO Enclosure 3D

Here is the final assembled Ludo board with the enclosure.

Fully Assembled Ludo Board PCB

Arduino Digital Ludo Board Code Explanation

To bring the digital Ludo board to life, the firmware incorporates a range of libraries and techniques, all seamlessly integrated within the Arduino IDE. Starting with the essential hardware components, the NeoPixel LEDs, touch matrix, and TFT display form the backbone of the system, complemented by efficient use of ESP32 features for power management.

The Arduino sketch begins by including necessary libraries like PNGdec and AnimatedGIF for graphical rendering, Adafruit NeoPixel for LED control, and TFT_eSPI for display management. These libraries are crucial for handling dynamic animations, vivid graphics, and interactive LED feedback. The initialization code ensures all peripherals are ready for use. You can find the exact version of each library along with the download links in the sketch file’s header section. The Adafruit NeoPixel library we have used is a modified version and not the original version. So make sure to uninstall the original version if it is already installed and install one from the link or you can use the library from the library zip file in the project repo. Along with all the necessary libraries included we also created all the required instances and global variables for the code to work.

#include "esp_sleep.h"
/*PNG decoder library*/
#include <PNGdec.h>          // Include the PNG decoder library
PNG png;                     // PNG decoder instance
#define MAX_IMAGE_WIDTH 240  // Adjust for your images
int16_t xpos = 0;
int16_t ypos = 0;
/*PNG decoder library*/
#include <Adafruit_NeoPixel.h>  //Adafruit neopixel Library
Adafruit_NeoPixel strip(88, 16, NEO_GRB + NEO_KHZ800);
const unsigned long flashInterval = 300;  // Fixed flash interval (300ms)
#include <SPI.h>
#include <TFT_eSPI.h>  //.  TFT_eSPI Library
TFT_eSPI tft = TFT_eSPI();
#include <AnimatedGIF.h>  // AnimatedGIF Library
#include "NotoSansBold36.h"
#include "ImgData.h"
AnimatedGIF gif;
#define GIF_IMAGE0 Ludo_intro
#define GIF_IMAGE00 Confetti
#define GIF_IMAGE11 Congrats
#define GIF_IMAGE1 Dice_1
#define GIF_IMAGE2 Dice_2
#define GIF_IMAGE3 Dice_3
#define GIF_IMAGE4 Dice_4
#define GIF_IMAGE5 Dice_5
#define GIF_IMAGE6 Dice_6
#define AA_FONT_LARGE NotoSansBold36
const uint8_t *gifImages[] = { GIF_IMAGE1, GIF_IMAGE2, GIF_IMAGE3, GIF_IMAGE4, GIF_IMAGE5, GIF_IMAGE6 };
const size_t gifSizes[] = {
sizeof(GIF_IMAGE1),
sizeof(GIF_IMAGE2),
sizeof(GIF_IMAGE3),
sizeof(GIF_IMAGE4),
sizeof(GIF_IMAGE5),
sizeof(GIF_IMAGE6)
};
// Define shift register pins
#define DATA_PIN 13   // SER (Serial Data Input)
#define LATCH_PIN 14  // RCLK (Register Clock)
#define CLOCK_PIN 15  // SRCLK (Shift Register Clock)
// GPIO Management
const int adcPins[7] = { 1, 2, 3, 4, 5, 6, 7 };  // GPIO1 to GPIO7
const int SPIPins[6] = { 8, 9, 10, 11, 12, 21 };
//RGB LED Address Mapping
int ledMapping[16][6] = {
{ 9, 8, 7, 6, 5, 4 },
{ 10, 11, 12, 13, 14, 15 },
{ 21, 20, 19, 18, 17, 16 },
{ 31, 30, 29, 28, 23, 22 },
{ 32, 33, 34, 35, 36, 37 },
{ 43, 42, 41, 40, 39, 38 },
{ 53, 52, 51, 50, 45, 44 },
{ 54, 55, 56, 57, 58, 59 },
{ 65, 64, 63, 62, 61, 60 },
{ 75, 74, 73, 72, 67, 66 },
{ 76, 77, 78, 79, 80, 81 },
{ 87, 86, 85, 84, 83, 82 },
{ 0, 1, 2, 3, -1, -1 },
{ 24, 25, 26, 27, -1, -1 },
{ 46, 47, 48, 49, -1, -1 },
{ 68, 69, 70, 71, -1, -1 },
};
// Touch detection threshold (adjust based on calibration)
int TOUCH_THRESHOLD = 20;  // Example value; calibrate for your setup
// Debounce settings
#define DEBOUNCE_COUNT 3
unsigned long lastdbug = 0;
// Arrays to hold touch states and debounce counters
uint8_t touchState[16][7] = { 0 };
uint8_t debounceCounter[16][7] = { 0 };
uint8_t stableState[16][7] = { 0 };
//Global Variables for Game Logic
int currentPlayer = 0;
int mode = 0;
int TotalWins = 0;  //To store number of player with all 4 tokens in home
int rollsInTurn = 0;
int diceRoll;            // Count of rolls in the current turn
bool extraRoll = false;  // Track if player gets a bonus roll
int highestRoll = 0;
int startingPlayer = -1;
int PlayerSetupCount = 0;
String Player[] = { "Blue", "Yellow", "Green", "Red" };
int rolls[4] = { 0 };
int HighRoller[] = { 0, 0, 0, 0 };
int PlayerPlace[4] = { 0, 0, 0, 0 };   // Player position for Blue, Yellow, Green, Red
int tokensInHome[4] = { 0, 0, 0, 0 };  // Tokens in home for Blue, Yellow, Green, Red
int tokensInMove[4] = { 0, 0, 0, 0 };
int tokensInStart[4] = { 4, 4, 4, 4 };  // Tokens in start for Blue, Yellow, Green, Red
//int playerPositions[4][4] = {{5,5,3,4},{44,44,3,4},{32,32,3,4},{19,19,3,4}}; // Token positions for each player (relative to their path)
int playerPositions[4][4] = { { 1, 2, 3, 4 }, { 1, 2, 3, 4 }, { 1, 2, 3, 4 }, { 1, 2, 3, 4 } };  // Token positions for each player (relative to their path)
// Paths for each player
const int bluePath[] = { -1, 0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 22, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 66, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87 };
const int yellowPath[] = { -1, 24, 25, 26, 27, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 66, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 };
const int greenPath[] = { -1, 46, 47, 48, 49, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 66, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 22, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43 };
const int redPath[] = { -1, 68, 69, 70, 71, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 22, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65 };
const int *playerPaths[] = { bluePath, yellowPath, greenPath, redPath };
const int pathLengths[] = { sizeof(bluePath) / sizeof(int), sizeof(yellowPath) / sizeof(int), sizeof(greenPath) / sizeof(int), sizeof(redPath) / sizeof(int) };
// Player colors
#define COLOR_BLUE strip.Color(0, 0, 255)
#define COLOR_YELLOW strip.Color(255, 255, 0)
#define COLOR_GREEN strip.Color(0, 255, 0)
#define COLOR_RED strip.Color(255, 0, 0)
const uint32_t playerColors[] = { COLOR_BLUE, COLOR_YELLOW, COLOR_GREEN, COLOR_RED };
// Safe squares
const int safeSquares[] = { 5, 13, 23, 35, 45, 57, 67, 79 };
#define SAFE_SQUARES_COUNT (sizeof(safeSquares) / sizeof(int))
struct LEDState {
int playerColors[4];       // Colors of overlapping players
int count;                 // Number of overlapping players
unsigned long lastUpdate;  // Timestamp of the last update
int currentIndex;          // Current color index for flashing
bool state;                // ON/OFF state for flashing
};
LEDState ledStates[150];  // Array to track LED states (adjust size as needed)
// Global variable to track inactivity
unsigned long lastTouchTime = 0;                  // Tracks the last touch input time
const unsigned long INACTIVITY_TIMEOUT = 900000;  // 15 minutes in milliseconds


The goToSleep function and handleInactivity functions are responsible for power saving. They continuously monitor when was last user interaction, and if there is no user interaction for 15 minutes the ESP32 will turn off all the peripherals and then will go to a deep sleep state.  
// Function to handle deep sleep
void goToSleep() {
// Turn off all LEDs
strip.clear();
strip.show();
// Turn off the display
tft.writecommand(TFT_DISPOFF);  // Turn off display
tft.writecommand(TFT_SLPIN);    // Enter sleep mode for display
pinMode(21, OUTPUT);
digitalWrite(21, LOW);  //turn of lcd backlight
// Configure wake-up source (e.g., touch input, GPIO, timer, etc.)
esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);  // Replace GPIO_NUM_33 with your desired GPIO
//esp_sleep_enable_timer_wakeup(60000000);     // Optional: Wake after 1 minute if needed
Serial.println("Going to sleep......!");
// Enter deep sleep
esp_deep_sleep_start();
}
// Function to handle inactivity
void handleInactivity() {
unsigned long currentMillis = millis();
// Check if 5 minutes of inactivity have passed
if (currentMillis - lastTouchTime > INACTIVITY_TIMEOUT) {
  goToSleep();
}
}
The  setupShiftRegisters, setupADC, scanMatrix, activateRow, shiftOutData, readColumns, debounceKeys, handleKeyEvent and calibrateTouchThreshold functions are part of our touch detection. The setupShiftRegisters and setupADC functions are called in the setup functions to initialise the pins to their respective roles. All of the other functions are used to control the shift registers and detect the touch inputs. The scanMatrix function scans the 16x7 matrix, using shift registers to activate rows and ADC readings to detect touch inputs. To prevent false triggers, debouncing is implemented with counters. If a touch state remains consistent for a predefined count, it is registered as stable.

// Function to initialize shift register pins
void setupShiftRegisters() {
pinMode(DATA_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
// Initialize shift registers to all zeros
shiftOutData(0);
}
// Function to initialize ADC input pins
void setupADC() {
for (int i = 0; i < 7; i++) {
  pinMode(adcPins[i], OUTPUT);
}
}
// Function to scan the entire matrix
void scanMatrix() {
for (int row = 0; row < 16; row++) {
  activateRow(row);
  delayMicroseconds(50);  // Small delay to allow signals to stabilize
  readColumns(row);
}
}
// Function to activate a specific row using shift registers
void activateRow(int row) {
uint16_t data = 1 << row;  // Create a bitmask to activate the current row
shiftOutData(data);
}
// Function to shift out data to the shift registers
void shiftOutData(uint16_t data) {
digitalWrite(LATCH_PIN, LOW);
for (int i = 15; i >= 0; i--) {
  digitalWrite(CLOCK_PIN, LOW);
  digitalWrite(DATA_PIN, (data & (1 << i)) ? HIGH : LOW);
  digitalWrite(CLOCK_PIN, HIGH);
}
digitalWrite(LATCH_PIN, HIGH);
}
// Function to read the columns (ADC inputs) for the active row
void readColumns(int row) {
for (int col = 0; col < 7; col++) {
  pinMode(adcPins[col], INPUT);
  int adcValue = analogRead(adcPins[col]);
  pinMode(adcPins[col], OUTPUT);
  digitalWrite(adcPins[col], 0);
  touchState[row][col] = (adcValue > TOUCH_THRESHOLD) ? 1 : 0;
}
}
// Function to handle debounce logic
void debounceKeys() {
for (int row = 0; row < 16; row++) {
  for (int col = 0; col < 7; col++) {
    if (touchState[row][col] == stableState[row][col]) {
      debounceCounter[row][col] = 0;  // Reset counter if state is stable
    } else {
      debounceCounter[row][col]++;
      if (debounceCounter[row][col] >= DEBOUNCE_COUNT) {
        stableState[row][col] = touchState[row][col];
        debounceCounter[row][col] = 0;
        handleKeyEvent(row, col, stableState[row][col]);
      }
    }
  }
}
}
// Function to handle key events (presses and releases)
void handleKeyEvent(int row, int col, uint8_t state) {
if (state == 1) {
  return;
} else {
  // Key released
  lastTouchTime = millis();  // Reset the inactivity timer
  if (row < 12) {
    Serial.print("Touch at  : ");
    Serial.println(ledMapping[row][col]);
    handleTokenMove(ledMapping[row][col]);
  } else {
    Serial.print("Player  : ");
    Serial.println(row - 12);
    handleTokenMove(100 + (row - 12));
  }
}
}
// Optional: Function to calibrate touch threshold
void calibrateTouchThreshold() {
int maxValue = 0;
for (int col = 0; col < 7; col++) {
  int adcValue = analogRead(adcPins[col]);
  if (adcValue > maxValue) {
    maxValue = adcValue;
  }
}
// Set TOUCH_THRESHOLD slightly above the maximum observed value
TOUCH_THRESHOLD = maxValue + 100;  // Adjust the offset as needed
}
The functions pngDraw, DisplayPNG, playGIF, drawLudoHomeArea, and displayHighRollers are our graphics-related functions, designed for rendering and managing graphical elements on a TFT display. The pngDraw function decodes a PNG file line by line, converting it into RGB565 format, and uses a buffer to push the decoded line to the display at the correct coordinates. The DisplayPNG function takes a PNG file stored in flash memory, initializes its decoding, and renders it on the screen with the help of pngDraw function. Similarly, the playGIF function handles GIF animations, decoding and displaying each frame sequentially for smooth playback. The drawLudoHomeArea function visually represents the home area of the board, dividing the screen into quadrants coloured for each player, with tokens or placeholders arranged to reflect the current game state. Lastly, the displayHighRollers function updates the display with the highest dice rolls of players during selecting the first player.

void pngDraw(PNGDRAW *pDraw) {
uint16_t lineBuffer[MAX_IMAGE_WIDTH];
png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, lineBuffer);
}
void DisplayPNG(const uint8_t *pngArray, size_t arraySize) {
int16_t rc = png.openFLASH((uint8_t *)pngArray, arraySize, pngDraw);
if (rc == PNG_SUCCESS) {
  Serial.println("Successfully opened PNG file");
  Serial.printf("Image specs: (%d x %d), %d bpp, pixel type: %d\n",
                png.getWidth(), png.getHeight(), png.getBpp(), png.getPixelType());
  tft.startWrite();
  uint32_t dt = millis();
  rc = png.decode(NULL, 0);
  Serial.print(millis() - dt);
  Serial.println("ms");
  tft.endWrite();
  // png.close(); // Not needed for memory-to-memory decode
} else {
  Serial.println("Failed to open PNG file");
}
}
void playGIF(const uint8_t *gifImage, size_t gifSize) {
//size_t gifSize = sizeof(gifImage); // Automatically calculate size
if (gif.open((uint8_t *)gifImage, gifSize, GIFDraw)) {
  Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
  tft.startWrite();
  while (gif.playFrame(true, NULL)) {
    yield();
  }
  gif.close();
  tft.endWrite();
} else {
  Serial.println("Failed to open GIF!");
}
}
void drawLudoHomeArea(int tokensBlue, int tokensYellow, int tokensGreen, int tokensRed, int centerColorFlag) {
// Colors for tokens and placeholders
uint16_t blueColor = 0x0294;
uint16_t yellowColor = 0xd5a0;
uint16_t greenColor = 0x4420;
uint16_t redColor = 0xb021;
uint16_t centerCircleColor;
// Set center circle color based on the flag
switch (centerColorFlag) {
  case 0:
    centerCircleColor = blueColor;
    break;  // Light Blue
  case 1:
    centerCircleColor = yellowColor;
    break;
  case 2:
    centerCircleColor = greenColor;
    break;
  case 3:
    centerCircleColor = redColor;
    break;
  default:
    centerCircleColor = blueColor;
    break;  // Default to Light Blue
}
// Screen dimensions (assuming circular display of 240x240 pixels)
int screenWidth = 240;
int screenHeight = 240;
int centerX = screenWidth / 2;
int centerY = screenHeight / 2;
int radius = screenWidth / 2;
// Clear screen
tft.fillScreen(TFT_BLACK);
// Draw quadrant backgrounds
tft.fillTriangle(centerX, centerY, 0, 0, screenWidth, 0, yellowColor);                      // Top-right (Yellow)
tft.fillTriangle(centerX, centerY, screenWidth, 0, screenWidth, screenHeight, greenColor);  // Bottom-right (Green)
tft.fillTriangle(centerX, centerY, screenWidth, screenHeight, 0, screenHeight, redColor);   // Bottom-left (Red)
tft.fillTriangle(centerX, centerY, 0, screenHeight, 0, 0, blueColor);                       // Top-left (Blue)
// Draw center circle
int centerRadius = 49;                                          // Circle radius is 50 pixels diameter
tft.fillCircle(centerX, centerY, centerRadius + 2, TFT_BLACK);  // Outer border
tft.fillCircle(centerX, centerY, centerRadius, centerCircleColor);
// Function to draw tokens and placeholders at left, right, top, and bottom centers
auto drawTokens = [&](int cx, int cy, uint16_t color, int tokenCount) {
  int tokenRadius = 10;
  int spacing = 20;   // Spacing between tokens
  int maxTokens = 4;  // Max tokens per quadrant
  // Calculate positions for tokens (left, right, top, bottom centers of the quadrant)
  int positions[4][2] = {
    { cx, cy + spacing },  // Left
    { cx, cy - spacing },  // Right
    { cx - spacing, cy },  // Top
    { cx + spacing, cy }   // Bottom
  };
  for (int i = 0; i < maxTokens; i++) {
    int x = positions[i][0];
    int y = positions[i][1];
    if (i < tokenCount) {
      tft.fillCircle(x, y, tokenRadius + 2, TFT_BLACK);  // Outer outline
      tft.fillCircle(x, y, tokenRadius, color);          // Filled token
      tft.fillCircle(x, y, tokenRadius - 4, TFT_BLACK);  // Inner outline
    } else {
      tft.fillCircle(x, y, tokenRadius + 2, TFT_BLACK);  // Outer outline
      tft.fillCircle(x, y, tokenRadius, TFT_WHITE);      // Placeholder
    }
  }
};
// Draw tokens for each quadrant
drawTokens(34, 120, blueColor, tokensBlue);      // Top-left (Blue)
drawTokens(120, 34, yellowColor, tokensYellow);  // Top-right (Yellow)
drawTokens(206, 120, greenColor, tokensGreen);   // Bottom-right (Green)
drawTokens(120, 206, redColor, tokensRed);       // Bottom-left (Red)
}
void displayHighRollers(int left, int top, int right, int bottom, int offset) {
if (left > 0) {
  tft.drawNumber(left, offset, 120);
}
if (top > 0) {
  tft.drawNumber(top, 120, offset);
}
if (right > 0) {
  tft.drawNumber(right, 240 - offset, 120);
}
if (bottom > 0) {
  tft.drawNumber(bottom, 120, 240 - offset);
}
}
The updateLEDs function is used to manage the addressable RGB LEDs. this function will handle all of the RGB LEDs and illuminate the required ones to indicate the token positions. If there are multiple tokens in the same square, this function will indicate that by blinking the LEDs with the respective colours and numbers.

void updateLEDs() {
const int numPlayers = 4;               // Total players
const int numTokens = 4;                // Tokens per player
const int numLEDs = strip.numPixels();  // Total LEDs
// Reset the LED states
for (int i = 0; i < numLEDs; i++) {
  ledStates[i].count = 0;
}
// Populate LED states
for (int player = 0; player < numPlayers; player++) {
  for (int token = 0; token < numTokens; token++) {
    int pos = playerPositions[player][token];
    if (pos > 0 && pos < pathLengths[player]) {
      int ledIndex = playerPaths[player][pos];
      if (ledStates[ledIndex].count < numPlayers) {
        ledStates[ledIndex].playerColors[ledStates[ledIndex].count] = playerColors[player];
        ledStates[ledIndex].count++;
      }
    }
  }
}
// Update LEDs based on their states
unsigned long currentMillis = millis();
for (int i = 0; i < numLEDs; i++) {
  if (ledStates[i].count == 0) {
    // No tokens on this LED, turn it off
    strip.setPixelColor(i, 0);
  } else if (ledStates[i].count == 1) {
    // Single token, set to static color
    strip.setPixelColor(i, ledStates[i].playerColors[0]);
  } else {
    // Multiple tokens, handle flashing
    if (currentMillis - ledStates[i].lastUpdate >= flashInterval) {
      ledStates[i].lastUpdate = currentMillis;
      if (ledStates[i].state) {
        // Turn the LED off
        strip.setPixelColor(i, 0);
      } else {
        // Set the LED to the current color
        strip.setPixelColor(i, ledStates[i].playerColors[ledStates[i].currentIndex]);
        // Move to the next color
        ledStates[i].currentIndex = (ledStates[i].currentIndex + 1) % ledStates[i].count;
      }
      ledStates[i].state = !ledStates[i].state;
    }
  }
}
strip.show();
}
The functions setupPlayers, nextPlayer, TakeTokenOut, detectCollision, checkValidMove, and handleTokenMove are part of the core gameplay mechanics and are designed to manage the player interactions. The setupPlayers function initializes the game by displaying a start message and waiting for the first player to touch the start area, assigning the first turn based on the highest dice roll during player setup. The nextPlayer function shifts the game control to the next eligible player who still has active tokens. The TakeTokenOut function moves a player's token from the starting area to the game board, activating it for movement while ensuring collision detection via the detectCollision function, which checks for safe squares or conflicts with tokens from other players, resetting colliding tokens to their starting positions. The checkValidMove function evaluates all possible moves for the current player, determining if there are valid options, and either automates the move if only one is valid or prompts the player to choose among multiple valid tokens. Finally, the handleTokenMove function orchestrates the movement logic for the player's tokens based on touch inputs, dice rolls, and game rules, dynamically updating the display and player statuses while managing scenarios such as bonus rolls, collisions, and winning conditions. 

void setupPlayers() {
DisplayPNG((uint8_t *)StartMessage, sizeof(StartMessage));
Serial.println("Any Player touch start area!");
while (mode == 5) {
  scanMatrix();
  debounceKeys();
  updateLEDs();
  handleInactivity();
  if (millis() - lastdbug > 1000) {
    Serial.print("Mode: ");
    Serial.print(mode);
    Serial.print(" Player with High Roll: ");
    Serial.println(startingPlayer + 1);
    lastdbug = millis();
  }
}
currentPlayer = startingPlayer;
DisplayPNG((uint8_t *)FirstPlayer, sizeof(FirstPlayer));
tft.drawString(Player[currentPlayer], 120, 91);
delay(2000);
}
// Manage player turns
void nextPlayer() {
if (rollsInTurn > 0) {
  rollsInTurn = 0;
}
do {
  currentPlayer = (currentPlayer + 1) % 4;   // Move to the next player
} while (tokensInHome[currentPlayer] == 4);  // Continue until a valid player is found
mode = 0;
}
// Take token out of start area
void TakeTokenOut() {
for (int i = 0; i < 4; i++) {
  if (playerPositions[currentPlayer][i] < 5) {
    playerPositions[currentPlayer][i] = 5;
    detectCollision(currentPlayer, playerPositions[currentPlayer][i]);
    tokensInStart[currentPlayer]--;
    tokensInMove[currentPlayer]++;
    return;
  }
}
}
//Detect collision
void detectCollision(int currentPlayer, int movedTokenIndex) {
// Get the new position of the moved token
int newPosition = playerPaths[currentPlayer][movedTokenIndex];
// Check if the new position is a safe square
for (int i = 0; i < SAFE_SQUARES_COUNT; i++) {
  if (newPosition == safeSquares[i]) {
    // Safe square; do nothing
    return;
  }
}
// Check for collisions with other players
for (int otherPlayer = 0; otherPlayer < 4; otherPlayer++) {
  if (otherPlayer == currentPlayer) {
    continue;  // Skip the current player
  }
  for (int otherToken = 0; otherToken < 4; otherToken++) {
    if (playerPaths[otherPlayer][otherToken] == newPosition) {
      // Collision detected; move the other player's token to a vacant start point
      for (int i = 1; i < 5; i++) {
        if (playerPositions[otherPlayer][0] != i && playerPositions[otherPlayer][1] != i && playerPositions[otherPlayer][2] != i && playerPositions[otherPlayer][3] != i) {
          playerPositions[otherPlayer][otherToken] = i;  // Move to start
          tokensInStart[otherPlayer]++;
          tokensInMove[otherPlayer]--;
          Serial.print("Collision! Token from player ");
          Serial.print(otherPlayer + 1);
          Serial.print(" moved back to start position.");
          Serial.println();
          break;
        }
        return;
      }
    }
  }
}
}
//Check if there is any valid move for the player
void checkValidMove() {
Serial.println("Checking for valid moves!");
int validMoves = 0;
int selectedToken = -1;
// Check all tokens of the current player
for (int token = 0; token < 4; token++) {
  int pos1 = playerPositions[currentPlayer][token];
  int pos = playerPaths[currentPlayer][pos1];
  // Check if the token can make a valid move
  bool isValid = true;
  // Check if the new position is any of the restricted positions
  for (int i = 0; i <= 4; i++) {
    if (pos == playerPaths[currentPlayer][i]) {
      isValid = false;
      break;
    }
  }
  // Ensure `playerPaths[currentPlayer][5]` is always valid
  if (pos == playerPaths[currentPlayer][5]) {
    isValid = true;
  }
  // Check the boundary condition
  if (pos1 + diceRoll > (pathLengths[currentPlayer] + 1)) {
    isValid = false;
  }
  // If valid, count the move and store the token index
  if (isValid) {
    validMoves++;
    selectedToken = token;
  }
  // Print debugging information
  Serial.print(" : Valid: ");
  Serial.println(isValid);
}
// If no valid moves are found
if (validMoves == 0) {
  Serial.println("No valid moves are found!");
  DisplayPNG((uint8_t *)NoValidMoveMessage, sizeof(NoValidMoveMessage));  // Replace with your actual PNG for the message
  delay(2000);                                                            // Wait to allow the player to see the message
  if (diceRoll == 6 && rollsInTurn < 3) {
    mode = 0;
    return;
  }
  nextPlayer();  // Move to the next player
  return;
}
// If only one valid move is found
if (validMoves == 1) {
  Serial.println("Single valid move found! Auto move!");
  playerPositions[currentPlayer][selectedToken] += diceRoll;
  detectCollision(currentPlayer, playerPositions[currentPlayer][selectedToken]);
  // Update LEDs and move to the next player
  // Wait to allow the player to see the message
  if (diceRoll == 6 && rollsInTurn < 3) {
    mode = 0;
    return;
  } else {
    nextPlayer();
    return;
  }
}
// If multiple valid moves are found
Serial.println("Multiple valid moves are found! Select your token.");
mode = 1;  // Change mode to 1 to allow token selection
}
// Token movement logic
void handleTokenMove(int TouchNumb) {
int tmap = TouchNumb;
if (mode == 5 && TouchNumb > 99) {
  tmap -= 100;
  if (rolls[tmap] == 1) {
    return;
  }
  rolls[tmap] = 1;
  int temproll = random(1, 7);
  playGIF(gifImages[temproll - 1], gifSizes[temproll - 1]);
  HighRoller[tmap] = temproll;
  DisplayPNG((uint8_t *)MessageBg, sizeof(MessageBg));
  displayHighRollers(HighRoller[0], HighRoller[1], HighRoller[2], HighRoller[3], 60);
  PlayerSetupCount++;
  if (temproll > highestRoll) {
    highestRoll = temproll;
    startingPlayer = tmap;
  }
  if (PlayerSetupCount == 4) {
    delay(1000);
    mode = 0;
  }
} else {
  if (mode == 0) {
    if (tmap < 100) {
      return;
    }
    if (tmap >= 100 && currentPlayer == (tmap - 100)) {  // Touch detected in the starting area
      diceRoll = random(1, 7);
      playGIF(gifImages[diceRoll - 1], gifSizes[diceRoll - 1]);
      Serial.print("Dice roll: ");
      Serial.println(diceRoll);
      if (diceRoll == 6 || diceRoll == 1) {
        if (tokensInStart[currentPlayer] == 0) {
          //Serial.println("Select your tokenxx");
          checkValidMove();
        } else {
          TakeTokenOut();
          Serial.println("Token is out");
        }
        if (rollsInTurn == 3) {
          nextPlayer();
        }
        rollsInTurn++;
      } else {
        //rollsInTurn = 0;
        if (tokensInMove[currentPlayer] > 0) {
          Serial.println("Select your token");
          checkValidMove();
        } else {
          Serial.println("better luck next time");
          nextPlayer();
        }
      }
    }
    drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
    return;
  }
  if (mode == 1) {
    for (int i = 0; i < 4; i++) {
      int relativePos = playerPaths[currentPlayer][playerPositions[currentPlayer][i]];
      Serial.print("Token ");
      Serial.print(i);
      Serial.print(": Current Position ");
      Serial.print(relativePos);
      Serial.print(", Touch Map ");
      Serial.print(tmap);
      Serial.print(", Dice Roll ");
      Serial.println(diceRoll);
      if (relativePos == tmap && playerPositions[currentPlayer][i] + diceRoll < 61) {
        playerPositions[currentPlayer][i] += diceRoll;
        Serial.println("token moved");
        if (playerPositions[currentPlayer][i] == 60) {


          tft.fillScreen(TFT_BLACK);
          playGIF(Confetti, sizeof(Confetti));
          tft.fillScreen(TFT_BLACK);
          playGIF(Confetti, sizeof(Confetti));
          tft.fillScreen(TFT_BLACK);
          playGIF(Confetti, sizeof(Confetti));
          tokensInHome[currentPlayer]++;
          tokensInMove[currentPlayer]--;
        } else {
          detectCollision(currentPlayer, playerPositions[currentPlayer][i]);
        }
        if (tokensInHome[currentPlayer] == 4) {
          TotalWins += 1;
        }
        // Wait to allow the player to see the message
        if (diceRoll == 6) {
          if (rollsInTurn == 3) {
            nextPlayer();
            drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
            mode = 0;
            return;
          } else {
            drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
            mode = 0;
            return;
          }
        } else {
          nextPlayer();
          drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
          return;
        }
      }
    }
    return;
  }
  if (mode == 2) {
    return;
  }
}
}
The setup function configures all hardware components and prepares the game environment. It initializes the serial communication for debugging and sets up the NeoPixel LED strip, ensuring all LEDs are off and brightness is set to a moderate level. It configures the shift registers and ADC for touch matrix scanning and initializes the TFT display, setting its rotation, clearing the screen, and loading a large font for game visuals. The game mode is set to 5, indicating the setup phase and the startup animation is played to welcome players. The player setup process is triggered through the setupPlayers function, which waits for player interactions and assigns the first turn. Finally, the Ludo home area is drawn with token positions visualized based on their starting states.

void setup() {
Serial.begin(115200);
strip.begin();            // Initialize NeoPixel strip object
strip.show();             // Turn OFF all pixels ASAP
strip.setBrightness(50);  // Set brightness
strip.clear();
strip.show();
setupShiftRegisters();
setupADC();
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
tft.loadFont(AA_FONT_LARGE);
tft.setTextColor(TFT_WHITE);  // Set the text color to white
tft.setTextDatum(MC_DATUM);
tft.setTextSize(2);  // Set the text size
mode = 5;
gif.begin(BIG_ENDIAN_PIXELS);
playGIF(GIF_IMAGE0, sizeof(GIF_IMAGE0));
lastTouchTime = millis();  // Initialize the last touch time
setupPlayers();
drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
//calibrateTouchThreshold();
lastTouchTime = millis();  // Initialize the last touch time
}

The loop function is the core of the gameplay logic, continuously scanning for player actions and updating the game state. It checks if any player has moved all tokens to the home area and, if so, enters a celebratory state where the winning player’s name is displayed alongside a sequence of congratulatory GIFs. The function also handles regular game activities such as scanning the touch matrix, debouncing inputs, updating the LEDs to reflect token positions, and checking for inactivity to trigger sleep mode if necessary. A short delay ensures smooth execution and prevents unnecessary resource usage.

void loop() {
int count = 0;
int winner = -1;  // Variable to track the winning player
for (int i = 0; i < 4; i++) {
  if (tokensInHome[i] == 4) {
    count++;
    winner = i;  // Set the winner
  }
}
// If any player has all tokens in home, play the Congrats GIF
if (count > 0) {
  while (true) {                // Loop indefinitely to show the Congrats GIF
    tft.fillScreen(TFT_BLACK);  // Clear the screen
    playGIF(Confetti, sizeof(Confetti));
    playGIF(Congrats, sizeof(Congrats));
    tft.drawString("WINNER", 120, 40);
    tft.drawString(Player[winner], 120, 200);
    delay(2000);
  }
}
scanMatrix();
debounceKeys();
updateLEDs();
handleInactivity();
delay(10);  // Adjust the delay as needed for your application
}

How to Use the Digital Ludo Board

To play the Ludo game with the Digital Ludo Board, begin by powering on the board. The game starts with a boot animation on the screen, followed by a message prompt,  prompting players to touch their respective start areas to register. A total of four players can participate, represented by the colours Blue, Yellow, Green, and Red. When a players register, the game will determine the starting player by rolling a virtual die for each participant. The player with the highest roll goes first, and their turn is highlighted on the board.

During a player's turn, they touch their designated start area to roll the dice, and the roll result will be displayed on the screen after a small rice-rolling animation. If the roll is 6 or 1, the player can move a token from the starting area onto the main board or move a token already in play. The 6 and 1 will be used to take out the tokens from the starting area until all the tokens are out of the starting area. After a dice roll, if there is only one valid move the system will automatically do that movement for the player. If there are multiple possible moves the system will wait for the user to select the token to move. The system automatically highlights valid moves and resolves any collisions by sending the opponent's token back to their starting area. If a player has no valid moves, their turn is skipped. The game continues with turns rotating among players until a winner emerges. A player wins when all four of their tokens reach their respective home zone. The system celebrates the winner with animations and highlights their achievement on the screen. 

Supporting Files

Here is the link to our GitHub repo, where you'll find the source code, schematics, and all other necessary files to build your own Digital Ludo Board.

Custom Multicolor PCB CodeCustom Multicolor PCB Zip File

Code

/*
* Project Name: Digital Ludo Board
* Project Brief: Firmware for Digital Ludo Board built around ESP-32S3
* Author: Jobit Joseph @ https://github.com/jobitjoseph
* IDE: Arduino IDE 2.3.4
* Arduino Core: ESP32 Arduino Core V 3.1.0
* Arduino Board Config: Board: ESP32-S3 Dev Module
*                       USB CDC On Boot : Enable
*                       Flash Size : 16MB(128mb)
*                       Partition Scheme : Custom
*                       PSRAM : OPI PSRAM
* Dependencies : Adafruit NeoPixel Library V 1.12.2 @ https://github.com/teknynja/Adafruit_NeoPixel
*                PNGDec Library V 1.0.3 @https://github.com/bitbank2/PNGdec
*                TFT_eSPI Library V 2.5.43 @https://github.com/Bodmer/TFT_eSPI
*                AnimatedGIF Library V 2.1.1 @ https://github.com/bitbank2/AnimatedGIF
* Hardware : ESP32-S3-WROOM-1-N16R8
*            Waveshare 1.28inch Round LCD Display Module, 65K RGB 240x240
*            74HC595 Shiftregister
*            SK6812MINI-E RGB LED
*            IP5306 Power Managment IC
* Copyright B) Jobit Joseph
* Copyright B) Semicon Media Pvt Ltd
* Copyright B) Circuitdigest.com
*
* This code is licensed under the following conditions:
*
* 1. Non-Commercial Use:
* This program is free software: you can redistribute it and/or modify it
* for personal or educational purposes under the condition that credit is given
* to the original author. Attribution is required, and the original author
* must be credited in any derivative works or distributions.
*
* 2. Commercial Use:
* For any commercial use of this software, you must obtain a separate license
* from the original author. Contact the author for permissions or licensing
* options before using this software for commercial purposes.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
* FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* Author: Jobit Joseph
* Date: 25 December 2024
*
* For commercial use or licensing requests, please contact [jobitjoseph1@gmail.com].
*/

#include "esp_sleep.h"
/*PNG decoder library*/
#include <PNGdec.h>          // Include the PNG decoder library
PNG png;                     // PNG decoder instance
#define MAX_IMAGE_WIDTH 240  // Adjust for your images
int16_t xpos = 0;
int16_t ypos = 0;
/*PNG decoder library*/
#include <Adafruit_NeoPixel.h>  //Adafruit neopixel Library
Adafruit_NeoPixel strip(88, 16, NEO_GRB + NEO_KHZ800);
const unsigned long flashInterval = 300;  // Fixed flash interval (300ms)

#include <SPI.h>
#include <TFT_eSPI.h>  //.  TFT_eSPI Library
TFT_eSPI tft = TFT_eSPI();

#include <AnimatedGIF.h>  // AnimatedGIF Library
#include "NotoSansBold36.h"
#include "ImgData.h"
AnimatedGIF gif;
#define GIF_IMAGE0 Ludo_intro
#define GIF_IMAGE00 Confetti
#define GIF_IMAGE11 Congrats
#define GIF_IMAGE1 Dice_1
#define GIF_IMAGE2 Dice_2
#define GIF_IMAGE3 Dice_3
#define GIF_IMAGE4 Dice_4
#define GIF_IMAGE5 Dice_5
#define GIF_IMAGE6 Dice_6
#define AA_FONT_LARGE NotoSansBold36
const uint8_t *gifImages[] = { GIF_IMAGE1, GIF_IMAGE2, GIF_IMAGE3, GIF_IMAGE4, GIF_IMAGE5, GIF_IMAGE6 };

const size_t gifSizes[] = {
 sizeof(GIF_IMAGE1),
 sizeof(GIF_IMAGE2),
 sizeof(GIF_IMAGE3),
 sizeof(GIF_IMAGE4),
 sizeof(GIF_IMAGE5),
 sizeof(GIF_IMAGE6)
};

// Define shift register pins
#define DATA_PIN 13   // SER (Serial Data Input)
#define LATCH_PIN 14  // RCLK (Register Clock)
#define CLOCK_PIN 15  // SRCLK (Shift Register Clock)


// GPIO Management
const int adcPins[7] = { 1, 2, 3, 4, 5, 6, 7 };  // GPIO1 to GPIO7
const int SPIPins[6] = { 8, 9, 10, 11, 12, 21 };

//RGB LED Address Mapping
int ledMapping[16][6] = {
 { 9, 8, 7, 6, 5, 4 },
 { 10, 11, 12, 13, 14, 15 },
 { 21, 20, 19, 18, 17, 16 },
 { 31, 30, 29, 28, 23, 22 },
 { 32, 33, 34, 35, 36, 37 },
 { 43, 42, 41, 40, 39, 38 },
 { 53, 52, 51, 50, 45, 44 },
 { 54, 55, 56, 57, 58, 59 },
 { 65, 64, 63, 62, 61, 60 },
 { 75, 74, 73, 72, 67, 66 },
 { 76, 77, 78, 79, 80, 81 },
 { 87, 86, 85, 84, 83, 82 },
 { 0, 1, 2, 3, -1, -1 },
 { 24, 25, 26, 27, -1, -1 },
 { 46, 47, 48, 49, -1, -1 },
 { 68, 69, 70, 71, -1, -1 },
};


// Touch detection threshold (adjust based on calibration)
int TOUCH_THRESHOLD = 20;  // Example value; calibrate for your setup
// Debounce settings
#define DEBOUNCE_COUNT 3
unsigned long lastdbug = 0;
// Arrays to hold touch states and debounce counters
uint8_t touchState[16][7] = { 0 };
uint8_t debounceCounter[16][7] = { 0 };
uint8_t stableState[16][7] = { 0 };


//Global Variables for Game Logic
int currentPlayer = 0;
int mode = 0;
int TotalWins = 0;  //To store number of player with all 4 tokens in home
int rollsInTurn = 0;
int diceRoll;            // Count of rolls in the current turn
bool extraRoll = false;  // Track if player gets a bonus roll
int highestRoll = 0;
int startingPlayer = -1;
int PlayerSetupCount = 0;

String Player[] = { "Blue", "Yellow", "Green", "Red" };
int rolls[4] = { 0 };
int HighRoller[] = { 0, 0, 0, 0 };
int PlayerPlace[4] = { 0, 0, 0, 0 };   // Player position for Blue, Yellow, Green, Red
int tokensInHome[4] = { 0, 0, 0, 0 };  // Tokens in home for Blue, Yellow, Green, Red
int tokensInMove[4] = { 0, 0, 0, 0 };
int tokensInStart[4] = { 4, 4, 4, 4 };  // Tokens in start for Blue, Yellow, Green, Red
//int playerPositions[4][4] = {{5,5,3,4},{44,44,3,4},{32,32,3,4},{19,19,3,4}}; // Token positions for each player (relative to their path)
int playerPositions[4][4] = { { 1, 2, 3, 4 }, { 1, 2, 3, 4 }, { 1, 2, 3, 4 }, { 1, 2, 3, 4 } };  // Token positions for each player (relative to their path)
// Paths for each player
const int bluePath[] = { -1, 0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 22, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 66, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87 };
const int yellowPath[] = { -1, 24, 25, 26, 27, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 66, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 };
const int greenPath[] = { -1, 46, 47, 48, 49, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 66, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 22, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43 };
const int redPath[] = { -1, 68, 69, 70, 71, 67, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 22, 23, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 45, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65 };
const int *playerPaths[] = { bluePath, yellowPath, greenPath, redPath };
const int pathLengths[] = { sizeof(bluePath) / sizeof(int), sizeof(yellowPath) / sizeof(int), sizeof(greenPath) / sizeof(int), sizeof(redPath) / sizeof(int) };

// Player colors
#define COLOR_BLUE strip.Color(0, 0, 255)
#define COLOR_YELLOW strip.Color(255, 255, 0)
#define COLOR_GREEN strip.Color(0, 255, 0)
#define COLOR_RED strip.Color(255, 0, 0)
const uint32_t playerColors[] = { COLOR_BLUE, COLOR_YELLOW, COLOR_GREEN, COLOR_RED };

// Safe squares
const int safeSquares[] = { 5, 13, 23, 35, 45, 57, 67, 79 };
#define SAFE_SQUARES_COUNT (sizeof(safeSquares) / sizeof(int))

struct LEDState {
 int playerColors[4];       // Colors of overlapping players
 int count;                 // Number of overlapping players
 unsigned long lastUpdate;  // Timestamp of the last update
 int currentIndex;          // Current color index for flashing
 bool state;                // ON/OFF state for flashing
};
LEDState ledStates[150];  // Array to track LED states (adjust size as needed)


// Global variable to track inactivity
unsigned long lastTouchTime = 0;                  // Tracks the last touch input time
const unsigned long INACTIVITY_TIMEOUT = 900000;  // 15 minutes in milliseconds

// Function to handle deep sleep
void goToSleep() {
 // Turn off all LEDs
 strip.clear();
 strip.show();

 // Turn off the display
 tft.writecommand(TFT_DISPOFF);  // Turn off display
 tft.writecommand(TFT_SLPIN);    // Enter sleep mode for display
 pinMode(21, OUTPUT);
 digitalWrite(21, LOW);  //turn of lcd backlight

 // Configure wake-up source (e.g., touch input, GPIO, timer, etc.)
 esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);  // Replace GPIO_NUM_33 with your desired GPIO
 //esp_sleep_enable_timer_wakeup(60000000);     // Optional: Wake after 1 minute if needed

 Serial.println("Going to sleep......!");

 // Enter deep sleep
 esp_deep_sleep_start();
}

// Function to handle inactivity
void handleInactivity() {
 unsigned long currentMillis = millis();

 // Check if 5 minutes of inactivity have passed
 if (currentMillis - lastTouchTime > INACTIVITY_TIMEOUT) {
   goToSleep();
 }
}

// Function to initialize shift register pins
void setupShiftRegisters() {
 pinMode(DATA_PIN, OUTPUT);
 pinMode(LATCH_PIN, OUTPUT);
 pinMode(CLOCK_PIN, OUTPUT);
 // Initialize shift registers to all zeros
 shiftOutData(0);
}

// Function to initialize ADC input pins
void setupADC() {
 for (int i = 0; i < 7; i++) {
   pinMode(adcPins[i], OUTPUT);
 }
}

// Function to scan the entire matrix
void scanMatrix() {
 for (int row = 0; row < 16; row++) {
   activateRow(row);
   delayMicroseconds(50);  // Small delay to allow signals to stabilize
   readColumns(row);
 }
}

// Function to activate a specific row using shift registers
void activateRow(int row) {
 uint16_t data = 1 << row;  // Create a bitmask to activate the current row
 shiftOutData(data);
}

// Function to shift out data to the shift registers
void shiftOutData(uint16_t data) {
 digitalWrite(LATCH_PIN, LOW);
 for (int i = 15; i >= 0; i--) {
   digitalWrite(CLOCK_PIN, LOW);
   digitalWrite(DATA_PIN, (data & (1 << i)) ? HIGH : LOW);
   digitalWrite(CLOCK_PIN, HIGH);
 }
 digitalWrite(LATCH_PIN, HIGH);
}

// Function to read the columns (ADC inputs) for the active row
void readColumns(int row) {
 for (int col = 0; col < 7; col++) {
   pinMode(adcPins[col], INPUT);
   int adcValue = analogRead(adcPins[col]);
   pinMode(adcPins[col], OUTPUT);
   digitalWrite(adcPins[col], 0);
   touchState[row][col] = (adcValue > TOUCH_THRESHOLD) ? 1 : 0;
 }
}

// Function to handle debounce logic
void debounceKeys() {
 for (int row = 0; row < 16; row++) {
   for (int col = 0; col < 7; col++) {
     if (touchState[row][col] == stableState[row][col]) {
       debounceCounter[row][col] = 0;  // Reset counter if state is stable
     } else {
       debounceCounter[row][col]++;
       if (debounceCounter[row][col] >= DEBOUNCE_COUNT) {
         stableState[row][col] = touchState[row][col];
         debounceCounter[row][col] = 0;
         handleKeyEvent(row, col, stableState[row][col]);
       }
     }
   }
 }
}

// Function to handle key events (presses and releases)
void handleKeyEvent(int row, int col, uint8_t state) {
 if (state == 1) {
   return;
 } else {
   // Key released
   lastTouchTime = millis();  // Reset the inactivity timer
   if (row < 12) {
     Serial.print("Touch at  : ");
     Serial.println(ledMapping[row][col]);
     handleTokenMove(ledMapping[row][col]);
   } else {
     Serial.print("Player  : ");
     Serial.println(row - 12);
     handleTokenMove(100 + (row - 12));
   }
 }
}

// Optional: Function to calibrate touch threshold
void calibrateTouchThreshold() {
 int maxValue = 0;
 for (int col = 0; col < 7; col++) {
   int adcValue = analogRead(adcPins[col]);
   if (adcValue > maxValue) {
     maxValue = adcValue;
   }
 }
 // Set TOUCH_THRESHOLD slightly above the maximum observed value
 TOUCH_THRESHOLD = maxValue + 100;  // Adjust the offset as needed
}

void pngDraw(PNGDRAW *pDraw) {
 uint16_t lineBuffer[MAX_IMAGE_WIDTH];
 png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
 tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, lineBuffer);
}

void DisplayPNG(const uint8_t *pngArray, size_t arraySize) {
 int16_t rc = png.openFLASH((uint8_t *)pngArray, arraySize, pngDraw);
 if (rc == PNG_SUCCESS) {
   Serial.println("Successfully opened PNG file");
   Serial.printf("Image specs: (%d x %d), %d bpp, pixel type: %d\n",
                 png.getWidth(), png.getHeight(), png.getBpp(), png.getPixelType());

   tft.startWrite();
   uint32_t dt = millis();
   rc = png.decode(NULL, 0);
   Serial.print(millis() - dt);
   Serial.println("ms");
   tft.endWrite();
   // png.close(); // Not needed for memory-to-memory decode
 } else {
   Serial.println("Failed to open PNG file");
 }
}

void playGIF(const uint8_t *gifImage, size_t gifSize) {
 //size_t gifSize = sizeof(gifImage); // Automatically calculate size
 if (gif.open((uint8_t *)gifImage, gifSize, GIFDraw)) {
   Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
   tft.startWrite();
   while (gif.playFrame(true, NULL)) {
     yield();
   }
   gif.close();
   tft.endWrite();
 } else {
   Serial.println("Failed to open GIF!");
 }
}
void drawLudoHomeArea(int tokensBlue, int tokensYellow, int tokensGreen, int tokensRed, int centerColorFlag) {
 // Colors for tokens and placeholders
 uint16_t blueColor = 0x0294;
 uint16_t yellowColor = 0xd5a0;
 uint16_t greenColor = 0x4420;
 uint16_t redColor = 0xb021;
 uint16_t centerCircleColor;

 // Set center circle color based on the flag
 switch (centerColorFlag) {
   case 0:
     centerCircleColor = blueColor;
     break;  // Light Blue
   case 1:
     centerCircleColor = yellowColor;
     break;
   case 2:
     centerCircleColor = greenColor;
     break;
   case 3:
     centerCircleColor = redColor;
     break;
   default:
     centerCircleColor = blueColor;
     break;  // Default to Light Blue
 }

 // Screen dimensions (assuming circular display of 240x240 pixels)
 int screenWidth = 240;
 int screenHeight = 240;
 int centerX = screenWidth / 2;
 int centerY = screenHeight / 2;
 int radius = screenWidth / 2;

 // Clear screen
 tft.fillScreen(TFT_BLACK);

 // Draw quadrant backgrounds
 tft.fillTriangle(centerX, centerY, 0, 0, screenWidth, 0, yellowColor);                      // Top-right (Yellow)
 tft.fillTriangle(centerX, centerY, screenWidth, 0, screenWidth, screenHeight, greenColor);  // Bottom-right (Green)
 tft.fillTriangle(centerX, centerY, screenWidth, screenHeight, 0, screenHeight, redColor);   // Bottom-left (Red)
 tft.fillTriangle(centerX, centerY, 0, screenHeight, 0, 0, blueColor);                       // Top-left (Blue)

 // Draw center circle
 int centerRadius = 49;                                          // Circle radius is 50 pixels diameter
 tft.fillCircle(centerX, centerY, centerRadius + 2, TFT_BLACK);  // Outer border
 tft.fillCircle(centerX, centerY, centerRadius, centerCircleColor);

 // Function to draw tokens and placeholders at left, right, top, and bottom centers
 auto drawTokens = [&](int cx, int cy, uint16_t color, int tokenCount) {
   int tokenRadius = 10;
   int spacing = 20;   // Spacing between tokens
   int maxTokens = 4;  // Max tokens per quadrant

   // Calculate positions for tokens (left, right, top, bottom centers of the quadrant)
   int positions[4][2] = {
     { cx, cy + spacing },  // Left
     { cx, cy - spacing },  // Right
     { cx - spacing, cy },  // Top
     { cx + spacing, cy }   // Bottom
   };

   for (int i = 0; i < maxTokens; i++) {
     int x = positions[i][0];
     int y = positions[i][1];

     if (i < tokenCount) {
       tft.fillCircle(x, y, tokenRadius + 2, TFT_BLACK);  // Outer outline
       tft.fillCircle(x, y, tokenRadius, color);          // Filled token
       tft.fillCircle(x, y, tokenRadius - 4, TFT_BLACK);  // Inner outline
     } else {
       tft.fillCircle(x, y, tokenRadius + 2, TFT_BLACK);  // Outer outline
       tft.fillCircle(x, y, tokenRadius, TFT_WHITE);      // Placeholder
     }
   }
 };

 // Draw tokens for each quadrant
 drawTokens(34, 120, blueColor, tokensBlue);      // Top-left (Blue)
 drawTokens(120, 34, yellowColor, tokensYellow);  // Top-right (Yellow)
 drawTokens(206, 120, greenColor, tokensGreen);   // Bottom-right (Green)
 drawTokens(120, 206, redColor, tokensRed);       // Bottom-left (Red)
}

void displayHighRollers(int left, int top, int right, int bottom, int offset) {
 if (left > 0) {
   tft.drawNumber(left, offset, 120);
 }
 if (top > 0) {
   tft.drawNumber(top, 120, offset);
 }
 if (right > 0) {
   tft.drawNumber(right, 240 - offset, 120);
 }
 if (bottom > 0) {
   tft.drawNumber(bottom, 120, 240 - offset);
 }
}

// Update LEDs
void updateLEDs() {
 const int numPlayers = 4;               // Total players
 const int numTokens = 4;                // Tokens per player
 const int numLEDs = strip.numPixels();  // Total LEDs

 // Reset the LED states
 for (int i = 0; i < numLEDs; i++) {
   ledStates[i].count = 0;
 }

 // Populate LED states
 for (int player = 0; player < numPlayers; player++) {
   for (int token = 0; token < numTokens; token++) {
     int pos = playerPositions[player][token];
     if (pos > 0 && pos < pathLengths[player]) {
       int ledIndex = playerPaths[player][pos];
       if (ledStates[ledIndex].count < numPlayers) {
         ledStates[ledIndex].playerColors[ledStates[ledIndex].count] = playerColors[player];
         ledStates[ledIndex].count++;
       }
     }
   }
 }

 // Update LEDs based on their states
 unsigned long currentMillis = millis();
 for (int i = 0; i < numLEDs; i++) {
   if (ledStates[i].count == 0) {
     // No tokens on this LED, turn it off
     strip.setPixelColor(i, 0);
   } else if (ledStates[i].count == 1) {
     // Single token, set to static color
     strip.setPixelColor(i, ledStates[i].playerColors[0]);
   } else {
     // Multiple tokens, handle flashing
     if (currentMillis - ledStates[i].lastUpdate >= flashInterval) {
       ledStates[i].lastUpdate = currentMillis;
       if (ledStates[i].state) {
         // Turn the LED off
         strip.setPixelColor(i, 0);
       } else {
         // Set the LED to the current color
         strip.setPixelColor(i, ledStates[i].playerColors[ledStates[i].currentIndex]);
         // Move to the next color
         ledStates[i].currentIndex = (ledStates[i].currentIndex + 1) % ledStates[i].count;
       }
       ledStates[i].state = !ledStates[i].state;
     }
   }
 }

 strip.show();
}

void setupPlayers() {
 DisplayPNG((uint8_t *)StartMessage, sizeof(StartMessage));
 Serial.println("Any Player touch start area!");
 while (mode == 5) {
   scanMatrix();
   debounceKeys();
   updateLEDs();
   handleInactivity();
   if (millis() - lastdbug > 1000) {
     Serial.print("Mode: ");
     Serial.print(mode);
     Serial.print(" Player with High Roll: ");
     Serial.println(startingPlayer + 1);
     lastdbug = millis();
   }
 }
 currentPlayer = startingPlayer;
 DisplayPNG((uint8_t *)FirstPlayer, sizeof(FirstPlayer));
 tft.drawString(Player[currentPlayer], 120, 91);
 delay(2000);
}

// Manage player turns
void nextPlayer() {
 if (rollsInTurn > 0) {
   rollsInTurn = 0;
 }
 do {
   currentPlayer = (currentPlayer + 1) % 4;   // Move to the next player
 } while (tokensInHome[currentPlayer] == 4);  // Continue until a valid player is found
 mode = 0;
}

// Take token out of start area
void TakeTokenOut() {
 for (int i = 0; i < 4; i++) {
   if (playerPositions[currentPlayer][i] < 5) {
     playerPositions[currentPlayer][i] = 5;
     detectCollision(currentPlayer, playerPositions[currentPlayer][i]);
     tokensInStart[currentPlayer]--;
     tokensInMove[currentPlayer]++;
     return;
   }
 }
}

//Detect collision
void detectCollision(int currentPlayer, int movedTokenIndex) {
 // Get the new position of the moved token
 int newPosition = playerPaths[currentPlayer][movedTokenIndex];

 // Check if the new position is a safe square
 for (int i = 0; i < SAFE_SQUARES_COUNT; i++) {
   if (newPosition == safeSquares[i]) {
     // Safe square; do nothing
     return;
   }
 }

 // Check for collisions with other players
 for (int otherPlayer = 0; otherPlayer < 4; otherPlayer++) {
   if (otherPlayer == currentPlayer) {
     continue;  // Skip the current player
   }

   for (int otherToken = 0; otherToken < 4; otherToken++) {
     if (playerPaths[otherPlayer][otherToken] == newPosition) {
       // Collision detected; move the other player's token to a vacant start point
       for (int i = 1; i < 5; i++) {
         if (playerPositions[otherPlayer][0] != i && playerPositions[otherPlayer][1] != i && playerPositions[otherPlayer][2] != i && playerPositions[otherPlayer][3] != i) {
           playerPositions[otherPlayer][otherToken] = i;  // Move to start
           tokensInStart[otherPlayer]++;
           tokensInMove[otherPlayer]--;
           Serial.print("Collision! Token from player ");
           Serial.print(otherPlayer + 1);
           Serial.print(" moved back to start position.");
           Serial.println();
           break;
         }
         return;
       }
     }
   }
 }
}

//Check if there is any valid move for the player
void checkValidMove() {
 Serial.println("Checking for valid moves!");
 int validMoves = 0;
 int selectedToken = -1;

 // Check all tokens of the current player
 for (int token = 0; token < 4; token++) {
   int pos1 = playerPositions[currentPlayer][token];
   int pos = playerPaths[currentPlayer][pos1];
   // Check if the token can make a valid move
   bool isValid = true;
   // Check if the new position is any of the restricted positions
   for (int i = 0; i <= 4; i++) {
     if (pos == playerPaths[currentPlayer][i]) {
       isValid = false;
       break;
     }
   }
   // Ensure `playerPaths[currentPlayer][5]` is always valid
   if (pos == playerPaths[currentPlayer][5]) {
     isValid = true;
   }
   // Check the boundary condition
   if (pos1 + diceRoll > (pathLengths[currentPlayer] + 1)) {
     isValid = false;
   }
   // If valid, count the move and store the token index
   if (isValid) {
     validMoves++;
     selectedToken = token;
   }
   // Print debugging information
   Serial.print(" : Valid: ");
   Serial.println(isValid);
 }

 // If no valid moves are found
 if (validMoves == 0) {
   Serial.println("No valid moves are found!");
   DisplayPNG((uint8_t *)NoValidMoveMessage, sizeof(NoValidMoveMessage));  // Replace with your actual PNG for the message
   delay(2000);                                                            // Wait to allow the player to see the message
   if (diceRoll == 6 && rollsInTurn < 3) {
     mode = 0;
     return;
   }
   nextPlayer();  // Move to the next player
   return;
 }

 // If only one valid move is found
 if (validMoves == 1) {
   Serial.println("Single valid move found! Auto move!");
   playerPositions[currentPlayer][selectedToken] += diceRoll;
   detectCollision(currentPlayer, playerPositions[currentPlayer][selectedToken]);

   // Update LEDs and move to the next player
   // Wait to allow the player to see the message
   if (diceRoll == 6 && rollsInTurn < 3) {
     mode = 0;
     return;
   } else {
     nextPlayer();
     return;
   }
 }

 // If multiple valid moves are found
 Serial.println("Multiple valid moves are found! Select your token.");
 mode = 1;  // Change mode to 1 to allow token selection
}

// Token movement logic
void handleTokenMove(int TouchNumb) {
 int tmap = TouchNumb;
 if (mode == 5 && TouchNumb > 99) {
   tmap -= 100;
   if (rolls[tmap] == 1) {
     return;
   }
   rolls[tmap] = 1;
   int temproll = random(1, 7);
   playGIF(gifImages[temproll - 1], gifSizes[temproll - 1]);
   HighRoller[tmap] = temproll;
   DisplayPNG((uint8_t *)MessageBg, sizeof(MessageBg));
   displayHighRollers(HighRoller[0], HighRoller[1], HighRoller[2], HighRoller[3], 60);
   PlayerSetupCount++;
   if (temproll > highestRoll) {
     highestRoll = temproll;
     startingPlayer = tmap;
   }

   if (PlayerSetupCount == 4) {
     delay(1000);
     mode = 0;
   }
 } else {

   if (mode == 0) {
     if (tmap < 100) {
       return;
     }
     if (tmap >= 100 && currentPlayer == (tmap - 100)) {  // Touch detected in the starting area
       diceRoll = random(1, 7);
       playGIF(gifImages[diceRoll - 1], gifSizes[diceRoll - 1]);
       Serial.print("Dice roll: ");
       Serial.println(diceRoll);
       if (diceRoll == 6 || diceRoll == 1) {

         if (tokensInStart[currentPlayer] == 0) {
           //Serial.println("Select your tokenxx");
           checkValidMove();
         } else {
           TakeTokenOut();
           Serial.println("Token is out");
         }
         if (rollsInTurn == 3) {
           nextPlayer();
         }
         rollsInTurn++;
       } else {
         //rollsInTurn = 0;
         if (tokensInMove[currentPlayer] > 0) {
           Serial.println("Select your token");
           checkValidMove();
         } else {
           Serial.println("better luck next time");
           nextPlayer();
         }
       }
     }
     drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
     return;
   }
   if (mode == 1) {

     for (int i = 0; i < 4; i++) {
       int relativePos = playerPaths[currentPlayer][playerPositions[currentPlayer][i]];
       Serial.print("Token ");
       Serial.print(i);
       Serial.print(": Current Position ");
       Serial.print(relativePos);
       Serial.print(", Touch Map ");
       Serial.print(tmap);
       Serial.print(", Dice Roll ");
       Serial.println(diceRoll);
       if (relativePos == tmap && playerPositions[currentPlayer][i] + diceRoll < 61) {
         playerPositions[currentPlayer][i] += diceRoll;
         Serial.println("token moved");
         if (playerPositions[currentPlayer][i] == 60) {

           tft.fillScreen(TFT_BLACK);
           playGIF(Confetti, sizeof(Confetti));
           tft.fillScreen(TFT_BLACK);
           playGIF(Confetti, sizeof(Confetti));
           tft.fillScreen(TFT_BLACK);
           playGIF(Confetti, sizeof(Confetti));
           tokensInHome[currentPlayer]++;
           tokensInMove[currentPlayer]--;
         } else {
           detectCollision(currentPlayer, playerPositions[currentPlayer][i]);
         }
         if (tokensInHome[currentPlayer] == 4) {
           TotalWins += 1;
         }
         // Wait to allow the player to see the message
         if (diceRoll == 6) {
           if (rollsInTurn == 3) {
             nextPlayer();
             drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
             mode = 0;
             return;
           } else {
             drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
             mode = 0;
             return;
           }

         } else {
           nextPlayer();
           drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
           return;
         }
       }
     }
     return;
   }
   if (mode == 2) {
     return;
   }
 }
}


void setup() {
 Serial.begin(115200);

 strip.begin();            // Initialize NeoPixel strip object
 strip.show();             // Turn OFF all pixels ASAP
 strip.setBrightness(50);  // Set brightness
 strip.clear();
 strip.show();
 setupShiftRegisters();

 setupADC();

 tft.init();
 tft.setRotation(0);
 tft.fillScreen(TFT_BLACK);
 tft.loadFont(AA_FONT_LARGE);
 tft.setTextColor(TFT_WHITE);  // Set the text color to white
 tft.setTextDatum(MC_DATUM);
 tft.setTextSize(2);  // Set the text size

 mode = 5;

 gif.begin(BIG_ENDIAN_PIXELS);
 playGIF(GIF_IMAGE0, sizeof(GIF_IMAGE0));

 lastTouchTime = millis();  // Initialize the last touch time

 setupPlayers();

 drawLudoHomeArea(tokensInHome[0], tokensInHome[1], tokensInHome[2], tokensInHome[3], currentPlayer);
 //calibrateTouchThreshold();

 lastTouchTime = millis();  // Initialize the last touch time
}

void loop() {
 int count = 0;
 int winner = -1;  // Variable to track the winning player

 for (int i = 0; i < 4; i++) {
   if (tokensInHome[i] == 4) {
     count++;
     winner = i;  // Set the winner
   }
 }
 // If any player has all tokens in home, play the Congrats GIF
 if (count > 0) {

   while (true) {                // Loop indefinitely to show the Congrats GIF
     tft.fillScreen(TFT_BLACK);  // Clear the screen
     playGIF(Confetti, sizeof(Confetti));
     playGIF(Congrats, sizeof(Congrats));
     tft.drawString("WINNER", 120, 40);
     tft.drawString(Player[winner], 120, 200);
     delay(2000);
   }
 }

 scanMatrix();
 debounceKeys();
 updateLEDs();
 handleInactivity();
 delay(10);  // Adjust the delay as needed for your application
}

Have any question realated to this Article?

Ask Our Community Members