1 /// Functions for "luck", or influenced results
2 module unexpected.influence;
3 
4 import std.range : ElementType, isInfinite;
5 
6 enum hasMinMax(T) = is(typeof(T.min) == typeof(T.max)) && is(typeof(T.min) == T);
7 
8 auto influencedChoice(Range, Rand, Element = ElementType!Range)(double luck, ref Rand rng, Range range) if (hasMinMax!(ElementType!Range)) {
9 	return influencedChoiceCommon!true(luck, rng, range, ElementType!Range.min, ElementType!Range.max);
10 }
11 auto influencedChoice(Range, Rand, Element = ElementType!Range)(double luck, ref Rand rng, Range range, Element worst, Element best) {
12 	return influencedChoiceCommon!true(luck, rng, range, worst, best);
13 }
14 auto influencedChoice(Range, Rand, Element = ElementType!Range)(double luck, ref Rand rng, Range range) if (!isInfinite!Range && !hasMinMax!(ElementType!Range)) {
15 	return influencedChoiceCommon!false(luck, rng, range, Element.init, Element.init);
16 }
17 auto influencedChoiceCommon(bool useWorstBest = true, Range, Rand, Element = ElementType!Range)(double luck, ref Rand rng, Range range, Element worst, Element best)
18 	in(!range.empty, "Nothing to choose")
19 	in(!useWorstBest || (best >= worst), "Worst value must be less than best")
20 	out(result; !useWorstBest || (result >= worst), "Result is less than worst value")
21 	out(result; !useWorstBest || (result <= best), "Result is greater than best value")
22 {
23 	import std.algorithm.comparison : min;
24 	import std.math : isInfinity;
25 	import std.random : uniform01;
26 	static if (useWorstBest) {
27 		if (luck.isInfinity) {
28 			if (luck > 0) {
29 				return best;
30 			} else {
31 				return worst;
32 			}
33 		}
34 	}
35 	auto chosen = range.front;
36 	range.popFront();
37 	bool negative = luck < 0.0;
38 	if (luck < 0.0) {
39 		luck = -luck;
40 	}
41 	while ((luck > 0.0) && !range.empty) {
42 		static if (useWorstBest) {
43 			if (chosen > best) {
44 				return best;
45 			} else if (chosen < worst) {
46 				return worst;
47 			}
48 		}
49 		const remaining = min(luck, 1.0);
50 		luck -= remaining;
51 		if (remaining >= uniform01(rng)) {
52 			auto next = range.front;
53 			range.popFront();
54 			if (negative ^ (next > chosen)) {
55 				chosen = next;
56 			}
57 		}
58 	}
59 	return chosen;
60 }
61 
62 @safe pure unittest {
63 	import std.random : Random, uniform;
64 	import std.range : generate;
65 	static struct Result {
66 		enum min = Result(0);
67 		enum max = Result(100);
68 		int x;
69 		int opCmp(Result b) const pure @safe {
70 			return x - b.x;
71 		}
72 	}
73 	Random rand;
74 	enum iterations = 100;
75 	double test(double luck) {
76 		long total;
77 		foreach (i; 0 .. iterations) {
78 			total += influencedChoice(luck, rand, generate!(() => Result(uniform(0, 100, rand)))).x;
79 		}
80 		return total / double(iterations);
81 	}
82 	assert(test(0.0) < test(1.0));
83 	assert(test(0.0) > test(-1.0));
84 	assert(test(double.infinity) == 100);
85 	assert(test(-double.infinity) == 0);
86 
87 	double test2(double luck, int min = int.min, int max = int.max) {
88 		long total;
89 		foreach (i; 0 .. iterations) {
90 			const value = influencedChoice(luck, rand, generate!(() => uniform(0, 100, rand)), min, max);
91 			assert(value >= min);
92 			assert(value <= max);
93 			total += value;
94 		}
95 		return total / double(iterations);
96 	}
97 	assert(test2(0.0) < test2(1.0));
98 	assert(test2(0.0) > test2(-1.0));
99 	assert(test2(double.infinity) == int.max);
100 	assert(test2(-double.infinity) == int.min);
101 	assert(test2(double.infinity, 0, 100) == 100);
102 	assert(test2(-double.infinity, 0, 100) == 0);
103 	assert(test2(0.0, 0, 100) > test2(-1.0, 0, 100));
104 }
105 
106 @safe pure unittest {
107 	import std.random : Random, uniform;
108 	import std.range : generate, take;
109 	static struct Result {
110 		int x;
111 		int opCmp(Result b) const pure @safe {
112 			return x - b.x;
113 		}
114 	}
115 	Random rand;
116 	enum iterations = 100;
117 	double test(double luck, int min = int.min, int max = int.max) {
118 		long total;
119 		foreach (i; 0 .. iterations) {
120 			const value = influencedChoice(luck, rand, generate!(() => Result(uniform(0, 100, rand))).take(100));
121 			assert(value.x >= min);
122 			assert(value.x <= max);
123 			total += value.x;
124 		}
125 		return total / double(iterations);
126 	}
127 	assert(test(0.0) < test(1.0));
128 	assert(test(0.0) > test(-1.0));
129 	assert(test(double.infinity) > test(-double.infinity));
130 	assert(test(double.infinity, 0, 100) > test(-double.infinity, 0, 100));
131 	assert(test(0.0, 0, 100) > test(-1.0, 0, 100));
132 }
133 
134 auto influencedWeightedChoice(Range, Weights, Rand, Element = ElementType!Range)(double luck, ref Rand rng, Range range, Weights weights) if (hasMinMax!(ElementType!Range)) {
135 	return influencedWeightedChoice(luck, rng, range, weights, Element.min, Element.max);
136 }
137 auto influencedWeightedChoice(Range, Weights, Rand, Element = ElementType!Range)(double luck, ref Rand rng, Range range, Weights weights) if (!hasMinMax!(ElementType!Range)) {
138 	import std.random : dice;
139 	import std.range : drop, front, generate;
140 	return influencedChoiceCommon!false(luck, rng, generate!({ return range.drop(dice(rng, weights)).front; }), range.front, range.front);
141 }
142 auto influencedWeightedChoice(Range, Weights, Rand, Element = ElementType!Range)(double luck, ref Rand rng, Range range, Weights weights, Element worst, Element best) {
143 	import std.random : dice;
144 	import std.range : drop, front, generate;
145 	return influencedChoiceCommon!true(luck, rng, generate!({ return range.drop(dice(rng, weights)).front; }), worst, best);
146 }
147 
148 @safe pure unittest {
149 	import std.random : Random;
150 	import std.range : iota;
151 	Random rand;
152 	enum iterations = 100;
153 	enum vals = [100, 50, 33, 0, 25];
154 	double test2(double luck, int min, int max) {
155 		long total;
156 		foreach (i; 0 .. iterations) {
157 			const value = influencedWeightedChoice(luck, rand, vals, iota(5), min, max);
158 			assert(value >= min);
159 			assert(value <= max);
160 			total += value;
161 		}
162 		return total / double(iterations);
163 	}
164 	assert(test2(0.0, 0, 100) < test2(1.0, 0, 100));
165 	assert(test2(0.0, 0, 100) > test2(-1.0, 0, 100));
166 	assert(test2(double.infinity, 0, 100) == 100);
167 	assert(test2(-double.infinity, 0, 100) == 0);
168 	assert(test2(0.0, 0, 100) > test2(-1.0, 0, 100));
169 }
170 
171 auto influencedUniform(Rand, T)(double luck, ref Rand rng, T worst, T best) {
172 	import std.random : uniform;
173 	static auto uniformRange(Rand rng, T worst, T best) {
174 		struct Result {
175 			T front;
176 			enum bool empty = false;
177 			void popFront() {
178 				front = uniform(worst, best, rng);
179 			}
180 		}
181 		Result result;
182 		result.popFront();
183 		return result;
184 	}
185 	return influencedChoiceCommon!true(luck, rng, uniformRange(rng, worst, best), worst, best);
186 }
187 
188 @safe pure unittest {
189 	import std.random : Random;
190 	Random rand;
191 	enum iterations = 100;
192 	double test(double luck, int min, int max) {
193 		long total;
194 		foreach (i; 0 .. iterations) {
195 			const value = influencedUniform(luck, rand, min, max);
196 			assert(value >= min);
197 			assert(value <= max);
198 			total += value;
199 		}
200 		return total / double(iterations);
201 	}
202 	assert(test(0.0, 0, 100) < test(1.0, 0, 100));
203 	assert(test(0.0, 0, 100) > test(-1.0, 0, 100));
204 	assert(test(double.infinity, 0, 100) == 100);
205 	assert(test(-double.infinity, 0, 100) == 0);
206 	assert(test(0.0, 0, 100) > test(-1.0, 0, 100));
207 }