games_dice 0.3.12 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +38 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +53 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +12 -0
  8. data/README.md +11 -33
  9. data/Rakefile +10 -23
  10. data/ext/games_dice/extconf.rb +4 -22
  11. data/ext/games_dice/games_dice.c +1 -1
  12. data/ext/games_dice/probabilities.c +128 -9
  13. data/ext/games_dice/probabilities.h +1 -1
  14. data/games_dice.gemspec +22 -36
  15. data/lib/games_dice/bunch.rb +203 -247
  16. data/lib/games_dice/bunch_helpers.rb +68 -0
  17. data/lib/games_dice/complex_die.rb +151 -270
  18. data/lib/games_dice/complex_die_helpers.rb +260 -60
  19. data/lib/games_dice/constants.rb +10 -10
  20. data/lib/games_dice/dice.rb +153 -143
  21. data/lib/games_dice/die.rb +101 -97
  22. data/lib/games_dice/die_result.rb +206 -189
  23. data/lib/games_dice/map_rule.rb +72 -70
  24. data/lib/games_dice/marshal.rb +41 -13
  25. data/lib/games_dice/parser.rb +245 -218
  26. data/lib/games_dice/reroll_rule.rb +76 -77
  27. data/lib/games_dice/version.rb +4 -1
  28. data/lib/games_dice.rb +23 -25
  29. data/spec/bunch_spec.rb +399 -420
  30. data/spec/complex_die_spec.rb +314 -305
  31. data/spec/dice_spec.rb +33 -34
  32. data/spec/die_result_spec.rb +174 -181
  33. data/spec/die_spec.rb +81 -81
  34. data/spec/helpers.rb +25 -25
  35. data/spec/map_rule_spec.rb +40 -44
  36. data/spec/parser_spec.rb +106 -82
  37. data/spec/probability_spec.rb +522 -526
  38. data/spec/readme_spec.rb +410 -390
  39. data/spec/reroll_rule_spec.rb +40 -44
  40. metadata +17 -129
  41. data/.travis.yml +0 -14
  42. data/lib/games_dice/prob_helpers.rb +0 -259
  43. data/lib/games_dice/probabilities.rb +0 -244
