games_dice 0.3.12 → 0.4.2

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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +38 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +53 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +12 -0
  8. data/README.md +11 -33
  9. data/Rakefile +10 -23
  10. data/ext/games_dice/extconf.rb +4 -22
  11. data/ext/games_dice/games_dice.c +1 -1
  12. data/ext/games_dice/probabilities.c +128 -9
  13. data/ext/games_dice/probabilities.h +1 -1
  14. data/games_dice.gemspec +22 -36
  15. data/lib/games_dice/bunch.rb +203 -247
  16. data/lib/games_dice/bunch_helpers.rb +68 -0
  17. data/lib/games_dice/complex_die.rb +151 -270
  18. data/lib/games_dice/complex_die_helpers.rb +260 -60
  19. data/lib/games_dice/constants.rb +10 -10
  20. data/lib/games_dice/dice.rb +153 -143
  21. data/lib/games_dice/die.rb +101 -97
  22. data/lib/games_dice/die_result.rb +206 -189
  23. data/lib/games_dice/map_rule.rb +72 -70
  24. data/lib/games_dice/marshal.rb +41 -13
  25. data/lib/games_dice/parser.rb +245 -218
  26. data/lib/games_dice/reroll_rule.rb +76 -77
  27. data/lib/games_dice/version.rb +4 -1
  28. data/lib/games_dice.rb +23 -25
  29. data/spec/bunch_spec.rb +399 -420
  30. data/spec/complex_die_spec.rb +314 -305
  31. data/spec/dice_spec.rb +33 -34
  32. data/spec/die_result_spec.rb +174 -181
  33. data/spec/die_spec.rb +81 -81
  34. data/spec/helpers.rb +25 -25
  35. data/spec/map_rule_spec.rb +40 -44
  36. data/spec/parser_spec.rb +106 -82
  37. data/spec/probability_spec.rb +522 -526
  38. data/spec/readme_spec.rb +410 -390
  39. data/spec/reroll_rule_spec.rb +40 -44
  40. metadata +17 -129
  41. data/.travis.yml +0 -14
  42. data/lib/games_dice/prob_helpers.rb +0 -259
  43. data/lib/games_dice/probabilities.rb +0 -244
data/games_dice.gemspec CHANGED
@@ -1,46 +1,32 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ lib = File.expand_path('lib', __dir__)
3
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
6
  require 'games_dice/version'
5
7
 
6
8
  Gem::Specification.new do |gem|
7
- gem.name = "games_dice"
9
+ gem.name = 'games_dice'
8
10
  gem.version = GamesDice::VERSION
