games_dice 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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