I've been poking at writing a Discord bot in Golang for the last few weeks. It's actually weirdly easy to do with some of the libraries that are available out there.
I wanted to create something a bit more interesting in the long-term; something that lets you play certain kinds of dungeon exploring games. But I have been thinking to dip my toe into the bot-writing process by producing a bot that rolls dice. And here lies the rabbit hole that this post concerns.
The problem I see with current dice rolling bots is that they are very simple. They use regular expressions to match against the written roll expression, and that's fine, except it's limiting in what the roll can actually do.
For example, a bot might match a roll expression against this regular expression:
(\d+)d(\d+)(?:\+(\d+))? And that's fine, since it captures the number of dice to roll, the number of sides that the die has, and a bonus. For most purposes, this is great. But it does not capture what I want -- not nearly.
I have written some rules for the dice roller that I'd like to build. They follow:
1. Ability to express typical rolls via common syntax
It should be possile to write the rolls using well-understood dice-rolling syntax. Ever since I was a kid, the D&D rules specified a die roll using the syntax of
3d6, where the
3 is the number of dice to roll, which is followed by a
d and then a number that indicates how many sides the polyhedral die has, in this case, six. In the typical D&D set, there are dice with 4, 6, 8, 10, 12, 20, and 100 (usually via two 10-sided dice) sides. These would have d4, d6, d8, d10, d12, and d% as their indicators, respectively.
It should be possible to roll dice of an arbitrary number of sides, like
d13. I do not know what this might be for, but since I actually own dice with sides of this number, it seems more than reasonable.
2. Roll engine is able to perform basic math
d20 roll will roll one 20-sided die for the result, you usually want to add some value to that roll for the purpose of the game you're playing. For this reason, the roll engine should be able to perform basic math that also includes the dice results rolled.
In play, you might have a
+4 bonus for strength. To perform a feat of strength, you would roll
1d20 + 4 and compare it to a target number. This is why the roll engine needs math. But Why stop with basic addition?
In actuality, it should be possible to perform any basic math with familiar calculator symbols.
(2+3)*4+7^2 should be parseable and calculable by the rolling engine with a result of 69. It should also obey well-understood order of operations for infix expressions. That is, multiplication comes before addition.
Mix in some dice syntax, and it should be able to calculate based on the dice rolled, like
2d6+2 or even
3. Dice results rolled are captured in calculation
It is often useful to know what the individual dice results were in a calculation. This is one place where current dice bots excel, because they limit what you're allowed to enter as a valid expression to a very simple construction.
Nonetheless, it should be possible with the parser I'd like to build to output the individual random dice results so that a player can inspect them. If the system is asked for the result of
2d10+4, then the output should be able to show the values of the 10-sided dice that were rolled to produce the result, like
[3, 7] + 4 = 14
This is where you can start to see things getting complicated. For a very basic die roller, you'd simply compute the random numbers and output the sum. For a slightly more advanced roller that uses regular expressions, you could roll the individual values and display them with the sum. For the roller I have in mind, the dice result sets could be anywhere in an expression, and would be somewhat harder to include in the computation display. For example, a roll expression could be
2d4+3d6, and output from that should allow for a result of, for example,
[1, 4] + [3, 5, 6] = 17.
Dice as sets
I think an important concept in making this work will be to consider dice as sets of values from which multiple random options can be picked. At first, I thought of the
d in the dice roll syntax as an operator, but I think that's not entirely viable. Instead a
d6 really represents a set of values,
[1,2,3,4,5,6], and when you implicitly "multiply" that set by some number on the left (the order of multiplication is non-associative for sets like this), then it necessarily selects that number of random things from the set.
That said, if you were to write the dice expression
3 * [1, 2, 3, 4], then this would be equivalent to rolling
3d4. You could also abbreviate the dice expression to
3 [1, 2, 3, 4], using implicit multiplication. And if
d4 was simply a "macro" for
[1, 2, 3, 4], then you can see how you could expand dice syntax into this set syntax, which will be important for things later and for internal representation of a set of things to choose from.
I have also been toying with the notion of a syntax that allows a range, so that you can easily convert the
d notation into a viable set such as
[1..6] for a
d6. I think this could be useful.
4. Variables from a provided source are subbed in
Having to always supply the bonuses for certain rolls as numbers could be tedious. Keeping track of your stats is something that a computer should be useful for, after all. Therefore, it should be possible to provide a kind of key-value array of values to the rolling library that can be used to substitute into expressions.
I was thinking that the dice rolling syntax on the bot could be something like
,roll 3d6 or simpler,
,r 3d6. But to set a value into a list of values, you could do something like
,set str_bonus 3. This would set the value of an identifier named
str_bonus to 4. When you then
,r d20 + str_bonus, it would add that 4 to the
d20 result. It might look like
 + 4:str_bonus = 17