@@ -1,218 +1,245 @@
1
- require 'parslet'
2
-
3
- # Based on the parslet gem, this class defines the dice mini-language used by GamesDice.create
4
- #
5
- # An instance of this class is a parser for the language. There are no user-definable instance
6
- # variables.
7
- #
8
-
9
- class GamesDice::Parser < Parslet::Parser
10
-
11
- # Parslet rules that define the dice string grammar.
12
- rule(:integer) { match('[0-9]').repeat(1) }
13
- rule(:plus_minus_integer) { ( match('[+-]') >> integer ) | integer }
14
- rule(:range) { integer.as(:range_start) >> str('..') >> integer.as(:range_end) }
15
- rule(:dlabel) { match('[d]') }
16
- rule(:space) { match('\s').repeat(1) }
17
- rule(:space?) { space.maybe }
18
- rule(:underscore) { str('_').repeat(1) }
19
- rule(:underscore?) { space.maybe }
20
-
21
- rule(:bunch_start) { integer.as(:ndice) >> dlabel >> integer.as(:sides) }
22
-
23
- rule(:reroll_label) { match(['r']).as(:reroll) }
24
- rule(:keep_label) { match(['k']).as(:keep) }
25
- rule(:map_label) { match(['m']).as(:map) }
26
- rule(:alias_label) { match(['x']).as(:alias) }
27
-
28
- rule(:single_modifier) { alias_label }
29
- rule(:modifier_label) { reroll_label | keep_label | map_label }
30
- rule(:simple_modifier) { modifier_label >> integer.as(:simple_value) }
31
- rule(:comparison_op) { str('>=') | str('<=') | str('==') | str('>') | str('<') }
32
- rule(:ctl_string) { match('[a-z_]').repeat(1) }
33
- rule(:output_string) { match('[A-Za-z0-9_]').repeat(1) }
34
-
35
- rule(:opint_or_int) { (comparison_op.as(:comparison) >> integer.as(:compare_num)) | integer.as(:compare_num) }
36
- rule(:comma) { str(',') }
37
- rule(:stop) { str('.') }
38
-
39
- rule(:condition_only) { opint_or_int.as(:condition) }
40
- rule(:num_only) { integer.as(:num) }
41
-
42
-
43
- rule(:condition_and_type) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) }
44
- rule(:condition_and_num) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) }
45
-
46
- rule(:condition_type_and_num) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) >> comma >> integer.as(:num) }
47
- rule(:condition_num_and_output) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) >> comma >> output_string.as(:output) }
48
- rule(:num_and_type) { integer.as(:num) >> comma >> ctl_string.as(:type) }
49
-
50
- rule(:reroll_params) { condition_type_and_num | condition_and_type | condition_only }
51
- rule(:map_params) { condition_num_and_output | condition_and_num | condition_only }
52
- rule(:keeper_params) { num_and_type | num_only }
53
-
54
- rule(:full_reroll) { reroll_label >> str(':') >> reroll_params >> stop }
55
- rule(:full_map) { map_label >> str(':') >> map_params >> stop }
56
- rule(:full_keepers) { keep_label >> str(':') >> keeper_params >> stop }
57
-
58
- rule(:complex_modifier) { full_reroll | full_map | full_keepers }
59
-
60
- rule(:bunch_modifier) { complex_modifier | ( single_modifier >> stop.maybe ) | ( simple_modifier >> stop.maybe ) }
61
- rule(:bunch) { bunch_start >> bunch_modifier.repeat.as(:mods) }
62
-
63
- rule(:operator) { match('[+-]').as(:op) >> space? }
64
- rule(:add_bunch) { operator >> bunch >> space? }
65
- rule(:add_constant) { operator >> integer.as(:constant) >> space? }
66
- rule(:dice_expression) { add_bunch | add_constant }
67
- rule(:expressions) { dice_expression.repeat.as(:bunches) }
68
- root :expressions
69
-
70
- # Parses a string description in the dice mini-language, and returns data for feeding into
71
- # GamesDice::Dice constructor.
72
- # @param [String] dice_description Text to parse e.g. '1d6'
73
- # @return [Hash] Analysis of dice_description
74
- def parse dice_description
75
- dice_description = dice_description.to_s.strip
76
- # Force first item to start '+' for simpler parse rules
77
- dice_description = '+' + dice_description unless dice_description =~ /\A[+-]/
78
- dice_expressions = super( dice_description )
79
- { :bunches => collect_bunches( dice_expressions ), :offset => collect_offset( dice_expressions ) }
80
- end
81
-
82
- private
83
-
84
- def collect_bunches dice_expressions
85
- dice_expressions[:bunches].select {|h| h[:ndice] }.map do |in_hash|
86
- out_hash = {}
87
- # Convert integers
88
- [:ndice, :sides].each do |s|
89
- next unless in_hash[s]
90
- out_hash[s] = in_hash[s].to_i
91
- end
92
-
93
- # Multiplier
94
- if in_hash[:op]
95
- optype = in_hash[:op].to_s
96
- out_hash[:multiplier] = case optype
97
- when '+' then 1
98
- when '-' then -1
99
- end
100
- end
101
-
102
- # Modifiers
103
- if in_hash[:mods]
104
- in_hash[:mods].each do |mod|
105
- case
106
- when mod[:alias]
107
- collect_alias_modifier mod, out_hash
108
- when mod[:keep]
109
- collect_keeper_rule mod, out_hash
110
- when mod[:map]
111
- out_hash[:maps] ||= []
112
- collect_map_rule mod, out_hash
113
- when mod[:reroll]
114
- out_hash[:rerolls] ||= []
115
- collect_reroll_rule mod, out_hash
116
- end
117
- end
118
- end
119
-
120
- out_hash
121
- end
122
- end
123
-
124
- def collect_offset dice_expressions
125
- dice_expressions[:bunches].select {|h| h[:constant] }.inject(0) do |total, in_hash|
126
- c = in_hash[:constant].to_i
127
- optype = in_hash[:op].to_s
128
- if optype == '+'
129
- total += c
130
- else
131
- total -= c
132
- end
133
- total
134
- end
135
- end
136
-
137
- # Called when we have a single letter convenient alias for common dice adjustments
138
- def collect_alias_modifier alias_mod, out_hash
139
- alias_name = alias_mod[:alias].to_s
140
- case alias_name
141
- when 'x' # Exploding re-roll
142
- out_hash[:rerolls] ||= []
143
- out_hash[:rerolls] << [ out_hash[:sides], :==, :reroll_add ]
144
- end
145
- end
146
-
147
- # Called for any parsed reroll rule
148
- def collect_reroll_rule reroll_mod, out_hash
149
- out_hash[:rerolls] ||= []
150
- if reroll_mod[:simple_value]
151
- out_hash[:rerolls] << [ reroll_mod[:simple_value].to_i, :>=, :reroll_replace ]
152
- return
153
- end
154
-
155
- # Typical reroll_mod: {:reroll=>"r"@5, :condition=>{:compare_num=>"10"@7}, :type=>"add"@10}
156
- op = get_op_symbol( reroll_mod[:condition][:comparison] || '==' )
157
- v = reroll_mod[:condition][:compare_num].to_i
158
- type = ( 'reroll_' + ( reroll_mod[:type] || 'replace' ) ).to_sym
159
-
160
- if reroll_mod[:num]
161
- out_hash[:rerolls] << [ v, op, type, reroll_mod[:num].to_i ]
162
- else
163
- out_hash[:rerolls] << [ v, op, type ]
164
- end
165
- end
166
-
167
- # Called for any parsed keeper mode
168
- def collect_keeper_rule keeper_mod, out_hash
169
- raise "Cannot set keepers for a bunch twice" if out_hash[:keep_mode]
170
- if keeper_mod[:simple_value]
171
- out_hash[:keep_mode] = :keep_best
172
- out_hash[:keep_number] = keeper_mod[:simple_value].to_i
173
- return
174
- end
175
-
176
- # Typical keeper_mod: {:keep=>"k"@5, :num=>"1"@7, :type=>"worst"@9}
177
- out_hash[:keep_number] = keeper_mod[:num].to_i
178
- out_hash[:keep_mode] = ( 'keep_' + ( keeper_mod[:type] || 'best' ) ).to_sym
179
- end
180
-
181
- # Called for any parsed map mode
182
- def collect_map_rule map_mod, out_hash
183
- out_hash[:maps] ||= []
184
- if map_mod[:simple_value]
185
- out_hash[:maps] << [ map_mod[:simple_value].to_i, :<=, 1 ]
186
- return
187
- end
188
-
189
- # Typical map_mod: {:map=>"m"@4, :condition=>{:compare_num=>"5"@6}, :num=>"2"@8, :output=>"Qwerty"@10}
190
- op = get_op_symbol( map_mod[:condition][:comparison] || '>=' )
191
- v = map_mod[:condition][:compare_num].to_i
192
- out_val = 1
193
- if map_mod[:num]
194
- out_val = map_mod[:num].to_i
195
- end
196
-
197
- if map_mod[:output]
198
- out_hash[:maps] << [ v, op, out_val, map_mod[:output].to_s ]
199
- else
200
- out_hash[:maps] << [ v, op, out_val ]
201
- end
202
- end
203
-
204
- # The dice description language uses (r).op.x, whilst GamesDice::RerollRule uses x.op.(r), so
205
- # as well as converting to a symbol, we must reverse sense of input to constructor
206
- OP_CONVERSION = {
207
- '==' => :==,
208
- '>=' => :<=,
209
- '>' => :<,
210
- '<' => :>,
211
- '<=' => :>=,
212
- }
213
-
214
- def get_op_symbol parsed_op_string
215
- OP_CONVERSION[ parsed_op_string.to_s ]
216
- end
217
-
218
- end # class Parser
1
+ # frozen_string_literal: true
2
+
3
+ require 'parslet'
4
+
5
+ module GamesDice
6
+ # Based on the parslet gem, this class defines the dice mini-language used by GamesDice.create
7
+ #
8
+ # An instance of this class is a parser for the language. There are no user-definable instance
9
+ # variables.
10
+ #
11
+ class Parser < Parslet::Parser
12
+ # Parslet rules that define the dice string grammar.
13
+ rule(:integer) { match('[0-9]').repeat(1) }
14
+ rule(:plus_minus_integer) { (match('[+-]') >> integer) | integer }
15
+ rule(:range) { integer.as(:range_start) >> str('..') >> integer.as(:range_end) }
16
+ rule(:dlabel) { match('[d]') }
17
+ rule(:space) { match('\s').repeat(1) }
18
+ rule(:space?) { space.maybe }
19
+ rule(:underscore) { str('_').repeat(1) }
20
+ rule(:underscore?) { space.maybe }
21
+
22
+ rule(:bunch_start) { integer.as(:ndice) >> dlabel >> integer.as(:sides) }
23
+
24
+ rule(:reroll_label) { match(['r']).as(:reroll) }
25
+ rule(:keep_label) { match(['k']).as(:keep) }
26
+ rule(:map_label) { match(['m']).as(:map) }
27
+ rule(:alias_label) { match(['x']).as(:alias) }
28
+
29
+ rule(:single_modifier) { alias_label }
30
+ rule(:modifier_label) { reroll_label | keep_label | map_label }
31
+ rule(:simple_modifier) { modifier_label >> integer.as(:simple_value) }
32
+ rule(:comparison_op) { str('>=') | str('<=') | str('==') | str('>') | str('<') }
33
+ rule(:ctl_string) { match('[a-z_]').repeat(1) }
34
+ rule(:output_string) { match('[A-Za-z0-9_]').repeat(1) }
35
+
36
+ rule(:opint_or_int) { (comparison_op.as(:comparison) >> integer.as(:compare_num)) | integer.as(:compare_num) }
37
+ rule(:comma) { str(',') }
38
+ rule(:stop) { str('.') }
39
+
40
+ rule(:condition_only) { opint_or_int.as(:condition) }
41
+ rule(:num_only) { integer.as(:num) }
42
+
43
+ rule(:condition_and_type) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) }
44
+ rule(:condition_and_num) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) }
45
+
46
+ rule(:condition_type_and_num) do
47
+ opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) >> comma >> integer.as(:num)
48
+ end
49
+
50
+ rule(:condition_num_and_output) do
51
+ opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) >> comma >> output_string.as(:output)
52
+ end
53
+
54
+ rule(:num_and_type) { integer.as(:num) >> comma >> ctl_string.as(:type) }
55
+
56
+ rule(:reroll_params) { condition_type_and_num | condition_and_type | condition_only }
57
+ rule(:map_params) { condition_num_and_output | condition_and_num | condition_only }
58
+ rule(:keeper_params) { num_and_type | num_only }
59
+
60
+ rule(:full_reroll) { reroll_label >> str(':') >> reroll_params >> stop }
61
+ rule(:full_map) { map_label >> str(':') >> map_params >> stop }
62
+ rule(:full_keepers) { keep_label >> str(':') >> keeper_params >> stop }
63
+
64
+ rule(:complex_modifier) { full_reroll | full_map | full_keepers }
65
+
66
+ rule(:bunch_modifier) { complex_modifier | (single_modifier >> stop.maybe) | (simple_modifier >> stop.maybe) }
67
+ rule(:bunch) { bunch_start >> bunch_modifier.repeat.as(:mods) }
68
+
69
+ rule(:operator) { match('[+-]').as(:op) >> space? }
70
+ rule(:add_bunch) { operator >> bunch >> space? }
71
+ rule(:add_constant) { operator >> integer.as(:constant) >> space? }
72
+ rule(:dice_expression) { add_bunch | add_constant }
73
+ rule(:expressions) { dice_expression.repeat.as(:bunches) }
74
+ root :expressions
75
+
76
+ # Parses a string description in the dice mini-language, and returns data for feeding into
77
+ # GamesDice::Dice constructor.
78
+ # @param [String] dice_description Text to parse e.g. '1d6'
79
+ # @return [Hash] Analysis of dice_description
80
+ def parse(dice_description)
81
+ dice_description = dice_description.to_s.strip
82
+ # Force first item to start '+' for simpler parse rules
83
+ dice_description = "+#{dice_description}" unless /\A[+-]/.match?(dice_description)
84
+ dice_expressions = super
85
+ {
86
+ bunches: ParseTreeProcessor.collect_bunches(dice_expressions),
87
+ offset: ParseTreeProcessor.collect_offset(dice_expressions)
88
+ }
89
+ end
90
+
91
+ # Class converts parse tree to GamesDice hash model
92
+ # @!visibility private
93
+ class ParseTreeProcessor
94
+ class << self
95
+ def collect_bunches(dice_expressions)
96
+ dice_expressions[:bunches].select { |h| h[:ndice] }.map do |in_hash|
97
+ out_hash = {}
98
+ collect_bunch_basics(in_hash, out_hash)
99
+ collect_bunch_multiplier(in_hash, out_hash) if in_hash[:op]
100
+
101
+ in_hash[:mods]&.each do |mod|
102
+ collect_bunch_modifier(mod, out_hash)
103
+ end
104
+
105
+ out_hash
106
+ end
107
+ end
108
+
109
+ def collect_bunch_basics(in_hash, out_hash)
110
+ %i[ndice sides].each do |s|
111
+ next unless in_hash[s]
112
+
113
+ out_hash[s] = in_hash[s].to_i
114
+ end
115
+ end
116
+
117
+ def collect_bunch_multiplier(in_hash, out_hash)
118
+ optype = in_hash[:op].to_s
119
+ out_hash[:multiplier] = case optype
120
+ when '+' then 1
121
+ when '-' then -1
122
+ end
123
+ end
124
+
125
+ def collect_bunch_modifier(mod, out_hash)
126
+ if mod[:alias]
127
+ ParseTreeBunchModifier.collect_alias_modifier mod, out_hash
128
+ elsif mod[:keep]
129
+ ParseTreeBunchModifier.collect_keeper_rule mod, out_hash
130
+ elsif mod[:map]
131
+ ParseTreeBunchModifier.collect_map_rule mod, out_hash
132
+ elsif mod[:reroll]
133
+ ParseTreeBunchModifier.collect_reroll_rule mod, out_hash
134
+ end
135
+ end
136
+
137
+ def collect_offset(dice_expressions)
138
+ dice_expressions[:bunches].select { |h| h[:constant] }.inject(0) do |total, in_hash|
139
+ c = in_hash[:constant].to_i
140
+ optype = in_hash[:op].to_s
141
+ if optype == '+'
142
+ total + c
143
+ else
144
+ total - c
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ # Class for collating bunch data into bunch construction hash
152
+ # @!visibility private
153
+ class ParseTreeBunchModifier
154
+ class << self
155
+ # Called when we have a single letter convenient alias for common dice adjustments
156
+ def collect_alias_modifier(alias_mod, out_hash)
157
+ alias_name = alias_mod[:alias].to_s
158
+ case alias_name
159
+ when 'x' # Exploding re-roll
160
+ out_hash[:rerolls] ||= []
161
+ out_hash[:rerolls] << [out_hash[:sides], :==, :reroll_add]
162
+ end
163
+ end
164
+
165
+ # Called for any parsed reroll rule
166
+ def collect_reroll_rule(reroll_mod, out_hash)
167
+ out_hash[:rerolls] ||= []
168
+ if reroll_mod[:simple_value]
169
+ out_hash[:rerolls] << [reroll_mod[:simple_value].to_i, :>=, :reroll_replace]
170
+ return
171
+ end
172
+
173
+ collect_complex_reroll_rule(reroll_mod, out_hash)
174
+ end
175
+
176
+ def collect_complex_reroll_rule(reroll_mod, out_hash)
177
+ # Typical reroll_mod: {:reroll=>"r"@5, :condition=>{:compare_num=>"10"@7}, :type=>"add"@10}
178
+ op = get_op_symbol(reroll_mod[:condition][:comparison] || '==')
179
+ v = reroll_mod[:condition][:compare_num].to_i
180
+ type = :"reroll_#{reroll_mod[:type] || 'replace'}"
181
+
182
+ out_hash[:rerolls] << if reroll_mod[:num]
183
+ [v, op, type, reroll_mod[:num].to_i]
184
+ else
185
+ [v, op, type]
186
+ end
187
+ end
188
+
189
+ # Called for any parsed keeper mode
190
+ def collect_keeper_rule(keeper_mod, out_hash)
191
+ raise 'Cannot set keepers for a bunch twice' if out_hash[:keep_mode]
192
+
193
+ if keeper_mod[:simple_value]
194
+ out_hash[:keep_mode] = :keep_best
195
+ out_hash[:keep_number] = keeper_mod[:simple_value].to_i
196
+ return
197
+ end
198
+
199
+ # Typical keeper_mod: {:keep=>"k"@5, :num=>"1"@7, :type=>"worst"@9}
200
+ out_hash[:keep_number] = keeper_mod[:num].to_i
201
+ out_hash[:keep_mode] = :"keep_#{keeper_mod[:type] || 'best'}"
202
+ end
203
+
204
+ # Called for any parsed map mode
205
+ def collect_map_rule(map_mod, out_hash)
206
+ out_hash[:maps] ||= []
207
+ if map_mod[:simple_value]
208
+ out_hash[:maps] << [map_mod[:simple_value].to_i, :<=, 1]
209
+ return
210
+ end
211
+
212
+ collect_complex_map_rule(map_mod, out_hash)
213
+ end
214
+
215
+ def collect_complex_map_rule(map_mod, out_hash)
216
+ # Typical map_mod: {:map=>"m"@4, :condition=>{:compare_num=>"5"@6}, :num=>"2"@8, :output=>"Qwerty"@10}
217
+ op = get_op_symbol(map_mod[:condition][:comparison] || '>=')
218
+ v = map_mod[:condition][:compare_num].to_i
219
+ out_val = 1
220
+ out_val = map_mod[:num].to_i if map_mod[:num]
221
+
222
+ out_hash[:maps] << if map_mod[:output]
223
+ [v, op, out_val, map_mod[:output].to_s]
224
+ else
225
+ [v, op, out_val]
226
+ end
227
+ end
228
+
229
+ # The dice description language uses (r).op.x, whilst GamesDice::RerollRule uses x.op.(r), so
230
+ # as well as converting to a symbol, we must reverse sense of input to constructor
231
+ OP_CONVERSION = {
232
+ '==' => :==,
233
+ '>=' => :<=,
234
+ '>' => :<,
235
+ '<' => :>,
236
+ '<=' => :>=
237
+ }.freeze
238
+
239
+ def get_op_symbol(parsed_op_string)
240
+ OP_CONVERSION[parsed_op_string.to_s]
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -1,77 +1,76 @@
1
- # This class models a variety of game rules that cause dice to be re-rolled.
2
- #
3
- # An object of the class represents a single rule, such as "re-roll a result of 1
4
- # and use the new value".
5
- #
6
- # @example A rule for "exploding" dice
7
- # rule = GamesDice::RerollRule.new( 6, :<=, :reroll_add )
8
- # # Test whether the rule applies . . .
9
- # rule.applies? 4 # => false
10
- # rule.applies? 6 # => true
11
- # rule.type # => :reroll_add
12
- #
13
- # @example A rule for re-rolling and taking best value if first attempt is lower than a threshold
14
- # rule = GamesDice::RerollRule.new( 11, :>, :reroll_use_best, 1 )
15
- # # Test whether the rule applies . . .
16
- # rule.applies? 4 # => true
17
- # rule.applies? 14 # => false
18
- # rule.type # => :reroll_use_best
19
- #
20
-
21
- class GamesDice::RerollRule
22
-
23
- # Creates new instance of GamesDice::RerollRule. The rule will be assessed as
24
- # trigger_value.send( trigger_op, x )
25
- # where x is the Integer value shown on a die.
26
- # @param [Integer,Range<Integer>,Object] trigger_value Any object is allowed, but typically an Integer
27
- # @param [Symbol] trigger_op A method of trigger_value that takes an Integer param and returns Boolean
28
- # @param [Symbol] type The type of reroll
29
- # @param [Integer] limit Maximum number of times this rule can be applied to a single die
30
- # @return [GamesDice::RerollRule]
31
- def initialize trigger_value, trigger_op, type, limit = 1000
32
-
33
- if ! trigger_value.respond_to?( trigger_op )
34
- raise ArgumentError, "trigger_value #{trigger_value.inspect} cannot respond to trigger_op #{trigger_value.inspect}"
35
- end
36
-
37
- unless GamesDice::REROLL_TYPES.has_key?(type)
38
- raise ArgumentError, "Unrecognised reason for a re-roll #{type}"
39
- end
40
-
41
- @trigger_value = trigger_value
42
- @trigger_op = trigger_op
43
- @type = type
44
- @limit = limit ? Integer( limit ) : 1000
45
- @limit = 1 if @type == :reroll_subtract
46
- end
47
-
48
- # Trigger operation. How the rule is assessed against #trigger_value.
49
- # @return [Symbol] Method name to be sent to #trigger_value
50
- attr_reader :trigger_op
51
-
52
- # Trigger value. An object that will use #trigger_op to assess a die result for a reroll.
53
- # @return [Integer,Range,Object] Object that receives (#trigger_op, die_result)
54
- attr_reader :trigger_value
55
-
56
- # The reroll behaviour that this rule triggers.
57
- # @return [Symbol] A category for the re-roll, declares how it should be processed
58
- # The following values are supported:
59
- # +:reroll_add+:: add result of reroll to running total, and ignore :reroll_subtract for this die
60
- # +reroll_subtract+:: subtract result of reroll from running total, and reverse sense of any further :reroll_add results
61
- # +:reroll_replace+:: use the new value in place of existing value for the die
62
- # +:reroll_use_best+:: use the new value if it is higher than the existing value
63
- # +:reroll_use_worst+:: use the new value if it is higher than the existing value
64
- attr_reader :type
65
-
66
- # Maximum to number of times that this rule can be applied to a single die.
67
- # @return [Integer] A number of rolls.
68
- attr_reader :limit
69
-
70
- # Assesses the rule against a die result value.
71
- # @param [Integer] test_value Value that is result of rolling a single die.
72
- # @return [Boolean] Whether the rule applies.
73
- def applies? test_value
74
- @trigger_value.send( @trigger_op, test_value ) ? true : false
75
- end
76
-
77
- end # class RerollRule
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ # This class models a variety of game rules that cause dice to be re-rolled.
5
+ #
6
+ # An object of the class represents a single rule, such as "re-roll a result of 1
7
+ # and use the new value".
8
+ #
9
+ # @example A rule for "exploding" dice
10
+ # rule = GamesDice::RerollRule.new( 6, :<=, :reroll_add )
11
+ # # Test whether the rule applies . . .
12
+ # rule.applies? 4 # => false
13
+ # rule.applies? 6 # => true
14
+ # rule.type # => :reroll_add
15
+ #
16
+ # @example A rule for re-rolling and taking best value if first attempt is lower than a threshold
17
+ # rule = GamesDice::RerollRule.new( 11, :>, :reroll_use_best, 1 )
18
+ # # Test whether the rule applies . . .
19
+ # rule.applies? 4 # => true
20
+ # rule.applies? 14 # => false
21
+ # rule.type # => :reroll_use_best
22
+ #
23
+ class RerollRule
24
+ # Creates new instance of GamesDice::RerollRule. The rule will be assessed as
25
+ # trigger_value.send( trigger_op, x )
26
+ # where x is the Integer value shown on a die.
27
+ # @param [Integer,Range<Integer>,Object] trigger_value Any object is allowed, but typically an Integer
28
+ # @param [Symbol] trigger_op A method of trigger_value that takes an Integer param and returns Boolean
29
+ # @param [Symbol] type The type of reroll
30
+ # @param [Integer] limit Maximum number of times this rule can be applied to a single die
31
+ # @return [GamesDice::RerollRule]
32
+ def initialize(trigger_value, trigger_op, type, limit = 1000)
33
+ unless trigger_value.respond_to?(trigger_op)
34
+ raise ArgumentError,
35
+ "trigger_value #{trigger_value.inspect} cannot respond to trigger_op #{trigger_value.inspect}"
36
+ end
37
+
38
+ raise ArgumentError, "Unrecognised reason for a re-roll #{type}" unless GamesDice::REROLL_TYPES.key?(type)
39
+
40
+ @trigger_value = trigger_value
41
+ @trigger_op = trigger_op
42
+ @type = type
43
+ @limit = limit ? Integer(limit) : 1000
44
+ @limit = 1 if @type == :reroll_subtract
45
+ end
46
+
47
+ # Trigger operation. How the rule is assessed against #trigger_value.
48
+ # @return [Symbol] Method name to be sent to #trigger_value
49
+ attr_reader :trigger_op
50
+
51
+ # Trigger value. An object that will use #trigger_op to assess a die result for a reroll.
52
+ # @return [Integer,Range,Object] Object that receives (#trigger_op, die_result)
53
+ attr_reader :trigger_value
54
+
55
+ # The reroll behaviour that this rule triggers.
56
+ # @return [Symbol] A category for the re-roll, declares how it should be processed
57
+ # The following values are supported:
58
+ # +:reroll_add+:: add result of reroll to running total, and ignore :reroll_subtract for this die
59
+ # +reroll_subtract+:: subtract result of reroll from running total, reverse sense of any further :reroll_add results
60
+ # +:reroll_replace+:: use the new value in place of existing value for the die
61
+ # +:reroll_use_best+:: use the new value if it is higher than the existing value
62
+ # +:reroll_use_worst+:: use the new value if it is higher than the existing value
63
+ attr_reader :type
64
+
65
+ # Maximum to number of times that this rule can be applied to a single die.
66
+ # @return [Integer] A number of rolls.
67
+ attr_reader :limit
68
+
69
+ # Assesses the rule against a die result value.
70
+ # @param [Integer] test_value Value that is result of rolling a single die.
71
+ # @return [Boolean] Whether the rule applies.
72
+ def applies?(test_value)
73
+ @trigger_value.send(@trigger_op, test_value) ? true : false
74
+ end
75
+ end
76
+ end