lib/bcdice/game_system/CthulhuTech.rb
# frozen_string_literal: true
require 'bcdice/arithmetic_evaluator'
module BCDice
module GameSystem
class CthulhuTech < Base
register_prefix('\d+D10')
# ゲームシステムの識別子
ID = 'CthulhuTech'
# ゲームシステム名
NAME = 'クトゥルフテック'
# ゲームシステム名の読みがな
SORT_KEY = 'くとうるふてつく'
# ダイスボットの使い方
HELP_MESSAGE = <<~INFO_MESSAGE_TEXT
・行為判定(test):nD10+m>=d
n個のダイスを使用して、修正値m、難易度dで行為判定(test)を行います。
修正値mは省略可能、複数指定可能(例:+2-4)です。
成功、失敗、クリティカル、ファンブルを自動判定します。
例)2D10>=12 4D10+2>=28 5D10+2-4>=32
・対抗判定(contest):nD10+m>d
行為判定と同様ですが、防御側有利のため「>=」ではなく「>」を入力します。
ダメージダイスも表示します。
INFO_MESSAGE_TEXT
# 行為判定のノード
class Test
# 判定で用いる比較演算子
#
# 対抗判定で変えられるように定数で定義する。
COMPARE_OP = :>=
# ノードを初期化する
# @param [Integer] num ダイス数
# @param [Integer] modifier 修正値
# @param [Integer] difficulty 難易度
def initialize(num, modifier, difficulty)
@num = num
@modifier = modifier
@difficulty = difficulty
end
# 判定を行う
# @param randomizer [Randoizer]
# @return [String] 判定結果
def execute(randomizer)
dice_values = randomizer.roll_barabara(@num, 10)
# ファンブル:出目の半分(小数点以下切り上げ)以上が1の場合
fumble = dice_values.count(1) >= (dice_values.length + 1) / 2
sorted_dice_values = dice_values.sort
roll_result = calculate_roll_result(sorted_dice_values)
test_value = roll_result + @modifier
diff = test_value - @difficulty
# diff と @difficulty との比較の演算子が変わるので、send で対応
# 例:COMPARE_OP が :>= ならば、diff >= 0 と同じ
success = !fumble && diff.send(self.class::COMPARE_OP, 0)
critical = diff >= 10
output_parts = [
"(#{expression()})",
test_value_expression(sorted_dice_values, roll_result),
test_value,
result_str(success, fumble, critical, diff)
]
return output_parts.join(' > ')
end
private
# 数式表現を返す
# @return [String]
def expression
modifier_str = Format.modifier(@modifier)
return "#{@num}D10#{modifier_str}#{self.class::COMPARE_OP}#{@difficulty}"
end
# 判定値の数式表現を返す
# @param [Array<Integer>] dice_values 出目の配列
# @param [Integer] roll_result ダイスロール結果の値
# @return [String]
def test_value_expression(dice_values, roll_result)
dice_str = dice_values.join(',')
modifier_str = Format.modifier(@modifier)
return "#{roll_result}[#{dice_str}]#{modifier_str}"
end
# 判定結果の文字列を返す
# @param [Boolean] success 成功したか
# @param [Boolean] fumble ファンブルだったか
# @param [Boolean] critical クリティカルだったか
# @param [Integer] _diff 判定値と難易度の差
# @return [String]
def result_str(success, fumble, critical, _diff)
return 'ファンブル' if fumble
return 'クリティカル' if critical
return success ? '成功' : '失敗'
end
# ダイスロール結果を計算する
#
# 以下のうち最大のものを返す。
#
# * 出目の最大値
# * ゾロ目の和の最大値
# * ストレート(昇順で連続する3個以上の値)の和の最大値
#
# @param [Array<Integer>] sorted_dice_values 昇順でソートされた出目の配列
# @return [Integer]
def calculate_roll_result(sorted_dice_values)
highest_single_roll = sorted_dice_values.last
sum_of_highest_set_of_multiples = sorted_dice_values
.group_by(&:itself)
.values
.map(&:sum)
.max
candidates = [
highest_single_roll,
sum_of_highest_set_of_multiples,
sum_of_largest_straight(sorted_dice_values)
]
return candidates.max
end
# ストレートの和の最大値を求める
#
# ストレートとは、昇順で3個以上連続した値のこと。
#
# @param [Array<Integer>] sorted_dice_values 昇順にソートされた出目の配列
# @return [Integer] ストレートの和の最大値
# @return [0] ストレートが存在しなかった場合
def sum_of_largest_straight(sorted_dice_values)
# 出目が3個未満ならば、ストレートは存在しない
return 0 if sorted_dice_values.length < 3
# ストレートの和の最大値
max_sum = 0
# 連続した値の数
n_consecutive_values = 0
# 連続した値の和
sum = 0
# 直前の値
# 初期値を負の値にして、最初の値と連続にならないようにする
last = -1
sorted_dice_values.uniq.each do |value|
# 値が連続でなければ、状態を初期化する(現在の値を連続1個目とする)
if value - last > 1
n_consecutive_values = 1
sum = value
last = value
next
end
# 連続した値なので溜める
n_consecutive_values += 1
sum += value
last = value
# ストレートならば、和の最大値を更新する
if n_consecutive_values >= 3 && sum > max_sum
max_sum = sum
end
end
return max_sum
end
end
# 対抗判定のノード
class Contest < Test
# 判定で用いる比較演算子
COMPARE_OP = :>
# 判定結果の文字列を返す
#
# 成功した場合(クリティカルを含む)、ダメージロールのコマンドを末尾に
# 追加する。
#
# @param [Boolean] success 成功したか
# @param [Integer] diff 判定値と難易度の差
# @return [String]
def result_str(success, _fumble, _critical, diff)
formatted = super
if success
damage_roll_num = (diff / 5.0).ceil
damage_roll = "#{damage_roll_num}D10"
"#{formatted}(ダメージ:#{damage_roll})"
else
formatted
end
end
end
# ダイスボットを初期化する
def initialize(command)
super(command)
# 加算ロールで出目をソートする
@sort_add_dice = true
end
# ダイスボット固有コマンドの処理を行う
# @param [String] command コマンド
# @return [String] ダイスボット固有コマンドの結果
# @return [nil] 無効なコマンドだった場合
def eval_game_system_specific_command(command)
node = parse(command)
return nil unless node
return node.execute(@randomizer)
end
private
# 判定コマンドの正規表現
TEST_RE = /\A(\d+)D10((?:[-+]\d+)+)?(>=?)(\d+)\z/.freeze
# 構文解析する
# @param [String] command コマンド
# @return [Test, Contest] 判定のノード
# @return [nil] 無効なコマンドだった場合
def parse(command)
m = TEST_RE.match(command)
return nil unless m
num = m[1].to_i
modifier = m[2] ? ArithmeticEvaluator.eval(m[2]) : 0
node_class = m[3] == '>' ? Contest : Test
difficulty = m[4].to_i
return node_class.new(num, modifier, difficulty)
end
end
end
end