この記事は、いろいろな言語・ゲームに用意されている乱数の実装例と、C# で実装した Xor shift RNGs による疑似乱数生成を使用する雰囲気をまとめた記事です。
いろんな言語 標準ライブラリーの乱数
C
GNU 実装の rand は、線形合同法のようです。C++ は 11 からメルセンヌツイスターを利用しているようです。
C#
System.Random のほうは Numerical Recipes に従った乱数を利用しているようです。
System.Security.Cryptography.RNGCryptoServiceProvider のほうは、暗号化サービス プロバイダー (CSP : Cryptographic Service Provider) によって提供される実装です。
Java
線形合同法の擬似乱数生成と暗号論的擬似乱数です。C# と同じような構成です。(他にもあるみたいです)
Python
Python は、乱数にメルセンヌツイスター。R や PHP なども同じ。
Ruby
PRNG は、修正メルセンヌツイスターとして実装されています。
JavaScript
JavaScript エンジンによってアルゴリズムの変更があるようです。
Unity
実装はよくわかりませんでしたが、一番利用しやすい形で乱数が提供されています。
ゲーム
乱数の扱い方が固まっていないころのゲームは、ロマンシングサガのような乱数表を使うことが多かったようです。カルドセプトサーガは、ダイスの目が偶数と奇数を交互に繰り返すバグがあり、店頭在庫回収になる事態がありました。
ポケットモンスターは、正確な出典元はわかりません(ROM 解析だろうし)でしたが、「乱数調整概論」の資料が参考になりそうです。
ファイアーエムブレムは、乱数を起動時は毎回固定にすることで「詰めエムブレム」という遊び方が提供されていました。
Xorshift による実装
中途半端なコードの長さだったので GitHub の「gist」に公開しました。
仕様の詳細は「xorshift - 論文 PDF」を参照。
Xorshift はシンプルなので、実装が簡単なのがよいところだと思いました。他にもサンプルがたくさんあると思うので、ちょっとひと手間加えて、乱数を使いやすくするためにノーマライズした乱数取得 0~1(実数)
、Range(min, max)
に対応しています。
実装方法の雰囲気は Unity の Random クラスを参考にしています。
乱数のポイント
ソフトウェアが実装する疑似乱数は、次のような特徴を持ちます
- シードの初期値から、ある回数で乱数が循環する
- シード値が同じなら、値が再現する
- 値がバラつく(分布する状態)
循環するまでの回数がとても長いこと、値のバラつきが十分であることが品質として評価されているようです。
ただし、乱数のバラつきを評価する方法は、「擬似乱数検証ツールの調査開発 - IPA」などでもまとめられていますが、かなり数学的で専門性を必要とします。
ここでは、よくある簡易テストの画像でチェックしてみます。
画像によるテスト
using System; using System.Drawing; using System.Drawing.Imaging; public void ImageTest() { var random = new RandomState(); var seed = DateTime.Now.Ticks; var width = 640; var height = 640; var white = 0; var black = 0; var bmp = new Bitmap(width, height); random.SetSeed(seed); for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { Color color; var r = random.Range(0, 1); if (r == 0) { color = Color.White; white += 1; } else { color = Color.Black; black += 1; } bmp.SetPixel(x, y, color); } } Console.WriteLine($"white:{white}({white / (double)(white + black) * 100:00.00}%), black:{black}({black / (double)(white + black) * 100:00.00}%), total:{white+black}({width}x{height})"); bmp.Save($"image-{DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss")} seed({seed}).png", ImageFormat.Png); }
画像を見てみると、白と黒が交互に発生している雰囲気はありません。サイコロをテストしたいなら6色でチェックすればより良さそうですね。(1~6の数字で偶数奇数のチェックをしてみるのも簡単ですね)
なお、同じシード値をつかってもう一回生成しても、同じ画像が作られます。これが再現です。
再現性が必要になる例を挙げると、ゲームのリプレイで、前回のプレイを見直す機能を実装する場合は、シード値を保存することで、乱数の再現をとります。
乱数は循環するものなので、エフェクトなどで不用意に乱数を消費すると、乱数の消費にズレが生じることもありえます。そのため、通常は再現性を必要とするゲーム部分と、再現性を必要としない部分(演出など)で乱数インスタンスを別々に使い分けるほうが無難な設計になるはずです。
(リプレイなど)の〇〇専用の乱数インスタンスが作れること。
静的クラスが提供する乱数しか用意していないと面倒になります。アプリケーションのあらゆる部分から乱数が消費される恐れがあるためです。Unity の Random っぽい設計や、今回、テストで作成した「Xorshift」のサンプルみたいな感じが好ましいこともあると思いました。
ファイアーエムブレムの命中率のテストケースの例
あるていど具体的なテストを試しにやってみます。
「ファイアーエムブレムの回避はあてにならない」、「スーパーロボット大戦のマサキの回避はよくあたる」といったジョークを加味したやつでどうでしょうか。設計上の問題で本当に命中率がズレてたら笑えない。
[TestCase(1234567890, 1000 * 1000 * 10, 70.0D)] public void Test_ファイアーエムブレムの命中率(long seed, long count, double percent) { var random = new RandomState(); var avoidPercent = 100 - percent; // 命中率を敵の回避率に変換 long hit = 0; long miss = 0; random.SetSeed(seed); for (int i = 0; i < count; i++) { int value = random.Range(0, 255); var hitPercent = (value / (double)255) * 100; // 命中率が回避率を上回ったとき、攻撃が成立する if (hitPercent >= avoidPercent) hit += 1; else miss += 1; } // 最小値と最大値の誤差率 0.01% 以下 Assert.AreEqual(percent, hit / (double)(hit + miss) * 100, 0.01D); Assert.AreEqual((100 - percent), miss / (double)(hit + miss) * 100, 0.01D); }
微妙に命中率がズレる実装バグの例を挙げるなら、random.Range(0, 256)
にして計算してしまうなどが考えられます。大きい数が1つ増えることになるので、命中率はやや大きくなりやすいかもしれません。
実際のところ、古い FE は乱数表を使っているため、乱数表の数値のバラつきの影響を強く受けます。循環するまでの回数も数百回なので悩ましい。
乱数表に似たような実装例として、アリスソフトのゲームは、ゲームを開始した時点からアイテムドロップテーブルが作成されていて、次にドロップするアイテムが常に決まっていたと思います。
ゲームリセットで乱数を初期化されてアイテムを拾い直す(厳選する)ようなプレイング対策や、ゲームの世界観にあわせるためなど、乱数は意外と考えるところがありそうに思いました。
計算式は「TAS動画まとめ - 各種計算式」を参考にしました。
サンプル
テストを含めた完全なプログラムは GitHub の「Samples」に公開しています。今回のプログラムは「XorshiftRandom」です。
参考
- 作者:伏見 正則
- メディア: 単行本
- 作者:四辻 哲章
- 発売日: 2010/06/01
- メディア: 単行本
- 作者:小柴 健史
- 発売日: 2014/11/28
- メディア: 単行本