games_dice 0.0.3 → 0.0.5

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.
@@ -13,8 +13,10 @@ Gem::Specification.new do |gem|
13
13
  gem.summary = %q{Simulates and explains dice rolls from a variety of game systems.}
14
14
  gem.homepage = "https://github.com/neilslater/games_dice"
15
15
 
16
- gem.add_development_dependency "rspec"
17
- gem.add_development_dependency "rake"
16
+ gem.add_development_dependency "rspec", ">= 2.13.0"
17
+ gem.add_development_dependency "rake", ">= 1.9.1"
18
+
19
+ gem.add_dependency "parslet", "~> 1.5.0"
18
20
 
19
21
  gem.files = `git ls-files`.split($/)
20
22
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -1,10 +1,13 @@
1
1
  require "games_dice/version"
2
+ require "games_dice/constants"
3
+ require "games_dice/probabilities"
2
4
  require "games_dice/die"
3
5
  require "games_dice/die_result"
4
6
  require "games_dice/reroll_rule"
5
7
  require "games_dice/map_rule"
6
8
  require "games_dice/complex_die"
7
9
  require "games_dice/bunch"
10
+ require "games_dice/dice"
8
11
 
9
12
  module GamesDice
10
13
  # TODO: Factory methods for various dice schemes
@@ -1,367 +1,322 @@
1
- module GamesDice
2
-
3
- # models a set of identical dice, that can be "rolled" and combined into a simple integer result. The
4
- # dice are identical in number of sides, and any rolling rules that apply to them
5
- class Bunch
6
- # attributes is a hash of symbols used to set attributes of the new Bunch object. Each
7
- # attribute is explained in more detail in its own section. The following hash keys and values
8
- # are mandatory:
9
- # :ndice
10
- # :sides
11
- # The following are optional, and modify the behaviour of the Bunch object
12
- # :name
13
- # :prng
14
- # :rerolls
15
- # :maps
16
- # :keep_mode
17
- # :keep_number
18
- # Any other keys provided to the constructor are ignored
19
- def initialize( attributes )
20
- @name = attributes[:name].to_s
21
- @ndice = Integer(attributes[:ndice])
22
- raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
23
- @sides = Integer(attributes[:sides])
24
- raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
25
-
26
- options = Hash.new
27
-
28
- if attributes[:prng]
29
- # We deliberately do not clone this object, it will often be intended that it is shared
30
- prng = attributes[:prng]
31
- raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
32
- end
33
-
34
- needs_complex_die = false
35
-
36
- if attributes[:rerolls]
37
- needs_complex_die = true
38
- options[:rerolls] = attributes[:rerolls].clone
39
- end
40
-
41
- if attributes[:maps]
42
- needs_complex_die = true
43
- options[:maps] = attributes[:maps].clone
44
- end
45
-
46
- if needs_complex_die
47
- options[:prng] = prng
48
- @single_die = GamesDice::ComplexDie.new( @sides, options )
49
- else
50
- @single_die = GamesDice::Die.new( @sides, prng )
51
- end
52
-
53
- case attributes[:keep_mode]
54
- when nil then
55
- @keep_mode = nil
56
- when :keep_best then
57
- @keep_mode = :keep_best
58
- @keep_number = Integer(attributes[:keep_number] || 1)
59
- when :keep_worst then
60
- @keep_mode = :keep_worst
61
- @keep_number = Integer(attributes[:keep_number] || 1)
62
- else
63
- raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{attributes[:keep_mode].inspect}"
64
- end
1
+ # models a set of identical dice, that can be "rolled" and combined into a simple integer result. The
2
+ # dice are identical in number of sides, and any re-roll or mapping rules that apply to them
3
+ class GamesDice::Bunch
4
+ # attributes is a hash of symbols used to set attributes of the new Bunch object. Each
5
+ # attribute is explained in more detail in its own section. The following hash keys and values
6
+ # are mandatory:
7
+ # :ndice
8
+ # :sides
9
+ # The following are optional, and modify the behaviour of the Bunch object
10
+ # :name
11
+ # :prng
12
+ # :rerolls
13
+ # :maps
14
+ # :keep_mode
15
+ # :keep_number
16
+ # Any other keys provided to the constructor are ignored
17
+ def initialize( attributes )
18
+ @name = attributes[:name].to_s
19
+ @ndice = Integer(attributes[:ndice])
20
+ raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
21
+ @sides = Integer(attributes[:sides])
22
+ raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
23
+
24
+ options = Hash.new
25
+
26
+ if attributes[:prng]
27
+ # We deliberately do not clone this object, it will often be intended that it is shared
28
+ prng = attributes[:prng]
29
+ raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
65
30
  end
