games_dice 0.5.0 → 0.6.0

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