games_dice 0.3.12 → 0.4.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.
@@ -1,526 +1,530 @@
1
- require 'helpers'
2
-
3
- describe GamesDice::Probabilities do
4
- describe "class methods" do
5
- describe "#new" do
6
- it "should create a new distribution from an array and offset" do
7
- pr = GamesDice::Probabilities.new( [1.0], 1 )
8
- pr.should be_a GamesDice::Probabilities
9
- pr.to_h.should be_valid_distribution
10
- end
11
-
12
- it "should raise an error if passed incorrect parameter types" do
13
- lambda { GamesDice::Probabilities.new( [ nil ], 20 ) }.should raise_error TypeError
14
- lambda { GamesDice::Probabilities.new( [0.3,nil,0.5], 7 ) }.should raise_error TypeError
15
- lambda { GamesDice::Probabilities.new( [0.3,0.2,0.5], {} ) }.should raise_error TypeError
16
- lambda { GamesDice::Probabilities.new( {:x=>:y}, 17 ) }.should raise_error TypeError
17
- end
18
-
19
- it "should raise an error if distribution is incomplete or inaccurate" do
20
- lambda { GamesDice::Probabilities.new( [0.3,0.2,0.6], 3 ) }.should raise_error ArgumentError
21
- lambda { GamesDice::Probabilities.new( [], 1 ) }.should raise_error ArgumentError
22
- lambda { GamesDice::Probabilities.new( [0.9], 1 ) }.should raise_error ArgumentError
23
- lambda { GamesDice::Probabilities.new( [-0.9,0.2,0.9], 1 ) }.should raise_error ArgumentError
24
- end
25
- end
26
-
27
- describe "#for_fair_die" do
28
- it "should create a new distribution based on number of sides" do
29
- pr2 = GamesDice::Probabilities.for_fair_die( 2 )
30
- pr2.should be_a GamesDice::Probabilities
31
- pr2.to_h.should == { 1 => 0.5, 2 => 0.5 }
32
- (1..20).each do |sides|
33
- pr = GamesDice::Probabilities.for_fair_die( sides )
34
- pr.should be_a GamesDice::Probabilities
35
- h = pr.to_h
36
- h.should be_valid_distribution
37
- h.keys.count.should == sides
38
- h.values.each { |v| v.should be_within(1e-10).of 1.0/sides }
39
- end
40
- end
41
-
42
- it "should raise an error if number of sides is not an integer" do
43
- lambda { GamesDice::Probabilities.for_fair_die( {} ) }.should raise_error TypeError
44
- end
45
-
46
- it "should raise an error if number of sides is too low or too high" do
47
- lambda { GamesDice::Probabilities.for_fair_die( 0 ) }.should raise_error ArgumentError
48
- lambda { GamesDice::Probabilities.for_fair_die( 1000001 ) }.should raise_error ArgumentError
49
- end
50
- end
51
-
52
- describe "#add_distributions" do
53
- it "should combine two distributions to create a third one" do
54
- d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
55
- d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
56
- pr = GamesDice::Probabilities.add_distributions( d4a, d4b )
57
- pr.to_h.should be_valid_distribution
58
- end
59
-
60
- it "should calculate a classic 2d6 distribution accurately" do
61
- d6 = GamesDice::Probabilities.for_fair_die( 6 )
62
- pr = GamesDice::Probabilities.add_distributions( d6, d6 )
63
- h = pr.to_h
64
- h.should be_valid_distribution
65
- h[2].should be_within(1e-9).of 1.0/36
66
- h[3].should be_within(1e-9).of 2.0/36
67
- h[4].should be_within(1e-9).of 3.0/36
68
- h[5].should be_within(1e-9).of 4.0/36
69
- h[6].should be_within(1e-9).of 5.0/36
70
- h[7].should be_within(1e-9).of 6.0/36
71
- h[8].should be_within(1e-9).of 5.0/36
72
- h[9].should be_within(1e-9).of 4.0/36
73
- h[10].should be_within(1e-9).of 3.0/36
74
- h[11].should be_within(1e-9).of 2.0/36
75
- h[12].should be_within(1e-9).of 1.0/36
76
- end
77
-
78
- it "should raise an error if either parameter is not a GamesDice::Probabilities object" do
79
- d10 = GamesDice::Probabilities.for_fair_die( 10 )
80
- lambda { GamesDice::Probabilities.add_distributions( '', 6 ) }.should raise_error TypeError
81
- lambda { GamesDice::Probabilities.add_distributions( d10, 6 ) }.should raise_error TypeError
82
- lambda { GamesDice::Probabilities.add_distributions( '', d10 ) }.should raise_error TypeError
83
- end
84
- end
85
-
86
- describe "#add_distributions_mult" do
87
- it "should combine two multiplied distributions to create a third one" do
88
- d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
89
- d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
90
- pr = GamesDice::Probabilities.add_distributions_mult( 2, d4a, -1, d4b )
91
- pr.to_h.should be_valid_distribution
92
- end
93
-
94
- it "should calculate a distribution for '1d6 - 1d4' accurately" do
95
- d6 = GamesDice::Probabilities.for_fair_die( 6 )
96
- d4 = GamesDice::Probabilities.for_fair_die( 4 )
97
- pr = GamesDice::Probabilities.add_distributions_mult( 1, d6, -1, d4 )
98
- h = pr.to_h
99
- h.should be_valid_distribution
100
- h[-3].should be_within(1e-9).of 1.0/24
101
- h[-2].should be_within(1e-9).of 2.0/24
102
- h[-1].should be_within(1e-9).of 3.0/24
103
- h[0].should be_within(1e-9).of 4.0/24
104
- h[1].should be_within(1e-9).of 4.0/24
105
- h[2].should be_within(1e-9).of 4.0/24
106
- h[3].should be_within(1e-9).of 3.0/24
107
- h[4].should be_within(1e-9).of 2.0/24
108
- h[5].should be_within(1e-9).of 1.0/24
109
- end
110
-
111
- it "should add asymmetric distributions accurately" do
112
- da = GamesDice::Probabilities.new( [0.7,0.0,0.3], 2 )
113
- db = GamesDice::Probabilities.new( [0.5,0.3,0.2], 2 )
114
- pr = GamesDice::Probabilities.add_distributions_mult( 1, da, 2, db )
115
- h = pr.to_h
116
- h.should be_valid_distribution
117
- h[6].should be_within(1e-9).of 0.7 * 0.5
118
- h[8].should be_within(1e-9).of 0.7 * 0.3 + 0.3 * 0.5
119
- h[10].should be_within(1e-9).of 0.7 * 0.2 + 0.3 * 0.3
120
- h[12].should be_within(1e-9).of 0.3 * 0.2
121
- end
122
-
123
- it "should raise an error if passed incorrect objects for distributions" do
124
- d10 = GamesDice::Probabilities.for_fair_die( 10 )
125
- lambda { GamesDice::Probabilities.add_distributions_mult( 1, '', -1, 6 ) }.should raise_error TypeError
126
- lambda { GamesDice::Probabilities.add_distributions_mult( 2, d10, 3, 6 ) }.should raise_error TypeError
127
- lambda { GamesDice::Probabilities.add_distributions_mult( 1, '', -1, d10 ) }.should raise_error TypeError
128
- end
129
-
130
- it "should raise an error if passed incorrect objects for multipliers" do
131
- d10 = GamesDice::Probabilities.for_fair_die( 10 )
132
- lambda { GamesDice::Probabilities.add_distributions_mult( {}, d10, [], d10 ) }.should raise_error TypeError
133
- lambda { GamesDice::Probabilities.add_distributions_mult( [7], d10, 3, d10 ) }.should raise_error TypeError
134
- lambda { GamesDice::Probabilities.add_distributions_mult( 1, d10, {}, d10 ) }.should raise_error TypeError
135
- end
136
- end
137
-
138
- describe "#from_h" do
139
- it "should create a Probabilities object from a valid hash" do
140
- pr = GamesDice::Probabilities.from_h( { 7 => 0.5, 9 => 0.5 } )
141
- pr.should be_a GamesDice::Probabilities
142
- end
143
-
144
- it "should raise an ArgumentError when called with a non-valid hash" do
145
- lambda { GamesDice::Probabilities.from_h( { 7 => 0.5, 9 => 0.6 } ) }.should raise_error ArgumentError
146
- end
147
-
148
- it "should raise an TypeError when called with data that is not a hash" do
149
- lambda { GamesDice::Probabilities.from_h( :foo ) }.should raise_error TypeError
150
- end
151
-
152
- it "should raise a TypeError when called when keys and values are not all integers and floats" do
153
- lambda { GamesDice::Probabilities.from_h( { 'x' => 0.5, 9 => 0.5 } ) }.should raise_error
154
- lambda { GamesDice::Probabilities.from_h( { 7 => [], 9 => 0.5 } ) }.should raise_error TypeError
155
- end
156
-
157
- it "should raise an ArgumentError when results are spread very far apart" do
158
- lambda { GamesDice::Probabilities.from_h( { 0 => 0.5, 2000000 => 0.5 } ) }.should raise_error ArgumentError
159
- end
160
- end
161
-
162
- describe "#implemented_in" do
163
- it "should be either :c or :ruby" do
164
- lang = GamesDice::Probabilities.implemented_in
165
- lang.should be_a Symbol
166
- [:c, :ruby].member?( lang ).should == true
167
- end
168
- end
169
- end # describe "class methods"
170
-
171
- describe "instance methods" do
172
- let(:pr2) { GamesDice::Probabilities.for_fair_die( 2 ) }
173
- let(:pr4) { GamesDice::Probabilities.for_fair_die( 4 ) }
174
- let(:pr6) { GamesDice::Probabilities.for_fair_die( 6 ) }
175
- let(:pr10) { GamesDice::Probabilities.for_fair_die( 10 ) }
176
- let(:pra) { GamesDice::Probabilities.new( [ 0.4, 0.2, 0.4 ], -1 ) }
177
-
178
- describe "#each" do
179
- it "should iterate through all result/probability pairs" do
180
- yielded = []
181
- pr4.each { |r,p| yielded << [r,p] }
182
- yielded.should == [ [1,0.25], [2,0.25], [3,0.25], [4,0.25] ]
183
- end
184
-
185
- it "should skip zero probabilities" do
186
- pr_plus_minus = GamesDice::Probabilities.new( [ 0.5, 0.0, 0.5 ], -1 )
187
- yielded = []
188
- pr_plus_minus .each { |r,p| yielded << [r,p] }
189
- yielded.should == [ [-1,0.5], [1,0.5] ]
190
- end
191
- end
192
-
193
- describe "#p_eql" do
194
- it "should return probability of getting a number inside the range" do
195
- pr2.p_eql(2).should be_within(1.0e-9).of 0.5
196
- pr4.p_eql(1).should be_within(1.0e-9).of 0.25
197
- pr6.p_eql(6).should be_within(1.0e-9).of 1.0/6
198
- pr10.p_eql(3).should be_within(1.0e-9).of 0.1
199
- pra.p_eql(-1).should be_within(1.0e-9).of 0.4
200
- end
201
-
202
- it "should return 0.0 for values not covered by distribution" do
203
- pr2.p_eql(3).should == 0.0
204
- pr4.p_eql(-1).should == 0.0
205
- pr6.p_eql(8).should == 0.0
206
- pr10.p_eql(11).should == 0.0
207
- pra.p_eql(2).should == 0.0
208
- end
209
-
210
- it "should raise a TypeError if asked for probability of non-Integer" do
211
- lambda { pr2.p_eql( [] ) }.should raise_error TypeError
212
- end
213
- end # describe "#p_eql"
214
-
215
- describe "#p_gt" do
216
- it "should return probability of getting a number greater than target" do
217
- pr2.p_gt(1).should be_within(1.0e-9).of 0.5
218
- pr4.p_gt(3).should be_within(1.0e-9).of 0.25
219
- pr6.p_gt(2).should be_within(1.0e-9).of 4.0/6
220
- pr10.p_gt(6).should be_within(1.0e-9).of 0.4
221
-
222
- # Trying more than one, due to possibilities of caching error (in pure Ruby implementation)
223
- pra.p_gt(-2).should be_within(1.0e-9).of 1.0
224
- pra.p_gt(-1).should be_within(1.0e-9).of 0.6
225
- pra.p_gt(0).should be_within(1.0e-9).of 0.4
226
- pra.p_gt(1).should be_within(1.0e-9).of 0.0
227
- end
228
-
229
- it "should return 0.0 when the target number is equal or higher than maximum possible" do
230
- pr2.p_gt(2).should == 0.0
231
- pr4.p_gt(5).should == 0.0
232
- pr6.p_gt(6).should == 0.0
233
- pr10.p_gt(20).should == 0.0
234
- pra.p_gt(3).should == 0.0
235
- end
236
-
237
- it "should return 1.0 when the target number is lower than minimum" do
238
- pr2.p_gt(0).should == 1.0
239
- pr4.p_gt(-5).should == 1.0
240
- pr6.p_gt(0).should == 1.0
241
- pr10.p_gt(-200).should == 1.0
242
- pra.p_gt(-2).should == 1.0
243
- end
244
-
245
- it "should raise a TypeError if asked for probability of non-Integer" do
246
- lambda { pr2.p_gt( {} ) }.should raise_error TypeError
247
- end
248
- end # describe "#p_gt"
249
-
250
- describe "#p_ge" do
251
- it "should return probability of getting a number greater than or equal to target" do
252
- pr2.p_ge(2).should be_within(1.0e-9).of 0.5
253
- pr4.p_ge(3).should be_within(1.0e-9).of 0.5
254
- pr6.p_ge(2).should be_within(1.0e-9).of 5.0/6
255
- pr10.p_ge(6).should be_within(1.0e-9).of 0.5
256
- end
257
-
258
- it "should return 0.0 when the target number is higher than maximum possible" do
259
- pr2.p_ge(6).should == 0.0
260
- pr4.p_ge(5).should == 0.0
261
- pr6.p_ge(7).should == 0.0
262
- pr10.p_ge(20).should == 0.0
263
- end
264
-
265
- it "should return 1.0 when the target number is lower than or equal to minimum possible" do
266
- pr2.p_ge(1).should == 1.0
267
- pr4.p_ge(-5).should == 1.0
268
- pr6.p_ge(1).should == 1.0
269
- pr10.p_ge(-200).should == 1.0
270
- end
271
-
272
- it "should raise a TypeError if asked for probability of non-Integer" do
273
- lambda { pr4.p_ge( {} ) }.should raise_error TypeError
274
- end
275
- end # describe "#p_ge"
276
-
277
- describe "#p_le" do
278
- it "should return probability of getting a number less than or equal to target" do
279
- pr2.p_le(1).should be_within(1.0e-9).of 0.5
280
- pr4.p_le(2).should be_within(1.0e-9).of 0.5
281
- pr6.p_le(2).should be_within(1.0e-9).of 2.0/6
282
- pr10.p_le(6).should be_within(1.0e-9).of 0.6
283
- end
284
-
285
- it "should return 1.0 when the target number is higher than or equal to maximum possible" do
286
- pr2.p_le(6).should == 1.0
287
- pr4.p_le(4).should == 1.0
288
- pr6.p_le(7).should == 1.0
289
- pr10.p_le(10).should == 1.0
290
- end
291
-
292
- it "should return 0.0 when the target number is lower than minimum possible" do
293
- pr2.p_le(0).should == 0.0
294
- pr4.p_le(-5).should == 0.0
295
- pr6.p_le(0).should == 0.0
296
- pr10.p_le(-200).should == 0.0
297
- end
298
-
299
- it "should raise a TypeError if asked for probability of non-Integer" do
300
- lambda { pr4.p_le( [] ) }.should raise_error TypeError
301
- end
302
- end # describe "#p_le"
303
-
304
- describe "#p_lt" do
305
- it "should return probability of getting a number less than target" do
306
- pr2.p_lt(2).should be_within(1.0e-9).of 0.5
307
- pr4.p_lt(3).should be_within(1.0e-9).of 0.5
308
- pr6.p_lt(2).should be_within(1.0e-9).of 1/6.0
309
- pr10.p_lt(6).should be_within(1.0e-9).of 0.5
310
- end
311
-
312
- it "should return 1.0 when the target number is higher than maximum possible" do
313
- pr2.p_lt(6).should == 1.0
314
- pr4.p_lt(5).should == 1.0
315
- pr6.p_lt(7).should == 1.0
316
- pr10.p_lt(20).should == 1.0
317
- end
318
-
319
- it "should return 0.0 when the target number is lower than or equal to minimum possible" do
320
- pr2.p_lt(1).should == 0.0
321
- pr4.p_lt(-5).should == 0.0
322
- pr6.p_lt(1).should == 0.0
323
- pr10.p_lt(-200).should == 0.0
324
- end
325
-
326
- it "should raise a TypeError if asked for probability of non-Integer" do
327
- lambda { pr6.p_lt( {} ) }.should raise_error TypeError
328
- end
329
- end # describe "#p_lt"
330
-
331
- describe "#to_h" do
332
- # This is used loads in other tests
333
- it "should represent a valid distribution with each integer result associated with its probability" do
334
- pr2.to_h.should be_valid_distribution
335
- pr4.to_h.should be_valid_distribution
336
- pr6.to_h.should be_valid_distribution
337
- pr10.to_h.should be_valid_distribution
338
- end
339
- end
340
-
341
- describe "#min" do
342
- it "should return lowest possible result allowed by distribution" do
343
- pr2.min.should == 1
344
- pr4.min.should == 1
345
- pr6.min.should == 1
346
- pr10.min.should == 1
347
- GamesDice::Probabilities.add_distributions( pr6, pr10 ).min.should == 2
348
- end
349
- end
350
-
351
- describe "#max" do
352
- it "should return highest possible result allowed by distribution" do
353
- pr2.max.should == 2
354
- pr4.max.should == 4
355
- pr6.max.should == 6
356
- pr10.max.should == 10
357
- GamesDice::Probabilities.add_distributions( pr6, pr10 ).max.should == 16
358
- end
359
- end
360
-
361
- describe "#expected" do
362
- it "should return the weighted mean value" do
363
- pr2.expected.should be_within(1.0e-9).of 1.5
364
- pr4.expected.should be_within(1.0e-9).of 2.5
365
- pr6.expected.should be_within(1.0e-9).of 3.5
366
- pr10.expected.should be_within(1.0e-9).of 5.5
367
- GamesDice::Probabilities.add_distributions( pr6, pr10 ).expected.should be_within(1.0e-9).of 9.0
368
- end
369
- end
370
-
371
- describe "#given_ge" do
372
- it "should return a new distribution with probabilities calculated assuming value is >= target" do
373
- pd = pr2.given_ge(2)
374
- pd.to_h.should == { 2 => 1.0 }
375
- pd = pr10.given_ge(4)
376
- pd.to_h.should be_valid_distribution
377
- pd.p_eql( 3 ).should == 0.0
378
- pd.p_eql( 10 ).should be_within(1.0e-9).of 0.1/0.7
379
- end
380
-
381
- it "should raise a TypeError if asked for probability of non-Integer" do
382
- lambda { pr10.given_ge( [] ) }.should raise_error TypeError
383
- end
384
- end
385
-
386
- describe "#given_le" do
387
- it "should return a new distribution with probabilities calculated assuming value is <= target" do
388
- pd = pr2.given_le(2)
389
- pd.to_h.should == { 1 => 0.5, 2 => 0.5 }
390
- pd = pr10.given_le(4)
391
- pd.to_h.should be_valid_distribution
392
- pd.p_eql( 3 ).should be_within(1.0e-9).of 0.1/0.4
393
- pd.p_eql( 10 ).should == 0.0
394
- end
395
-
396
- it "should raise a TypeError if asked for probability of non-Integer" do
397
- lambda { pr10.given_le( {} ) }.should raise_error TypeError
398
- end
399
- end
400
-
401
- describe "#repeat_sum" do
402
- it "should output a valid distribution if params are valid" do
403
- d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
404
- d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
405
- pr = d4a.repeat_sum( 7 )
406
- pr.to_h.should be_valid_distribution
407
- pr = d4b.repeat_sum( 12 )
408
- pr.to_h.should be_valid_distribution
409
- end
410
-
411
- it "should raise an error if any param is unexpected type" do
412
- d6 = GamesDice::Probabilities.for_fair_die( 6 )
413
- lambda{ d6.repeat_sum( {} ) }.should raise_error TypeError
414
- end
415
-
416
- it "should raise an error if distribution would have more than a million results" do
417
- d1000 = GamesDice::Probabilities.for_fair_die( 1000 )
418
- lambda{ d1000.repeat_sum( 11000 ) }.should raise_error
419
- end
420
-
421
- it "should calculate a '3d6' distribution accurately" do
422
- d6 = GamesDice::Probabilities.for_fair_die( 6 )
423
- pr = d6.repeat_sum( 3 )
424
- h = pr.to_h
425
- h.should be_valid_distribution
426
- h[3].should be_within(1e-9).of 1.0/216
427
- h[4].should be_within(1e-9).of 3.0/216
428
- h[5].should be_within(1e-9).of 6.0/216
429
- h[6].should be_within(1e-9).of 10.0/216
430
- h[7].should be_within(1e-9).of 15.0/216
431
- h[8].should be_within(1e-9).of 21.0/216
432
- h[9].should be_within(1e-9).of 25.0/216
433
- h[10].should be_within(1e-9).of 27.0/216
434
- h[11].should be_within(1e-9).of 27.0/216
435
- h[12].should be_within(1e-9).of 25.0/216
436
- h[13].should be_within(1e-9).of 21.0/216
437
- h[14].should be_within(1e-9).of 15.0/216
438
- h[15].should be_within(1e-9).of 10.0/216
439
- h[16].should be_within(1e-9).of 6.0/216
440
- h[17].should be_within(1e-9).of 3.0/216
441
- h[18].should be_within(1e-9).of 1.0/216
442
- end
443
- end # describe "#repeat_sum"
444
-
445
- describe "#repeat_n_sum_k" do
446
- it "should output a valid distribution if params are valid" do
447
- d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
448
- d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
449
- pr = d4a.repeat_n_sum_k( 3, 2 )
450
- pr.to_h.should be_valid_distribution
451
- pr = d4b.repeat_n_sum_k( 12, 4 )
452
- pr.to_h.should be_valid_distribution
453
- end
454
-
455
- it "should raise an error if any param is unexpected type" do
456
- d6 = GamesDice::Probabilities.for_fair_die( 6 )
457
- lambda{ d6.repeat_n_sum_k( {}, 10 ) }.should raise_error TypeError
458
- lambda{ d6.repeat_n_sum_k( 10, {} ) }.should raise_error TypeError
459
- end
460
-
461
- it "should raise an error if n is greater than 170" do
462
- d6 = GamesDice::Probabilities.for_fair_die( 6 )
463
- lambda{ d6.repeat_n_sum_k( 171, 10 ) }.should raise_error
464
- end
465
-
466
- it "should calculate a '4d6 keep best 3' distribution accurately" do
467
- d6 = GamesDice::Probabilities.for_fair_die( 6 )
468
- pr = d6.repeat_n_sum_k( 4, 3 )
469
- h = pr.to_h
470
- h.should be_valid_distribution
471
- h[3].should be_within(1e-10).of 1/1296.0
472
- h[4].should be_within(1e-10).of 4/1296.0
473
- h[5].should be_within(1e-10).of 10/1296.0
474
- h[6].should be_within(1e-10).of 21/1296.0
475
- h[7].should be_within(1e-10).of 38/1296.0
476
- h[8].should be_within(1e-10).of 62/1296.0
477
- h[9].should be_within(1e-10).of 91/1296.0
478
- h[10].should be_within(1e-10).of 122/1296.0
479
- h[11].should be_within(1e-10).of 148/1296.0
480
- h[12].should be_within(1e-10).of 167/1296.0
481
- h[13].should be_within(1e-10).of 172/1296.0
482
- h[14].should be_within(1e-10).of 160/1296.0
483
- h[15].should be_within(1e-10).of 131/1296.0
484
- h[16].should be_within(1e-10).of 94/1296.0
485
- h[17].should be_within(1e-10).of 54/1296.0
486
- h[18].should be_within(1e-10).of 21/1296.0
487
- end
488
-
489
- it "should calculate a '2d20 keep worst result' distribution accurately" do
490
- d20 = GamesDice::Probabilities.for_fair_die( 20 )
491
- pr = d20.repeat_n_sum_k( 2, 1, :keep_worst )
492
- h = pr.to_h
493
- h.should be_valid_distribution
494
- h[1].should be_within(1e-10).of 39/400.0
495
- h[2].should be_within(1e-10).of 37/400.0
496
- h[3].should be_within(1e-10).of 35/400.0
497
- h[4].should be_within(1e-10).of 33/400.0
498
- h[5].should be_within(1e-10).of 31/400.0
499
- h[6].should be_within(1e-10).of 29/400.0
500
- h[7].should be_within(1e-10).of 27/400.0
501
- h[8].should be_within(1e-10).of 25/400.0
502
- h[9].should be_within(1e-10).of 23/400.0
503
- h[10].should be_within(1e-10).of 21/400.0
504
- h[11].should be_within(1e-10).of 19/400.0
505
- h[12].should be_within(1e-10).of 17/400.0
506
- h[13].should be_within(1e-10).of 15/400.0
507
- h[14].should be_within(1e-10).of 13/400.0
508
- h[15].should be_within(1e-10).of 11/400.0
509
- h[16].should be_within(1e-10).of 9/400.0
510
- h[17].should be_within(1e-10).of 7/400.0
511
- h[18].should be_within(1e-10).of 5/400.0
512
- h[19].should be_within(1e-10).of 3/400.0
513
- h[20].should be_within(1e-10).of 1/400.0
514
- end
515
- end # describe "#repeat_n_sum_k"
516
-
517
- end # describe "instance methods"
518
-
519
- describe "serialisation via Marshall" do
520
- it "can load a saved GamesDice::Probabilities" do
521
- pd6 = File.open( fixture('probs_fair_die_6.dat') ) { |file| Marshal.load(file) }
522
- pd6.to_h.should be_valid_distribution
523
- pd6.p_gt(4).should be_within(1e-10).of 1.0/3
524
- end
525
- end
526
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+
5
+ describe GamesDice::Probabilities do
6
+ describe 'class methods' do
7
+ describe '#new' do
8
+ it 'should create a new distribution from an array and offset' do
9
+ pr = GamesDice::Probabilities.new([1.0], 1)
10
+ expect(pr).to be_a GamesDice::Probabilities
11
+ expect(pr.to_h).to be_valid_distribution
12
+ end
13
+
14
+ it 'should raise an error if passed incorrect parameter types' do
15
+ expect(-> { GamesDice::Probabilities.new([nil], 20) }).to raise_error TypeError
16
+ expect(-> { GamesDice::Probabilities.new([0.3, nil, 0.5], 7) }).to raise_error TypeError
17
+ expect(-> { GamesDice::Probabilities.new([0.3, 0.2, 0.5], {}) }).to raise_error TypeError
18
+ expect(-> { GamesDice::Probabilities.new({ x: :y }, 17) }).to raise_error TypeError
19
+ end
20
+
21
+ it 'should raise an error if distribution is incomplete or inaccurate' do
22
+ expect(-> { GamesDice::Probabilities.new([0.3, 0.2, 0.6], 3) }).to raise_error ArgumentError
23
+ expect(-> { GamesDice::Probabilities.new([], 1) }).to raise_error ArgumentError
24
+ expect(-> { GamesDice::Probabilities.new([0.9], 1) }).to raise_error ArgumentError
25
+ expect(-> { GamesDice::Probabilities.new([-0.9, 0.2, 0.9], 1) }).to raise_error ArgumentError
26
+ end
27
+ end
28
+
29
+ describe '#for_fair_die' do
30
+ it 'should create a new distribution based on number of sides' do
31
+ pr2 = GamesDice::Probabilities.for_fair_die(2)
32
+ expect(pr2).to be_a GamesDice::Probabilities
33
+ expect(pr2.to_h).to eql({ 1 => 0.5, 2 => 0.5 })
34
+ (1..20).each do |sides|
35
+ pr = GamesDice::Probabilities.for_fair_die(sides)
36
+ expect(pr).to be_a GamesDice::Probabilities
37
+ h = pr.to_h
38
+ expect(h).to be_valid_distribution
39
+ expect(h.keys.count).to eql sides
40
+ h.each_value { |v| expect(v).to be_within(1e-10).of 1.0 / sides }
41
+ end
42
+ end
43
+
44
+ it 'should raise an error if number of sides is not an integer' do
45
+ expect(-> { GamesDice::Probabilities.for_fair_die({}) }).to raise_error TypeError
46
+ end
47
+
48
+ it 'should raise an error if number of sides is too low or too high' do
49
+ expect(-> { GamesDice::Probabilities.for_fair_die(0) }).to raise_error ArgumentError
50
+ expect(-> { GamesDice::Probabilities.for_fair_die(1_000_001) }).to raise_error ArgumentError
51
+ end
52
+ end
53
+
54
+ describe '#add_distributions' do
55
+ it 'should combine two distributions to create a third one' do
56
+ d4a = GamesDice::Probabilities.new([1.0 / 4, 1.0 / 4, 1.0 / 4, 1.0 / 4], 1)
57
+ d4b = GamesDice::Probabilities.new([1.0 / 10, 2.0 / 10, 3.0 / 10, 4.0 / 10], 1)
58
+ pr = GamesDice::Probabilities.add_distributions(d4a, d4b)
59
+ expect(pr.to_h).to be_valid_distribution
60
+ end
61
+
62
+ it 'should calculate a classic 2d6 distribution accurately' do
63
+ d6 = GamesDice::Probabilities.for_fair_die(6)
64
+ pr = GamesDice::Probabilities.add_distributions(d6, d6)
65
+ h = pr.to_h
66
+ expect(h).to be_valid_distribution
67
+ expect(h[2]).to be_within(1e-9).of 1.0 / 36
68
+ expect(h[3]).to be_within(1e-9).of 2.0 / 36
69
+ expect(h[4]).to be_within(1e-9).of 3.0 / 36
70
+ expect(h[5]).to be_within(1e-9).of 4.0 / 36
71
+ expect(h[6]).to be_within(1e-9).of 5.0 / 36
72
+ expect(h[7]).to be_within(1e-9).of 6.0 / 36
73
+ expect(h[8]).to be_within(1e-9).of 5.0 / 36
74
+ expect(h[9]).to be_within(1e-9).of 4.0 / 36
75
+ expect(h[10]).to be_within(1e-9).of 3.0 / 36
76
+ expect(h[11]).to be_within(1e-9).of 2.0 / 36
77
+ expect(h[12]).to be_within(1e-9).of 1.0 / 36
78
+ end
79
+
80
+ it 'should raise an error if either parameter is not a GamesDice::Probabilities object' do
81
+ d10 = GamesDice::Probabilities.for_fair_die(10)
82
+ expect(-> { GamesDice::Probabilities.add_distributions('', 6) }).to raise_error TypeError
83
+ expect(-> { GamesDice::Probabilities.add_distributions(d10, 6) }).to raise_error TypeError
84
+ expect(-> { GamesDice::Probabilities.add_distributions('', d10) }).to raise_error TypeError
85
+ end
86
+ end
87
+
88
+ describe '#add_distributions_mult' do
89
+ it 'should combine two multiplied distributions to create a third one' do
90
+ d4a = GamesDice::Probabilities.new([1.0 / 4, 1.0 / 4, 1.0 / 4, 1.0 / 4], 1)
91
+ d4b = GamesDice::Probabilities.new([1.0 / 10, 2.0 / 10, 3.0 / 10, 4.0 / 10], 1)
92
+ pr = GamesDice::Probabilities.add_distributions_mult(2, d4a, -1, d4b)
93
+ expect(pr.to_h).to be_valid_distribution
94
+ end
95
+
96
+ it "should calculate a distribution for '1d6 - 1d4' accurately" do
97
+ d6 = GamesDice::Probabilities.for_fair_die(6)
98
+ d4 = GamesDice::Probabilities.for_fair_die(4)
99
+ pr = GamesDice::Probabilities.add_distributions_mult(1, d6, -1, d4)
100
+ h = pr.to_h
101
+ expect(h).to be_valid_distribution
102
+ expect(h[-3]).to be_within(1e-9).of 1.0 / 24
103
+ expect(h[-2]).to be_within(1e-9).of 2.0 / 24
104
+ expect(h[-1]).to be_within(1e-9).of 3.0 / 24
105
+ expect(h[0]).to be_within(1e-9).of 4.0 / 24
106
+ expect(h[1]).to be_within(1e-9).of 4.0 / 24
107
+ expect(h[2]).to be_within(1e-9).of 4.0 / 24
108
+ expect(h[3]).to be_within(1e-9).of 3.0 / 24
109
+ expect(h[4]).to be_within(1e-9).of 2.0 / 24
110
+ expect(h[5]).to be_within(1e-9).of 1.0 / 24
111
+ end
112
+
113
+ it 'should add asymmetric distributions accurately' do
114
+ da = GamesDice::Probabilities.new([0.7, 0.0, 0.3], 2)
115
+ db = GamesDice::Probabilities.new([0.5, 0.3, 0.2], 2)
116
+ pr = GamesDice::Probabilities.add_distributions_mult(1, da, 2, db)
117
+ h = pr.to_h
118
+ expect(h).to be_valid_distribution
119
+ expect(h[6]).to be_within(1e-9).of 0.7 * 0.5
120
+ expect(h[8]).to be_within(1e-9).of (0.7 * 0.3) + (0.3 * 0.5)
121
+ expect(h[10]).to be_within(1e-9).of (0.7 * 0.2) + (0.3 * 0.3)
122
+ expect(h[12]).to be_within(1e-9).of 0.3 * 0.2
123
+ end
124
+
125
+ it 'should raise an error if passed incorrect objects for distributions' do
126
+ d10 = GamesDice::Probabilities.for_fair_die(10)
127
+ expect(-> { GamesDice::Probabilities.add_distributions_mult(1, '', -1, 6) }).to raise_error TypeError
128
+ expect(-> { GamesDice::Probabilities.add_distributions_mult(2, d10, 3, 6) }).to raise_error TypeError
129
+ expect(-> { GamesDice::Probabilities.add_distributions_mult(1, '', -1, d10) }).to raise_error TypeError
130
+ end
131
+
132
+ it 'should raise an error if passed incorrect objects for multipliers' do
133
+ d10 = GamesDice::Probabilities.for_fair_die(10)
134
+ expect(-> { GamesDice::Probabilities.add_distributions_mult({}, d10, [], d10) }).to raise_error TypeError
135
+ expect(-> { GamesDice::Probabilities.add_distributions_mult([7], d10, 3, d10) }).to raise_error TypeError
136
+ expect(-> { GamesDice::Probabilities.add_distributions_mult(1, d10, {}, d10) }).to raise_error TypeError
137
+ end
138
+ end
139
+
140
+ describe '#from_h' do
141
+ it 'should create a Probabilities object from a valid hash' do
142
+ pr = GamesDice::Probabilities.from_h({ 7 => 0.5, 9 => 0.5 })
143
+ expect(pr).to be_a GamesDice::Probabilities
144
+ end
145
+
146
+ it 'should raise an ArgumentError when called with a non-valid hash' do
147
+ expect(-> { GamesDice::Probabilities.from_h({ 7 => 0.5, 9 => 0.6 }) }).to raise_error ArgumentError
148
+ end
149
+
150
+ it 'should raise an TypeError when called with data that is not a hash' do
151
+ expect(-> { GamesDice::Probabilities.from_h(:foo) }).to raise_error TypeError
152
+ end
153
+
154
+ it 'should raise a TypeError when called when keys and values are not all integers and floats' do
155
+ expect(-> { GamesDice::Probabilities.from_h({ 'x' => 0.5, 9 => 0.5 }) }).to raise_error TypeError
156
+ expect(-> { GamesDice::Probabilities.from_h({ 7 => [], 9 => 0.5 }) }).to raise_error TypeError
157
+ end
158
+
159
+ it 'should raise an ArgumentError when results are spread very far apart' do
160
+ expect(-> { GamesDice::Probabilities.from_h({ 0 => 0.5, 2_000_000 => 0.5 }) }).to raise_error ArgumentError
161
+ end
162
+ end
163
+
164
+ describe '#implemented_in' do
165
+ it 'should be either :c or :ruby' do
166
+ lang = GamesDice::Probabilities.implemented_in
167
+ expect(lang).to be_a Symbol
168
+ expect(%i[c ruby].member?(lang)).to eql true
169
+ end
170
+ end
171
+ end
172
+
173
+ describe 'instance methods' do
174
+ let(:pr2) { GamesDice::Probabilities.for_fair_die(2) }
175
+ let(:pr4) { GamesDice::Probabilities.for_fair_die(4) }
176
+ let(:pr6) { GamesDice::Probabilities.for_fair_die(6) }
177
+ let(:pr10) { GamesDice::Probabilities.for_fair_die(10) }
178
+ let(:pra) { GamesDice::Probabilities.new([0.4, 0.2, 0.4], -1) }
179
+
180
+ describe '#each' do
181
+ it 'should iterate through all result/probability pairs' do
182
+ yielded = []
183
+ pr4.each { |r, p| yielded << [r, p] }
184
+ expect(yielded).to eql [[1, 0.25], [2, 0.25], [3, 0.25], [4, 0.25]]
185
+ end
186
+
187
+ it 'should skip zero probabilities' do
188
+ pr_plus_minus = GamesDice::Probabilities.new([0.5, 0.0, 0.5], -1)
189
+ yielded = []
190
+ pr_plus_minus.each { |r, p| yielded << [r, p] }
191
+ expect(yielded).to eql [[-1, 0.5], [1, 0.5]]
192
+ end
193
+ end
194
+
195
+ describe '#p_eql' do
196
+ it 'should return probability of getting a number inside the range' do
197
+ expect(pr2.p_eql(2)).to be_within(1.0e-9).of 0.5
198
+ expect(pr4.p_eql(1)).to be_within(1.0e-9).of 0.25
199
+ expect(pr6.p_eql(6)).to be_within(1.0e-9).of 1.0 / 6
200
+ expect(pr10.p_eql(3)).to be_within(1.0e-9).of 0.1
201
+ expect(pra.p_eql(-1)).to be_within(1.0e-9).of 0.4
202
+ end
203
+
204
+ it 'should return 0.0 for values not covered by distribution' do
205
+ expect(pr2.p_eql(3)).to eql 0.0
206
+ expect(pr4.p_eql(-1)).to eql 0.0
207
+ expect(pr6.p_eql(8)).to eql 0.0
208
+ expect(pr10.p_eql(11)).to eql 0.0
209
+ expect(pra.p_eql(2)).to eql 0.0
210
+ end
211
+
212
+ it 'should raise a TypeError if asked for probability of non-Integer' do
213
+ expect(-> { pr2.p_eql([]) }).to raise_error TypeError
214
+ end
215
+ end
216
+
217
+ describe '#p_gt' do
218
+ it 'should return probability of getting a number greater than target' do
219
+ expect(pr2.p_gt(1)).to be_within(1.0e-9).of 0.5
220
+ expect(pr4.p_gt(3)).to be_within(1.0e-9).of 0.25
221
+ expect(pr6.p_gt(2)).to be_within(1.0e-9).of 4.0 / 6
222
+ expect(pr10.p_gt(6)).to be_within(1.0e-9).of 0.4
223
+
224
+ # Trying more than one, due to possibilities of caching error (in pure Ruby implementation)
225
+ expect(pra.p_gt(-2)).to be_within(1.0e-9).of 1.0
226
+ expect(pra.p_gt(-1)).to be_within(1.0e-9).of 0.6
227
+ expect(pra.p_gt(0)).to be_within(1.0e-9).of 0.4
228
+ expect(pra.p_gt(1)).to be_within(1.0e-9).of 0.0
229
+ end
230
+
231
+ it 'should return 0.0 when the target number is equal or higher than maximum possible' do
232
+ expect(pr2.p_gt(2)).to eql 0.0
233
+ expect(pr4.p_gt(5)).to eql 0.0
234
+ expect(pr6.p_gt(6)).to eql 0.0
235
+ expect(pr10.p_gt(20)).to eql 0.0
236
+ expect(pra.p_gt(3)).to eql 0.0
237
+ end
238
+
239
+ it 'should return 1.0 when the target number is lower than minimum' do
240
+ expect(pr2.p_gt(0)).to eql 1.0
241
+ expect(pr4.p_gt(-5)).to eql 1.0
242
+ expect(pr6.p_gt(0)).to eql 1.0
243
+ expect(pr10.p_gt(-200)).to eql 1.0
244
+ expect(pra.p_gt(-2)).to eql 1.0
245
+ end
246
+
247
+ it 'should raise a TypeError if asked for probability of non-Integer' do
248
+ expect(-> { pr2.p_gt({}) }).to raise_error TypeError
249
+ end
250
+ end
251
+
252
+ describe '#p_ge' do
253
+ it 'should return probability of getting a number greater than or equal to target' do
254
+ expect(pr2.p_ge(2)).to be_within(1.0e-9).of 0.5
255
+ expect(pr4.p_ge(3)).to be_within(1.0e-9).of 0.5
256
+ expect(pr6.p_ge(2)).to be_within(1.0e-9).of 5.0 / 6
257
+ expect(pr10.p_ge(6)).to be_within(1.0e-9).of 0.5
258
+ end
259
+
260
+ it 'should return 0.0 when the target number is higher than maximum possible' do
261
+ expect(pr2.p_ge(6)).to eql 0.0
262
+ expect(pr4.p_ge(5)).to eql 0.0
263
+ expect(pr6.p_ge(7)).to eql 0.0
264
+ expect(pr10.p_ge(20)).to eql 0.0
265
+ end
266
+
267
+ it 'should return 1.0 when the target number is lower than or equal to minimum possible' do
268
+ expect(pr2.p_ge(1)).to eql 1.0
269
+ expect(pr4.p_ge(-5)).to eql 1.0
270
+ expect(pr6.p_ge(1)).to eql 1.0
271
+ expect(pr10.p_ge(-200)).to eql 1.0
272
+ end
273
+
274
+ it 'should raise a TypeError if asked for probability of non-Integer' do
275
+ expect(-> { pr4.p_ge({}) }).to raise_error TypeError
276
+ end
277
+ end
278
+
279
+ describe '#p_le' do
280
+ it 'should return probability of getting a number less than or equal to target' do
281
+ expect(pr2.p_le(1)).to be_within(1.0e-9).of 0.5
282
+ expect(pr4.p_le(2)).to be_within(1.0e-9).of 0.5
283
+ expect(pr6.p_le(2)).to be_within(1.0e-9).of 2.0 / 6
284
+ expect(pr10.p_le(6)).to be_within(1.0e-9).of 0.6
285
+ end
286
+
287
+ it 'should return 1.0 when the target number is higher than or equal to maximum possible' do
288
+ expect(pr2.p_le(6)).to eql 1.0
289
+ expect(pr4.p_le(4)).to eql 1.0
290
+ expect(pr6.p_le(7)).to eql 1.0
291
+ expect(pr10.p_le(10)).to eql 1.0
292
+ end
293
+
294
+ it 'should return 0.0 when the target number is lower than minimum possible' do
295
+ expect(pr2.p_le(0)).to eql 0.0
296
+ expect(pr4.p_le(-5)).to eql 0.0
297
+ expect(pr6.p_le(0)).to eql 0.0
298
+ expect(pr10.p_le(-200)).to eql 0.0
299
+ end
300
+
301
+ it 'should raise a TypeError if asked for probability of non-Integer' do
302
+ expect(-> { pr4.p_le([]) }).to raise_error TypeError
303
+ end
304
+ end
305
+
306
+ describe '#p_lt' do
307
+ it 'should return probability of getting a number less than target' do
308
+ expect(pr2.p_lt(2)).to be_within(1.0e-9).of 0.5
309
+ expect(pr4.p_lt(3)).to be_within(1.0e-9).of 0.5
310
+ expect(pr6.p_lt(2)).to be_within(1.0e-9).of 1 / 6.0
311
+ expect(pr10.p_lt(6)).to be_within(1.0e-9).of 0.5
312
+ end
313
+
314
+ it 'should return 1.0 when the target number is higher than maximum possible' do
315
+ expect(pr2.p_lt(6)).to eql 1.0
316
+ expect(pr4.p_lt(5)).to eql 1.0
317
+ expect(pr6.p_lt(7)).to eql 1.0
318
+ expect(pr10.p_lt(20)).to eql 1.0
319
+ end
320
+
321
+ it 'should return 0.0 when the target number is lower than or equal to minimum possible' do
322
+ expect(pr2.p_lt(1)).to eql 0.0
323
+ expect(pr4.p_lt(-5)).to eql 0.0
324
+ expect(pr6.p_lt(1)).to eql 0.0
325
+ expect(pr10.p_lt(-200)).to eql 0.0
326
+ end
327
+
328
+ it 'should raise a TypeError if asked for probability of non-Integer' do
329
+ expect(-> { pr6.p_lt({}) }).to raise_error TypeError
330
+ end
331
+ end
332
+
333
+ describe '#to_h' do
334
+ # This is used loads in other tests
335
+ it 'should represent a valid distribution with each integer result associated with its probability' do
336
+ expect(pr2.to_h).to be_valid_distribution
337
+ expect(pr4.to_h).to be_valid_distribution
338
+ expect(pr6.to_h).to be_valid_distribution
339
+ expect(pr10.to_h).to be_valid_distribution
340
+ end
341
+ end
342
+
343
+ describe '#min' do
344
+ it 'should return lowest possible result allowed by distribution' do
345
+ expect(pr2.min).to eql 1
346
+ expect(pr4.min).to eql 1
347
+ expect(pr6.min).to eql 1
348
+ expect(pr10.min).to eql 1
349
+ expect(GamesDice::Probabilities.add_distributions(pr6, pr10).min).to eql 2
350
+ end
351
+ end
352
+
353
+ describe '#max' do
354
+ it 'should return highest possible result allowed by distribution' do
355
+ expect(pr2.max).to eql 2
356
+ expect(pr4.max).to eql 4
357
+ expect(pr6.max).to eql 6
358
+ expect(pr10.max).to eql 10
359
+ expect(GamesDice::Probabilities.add_distributions(pr6, pr10).max).to eql 16
360
+ end
361
+ end
362
+
363
+ describe '#expected' do
364
+ it 'should return the weighted mean value' do
365
+ expect(pr2.expected).to be_within(1.0e-9).of 1.5
366
+ expect(pr4.expected).to be_within(1.0e-9).of 2.5
367
+ expect(pr6.expected).to be_within(1.0e-9).of 3.5
368
+ expect(pr10.expected).to be_within(1.0e-9).of 5.5
369
+ expect(GamesDice::Probabilities.add_distributions(pr6, pr10).expected).to be_within(1.0e-9).of 9.0
370
+ end
371
+ end
372
+
373
+ describe '#given_ge' do
374
+ it 'should return a new distribution with probabilities calculated assuming value is >= target' do
375
+ pd = pr2.given_ge(2)
376
+ expect(pd.to_h).to eql({ 2 => 1.0 })
377
+ pd = pr10.given_ge(4)
378
+ expect(pd.to_h).to be_valid_distribution
379
+ expect(pd.p_eql(3)).to eql 0.0
380
+ expect(pd.p_eql(10)).to be_within(1.0e-9).of 0.1 / 0.7
381
+ end
382
+
383
+ it 'should raise a TypeError if asked for probability of non-Integer' do
384
+ expect(-> { pr10.given_ge([]) }).to raise_error TypeError
385
+ end
386
+ end
387
+
388
+ describe '#given_le' do
389
+ it 'should return a new distribution with probabilities calculated assuming value is <= target' do
390
+ pd = pr2.given_le(2)
391
+ expect(pd.to_h).to eql({ 1 => 0.5, 2 => 0.5 })
392
+ pd = pr10.given_le(4)
393
+ expect(pd.to_h).to be_valid_distribution
394
+ expect(pd.p_eql(3)).to be_within(1.0e-9).of 0.1 / 0.4
395
+ expect(pd.p_eql(10)).to eql 0.0
396
+ end
397
+
398
+ it 'should raise a TypeError if asked for probability of non-Integer' do
399
+ expect(-> { pr10.given_le({}) }).to raise_error TypeError
400
+ end
401
+ end
402
+
403
+ describe '#repeat_sum' do
404
+ it 'should output a valid distribution if params are valid' do
405
+ d4a = GamesDice::Probabilities.new([1.0 / 4, 1.0 / 4, 1.0 / 4, 1.0 / 4], 1)
406
+ d4b = GamesDice::Probabilities.new([1.0 / 10, 2.0 / 10, 3.0 / 10, 4.0 / 10], 1)
407
+ pr = d4a.repeat_sum(7)
408
+ expect(pr.to_h).to be_valid_distribution
409
+ pr = d4b.repeat_sum(12)
410
+ expect(pr.to_h).to be_valid_distribution
411
+ end
412
+
413
+ it 'should raise an error if any param is unexpected type' do
414
+ d6 = GamesDice::Probabilities.for_fair_die(6)
415
+ expect(-> { d6.repeat_sum({}) }).to raise_error TypeError
416
+ end
417
+
418
+ it 'should raise an error if distribution would have more than a million results' do
419
+ d1000 = GamesDice::Probabilities.for_fair_die(1000)
420
+ expect(-> { d1000.repeat_sum(11_000) }).to raise_error(RuntimeError, /Too many probability slots/)
421
+ end
422
+
423
+ it "should calculate a '3d6' distribution accurately" do
424
+ d6 = GamesDice::Probabilities.for_fair_die(6)
425
+ pr = d6.repeat_sum(3)
426
+ h = pr.to_h
427
+ expect(h).to be_valid_distribution
428
+ expect(h[3]).to be_within(1e-9).of 1.0 / 216
429
+ expect(h[4]).to be_within(1e-9).of 3.0 / 216
430
+ expect(h[5]).to be_within(1e-9).of 6.0 / 216
431
+ expect(h[6]).to be_within(1e-9).of 10.0 / 216
432
+ expect(h[7]).to be_within(1e-9).of 15.0 / 216
433
+ expect(h[8]).to be_within(1e-9).of 21.0 / 216
434
+ expect(h[9]).to be_within(1e-9).of 25.0 / 216
435
+ expect(h[10]).to be_within(1e-9).of 27.0 / 216
436
+ expect(h[11]).to be_within(1e-9).of 27.0 / 216
437
+ expect(h[12]).to be_within(1e-9).of 25.0 / 216
438
+ expect(h[13]).to be_within(1e-9).of 21.0 / 216
439
+ expect(h[14]).to be_within(1e-9).of 15.0 / 216
440
+ expect(h[15]).to be_within(1e-9).of 10.0 / 216
441
+ expect(h[16]).to be_within(1e-9).of 6.0 / 216
442
+ expect(h[17]).to be_within(1e-9).of 3.0 / 216
443
+ expect(h[18]).to be_within(1e-9).of 1.0 / 216
444
+ end
445
+ end
446
+
447
+ describe '#repeat_n_sum_k' do
448
+ it 'should output a valid distribution if params are valid' do
449
+ d4a = GamesDice::Probabilities.new([1.0 / 4, 1.0 / 4, 1.0 / 4, 1.0 / 4], 1)
450
+ d4b = GamesDice::Probabilities.new([1.0 / 10, 2.0 / 10, 3.0 / 10, 4.0 / 10], 1)
451
+ pr = d4a.repeat_n_sum_k(3, 2)
452
+ expect(pr.to_h).to be_valid_distribution
453
+ pr = d4b.repeat_n_sum_k(12, 4)
454
+ expect(pr.to_h).to be_valid_distribution
455
+ end
456
+
457
+ it 'should raise an error if any param is unexpected type' do
458
+ d6 = GamesDice::Probabilities.for_fair_die(6)
459
+ expect(-> { d6.repeat_n_sum_k({}, 10) }).to raise_error TypeError
460
+ expect(-> { d6.repeat_n_sum_k(10, {}) }).to raise_error TypeError
461
+ end
462
+
463
+ it 'should raise an error if n is greater than 170' do
464
+ d6 = GamesDice::Probabilities.for_fair_die(6)
465
+ expect(-> { d6.repeat_n_sum_k(171, 10) }).to raise_error(RuntimeError, /Too many dice/)
466
+ end
467
+
468
+ it "should calculate a '4d6 keep best 3' distribution accurately" do
469
+ d6 = GamesDice::Probabilities.for_fair_die(6)
470
+ pr = d6.repeat_n_sum_k(4, 3)
471
+ h = pr.to_h
472
+ expect(h).to be_valid_distribution
473
+ expect(h[3]).to be_within(1e-10).of 1 / 1296.0
474
+ expect(h[4]).to be_within(1e-10).of 4 / 1296.0
475
+ expect(h[5]).to be_within(1e-10).of 10 / 1296.0
476
+ expect(h[6]).to be_within(1e-10).of 21 / 1296.0
477
+ expect(h[7]).to be_within(1e-10).of 38 / 1296.0
478
+ expect(h[8]).to be_within(1e-10).of 62 / 1296.0
479
+ expect(h[9]).to be_within(1e-10).of 91 / 1296.0
480
+ expect(h[10]).to be_within(1e-10).of 122 / 1296.0
481
+ expect(h[11]).to be_within(1e-10).of 148 / 1296.0
482
+ expect(h[12]).to be_within(1e-10).of 167 / 1296.0
483
+ expect(h[13]).to be_within(1e-10).of 172 / 1296.0
484
+ expect(h[14]).to be_within(1e-10).of 160 / 1296.0
485
+ expect(h[15]).to be_within(1e-10).of 131 / 1296.0
486
+ expect(h[16]).to be_within(1e-10).of 94 / 1296.0
487
+ expect(h[17]).to be_within(1e-10).of 54 / 1296.0
488
+ expect(h[18]).to be_within(1e-10).of 21 / 1296.0
489
+ end
490
+
491
+ it "should calculate a '2d20 keep worst result' distribution accurately" do
492
+ d20 = GamesDice::Probabilities.for_fair_die(20)
493
+ pr = d20.repeat_n_sum_k(2, 1, :keep_worst)
494
+ h = pr.to_h
495
+ expect(h).to be_valid_distribution
496
+ expect(h[1]).to be_within(1e-10).of 39 / 400.0
497
+ expect(h[2]).to be_within(1e-10).of 37 / 400.0
498
+ expect(h[3]).to be_within(1e-10).of 35 / 400.0
499
+ expect(h[4]).to be_within(1e-10).of 33 / 400.0
500
+ expect(h[5]).to be_within(1e-10).of 31 / 400.0
501
+ expect(h[6]).to be_within(1e-10).of 29 / 400.0
502
+ expect(h[7]).to be_within(1e-10).of 27 / 400.0
503
+ expect(h[8]).to be_within(1e-10).of 25 / 400.0
504
+ expect(h[9]).to be_within(1e-10).of 23 / 400.0
505
+ expect(h[10]).to be_within(1e-10).of 21 / 400.0
506
+ expect(h[11]).to be_within(1e-10).of 19 / 400.0
507
+ expect(h[12]).to be_within(1e-10).of 17 / 400.0
508
+ expect(h[13]).to be_within(1e-10).of 15 / 400.0
509
+ expect(h[14]).to be_within(1e-10).of 13 / 400.0
510
+ expect(h[15]).to be_within(1e-10).of 11 / 400.0
511
+ expect(h[16]).to be_within(1e-10).of 9 / 400.0
512
+ expect(h[17]).to be_within(1e-10).of 7 / 400.0
513
+ expect(h[18]).to be_within(1e-10).of 5 / 400.0
514
+ expect(h[19]).to be_within(1e-10).of 3 / 400.0
515
+ expect(h[20]).to be_within(1e-10).of 1 / 400.0
516
+ end
517
+ end
518
+ end
519
+
520
+ describe 'serialisation via Marshall' do
521
+ it 'can load a saved GamesDice::Probabilities' do
522
+ # rubocop:disable Security/MarshalLoad
523
+ # This is a test of using Marshal on a fixed test file
524
+ pd6 = File.open(fixture('probs_fair_die_6.dat')) { |file| Marshal.load(file) }
525
+ # rubocop:enable Security/MarshalLoad
526
+ expect(pd6.to_h).to be_valid_distribution
527
+ expect(pd6.p_gt(4)).to be_within(1e-10).of 1.0 / 3
528
+ end
529
+ end
530
+ end