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 }