Essential part of many tabletop games are random events. They are often introduced to the game in form of dice rolls. This program can help you to understand those dice rolls. You can give it a dice expression and it computes the probabilities and some statistics for you. It can even deal with some form of depedence using names (see examples).
In all of these examples I assume that dice rolls are independent discrete (integer) random variables.
2d6: roll 2 six sided dice and add the results(1d4)d6: first, roll a 4 sided die. What you've rolled will determine how many times you'll roll a 6 sided die.1d4 * 2: roll a 4 sided die and multiply the result by 2(1 + 2) * 3:9(you don't have to use the dice operator)(1d4)d(1d2*4):1d4will determine the number of dice and1d2 * 4will determine the number of dice sidesvariance(2d6): compute the variance of2d6expectation(2d6): compute the expectation of2d65d100 < 50: compute the indicator (i.e. random variable with bernoulli distribution) that5d100is less than 503d6 == 6: compute the indicator that3d6is exactly eqaul to 62d100 in [4, 32]: compute the indicator that2d100is in the[4, 32]closed intervalvar roll = 1d20; (roll in [10, 19]) * 2d8 + (roll == 20) * 4d8: roll with a 20 sided die. If we get a number between 10 and 19 (including 10 and 19), roll2d8, if we get a 20, roll4d8. Otherwise it's 0. Notice the 2 occurances ofrollare not independent. They are considered as one roll.
- independence: The program works with random variables. Each operation on them assumes their independence. This assumption is quite limiting. Consider following expression:
2 * (1d20 == 19) + 3 * (1d20 == 20). Some misguided assumption clould be that this expression is equal to 2 if we roll a 19 and 3 if we roll a 20. This is not the case. Both subexpressions1d20are independent - they are completely different rolls. In this example, we can get 0, 2, 3 or 5. You can use variables to resolve this issue:var X = 1d20; 2 * (X == 19) + 3 * (X == 20) - int vs real: The program can work only with integer random variables (that is, value of a variable can only be an integer). Therefore it is invalid to use operands on random variables in conjunction with real numbers. For example:
1d6 * 2.5or even1d6 + 2.0are invalid.
The program internally works with 3 basic types: int (a signed integer value), real (a 64-bit floating point number) and rand_var (a discrete integer random variable). Numbers without a decimal part are treated as int. The program will automatically convert between int and real or rand_var in function calls and operators but not vice versa (it can't convert real to int implicitly).
Current implementation will automatically add functions in the following list. There is a mechanism to add new functions implemented in C++ (see the add_function method in the environment class). Special type any in this list means that you can use it with any type as long as you substitute the same type for all occurances of any.
real expectation(rand_var): takes a random variable and computes its expected valuereal variance(rand_var): takes a random variable and computes its variancereal deviation(rand_var): takes a random variable and computes its standard deviationint roll(rand_var): generate a random number from given distributionany max(any, any): takes 2 values and computes the maximum (it can be a random variable ifanyisrand_var)any min(any, any): takes 2 values and computes the minimum (it can be a random variable ifanyisrand_var)int quantile(rand_var, real): takes a random varialbe, a probability and computes a quantile (denoteXa random varialbe,quantile(X, p) = min{ k : P(X <= k) >= p})
Operators in this list are sorted by precedence from lowest to highest. All operators are left-associative unless stated otherwise:
=(assign operator): non-associative<(less than),<=(less than or equal),!=(not equal),==(equal),>=(greater than or equal),>(greater than),in(is in interval): all non-associative+(addition),-(subtraction)*(multiplication),/(division)-(unary minus)dorD(dice roll operator)
The program implements a predictive parser for it is easy to write it by hand. Original grammar is listed here. It is necessary to modify the grammar for a straightforward implementation (for example: get rid of the left recursion).
<stmts> ::= <stmt>; <stmts>
| <stmt>
| ""
<stmt> ::= var <id> = <expr>
| <expr>
<expr> ::= <add> in [<add>, <add>]
| <add> <rel_op> <add>
| <add>
<add> ::= <add> + <mult>
| <add> - <mult>
| <mult>
<mult> ::= <mult> * <minus>
| <mult> / <minus>
| <minus>
<minus> ::= -<minus>
| <dice_roll>
<dice_roll> ::= <dice_roll> d <factor>
| <factor>
<factor> ::= (<expr>)
| <number>
| <func_id>(<opt_params>)
| <id>
<opt_params> ::= <param_list>
| ""
<param_list> ::= <param_list>, <expr>
| <expr>
Here are some non-trivial terminals and their regular expressions:
<number>:[0-9]+(\.[0-9]+)?<id>:[A-Za-z][A-Za-z0-9_]*<func_id>:[A-Za-z][A-Za-z0-9_]*/{whitespace}*\((that is, same as<id>but there is a(after it)<rel_op>:<|<=|==|!=|>=|>
The project is separated into 3 parts: a static library, simple CLI (command line interface) program and tests. The static library is linked with the CLI program and the tests.
You will need cmake version 3.0 or later and a C++ compiler that supports C++14 (tested on gcc version 7.2.0).
You'll need doxygen, gcov and lcov in order to build documentation and code coverage report respectively. They are not needed in order to build and run the CLI program and tests.
- Create a build directory (say
build) in the project root - In this directory run
cmake ..(use the-Goption to specify generator) - Compile (for example: run
makeif you've usedUnix Makefiles)