nerd_dice 0.1.0 → 0.4.0

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