games_dice 0.3.12 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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