5. Variables can be whole expressions
Thinking of the variables as just individual numeric values is pretty limiting. What if those variables could hold whole expressions? Then you could define a specific, commonly-used roll as a variable. Define
,set str_bonus 4 as before, but then also define
,set str 1d20 + str_bonus. Then, when you
,roll str, the output would be something like
( + 4:str_bonus):str = 17.
Of course, this opens the possibly for endless recursion, which would need to be detected to avoid endless loops in computing a roll. It might be useful to generally limit the amount of depth that this could use, for performance reasons.
6. Dice can be non-numeric
Here's were things go totally bonkers. There are games that use non-numbered dice that would be fun to play (somehow?) over Discord. I will use the example of the game Elder Sign. Elder Sign has different kinds of dice that you roll to help investigators overcome certain mysteries. The dice do not have numbered sides. The sides of some of the dice include a magnifying glass, a boot footprint, and even an elder sign.
Thinking about the dice set syntax mentioned earlier, you can easily create these dice: `['footprint', 'footprint', 'magnifier', 'elder sign'] This die would have 1 footprint sides, and one of the other two kinds of sides. I can't recall the exact sides of the dice used in this game offhand, but you can see how you could define these dice with the proper frequency of sides using the set syntax.
If you assigned the set syntax to a variable, you could easily roll
2 * clue_die or similar to get your results back in a set, like
['footprint', 'elder sign'].
How would you then use the rest of the math capabilities with these dice? You wouldn't; it would generate an error. This is one of the issues I'm having right now, resolving the differences between string-sides and number-sides for dice, and how they behave when operators are applied to them. I believe that a specific ruleset could be written to have expectations be met, though.
7. Functions can apply to dice rolls before summation
One of the crazy things people have done (myself included) is extend the common rolling syntax to do things that we frequently do with dice but aren't as well defined within the commonly used syntax. For example, one way that is frequently used to create new D&D character stats is to roll
4d6 but then drop the lowest die before calculating the total for use in the stat.
In the past, I might have used a syntax like
4d6h3 meaning, "roll 4 6-sided dice then keep the highest 3". This is insanity. How can anyone remember this? And it doesn't really look very intelligible. I have a better idea: I want to apply functions to dice sets.
If I have a set of
[1,2,3] I should be able to apply a function to it, like
[1, 2 ,3].highest(2). This would return
[2, 3], since this function returns a set of the 2 highest values in the set it operates on. A
d6.highest(1) would always return
. A strength skill roll with advantage in D&D 5th edition could therefore be written as
2d20.highest(1) + str_bonus.
Another example is from Shadowrun, and other systems, that use "exploding" dice, where if you roll the maximum value on a single die, you re-roll it and add the new value. If the new value is also the maximum, then you do it again. This can result in a single die having a value that is significantly higher than its usual maximum. One might roll
4d6.explode(d6), meaning to "explode" any value in the set that is the maximum of a
d6 roll. Note that passing in the set of
d6 to the function is important since it will not otherwise know what die value to add to the roll to explode it.
Since these functions can return sets, they can chain:
Casting sets to integer
Now seems to be a good time to mention how sets might interact with different operators. I don't know the exact answer to this yet, as I meantioned earlier. I think there are some obvious places with implicit casts, though. Like, if a set is then added to a number, then the set values are all summed, for example
[1, 2, 3] + 2 should equal 8.
How all of the operators interact when sets are on either side is likely something that should be explicitly plotted out. As mentioned before, multiplying (whether explicitly with the
* symbol or implicitly with no operator) something by a set should not actually multiply or case sets to integers, but instead pick that number of random things from the set. This makes the
3d6 syntax work when the
d6 is converted into a set and then implicitly multiplied by the
3. But there may be accomodations made for the set being multiplied by a number when the left and right arguments of the operator are swapped. That is, while
3 * d6 may produce a set of three values from 1 to 6,
d6 * 3 might choose a value from 1 to 6 and multiply it by 3.
And who knows what happens when you multiply sets by each other. I won't think about division now, either...
8. Fudge and percentile dice are supported
I implied something like this previously, but I'd really like for Fudge and percentile dice to have support. This should be pretty easy, as long as macros of
d% are converted into
[-1, 0, 1] and
9. Success operators show dice and results
This feature should allow you to specify a target number within your roll, and have your dice compared to that target. When it does this the individual dice results should still be output.
,r d20+4 >= 17 would roll
d20+4 and if the value is greater than or equal to 17, then it would be a success. The output of such a roll might look like
+4 >= 17 = 13 >= 17 => failure
Note that using this with variable syntax might make what you're doing a lot more readable.
10. Multiple rolls can be combined with commas
When you're in combat in D&D, it's always faster just to roll your attack and damage dice all together. So why not do it in your Discord bot?
Here's your syntax:
,r 1d20 + 4, 2d8 With a rsult that looks like:
 + 4 = 17, [3,5] = 8
11. Comments at the end of the line can be produced with semicolons
Soemtimes it's nice to include what the heck you rolled the dice for along with the roll. Reviewing the above, you could alter the roll to read as
,r 1d20 + 4, 2d8 ; Smack the goblin with my mace
And when the roll is output, the comment would be included so that the results had some context.
Well, that's the whole thing as far as I've defined things. You can find an initial repo for the bot with just the start of implementation of this. The set parts are proving particularly complicated, but I think it's just a matter of time before a breakthrough with the lexer/parser results in what I want.