I actually caught an error in the above code and fixed it... as well as a couple of cosmetic changes that were leftover code bloat from when I was testing the code. Here is an annotated version, if it helps. First, this is declared as a function using fat arrow syntax . This would be in your code body so that you could call it once you had determined how many dice to roll with. const getRollResult = (n, options = {}) => { n will represent the number of dice to roll; options will be an object with settings we can supply to the function. The next part declares an object to establish some default values for our config options. let o = { cancel: 1, again: 10 }; Doing it that way (as opposed to supplying default values to these properties in the function declaration) allows you to pass in a limited number of properties without overwriting the default options completely. In other words, if I'd supplied this info in the function declaration, when you wanted to call the function you could have called it without supplying any options value (which you can still do in the alternate way I wrote it)... but if you wanted to overwrite a single property (say, to make it a 9-again roll instead of a 10-again), you'd have to supply *all* of the properties... even the default value for the cancel property, which you never meant to alter. You'd have to do that in every call to this function, everywhere in your code. Much better if the object (the function, in this case) is in charge of its own properties. So, now that we have our default config values attached to an object, the next bit of code sets about writing any properties you *did* supply over the top of those established defaults. This next bit says, "For each of the keys of our config object, pass them into a function; in that function, the value of that key ( k ) on our config object ( o ) will be assigned either the value of that key from the options that were passed in (ie, options[k] ) OR what was already defined on our config object (ie, o[k] )." Object.keys(o).forEach(k => o[k] = options[k] || o[k]); And since the logical OR operator returns the first truthy evaluation, it will either take the property from what you pass (if it exists), or it will take the value from what's already there. (Undefined properties are false-y values, so if the property isn't on your passed object, evaluation proceeds to the next operand.) Next is our actual rolling process abstracted into a function. We needed this in a few different places in the code, so I put it in a function that gives us a single point of maintenance if we need to tweak it down the line. const roll = (d) => [...Array(d).fill().map(e => randomInteger(10))]; The roll function takes a single argument, d , representing the number of dice to roll. The code reads easiest starting with Array() , which will create an array numbering d entries. We then fill() that (we supply nothing to the function, so they are simply filled to undefined ), and finally we map those undefined entries into new values. The map function can take more arguments than this, but we only need the first, which represents each element, e , we will be mapping to a new value. The value returned from the map operation is the result of the call to Roll20s randomInteger() function... so, a number between 1 and 10. map returns an array. Then, in deciphering the code, you can back up to the spread operator ( ... ) which is going to feed each element of the returned array into an array constructor (the brackets enclosing it all). This might be overkill, but there are cases where you'll get undefined or null where you'd instead want an empty array, and this sort of syntax solves that problem. In short, the roll function will return an array of numbers between 1 and 10, with the size of the array being whatever you feed it. So let's use it: let r = roll(n); That's the same n as was supplied to the getRollResults() function initially. Now r represents the array of our initial roll of n dice. It will stay as it is for the entire function. From our base roll, we need to derive our cancels (defaulted to be 1s), our successes (8, 9, or 10), and everything else (the filler). That's what the next bit of code does: let [c,f,s] = [r.filter(e => e <= o.cancel), r.filter(e => e > o.cancel && e < 8), r.filter(e => e >= 8).sort((a,b) => a > b ? -1 : 1)]; This is an example of destructuring assignment . You can see the above assignment in a simplified form as: [a,b,c] = [1,2,3] So that a = 1, b = 2, and c = 3. Except what we're doing is to filter our initial roll ( r ) in three different ways. In our case, c (for cancels) will be the array resulting from filtering the original roll and testing every element ( e ) to be less than or equal to our config option for cancel . f (for filler) will be everything between the cancel and again config options (again, using an array returned by a filter() function applied to our original roll). And s (for successes) will be what is left when we filter for everything at or above our again option. For s, however, we're going to apply one more operation to the returned array; we're going to sort it in descending order so that the higher success values (ie, 10s) will appear first. Now that we have that sorted, we need to handle explosions. let xs = roll(s.slice(c.length).filter(e => e >= o.again).length); let xd = []; In the first line, we declare xs , which will represent our "exploded successes". In the second line, we declare xd , which will hold all of the dice returned from any explosion roll. xs is set to equal the result of the roll() function (an array). The value for the number of dice we want to roll (the size of the array) is arrived at by slicing the s array at a position equal to the number of items in the c array (in other words, subtracting our 1s from our successes). Since slice operates from the left (with a positive value, anyway), and since we already sorted s to have the higher values first, we're losing our highest successes. Then we filter what is left, comparing each item, e , against our again config setting. In plain english, "we drop as many of our highest successes as we have cancellations, then we test what is left to see if anything is above our roll again threshold." Since filter() returns an array, we get the length to know how many dice should be rolled as exploded successes. Now we have to process the returns to look for recurring explosions: while (xs.length > 0) { xd = [...xd,...xs]; xs = xs.filter(e => e >= o.again); xs = xs.length ? roll(xs.length) : xs; } The while loop will run as long as there is anything in the xs array, so if there were no exploding successes in the initial roll (eg, no 10s in a 10-again roll), this will be skipped. Our exploding successes were already depleted by any cancellations we had (any 1s), so they've had their effect. The rest of the while loop assigns the contents of the xs array to the xd array (again using the spread operator). Then xs , which has the results of our latest exploded success roll, is reassigned to be the filtered set of values which are equal to or greater than our again setting. We only need the dice which rolled a value which should be rolled again. xs at this point could have no entries, or it could have a number of entries equaling the number of dice we need to reroll, so the last line of active code in the while loop tests that... reassigning to xs the results of a roll() function where we ask for the number of dice we need (that is, the length of the current xs array), or it is assigned to be itself. This is handled with a ternary check . That means that when we cycle back up to our while and our condition is reevaluated, either xs is empty (and we exit the while ), or it has the results of another roll which we need to manage (and we step through the while code again). This might be the step that makes it clearer why we're appending the results of xs onto xd in the while loop. Once we're out of the while loop, we need to append anything in the xd array which is greater than or equal to 8 (the WoD success threshold) to our successes array ( s ): s = [...s,...xd.filter(e => e >= 8)]; That line, again, uses the spread operator to merge the elements of two arrays. Finally we're done, and we can return our results... but there are a couple of things that happen in the first line, here: return {orig: r, final: [...r,...xd], c: c, f: f, s: s, xs: xs, successes: Math.max(s.length - c.length,0)}; }; Our final roll is all of the dice rolled originally ( r , which was never changed) as well as all of the dice rolled during any of our explosion rerolls ( xd ). Also our number of successes can't be simply related to our successes array ( s ), since that contains successes that were "cancelled" should any cancel values been rolled in the original roll. Instead, we subtract the number of cancellations (in c ) from our successes (in s ). The math is correct because c never inflates with new values from the while loop... it only ever has the amount from the original roll. Lastly, the Math.max() function makes sure that if we had more cancellations than successes it will report as a 0 rather than a negative number. Hope that helps!