9
- gem.authors = ["Neil Slater"]
10
- gem.email = ["slobo777@gmail.com"]
11
- gem.description = %q{A library for simulating dice. Use it to construct dice-rolling systems used in role-playing and board games.}
12
- gem.summary = %q{Simulates and explains dice rolls from simple "1d6" to complex "roll 7 ten-sided dice, take best 3,
13
- results of 10 roll again and add on".}
14
- gem.homepage = "https://github.com/neilslater/games_dice"
15
- gem.license = "MIT"
16
-
17
- gem.add_development_dependency "rspec", ">= 2.13.0"
18
- gem.add_development_dependency "rake", ">= 1.9.1"
19
- gem.add_development_dependency "yard", ">= 0.8.6"
20
- gem.add_development_dependency "coveralls", ">= 0.6.7"
21
- gem.add_development_dependency "json", ">= 1.7.7"
22
- gem.add_development_dependency "rake-compiler", ">= 0.8.3"
23
- if RUBY_VERSION <= "1.9.2"
24
- gem.add_development_dependency "mime-types", "< 2.0.0"
25
- end
26
-
11
+ gem.authors = ['Neil Slater']
12
+ gem.email = ['slobo777@gmail.com']
13
+ gem.description = <<~GEMDESC
14
+ A library for simulating dice. Use it to construct dice-rolling systems used in role-playing and board games.
15
+ GEMDESC
16
+ gem.summary = <<~GEMSUMM
17
+ Simulates and explains dice rolls from simple "1d6" to complex "roll 7 ten-sided dice, take best 3,
18
+ results of 10 roll again and add on".
19
+ GEMSUMM
20
+ gem.homepage = 'https://github.com/neilslater/games_dice'
21
+ gem.license = 'MIT'
27
22
 
28
- # Red Carpet renders README.md, and is optional even when developing the gem.
29
- # 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
30
- # is only really used in environments like Travis, and is safe to wrap like this in the gemspec.
31
- if RUBY_DESCRIPTION !~ /jruby/
32
- if RUBY_VERSION >= "1.9.0"
33
- gem.add_development_dependency "redcarpet", ">=2.3.0"
34
- else
35
- gem.add_development_dependency "redcarpet", ">=2.3.0", "<3.0.0"
36
- end
37
- end
23
+ gem.required_ruby_version = '>= 3.3.0'
38
24
 
39
- gem.add_dependency "parslet", ">= 1.5.0"
25
+ gem.add_dependency 'parslet', '~> 2.0'
40
26
 
41
- gem.files = `git ls-files`.split($/)
42
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
27
+ gem.files = `git ls-files -z`.split("\0").select { |file| File.file?(file) }
28
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
43
29
  gem.extensions = gem.files.grep(%r{/extconf\.rb$})
44
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
45
- gem.require_paths = ["lib"]
30
+ gem.require_paths = ['lib']
31
+ gem.metadata['rubygems_mfa_required'] = 'true'
46
32
  end
@@ -1,247 +1,203 @@
1
- # This class models a number of identical dice, which may be either GamesDice::Die or
2
- # GamesDice::ComplexDie objects.
3
- #
4
- # An object of this class represents a fixed number of indentical dice that may be rolled and their
5
- # values summed to make a total for the bunch.
6
- #
7
- # @example The ubiquitous '3d6'
8
- # d = GamesDice::Bunch.new( :ndice => 3, :sides => 6 )
9
- # d.roll # => 14
10
- # d.result # => 14
11
- # d.explain_result # => "2 + 6 + 6 = 14"
12
- # d.max # => 18
13
- #
14
- # @example Roll 5d10, and keep the best 2
15
- # d = GamesDice::Bunch.new( :ndice => 5, :sides => 10 , :keep_mode => :keep_best, :keep_number => 2 )
16
- # d.roll # => 18
17
- # d.result # => 18
18
- # d.explain_result # => "4, 9, 2, 9, 1. Keep: 9 + 9 = 18"
19
- #
20
-
21
- class GamesDice::Bunch
22
- # The constructor accepts parameters that are suitable for either GamesDice::Die or GamesDice::ComplexDie
23
- # and decides which of those classes to instantiate.
24
- # @param [Hash] options
25
- # @option options [Integer] :ndice Number of dice in the bunch, *mandatory*
26
- # @option options [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
27
- # @option options [String] :name Optional name for the bunch
28
- # @option options [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
29
- # @option options [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
30
- # @option options [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
31
- # @option options [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
32
- # @option options [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
33
- # @return [GamesDice::Bunch]
34
- def initialize( options )
35
- name_number_sides_from_hash( options )
36
- keep_mode_from_hash( options )
37
-
38
- if options[:prng]
39
- raise ":prng does not support the rand() method" if ! options[:prng].respond_to?(:rand)
40
- end
41
-
42
- if options[:rerolls] || options[:maps]
43
- @single_die = GamesDice::ComplexDie.new( @sides, complex_die_params_from_hash( options ) )
44
- else
45
- @single_die = GamesDice::Die.new( @sides, options[:prng] )
46
- end
47
- end
48
-
49
- # Name to help identify bunch
50
- # @return [String]
51
- attr_reader :name
52
-
53
- # Number of dice to roll
54
- # @return [Integer]
55
- attr_reader :ndice
56
-
57
- # Individual die from the bunch
58
- # @return [GamesDice::Die,GamesDice::ComplexDie]
59
- attr_reader :single_die
60
-
61
- # Can be nil, :keep_best or :keep_worst
62
- # @return [Symbol,nil]
63
- attr_reader :keep_mode
64
-
65
- # Number of "best" or "worst" results to select when #keep_mode is not nil.
66
- # @return [Integer,nil]
67
- attr_reader :keep_number
68
-
69
- # Result of most-recent roll, or nil if no roll made yet.
70
- # @return [Integer,nil]
71
- attr_reader :result
72
-
73
- # @!attribute [r] label
74
- # Description that will be used in explanations with more than one bunch
75
- # @return [String]
76
- def label
77
- return @name if @name != ''
78
- return @ndice.to_s + 'd' + @sides.to_s
79
- end
80
-
81
- # @!attribute [r] rerolls
82
- # Sequence of re-roll rules, or nil if re-rolls are not required.
83
- # @return [Array<GamesDice::RerollRule>, nil]
84
- def rerolls
85
- @single_die.rerolls
86
- end
87
-
88
- # @!attribute [r] maps
89
- # Sequence of map rules, or nil if mapping is not required.
90
- # @return [Array<GamesDice::MapRule>, nil]
91
- def maps
92
- @single_die.maps
93
- end
94
-
95
- # @!attribute [r] result_details
96
- # After calling #roll, this is an array of GamesDice::DieResult objects. There is one from each #single_die rolled,
97
- # allowing inspection of how the result was obtained.
98
- # @return [Array<GamesDice::DieResult>, nil] Sequence of GamesDice::DieResult objects.
99
- def result_details
100
- return nil unless @raw_result_details
101
- @raw_result_details.map { |r| r.is_a?(Integer) ? GamesDice::DieResult.new(r) : r }
102
- end
103
-
104
- # @!attribute [r] min
105
- # Minimum possible result from a call to #roll
106
- # @return [Integer]
107
- def min
108
- n = @keep_mode ? [@keep_number,@ndice].min : @ndice
109
- return n * @single_die.min
110
- end
111
-
112
- # @!attribute [r] max
113
- # Maximum possible result from a call to #roll
114
- # @return [Integer]
115
- def max
116
- n = @keep_mode ? [@keep_number,@ndice].min : @ndice
117
- return n * @single_die.max
118
- end
119
-
120
- # Calculates the probability distribution for the bunch. When the bunch is composed of dice with
121
- # open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
122
- # recursion.
123
- # @return [GamesDice::Probabilities] Probability distribution of bunch.
124
- def probabilities
125
- return @probabilities if @probabilities
126
-
127
- if @keep_mode && @ndice > @keep_number
128
- @probabilities = @single_die.probabilities.repeat_n_sum_k( @ndice, @keep_number, @keep_mode )
129
- else
130
- @probabilities = @single_die.probabilities.repeat_sum( @ndice )
131
- end
132
-
133
- return @probabilities
134
- end
135
-
136
- # Simulates rolling the bunch of identical dice
137
- # @return [Integer] Sum of all rolled dice, or sum of all keepers
138
- def roll
139
- @result = 0
140
- @raw_result_details = []
141
-
142
- @ndice.times do
143
- @result += @single_die.roll
144
- @raw_result_details << @single_die.result
145
- end
146
-
147
- if ! @keep_mode
148
- return @result
149
- end
150
-
151
- use_dice = if @keep_mode && @keep_number < @ndice
152
- case @keep_mode
153
- when :keep_best then @raw_result_details.sort[-@keep_number..-1]
154
- when :keep_worst then @raw_result_details.sort[0..(@keep_number-1)]
155
- end
156
- else
157
- @raw_result_details
158
- end
159
-
160
- @result = use_dice.inject(0) { |so_far, die_result| so_far + die_result }
161
- end
162
-
163
- # @!attribute [r] explain_result
164
- # Explanation of result, or nil if no call to #roll yet.
165
- # @return [String,nil]
166
- def explain_result
167
- return nil unless @result
168
-
169
- explanation = ''
170
-
171
- # With #keep_mode, we may need to show unused and used dice separately
172
- used_dice = result_details
173
- unused_dice = []
174
-
175
- # Pick highest numbers and their associated details
176
- if @keep_mode && @keep_number < @ndice
177
- full_dice = result_details.sort_by { |die_result| die_result.total }
178
- case @keep_mode
179
- when :keep_best then
180
- used_dice = full_dice[-@keep_number..-1]
181
- unused_dice = full_dice[0..full_dice.length-1-@keep_number]
182
- when :keep_worst then
183
- used_dice = full_dice[0..(@keep_number-1)]
184
- unused_dice = full_dice[@keep_number..(full_dice.length-1)]
185
- end
186
- end
187
-
188
- # Show unused dice (if any)
189
- if @keep_mode || @single_die.maps
190
- explanation += result_details.map do |die_result|
191
- die_result.explain_value
192
- end.join(', ')
193
- if @keep_mode
194
- separator = @single_die.maps ? ', ' : ' + '
195
- explanation += ". Keep: " + used_dice.map do |die_result|
196
- die_result.explain_total
197
- end.join( separator )
198
- end
199
- if @single_die.maps
200
- explanation += ". Successes: #{@result}"
201
- end
202
- explanation += " = #{@result}" if @keep_mode && ! @single_die.maps && @keep_number > 1
203
- else
204
- explanation += used_dice.map do |die_result|
205
- die_result.explain_value
206
- end.join(' + ')
207
- explanation += " = #{@result}" if @ndice > 1
208
- end
209
-
210
- explanation
211
- end
212
-
213
- private
214
-
215
- def name_number_sides_from_hash options
216
- @name = options[:name].to_s
217
- @ndice = Integer(options[:ndice])
218
- raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
219
- @sides = Integer(options[:sides])
220
- raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
221
- end
222
-
223
- def keep_mode_from_hash options
224
- case options[:keep_mode]
225
- when nil then
226
- @keep_mode = nil
227
- when :keep_best then
228
- @keep_mode = :keep_best
229
- @keep_number = Integer(options[:keep_number] || 1)
230
- when :keep_worst then
231
- @keep_mode = :keep_worst
232
- @keep_number = Integer(options[:keep_number] || 1)
233
- else
234
- raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{options[:keep_mode].inspect}"
235
- end
236
- end
237
-
238
- def complex_die_params_from_hash options
239
- cd_hash = Hash.new
240
- [:maps,:rerolls].each do |k|
241
- cd_hash[k] = options[k].clone if options[k]
242
- end
243
- # We deliberately do not clone this object, it will often be intended that it is shared
244
- cd_hash[:prng] = options[:prng]
245
- cd_hash
246
- end
247
- end # class Bunch
1
+ # frozen_string_literal: true
2
+
3
+ require 'games_dice/bunch_helpers'
4
+
5
+ module GamesDice
6
+ # This class models a number of identical dice, which may be either GamesDice::Die or
7
+ # GamesDice::ComplexDie objects.
8
+ #
9
+ # An object of this class represents a fixed number of indentical dice that may be rolled and their
10
+ # values summed to make a total for the bunch.
11
+ #
12
+ # @example The ubiquitous '3d6'
13
+ # d = GamesDice::Bunch.new( :ndice => 3, :sides => 6 )
14
+ # d.roll # => 14
15
+ # d.result # => 14
16
+ # d.explain_result # => "2 + 6 + 6 = 14"
17
+ # d.max # => 18
18
+ #
19
+ # @example Roll 5d10, and keep the best 2
20
+ # d = GamesDice::Bunch.new( :ndice => 5, :sides => 10 , :keep_mode => :keep_best, :keep_number => 2 )
21
+ # d.roll # => 18
22
+ # d.result # => 18
23
+ # d.explain_result # => "4, 9, 2, 9, 1. Keep: 9 + 9 = 18"
24
+ #
25
+ class Bunch
26
+ include KeepHelpers
27
+ include ExplainHelpers
28
+
29
+ # The constructor accepts parameters that are suitable for either GamesDice::Die or GamesDice::ComplexDie
30
+ # and decides which of those classes to instantiate.
31
+ # @param [Hash] options
32
+ # @option options [Integer] :ndice Number of dice in the bunch, *mandatory*
33
+ # @option options [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
34
+ # @option options [String] :name Optional name for the bunch
35
+ # @option options [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
36
+ # @option options [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result
37
+ # for the die
38
+ # @option options [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to
39
+ # GamesDice::Die's constructor
40
+ # @option options [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
41
+ # @option options [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
42
+ # @return [GamesDice::Bunch]
43
+ def initialize(options)
44
+ name_number_sides_from_hash(options)
45
+ keep_mode_from_hash(options)
46
+
47
+ raise ':prng does not support the rand() method' if options[:prng] && !options[:prng].respond_to?(:rand)
48
+
49
+ @single_die = if options[:rerolls] || options[:maps]
50
+ GamesDice::ComplexDie.new(@sides, complex_die_params_from_hash(options))
51
+ else
52
+ GamesDice::Die.new(@sides, options[:prng])
53
+ end
54
+ end
55
+
56
+ # Name to help identify bunch
57
+ # @return [String]
58
+ attr_reader :name
59
+
60
+ # Number of dice to roll
61
+ # @return [Integer]
62
+ attr_reader :ndice
63
+
64
+ # Individual die from the bunch
65
+ # @return [GamesDice::Die,GamesDice::ComplexDie]
66
+ attr_reader :single_die
67
+
68
+ # Can be nil, :keep_best or :keep_worst
69
+ # @return [Symbol,nil]
70
+ attr_reader :keep_mode
71
+
72
+ # Number of "best" or "worst" results to select when #keep_mode is not nil.
73
+ # @return [Integer,nil]
74
+ attr_reader :keep_number
75
+
76
+ # Result of most-recent roll, or nil if no roll made yet.
77
+ # @return [Integer,nil]
78
+ attr_reader :result
79
+
80
+ # @!attribute [r] label
81
+ # Description that will be used in explanations with more than one bunch
82
+ # @return [String]
83
+ def label
84
+ return @name if @name != ''
85
+
86
+ "#{@ndice}d#{@sides}"
87
+ end
88
+
89
+ # @!attribute [r] rerolls
90
+ # Sequence of re-roll rules, or nil if re-rolls are not required.
91
+ # @return [Array<GamesDice::RerollRule>, nil]
92
+ def rerolls
93
+ @single_die.rerolls
94
+ end
95
+
96
+ # @!attribute [r] maps
97
+ # Sequence of map rules, or nil if mapping is not required.
98
+ # @return [Array<GamesDice::MapRule>, nil]
99
+ def maps
100
+ @single_die.maps
101
+ end
102
+
103
+ # @!attribute [r] result_details
104
+ # After calling #roll, this is an array of GamesDice::DieResult objects. There is one from each #single_die rolled,
105
+ # allowing inspection of how the result was obtained.
106
+ # @return [Array<GamesDice::DieResult>, nil] Sequence of GamesDice::DieResult objects.
107
+ def result_details
108
+ return nil unless @raw_result_details
109
+
110
+ @raw_result_details.map { |r| r.is_a?(Integer) ? GamesDice::DieResult.new(r) : r }
111
+ end
112
+
113
+ # @!attribute [r] min
114
+ # Minimum possible result from a call to #roll
115
+ # @return [Integer]
116
+ def min
117
+ n = @keep_mode ? [@keep_number, @ndice].min : @ndice
118
+ n * @single_die.min
119
+ end
120
+
121
+ # @!attribute [r] max
122
+ # Maximum possible result from a call to #roll
123
+ # @return [Integer]
124
+ def max
125
+ n = @keep_mode ? [@keep_number, @ndice].min : @ndice
126
+ n * @single_die.max
127
+ end
128
+
129
+ # Calculates the probability distribution for the bunch. When the bunch is composed of dice with
130
+ # open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
131
+ # recursion.
132
+ # @return [GamesDice::Probabilities] Probability distribution of bunch.
133
+ def probabilities
134
+ return @probabilities if @probabilities
135
+
136
+ @probabilities = if @keep_mode && @ndice > @keep_number
137
+ @single_die.probabilities.repeat_n_sum_k(@ndice, @keep_number, @keep_mode)
138
+ else
139
+ @single_die.probabilities.repeat_sum(@ndice)
140
+ end
141
+
142
+ @probabilities
143
+ end
144
+
145
+ # Simulates rolling the bunch of identical dice
146
+ # @return [Integer] Sum of all rolled dice, or sum of all keepers
147
+ def roll
148
+ generate_raw_results
149
+ return @result if !@keep_mode || @keep_number.to_i >= @ndice
150
+
151
+ use_dice = case @keep_mode
152
+ when :keep_best then @raw_result_details.sort[-@keep_number..]
153
+ when :keep_worst then @raw_result_details.sort[0..(@keep_number - 1)]
154
+ end
155
+
156
+ @result = use_dice.sum
157
+ end
158
+
159
+ # @!attribute [r] explain_result
160
+ # Explanation of result, or nil if no call to #roll yet.
161
+ # @return [String,nil]
162
+ def explain_result
163
+ return nil unless @result
164
+
165
+ # With #keep_mode, we may need to show unused and used dice separately
166
+ used_dice = result_details
167
+ used_dice, = find_used_dice_due_to_keep_mode(result_details) if @keep_mode && @keep_number < @ndice
168
+
169
+ build_explanation(used_dice)
170
+ end
171
+
172
+ private
173
+
174
+ def generate_raw_results
175
+ @result = 0
176
+ @raw_result_details = []
177
+
178
+ @ndice.times do
179
+ @result += @single_die.roll
180
+ @raw_result_details << @single_die.result
181
+ end
182
+ end
183
+
184
+ def name_number_sides_from_hash(options)
185
+ @name = options[:name].to_s
186
+ @ndice = Integer(options[:ndice])
187
+ raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice.positive?
188
+
189
+ @sides = Integer(options[:sides])
190
+ raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides.positive?
191
+ end
192
+
193
+ def complex_die_params_from_hash(options)
194
+ cd_hash = {}
195
+ %i[maps rerolls].each do |k|
196
+ cd_hash[k] = options[k].clone if options[k]
197
+ end
198
+ # We deliberately do not clone this object, it will often be intended that it is shared
199
+ cd_hash[:prng] = options[:prng]
200
+ cd_hash
201
+ end
202
+ end
203
+ end
@@ -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