games_dice 0.3.9 → 0.3.10

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.
@@ -1,59 +1,64 @@
1
1
  # GamesDice Changelog
2
2
 
3
- ## 0.3.9
3
+ ## 0.3.10 ( 29 July 2013 )
4
+
5
+ * Non-functional changes to improve code quality metrics on CodeClimate
6
+ * Altered specs to improve accuracy of coverage metrics on Coveralls
7
+
8
+ ## 0.3.9 ( 23 July 2013 )
4
9
 
5
10
  * New methods for inspecting and iterating over potential values in GamesDice::Die
6
11
  * Code metric integration and badges for github
7
12
  * Non-functional changes to improve code quality metrics on CodeClimate
8
13
 
9
- ## 0.3.7
14
+ ## 0.3.7 ( 17 July 2013 )
10
15
 
11
16
  * Compatibility between pure Ruby and native extension code when handling bad method params
12
17
  * Added this changelog to documentation
13
18
 
14
- ## 0.3.6
19
+ ## 0.3.6 ( 15 July 2013 )
15
20
 
16
21
  * Extension building skipped, with fallback to pure Ruby, for JRuby compatibility
17
22
 
18
- ## 0.3.5
23
+ ## 0.3.5 ( 14 July 2013 )
19
24
 
20
25
  * Adjust C code to avoid warnings about C90 compatibility (warnings seen on Travis)
21
26
  * Note MIT license in gemspec
22
27
  * Add class method GamesDice::Probabilities.implemented_in
23
28
 
24
- ## 0.3.3
29
+ ## 0.3.3 ( 11 July 2013 )
25
30
 
26
31
  * Standardised code for Ruby 1.8.7 compatibility in GamesDice::Probabilities
27
32
  * Bug fix for probability calculations where distributions are added with mulipliers e.g. '2d6 - 1d8'
28
33
 
29
- ## 0.3.2
34
+ ## 0.3.2 ( 10 July 2013 )
30
35
 
31
36
  * Bug fix for Ruby 1.8.7 compatibility in GamesDice::Probabilities
32
37
 
33
- ## 0.3.1
38
+ ## 0.3.1 ( 10 July 2013 )
34
39
 
35
40
  * Bug fix for Ruby 1.8.7 compatibility in GamesDice::Probabilities
36
41
 
37
- ## 0.3.0
42
+ ## 0.3.0 ( 10 July 2013 )
38
43
 
39
44
  * Implemented GamesDice::Probabilities as native extension
40
45
 
41
- ## 0.2.4
46
+ ## 0.2.4 ( 18 June 2013 )
42
47
 
43
48
  * Minor speed improvements to GamesDice::Probabilities
44
49
 
45
- ## 0.2.3
50
+ ## 0.2.3 ( 12 June 2013 )
46
51
 
47
52
  * More YARD documentation
48
53
 
49
- ## 0.2.2
54
+ ## 0.2.2 ( 10 June 2013 )
50
55
 
51
56
  * Extended YARD documentation
52
57
 
53
- ## 0.2.1
58
+ ## 0.2.1 ( 5 June 2013 )
54
59
 
55
60
  * Started basic YARD documentation
56
61
 
57
- ## 0.2.0
62
+ ## 0.2.0 ( 30 May 2013 )
58
63
 
59
64
  * First version with a complete feature set
@@ -19,16 +19,16 @@ Gem::Specification.new do |gem|
19
19
  gem.add_development_dependency "yard", ">= 0.8.6"
20
20
  gem.add_development_dependency "coveralls", ">= 0.6.7"
21
21
  gem.add_development_dependency "json", ">= 1.7.7"
22
- gem.add_development_dependency "rake-compiler"
22
+ gem.add_development_dependency "rake-compiler", ">= 0.8.3"
23
23
 
24
24
  # Red Carpet renders README.md, and is optional even when developing the gem.
25
25
  # However, it has a C extension, and v3.0.0 is does not compile for 1.8.7. This only affects the gem build process, so
26
26
  # is only really used in environments like Travis, and is safe to wrap like this in the gemspec.
27
27
  if RUBY_DESCRIPTION !~ /jruby/
28
- if RUBY_VERSION < "1.9.0"
29
- gem.add_development_dependency "redcarpet", ">=2.3.0", "<3.0.0"
30
- else
28
+ if RUBY_VERSION >= "1.9.0"
31
29
  gem.add_development_dependency "redcarpet", ">=2.3.0"
30
+ else
31
+ gem.add_development_dependency "redcarpet", ">=2.3.0", "<3.0.0"
32
32
  end
33
33
  end
34
34
 
@@ -1,3 +1,5 @@
1
+ require "games_dice/complex_die_helpers"
2
+
1
3
  # This class models a die that is built up from a simpler unit by adding rules to re-roll
2
4
  # and interpret the value shown.
3
5
  #
@@ -20,6 +22,7 @@
20
22
  #
21
23
 
22
24
  class GamesDice::ComplexDie
25
+ include GamesDice::ComplexDieHelpers
23
26
 
24
27
  # @!visibility private
25
28
  # arbitrary limit to speed up probability calculations. It should
@@ -104,18 +107,14 @@ class GamesDice::ComplexDie
104
107
  reroll_probs = recursive_probabilities
105
108
  prob_hash = {}
106
109
  reroll_probs.each do |v,p|
