nerd_dice 0.1.0 → 0.4.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/main.yml +67 -0
  4. data/.rubocop.yml +114 -0
  5. data/CHANGELOG.md +76 -2
  6. data/Gemfile +2 -2
  7. data/Gemfile.lock +54 -32
  8. data/README.md +372 -5
  9. data/bin/generate_checksums +13 -0
  10. data/bin/nerd_dice_benchmark +322 -0
  11. data/certs/msducheminjr.pem +26 -0
  12. data/checksum/nerd_dice-0.1.0.gem.sha256 +1 -0
  13. data/checksum/nerd_dice-0.1.0.gem.sha512 +1 -0
  14. data/checksum/nerd_dice-0.1.1.gem.sha256 +1 -0
  15. data/checksum/nerd_dice-0.1.1.gem.sha512 +1 -0
  16. data/checksum/nerd_dice-0.2.0.gem.sha256 +1 -0
  17. data/checksum/nerd_dice-0.2.0.gem.sha512 +1 -0
  18. data/checksum/nerd_dice-0.3.0.gem.sha256 +1 -0
  19. data/checksum/nerd_dice-0.3.0.gem.sha512 +1 -0
  20. data/lib/nerd_dice/class_methods/configure.rb +50 -0
  21. data/lib/nerd_dice/class_methods/execute_die_roll.rb +47 -0
  22. data/lib/nerd_dice/class_methods/harvest_totals.rb +40 -0
  23. data/lib/nerd_dice/class_methods/refresh_seed.rb +83 -0
  24. data/lib/nerd_dice/class_methods/roll_ability_scores.rb +73 -0
  25. data/lib/nerd_dice/class_methods/roll_dice.rb +45 -0
  26. data/lib/nerd_dice/class_methods/total_ability_scores.rb +52 -0
  27. data/lib/nerd_dice/class_methods/total_dice.rb +44 -0
  28. data/lib/nerd_dice/class_methods.rb +30 -0
  29. data/lib/nerd_dice/configuration.rb +91 -0
  30. data/lib/nerd_dice/convenience_methods.rb +279 -0
  31. data/lib/nerd_dice/dice_set.rb +166 -0
  32. data/lib/nerd_dice/die.rb +51 -0
  33. data/lib/nerd_dice/sets_randomization_technique.rb +19 -0
  34. data/lib/nerd_dice/version.rb +1 -1
  35. data/lib/nerd_dice.rb +15 -33
  36. data/nerd_dice.gemspec +12 -7
  37. data.tar.gz.sig +0 -0
  38. metadata +97 -21
  39. metadata.gz.sig +0 -0
  40. data/.travis.yml +0 -6
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ ############################
4
+ # roll_dice class method
5
+ ############################
6
+ # Usage:
7
+ # If you wanted to roll a single d4, you would execute:
8
+ # <tt>dice_set = NerdDice.roll_dice(4)</tt>
9
+ #
10
+ # If you wanted to roll 3d6, you would execute
11
+ # <tt>dice_set = NerdDice.roll_dice(6, 3)</tt>
12
+ #
13
+ # If you wanted to roll a d20 and add 5 to the value, you would execute
14
+ # <tt>dice_set = NerdDice.roll_dice(20, 1, bonus: 5)</tt>
15
+ #
16
+ # Since this method returns a DiceSet, you can call any of the DiceSet
17
+ # methods on the result. See the README for more details and options
18
+ module NerdDice
19
+ class << self
20
+ ############################
21
+ # roll_dice class method
22
+ ############################
23
+ # Arguments:
24
+ # number_of_sides (Integer) => the number of sides of the dice to roll
25
+ # number_of_dice (Integer, DEFAULT: 1) => the quantity to roll of the type
26
+ # of die specified in the number_of_sides argument.
27
+ # opts (options Hash, DEFAULT: {}) any additional options you wish to include
28
+ # :bonus (Integer) => The total bonus (positive integer) or penalty
29
+ # (negative integer) to modify the total by. Is added to the total of
30
+ # all dice after they are totaled, not to each die rolled
31
+ # :randomization_technique (Symbol) => must be one of the symbols in
32
+ # RANDOMIZATION_TECHNIQUES or nil
33
+ # :foreground_color (String) => should resolve to a valid CSS color (format flexible)
34
+ # :background_color (String) => should resolve to a valid CSS color (format flexible)
35
+ # :damage_type: (String) => damage type for the dice set, if applicable
36
+ #
37
+ # Return (NerdDice::DiceSet) => Collection object with one or more Die objects
38
+ #
39
+ # You can call roll_dice().total to get similar functionality to total_dice
40
+ # or you can chain methods together roll_dice(6, 4, bonus: 3).with_advantage(3).total
41
+ def roll_dice(number_of_sides, number_of_dice = 1, **opts)
42
+ DiceSet.new(number_of_sides, number_of_dice, **opts)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ ############################
4
+ # total_ability_scores method
5
+ ############################
6
+ # Usage:
7
+ # If you wanted to get an array of only the values of ability scores and
8
+ # default configuration you would execute:
9
+ # <tt>ability_score_array = NerdDice.total_ability_scores</tt>
10
+ # => [15, 14, 13, 12, 10, 8]
11
+ #
12
+ # If you wanted to specify configuration for the current operation without
13
+ # modifying the NerdDice.configuration, you can supply options for both the
14
+ # ability_score configuration and the properties of the DiceSet objects returned.
15
+ #
16
+ # Many of the properties available in roll_ability_scores will not be relevant
17
+ # to total_ability_scores but the method delegates all options passed without
18
+ # filtering.
19
+ # <tt>
20
+ # ability_score_array = NerdDice.total_ability_scores(
21
+ # ability_score_array_size: 7,
22
+ # ability_score_number_of_sides: 8,
23
+ # ability_score_dice_rolled: 5,
24
+ # ability_score_dice_kept: 4,
25
+ # randomization_technique: :randomized
26
+ # )
27
+ # => [27, 22, 13, 23, 20, 24, 23]
28
+ # </tt>
29
+ module NerdDice
30
+ class << self
31
+ # Arguments:
32
+ # opts (options Hash, DEFAULT: {}) any options you wish to include
33
+ # ABILITY SCORE OPTIONS
34
+ # :ability_score_array_size DEFAULT NerdDice.configuration.ability_score_array_size
35
+ # :ability_score_number_of_sides DEFAULT NerdDice.configuration.ability_score_number_of_sides
36
+ # :ability_score_dice_rolled DEFAULT NerdDice.configuration.ability_score_dice_rolled
37
+ # :ability_score_dice_kept DEFAULT NerdDice.configuration.ability_score_dice_kept
38
+ #
39
+ # DICE SET OPTIONS
40
+ # :randomization_technique (Symbol) => must be one of the symbols in
41
+ # RANDOMIZATION_TECHNIQUES or nil
42
+ #
43
+ # ARGUMENTS PASSED ON THAT DO NOT REALLY MATTER
44
+ # :foreground_color (String) => should resolve to a valid CSS color (format flexible)
45
+ # :background_color (String) => should resolve to a valid CSS color (format flexible)
46
+ #
47
+ # Return (Array of Integers) => One Integer element for each ability score
48
+ def total_ability_scores(**opts)
49
+ harvest_totals(roll_ability_scores(**opts))
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ ############################
4
+ # total_dice class method
5
+ ############################
6
+ # Usage:
7
+ # If you wanted to roll a single d4, you would execute:
8
+ # <tt>NerdDice.total_dice(4)</tt>
9
+ #
10
+ # If you wanted to roll 3d6, you would execute
11
+ # <tt>NerdDice.total_dice(6, 3)</tt>
12
+ #
13
+ # If you wanted to roll a d20 and add 5 to the value, you would execute
14
+ # <tt>NerdDice.total_dice(20, 1, bonus: 5)</tt>
15
+ #
16
+ # The bonus in the options hash must be an Integer duck type or nil
17
+ module NerdDice
18
+ class << self
19
+ # Arguments:
20
+ # number_of_sides (Integer) => the number of sides of the dice to roll
21
+ # number_of_dice (Integer, DEFAULT: 1) => the quantity to roll of the type
22
+ # of die specified in the number_of_sides argument.
23
+ # opts (options Hash, DEFAULT: {}) any additional options you wish to include
24
+ # :bonus (Integer) => The total bonus (positive integer) or penalty
25
+ # (negative integer) to modify the total by. Is added to the total of
26
+ # all dice after they are totaled, not to each die rolled
27
+ # :randomization_technique (Symbol) => must be one of the symbols in
28
+ # RANDOMIZATION_TECHNIQUES or nil
29
+ #
30
+ # Return (Integer) => Total of the dice rolled, plus modifier if applicable
31
+ def total_dice(number_of_sides, number_of_dice = 1, **opts)
32
+ total = 0
33
+ number_of_dice.times do
34
+ total += execute_die_roll(number_of_sides, opts[:randomization_technique])
35
+ end
36
+ begin
37
+ total += opts[:bonus].to_i
38
+ rescue NoMethodError
39
+ raise ArgumentError, "Bonus must be a value that responds to :to_i"
40
+ end
41
+ total
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nerd_dice/class_methods/configure"
4
+ require "nerd_dice/class_methods/refresh_seed"
5
+ require "nerd_dice/class_methods/execute_die_roll"
6
+ require "nerd_dice/class_methods/total_dice"
7
+ require "nerd_dice/class_methods/roll_dice"
8
+ require "nerd_dice/class_methods/roll_ability_scores"
9
+ require "nerd_dice/class_methods/harvest_totals"
10
+ require "nerd_dice/class_methods/total_ability_scores"
11
+
12
+ ############################
13
+ # NerdDice class methods
14
+ # This file contains module-level attribute readers and writers and includes the other
15
+ # files that provide the class method functionality.
16
+ #
17
+ # PUBLIC METHODS
18
+ # NerdDice.configure => ./class_methods/configure.rb
19
+ # NerdDice.configuration => ./class_methods/configure.rb
20
+ # NerdDice.refresh_seed => ./class_methods/refresh_seed.rb
21
+ # NerdDice.execute_die_roll => ./class_methods/execute_die_roll.rb
22
+ # NerdDice.total_dice => ./class_methods/total_dice.rb
23
+ # NerdDice.roll_dice => ./class_methods/roll_dice.rb
24
+ # NerdDice.roll_ability_scores => ./class_methods/roll_ability_scores.rb
25
+ ############################
26
+ module NerdDice
27
+ class << self
28
+ attr_reader :count_since_last_refresh
29
+ end
30
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NerdDice
4
+ # The NerdDice::Configuration class allows you to configure and customize the
5
+ # options of the NerdDice gem to suit your specific needs. You can specify
6
+ # properties like the randomization technique used by the gem, the number of
7
+ # ability scores in an ability score array, etc. See the README for a list of
8
+ # configurable attributes.
9
+ #
10
+ # Usage:
11
+ # The configuration can either be set via a configure block:
12
+ # <tt>NerdDice.configure do |config|
13
+ # config.randomization_technique = :random_new_interval
14
+ # config.new_random_interval = 100
15
+ # end
16
+ # </tt>
17
+ #
18
+ # You can also set a particular property without a block using inline assignment
19
+ # <tt>NerdDice.configuration.randomization_technique = :random_new_once</tt>
20
+ class Configuration
21
+ attr_reader :randomization_technique, :refresh_seed_interval, :ability_score_array_size,
22
+ :ability_score_dice_rolled, :ability_score_number_of_sides, :ability_score_dice_kept
23
+ attr_accessor :die_background_color, :die_foreground_color
24
+
25
+ DEFAULT_BACKGROUND_COLOR = "#0000DD"
26
+ DEFAULT_FOREGROUND_COLOR = "#DDDDDD"
27
+
28
+ def randomization_technique=(value)
29
+ unless RANDOMIZATION_TECHNIQUES.include?(value)
30
+ raise NerdDice::Error, "randomization_technique must be one of #{RANDOMIZATION_TECHNIQUES.join(', ')}"
31
+ end
32
+
33
+ @randomization_technique = value
34
+ end
35
+
36
+ def refresh_seed_interval=(value)
37
+ unless value.nil?
38
+ value = value&.to_i
39
+ raise NerdDice::Error, "refresh_seed_interval must be a positive integer or nil" unless value.positive?
40
+ end
41
+ @refresh_seed_interval = value
42
+ end
43
+
44
+ def ability_score_array_size=(value)
45
+ @ability_score_array_size = ensure_positive_integer!("ability_score_array_size", value)
46
+ end
47
+
48
+ def ability_score_number_of_sides=(value)
49
+ @ability_score_number_of_sides = ensure_positive_integer!("ability_score_number_of_sides", value)
50
+ end
51
+
52
+ def ability_score_dice_rolled=(value)
53
+ @ability_score_dice_rolled = ensure_positive_integer!("ability_score_dice_rolled", value)
54
+ return unless ability_score_dice_kept > @ability_score_dice_rolled
55
+
56
+ warn "WARNING: ability_score_dice_rolled set to lower value than ability_score_dice_kept. " \
57
+ "Reducing ability_score_dice_kept from #{ability_score_dice_kept} to #{@ability_score_dice_rolled}"
58
+ self.ability_score_dice_kept = @ability_score_dice_rolled
59
+ end
60
+
61
+ def ability_score_dice_kept=(value)
62
+ compare_error_message = "cannot set ability_score_dice_kept greater than ability_score_dice_rolled"
63
+ new_value = ensure_positive_integer!("ability_score_dice_kept", value)
64
+ raise NerdDice::Error, compare_error_message if new_value > @ability_score_dice_rolled
65
+
66
+ @ability_score_dice_kept = new_value
67
+ end
68
+
69
+ private
70
+
71
+ def initialize
72
+ @ability_score_array_size = 6
73
+ @ability_score_number_of_sides = 6
74
+ @ability_score_dice_rolled = 4
75
+ @ability_score_dice_kept = 3
76
+ @randomization_technique = :random_object
77
+ @die_background_color = DEFAULT_BACKGROUND_COLOR
78
+ @die_foreground_color = DEFAULT_FOREGROUND_COLOR
79
+ end
80
+
81
+ def ensure_positive_integer!(attribute_name, argument_value)
82
+ error_message = "#{attribute_name} must be must be a positive value that responds to :to_i"
83
+ new_value = argument_value.to_i
84
+ raise ArgumentError, error_message unless new_value.positive?
85
+
86
+ new_value
87
+ rescue NoMethodError
88
+ raise ArgumentError, error_message
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NerdDice
4
+ # The NerdDice::ConvenienceMethods module overrides the default behavior of method_missing to
5
+ # provide the ability to dynamically roll dice with methods names that match specific patterns.
6
+ #
7
+ # Examples:
8
+ # `roll_dNN` and `total_dNN` roll one die
9
+ # <tt>
10
+ # roll_d20 # => DiceSet: NerdDice.roll_dice(20)
11
+ # roll_d8 # => DiceSet: NerdDice.roll_dice(8)
12
+ # roll_d1000 # => DiceSet: NerdDice.roll_dice(1000)
13
+ # total_d20 # => Integer NerdDice.total_dice(20)
14
+ # total_d8 # => Integer NerdDice.total_dice(8)
15
+ # total_d1000 # => Integer NerdDice.total_dice(1000)
16
+ # </tt>
17
+ #
18
+ # `roll_NNdNN` and `total_NNdNN` roll specified quantity of dice
19
+ # <tt>
20
+ # roll_2d20 # => DiceSet: NerdDice.roll_dice(20, 2)
21
+ # roll_3d8 # => DiceSet: NerdDice.roll_dice(8, 3)
22
+ # roll_22d1000 # => DiceSet: NerdDice.roll_dice(1000, 22)
23
+ # total_2d20 # => Integer NerdDice.total_dice(20, 2)
24
+ # total_3d8 # => Integer NerdDice.total_dice(8, 3)
25
+ # total_22d1000 # => Integer NerdDice.total_dice(1000, 22)
26
+ # </tt>
27
+ #
28
+ # Keyword arguments are passed on to `roll_dice`/`total_dice` method
29
+ # <tt>
30
+ # roll_2d20 foreground_color: 'blue' # => DiceSet: NerdDice.roll_dice(20, 2, foreground_color: 'blue')
31
+ # roll_2d20 foreground_color: 'blue' # => DiceSet: NerdDice.roll_dice(20, 2, foreground_color: 'blue')
32
+ # total_d12 randomization_technique: :randomized
33
+ # # => Integer NerdDice.total_dice(12, randomization_technique: :randomized)
34
+ # total_22d1000 randomization_technique: :random_rand
35
+ # # => Integer NerdDice.total_dice(1000, 22, randomization_technique: :random_rand)
36
+ # roll_4d6_with_advantage3 foreground_color: 'blue'
37
+ # # => DiceSet: NerdDice.roll_dice(4, 3, foreground_color: 'blue').highest(3)
38
+ # total_4d6_with_advantage3 randomization_technique: :random_rand
39
+ # # => Integer: NerdDice.roll_dice(4, 3, randomization_technique: :random_rand).highest(3).total
40
+ # </tt>
41
+ #
42
+ # Positive and negative bonuses can be used with `plus` (alias `p`) or `minus` (alias `m`) in DSL
43
+ # <tt>
44
+ # roll_d20_plus6 # => DiceSet: NerdDice.roll_dice(20, bonus: 6)
45
+ # total_3d8_p2 # => Integer: NerdDice.total_dice(8, 3, bonus: 2)
46
+ # total_d20_minus5 # => Integer: NerdDice.total_dice(20, bonus: -6)
47
+ # roll_3d8_m3 # => DiceSet: NerdDice.roll_dice(8, 3, bonus: -3)
48
+ # </tt>
49
+ #
50
+ # Advantage and disadvantage
51
+ # * `_with_advantageN` or `highestN` roll with advantage
52
+ # * `_with_disadvantageN` or `lowestN` roll with disadvantage
53
+ # * Calling `roll_dNN_with_advantage` \(and variants\) rolls 2 dice and keeps one
54
+ # <tt>
55
+ # # equivalent
56
+ # roll_3d8_with_advantage1
57
+ # roll_3d8_highest1
58
+ # # => DiceSet: NerdDice.roll_dice(8, 3).with_advantage(1)
59
+ # # calls roll_dice and total to return an integer
60
+ # total_3d8_with_advantage1
61
+ # total_3d8_highest1
62
+ # # => Integer: NerdDice.roll_dice(8, 3).with_advantage(1).total
63
+ # # rolls two dice in this case
64
+ # # equal to roll_2d20_with_advantage but more natural
65
+ # roll_d20_with_advantage # => DiceSet: NerdDice.roll_dice(20, 2).with_advantage(1)
66
+ # # equal to total_2d20_with_advantage but more natural
67
+ # total_d20_with_advantage # => Integer: NerdDice.roll_dice(20, 2).with_advantage(1).total
68
+ # </tt>
69
+ #
70
+ # Error Handling
71
+ # * If you try to call with a plus and a minus, an Exception is raised
72
+ # * If you call with a bonus and a keyword argument and they don't match, an Exception is raised
73
+ # * Any combination not expressly allowed or matched will call `super`
74
+ # <tt>
75
+ # roll_3d8_plus3_m2 # raise NerdDice::Error
76
+ # roll_3d8_plus3 bonus: 1 # raise NerdDice::Error
77
+ # roll_d20_with_advantage_lowest # will raise NameError using super method_missing
78
+ # total_4d6_lowest3_highest2 # will raise NameError using super method_missing
79
+ module ConvenienceMethods
80
+ DIS = /_(with_disadvantage|lowest)/.freeze
81
+ ADV = /_(with_advantage|highest)/.freeze
82
+ MOD = /(_p(lus)?\d+|_m(inus)?\d+)/.freeze
83
+ OVERALL_REGEXP = /\A(roll|total)_\d*d\d+((#{ADV}|#{DIS})\d*)?#{MOD}?\z/.freeze
84
+
85
+ # Override of method_missing
86
+ # * Attempts to match pattern to the regular expression matching the methods
87
+ # we want to intercept
88
+ # * If the method matches the pattern, it is defined and executed with send
89
+ # * Subsequent calls to the same method name will not hit method_missing
90
+ # * If the method name does not match the regular expression pattern, the default
91
+ # implementation of method_missing is called by invoking super
92
+ def method_missing(method_name, *args, **kwargs, &block)
93
+ # returns false if no match
94
+ if match_pattern_and_delegate(method_name, *args, **kwargs, &block)
95
+ # send the method after defining it
96
+ send(method_name, *args, **kwargs, &block)
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ # Override :respond_to_missing? so that :respond_to? works as expected when the
103
+ # module is included.
104
+ def respond_to_missing?(symbol, include_all)
105
+ symbol.to_s.match?(OVERALL_REGEXP) || super
106
+ end
107
+
108
+ private
109
+
110
+ # Compares the method name to the regular expression patterns for the module
111
+ # * If the pattern matches the convenience method is defined and a truthy value is returned
112
+ # * If the pattern does not match then the method returns false and method_missing invokes `super`
113
+ def match_pattern_and_delegate(method_name, *args, **kwargs, &block)
114
+ case method_name.to_s
115
+ when /\Aroll_\d*d\d+((#{ADV}|#{DIS})\d*)?#{MOD}?\z/o then define_roll_nndnn(method_name, *args, **kwargs,
116
+ &block)
117
+ when /\Atotal_\d*d\d+((#{ADV}|#{DIS})\d*)?#{MOD}?\z/o then define_total_nndnn(method_name, *args, **kwargs,
118
+ &block)
119
+ else
120
+ false
121
+ end
122
+ end
123
+
124
+ # * Evaluates the method name and then uses class_eval and define_method to define the method
125
+ # * Subsequent calls to the method will bypass method_missing
126
+ #
127
+ # Defined method will return a NerdDice::DiceSet
128
+ def define_roll_nndnn(method_name, *_args, **_kwargs)
129
+ sides, number_of_dice, number_to_keep = parse_from_method_name(method_name)
130
+ # defines the method on the class mixing in the module
131
+ (class << self; self; end).class_eval do
132
+ define_method method_name do |*_args, **kwargs|
133
+ modifier = get_modifier_from_method_name!(method_name, kwargs)
134
+ kwargs[:bonus] = modifier if modifier
135
+ # NerdDice::DiceSet object
136
+ dice = NerdDice.roll_dice(sides, number_of_dice, **kwargs)
137
+ # invoke highest or lowest on the DiceSet if applicable
138
+ parse_number_to_keep(dice, method_name, number_to_keep)
139
+ end
140
+ end
141
+ end
142
+
143
+ # The implementation of this is different than the roll_ version because NerdDice.total_dice
144
+ # cannot deal with highest/lowest calls
145
+ # * Evaluates the method name and then uses class_eval and define_method to define the method
146
+ # * Calls :determine_total_method to allow for handling of highest/lowest mechanic
147
+ # * Subsequent calls to the method will bypass method_missing
148
+ #
149
+ # Defined method will return an Integer
150
+ def define_total_nndnn(method_name, *_args, **_kwargs)
151
+ (class << self; self; end).class_eval do
152
+ define_method method_name do |*_args, **kwargs|
153
+ # parse out bonus before calling determine_total_method
154
+ modifier = get_modifier_from_method_name!(method_name, kwargs)
155
+ kwargs[:bonus] = modifier if modifier
156
+ # parse the method and take different actions based on method_name pattern
157
+ determine_total_method(method_name, kwargs)
158
+ end
159
+ end
160
+ end
161
+
162
+ # Determines whether to call NerdDice.total_dice or NerdDice.roll_dice.total based on RegEx pattern
163
+ # * If the method matches the ADV or DIS regular expressions, the method will call :roll_dice and :total
164
+ # * If the method does not match ADV or DIS, the method will call :total_dice (which is faster)
165
+ #
166
+ # Returns Integer irrespective of which methodology is used
167
+ def determine_total_method(method_name, kwargs)
168
+ sides, number_of_dice, number_to_keep = parse_from_method_name(method_name)
169
+ if number_to_keep
170
+ # NerdDice::DiceSet
171
+ dice = NerdDice.roll_dice(sides, number_of_dice, **kwargs)
172
+ # invoke the highest or lowest method to define dice included and then return the total
173
+ parse_number_to_keep(dice, method_name, number_to_keep).total
174
+ else
175
+ NerdDice.total_dice(sides, number_of_dice, **kwargs)
176
+ end
177
+ end
178
+
179
+ # calls a series of parse methods and then returns them as an array so the define_ methods above can
180
+ # use a one-liner to assign the variables
181
+ def parse_from_method_name(method_name)
182
+ sides = get_sides_from_method_name(method_name)
183
+ number_of_dice = get_number_of_dice_from_method_name(method_name)
184
+ number_to_keep = get_number_to_keep_from_method_name(method_name, number_of_dice)
185
+ [sides, number_of_dice, number_to_keep]
186
+ end
187
+
188
+ # parses out the Integer value of number of sides from the method name
189
+ # will only ever get called if the pattern matches so no need for guard against nil
190
+ def get_sides_from_method_name(method_name)
191
+ method_name.to_s.match(/d\d+/).to_s[1..].to_i
192
+ end
193
+
194
+ # parses the number of dice from the method name and returns the applicable Integer value
195
+ # * If number of dice are specified in the method name, that value is used
196
+ # * If number of dice are not specified and ADV or DIS match, the number is set to 2
197
+ # * If number of dice are not specified and no ADV or DIS match, the number is set to 1
198
+ def get_number_of_dice_from_method_name(method_name)
199
+ match_data = method_name.to_s.match(/_\d+d/)
200
+ default = method_name.to_s.match?(/_d\d+((#{ADV}|#{DIS})\d*)/o) ? 2 : 1
201
+ match_data ? match_data.to_s[1...-1].to_i : default
202
+ end
203
+
204
+ # parses out the modifier from the method name
205
+ # * Input must be a valid MatchData object matching the MOD regular expression
206
+ # * Does not handle nil
207
+ # * Only called from get_modifier_from_method_name!
208
+ def get_modifier_from_match_data(match_data)
209
+ if match_data.to_s.match?(/_p(lus)?\d+/)
210
+ match_data.to_s.match(/_p(lus)?\d+/).to_s.match(/\d+/).to_s.to_i
211
+ else
212
+ match_data.to_s.match(/_m(inus)?\d+/).to_s.match(/\d+/).to_s.to_i * -1
213
+ end
214
+ end
215
+
216
+ # Parses the method name to determine if the modifier pattern is present
217
+ # * Returns nil if method name does not match pattern
218
+ # * Calls get_modifier_from_match_data to get the Integer value of the modifier
219
+ # * Calls check_bonus_integrity! to ensure that the parsed modifier matches the
220
+ # keyword argument if present (which will raise an error if they don't match)
221
+ # * Returns the Integer value of the modifier
222
+ def get_modifier_from_method_name!(method_name, kwargs)
223
+ match_data = method_name.to_s.match(MOD)
224
+
225
+ return nil unless match_data
226
+
227
+ modifier = get_modifier_from_match_data(match_data)
228
+
229
+ # will raise error if mismatch
230
+ check_bonus_integrity!(kwargs, modifier)
231
+ modifier
232
+ end
233
+
234
+ # Determines whether the highest/lowest/with_advantage/with_disadvantage pattern is present in the
235
+ # method name and takes appropriate action
236
+ # * Returns nil if no match
237
+ # * Returns Integer value of number to keep otherwise (irrespective of highest/lowest)
238
+ # * Takes the number to keep from the method name if specified
239
+ # * Returns 1 if number to keep not specified and number of dice equals 1
240
+ # * Returns number_of_dice -1 if number to keep not specified and more than one die
241
+ def get_number_to_keep_from_method_name(method_name, number_of_dice)
242
+ return nil unless method_name.to_s.match?(/(#{ADV}|#{DIS})/o)
243
+
244
+ specified_number = method_name.to_s.match(/(#{ADV}|#{DIS})\d+/o)
245
+
246
+ # set the default to 1 if only one die or number minus 1 if multiple
247
+ default = number_of_dice == 1 ? 1 : number_of_dice - 1
248
+
249
+ # return pattern match if one exists or number of dice -1 if no pattern match
250
+ specified_number ? specified_number.to_s.match(/\d+/).to_s.to_i : default
251
+ end
252
+
253
+ # Checks that there is not a mismatch between the modifier specified in the method name and the
254
+ # modifier specified in the keyword arguments if one is specified
255
+ # * Raises a NerdDice::Error if there is a mismatch
256
+ # * Returns true if no keyword argument bonus
257
+ # * Returns true if keyword argument bonus and method name modifier are consistent
258
+ def check_bonus_integrity!(kwargs, bonus)
259
+ bonus_error_message = "Bonus integrity failure: "
260
+ bonus_error_message += "Modifier specified in keyword arguments was #{kwargs[:bonus]}. "
261
+ bonus_error_message += "Modifier specified in method_name was #{bonus}. "
262
+ raise NerdDice::Error, bonus_error_message if kwargs && kwargs[:bonus] && kwargs[:bonus].to_i != bonus
263
+
264
+ true
265
+ end
266
+
267
+ # Parses number to keep on a NerdDice::DiceSet
268
+ # * If number_to_keep falsey, just return the DiceSet object
269
+ # * If number_to_keep matches ADV pattern return DiceSet with highest called
270
+ # * If number_to_keep matches DIS pattern return DiceSet with lowest called
271
+ def parse_number_to_keep(dice, method_name, number_to_keep)
272
+ return dice unless number_to_keep
273
+
274
+ # use match against ADV to determine truth value of ternary expression
275
+ match_data = method_name.to_s.match(ADV)
276
+ match_data ? dice.highest(number_to_keep) : dice.lowest(number_to_keep)
277
+ end
278
+ end
279
+ end