games_dice 0.3.12 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,244 +0,0 @@
1
- require 'games_dice/prob_helpers'
2
-
3
- # This class models probability distributions for dice systems.
4
- #
5
- # An object of this class represents a single distribution, which might be the result of a complex
6
- # combination of dice.
7
- #
8
- # @example Distribution for a six-sided die
9
- # probs = GamesDice::Probabilities.for_fair_die( 6 )
10
- # probs.min # => 1
11
- # probs.max # => 6
12
- # probs.expected # => 3.5
13
- # probs.p_ge( 4 ) # => 0.5
14
- #
15
- # @example Adding two distributions
16
- # pd6 = GamesDice::Probabilities.for_fair_die( 6 )
17
- # probs = GamesDice::Probabilities.add_distributions( pd6, pd6 )
18
- # probs.min # => 2
19
- # probs.max # => 12
20
- # probs.expected # => 7.0
21
- # probs.p_ge( 10 ) # => 0.16666666666666669
22
- #
23
- class GamesDice::Probabilities
24
- include GamesDice::ProbabilityValidations
25
- include GamesDice::ProbabilityCalcSums
26
- extend GamesDice::ProbabilityCalcAddDistributions
27
-
28
- # Creates new instance of GamesDice::Probabilities.
29
- # @param [Array<Float>] probs Each entry in the array is the probability of getting a result
30
- # @param [Integer] offset The result associated with index of 0 in the array
31
- # @return [GamesDice::Probabilities]
32
- def initialize( probs = [1.0], offset = 0 )
33
- # This should *probably* be validated in future, but that would impact performance
34
- @probs = check_probs_array probs.clone
35
- @offset = Integer(offset)
36
- end
37
-
38
- # Iterates through value, probability pairs
39
- # @yieldparam [Integer] result A result that may be possible in the dice scheme
40
- # @yieldparam [Float] probability Probability of result, in range 0.0..1.0
41
- # @return [GamesDice::Probabilities] this object
42
- def each
43
- @probs.each_with_index { |p,i| yield( i+@offset, p ) if p > 0.0 }
44
- return self
45
- end
46
-
47
- # A hash representation of the distribution. Each key is an integer result,
48
- # and the matching value is probability of getting that result. A new hash is generated on each
49
- # call to this method.
50
- # @return [Hash]
51
- def to_h
52
- GamesDice::Probabilities.prob_ao_to_h( @probs, @offset )
53
- end
54
-
55
- # @!attribute [r] min
56
- # Minimum result in the distribution
57
- # @return [Integer]
58
- def min
59
- @offset
60
- end
61
-
62
- # @!attribute [r] max
63
- # Maximum result in the distribution
64
- # @return [Integer]
65
- def max
66
- @offset + @probs.count() - 1
67
- end
68
-
69
- # @!attribute [r] expected
70
- # Expected value of distribution.
71
- # @return [Float]
72
- def expected
73
- @expected ||= calc_expected
74
- end
75
-
76
- # Probability of result equalling specific target
77
- # @param [Integer] target
78
- # @return [Float] in range (0.0..1.0)
79
- def p_eql target
80
- i = Integer(target) - @offset
81
- return 0.0 if i < 0 || i >= @probs.count
82
- @probs[ i ]
83
- end
84
-
85
- # Probability of result being greater than specific target
86
- # @param [Integer] target
87
- # @return [Float] in range (0.0..1.0)
88
- def p_gt target
89
- p_ge( Integer(target) + 1 )
90
- end
91
-
92
- # Probability of result being equal to or greater than specific target
93
- # @param [Integer] target
94
- # @return [Float] in range (0.0..1.0)
95
- def p_ge target
96
- target = Integer(target)
97
- return @prob_ge[target] if @prob_ge && @prob_ge[target]
98
- @prob_ge = {} unless @prob_ge
99
-
100
- return 1.0 if target <= min
101
- return 0.0 if target > max
102
- @prob_ge[target] = @probs[target-@offset,@probs.count-1].inject(0.0) {|so_far,p| so_far + p }
103
- end
104
-
105
- # Probability of result being equal to or less than specific target
106
- # @param [Integer] target
107
- # @return [Float] in range (0.0..1.0)
108
- def p_le target
109
- target = Integer(target)
110
- return @prob_le[target] if @prob_le && @prob_le[target]
111
- @prob_le = {} unless @prob_le
112
-
113
- return 1.0 if target >= max
114
- return 0.0 if target < min
115
- @prob_le[target] = @probs[0,1+target-@offset].inject(0.0) {|so_far,p| so_far + p }
116
- end
117
-
118
- # Probability of result being less than specific target
119
- # @param [Integer] target
120
- # @return [Float] in range (0.0..1.0)
121
- def p_lt target
122
- p_le( Integer(target) - 1 )
123
- end
124
-
125
- # Probability distribution derived from this one, where we know (or are only interested in
126
- # situations where) the result is greater than or equal to target.
127
- # @param [Integer] target
128
- # @return [GamesDice::Probabilities] new distribution.
129
- def given_ge target
130
- target = Integer(target)
131
- target = min if min > target
132
- p = p_ge(target)
133
- raise "There is no valid distribution given a result >= #{target}" unless p > 0.0
134
- mult = 1.0/p
135
- new_probs = @probs[target-@offset,@probs.count-1].map { |x| x * mult }
136
- GamesDice::Probabilities.new( new_probs, target )
137
- end
138
-
139
- # Probability distribution derived from this one, where we know (or are only interested in
140
- # situations where) the result is less than or equal to target.
141
- # @param [Integer] target
142
- # @return [GamesDice::Probabilities] new distribution.
143
- def given_le target
144
- target = Integer(target)
145
- target = max if max < target
146
- p = p_le(target)
147
- raise "There is no valid distribution given a result <= #{target}" unless p > 0.0
148
- mult = 1.0/p
149
- new_probs = @probs[0..target-@offset].map { |x| x * mult }
150
- GamesDice::Probabilities.new( new_probs, @offset )
151
- end
152
-
153
- # Creates new instance of GamesDice::Probabilities.
154
- # @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
155
- # and the matching value is probability of getting that result
156
- # @return [GamesDice::Probabilities]
157
- def self.from_h prob_hash
158
- raise TypeError, "from_h expected a Hash" unless prob_hash.is_a? Hash
159
- probs, offset = prob_h_to_ao( prob_hash )
160
- GamesDice::Probabilities.new( probs, offset )
161
- end
162
-
163
- # Distribution for a die with equal chance of rolling 1..N
164
- # @param [Integer] sides Number of sides on die
165
- # @return [GamesDice::Probabilities]
166
- def self.for_fair_die sides
167
- sides = Integer(sides)
168
- raise ArgumentError, "sides must be at least 1" unless sides > 0
169
- raise ArgumentError, "sides can be at most 100000" if sides > 100000
170
- GamesDice::Probabilities.new( Array.new( sides, 1.0/sides ), 1 )
171
- end
172
-
173
- # Combines two distributions to create a third, that represents the distribution created when adding
174
- # results together.
175
- # @param [GamesDice::Probabilities] pd_a First distribution
176
- # @param [GamesDice::Probabilities] pd_b Second distribution
177
- # @return [GamesDice::Probabilities]
178
- def self.add_distributions pd_a, pd_b
179
- check_is_gdp( pd_a, pd_b )
180
- combined_min = pd_a.min + pd_b.min
181
- combined_max = pd_a.max + pd_b.max
182
-
183
- add_distributions_internal( combined_min, combined_max, 1, pd_a, 1, pd_b )
184
- end
185
-
186
- # Combines two distributions with multipliers to create a third, that represents the distribution
187
- # created when adding weighted results together.
188
- # @param [Integer] m_a Weighting for first distribution
189
- # @param [GamesDice::Probabilities] pd_a First distribution
190
- # @param [Integer] m_b Weighting for second distribution
191
- # @param [GamesDice::Probabilities] pd_b Second distribution
192
- # @return [GamesDice::Probabilities]
193
- def self.add_distributions_mult m_a, pd_a, m_b, pd_b
194
- check_is_gdp( pd_a, pd_b )
195
- m_a = Integer(m_a)
196
- m_b = Integer(m_b)
197
-
198
- combined_min, combined_max = calc_combined_extremes( m_a, pd_a, m_b, pd_b ).minmax
199
-
200
- add_distributions_internal( combined_min, combined_max, m_a, pd_a, m_b, pd_b )
201
- end
202
-
203
- # Returns a symbol for the language name that this class is implemented in. The C version of the
204
- # code is noticeably faster when dealing with larger numbers of possible results.
205
- # @return [Symbol] Either :c or :ruby
206
- def self.implemented_in
207
- :ruby
208
- end
209
-
210
- # Adds a distribution to itself repeatedly, to simulate a number of dice
211
- # results being summed.
212
- # @param [Integer] n Number of repetitions, must be at least 1
213
- # @return [GamesDice::Probabilities] new distribution
214
- def repeat_sum n
215
- n = Integer( n )
216
- raise "Cannot combine probabilities less than once" if n < 1
217
- raise "Probability distribution too large" if ( n * @probs.count ) > 1000000
218
- repeat_sum_internal( n )
219
- end
220
-
221
- # Calculates distribution generated by summing best k results of n iterations
222
- # of the distribution.
223
- # @param [Integer] n Number of repetitions, must be at least 1
224
- # @param [Integer] k Number of best results to keep and sum
225
- # @return [GamesDice::Probabilities] new distribution
226
- def repeat_n_sum_k n, k, kmode = :keep_best
227
- n = Integer( n )
228
- k = Integer( k )
229
- raise "Cannot combine probabilities less than once" if n < 1
230
- # Technically this is a limitation of C code, but Ruby version is most likely slow and inaccurate beyond 170
231
- raise "Too many dice to calculate numbers of arrangements" if n > 170
232
- check_keep_mode( kmode )
233
- repeat_n_sum_k_internal( n, k, kmode )
234
- end
235
-
236
- private
237
-
238
- def calc_expected
239
- total = 0.0
240
- @probs.each_with_index { |v,i| total += (i+@offset)*v }
241
- total
242
- end
243
-
244
- end # class GamesDice::Probabilities