107
- m, n = calc_maps(v)
108
- prob_hash[m] ||= 0.0
109
- prob_hash[m] += p
110
+ add_mapped_to_prob_hash( prob_hash, v, p )
110
111
  end
111
112
  elsif @rerolls
112
113
  prob_hash = recursive_probabilities
113
114
  elsif @maps
114
115
  prob_hash = {}
115
116
  @basic_die.probabilities.each do |v,p|
116
- m, n = calc_maps(v)
117
- prob_hash[m] ||= 0.0
118
- prob_hash[m] += p
117
+ add_mapped_to_prob_hash( prob_hash, v, p )
119
118
  end
120
119
  else
121
120
  @probabilities = @basic_die.probabilities
@@ -136,29 +135,42 @@ class GamesDice::ComplexDie
136
135
 
137
136
  private
138
137
 
138
+ def add_mapped_to_prob_hash prob_hash, v, p
139
+ m, n = calc_maps(v)
140
+ prob_hash[m] ||= 0.0
141
+ prob_hash[m] += p
142
+ end
143
+
139
144
  def roll_apply_rerolls
140
145
  return unless @rerolls
141
146
  subtracting = false
142
147
  rerolls_remaining = @rerolls.map { |rule| rule.limit }
143
148
 
144
149
  loop do
145
- # Find which rule, if any, is being triggered
146
- rule_idx = @rerolls.zip(rerolls_remaining).find_index do |rule,remaining|
147
- next if rule.type == :reroll_subtract && @result.rolls.length > 1
148
- remaining > 0 && rule.applies?( @basic_die.result )
149
- end
150
+ rule_idx = find_matching_reroll_rule( @basic_die.result, @result.rolls.length ,rerolls_remaining )
150
151
  break unless rule_idx
151
152
 
152
153
  rule = @rerolls[ rule_idx ]
153
154
  rerolls_remaining[ rule_idx ] -= 1
154
155
  subtracting = true if rule.type == :reroll_subtract
156
+ roll_apply_reroll_rule rule, subtracting
157
+ end
158
+ end
155
159
 
156
- # Apply the rule (note reversal for additions, after a subtract)
157
- if subtracting && rule.type == :reroll_add
158
- @result.add_roll( @basic_die.roll, :reroll_subtract )
159
- else
160
- @result.add_roll( @basic_die.roll, rule.type )
161
- end
160
+ def roll_apply_reroll_rule rule, is_subtracting
161
+ # Apply the rule (note reversal for additions, after a subtract)
162
+ if is_subtracting && rule.type == :reroll_add
163
+ @result.add_roll( @basic_die.roll, :reroll_subtract )
164
+ else
165
+ @result.add_roll( @basic_die.roll, rule.type )
166
+ end
167
+ end
168
+
169
+ # Find which rule, if any, is being triggered
170
+ def find_matching_reroll_rule check_value, num_rolls, rerolls_remaining
171
+ @rerolls.zip(rerolls_remaining).find_index do |rule,remaining|
172
+ next if rule.type == :reroll_subtract && num_rolls > 1
173
+ remaining > 0 && rule.applies?( check_value )
162
174
  end
163
175
  end
164
176
 
@@ -214,6 +226,8 @@ class GamesDice::ComplexDie
214
226
  end
215
227
 
216
228
  # This isn't 100% accurate, but does cover most "normal" scenarios, and we're only falling back to it when we have to
229
+ # The inaccuracy is that min_result..max_result may contain 'holes' which have extreme map values that cannot actually
230
+ # occur. In practice it is likely a non-issue unless someone went out of their way to invent a dice scheme that broke it.
217
231
  def logical_minmax
218
232
  return [@basic_die.min,@basic_die.max] unless @rerolls || @maps
219
233
  return minmax_mappings( @basic_die.all_values ) unless @rerolls
@@ -225,79 +239,32 @@ class GamesDice::ComplexDie
225
239
  def logical_rerolls_minmax
226
240
  min_result = @basic_die.min
227
241
  max_result = @basic_die.max
228
- min_subtract, max_add = find_add_subtract_extremes
242
+ min_subtract = find_minimum_possible_subtract
243
+ max_add = find_maximum_possible_adds
229
244
  if min_subtract
230
245
  min_result = [ min_subtract - max_add, min_subtract - max_result ].min
231
246
  end
232
247
  [ min_result, max_add + max_result ]
233
248
  end
234
249
 
235
- def find_add_subtract_extremes
250
+ def find_minimum_possible_subtract
236
251
  min_subtract = nil
237
- total_add = 0
238
- @rerolls.select {|r| [:reroll_add, :reroll_subtract].member?( r.type ) }.each do |rule|
239
- min_reroll,max_reroll = @basic_die.all_values.select { |v| rule.applies?( v ) }.minmax
252
+ @rerolls.select { |r| r.type == :reroll_subtract }.each do |rule|
253
+ min_reroll = @basic_die.all_values.select { |v| rule.applies?( v ) }.min
240
254
  next unless min_reroll
