games_dice 0.0.3 → 0.0.5

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