66
31
 
67
- # the string name as provided to the constructor, it will appear in explain_result
68
- attr_reader :name
69
-
70
- # integer number of dice to roll (initially, before re-rolls etc)
71
- attr_reader :ndice
72
-
73
- # individual die that will be rolled, #ndice times, an GamesDice::Die or GamesDice::ComplexDie object.
74
- attr_reader :single_die
75
-
76
- # may be nil, :keep_best or :keep_worst
77
- attr_reader :keep_mode
32
+ needs_complex_die = false
78
33
 
79
- # number of "best" or "worst" results to select when #keep_mode is not nil. This attribute is
80
- # 1 by default if :keep_mode is supplied, or nil by default otherwise.
81
- attr_reader :keep_number
82
-
83
- # after calling #roll, this is set to the final integer value from using the dice as specified
84
- attr_reader :result
85
-
86
- # either nil, or an array of GamesDice::RerollRule objects that are assessed on each roll of #single_die
87
- # Reroll types :reroll_new_die and :reroll_new_keeper do not affect the #single_die, but are instead
88
- # assessed in this container object
89
- def rerolls
90
- @single_die.rerolls
91
- end
92
-
93
- # either nil, or an array of GamesDice::MapRule objects that are assessed on each result of #single_die (after rerolls are completed)
94
- def maps
95
- @single_die.rerolls
34
+ if attributes[:rerolls]
35
+ needs_complex_die = true
36
+ options[:rerolls] = attributes[:rerolls].clone
96
37
  end
97
38
 
98
- # after calling #roll, this is an array of GamesDice::DieResult objects, one from each #single_die rolled,
99
- # allowing inspection of how the result was obtained.
100
- def result_details
101
- return nil unless @raw_result_details
102
- @raw_result_details.map { |r| r.is_a?(Fixnum) ? GamesDice::DieResult.new(r) : r }
39
+ if attributes[:maps]
40
+ needs_complex_die = true
41
+ options[:maps] = attributes[:maps].clone
103
42
  end
104
43
 
105
- # minimum possible integer value
106
- def min
107
- n = @keep_mode ? [@keep_number,@ndice].min : @ndice
108
- return n * @single_die.min
44
+ if needs_complex_die
45
+ options[:prng] = prng
46
+ @single_die = GamesDice::ComplexDie.new( @sides, options )
47
+ else
48
+ @single_die = GamesDice::Die.new( @sides, prng )
109
49
  end
110
50
 
111
- # maximum possible integer value
112
- def max
113
- n = @keep_mode ? [@keep_number,@ndice].min : @ndice
114
- return n * @single_die.max
51
+ case attributes[:keep_mode]
52
+ when nil then
53
+ @keep_mode = nil
54
+ when :keep_best then
55
+ @keep_mode = :keep_best
56
+ @keep_number = Integer(attributes[:keep_number] || 1)
57
+ when :keep_worst then
58
+ @keep_mode = :keep_worst
59
+ @keep_number = Integer(attributes[:keep_number] || 1)
60
+ else
61
+ raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{attributes[:keep_mode].inspect}"
115
62
  end
