games_dice 0.3.12 → 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.
@@ -1,70 +1,72 @@
1
- # This class models rules that convert numbers shown on a die to values used in a game. A
2
- # common use for this is to count "successes" - dice that score a certain number or higher.
3
- #
4
- # An object of the class represents a single rule, such as "count a die result of 5 or more as 1
5
- # _success_".
6
- #
7
- # @example A rule for counting successes
8
- # rule = GamesDice::MapRule.new( 6, :<=, 1, 'Success' )
9
- # # Test how the rule applies . . .
10
- # rule.map_from 4 # => nil
11
- # rule.map_from 6 # => 1
12
- #
13
- # @example A rule for counting "fumbles" which reduce total successes
14
- # rule = GamesDice::MapRule.new( 1, :==, -1, 'Fumble' )
15
- # # Test how the rule applies . . .
16
- # rule.map_from 7 # => nil
17
- # rule.map_from 1 # => -1
18
- #
19
-
20
- class GamesDice::MapRule
21
-
22
- # Creates new instance of GamesDice::MapRule. The rule will be assessed as
23
- # trigger_value.send( trigger_op, x )
24
- # where x is the Integer value shown on a die.
25
- # @param [Integer,Range<Integer>,Object] trigger_value Any object is allowed, but typically an Integer
26
- # @param [Symbol] trigger_op A method of trigger_value that takes an Integer param and returns Boolean
27
- # @param [Integer] mapped_value The value to use in place of the trigger value
28
- # @param [String] mapped_name Name of mapped value, for use in descriptions
29
- # @return [GamesDice::MapRule]
30
- def initialize trigger_value, trigger_op, mapped_value=0, mapped_name=''
31
-
32
- if ! trigger_value.respond_to?( trigger_op )
33
- raise ArgumentError, "trigger_value #{trigger_value.inspect} cannot respond to trigger_op #{trigger_value.inspect}"
34
- end
35
-
36
- @trigger_value = trigger_value
37
- @trigger_op = trigger_op
38
- raise TypeError if ! mapped_value.is_a? Numeric
39
- @mapped_value = Integer(mapped_value)
40
- @mapped_name = mapped_name.to_s
41
- end
42
-
43
- # Trigger operation. How the rule is assessed against #trigger_value.
44
- # @return [Symbol] Method name to be sent to #trigger_value
45
- attr_reader :trigger_op
46
-
47
- # Trigger value. An object that will use #trigger_op to assess a die result for a reroll.
48
- # @return [Integer,Range,Object] Object that receives (#trigger_op, die_result)
49
- attr_reader :trigger_value
50
-
51
- # Value that a die will use after the value has been mapped.
52
- # @return [Integer]
53
- attr_reader :mapped_value
54
-
55
- # Name for mapped value, used in explanations.
56
- # @return [String]
57
- attr_reader :mapped_name
58
-
59
- # Assesses the rule against a die result value.
60
- # @param [Integer] test_value Value that is result of rolling a single die.
61
- # @return [Integer,nil] Replacement value, or nil if this rule doesn't apply
62
- def map_from test_value
63
- op_result = @trigger_value.send( @trigger_op, test_value )
64
- return nil unless op_result
65
- if op_result == true
66
- return @mapped_value
67
- end
68
- return op_result
69
- end
70
- end # class MapRule
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ # This class models rules that convert numbers shown on a die to values used in a game. A
5
+ # common use for this is to count "successes" - dice that score a certain number or higher.
6
+ #
7
+ # An object of the class represents a single rule, such as "count a die result of 5 or more as 1
8
+ # _success_".
9
+ #
10
+ # @example A rule for counting successes
11
+ # rule = GamesDice::MapRule.new( 6, :<=, 1, 'Success' )
12
+ # # Test how the rule applies . . .
13
+ # rule.map_from 4 # => nil
14
+ # rule.map_from 6 # => 1
15
+ #
16
+ # @example A rule for counting "fumbles" which reduce total successes
17
+ # rule = GamesDice::MapRule.new( 1, :==, -1, 'Fumble' )
18
+ # # Test how the rule applies . . .
19
+ # rule.map_from 7 # => nil
20
+ # rule.map_from 1 # => -1
21
+ #
22
+ class MapRule
23
+ # Creates new instance of GamesDice::MapRule. 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 [Integer] mapped_value The value to use in place of the trigger value
29
+ # @param [String] mapped_name Name of mapped value, for use in descriptions
30
+ # @return [GamesDice::MapRule]
31
+ def initialize(trigger_value, trigger_op, mapped_value = 0, mapped_name = '')
32
+ unless trigger_value.respond_to?(trigger_op)
33
+ raise ArgumentError,
34
+ "trigger_value #{trigger_value.inspect} cannot respond to trigger_op #{trigger_value.inspect}"
35
+ end
36
+
37
+ @trigger_value = trigger_value
38
+ @trigger_op = trigger_op
39
+ raise TypeError unless mapped_value.is_a? Numeric
40
+
41
+ @mapped_value = Integer(mapped_value)
42
+ @mapped_name = mapped_name.to_s
43
+ end
44
+
45
+ # Trigger operation. How the rule is assessed against #trigger_value.
46
+ # @return [Symbol] Method name to be sent to #trigger_value
47
+ attr_reader :trigger_op
48
+
49
+ # Trigger value. An object that will use #trigger_op to assess a die result for a reroll.
50
+ # @return [Integer,Range,Object] Object that receives (#trigger_op, die_result)
51
+ attr_reader :trigger_value
52
+
53
+ # Value that a die will use after the value has been mapped.
54
+ # @return [Integer]
55
+ attr_reader :mapped_value
56
+
57
+ # Name for mapped value, used in explanations.
58
+ # @return [String]
59
+ attr_reader :mapped_name
60
+
61
+ # Assesses the rule against a die result value.
62
+ # @param [Integer] test_value Value that is result of rolling a single die.
63
+ # @return [Integer,nil] Replacement value, or nil if this rule doesn't apply
64
+ def map_from(test_value)
65
+ op_result = @trigger_value.send(@trigger_op, test_value)
66
+ return nil unless op_result
67
+ return @mapped_value if op_result == true
68
+
69
+ op_result
70
+ end
71
+ end
72
+ end
@@ -1,13 +1,18 @@
1
- class GamesDice::Probabilities
2
- # @!visibility private
3
- # Adds support for Marshal, via to_h and from_h methods
4
- def _dump *ignored
5
- Marshal.dump to_h
6
- end
7
-
8
- # @!visibility private
9
- def self._load buf
10
- h = Marshal.load buf
11
- from_h h
12
- end
13
- end
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ # Probability calculations
5
+ class Probabilities
6
+ # @!visibility private
7
+ # Adds support for Marshal, via to_h and from_h methods
8
+ def _dump(*_ignored)
9
+ Marshal.dump to_h
10
+ end
11
+
12
+ # @!visibility private
13
+ def self._load(buf)
14
+ h = Marshal.load buf
15
+ from_h h
16
+ end
17
+ end
18
+ end
@@ -1,218 +1,219 @@
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
+ rule(:condition_num_and_output) do
50
+ opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) >> comma >> output_string.as(:output)
51
+ end
52
+ rule(:num_and_type) { integer.as(:num) >> comma >> ctl_string.as(:type) }
53
+
54
+ rule(:reroll_params) { condition_type_and_num | condition_and_type | condition_only }
55
+ rule(:map_params) { condition_num_and_output | condition_and_num | condition_only }
56
+ rule(:keeper_params) { num_and_type | num_only }
57
+
58
+ rule(:full_reroll) { reroll_label >> str(':') >> reroll_params >> stop }
59
+ rule(:full_map) { map_label >> str(':') >> map_params >> stop }
60
+ rule(:full_keepers) { keep_label >> str(':') >> keeper_params >> stop }
61
+
62
+ rule(:complex_modifier) { full_reroll | full_map | full_keepers }
63
+
64
+ rule(:bunch_modifier) { complex_modifier | (single_modifier >> stop.maybe) | (simple_modifier >> stop.maybe) }
65
+ rule(:bunch) { bunch_start >> bunch_modifier.repeat.as(:mods) }
66
+
67
+ rule(:operator) { match('[+-]').as(:op) >> space? }
68
+ rule(:add_bunch) { operator >> bunch >> space? }
69
+ rule(:add_constant) { operator >> integer.as(:constant) >> space? }
70
+ rule(:dice_expression) { add_bunch | add_constant }
71
+ rule(:expressions) { dice_expression.repeat.as(:bunches) }
72
+ root :expressions
73
+
74
+ # Parses a string description in the dice mini-language, and returns data for feeding into
75
+ # GamesDice::Dice constructor.
76
+ # @param [String] dice_description Text to parse e.g. '1d6'
77
+ # @return [Hash] Analysis of dice_description
78
+ def parse(dice_description)
79
+ dice_description = dice_description.to_s.strip
80
+ # Force first item to start '+' for simpler parse rules
81
+ dice_description = "+#{dice_description}" unless dice_description =~ /\A[+-]/
82
+ dice_expressions = super(dice_description)
83
+ { bunches: collect_bunches(dice_expressions), offset: collect_offset(dice_expressions) }
84
+ end
85
+
86
+ private
87
+
88
+ def collect_bunches(dice_expressions)
89
+ dice_expressions[:bunches].select { |h| h[:ndice] }.map do |in_hash|
90
+ out_hash = {}
91
+ # Convert integers
92
+ %i[ndice sides].each do |s|
93
+ next unless in_hash[s]
94
+
95
+ out_hash[s] = in_hash[s].to_i
96
+ end
97
+
98
+ # Multiplier
99
+ if in_hash[:op]
100
+ optype = in_hash[:op].to_s
101
+ out_hash[:multiplier] = case optype
102
+ when '+' then 1
103
+ when '-' then -1
104
+ end
105
+ end
106
+
107
+ # Modifiers
108
+ in_hash[:mods]&.each do |mod|
109
+ if mod[:alias]
110
+ collect_alias_modifier mod, out_hash
111
+ elsif mod[:keep]
112
+ collect_keeper_rule mod, out_hash
113
+ elsif mod[:map]
114
+ out_hash[:maps] ||= []
115
+ collect_map_rule mod, out_hash
116
+ elsif mod[:reroll]
117
+ out_hash[:rerolls] ||= []
118
+ collect_reroll_rule mod, out_hash
119
+ end
120
+ end
121
+
122
+ out_hash
123
+ end
124
+ end
125
+
126
+ def collect_offset(dice_expressions)
127
+ dice_expressions[:bunches].select { |h| h[:constant] }.inject(0) do |total, in_hash|
128
+ c = in_hash[:constant].to_i
129
+ optype = in_hash[:op].to_s
130
+ if optype == '+'
131
+ total += c
132
+ else
133
+ total -= c
134
+ end
135
+ total
136
+ end
137
+ end
138
+
139
+ # Called when we have a single letter convenient alias for common dice adjustments
140
+ def collect_alias_modifier(alias_mod, out_hash)
141
+ alias_name = alias_mod[:alias].to_s
142
+ case alias_name
143
+ when 'x' # Exploding re-roll
144
+ out_hash[:rerolls] ||= []
145
+ out_hash[:rerolls] << [out_hash[:sides], :==, :reroll_add]
146
+ end
147
+ end
148
+
149
+ # Called for any parsed reroll rule
150
+ def collect_reroll_rule(reroll_mod, out_hash)
151
+ out_hash[:rerolls] ||= []
152
+ if reroll_mod[:simple_value]
153
+ out_hash[:rerolls] << [reroll_mod[:simple_value].to_i, :>=, :reroll_replace]
154
+ return
155
+ end
156
+
157
+ # Typical reroll_mod: {:reroll=>"r"@5, :condition=>{:compare_num=>"10"@7}, :type=>"add"@10}
158
+ op = get_op_symbol(reroll_mod[:condition][:comparison] || '==')
159
+ v = reroll_mod[:condition][:compare_num].to_i
160
+ type = "reroll_#{reroll_mod[:type] || 'replace'}".to_sym
161
+
162
+ out_hash[:rerolls] << if reroll_mod[:num]
163
+ [v, op, type, reroll_mod[:num].to_i]
164
+ else
165
+ [v, op, type]
166
+ end
167
+ end
168
+
169
+ # Called for any parsed keeper mode
170
+ def collect_keeper_rule(keeper_mod, out_hash)
171
+ raise 'Cannot set keepers for a bunch twice' if out_hash[:keep_mode]
172
+
173
+ if keeper_mod[:simple_value]
174
+ out_hash[:keep_mode] = :keep_best
175
+ out_hash[:keep_number] = keeper_mod[:simple_value].to_i
176
+ return
177
+ end
178
+
179
+ # Typical keeper_mod: {:keep=>"k"@5, :num=>"1"@7, :type=>"worst"@9}
180
+ out_hash[:keep_number] = keeper_mod[:num].to_i
181
+ out_hash[:keep_mode] = "keep_#{keeper_mod[:type] || 'best'}".to_sym
182
+ end
183
+
184
+ # Called for any parsed map mode
185
+ def collect_map_rule(map_mod, out_hash)
186
+ out_hash[:maps] ||= []
187
+ if map_mod[:simple_value]
188
+ out_hash[:maps] << [map_mod[:simple_value].to_i, :<=, 1]
189
+ return
190
+ end
191
+
192
+ # Typical map_mod: {:map=>"m"@4, :condition=>{:compare_num=>"5"@6}, :num=>"2"@8, :output=>"Qwerty"@10}
193
+ op = get_op_symbol(map_mod[:condition][:comparison] || '>=')
194
+ v = map_mod[:condition][:compare_num].to_i
195
+ out_val = 1
196
+ out_val = map_mod[:num].to_i if map_mod[:num]
197
+
198
+ out_hash[:maps] << if map_mod[:output]
199
+ [v, op, out_val, map_mod[:output].to_s]
200
+ else
201
+ [v, op, out_val]
202
+ end
203
+ end
204
+
205
+ # The dice description language uses (r).op.x, whilst GamesDice::RerollRule uses x.op.(r), so
206
+ # as well as converting to a symbol, we must reverse sense of input to constructor
207
+ OP_CONVERSION = {
208
+ '==' => :==,
209
+ '>=' => :<=,
210
+ '>' => :<,
211
+ '<' => :>,
212
+ '<=' => :>=
213
+ }.freeze
214
+
215
+ def get_op_symbol(parsed_op_string)
216
+ OP_CONVERSION[parsed_op_string.to_s]
217
+ end
218
+ end
219
+ end