games_dice 0.3.12 → 0.4.2

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