Class: GamesDice::Probabilities

Inherits:
Object
  • Object
show all
Defined in:
lib/games_dice/probabilities.rb,
lib/games_dice/marshal.rb

Overview

This class models probability distributions for dice systems.

An object of this class represents a single distribution, which might be the result of a complex combination of dice.

Examples:

Distribution for a six-sided die

probs = GamesDice::Probabilities.for_fair_die( 6 )
probs.min # => 1
probs.max # => 6
probs.expected # => 3.5
probs.p_ge( 4 ) # => 0.5

Adding two distributions

pd6 = GamesDice::Probabilities.for_fair_die( 6 )
probs = GamesDice::Probabilities.add_distributions( pd6, pd6 )
probs.min # => 2
probs.max # => 12
probs.expected # => 7.0
probs.p_ge( 10 ) # => 0.16666666666666669

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(probs = [1.0], offset = 0) ⇒ GamesDice::Probabilities

Creates new instance of GamesDice::Probabilities.

Parameters:

  • probs (Array<Float>) (defaults to: [1.0])

    Each entry in the array is the probability of getting a result

  • offset (Integer) (defaults to: 0)

    The result associated with index of 0 in the array



27
28
29
30
31
# File 'lib/games_dice/probabilities.rb', line 27

def initialize( probs = [1.0], offset = 0 )
  # This should *probably* be validated in future, but that would impact performance
  @probs = check_probs_array probs.clone
  @offset = Integer(offset)
end

Instance Attribute Details

#expectedFloat (readonly)

Expected value of distribution.

Returns:

  • (Float)


73
74
75
# File 'lib/games_dice/probabilities.rb', line 73

def expected
  @expected ||= calc_expected
end

#maxInteger (readonly)

Maximum result in the distribution

Returns:

  • (Integer)


66
67
68
# File 'lib/games_dice/probabilities.rb', line 66

def max
  @offset + @probs.count() - 1
end

#minInteger (readonly)

Minimum result in the distribution

Returns:

  • (Integer)


59
60
61
# File 'lib/games_dice/probabilities.rb', line 59

def min
  @offset
end

Class Method Details

.add_distributions(pd_a, pd_b) ⇒ GamesDice::Probabilities

Combines two distributions to create a third, that represents the distribution created when adding results together.

Parameters:

Returns:



179
180
181
182
183
184
185
186
187
188
# File 'lib/games_dice/probabilities.rb', line 179

def self.add_distributions pd_a, pd_b
  unless pd_a.is_a?( GamesDice::Probabilities ) && pd_b.is_a?( GamesDice::Probabilities )
    raise TypeError, "parameter to add_distributions is not a GamesDice::Probabilities"
  end

  combined_min = pd_a.min + pd_b.min
  combined_max = pd_a.max + pd_b.max

  add_distributions_internal combined_min, combined_max, 1, pd_a, 1, pd_b
end

.add_distributions_mult(m_a, pd_a, m_b, pd_b) ⇒ GamesDice::Probabilities

Combines two distributions with multipliers to create a third, that represents the distribution created when adding weighted results together.

Parameters:

Returns:



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/games_dice/probabilities.rb', line 197

def self.add_distributions_mult m_a, pd_a, m_b, pd_b
  unless pd_a.is_a?( GamesDice::Probabilities ) && pd_b.is_a?( GamesDice::Probabilities )
    raise TypeError, "parameter to add_distributions_mult is not a GamesDice::Probabilities"
  end

  m_a = Integer(m_a)
  m_b = Integer(m_b)

  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

  add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
end

.for_fair_die(sides) ⇒ GamesDice::Probabilities

Distribution for a die with equal chance of rolling 1..N

Parameters:

  • sides (Integer)

    Number of sides on die

Returns:

Raises:

  • (ArgumentError)


167
168
169
170
171
172
# File 'lib/games_dice/probabilities.rb', line 167

def self.for_fair_die sides
  sides = Integer(sides)
  raise ArgumentError, "sides must be at least 1" unless sides > 0
  raise ArgumentError, "sides can be at most 100000" if sides > 100000
  GamesDice::Probabilities.new( Array.new( sides, 1.0/sides ), 1 )
end

.from_h(prob_hash) ⇒ GamesDice::Probabilities

Creates new instance of GamesDice::Probabilities.

Parameters:

  • prob_hash (Hash)

    A hash representation of the distribution, each key is an integer result, and the matching value is probability of getting that result

Returns:

Raises:

  • (TypeError)


158
159
160
161
162
# File 'lib/games_dice/probabilities.rb', line 158

def self.from_h prob_hash
  raise TypeError, "from_h expected a Hash" unless prob_hash.is_a? Hash
  probs, offset = prob_h_to_ao( prob_hash )
  GamesDice::Probabilities.new( probs, offset )
end

.implemented_inSymbol

Returns a symbol for the language name that this class is implemented in. The C version of the code is noticeably faster when dealing with larger numbers of possible results.

Returns:

  • (Symbol)

    Either :c or :ruby



216
217
218
# File 'lib/games_dice/probabilities.rb', line 216

def self.implemented_in
  :ruby
end

Instance Method Details

#each {|result, probability| ... } ⇒ GamesDice::Probabilities

Iterates through value, probability pairs

Yield Parameters:

  • result (Integer)

    A result that may be possible in the dice scheme

  • probability (Float)

    Probability of result, in range 0.0..1.0

Returns:



43
44
45
46
# File 'lib/games_dice/probabilities.rb', line 43