241
- if rule.type == :reroll_subtract
242
- min_subtract = min_reroll if min_subtract.nil?
243
- min_subtract = min_reroll if min_subtract > min_reroll
244
- else
245
- total_add += max_reroll * rule.limit
246
- end
247
- end
248
- [ min_subtract, total_add ]
249
- end
250
-
251
- def recursive_probabilities probabilities={},prior_probability=1.0,depth=0,prior_result=nil,rerolls_left=nil,roll_reason=:basic,subtracting=false
252
- each_probability = prior_probability / @basic_die.sides
253
- depth += 1
254
- if depth >= 20 || each_probability < 1.0e-12
255
- @probabilities_complete = false
256
- stop_recursing = true
257
- end
258
-
259
- @basic_die.each_value do |v|
260
- # calculate value, recurse if there is a reroll
261
- result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason )
262
-
263
- # Find which rule, if any, is being triggered
264
- rule_idx = @rerolls.zip(rerolls_remaining).find_index do |rule,remaining|
265
- next if rule.type == :reroll_subtract && result_so_far.rolls.length > 1
266
- remaining > 0 && rule.applies?( v )
267
- end
268
-
269
- if rule_idx && ! stop_recursing
270
- rule = @rerolls[ rule_idx ]
271
- rerolls_remaining[ rule_idx ] -= 1
272
- is_subtracting = true if subtracting || rule.type == :reroll_subtract
273
-
274
- # Apply the rule (note reversal for additions, after a subtract)
275
- if subtracting && rule.type == :reroll_add
276
- recursive_probabilities probabilities,each_probability,depth,result_so_far,rerolls_remaining,:reroll_subtract,is_subtracting
277
- else
278
- recursive_probabilities probabilities,each_probability,depth,result_so_far,rerolls_remaining,rule.type,is_subtracting
279
- end
280
- # just accumulate value on a regular roll
281
- else
282
- t = result_so_far.total
283
- probabilities[ t ] ||= 0.0
284
- probabilities[ t ] += each_probability
285
- end
286
-
255
+ min_subtract = [min_reroll,min_subtract].compact.min
287
256
  end
288
- probabilities
257
+ min_subtract
289
258
  end
290
259
 
291
- def calc_result_so_far prior_result, rerolls_left, v, roll_reason
292
- if prior_result
293
- result_so_far = prior_result.clone
294
- result_so_far.add_roll(v,roll_reason)
295
- rerolls_remaining = rerolls_left.clone
296
- else
297
- result_so_far = GamesDice::DieResult.new(v,roll_reason)
298
- rerolls_remaining = @rerolls.map { |rule| rule.limit }
260
+ def find_maximum_possible_adds
261
+ total_add = 0
262
+ @rerolls.select { |r| r.type == :reroll_add }.each do |rule|
263
+ max_reroll = @basic_die.all_values.select { |v| rule.applies?( v ) }.max
264
+ next unless max_reroll
265
+ total_add += max_reroll * rule.limit
299
266
  end
300
- [result_so_far, rerolls_remaining]
267
+ total_add
301
268
  end
302
269
 
303
270
  end # class ComplexDie
