3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++ 合成関数と逆関数; 艦これのダメージ検証における利用

3
Posted at

C++ 合成関数と逆関数; 艦これのダメージ検証における利用

これはC++ Advent Calendar 2025の7日目の記事です。

前日は@wx257osn2さんのRe: C++は常に進化している! C++26・C++23の新機能と今後のトレンドでした。

はじめに

艦これのダメージ検証での利用を目的としてC++で合成関数と逆関数を実装するとともに、簡単な利用例を示します。

『艦隊これくしょん -艦これ-』(以下、艦これ)は4000万人(公式発表・推定)のユーザを抱える世界的超大人気ブラウザゲームです。
艦これの攻略を支える、有志が検証して得られた各種情報は「柔らかなソーシャル」のもと日々提供されコミュニティを賑やかせています。
数多くある攻略情報の中でも、ダメージ検証は特に重要であり、艦娘や装備の采配に多大なる影響を与えます。
艦これのダメージ式は12ヶ年間に渡るダメージ検証によってほとんど明らかにされており、1さえ違うことなく正確に計算できます。

ゲームの更新などによって仮定しているダメージ式では説明できないダメージを観測したとき、検証者はダメージ式に対して任意に補正を仮定してダメージの説明を試みます。
この補正の仮定と解析に関わる数値計算を逆算といいます。

ダメージ式は少し複雑ですが、各補正を関数として解釈すれば簡単に合成関数で表せます。
加えて、合成された各関数に逆関数を定義すれば、逆算における方程式の解を容易に記述できるとともに、実装も容易となります。

艦これのダメージ式と逆算の概要

艦これのダメージ式の概形と、ダメージ検証における逆算の概要とについて述べます。
なお、簡単のためダメージ式の詳細や数式の厳密な取り回しは省いて近似で示しています。
詳細はインターネットでお求めください。

ダメージ式の概形

艦これのダメージは大まかには次のようになります:

$
ダメージ = \lfloor 攻撃力 - 防御力 \rfloor
$

攻撃力は、ベースとなる基本攻撃力に次々と補正を適用する形で、次のコードのように計算すると推測されています。
ソフトキャップ補正は、攻撃力が大きくなりすぎないように途中で適用されます。

// 基本攻撃力
double base;

// キャップ前補正1
if (cond1) {
    base *= a1;
    base += b1;
}

// キャップ前補正2
if (cond2) {
    base *= a2;
    base += b2;
}

// ソフトキャップ
base = softcap(base, cap);

// キャップ後補正3
if (cond3) {
    base *= a3;
    base += b3;
}

// キャップ後補正4
if (cond4) {
    base *= a4;
    base += b4;
}

ソフトキャップ補正$y=\mathrm{softcap(x)}$までの補正をキャップ前補正$y=\mathrm{precap}(x)$、あとの補正をキャップ後補正$y=\mathrm{postcap}(x)$といいます。
数式で表せば次のようになります:

$
攻撃力 = (\mathrm{softcap}((基本攻撃力 \times a_{1} + b_{1}) \times a_{2} + b_{2}) \times a_{3} + b_{3}) \times a_{4} + b_{4}
$

攻撃力がどうなっているのかとても読みづらいですが、ここで補正を関数としてとらえれば合成関数で表現でき、すっきり書けます:

$
攻撃力 = (\mathrm{postcap} \circ \mathrm{softcap} \circ \mathrm{precap})(基本攻撃力)
$

$
\mathrm{precap}(x) = (f_{2} \circ f{_1})(x)
$

$
\mathrm{postcap}(x) = (f_{4} \circ f{_3})(x)
$

$
f_{n} = xa_{n} + b_{n}, a_{n} > 0, n = 1,2,3,4
$

防御力は、概ね装甲値の0.7倍から1.3倍の乱数です。

$
防御力 = 装甲乱数
$

逆算

逆算では、ダメージ式に仮定した未知の補正の振る舞いを調べます。

