In addition to the many other geekdom hobbies I practice, I am a big fan of tabletop RPGs. I got into it about four years ago with the free version of the D&D 3.5 rules, moved from that to Pathfinder, and just recently got the D&D 5e core rule books this last Christmas.
I love the math and the systems behind the gameplay, which is probably one of the reasons that I end up being the dungeon master for most groups I play with. The combination of the love of systems with the creativity of group storytelling is something I can’t get enough of.
But to be completely honest, keeping track of a whole world takes a lot of time and creativity, a burden that’s hard to balance with responsibilities and good time management. My solution, build tools to make the whole process easier and faster.
But before we do anything like that, we have to build a very simple foundation that everything else will build from.
The Problem
Almost all tabletop RPG systems are built on psuedo-random number generators, represented by dice of different sizes and sides. Probability is stacked for and against the players with groups of these dice, and any tools that automate any portions of these games are going to need a good way to simulate dice rolls.
We can break this down to four core requirements:
- We need an underlying random number generator that accepts a random min and max.
- We need a dice notation interpreter to better facilitate translating rolls into something our system can simulate.
- We need a process for adding simple mathematics into the rolls.
- We need the ability to combine multiple dice and mathematical processes into large equations.
None of these are overly complex, but they do build on each other so that the last requirement is built on the systems we will write to satisfy the requirement before it.
The Solution & Method
We’re combining these headings for this post because we’re solving four problems, and we should discuss the solution and the method for one before moving onto the next.
First up, we need our random number generator. As anyone who has used real dice can tell you, they’re psuedo-random generators, at best. They tend to favor certain rolls based on the way the physical world interacts with them, so we can safely build on the psuedo-random generator built into the Math library. We’re not doing cryptography, so we don’t need truly random bytes.
With that in mind, the method is going to be very simple. In all the following code examples, we are attaching them to the app object, as that’s where you will see this code used in the DM Tools in the experiments section of this website.
app.rand = function (min, max){ return Math.floor(Math.random() * (max - min + 1)) + min; }
Here our base level generator function accepts two parameters, the minimum possible, and the maximum possible. JavaScript’s Math.random() returns a random value from 0 – 1, by multiplying that value by a number, we can get random numbers larger than 1. The number we multiply that number by becomes the new maximum number we can return. By adding one to our minimum, subtracting it from the maximum, and adding the minimum back to the number after we have our number generated, we can get a minimum larger than zero. The only other thing we do is Math.floor() the result of Math.random() * our modified maximum so that we can be sure we are working with an integer.
With that simple function, we can technically simulate any dice roll we can think of by simply plugging in our min and max values.
But next, we need to translate traditional dice notation to something that our random number generator can work with, a min and a max value. Dice are usually pretty easy. Dice in a standard RPG system tend to include the following variations (represented here in dice notation): d4, d6, d8, d10, d12, d20, d100. In each case, the minimum possible is 1, and the max is the number of sides.
But we’re going to throw a wrinkle into this. We can roll more than one of these dice at a time, and that will create different minimums and maximums based on the number we roll. Additionally, if we every want to show an itemized list of the dice we roll, we’ll want to keep all this separated out, even if what we’re really interested in is a single integer total.
app.roll = function (sides, number){ var output = []; if(number == undefined){ number = 1; } for(i = 0; i < number; i++){ output.push(app.rand(1, sides)); } return output; }
The function above is relatively simple as well. It accepts two parameters, like our first one, the number of sides, and the number of dice we want to roll. It creates an array of roll results and for each die we are rolling it pushes the result to that array, which it returns. This will give us the itemized results we were looking for.
But what if we’re only really interested in an integer result, we’ll have to add those results together. So, we write a function that does just that:
app.rollStack = function (sides, number){ var dice = app.roll(sides, number); var output = 0; for(i = 0; i < dice.length; i++){ output += dice[i]; } return output; }
Now we have to build an interpreter that will turn dice notation into these random rolls. If you are unfamiliar, dice notation looks like this: 2d4. Where the number of dice comes before the d and the number of sides comes after. In addition we will add some basic mathematics so we can process equations like: 2d4 + 4.
app.dice = function (string){ //Split the dice and the procedures first var components = string.split(/[x\*\/+\-]/i);var dice = components[0].trim().split(/[dD]/);
var sides = Number(dice[1]);
var number = Number(dice[0]);
if(number == 0){
number = 1;
}
var value = app.rollStack(sides, number);
if(components[1] != undefined){
if(string.search(/x/i) !== -1 || string.search(/\*/) !== -1){
value *= Number(components[1].trim());
}else if(string.search(/\//) !== -1){
value /= Number(components[1].trim());
}else if(string.search(/\+/) !== -1){
value += Number(components[1].trim());
}else if(string.search(/\-/) !== -1){
value -= Number(components[1].trim());
}
}
return value;
}
This function only accepts a string as a parameter. This is relatively limited (or brittle depending on how you feel), as it expects the string to be in the format of dice and then a single mathematical procedure. If you can believe it, this was the entire dice roller in its first iteration, as 85% of all dice rolls can be simplified down that way. But don’t worry, we’ll be addressing these shortcomings in the next step, for the moment let’s talk about what we are doing.
For this, we are making liberal use of regular expressions. If you are unfamiliar with regular expressions, I highly suggest you open regex101.com and copy the expressions into the input, it will break them down for you and explain each component. Don’t forget to switch the interpreter (it calls them flavors) to EMCAScript (JavaScript). It shouldn’t matter for these, but sometimes the differences between interpreters are subtle. It’s a wonderful tool.
Basically, we are using three basic regular expressions in this function. The first is used to split the provided string into two components, the dice and the procedure, if any. We then split the dice into number and sides. After that we feed those values to our app.rollStack() function to get our random value. We then identify if we have a procedure and which it is (via regular expression), and apply it to our value as needed.
Which is fine and dandy. In fact it’s plenty for our NPC and treasure generators that will be built on this, but it isn’t enough for a full dice roller. In a gameplay session, dungeon or game masters can frequently find themselves rolling multiple types of dice, adding, subtracting, multiplying, and dividing all at the same time. This means we need something as robust as possible.
Now I don’t know about you, but as much fun as it might be to write a toy math interpreter, I don’t want to do it for this project, not when we have a powerful tool at our fingertips like eval().
People will frequently warn you against using eval(), if possible, as it will evaluate any string passed to it as if it were pre-written javascript. This is extremely powerful, and can be ripe for abuse.
There’s a counterpoint to this argument though, and that is that so is the console included with all major web browsers. We’re going to be walking a line between both of these viewpoints. We’ll be taking some basic precautions in that we won’t be evaluating anything from the GET, POST, or localStorage objects. That way people probably won’t be able to create malicious links or forms abusing our code. Cutting off most obvious XSS vulnerabilities. In addition, we’ll be doing some basic filtering to remove most general purpose JavaScript from what we are going to eval(). But at the end of the day, this isn’t a secure application. It’s just a tool for a game, and if someone wants to break the dice roller by removing those filters, that’s ultimately their business.
So, how are we going to filter the string before passing it to eval()? With another regular expression:
app.mathEval = function(exp){ /* Name: Stephen Kennedy Date: 7/10/19 Comment: This function was provided by Andy E in the answers to the following stack overflow question: https://stackoverflow.com/questions/5066824/safe-evaluation-of-arithmetic-expressions-in-javascript */ var reg = /(?:[a-z$_][a-z0-9$_])|(?:[;={}[]"'!&<>^\?:])/ig, valid = true;// Detect valid JS identifier names and replace them
exp = exp.replace(reg, function ($0) { // If the name is a direct member of Math, allow
if (Math.hasOwnProperty($0)){
return "Math."+$0;
}else if(Math.hasOwnProperty($0.toUpperCase())){
return "Math."+$0.toUpperCase();
}else{
// Otherwise the expression is invalid
valid = false;
}
});
// Don't eval if our replace function flagged as invalid
if (!valid){
return false;
}else{
try { return eval(exp); } catch (e) {
console.log(e);
return false;
}
}
}
As the comment in the code points out, this function was pulled from a Stack Overflow answer. I only changed the formatting, and removed the alerts.
I strongly recommend dropping the regular expression in regex101.com to really understand it, but the gist is that we are looking for anything that looks like it’s not a number or an operation: + – / *. If we do find something that doesn’t meet those criteria, we will check if it is a property of the Math object. If it is, we’ll go ahead and replace it with a Math call and eval the result, otherwise we’ll declare the whole thing invalid and not do anything else.
Now that’s great. It lets us evaluate complex math statements, but it doesn’t natively support dice rolls, so we’ll need a shim function to add that in.
app.diceEval = function(exp){ var reg = /[0-9]*[dD][0-9]+/g; exp = exp.replace(reg, function($0){ console.log($0); return app.dice($0); }); console.log(exp); return app.mathEval(exp); }
Here we’re turning to regular expressions again. This one I will break down for you. We are looking for zero to infinite integers between 0-9 followed by either a “d” or a “D” and one to infinite integers. We are also going to look for all occurrences of that pattern.
We pass that pattern to a string.replace(), where we will evaluate each instance of the pattern as its own dice roll and replace the pattern with the result. We then pass the resulting string with no dice notation to our app.mathEval() function where it will evaluate it based on traditional order of operations and return us a result.
The Results
I’m pretty satisfied, but play with it and make your own decision: DM Tools, as of right now it’s the left hand tray. We don’t cover a lot of fringe cases like exploding dice (4d8!), advantage and disadvantage, discarding outliers, and many of the other system specific things you might need to do. If you’re looking for a project that does these already, go ahead and check out this project on GitHub: https://github.com/GreenImp/rpg-dice-roller, as it is regularly updated and aims to support all operations in standard dice notation.
Otherwise, keep an eye on this project. I know for a fact I’ll be adding advantage and disadvantage here in the near future, as well as casting off low or high rolls.