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.
- 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
|