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.
- checksums.yaml +5 -5
- data/.rubocop.yml +15 -0
- data/.travis.yml +7 -10
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -0
- data/Rakefile +14 -11
- data/ext/games_dice/extconf.rb +4 -22
- data/ext/games_dice/probabilities.c +1 -1
- data/games_dice.gemspec +26 -32
- data/lib/games_dice/bunch.rb +241 -247
- data/lib/games_dice/complex_die.rb +287 -270
- data/lib/games_dice/complex_die_helpers.rb +68 -60
- data/lib/games_dice/constants.rb +10 -10
- data/lib/games_dice/dice.rb +146 -143
- data/lib/games_dice/die.rb +101 -97
- data/lib/games_dice/die_result.rb +193 -189
- data/lib/games_dice/map_rule.rb +72 -70
- data/lib/games_dice/marshal.rb +18 -13
- data/lib/games_dice/parser.rb +219 -218
- data/lib/games_dice/reroll_rule.rb +76 -77
- data/lib/games_dice/version.rb +3 -1
- data/lib/games_dice.rb +19 -19
- data/spec/bunch_spec.rb +399 -420
- data/spec/complex_die_spec.rb +314 -305
- data/spec/dice_spec.rb +33 -34
- data/spec/die_result_spec.rb +162 -169
- data/spec/die_spec.rb +81 -81
- data/spec/helpers.rb +23 -21
- data/spec/map_rule_spec.rb +40 -44
- data/spec/parser_spec.rb +106 -82
- data/spec/probability_spec.rb +530 -526
- data/spec/readme_spec.rb +404 -390
- data/spec/reroll_rule_spec.rb +40 -44
- metadata +39 -28
- data/lib/games_dice/prob_helpers.rb +0 -259
- data/lib/games_dice/probabilities.rb +0 -244
@@ -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
|