lib/games_dice/probabilities.rb in games_dice-0.2.3 vs lib/games_dice/probabilities.rb in games_dice-0.2.4
- old
+ new
@@ -19,56 +19,70 @@
# probs.p_ge( 10 ) # => 0.16666666666666669
#
class GamesDice::Probabilities
# Creates new instance of GamesDice::Probabilities.
- # @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
- # and the matching value is probability of getting that result
+ # @param [Array<Float>] probs Each entry in the array is the probability of getting a result
+ # @param [Integer] offset The result associated with index of 0 in the array
# @return [GamesDice::Probabilities]
- def initialize( prob_hash = { 0 => 1.0 } )
+ def initialize( probs = [1.0], offset = 0 )
# This should *probably* be validated in future, but that would impact performance
- @ph = prob_hash
+ @probs = probs
+ @offset = offset
end
# @!visibility private
- # the Hash representation of probabilities.
- attr_reader :ph
+ # the Array, Offset representation of probabilities.
+ def to_ao
+ [ @probs, @offset ]
+ end
+ # Iterates through value, probability pairs
+ # @yieldparam [Integer] result A result that may be possible in the dice scheme
+ # @yieldparam [Float] probability Probability of result, in range 0.0..1.0
+ # @return [GamesDice::Probabilities] this object
+ def each
+ @probs.each_with_index { |p,i| yield( i+@offset, p ) }
+ return self
+ end
+
# A hash representation of the distribution. Each key is an integer result,
# and the matching value is probability of getting that result. A new hash is generated on each
# call to this method.
# @return [Hash]
def to_h
- @ph.clone
+ GamesDice::Probabilities.prob_ao_to_h( @probs, @offset )
end
# @!attribute [r] min
# Minimum result in the distribution
# @return [Integer]
def min
- (@minmax ||= @ph.keys.minmax )[0]
+ @offset
end
# @!attribute [r] max
# Maximum result in the distribution
# @return [Integer]
def max
- (@minmax ||= @ph.keys.minmax )[1]
+ @offset + @probs.count() - 1
end
# @!attribute [r] expected
# Expected value of distribution.
# @return [Float]
def expected
- @expected ||= @ph.inject(0.0) { |accumulate,p| accumulate + p[0] * p[1] }
+ @expected ||= calc_expected
end
# Probability of result equalling specific target
# @param [Integer] target
# @return [Float] in range (0.0..1.0)
def p_eql target
- @ph[ Integer(target) ] || 0.0
+ i = Integer(target) - @offset
+ return 0.0 if i < 0 || i >= @probs.count
+ @probs[ i ]
end
# Probability of result being greater than specific target
# @param [Integer] target
# @return [Float] in range (0.0..1.0)
@@ -84,11 +98,11 @@
return @prob_ge[target] if @prob_ge && @prob_ge[target]
@prob_ge = {} unless @prob_ge
return 1.0 if target <= min
return 0.0 if target > max
- @prob_ge[target] = @ph.select {|k,v| target <= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
+ @prob_ge[target] = @probs[target-@offset,@probs.count-1].inject(0.0) {|so_far,p| so_far + p }
end
# Probability of result being equal to or less than specific target
# @param [Integer] target
# @return [Float] in range (0.0..1.0)
@@ -97,64 +111,162 @@
return @prob_le[target] if @prob_le && @prob_le[target]
@prob_le = {} unless @prob_le
return 1.0 if target >= max
return 0.0 if target < min
- @prob_le[target] = @ph.select {|k,v| target >= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
+ @prob_le[target] = @probs[0,1+target-@offset].inject(0.0) {|so_far,p| so_far + p }
end
# Probability of result being less than specific target
# @param [Integer] target
# @return [Float] in range (0.0..1.0)
def p_lt target
p_le( Integer(target) - 1 )
end
+ # Probability distribution derived from this one, where we know (or are only interested in
+ # situations where) the result is greater than or equal to target.
+ # @param [Integer] target
+ # @return [GamesDice::Probabilities] new distribution.
+ def given_ge target
+ target = Integer(target)
+ target = min if min > target
+ p = p_ge(target)
+ raise "There is no valid distribution given a result >= #{target}" unless p > 0.0
+ mult = 1.0/p
+ new_probs = @probs[target-@offset,@probs.count-1].map { |x| x * mult }
+ GamesDice::Probabilities.new( new_probs, target )
+ end
+
+ # Probability distribution derived from this one, where we know (or are only interested in
+ # situations where) the result is less than or equal to target.
+ # @param [Integer] target
+ # @return [GamesDice::Probabilities] new distribution.
+ def given_le target
+ target = Integer(target)
+ target = max if max < target
+ p = p_le(target)
+ raise "There is no valid distribution given a result <= #{target}" unless p > 0.0
+ mult = 1.0/p
+ new_probs = @probs[0..target-@offset].map { |x| x * mult }
+ GamesDice::Probabilities.new( new_probs, @offset )
+ end
+
+ # Creates new instance of GamesDice::Probabilities.
+ # @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
+ # and the matching value is probability of getting that result
+ # @return [GamesDice::Probabilities]
+ def self.from_h prob_hash
+ probs, offset = prob_h_to_ao( prob_hash )
+ GamesDice::Probabilities.new( probs, offset )
+ end
+
# Distribution for a die with equal chance of rolling 1..N
# @param [Integer] sides Number of sides on die
# @return [GamesDice::Probabilities]
def self.for_fair_die sides
sides = Integer(sides)
raise ArgumentError, "sides must be at least 1" unless sides > 0
- h = {}
- p = 1.0/sides
- (1..sides).each { |x| h[x] = p }
- GamesDice::Probabilities.new( h )
+ GamesDice::Probabilities.new( Array.new( sides, 1.0/sides ), 1 )
end
# Combines two distributions to create a third, that represents the distribution created when adding
# results together.
# @param [GamesDice::Probabilities] pd_a First distribution
# @param [GamesDice::Probabilities] pd_b Second distribution
# @return [GamesDice::Probabilities]
def self.add_distributions pd_a, pd_b
- h = {}
- pd_a.ph.each do |ka,pa|
- pd_b.ph.each do |kb,pb|
- kc = ka + kb
+ combined_min = pd_a.min + pd_b.min
+ combined_max = pd_a.max + pd_b.max
+ new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
+ probs_a, offset_a = pd_a.to_ao
+ probs_b, offset_b = pd_b.to_ao
+
+ probs_a.each_with_index do |pa,i|
+ probs_b.each_with_index do |pb,j|
+ k = i + j
pc = pa * pb
- h[kc] = h[kc] ? h[kc] + pc : pc
+ new_probs[ k ] += pc
end
end
- GamesDice::Probabilities.new( h )
+ GamesDice::Probabilities.new( new_probs, combined_min )
end
# Combines two distributions with multipliers to create a third, that represents the distribution
# created when adding weighted results together.
# @param [Integer] m_a Weighting for first distribution
# @param [GamesDice::Probabilities] pd_a First distribution
# @param [Integer] m_b Weighting for second distribution
# @param [GamesDice::Probabilities] pd_b Second distribution
# @return [GamesDice::Probabilities]
def self.add_distributions_mult m_a, pd_a, m_b, pd_b
- h = {}
- pd_a.ph.each do |ka,pa|
- pd_b.ph.each do |kb,pb|
- kc = m_a * ka + m_b * kb
+ combined_min, combined_max = [
+ m_a * pd_a.min + m_b * pd_b.min, m_a * pd_a.max + m_b * pd_b.min,
+ m_a * pd_a.min + m_b * pd_b.max, m_a * pd_a.max + m_b * pd_b.max,
+ ].minmax
+
+ new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
+ probs_a, offset_a = pd_a.to_ao
+ probs_b, offset_b = pd_b.to_ao
+
+ probs_a.each_with_index do |pa,i|
+ probs_b.each_with_index do |pb,j|
+ k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
pc = pa * pb
- h[kc] = h[kc] ? h[kc] + pc : pc
+ new_probs[ k ] += pc
end
end
- GamesDice::Probabilities.new( h )
+ GamesDice::Probabilities.new( new_probs, combined_min )
+ end
+
+
+ # Adds a distribution to itself repeatedly, to simulate a number of dice
+ # results being summed.
+ # @param [GamesDice::Probabilities] pd Distribution to repeat
+ # @param [Integer] n Number of repetitions, must be at least 1
+ # @return [GamesDice::Probabilities]
+ def self.repeat_distribution pd, n
+ n = Integer( n )
+ raise "Cannot combine probabilities less than once" if n < 1
+ revbin = n.to_s(2).reverse.each_char.to_a.map { |c| c == '1' }
+ pd_power = pd
+ pd_result = nil
+ max_power = revbin.count - 1
+
+ revbin.each_with_index do |use_power, i|
+ if use_power
+ if pd_result
+ pd_result = add_distributions( pd_result, pd_power )
+ else
+ pd_result = pd_power
+ end
+ end
+ pd_power = add_distributions( pd_power, pd_power ) unless i == max_power
+ end
+ pd_result
+ end
+
+ private
+
+ # Convert hash to array,offset notation
+ def self.prob_h_to_ao h
+ rmin,rmax = h.keys.minmax
+ o = rmin
+ a = Array.new( 1 + rmax - rmin, 0.0 )
+ h.each { |k,v| a[k-rmin] = v }
+ [a,o]
+ end
+
+ # Convert array,offset notation to hash
+ def self.prob_ao_to_h a, o
+ h = Hash.new
+ a.each_with_index { |v,i| h[i+o] = v if v > 0.0 }
+ h
+ end
+
+ def calc_expected
+ total = 0.0
+ @probs.each_with_index { |v,i| total += (i+@offset)*v }
+ total
end
end # class GamesDice::Probabilities