Build a dice you shake that shows the roll on your phone

Shake the printed dice cube and the MPU-6050 inside feels it. When it stops, the ESP32 rolls one or two dice, the RGB LED flashes, and every phone at the table shows the result and the statistics.
  • Level:Intermediate
  • Duration: 1 h
  • Ages 10+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a dice you shake that shows the roll on your phone – EEMENTOR STEM™ lesson

Board games need dice, and dice love to roll off the table. This one stays in your hand. Shake the printed cube: the MPU-6050 inside feels the shaking, and as soon as you stop, the ESP32 rolls one or two dice. The RGB LED in the cube flashes, and a web page that everyone can open on their phone shows the dice faces, the last rolls and how often each number has come up. Is the dice fair? After a hundred rolls, you'll see.

What you'll learn

  • How an accelerometer notices shaking
  • How the ESP32 makes truly random numbers
  • How to keep statistics in an array
  • How to show dice faces on a web page that updates by itself

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • GY-521 MPU-6050 module (45-in-1 Sensor Kit)
  • RGB LED module (45-in-1 Sensor Kit)
  • 3 resistors, 220 Ω (ESP32 IoT Starter Kit)
  • 7 long female-to-male jumper wires
  • A 2.4 GHz Wi-Fi network
  • The printed parts from the 3D printing box below (dice cube), or the cardboard version described there
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Build a dice you shake that shows the roll on your phone
Breadboard view. Check each connection against the wiring table below.
From To
MPU-6050 VCC and GND 3V3 and GND
MPU-6050 SCL ESP32 GPIO 22
MPU-6050 SDA ESP32 GPIO 21
ESP32 GPIO 25 220 Ω resistor, then RGB module R
ESP32 GPIO 26 220 Ω resistor, then RGB module G
ESP32 GPIO 27 220 Ω resistor, then RGB module B
RGB module − GND

Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.

The sketch

Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).

#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const int MPU = 0x68;
const int RED_PIN = 25, GREEN_PIN = 26, BLUE_PIN = 27;
const float SHAKE = 0.5;            // more than half a g away from still: shaking

WebServer server(80);
int diceCount = 1;
int roll[2] = {1, 1};
int counts[13];                     // how often each total came up (index = total)
int rolls = 0;
String history = "";
bool shaking = false;
unsigned long lastShake = 0, lastRead = 0;

float readG() {
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU, 6);
  int16_t ax = Wire.read() << 8 | Wire.read();
  int16_t ay = Wire.read() << 8 | Wire.read();
  int16_t az = Wire.read() << 8 | Wire.read();
  float x = ax / 16384.0, y = ay / 16384.0, z = az / 16384.0;
  return sqrt(x * x + y * y + z * z);
}

void colour(int r, int g, int b) {
  analogWrite(RED_PIN, r);
  analogWrite(GREEN_PIN, g);
  analogWrite(BLUE_PIN, b);
}

void rollDice() {
  int total = 0;
  for (int i = 0; i < diceCount; i++) {
    roll[i] = esp_random() % 6 + 1;  // hardware random number, 1 to 6
    total += roll[i];
  }
  counts[total]++;
  rolls++;
  history = String(total) + (history.length() ? " " : "") + history;
  if (history.length() > 40) history = history.substring(0, history.lastIndexOf(' '));
  Serial.printf("Rolled %d\n", total);
  for (int i = 0; i < 3; i++) {      // flash: green for a six or more, blue otherwise
    if (total >= 6) colour(0, 255, 0);
    else colour(0, 0, 255);
    delay(120);
    colour(0, 0, 0);
    delay(80);
  }
}

void handleState() {
  String json = "{\"dice\":" + String(diceCount) + ",\"roll\":[" + String(roll[0]) + "," + String(roll[1]) +
                "],\"rolls\":" + String(rolls) + ",\"shaking\":" + (shaking ? "true" : "false") +
                ",\"history\":\"" + history + "\",\"counts\":[";
  for (int t = 0; t <= 12; t++) json += String(counts[t]) + (t < 12 ? "," : "");
  json += "]}";
  server.send(200, "application/json", json);
}

void handleSet() {
  if (server.hasArg("dice")) diceCount = constrain(server.arg("dice").toInt(), 1, 2);
  if (server.hasArg("reset")) {
    for (int t = 0; t <= 12; t++) counts[t] = 0;
    rolls = 0;
    history = "";
  }
  server.send(200, "text/plain", "ok");
}

