games_dice 0.3.12 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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