@@ -0,0 +1,60 @@
1
+ # @!visibility private
2
+ module GamesDice::ComplexDieHelpers
3
+
4
+ private
5
+
6
+ def recursive_probabilities probabilities={},prior_probability=1.0,depth=0,prior_result=nil,rerolls_left=nil,roll_reason=:basic,subtracting=false
7
+ each_probability = prior_probability / @basic_die.sides
8
+ depth += 1
9
+ if depth >= 20 || each_probability < 1.0e-16
10
+ @probabilities_complete = false
11
+ stop_recursing = true
12
+ end
13
+
14
+ @basic_die.each_value do |v|
15
+ recurse_probs_for_value( v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing )
16
+ end
17
+ probabilities
18
+ end
19
+
20
+ def recurse_probs_for_value v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing
21
+ # calculate value, recurse if there is a reroll
22
+ result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason )
23
+
24
+ # Find which rule, if any, is being triggered
25
+ rule_idx = find_matching_reroll_rule( v, result_so_far.rolls.length, rerolls_remaining )
26
+
27
+ if rule_idx && ! stop_recursing
28
+ recurse_probs_with_rule( probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting )
29
+ else
30
+ t = result_so_far.total
31
+ probabilities[ t ] ||= 0.0
32
+ probabilities[ t ] += each_probability
33
+ end
34
+ end
35
+
36
+ def recurse_probs_with_rule probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting
37
+ rule = @rerolls[ rule_idx ]
38
+ rerolls_remaining[ rule_idx ] -= 1
39
+ is_subtracting = true if subtracting || rule.type == :reroll_subtract
40
+
41
+ # Apply the rule (note reversal for additions, after a subtract)
42
+ if subtracting && rule.type == :reroll_add
43
+ recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, :reroll_subtract, is_subtracting
44
+ else
45
+ recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule.type, is_subtracting
46
+ end
47
+ end
48
+
49
+ def calc_result_so_far prior_result, rerolls_left, v, roll_reason
50
+ if prior_result
51
+ result_so_far = prior_result.clone
52
+ result_so_far.add_roll(v,roll_reason)
53
+ rerolls_remaining = rerolls_left.clone
54
+ else
55
+ result_so_far = GamesDice::DieResult.new(v,roll_reason)
56
+ rerolls_remaining = @rerolls.map { |rule| rule.limit }
57
+ end
58
+ [result_so_far, rerolls_remaining]
59
+ end
60
+ end
@@ -0,0 +1,259 @@
1
+ # @!visibility private
2
+ module GamesDice::ProbabilityValidations
3
+
4
+ # @!visibility private
5
+ # the Array, Offset representation of probabilities.
6
+ def to_ao
7
+ [ @probs, @offset ]
8
+ end
9
+
10
+ def self.included(klass)
11
+ klass.extend ClassMethods
12
+ end
13
+
14
+ private
15
+
16
+ def check_probs_array probs_array
17
+ raise TypeError unless probs_array.is_a?( Array )
18
+ probs_array.map!{ |n| Float(n) }
19
+ total = probs_array.inject(0.0) do |t,x|
20
+ if x < 0.0 || x > 1.0
21
+ raise ArgumentError, "Found probability value #{x} which is not in range 0.0..1.0"
22
+ end
23
+ t+x
24
+ end
25
+ if (total-1.0).abs > 1e-6
26
+ raise ArgumentError, "Total probabilities too far from 1.0 for a valid distribution"
27
+ end
28
+ probs_array
29
+ end
30
+
31
+ def check_keep_mode kmode
32
+ raise "Keep mode #{kmode.inspect} not recognised" unless [:keep_best,:keep_worst].member?( kmode )
33
+ end
34
+
35
+ module ClassMethods
36
+ # Convert hash to array,offset notation
37
+ def prob_h_to_ao h
38
+ rmin,rmax = h.keys.minmax
39
+ o = rmin
40
+ s = 1 + rmax - rmin
41
+ raise ArgumentError, "Range of possible results too large" if s > 1000000
42
+ a = Array.new( s, 0.0 )
43
+ h.each { |k,v| a[k-rmin] = Float(v) }
44
+ [a,o]
45
+ end
46
+
47
+ # Convert array,offset notation to hash
48
+ def prob_ao_to_h a, o
49
+ h = Hash.new
50
+ a.each_with_index { |v,i| h[i+o] = v if v > 0.0 }
51
+ h
52
+ end
53
+
54
+ private
55
+
56
+ def check_is_gdp *probs
57
+ probs.each do |prob|
58
+ unless prob.is_a?( GamesDice::Probabilities )
59
+ raise TypeError, "parameter is not a GamesDice::Probabilities"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # @!visibility private
67
+ # This module is a set of related private methods for GamesDice::Probabilities that
68
+ # calculate how two distributions can be combined.
69
+ module GamesDice::ProbabilityCalcAddDistributions
70
+ private
71
+
72
+ def calc_combined_extremes m_a, pd_a, m_b, pd_b
73
+ [ [ :min, :min ], [ :min, :max ], [ :max, :min ], [ :max, :max ] ].map do |pda_meth, pdb_meth|
74
+ m_a * pd_a.send(pda_meth) + m_b * pd_b.send(pdb_meth)
75
+ end
76
+ end
77
+
78
+ def add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
79
+ new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
80
+ probs_a, offset_a = pd_a.to_ao
81
+ probs_b, offset_b = pd_b.to_ao
82
+
83
+ probs_a.each_with_index do |pa,i|
84
+ probs_b.each_with_index do |pb,j|
85
+ k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
86
+ pc = pa * pb
87
+ new_probs[ k ] += pc
88
+ end
89
+ end
90
+ GamesDice::Probabilities.new( new_probs, combined_min )
91
+ end
92
+ end
93
+
94
+
95
+ # @!visibility private
96
+ # This module is a set of related private methods for GamesDice::Probabilities that
97
+ # calculate how a distribution can be combined with itself.
98
+ module GamesDice::ProbabilityCalcSums
99
+
100
+ private
101
+
102
+ def repeat_sum_internal( n )
103
+ pd_power = self
104
+ pd_result = nil
105
+
106
+ use_power = 1
107
+ loop do
108
+ if ( use_power & n ) > 0
109
+ if pd_result
110
+ pd_result = GamesDice::Probabilities.add_distributions( pd_result, pd_power )
111
+ else
112
+ pd_result = pd_power
113
+ end
114
+ end
115
+ use_power = use_power << 1
116
+ break if use_power > n
117
+ pd_power = GamesDice::Probabilities.add_distributions( pd_power, pd_power )
118
+ end
119
+ pd_result
120
+ end
121
+
122
+ def repeat_n_sum_k_internal( n, k, kmode )
123
+ if k >= n
124
+ return repeat_sum_internal( n )
125
+ end
126
+ new_probs = Array.new( @probs.count * k, 0.0 )
127
+ new_offset = @offset * k
128
+ d = n - k
129
+
130
+ each do | q, p_maybe |
131
+ repeat_n_sum_k_each_q( q, p_maybe, n, k, kmode, d, new_probs, new_offset )
132
+ end
133
+
134
+ GamesDice::Probabilities.new( new_probs, new_offset )
135
+ end
136
+
137
+ def repeat_n_sum_k_each_q q, p_maybe, n, k, kmode, d, new_probs, new_offset
138
+ # keep_distributions is array of Probabilities, indexed by number of keepers > q, which is in 0...k
139
+ keep_distributions = calc_keep_distributions( k, q, kmode )
140
+ p_table = calc_p_table( q, p_maybe, kmode )
141
+ (0...k).each do |kn|
142
+ repeat_n_sum_k_each_q_kn( k, kn, d, new_probs, new_offset, keep_distributions, p_table )
143
+ end
144
+ end
145
+
146
+ def repeat_n_sum_k_each_q_kn k, kn, d, new_probs, new_offset, keep_distributions, p_table
147
+ keepers = [2] * kn + [1] * (k-kn)
148
+ p_so_far = keepers.inject(1.0) { |p,idx| p * p_table[idx] }
149
+ return unless p_so_far > 0.0
150
+ (0..d).each do |dn|
151
+ repeat_n_sum_k_each_q_kn_dn( keepers, kn, d, dn, p_so_far, new_probs, new_offset, keep_distributions, p_table )
152
+ end
153
+ end
154
+
155
+ def repeat_n_sum_k_each_q_kn_dn keepers, kn, d, dn, p_so_far, new_probs, new_offset, keep_distributions, p_table
156
+ discards = [1] * (d-dn) + [0] * dn
157
+ sequence = keepers + discards
158
+ p_sequence = discards.inject( p_so_far ) { |p,idx| p * p_table[idx] }
159
+ return unless p_sequence > 0.0
160
+ p_sequence *= GamesDice::Combinations.count_variations( sequence )
161
+ kd = keep_distributions[kn]
162
+ kd.each { |r,p_r| new_probs[r-new_offset] += p_r * p_sequence }
163
+ end
164
+
165
+ def calc_keep_distributions k, q, kmode
166
+ kd_probabilities = calc_keep_definite_distributions q, kmode
167
+
168
+ keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ]
169
+ if kd_probabilities && k > 1
170
+ (1...k).each do |n|
171
+ extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) )
172
+ n_probs = kd_probabilities.repeat_sum( n )
173
+ keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs )
174
+ end
175
+ end
176
+
177
+ keep_distributions
178
+ end
179
+
180
+ def calc_keep_definite_distributions q, kmode
181
+ kd_probabilities = nil
182
+ case kmode
183
+ when :keep_best
184
+ p_definites = p_gt(q)
185
+ kd_probabilities = given_ge( q + 1 ) if p_definites > 0.0
186
+ when :keep_worst
187
+ p_definites = p_lt(q)
188
+ kd_probabilities = given_le( q - 1 ) if p_definites > 0.0
189
+ end
190
+ kd_probabilities
191
+ end
192
+
193
+ def calc_p_table q, p_maybe, kmode
194
+ case kmode
195
+ when :keep_best
196
+ p_kept = p_gt(q)
197
+ p_rejected = p_lt(q)
198
+ when :keep_worst
199
+ p_kept = p_lt(q)
200
+ p_rejected = p_gt(q)
201
+ end
202
+ [ p_rejected, p_maybe, p_kept ]
203
+ end
204
+
205
+ end
206
+
207
+
208
+ # @!visibility private
209
+ # Helper module with optimised Ruby for counting variations of arrays, such as those returned by
210
+ # Array#repeated_combination
211
+ #
212
+ # @example How many ways can [3,3,6] be arranged?
213
+ # GamesDice::Combinations.count_variations( [3,3,6] )
214
+ # => 3
215
+ #
216
+ # @example When prob( a ) and result( a ) are same for any arrangement of Array a
217
+ # items = [1,2,3,4,5,6]
218
+ # items.repeated_combination(5).each do |a|
219
+ # this_result = result( a )
220
+ # this_prob = prob( a ) * GamesDice::Combinations.count_variations( a )
221
+ # # Do something useful with this knowledge! E.g. save it to probability array.
222
+ # end
223
+ #
224
+ module GamesDice::Combinations
225
+ @@variations_cache = {}
226
+ @@factorial_cache = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
227
+
228
+ # Counts variations of an array. A unique variation is an arrangement of the array elements which
229
+ # is detectably different (using ==) from any other. So [1,1,1] has only 1 unique arrangement,
230
+ # but [1,2,3] has 6 possibilities.
231
+ # @param [Array] array List of things that can be arranged
232
+ # @return [Integer] Number of unique arrangements
233
+ def self.count_variations array
234
+ all_count = array.count
235
+ group_sizes = group_counts( array )
236
+ cache_key = all_count.to_s + ":" + group_sizes.join(',')
237
+ @@variations_cache[cache_key] ||= variations_of( all_count, group_sizes )
238
+ end
239
+
240
+ private
241
+
242
+ def self.variations_of all_count, groups
243
+ all_arrangements = factorial( all_count )
244
+ # The reject is an optimisation to avoid calculating and multplying by factorial(1) (==1)
245
+ identical_arrangements = groups.reject {|x| x==1 }.inject(1) { |prod,g| prod * factorial(g) }
246
+ all_arrangements/identical_arrangements
247
+ end
248
+
249
+ # Returns counts of unique items in array e.g. [8,8,8,7,6,6] returns [1,2,3]
250
+ # Sort is for caching
251
+ def self.group_counts array
252
+ array.group_by {|x| x}.values.map {|v| v.count}.sort
253
+ end
254
+
255
+ def self.factorial n
256
+ # Can start range from 2 because we have pre-cached the result for n=1
257
+ @@factorial_cache[n] ||= (2..n).inject(:*)
258
+ end
259
+ end
@@ -1,3 +1,5 @@
1
+ require 'games_dice/prob_helpers'
2
+
1
3
  # This class models probability distributions for dice systems.
