Add more fun to your board games by creating an electronic die!
My grandkids love to play Monopoly, but sometimes they get a little carried away rolling the die. More than once the die goes skittering across the floor, sometimes never to be found. Since I've been learning to code with CircuitPython on a Raspberry Pi Pico, along with the magic of 3D printing, I thought building an electronic die for the game would be a great project to put all the skills together.
The design concept was a 3D printed cube with 9 holes to slot in the LEDs, a tilt switch to trigger a roll, a piezo element to add a bit of sound, all run on a Raspberry Pi Pico powered by 3 AAA batteries. Using random function to generate a random integer between 1 and 6 would nicely emulate the single die used in the game. I also added a couple of specials - read on for more on those!
Considering the size of the components that would need to fit inside the cube I decided to make a 70x70mm cube 85mm tall. Since the top face would be about 50x50mm, using 10mm jumbo LEDs made the most sense.
Parts
Here are the electronic components used:
![Angle shot of Raspberry Pi Pico RP2040](https://cdn-shop.adafruit.com/640x480/4864-00.jpg)
![scattered pile of multi colored unlit LEDs](https://cdn-shop.adafruit.com/640x480/4204-00.jpg)
![Metal cylinder with two wires sticking out](https://cdn-shop.adafruit.com/640x480/173-02.jpg)
![Large Enclosed Piezo Element with two Wires](https://cdn-shop.adafruit.com/640x480/1739-00.jpg)
Connecting the Electronics
The cathodes of the 9 LEDs are tied to a common ground connected to pin 3 of the Pico. The anodes of the LEDs are connected to GPIOs 0-8 (pins 1, 2, 4, 5, 6, 7, 9, 10, 11) through 330 ohm current limiting resistors.
The LED pips are wired and arranged:
![pips.png](https://cdn-learn.adafruit.com/user_assets/assets/000/000/904/large1024/pips.png?1717129007)
The piezo element is connected to GP13 (pin 17) and a ground pin. The tilt switch is connected to GP26 (pin 31) and the other side to a ground pin. Positive side of the battery is connected through one side of the slide switch to VBUS (pin 40) with the other side to ground (I know, I know there should be a Schottky diode in there).
I made two other connections for the special features I planned to add: I used the second slide switch to connect GP9 (pin 12) to a ground pin when the switch is on. I then used the other side of the power slide switch to connect GP15 (pin 20) to a ground pin when power is one. More on these later.
3D Case Design
I used TinkerCad (https://www.tinkercad.com) for my 3D design work. The original thought was to make two halves - a top and bottom that overlap/slide together. but I ran into some difficulties with this design. Ultimately, I broke it down into 3 parts - an outer 70x70x85mm cube with a 50mm square "hole" through the middle. Then I made two 50mm cubes for the top and bottom that mate open end to open end and slide into center of the cube. The heights of each are one half the depth of the cube so when assembled the closed ends are flush with the top and bottom of the cube. The top has the 9 holes which just fit the 10mm LEDs. The bottom had places to support the Pico and battery back with cut-outs for the slide switches and the USB connector on the Pico.
Here are what the three parts look like in TinkerCad:
![Dice_TC.png](https://cdn-learn.adafruit.com/user_assets/assets/000/000/901/large1024/Dice_TC.png?1717118243)
Some design notes:
- The top of the cube (yellow, top facing down) has a bevel. This matches the:
- LED top section (pink) bevel - this keeps the top from sliding through the outer cube.
- The bottom layer of the bottom section (tan) is about 4mm thick. The switches are about 4mm tall, so they would be flush with the bottom, except in order to make one switch more prominent (the power switch) I carved out about 1mm so that switch protrudes about 1mm below the bottom.
- There is a box just large enough to squeeze in the battery holder keeping it firmly in the place in the assembled cube.
- On the outside of the battery holder box is a shallow half-box which supports the Pico vertically, with the USB facing downward. The box is about 5mm wider than the Pico itself so there is room for the wiring to extend bast the edge of the board connected to the GPIO pins. The wired Pico is hot-glued to the support plate to keep it firmly in place when connecting a USB cable.
- There is also 4mm groove in the base to allow the Pico's USB port to sit only 1mm above the bottom so a cable can be plugged in the assembled cube for updating the code without having to disassemble the cube.
![IMG_7903.jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/910/large1024/IMG_7903.jpg?1717190412)
Assembly
First, since the tilt switch is best positioned on the LED side of the perf board (so it is upright/closed when the cube is sitting upright), I put the leads through approximately where it will be between P1, P2, P6 and P8. This is important to know the minimum distance needed between the perf board and the bottom of the LEDs.
The challenge is getting the LEDs arranged with the leads through the perf board in the right places. I found the best way to do this was to invert the top shell section on the workbench and insert the LEDs backwards with the leads facing upward. Then center the perf board and guide the LED leads through the best fitting holes. I actually did this one LED at a time starting in the center and tacking each LED cathode lead to the perf board. This helps with the horizontal spacing but also watch the vertical spacing so the perf board is level across all of the LEDs. I actually 3D printed a jig (3x3 square grid the depth I wanted the LEDs at) which helped a lot.
![AlignLED.jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/905/large1024/AlignLED.jpg?1717189072)
Once the perf board is in place, connect all of the cathodes and one side of the tilt switch to a common ground pin and solder a 330 ohm resistor to each anode. I used 30ga silicone hook-up wire (the silicone is much easier to work with due to its flexibility) to connect the resistors to the appropriate GPIO pins as well as the tilt switch and the perf board common ground. Once the perf board is all wired, a couple of dabs of hot glue around the edge holds the board in place in the top section, Keep in mind that kids will be poking at the LEDs so this board needs to be anchored pretty firmly.
![IMG_7894.jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/906/large1024/IMG_7894.jpg?1717189350)
The rest of the connections are in the base cube. The battery holder positive goes to one side of the slide switch and the negative goes to a ground pin. The other connection on the power side of the switch got to VBUS (pin 40). On the other side of the power switch run one connection to GP15 (pin 20) and the other side to ground so that when the power switch is in the on position GP15 is connected to ground.
![IMG_7893.jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/907/large1024/IMG_7893.jpg?1717189979)
For the other slide switch connect one lead to ground and the other lead to GP9 (pin12) so that when this side is on GP9 is grounded. The slide switches are held on to the base with 2mm screws. I used a 2mm tap to make threads however it also works just to force the screws in and they will easily cut their own threads in the PLA.
Finally connect the piezo element to ground and GP13 (pin 17).
That's all the connections!
![IMG_7897.jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/908/large1024/IMG_7897.jpg?1717190038)
Stack the two inner cubes and then slide them into the outer cube. Apply a few dabs of hot glue to the corners on the bottom to keep the inner cubes from sliding out of the outer cube.
![IMG_7902_(1).jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/909/large1024/IMG_7902_%281%29.jpg?1717190302)
Special Features
Sometimes when playing the game the kids get a little bored or they feel cheated when rolling a low number. Since we can make our own rules, I thought I'd add a bit of fun by randomly adding some special rolls. This is the purpose of the second switch. Set the switch to normal and you get a roll of 1 to 6. When the special switch is set, the random number generator chooses a number between 1 and 7. With a little magic in the code, if a 7 comes up, a second random number is generated, and if that is greater than a threshold (in the code it is 1 in 4 chance), the roll can be a 7, 8, or 9! Extra pips light and a little ditty plays to announce they got something special. At the other end of the spectrum, if the player rolls a 1 or a 2, there is a 1 in 20 chance the cube will play Happy Birthday and the player can roll again.
The other special feature I added was not so much in game play but a data logger to track the rolls over time, so that we can check how random the rolls are (interestingly so far that data seems to support a pretty even distribution). Collection of the roll results is accomplished using the data logger approach saving each roll in a text file logger.txt. The technique used comes from this Learn Guide:
In the boot.py file you specify GP15 (pin 20) as the write-enable pin. Using the power switch to make this connection only when it is powered on from the battery means that when the power is off (and you connect to your PC through the bottom with a USB cable) the write protect will be disabled and you will be able to read the text file and alter the code on your board.
Since the battery is connected directly to VBUS, only connect your cube with a USB cable when the power switch is in the off position. If connected when the power switch is on you will be connecting the internal AAA batteries to 5v power and bad things will probably happen! Plus, when the power switch is in the on position you cannot read or write to the Pico anyway.
Coding
At this point you are ready to add the code. We will create three files:
- boot.py - to enable/disable data logging
- code.py - the main program code
- note_freq.py - placed in the lib folder, this file defines the frequency of the notes and durations. Although this can be added to code.py, I thought this would be a good use to break out in a separate file and import at the top of the program to neaten things up a bit.
First let's start with boot.py:
# This file uses GP15 to write protect the board to allow data logging import board import digitalio import storage write_pin = digitalio.DigitalInOut(board.GP15) write_pin.direction = digitalio.Direction.INPUT write_pin.pull = digitalio.Pull.UP if not write_pin.value: storage.remount("/", readonly=False)
Next let's add the library file note_freq.py for note values. This is saved in the lib folder:
# Notes class assigns frequencies for musical notes used to produce tones by the piezo element. # Dur class sets the duration of the notes class Notes: B0 = 31 C1 = 33 CS1 = 35 D1 = 37 DS1 = 39 E1 = 41 F1 = 44 FS1 = 46 G1 = 49 GS1 = 52 A1 = 55 AS1 = 58 B1 = 62 C2 = 65 CS2 = 69 D2 = 73 DS2 = 78 E2 = 82 F2 = 87 FS2 = 93 G2 = 98 GS2 = 104 A2 = 110 AS2 = 117 B2 = 123 C3 = 131 CS3 = 139 D3 = 147 DS3 = 156 E3 = 165 F3 = 175 FS3 = 185 G3 = 196 GS3 = 208 A3 = 220 AS3 = 233 B3 = 247 C4 = 262 CS4 = 277 D4 = 294 DS4 = 311 E4 = 330 F4 = 349 FS4 = 370 G4 = 392 GS4 = 415 A4 = 440 AS4 = 466 B4 = 494 C5 = 523 CS5 = 554 D5 = 587 DS5 = 622 E5 = 659 F5 = 698 FS5 = 740 G5 = 784 GS5 = 831 A5 = 880 AS5 = 932 B5 = 988 C6 = 1047 CS6 = 1109 D6 = 1175 DS6 = 1245 E6 = 1319 F6 = 1397 FS6 = 1480 G6 = 1568 GS6 = 1661 A6 = 1760 AS6 = 1865 B6 = 1976 C7 = 2093 CS7 = 2217 D7 = 2349 DS7 = 2489 E7 = 2637 F7 = 2794 FS7 = 2960 G7 = 3136 GS7 = 3322 A7 = 3520 AS7 = 3729 B7 = 3951 C8 = 4186 CS8 = 4435 D8 = 4699 DS8 = 4978 class Dur: e = .1 # eighth note q = .2 # quarter note h = .4 # half note w = .8 # whole note
Finally, the main code in code.py:
import time import random import digitalio import board import pwmio from note_freq import Notes from note_freq import Dur buzzer1 = pwmio.PWMOut(board.GP13, variable_frequency=True) # set the piezo up on GP13 OFF = 0 # pwm duty cycle when piezo is silent ON = 2 ** 15 # pwm duty cycle when piezo is sounding - 50% ph = 0.003 # short sleep time used in phaser sound effect raf = 20 # roll again factor - if 1 or 2 comes up, how ofter will the player be chosen to roll again. Factor of 20 is a 1 in 20 chance # Set up the LEDs for the pips p1 = digitalio.DigitalInOut(board.GP0) p1.direction = digitalio.Direction.OUTPUT p2 = digitalio.DigitalInOut(board.GP1) p2.direction = digitalio.Direction.OUTPUT p3 = digitalio.DigitalInOut(board.GP3) p3.direction = digitalio.Direction.OUTPUT p4 = digitalio.DigitalInOut(board.GP2) p4.direction = digitalio.Direction.OUTPUT p5 = digitalio.DigitalInOut(board.GP4) p5.direction = digitalio.Direction.OUTPUT p6 = digitalio.DigitalInOut(board.GP5) p6.direction = digitalio.Direction.OUTPUT p7 = digitalio.DigitalInOut(board.GP6) p7.direction = digitalio.Direction.OUTPUT p8 = digitalio.DigitalInOut(board.GP7) p8.direction = digitalio.Direction.OUTPUT p9 = digitalio.DigitalInOut(board.GP8) p9.direction = digitalio.Direction.OUTPUT # set up the tilt switch roll = digitalio.DigitalInOut(board.GP26) roll.direction = digitalio.Direction.INPUT roll.pull = digitalio.Pull.UP # set up the special switch - when low special fetures are enabled special = digitalio.DigitalInOut(board.GP9) special.direction = digitalio.Direction.INPUT special.pull = digitalio.Pull.UP # define functions def bn(): # stops the piezo sounding buzzer1.duty_cycle = OFF time.sleep(0.05) def hbd(d): # Happy Birthday melody when a player rolls a 1 or 2 and is selected to roll again x = 0 while x < d: buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.A5 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.B5 buzzer1.duty_cycle = ON time.sleep(Dur.w) bn() bn() buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.A5 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.D6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(Dur.w) bn() bn() buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.G6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.E6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.B5 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.A5 buzzer1.duty_cycle = ON time.sleep(Dur.w) bn() bn() buzzer1.frequency = Notes.F6 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.F6 buzzer1.duty_cycle = ON time.sleep(Dur.q) bn() buzzer1.frequency = Notes.E6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.D6 buzzer1.duty_cycle = ON time.sleep(Dur.h) bn() buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(Dur.w) bn() x = x + 1 buzzer1.duty_cycle = OFF time.sleep(0.25) def gliss(g, d): # glissando sound effect x = 0 while x < d: buzzer1.frequency = Notes.C4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.D4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.E4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.F4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.G4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.A4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.B4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.C5 buzzer1.duty_cycle = ON time.sleep(g * 4) x = x + 1 buzzer1.duty_cycle = OFF time.sleep(g * 2) def mjr(g, d): # major chord sound effect x = 0 while x < d: buzzer1.frequency = Notes.C5 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.E5 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.G5 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.C5 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(g * 4) x = x + 1 buzzer1.duty_cycle = OFF time.sleep(g * 2) def mnr(g, d): # minor chord sound effect x = 0 while x < d: buzzer1.frequency = Notes.AS5 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.CS5 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.A4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.FS4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.GS4 buzzer1.duty_cycle = ON time.sleep(g) buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(g * 1) x = x + 1 buzzer1.duty_cycle = OFF time.sleep(g * 2) def phaser(r): # phaser sound effect x = 0 while x < r: buzzer1.frequency = Notes.C5 buzzer1.duty_cycle = ON time.sleep(ph) buzzer1.frequency = Notes.C6 buzzer1.duty_cycle = ON time.sleep(ph) buzzer1.frequency - Notes.C7 buzzer1.duty_cycle = ON time.sleep(ph) x = x + 1 def rollthedice(): # main function when the dice is rolled (tilt switch opens) if special.value == True: maxy = 6 # only allow roll of 1 - 6 when special switch is off else: maxy = 7 x = random.randint(1, maxy) if x == 1: flashy(1, 0.05) # flashy function flashes all of the LEDs before showing roll result r1() if maxy == 7: # check to see if the player has won a roll again ra = random.randint(1, raf) if ra == raf: hbd(1) try: # data logging of resulting roll with open("/logger.txt", "a") as datalog: datalog.write("1 \n") datalog.flush() except OSError as e: print("is error") elif x == 2: flashy(1, 0.05) r2() if maxy == 7: ra = random.randint(1, raf) if ra == raf: hbd(1) try: with open("/logger.txt", "a") as datalog: datalog.write("2 \n") datalog.flush() except OSError as e: print("is error") elif x == 3: flashy(1, 0.05) r3() try: with open("/logger.txt", "a") as datalog: datalog.write("3 \n") datalog.flush() except OSError as e: print("is error") elif x == 4: flashy(1, 0.05) r4() try: with open("/logger.txt", "a") as datalog: datalog.write("4 \n") datalog.flush() except OSError as e: print("is error") elif x == 5: flashy(1, 0.05) r5() try: with open("/logger.txt", "a") as datalog: datalog.write("5 \n") datalog.flush() except OSError as e: print("is error") elif x == 6: flashy(1, 0.05) r6() try: with open("/logger.txt", "a") as datalog: datalog.write("6 \n") datalog.flush() except OSError as e: print("is error") else: # roll is 7 therefore check to see if the player gets something greater than 6 ctseven = random.randint(1, 20) if ctseven > 15: # if random number is between 16 to 20 - 1 in 4 chance - player rolls something greater than 6 flashy(1, 0.03) adr = random.randint(0, 2) # determine if player gets a 7 8 or 9 if adr == 0: r7() gliss(.03, 3) bn() try: with open("/logger.txt", "a") as datalog: datalog.write("7 \n") datalog.flush() except OSError as e: print("is error") elif adr == 1: r8() mjr(0.10, 3) bn() try: with open("/logger.txt", "a") as datalog: datalog.write("8 \n") datalog.flush() except OSError as e: print("is error") else: r9() mnr(0.10, 3) bn() try: with open("/logger.txt", "a") as datalog: datalog.write("9 \n") datalog.flush() except OSError as e: print("is error") else: # if player not entitled to a 7 8 or 9 - 3 out of 4 chance - just roll again rollthedice() def clearall(): # turns off all of the pips p1.value = False p2.value = False p3.value = False p4.value = False p5.value = False p6.value = False p7.value = False p8.value = False p9.value = False # Functions defining each roll outcome - which pips to light up def r1(): p1.value = True def r2(): p2.value = True p3.value = True def r3(): p1.value = True p2.value = True p3.value = True def r4(): p2.value = True p4.value = True p5.value = True p3.value = True def r5(): p1.value = True p2.value = True p4.value = True p5.value = True p3.value = True def r6(): p2.value = True p4.value = True p5.value = True p3.value = True p7.value = True p6.value = True def r7(): p2.value = True p4.value = True p5.value = True p3.value = True p7.value = True p6.value = True p1.value = True def r8(): p2.value = True p3.value = True p4.value = True p5.value = True p6.value = True p7.value = True p8.value = True p9.value = True def r9(): p1.value = True p2.value = True p3.value = True p4.value = True p5.value = True p6.value = True p7.value = True p8.value = True p9.value = True def flashy(c, t): # pip flash sequence before roll is revealed z = 0 while z < c: r1() time.sleep(t) clearall() r2() time.sleep(t) clearall() r3() time.sleep(t) clearall() r4() time.sleep(t) clearall() r5() time.sleep(t) clearall() r6() time.sleep(t) clearall() z = z + 1 while True: # main loop waiting for a roll to begin if roll.value == True: clearall() flashy(2, 0.05) rollthedice() time.sleep(1) else: time.sleep(0.3)
I learned a huge amount developing the project. Use this a s jumping off point for you to make it your own!
For example, the first time we played with it my granddaughter pretty quickly figured out how to game the system - she tilted the die (it keeps rolling as long as the tilt switch is open) and righted it when it came up to what she wanted. Therefore, I went back and modified the code to blank out between rolls and it will only reveal and stay lit once the cube is sitting upright, I also added a short beep to signal the roll is complete.
I also have ideas for the next version - we can add a second and a third piezo so the melodies can be polyphonic. I'm also thinking of adding an accelerometer to check the tilt level rather than just using a tilt switch or adding a way to select how many pips to show (so it can be used in another game that has a 1-3 spinner). Add functionality to work with other games like Hot Potato:
The possibilities are endless!