ボレロ村上 - ENiyGmaA Code

中3女子です。

吉里吉里で様々な乱数と確率分布を使う - ktl/VariateRandom.tjs

乱数を扱う吉里吉里用のネイティブプラグインを書いた。


ところで吉里吉里(TJS2)には、組み込みで Math.RandomGenerator クラスという疑似乱数生成クラスがある。

これはメルセンヌツイスタ法を使った高品質な疑似乱数エンジンであり、C言語の rand 関数などと比べて非常に優秀である。
ゲーム用途で範囲固定の一様分布な疑似乱数を必要とするならば、これを使っておけばまず問題ないだろう。


であるが、更に高機能・多機能なモジュールを提供してみたいと思うのはコーダの人情だ。
ここでは、更に進んだ乱数の運用について考えてみたい。


さしあたって、僕が公開しているネイティブプラグインの DLL、
必要ならば TJS スクリプトも必要なので、ダウンロードしてほしい。


なお実装には Boost.Random ライブラリを使用しており、インタフェースも似せている。
Boost は C++ の準標準ライブラリと呼ばれるものであり、非常に高機能で有用な多くのライブラリが公開されている。


まずは、TJS2 の Math.RandomGenerator クラスと等価な機能として使うにはどうするか。

    • Math.RandomGenerator を使う
// 乱数生成エンジン
var gen = new Math.RandomGenerator();

// 乱数生成 = [0..1) の実数
Debug.message(gen.random());

// 乱数生成 = [0..0xffffffff] の整数
Debug.message(gen.random32());

// 乱数生成 = [0..0x7fffffffffffffff] の整数
Debug.message(gen.random63());

// 乱数生成 = [-9223372036854775808..9223372036854775807] の整数
Debug.message(gen.random64());
    • VariateRandom を使う
// 乱数生成エンジン
var eng = new MT19937(RandomDevice.random());

// [0..1) の実数を返すジェネレータ
var gen = new VariateRandom(eng, new Uniform01());
Debug.message(gen.random());

// [0..0xffffffff] の整数を返すジェネレータ
var gen32 = new VariateRandom(eng, new UniformInt(0, 0xffffffff));
Debug.message(gen32.random());

// [0..0x7fffffffffffffff] の整数を返すジェネレータ
var gen63 = new VariateRandom(eng, new UniformInt(0, 0x7fffffffffffffff));
Debug.message(gen63.random());

// [-9223372036854775808..9223372036854775807] の整数を返すジェネレータ
var gen64 = new VariateRandom(eng, new UniformInt(-9223372036854775808, 9223372036854775807));
Debug.message(gen64.random());


まず大きく違うのは、Math.RandomGenerator はひとつの乱数生成エンジンが色々の範囲の乱数を返すメソッドを持っているのに対し、
VariateRandom は乱数生成エンジンと範囲(分布)を表すクラスが分かれていることである。


MT19937 クラスはネイティブプラグインが提供する乱数生成エンジンのひとつであり、
Math.RandomGenerator クラスの実装に使われているものとほぼ同じである。

RandomDevice クラスは外部デバイスを入力とする乱数生成エンジンであり、
Math.RandomGenerator クラスのデフォルトコンストラクタも同様な方法で乱数種を生成する。

Uniform〜 クラスはそれぞれ一様分布を表すクラスである。


VariateRandom クラスは、コンストラクタに乱数生成エンジン(engine)と分布(distribution)をとり、それらを組み合わせた乱数を返す。
ちなみに VariateRandom クラスを使わなくとも、下の二つの式は同じ意味になる。

distribution.random(engine);
(new VariateRandom(engine, distribution)).random();

しかし VariateRandom クラスは内部で呼び出しの最適化が為されていて高速なため、何度も乱数を生成する場合、
VariateRandom クラスのインスタンスを使うのが良いだろう。


さて、もう一度最初の二つのコードを見てみると、わざわざ値の範囲ごとに異なる VariateRandom インスタンスを使う下のコードは、
Math.RandomGenerator クラスを使う場合と比べて冗長に見えるかもしれない。


しかし考えてみるべきは、実際に乱数を運用するユースケースである。
例えば [0..0xffffffff] の固定範囲の乱数値をそのまま使うということは少ないはずだ。


例えばダイスを使うミニゲームを実装しようとしたとき、 [1..6] の整数範囲の乱数が必要になるだろう。
これは以下のように書ける。

var dice = new VariateRandom(eng, new UniformSmallint(1, 6));
Debug.message("ダイスロール: " + dice.random());
Debug.message("ダイスロール: " + dice.random());
Debug.message("ダイスロール: " + dice.random());

実に簡単だ。賽は投げられた。


また、RPGの戦闘におけるダメージ計算で乱数によって値を一定値から増減させるのは一般的な手法だ。
その際、一様な確率分布では、最大値や最小値に近い値が高確率で出やすいのでよろしくない。
そのような場合、正規分布という確率分布が使われることが多い。
正規分布を使うには以下のようにすればよい。

var nor = new VariateRandom(eng, new NormalDistribution());
Debug.message("正規分布: " + nor.random());
Debug.message("正規分布: " + nor.random());
Debug.message("正規分布: " + nor.random());

実に簡単だ。


もっと簡単な例では、例えば宝箱を開けたときに一定確率でアタリが出るという計算がある。
これにはベルヌーイ分布を使えばよい。

var treasure = new VariateRandom(eng, new BernoulliDistribution(0.7 /* 70% でアタリ */));
Debug.message("宝箱: " + (treasure.random() ? "アタリ!" : "ハズレ"));
Debug.message("宝箱: " + (treasure.random() ? "アタリ!" : "ハズレ"));
Debug.message("宝箱: " + (treasure.random() ? "アタリ!" : "ハズレ"));

実に簡単だ。


この他、おそらく限りなくニッチな部分まで、様々な乱数生成エンジンや確率分布のヴァリエーションを提供している。
もし吉里吉里でゲームを作るとき、リアルな確率論を用いた乱数を利用したい場合は、利用を考えてみては如何だろうか。


最後に、Boost かわいいよ Boost!!!