A Simple Dungeons & Dragons Dice Roller

So I used to be a big fan of Dungeons and Dragons and one of the things that was a problem for me was the time it took to roll a heap of dice sometimes. So I have written a quick and dirty dice roller that caters for all the cominations of dice that you might have in the game.

The first predicates handle the rolling algorithm. There are three parts to a dice roll which are split up like so: 'times_rolls' [d] 'dice_type' [+-] 'modifier'

For example, 2d20+1 is a 20 sided dice that will be rolled twice and then 1 is added to the result. Rather than rolling several times the code can calculate the upper and lower values and pass them to the random number predicate.

roll(Rolls, Dice, Modifier, Result) :-
    LowerBound is Rolls + Modifier,
    UpperBound is Dice * Rolls + Modifier,
    random_between(LowerBound, UpperBound, Result).

roll/4 is pretty much all you need to actually do the rolling but it's not that usable, because users would rather type in the dice in 'human' form. A DCG can be used to parse the humanistic dice into the parts required.

As most of the parts of the dice specification are optional (apart from the dice itself) then there a few forms required, (eg: d4, 1d4, d4+1, 1d4+1). parse_dice will provide the main parsing of these forms.

parse_dice(R, D, M) --> gt1(R), [d], gt1(D), modi(M). % eg: 1d4+1
parse_dice(1, D, M) --> [d], gt1(D), modi(M). % eg: d12-2
parse_dice(R, D, 0) --> gt1(R), [d], gt1(D). % eg: 2d6
parse_dice(1, D, 0) --> [d], gt1(D). % eg: d6

Then there are two more parts 1) a number that is greater than or equal to 1, and the same number with a + or - in front of it.

gt1(X) --> number_(Num), { number_codes(X, Num), X >= 1 }.
modi(X) --> [-], gt1(N), { X is 0 - N }.
modi(X) --> [+], gt1(X).

finally, we need to specify what a number looks like (this didn't work with in the inbuilt library).

number_([D|T]) --> digit(D), number_(T).
number_([D]) --> digit(D).
digit(D) --> [D], { char_type(D, digit) }.

Alright, so after the DCG is specified, then a way to call it is required. do_roll/1 takes a list of characters parses them and then rolls the dice and prints the result.

do_roll(Die) :-
    phrase(parse_dice(R,D,M), Die, []),
    roll(R, D, M, Roll),
    format('roll result = ~p~n', Roll).

Finally, a predicate to easily read a line of text that can be parsed and we are done!

die :-
    read_line_to_codes(user_input, DieCodes),
    maplist(char_code, Die, DieCodes),
    do_roll(Die) -> die 
    format('Not a dice, see ya!~n').