116
-
117
- # returns a hash of value (Integer) => probability (Float) pairs. Warning: Some dice schemes
118
- # cause this method to take a long time, and use a lot of memory. The worst-case offenders are
119
- # dice schemes with a #keep_mode of :keep_best or :keep_worst.
120
- def probabilities
121
- return @probabilities if @probabilities
122
- @probabilities_complete = true
123
-
124
- # TODO: It is possible to optimise this slightly by combining already-calculated values
125
- # Adding dice is same as multiplying probability sets for that number of dice
126
- # Combine(probabililities_3_dice, probabililities_single_die) == Combine(probabililities_2_dice, probabililities_2_dice)
127
- # It is possible to minimise the total number of multiplications, gaining about 30% efficiency, with careful choices
128
- single_roll_probs = @single_die.probabilities
129
- if @keep_mode && @ndice > @keep_number
130
- preadd_probs = {}
131
- single_roll_probs.each { |k,v| preadd_probs[k.to_s] = v }
132
-
133
- (@keep_number-1).times do
134
- preadd_probs = prob_accumulate_combinations preadd_probs, single_roll_probs
135
- end
136
- extra_dice = @ndice - @keep_number
137
- extra_dice.times do
138
- preadd_probs = prob_accumulate_combinations preadd_probs, single_roll_probs, @keep_mode
139
- end
140
- combined_probs = {}
141
- preadd_probs.each do |k,v|
142
- total = k.split(';').map { |s| s.to_i }.inject(:+)
143
- combined_probs[total] ||= 0.0
144
- combined_probs[total] += v
145
- end
146
- else
147
- combined_probs = single_roll_probs.clone
148
- (@ndice-1).times do
149
- combined_probs = prob_accumulate combined_probs, single_roll_probs
150
- end
63
+ end
64
+
65
+ # the string name as provided to the constructor, it will appear in explain_result
66
+ attr_reader :name
67
+
68
+ # integer number of dice to roll (initially, before re-rolls etc)
69
+ attr_reader :ndice
70
+
71
+ # individual die that will be rolled, #ndice times, an GamesDice::Die or GamesDice::ComplexDie object.
72
+ attr_reader :single_die
73
+
74
+ # may be nil, :keep_best or :keep_worst
75
+ attr_reader :keep_mode
76
+
77
+ # number of "best" or "worst" results to select when #keep_mode is not nil. This attribute is
78
+ # 1 by default if :keep_mode is supplied, or nil by default otherwise.
79
+ attr_reader :keep_number
80
+
81
+ # after calling #roll, this is set to the final integer value from using the dice as specified
82
+ attr_reader :result
83
+
84
+ # either nil, or an array of GamesDice::RerollRule objects that are assessed on each roll of #single_die
85
+ # Reroll types :reroll_new_die and :reroll_new_keeper do not affect the #single_die, but are instead
86
+ # assessed in this container object
87
+ def rerolls
88
+ @single_die.rerolls
89
+ end
90
+
91
+ # either nil, or an array of GamesDice::MapRule objects that are assessed on each result of #single_die (after rerolls are completed)
92
+ def maps
93
+ @single_die.rerolls
94
+ end
95
+
96
+ # after calling #roll, this is an array of GamesDice::DieResult objects, one from each #single_die rolled,
97
+ # allowing inspection of how the result was obtained.
98
+ def result_details
99
+ return nil unless @raw_result_details
100
+ @raw_result_details.map { |r| r.is_a?(Fixnum) ? GamesDice::DieResult.new(r) : r }
101
+ end
102
+
103
+ # minimum possible integer value
104
+ def min
105
+ n = @keep_mode ? [@keep_number,@ndice].min : @ndice
106
+ return n * @single_die.min
107
+ end
108
+
109
+ # maximum possible integer value
110
+ def max
111
+ n = @keep_mode ? [@keep_number,@ndice].min : @ndice
112
+ return n * @single_die.max
113
+ end
114
+
115
+ # returns a hash of value (Integer) => probability (Float) pairs. Warning: Some dice schemes
116
+ # cause this method to take a long time, and use a lot of memory. The worst-case offenders are
117
+ # dice schemes with a #keep_mode of :keep_best or :keep_worst.
118
+ def probabilities
119
+ return @probabilities if @probabilities
120
+ @probabilities_complete = true
121
+
122
+ # TODO: It is possible to optimise this slightly by combining already-calculated values
123
+ # Adding dice is same as multiplying probability sets for that number of dice
124
+ # Combine(probabililities_3_dice, probabililities_single_die) == Combine(probabililities_2_dice, probabililities_2_dice)
125
+ # It is possible to minimise the total number of multiplications, gaining about 30% efficiency, with careful choices
126
+ single_roll_probs = @single_die.probabilities.to_h
127
+ if @keep_mode && @ndice > @keep_number
128
+ preadd_probs = {}
129
+ single_roll_probs.each { |k,v| preadd_probs[k.to_s] = v }
130
+
131
+ (@keep_number-1).times do
132
+ preadd_probs = prob_accumulate_combinations preadd_probs, single_roll_probs
133
+ end
134
+ extra_dice = @ndice - @keep_number
135
+ extra_dice.times do
136
+ preadd_probs = prob_accumulate_combinations preadd_probs, single_roll_probs, @keep_mode
137
+ end
138
+ combined_probs = {}
139
+ preadd_probs.each do |k,v|
140
+ total = k.split(';').map { |s| s.to_i }.inject(:+)
141
+ combined_probs[total] ||= 0.0
142
+ combined_probs[total] += v
143
+ end
144
+ else
145
+ combined_probs = single_roll_probs.clone
146
+ (@ndice-1).times do
147
+ combined_probs = prob_accumulate combined_probs, single_roll_probs
151
148
  end