例として、キャップ前補正の$f_{1}$の位置に未知の補正があると仮定して、$f_{1}(x)=xa_{1}+b_{1}, a_{1} > 0$の$a_{1}$を解きます。
観測したダメージを用いて攻撃力を表すと概ね次の形になります:

$
攻撃力 \in [ダメージ + 防御力)
$

各関数に逆関数が定義されているならば、

$
f_{1}(基本攻撃力) \in (f_{2}^{-1} \circ \mathrm{softcap}^{-1} \circ \mathrm{postcap}^{-1})([ダメージ + 最大防御力))
$

となり、ここで見やすさのために右辺を$Y$とおけば

$
a_{1} \in (Y - b_{1}) / 基本攻撃力
$

を得ます。

逆算は観測した全てのデータに対して行うため、各データで得られた$a_{1}$全てが重なる範囲を求めます。
重なる範囲があれば尤もらしい値に丸めて結論します。
重なる範囲が無ければ、仮定に誤りがあります。

例えばaとbと両方に補正値が設定されていれば上記方法で解くことは難しいと思います。
この場合は下図のようにプロットして関数の振る舞いを推測します。
図は実際のものを流用しているため補正位置($n$)が異なりますがだいたい同じです。

image.png
青($min$)と赤($sup$)とを二分する黄色の直線は、$f(x) = x \times 3.2 + 63.5$を表します。
余談ではありますが、浅学菲才の私にはこの直線の取り方がとても困難でありました。
参考: google slides

合成関数と逆関数の実装

関数の合成をoperator|のオーバーロードによって表現します。
どうしても数学表記と逆順になってしまいますが、<ranges>がそうであるのでよしとします。
もちろんf(x)で呼び出せます。

// f3 ∘ f2 ∘ f1 に等しい
const auto f = f1 | f2 | f3;

// 適当な引数与えて適用
const auto x = 42;
const auto y = f(x);

続いて、逆関数の取得をoperator^(int)のオーバーロードによって表現します。
つまり、f ^ -1fの逆関数を得ます。
f ^ 0f ^ 1でも同じく取得できますが、私の用途では構いません。
もし拡張が必要ならば適当なタグ型を引数にすれば解決できるでしょう。

// f^-1 ∘ f に等しい
const auto i = f | f ^ -1;

// 浮動小数点数演算の誤差を無視すれば、i(x)の結果はxに等しい 
const auto x  = 42;
const auto x_ = i(x);

合成関数クラスclass composed_functionの実装詳細について、二通りを考えました。

  1. 二つの型引数を受け取るもの

    template <typename F1, typename F2>
    class composed_function {
       public:
        template <typename T>
        constexpr auto operator()(T&& x) const;
    
       private:
        F1 f1_;
        F2 f2_;
    };
    
  2. 任意個の型引数を受け取るもの

    template <typename ...Fs>
    class composed_function {
       public:
        template <typename T>
        constexpr auto operator()(T&& x) const;
        
       private:
        std::tuple<Fs...> fs_;
    };
    

結論として、採用したのは任意個の型引数を受け取るものです。
なぜならば、逆算の実装が容易だからです。

operator|を実装します。
これはC++23<ranges>のGCC実装を真似ます。真似切れていませんがよしとしましょう。

// CRTPでoperator|による関数の合成を行うinterfaceを提供.
template <typename F>
    requires std::is_class_v<F> and std::same_as<F, std::remove_cv_t<F>>
class composable;

template <typename ...Fs>
class composed_function final : public coomposable<composed_function<Fs...>> {
   public:
    // 適当にコンストラクタを定義.
    constexpr composed_function(const Fs&... fs)
        : fs_{fs...} {}

    // 適当にコンストラクタを定義.
    constexpr composed_function(const std::tuple<Fs...>& fs)
        : fs_{fs} {}

    template <typename T>
    constexpr auto operator()(const T& x) const;

    // 合成関数に内部表現を取得するメンバ関数を追加.
    constexpr auto decompose() const noexcept -> const std::tuple<Fs...>& {
        return this->fs_;
    }
    
   private:
    std::tuple<Fs...> fs_;
};

