games_dice 0.3.9 → 0.3.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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