games_dice 0.2.3 → 0.2.4

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.
data/.travis.yml CHANGED
@@ -7,4 +7,8 @@ rvm:
7
7
  - rbx-19mode
8
8
  - ruby-head
9
9
  - ree
10
-
10
+ - jruby-18mode
11
+ - jruby-19mode
12
+ env:
13
+ global:
14
+ - "JRUBY_OPTS=-Xcext.enabled=true"
@@ -158,12 +158,8 @@ class GamesDice::Bunch
158
158
  return @probabilities if @probabilities
159
159
  @probabilities_complete = true
160
160
 
161
- # TODO: It is possible to optimise this slightly by combining already-calculated values
162
- # Adding dice is same as multiplying probability sets for that number of dice
163
- # Combine(probabililities_3_dice, probabililities_single_die) == Combine(probabililities_2_dice, probabililities_2_dice)
164
- # It is possible to minimise the total number of multiplications, gaining about 30% efficiency, with careful choices
165
- single_roll_probs = @single_die.probabilities.to_h
166
161
  if @keep_mode && @ndice > @keep_number
162
+ single_roll_probs = @single_die.probabilities.to_h
167
163
  preadd_probs = {}
168
164
  single_roll_probs.each { |k,v| preadd_probs[k.to_s] = v }
169
165
 
@@ -180,15 +176,12 @@ class GamesDice::Bunch
180
176
  combined_probs[total] ||= 0.0
181
177
  combined_probs[total] += v
182
178
  end
179
+ @probabilities = GamesDice::Probabilities.from_h( combined_probs )
183
180
  else
184
- combined_probs = single_roll_probs.clone
185
- (@ndice-1).times do
186
- combined_probs = prob_accumulate combined_probs, single_roll_probs
187
- end
181
+ @probabilities = GamesDice::Probabilities.repeat_distribution( @single_die.probabilities, @ndice )
188
182
  end
189
183
 
190
- @probabilities_min, @probabilities_max = combined_probs.keys.minmax
191
- @probabilities = GamesDice::Probabilities.new( combined_probs )
184
+ return @probabilities
192
185
  end
193
186
 
194
187
  # Simulates rolling the bunch of identical dice
@@ -270,26 +263,10 @@ class GamesDice::Bunch
270
263
 
271
264
  private
272
265
 
273
- # combines two sets of probabilities where the end result is the first set of keys plus
274
- # the second set of keys, at the associated probailities of the values
275
- def prob_accumulate first_probs, second_probs
276
- accumulator = Hash.new
277
-
278
- first_probs.each do |v1,p1|
279
- second_probs.each do |v2,p2|
280
- v3 = v1 + v2
281
- p3 = p1 * p2
282
- accumulator[v3] ||= 0.0
283
- accumulator[v3] += p3
284
- end
285
- end
286
-
287
- accumulator
288
- end
289
-
290
266
  # combines two sets of probabilities, as above, except tracking unique permutations
291
267
  def prob_accumulate_combinations so_far, die_probs, keep_rule = nil
292
268
  accumulator = Hash.new
269
+ accumulator.default = 0.0
293
270
 
294
271
  so_far.each do |sig,p1|
295
272
  combo = sig.split(';').map { |s| s.to_i }
@@ -297,33 +274,32 @@ class GamesDice::Bunch
297
274
  case keep_rule
298
275
  when nil then
299
276
  die_probs.each do |v2,p2|
300
- new_sig = (combo + [v2]).sort.join(';')
277
+ new_sig = (combo + [v2]).sort!.join(';')
301
278
  p3 = p1 * p2
302
- accumulator[new_sig] ||= 0.0
303
279
  accumulator[new_sig] += p3
304
280
  end
305
281
  when :keep_best then
306
282
  need_more_than = combo.min
283
+ len = combo.size
307
284
  die_probs.each do |v2,p2|
308
285
  if v2 > need_more_than
309
- new_sig = (combo + [v2]).sort[1..combo.size].join(';')
286
+ new_sig = (combo + [v2]).sort![1,len].join(';')
310
287
  else
