games_dice 0.4.0 → 0.5.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 +4 -4
- data/.github/workflows/ci.yml +92 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +41 -3
- data/.yardopts +1 -1
- data/CHANGELOG.md +19 -0
- data/Gemfile +11 -0
- data/README.md +11 -33
- data/Rakefile +79 -14
- data/ext/games_dice/extconf.rb +29 -0
- data/ext/games_dice/games_dice.c +2 -2
- data/ext/games_dice/probabilities.c +178 -29
- data/ext/games_dice/probabilities.h +1 -17
- data/games_dice.gemspec +11 -19
- data/lib/games_dice/bunch.rb +27 -65
- data/lib/games_dice/bunch_helpers.rb +68 -0
- data/lib/games_dice/complex_die.rb +10 -146
- data/lib/games_dice/complex_die_helpers.rb +238 -46
- data/lib/games_dice/dice.rb +17 -10
- data/lib/games_dice/die.rb +2 -2
- data/lib/games_dice/die_result.rb +84 -71
- data/lib/games_dice/marshal.rb +26 -3
- data/lib/games_dice/parser.rb +128 -102
- data/lib/games_dice/version.rb +2 -1
- data/lib/games_dice.rb +7 -9
- data/spec/bunch_spec.rb +72 -72
- data/spec/complex_die_spec.rb +158 -158
- data/spec/dice_spec.rb +5 -5
- data/spec/die_result_spec.rb +98 -98
- data/spec/die_spec.rb +19 -19
- data/spec/helpers.rb +2 -4
- data/spec/map_rule_spec.rb +17 -17
- data/spec/parser_spec.rb +7 -7
- data/spec/probability_spec.rb +248 -194
- data/spec/readme_spec.rb +28 -22
- data/spec/reroll_rule_spec.rb +15 -15
- metadata +16 -139
- data/.travis.yml +0 -11
data/spec/probability_spec.rb
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'helpers'
|
|
4
|
+
require 'objspace'
|
|
4
5
|
|
|
5
6
|
describe GamesDice::Probabilities do
|
|
6
7
|
describe 'class methods' do
|
|
7
8
|
describe '#new' do
|
|
8
|
-
it '
|
|
9
|
-
pr =
|
|
10
|
-
expect(pr).to be_a
|
|
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
|
|
11
12
|
expect(pr.to_h).to be_valid_distribution
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
it '
|
|
15
|
-
expect
|
|
16
|
-
expect
|
|
17
|
-
expect
|
|
18
|
-
expect
|
|
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
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
it '
|
|
22
|
-
expect
|
|
23
|
-
expect
|
|
24
|
-
expect
|
|
25
|
-
expect
|
|
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
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
describe '#for_fair_die' do
|
|
30
|
-
it '
|
|
31
|
-
pr2 =
|
|
32
|
-
expect(pr2).to be_a
|
|
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
|
|
33
35
|
expect(pr2.to_h).to eql({ 1 => 0.5, 2 => 0.5 })
|
|
34
36
|
(1..20).each do |sides|
|
|
35
|
-
pr =
|
|
36
|
-
expect(pr).to be_a
|
|
37
|
+
pr = described_class.for_fair_die(sides)
|
|
38
|
+
expect(pr).to be_a described_class
|
|
37
39
|
h = pr.to_h
|
|
38
40
|
expect(h).to be_valid_distribution
|
|
39
41
|
expect(h.keys.count).to eql sides
|
|
@@ -41,27 +43,27 @@ describe GamesDice::Probabilities do
|
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
it '
|
|
45
|
-
expect
|
|
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
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
it '
|
|
49
|
-
expect
|
|
50
|
-
expect
|
|
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
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
describe '#add_distributions' do
|
|
55
|
-
it '
|
|
56
|
-
d4a =
|
|
57
|
-
d4b =
|
|
58
|
-
pr =
|
|
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)
|
|
59
61
|
expect(pr.to_h).to be_valid_distribution
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
it '
|
|
63
|
-
d6 =
|
|
64
|
-
pr =
|
|
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)
|
|
65
67
|
h = pr.to_h
|
|
66
68
|
expect(h).to be_valid_distribution
|
|
67
69
|
expect(h[2]).to be_within(1e-9).of 1.0 / 36
|
|
@@ -77,26 +79,26 @@ describe GamesDice::Probabilities do
|
|
|
77
79
|
expect(h[12]).to be_within(1e-9).of 1.0 / 36
|
|
78
80
|
end
|
|
79
81
|
|
|
80
|
-
it '
|
|
81
|
-
d10 =
|
|
82
|
-
expect
|
|
83
|
-
expect
|
|
84
|
-
expect
|
|
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
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
describe '#add_distributions_mult' do
|
|
89
|
-
it '
|
|
90
|
-
d4a =
|
|
91
|
-
d4b =
|
|
92
|
-
pr =
|
|
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)
|
|
93
95
|
expect(pr.to_h).to be_valid_distribution
|
|
94
96
|
end
|
|
95
97
|
|
|
96
|
-
it "
|
|
97
|
-
d6 =
|
|
98
|
-
d4 =
|
|
99
|
-
pr =
|
|
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)
|
|
100
102
|
h = pr.to_h
|
|
101
103
|
expect(h).to be_valid_distribution
|
|
102
104
|
expect(h[-3]).to be_within(1e-9).of 1.0 / 24
|
|
@@ -110,10 +112,10 @@ describe GamesDice::Probabilities do
|
|
|
110
112
|
expect(h[5]).to be_within(1e-9).of 1.0 / 24
|
|
111
113
|
end
|
|
112
114
|
|
|
113
|
-
it '
|
|
114
|
-
da =
|
|
115
|
-
db =
|
|
116
|
-
pr =
|
|
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)
|
|
117
119
|
h = pr.to_h
|
|
118
120
|
expect(h).to be_valid_distribution
|
|
119
121
|
expect(h[6]).to be_within(1e-9).of 0.7 * 0.5
|
|
@@ -122,70 +124,83 @@ describe GamesDice::Probabilities do
|
|
|
122
124
|
expect(h[12]).to be_within(1e-9).of 0.3 * 0.2
|
|
123
125
|
end
|
|
124
126
|
|
|
125
|
-
it '
|
|
126
|
-
d10 =
|
|
127
|
-
expect
|
|
128
|
-
expect
|
|
129
|
-
expect
|
|
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
|
|
130
132
|
end
|
|
131
133
|
|
|
132
|
-
it '
|
|
133
|
-
d10 =
|
|
134
|
-
expect
|
|
135
|
-
expect
|
|
136
|
-
expect
|
|
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
|
|
137
139
|
end
|
|
138
140
|
end
|
|
139
141
|
|
|
140
142
|
describe '#from_h' do
|
|
141
|
-
it '
|
|
142
|
-
pr =
|
|
143
|
-
expect(pr).to be_a
|
|
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
|
|
144
146
|
end
|
|
145
147
|
|
|
146
|
-
it '
|
|
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 })
|
|
148
151
|
end
|
|
149
152
|
|
|
150
|
-
it '
|
|
151
|
-
expect
|
|
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
|
|
152
156
|
end
|
|
153
157
|
|
|
154
|
-
it '
|
|
155
|
-
expect
|
|
156
|
-
expect(-> { GamesDice::Probabilities.from_h({ 7 => [], 9 => 0.5 }) }).to raise_error TypeError
|
|
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
|
|
157
160
|
end
|
|
158
161
|
|
|
159
|
-
it '
|
|
160
|
-
expect
|
|
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
|
|
161
165
|
end
|
|
162
|
-
end
|
|
163
166
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
lang = GamesDice::Probabilities.implemented_in
|
|
167
|
-
expect(lang).to be_a Symbol
|
|
168
|
-
expect(%i[c ruby].member?(lang)).to eql true
|
|
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
169
|
end
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
describe 'instance methods' do
|
|
174
|
-
let(:pr2) {
|
|
175
|
-
let(:pr4) {
|
|
176
|
-
let(:pr6) {
|
|
177
|
-
let(:pr10) {
|
|
178
|
-
let(:pra) {
|
|
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
|
|
179
194
|
|
|
180
195
|
describe '#each' do
|
|
181
|
-
it '
|
|
196
|
+
it 'iterate through all result/probability pairs' do
|
|
182
197
|
yielded = []
|
|
183
198
|
pr4.each { |r, p| yielded << [r, p] }
|
|
184
199
|
expect(yielded).to eql [[1, 0.25], [2, 0.25], [3, 0.25], [4, 0.25]]
|
|
185
200
|
end
|
|
186
201
|
|
|
187
|
-
it '
|
|
188
|
-
pr_plus_minus =
|
|
202
|
+
it 'skip zero probabilities' do
|
|
203
|
+
pr_plus_minus = described_class.new([0.5, 0.0, 0.5], -1)
|
|
189
204
|
yielded = []
|
|
190
205
|
pr_plus_minus.each { |r, p| yielded << [r, p] }
|
|
191
206
|
expect(yielded).to eql [[-1, 0.5], [1, 0.5]]
|
|
@@ -193,7 +208,7 @@ describe GamesDice::Probabilities do
|
|
|
193
208
|
end
|
|
194
209
|
|
|
195
210
|
describe '#p_eql' do
|
|
196
|
-
it '
|
|
211
|
+
it 'return probability of getting a number inside the range' do
|
|
197
212
|
expect(pr2.p_eql(2)).to be_within(1.0e-9).of 0.5
|
|
198
213
|
expect(pr4.p_eql(1)).to be_within(1.0e-9).of 0.25
|
|
199
214
|
expect(pr6.p_eql(6)).to be_within(1.0e-9).of 1.0 / 6
|
|
@@ -201,21 +216,21 @@ describe GamesDice::Probabilities do
|
|
|
201
216
|
expect(pra.p_eql(-1)).to be_within(1.0e-9).of 0.4
|
|
202
217
|
end
|
|
203
218
|
|
|
204
|
-
it '
|
|
205
|
-
expect(pr2.p_eql(3)).to
|
|
206
|
-
expect(pr4.p_eql(-1)).to
|
|
207
|
-
expect(pr6.p_eql(8)).to
|
|
208
|
-
expect(pr10.p_eql(11)).to
|
|
209
|
-
expect(pra.p_eql(2)).to
|
|
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
|
|
210
225
|
end
|
|
211
226
|
|
|
212
|
-
it '
|
|
213
|
-
expect
|
|
227
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
228
|
+
expect { pr2.p_eql([]) }.to raise_error TypeError
|
|
214
229
|
end
|
|
215
230
|
end
|
|
216
231
|
|
|
217
232
|
describe '#p_gt' do
|
|
218
|
-
it '
|
|
233
|
+
it 'return probability of getting a number greater than target' do
|
|
219
234
|
expect(pr2.p_gt(1)).to be_within(1.0e-9).of 0.5
|
|
220
235
|
expect(pr4.p_gt(3)).to be_within(1.0e-9).of 0.25
|
|
221
236
|
expect(pr6.p_gt(2)).to be_within(1.0e-9).of 4.0 / 6
|
|
@@ -228,111 +243,111 @@ describe GamesDice::Probabilities do
|
|
|
228
243
|
expect(pra.p_gt(1)).to be_within(1.0e-9).of 0.0
|
|
229
244
|
end
|
|
230
245
|
|
|
231
|
-
it '
|
|
232
|
-
expect(pr2.p_gt(2)).to
|
|
233
|
-
expect(pr4.p_gt(5)).to
|
|
234
|
-
expect(pr6.p_gt(6)).to
|
|
235
|
-
expect(pr10.p_gt(20)).to
|
|
236
|
-
expect(pra.p_gt(3)).to
|
|
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
|
|
237
252
|
end
|
|
238
253
|
|
|
239
|
-
it '
|
|
240
|
-
expect(pr2.p_gt(0)).to
|
|
241
|
-
expect(pr4.p_gt(-5)).to
|
|
242
|
-
expect(pr6.p_gt(0)).to
|
|
243
|
-
expect(pr10.p_gt(-200)).to
|
|
244
|
-
expect(pra.p_gt(-2)).to
|
|
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
|
|
245
260
|
end
|
|
246
261
|
|
|
247
|
-
it '
|
|
248
|
-
expect
|
|
262
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
263
|
+
expect { pr2.p_gt({}) }.to raise_error TypeError
|
|
249
264
|
end
|
|
250
265
|
end
|
|
251
266
|
|
|
252
267
|
describe '#p_ge' do
|
|
253
|
-
it '
|
|
268
|
+
it 'return probability of getting a number greater than or equal to target' do
|
|
254
269
|
expect(pr2.p_ge(2)).to be_within(1.0e-9).of 0.5
|
|
255
270
|
expect(pr4.p_ge(3)).to be_within(1.0e-9).of 0.5
|
|
256
271
|
expect(pr6.p_ge(2)).to be_within(1.0e-9).of 5.0 / 6
|
|
257
272
|
expect(pr10.p_ge(6)).to be_within(1.0e-9).of 0.5
|
|
258
273
|
end
|
|
259
274
|
|
|
260
|
-
it '
|
|
261
|
-
expect(pr2.p_ge(6)).to
|
|
262
|
-
expect(pr4.p_ge(5)).to
|
|
263
|
-
expect(pr6.p_ge(7)).to
|
|
264
|
-
expect(pr10.p_ge(20)).to
|
|
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
|
|
265
280
|
end
|
|
266
281
|
|
|
267
|
-
it '
|
|
268
|
-
expect(pr2.p_ge(1)).to
|
|
269
|
-
expect(pr4.p_ge(-5)).to
|
|
270
|
-
expect(pr6.p_ge(1)).to
|
|
271
|
-
expect(pr10.p_ge(-200)).to
|
|
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
|
|
272
287
|
end
|
|
273
288
|
|
|
274
|
-
it '
|
|
275
|
-
expect
|
|
289
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
290
|
+
expect { pr4.p_ge({}) }.to raise_error TypeError
|
|
276
291
|
end
|
|
277
292
|
end
|
|
278
293
|
|
|
279
294
|
describe '#p_le' do
|
|
280
|
-
it '
|
|
295
|
+
it 'return probability of getting a number less than or equal to target' do
|
|
281
296
|
expect(pr2.p_le(1)).to be_within(1.0e-9).of 0.5
|
|
282
297
|
expect(pr4.p_le(2)).to be_within(1.0e-9).of 0.5
|
|
283
298
|
expect(pr6.p_le(2)).to be_within(1.0e-9).of 2.0 / 6
|
|
284
299
|
expect(pr10.p_le(6)).to be_within(1.0e-9).of 0.6
|
|
285
300
|
end
|
|
286
301
|
|
|
287
|
-
it '
|
|
288
|
-
expect(pr2.p_le(6)).to
|
|
289
|
-
expect(pr4.p_le(4)).to
|
|
290
|
-
expect(pr6.p_le(7)).to
|
|
291
|
-
expect(pr10.p_le(10)).to
|
|
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
|
|
292
307
|
end
|
|
293
308
|
|
|
294
|
-
it '
|
|
295
|
-
expect(pr2.p_le(0)).to
|
|
296
|
-
expect(pr4.p_le(-5)).to
|
|
297
|
-
expect(pr6.p_le(0)).to
|
|
298
|
-
expect(pr10.p_le(-200)).to
|
|
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
|
|
299
314
|
end
|
|
300
315
|
|
|
301
|
-
it '
|
|
302
|
-
expect
|
|
316
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
317
|
+
expect { pr4.p_le([]) }.to raise_error TypeError
|
|
303
318
|
end
|
|
304
319
|
end
|
|
305
320
|
|
|
306
321
|
describe '#p_lt' do
|
|
307
|
-
it '
|
|
322
|
+
it 'return probability of getting a number less than target' do
|
|
308
323
|
expect(pr2.p_lt(2)).to be_within(1.0e-9).of 0.5
|
|
309
324
|
expect(pr4.p_lt(3)).to be_within(1.0e-9).of 0.5
|
|
310
325
|
expect(pr6.p_lt(2)).to be_within(1.0e-9).of 1 / 6.0
|
|
311
326
|
expect(pr10.p_lt(6)).to be_within(1.0e-9).of 0.5
|
|
312
327
|
end
|
|
313
328
|
|
|
314
|
-
it '
|
|
315
|
-
expect(pr2.p_lt(6)).to
|
|
316
|
-
expect(pr4.p_lt(5)).to
|
|
317
|
-
expect(pr6.p_lt(7)).to
|
|
318
|
-
expect(pr10.p_lt(20)).to
|
|
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
|
|
319
334
|
end
|
|
320
335
|
|
|
321
|
-
it '
|
|
322
|
-
expect(pr2.p_lt(1)).to
|
|
323
|
-
expect(pr4.p_lt(-5)).to
|
|
324
|
-
expect(pr6.p_lt(1)).to
|
|
325
|
-
expect(pr10.p_lt(-200)).to
|
|
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
|
|
326
341
|
end
|
|
327
342
|
|
|
328
|
-
it '
|
|
329
|
-
expect
|
|
343
|
+
it 'raise a TypeError if asked for probability of non-Integer' do
|
|
344
|
+
expect { pr6.p_lt({}) }.to raise_error TypeError
|
|
330
345
|
end
|
|
331
346
|
end
|
|
332
347
|
|
|
333
348
|
describe '#to_h' do
|
|
334
349
|
# This is used loads in other tests
|
|
335
|
-
it '
|
|
350
|
+
it 'represent a valid distribution with each integer result associated with its probability' do
|
|
336
351
|
expect(pr2.to_h).to be_valid_distribution
|
|
337
352
|
expect(pr4.to_h).to be_valid_distribution
|
|
338
353
|
expect(pr6.to_h).to be_valid_distribution
|
|
@@ -341,87 +356,101 @@ describe GamesDice::Probabilities do
|
|
|
341
356
|
end
|
|
342
357
|
|
|
343
358
|
describe '#min' do
|
|
344
|
-
it '
|
|
345
|
-
expect(pr2.min).to
|
|
346
|
-
expect(pr4.min).to
|
|
347
|
-
expect(pr6.min).to
|
|
348
|
-
expect(pr10.min).to
|
|
349
|
-
expect(
|
|
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
|
|
350
365
|
end
|
|
351
366
|
end
|
|
352
367
|
|
|
353
368
|
describe '#max' do
|
|
354
|
-
it '
|
|
355
|
-
expect(pr2.max).to
|
|
356
|
-
expect(pr4.max).to
|
|
357
|
-
expect(pr6.max).to
|
|
358
|
-
expect(pr10.max).to
|
|
359
|
-
expect(
|
|
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
|
|
360
375
|
end
|
|
361
376
|
end
|
|
362
377
|
|
|
363
378
|
describe '#expected' do
|
|
364
|
-
it '
|
|
379
|
+
it 'return the weighted mean value' do
|
|
365
380
|
expect(pr2.expected).to be_within(1.0e-9).of 1.5
|
|
366
381
|
expect(pr4.expected).to be_within(1.0e-9).of 2.5
|
|
367
382
|
expect(pr6.expected).to be_within(1.0e-9).of 3.5
|
|
368
383
|
expect(pr10.expected).to be_within(1.0e-9).of 5.5
|
|
369
|
-
expect(
|
|
384
|
+
expect(described_class.add_distributions(pr6, pr10).expected).to be_within(1.0e-9).of 9.0
|
|
370
385
|
end
|
|
371
386
|
end
|
|
372
387
|
|
|
373
388
|
describe '#given_ge' do
|
|
374
|
-
it '
|
|
389
|
+
it 'return a new distribution with probabilities calculated assuming value is >= target' do
|
|
375
390
|
pd = pr2.given_ge(2)
|
|
376
391
|
expect(pd.to_h).to eql({ 2 => 1.0 })
|
|
377
392
|
pd = pr10.given_ge(4)
|
|
378
393
|
expect(pd.to_h).to be_valid_distribution
|
|
379
|
-
expect(pd.p_eql(3)).to
|
|
394
|
+
expect(pd.p_eql(3)).to be 0.0
|
|
380
395
|
expect(pd.p_eql(10)).to be_within(1.0e-9).of 0.1 / 0.7
|
|
381
396
|
end
|
|
382
397
|
|
|
383
|
-
it '
|
|
384
|
-
expect
|
|
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/)
|
|
385
405
|
end
|
|
386
406
|
end
|
|
387
407
|
|
|
388
408
|
describe '#given_le' do
|
|
389
|
-
it '
|
|
409
|
+
it 'return a new distribution with probabilities calculated assuming value is <= target' do
|
|
390
410
|
pd = pr2.given_le(2)
|
|
391
411
|
expect(pd.to_h).to eql({ 1 => 0.5, 2 => 0.5 })
|
|
392
412
|
pd = pr10.given_le(4)
|
|
393
413
|
expect(pd.to_h).to be_valid_distribution
|
|
394
414
|
expect(pd.p_eql(3)).to be_within(1.0e-9).of 0.1 / 0.4
|
|
395
|
-
expect(pd.p_eql(10)).to
|
|
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
|
|
396
420
|
end
|
|
397
421
|
|
|
398
|
-
it '
|
|
399
|
-
expect(
|
|
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/)
|
|
400
425
|
end
|
|
401
426
|
end
|
|
402
427
|
|
|
403
428
|
describe '#repeat_sum' do
|
|
404
|
-
it '
|
|
405
|
-
d4a =
|
|
406
|
-
d4b =
|
|
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)
|
|
407
432
|
pr = d4a.repeat_sum(7)
|
|
408
433
|
expect(pr.to_h).to be_valid_distribution
|
|
409
434
|
pr = d4b.repeat_sum(12)
|
|
410
435
|
expect(pr.to_h).to be_valid_distribution
|
|
411
436
|
end
|
|
412
437
|
|
|
413
|
-
it '
|
|
414
|
-
d6 =
|
|
415
|
-
expect
|
|
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/)
|
|
416
445
|
end
|
|
417
446
|
|
|
418
|
-
it '
|
|
419
|
-
d1000 =
|
|
420
|
-
expect
|
|
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/)
|
|
421
450
|
end
|
|
422
451
|
|
|
423
|
-
it "
|
|
424
|
-
d6 =
|
|
452
|
+
it "calculate a '3d6' distribution accurately" do
|
|
453
|
+
d6 = described_class.for_fair_die(6)
|
|
425
454
|
pr = d6.repeat_sum(3)
|
|
426
455
|
h = pr.to_h
|
|
427
456
|
expect(h).to be_valid_distribution
|
|
@@ -445,28 +474,46 @@ describe GamesDice::Probabilities do
|
|
|
445
474
|
end
|
|
446
475
|
|
|
447
476
|
describe '#repeat_n_sum_k' do
|
|
448
|
-
it '
|
|
449
|
-
d4a =
|
|
450
|
-
d4b =
|
|
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)
|
|
451
480
|
pr = d4a.repeat_n_sum_k(3, 2)
|
|
452
481
|
expect(pr.to_h).to be_valid_distribution
|
|
453
482
|
pr = d4b.repeat_n_sum_k(12, 4)
|
|
454
483
|
expect(pr.to_h).to be_valid_distribution
|
|
455
484
|
end
|
|
456
485
|
|
|
457
|
-
it '
|
|
458
|
-
d6 =
|
|
459
|
-
expect
|
|
460
|
-
expect
|
|
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
|
|
461
490
|
end
|
|
462
491
|
|
|
463
|
-
it '
|
|
464
|
-
|
|
465
|
-
expect
|
|
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/)
|
|
466
495
|
end
|
|
467
496
|
|
|
468
|
-
it
|
|
469
|
-
|
|
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)
|
|
470
517
|
pr = d6.repeat_n_sum_k(4, 3)
|
|
471
518
|
h = pr.to_h
|
|
472
519
|
expect(h).to be_valid_distribution
|
|
@@ -488,8 +535,8 @@ describe GamesDice::Probabilities do
|
|
|
488
535
|
expect(h[18]).to be_within(1e-10).of 21 / 1296.0
|
|
489
536
|
end
|
|
490
537
|
|
|
491
|
-
it "
|
|
492
|
-
d20 =
|
|
538
|
+
it "calculate a '2d20 keep worst result' distribution accurately" do
|
|
539
|
+
d20 = described_class.for_fair_die(20)
|
|
493
540
|
pr = d20.repeat_n_sum_k(2, 1, :keep_worst)
|
|
494
541
|
h = pr.to_h
|
|
495
542
|
expect(h).to be_valid_distribution
|
|
@@ -514,6 +561,13 @@ describe GamesDice::Probabilities do
|
|
|
514
561
|
expect(h[19]).to be_within(1e-10).of 3 / 400.0
|
|
515
562
|
expect(h[20]).to be_within(1e-10).of 1 / 400.0
|
|
516
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
|
|
517
571
|
end
|
|
518
572
|
end
|
|
519
573
|
|