nerd_dice 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "benchmark"
6
+ require "nerd_dice"
7
+
8
+ n = 50_000
9
+
10
+ RATIOS = {
11
+ total_dice_securerandom: 2.1,
12
+ total_dice_random_rand: 11.1,
13
+ total_dice_random_object: 13.0,
14
+ total_dice_randomized: 5.5,
15
+ total_dice_securerandom_3d6: 5.5,
16
+ total_dice_random_rand_3d6: 30.0,
17
+ total_dice_random_object_3d6: 25.5,
18
+ total_dice_randomized_3d6: 15.5,
19
+ roll_dice_securerandom: 4.0,
20
+ roll_dice_random_rand: 42.0,
21
+ roll_dice_random_object: 44.0,
22
+ roll_dice_randomized: 14.5,
23
+ roll_dice_securerandom_3d6: 13.0,
24
+ roll_dice_random_rand_3d6: 79.0,
25
+ roll_dice_random_object_3d6: 86.0,
26
+ roll_dice_randomized_3d6: 26.5,
27
+ roll_ability_scores_randomized: 26.5,
28
+ total_ability_scores_randomized: 26.5
29
+ }.freeze
30
+
31
+ def check_against_baseline!(baseline_value, test_value)
32
+ ratio = RATIOS[test_value.label.to_sym]
33
+ error_message = "Failed benchmark for #{test_value.label}. "
34
+ error_message += "Allowed ratio was #{ratio} actual ratio was #{test_value.real / baseline_value}"
35
+ raise NerdDice::Error, error_message if test_value.real > baseline_value * ratio
36
+ end
37
+
38
+ puts "Set baseline"
39
+ baselines = Benchmark.bmbm do |x|
40
+ # Random.rand()
41
+ x.report("Random.rand") do # standard rand()
42
+ n.times { Random.rand(1000) }
43
+ end
44
+
45
+ # SecureRandom.rand()
46
+ x.report("Sec.rand") do
47
+ n.times { SecureRandom.rand(1000) }
48
+ end
49
+ end
50
+
51
+ random_rand_baseline = baselines[0].real
52
+ securerandom_baseline = baselines[1].real
53
+
54
+ puts "Roll d1000s"
55
+ total_dice_d1000_results = Benchmark.bmbm do |x|
56
+ # NerdDice.total_dice securerandom
57
+ x.report("total_dice_securerandom") do
58
+ NerdDice.configuration.randomization_technique = :securerandom
59
+ n.times { NerdDice.total_dice(1000) }
60
+ end
61
+
62
+ x.report("total_dice_random_rand") do
63
+ NerdDice.configuration.randomization_technique = :random_rand
64
+ n.times { NerdDice.total_dice(1000) }
65
+ end
66
+
67
+ x.report("total_dice_random_object") do
68
+ NerdDice.configuration.randomization_technique = :random_object
69
+ n.times { NerdDice.total_dice(1000) }
70
+ end
71
+
72
+ x.report("total_dice_randomized") do
73
+ NerdDice.configuration.randomization_technique = :randomized
74
+ n.times { NerdDice.total_dice(1000) }
75
+ end
76
+ end
77
+
78
+ total_dice_securerandom = total_dice_d1000_results[0]
79
+ check_against_baseline! securerandom_baseline, total_dice_securerandom
80
+ total_dice_random_rand = total_dice_d1000_results[1]
81
+ check_against_baseline! random_rand_baseline, total_dice_random_rand
82
+ total_dice_random_object = total_dice_d1000_results[2]
83
+ check_against_baseline! random_rand_baseline, total_dice_random_object
84
+ total_dice_randomized = total_dice_d1000_results[3]
85
+ check_against_baseline! ((random_rand_baseline * 0.75) + (securerandom_baseline * 0.25)), total_dice_randomized
86
+
87
+ roll_dice_d1000_results = Benchmark.bmbm do |x|
88
+ # NerdDice.roll_dice securerandom
89
+ x.report("roll_dice_securerandom") do
90
+ NerdDice.configuration.randomization_technique = :securerandom
91
+ n.times { NerdDice.roll_dice(1000) }
92
+ end
93
+
94
+ x.report("roll_dice_random_rand") do
95
+ NerdDice.configuration.randomization_technique = :random_rand
96
+ n.times { NerdDice.roll_dice(1000) }
97
+ end
98
+
99
+ x.report("roll_dice_random_object") do
100
+ NerdDice.configuration.randomization_technique = :random_object
101
+ n.times { NerdDice.roll_dice(1000) }
102
+ end
103
+
104
+ x.report("roll_dice_randomized") do
105
+ NerdDice.configuration.randomization_technique = :randomized
106
+ n.times { NerdDice.roll_dice(1000) }
107
+ end
108
+ end
109
+
110
+ roll_dice_securerandom = roll_dice_d1000_results[0]
111
+ check_against_baseline! securerandom_baseline, roll_dice_securerandom
112
+ roll_dice_random_rand = roll_dice_d1000_results[1]
113
+ check_against_baseline! random_rand_baseline, roll_dice_random_rand
114
+ roll_dice_random_object = roll_dice_d1000_results[2]
115
+ check_against_baseline! random_rand_baseline, roll_dice_random_object
116
+ roll_dice_randomized = roll_dice_d1000_results[3]
117
+ check_against_baseline! ((random_rand_baseline * 0.75) + (securerandom_baseline * 0.25)), roll_dice_randomized
118
+
119
+ puts "Roll 3d6"
120
+ total_dice_3d6_results = Benchmark.bmbm do |x|
121
+ # NerdDice.total_dice securerandom
122
+ x.report("total_dice_securerandom_3d6") do
123
+ NerdDice.configuration.randomization_technique = :securerandom
124
+ n.times { NerdDice.total_dice(6, 3) }
125
+ end
126
+
127
+ x.report("total_dice_random_rand_3d6") do
128
+ NerdDice.configuration.randomization_technique = :random_rand
129
+ n.times { NerdDice.total_dice(6, 3) }
130
+ end
131
+
132
+ x.report("total_dice_random_object_3d6") do
133
+ NerdDice.configuration.randomization_technique = :random_object
134
+ n.times { NerdDice.total_dice(6, 3) }
135
+ end
136
+
137
+ x.report("total_dice_randomized_3d6") do
138
+ NerdDice.configuration.randomization_technique = :randomized
139
+ n.times { NerdDice.total_dice(6, 3) }
140
+ end
141
+ end
142
+
143
+ total_dice_3d6_securerandom = total_dice_3d6_results[0]
144
+ check_against_baseline! securerandom_baseline, total_dice_3d6_securerandom
145
+ total_dice_3d6_random_rand = total_dice_3d6_results[1]
146
+ check_against_baseline! random_rand_baseline, total_dice_3d6_random_rand
147
+ total_dice_3d6_random_object = total_dice_3d6_results[2]
148
+ check_against_baseline! random_rand_baseline, total_dice_3d6_random_object
149
+ total_dice_3d6_randomized = total_dice_3d6_results[3]
150
+ check_against_baseline! ((random_rand_baseline * 0.75) + (securerandom_baseline * 0.25)), total_dice_3d6_randomized
151
+
152
+ roll_dice_3d6_results = Benchmark.bmbm do |x|
153
+ # NerdDice.roll_dice securerandom
154
+ x.report("roll_dice_securerandom_3d6") do
155
+ NerdDice.configuration.randomization_technique = :securerandom
156
+ n.times { NerdDice.roll_dice(6, 3) }
157
+ end
158
+
159
+ x.report("roll_dice_random_rand_3d6") do
160
+ NerdDice.configuration.randomization_technique = :random_rand
161
+ n.times { NerdDice.roll_dice(6, 3) }
162
+ end
163
+
164
+ x.report("roll_dice_random_object_3d6") do
165
+ NerdDice.configuration.randomization_technique = :random_object
166
+ n.times { NerdDice.roll_dice(6, 3) }
167
+ end
168
+
169
+ x.report("roll_dice_randomized_3d6") do
170
+ NerdDice.configuration.randomization_technique = :randomized
171
+ n.times { NerdDice.roll_dice(6, 3) }
172
+ end
173
+ end
174
+
175
+ roll_dice_3d6_securerandom = roll_dice_3d6_results[0]
176
+ check_against_baseline! securerandom_baseline, roll_dice_3d6_securerandom
177
+ roll_dice_3d6_random_rand = roll_dice_3d6_results[1]
178
+ check_against_baseline! random_rand_baseline, roll_dice_3d6_random_rand
179
+ roll_dice_3d6_random_object = roll_dice_3d6_results[2]
180
+ check_against_baseline! random_rand_baseline, roll_dice_3d6_random_object
181
+ roll_dice_3d6_randomized = roll_dice_3d6_results[3]
182
+ check_against_baseline! ((random_rand_baseline * 0.75) + (securerandom_baseline * 0.25)), roll_dice_3d6_randomized
183
+
184
+ puts "Setting n down to 5,000 due to more intensive methods"
185
+ n = 5_000
186
+
187
+ puts "Roll and total ability scores"
188
+ roll_ability_scores_results = Benchmark.bmbm do |x|
189
+ x.report("roll_ability_scores_randomized") do
190
+ NerdDice.configuration.randomization_technique = :randomized
191
+ n.times { NerdDice.roll_ability_scores }
192
+ end
193
+
194
+ x.report("total_ability_scores_randomized") do
195
+ NerdDice.configuration.randomization_technique = :randomized
196
+ n.times { NerdDice.total_ability_scores }
197
+ end
198
+ end
199
+
200
+ roll_ability_scores_randomized = roll_ability_scores_results[0]
201
+ check_against_baseline! ((random_rand_baseline * 0.75) + (securerandom_baseline * 0.25)), roll_ability_scores_randomized
202
+ total_ability_scores_randomized = roll_ability_scores_results[1]
203
+ check_against_baseline! ((random_rand_baseline * 0.75) + (securerandom_baseline * 0.25)),
204
+ total_ability_scores_randomized
@@ -0,0 +1 @@
1
+ 0a2d4765e24c21fecf99229b0cc4fad806ca256d2187c45c442b25771af1b9cc
@@ -0,0 +1 @@
1
+ e345190f870eabd2a4fd6993f1a1d70d008ef8e6464767ba5697a46f7c4d6912c78a18039cec06c18dd6f0ee9f425453c078f780d36ccf65043ec3f7fba790a2
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ #############################
4
+ # Configuration class methods
5
+ #############################
6
+ # Usage:
7
+ # NerdDice.configure takes a block and yields the NerdDice::Configuration
8
+ # properties:
9
+ # If you wanted to configure several properties in a block:
10
+ # <tt>
11
+ # NerdDice.configure do |config|
12
+ # config.randomization_technique = :randomized
13
+ # config.die_background_color = "#FF0000"
14
+ # end
15
+ # </tt>
16
+ #
17
+ # NerdDice.configuration returns the NerdDice::Configuration object and lets you
18
+ # set properties on the NerdDice::Configuration object without using a block:
19
+ # <tt>
20
+ # config = NerdDice.configuration
21
+ # config.randomization_technique = :randomized
22
+ # config.die_background_color = "#FF0000"
23
+ # </tt>
24
+ module NerdDice
25
+ class << self
26
+ ############################
27
+ # configure class method
28
+ ############################
29
+ # Arguments: None
30
+ # Expects and yields to a block where configuration is specified.
31
+ # See README and NerdDice::Configuration class for config options
32
+ # Return (NerdDice::Configuration) the Configuration object tied to the
33
+ # @configuration class instance variable
34
+ def configure
35
+ yield configuration
36
+ configuration
37
+ end
38
+
39
+ ############################
40
+ # configuration class method
41
+ ############################
42
+ # Arguments: None
43
+ # Provides the lazy-loaded class instance variable @configuration
44
+ # Return (NerdDice::Configuration) the Configuration object tied to the
45
+ # @configuration class instance variable
46
+ def configuration
47
+ @configuration ||= Configuration.new
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ ###############################
4
+ # execute_die_roll class method
5
+ ###############################
6
+ # Usage:
7
+ # If you wanted to execute a single d4 die roll without a Die object, you would execute:
8
+ # <tt>NerdDice.execute_die_roll(4)</tt>
9
+ #
10
+ # If you wanted to execute a die roll with a different randomization technique
11
+ # than the one in NerdDice.configuration, you can supply an optional second argument
12
+ # <tt>NerdDice.execute_die_roll(4, :randomized)</tt>
13
+ module NerdDice
14
+ class << self
15
+ # Arguments:
16
+ # number_of_sides (Integer) => the number of sides of the die to roll
17
+ # using_generator (Symbol) => must be one of the symbols in
18
+ # RANDOMIZATION_TECHNIQUES or nil
19
+ #
20
+ # Return (Integer) => Value of the single die rolled
21
+ def execute_die_roll(number_of_sides, using_generator = nil)
22
+ @count_since_last_refresh ||= 0
23
+ gen = get_number_generator(using_generator)
24
+ result = gen.rand(number_of_sides) + 1
25
+ increment_and_evalutate_refresh_seed
26
+ result
27
+ end
28
+
29
+ private
30
+
31
+ def get_number_generator(using_generator = nil)
32
+ using_generator ||= configuration.randomization_technique
33
+ case using_generator
34
+ when :securerandom then SecureRandom
35
+ when :random_rand then Random
36
+ when :random_object then @random_object ||= Random.new
37
+ when :randomized then random_generator
38
+ else raise ArgumentError, "Unrecognized generator. Must be one of #{RANDOMIZATION_TECHNIQUES.join(', ')}"
39
+ end
40
+ end
41
+
42
+ def random_generator
43
+ gen = RANDOMIZATION_TECHNIQUES.reject { |el| el == :randomized }.sample
44
+ get_number_generator(gen)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ ############################
4
+ # harvest_totals method
5
+ ############################
6
+ # Usage:
7
+ # This method will take any collection of objects where each element responds to
8
+ # :total and return an array of the results of the total method.
9
+ #
10
+ # Example
11
+ # <tt>
12
+ # ability_score_array = NerdDice.roll_ability_scores
13
+ # => Array of 6 DiceSet objects
14
+ # totals_array = NerdDice.harvest_totals(totals_array)
15
+ # => [15, 14, 13, 12, 10, 8]
16
+ # # yes, it just happened to be the standard array by amazing coincidence
17
+ # </tt>
18
+ module NerdDice
19
+ class << self
20
+ # Arguments:
21
+ # collection (Enumerable) a collection where each element responds to total
22
+ #
23
+ # Return (Array) => Data type of each element will be whatever is returned by total method
24
+ def harvest_totals(collection)
25
+ collection.map(&:total)
26
+ rescue NoMethodError => e
27
+ specific_message =
28
+ case e.message
29
+ when /`total'/ then "Each element must respond to :total."
30
+ when /`map'/ then "Argument must respond to :map."
31
+ end
32
+ specific_message ? raise(ArgumentError, "You must provide a valid collection. #{specific_message}") : raise
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ ############################
4
+ # refresh_seed! class method
5
+ ############################
6
+ # Usage:
7
+ # NerdDice.refresh_seed! by default will refresh the seed for the generator
8
+ # configured in NerdDice.configuration. It can also be used with arguments
9
+ # to set a particular seed for use with deterministic testing. It sets
10
+ # the count_since_last_refresh to 0 whenever called.
11
+ #
12
+ # It cannot refresh or manipulate the seed for SecureRandom
13
+ #
14
+ # <tt>NerdDice.refresh_seed!</tt>
15
+ #
16
+ # With options
17
+ # <tt>
18
+ # previous_seed_data = NerdDice.refresh_seed!(
19
+ # randomization_technique: :randomized,
20
+ # random_rand_seed: 1337,
21
+ # random_object_seed: 24601
22
+ # )
23
+ # </tt>
24
+ module NerdDice
25
+ class << self
26
+ # Options: (none required)
27
+ # randomization_technique (Symbol) => must be one of the symbols in
28
+ # RANDOMIZATION_TECHNIQUES if specified
29
+ # random_rand_seed (Integer) => Seed to set for Random
30
+ # random_object_seed (Integer) => Seed to set for new Random object
31
+ # Return (Hash or nil) => Previous values of generator seeds that were refreshed
32
+ def refresh_seed!(**opts)
33
+ technique, random_rand_new_seed, random_object_new_seed = parse_refresh_options(opts)
34
+ @count_since_last_refresh = 0
35
+ return nil if technique == :securerandom
36
+
37
+ reset_appropriate_seeds!(technique, random_rand_new_seed, random_object_new_seed)
38
+ end
39
+
40
+ private
41
+
42
+ def parse_refresh_options(opts)
43
+ [
44
+ opts[:randomization_technique] || configuration.randomization_technique,
45
+ opts[:random_rand_seed],
46
+ opts[:random_object_seed]
47
+ ]
48
+ end
49
+
50
+ # rubocop:disable Metrics/MethodLength
51
+ def reset_appropriate_seeds!(technique, random_rand_new_seed, random_object_new_seed)
52
+ return_hash = {}
53
+ case technique
54
+ when :random_rand
55
+ return_hash[:random_rand_prior_seed] = refresh_random_rand_seed!(random_rand_new_seed)
56
+ when :random_object
57
+ return_hash[:random_object_prior_seed] = refresh_random_object_seed!(random_object_new_seed)
58
+ when :randomized
59
+ return_hash[:random_rand_prior_seed] = refresh_random_rand_seed!(random_rand_new_seed)
60
+ return_hash[:random_object_prior_seed] = refresh_random_object_seed!(random_object_new_seed)
61
+ end
62
+ return_hash
63
+ end
64
+ # rubocop:enable Metrics/MethodLength
65
+
66
+ def refresh_random_rand_seed!(new_seed)
67
+ new_seed ? Random.srand(new_seed) : Random.srand
68
+ end
69
+
70
+ def refresh_random_object_seed!(new_seed)
71
+ old_seed = @random_object&.seed
72
+ @random_object = new_seed ? Random.new(new_seed) : Random.new
73
+ old_seed
74
+ end
75
+
76
+ def increment_and_evalutate_refresh_seed
77
+ @count_since_last_refresh += 1
78
+ return unless configuration.refresh_seed_interval
79
+
80
+ refresh_seed! if @count_since_last_refresh >= configuration.refresh_seed_interval
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ ############################
4
+ # roll_ability_scores method
5
+ ############################
6
+ # Usage:
7
+ # If you wanted to get an array of DiceSet objects with your ability scores and
8
+ # default configuration you would execute:
9
+ # <tt>ability_score_array = NerdDice.roll_ability_scores</tt>
10
+ #
11
+ # If you wanted to specify configuration for the current operation without
12
+ # modifying the NerdDice.configuration, you can supply options for both the
13
+ # ability_score configuration and the properties of the DiceSet objects returned.
14
+ # Properties are specified in the method comment
15
+ # <tt>
16
+ # ability_score_array = NerdDice.roll_ability_scores(
17
+ # ability_score_array_size: 7,
18
+ # ability_score_number_of_sides: 8,
19
+ # ability_score_dice_rolled: 5,
20
+ # ability_score_dice_kept: 4,
21
+ # randomization_technique: :randomized,
22
+ # foreground_color: "#FF0000",
23
+ # background_color: "#FFFFFF"
24
+ # )
25
+ # </tt>
26
+ module NerdDice
27
+ class << self
28
+ # Arguments:
29
+ # opts (options Hash, DEFAULT: {}) any options you wish to include
30
+ # ABILITY SCORE OPTIONS
31
+ # :ability_score_array_size DEFAULT NerdDice.configuration.ability_score_array_size
32
+ # :ability_score_number_of_sides DEFAULT NerdDice.configuration.ability_score_number_of_sides
33
+ # :ability_score_dice_rolled DEFAULT NerdDice.configuration.ability_score_dice_rolled
34
+ # :ability_score_dice_kept DEFAULT NerdDice.configuration.ability_score_dice_kept
35
+ #
36
+ # DICE SET OPTIONS
37
+ # :randomization_technique (Symbol) => must be one of the symbols in
38
+ # RANDOMIZATION_TECHNIQUES or nil
39
+ # :foreground_color (String) => should resolve to a valid CSS color (format flexible)
40
+ # :background_color (String) => should resolve to a valid CSS color (format flexible)
41
+ #
42
+ # Return (Array of NerdDice::DiceSet) => One NerdDice::DiceSet element for each ability score
43
+ # rubocop:disable Metrics/MethodLength
44
+ def roll_ability_scores(**opts)
45
+ dice_opts = opts.reject { |key, _value| key.to_s.match?(/\Aability_score_[a-z_]+\z/) }
46
+ ability_score_options = interpret_ability_score_options(opts)
47
+ ability_score_array = []
48
+ ability_score_options[:ability_score_array_size].times do
49
+ ability_score_array << roll_dice(
50
+ ability_score_options[:ability_score_number_of_sides],
51
+ ability_score_options[:ability_score_dice_rolled],
52
+ **dice_opts
53
+ ).highest(
54
+ ability_score_options[:ability_score_dice_kept]
55
+ )
56
+ end
57
+ ability_score_array
58
+ end
59
+ # rubocop:enable Metrics/MethodLength
60
+
61
+ private
62
+
63
+ def interpret_ability_score_options(opts)
64
+ return_hash = {}
65
+ ABILITY_SCORE_KEYS.each { |key| return_hash[key] = parse_ability_score_option(opts, key) }
66
+ return_hash
67
+ end
68
+
69
+ def parse_ability_score_option(option_hash, option_key)
70
+ option_hash[option_key] || configuration.send(option_key.to_s)
71
+ end
72
+ end
73
+ end