games_dice 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ class Bunch
5
+ # @!visibility private
6
+ # Private extension methods for GamesDice::Bunch keep rules
7
+ module KeepHelpers
8
+ private
9
+
10
+ def keep_mode_from_hash(options)
11
+ @keep_mode = options[:keep_mode]
12
+ case @keep_mode
13
+ when nil
14
+ @keep_mode = nil
15
+ when :keep_best, :keep_worst
16
+ @keep_number = Integer(options[:keep_number] || 1)
17
+ else
18
+ raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{options[:keep_mode].inspect}"
19
+ end
20
+ end
21
+
22
+ def find_used_dice_due_to_keep_mode(used_dice, unused_dice = [])
23
+ full_dice = result_details.sort_by(&:total)
24
+ case @keep_mode
25
+ when :keep_best
26
+ used_dice = full_dice[-@keep_number..]
27
+ unused_dice = full_dice[0..(full_dice.length - 1 - @keep_number)]
28
+ when :keep_worst
29
+ used_dice = full_dice[0..(@keep_number - 1)]
30
+ unused_dice = full_dice[@keep_number..]
31
+ end
32
+
33
+ [used_dice, unused_dice]
34
+ end
35
+
36
+ def explain_kept_dice(used_dice)
37
+ separator = @single_die.maps ? ', ' : ' + '
38
+ ". Keep: #{used_dice.map(&:explain_total).join(separator)}"
39
+ end
40
+ end
41
+
42
+ # @!visibility private
43
+ # Private extension methods for GamesDice::Bunch explaining
44
+ module ExplainHelpers
45
+ private
46
+
47
+ def build_explanation(used_dice)
48
+ if @keep_mode || @single_die.maps
49
+ explanation = explain_with_keep_or_map(used_dice)
50
+ else
51
+ explanation = used_dice.map(&:explain_value).join(' + ')
52
+ explanation += " = #{@result}" if @ndice > 1
53
+ end
54
+
55
+ explanation
56
+ end
57
+
58
+ def explain_with_keep_or_map(used_dice)
59
+ explanation = result_details.map(&:explain_value).join(', ')
60
+ explanation += explain_kept_dice(used_dice) if @keep_mode
61
+ explanation += ". Successes: #{@result}" if @single_die.maps
62
+ explanation += " = #{@result}" if @keep_mode && !@single_die.maps && @keep_number > 1
63
+
64
+ explanation
65
+ end
66
+ end
67
+ end
68
+ end
@@ -24,7 +24,9 @@ module GamesDice
24
24
  # d.explain_result # => "[6+5] 11 Success"
25
25
  #
26
26
  class ComplexDie
27
- include GamesDice::ComplexDieHelpers
27
+ include RollHelpers
28
+ include ProbabilityHelpers
29
+ include MinMaxHelpers
28
30
 
29
31
  # @!visibility private
30
32
  # arbitrary limit to speed up probability calculations. It should
@@ -35,8 +37,10 @@ module GamesDice
35
37
  # @param [Integer] sides Number of sides on a single die, passed to GamesDice::Die's constructor
36
38
  # @param [Hash] options
37
39
  # @option options [Array<GamesDice::RerollRule,Array>] :rerolls The rules that cause the die to roll again
38
- # @option options [Array<GamesDice::MapRule,Array>] :maps The rules to convert a value into a final result for the die
39
- # @option options [#rand] :prng An alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
40
+ # @option options [Array<GamesDice::MapRule,Array>] :maps The rules to convert a value into a final result
41
+ # for the die
42
+ # @option options [#rand] :prng An alternative source of randomness to Ruby's built-in #rand, passed to
43
+ # GamesDice::Die's constructor
40
44
  # @return [GamesDice::ComplexDie]
41
45
  def initialize(sides, options = {})
42
46
  @basic_die = GamesDice::Die.new(sides, options[:prng])
@@ -46,6 +50,8 @@ module GamesDice
46
50
 
47
51
  @total = nil
48
52
  @result = nil
53
+
54
+ @probabilities_complete = true
49
55
  end
50
56
 
51
57
  # The simple component used by this complex one
@@ -105,27 +111,7 @@ module GamesDice
105
111
  # 1e-9 at worst.
