1 /++ 2 + Module for managing complex dice rolling methods. 3 +/ 4 module unexpected.dice; 5 6 import std.random : isSeedable, isUniformRNG, Random, unpredictableSeed; 7 import std.range : isOutputRange; 8 9 enum Weight { 10 noWeighting, 11 positiveWeighting, 12 negativeWeighting 13 } 14 15 enum Bounding { 16 upper, 17 upperInclusive, 18 lower, 19 lowerInclusive 20 } 21 22 struct Roll { 23 bool counted; 24 uint value; 25 void toString(T)(T sink) const if (isOutputRange!(T, char[])) { 26 import std.format : formattedWrite; 27 import std.range : put; 28 if (counted) { 29 put(sink, "\x1B[92m"); 30 } else { 31 put(sink, "\x1B[91m"); 32 } 33 formattedWrite!"%s"(sink, value); 34 put(sink, "\x1B[0m"); 35 } 36 } 37 38 struct DiceRollResults { 39 ulong total; 40 Roll[] rolls; 41 void toString(T)(T sink) const if (isOutputRange!(T, char[])) { 42 import std.format : formattedWrite; 43 import std.range : put; 44 formattedWrite!"%(%s, %): %s"(sink, rolls, total); 45 } 46 } 47 48 struct DiceTargetResults { 49 uint count; 50 Roll[] rolls; 51 void toString(T)(T sink) const if (isOutputRange!(T, char[])) { 52 import std.format : formattedWrite; 53 import std.range : put; 54 formattedWrite!"%(%s, %): %s"(sink, rolls, count); 55 } 56 } 57 58 auto dice(RNG = Random)(uint seed) if (isUniformRNG!RNG && isSeedable!(RNG, uint)) { 59 return Dice!RNG(seed); 60 } 61 62 /++ 63 + A set of dice, ready to be rolled. 64 +/ 65 struct Dice(RNG) if (isUniformRNG!RNG) { 66 ///Number of sets of dice to roll. 67 uint sets = 1; 68 ///Number of dice per set to roll. 69 uint count = 1; 70 ///Number of sides on each die. 71 uint sides = 6; 72 ///Maximum number of dice to consider in results. 73 uint numDiceQualified = uint.max; 74 ///Whether to take the lowest or highest rolls. 75 bool takeLowestRolls; 76 ///Lower bound of results to reroll. 77 uint lowerRerollThreshold = 0; 78 ///Upper bound of results to reroll. 79 uint upperRerollThreshold = uint.max; 80 ///Value to add to the sum of each set. 81 int valAdd; 82 ///What sort of weighting to apply to the dice, if any. 83 Weight weighting; 84 private RNG rng; 85 /++ 86 + Performs all rolls at once. 87 +/ 88 DiceRollResults[] rolls() @safe pure { 89 DiceRollResults[] results; 90 foreach (i; 0..sets) { 91 results ~= roll(); 92 } 93 return results; 94 } 95 /++ 96 + Roll one set of dice according to the set parameters. 97 +/ 98 DiceRollResults roll() @safe pure { 99 DiceRollResults output; 100 import std.algorithm : makeIndex, sort, sum; 101 import std.range : indexed; 102 uint[] rolls; 103 foreach (die; 0..count) { 104 auto result = rollOne(); 105 output.rolls ~= Roll(true, result); 106 while ((result < lowerRerollThreshold) || (result > upperRerollThreshold)) { 107 output.rolls[$-1].counted = false; 108 result = rollOne(); 109 output.rolls ~= Roll(true, result); 110 } 111 rolls ~= result; 112 } 113 if (numDiceQualified < count) { 114 if (takeLowestRolls) { 115 rolls.sort!"a < b"(); 116 uint[] index = new uint[](output.rolls.length); 117 makeIndex!"a.value < b.value"(output.rolls, index); 118 int x = 0; 119 foreach (roll; index) { 120 if (output.rolls[roll].counted) { 121 x++; 122 } 123 if (x > numDiceQualified) { 124 output.rolls[roll].counted = false; 125 } 126 } 127 } else { 128 rolls.sort!"a > b"(); 129 uint[] index = new uint[](output.rolls.length); 130 makeIndex!"a.value > b.value"(output.rolls, index); 131 int x = 0; 132 foreach (roll; index) { 133 if (output.rolls[roll].counted) { 134 x++; 135 } 136 if (x > numDiceQualified) { 137 output.rolls[roll].counted = false; 138 } 139 } 140 } 141 rolls = rolls[0..numDiceQualified]; 142 } 143 output.total = rolls.sum + valAdd; 144 return output; 145 } 146 /++ 147 + Roll the dice and count the number of rolls that met the specified target. 148 + 149 + Params: 150 + bounding = whether the target is an upper/lower bound 151 + val = target to reach 152 +/ 153 DiceTargetResults meetTarget(Bounding bounding = Bounding.lowerInclusive)(uint val) @safe { 154 DiceTargetResults output; 155 foreach (die; 0..count) { 156 auto result = rollOne(); 157 bool satisfied = false; 158 159 static if (bounding == Bounding.upper) { 160 satisfied = (result < val); 161 } else static if (bounding == Bounding.upperInclusive) { 162 satisfied = (result <= val); 163 } else static if (bounding == Bounding.lower) { 164 satisfied = (result > val); 165 } else static if (bounding == Bounding.lowerInclusive) { 166 satisfied = (result >= val); 167 } 168 169 output.rolls ~= Roll(satisfied, result); 170 171 if (satisfied) { 172 output.count++; 173 } 174 } 175 return output; 176 } 177 private uint rollOne() @safe pure { 178 import std.algorithm.comparison : max, min; 179 import std.random : uniform; 180 final switch (weighting) { 181 case Weight.noWeighting: 182 return uniform!"[]"(1, sides, rng); 183 case Weight.negativeWeighting: 184 auto x = uniform!"[]"(1, sides, rng); 185 if (x > sides/2) { 186 return x-uniform!"[]"(0, sides/2, rng); 187 } else { 188 return x; 189 } 190 case Weight.positiveWeighting: 191 return min(sides, uniform!"[]"(1, sides, rng)+uniform!"[]"(0, sides/2, rng)); 192 } 193 } 194 static if (isSeedable!(RNG, uint)) { 195 /++ 196 + Initializes dice with a seed. 197 +/ 198 this(uint seed) @safe pure nothrow @nogc { 199 rng = RNG(seed); 200 } 201 } 202 } 203 /// 204 @safe pure unittest { 205 auto dice = dice(0); 206 dice.sides = 20; 207 assert(dice.roll().total == 5); 208 } 209 /** 210 * Generates a set of dice from a string. 211 * 212 * Supports various modifiers. 213 * [rolls]# = Number of sets of dice 214 * -/+[count]/ = Count only the x highest or lowest dice 215 * d[sides] = Number of sides on each die 216 * w-/+ = Whether to skew the dice values negatively or positively 217 * r[num] = Reroll dice above/below this value 218 * 219 * No modifiers is the same as just specifying number of sides. 220 * Defaults to 1 set containing 1 unweighted die with six sides. 221 * 222 * Params: 223 * str = string input 224 * seed = seed used to initialize the dice 225 */ 226 auto genDice(string str, uint seed = unpredictableSeed()) @safe { 227 import std.conv : to; 228 import std.regex; 229 uint sets = 1; 230 uint count = 1; 231 uint sides = 6; 232 uint numDiceQualified = uint.max; 233 bool takeLowestRolls = false; 234 uint lowerRerollThreshold = 0; 235 uint upperRerollThreshold = uint.max; 236 int valAdd = 0; 237 Weight weighting = Weight.noWeighting; 238 auto setRegex = ctRegex!`(\d+)#`; 239 if (auto countMatched = matchFirst(str, setRegex)) { 240 sets = countMatched[1].to!uint; 241 str = replaceFirst(str, setRegex, ""); 242 } 243 auto ndqRegex = ctRegex!`(-?)(\d+)/`; 244 if (auto countMatched = matchFirst(str, ndqRegex)) { 245 takeLowestRolls = countMatched[1] == "-"; 246 numDiceQualified = countMatched[2].to!uint; 247 str = replaceFirst(str, ndqRegex, ""); 248 } 249 auto lrtRegex = ctRegex!`r(\d+)`; 250 if (auto countMatched = matchFirst(str, lrtRegex)) { 251 lowerRerollThreshold = countMatched[1].to!uint; 252 str = replaceFirst(str, lrtRegex, ""); 253 } 254 auto weightRegex = ctRegex!`w([+-])`; 255 if (auto countMatched = matchFirst(str, weightRegex)) { 256 if (countMatched[1] == "+") { 257 weighting = Weight.positiveWeighting; 258 } else { 259 weighting = Weight.negativeWeighting; 260 } 261 str = replaceFirst(str, weightRegex, ""); 262 } 263 auto cmRegex = ctRegex!`(\d+)d`; 264 if (auto countMatched = matchFirst(str, cmRegex)) { 265 count = countMatched[1].to!uint; 266 str = replaceFirst(str, cmRegex, ""); 267 } 268 auto vaRegex = ctRegex!`\+(\d+)`; 269 if (auto countMatched = matchFirst(str, vaRegex)) { 270 valAdd = countMatched[1].to!int; 271 str = replaceFirst(str, vaRegex, ""); 272 } 273 auto vanRegex = ctRegex!`-(\d+)`; 274 if (auto countMatched = matchFirst(str, vanRegex)) { 275 valAdd = -countMatched[1].to!int; 276 str = replaceFirst(str, vanRegex, ""); 277 } 278 auto sidesRegex = ctRegex!`d(\d+)`; 279 if (auto countMatched = matchFirst(str, sidesRegex)) { 280 sides = countMatched[1].to!uint; 281 } else if (str == "") { 282 //just stick to the default 283 } else { 284 sides = str.to!uint; 285 } 286 if (numDiceQualified > count) { 287 numDiceQualified = count; 288 } 289 auto output = dice(seed); 290 output.sets = sets; 291 output.count = count; 292 output.sides = sides; 293 output.numDiceQualified = numDiceQualified; 294 output.takeLowestRolls = takeLowestRolls; 295 output.lowerRerollThreshold = lowerRerollThreshold; 296 output.upperRerollThreshold = upperRerollThreshold; 297 output.valAdd = valAdd; 298 output.weighting = weighting; 299 return output; 300 } 301 /// 302 @safe unittest { 303 auto rolls = genDice("2d8r1").roll(); 304 } 305 @safe unittest { 306 import std.stdio : writeln; 307 with(genDice("")) { 308 assert(sets == 1); 309 assert(count == 1); 310 assert(sides == 6); 311 assert(weighting == Weight.noWeighting); 312 } 313 with(genDice("30d6")) { 314 assert(count == 30); 315 assert(sides == 6); 316 assert(weighting == Weight.noWeighting); 317 assert(numDiceQualified == 30); 318 } 319 with(genDice("30d6+5")) { 320 assert(count == 30); 321 assert(sides == 6); 322 assert(weighting == Weight.noWeighting); 323 assert(numDiceQualified == 30); 324 assert(valAdd == 5); 325 } 326 with(genDice("20")) { 327 assert(count == 1); 328 assert(sides == 20); 329 assert(weighting == Weight.noWeighting); 330 assert(numDiceQualified == 1); 331 } 332 with(genDice("d20")) { 333 assert(count == 1); 334 assert(sides == 20); 335 assert(weighting == Weight.noWeighting); 336 assert(numDiceQualified == 1); 337 } 338 with(genDice("6#4d6")) { 339 assert(sets == 6); 340 assert(count == 4); 341 assert(sides == 6); 342 assert(weighting == Weight.noWeighting); 343 assert(numDiceQualified == 4); 344 } 345 with(genDice("6#3/4d6")) { 346 assert(sets == 6); 347 assert(count == 4); 348 assert(sides == 6); 349 assert(weighting == Weight.noWeighting); 350 assert(numDiceQualified == 3); 351 assert(!takeLowestRolls); 352 } 353 with(genDice("-3/4d6")) { 354 assert(count == 4); 355 assert(sides == 6); 356 assert(weighting == Weight.noWeighting); 357 assert(numDiceQualified == 3); 358 assert(takeLowestRolls); 359 } 360 with(genDice("6#4d6r1")) { 361 assert(sets == 6); 362 assert(count == 4); 363 assert(sides == 6); 364 assert(weighting == Weight.noWeighting); 365 assert(numDiceQualified == 4); 366 assert(lowerRerollThreshold == 1); 367 } 368 with(genDice("2d12+1")) { 369 assert(count == 2); 370 assert(sides == 12); 371 assert(weighting == Weight.noWeighting); 372 assert(numDiceQualified == 2); 373 assert(valAdd == 1); 374 } 375 with(genDice("4d6w-")) { 376 assert(count == 4); 377 assert(sides == 6); 378 assert(weighting == Weight.negativeWeighting); 379 assert(numDiceQualified == 4); 380 } 381 }