Make a DND Discord Dice Roller in Java (with regular expressions!)

Discord is a popular online service for gaming that really took off during the 2020’s pandemic. My friends and I play Dungeons and Dragons and we wanted a way to roll dice online without giving all our data to some wierdo company. So, being that I’m good with code, I wrote my own. Here’s my steps so that you can learn from what I did. We’re also going to use a really powerful tool called regular expressions to make reading dice requests super easy.

On the shoulders of giants

Writing my own code from scratch and figuring things out is fun and educational, but I’m getting old and I don’t have a lot of time left. To save time I used Java, available libraries, and a slightly out of date tutorial by Oliy Barrett. I recommend you read this for the basics.

Hello, World!

When using someone else’s library there’s always a bit of setup and teardown. Here’s the essentials.

token.txt is the private token for this bot. Never share it. If you check it into Github then Discord will send you a friendly email telling you the token has been rejected forever and you have to make a new one. use your .gitignore to make sure that never happens.

public class DiscordDND extends ListenerAdapter {
    static final String MY_ENTITY_ID = "***";
    static final String MY_ENTITY_NAME = "Dice Roller";
    static public final String ROLL_COMMAND = "~r";

    public static void main( String[] args ) throws LoginException {
        String token = readAllBytesFromFile(DiscordDND.class.getResource("token.txt"));
        JDA jda = JDABuilder.createDefault(token).build();
        jda.addEventListener(new DiscordDND());
    }

    private static String readAllBytesFromFile(URL filePath) {
        String content = "";
        try {
            System.out.println("Token search: "+filePath.toURI());
            content = new String ( Files.readAllBytes( Paths.get( filePath.toURI() ) ) );
        }  catch (Exception e) {
            e.printStackTrace();
	}
        return content;
    }

    @Override
    public void onMessageReceived(MessageReceivedEvent event) {
    	if(event.getAuthor().isBot()) return;

    	String message = event.getMessage().getContentDisplay();
    	
    	if(!message.startsWith(ROLL_COMMAND)) return;
    	// remove the prefix and continue
    	message = message.substring(ROLL_COMMAND.length());

        System.out.println("I heard: "+message);

        // handle the roll here
    }
}

When the app runs it loads the token, connects to Discord with the token, and gets ready to listen to things being said in the discord servers to which it has been invited. How do you invite a bot to a server? Uh… I don’t remember.

Understanding a roll request

The regular expression syntax for a dnd dice roll

The normal format for writing out a dice roll in DND is [number of dice]d(number of sides)[k(number to keep)][+/-(modifier)] with no spaces. I found a regular expression online that pretty closely matches this pattern and modified it.

  • Anything in a () is a group.
  • ? means “zero or one of the previous element” (in this case, the previous group).
  • [\+\-] means “any characters inside the [] braces”. Combined with the ? it means “at most one + or – symbol.” Because of text formatting rules in regular expressions the + and - have to escaped by putting a backslash \ in front of them.
  • \d means digit. \d+ means 1 or more digits.

So putting it all together it says:

  • The first group has a positive or negative whole number. The group is optional.
  • The second group starts with the letter ‘d’, then a positive or negative whole number. The group is required.
  • The third group starts with the letter ‘k’ and then a positive or negative whole number. The group is optional.
  • The fourth group MUST start with +/- and then a whole number. The group is optional.

Hey! Why negative dice and negative sides? It’s not that I want negative dice. But sometimes users type silly things. I thought it would be fun to catch those and deal with them in equally funny ways.

Why keep negative dice? In DND sometimes the player rolls with advantage to keep the highest dice and sometimes they roll with disadvantage to keep the lowest dice. Negative numbers mean keep the low dice.

To use regular expressions in Java (or OpenJDK) I’m using the Pattern and Matcher classes.

// remove all whitespace and the roll command from the start
String saneMessage = sanitizeMessage(event.message);

Pattern p = Pattern.compile("([\\+\\-]?\\d+)?(d[\\+\\-]?\\d+)(k[\\+\\-]?\\d+)?([\\+\\-]\\d+)?");
Matcher m = p.matcher(saneMessage);
id(m.find()) {
	int numDice=1, numSides=20, numKeep, modifier=0;
	if(m.group(1) !=null && !m.group(1).isEmpty()) numDice  = Integer.parseInt(m.group(1));
	if(m.group(2) !=null && !m.group(2).isEmpty()) numSides = Integer.parseInt(m.group(2).substring(1));
	if(m.group(3) !=null && !m.group(3).isEmpty()) numKeep  = Integer.parseInt(m.group(3).substring(1));
	else numKeep=numDice;
	if(m.group(4) !=null && !m.group(4).isEmpty()) modifier = Integer.parseInt(m.group(4));

	roll(event,numDice,numSides,numKeep,modifier);
	return;
}

In Java Strings the \ symbol is special so I have to escape them again – the double backslash is not a mistake.

Matcher returns the original expression in m.group(0). If Matcher does not find a group it returns null for that group index. That means I can reliably expect group 4 is always the modifier and so on.

Rolling and keeping

int [] rolls = rollDice(numDice,numSides);
if(numKeep!=numDice) {
	if(numKeep>0) keepSomeHighRolls(rolls,numKeep);
	else keepSomeLowRolls(rolls,-numKeep);
}
event.reply(event.actorName + ": "+renderResults(rolls,modifier));

The catch here is that I don’t want to sort the rolls into high > low because it would look wrong to the user. Organic rolls do not happen that way! I’m stuck searching for the worst roll, numRolls – numKeep times.

for(int k = numKeep;k<rolls.length;++k) {
	int worst = 0;
	for(int i=0;i<rolls.length;++i) {
		if(rolls[i]>0 && rolls[worst]>rolls[i]) worst = i;
	}
	rolls[worst] *= -1;  // mark it as rejected but keep the value.
}

Final thoughts

As a final bit of flair I add a meme pic for natural 20 and natural 1 rolls. If a natural 1 is not kept I put in Neo’s famous bullet dodge from The Matrix. What would be a good meme for a not-kept-natural-20?

All the code for this project can be found at https://github.com/i-make-robots/DiscordDND/. It’s got way more stuff!