const char PAGE[] = R"html(<!doctype html><meta name=viewport content='width=device-width'><title>Dice</title>
<style>body{font-family:sans-serif;max-width:480px;margin:20px auto;padding:0 16px;text-align:center}
#d{font-size:120px;line-height:1.1;color:#1B3A7B}#d.s{color:#bbb}button{padding:10px 14px;margin:4px;border:0;border-radius:10px;background:#eee}
.bars{display:flex;align-items:flex-end;gap:4px;height:120px;margin-top:10px}.bars div{flex:1;background:#DE544A;border-radius:4px 4px 0 0;position:relative}
.bars span{position:absolute;bottom:-20px;left:0;right:0;font-size:12px}</style>
<h1>Shake the dice</h1><div id=d>&#9856;</div><p id=t></p>
<p><button onclick="set('dice=1')">1 dice</button><button onclick="set('dice=2')">2 dice</button><button onclick="set('reset=1')">Reset</button></p>
<p>Last rolls: <b id=h></b></p><div class=bars id=b></div><p style='margin-top:28px' id=n></p>
<script>
const FACES = ['', '⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
function set(q) { fetch('/set?' + q).then(tick); }
async function tick() {
  try {
    const s = await (await fetch('/state')).json();
    const d = document.getElementById('d');
    d.textContent = s.dice == 1 ? FACES[s.roll[0]] : FACES[s.roll[0]] + FACES[s.roll[1]];
    d.className = s.shaking ? 's' : '';
    const total = s.dice == 1 ? s.roll[0] : s.roll[0] + s.roll[1];
    document.getElementById('t').textContent = s.shaking ? 'Shaking...' : (s.rolls ? 'Total: ' + total : 'Shake the cube to roll');
    document.getElementById('h').textContent = s.history || '-';
    const from = s.dice, to = s.dice * 6, max = Math.max(1, ...s.counts);
    let bars = '';
    for (let t = from; t <= to; t++) bars += '<div style="height:' + (s.counts[t] / max * 100) + '%"><span>' + t + '</span></div>';
    document.getElementById('b').innerHTML = bars;
    document.getElementById('n').textContent = s.rolls + ' rolls';
  } catch (e) {}
}
tick(); setInterval(tick, 400);
</script>)html";

void setup() {
  Serial.begin(115200);
  pinMode(RED_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(BLUE_PIN, OUTPUT);
  Wire.begin(21, 22);
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);                 // wake the MPU-6050 up
  Wire.write(0);
  Wire.endTransmission();

  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.print(" connected. Open http://");
  Serial.println(WiFi.localIP());
  server.on("/", []() { server.send(200, "text/html", PAGE); });
  server.on("/state", handleState);
  server.on("/set", handleSet);
  server.begin();
}

void loop() {
  server.handleClient();
  if (millis() - lastRead < 20) return;
  lastRead = millis();
  float g = readG();
  if (fabs(g - 1.0) > SHAKE) {      // being shaken
    shaking = true;
    lastShake = millis();
    colour(random(256), random(256), random(256));   // party colours while shaking
  } else if (shaking && millis() - lastShake > 400) { // still for 0.4 s: roll!
    shaking = false;
    rollDice();
  }
}

Play

Open the Serial Monitor at 115200 baud: after connecting, the ESP32 prints its address, such as http://192.168.1.42. Open it in a browser on a phone or computer in the same Wi-Fi.

  1. Shake the cube for a second and hold it still. The LED flashes and the dice face on the page changes.
  2. Tap 2 dice for games that need two. The bar chart shows how often each total came up.
  3. Roll 100 times with one dice. Each number should come up about 17 times. Then try two dice: why does 7 come up most often?

How it works

Lying still, the accelerometer only feels gravity: about 1 g. Shaking adds strong pushes in all directions, so the total moves far away from 1 g. More than half a g away counts as shaking. When the readings have been calm for 0.4 seconds, the shaking is over and the dice are rolled.

The roll itself doesn't come from the shaking. esp_random() uses radio noise inside the ESP32 to make truly random numbers, and % 6 + 1 turns a huge random number into 1 to 6. The shaking is only the signal, like throwing a real dice.

Every total is counted in the counts array, where the index is the total itself. With two dice there are six ways to make 7 (1+6, 2+5, 3+4 and the other way round) but only one way to make 2 or 12, which is why the bars build a hill. That is probability in action.

The page asks /state every 0.4 s, so every phone at the table updates by itself.

Try this next

  • Add a 20-sided dice for role-playing games.
  • Show each player's name and take turns: the page says whose roll it is.
  • Make the LED show the number by blinking that many times.

Troubleshooting

  • It never rolls: check SDA on GPIO 21 and SCL on GPIO 22. Shake harder, or make SHAKE smaller.
  • It rolls when you just pick it up: make SHAKE bigger, for example 0.8.
  • The dice faces show as squares: your phone's font has no dice signs. The total below them still works.
  • It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
  • A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit