11
\$\begingroup\$

This JavaScript object is part of a mobile application that rolls dice for our D&D games.

Here are some example of possible inputs from the user:

  • 2d4
  • 2d4 + 3d6
  • 2d4 + 3d6 + 12
function DiceRoller() {
    this.rollDices = function(input){

        var parsedInput = input.split("+");
        var totalScore = 0;

        for (var diceIndex = 0; diceIndex < parsedInput.length; diceIndex++) {

            var diceRoll = parsedInput[diceIndex];

            //If we can't find 'd', it means we only have to add the input
            if (diceRoll.indexOf("d") > 0) {
                var diceParts = diceRoll.split('d');

                //We need a random roll for each dice. ex: 4d6, needs 4 rolls.
                for (var numberOfDiceIndex = 0; numberOfDiceIndex < diceParts[0];numberOfDiceIndex++) {
                    totalScore += getRandomInt(1,diceParts[1]);
                }
            } else {
                totalScore += parseInt(diceRoll);
            }
        }

        return totalScore;
    };

    var getRandomInt = function(min, max){
        return Math.floor(Math.random()*(max-min+1)+min);
    }
}

I'm concerned about the performance of the rollDices method, considering I use split quite a lot and I have nested loops.

\$\endgroup\$
6
  • \$\begingroup\$ Is the app available somewhere? I know some people who play D&D who might like it! \$\endgroup\$
    – Phrancis
    Commented Apr 10, 2015 at 19:41
  • \$\begingroup\$ Well, I didn't really test it yet + it has no UI ahah. I started this yesterday. I'll get back to you about it when I'll be okay to share it :) \$\endgroup\$
    – IEatBagels
    Commented Apr 10, 2015 at 19:52
  • 2
    \$\begingroup\$ As small as your expected input is, I'm not sure you really need to worry about the performance. \$\endgroup\$ Commented Apr 10, 2015 at 19:52
  • \$\begingroup\$ Hmm. Indeed! Though I'd appreciate to learn if something is wrong if I had a bigger input. \$\endgroup\$
    – IEatBagels
    Commented Apr 10, 2015 at 20:02
  • \$\begingroup\$ Can I know how to test that Dice rolling app? Because when I click run it, it hows done but nothing appear on the screen. \$\endgroup\$
    – user105283
    Commented May 12, 2016 at 18:21

2 Answers 2

13
\$\begingroup\$

Performance here won't be a problem. The splits you have are relatively trivial, and presumably there is a human component in here too (someone entering the roll-specs), so the parse time will essentially be negligible compared to that.

The code itself though has some neat features, and also some concerns.

Random

First up, the getRandomInt method is good. It follows the classic definition for retrieving a random value from a specified closed range [low,high]. I would prefer for there to be spaces on the operators though. Your code:

return Math.floor(Math.random()*(max-min+1)+min);

should be:

return Math.floor(Math.random() * (max - min + 1) + min);

Numbers

Your code makes some lucky guesses about numbers, and although it works, it is also easy to break. For example, if I take your random function again:

return Math.floor(Math.random()*(max-min+1)+min);

and instead make it:

return Math.floor(Math.random()*(1+max-min)+min);

it should work fine, right? Except, it won't, because you give the max value to the function as string:

var diceParts = diceRoll.split('d');

.....
    totalScore += getRandomInt(1,diceParts[1]);
.....

So, since you pass in diceParts[1] as a string, the 1+max-min will become 1 + "4" - 1 which in turn becomes 14 - 1 instead of 5 - 1.... so, your code works, but is it an accident? Well, not really, because you figured this out in the other side of the situation - where there is no d in the spec. For that, you have:

totalScore += parseInt(diceRoll);

You should be more defensive about number-handling in your code. You will run into mysterious bugs at some time in the future if you don't control them now.

parseInt(...)

You should always give parseInt a radix to work with. The documentation says:

Specify 10 for the decimal numeral system commonly used by humans. Always specify this parameter to eliminate reader confusion and to guarantee predictable behavior. Different implementations produce different results when a radix is not specified.

(note: the bold emphasis is from the documentation, it is not my addition)

Since parseInt() ignores leading whitespace, and stops parsing on the first non-digit, it is OK to give parseInt(...) padded values - you don't need to trim them first.

Map->Reduce

I would have liked to have seen a map-reduce process happening here:

  1. you have a + separated sequence of inputs
  2. each part has a roll/limit
  3. each result should be summed.

I would be tempted to build the process as:

this.rollDices = function(spec) {
    return spec.split(/\+/)
          .map(parseRoll)
          .reduce(function(a,b){return a + b;});
};

The above code describes the process quite well:

  1. split on the +
  2. convert parts to values
  3. sum each part.

The implementation details for parseRoll is:

var getRandomInt = function(min, max){
    return Math.floor(Math.random() * (max - min + 1) + min);
};

var parseValue = function(val) {
    return parseInt(val || "1", 10);
}

var parseRoll = function(roll) {
    var parts = roll.split(/d/);
    var sum = 0;
    var limit = parseValue(parts[1]);
    for (var i = parseValue(parts[0]) - 1; i >= 0; i--) {
        var got = getRandomInt(1, limit);
        sum += got;
        // console.log("From roll " + roll + " part " + i + " got " + got + " sum " + sum);
    }
    return sum;
};

I have put the above in to a snippet here:

var getRandomInt = function(min, max){
    return Math.floor(Math.random() * (max - min + 1) + min);
};

var parseValue = function(val) {
    return parseInt(val || "1");
}

var parseRoll = function(roll) {
  
    // only here for the snippet
    debug("Parsing " + roll);
  
    var parts = roll.split(/d/);
    var sum = 0;
    var limit = parseValue(parts[1]);
    for (var i = parseValue(parts[0]) - 1; i >= 0; i--) {
        var got = getRandomInt(1, limit);
        sum += got;
      
        // only here for the snippet
        debug("  From roll " + roll + " part " + i + " got " + got + " sum " + sum);
      
    }
    return sum;
};

var rollDices = function(spec) {
    // only here for the snippet
    debugarea.value = "";
  
    return spec.replace(/[^+0-9d]+/g, "")
          .split(/\+/)
          .map(parseRoll)
          .reduce(function(a,b){return a + b;});
};

// *************************
// only here for the snippet
// *************************


var results = document.getElementById("result");

var debugarea = document.getElementById("debug");

var debug = function(txt) {
    debugarea.value = debugarea.value + txt + "\n";
}

function updated(input) {
    results.value = rollDices(input.value);
}
#spec {
  background-color: lightgreen;
}

#result, #debug: {
  background-color: lightgray;
}
Dice Spec :<br>
<input id="spec" type="text" size="20"  oninput="updated(this)" />
<p>
Dice Value:<br>
<input id="result" type="text" size="5" readonly="true">
<p>
Debug:<br>
<textarea id="debug" rows="20" cols="40"></textarea>

\$\endgroup\$
5
  • \$\begingroup\$ I agree with nearly everything, except spec.replace(/[^+0-9d]+/g, "") is too lenient and would lead to garbage input being processed as if it were legitimate. \$\endgroup\$ Commented Apr 11, 2015 at 7:15
  • \$\begingroup\$ @200_success - in retrospect, I agree. I have revised that suggestion (removed it in fact). I have also: added a part about parseInt(), and additionally added a working stack snippet. \$\endgroup\$
    – rolfl
    Commented Apr 11, 2015 at 14:49
  • \$\begingroup\$ Why do you use /\+/ instead of '+'? \$\endgroup\$
    – IEatBagels
    Commented Apr 13, 2015 at 21:36
  • \$\begingroup\$ It isn't super intuitive how you deal with +5 in 1d4+5. I understood by using the snippet though! If I happened to have 1d4+80, I'd go in an 80 iteration loop, isn't this a problem? (I didn't specify the input could be so big in my question, my bad) \$\endgroup\$
    – IEatBagels
    Commented Apr 13, 2015 at 22:12
  • 1
    \$\begingroup\$ Correct, +80 is interpreted as 80d1 - or, 80 rolls of a 1-sided dice. Without the debug messages, this would compute is a minuscule amount of time... even in the 100's of thousands. You are right that this would be slower than an alternative special-case for no d in the spec, but that would ruin the elegance of the code ;-) \$\endgroup\$
    – rolfl
    Commented Apr 13, 2015 at 23:00
5
\$\begingroup\$

As already mentioned in comments and answers, there's no need to worry about performance in this case. You'd probably have to work hard for it to be slow :)

Like rolfl I'd rather work on the structure in order to "unnest" some the code, and split the logic for parsing the string and rolling dice into separate functions.

Sidenote: "Dice" is the plural form ("two dice"), "die" is the singular form ("one die"). So rollDices is grammatically incorrect, and should just be rollDice.

Also, while I've never played D&D, I do know that there are some rules about dice rolls that you might want to take into account. For instance, the question of what dice actually exist. Your current code would gladly roll a d3 die, even though that's not actually a thing. (Edit: aaaaaand I was wrong. According to the comments a d3 is a thing. But the code below will support a d3 just fine regardless; you can just add it if you need it.)

I'd try to make a slightly more generic model of some dice, and then worry about parsing string input. You can then use said dice in whatever way you need later.

For instance, in a sorta DSL-style you could do:

function makeDie(sides) {
  var die = function () {
    return 1 + Math.random() * sides | 0;
  };

  die.times = function (count) {
    var rolls = [];
    for(var i = 0 ; i < count ; i++) {
      rolls.push(this());
    }
    return rolls;
  };

  return die;
}

var dice = {
  d4: makeDie(4),
  d6: makeDie(6),
  d8: makeDie(8),
  d10: makeDie(10),
  d12: makeDie(12),
  d20: makeDie(20)
};

which you can use like so:

dice.d6();       // => random d6 roll
dice.d8.times(3) // => array of 3 random d8 rolls

You can make any die you want, e.g makeDie(3) or makeDie(17) if necessary.

As for parsing a string expression, I'd use String.replace with a callback to "map" the parts of the string to individual rolls, and then use reduce as rolfl explained well:

function roll(expression) {
  var rolls = [];

  expression.toLowerCase().replace(/(\d+)(d\d+)?/g, function (_, count, die) {
    if(die) {
      rolls = rolls.concat(dice[die].times(+count));
    } else {
      rolls.push(+count);
    }
  });

  return rolls.reduce(function (sum, roll) {
    return sum + roll;
  });
}

I'm using the +variable trick in lieu of parseInt, but parseInt might be a good idea. The regular expression will match a number, and an optional following by a "d" and another number. E.g. it'll match "4" and "2d6".

There's a gotcha, though, since the above doesn't check the input too heavily. You could pass a string like "2d6 foobar! 3", and it'd roll 2 d6 and add 3, even though the input isn't really correct. However, it'll throw an exception if you try to roll a die that doesn't exist.

There's also the question of negative values. As far as I know, the modifier can be negative? Not sure. But point is that that's not handled by the above. It kinda can be, though, by rewriting the regular expression to /(-?\d+)(d\d+)?/g which allow you to write "2d6 -5", but it's very whitespace sensitive. Still, in terms of the overall approach, I think it's a good track.

Continuing the DSL style, you can add the roll function to the dice object:

var dice = {
  d4: makeDie(4),
  d6: makeDie(6),
  d8: makeDie(8),
  d10: makeDie(10),
  d12: makeDie(12),
  d20: makeDie(20),
  roll: function (expression) {
    var self = this, rolls = [];

    expression.toLowerCase().replace(/(\d+)(d\d+)?/g, function (_, count, die) {
      if(die) {
        rolls = rolls.concat(self[die].times(+count));
      } else {
        rolls.push(+count);
      }
    });

    return rolls.reduce(function (sum, roll) {
      return sum + roll;
    });
  }
};

And you've got a dice bag, you can use like so:

dice.d6();                   // => random d6 roll
dice.d8.times(3)             // => array of 3 random d8 rolls
dice.roll("2d6 + 2d10 + 2"); // => sum of die rolls plus modifier
\$\endgroup\$
4
  • \$\begingroup\$ There actually are d3s in D&D, although they're quite rare in practice and usually rolled out by halving the output of a d6. The more you know! \$\endgroup\$ Commented Apr 11, 2015 at 17:31
  • \$\begingroup\$ @NathanTuggy oh gawd, I should have figured. Thankfully, it's easy to add to the code (or you can do what you apparently do in real [fantasy] life and roll a d6 and halve the value). I figured I was safe since a d3 is geometrically impossible (as a strictly polygonal die, that is; I guess you just shave three faces on a sphere and make it work). But much like taking the square root of a negative number and getting imaginary numbers, some enterprising nerd found a way to make "imaginary dice". See, this is why I don't play! ;-) \$\endgroup\$
    – Flambino
    Commented Apr 11, 2015 at 17:39
  • \$\begingroup\$ Actually, they are a physical thing too! :p You should check it out if you don't believe me! I like the object approach you used in your review and I will definitively look into it! I'm wondering though, what is the _ variable you use in function (_, count, die)? \$\endgroup\$
    – IEatBagels
    Commented Apr 12, 2015 at 0:29
  • \$\begingroup\$ @TopinFrassi Of course they are... :P I'm glad I added that it was only impossible as a polyhedron, specifically. Slightly less egg on my face that way :) As for the _, that's just a placeholder to say "we're not using this argument for anything". The 1st argument to the callback is the entire matched string (capture 0), e.g. "3d6", but we only care about the sub-captures, e.g. "3" and "d6". So the _ is just there to soak up the first argument. \$\endgroup\$
    – Flambino
    Commented Apr 12, 2015 at 9:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.