Messing around with DiceRoll

I happened to look at the DiceRoll function in the core recently, and found the code a little odd. Among other things, it will let you do "2d6+3" or "3d6-5", but not "4-2d6" or "2d6+2d8". And I started wondering how hard it would be to make something like that work.

Here's two versions that came off the top of my head:

<function name="DiceRoll" parameters="expression" type="int">
  expression = Replace (Replace (LCase(expression), " ", ""), "-", "+-")
  total = 0
  foreach (dice, Split (expression, "+")) {
    if (IsInt (dice)) {
      total = total + dice
    }
    else {
      if (Left (dice, 1) = "-") {
        expression = Mid (dice, 2)
        negative = -1
      }
      else {
        negative = 1
      }
      if (Left (dice, 1) = "d") {
        dice = "1"+dice
      }
      parts = Split (dice, "d")
      if (ListCount (parts) = 2 and IsInt (StringListItem (parts, 0)) and IsInt (StringListItem (parts, 1))) {
        sides = ToInt (StringListItem (parts, 1))
        for (i, 1, ToInt (StringListItem (parts, 0))) {
          total = total + negative * GetRandomInt (1, sides)
        }
      }
      else {
        error ("Can't parse dice type: "+dice)
      }
    }
  }
  return (total)
</function>

The use of Replace probably makes this a little less efficient, but I think it's pretty close to the original; and will happily handle systems like the Time Lord RPG ("d6 - d6" is common) or similar.

But once I got thinking about it, I wondered how hard it would be to make one that will understand expressions like "2d6 * 1d6", or "1d12 - (d6 + d8)" too…

<function name="DiceRoll" parameters="expression" type="int">
  expression = LCase (expression)
  if (IsRegexMatch ("[^x/×÷+\\-d^*\\s\\d\\(\\)]", expression)) {
    error ("Invalid dice roll: "+expression)
  }
  expression = Replace (expression, "×", "*")
  expression = Replace (expression, "x", "*")
  expression = Replace (expression, "÷", "/")
  while (Instr (expression, "d") > 0)) {
    parts = Populate ("^(?<pre>.*?)(?<count>\\d+)?\\s*d\\s*(?<dice>\\d+)(?<post>.*)$", expression)
    if (DictionaryContains (parts, "count")) {
      count = ToInt (DictionaryItem (parts, "count"))
    }
    else {
      count = 1
    }
    dice = ToInt (DictionaryItem (parts, "dice"))
    total = 0
    for (i, 1, count) {
      total = total + GetRandomInt (1, dice)
    }
    expression = DictionaryItem (parts, "pre") + ToString (total) + DictionaryItem (parts, "post")
  }
  result = eval (expression)
  return (result)
</function>

(Yes, I'm cheating. That searches the string for any part that looks like "2d12" or whatever, and replaces that expression with a number. So by the time there's no "d" left in the string, we've got something like "12 + (3 * 4)" which I can then pass to eval to do the maths.)

I kind of wish I could make the 'd' work like a real operator, so you could do "(1d6+2)d8", but that would mean making the code as long as the original DiceRoll function… ahh, but let's do it anyway

<function name="DiceRoll" parameters="expression" type="int">
  expression = LCase (expression)
  if (IsRegexMatch ("[^x/×÷+\\-d^%*\\s\\d\\(\\)]", expression)) {
    error ("Invalid dice roll: "+expression)
  }
  expression = Replace (expression, "×", "*")
  expression = Replace (expression, "x", "*")
  expression = Replace (expression, "÷", "/")
  while (Instr (expression, "d") > 0)) {
    while (IsRegexMatch ("^(?<pre>.*)\\((?<expr>[+-*/^%\\d\\s]+)\\)(?<post>.*)$", expression)) {
      parts = Populate ("^(?<pre>.*)\\((?<expr>[+-*/^%\\d\\s]+)\\)(?<post>.*)$", expression)
      expression = DictionaryItem (parts, "pre") + ToString (eval (DictionaryItem (parts, "expr"))) + DictionaryItem (parts, "post")
    }
    parts = Populate ("^(?<pre>.*?)(?<count>\\d+)?\\s*d\\s*(?<dice>\\d+)(?<post>.*)$", expression)
    if (DictionaryContains (parts, "count")) {
      count = ToInt (DictionaryItem (parts, "count"))
    }
    else {
      count = 1
    }
    dice = ToInt (DictionaryItem (parts, "dice"))
    total = 0
    for (i, 1, count) {
      total = total + GetRandomInt (1, dice)
    }
    expression = DictionaryItem (parts, "pre") + ToString (total) + DictionaryItem (parts, "post")
  }
  result = eval (expression)
  return (result)