// decomposedを呼び出せるかを確認するために定義.
// requires { f.decompose(); } でもよい.
template <typename... Fs>
inline constexpr bool is_composed_function_v = false;

template <typename... Fs>
inline constexpr bool is_composed_function_v<composed_function<Fs...>> = true;

// operator|による関数の合成を提供する. CRTP.
template <typename F>
    requires std::is_class_v<F> and std::same_as<F, std::remove_cv_t<F>>
class composable {
    template <typename G>
    friend constexpr auto operator|(const G& g, const F& f) {
        if constexpr (is_composed_function_v<G>) {
            if constexpr (is_composed_function_v<F>) {
                return composed_function{std::tuple_cat(g.decompose(), f.decompose())};
            } else {
                return composed_function{std::tuple_cat(g.decompose(), std::make_tuple(f))};
            }
        } else {
            if constexpr (is_composed_function_v<F>) {
                return composed_function{std::tuple_cat(std::make_tuple(g), f.decompose())};
            } else {
                return composed_function{g, f};
            }
        }
    }
};

次に、補正関数$f(x)=xa+b$とその逆関数を定義します。
特に変わった点は無いと思います。
敢えて言うならば、CRTPにしたためコンストラクタが必要なことと、実際においてauto liner_inverse::operator^(int) const noexcept -> inverse_function;は使わないことくらいです。
ソフトキャップ補正は区間演算型を用意しなければ定義できないので省略します。

class liner;
class liner_inverse;

class liner final : public composable<liner> {
   public:
    using inverse_function = liner_inverse;

    constexpr liner(double a = 1.0, double b = 0.0) noexcept
        : a_{a}
        , b_{b} {}

    constexpr double operator()(double x) const noexcept {
        return x * this->a_ + this->b_;
    }

    constexpr auto operator^(int) const noexcept -> inverse_function;

    constexpr double a() const noexcept {
        return this->a_;
    }

    constexpr double b() const noexcept {
        return this->b_;
    }

   private:
    double a_;
    double b_;
};

class liner_inverse final : public composable<liner_inverse> {
   public:
    using inverse_function = liner;

    constexpr liner_inverse(double a = 1.0, double b = 0.0) noexcept
        : a_{a}
        , b_{b} {}

    constexpr auto operator()(double x) const noexcept -> std::optional<double> {
        if (this->a_ == 0.0) {
            return std::nullopt;
        }

        return (x - this->b_) / this->a_;
    }

    constexpr auto operator^(int) const noexcept -> inverse_function;

    constexpr double a() const noexcept {
        return this->a_;
    }

    constexpr double b() const noexcept {
        return this->b_;
    }

   private:
    double a_;
    double b_;
};

constexpr auto liner::operator^(int) const noexcept -> inverse_function {
    return inverse_function{this->a_, this->b_};
}

constexpr auto liner_inverse::operator^(int) const noexcept -> inverse_function {
    return inverse_function{this->a_, this->b_};
}

合成される関数を定義しましたので、次はこの関数を呼び出せるようにします。
つまり、composed_functionのメンバ関数template <typename T> constexpr auto operator()(const T& x) const;を定義します。

その前に簡単にinvokeoverloadとを定義します:

template <typename T>
inline constexpr bool is_optional_v = false;

template <typename T>
inline constexpr bool is_optional_v<std::optional<T>> = true;

struct invoke_fn final {
    // オレが標準関数だ!!!
    // `using std::invoke;` で取り込めると嬉しい.
    template <typename F, typename... Args>
    static constexpr auto operator()(F&& f, Args&&... args) noexcept(std::is_nothrow_invocable_r_v<F, Args...>)
        -> std::invoke_result_t<F, Args...> {
        return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    }

    // 戻り値の型が std::optional<std::optional< ... T>> のように, optionalで何重にもラップしたものにしないため.
    template <typename F, typename T>
    static constexpr auto operator()(F&& f, const std::optional<T>& x) noexcept -> std::optional<T> {
        if constexpr (is_optional_v<std::invoke_result_t<F, T>>) {
            return x.and_then(std::forward<F>(f));
        } else {
            return x.transform(std::forward<F>(f));
        }
    }
};