106
112
  # @return [GamesDice::Probabilities] Probability distribution of die.
107
113
  def probabilities
108
- return @probabilities if @probabilities
109
-
110
- @probabilities_complete = true
111
- if @rerolls && @maps
112
- reroll_probs = recursive_probabilities
113
- prob_hash = {}
114
- reroll_probs.each do |v, p|
115
- add_mapped_to_prob_hash(prob_hash, v, p)
116
- end
117
- elsif @rerolls
118
- prob_hash = recursive_probabilities
119
- elsif @maps
120
- prob_hash = {}
121
- @basic_die.probabilities.each do |v, p|
122
- add_mapped_to_prob_hash(prob_hash, v, p)
123
- end
124
- else
125
- @probabilities = @basic_die.probabilities
126
- return @probabilities
127
- end
128
- @probabilities = GamesDice::Probabilities.from_h(prob_hash)
114
+ @probabilities ||= calculate_probabilities
129
115
  end
130
116
 
131
117
  # Simulates rolling the die
@@ -140,63 +126,6 @@ module GamesDice
140
126
 
141
127
  private
142
128
 
143
- def add_mapped_to_prob_hash(prob_hash, v, p)
144
- m, n = calc_maps(v)
145
- prob_hash[m] ||= 0.0
146
- prob_hash[m] += p
147
- end
148
-
149
- def roll_apply_rerolls
150
- return unless @rerolls
151
-
152
- subtracting = false
153
- rerolls_remaining = @rerolls.map(&:limit)
154
-
155
- loop do
156
- rule_idx = find_matching_reroll_rule(@basic_die.result, @result.rolls.length, rerolls_remaining)
157
- break unless rule_idx
158
-
159
- rule = @rerolls[rule_idx]
160
- rerolls_remaining[rule_idx] -= 1
161
- subtracting = true if rule.type == :reroll_subtract
162
- roll_apply_reroll_rule rule, subtracting
163
- end
164
- end
165
-
166
- def roll_apply_reroll_rule(rule, is_subtracting)
167
- # Apply the rule (note reversal for additions, after a subtract)
168
- if is_subtracting && rule.type == :reroll_add
169
- @result.add_roll(@basic_die.roll, :reroll_subtract)
170
- else
171
- @result.add_roll(@basic_die.roll, rule.type)
172
- end
173
- end
174
-
175
- # Find which rule, if any, is being triggered
176
- def find_matching_reroll_rule(check_value, num_rolls, rerolls_remaining)
177
- @rerolls.zip(rerolls_remaining).find_index do |rule, remaining|
178
- next if rule.type == :reroll_subtract && num_rolls > 1
179
-
180
- remaining.positive? && rule.applies?(check_value)
181
- end
182
- end
183
-
184
- def roll_apply_maps
185
- return unless @maps
186
-
187
- m, n = calc_maps(@result.value)
188
- @result.apply_map(m, n)
189
- end
190
-
191
- def calc_minmax
192
- @min_result = probabilities.min
193
- @max_result = probabilities.max
194
- return if @probabilities_complete
195
-
196
- logical_min, logical_max = logical_minmax
197
- @min_result, @max_result = [@min_result, @max_result, logical_min, logical_max].minmax
198
- end
199
-
200
129
  def construct_rerolls(rerolls_input)
201
130
  check_and_construct rerolls_input, GamesDice::RerollRule, 'rerolls'
202
131
  end
@@ -218,70 +147,5 @@ module GamesDice
218
147
  end
219
148
  end
220
149
  end
