ESP32 Tilt Dice: Shake-to-Roll Digital Dice Project

Dice are an essential part of many board games and decision-making activities. But what if you could build your own digital dice that rolls when you shake it?

In this project, we build a digital dice roller using an ESP32 board with a 1.9″ TFT LCD and a tilt sensor. When you shake the device, a random number between 1 and 6 appears on the screen as a traditional red-dot dice face — all displayed inside a stylish 3D-looking square.

Whether you’re building a game, learning embedded graphics, or just want to replace physical dice with tech, this is a fun, practical, and interactive project.

PCBWay offers high-quality PCB prototyping and assembly at an affordable price, starting at just $5 for 5 PCBs. With fast turnaround, great customer support, and easy online ordering, it’s a top choice for hobbyists and professionals alike.

Hardware Components

You’ll need the following hardware components to get started:

ComponentQuantityDescription
ESP32 with built-in 1.9″ ST7789 LCD1Main microcontroller with LCD (e.g. this one)
Tilt Sensor (Ball Switch)1Detects motion to simulate a dice roll
Breadboard or jumper wires1 setFor prototyping the connections
(Optional) 10kΩ Resistor1For additional pull-up if needed (not required here)

Schematic

Make connections according to the circuit diagram given below.

Wiring / Connections

Tilt Sensor PinESP32 Pin
Ground (-)GND
VCC (+)3V3
Signal (S)GPIO 33

Arduino Code

#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>

#define TFT_CS    15
#define TFT_DC    2
#define TFT_RST   4
#define TILT_PIN  33  // Connected to tilt sensor

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

// Dice layout [3x3 grid]
const uint8_t diceDots[6][9] = {
  {0,0,0, 0,1,0, 0,0,0}, // 1
  {1,0,0, 0,0,0, 0,0,1}, // 2
  {1,0,0, 0,1,0, 0,0,1}, // 3
  {1,0,1, 0,0,0, 1,0,1}, // 4
  {1,0,1, 0,1,0, 1,0,1}, // 5
  {1,0,1, 1,0,1, 1,0,1}  // 6
};

unsigned long lastShakeTime = 0;
const unsigned long debounceDelay = 200;
bool wasTilted = false;

void setup() {
  pinMode(TILT_PIN, INPUT_PULLUP);
  Serial.begin(115200);

  tft.init(170, 320);
  tft.setRotation(1);
  tft.fillScreen(ST77XX_BLACK);

  showCenterText("Shake to Roll Dice", 2, ST77XX_WHITE);
}

void loop() {
  bool tilt = digitalRead(TILT_PIN) == LOW;

  if (tilt && !wasTilted && (millis() - lastShakeTime > debounceDelay)) {
    lastShakeTime = millis();
    wasTilted = true;

    int value = random(1, 7);
    Serial.println("Rolled: " + String(value));
    displayDice(value);
  } 
  else if (!tilt) {
    wasTilted = false;
  }
}

void showCenterText(const char* msg, uint8_t textSize, uint16_t color) {
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextSize(textSize);
  int16_t x1, y1;
  uint16_t w, h;
  tft.getTextBounds(msg, 0, 0, &x1, &y1, &w, &h);
  int x = (tft.width() - w) / 2;
  int y = 10;  // Move closer to top
  tft.setCursor(x, y);
  tft.setTextColor(color);
  tft.println(msg);
}

void displayDice(int num) {
  int diceSize = 120;
  int diceX = (tft.width() - diceSize) / 2;
  int diceY = 60;  // Moved up to avoid cropping

  // Draw white square dice with black border
  tft.fillRect(diceX, diceY, diceSize, diceSize, ST77XX_WHITE);
  tft.drawRect(diceX, diceY, diceSize, diceSize, ST77XX_BLACK);

  // 3D shadow effect (within screen bounds)
  if (diceY + diceSize + 1 < tft.height())
    tft.drawLine(diceX, diceY + diceSize, diceX + diceSize, diceY + diceSize, ST77XX_BLACK);
  if (diceX + diceSize + 1 < tft.width())
    tft.drawLine(diceX + diceSize, diceY, diceX + diceSize, diceY + diceSize, ST77XX_BLACK);

  int dotSize = 10;
  int spacing = diceSize / 4;
  int x0 = diceX + spacing;
  int y0 = diceY + spacing;

  const uint8_t* layout = diceDots[num - 1];
  for (int i = 0; i < 9; i++) {
    if (layout[i]) {
      int row = i / 3;
      int col = i % 3;
      int cx = x0 + col * spacing;
      int cy = y0 + row * spacing;
      tft.fillCircle(cx, cy, dotSize, ST77XX_RED);
    }
  }
}

Tilt Detection:
digitalRead(TILT_PIN) checks if the ball switch is closed (i.e. tilted). A LOW means the device was shaken.

Debouncing Logic:
Ensures that only one roll is triggered per physical shake using wasTilted and millis() timing.

Random Roll:
random(1, 7) generates values from 1 to 6 inclusively.

Dice Display:
A perfect square is centered on the screen. The layout uses a 3×3 grid of red circles based on a binary layout.

UI Styling:
Basic 3D shadow lines and a fillRect + drawRect combo make the dice look crisp and modern.

Conclusion

This digital dice roller is a fantastic mini-project combining fun and function. It turns the ESP32 and a tilt sensor into a playful gadget that mimics real dice in both appearance and behavior. Whether you’re learning, teaching, or just exploring, this is a great step into microcontroller displays and sensors.

If you liked this project, follow more on circuit-diy.com for tutorials, projects, and practical circuits for makers like you!