inline constexpr auto invoke = invoke_fn{};

template <typename... Ts>
struct overload : public Ts... {
    using Ts::operator()...;
};

template <typename... Ts>
overload(Ts...) -> overload<Ts...>;

では、operator()を定義します。
併せてoperator^(int)も定義します。

template <typename... Fs>
class composed_function final : composable<composed_function<Fs...>> {
   public:
    constexpr composed_function(const Fs&... fs)
        : fs_{fs...} {}

    constexpr composed_function(const std::tuple<Fs...>& fs)
        : fs_{fs} {}

    template <typename T>
    constexpr auto operator()(const T& x) const {
        // tuple<0>から順に各関数を適用.
        return []<std::size_t... I>(const T& x, const std::tuple<Fs...>& fs, std::index_sequence<I...>) static {
            // オーバーロードセットに実装詳細を詰め込む.
            return overload{
                [](const auto& x) static { return x; },
                [](this auto&& self, const auto& x, const auto& f, const auto&... fs) {
                    return self(invoke(f, x), fs...);
                },
            }(x, std::get<I>(fs)...);
        }(x, this->fs_, std::index_sequence_for<Fs...>{});
    }

    constexpr auto operator^(int) const {
        // tupleの各要素を逆順で逆関数を取得し, これらを合成.
        return []<std::size_t... I>(const std::tuple<Fs...>& fs, std::index_sequence<I...>) static {
            // 型を明示しなければFs...で構築しようとしてエラーとなる.
            return composed_function<decltype(std::get<sizeof...(I) - 1 - I>(fs) ^ -1)...>{
                (std::get<sizeof...(I) - 1 - I>(fs) ^ -1)...
            };
        }(this->fs_, std::index_sequence_for<Fs...>{});
    }

    constexpr auto decompose() const noexcept -> const std::tuple<Fs...>& {
        return this->fs_;
    }

   private:
    std::tuple<Fs...> fs_;
};

ここまでで、以下を実行できるはずです:

#include <concepts>
#include <cstddef>
#include <optional>
#include <print>
#include <tuple>
#include <type_traits>
#include <utility>

// 上記コード片を整理してここに張り付け.

int main() {
    // 関数を合成して, 適用.
    const auto f1 = liner{1.1, 10};
    const auto f2 = liner{1.2, 20};
    const auto f3 = liner{1.3, 30};
    const auto f4 = liner{1.4, 40};
    const auto f5 = liner{1.5, 50};
    const auto f6 = liner{1.6, 60};
    const auto f  = f1 | (f2 | f3) | f4 | (f5 | f6);
    const auto x  = 42;
    const auto y  = f(x);
    std::println("{}", y);

    // 合成関数の型.
    using func_t = composed_function<
        liner,
        liner,
        liner,
        liner,
        liner,
        liner 
    >;
    static_assert(std::same_as<std::remove_cv_t<decltype(f)>, func_t>);

    // 合成関数に逆関数を適用して, 恒等変換.
    const auto i  = f | f ^ -1;
    const auto x_ = i(x);
    std::println("{}", x_.value());
}

逆算における利用例

続いて逆算を実装します。
とはいえ、区間演算を定義しなければきちんと解けないので雰囲気実装です。

template <typename T>
struct inverse_result {};

template <>
struct inverse_result<liner> {
    // a ∊ [a_min, a_sup)
    double a_min;
    double a_sup;

    // b ∊ [b_min, b_sup)
    double b_min;
    double b_sup;
};

// linerのaとbとについて解く. (連立しない)二元一次不等式を解く.
// ふつうには解けないので, b=0としてaを, a=1としてbを解く.
// 実際のダメージ検証では検証者がエスパーとなって, 舌の上に書かれた数値をb, aに設定してa, bを解く.
template <typename T, typename U>
auto solve(T x, U min, U sup, const liner& f) -> inverse_result<liner> {
    // 簡単のため, 0除算や区間演算は未考慮.
    // optionalの有効性も検証せず.
    if constexpr (is_optional_v<U>) {
        return inverse_result<liner>{
            .a_min = (*min - f.b()) / x,
            .a_sup = (*sup - f.b()) / x,
            .b_min = *min - x * f.a(),
            .b_sup = *sup - x * f.a(),
        };
    } else {
        return inverse_result<liner>{
            .a_min = (min - f.b()) / x,
            .a_sup = (sup - f.b()) / x,
            .b_min = min - x * f.a(),
            .b_sup = sup - x * f.a(),
        };
    }
}

// 逆算
// この実装では逆算結果をtupleに閉じ込めて返す.
// 戻り値の型: std::tuple< ignore_t, inverse_result<T1>, inverse_result<T2>, ... >
template <typename... Fs>
auto inverse(double x, double min, double sup, const composed_function<Fs...>& f) {
    // 合成関数の内部表現のtupleを展開
    return []<std::size_t... I>(auto x, auto min, auto sup, const std::tuple<Fs...>& fs, std::index_sequence<I...>) {
        // 実装詳細のオーバーロードセット.
        return overload{
            // 空のparameter packに対して:
            []([[maybe_unused]] auto x, [[maybe_unused]] auto min, [[maybe_unused]] auto sup) static {
                return std::make_tuple(std::ignore);
            },
            // 合成関数をなす各関数に対して:
            [](this auto&& self, auto x, auto min, auto sup, const auto& f, const auto&... fs) {
                // min ≦ y = (f3 ∘ f2 ∘ f1)(x) < sup に対して,
                // y_min ≦ (f2^-1 ∘ f3^-1)(y) = f1(x) < y_sup と表せる.
                // `solve`関数は, 例えばf1=linerのとき, y_min ≦ f1(x) < y_supにおいてf1のaとbとを解く.
                const auto inv   = composed_function{fs...} ^ -1;
                const auto y_min = invoke(inv, min);
                const auto y_sup = invoke(inv, sup);
                return std::tuple_cat(
                    self(invoke(f, x), min, sup, fs...),        //
                    std::make_tuple(solve(x, y_min, y_sup, f))  //
                );
            },
        }(x, min, sup, std::get<I>(fs)...);
    }(x, min, sup, f.decompose(), std::index_sequence_for<Fs...>{});
}


// 逆算諸元:
// 以下のとき, y_min=179.5, y_sup=180.5となった.
// f1とf2との間に未知の一次補正gが追加されたと仮定して, gのaを求める.
// const auto f1 = liner{1.1, 10};
// const auto f2 = liner{1.2, 20};
// const auto f3 = liner{1.3, 30};
// const auto f  = f1 | f2 | f3;
// const auto x  = 42;
// const auto y  = f(x);

int main() {
    const auto f1 = liner{1.1, 10};
    const auto g  = liner{};
    const auto f2 = liner{1.2, 20};
    const auto f3 = liner{1.3, 30};
    const auto f  = f1 | g | f2 | f3;
    const auto x  = 42;
    const auto y = f(x);
    std::println("y = {}", y);

    const auto y_min = 179.5;
    const auto y_sup = 180.5;

    const auto& [_, r3, r2, r, r1] = inverse(x, y_min, y_sup, f);

    const auto print = [](auto name, const inverse_result<liner>& r) static {
        std::println("{}: a=[{:.3f}, {:.3f}), b=[{:.3f}, {:.3f})", name, r.a_min, r.a_sup, r.b_min, r.b_sup);
    };
    print("f3", r3);
    print("f2", r2);
    print("g ", r);
    print("f1", r1);
}

これを実行すると次のようになると思います。

y = 143.672
f3: a=[1.296, 1.304), b=[29.513, 30.513)
f2: a=[1.195, 1.205), b=[19.625, 20.395)
g : a=[1.409, 1.420), b=[-0.312, 0.329)
f1: a=[1.095, 1.106), b=[9.779, 10.233)