311
288
  new_sig = sig
312
289
  end
313
290
  p3 = p1 * p2
314
- accumulator[new_sig] ||= 0.0
315
291
  accumulator[new_sig] += p3
316
292
  end
317
293
  when :keep_worst then
318
294
  need_less_than = combo.max
295
+ len = combo.size
319
296
  die_probs.each do |v2,p2|
320
297
  if v2 < need_less_than
321
- new_sig = (combo + [v2]).sort[0..(combo.size-1)].join(';')
298
+ new_sig = (combo + [v2]).sort![0,len].join(';')
322
299
  else
323
300
  new_sig = sig
324
301
  end
325
302
  p3 = p1 * p2
326
- accumulator[new_sig] ||= 0.0
327
303
  accumulator[new_sig] += p3
328
304
  end
329
305
  end
@@ -332,34 +308,4 @@ class GamesDice::Bunch
332
308
  accumulator
333
309
  end
334
310
 
335
- # Generates all sets of [throw_away,may_keep_exactly,keep_preferentially,combinations] that meet
336
- # criteria for correct total number of dice and keep dice. These then need to be assessed for every
337
- # die value by the caller to get a full set of probabilities
338
- def generate_item_counts total_dice, keep_dice
339
- # Constraints are:
340
- # may_keep_exactly must be at least 1, and at most is all the dice
341
- # keep_preferentially plus may_keep_exactly must be >= keep_dice, but keep_preferentially < keep dice
342
- # sum of all three always == total_dice
343
- item_counts = []
344
- (1..total_dice).each do |may_keep_exactly|
345
- min_kp = [keep_dice - may_keep_exactly, 0].max
346
- max_kp = [keep_dice - 1, total_dice - may_keep_exactly].min
347
- (min_kp..max_kp).each do |keep_preferentially|
348
- counts = [ total_dice - may_keep_exactly - keep_preferentially, may_keep_exactly, keep_preferentially ]
349
- counts << combinations(counts)
350
- item_counts << counts
351
- end
352
- end
353
- item_counts
354
- end
355
-
356
- # How many unique ways can a set of items, some of which are identical, be arranged?
357
- def combinations item_counts
358
- item_counts = item_counts.map { |i| Integer(i) }.select { |i| i > 0 }
359
- total_items = item_counts.inject(:+)
360
- numerator = 1.upto(total_items).inject(:*)
361
- denominator = item_counts.map { |i| 1.upto(i).inject(:*) }.inject(:*)
362
- numerator / denominator
363
- end
364
-
365
311
  end # class Bunch
@@ -75,8 +75,10 @@ class GamesDice::ComplexDie
75
75
  @result.explain_value
76
76
  end
77
77
 
78
+ # The minimum possible result from a call to #roll. This is not always the same as the theoretical
79
+ # minimum, due to limits on the maximum number of rerolls.
78
80
  # @!attribute [r] min
79
- # @return [Integer] Minimum possible result from a call to #roll
81
+ # @return [Integer]
80
82
  def min
81
83
  return @min_result if @min_result
82
84
  @min_result, @max_result = [probabilities.min, probabilities.max]
@@ -115,19 +117,17 @@ class GamesDice::ComplexDie
115
117
  elsif @rerolls
116
118
  prob_hash = recursive_probabilities
117
119
  elsif @maps
118
- probs = @basic_die.probabilities.to_h
119
120
  prob_hash = {}
120
- probs.each do |v,p|
121
+ @basic_die.probabilities.each do |v,p|
121
122
  m, n = calc_maps(v)
122
123
  prob_hash[m] ||= 0.0
123
124
  prob_hash[m] += p
124
125
  end
125
126
  else
126
- prob_hash = @basic_die.probabilities.to_h
127
+ @probabilities = @basic_die.probabilities
128
+ return @probabilities
127
129
  end
