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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/main.yml +67 -0
- data/.rubocop.yml +114 -0
- data/CHANGELOG.md +76 -2
- data/Gemfile +2 -2
- data/Gemfile.lock +54 -32
- data/README.md +372 -5
- data/bin/generate_checksums +13 -0
- data/bin/nerd_dice_benchmark +322 -0
- data/certs/msducheminjr.pem +26 -0
- data/checksum/nerd_dice-0.1.0.gem.sha256 +1 -0
- data/checksum/nerd_dice-0.1.0.gem.sha512 +1 -0
- data/checksum/nerd_dice-0.1.1.gem.sha256 +1 -0
- data/checksum/nerd_dice-0.1.1.gem.sha512 +1 -0
- data/checksum/nerd_dice-0.2.0.gem.sha256 +1 -0
- data/checksum/nerd_dice-0.2.0.gem.sha512 +1 -0
- data/checksum/nerd_dice-0.3.0.gem.sha256 +1 -0
- data/checksum/nerd_dice-0.3.0.gem.sha512 +1 -0
- data/lib/nerd_dice/class_methods/configure.rb +50 -0
- data/lib/nerd_dice/class_methods/execute_die_roll.rb +47 -0
- data/lib/nerd_dice/class_methods/harvest_totals.rb +40 -0
- data/lib/nerd_dice/class_methods/refresh_seed.rb +83 -0
- data/lib/nerd_dice/class_methods/roll_ability_scores.rb +73 -0
- data/lib/nerd_dice/class_methods/roll_dice.rb +45 -0
- data/lib/nerd_dice/class_methods/total_ability_scores.rb +52 -0
- data/lib/nerd_dice/class_methods/total_dice.rb +44 -0
- data/lib/nerd_dice/class_methods.rb +30 -0
- data/lib/nerd_dice/configuration.rb +91 -0
- data/lib/nerd_dice/convenience_methods.rb +279 -0
- data/lib/nerd_dice/dice_set.rb +166 -0
- data/lib/nerd_dice/die.rb +51 -0
- data/lib/nerd_dice/sets_randomization_technique.rb +19 -0
- data/lib/nerd_dice/version.rb +1 -1
- data/lib/nerd_dice.rb +15 -33
- data/nerd_dice.gemspec +12 -7
- data.tar.gz.sig +0 -0
- metadata +97 -21
- metadata.gz.sig +0 -0
- 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
|