221
-
222
- def calc_maps(x)
223
- y = 0
224
- n = ''
225
- @maps.find do |rule|
226
- maybe_y = rule.map_from(x)
227
- if maybe_y
228
- y = maybe_y
229
- n = rule.mapped_name
230
- end
231
- maybe_y
232
- end
233
- [y, n]
234
- end
235
-
236
- def minmax_mappings(possible_values)
237
- possible_values.map do |x|
238
- m, n = calc_maps(x)
239
- m
240
- end.minmax
241
- end
242
-
243
- # This isn't 100% accurate, but does cover most "normal" scenarios, and we're only falling back to it when we have to
244
- # The inaccuracy is that min_result..max_result may contain 'holes' which have extreme map values that cannot actually
245
- # occur. In practice it is likely a non-issue unless someone went out of their way to invent a dice scheme that broke it.
246
- def logical_minmax
247
- return @basic_die.minmax unless @rerolls || @maps
248
- return minmax_mappings(@basic_die.all_values) unless @rerolls
249
-
250
- min_result, max_result = logical_rerolls_minmax
251
- return minmax_mappings((min_result..max_result)) if @maps
252
-
253
- [min_result, max_result]
254
- end
255
-
256
- def logical_rerolls_minmax
257
- min_result = @basic_die.min
258
- max_result = @basic_die.max
259
- min_subtract = find_minimum_possible_subtract
260
- max_add = find_maximum_possible_adds
261
- min_result = [min_subtract - max_add, min_subtract - max_result].min if min_subtract
262
- [min_result, max_add + max_result]
263
- end
264
-
265
- def find_minimum_possible_subtract
266
- min_subtract = nil
267
- @rerolls.select { |r| r.type == :reroll_subtract }.each do |rule|
268
- min_reroll = @basic_die.all_values.select { |v| rule.applies?(v) }.min
269
- next unless min_reroll
270
-
271
- min_subtract = [min_reroll, min_subtract].compact.min
272
- end
273
- min_subtract
274
- end
275
-
276
- def find_maximum_possible_adds
277
- total_add = 0
278
- @rerolls.select { |r| r.type == :reroll_add }.each do |rule|
279
- max_reroll = @basic_die.all_values.select { |v| rule.applies?(v) }.max
280
- next unless max_reroll
281
-
282
- total_add += max_reroll * rule.limit
283
- end
284
- total_add
285
- end
286
150
  end
287
151
  end
@@ -1,68 +1,260 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # @!visibility private
4
3
  module GamesDice
5
- # Private extension methods for GamesDice::ComplexDie
6
- module ComplexDieHelpers
7
- private
4
+ class ComplexDie
5
+ # @!visibility private
6
+ # Private extension methods for GamesDice::ComplexDie probability calculations
7
+ module ProbabilityHelpers
8
+ private
8
9
 
9
- def recursive_probabilities(probabilities = {}, prior_probability = 1.0, depth = 0, prior_result = nil, rerolls_left = nil, roll_reason = :basic, subtracting = false)
10
- each_probability = prior_probability / @basic_die.sides
11
- depth += 1
12
- if depth >= 20 || each_probability < 1.0e-16
13
- @probabilities_complete = false
14
- stop_recursing = true
10
+ def calculate_probabilities
11
+ if @rerolls && @maps
12
+ GamesDice::Probabilities.from_h(prob_hash_with_rerolls_and_maps)
13
+ elsif @rerolls
14
+ GamesDice::Probabilities.from_h(recursive_probabilities)
15
+ elsif @maps
16
+ GamesDice::Probabilities.from_h(prob_hash_with_just_maps)
17
+ else
18
+ @basic_die.probabilities
19
+ end
15
20
  end
16
21
 
17
- @basic_die.each_value do |v|
18
- recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left,
19
- subtracting, stop_recursing)
22
+ def prob_hash_with_rerolls_and_maps
23
+ prob_hash = {}
24
+ reroll_probs = recursive_probabilities
25
+ reroll_probs.each do |v, p|
26
+ add_mapped_to_prob_hash(prob_hash, v, p)
27
+ end
28
+ prob_hash
20
29
  end
21
- probabilities
22
- end
23
30
 
24
- def recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing)
25
- # calculate value, recurse if there is a reroll
26
- result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason)
31
+ def prob_hash_with_just_maps
32
+ prob_hash = {}
33
+ @basic_die.probabilities.each do |v, p|
34
+ add_mapped_to_prob_hash(prob_hash, v, p)
35
+ end
36
+ prob_hash
37
+ end
27
38
 
