diff --git a/src/dice/add_dice/node.rb b/src/dice/add_dice/node.rb index 7ddc2b45b..065a223a7 100644 --- a/src/dice/add_dice/node.rb +++ b/src/dice/add_dice/node.rb @@ -337,6 +337,96 @@ def s_exp end end + # フィルタ処理付きダイスロールのノード。 + # + # ダイスロール後、条件に従って出目を選択し、和を求める。 + class DiceRollWithFilter + # フィルタの構造体 + # + # 各フィルタには、あらかじめソートされた出目の配列が渡される。 + # + # @!attribute abbr + # @return [Symbol] フィルタの略称 + # @!attribute apply + # @return [Proc] フィルタ処理の内容 + Filter = Struct.new(:abbr, :apply) + + # 大きな出目から複数個取る + KEEP_HIGHEST = Filter.new( + :KH, + lambda { |sorted_values, n| sorted_values.reverse.take(n) } + ).freeze + + # 小さな出目から複数個取る + KEEP_LOWEST = Filter.new( + :KL, + lambda { |sorted_values, n| sorted_values.take(n) } + ).freeze + + # 大きな出目から複数個除く + DROP_HIGHEST = Filter.new( + :DH, + lambda { |sorted_values, n| sorted_values.reverse.drop(n) } + ).freeze + + # 小さな出目から複数個除く + DROP_LOWEST = Filter.new( + :DL, + lambda { |sorted_values, n| sorted_values.drop(n) } + ).freeze + + # ノードを初期化する + # @param [Number] times ダイスを振る回数のノード + # @param [Number] sides ダイスの面数のノード + # @param [Number] n_filtering ダイスを残す/減らす個数のノード + # @param [Filter] filter フィルタ + def initialize(times, sides, n_filtering, filter) + @times = times.literal + @sides = sides.literal + @n_filtering = n_filtering.literal + @filter = filter + + # ダイスを振った結果の出力 + @text = nil + end + + # ノードを評価する(ダイスを振り、出目を選択して和を求める) + # + # 評価結果は出目の合計値になる。 + # 出目はランダマイザに記録される。 + # + # @param [Randomizer] randomizer ランダマイザ + # @return [Integer] 評価結果(出目の合計値) + def eval(randomizer) + sorted_values = randomizer.roll_once(@times, @sides).sort + total = @filter. + apply[sorted_values, @n_filtering]. + reduce(0, &:+) + + @text = "#{total}[#{sorted_values.join(',')}]" + + return total + end + + # 文字列に変換する + # @return [String] + def to_s + "#{@times}D#{@sides}#{@filter.abbr}#{@n_filtering}" + end + + # メッセージへの出力を返す + # @return [String] + def output + @text + end + + # ノードのS式を返す + # @return [String] + def s_exp + "(DiceRollWithFilter #{@times} #{@sides} #{@filter.abbr.inspect} #{@n_filtering})" + end + end + # 数値のノード class Number # 値 diff --git a/src/dice/add_dice/parser.rb b/src/dice/add_dice/parser.rb index 442d3fe48..6daa03108 100644 --- a/src/dice/add_dice/parser.rb +++ b/src/dice/add_dice/parser.rb @@ -52,7 +52,7 @@ def error? # 構文解析対象の文字列をトークンの配列に変換する # @return [Array] def tokenize(expr) - expr.gsub(%r{[\+\-\*/DURS@]}) { |e| " #{e} " }.split(' ') + expr.gsub(%r{[\+\-\*/DURSKHL@]}) { |e| " #{e} " }.split(' ') end # 式 @@ -150,17 +150,43 @@ def unary # 項:ダイスロール、数値 def term - ret = expect_number() + num = expect_number() if consume("D") - times = ret + times = num sides = expect_number() - critical = consume("@") ? expect_number() : nil + filter = dice_roll_filter() + if filter + # ダイスロール後のフィルタリングあり + n_filtering = expect_number() + @contain_dice_roll = true + return Node::DiceRollWithFilter.new(times, sides, n_filtering, filter) + end + + # 通常のダイスロール + critical = consume("@") ? expect_number() : nil @contain_dice_roll = true - ret = AddDice::Node::DiceRoll.new(times, sides, critical) + return Node::DiceRoll.new(times, sides, critical) end - ret + return num + end + + # ダイスロール:フィルタ処理 + def dice_roll_filter + if consume('K', 'H') + # 大きな出目から複数個取る + Node::DiceRollWithFilter::KEEP_HIGHEST + elsif consume('K', 'L') + # 小さな出目から複数個取る + Node::DiceRollWithFilter::KEEP_LOWEST + elsif consume('D', 'H') + # 大きな出目から複数個除く + Node::DiceRollWithFilter::DROP_HIGHEST + elsif consume('D', 'L') + # 小さな出目から複数個除く + Node::DiceRollWithFilter::DROP_LOWEST + end end # トークンを消費する @@ -168,15 +194,16 @@ def term # トークンと期待した文字列が合致していた場合、次のトークンに進む。 # 合致していなかった場合は、進まない。 # - # @param [String] str 期待する文字列 + # @param [String] expected 期待する文字列 # @return [true] トークンと期待した文字列が合致していた場合 # @return [false] トークンと期待した文字列が合致していなかった場合 - def consume(str) - if @tokens[@idx] != str + def consume(*expected) + target = @tokens.slice(@idx, expected.length) + unless target == expected return false end - @idx += 1 + @idx += expected.length return true end diff --git a/src/dice/add_dice/randomizer.rb b/src/dice/add_dice/randomizer.rb index a62995cf2..345ba3254 100644 --- a/src/dice/add_dice/randomizer.rb +++ b/src/dice/add_dice/randomizer.rb @@ -43,8 +43,6 @@ def roll(times, sides, critical) return dice_groups end - private - # ダイスを振る(振り足しなしの1回分) # @param [Integer] times ダイス数 # @param [Integer] sides 面数 @@ -62,6 +60,8 @@ def roll_once(times, sides) return dice_str.split(',').map(&:to_i) end + private + def double_check? if @dicebot.sameDiceRerollCount != 0 # 振り足しありのゲームでダイスが二個以上 if @dicebot.sameDiceRerollType <= 0 # 判定のみ振り足し diff --git a/src/test/add_dice_parser_test.rb b/src/test/add_dice_parser_test.rb index bfcf61a0b..6eb810c9b 100644 --- a/src/test/add_dice_parser_test.rb +++ b/src/test/add_dice_parser_test.rb @@ -104,6 +104,70 @@ def test_parse_target_value_constant_fonding ) end + # 大きな出目から複数個取る + def test_parse_keep_high + test_parse( + '5D10KH3', + '(Command (DiceRollWithFilter 5 10 :KH 3))' + ) + end + + # 小さな出目から複数個取る + def test_parse_keep_low + test_parse( + '5D10KL3', + '(Command (DiceRollWithFilter 5 10 :KL 3))' + ) + end + + # 大きな出目から複数個除く + def test_parse_drop_high + test_parse( + '5D10DH3', + '(Command (DiceRollWithFilter 5 10 :DH 3))' + ) + end + + # 小さな出目から複数個除く + def test_parse_drop_low + test_parse( + '5D10DL3', + '(Command (DiceRollWithFilter 5 10 :DL 3))' + ) + end + + # 大きな値キープ機能、修正値付き + def test_parse_keep_high_with_modifier + test_parse( + '5D10KH3+1', + '(Command (+ (DiceRollWithFilter 5 10 :KH 3) 1))' + ) + end + + # 小さな値キープ機能、修正値付き + def test_parse_keep_low_with_modifier + test_parse( + '5D10KL3+1', + '(Command (+ (DiceRollWithFilter 5 10 :KL 3) 1))' + ) + end + + # 大きな値ドロップ機能、修正値付き + def test_parse_drop_high_with_modifier + test_parse( + '5D10DH3+1', + '(Command (+ (DiceRollWithFilter 5 10 :DH 3) 1))' + ) + end + + # 小さな値ドロップ機能、修正値付き + def test_parse_drop_low_with_modifier + test_parse( + '5D10DL3+1', + '(Command (+ (DiceRollWithFilter 5 10 :DL 3) 1))' + ) + end + private # 構文解析をテストする diff --git a/src/test/data/None.txt b/src/test/data/None.txt index 51681d637..b99bf5dfb 100644 --- a/src/test/data/None.txt +++ b/src/test/data/None.txt @@ -1098,3 +1098,51 @@ S2R6[3] output: DiceBot : (2R6[3]) > 3,2 + 1 > 成功数0###secret dice### rand:3/6,2/6,1/6 +============================ +input: +5D10KH3 大きな出目から3個取った後の和 +output: +DiceBot : (5D10KH3) > 21[2,3,5,8,8] > 21 +rand:8/10,3/10,2/10,5/10,8/10 +============================ +input: +5D10KL3 小さな出目から3個取った後の和 +output: +DiceBot : (5D10KL3) > 10[2,3,5,8,8] > 10 +rand:8/10,3/10,2/10,5/10,8/10 +============================ +input: +5D10DH3 大きな出目から3個除いた後の和 +output: +DiceBot : (5D10DH3) > 5[2,3,5,8,8] > 5 +rand:8/10,3/10,2/10,5/10,8/10 +============================ +input: +5D10DL3 小さな出目から3個除いた後の和 +output: +DiceBot : (5D10DL3) > 16[2,3,5,8,8] > 16 +rand:8/10,3/10,2/10,5/10,8/10 +============================ +input: +5D10KH0 取る数が0の場合でも動く +output: +DiceBot : (5D10KH0) > 0[2,3,5,8,8] > 0 +rand:8/10,3/10,2/10,5/10,8/10 +============================ +input: +5D10KH7 取る数が多すぎても動く +output: +DiceBot : (5D10KH7) > 26[2,3,5,8,8] > 26 +rand:8/10,3/10,2/10,5/10,8/10 +============================ +input: +2D20KH1+1 キープ機能修正値付き +output: +DiceBot : (2D20KH1+1) > 20[1,20]+1 > 21 +rand:1/20,20/20 +============================ +input: +2D20DH1+1 ドロップ機能修正値付き +output: +DiceBot : (2D20DH1+1) > 1[1,20]+1 > 2 +rand:1/20,20/20