1 /**
2 * dice, a dice notation parser and resolver
3 * Authors: Evan Walsh <evan@hellowelcome.org>
4 * License: AGPL-3.0
5 */
6 
7 module dice;
8 
9 import std.algorithm, std.array, std.math, std.random, std.range;
10 import std.stdio : writefln;
11 import std.conv : to;
12 import pegged.grammar;
13 
14 private mixin(grammar("
15 Roll:
16     Expr     <- Factor (Add / Sub)*
17     Add      <- '+' Factor
18     Sub      <- '-' Factor
19     Factor   <- Primary (Mul / Div)*
20     Mul      <- ('*' / 'x') Primary
21     Div      <- '/' Primary
22     Primary  <- Ceil
23               / Floor
24               / Round
25               / Abs
26               / '(' Expr ')'
27               / Grouped
28               / SFDice
29               / FateDice
30               / DKDice
31               / ExplDice
32               / Dice
33               / Number
34               / Negative
35               / Variable
36 
37     Number   <- ~([0-9]+)
38     Negative <- '-' Primary
39     Variable <- identifier
40     Dice     <- Number 'd' Number
41     ExplDice <- Dice '!'
42     DKDice   <- Dice DropKeep
43     FateDice <- Number 'dF'
44     SFDice   <- Dice (Succ Fail / Succ / Fail)
45     Fail     <- ('f>' / 'f<' / 'f=') Expr
46     Succ     <- ('>' / '<' / '=') Expr
47     DropKeep <- ('dh' / 'dl' / 'd' / 'kh' / 'kl' / 'k') Number
48     Floor    <- 'floor(' Expr ')'
49     Round    <- 'round(' Expr ')'
50     Ceil     <- 'ceil(' Expr ')'
51     Abs      <- 'abs(' Expr ')'
52     Grouped  <- '{' (Expr ',')* Expr '}' DropKeep
53 "));
54 
55 /**
56  * Parse a dice notation string and roll it
57  *
58  * Params:
59  *   input = String of dice notation
60  *
61  * Returns: The result of the roll as a float
62  */
63 float roll(string input)
64 {
65   auto parsed = Roll(input);
66   return value(parsed);
67 }
68 
69 ///
70 unittest
71 {
72   auto rolled = roll("3d6k2+1d20");
73   assert(rolled < 33);
74   assert(rolled > 2);
75 
76   rolled = roll("1d20+8-3*2");
77   assert(rolled < 23);
78   assert(rolled > 2);
79 
80   rolled = roll("1d20d2");
81   assert(rolled == 0);
82 
83   rolled = roll("2d20k1");
84   assert(rolled < 21);
85   assert(rolled > 0);
86 
87   rolled = roll("3d10kl2");
88   assert(rolled < 21);
89   assert(rolled > 1);
90 
91   rolled = roll("10d20dh5");
92   assert(rolled < 101);
93   assert(rolled > 4);
94 
95   rolled = roll("ceil(1d6/2)");
96   assert(rolled > 0);
97   assert(rolled < 4);
98 
99   rolled = roll("floor(1d6/2)");
100   assert(rolled > -1);
101   assert(rolled < 4);
102 
103   rolled = roll("round(1d6/2)");
104   assert(rolled > 0);
105   assert(rolled < 4);
106 
107   rolled = roll("abs(1d6-100)");
108   assert(rolled > 0);
109   assert(rolled < 100);
110 
111   rolled = roll("(2000-1999)d20");
112   assert(rolled > 0);
113   assert(rolled < 21);
114 
115   rolled = roll("10d2!");
116   assert(rolled > 9);
117 
118   rolled = roll("10x20");
119   assert(rolled == 200);
120 
121   rolled = roll("{3d6,3d6,3d6,3d6}k1");
122   assert(rolled > 2);
123   assert(rolled < 19);
124 
125   rolled = roll("4dF");
126   assert(rolled > -5);
127   assert(rolled < 5);
128 
129   rolled = roll("3d6>3f>5");
130   assert(rolled > -1);
131   assert(rolled < 4);
132 
133   rolled = roll("3d6=4");
134   assert(rolled > -1);
135   assert(rolled < 4);
136 }
137 
138 private float value(ParseTree tree)
139 {
140   switch (tree.name)
141   {
142   case "Roll":
143     return value(tree.children[0]);
144   case "Roll.Expr":
145     float expression = 0.0;
146     foreach (child; tree.children)
147     {
148       expression += value(child);
149     }
150     return expression;
151   case "Roll.Dice":
152     int expression = 0;
153     const float amount = value(tree.children[0]);
154     const float sides = value(tree.children[1]);
155 
156     for (int i = 0; i < amount; i++)
157     {
158       expression += rollDie(to!int(sides));
159     }
160 
161     return to!float(expression);
162   case "Roll.SFDice":
163     return rollSuccessFail(tree);
164   case "Roll.FateDice":
165     return rollFate(tree);
166   case "Roll.DKDice":
167     return rollDropKeep(tree);
168   case "Roll.ExplDice":
169     return rollExploding(tree);
170   case "Roll.Grouped":
171     return rollGrouped(tree);
172   case "Roll.Add":
173     return value(tree.children[0]);
174   case "Roll.Sub":
175     return -value(tree.children[0]);
176   case "Roll.Factor":
177     float expression = 1.0;
178     foreach (child; tree.children)
179     {
180       expression *= value(child);
181     }
182     return expression;
183   case "Roll.Mul":
184     return value(tree.children[0]);
185   case "Roll.Div":
186     return 1.0 / value(tree.children[0]);
187   case "Roll.Primary":
188     return value(tree.children[0]);
189   case "Roll.Negative":
190     return -value(tree.children[0]);
191   case "Roll.Number":
192     return to!float(tree.matches[0]);
193   case "Roll.Ceil":
194     return ceil(value(tree.children[0]));
195   case "Roll.Floor":
196     return floor(value(tree.children[0]));
197   case "Roll.Round":
198     return round(value(tree.children[0]));
199   case "Roll.Abs":
200     return abs(value(tree.children[0]));
201   default:
202     return 0;
203   }
204 }
205 
206 private float rollSuccessFail(ParseTree tree)
207 {
208   auto dice = tree.children[0];
209   auto successFail = tree.children[1];
210   const auto highTarget = value(successFail.children[0]);
211   const auto highMode = successFail.matches[0];
212   auto lowTarget = 0.0;
213   auto lowMode = "";
214 
215   if (tree.children[$ - 1] != tree.children[1])
216   {
217     auto secondChild = tree.children[$ - 1];
218     lowTarget = value(secondChild.children[0]);
219     lowMode = tree.children[$ - 1].matches[0];
220   }
221 
222   const int amount = to!int(dice.matches[0]);
223   const int sides = to!int(dice.matches[2]);
224   auto counter = 0.0;
225 
226   float checkSuccess(float number, float target, string mode)
227   {
228     auto counter = 0.0;
229 
230     switch (mode)
231     {
232     case ">":
233       if (number > target)
234         counter += 1;
235       break;
236     case "<":
237       if (number < target)
238         counter += 1;
239       break;
240     case "=":
241       if (number == target)
242         counter += 1;
243       break;
244     case "f>":
245       if (number > target)
246         counter -= 1;
247       break;
248     case "f<":
249       if (number < target)
250         counter -= 1;
251       break;
252     case "f=":
253       if (number == target)
254         counter -= 1;
255       break;
256     default:
257       break;
258     }
259 
260     return counter;
261   }
262 
263   for (int i = 0; i < amount; i++)
264   {
265     const auto rolled = rollDie(sides);
266     counter += checkSuccess(rolled, highTarget, highMode);
267     if (lowTarget > 0)
268     {
269       counter += checkSuccess(rolled, lowTarget, lowMode);
270     }
271   }
272 
273   return counter;
274 }
275 
276 private float rollFate(ParseTree tree)
277 {
278   int expression = 0;
279   const float amount = value(tree.children[0]);
280 
281   for (int i = 0; i < amount; i++)
282     expression += uniform(-1, 2, rndGen);
283 
284   return to!float(expression);
285 }
286 
287 private float rollDropKeep(ParseTree tree)
288 {
289   auto dice = tree.children[0];
290   auto dropKeep = tree.children[1];
291   const int amount = to!int(dice.matches[0]);
292   const int sides = to!int(dice.matches[2]);
293   int[] rolls = new int[0];
294 
295   for (int i = 0; i < amount; i++)
296     rolls ~= rollDie(sides);
297 
298   return handleDropKeep(rolls, dropKeep.matches[0], to!int(value(dropKeep.children[0])));
299 }
300 
301 private float rollExploding(ParseTree tree)
302 {
303   const auto amount = to!int(tree.matches[0]);
304   const auto sides = to!int(tree.matches[2]);
305   int[] rolls = new int[0];
306   auto keepRolling = true;
307   auto bonusRolls = 0;
308 
309   do
310   {
311     if (rolls.length >= amount)
312       bonusRolls -= 1;
313 
314     auto latestRoll = rollDie(sides);
315     rolls ~= latestRoll;
316 
317     if (latestRoll == sides)
318       bonusRolls += 1;
319 
320     keepRolling = rolls.length < amount || bonusRolls > 0;
321   }
322   while (keepRolling);
323 
324   return to!float(rolls.sum());
325 }
326 
327 private float handleDropKeep(int[] rolls, string mode, int amount)
328 {
329   rolls.sort();
330   if (rolls.length > amount)
331   {
332     switch (mode)
333     {
334     case "k", "kh":
335       rolls = rolls.reverse().take(amount);
336       break;
337     case "kl":
338       rolls = rolls.take(amount);
339       break;
340     case "d", "dl":
341       rolls = rolls.drop(rolls.length - amount);
342       break;
343     case "dh":
344       rolls = rolls.dropBack(rolls.length - amount);
345       break;
346     default:
347       break;
348     }
349   }
350   else
351   {
352     switch (mode)
353     {
354     case "k", "kh", "kl":
355       break;
356     case "d", "dh", "dl":
357       return 0.0;
358     default:
359       break;
360     }
361   }
362 
363   return to!float(rolls.sum());
364 }
365 
366 private float rollGrouped(ParseTree tree)
367 {
368   int[] rolls = new int[0];
369   foreach (child; tree.children)
370   {
371     if (child.name != "Roll.DropKeep")
372     {
373       // TODO: Handle floats passing through here?
374       rolls ~= to!int(value(child));
375     }
376   }
377 
378   auto dropKeep = tree.children[$ - 1];
379   auto mode = dropKeep.matches[0];
380   auto amount = to!int(value(dropKeep.children[0]));
381 
382   return handleDropKeep(rolls, mode, amount);
383 }
384 
385 /**
386  * Roll a die with a number of sides
387  *
388  * Params:
389  *   sides = Number of sides on the die to roll
390  *
391  * Returns: The result of the roll as a float
392  */
393 int rollDie(int sides)
394 {
395   return uniform(1, sides + 1, rndGen);
396 }
397 
398 ///
399 unittest
400 {
401   auto rolled = rollDie(6);
402   assert(rolled > 0);
403   assert(rolled < 7);
404 
405   rolled = rollDie(20);
406   assert(rolled > 0);
407   assert(rolled < 21);
408 }