28
- # Find which rule, if any, is being triggered
29
- rule_idx = find_matching_reroll_rule(v, result_so_far.rolls.length, rerolls_remaining)
39
+ def add_mapped_to_prob_hash(prob_hash, orig_val, prob)
40
+ mapped_val, = calc_maps(orig_val)
41
+ prob_hash[mapped_val] ||= 0.0
42
+ prob_hash[mapped_val] += prob
43
+ end
44
+
45
+ RecurseStack = Struct.new(:depth, :roll_reason, :subtracting, :probabilities, :prior_probability, :prior_result,
46
+ :rerolls_left) do
47
+ def initialize
48
+ self.depth = 0
49
+ self.roll_reason = :basic
50
+ self.subtracting = false
51
+ self.probabilities = {}
52
+ self.prior_probability = 1.0
53
+ end
54
+ end
55
+ private_constant :RecurseStack
56
+
57
+ def recursive_probabilities(stack = RecurseStack.new)
58
+ stack.prior_probability = stack.prior_probability / @basic_die.sides
59
+ stack.depth += 1
60
+
61
+ @basic_die.each_value do |die_val|
62
+ recurse_probs_for_value(die_val, stack)
63
+ end
64
+ stack.probabilities
65
+ end
66
+
67
+ def recurse_probs_for_value(die_val, stack)
68
+ result_so_far, rerolls_remaining = calc_result_so_far(die_val, stack)
69
+ rule_idx = find_matching_reroll_rule(die_val, result_so_far.rolls.length, rerolls_remaining)
70
+
71
+ if conintue_recursing?(stack, rule_idx)
72
+ continue_recursion(stack, result_so_far, rerolls_remaining, rule_idx)
73
+ else
74
+ end_recursion_store_probs(stack, result_so_far)
75
+ end
76
+ end
77
+
78
+ def conintue_recursing?(stack, rule_idx)
79
+ if stack.depth >= 20 || stack.prior_probability < 1.0e-16
80
+ @probabilities_complete = false
81
+ return false
82
+ end
83
+
84
+ !rule_idx.nil?
85
+ end
30
86
 
31
- if rule_idx && !stop_recursing
32
- recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx,
33
- subtracting)
34
- else
87
+ def continue_recursion(stack, result_so_far, rerolls_remaining, rule_idx)
88
+ rule = @rerolls[rule_idx]
89
+ rerolls_remaining[rule_idx] -= 1
90
+ recurse_probs_with_rule(stack, result_so_far, rerolls_remaining, rule)
91
+ end
92
+
93
+ def end_recursion_store_probs(stack, result_so_far)
35
94
  t = result_so_far.total
36
- probabilities[t] ||= 0.0
37
- probabilities[t] += each_probability
95
+ stack.probabilities[t] ||= 0.0
96
+ stack.probabilities[t] += stack.prior_probability
38
97
  end
39
- end
40
98
 
41
- def recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting)
42
- rule = @rerolls[rule_idx]
43
- rerolls_remaining[rule_idx] -= 1
44
- is_subtracting = true if subtracting || rule.type == :reroll_subtract
99
+ def recurse_probs_with_rule(stack, result_so_far, rerolls_remaining, rule)
100
+ next_stack = stack.clone
101
+ next_stack.prior_result = result_so_far
102
+ next_stack.rerolls_left = rerolls_remaining
103
+ next_stack.subtracting = true if stack.subtracting || rule.type == :reroll_subtract
104
+
105
+ # Apply the rule (note reversal for additions, after a subtract)
106
+ next_stack.roll_reason = if stack.subtracting && rule.type == :reroll_add
107
+ :reroll_subtract
108
+ else
109
+ rule.type
110
+ end
45
111
 
46
- # Apply the rule (note reversal for additions, after a subtract)
47
- if subtracting && rule.type == :reroll_add
48
- recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining,
49
- :reroll_subtract, is_subtracting
50
- else
51
- recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule.type,
52
- is_subtracting
112
+ recursive_probabilities next_stack
113
+ end
114
+
115
+ def calc_result_so_far(die_val, stack)
116
+ if stack.prior_result
117
+ result_so_far = stack.prior_result.clone
118
+ rerolls_remaining = stack.rerolls_left.clone
119
+ result_so_far.add_roll(die_val, stack.roll_reason)
120
+ else
121
+ rerolls_remaining = @rerolls.map(&:limit)
122
+ result_so_far = GamesDice::DieResult.new(die_val, stack.roll_reason)
123
+ end
124
+ [result_so_far, rerolls_remaining]
53
125
  end