def each
  @probs.each_with_index { |p,i| yield( i+@offset, p ) if p > 0.0 }
  return self
end

#given_ge(target) ⇒ GamesDice::Probabilities

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.

Parameters:

  • target (Integer)

Returns:



130
131
132
133
134
135
136
137
138
# File 'lib/games_dice/probabilities.rb', line 130

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

#given_le(target) ⇒ GamesDice::Probabilities

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.

Parameters:

  • target (Integer)

Returns:



144
145
146
147
148
149
150
151
152
# File 'lib/games_dice/probabilities.rb', line 144

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

#p_eql(target) ⇒ Float

Probability of result equalling specific target

Parameters:

  • target (Integer)

Returns:

  • (Float)

    in range (0.0..1.0)



80
81
82
83
84
# File 'lib/games_dice/probabilities.rb', line 80

def p_eql target
  i = Integer(target) - @offset
  return 0.0 if i < 0 || i >= @probs.count
  @probs[ i ]
end

#p_ge(target) ⇒ Float

Probability of result being equal to or greater than specific target

Parameters:

  • target (Integer)

Returns:

  • (Float)

    in range (0.0..1.0)



96
97
98
99
100
101
102
103
104
# File 'lib/games_dice/probabilities.rb', line 96

def p_ge target
  target = Integer(target)
  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] = @probs[target-@offset,@probs.count-1].inject(0.0) {|so_far,p| so_far + p }
end

#p_gt(target) ⇒ Float

Probability of result being greater than specific target

Parameters:

  • target (Integer)

Returns:

  • (Float)

    in range (0.0..1.0)



89
90
91
# File 'lib/games_dice/probabilities.rb', line 89

def p_gt target
  p_ge( Integer(target) + 1 )
end

#p_le(target) ⇒ Float

Probability of result being equal to or less than specific target

Parameters:

  • target (Integer)

Returns:

  • (Float)

    in range (0.0..1.0)



109
110
111
112
113
114
115
116
117
# File 'lib/games_dice/probabilities.rb', line 109

def p_le target
  target = Integer(target)
  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] = @probs[0,1+target-@offset].inject(0.0) {|so_far,p| so_far + p }
end

#p_lt(target) ⇒ Float

Probability of result being less than specific target

Parameters:

  • target (Integer)

Returns:

  • (Float)

    in range (0.0..1.0)



122
123
124
# File 'lib/games_dice/probabilities.rb', line 122

def p_lt target
  p_le( Integer(target) - 1 )
end

#repeat_n_sum_k(n, k, kmode = :keep_best) ⇒ GamesDice::Probabilities

Calculates distribution generated by summing best k results of n iterations of the distribution.

Parameters:

  • n (Integer)

    Number of repetitions, must be at least 1

  • k (Integer)

    Number of best results to keep and sum

Returns:



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/games_dice/probabilities.rb', line 252

def repeat_n_sum_k n, k, kmode = :keep_best
  n = Integer( n )
  k = Integer( k )
  raise "Cannot combine probabilities less than once" if n < 1
  # Technically this is a limitation of C code, but Ruby version is most likely slow and inaccurate beyond 170
  raise "Too many dice to calculate numbers of arrangements" if n > 170
  check_keep_mode( kmode )

  if k >= n
    return repeat_sum( n )
  end
  new_probs = Array.new( @probs.count * k, 0.0 )
  new_offset = @offset * k
  d = n - k

  each do | q, p_maybe |
    next unless p_maybe > 0.0

    # keep_distributions is array of Probabilities, indexed by number of keepers > q, which is in 0...k
    keep_distributions = calc_keep_distributions( k, q, kmode )
    p_table = calc_p_table( q, p_maybe, kmode )

    (0...k).each do |n|
      keepers = [2] * n + [1] * (k-n)
      p_so_far = keepers.inject(1.0) { |p,idx| p * p_table[idx] }
      next unless p_so_far > 0.0
      (0..d).each do |dn|
        discards = [1] * (d-dn) + [0] * dn
        sequence = keepers + discards
        p_sequence = discards.inject( p_so_far ) { |p,idx| p * p_table[idx] }
        next unless p_sequence > 0.0
        p_sequence *= GamesDice::Combinations.count_variations( sequence )
        kd = keep_distributions[n]
        kd.each { |r,p_r| new_probs[r-new_offset] += p_r * p_sequence }
      end
    end
  end
  GamesDice::Probabilities.new( new_probs, new_offset )
end

#repeat_sum(n) ⇒ GamesDice::Probabilities

Adds a distribution to itself repeatedly, to simulate a number of dice results being summed.

Parameters:

  • n (Integer)

    Number of repetitions, must be at least 1

Returns:



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/games_dice/probabilities.rb', line 224

def repeat_sum n
  n = Integer( n )
  raise "Cannot combine probabilities less than once" if n < 1
  raise "Probability distribution too large" if ( n * @probs.count ) > 1000000
  pd_power = self
  pd_result = nil

  use_power = 1
  loop do
    if ( use_power & n ) > 0
      if pd_result
        pd_result = GamesDice::Probabilities.add_distributions( pd_result, pd_power )
      else
        pd_result = pd_power
      end
    end
    use_power = use_power << 1
    break if use_power > n
    pd_power = GamesDice::Probabilities.add_distributions( pd_power, pd_power )
  end
  pd_result
end

#to_hHash

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.

Returns:

  • (Hash)


52
53
54
# File 'lib/games_dice/probabilities.rb', line 52

def to_h
  GamesDice::Probabilities.prob_ao_to_h( @probs, @offset )
end