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