gaming_Dados_4_a_20_caras_trans.png
Polyhedral dice by SharkD on Wikipedia CCA-SA 3.0

The code in its entirety is shown at the end of this page. You should click on Download: Project Zip in order to get the fonts used as well as the code. 

Ensure your CLUE is plugged into your computer via a known good USB cable. The CLUE should show up as a flash drive called CIRCUITPY.

Open the zip file and copy all the files listed below to CIRCUITPY.

  • Anton-Regular-104.bdf
  • Helvetica-Bold-36.bdf
  • clue_dice_roller.py

Finally rename clue_dice_roller.py to code.py.

Note: The boot_out.txt file you may see on the CIRCUITPY drive is generated by CircuitPython and is not part of the needed files for this project.

Set up

As usual, we start with some setup: importing libraries, assigning constants and initializing variables

import time
from random import randint
import board
from adafruit_clue import clue
from adafruit_debouncer import Debouncer
import displayio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label


# input constraints
MAX_NUMBER_OF_DICE = 6
SIDES = [4, 6, 8, 10, 12, 20, 100]

# modes: selecting/result
SELECTING = 0
ROLL_RESULT = 1

# 0-relative, gets adjusted to 1-relative before display/use
number_of_dice = 0
side_selection = 0

There are two constants that you can play around with.

MAX_NUMBER_OF_DICE sets the largest number of dice you can roll at once. Since, as we'll see later, setting the number of dice is done my incrementing the number, eventually wrapping around back to 1, you don't want this to be too big since it gets laborious to set if the maximum is too big.

SIDES contains the different dice that can be rolled, and contains the standard polyhedral dice typically used. Sometimes you want a d3 or a coin flip (a d2). If so, you can add those to the start of the array. The rest of the code adjusts to the size and contents of this array.

SELECTING and ROLL_RESULT are the two modes that the code can be in and determine whether the dice to be rolled are being set or the result of the most recent roll is displayed. We'll see these in use later.

Finally, there are the two variables that contain the currently selected count and die.

Dealing with buttons

We have the classic situation of wanting to do something (advancing the count or die to use) when a button is pressed... not when it's released, and not while it's pressed. If you've been using switches much, you've run into bounce and know that a switch can give you several false presses while it settles into a stable pressed state. Using a debouncer avoids that by filtering out those false presses and providing a definitive the button was pressed indication. See the debouncer module guide for more information.

button_a = Debouncer(lambda: clue.button_a)
button_b = Debouncer(lambda: clue.button_b)

This creates a debouncer for each button. Note that we use a lambda that fetches the value of the button from the Clue object.  See the tutorial guide on functions for more information about lambda.

The display

The next step is to set up the displayio groups and labels. See this guide on using CircuitPython's displayio module.

select_font = bitmap_font.load_font('/Helvetica-Bold-36.bdf')
select_font.load_glyphs(b'0123456789XDd')
select_color = 0x0000FF

roll_font = bitmap_font.load_font('/Anton-Regular-104.bdf')
roll_font.load_glyphs(b'0123456789X')
roll_color = 0xFFFFFF

select_label = label.Label(select_font, x=0, y=25, text='XdXXX', color=select_color)
roll_label = label.Label(roll_font, x=0, y=150, text='XXX', color=roll_color)

group = displayio.Group()
group.append(select_label)
group.append(roll_label)

board.DISPLAY.show(group)

This project uses two fonts: one for the dice selection and one for the result of rolling. They are loaded, and the glyphs they will use are preloaded.

The labels are then created and the group structure is constructed. Finally, the group is placed on the display.

Roll the bones

The function roll does the actual roll of the dice.

