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.
- checksums.yaml +7 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +9 -12
- data/CHANGELOG.md +29 -13
- data/Gemfile +2 -0
- data/README.md +5 -5
- data/Rakefile +14 -11
- data/ext/games_dice/extconf.rb +4 -22
- data/ext/games_dice/probabilities.c +1 -1
- data/games_dice.gemspec +26 -28
- data/lib/games_dice/bunch.rb +241 -247
- data/lib/games_dice/complex_die.rb +287 -303
- data/lib/games_dice/complex_die_helpers.rb +68 -0
- data/lib/games_dice/constants.rb +10 -10
- data/lib/games_dice/dice.rb +146 -143
- data/lib/games_dice/die.rb +101 -97
- data/lib/games_dice/die_result.rb +193 -189
- data/lib/games_dice/map_rule.rb +72 -70
- data/lib/games_dice/marshal.rb +18 -13
- data/lib/games_dice/parser.rb +219 -218
- data/lib/games_dice/reroll_rule.rb +76 -77
- data/lib/games_dice/version.rb +3 -1
- data/lib/games_dice.rb +19 -16
- data/spec/bunch_spec.rb +399 -421
- data/spec/complex_die_spec.rb +314 -306
- data/spec/dice_spec.rb +33 -34
- data/spec/die_result_spec.rb +163 -170
- data/spec/die_spec.rb +81 -82
- data/spec/helpers.rb +26 -22
- data/spec/map_rule_spec.rb +40 -44
- data/spec/parser_spec.rb +106 -82
- data/spec/probability_spec.rb +530 -527
- data/spec/readme_spec.rb +404 -384
- data/spec/reroll_rule_spec.rb +40 -44
- metadata +63 -74
- data/lib/games_dice/probabilities.rb +0 -445
data/spec/probability_spec.rb
CHANGED
@@ -1,527 +1,530 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
pr
|
10
|
-
pr.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
pr2
|
32
|
-
pr2.
|
33
|
-
(
|
34
|
-
|
35
|
-
pr
|
36
|
-
|
37
|
-
h.
|
38
|
-
h.
|
39
|
-
h.
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
pr.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
h.
|
66
|
-
h
|
67
|
-
h[
|
68
|
-
h[
|
69
|
-
h[
|
70
|
-
h[
|
71
|
-
h[
|
72
|
-
h[
|
73
|
-
h[
|
74
|
-
h[
|
75
|
-
h[
|
76
|
-
h[
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
pr.
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
h.
|
101
|
-
h
|
102
|
-
h[-
|
103
|
-
h[-
|
104
|
-
h[
|
105
|
-
h[
|
106
|
-
h[
|
107
|
-
h[
|
108
|
-
h[
|
109
|
-
h[
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
h.
|
118
|
-
h
|
119
|
-
h[
|
120
|
-
h[
|
121
|
-
h[
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
pr
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
lang
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
let(:
|
175
|
-
let(:
|
176
|
-
let(:
|
177
|
-
let(:
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
pra.p_gt(-
|
226
|
-
pra.p_gt(
|
227
|
-
pra.p_gt(
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
pd
|
376
|
-
pd
|
377
|
-
pd.
|
378
|
-
pd.
|
379
|
-
pd.p_eql(
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
pd
|
391
|
-
pd
|
392
|
-
pd.
|
393
|
-
pd.
|
394
|
-
pd.p_eql(
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
pr.
|
408
|
-
pr
|
409
|
-
pr.
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
h.
|
427
|
-
h
|
428
|
-
h[
|
429
|
-
h[
|
430
|
-
h[
|
431
|
-
h[
|
432
|
-
h[
|
433
|
-
h[
|
434
|
-
h[
|
435
|
-
h[
|
436
|
-
h[
|
437
|
-
h[
|
438
|
-
h[
|
439
|
-
h[
|
440
|
-
h[
|
441
|
-
h[
|
442
|
-
h[
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
pr.
|
452
|
-
pr
|
453
|
-
pr.
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
h.
|
472
|
-
h
|
473
|
-
h[
|
474
|
-
h[
|
475
|
-
h[
|
476
|
-
h[
|
477
|
-
h[
|
478
|
-
h[
|
479
|
-
h[
|
480
|
-
h[
|
481
|
-
h[
|
482
|
-
h[
|
483
|
-
h[
|
484
|
-
h[
|
485
|
-
h[
|
486
|
-
h[
|
487
|
-
h[
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
h.
|
495
|
-
h
|
496
|
-
h[
|
497
|
-
h[
|
498
|
-
h[
|
499
|
-
h[
|
500
|
-
h[
|
501
|
-
h[
|
502
|
-
h[
|
503
|
-
h[
|
504
|
-
h[
|
505
|
-
h[
|
506
|
-
h[
|
507
|
-
h[
|
508
|
-
h[
|
509
|
-
h[
|
510
|
-
h[
|
511
|
-
h[
|
512
|
-
h[
|
513
|
-
h[
|
514
|
-
h[
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
end
|
519
|
-
|
520
|
-
describe
|
521
|
-
it
|
522
|
-
|
523
|
-
|
524
|
-
pd6.
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|