2
4
  #
3
5
  # An object of this class represents a single distribution, which might be the result of a complex
@@ -19,6 +21,9 @@
19
21
  # probs.p_ge( 10 ) # => 0.16666666666666669
20
22
  #
21
23
  class GamesDice::Probabilities
24
+ include GamesDice::ProbabilityValidations
25
+ include GamesDice::ProbabilityCalcSums
26
+ extend GamesDice::ProbabilityCalcAddDistributions
22
27
 
23
28
  # Creates new instance of GamesDice::Probabilities.
24
29
  # @param [Array<Float>] probs Each entry in the array is the probability of getting a result
@@ -30,12 +35,6 @@ class GamesDice::Probabilities
30
35
  @offset = Integer(offset)
31
36
  end
32
37
 
33
- # @!visibility private
34
- # the Array, Offset representation of probabilities.
35
- def to_ao
36
- [ @probs, @offset ]
37
- end
38
-
39
38
  # Iterates through value, probability pairs
40
39
  # @yieldparam [Integer] result A result that may be possible in the dice scheme
41
40
  # @yieldparam [Float] probability Probability of result, in range 0.0..1.0
@@ -177,14 +176,11 @@ class GamesDice::Probabilities
177
176
  # @param [GamesDice::Probabilities] pd_b Second distribution