def roll(count, sides):
    select_label.text = ''
    for i in range(15):
        roll_value = sum([randint(1, sides) for d in range(count + 1)])
        roll_label.text = str(roll_value)
        roll_label.x = 120 - (roll_label.bounding_box[2] // 2)
        duration = (i * 0.05) / 2
        clue.play_tone(2000, duration)
        time.sleep(duration)

The first thing done is to empty the selection label. This makes it clear that you can't change the dice settings while in roll mode.

To roughly simulate the tumbling of the dice as they settle on to a final value, roll generates a sequence of random values, displaying each one in turn and beeping as it does. There are a couple things of note.

First, the delay between generating and displaying each value in the sequence increases each time. There's a beep and a silent delay, each the same length. This gives the effect of the dice slowly tumbling and stopping.

Second, it doesn't simply generate a random number between 1 and count * sides. Instead it takes a more realistic approach of rolling count dice, each with with a possible value between 1 and sides, inclusive, which are then summed. A list comprehension is used to do this. It generates a list of count random values, one for each value in range(count + 1). This is then passed to the sum method.

count + 1 instead of count, because count is zero based to make the math easier when cycling through values.

The x coordinate of the label is adjusted each time to center the text.

Updating the selection values

As the user presses buttons to change the count and die, the display in select_label needs to be updated. The update_display function does that, taking the values to display as arguments.

def update_display(count, sides):
    select_label.text = '{0}d{1}'.format(count + 1, SIDES[sides])
    select_label.x = 120 - (select_label.bounding_box[2] // 2)
    roll_label.text = ''

There isn't much to this: format the text to be displayed, put it in the label, and recenter the label. It also clears the roll result display since it's no longer valid while the settings are being adjusted.

Preparing to run

Finally the initial mode is set and the display refreshed.

mode = SELECTING
update_display(number_of_dice, side_selection)

The main loop

The final code to consider is the main loop. Let's step through it.

while True:
    button_a.update()
    button_b.update()

    if mode == SELECTING:
        if button_a.rose:
            number_of_dice = ((number_of_dice + 1) % MAX_NUMBER_OF_DICE)
            update_display(number_of_dice, side_selection)
        elif button_b.rose:
            side_selection = (side_selection + 1) % len(SIDES)
            update_display(number_of_dice, side_selection)
        elif clue.shake(shake_threshold=25):
            mode = ROLL_RESULT
            if SIDES[side_selection] == 100:   # only roll one percentile
                number_of_dice = 0
                update_display(number_of_dice, side_selection)
            roll(number_of_dice, SIDES[side_selection])
    else:
        if button_a.rose or button_b.rose:   # back to dice selection
            mode = SELECTING
            update_display(number_of_dice, side_selection)
        elif clue.shake(shake_threshold=25):   # reroll
            roll(number_of_dice, SIDES[side_selection])

Since we are using debouncers, the very first thing that's done in the loop is the update of all them. This is what makes them work.

What happens next depends on what mode the system is in.

If the mode is SELECTING, a press of button A or B advance the number or type of die, respectively. A shake results in a few things happening. First the mode being changed to ROLL_RESULT. Then it checks to see if the selected die is a percentile die (a 100 sided die), and if so the count is set to one since you never roll more than one. Finally a roll is made using the roll function described above.

If the mode is ROLL_RESULT, a press of button A or B will switch back to the SELECTING mode. A shake in this mode will reroll the selected number and type of dice.

The Complete Code

Remember to download the zip so you have the fonts as well as the code.

"""
Dice roller for CLUE

Set the number of dice with button A (1-2-3-4-5-6)
and the type with button B (d4-d6-d8-d10-d12-d20-d100).
Roll by shaking.
Pressing either button returns to the dice selection mode.
"""

import time
from random import randint
import board
from adafruit_clue import clue
from adafruit_debouncer import Debouncer
import displayio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label


# input constraints
MAX_NUMBER_OF_DICE = 6
SIDES = [4, 6, 8, 10, 12, 20, 100]

# modes: selecting/result
SELECTING = 0
ROLL_RESULT = 1

# 0-relative, gets adjusted to 1-relative before display/use
number_of_dice = 0
side_selection = 0

button_a = Debouncer(lambda: clue.button_a)
button_b = Debouncer(lambda: clue.button_b)

# Set up display

select_font = bitmap_font.load_font('/Helvetica-Bold-36.bdf')
select_font.load_glyphs(b'0123456789XDd')
select_color = 0xDB4379

roll_font = bitmap_font.load_font('/Anton-Regular-104.bdf')
roll_font.load_glyphs(b'0123456789X')
roll_color = 0xFFFFFF

select_label = label.Label(select_font, x=0, y=25, text='XdXXX', color=select_color)
roll_label = label.Label(roll_font, x=0, y=150, text='XXX', color=roll_color)

group = displayio.Group()
group.append(select_label)
group.append(roll_label)

board.DISPLAY.show(group)

# Helper functions

def roll(count, sides):
    select_label.text = ''
    for i in range(15):
        roll_value = sum([randint(1, sides) for _ in range(count + 1)])
        roll_label.text = str(roll_value)
        roll_label.x = 120 - (roll_label.bounding_box[2] // 2)
        duration = (i * 0.05) / 2
        clue.play_tone(2000, duration)
        time.sleep(duration)


def update_display(count, sides):
    select_label.text = '{0}d{1}'.format(count + 1, SIDES[sides])
    select_label.x = 120 - (select_label.bounding_box[2] // 2)
    roll_label.text = ''


mode = SELECTING
update_display(number_of_dice, side_selection)

while True:
    button_a.update()
    button_b.update()

    if mode == SELECTING:
        if button_a.rose:
            number_of_dice = ((number_of_dice + 1) % MAX_NUMBER_OF_DICE)
            update_display(number_of_dice, side_selection)
        elif button_b.rose:
            side_selection = (side_selection + 1) % len(SIDES)
            update_display(number_of_dice, side_selection)
        elif clue.shake(shake_threshold=25):
            mode = ROLL_RESULT
            if SIDES[side_selection] == 100:   # only roll one percentile
                number_of_dice = 0
                update_display(number_of_dice, side_selection)
            roll(number_of_dice, SIDES[side_selection])
    else:
        if button_a.rose or button_b.rose:   # back to dice selection
            mode = SELECTING
            update_display(number_of_dice, side_selection)
        elif clue.shake(shake_threshold=25):   # reroll
            roll(number_of_dice, SIDES[side_selection])

This guide was first published on Mar 04, 2020. It was last updated on Mar 04, 2020.

This page (Code) was last updated on Mar 16, 2021.

Text editor powered by tinymce.