求めたいのはgaなので、a=[1.409, 1.420)です。
この値が尤もらしいかを確認するため、例えばconst auto g = liner{1.41};として再実行すると、y ∊ [y_min, y_sup)であり、全ての補正値abが範囲に収まっていることを確認できます:

y = 179.61752
f3: a=[1.299, 1.308), b=[29.882, 30.882)
f2: a=[1.199, 1.209), b=[19.910, 20.679)
g : a=[1.409, 1.420), b=[-0.075, 0.566)
f1: a=[1.099, 1.110), b=[9.947, 10.401)
全体コード
// GCC15.2 -std=c++23 --pedantic-errorsで動作確認
#include <concepts>
#include <cstddef>
#include <functional>
#include <optional>
#include <print>
#include <tuple>
#include <type_traits>
#include <utility>

template <typename T>
inline constexpr bool is_optional_v = false;

template <typename T>
inline constexpr bool is_optional_v<std::optional<T>> = true;

struct invoke_fn final {
    template <typename F, typename... Args>
    static constexpr auto operator()(F&& f, Args&&... args) noexcept(std::is_nothrow_invocable_r_v<F, Args...>)
        -> std::invoke_result_t<F, Args...> {
        return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    }

    template <typename F, typename T>
    static constexpr auto operator()(F&& f, const std::optional<T>& x) noexcept -> std::optional<T> {
        if constexpr (is_optional_v<std::invoke_result_t<F, T>>) {
            return x.and_then(std::forward<F>(f));
        } else {
            return x.transform(std::forward<F>(f));
        }
    }
};

inline constexpr auto invoke = invoke_fn{};

template <typename... Ts>
struct overload : public Ts... {
    using Ts::operator()...;
};

template <typename... Ts>
overload(Ts...) -> overload<Ts...>;

template <typename F>
    requires std::is_class_v<F> and std::same_as<F, std::remove_cv_t<F>>
class composable;

template <typename... Fs>
class composed_function final : composable<composed_function<Fs...>> {
   public:
    constexpr composed_function(const Fs&... fs)
        : fs_{fs...} {}

    constexpr composed_function(const std::tuple<Fs...>& fs)
        : fs_{fs} {}

    template <typename T>
    constexpr auto operator()(const T& x) const {
        return []<std::size_t... I>(const T& x, const std::tuple<Fs...>& fs, std::index_sequence<I...>) static {
            return overload{
                [](const auto& x) static { return x; },
                [](this auto&& self, const auto& x, const auto& f, const auto&... fs) {
                    return self(invoke(f, x), fs...);
                },
            }(x, std::get<I>(fs)...);
        }(x, this->fs_, std::index_sequence_for<Fs...>{});
    }

    constexpr auto operator^(int) const {
        return []<std::size_t... I>(const std::tuple<Fs...>& fs, std::index_sequence<I...>) static {
            // 型を明示しなければFs...で構築しようとしてエラーとなる.
            return composed_function<decltype(std::get<sizeof...(I) - 1 - I>(fs) ^ -1)...>{
                (std::get<sizeof...(I) - 1 - I>(fs) ^ -1)...
            };
        }(this->fs_, std::index_sequence_for<Fs...>{});
    }

    constexpr auto decompose() const noexcept -> const std::tuple<Fs...>& {
        return this->fs_;
    }

   private:
    std::tuple<Fs...> fs_;
};

template <typename... Fs>
inline constexpr bool is_composed_function_v = false;

template <typename... Fs>
inline constexpr bool is_composed_function_v<composed_function<Fs...>> = true;

/// @brief operator|による関数の合成を提供する. CRTP.
template <typename F>
    requires std::is_class_v<F> and std::same_as<F, std::remove_cv_t<F>>
class composable {
    template <typename G>
    friend constexpr auto operator|(const G& g, const F& f) {
        if constexpr (is_composed_function_v<G>) {
            if constexpr (is_composed_function_v<F>) {
                return composed_function{std::tuple_cat(g.decompose(), f.decompose())};
            } else {
                return composed_function{std::tuple_cat(g.decompose(), std::make_tuple(f))};
            }
        } else {
            if constexpr (is_composed_function_v<F>) {
                return composed_function{std::tuple_cat(std::make_tuple(g), f.decompose())};
            } else {
                return composed_function{g, f};
            }
        }
    }
};