178
177
  # @return [GamesDice::Probabilities]
179
178
  def self.add_distributions pd_a, pd_b
180
- unless pd_a.is_a?( GamesDice::Probabilities ) && pd_b.is_a?( GamesDice::Probabilities )
181
- raise TypeError, "parameter to add_distributions is not a GamesDice::Probabilities"
182
- end
183
-
179
+ check_is_gdp( pd_a, pd_b )
184
180
  combined_min = pd_a.min + pd_b.min
185
181
  combined_max = pd_a.max + pd_b.max
186
182
 
187
- add_distributions_internal combined_min, combined_max, 1, pd_a, 1, pd_b
183
+ add_distributions_internal( combined_min, combined_max, 1, pd_a, 1, pd_b )
188
184
  end
189
185
 
190
186
  # Combines two distributions with multipliers to create a third, that represents the distribution
@@ -195,19 +191,13 @@ class GamesDice::Probabilities
195
191
  # @param [GamesDice::Probabilities] pd_b Second distribution
196
192
  # @return [GamesDice::Probabilities]
197
193
  def self.add_distributions_mult m_a, pd_a, m_b, pd_b
198
- unless pd_a.is_a?( GamesDice::Probabilities ) && pd_b.is_a?( GamesDice::Probabilities )
199
- raise TypeError, "parameter to add_distributions_mult is not a GamesDice::Probabilities"
200
- end
201
-
194
+ check_is_gdp( pd_a, pd_b )
202
195
  m_a = Integer(m_a)
203
196
  m_b = Integer(m_b)
204
197
 
205
- combined_min, combined_max = [
206
- m_a * pd_a.min + m_b * pd_b.min, m_a * pd_a.max + m_b * pd_b.min,
207
- m_a * pd_a.min + m_b * pd_b.max, m_a * pd_a.max + m_b * pd_b.max,
208
- ].minmax
198
+ combined_min, combined_max = calc_combined_extremes( m_a, pd_a, m_b, pd_b ).minmax
209
199
 
210
- add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
200
+ add_distributions_internal( combined_min, combined_max, m_a, pd_a, m_b, pd_b )
211
201
  end
212
202
 
213
203
  # Returns a symbol for the language name that this class is implemented in. The C version of the
@@ -225,23 +215,7 @@ class GamesDice::Probabilities
225
215
  n = Integer( n )
226
216
  raise "Cannot combine probabilities less than once" if n < 1
227
217
  raise "Probability distribution too large" if ( n * @probs.count ) > 1000000
228
- pd_power = self
229
- pd_result = nil
230
-
231
- use_power = 1
232
- loop do
233
- if ( use_power & n ) > 0
234
- if pd_result
235
- pd_result = GamesDice::Probabilities.add_distributions( pd_result, pd_power )
236
- else
237
- pd_result = pd_power
238
- end
239
- end
240
- use_power = use_power << 1
241
- break if use_power > n
242
- pd_power = GamesDice::Probabilities.add_distributions( pd_power, pd_power )
243
- end
244
- pd_result
218
+ repeat_sum_internal( n )
245
219
  end
246
220
 
247
221
  # Calculates distribution generated by summing best k results of n iterations
@@ -256,190 +230,15 @@ class GamesDice::Probabilities
256
230
  # Technically this is a limitation of C code, but Ruby version is most likely slow and inaccurate beyond 170
257
231
  raise "Too many dice to calculate numbers of arrangements" if n > 170
258
232
  check_keep_mode( kmode )
259
-
260
- if k >= n
261
- return repeat_sum( n )
262
- end
263
- new_probs = Array.new( @probs.count * k, 0.0 )
264
- new_offset = @offset * k
265
- d = n - k
266
-
267
- each do | q, p_maybe |
268
- next unless p_maybe > 0.0
269
-
270
- # keep_distributions is array of Probabilities, indexed by number of keepers > q, which is in 0...k
271
- keep_distributions = calc_keep_distributions( k, q, kmode )
272
- p_table = calc_p_table( q, p_maybe, kmode )
273
-
274
- (0...k).each do |n|
275
- keepers = [2] * n + [1] * (k-n)
276
- p_so_far = keepers.inject(1.0) { |p,idx| p * p_table[idx] }
277
- next unless p_so_far > 0.0
278
- (0..d).each do |dn|
279
- discards = [1] * (d-dn) + [0] * dn
280
- sequence = keepers + discards
281
- p_sequence = discards.inject( p_so_far ) { |p,idx| p * p_table[idx] }
282
- next unless p_sequence > 0.0
283
- p_sequence *= GamesDice::Combinations.count_variations( sequence )
284
- kd = keep_distributions[n]
285
- kd.each { |r,p_r| new_probs[r-new_offset] += p_r * p_sequence }
286
- end
287
- end
288
- end
289
- GamesDice::Probabilities.new( new_probs, new_offset )
233
+ repeat_n_sum_k_internal( n, k, kmode )
290
234
  end
