games_dice 0.3.12 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|