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.
@@ -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 '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
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 '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
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 '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
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 '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
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 = GamesDice::Probabilities.for_fair_die(sides)
36
- expect(pr).to be_a GamesDice::Probabilities
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 '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
+ 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 '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
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 '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)
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 'should calculate a classic 2d6 distribution accurately' do
63
- d6 = GamesDice::Probabilities.for_fair_die(6)
64
- pr = GamesDice::Probabilities.add_distributions(d6, d6)
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 '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
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 '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)
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 "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)
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 '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)
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 '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
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 '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
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 '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
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 '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
+ 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 '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
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 '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
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 '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
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
- 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
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) { 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) }
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 'should iterate through all result/probability pairs' do
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 'should skip zero probabilities' do
188
- pr_plus_minus = GamesDice::Probabilities.new([0.5, 0.0, 0.5], -1)
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 'should return probability of getting a number inside the range' do
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 '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
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 'should raise a TypeError if asked for probability of non-Integer' do
213
- expect(-> { pr2.p_eql([]) }).to raise_error TypeError
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 'should return probability of getting a number greater than target' do
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 '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
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 '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
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 'should raise a TypeError if asked for probability of non-Integer' do
248
- expect(-> { pr2.p_gt({}) }).to raise_error TypeError
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 'should return probability of getting a number greater than or equal to target' do
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 '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
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 '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
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 'should raise a TypeError if asked for probability of non-Integer' do
275
- expect(-> { pr4.p_ge({}) }).to raise_error TypeError
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 'should return probability of getting a number less than or equal to target' do
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 '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
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 '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
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 'should raise a TypeError if asked for probability of non-Integer' do
302
- expect(-> { pr4.p_le([]) }).to raise_error TypeError
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 'should return probability of getting a number less than target' do
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 '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
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 '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
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 'should raise a TypeError if asked for probability of non-Integer' do
329
- expect(-> { pr6.p_lt({}) }).to raise_error TypeError
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 'should represent a valid distribution with each integer result associated with its probability' do
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 '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
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 '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
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 'should return the weighted mean value' do
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(GamesDice::Probabilities.add_distributions(pr6, pr10).expected).to be_within(1.0e-9).of 9.0
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 'should return a new distribution with probabilities calculated assuming value is >= target' do
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 eql 0.0
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 'should raise a TypeError if asked for probability of non-Integer' do
384
- expect(-> { pr10.given_ge([]) }).to raise_error TypeError
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 'should return a new distribution with probabilities calculated assuming value is <= target' do
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 eql 0.0
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 'should raise a TypeError if asked for probability of non-Integer' do
399
- expect(-> { pr10.given_le({}) }).to raise_error TypeError
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 '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)
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 '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
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 '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/)
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 "should calculate a '3d6' distribution accurately" do
424
- d6 = GamesDice::Probabilities.for_fair_die(6)
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 '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)
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 '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
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 '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/)
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 "should calculate a '4d6 keep best 3' distribution accurately" do
469
- d6 = GamesDice::Probabilities.for_fair_die(6)
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 "should calculate a '2d20 keep worst result' distribution accurately" do
492
- d20 = GamesDice::Probabilities.for_fair_die(20)
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