Maker.io main logo

CLUE Dice Roller

2023-11-28 | By Adafruit Industries

License: See Original Project Programmers

Courtesy of Adafruit

Guide by Dave Astels

Overview

How many times have you been sitting at the game table and ‎needing to roll some dice. But you forgot to bring yours and the dice ‎box is on the other side of the table. All you have within reach is a ‎CLUE (maybe in a snazzy case). If only there was a way to roll dice ‎with your CLUE.‎

gaming_ezgifcom-optimize2

There is. All you need is the code in this guide. Read on to see how ‎to set your CLUE up as a universal set of dice.‎

Parts

CircuitPython on CLUE

CircuitPython is a derivative of MicroPython designed to simplify ‎experimentation and education on low-cost microcontrollers. It ‎makes it easier than ever to get prototyping by requiring no upfront ‎desktop software downloads. Simply copy and edit files on ‎the CIRCUITPY flash drive to iterate.‎

The following instructions will show you how to install CircuitPython. ‎If you've already installed CircuitPython but are looking to update it ‎or reinstall it, the same steps work for that as well!‎

Set up CircuitPython Quick Start!‎

Follow this quick step-by-step for super-fast Python power :)‎

Download the latest version of CircuitPython for CLUE from ‎circuitpython.org

Click the link above to download the latest version of ‎CircuitPython for the CLUE.

Download and save it to your desktop (or wherever is handy).‎

version_2

Plug your CLUE into your computer using a known-good USB cable.‎

A lot of people end up using charge-only USB cables and it is very ‎frustrating! So, make sure you have a USB cable you know is good ‎for data sync.‎

Double-click the Reset button on the top (magenta arrow) on your ‎board, and you will see the NeoPixel RGB LED (green arrow) turn ‎green. If it turns red, check the USB cable, try another USB port, ‎etc. Note: The little red LED next to the USB connector will pulse red. ‎That's ok!‎

If double-clicking doesn't work the first time, try again. Sometimes it ‎can take a few tries to get the rhythm right!‎

plug_3

You will see a new disk drive appear called CLUEBOOT.‎

Drag the adafruit-circuitpython-clue-etc.uf2 file to CLUEBOOT.‎

drive_4

drive_5

The LED will flash. Then, the CLUEBOOT drive will disappear, and a ‎new disk drive called CIRCUITPY will appear.‎

If this is the first time, you're installing CircuitPython or you're doing ‎a completely fresh install after erasing the filesystem, you will have ‎two files - boot_out.txt, and code.py, and one folder - lib on ‎your CIRCUITPY drive.‎

If CircuitPython was already installed, the files present before ‎reloading CircuitPython should still be present on ‎your CIRCUITPY drive. Loading CircuitPython will not create new ‎files if there was already a CircuitPython filesystem present.‎

That's it, you're done! :)‎

files_6

Code

code_7

Polyhedral dice by SharkD on Wikipedia CCA-SA 3.0‎

The code in its entirety is shown at the end of this page along with ‎instructions for installing it.‎

Set up

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

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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 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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Installing Project Code

To use with CircuitPython, you need to first install a few libraries, into ‎the lib folder on your CIRCUITPY drive. Then you need to ‎update code.py with the example script.‎

Thankfully, we can do this in one go. In the example below, click ‎the Download Project Bundle button below to download the ‎necessary libraries and the code.py file in a zip file. Extract the ‎contents of the zip file, open the directory CLUE_Dice_Roller/ and ‎then click on the directory that matches the version of CircuitPython ‎you're using and copy the contents of that directory to ‎your CIRCUITPY drive.‎

Your CIRCUITPY drive should now look similar to the following ‎image:‎

circuitpy_8

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2020 Dave Astels for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
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])

View on GitHub

Roll with it

play_9

Portion of Medieval People at Play, The Walters Art Museum, Baltimore - Public Domain

There are a few ways you could extend this project.‎

This project has just used the A and B buttons for selecting dice. That ‎limits what we can do. However, the CLUE has touchpads as well. If ‎we used them, we could make a more complex interface. One ‎possibility would be to support selecting multiple groups of dice. E.g., ‎‎1d2 + 2d4. We might add a modifier to that to support things like ‎‎1d10 + 2d4 + 6.‎

Another possibility is to make the motion trigger more advanced. In ‎addition to a basic shake, maybe slamming the CLUE (gently) on the ‎table could automatically roll 1d20 (probably the most common roll) ‎regardless of what was selected.‎

To take it in a different direction, consider that the code doesn't try to ‎model the physics of a tumbling die at all. It blindly generates ‎random numbers. One step to improve this would be to ensure that ‎it won't generate the same number two times in a row. This would ‎make the visual effect better as well: each step in the roll would be ‎more likely to be a different number. This would be especially ‎noticeable when rolling a single die.‎

These are a few ways to expand on the code in this guide. If you ‎come up with an interesting extension, bring it to our weekly Show ‎and Tell and share it with the community.

TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum