【C#/Unity】出現率から項目を一つルーレット選択で選ぶ拡張メソッド

RPGなどで、出現率(選択確率)などからある項目を一つ選びたい場面がしばしばあると考えられます。 例えば、敵の各行動が行われる確率を以下のようにDictionary形式で表すとするとき

    var enemyActionProbs = new Dictionary<string, int>()
    {
        { "attack", 60 },
        { "guard", 30 },
        { "escape", 10 },
    };

attack,guard,escapeの各項目が、それぞれ60%、30%、10%の確率で得られるメソッドかなにかが欲しいわけです。 こういった場合、おそらく遺伝的アルゴリズムなどで用いられるルーレット選択を用いて得ることが一般的であると思われます(実際にはどうかわかりません。調べてもあまり出てこなかったので)。

簡単に説明すると、それぞれが選択される確率の総和をとり、0からその和までの乱数の値を決定し、 その値に応じた値を選択するのがルーレット選択です。 以下は適当に作った図ですが、例えば乱数の数が65だった場合、上の例だとguardが選ばれます。

f:id:Kanchi0914:20190930053935p:plain:w600

なおこの例では各確率の総和が100になるようにしていますが、別にそうしなくても構いません。 例えばアイテムの出現割合を記したDictionaryから一つを選びたいときなどにも使えます。

自分は今まではクラス毎に実装していたのですが、流石に効率が悪いのでジェネリックを用いた拡張メソッドを使って実装することにしました。

using System.Collections.Generic;

public static class DictExtensions
{
    public static T GetByRouletteSelection<T>(this Dictionary<T, int> dict)
    {
        var sum = 0;
        T selected = default;

        foreach (var key in dict.Keys)
        {
            sum += dict[key];
        }

        int tempSum = UnityEngine.Random.Range(0, sum);

        foreach (var key in dict.Keys)
        {
            tempSum -= dict[key];
            selected = key;
            if (tempSum < 0)
            {
                break;
            }
        }

        return selected;
    }
}

以下のように使います。キーの型は何でも構わないので自作クラス等をキーにしたDictionaryにも使うことができます。

    public void DictTest()
    {
        var enemyActionProbs = new Dictionary<string, int>()
        {
            { "attack", 60 },
            { "guard", 30 },
            { "escape", 10 },
        };
        
        for (int i = 0; i < 50; i++)
        {
            Debug.Log(enemyActionProbs.GetByRouletteSelection());
        }
    }

(実行結果)

attack
guard
attack
attack
attack
escape
attack
guard
guard
attack
attack
...