class liner;
class liner_inverse;

class liner final : public composable<liner> {
   public:
    using inverse_function = liner_inverse;

    constexpr liner(double a = 1.0, double b = 0.0) noexcept
        : a_{a}
        , b_{b} {}

    constexpr double operator()(double x) const noexcept {
        return x * this->a_ + this->b_;
    }

    constexpr auto operator^(int) const noexcept -> inverse_function;

    constexpr double a() const noexcept {
        return this->a_;
    }

    constexpr double b() const noexcept {
        return this->b_;
    }

   private:
    double a_;
    double b_;
};

class liner_inverse final : public composable<liner_inverse> {
   public:
    using inverse_function = liner;

    constexpr liner_inverse(double a = 1.0, double b = 0.0) noexcept
        : a_{a}
        , b_{b} {}

    constexpr auto operator()(double x) const noexcept -> std::optional<double> {
        if (this->a_ == 0.0) {
            return std::nullopt;
        }

        return (x - this->b_) / this->a_;
    }

    constexpr auto operator^(int) const noexcept -> inverse_function;

    constexpr double a() const noexcept {
        return this->a_;
    }

    constexpr double b() const noexcept {
        return this->b_;
    }

   private:
    double a_;
    double b_;
};

constexpr auto liner::operator^(int) const noexcept -> inverse_function {
    return inverse_function{this->a_, this->b_};
}

constexpr auto liner_inverse::operator^(int) const noexcept -> inverse_function {
    return inverse_function{this->a_, this->b_};
}

template <typename T>
struct inverse_result {};

template <>
struct inverse_result<liner> {
    double a_min;
    double a_sup;
    double b_min;
    double b_sup;
};

// linerのaとbとについて解く. (連立しない)二元一次不等式を解く.
// ふつうには解けないので, b=0としてaを, a=1としてbを解く.
// 実際のダメージ検証では検証者がエスパーとなって, 舌の上に書かれた数値をb, aに設定して試す.
template <typename T, typename U>
auto solve(T x, U min, U sup, const liner& f) -> inverse_result<liner> {
    // 簡単のため, 0除算や区間演算は未考慮.
    // optionalの有効性も検証せず.
    if constexpr (is_optional_v<U>) {
        return inverse_result<liner>{
            .a_min = (*min - f.b()) / x,
            .a_sup = (*sup - f.b()) / x,
            .b_min = *min - x * f.a(),
            .b_sup = *sup - x * f.a(),
        };
    } else {
        return inverse_result<liner>{
            .a_min = (min - f.b()) / x,
            .a_sup = (sup - f.b()) / x,
            .b_min = min - x * f.a(),
            .b_sup = sup - x * f.a(),
        };
    }
}

// 逆算
// この実装では逆算結果をtupleに閉じ込めて返す -> std::tuple< inverse_result<T1>, inverse_result<T2>, ... >
template <typename... Fs>
auto inverse(double x, double min, double sup, const composed_function<Fs...>& f) {
    // 合成関数の内部表現のtupleを展開
    return []<std::size_t... I>(auto x, auto min, auto sup, const std::tuple<Fs...>& fs, std::index_sequence<I...>) {
        // "inverse_impl"に相当するオーバーロードセット.
        return overload{
            // 空のparameter packに対して:
            []([[maybe_unused]] auto x, [[maybe_unused]] auto min, [[maybe_unused]] auto sup) static {
                return std::make_tuple(std::ignore);
            },
            // 合成関数をなす各関数に対して:
            [](this auto&& self, auto x, auto min, auto sup, const auto& f, const auto&... fs) {
                // min ≦ y = (f3 ∘ f2 ∘ f1)(x) < sup に対して,
                // y_min ≦ (f2^-1 ∘ f3^-1)(y) = f1(x) < y_sup と表せる.
                // `solve`関数は, 例えばf1=linerのとき, y_min ≦ f1(x) < y_supにおいてf1のaとbとを解く.
                const auto inv   = composed_function{fs...} ^ -1;
                const auto y_min = invoke(inv, min);
                const auto y_sup = invoke(inv, sup);
                return std::tuple_cat(
                    self(invoke(f, x), min, sup, fs...),        //
                    std::make_tuple(solve(x, y_min, y_sup, f))  //
                );
            },
        }(x, min, sup, std::get<I>(fs)...);
    }(x, min, sup, f.decompose(), std::index_sequence_for<Fs...>{});
}

