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.
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.
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.
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.
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.
And here is the top and bottom 3D view of the 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.
On the Quick Order page upload your PCB Gerber file, so that the PCBWAY tool will automatically populate the PCB dimensions.
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.
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.
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.
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.
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.
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.
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.
Here is the final assembled Ludo board with the enclosure.
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.
/*
* 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
}