</function>

Now that's certainly less efficient than the original DiceRoll function… or maybe not. Because rather than using Quest code with Instr and similar to disassemble the string, we're using a regular expression. It'll run over the string more times, but it only gets really expensive if you give it a more complex expression.

(I did simple substitutions to replace × (or x) and ÷ with * and /, just in case you're making some RPG-styled thing and you want to have an expression like 6d6 ÷ 4 to show to the player, and then pass the same string to DiceRoll)


"2d6+3" or "3d6-5", OK, you got that easy, but you are over thinking the rest...
"4-2d6" -> R=4-DiceRoll("2d6")
"2d6+2d8". -> R=DiceRoll("2d6") + DiceRoll("2d8")
Also:
5d6÷4 -> R=DiceRoll("5d6")/4

Simpler, no extra coding required.
Altho, from something I recently learned...
DiceRoll is an easy to use function, for the programmer, it is a very hard function for Quest to process.
GetRandomInt() is much easer on Quest, and not that hard to use...
After all, earlier programmers used rnd(0) to generate numbers between 0 and 1, the multiplied the range needed, then "int"ed it...
R=int(rnd(0)*6)+1
to get a 1d6 result...


However, there are situations where a DiceRoll function is useful. Most notably if you're trying to implement a tabletop RPG system in Quest; or to make a game that looks like an RPG system. If these rolls are, for example, a weapon's damage, then where do you put that additional code you're suggesting? Do you have a combat function with dozens of special cases for different weapons?

The first version was vaguely grounded in the real world; because in D&D (2nd ed) it was possible to have weapons with a damage rating of "d8 + d6" or "4 - d6". So in the situation where the DiceRoll function has a real advantage over just using GetRandomInt, there could be a case where someone expects it to be able to handle those.

The other two are pretty much "take the concept and run with it". I know they're not likely to be used, but something irks me about having an arbitrary line in the sand. Saying that a function can handle addition and subtraction, but that's all, seems a little arbitrary. I'm pretty sure I've seen tabletop systems that want you to roll half of 2d6, though the name of the system escapes me at present (I've played a lot of games of varying degrees of obscurity).

The disadvantage of DiceRoll is that it is a heavy load on the processor, so should be avoided where possible. The advantage of it is that a string specifier allows a lot of flexibility to be stores in a single attribute. I'm just maximising that flexibility.

(and if you're going to point out that 2nd ed is very old, I know that. But if it's been done once, that puts it in the set of "things that game designers might want to do".)

>> Alternate method

If there wasn't an existing DiceRoll method, I'd more likely suggest one with more sensible arguments.

<function name="Dice" parameters="count,sides,bonus" type="int">
  if (not IsDefined("sides")) return (GetRandomInt (1, count))
  total = 0
  for (i, 1, count) {
    total = total + GetRandomInt  (1, sides)
  }
  if (IsDefined (bonus)) total = total + bonus
  return (total)
</function>

Then instead of DiceRoll ("2d6+4") you'd be writing Dice(2,6,4) and not have the extra inefficiency. And a game designer who's trying to implement their favourite old-and-overcomplex tabletop system could write:

shortsword.damage = "Dice(6)"
longsword.damage = "Dice(8)"
volleybow.damage = "9 - Dice(2,6)"
caltrops.damage = "Dice(2,4) / 2"
mastercraftedfidgetsword.damage = "Dice(8)+Dice(6)"

and then the damage resolution function can pass those strings directly to eval() instead of to DiceRoll().

Although… if you're trying to use the system from a tabletop game, there's a good chance you want the player to have the feel of the game as well. So you might want to have the line "Damage: d8 + d6" show up as part of a weapon's description. In which case, it would save so much work if you have a DiceRoll function that can operate on the same string.


Now, I really want to run a benchmark. Use the core DiceRoll function to roll "2d6+4" as many times as possible in three seconds, and then do the same using the functions above. Report how many times each of them managed to make the roll, and see how it stacks up.
>> An even sillier (but less processor-hungry) way to handle arbitrary dice rolls I know this is a weird way to do it. But it should be faster than the existing method; maybe even faster than using GetRandomInt directly…
<function name="DiceRoll" parameters="expression" type="int">
  dice = CompileDice (expression)
  total = ListItem (dice, 0)
  options = ListItem (dice, 1)
  roll = GetRandomInt (1, options)
  for (i, 2, ListCount (dice)-1) {
    d = ListItem (dice, i)
    if (d > 0) total = total + (roll % d) + 1
    else total = total - (roll % -d) - 1
    roll = roll / d
  }
  return (total)
</function>

<function name="CompileDice" parameters="expression" type="list">
  if (HasAttribute (game, "dicecache")) {
    if (DictionaryContains (game.dicecache, expression)) {
      return (DictionaryItem (game.dicecache, expression))
    }
  }
  else {
    game.dicecache = NewDictionary()
  }
  normalexpr = Replace (Replace (expression, " ", ""), "-", "+-")
  result = NewList()
  numeric = 0
  options = 1
  foreach (part, Split (normalexpr, "+")) {
    pos = Instr (part, "d")
    if (pos = 0) {
      numeric = numeric + ToInt (part)
      count = 0
    }
    else if (pos = 1) {
      count = 1
      sides = ToInt (Mid (part, 2))
    }
    else {
      if (pos = 2 and Left (part, 1) = "-") count = -1
      else sides = ToInt (Left (part, pos-1))
      sides = ToInt (Mid (part, pos+1))
    }
    if (count < 0) {
      count = -count
      sides = -sides
    }
    for (i, 1, count) {
      list add (result, sides)
      options = options * sides
    }
  }
  list add (result, numeric)
  if (options < 0) {
    options = -options
  }
  list add (result, options)
  for (i, 1, ListCount (result) - 2) {
    die = ListItem (result, 0)
    list remove (result, die)
    list add (result, die)
  }
  dictionary add (game.dicecache, expression, result)
  return (result)
</function>

That should handle all the plus and minus options; but it only needs to examine the string you pass to it the first time you use it. After that, it caches a table of the possible results of that dice roll, and determines a single random int as a lookup against that table.

Now I'd really love to see how that performs… but it's not something I can test using the online version.


just a small thing for consideration:

the built-in 'DiceRoll ()' doesn't have the means (as far as I know, which is nothing, of this built-in Function's coding, lol) of deciding/inputting the values of each of the sides, which maybe people may want such a feature/mechanic available to them

you choose/input the number of desired sides, but the sides' Values, start at 1 and increase by 1 for each additional side (example, a 6-sided dice's sides' values: 1, 2, 3, 4, 5, 6)

but someone may want for example, a six-sided dice with sides' Values of: 3, 14, 37, 61, 79, 85


but someone may want for example, a six-sided dice with sides' Values of: 3, 14, 37, 61, 79, 85

result = ToInt (PickOneString (Split ("3;14;37;61;79;85")))

Not quite as simple. However, the general case of this problem risks turning the dice roller's expressions into a full-blown expression language. When you start thinking of things like this, you'd probably be better making the string an expression which can be evaluated.

Hmm… using the regular expressions method, it wouldn't be too hard to transform "XdY" into "RollDice(X, Y)"; and maybe have an expression for an array literal. So something like "3d[3, 14, 37, 61, 79, 85]" turns into RollDice (3, Split("3;14;37;61;79;85")) which can then be passed to eval.
But the problem is that once you start doing things like that, you pretty much have to parse the string. You end up implementing a whole programming language just for one function. That's why I figured that it would be both easier and more powerful to only deal with the actual dice, and let eval handle the rest.


Log in to post a reply.

Support

Forums