int main() {
    {
        // 関数を合成して, 適用.
        const auto f1 = liner{1.1, 10};
        const auto f2 = liner{1.2, 20};
        const auto f3 = liner{1.3, 30};
        const auto f4 = liner{1.4, 40};
        const auto f5 = liner{1.5, 50};
        const auto f6 = liner{1.6, 60};
        const auto f  = f1 | (f2 | f3) | f4 | (f5 | f6);
        const auto x  = 42;
        const auto y  = f(x);
        std::println("{}", y);

        // 合成関数の型.
        using func_t = composed_function<
            liner,  //
            liner,  //
            liner,  //
            liner,  //
            liner,  //
            liner   //
            >;
        static_assert(std::same_as<std::remove_cv_t<decltype(f)>, func_t>);

        // 合成関数に逆関数を適用して, 恒等変換.
        const auto i  = f | f ^ -1;
        const auto x_ = i(x);
        std::println("{}", x_.value());
    }

    {
        const auto f1 = liner{1.1, 10};
        const auto g  = liner{};
        const auto f2 = liner{1.2, 20};
        const auto f3 = liner{1.3, 30};
        const auto f  = f1 | g | f2 | f3;
        const auto x  = 42;
        std::println("f(x) = {}", f(x));

        const auto y_min = 179.5;
        const auto y_sup = 180.5;

        const auto& [_, r3, r2, r_, r1] = inverse(x, y_min, y_sup, f);

        const auto print = [](auto name, const inverse_result<liner>& r) {
            std::println("{}: a=[{:.3f}, {:.3f}), b=[{:.3f}, {:.3f})", name, r.a_min, r.a_sup, r.b_min, r.b_sup);
        };
        print("f3", r3);
        print("f2", r2);
        print("g ", r_);
        print("f1", r1);
    }
}

以上、合成関数と逆関数とを利用して逆算できました🎉

読者への課題として、未知の一次補正がf2f3との間にある場合を仮定してbについて解いてください。
少ない変更で問題を解けることと思います。

あとがき

本日2025-12-07はダメージ検証用スプレ1の公開からちょうど10年になります。
このスプレッドシートが艦これのダメージ検証の一端を担ってきました。
しかしながら、艦これの12ヶ年間というのはとても長いもので、プレイヤーの絶対数はもちろんのこと、検証人口も減っています。
全盛期ではデータを募ればたくさんもらえたようですが、いまとなっては検証者がいないありさまです。
私自身でさえ、近頃は検証の実働がありません。
近年ではDBにデータを送信してあとはそのDBにアクセスできる人だけが検証する...といった形になっています。
技術のありようとしては尤もですが、攻略情報が天から降ってくるわけですから「柔らかなソーシャル」に対立すると私は思います。
と、ここに綴っても仕方がないのでこれくらいにします。
もし、他のゲームでダメージ検証を行う際は、12ヶ年間の積み重ねのある艦これのダメージ検証を参考にしていただければ幸いです。
いまから艦これを...と勧めるのは少し心苦しいですが、アカウントをお持ちの方は再ログインだけでもお願いいたします。
未知を追いかけるダメージ検証...楽しかったな...

はーやくこいこいstd::interval

  1. ダメージ検証用スプレ その最新版
    私が艦これを始めて間もない頃、このスプレ(の当時の版)を目にして脳を焼かれました。原体験です。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?