128
- @prob_ge = {}
129
- @prob_le = {}
130
- @probabilities = GamesDice::Probabilities.new( prob_hash )
130
+ @probabilities = GamesDice::Probabilities.from_h( prob_hash )
131
131
  end
132
132
 
133
133
  # Simulates rolling the die
@@ -250,9 +250,7 @@ class GamesDice::ComplexDie
250
250
 
251
251
  (1..@basic_die.sides).each do |v|
252
252
  # calculate value, recurse if there is a reroll
253
- result_so_far = prior_result ? prior_result.clone : GamesDice::DieResult.new(v,roll_reason)
254
- result_so_far.add_roll(v,roll_reason) if prior_result
255
- rerolls_remaining = rerolls_left ? rerolls_left.clone : @rerolls.map { |rule| rule.limit }
253
+ result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason )
256
254
 
257
255
  # Find which rule, if any, is being triggered
258
256
  rule_idx = @rerolls.zip(rerolls_remaining).find_index do |rule,remaining|
@@ -279,7 +277,19 @@ class GamesDice::ComplexDie
279
277
  end
280
278
 
281
279
  end
282
- probabilities.clone
280
+ probabilities
281
+ end
282
+
283
+ def calc_result_so_far prior_result, rerolls_left, v, roll_reason
284
+ if prior_result
285
+ result_so_far = prior_result.clone
286
+ result_so_far.add_roll(v,roll_reason)
287
+ rerolls_remaining = rerolls_left.clone
288
+ else
289
+ result_so_far = GamesDice::DieResult.new(v,roll_reason)
290
+ rerolls_remaining = @rerolls.map { |rule| rule.limit }
291
+ end
292
+ [result_so_far, rerolls_remaining]
283
293
  end
284
294
 
285
295
  end # class ComplexDie
@@ -102,7 +102,7 @@ class GamesDice::Dice
102
102
  # @return [GamesDice::Probabilities] Probability distribution of dice.
103
103
  def probabilities
104
104
  return @probabilities if @probabilities
105
- probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new( { @offset => 1.0 } ) ) do |probs, mb|
105
+ probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new( [1.0], @offset ) ) do |probs, mb|
106
106
  m,b = mb
107
107
  GamesDice::Probabilities.add_distributions_mult( 1, probs, m, b.probabilities )
108
108
  end
@@ -55,8 +55,7 @@ class GamesDice::Die
55
55
  # Calculates probability distribution for this die.
56
56
  # @return [GamesDice::Probabilities] probability distribution of the die
57
57
  def probabilities
58
- return @probabilities if @probabilities
59
- @probabilities = GamesDice::Probabilities.for_fair_die( @sides )
58
+ @probabilities ||= GamesDice::Probabilities.for_fair_die( @sides )
60
59
  end
61
60
 
62
61
  # Simulates rolling the die
@@ -21,52 +21,66 @@
21
21
  class GamesDice::Probabilities
22
22
 
23
23
  # Creates new instance of GamesDice::Probabilities.
24
- # @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
25
- # and the matching value is probability of getting that result
24
+ # @param [Array<Float>] probs Each entry in the array is the probability of getting a result
25
+ # @param [Integer] offset The result associated with index of 0 in the array
26
26
  # @return [GamesDice::Probabilities]
27
- def initialize( prob_hash = { 0 => 1.0 } )
27
+ def initialize( probs = [1.0], offset = 0 )
28
28
  # This should *probably* be validated in future, but that would impact performance
29
- @ph = prob_hash
29
+ @probs = probs
30
+ @offset = offset
30
31
  end
31
32
 
32
33
  # @!visibility private
33
- # the Hash representation of probabilities.
34
- attr_reader :ph
34
+ # the Array, Offset representation of probabilities.
35
+ def to_ao
36
+ [ @probs, @offset ]
37
+ end
38
+
39
+ # Iterates through value, probability pairs
40
+ # @yieldparam [Integer] result A result that may be possible in the dice scheme
41
+ # @yieldparam [Float] probability Probability of result, in range 0.0..1.0
42
+ # @return [GamesDice::Probabilities] this object
43
+ def each
44
+ @probs.each_with_index { |p,i| yield( i+@offset, p ) }
45
+ return self
46
+ end
35
47
 
