games_dice 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +11 -1
- data/.gitignore +0 -1
- data/.rubocop.yml +13 -48
- data/CHANGELOG.md +19 -0
- data/Gemfile +5 -3
- data/README.md +46 -5
- data/Rakefile +12 -5
- data/ext/games_dice/extconf.rb +0 -3
- data/ext/games_dice/probabilities.c +293 -193
- data/ext/games_dice/probabilities.h +2 -12
- data/games_dice.gemspec +1 -1
- data/lib/games_dice/bunch.rb +50 -36
- data/lib/games_dice/bunch_helpers.rb +35 -14
- data/lib/games_dice/complex_die.rb +64 -22
- data/lib/games_dice/complex_die_helpers.rb +122 -144
- data/lib/games_dice/dice.rb +61 -17
- data/lib/games_dice/dice_helpers.rb +37 -0
- data/lib/games_dice/die_result.rb +6 -8
- data/lib/games_dice/marshal.rb +2 -7
- data/lib/games_dice/parser.rb +30 -0
- data/lib/games_dice/probability_budget.rb +90 -0
- data/lib/games_dice/probability_calculation.rb +95 -0
- data/lib/games_dice/probability_shortcuts.rb +65 -0
- data/lib/games_dice/recipe_cache.rb +155 -0
- data/lib/games_dice/version.rb +1 -1
- data/lib/games_dice.rb +4 -0
- data/script/check_documentation.rb +10 -0
- data/spec/games_dice/bunch_regressions_spec.rb +46 -0
- data/spec/{bunch_spec.rb → games_dice/bunch_spec.rb} +65 -112
- data/spec/games_dice/complex_die_budget_spec.rb +60 -0
- data/spec/games_dice/complex_die_limits_spec.rb +119 -0
- data/spec/games_dice/complex_die_spec.rb +228 -0
- data/spec/games_dice/dice_mutation_spec.rb +165 -0
- data/spec/games_dice/dice_regressions_spec.rb +50 -0
- data/spec/games_dice/dice_spec.rb +61 -0
- data/spec/games_dice/die_result_spec.rb +221 -0
- data/spec/{die_spec.rb → games_dice/die_spec.rb} +15 -14
- data/spec/{map_rule_spec.rb → games_dice/map_rule_spec.rb} +8 -1
- data/spec/{parser_spec.rb → games_dice/parser_spec.rb} +12 -2
- data/spec/games_dice/probabilities_boundaries_spec.rb +161 -0
- data/spec/games_dice/probabilities_marshal_spec.rb +62 -0
- data/spec/games_dice/probabilities_spec.rb +484 -0
- data/spec/games_dice/probability_budget_spec.rb +17 -0
- data/spec/{reroll_rule_spec.rb → games_dice/reroll_rule_spec.rb} +5 -1
- data/spec/games_dice_spec.rb +382 -0
- data/spec/helpers.rb +64 -2
- data/spec/simple_cov_spec.rb +42 -0
- data/spec/support/coverage.rb +12 -0
- data/spec/support/validation.rb +14 -0
- data/spec/yard/cli/stats_spec.rb +43 -0
- metadata +29 -12
- data/spec/complex_die_spec.rb +0 -314
- data/spec/dice_spec.rb +0 -33
- data/spec/die_result_spec.rb +0 -254
- data/spec/fixtures/probs_fair_die_6.dat +0 -2
- data/spec/probability_spec.rb +0 -584
- data/spec/readme_spec.rb +0 -410
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'helpers'
|
|
4
|
+
require 'objspace'
|
|
5
|
+
|
|
6
|
+
describe GamesDice::Probabilities, :aggregate_failures do
|
|
7
|
+
let(:distributions) do
|
|
8
|
+
{
|
|
9
|
+
two: described_class.for_fair_die(2), four: described_class.for_fair_die(4),
|
|
10
|
+
six: described_class.for_fair_die(6), ten: described_class.for_fair_die(10),
|
|
11
|
+
twenty: described_class.for_fair_die(20), uniform_four: described_class.new([0.25, 0.25, 0.25, 0.25], 1),
|
|
12
|
+
weighted_four: described_class.new([0.1, 0.2, 0.3, 0.4], 1),
|
|
13
|
+
left_asymmetric: described_class.new([0.7, 0.0, 0.3], 2),
|
|
14
|
+
right_asymmetric: described_class.new([0.5, 0.3, 0.2], 2),
|
|
15
|
+
offset_asymmetric: described_class.new([0.4, 0.2, 0.4], -1)
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def distribution(name)
|
|
20
|
+
distributions.fetch(name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'class methods' do
|
|
24
|
+
describe '#new' do
|
|
25
|
+
it 'create a new distribution from an array and offset' do
|
|
26
|
+
pr = described_class.new([1.0], 1)
|
|
27
|
+
expect(pr).to be_a described_class
|
|
28
|
+
expect(pr.to_h).to be_valid_distribution
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'raise an error if passed incorrect parameter types' do
|
|
32
|
+
expect { described_class.new([nil], 20) }.to raise_error TypeError
|
|
33
|
+
expect { described_class.new([0.3, nil, 0.5], 7) }.to raise_error TypeError
|
|
34
|
+
expect { described_class.new([0.3, 0.2, 0.5], {}) }.to raise_error TypeError
|
|
35
|
+
expect { described_class.new({ x: :y }, 17) }.to raise_error TypeError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'raise an error if distribution is incomplete or inaccurate' do
|
|
39
|
+
expect { described_class.new([0.3, 0.2, 0.6], 3) }.to raise_error ArgumentError
|
|
40
|
+
expect { described_class.new([], 1) }.to raise_error ArgumentError
|
|
41
|
+
expect { described_class.new([0.9], 1) }.to raise_error ArgumentError
|
|
42
|
+
expect { described_class.new([-0.9, 0.2, 0.9], 1) }.to raise_error ArgumentError
|
|
43
|
+
expect { described_class.new([1.1], 1) }.to raise_error ArgumentError
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#for_fair_die' do
|
|
48
|
+
it 'create a new distribution based on number of sides' do
|
|
49
|
+
(1..20).each { |sides| expect_fair_die(described_class, sides) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'raise an error if number of sides is not an integer' do
|
|
53
|
+
expect { described_class.for_fair_die({}) }.to raise_error TypeError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'raise an error if number of sides is too low or too high' do
|
|
57
|
+
expect { described_class.for_fair_die(0) }.to raise_error ArgumentError
|
|
58
|
+
expect { described_class.for_fair_die(1_000_001) }.to raise_error ArgumentError
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '#add_distributions' do
|
|
63
|
+
it 'combine two distributions to create a third one' do
|
|
64
|
+
d4a = described_class.new([1.0 / 4, 1.0 / 4, 1.0 / 4, 1.0 / 4], 1)
|
|
65
|
+
d4b = described_class.new([1.0 / 10, 2.0 / 10, 3.0 / 10, 4.0 / 10], 1)
|
|
66
|
+
pr = described_class.add_distributions(d4a, d4b)
|
|
67
|
+
expect(pr.to_h).to be_valid_distribution
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'calculate a classic 2d6 distribution accurately' do
|
|
71
|
+
h = described_class.add_distributions(distribution(:six), distribution(:six)).to_h
|
|
72
|
+
expect(h).to be_valid_distribution
|
|
73
|
+
expected = (2..12).to_h { |result| [result, (6 - (7 - result).abs) / 36.0] }
|
|
74
|
+
expect_probability_values(h, expected, tolerance: 1e-9, total_tolerance: nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'raise an error if either parameter is not a GamesDice::Probabilities object' do
|
|
78
|
+
d10 = described_class.for_fair_die(10)
|
|
79
|
+
expect { described_class.add_distributions('', 6) }.to raise_error TypeError
|
|
80
|
+
expect { described_class.add_distributions(d10, 6) }.to raise_error TypeError
|
|
81
|
+
expect { described_class.add_distributions('', d10) }.to raise_error TypeError
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '#add_distributions_mult' do
|
|
86
|
+
it 'combine two multiplied distributions to create a third one' do
|
|
87
|
+
d4a = described_class.new([1.0 / 4, 1.0 / 4, 1.0 / 4, 1.0 / 4], 1)
|
|
88
|
+
d4b = described_class.new([1.0 / 10, 2.0 / 10, 3.0 / 10, 4.0 / 10], 1)
|
|
89
|
+
pr = described_class.add_distributions_mult(2, d4a, -1, d4b)
|
|
90
|
+
expect(pr.to_h).to be_valid_distribution
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "calculate a distribution for '1d6 - 1d4' accurately" do
|
|
94
|
+
h = described_class.add_distributions_mult(1, distribution(:six), -1, distribution(:four)).to_h
|
|
95
|
+
expect(h).to be_valid_distribution
|
|
96
|
+
numerators = [1, 2, 3, 4, 4, 4, 3, 2, 1]
|
|
97
|
+
expected = (-3..5).zip(numerators.map { |numerator| numerator / 24.0 }).to_h
|
|
98
|
+
expect_probability_values(h, expected, tolerance: 1e-9, total_tolerance: nil)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'add asymmetric distributions accurately' do
|
|
102
|
+
h = described_class.add_distributions_mult(1, distribution(:left_asymmetric), 2,
|
|
103
|
+
distribution(:right_asymmetric)).to_h
|
|
104
|
+
expect(h).to be_valid_distribution
|
|
105
|
+
expected = { 6 => 0.7 * 0.5, 8 => (0.7 * 0.3) + (0.3 * 0.5),
|
|
106
|
+
10 => (0.7 * 0.2) + (0.3 * 0.3), 12 => 0.3 * 0.2 }
|
|
107
|
+
expect_probability_values(h, expected, tolerance: 1e-9, total_tolerance: nil)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'raise an error if passed incorrect objects for distributions' do
|
|
111
|
+
d10 = described_class.for_fair_die(10)
|
|
112
|
+
expect { described_class.add_distributions_mult(1, '', -1, 6) }.to raise_error TypeError
|
|
113
|
+
expect { described_class.add_distributions_mult(2, d10, 3, 6) }.to raise_error TypeError
|
|
114
|
+
expect { described_class.add_distributions_mult(1, '', -1, d10) }.to raise_error TypeError
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'raise an error if passed incorrect objects for multipliers' do
|
|
118
|
+
d10 = described_class.for_fair_die(10)
|
|
119
|
+
expect { described_class.add_distributions_mult({}, d10, [], d10) }.to raise_error TypeError
|
|
120
|
+
expect { described_class.add_distributions_mult([7], d10, 3, d10) }.to raise_error TypeError
|
|
121
|
+
expect { described_class.add_distributions_mult(1, d10, {}, d10) }.to raise_error TypeError
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '#from_h' do
|
|
126
|
+
it 'create a Probabilities object from a valid hash' do
|
|
127
|
+
pr = described_class.from_h({ 7 => 0.5, 9 => 0.5 })
|
|
128
|
+
expect(pr).to be_a described_class
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'create the same distribution when hash keys descend' do
|
|
132
|
+
pr = described_class.from_h({ 9 => 0.5, 7 => 0.5 })
|
|
133
|
+
expect(pr.to_h).to eql({ 7 => 0.5, 9 => 0.5 })
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'raise an ArgumentError when called with a non-valid hash' do
|
|
137
|
+
expect { described_class.from_h({ 7 => 0.5, 9 => 0.6 }) }.to raise_error ArgumentError
|
|
138
|
+
expect { described_class.from_h({ 7 => 0.4, 9 => 0.5 }) }.to raise_error ArgumentError
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'raise an TypeError when called with data that is not a hash' do
|
|
142
|
+
expect { described_class.from_h(:foo) }.to raise_error TypeError
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'raise a TypeError when called when keys and values are not all integers and floats' do
|
|
146
|
+
expect { described_class.from_h({ 'x' => 0.5, 9 => 0.5 }) }.to raise_error TypeError
|
|
147
|
+
expect { described_class.from_h({ 7 => [], 9 => 0.5 }) }.to raise_error TypeError
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'raise an ArgumentError when results are spread very far apart' do
|
|
151
|
+
expect { described_class.from_h({ 0 => 0.5, 2_000_000 => 0.5 }) }.to raise_error ArgumentError
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe 'instance methods' do
|
|
157
|
+
describe '#clone' do
|
|
158
|
+
it 'create an independent object with the same distribution' do
|
|
159
|
+
copy = distribution(:offset_asymmetric).clone
|
|
160
|
+
expect(copy).not_to equal(distribution(:offset_asymmetric))
|
|
161
|
+
expect(copy.to_h).to eql(distribution(:offset_asymmetric).to_h)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe 'native memory accounting' do
|
|
166
|
+
it 'include allocated probability arrays' do
|
|
167
|
+
empty = described_class.allocate
|
|
168
|
+
expect(ObjectSpace.memsize_of(distribution(:ten))).to be > ObjectSpace.memsize_of(empty)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
describe '#each' do
|
|
173
|
+
it 'iterate through all result/probability pairs' do
|
|
174
|
+
yielded = distribution(:four).to_enum.map { |result, probability| [result, probability] }
|
|
175
|
+
expect(yielded).to eql [[1, 0.25], [2, 0.25], [3, 0.25], [4, 0.25]]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'skip zero probabilities' do
|
|
179
|
+
pr_plus_minus = described_class.new([0.5, 0.0, 0.5], -1)
|
|
180
|
+
yielded = pr_plus_minus.to_enum.map { |result, probability| [result, probability] }
|
|
181
|
+
expect(yielded).to eql [[-1, 0.5], [1, 0.5]]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe '#p_eql' do
|
|
186
|
+
it 'return probability of getting a number inside the range' do
|
|
187
|
+
expect(distribution(:two).p_eql(2)).to be_within(1.0e-9).of 0.5
|
|
188
|
+
expect(distribution(:four).p_eql(1)).to be_within(1.0e-9).of 0.25
|
|
189
|
+
expect(distribution(:six).p_eql(6)).to be_within(1.0e-9).of 1.0 / 6
|
|
190
|
+
expect(distribution(:ten).p_eql(3)).to be_within(1.0e-9).of 0.1
|
|
191
|
+
expect(distribution(:offset_asymmetric).p_eql(-1)).to be_within(1.0e-9).of 0.4
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'return 0.0 for values not covered by distribution' do
|
|
195
|
+
expect(distribution(:two).p_eql(3)).to be 0.0
|
|
196
|
+
expect(distribution(:four).p_eql(-1)).to be 0.0
|
|
197
|
+
expect(distribution(:six).p_eql(8)).to be 0.0
|
|
198
|
+
expect(distribution(:ten).p_eql(11)).to be 0.0
|
|
199
|
+
expect(distribution(:offset_asymmetric).p_eql(2)).to be 0.0
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
203
|
+
expect { distribution(:two).p_eql([]) }.to raise_error TypeError
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
describe '#p_gt' do
|
|
208
|
+
it 'return probability of getting a number greater than target' do
|
|
209
|
+
# Trying more than one, due to possibilities of caching error (in pure Ruby implementation)
|
|
210
|
+
cases = [[distribution(:two), 1, 0.5], [distribution(:four), 3, 0.25],
|
|
211
|
+
[distribution(:six), 2, 4.0 / 6], [distribution(:ten), 6, 0.4],
|
|
212
|
+
[distribution(:offset_asymmetric), -2, 1.0],
|
|
213
|
+
[distribution(:offset_asymmetric), -1, 0.6], [distribution(:offset_asymmetric), 0, 0.4],
|
|
214
|
+
[distribution(:offset_asymmetric), 1, 0.0]]
|
|
215
|
+
cases.each { |probability, target, expected| expect(probability.p_gt(target)).to be_within(1e-9).of(expected) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'return 0.0 when the target number is equal or higher than maximum possible' do
|
|
219
|
+
expect(distribution(:two).p_gt(2)).to be 0.0
|
|
220
|
+
expect(distribution(:four).p_gt(5)).to be 0.0
|
|
221
|
+
expect(distribution(:six).p_gt(6)).to be 0.0
|
|
222
|
+
expect(distribution(:ten).p_gt(20)).to be 0.0
|
|
223
|
+
expect(distribution(:offset_asymmetric).p_gt(3)).to be 0.0
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'return 1.0 when the target number is lower than minimum' do
|
|
227
|
+
expect(distribution(:two).p_gt(0)).to be 1.0
|
|
228
|
+
expect(distribution(:four).p_gt(-5)).to be 1.0
|
|
229
|
+
expect(distribution(:six).p_gt(0)).to be 1.0
|
|
230
|
+
expect(distribution(:ten).p_gt(-200)).to be 1.0
|
|
231
|
+
expect(distribution(:offset_asymmetric).p_gt(-2)).to be 1.0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
235
|
+
expect { distribution(:two).p_gt({}) }.to raise_error TypeError
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
describe '#p_ge' do
|
|
240
|
+
it 'return probability of getting a number greater than or equal to target' do
|
|
241
|
+
expect(distribution(:two).p_ge(2)).to be_within(1.0e-9).of 0.5
|
|
242
|
+
expect(distribution(:four).p_ge(3)).to be_within(1.0e-9).of 0.5
|
|
243
|
+
expect(distribution(:six).p_ge(2)).to be_within(1.0e-9).of 5.0 / 6
|
|
244
|
+
expect(distribution(:ten).p_ge(6)).to be_within(1.0e-9).of 0.5
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'return 0.0 when the target number is higher than maximum possible' do
|
|
248
|
+
expect(distribution(:two).p_ge(6)).to be 0.0
|
|
249
|
+
expect(distribution(:four).p_ge(5)).to be 0.0
|
|
250
|
+
expect(distribution(:six).p_ge(7)).to be 0.0
|
|
251
|
+
expect(distribution(:ten).p_ge(20)).to be 0.0
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'return 1.0 when the target number is lower than or equal to minimum possible' do
|
|
255
|
+
expect(distribution(:two).p_ge(1)).to be 1.0
|
|
256
|
+
expect(distribution(:four).p_ge(-5)).to be 1.0
|
|
257
|
+
expect(distribution(:six).p_ge(1)).to be 1.0
|
|
258
|
+
expect(distribution(:ten).p_ge(-200)).to be 1.0
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
262
|
+
expect { distribution(:four).p_ge({}) }.to raise_error TypeError
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
describe '#p_le' do
|
|
267
|
+
it 'return probability of getting a number less than or equal to target' do
|
|
268
|
+
expect(distribution(:two).p_le(1)).to be_within(1.0e-9).of 0.5
|
|
269
|
+
expect(distribution(:four).p_le(2)).to be_within(1.0e-9).of 0.5
|
|
270
|
+
expect(distribution(:six).p_le(2)).to be_within(1.0e-9).of 2.0 / 6
|
|
271
|
+
expect(distribution(:ten).p_le(6)).to be_within(1.0e-9).of 0.6
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'return 1.0 when the target number is higher than or equal to maximum possible' do
|
|
275
|
+
expect(distribution(:two).p_le(6)).to be 1.0
|
|
276
|
+
expect(distribution(:four).p_le(4)).to be 1.0
|
|
277
|
+
expect(distribution(:six).p_le(7)).to be 1.0
|
|
278
|
+
expect(distribution(:ten).p_le(10)).to be 1.0
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it 'return 0.0 when the target number is lower than minimum possible' do
|
|
282
|
+
expect(distribution(:two).p_le(0)).to be 0.0
|
|
283
|
+
expect(distribution(:four).p_le(-5)).to be 0.0
|
|
284
|
+
expect(distribution(:six).p_le(0)).to be 0.0
|
|
285
|
+
expect(distribution(:ten).p_le(-200)).to be 0.0
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
289
|
+
expect { distribution(:four).p_le([]) }.to raise_error TypeError
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
describe '#p_lt' do
|
|
294
|
+
it 'return probability of getting a number less than target' do
|
|
295
|
+
expect(distribution(:two).p_lt(2)).to be_within(1.0e-9).of 0.5
|
|
296
|
+
expect(distribution(:four).p_lt(3)).to be_within(1.0e-9).of 0.5
|
|
297
|
+
expect(distribution(:six).p_lt(2)).to be_within(1.0e-9).of 1 / 6.0
|
|
298
|
+
expect(distribution(:ten).p_lt(6)).to be_within(1.0e-9).of 0.5
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'return 1.0 when the target number is higher than maximum possible' do
|
|
302
|
+
expect(distribution(:two).p_lt(6)).to be 1.0
|
|
303
|
+
expect(distribution(:four).p_lt(5)).to be 1.0
|
|
304
|
+
expect(distribution(:six).p_lt(7)).to be 1.0
|
|
305
|
+
expect(distribution(:ten).p_lt(20)).to be 1.0
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'return 0.0 when the target number is lower than or equal to minimum possible' do
|
|
309
|
+
expect(distribution(:two).p_lt(1)).to be 0.0
|
|
310
|
+
expect(distribution(:four).p_lt(-5)).to be 0.0
|
|
311
|
+
expect(distribution(:six).p_lt(1)).to be 0.0
|
|
312
|
+
expect(distribution(:ten).p_lt(-200)).to be 0.0
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
316
|
+
expect { distribution(:six).p_lt({}) }.to raise_error TypeError
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
describe '#to_h' do
|
|
321
|
+
# This is used loads in other tests
|
|
322
|
+
it 'represent a valid distribution with each integer result associated with its probability' do
|
|
323
|
+
expect(distribution(:two).to_h).to be_valid_distribution
|
|
324
|
+
expect(distribution(:four).to_h).to be_valid_distribution
|
|
325
|
+
expect(distribution(:six).to_h).to be_valid_distribution
|
|
326
|
+
expect(distribution(:ten).to_h).to be_valid_distribution
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
describe '#min' do
|
|
331
|
+
it 'return lowest possible result allowed by distribution' do
|
|
332
|
+
expect(distribution(:two).min).to be 1
|
|
333
|
+
expect(distribution(:four).min).to be 1
|
|
334
|
+
expect(distribution(:six).min).to be 1
|
|
335
|
+
expect(distribution(:ten).min).to be 1
|
|
336
|
+
expect(described_class.add_distributions(distribution(:six), distribution(:ten)).min).to be 2
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
describe '#max' do
|
|
341
|
+
it 'return highest possible result allowed by distribution' do
|
|
342
|
+
expect(distribution(:two).max).to be 2
|
|
343
|
+
expect(distribution(:four).max).to be 4
|
|
344
|
+
expect(distribution(:six).max).to be 6
|
|
345
|
+
expect(distribution(:ten).max).to be 10
|
|
346
|
+
expect(described_class.add_distributions(distribution(:six), distribution(:ten)).max).to be 16
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
describe '#expected' do
|
|
351
|
+
it 'return the weighted mean value' do
|
|
352
|
+
combined = described_class.add_distributions(distribution(:six), distribution(:ten))
|
|
353
|
+
cases = [[distribution(:two), 1.5], [distribution(:four), 2.5], [distribution(:six), 3.5],
|
|
354
|
+
[distribution(:ten), 5.5], [combined, 9.0]]
|
|
355
|
+
cases.each { |probability, expected| expect(probability.expected).to be_within(1e-9).of(expected) }
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
describe '#given_ge' do
|
|
360
|
+
it 'return a new distribution with probabilities calculated assuming value is >= target' do
|
|
361
|
+
conditioned = distribution(:ten).given_ge(4)
|
|
362
|
+
expect(distribution(:two).given_ge(2).to_h).to eql({ 2 => 1.0 })
|
|
363
|
+
expect(conditioned.to_h).to be_valid_distribution
|
|
364
|
+
expect(conditioned.p_eql(3)).to be 0.0
|
|
365
|
+
expect(conditioned.p_eql(10)).to be_within(1.0e-9).of 0.1 / 0.7
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
369
|
+
expect { distribution(:ten).given_ge([]) }.to raise_error TypeError
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
it 'clamp targets below the minimum and reject targets above the maximum' do
|
|
373
|
+
expect(distribution(:ten).given_ge(0).to_h).to eql(distribution(:ten).to_h)
|
|
374
|
+
expect { distribution(:ten).given_ge(11) }.to raise_error(RuntimeError, /divide by zero/)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
describe '#given_le' do
|
|
379
|
+
it 'return a new distribution with probabilities calculated assuming value is <= target' do
|
|
380
|
+
conditioned = distribution(:ten).given_le(4)
|
|
381
|
+
expect(distribution(:two).given_le(2).to_h).to eql({ 1 => 0.5, 2 => 0.5 })
|
|
382
|
+
expect(conditioned.to_h).to be_valid_distribution
|
|
383
|
+
expect(conditioned.p_eql(3)).to be_within(1.0e-9).of 0.1 / 0.4
|
|
384
|
+
expect(conditioned.p_eql(10)).to be 0.0
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
388
|
+
expect { distribution(:ten).given_le({}) }.to raise_error TypeError
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
it 'clamp targets above the maximum and reject targets below the minimum' do
|
|
392
|
+
expect(distribution(:ten).given_le(11).to_h).to eql(distribution(:ten).to_h)
|
|
393
|
+
expect { distribution(:ten).given_le(0) }.to raise_error(RuntimeError, /divide by zero/)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
describe '#repeat_sum' do
|
|
398
|
+
it 'output a valid distribution if params are valid' do
|
|
399
|
+
expect(distribution(:uniform_four).repeat_sum(7).to_h).to be_valid_distribution
|
|
400
|
+
expect(distribution(:weighted_four).repeat_sum(12).to_h).to be_valid_distribution
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
it 'raise an error if any param is unexpected type' do
|
|
404
|
+
d6 = described_class.for_fair_die(6)
|
|
405
|
+
expect { d6.repeat_sum({}) }.to raise_error TypeError
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
it 'raise an error if repetitions are not positive' do
|
|
409
|
+
expect { distribution(:six).repeat_sum(0) }.to raise_error(RuntimeError, /n < 1/)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it 'raise an error if distribution would have more than a million results' do
|
|
413
|
+
d1000 = described_class.for_fair_die(1000)
|
|
414
|
+
expect { d1000.repeat_sum(11_000) }.to raise_error(RuntimeError, /Too many probability slots/)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
it "calculate a '3d6' distribution accurately" do
|
|
418
|
+
h = distribution(:six).repeat_sum(3).to_h
|
|
419
|
+
expect(h).to be_valid_distribution
|
|
420
|
+
numerators = [1, 3, 6, 10, 15, 21, 25, 27, 27, 25, 21, 15, 10, 6, 3, 1]
|
|
421
|
+
expected = (3..18).zip(numerators.map { |numerator| numerator / 216.0 }).to_h
|
|
422
|
+
expect_probability_values(h, expected, tolerance: 1e-9, total_tolerance: nil)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
describe '#repeat_n_sum_k' do
|
|
427
|
+
it 'output a valid distribution if params are valid' do
|
|
428
|
+
expect(distribution(:uniform_four).repeat_n_sum_k(3, 2).to_h).to be_valid_distribution
|
|
429
|
+
expect(distribution(:weighted_four).repeat_n_sum_k(12, 4).to_h).to be_valid_distribution
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
it 'raise an error if any param is unexpected type' do
|
|
433
|
+
d6 = described_class.for_fair_die(6)
|
|
434
|
+
expect { d6.repeat_n_sum_k({}, 10) }.to raise_error TypeError
|
|
435
|
+
expect { d6.repeat_n_sum_k(10, {}) }.to raise_error TypeError
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
it 'raise an error if repetitions or keepers are not positive' do
|
|
439
|
+
expect { distribution(:six).repeat_n_sum_k(0, 1) }.to raise_error(RuntimeError, /n < 1/)
|
|
440
|
+
expect { distribution(:six).repeat_n_sum_k(2, 0) }.to raise_error(RuntimeError, /k < 1/)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it 'sum every repetition when the keeper count is at least the repetition count' do
|
|
444
|
+
expect(distribution(:six).repeat_n_sum_k(3, 3).to_h).to eql(distribution(:six).repeat_sum(3).to_h)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it 'raise an error if the kept distribution would have more than a million results' do
|
|
448
|
+
d1000 = described_class.for_fair_die(1000)
|
|
449
|
+
expect { d1000.repeat_n_sum_k(1003, 1002) }.to raise_error(RuntimeError, /Too many probability slots/)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
it 'raise an error for an unknown keep mode' do
|
|
453
|
+
expect { distribution(:six).repeat_n_sum_k(3, 2, :middle) }.to raise_error(ArgumentError, /Keep mode/)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it 'raise an error if n is greater than 170' do
|
|
457
|
+
d6 = described_class.for_fair_die(6)
|
|
458
|
+
expect { d6.repeat_n_sum_k(171, 10) }.to raise_error(RuntimeError, /Too many dice/)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it "calculate a '4d6 keep best 3' distribution accurately" do
|
|
462
|
+
h = distribution(:six).repeat_n_sum_k(4, 3).to_h
|
|
463
|
+
expect(h).to be_valid_distribution
|
|
464
|
+
numerators = [1, 4, 10, 21, 38, 62, 91, 122, 148, 167, 172, 160, 131, 94, 54, 21]
|
|
465
|
+
expected = (3..18).zip(numerators.map { |numerator| numerator / 1296.0 }).to_h
|
|
466
|
+
expect_probability_values(h, expected, total_tolerance: nil)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it "calculate a '2d20 keep worst result' distribution accurately" do
|
|
470
|
+
h = distribution(:twenty).repeat_n_sum_k(2, 1, :keep_worst).to_h
|
|
471
|
+
expect(h).to be_valid_distribution
|
|
472
|
+
expected = (1..20).to_h { |result| [result, (41 - (2 * result)) / 400.0] }
|
|
473
|
+
expect_probability_values(h, expected, total_tolerance: nil)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
it "calculate a '4d6 keep worst 3' distribution accurately at its bounds" do
|
|
477
|
+
h = distribution(:six).repeat_n_sum_k(4, 3, :keep_worst).to_h
|
|
478
|
+
expect(h).to be_valid_distribution
|
|
479
|
+
expect(h[3]).to be_within(1e-10).of 21 / 1296.0
|
|
480
|
+
expect(h[18]).to be_within(1e-10).of 1 / 1296.0
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'helpers'
|
|
4
|
+
|
|
5
|
+
describe GamesDice::ProbabilityBudget, :aggregate_failures do
|
|
6
|
+
it 'rejects an aggregate certificate exceeding its requested budget' do
|
|
7
|
+
expect { described_class.check(1e-9, 1e-10) }
|
|
8
|
+
.to raise_error(GamesDice::ProbabilityLimitError, /truncation error bound/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'rounds component union bounds outwards' do
|
|
12
|
+
error = 1.0 / 3
|
|
13
|
+
expect(described_class.scale(error, 10).to_r).to be >= 10 * error.to_r
|
|
14
|
+
expect(described_class.add(error, error).to_r).to be >= 2 * error.to_r
|
|
15
|
+
expect(described_class.share(error, 10).to_r).to be <= error.to_r / 10
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'helpers'
|
|
4
4
|
|
|
5
|
-
describe GamesDice::RerollRule do
|
|
5
|
+
describe GamesDice::RerollRule, :aggregate_failures do
|
|
6
6
|
describe '#new' do
|
|
7
7
|
it 'accept self-consistent operator/value pairs as a trigger' do
|
|
8
8
|
expect { described_class.new(5, :>, :reroll_subtract) }.not_to raise_error
|
|
@@ -37,4 +37,8 @@ describe GamesDice::RerollRule do
|
|
|
37
37
|
expect(rule.applies?(6)).to be false
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
it 'uses the default limit when explicitly given nil' do
|
|
42
|
+
expect(described_class.new(6, :==, :reroll_add, nil).limit).to eq(1000)
|
|
43
|
+
end
|
|
40
44
|
end
|