291
235
 
292
236
  private
293
237
 
294
- def self.add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
295
- new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
296
- probs_a, offset_a = pd_a.to_ao
297
- probs_b, offset_b = pd_b.to_ao
298
-
299
- probs_a.each_with_index do |pa,i|
300
- probs_b.each_with_index do |pb,j|
301
- k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
302
- pc = pa * pb
303
- new_probs[ k ] += pc
304
- end
305
- end
306
- GamesDice::Probabilities.new( new_probs, combined_min )
307
- end
308
-
309
- def check_probs_array probs_array
310
- raise TypeError unless probs_array.is_a?( Array )
311
- probs_array.map!{ |n| Float(n) }
312
- total = probs_array.inject(0.0) do |t,x|
313
- if x < 0.0 || x > 1.0
314
- raise ArgumentError, "Found probability value #{x} which is not in range 0.0..1.0"
315
- end
316
- t+x
317
- end
318
- if (total-1.0).abs > 1e-6
319
- raise ArgumentError, "Total probabilities too far from 1.0 for a valid distribution"
320
- end
321
- probs_array
322
- end
323
-
324
- def calc_keep_distributions k, q, kmode
325
- kd_probabilities = calc_keep_definite_distributions q, kmode
326
-
327
- keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ]
328
- if kd_probabilities && k > 1
329
- (1...k).each do |n|
330
- extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) )
331
- n_probs = kd_probabilities.repeat_sum( n )
332
- keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs )
333
- end
334
- end
335
-
336
- keep_distributions
337
- end
338
-
339
- def calc_keep_definite_distributions q, kmode
340
- kd_probabilities = nil
341
- case kmode
342
- when :keep_best
343
- p_definites = p_gt(q)
344
- kd_probabilities = given_ge( q + 1 ) if p_definites > 0.0
345
- when :keep_worst
346
- p_definites = p_lt(q)
347
- kd_probabilities = given_le( q - 1 ) if p_definites > 0.0
348
- end
349
- kd_probabilities
350
- end
351
-
352
- def calc_p_table q, p_maybe, kmode
353
- case kmode
354
- when :keep_best
355
- p_kept = p_gt(q)
356
- p_rejected = p_lt(q)
357
- when :keep_worst
358
- p_kept = p_lt(q)
359
- p_rejected = p_gt(q)
360
- end
361
- [ p_rejected, p_maybe, p_kept ]
362
- end
363
-
364
- # Convert hash to array,offset notation
365
- def self.prob_h_to_ao h
366
- rmin,rmax = h.keys.minmax
367
- o = rmin
368
- s = 1 + rmax - rmin
369
- raise ArgumentError, "Range of possible results too large" if s > 1000000
370
- a = Array.new( s, 0.0 )
371
- h.each { |k,v| a[k-rmin] = Float(v) }
372
- [a,o]
373
- end
374
-
375
- # Convert array,offset notation to hash
376
- def self.prob_ao_to_h a, o
377
- h = Hash.new
378
- a.each_with_index { |v,i| h[i+o] = v if v > 0.0 }
379
- h
380
- end
381
-
382
238
  def calc_expected
383
239
  total = 0.0
384
240
  @probs.each_with_index { |v,i| total += (i+@offset)*v }
385
241
  total
386
242
  end
387
243
 
388
- def check_keep_mode kmode
389
- raise "Keep mode #{kmode.inspect} not recognised" unless [:keep_best,:keep_worst].member?( kmode )
390
- end
391
-
392
244
  end # class GamesDice::Probabilities
393
-
394
- # @!visibility private
395
- # Helper module with optimised Ruby for counting variations of arrays, such as those returned by
396
- # Array#repeated_combination
397
- #
398
- # @example How many ways can [3,3,6] be arranged?
399
- # GamesDice::Combinations.count_variations( [3,3,6] )
400
- # => 3
401
- #
402
- # @example When prob( a ) and result( a ) are same for any arrangement of Array a
403
- # items = [1,2,3,4,5,6]
404
- # items.repeated_combination(5).each do |a|
405
- # this_result = result( a )
406
- # this_prob = prob( a ) * GamesDice::Combinations.count_variations( a )
407
- # # Do something useful with this knowledge! E.g. save it to probability array.
408
- # end
409
- #
410
- module GamesDice::Combinations
411
- @@variations_cache = {}
412
- @@factorial_cache = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
413
-
414
- # Counts variations of an array. A unique variation is an arrangement of the array elements which
415
- # is detectably different (using ==) from any other. So [1,1,1] has only 1 unique arrangement,
416
- # but [1,2,3] has 6 possibilities.
417
- # @param [Array] array List of things that can be arranged
418
- # @return [Integer] Number of unique arrangements
419
- def self.count_variations array
420
- all_count = array.count
421
- group_sizes = group_counts( array )
422
- cache_key = all_count.to_s + ":" + group_sizes.join(',')
423
- @@variations_cache[cache_key] ||= variations_of( all_count, group_sizes )
424
- end
425
-
426
- private
427
-
428
- def self.variations_of all_count, groups
429
- all_arrangements = factorial( all_count )
430
- # The reject is an optimisation to avoid calculating and multplying by factorial(1) (==1)
431
- identical_arrangements = groups.reject {|x| x==1 }.inject(1) { |prod,g| prod * factorial(g) }
432
- all_arrangements/identical_arrangements
433
- end
434
-
435
- # Returns counts of unique items in array e.g. [8,8,8,7,6,6] returns [1,2,3]
436
- # Sort is for caching
437
- def self.group_counts array
438
- array.group_by {|x| x}.values.map {|v| v.count}.sort
439
- end
440
-
441
- def self.factorial n
442
- # Can start range from 2 because we have pre-cached the result for n=1
443
- @@factorial_cache[n] ||= (2..n).inject(:*)
444
- end
445
- end
@@ -1,3 +1,3 @@
1
1
  module GamesDice