36
48
  # A hash representation of the distribution. Each key is an integer result,
37
49
  # and the matching value is probability of getting that result. A new hash is generated on each
38
50
  # call to this method.
39
51
  # @return [Hash]
40
52
  def to_h
41
- @ph.clone
53
+ GamesDice::Probabilities.prob_ao_to_h( @probs, @offset )
42
54
  end
43
55
 
44
56
  # @!attribute [r] min
45
57
  # Minimum result in the distribution
46
58
  # @return [Integer]
47
59
  def min
48
- (@minmax ||= @ph.keys.minmax )[0]
60
+ @offset
49
61
  end
50
62
 
51
63
  # @!attribute [r] max
52
64
  # Maximum result in the distribution
53
65
  # @return [Integer]
54
66
  def max
55
- (@minmax ||= @ph.keys.minmax )[1]
67
+ @offset + @probs.count() - 1
56
68
  end
57
69
 
58
70
  # @!attribute [r] expected
59
71
  # Expected value of distribution.
60
72
  # @return [Float]
61
73
  def expected
62
- @expected ||= @ph.inject(0.0) { |accumulate,p| accumulate + p[0] * p[1] }
74
+ @expected ||= calc_expected
63
75
  end
64
76
 
65
77
  # Probability of result equalling specific target
66
78
  # @param [Integer] target
67
79
  # @return [Float] in range (0.0..1.0)
68
80
  def p_eql target
69
- @ph[ Integer(target) ] || 0.0
81
+ i = Integer(target) - @offset
82
+ return 0.0 if i < 0 || i >= @probs.count
83
+ @probs[ i ]
70
84
  end
71
85
 
72
86
  # Probability of result being greater than specific target
@@ -86,7 +100,7 @@ class GamesDice::Probabilities
86
100
 
87
101
  return 1.0 if target <= min
88
102
  return 0.0 if target > max