152
-
153
- @probabilities = combined_probs
154
- @probabilities_min, @probabilities_max = @probabilities.keys.minmax
155
- @prob_ge = {}
156
- @prob_le = {}
157
- @probabilities
158
149
  end
159
150
 
160
- # returns probability than a roll will produce a number greater than target integer
161
- def probability_gt target
162
- probability_ge( Integer(target) + 1 )
163
- end
151
+ @probabilities_min, @probabilities_max = combined_probs.keys.minmax
152
+ @probabilities = GamesDice::Probabilities.new( combined_probs )
153
+ end
164
154
 
165
- # returns probability than a roll will produce a number greater than or equal to target integer
166
- def probability_ge target
167
- target = Integer(target)
168
- return @prob_ge[target] if @prob_ge && @prob_ge[target]
155
+ # simulate dice roll according to spec. Returns integer final total, and also stores it in #result
156
+ def roll
157
+ @result = 0
158
+ @raw_result_details = []
169
159
 
170
- # Force caching if not already done
171
- probabilities
172
- return 1.0 if target <= @probabilities_min
173
- return 0.0 if target > @probabilities_max
174
- @prob_ge[target] = probabilities.select {|k,v| target <= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
160
+ @ndice.times do
161
+ @result += @single_die.roll
162
+ @raw_result_details << @single_die.result
175
163
  end
176
164
 
177
- # returns probability than a roll will produce a number less than or equal to target integer
178
- def probability_le target
179
- target = Integer(target)
180
- return @prob_le[target] if @prob_le && @prob_le[target]
181
-
182
- # Force caching of probability table if not already done
183
- probabilities
184
- return 1.0 if target >= @probabilities_max
185
- return 0.0 if target < @probabilities_min
186
- @prob_le[target] = probabilities.select {|k,v| target >= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
165
+ if ! @keep_mode
166
+ return @result
187
167
  end
188
168
 
189
- # returns probability than a roll will produce a number less than target integer
190
- def probability_lt target
191
- probability_le( Integer(target) - 1 )
169
+ use_dice = if @keep_mode && @keep_number < @ndice
170
+ case @keep_mode
171
+ when :keep_best then @raw_result_details.sort[-@keep_number..-1]
172
+ when :keep_worst then @raw_result_details.sort[0..(@keep_number-1)]
173
+ end
174
+ else
175
+ @raw_result_details
192
176
  end
193
177
 
194
- # returns mean expected value as a Float
195
- def expected_result
196
- @expected_result ||= probabilities.inject(0.0) { |accumulate,p| accumulate + p[0] * p[1] }
197
- end
178
+ @result = use_dice.inject(0) { |so_far, die_result| so_far + die_result }
179
+ end
198
180
 
199
- # simulate dice roll according to spec. Returns integer final total, and also stores it in #result
200
- def roll
201
- @result = 0
202
- @raw_result_details = []
181
+ def explain_result
182
+ return nil unless @result
203
183
 
204
- @ndice.times do
205
- @result += @single_die.roll
206
- @raw_result_details << @single_die.result
207
- end
184
+ explanation = ''
208
185
 
209
- if ! @keep_mode
210
- return @result
211
- end
186
+ # With #keep_mode, we may need to show unused and used dice separately
187
+ used_dice = result_details
188
+ unused_dice = []
212
189
 
213
- use_dice = if @keep_mode && @keep_number < @ndice
214
- case @keep_mode
215
- when :keep_best then @raw_result_details.sort[-@keep_number..-1]
216
- when :keep_worst then @raw_result_details.sort[0..(@keep_number-1)]
217
- end
218
- else
219
- @raw_result_details
190
+ # Pick highest numbers and their associated details
191
+ if @keep_mode && @keep_number < @ndice
192
+ full_dice = result_details.sort_by { |die_result| die_result.total }
193
+ case @keep_mode
194
+ when :keep_best then
195
+ used_dice = full_dice[-@keep_number..-1]
196
+ unused_dice = full_dice[0..full_dice.length-1-@keep_number]
197
+ when :keep_worst then
198
+ used_dice = full_dice[0..(@keep_number-1)]
199
+ unused_dice = full_dice[@keep_number..(full_dice.length-1)]
220
200
  end
221
-
222
- @result = use_dice.inject(0) { |so_far, die_result| so_far + die_result }
223
201
  end
224
202
 
225
- def explain_result
226
- return nil unless @result
227
-
228
- explanation = ''
229
-
230
- # With #keep_mode, we may need to show unused and used dice separately
231
- used_dice = result_details
232
- unused_dice = []
233
-
234
- # Pick highest numbers and their associated details
235
- if @keep_mode && @keep_number < @ndice
236
- full_dice = result_details.sort_by { |die_result| die_result.total }
237
- case @keep_mode
238
- when :keep_best then
239
- used_dice = full_dice[-@keep_number..-1]
240
- unused_dice = full_dice[0..full_dice.length-1-@keep_number]
241
- when :keep_worst then
242
- used_dice = full_dice[0..(@keep_number-1)]
243
- unused_dice = full_dice[@keep_number..(full_dice.length-1)]
244
- end
203
+ # Show unused dice (if any)
204
+ if @keep_mode || @single_die.maps
205
+ explanation += result_details.map do |die_result|
206
+ die_result.explain_value
207
+ end.join(', ')
208
+ if @keep_mode
209
+ separator = @single_die.maps ? ', ' : ' + '
210
+ explanation += ". Keep: " + used_dice.map do |die_result|
211
+ die_result.explain_total
212
+ end.join( separator )
245
213
  end
246
-
247
- # Show unused dice (if any)
248
- if @keep_mode || @single_die.maps
249
- explanation += result_details.map do |die_result|
250
- die_result.explain_value
251
- end.join(', ')
252
- if @keep_mode
253
- separator = @single_die.maps ? ', ' : ' + '
254
- explanation += ". Keep: " + used_dice.map do |die_result|
255
- die_result.explain_total
256
- end.join( separator )
257
- end
258
- if @single_die.maps
259
- explanation += ". Successes: #{@result}"
260
- end
261
- explanation += " = #{@result}" if @keep_mode && ! @single_die.maps && @keep_number > 1
262
- else
263
- explanation += used_dice.map do |die_result|
264
- die_result.explain_value
265
- end.join(' + ')
266
- explanation += " = #{@result}" if @ndice > 1
214
+ if @single_die.maps
215
+ explanation += ". Successes: #{@result}"
267
216
  end
268
-
269
- explanation
217
+ explanation += " = #{@result}" if @keep_mode && ! @single_die.maps && @keep_number > 1
218
+ else
219
+ explanation += used_dice.map do |die_result|
220
+ die_result.explain_value
221
+ end.join(' + ')
222
+ explanation += " = #{@result}" if @ndice > 1
270
223
  end
271
224
 
272
- private
225
+ explanation
226
+ end
273
227
 
274
- # combines two sets of probabilities where the end result is the first set of keys plus
275
- # the second set of keys, at the associated probailities of the values
276
- def prob_accumulate first_probs, second_probs
277
- accumulator = Hash.new
228
+ private
278
229
 
279
- first_probs.each do |v1,p1|
280
- second_probs.each do |v2,p2|
281
- v3 = v1 + v2
282
- p3 = p1 * p2
283
- accumulator[v3] ||= 0.0
284
- accumulator[v3] += p3
285
- end
286
- end
230
+ # combines two sets of probabilities where the end result is the first set of keys plus
231
+ # the second set of keys, at the associated probailities of the values
232
+ def prob_accumulate first_probs, second_probs
233
+ accumulator = Hash.new
287
234
 
288
- accumulator
235
+ first_probs.each do |v1,p1|
236
+ second_probs.each do |v2,p2|
237
+ v3 = v1 + v2
238
+ p3 = p1 * p2
239
+ accumulator[v3] ||= 0.0
240
+ accumulator[v3] += p3
241
+ end
289
242
  end
290
243
 
291
- # combines two sets of probabilities, as above, except tracking unique permutations
292
- def prob_accumulate_combinations so_far, die_probs, keep_rule = nil
293
- accumulator = Hash.new
244
+ accumulator
245
+ end
294
246
 
295
- so_far.each do |sig,p1|
296
- combo = sig.split(';').map { |s| s.to_i }
247
+ # combines two sets of probabilities, as above, except tracking unique permutations
248
+ def prob_accumulate_combinations so_far, die_probs, keep_rule = nil
249
+ accumulator = Hash.new
297
250
 
298
- case keep_rule
299
- when nil then
300
- die_probs.each do |v2,p2|
301
- new_sig = (combo + [v2]).sort.join(';')
302
- p3 = p1 * p2
303
- accumulator[new_sig] ||= 0.0
304
- accumulator[new_sig] += p3
305
- end
306
- when :keep_best then
307
- need_more_than = combo.min
308
- die_probs.each do |v2,p2|
309
- if v2 > need_more_than
310
- new_sig = (combo + [v2]).sort[1..combo.size].join(';')
311
- else
312
- new_sig = sig
313
- end
314
- p3 = p1 * p2
315
- accumulator[new_sig] ||= 0.0
316
- accumulator[new_sig] += p3
251
+ so_far.each do |sig,p1|
252
+ combo = sig.split(';').map { |s| s.to_i }
253
+
254
+ case keep_rule
255
+ when nil then
256
+ die_probs.each do |v2,p2|
257
+ new_sig = (combo + [v2]).sort.join(';')
258
+ p3 = p1 * p2
259
+ accumulator[new_sig] ||= 0.0
260
+ accumulator[new_sig] += p3
261
+ end
262
+ when :keep_best then
263
+ need_more_than = combo.min
264
+ die_probs.each do |v2,p2|
265
+ if v2 > need_more_than
266
+ new_sig = (combo + [v2]).sort[1..combo.size].join(';')
267
+ else
268
+ new_sig = sig
317
269
  end
318
- when :keep_worst then
319
- need_less_than = combo.max
320
- die_probs.each do |v2,p2|
321
- if v2 < need_less_than
322
- new_sig = (combo + [v2]).sort[0..(combo.size-1)].join(';')
323
- else
324
- new_sig = sig
325
- end
326
- p3 = p1 * p2
327
- accumulator[new_sig] ||= 0.0
328
- accumulator[new_sig] += p3
270
+ p3 = p1 * p2
271
+ accumulator[new_sig] ||= 0.0
272
+ accumulator[new_sig] += p3
273
+ end
274
+ when :keep_worst then
275
+ need_less_than = combo.max
276
+ die_probs.each do |v2,p2|
277
+ if v2 < need_less_than
278
+ new_sig = (combo + [v2]).sort[0..(combo.size-1)].join(';')
279
+ else
280
+ new_sig = sig
329
281
  end
282
+ p3 = p1 * p2
283
+ accumulator[new_sig] ||= 0.0
284
+ accumulator[new_sig] += p3
330
285
  end
331
286
  end
332
-
333
- accumulator
334
287
  end
335
288
 
336
- # Generates all sets of [throw_away,may_keep_exactly,keep_preferentially,combinations] that meet
337
- # criteria for correct total number of dice and keep dice. These then need to be assessed for every
338
- # die value by the caller to get a full set of probabilities
339
- def generate_item_counts total_dice, keep_dice
340
- # Constraints are:
341
- # may_keep_exactly must be at least 1, and at most is all the dice
342
- # keep_preferentially plus may_keep_exactly must be >= keep_dice, but keep_preferentially < keep dice
343
- # sum of all three always == total_dice
344
- item_counts = []
345
- (1..total_dice).each do |may_keep_exactly|
346
- min_kp = [keep_dice - may_keep_exactly, 0].max
347
- max_kp = [keep_dice - 1, total_dice - may_keep_exactly].min
348
- (min_kp..max_kp).each do |keep_preferentially|
349
- counts = [ total_dice - may_keep_exactly - keep_preferentially, may_keep_exactly, keep_preferentially ]
350
- counts << combinations(counts)
351
- item_counts << counts
352
- end
289
+ accumulator
290
+ end
291
+
292
+ # Generates all sets of [throw_away,may_keep_exactly,keep_preferentially,combinations] that meet
293
+ # criteria for correct total number of dice and keep dice. These then need to be assessed for every
294
+ # die value by the caller to get a full set of probabilities
295
+ def generate_item_counts total_dice, keep_dice
296
+ # Constraints are:
297
+ # may_keep_exactly must be at least 1, and at most is all the dice
298
+ # keep_preferentially plus may_keep_exactly must be >= keep_dice, but keep_preferentially < keep dice
299
+ # sum of all three always == total_dice
300
+ item_counts = []
301
+ (1..total_dice).each do |may_keep_exactly|
302
+ min_kp = [keep_dice - may_keep_exactly, 0].max
303
+ max_kp = [keep_dice - 1, total_dice - may_keep_exactly].min
304
+ (min_kp..max_kp).each do |keep_preferentially|
305
+ counts = [ total_dice - may_keep_exactly - keep_preferentially, may_keep_exactly, keep_preferentially ]
306
+ counts << combinations(counts)
307
+ item_counts << counts
353
308
  end
354
- item_counts
355
309
  end
356
-
357
- # How many unique ways can a set of items, some of which are identical, be arranged?
358
- def combinations item_counts
359
- item_counts = item_counts.map { |i| Integer(i) }.select { |i| i > 0 }
360
- total_items = item_counts.inject(:+)
361
- numerator = 1.upto(total_items).inject(:*)
362
- denominator = item_counts.map { |i| 1.upto(i).inject(:*) }.inject(:*)
363
- numerator / denominator
364
- end
365
-
366
- end # class Bunch
367
- end # module GamesDice
310
+ item_counts
311
+ end
312
+
313
+ # How many unique ways can a set of items, some of which are identical, be arranged?
314
+ def combinations item_counts
315
+ item_counts = item_counts.map { |i| Integer(i) }.select { |i| i > 0 }
316
+ total_items = item_counts.inject(:+)
317
+ numerator = 1.upto(total_items).inject(:*)
318
+ denominator = item_counts.map { |i| 1.upto(i).inject(:*) }.inject(:*)
319
+ numerator / denominator
320
+ end
321
+
322
+ end # class Bunch