54
126
  end
55
127
 
56
- def calc_result_so_far(prior_result, rerolls_left, v, roll_reason)
57
- if prior_result
58
- result_so_far = prior_result.clone
59
- result_so_far.add_roll(v, roll_reason)
60
- rerolls_remaining = rerolls_left.clone
61
- else
62
- result_so_far = GamesDice::DieResult.new(v, roll_reason)
128
+ # @!visibility private
129
+ # Private extension methods for GamesDice::ComplexDie simulating rolls
130
+ module RollHelpers
131
+ private
132
+
133
+ def roll_apply_rerolls
134
+ return unless @rerolls
135
+
136
+ subtracting = false
63
137
  rerolls_remaining = @rerolls.map(&:limit)
138
+
139
+ rerolls_loop(subtracting, rerolls_remaining)
140
+ end
141
+
142
+ def rerolls_loop(subtracting, rerolls_remaining)
143
+ loop do
144
+ rule_idx = find_matching_reroll_rule(@basic_die.result, @result.rolls.length, rerolls_remaining)
145
+ break unless rule_idx
146
+
147
+ rule = @rerolls[rule_idx]
148
+ rerolls_remaining[rule_idx] -= 1
149
+ subtracting = true if rule.type == :reroll_subtract
150
+ roll_apply_reroll_rule rule, subtracting
151
+ end
152
+ end
153
+
154
+ def roll_apply_reroll_rule(rule, is_subtracting)
155
+ # Apply the rule (note reversal for additions, after a subtract)
156
+ if is_subtracting && rule.type == :reroll_add
157
+ @result.add_roll(@basic_die.roll, :reroll_subtract)
158
+ else
159
+ @result.add_roll(@basic_die.roll, rule.type)
160
+ end
161
+ end
162
+
163
+ # Find which rule, if any, is being triggered
164
+ def find_matching_reroll_rule(check_value, num_rolls, rerolls_remaining)
165
+ @rerolls.zip(rerolls_remaining).find_index do |rule, remaining|
166
+ next if rule.type == :reroll_subtract && num_rolls > 1
167
+
168
+ remaining.positive? && rule.applies?(check_value)
169
+ end
170
+ end
171
+
172
+ def roll_apply_maps
173
+ return unless @maps
174
+
175
+ m, n = calc_maps(@result.value)
176
+ @result.apply_map(m, n)
177
+ end
178
+
179
+ def calc_maps(original_value)
180
+ y = 0
181
+ n = ''
182
+ @maps.find do |rule|
183
+ if (maybe_y = rule.map_from(original_value))
184
+ y = maybe_y
185
+ n = rule.mapped_name
186
+ end
187
+ maybe_y
188
+ end
189
+ [y, n]
190
+ end
191
+ end
192
+
193
+ # @!visibility private
194
+ # Private extension methods for GamesDice::ComplexDie calculating min and max (which is surprisingly complex)
195
+ module MinMaxHelpers
196
+ private
197
+
198
+ def calc_minmax
199
+ @min_result = probabilities.min
200
+ @max_result = probabilities.max
201
+ return if @probabilities_complete
202
+
203
+ logical_min, logical_max = logical_minmax
204
+ @min_result, @max_result = [@min_result, @max_result, logical_min, logical_max].minmax
205
+ end
206
+
207
+ def minmax_mappings(possible_values)
208
+ possible_values.map do |x|
209
+ map_val, = calc_maps(x)
210
+ map_val
211
+ end.minmax
212
+ end
213
+
214
+ # This isn't 100% accurate, but does cover most "normal" scenarios, and we're only falling back to it when we
215
+ # have to. The inaccuracy is that min_result..max_result may contain 'holes' which have extreme map values that
216
+ # cannot actually occur. In practice it is likely a non-issue unless someone went out of their way to invent a
217
+ # dice schem that broke it.
218
+ def logical_minmax
219
+ return @basic_die.minmax unless @rerolls || @maps
220
+ return minmax_mappings(@basic_die.all_values) unless @rerolls
221
+
222
+ min_result, max_result = logical_rerolls_minmax
223
+ return minmax_mappings(min_result..max_result) if @maps
224
+
225
+ [min_result, max_result]
226
+ end
227
+
228
+ def logical_rerolls_minmax
229
+ min_result = @basic_die.min
230
+ max_result = @basic_die.max
231
+ min_subtract = find_minimum_possible_subtract
232
+ max_add = find_maximum_possible_adds
233
+ min_result = [min_subtract - max_add, min_subtract - max_result].min if min_subtract
234
+ [min_result, max_add + max_result]
235
+ end
236
+
237
+ def find_minimum_possible_subtract
238
+ min_subtract = nil
239
+ @rerolls.select { |r| r.type == :reroll_subtract }.each do |rule|
240
+ min_reroll = @basic_die.all_values.select { |v| rule.applies?(v) }.min
241
+ next unless min_reroll
242
+
243
+ min_subtract = [min_reroll, min_subtract].compact.min
244
+ end
245
+ min_subtract
246
+ end
247
+
248
+ def find_maximum_possible_adds
249
+ total_add = 0
250
+ @rerolls.select { |r| r.type == :reroll_add }.each do |rule|
251
+ max_reroll = @basic_die.all_values.select { |v| rule.applies?(v) }.max
252
+ next unless max_reroll
253
+
254
+ total_add += max_reroll * rule.limit
255
+ end
256
+ total_add
64
257
  end