89
- @prob_ge[target] = @ph.select {|k,v| target <= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
103
+ @prob_ge[target] = @probs[target-@offset,@probs.count-1].inject(0.0) {|so_far,p| so_far + p }
90
104
  end
91
105
 
92
106
  # Probability of result being equal to or less than specific target
@@ -99,7 +113,7 @@ class GamesDice::Probabilities
99
113
 
100
114
  return 1.0 if target >= max
101
115
  return 0.0 if target < min
102
- @prob_le[target] = @ph.select {|k,v| target >= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
116
+ @prob_le[target] = @probs[0,1+target-@offset].inject(0.0) {|so_far,p| so_far + p }
103
117
  end
104
118
 
105
119
  # Probability of result being less than specific target
@@ -109,16 +123,50 @@ class GamesDice::Probabilities
109
123
  p_le( Integer(target) - 1 )
110
124
  end
111
125
 
126
+ # Probability distribution derived from this one, where we know (or are only interested in
127
+ # situations where) the result is greater than or equal to target.
128
+ # @param [Integer] target
129
+ # @return [GamesDice::Probabilities] new distribution.
130
+ def given_ge target
131
+ target = Integer(target)
132
+ target = min if min > target
133
+ p = p_ge(target)
134
+ raise "There is no valid distribution given a result >= #{target}" unless p > 0.0
135
+ mult = 1.0/p
136
+ new_probs = @probs[target-@offset,@probs.count-1].map { |x| x * mult }
137
+ GamesDice::Probabilities.new( new_probs, target )
138
+ end
139
+
140
+ # Probability distribution derived from this one, where we know (or are only interested in
141
+ # situations where) the result is less than or equal to target.
142
+ # @param [Integer] target
143
+ # @return [GamesDice::Probabilities] new distribution.
144
+ def given_le target
145
+ target = Integer(target)
146
+ target = max if max < target
147
+ p = p_le(target)
148
+ raise "There is no valid distribution given a result <= #{target}" unless p > 0.0
149
+ mult = 1.0/p
150
+ new_probs = @probs[0..target-@offset].map { |x| x * mult }
151
+ GamesDice::Probabilities.new( new_probs, @offset )
152
+ end
153
+
154
+ # Creates new instance of GamesDice::Probabilities.
155
+ # @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
156
+ # and the matching value is probability of getting that result
157
+ # @return [GamesDice::Probabilities]
158
+ def self.from_h prob_hash
159
+ probs, offset = prob_h_to_ao( prob_hash )
160
+ GamesDice::Probabilities.new( probs, offset )
161
+ end
162
+
112
163
  # Distribution for a die with equal chance of rolling 1..N
113
164
  # @param [Integer] sides Number of sides on die
114
165
  # @return [GamesDice::Probabilities]
115
166
  def self.for_fair_die sides
116
167
  sides = Integer(sides)
117
168
  raise ArgumentError, "sides must be at least 1" unless sides > 0
118
- h = {}
119
- p = 1.0/sides
120
- (1..sides).each { |x| h[x] = p }
121
- GamesDice::Probabilities.new( h )
169
+ GamesDice::Probabilities.new( Array.new( sides, 1.0/sides ), 1 )
122
170
  end
123
171
 
124
172
  # Combines two distributions to create a third, that represents the distribution created when adding
@@ -127,15 +175,20 @@ class GamesDice::Probabilities
127
175
  # @param [GamesDice::Probabilities] pd_b Second distribution
128
176
  # @return [GamesDice::Probabilities]
129
177
  def self.add_distributions pd_a, pd_b
130
- h = {}
131
- pd_a.ph.each do |ka,pa|
132
- pd_b.ph.each do |kb,pb|
133
- kc = ka + kb
178
+ combined_min = pd_a.min + pd_b.min
179
+ combined_max = pd_a.max + pd_b.max
180
+ new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
181
+ probs_a, offset_a = pd_a.to_ao
182
+ probs_b, offset_b = pd_b.to_ao
183
+
184
+ probs_a.each_with_index do |pa,i|
185
+ probs_b.each_with_index do |pb,j|
186
+ k = i + j
134
187
  pc = pa * pb
135
- h[kc] = h[kc] ? h[kc] + pc : pc
188
+ new_probs[ k ] += pc
136
189
  end
137
190
  end
138
- GamesDice::Probabilities.new( h )
191
+ GamesDice::Probabilities.new( new_probs, combined_min )
139
192
  end
140
193
 
141
194
  # Combines two distributions with multipliers to create a third, that represents the distribution
@@ -146,15 +199,74 @@ class GamesDice::Probabilities
146
199
  # @param [GamesDice::Probabilities] pd_b Second distribution
147
200
  # @return [GamesDice::Probabilities]
148
201
  def self.add_distributions_mult m_a, pd_a, m_b, pd_b
149
- h = {}
150
- pd_a.ph.each do |ka,pa|
151
- pd_b.ph.each do |kb,pb|
152
- kc = m_a * ka + m_b * kb
202
+ combined_min, combined_max = [
203
+ m_a * pd_a.min + m_b * pd_b.min, m_a * pd_a.max + m_b * pd_b.min,
204
+ m_a * pd_a.min + m_b * pd_b.max, m_a * pd_a.max + m_b * pd_b.max,
205
+ ].minmax
206
+
207
+ new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
208
+ probs_a, offset_a = pd_a.to_ao
209
+ probs_b, offset_b = pd_b.to_ao
210
+
211
+ probs_a.each_with_index do |pa,i|
212
+ probs_b.each_with_index do |pb,j|
213
+ k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
153
214
  pc = pa * pb
154
- h[kc] = h[kc] ? h[kc] + pc : pc
215
+ new_probs[ k ] += pc
216
+ end
217
+ end
218
+ GamesDice::Probabilities.new( new_probs, combined_min )
219
+ end
220
+
221
+
222
+ # Adds a distribution to itself repeatedly, to simulate a number of dice
223
+ # results being summed.
224
+ # @param [GamesDice::Probabilities] pd Distribution to repeat
225
+ # @param [Integer] n Number of repetitions, must be at least 1
226
+ # @return [GamesDice::Probabilities]
227
+ def self.repeat_distribution pd, n
228
+ n = Integer( n )
229
+ raise "Cannot combine probabilities less than once" if n < 1
230
+ revbin = n.to_s(2).reverse.each_char.to_a.map { |c| c == '1' }
231
+ pd_power = pd
232
+ pd_result = nil
233
+ max_power = revbin.count - 1
234
+
235
+ revbin.each_with_index do |use_power, i|
236
+ if use_power
237
+ if pd_result
238
+ pd_result = add_distributions( pd_result, pd_power )
239
+ else
240
+ pd_result = pd_power
241
+ end
155
242
  end
243
+ pd_power = add_distributions( pd_power, pd_power ) unless i == max_power
156
244
  end
157
- GamesDice::Probabilities.new( h )
245
+ pd_result
246
+ end
247
+
248
+ private
249
+
250
+ # Convert hash to array,offset notation
251
+ def self.prob_h_to_ao h
252
+ rmin,rmax = h.keys.minmax
253
+ o = rmin
254
+ a = Array.new( 1 + rmax - rmin, 0.0 )
255
+ h.each { |k,v| a[k-rmin] = v }
256
+ [a,o]
257
+ end
258
+
259
+ # Convert array,offset notation to hash
260
+ def self.prob_ao_to_h a, o
261
+ h = Hash.new
262
+ a.each_with_index { |v,i| h[i+o] = v if v > 0.0 }
263
+ h
264
+ end
265
+
266
+ def calc_expected
267
+ total = 0.0
268
+ @probs.each_with_index { |v,i| total += (i+@offset)*v }
269
+ total
158
270
  end
159
271
 
160
272
  end # class GamesDice::Probabilities
@@ -1,3 +1,3 @@
1
1
  module GamesDice
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
data/spec/bunch_spec.rb CHANGED
@@ -260,12 +260,10 @@ describe GamesDice::Bunch do
260
260
  end
261
261
 
262
262
  it "should have a mean value of roughly 18.986" do
263
- pending "Too slow"
264
263
  bunch.probabilities.expected.should be_within(1e-9).of 18.9859925804
265
264
  end
266
265
 
267
266
  it "should calculate probabilities correctly" do
268
- pending "Too slow"
269
267
  prob_hash = bunch.probabilities.to_h
270
268
  prob_hash[2].should be_within(1e-10).of 0.00001
271
269
  prob_hash[3].should be_within(1e-10).of 0.00005
@@ -2,12 +2,10 @@ require 'games_dice'
2
2
  require 'helpers'
3
3
 
4
4
  describe GamesDice::Probabilities do
5
-
6
5
  describe "class methods" do
7
-
8
6
  describe "#new" do
9
- it "should create a new distribution from a hash" do
10
- p = GamesDice::Probabilities.new( { 1 => 1.0 } )
7
+ it "should create a new distribution from an array and offset" do
8
+ p = GamesDice::Probabilities.new( [1.0], 1 )
11
9
  p.is_a?( GamesDice::Probabilities ).should be_true
12
10
  p.to_h.should be_valid_distribution
13
11
  end
@@ -31,8 +29,8 @@ describe GamesDice::Probabilities do
31
29
 
32
30
  describe "#add_distributions" do
33
31
  it "should combine two distributions to create a third one" do
34
- d4a = GamesDice::Probabilities.new( { 1 => 1.0/4, 2 => 1.0/4, 3 => 1.0/4, 4 => 1.0/4 } )
35
- d4b = GamesDice::Probabilities.new( { 1 => 1.0/10, 2 => 2.0/10, 3 => 3.0/10, 4 => 4.0/10 } )
32
+ d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
33
+ d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
36
34
  p = GamesDice::Probabilities.add_distributions( d4a, d4b )
37
35
  p.to_h.should be_valid_distribution
38
36
  end
@@ -40,18 +38,79 @@ describe GamesDice::Probabilities do
40
38
  it "should calculate a classic 2d6 distribution accurately" do
41
39
  d6 = GamesDice::Probabilities.for_fair_die( 6 )
42
40
  p = GamesDice::Probabilities.add_distributions( d6, d6 )
41
+ h = p.to_h
42
+ h.should be_valid_distribution
43
+ h[2].should be_within(1e-9).of 1.0/36
44
+ h[3].should be_within(1e-9).of 2.0/36
45
+ h[4].should be_within(1e-9).of 3.0/36
46
+ h[5].should be_within(1e-9).of 4.0/36
47
+ h[6].should be_within(1e-9).of 5.0/36
48
+ h[7].should be_within(1e-9).of 6.0/36
49
+ h[8].should be_within(1e-9).of 5.0/36
50
+ h[9].should be_within(1e-9).of 4.0/36
51
+ h[10].should be_within(1e-9).of 3.0/36
52
+ h[11].should be_within(1e-9).of 2.0/36
53
+ h[12].should be_within(1e-9).of 1.0/36
54
+ end
55
+ end
56
+
57
+ describe "#repeat_distribution" do
58
+ it "should output a valid distribution if params are valid" do
59
+ d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
60
+ d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
61
+ p = GamesDice::Probabilities.repeat_distribution( d4a, 7 )
62
+ p.to_h.should be_valid_distribution
63
+ p = GamesDice::Probabilities.repeat_distribution( d4b, 12 )
43
64
  p.to_h.should be_valid_distribution
44
- p.to_h[2].should be_within(1e-9).of 1.0/36
45
- p.to_h[3].should be_within(1e-9).of 2.0/36
46
- p.to_h[4].should be_within(1e-9).of 3.0/36
47
- p.to_h[5].should be_within(1e-9).of 4.0/36
48
- p.to_h[6].should be_within(1e-9).of 5.0/36
49
- p.to_h[7].should be_within(1e-9).of 6.0/36
50
- p.to_h[8].should be_within(1e-9).of 5.0/36
51
- p.to_h[9].should be_within(1e-9).of 4.0/36
52
- p.to_h[10].should be_within(1e-9).of 3.0/36
53
- p.to_h[11].should be_within(1e-9).of 2.0/36
54
- p.to_h[12].should be_within(1e-9).of 1.0/36
65
+ end
66
+
67
+ it "should calculate a classic 3d6 distribution accurately" do
68
+ d6 = GamesDice::Probabilities.for_fair_die( 6 )
69
+ p = GamesDice::Probabilities.repeat_distribution( d6, 3 )
70
+ h = p.to_h
71
+ h.should be_valid_distribution
72
+ h[3].should be_within(1e-9).of 1.0/216
73
+ h[4].should be_within(1e-9).of 3.0/216
74
+ h[5].should be_within(1e-9).of 6.0/216
75
+ h[6].should be_within(1e-9).of 10.0/216
76
+ h[7].should be_within(1e-9).of 15.0/216
77
+ h[8].should be_within(1e-9).of 21.0/216
78
+ h[9].should be_within(1e-9).of 25.0/216
79
+ h[10].should be_within(1e-9).of 27.0/216
80
+ h[11].should be_within(1e-9).of 27.0/216
81
+ h[12].should be_within(1e-9).of 25.0/216
82
+ h[13].should be_within(1e-9).of 21.0/216
83
+ h[14].should be_within(1e-9).of 15.0/216
84
+ h[15].should be_within(1e-9).of 10.0/216
85
+ h[16].should be_within(1e-9).of 6.0/216
86
+ h[17].should be_within(1e-9).of 3.0/216
87
+ h[18].should be_within(1e-9).of 1.0/216
88
+ end
89
+ end # describe "#repeat_distribution"
90
+
91
+ describe "#add_distributions_mult" do
92
+ it "should combine two multiplied distributions to create a third one" do
93
+ d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
94
+ d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
95
+ p = GamesDice::Probabilities.add_distributions_mult( 2, d4a, -1, d4b )
96
+ p.to_h.should be_valid_distribution
97
+ end
98
+
99
+ it "should calculate a distribution for '1d6 - 1d4' accurately" do
100
+ d6 = GamesDice::Probabilities.for_fair_die( 6 )
101
+ d4 = GamesDice::Probabilities.for_fair_die( 4 )
102
+ p = GamesDice::Probabilities.add_distributions_mult( 1, d6, -1, d4 )
103
+ h = p.to_h
104
+ h.should be_valid_distribution
105
+ h[-3].should be_within(1e-9).of 1.0/24
106
+ h[-2].should be_within(1e-9).of 2.0/24
107
+ h[-1].should be_within(1e-9).of 3.0/24
108
+ h[0].should be_within(1e-9).of 4.0/24
109
+ h[1].should be_within(1e-9).of 4.0/24
110
+ h[2].should be_within(1e-9).of 4.0/24
111
+ h[3].should be_within(1e-9).of 3.0/24
112
+ h[4].should be_within(1e-9).of 2.0/24
113
+ h[5].should be_within(1e-9).of 1.0/24
55
114
  end
56
115
  end
57
116
 
@@ -62,7 +121,7 @@ describe GamesDice::Probabilities do
62
121
  let(:p4) { GamesDice::Probabilities.for_fair_die( 4 ) }
63
122
  let(:p6) { GamesDice::Probabilities.for_fair_die( 6 ) }
64
123
  let(:p10) { GamesDice::Probabilities.for_fair_die( 10 ) }
65
- let(:pa) { GamesDice::Probabilities.new( { -1 => 0.4, 0 => 0.2, 1 => 0.4 } ) }
124
+ let(:pa) { GamesDice::Probabilities.new( [ 0.4, 0.2, 0.4 ], -1 ) }
66
125
 
67
126
  describe "#p_eql" do
68
127
  it "should return probability of getting a number inside the range" do
@@ -222,6 +281,26 @@ describe GamesDice::Probabilities do
222
281
  end
223
282
  end
224
283
 
225
- end # describe "instance methods"
284
+ describe "#given_ge" do
285
+ it "should return a new distribution with probabilities calculated assuming value is >= target" do
286
+ pd = p2.given_ge(2)
287
+ pd.to_h.should == { 2 => 1.0 }
288
+ pd = p10.given_ge(4)
289
+ pd.to_h.should be_valid_distribution
290
+ pd.p_eql( 3 ).should == 0.0
291
+ pd.p_eql( 10 ).should be_within(1.0e-9).of 0.1/0.7
292
+ end
293
+ end
226
294
 
295
+ describe "#given_le" do
296
+ it "should return a new distribution with probabilities calculated assuming value is <= target" do
297
+ pd = p2.given_le(2)
298
+ pd.to_h.should == { 1 => 0.5, 2 => 0.5 }
299
+ pd = p10.given_le(4)
300
+ pd.to_h.should be_valid_distribution
301
+ pd.p_eql( 3 ).should be_within(1.0e-9).of 0.1/0.4
302
+ pd.p_eql( 10 ).should == 0.0
303
+ end
304
+ end
305
+ end # describe "instance methods"
227
306
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: games_dice
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-12 00:00:00.000000000 Z
12
+ date: 2013-06-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -144,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
144
  version: '0'
145
145
  segments:
146
146
  - 0
147
- hash: 1797445249921181101
147
+ hash: 1993011777003280822
148
148
  required_rubygems_version: !ruby/object:Gem::Requirement
149
149
  none: false
150
150
  requirements:
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
153
  version: '0'
154
154
  segments:
155
155
  - 0
156
- hash: 1797445249921181101
156
+ hash: 1993011777003280822
157
157
  requirements: []
158
158
  rubyforge_project:
159
159
  rubygems_version: 1.8.24