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:
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:
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:
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.
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.
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.
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.
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!
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.
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.
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!
This page (Raspberry Pi Pico Dice Programmed with CircuitPython) was last updated on May 31, 2024.