2
- VERSION = "0.3.9"
2
+ VERSION = "0.3.10"
3
3
  end
@@ -1,4 +1,3 @@
1
- require 'games_dice'
2
1
  require 'helpers'
3
2
 
4
3
  describe GamesDice::Bunch do
@@ -1,4 +1,3 @@
1
- require 'games_dice'
2
1
  require 'helpers'
3
2
 
4
3
  describe GamesDice::ComplexDie do
@@ -1,4 +1,4 @@
1
- require 'games_dice'
1
+ require 'helpers'
2
2
 
3
3
  describe GamesDice::Dice do
4
4
 
@@ -1,4 +1,4 @@
1
- require 'games_dice'
1
+ require 'helpers'
2
2
 
3
3
  describe GamesDice::DieResult do
4
4
 
@@ -1,4 +1,3 @@
1
- require 'games_dice'
2
1
  require 'helpers'
3
2
 
4
3
  describe GamesDice::Die do
@@ -1,9 +1,11 @@
1
1
  # games_dice/spec/helpers.rb
2
2
  require 'pathname'
3
3
  require 'coveralls'
4
-
5
4
  Coveralls.wear!
6
5
 
6
+ require 'games_dice'
7
+
8
+
7
9
  def fixture name
8
10
  (Pathname.new(__FILE__).dirname + "fixtures" + name).to_s
9
11
  end
@@ -1,4 +1,4 @@
1
- require 'games_dice'
1
+ require 'helpers'
2
2
 
3
3
  describe GamesDice::MapRule do
4
4
 
@@ -1,4 +1,4 @@
1
- require 'games_dice'
1
+ require 'helpers'
2
2
 
3
3
  describe GamesDice::Parser do
4
4
 
@@ -1,4 +1,3 @@
1
- require 'games_dice'
2
1
  require 'helpers'
3
2
 
4
3
  describe GamesDice::Probabilities do
@@ -1,4 +1,3 @@
1
- require 'games_dice'
2
1
  require 'helpers'
3
2
  # This spec demonstrates that documentation from the README.md works as intended
4
3
 
@@ -1,4 +1,4 @@
1
- require 'games_dice'
1
+ require 'helpers'
2
2
 
3
3
  describe GamesDice::RerollRule do
4
4
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: games_dice
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.3.10
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-23 00:00:00.000000000 Z
12
+ date: 2013-07-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -98,7 +98,7 @@ dependencies:
98
98
  requirements:
99
99
  - - ! '>='
100
100
  - !ruby/object:Gem::Version
101
- version: '0'
101
+ version: 0.8.3
102
102
  type: :development
103
103
  prerelease: false
104
104
  version_requirements: !ruby/object:Gem::Requirement
@@ -106,7 +106,7 @@ dependencies:
106
106
  requirements:
107
107
  - - ! '>='
108
108
  - !ruby/object:Gem::Version
109
- version: '0'
109
+ version: 0.8.3
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: redcarpet
112
112
  requirement: !ruby/object:Gem::Requirement
@@ -164,6 +164,7 @@ files:
164
164
  - lib/games_dice.rb
165
165
  - lib/games_dice/bunch.rb
166
166
  - lib/games_dice/complex_die.rb
167
+ - lib/games_dice/complex_die_helpers.rb
167
168
  - lib/games_dice/constants.rb
168
169
  - lib/games_dice/dice.rb
169
170
  - lib/games_dice/die.rb
@@ -171,6 +172,7 @@ files:
171
172
  - lib/games_dice/map_rule.rb
172
173
  - lib/games_dice/marshal.rb
173
174
  - lib/games_dice/parser.rb
175
+ - lib/games_dice/prob_helpers.rb
174
176
  - lib/games_dice/probabilities.rb
175
177
  - lib/games_dice/reroll_rule.rb
176
178
  - lib/games_dice/version.rb
@@ -201,7 +203,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
201
203
  version: '0'
202
204
  segments:
203
205
  - 0
204
- hash: -3597732090146629653
206
+ hash: -2123252871276968084
205
207
  required_rubygems_version: !ruby/object:Gem::Requirement
206
208
  none: false
207
209
  requirements:
@@ -210,7 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
210
212
  version: '0'
211
213
  segments:
212
214
  - 0
213
- hash: -3597732090146629653
215
+ hash: -2123252871276968084
214
216
  requirements: []
215
217
  rubyforge_project:
216
218
  rubygems_version: 1.8.24