C#で汎用性の高いシンプルな乱数テーブルの実装・解説

#概要
色々な種類のアイテムや敵をランダムで出現させたい、かつ、
確率だけ異なるパターンを複数用意させたい……。
そんなときにお手軽に使える、乱数テーブルを実装したので、詳しくご紹介します。
実装環境は、Unity C#です。

#解説
今回実装するプログラムが最も有効活用出来るのは、
わかりやすい例でいうと、マリオカートのアイテムボックスのような例ですね。

マリオカートでは、8人でのレースの場合、1位~8位の、計8パターンが存在し、順位が低くなるほどレアなアイテムの出現確率が上がります。

例えば、バナナ・こうら・キノコ・スター、それぞれ４種類のアイテムが、1~8位で異なる確率で出現するとして、確率を百分率で表にすると、以下のようになります。

||バナナ|こうら|キノコ|スター|
|:---:|---:|---:|---:|---:|---:|
|1位  |60|30|10|0|
|2位  |40|40|20|0|
|3位  |20|50|30|0|
|4位  |0|60|40|0|
|5位  |0|40|40|20|
|6位  |0|20|40|40|
|7位  |0|0|40|60|
|8位  |0|0|20|80|

このようなテーブルを実際にプログラムとして実装する場合、通常の配列では1~8位それぞれの順位ごとの出現確率を8個分用意しなければなりません。
そこで今回は「ジャグ配列」という、配列を複数重ね合わせられる機能を使います。

#乱数のアルゴリズム
今回は８つのアイテムの出現確率を、状況によって３つの確率パターンに変化させて出現させます。
使用するジャグ配列は、以下の通りになります。

```
private int[][] itemTable = new int[][]
{
　　　　new int[] { 4, 4, 4, 4, 4, 4, 4, 4},
　　　　new int[] { 0, 0, 0, 0, 4, 4, 4, 4},
　　　　new int[] { 8, 7, 6, 5, 4, 3, 2, 1}
};
```

まず初めに、
```private int[][] itemTable = new int[][]```
このコードですが、通常の配列と異なり、かっこが2つ付いていることがわかります。
これがジャグ配列の宣言となります。

そして、itemTableという配列の中で、
```new int[] { 4, 4, 4, 4, 4, 4, 4, 4},```
```new int[] { 0, 0, 0, 0, 4, 4, 4, 4},```
```new int[] { 8, 7, 6, 5, 4, 3, 2, 1}```
これらの３つの配列を宣言しています。
さらにこれら３つの配列には、それぞれ8つの値を持っていることがわかります。
この値こそが、実際に乱数テーブルとしての役割を持つ値となります。

まず配列の1番目の中身の値、
```new int[] { 4, 4, 4, 4, 4, 4, 4, 4},```
では、4の値が8つ並んでいます。
（以降、この4に該当する値のことを「範囲値」と呼びます）

ランダムなアイテムを1つだけ選ぶ、という機能を実装する場合は「1~範囲値の合計(今回の場合は4*8=32)」の中からランダムな数値を1つ選び、その値がどの範囲値に属するかを調べます。
その計算方法についてですが、例えば範囲値の1番目が4となっています。これは1~4の範囲の値を取る、ということです。
次の範囲値も4なので、前回の範囲値(4)を足して、その次は5~8の範囲の値を取ります。
このように範囲値を足していくことで、乱数が何番目の範囲値に属するかを計算します。

範囲値の確率は「範囲値/合計値」で今回は全ての値が4なので、1~8番目の範囲値のどれに属するか、という確率は全て4/32となり、約分すると1/8となります。
確率的には
```new int[] { 1, 1, 1, 1, 1, 1, 1, 1},```
と同じですね。

配列の2番目では、
```new int[] { 0, 0, 0, 0, 4, 4, 4, 4},```
となっています。
配列の1番目と異なり、1~4番目の範囲値が0なので、乱数がこれらの範囲値に属することはありません。
このように選択したくない範囲値に対しては0を入れて、状況によって絶対に選択させない、といったことが可能です。
5~8番目の範囲値の確率はそれぞれ4/16となり、約分すると1/4となります。

配列の3番目は、
```new int[] { 8, 7, 6, 5, 4, 3, 2, 1}```
となっています。
範囲値の最初が8で、そこから-1ずつ範囲値が小さくなっていき、範囲値の最後は1となっています。
範囲値の合計値は1/2*8*(8+1)=36で、範囲値の確率は、8/36 ~ 1/36となります。
分母が36だと、ちょっとわかりづらいですね。
百分率で確率を表すと、以下の通りになります。

8/36 = 22.222...
7/36 = 19.444...
6/36 = 16.666...
5/36 = 13.888...
4/36 = 11.111...
3/36 = 8.333...
2/36 = 5.555...
1/36 = 2.777..
#コードと実装方法
```
private int[][] itemTable = new int[][]
{
    new int[] { 4, 4, 4, 4, 4, 4, 4, 4},
    new int[] { 0, 0, 0, 0, 4, 4, 4, 4},
    new int[] { 8, 7, 6, 5, 4, 3, 2, 1}
};

private int TableGenerate(int tableNumber)
{
    // 検索用の配列を代入
    var _itemTable = itemTable[tableNumber - 1];

    // 判定値を計算
    var totalNumber = 0;
    var searchTable = new int[_itemTable.Length];
    for (int i = 0; _itemTable.Length > i; i++)
    {
        totalNumber += _itemTable[i];
        searchTable[i] = totalNumber;
    }

    // 乱数を計算
    var randomNumber = Random.Range(1, totalNumber + 1);

    // 乱数がどの範囲値に属するか検索
    for (int i = 0; searchTable.Length > i; i++)
    {
        if (searchTable[i] >= randomNumber)
        {
            // 乱数が、「判定値よりも小さい」ならば結果を返す
            return i + 1;
        }
    }

    //randomNumberがsearchTableの最後の値より大きいことはありえないのでここに到達することはない
    return 0;
}
```
使い方はとても簡単で、最初(1番目)の配列のテーブルを使用したい場合は、
```var itemValue = TableGenerate(1);```
と書くだけで、itemValueに配列の1番目を使用した乱数の結果(1~8のどれか)が返されます。
状況合わせて自由に2番目や3番目のテーブルに切り替えることもできます。

#乱数のテスト
試しに３つの配列をそれぞれ8000・4000・8000(有効な範囲値の数*1000)回数分、乱数を生成して、それぞれの範囲値の値が返された回数を計算してみました。

|値|1番目の配列  |2番目の配列  |3番目の配列  |
|:---:|---:|---:|---:|
|1  |979  |-  |1727  |
|2  |1029  |-  |1573  |
|3  |1005  |-  |1360  |
|4  |971  |-  |1149  |
|5  |994  |983  |901  |
|6  |1027  |1003  |656  |
|7  |999  |1017  |402  |
|8  |996  |997  |232  |

平均出現回数は「試行回数 / 範囲値の数」なので、3つとも平均出現回数は1000となります。
実際の結果も、平均がだいたい1000に違い数値で、しっかりと確率通りになっていますね。

あとはこの結果の値をSwitch文なりで分岐し、それぞれ異なるアイテムをインスタンスとして生成すればOKです。

また、配列テーブルを変更するだけで、確率を自分好みに変更することができます。

お疲れさまでした:bow: