lib/games_dice/parser.rb in games_dice-0.3.12 vs lib/games_dice/parser.rb in games_dice-0.4.0
- old
+ new
@@ -1,218 +1,219 @@
-require 'parslet'
-
-# Based on the parslet gem, this class defines the dice mini-language used by GamesDice.create
-#
-# An instance of this class is a parser for the language. There are no user-definable instance
-# variables.
-#
-
-class GamesDice::Parser < Parslet::Parser
-
- # Parslet rules that define the dice string grammar.
- rule(:integer) { match('[0-9]').repeat(1) }
- rule(:plus_minus_integer) { ( match('[+-]') >> integer ) | integer }
- rule(:range) { integer.as(:range_start) >> str('..') >> integer.as(:range_end) }
- rule(:dlabel) { match('[d]') }
- rule(:space) { match('\s').repeat(1) }
- rule(:space?) { space.maybe }
- rule(:underscore) { str('_').repeat(1) }
- rule(:underscore?) { space.maybe }
-
- rule(:bunch_start) { integer.as(:ndice) >> dlabel >> integer.as(:sides) }
-
- rule(:reroll_label) { match(['r']).as(:reroll) }
- rule(:keep_label) { match(['k']).as(:keep) }
- rule(:map_label) { match(['m']).as(:map) }
- rule(:alias_label) { match(['x']).as(:alias) }
-
- rule(:single_modifier) { alias_label }
- rule(:modifier_label) { reroll_label | keep_label | map_label }
- rule(:simple_modifier) { modifier_label >> integer.as(:simple_value) }
- rule(:comparison_op) { str('>=') | str('<=') | str('==') | str('>') | str('<') }
- rule(:ctl_string) { match('[a-z_]').repeat(1) }
- rule(:output_string) { match('[A-Za-z0-9_]').repeat(1) }
-
- rule(:opint_or_int) { (comparison_op.as(:comparison) >> integer.as(:compare_num)) | integer.as(:compare_num) }
- rule(:comma) { str(',') }
- rule(:stop) { str('.') }
-
- rule(:condition_only) { opint_or_int.as(:condition) }
- rule(:num_only) { integer.as(:num) }
-
-
- rule(:condition_and_type) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) }
- rule(:condition_and_num) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) }
-
- rule(:condition_type_and_num) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) >> comma >> integer.as(:num) }
- rule(:condition_num_and_output) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) >> comma >> output_string.as(:output) }
- rule(:num_and_type) { integer.as(:num) >> comma >> ctl_string.as(:type) }
-
- rule(:reroll_params) { condition_type_and_num | condition_and_type | condition_only }
- rule(:map_params) { condition_num_and_output | condition_and_num | condition_only }
- rule(:keeper_params) { num_and_type | num_only }
-
- rule(:full_reroll) { reroll_label >> str(':') >> reroll_params >> stop }
- rule(:full_map) { map_label >> str(':') >> map_params >> stop }
- rule(:full_keepers) { keep_label >> str(':') >> keeper_params >> stop }
-
- rule(:complex_modifier) { full_reroll | full_map | full_keepers }
-
- rule(:bunch_modifier) { complex_modifier | ( single_modifier >> stop.maybe ) | ( simple_modifier >> stop.maybe ) }
- rule(:bunch) { bunch_start >> bunch_modifier.repeat.as(:mods) }
-
- rule(:operator) { match('[+-]').as(:op) >> space? }
- rule(:add_bunch) { operator >> bunch >> space? }
- rule(:add_constant) { operator >> integer.as(:constant) >> space? }
- rule(:dice_expression) { add_bunch | add_constant }
- rule(:expressions) { dice_expression.repeat.as(:bunches) }
- root :expressions
-
- # Parses a string description in the dice mini-language, and returns data for feeding into
- # GamesDice::Dice constructor.
- # @param [String] dice_description Text to parse e.g. '1d6'
- # @return [Hash] Analysis of dice_description
- def parse dice_description
- dice_description = dice_description.to_s.strip
- # Force first item to start '+' for simpler parse rules
- dice_description = '+' + dice_description unless dice_description =~ /\A[+-]/
- dice_expressions = super( dice_description )
- { :bunches => collect_bunches( dice_expressions ), :offset => collect_offset( dice_expressions ) }
- end
-
- private
-
- def collect_bunches dice_expressions
- dice_expressions[:bunches].select {|h| h[:ndice] }.map do |in_hash|
- out_hash = {}
- # Convert integers
- [:ndice, :sides].each do |s|
- next unless in_hash[s]
- out_hash[s] = in_hash[s].to_i
- end
-
- # Multiplier
- if in_hash[:op]
- optype = in_hash[:op].to_s
- out_hash[:multiplier] = case optype
- when '+' then 1
- when '-' then -1
- end
- end
-
- # Modifiers
- if in_hash[:mods]
- in_hash[:mods].each do |mod|
- case
- when mod[:alias]
- collect_alias_modifier mod, out_hash
- when mod[:keep]
- collect_keeper_rule mod, out_hash
- when mod[:map]
- out_hash[:maps] ||= []
- collect_map_rule mod, out_hash
- when mod[:reroll]
- out_hash[:rerolls] ||= []
- collect_reroll_rule mod, out_hash
- end
- end
- end
-
- out_hash
- end
- end
-
- def collect_offset dice_expressions
- dice_expressions[:bunches].select {|h| h[:constant] }.inject(0) do |total, in_hash|
- c = in_hash[:constant].to_i
- optype = in_hash[:op].to_s
- if optype == '+'
- total += c
- else
- total -= c
- end
- total
- end
- end
-
- # Called when we have a single letter convenient alias for common dice adjustments
- def collect_alias_modifier alias_mod, out_hash
- alias_name = alias_mod[:alias].to_s
- case alias_name
- when 'x' # Exploding re-roll
- out_hash[:rerolls] ||= []
- out_hash[:rerolls] << [ out_hash[:sides], :==, :reroll_add ]
- end
- end
-
- # Called for any parsed reroll rule
- def collect_reroll_rule reroll_mod, out_hash
- out_hash[:rerolls] ||= []
- if reroll_mod[:simple_value]
- out_hash[:rerolls] << [ reroll_mod[:simple_value].to_i, :>=, :reroll_replace ]
- return
- end
-
- # Typical reroll_mod: {:reroll=>"r"@5, :condition=>{:compare_num=>"10"@7}, :type=>"add"@10}
- op = get_op_symbol( reroll_mod[:condition][:comparison] || '==' )
- v = reroll_mod[:condition][:compare_num].to_i
- type = ( 'reroll_' + ( reroll_mod[:type] || 'replace' ) ).to_sym
-
- if reroll_mod[:num]
- out_hash[:rerolls] << [ v, op, type, reroll_mod[:num].to_i ]
- else
- out_hash[:rerolls] << [ v, op, type ]
- end
- end
-
- # Called for any parsed keeper mode
- def collect_keeper_rule keeper_mod, out_hash
- raise "Cannot set keepers for a bunch twice" if out_hash[:keep_mode]
- if keeper_mod[:simple_value]
- out_hash[:keep_mode] = :keep_best
- out_hash[:keep_number] = keeper_mod[:simple_value].to_i
- return
- end
-
- # Typical keeper_mod: {:keep=>"k"@5, :num=>"1"@7, :type=>"worst"@9}
- out_hash[:keep_number] = keeper_mod[:num].to_i
- out_hash[:keep_mode] = ( 'keep_' + ( keeper_mod[:type] || 'best' ) ).to_sym
- end
-
- # Called for any parsed map mode
- def collect_map_rule map_mod, out_hash
- out_hash[:maps] ||= []
- if map_mod[:simple_value]
- out_hash[:maps] << [ map_mod[:simple_value].to_i, :<=, 1 ]
- return
- end
-
- # Typical map_mod: {:map=>"m"@4, :condition=>{:compare_num=>"5"@6}, :num=>"2"@8, :output=>"Qwerty"@10}
- op = get_op_symbol( map_mod[:condition][:comparison] || '>=' )
- v = map_mod[:condition][:compare_num].to_i
- out_val = 1
- if map_mod[:num]
- out_val = map_mod[:num].to_i
- end
-
- if map_mod[:output]
- out_hash[:maps] << [ v, op, out_val, map_mod[:output].to_s ]
- else
- out_hash[:maps] << [ v, op, out_val ]
- end
- end
-
- # The dice description language uses (r).op.x, whilst GamesDice::RerollRule uses x.op.(r), so
- # as well as converting to a symbol, we must reverse sense of input to constructor
- OP_CONVERSION = {
- '==' => :==,
- '>=' => :<=,
- '>' => :<,
- '<' => :>,
- '<=' => :>=,
- }
-
- def get_op_symbol parsed_op_string
- OP_CONVERSION[ parsed_op_string.to_s ]
- end
-
-end # class Parser
+# frozen_string_literal: true
+
+require 'parslet'
+
+module GamesDice
+ # Based on the parslet gem, this class defines the dice mini-language used by GamesDice.create
+ #
+ # An instance of this class is a parser for the language. There are no user-definable instance
+ # variables.
+ #
+ class Parser < Parslet::Parser
+ # Parslet rules that define the dice string grammar.
+ rule(:integer) { match('[0-9]').repeat(1) }
+ rule(:plus_minus_integer) { (match('[+-]') >> integer) | integer }
+ rule(:range) { integer.as(:range_start) >> str('..') >> integer.as(:range_end) }
+ rule(:dlabel) { match('[d]') }
+ rule(:space) { match('\s').repeat(1) }
+ rule(:space?) { space.maybe }
+ rule(:underscore) { str('_').repeat(1) }
+ rule(:underscore?) { space.maybe }
+
+ rule(:bunch_start) { integer.as(:ndice) >> dlabel >> integer.as(:sides) }
+
+ rule(:reroll_label) { match(['r']).as(:reroll) }
+ rule(:keep_label) { match(['k']).as(:keep) }
+ rule(:map_label) { match(['m']).as(:map) }
+ rule(:alias_label) { match(['x']).as(:alias) }
+
+ rule(:single_modifier) { alias_label }
+ rule(:modifier_label) { reroll_label | keep_label | map_label }
+ rule(:simple_modifier) { modifier_label >> integer.as(:simple_value) }
+ rule(:comparison_op) { str('>=') | str('<=') | str('==') | str('>') | str('<') }
+ rule(:ctl_string) { match('[a-z_]').repeat(1) }
+ rule(:output_string) { match('[A-Za-z0-9_]').repeat(1) }
+
+ rule(:opint_or_int) { (comparison_op.as(:comparison) >> integer.as(:compare_num)) | integer.as(:compare_num) }
+ rule(:comma) { str(',') }
+ rule(:stop) { str('.') }
+
+ rule(:condition_only) { opint_or_int.as(:condition) }
+ rule(:num_only) { integer.as(:num) }
+
+ rule(:condition_and_type) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) }
+ rule(:condition_and_num) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) }
+
+ rule(:condition_type_and_num) do
+ opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) >> comma >> integer.as(:num)
+ end
+ rule(:condition_num_and_output) do
+ opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) >> comma >> output_string.as(:output)
+ end
+ rule(:num_and_type) { integer.as(:num) >> comma >> ctl_string.as(:type) }
+
+ rule(:reroll_params) { condition_type_and_num | condition_and_type | condition_only }
+ rule(:map_params) { condition_num_and_output | condition_and_num | condition_only }
+ rule(:keeper_params) { num_and_type | num_only }
+
+ rule(:full_reroll) { reroll_label >> str(':') >> reroll_params >> stop }
+ rule(:full_map) { map_label >> str(':') >> map_params >> stop }
+ rule(:full_keepers) { keep_label >> str(':') >> keeper_params >> stop }
+
+ rule(:complex_modifier) { full_reroll | full_map | full_keepers }
+
+ rule(:bunch_modifier) { complex_modifier | (single_modifier >> stop.maybe) | (simple_modifier >> stop.maybe) }
+ rule(:bunch) { bunch_start >> bunch_modifier.repeat.as(:mods) }
+
+ rule(:operator) { match('[+-]').as(:op) >> space? }
+ rule(:add_bunch) { operator >> bunch >> space? }
+ rule(:add_constant) { operator >> integer.as(:constant) >> space? }
+ rule(:dice_expression) { add_bunch | add_constant }
+ rule(:expressions) { dice_expression.repeat.as(:bunches) }
+ root :expressions
+
+ # Parses a string description in the dice mini-language, and returns data for feeding into
+ # GamesDice::Dice constructor.
+ # @param [String] dice_description Text to parse e.g. '1d6'
+ # @return [Hash] Analysis of dice_description
+ def parse(dice_description)
+ dice_description = dice_description.to_s.strip
+ # Force first item to start '+' for simpler parse rules
+ dice_description = "+#{dice_description}" unless dice_description =~ /\A[+-]/
+ dice_expressions = super(dice_description)
+ { bunches: collect_bunches(dice_expressions), offset: collect_offset(dice_expressions) }
+ end
+
+ private
+
+ def collect_bunches(dice_expressions)
+ dice_expressions[:bunches].select { |h| h[:ndice] }.map do |in_hash|
+ out_hash = {}
+ # Convert integers
+ %i[ndice sides].each do |s|
+ next unless in_hash[s]
+
+ out_hash[s] = in_hash[s].to_i
+ end
+
+ # Multiplier
+ if in_hash[:op]
+ optype = in_hash[:op].to_s
+ out_hash[:multiplier] = case optype
+ when '+' then 1
+ when '-' then -1
+ end
+ end
+
+ # Modifiers
+ in_hash[:mods]&.each do |mod|
+ if mod[:alias]
+ collect_alias_modifier mod, out_hash
+ elsif mod[:keep]
+ collect_keeper_rule mod, out_hash
+ elsif mod[:map]
+ out_hash[:maps] ||= []
+ collect_map_rule mod, out_hash
+ elsif mod[:reroll]
+ out_hash[:rerolls] ||= []
+ collect_reroll_rule mod, out_hash
+ end
+ end
+
+ out_hash
+ end
+ end
+
+ def collect_offset(dice_expressions)
+ dice_expressions[:bunches].select { |h| h[:constant] }.inject(0) do |total, in_hash|
+ c = in_hash[:constant].to_i
+ optype = in_hash[:op].to_s
+ if optype == '+'
+ total += c
+ else
+ total -= c
+ end
+ total
+ end
+ end
+
+ # Called when we have a single letter convenient alias for common dice adjustments
+ def collect_alias_modifier(alias_mod, out_hash)
+ alias_name = alias_mod[:alias].to_s
+ case alias_name
+ when 'x' # Exploding re-roll
+ out_hash[:rerolls] ||= []
+ out_hash[:rerolls] << [out_hash[:sides], :==, :reroll_add]
+ end
+ end
+
+ # Called for any parsed reroll rule
+ def collect_reroll_rule(reroll_mod, out_hash)
+ out_hash[:rerolls] ||= []
+ if reroll_mod[:simple_value]
+ out_hash[:rerolls] << [reroll_mod[:simple_value].to_i, :>=, :reroll_replace]
+ return
+ end
+
+ # Typical reroll_mod: {:reroll=>"r"@5, :condition=>{:compare_num=>"10"@7}, :type=>"add"@10}
+ op = get_op_symbol(reroll_mod[:condition][:comparison] || '==')
+ v = reroll_mod[:condition][:compare_num].to_i
+ type = "reroll_#{reroll_mod[:type] || 'replace'}".to_sym
+
+ out_hash[:rerolls] << if reroll_mod[:num]
+ [v, op, type, reroll_mod[:num].to_i]
+ else
+ [v, op, type]
+ end
+ end
+
+ # Called for any parsed keeper mode
+ def collect_keeper_rule(keeper_mod, out_hash)
+ raise 'Cannot set keepers for a bunch twice' if out_hash[:keep_mode]
+
+ if keeper_mod[:simple_value]
+ out_hash[:keep_mode] = :keep_best
+ out_hash[:keep_number] = keeper_mod[:simple_value].to_i
+ return
+ end
+
+ # Typical keeper_mod: {:keep=>"k"@5, :num=>"1"@7, :type=>"worst"@9}
+ out_hash[:keep_number] = keeper_mod[:num].to_i
+ out_hash[:keep_mode] = "keep_#{keeper_mod[:type] || 'best'}".to_sym
+ end
+
+ # Called for any parsed map mode
+ def collect_map_rule(map_mod, out_hash)
+ out_hash[:maps] ||= []
+ if map_mod[:simple_value]
+ out_hash[:maps] << [map_mod[:simple_value].to_i, :<=, 1]
+ return
+ end
+
+ # Typical map_mod: {:map=>"m"@4, :condition=>{:compare_num=>"5"@6}, :num=>"2"@8, :output=>"Qwerty"@10}
+ op = get_op_symbol(map_mod[:condition][:comparison] || '>=')
+ v = map_mod[:condition][:compare_num].to_i
+ out_val = 1
+ out_val = map_mod[:num].to_i if map_mod[:num]
+
+ out_hash[:maps] << if map_mod[:output]
+ [v, op, out_val, map_mod[:output].to_s]
+ else
+ [v, op, out_val]
+ end
+ end
+
+ # The dice description language uses (r).op.x, whilst GamesDice::RerollRule uses x.op.(r), so
+ # as well as converting to a symbol, we must reverse sense of input to constructor
+ OP_CONVERSION = {
+ '==' => :==,
+ '>=' => :<=,
+ '>' => :<,
+ '<' => :>,
+ '<=' => :>=
+ }.freeze
+
+ def get_op_symbol(parsed_op_string)
+ OP_CONVERSION[parsed_op_string.to_s]
+ end
+ end
+end