diff --git a/Rakefile b/Rakefile index ed1b45110..4d6e24306 100644 --- a/Rakefile +++ b/Rakefile @@ -45,6 +45,7 @@ namespace :test do 'src/test/test_srs_help_messages.rb', 'src/test/test_detailed_rand_results.rb', 'src/test/range_table_test.rb', + 'src/test/add_dice_parser_test.rb', ] end end diff --git a/src/bcdiceCore.rb b/src/bcdiceCore.rb index 5c96368f2..9768b39c7 100755 --- a/src/bcdiceCore.rb +++ b/src/bcdiceCore.rb @@ -1485,14 +1485,13 @@ def sendMessageToChannels(message) def parren_killer(string) debug("parren_killer input", string) - while /^(.*?)\[(\d+[Dd]\d+)\](.*)/ =~ string - str_before = "" - str_after = "" - dice_cmd = Regexp.last_match(2) - str_before = Regexp.last_match(1) if Regexp.last_match(1) - str_after = Regexp.last_match(3) if Regexp.last_match(3) - rolled, = rollDiceAddingUp(dice_cmd) - string = "#{str_before}#{rolled}#{str_after}" + string = string.gsub(/\[\d+D\d+\]/i) do |matched| + # Remove '[' and ']' + command = matched[1..-2].upcase + times, sides = command.split("D").map(&:to_i) + rolled, = roll(times, sides) + + rolled end string = changeRangeTextToNumberText(string) @@ -1513,11 +1512,6 @@ def parren_killer(string) return string end - def rollDiceAddingUp(*arg) - dice = AddDice.new(self, @diceBot) - dice.rollDiceAddingUp(*arg) - end - # [1...4]D[2...7] -> 2D7 のように[n...m]をランダムな数値へ変換 def changeRangeTextToNumberText(string) debug('[st...ed] before string', string) diff --git a/src/dice/AddDice.rb b/src/dice/AddDice.rb index a89fa4287..af71d1a53 100644 --- a/src/dice/AddDice.rb +++ b/src/dice/AddDice.rb @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- require "utils/normalize" +require "dice/add_dice/parser" +require "dice/add_dice/randomizer" class AddDice def initialize(bcdice, diceBot) @@ -14,320 +16,38 @@ def initialize(bcdice, diceBot) #################### 加算ダイス ######################## def rollDice(string) - debug("AddDice.rollDice() begin string", string) + parser = Parser.new(string) - m = %r{(^|\s)S?(([\d\+\*\-]*[\d]+D[\d/UR@]*[\d\+\*\-D/UR]*)(([<>=]+)([?\-\d]+))?)($|\s)}i.match(string) - return "1" unless m - - string = m[2] - judgeText = m[4] # '>=10'といった成否判定文字 - judgeOperator = m[5] # '>=' といった判定の条件演算子 文字 - diffText = m[6] - - signOfInequality = "" - isCheckSuccess = false - - if judgeText - isCheckSuccess = true - string = m[3] - signOfInequality = @bcdice.marshalSignOfInequality(judgeOperator) - end - - dice_cnt = 0 - dice_max = 0 - total_n = 0 - dice_n = 0 - output = "" - n1 = 0 - n_max = 0 - - addUpTextList = string.split("+") - - addUpTextList.each do |addUpText| - subtractTextList = addUpText.split("-") - - subtractTextList.each_with_index do |subtractText, index| - next if subtractText.empty? - - debug("begin rollDiceAddingUp(subtractText, isCheckSuccess)", subtractText, isCheckSuccess) - dice_now, dice_n_wk, dice_str, n1_wk, n_max_wk, cnt_wk, max_wk = rollDiceAddingUp(subtractText, isCheckSuccess) - debug("end rollDiceAddingUp(subtractText, isCheckSuccess) -> dice_now", dice_now) - - rate = (index == 0 ? 1 : -1) - - total_n += dice_now * rate - dice_n += dice_n_wk * rate - n1 += n1_wk - n_max += n_max_wk - dice_cnt += cnt_wk - dice_max = max_wk if max_wk > dice_max - - next if @diceBot.sendMode == 0 - - operatorText = getOperatorText(rate, output) - output += "#{operatorText}#{dice_str}" - end - end - - if signOfInequality != "" - string += "#{signOfInequality}#{diffText}" + command = parser.parse() + if parser.error? + return '1' end - # ダイス目による補正処理(現状ナイトメアハンターディープ専用) - addText, revision = @diceBot.getDiceRevision(n_max, dice_max, total_n) - debug('addText, revision', addText, revision) + randomizer = Randomizer.new(@bcdice, @diceBot, command.cmp_op) + total = command.lhs.eval(randomizer) - debug("@nick_e", @nick_e) - if @diceBot.sendMode > 0 - if output =~ /[^\d\[\]]+/ - output = "#{@nick_e}: (#{string}) > #{output} > #{total_n}#{addText}" + output = + if randomizer.dice_list.size <= 1 && command.lhs.is_a?(Node::DiceRoll) + "#{@nick_e}: (#{command}) > #{total}" else - output = "#{@nick_e}: (#{string}) > #{total_n}#{addText}" - end - else - output = "#{@nick_e}: (#{string}) > #{total_n}#{addText}" - end - - total_n += revision - - if signOfInequality != "" # 成功度判定処理 - cmp_op = Normalize.comparison_operator(signOfInequality) - target = Normalize.target_number(diffText) - successText = @diceBot.check_result(total_n, dice_n, @dice_list, dice_max, cmp_op, target) - debug("check_suc successText", successText) - output += successText - end - - # ダイスロールによるポイント等の取得処理用(T&T悪意、ナイトメアハンター・ディープ宿命、特命転校生エクストラパワーポイントなど) - output += @diceBot.getDiceRolledAdditionalText(n1, n_max, dice_max) - - if (dice_cnt == 0) || (dice_max == 0) - output = '1' - end - - debug("AddDice.rollDice() end output", output) - return output - end - - def rollDiceAddingUp(string, isCheckSuccess = false) # 加算ダイスロール(個別処理) - debug("rollDiceAddingUp() begin string", string) - - dice_max = 0 - dice_total = 1 - dice_n = 0 - output = "" - n1 = 0 - n_max = 0 - dice_cnt_total = 0 - double_check = false - - if @diceBot.sameDiceRerollCount != 0 # 振り足しありのゲームでダイスが二個以上 - if @diceBot.sameDiceRerollType <= 0 # 判定のみ振り足し - debug('判定のみ振り足し') - double_check = true if isCheckSuccess - elsif @diceBot.sameDiceRerollType <= 1 # ダメージのみ振り足し - debug('ダメージのみ振り足し') - double_check = true unless isCheckSuccess - else # 両方振り足し - double_check = true - end - end - - debug("double_check", double_check) - - while (m = /(^([\d]+\*[\d]+)\*(.+)|(.+)\*([\d]+\*[\d]+)$|(.+)\*([\d]+\*[\d]+)\*(.+))/.match(string)) - if m[2] - string = @bcdice.parren_killer('(' + m[2] + ')') + '*' + m[3] - elsif m[5] - string = m[4] + '*' + @bcdice.parren_killer('(' + m[5] + ')') - elsif m[7] - string = m[6] + '*' + @bcdice.parren_killer('(' + m[7] + ')') + '*' + m[8] - end - end - - debug("string", string) - - emptyResult = [dice_total, dice_n, output, n1, n_max, dice_cnt_total, dice_max] - - mul_cmd = string.split("*") - mul_cmd.each do |mul_line| - if (m = mul_line.match(%r{([\d]+)D([\d]+)(@(\d+))?(/\d+[UR]?)?}i)) - dice_count = m[1].to_i - dice_max = m[2].to_i - critical = m[4].to_i - slashMark = m[5] - - return emptyResult if (critical != 0) && !@diceBot.is2dCritical - return emptyResult if dice_max > $DICE_MAXNUM - - dice_max, dice_now, output_tmp, n1_count, max_number_tmp, result_dice_count = - rollDiceAddingUpCommand(dice_count, dice_max, slashMark, double_check, isCheckSuccess, critical) - - output += "*" if output != "" - output += output_tmp - - dice_total *= dice_now - - dice_n += dice_now - dice_cnt_total += result_dice_count - n1 += n1_count - n_max += max_number_tmp - - else - mul_line = mul_line.to_i - debug('dice_total', dice_total) - debug('mul_line', mul_line) - - dice_total *= mul_line - - unless output.empty? - output += "*" - end - - if mul_line < 0 - output += "(#{mul_line})" - else - output += mul_line.to_s - end - end - end - - debug("rollDiceAddingUp() end output", dice_total, dice_n, output, n1, n_max, dice_cnt_total, dice_max) - return dice_total, dice_n, output, n1, n_max, dice_cnt_total, dice_max - end - - def rollDiceAddingUpCommand(dice_count, dice_max, slashMark, double_check, isCheckSuccess, critical) - result_dice_count = 0 - dice_now = 0 - n1_count = 0 - max_number = 0 - dice_str = "" - dice_arry = [] - dice_arry.push(dice_count) - loop_count = 0 - - debug("before while dice_arry", dice_arry) - - while !dice_arry.empty? - debug("IN while dice_arry", dice_arry) - - dice_wk = dice_arry.shift - result_dice_count += dice_wk - - debug('dice_wk', dice_wk) - debug('dice_max', dice_max) - debug('(sortType & 1)', (@diceBot.sortType & 1)) - - dice_dat = rollLocal(dice_wk, dice_max, (@diceBot.sortType & 1)) - debug('dice_dat', dice_dat) - - dice_new = dice_dat[0] - dice_now += dice_new - - debug('slashMark', slashMark) - dice_now = getSlashedDice(slashMark, dice_now) - - dice_str += "][" if dice_str != "" - debug('dice_str', dice_str) - - dice_str += dice_dat[1] - n1_count += dice_dat[2] - max_number += dice_dat[3] - - # 振り足しありでダイスが二個以上 - if double_check && (dice_wk >= 2) - addDiceArrayByAddDiceCount(dice_dat, dice_max, dice_arry, dice_wk) + "#{@nick_e}: (#{command}) > #{command.lhs.output} > #{total}" end - @diceBot.check2dCritical(critical, dice_new, dice_arry, loop_count) - loop_count += 1 - end + dice_list = randomizer.dice_list + num_one = dice_list.count(1) + num_max = dice_list.count(randomizer.sides) - # ダイス目文字列からダイス値を変更する場合の処理(現状クトゥルフ・テック専用) - dice_now = @diceBot.changeDiceValueByDiceText(dice_now, dice_str, isCheckSuccess, dice_max) + suffix, revision = @diceBot.getDiceRevision(num_max, randomizer.sides, total) + output += suffix + total += revision - output = "" - if @diceBot.sendMode > 1 - output += "#{dice_now}[#{dice_str}]" - elsif @diceBot.sendMode > 0 - output += dice_now.to_s + if command.cmp_op + dice_total = dice_list.inject(&:+) + output += @diceBot.check_result(total, dice_total, dice_list, randomizer.sides, command.cmp_op, command.rhs) end - return dice_max, dice_now, output, n1_count, max_number, result_dice_count - end - - def addDiceArrayByAddDiceCount(dice_dat, _dice_max, dice_queue, roll_times) - values = dice_dat[1].split(",").map(&:to_i) - count_bucket = {} - - values.each do |val| - count_bucket[val] ||= 0 - count_bucket[val] += 1 - end + output += @diceBot.getDiceRolledAdditionalText(num_one, num_max, randomizer.sides) - reroll_threshold = @diceBot.sameDiceRerollCount == 1 ? roll_times : @diceBot.sameDiceRerollCount - count_bucket.each do |_, num| - if num >= reroll_threshold - dice_queue.push(num) - end - end - end - - def getSlashedDice(slashMark, lhs) - m = %r{^/(\d+)(.)?$}i.match(slashMark) - return lhs unless m - - rhs = m[1].to_i - mark = m[2] - - return lhs if rhs == 0 - - value = lhs.to_f / rhs - - if mark == "U" - return value.ceil - elsif mark == "R" - return value.round - else - return value.floor - end - end - - def rollLocal(dice_wk, dice_max, sortType) - if dice_max == 66 - return rollD66(dice_wk) - end - - ret = @bcdice.roll(dice_wk, dice_max, sortType) - @dice_list.concat(ret[1].split(",").map(&:to_i)) - - return ret - end - - def rollD66(count) - d66List = [] - - count.times do - d66List << @bcdice.getD66Value() - end - - total = d66List.inject { |sum, i| sum + i } - text = d66List.join(',') - n1Count = d66List.count(1) - nMaxCount = d66List.count(66) - - @dice_list.concat(d66List) - - return [total, text, n1Count, nMaxCount, 0, 0, 0] - end - - def getOperatorText(rate, output) - if rate < 0 - '-' - elsif output.empty? - '' - else - "+" - end + return output end end diff --git a/src/dice/add_dice/node.rb b/src/dice/add_dice/node.rb new file mode 100644 index 000000000..99178e12c --- /dev/null +++ b/src/dice/add_dice/node.rb @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: true + +class AddDice + # 加算ロールの構文解析木のノードを格納するモジュール + module Node + # 加算ロールコマンドのノード。 + # + # 目標値が設定されていない場合は +lhs+ のみを使用する。 + # 目標値が設定されている場合は、+lhs+、+cmp_op+、+rhs+ を使用する。 + class Command + # 左辺のノード + # @return [Object] + attr_reader :lhs + # 比較演算子 + # @return [Symbol] + attr_reader :cmp_op + # 右辺のノード + # @return [Integer, String] + attr_reader :rhs + + # ノードを初期化する + # @param [Object] lhs 左辺のノード + # @param [Symbol] cmp_op 比較演算子 + # @param [Integer, String] rhs 右辺のノード + def initialize(lhs, cmp_op, rhs) + @lhs = lhs + @cmp_op = cmp_op + @rhs = rhs + end + + # 文字列に変換する + # @return [String] + def to_s + @lhs.to_s + cmp_op_text + @rhs.to_s + end + + # ノードのS式を返す + # @return [String] + def s_exp + if @cmp_op + "(Command (#{@cmp_op} #{@lhs.s_exp} #{@rhs}))" + else + "(Command #{@lhs.s_exp})" + end + end + + private + + # メッセージ中で比較演算子をどのように表示するかを返す + # @return [String] + def cmp_op_text + case @cmp_op + when :'!=' + '<>' + when :== + '=' + else + @cmp_op.to_s + end + end + end + + # 二項演算子のノード + class BinaryOp + # ノードを初期化する + # @param [Object] lhs 左のオペランドのノード + # @param [Symbol] op 演算子 + # @param [Object] rhs 右のオペランドのノード + def initialize(lhs, op, rhs) + @lhs = lhs + @op = op + @rhs = rhs + end + + # ノードを評価する + # + # 左右のオペランドをそれぞれ再帰的に評価した後で、演算を行う。 + # + # @param [Randomizer] randomizer ランダマイザ + # @return [Integer] 評価結果 + def eval(randomizer) + lhs = @lhs.eval(randomizer) + rhs = @rhs.eval(randomizer) + + return calc(lhs, rhs) + end + + # 文字列に変換する + # @return [String] + def to_s + "#{@lhs}#{@op}#{@rhs}" + end + + # メッセージへの出力を返す + # @return [String] + def output + "#{@lhs.output}#{@op}#{@rhs.output}" + end + + # ノードのS式を返す + # @return [String] + def s_exp + "(#{op_for_s_exp} #{@lhs.s_exp} #{@rhs.s_exp})" + end + + private + + # 演算を行う + # @param [Integer] lhs 左のオペランド + # @param [Integer] rhs 右のオペランド + # @return [Integer] 演算の結果 + def calc(lhs, rhs) + lhs.send(@op, rhs) + end + + # S式で使う演算子の表現を返す + # @return [String] + def op_for_s_exp + @op + end + end + + # 除算ノードの基底クラス + # + # 定数 +ROUNDING_METHOD+ で端数処理方法を示す記号 + # ( +'U'+, +'R'+, +''+ ) を定義すること。 + # また、除算および端数処理を行う +divide_and_round+ メソッドを実装すること。 + class DivideBase < BinaryOp + # ノードを初期化する + # @param [Object] lhs 左のオペランドのノード + # @param [Object] rhs 右のオペランドのノード + def initialize(lhs, rhs) + super(lhs, :/, rhs) + end + + # 文字列に変換する + # + # 通常の結果の末尾に、端数処理方法を示す記号を付加する。 + # + # @return [String] + def to_s + "#{super}#{rounding_method}" + end + + # メッセージへの出力を返す + # + # 通常の結果の末尾に、端数処理方法を示す記号を付加する。 + # + # @return [String] + def output + "#{super}#{rounding_method}" + end + + private + + # 端数処理方法を示す記号を返す + # @return [String] + def rounding_method + self.class::ROUNDING_METHOD + end + + # S式で使う演算子の表現を返す + # @return [String] + def op_for_s_exp + "#{@op}#{rounding_method}" + end + + # 演算を行う + # @param [Integer] lhs 左のオペランド + # @param [Integer] rhs 右のオペランド + # @return [Integer] 演算の結果 + def calc(lhs, rhs) + if rhs.zero? + return 1 + end + + return divide_and_round(lhs, rhs) + end + + # 除算および端数処理を行う + # @param [Integer] _dividend 被除数 + # @param [Integer] _divisor 除数(0以外) + # @return [Integer] + def divide_and_round(_dividend, _divisor) + raise NotImplementedError + end + end + + # 除算(切り上げ)のノード + class DivideWithRoundingUp < DivideBase + # 端数処理方法を示す記号 + ROUNDING_METHOD = 'U' + + private + + # 除算および端数処理を行う + # @param [Integer] dividend 被除数 + # @param [Integer] divisor 除数(0以外) + # @return [Integer] + def divide_and_round(dividend, divisor) + (dividend.to_f / divisor).ceil + end + end + + # 除算(四捨五入)のノード + class DivideWithRoundingOff < DivideBase + # 端数処理方法を示す記号 + ROUNDING_METHOD = 'R' + + private + + # 除算および端数処理を行う + # @param [Integer] dividend 被除数 + # @param [Integer] divisor 除数(0以外) + # @return [Integer] + def divide_and_round(dividend, divisor) + (dividend.to_f / divisor).round + end + end + + # 除算(切り捨て)のノード + class DivideWithRoundingDown < DivideBase + # 端数処理方法を示す記号 + ROUNDING_METHOD = '' + + private + + # 除算および端数処理を行う + # @param [Integer] dividend 被除数 + # @param [Integer] divisor 除数(0以外) + # @return [Integer] + def divide_and_round(dividend, divisor) + dividend / divisor + end + end + + # 符号反転のノード + class Negate + # 符号反転の対象 + # @return [Object] + attr_reader :body + + # ノードを初期化する + # @param [Object] body 符号反転の対象 + def initialize(body) + @body = body + end + + # ノードを評価する + # + # 対象オペランドを再帰的に評価した後、評価結果の符号を反転する。 + # + # @param [Randomizer] randomizer ランダマイザ + # @return [Integer] 評価結果 + def eval(randomizer) + -@body.eval(randomizer) + end + + # 文字列に変換する + # @return [String] + def to_s + "-#{@body}" + end + + # メッセージへの出力を返す + # @return [String] + def output + "-#{@body.output}" + end + + # ノードのS式を返す + # @return [String] + def s_exp + "(- #{@body.s_exp})" + end + end + + # ダイスロールのノード + class DiceRoll + # ノードを初期化する + # @param [Number] times ダイスを振る回数のノード + # @param [Number] sides ダイスの面数のノード + # @param [Number, nil] critical クリティカル値のノード + def initialize(times, sides, critical) + @times = times.literal + @sides = sides.literal + @critical = critical.nil? ? nil : critical.literal + + # ダイスを振った結果の出力 + @text = nil + end + + # ノードを評価する(ダイスを振る) + # + # 評価結果は出目の合計値になる。 + # 出目はランダマイザに記録される。 + # + # @param [Randomizer] randomizer ランダマイザ + # @return [Integer] 評価結果(出目の合計値) + def eval(randomizer) + total, @text = randomizer.roll(@times, @sides, @critical) + + total + end + + # 文字列に変換する + # @return [String] + def to_s + if @critical + "#{@times}D#{@sides}@#{@critical}" + else + "#{@times}D#{@sides}" + end + end + + # メッセージへの出力を返す + # @return [String] + def output + @text + end + + # ノードのS式を返す + # @return [String] + def s_exp + parts = [@times, @sides, @critical].compact + + "(DiceRoll #{parts.join(' ')})" + end + end + + # 数値のノード + class Number + # 値 + # @return [Integer] + attr_reader :literal + + # ノードを初期化する + # @param [Integer] literal 値 + def initialize(literal) + @literal = literal + end + + # 符号を反転した結果の数値ノードを返す + # @return [Number] + def negate + Number.new(-@literal) + end + + # ノードを評価する + # @return [Integer] 格納している値 + def eval(_randomizer) + @literal + end + + # 文字列に変換する + # @return [String] + def to_s + @literal.to_s + end + + alias output to_s + alias s_exp to_s + end + end +end diff --git a/src/dice/add_dice/parser.rb b/src/dice/add_dice/parser.rb new file mode 100644 index 000000000..4f8218efc --- /dev/null +++ b/src/dice/add_dice/parser.rb @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: true + +require "utils/ArithmeticEvaluator" +require "utils/normalize" +require "dice/add_dice/node" + +class AddDice + # 加算ロールの構文解析器のクラス + class Parser + # 構文解析器を初期化する + # @param [String] expr 構文解析対象の文字列 + def initialize(expr) + # 構文解析対象の文字列 + @expr = expr + # 読み込んだトークンのインデックス + @idx = 0 + # 構文解析エラーが発生したかどうか + @error = false + end + + # 構文解析を実行する + # @return [Node::Command] 加算ロールコマンド + def parse() + lhs, cmp_op, rhs = @expr.partition(/[<>=]+/) + + cmp_op = Normalize.comparison_operator(cmp_op) + if !rhs.empty? && rhs != "?" + rhs = ArithmeticEvaluator.new.eval(rhs) + end + + @tokens = tokenize(lhs) + lhs = expr() + + if @idx != @tokens.size + @error = true + end + + return AddDice::Node::Command.new(lhs, cmp_op, rhs) + end + + # 構文解析エラーが発生したかどうかを返す + # @return [Boolean] + def error? + @error + end + + private + + # 構文解析対象の文字列をトークンの配列に変換する + # @return [Array] + def tokenize(expr) + expr.gsub(%r{[\+\-\*/DURS@]}) { |e| " #{e} " }.split(' ') + end + + # 式 + def expr + consume("S") + + return add() + end + + # 加算、減算 + def add + node = mul() + + loop do + if consume("+") + op, rhs = sub_negative_number(:+, mul()) + node = AddDice::Node::BinaryOp.new(node, op, rhs) + elsif consume("-") + op, rhs = sub_negative_number(:-, mul()) + node = AddDice::Node::BinaryOp.new(node, op, rhs) + else + break + end + end + + return node + end + + # TODO: 処理の説明を書く + def sub_negative_number(op, rhs) + if rhs.is_a?(Node::Number) && rhs.literal < 0 + if op == :+ + return [:-, rhs.negate] + elsif op == :- + return [:+, rhs.negate] + end + end + + [op, rhs] + end + + # 乗算、除算 + def mul + node = unary() + + loop do + if consume("*") + node = AddDice::Node::BinaryOp.new(node, :*, unary()) + elsif consume("/") + rhs = unary() + klass = divide_node_class() + node = klass.new(node, rhs) + else + break + end + end + + return node + end + + # 端数処理方法を示す記号を読み込み、対応する除算ノードのクラスを返す + # @return [Class] 除算ノードのクラス + def divide_node_class + if consume('U') + # 切り上げ + Node::DivideWithRoundingUp + elsif consume('R') + # 四捨五入 + Node::DivideWithRoundingOff + else + # 切り捨て + Node::DivideWithRoundingDown + end + end + + # 単項演算 + def unary + if consume("+") + unary() + elsif consume("-") + node = unary() + + case node + when Node::Negate + node.body + when Node::Number + node.negate() + else + AddDice::Node::Negate.new(node) + end + else + term() + end + end + + # 項:ダイスロール、数値 + def term + ret = expect_number() + if consume("D") + times = ret + sides = expect_number() + critical = consume("@") ? expect_number() : nil + + ret = AddDice::Node::DiceRoll.new(times, sides, critical) + end + + ret + end + + # トークンを消費する + # + # トークンと期待した文字列が合致していた場合、次のトークンに進む。 + # 合致していなかった場合は、進まない。 + # + # @param [String] str 期待する文字列 + # @return [true] トークンと期待した文字列が合致していた場合 + # @return [false] トークンと期待した文字列が合致していなかった場合 + def consume(str) + if @tokens[@idx] != str + return false + end + + @idx += 1 + return true + end + + # 指定された文字列のトークンを要求する + # + # トークンと期待した文字列が合致していなかった場合、エラーとする。 + # エラーの有無にかかわらず、次のトークンに進む。 + # + # @param [String] str 期待する文字列 + # @return [void] + def expect(str) + if @tokens[@idx] != str + @error = true + end + + @idx += 1 + end + + # 整数のトークンを要求する + # + # 整数のトークンならば、対応する整数のノードを返す。 + # そうでなければエラーとし、整数0のノードを返す。 + # + # エラーの有無にかかわらず、次のトークンに進む。 + # + # @return [Node::Number] 整数のノード + def expect_number() + unless integer?(@tokens[@idx]) + @error = true + @idx += 1 + return AddDice::Node::Number.new(0) + end + + ret = @tokens[@idx].to_i + @idx += 1 + return AddDice::Node::Number.new(ret) + end + + # 文字列が整数かどうかを返す + # @param [String] str 対象文字列 + # @return [Boolean] + def integer?(str) + # Ruby 1.9 以降では Kernel.#Integer を使うべき + # Ruby 1.8 にもあるが、基数を指定できない問題がある + !/^\d+$/.match(str).nil? + end + end +end diff --git a/src/dice/add_dice/randomizer.rb b/src/dice/add_dice/randomizer.rb new file mode 100644 index 000000000..903fd9132 --- /dev/null +++ b/src/dice/add_dice/randomizer.rb @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +class AddDice + class Randomizer + attr_reader :dicebot, :cmp_op, :dice_list, :sides + + def initialize(bcdice, dicebot, cmp_op) + @bcdice = bcdice + @dicebot = dicebot + @cmp_op = cmp_op + @sides = 0 + @dice_list = [] + end + + def roll(times, sides, critical) + total = 0 + results_list = [] + + loop_count = 0 + queue = [times] + while !queue.empty? + times = queue.shift + val, dice_list = roll_once(times, sides) + + total += val + results_list.push(dice_list) + + enqueue_reroll(dice_list, queue, times) + @dicebot.check2dCritical(critical, val, queue, loop_count) + loop_count += 1 + end + + total = @dicebot.changeDiceValueByDiceText(total, results_list.flatten, @cmp_op, sides) + + text = total.to_s + results_list.each do |list| + text += '[' + list.join(',') + ']' + end + + [total, text] + end + + private + + def roll_once(times, sides) + @sides = sides if @sides < sides + + if sides == 66 + return rollD66(dice_wk) + end + + _, dice_list, = @bcdice.roll(times, sides, @dicebot.sortType & 1) + dice_list = dice_list.split(",").map(&:to_i) + @dice_list.concat(dice_list) + + total = dice_list.inject(0, &:+) + return [total, dice_list] + end + + def roll_d66(times) + dice_list = Array.new(times) { @bcdice.getD66Value() } + @dice_list.concat(dice_list) + + total = dice_list.inject(0, &:+) + return [total, dice_list] + end + + def double_check? + if @dicebot.sameDiceRerollCount != 0 # 振り足しありのゲームでダイスが二個以上 + if @dicebot.sameDiceRerollType <= 0 # 判定のみ振り足し + return true if @cmp_op + elsif @dicebot.sameDiceRerollType <= 1 # ダメージのみ振り足し + debug('ダメージのみ振り足し') + return true unless @cmp_op + else # 両方振り足し + return true + end + end + + return false + end + + def enqueue_reroll(dice_list, dice_queue, roll_times) + unless double_check? && (roll_times >= 2) + return + end + + count_bucket = {} + + dice_list.each do |val| + count_bucket[val] ||= 0 + count_bucket[val] += 1 + end + + reroll_threshold = @dicebot.sameDiceRerollCount == 1 ? roll_times : @dicebot.sameDiceRerollCount + count_bucket.each do |_, num| + if num >= reroll_threshold + dice_queue.push(num) + end + end + end + end +end diff --git a/src/diceBot/CthulhuTech.rb b/src/diceBot/CthulhuTech.rb index e77c5da39..3791c7542 100644 --- a/src/diceBot/CthulhuTech.rb +++ b/src/diceBot/CthulhuTech.rb @@ -81,23 +81,17 @@ def getDamageDice(total_n, diff) # ダイス目文字列からダイス値を変更する場合の処理 # クトゥルフ・テックの判定用ダイス計算 - def changeDiceValueByDiceText(dice_now, dice_str, isCheckSuccess, dice_max) - debug("changeDiceValueByDiceText dice_now, dice_str, isCheckSuccess, dice_max", dice_now, dice_str, isCheckSuccess, dice_max) - if isCheckSuccess && (dice_max == 10) - debug('cthulhutech_check(dice_str) called') - debug('dice_str, dice_now', dice_str, dice_now) - dice_now = cthulhutech_check(dice_str) + def changeDiceValueByDiceText(dice_total, dice_list, cmp_op, sides) + if cmp_op && (sides == 10) + dice_total = cthulhutech_check(dice_list) end - debug('dice_str, dice_now', dice_str, dice_now) - return dice_now + return dice_total end #################### CthulhuTech ######################## # CthulhuTechの判定用ダイス計算 - def cthulhutech_check(dice_str) - dice_aRR = dice_str.split(/,/).collect { |i| i.to_i } - + def cthulhutech_check(dice_aRR) dice_num = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] max_num = 0 diff --git a/src/diceBot/DiceBot.rb b/src/diceBot/DiceBot.rb index 68e4d0caa..08fc2afed 100644 --- a/src/diceBot/DiceBot.rb +++ b/src/diceBot/DiceBot.rb @@ -210,10 +210,6 @@ def d66(*args) @@bcdice.getD66Value(*args) end - def rollDiceAddingUp(*arg) - @@bcdice.rollDiceAddingUp(*arg) - end - def parren_killer(string) @@bcdice.parren_killer(string) end diff --git a/src/diceBot/Satasupe.rb b/src/diceBot/Satasupe.rb index 9d8cc6b25..1f2130042 100644 --- a/src/diceBot/Satasupe.rb +++ b/src/diceBot/Satasupe.rb @@ -532,10 +532,11 @@ def getNpcTableResult(counts) age_type -= 1 agen_text = agen[age_type] - age_num = agen_text.split(/\+/) + age_const, age_dice = agen_text.split("+") - total, = rollDiceAddingUp(age_num[1]) - ysold = total + age_num[0].to_i + times, sides = age_dice.split("D").map(&:to_i) + total, = roll(times, sides) + ysold = total + age_const.to_i lmodValue = lmood[(rand 6)] lageValue = lage[(rand 3)] diff --git a/src/test/add_dice_parser_test.rb b/src/test/add_dice_parser_test.rb new file mode 100644 index 000000000..bfcf61a0b --- /dev/null +++ b/src/test/add_dice_parser_test.rb @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: true + +bcdice_root = File.expand_path('..', File.dirname(__FILE__)) +$:.unshift(bcdice_root) unless $:.include?(bcdice_root) + +require 'test/unit' +require 'bcdiceCore' +require 'dice/add_dice/parser' + +class AddDiceParserTest < Test::Unit::TestCase + def setup + @bcdice = BCDiceMaker.new.newBcDice + end + + # ダイスロールのみ + def test_parse_dice_roll + test_parse('2D6', '(Command (DiceRoll 2 6))') + end + + # ダイスロール + 修正値 + def test_parse_modifier + test_parse('2D6+1', '(Command (+ (DiceRoll 2 6) 1))') + end + + # 定数畳み込みはしない + def test_parse_long_modifier + test_parse('2D6+1-1-2-3-4', '(Command (- (- (- (- (+ (DiceRoll 2 6) 1) 1) 2) 3) 4))') + end + + # 複数のダイスロール + def test_parse_multiple_dice_rolls + test_parse( + '2D6*3-1D6+1', + '(Command (+ (- (* (DiceRoll 2 6) 3) (DiceRoll 1 6)) 1))' + ) + end + + # 除法 + def test_parse_division + test_parse('5D6/10', '(Command (/ (DiceRoll 5 6) 10))') + end + + # 除法(切り上げ) + def test_parse_division_with_rounding_up + test_parse('3D6/2U', '(Command (/U (DiceRoll 3 6) 2))') + end + + # 除法(四捨五入) + def test_parse_division_with_rounding_off + test_parse('1D100/10R', '(Command (/R (DiceRoll 1 100) 10))') + end + + # 符号反転(負の整数で割る) + def test_parse_negation_1 + test_parse('1D6/-3', '(Command (/ (DiceRoll 1 6) -3))') + end + + # 符号反転(ダイスロールの符号反転) + def test_parse_negation_2 + test_parse('-1D6+1', '(Command (+ (- (DiceRoll 1 6)) 1))') + end + + # 二重符号反転(--1) + def test_parse_double_negation_1 + test_parse('2D6--1', '(Command (+ (DiceRoll 2 6) 1))') + end + + # 二重符号反転(---1) + def test_parse_double_negation_2 + test_parse('2D6---1', '(Command (- (DiceRoll 2 6) 1))') + end + + # 目標値あり(=) + def test_parse_target_value_eq_1 + test_parse('2D6=7', '(Command (== (DiceRoll 2 6) 7))') + end + + # 目標値あり(===) + def test_parse_target_value_eq_2 + test_parse('2D6===7', '(Command (== (DiceRoll 2 6) 7))') + end + + # 目標値あり(<>) + def test_parse_target_value_not_eq + test_parse('2D6<>7', '(Command (!= (DiceRoll 2 6) 7))') + end + + # 目標値あり(>=) + def test_parse_target_value_geq_1 + test_parse('2D6>=7', '(Command (>= (DiceRoll 2 6) 7))') + end + + # 目標値あり(=>) + def test_parse_target_value_geq_2 + test_parse('2D6=>7', '(Command (>= (DiceRoll 2 6) 7))') + end + + # 目標値あり、目標値の定数畳み込み + def test_parse_target_value_constant_fonding + test_parse( + '1D6+1-2>=1+2', + '(Command (>= (- (+ (DiceRoll 1 6) 1) 2) 3))' + ) + end + + private + + # 構文解析をテストする + # @param [String] command テストするコマンド + # @param [String] expected_s_exp 期待されるS式 + # @return [void] + def test_parse(command, expected_s_exp) + parser = AddDice::Parser.new(command) + node = parser.parse + + assert(!parser.error?, '構文解析に成功する') + assert_equal(expected_s_exp, node.s_exp, '結果の抽象構文木が正しい') + end +end diff --git a/src/test/data/None.txt b/src/test/data/None.txt index 46e5ba439..61cf7b347 100644 --- a/src/test/data/None.txt +++ b/src/test/data/None.txt @@ -1,4 +1,10 @@ input: +1D6 +output: +DiceBot : (1D6) > 5 +rand:5/6 +============================ +input: 2D6 output: DiceBot : (2D6) > 8[5,3] > 8 @@ -11,6 +17,12 @@ DiceBot : (2D4) > 3[1,2] > 3 rand:1/4,2/4 ============================ input: +1D6+1 +output: +DiceBot : (1D6+1) > 4[4]+1 > 5 +rand:4/6 +============================ +input: 2D6+1 output: DiceBot : (2D6+1) > 8[2,6]+1 > 9 @@ -29,6 +41,12 @@ DiceBot : (2D6+1-1-2-3-4) > 5[4,1]+1-1-2-3-4 > -4 rand:4/6,1/6 ============================ input: +2d10--3-4 +output: +DiceBot : (2D10+3-4) > 8[3,5]+3-4 > 7 +rand:3/10,5/10 +============================ +input: 2D6+4D10 output: DiceBot : (2D6+4D10) > 9[5,4]+21[1,9,7,4] > 30 @@ -47,6 +65,12 @@ DiceBot : (2D10+3-4) > 8[3,5]+3-4 > 7 rand:3/10,5/10 ============================ input: +2d10-4+3 +output: +DiceBot : (2D10-4+3) > 8[3,5]-4+3 > 7 +rand:3/10,5/10 +============================ +input: 2d10+3*4 output: DiceBot : (2D10+3*4) > 8[3,5]+3*4 > 20 @@ -79,64 +103,94 @@ rand:6/6,5/6,6/6,2/6,1/6,4/6 input: 1D6/2 output: -DiceBot : (1D6/2) > 0 +DiceBot : (1D6/2) > 1[1]/2 > 0 rand:1/6 ============================ input: 3D6/2 output: -DiceBot : (3D6/2) > 3[1,2,4] > 3 +DiceBot : (3D6/2) > 7[1,2,4]/2 > 3 rand:1/6,2/6,4/6 ============================ input: 3D6/2+1D6 output: -DiceBot : (3D6/2+1D6) > 3[1,2,4]+5[5] > 8 +DiceBot : (3D6/2+1D6) > 7[1,2,4]/2+5[5] > 8 rand:1/6,2/6,4/6,5/6 ============================ input: 3D6+1D6/2 output: -DiceBot : (3D6+1D6/2) > 7[1,2,4]+2[5] > 9 +DiceBot : (3D6+1D6/2) > 7[1,2,4]+5[5]/2 > 9 rand:1/6,2/6,4/6,5/6 ============================ input: 3D6+1D6/2U output: -DiceBot : (3D6+1D6/2U) > 7[1,2,4]+3[5] > 10 +DiceBot : (3D6+1D6/2U) > 7[1,2,4]+5[5]/2U > 10 rand:1/6,2/6,4/6,5/6 ============================ input: 5D6/10 output: -DiceBot : (5D6/10) > 2[6,6,6,6,5] > 2 +DiceBot : (5D6/10) > 29[6,6,6,6,5]/10 > 2 rand:6/6,6/6,6/6,6/6,5/6 ============================ input: 3D6/2U output: -DiceBot : (3D6/2U) > 4[1,2,4] > 4 +DiceBot : (3D6/2U) > 7[1,2,4]/2U > 4 rand:1/6,2/6,4/6 ============================ input: 5D6/10u output: -DiceBot : (5D6/10U) > 3[6,6,6,2,1] > 3 +DiceBot : (5D6/10U) > 21[6,6,6,2,1]/10U > 3 rand:6/6,6/6,6/6,2/6,1/6 ============================ input: 1D100/10R output: -DiceBot : (1D100/10R) > 6 +DiceBot : (1D100/10R) > 55[55]/10R > 6 rand:55/100 ============================ input: 1D100/10r output: -DiceBot : (1D100/10R) > 5 +DiceBot : (1D100/10R) > 54[54]/10R > 5 rand:54/100 ============================ input: +1d6/-3 +output: +DiceBot : (1D6/-3) > 4[4]/-3 > -2 +rand:4/6 +============================ +input: +1d6/-3u +output: +DiceBot : (1D6/-3U) > 4[4]/-3U > -1 +rand:4/6 +============================ +input: +1d6/-3r +output: +DiceBot : (1D6/-3R) > 4[4]/-3R > -1 +rand:4/6 +============================ +input: +1d6/2*3 +output: +DiceBot : (1D6/2*3) > 6[6]/2*3 > 9 +rand:6/6 +============================ +input: +1d6/-3r +output: +DiceBot : (1D6/-3R) > 5[5]/-3R > -2 +rand:5/6 +============================ +input: [1...5]D6 output: DiceBot : (4D6) > 15[5,3,4,3] > 15 @@ -167,9 +221,21 @@ DiceBot : (3D6-1) > 14[5,5,4]-1 > 13 rand:2/4,3/3,5/6,5/6,4/6 ============================ input: +1D6+[1D6] +output: +DiceBot : (1D6+2) > 3[3]+2 > 5 +rand:2/6,3/6 +============================ +input: +1d6+[1d6] +output: +DiceBot : (1D6+2) > 3[3]+2 > 5 +rand:2/6,3/6 +============================ +input: 1D6/0 output: -DiceBot : (1D6/0) > 1 +DiceBot : (1D6/0) > 1[1]/0 > 1 rand:1/6 ============================ input: @@ -316,6 +382,18 @@ DiceBot : (-2D6>=-7) > -8[3,5] > -8 > 失敗 rand:3/6,5/6 ============================ input: +1d6>=1+2 +output: +DiceBot : (1D6>=3) > 3 > 成功 +rand:3/6 +============================ +input: +1d6-(1-2)>=3+2 +output: +DiceBot : (1D6+1>=5) > 3[3]+1 > 4 > 失敗 +rand:3/6 +============================ +input: 2b6 output: DiceBot : (2B6) > 3,4 diff --git a/src/test/data/RuneQuest.txt b/src/test/data/RuneQuest.txt index df3bfed7d..c77b763ca 100644 --- a/src/test/data/RuneQuest.txt +++ b/src/test/data/RuneQuest.txt @@ -181,7 +181,8 @@ rand:11/100 input: 1d100>=68+12 output: -rand: +RuneQuest : (1D100>=80) > 11 > 失敗 +rand:11/100 ============================ input: 1d100>=(68+12) diff --git a/src/test/data/dummyBot.txt b/src/test/data/dummyBot.txt index 121131317..617436ae8 100644 --- a/src/test/data/dummyBot.txt +++ b/src/test/data/dummyBot.txt @@ -96,13 +96,13 @@ rand: input: 2D6*2*3 output: -DiceBot : (2D6*2*3) > 5[2,3]*6 > 30 +DiceBot : (2D6*2*3) > 5[2,3]*2*3 > 30 rand:2/6,3/6 ============================ input: 2D6*2*3-1 output: -DiceBot : (2D6*2*3-1) > 5[2,3]*6-1 > 29 +DiceBot : (2D6*2*3-1) > 5[2,3]*2*3-1 > 29 rand:2/6,3/6 ============================ input: @@ -120,66 +120,66 @@ rand: input: 2D10/10 output: -DiceBot : (2D10/10) > 0[1,8] > 0 +DiceBot : (2D10/10) > 9[1,8]/10 > 0 rand:1/10,8/10 ============================ input: 2D10/10 output: -DiceBot : (2D10/10) > 1[1,9] > 1 +DiceBot : (2D10/10) > 10[1,9]/10 > 1 rand:1/10,9/10 ============================ input: 2D10/10 output: -DiceBot : (2D10/10) > 1[1,10] > 1 +DiceBot : (2D10/10) > 11[1,10]/10 > 1 rand:1/10,10/10 ============================ input: 2D10/10R output: -DiceBot : (2D10/10R) > 0[1,3] > 0 +DiceBot : (2D10/10R) > 4[1,3]/10R > 0 rand:1/10,3/10 ============================ input: 2D10/10R output: -DiceBot : (2D10/10R) > 1[1,4] > 1 +DiceBot : (2D10/10R) > 5[1,4]/10R > 1 rand:1/10,4/10 ============================ input: 2D10/10R output: -DiceBot : (2D10/10R) > 1[10,4] > 1 +DiceBot : (2D10/10R) > 14[10,4]/10R > 1 rand:10/10,4/10 ============================ input: 2D10/10R output: -DiceBot : (2D10/10R) > 2[10,5] > 2 +DiceBot : (2D10/10R) > 15[10,5]/10R > 2 rand:10/10,5/10 ============================ input: 2D10/10U output: -DiceBot : (2D10/10U) > 1[1,1] > 1 +DiceBot : (2D10/10U) > 2[1,1]/10U > 1 rand:1/10,1/10 ============================ input: 2D10/10U output: -DiceBot : (2D10/10U) > 1[1,8] > 1 +DiceBot : (2D10/10U) > 9[1,8]/10U > 1 rand:1/10,8/10 ============================ input: 2D10/10U output: -DiceBot : (2D10/10U) > 1[1,9] > 1 +DiceBot : (2D10/10U) > 10[1,9]/10U > 1 rand:1/10,9/10 ============================ input: 2D10/10U output: -DiceBot : (2D10/10U) > 2[1,10] > 2 +DiceBot : (2D10/10U) > 11[1,10]/10U > 2 rand:1/10,10/10 ============================