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 }