65
- [result_so_far, rerolls_remaining]
66
258
  end
67
259
  end
68
260
  end
@@ -28,11 +28,14 @@ module GamesDice
28
28
  # @option bunches [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
29
29
  # @option bunches [String] :name Optional name for the bunch
30
30
  # @option bunches [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
31
- # @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
32
- # @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
31
+ # @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result
32
+ # for the die
33
+ # @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to
34
+ # GamesDice::Die's constructor
33
35
  # @option bunches [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
34
36
  # @option bunches [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
35
- # @option bunches [Integer] :multiplier Optional, defaults to 1, and typically 1 or -1 to describe whether the Bunch total is to be added or subtracted
37
+ # @option bunches [Integer] :multiplier Optional, defaults to 1, and typically 1 or -1 to describe whether the
38
+ # Bunch total is to be added or subtracted
36
39
  # @return [GamesDice::Dice]
37
40
  def initialize(bunches, offset = 0, name = '')
38
41
  @name = name
@@ -110,14 +113,11 @@ module GamesDice
110
113
 
111
114
  explanations = @bunches.map { |bunch| "#{bunch.label}: #{bunch.explain_result}" }
112
115
 
113
- return @offset.to_s if explanations.count.zero?
116
+ return @offset.to_s if explanations.none?
114
117
 
115
- return simple_explanation(explanations.first) if explanations.count == 1
118
+ return simple_explanation(explanations.first) if explanations.one?
116
119
 
117
- bunch_values = @bunch_multipliers.zip(@bunches).map { |m, b| m * b.result }
118
- bunch_values << @offset if @offset != 0
119
- explanations << array_to_sum(bunch_values)
120
- explanations.join('. ')
120
+ multi_explanations(explanations)
121
121
  end
122
122
 
123
123
  private
@@ -128,8 +128,15 @@ module GamesDice
128
128
  "#{explanation}. #{array_to_sum([@bunches[0].result, @offset])}"
129
129
  end
130
130
 
131
+ def multi_explanations(explanations)
132
+ bunch_values = @bunch_multipliers.zip(@bunches).map { |m, b| m * b.result }
133
+ bunch_values << @offset if @offset != 0
134
+ explanations << array_to_sum(bunch_values)
135
+ explanations.join('. ')
136
+ end
137
+
131
138
  def array_to_sum(array)
132
- (numbers_to_strings(array) + ['=', array.inject(:+)]).join(' ')
139
+ (numbers_to_strings(array) + ['=', array.sum]).join(' ')
133
140
  end
134
141
 
135
142
  def numbers_to_strings(array)
@@ -74,8 +74,8 @@ module GamesDice
74
74
  # Iterates through all possible results on die.
75
75
  # @yieldparam [Integer] result A potential result from the die
76
76
  # @return [GamesDice::Die] this object
77
- def each_value(&block)
78
- (1..@sides).each(&block)
77
+ def each_value(&)
78
+ (1..@sides).each(&)
79
79
  self
80
80
  end
81
81