分布
一様分布
どの値の出現確率も同じ分布のことを言う。試行回数が少ないうちは結果が偏ることはあり得るが、試行を無限に増やせば(≒ たくさん試行すれば)、結果は収束していく。ただし、一様分布は結果が均等になることを示すものではなく、出現確率が均等であることを示すものなので、多少の結果のばらつきは起こり得る。
正規分布
正規分布は時折欲しくなる分布だ。自然現象のように大体は平均に落ち着くが、それを中心として少しばらつきが欲しいときにうってつけの乱数となる。しかし、一般的な正規分布におけるパラメータには平均 と標準偏差 しかなく、上限と下限を保証できない。
正規分布を沿う乱数という文脈では、上限と下限を決め打ちしたいのだが、ここで乱数が離散的でいいのであれば、もっと簡単でいい方法がある。ダイスを使うゲームの文脈では、6面ダイスを1個振ることを1D6と表記する。つまり2D6であれは2~12の目が出ることになるし、3D4では4~12だ。
個数が1であれば一様分布だが、2個3個と増やしていくことで正規分布になる。確認してみて欲しいのだが、2D6と3D4では分布が異なり、個数が多いほど標準偏差つまり分散(結果の散らばり)が小さくなっていく。すなわちダイスの面数と個数をうまく調整することで望む形に近い正規分布を生み出せるのだ。
ゲームにおける乱数
カルドセプト サーガ
すごろくを使ったゲームでダイスの出目が必ず奇数と偶数の繰り返しになるという不具合(仕様)を抱えたゲームの話だ。実装についてはゲームを逆アセンブルないしコンパイルしないとわからないので、詳細を論じることは控えるが、卓越したプログラマであっても乱数をうまく扱えないことがある。
擬似乱数というかarc4random()の話。にちょっとした小噺が記載されているが、乱数は
- よい乱数生成器を使う
- 得られた乱数を正しく使う
このふたつが要求されるので、片手落ちにならないようにしたい。
C言語でもstdlib.hに rand() があるが、注意事項にこの関数を使ってはならないとある。令和になった今でも巷ではこの関数を使ったコードが溢れているし、人々は同じ過ちを繰り返し続けている。
このrand()は曲者で、0から RAND_MAX までの値を返すとある。よく乱数を使った文脈では、得られた乱数値になんらかの操作を加えて特定の範囲の値に納めることがある。ここで rand() を使って6面ダイスの挙動を模倣するために次のようなコードを書くとする。
int dice() {
// あらかじめsrand()で初期化しておく
return rand() % 6 + 1;
}このコード自体は正しく動くものだ。しかし、乱数として正しく動くかは別の話だ。例えば RAND_MAX が9の場合、結果として1, 2, 3, 4, 5, 6, 1, 2, 3, 4のどれかが返るようになり、5と6の出現確率はほかよりも小さい。通常 RAND_MAX がそこまで小さいことはないし、大きくなるにつれて誤差の範囲になるが、うまく乱数を扱わないと出現確率に差が出ることもある。
ただ、現代ではそもそも [0, 1) の乱数を返す乱数生成器や指定した範囲の乱数を返す機能が提供されていることが当たり前になってきているので、こうした課題に直面する機会もなくなってきている。
秘宝伝説Sa・Ga 2
スクウェアが製作したゲームボーイ向けソフトの秘宝伝説Sa・Ga21をプレイしてみるとわかるのだが、キャラクターの成長段階つまり乱数によって行動結果が変わりうる状況では、戦闘中に攻撃を外し続けるということがしばしば発生する。実際に体験すると非常にイライラするし、単純に運が悪すぎたというよりも、疑似乱数がよくないように感じられるのだ。
Sa・Ga2の乱数テーブル2やcheapなゲーム攻略情報を見れば全体的に線形合同法が使われていることがわかる。これはゲームボーイという今となっては機能に乏しい機器において、高速で良い乱数を生成することは難しいことにも起因している。ゲーム中でプレイヤーはいろんな行動をするし、それによって都度乱数が消費されれば、およそランダムに見えるので総合的には納得できる作りにはなっているのだ。
ちなみに で算出できると言われている乱数表をプロットすると以下の図のようになる。横軸は で縦軸は (左上起点)になっている。乱数表を見せられただけではわからないが、なんとなく左上から右下にかけて特徴的な分布が視覚的に見てとれる。これが線形合同法の弱点であり、あまりよくない乱数と言われる所以でもある。

とはいえ、外部から乱数シードが得られないゲームボーイという機器においても線形合同法はゲームの乱数としては成立しているので、そこまで気にするほどのことではないのかもしれない。
ファイアーエムブレム
ファイアーエムブレムでは実効命中率なるものが採用されていて、確率が高ければよりそれが当たるように、低ければより当たらないように補正する仕組みになっていて、人間の感覚との差を吸収するようになっている。命中率99%の攻撃を外したときや1%の攻撃が当たってしまったときの落胆は大きなものになるが、期待を裏切らないように、そういうことは起きないようになっているのだ。
よくある話題
1/100を引いた
1回しかできないクジ引き等で当てたのならまあまあ珍しいほうと言えるが、何回も引けるクジの場合には話が変わってくる。繰り返しクジを引いたときに一度でも当たりを引く確率は意外と高いし、人間の感覚は当てにならないので、1/100という確率は思ったほど低くないし、状況によってはかなり大きな確率に感じられることがある。
例えばソーシャルゲームで確率1%のガチャを100回実行したときに一度でもあたる確率は であることから約63.3%であるし、おおよそ100回やれば半分以上の確率で1%は引ける。
また、99%の確率で1億円をもらえるが、1%の確率で死ぬ、というゲームが与えられたときに、日本国民を1億人として全員が実行すると、およそ100万人は命を落とすことになる。1%と聞くと小さい確率のように思えるかもしれないが、数で示されると100万人の中に入ってしまうのではないかというおそれは生じるので、ここの1%はいつもよりも大きく見える。
偏りがある
一様分布においては結果に偏りがあっても乱数の性質を満たすので、同じ値が連続で出たとしても何ら不思議ではないし問題でもない。しかし、それがしばしば発生するとか、再現性があるとか、そういうことになると話が変わってくる。
あとは人間の感じ方の問題もある。なにか重要な状況である数字を引いたとか引けなかったとか、強い印象を伴う場合には、その思い出だけが強調されてほかの結果の認識が薄れてしまうので、人間の脳が偏りを作り出してしまっていることも考えられる。
面白いことに私の観測範囲内では、TRPGプレイヤーは結果が平均化されることを望んでいるようだ。 のダイスを振ったときに1が4回も連続するのは奇跡的な話で、プログラム上のバグを疑われることもあるのだが、よく考えてみれば、4回ダイスを振ればどんな目が出ようともその結果は の確率で起きえる未来なのである。
セッションツールでよく利用されているBCDiceではKernel.rand()が使われていて、内部実装としてはメルセンヌツイスタであり、乱数的には問題ないと言える。ただ、ダイスロールがサービス化されていれば、スレッド間で乱数生成器を使いまわしているかどうかにもよるし、ほかのプレイヤーによって乱数が消費されていることもあるので、たまたまタイミング的に自分にとって都合の悪い結果を引いている可能性だってある。
乱数生成器の選定
暗号レベルの乱数が必要か?
環境によっては暗号に利用できるほどの安全性を備えた乱数生成器が提供されているが、ゲームでそこまでの強度は必要ないと考えている。特にゲームでは応答性が重要視される傾向にあるので、ハードウェアノイズから乱数を取り出すことは意図しないロックを引き起こしかねない。
UNIX環境であれば /dev/urandom から簡単にハードウェア乱数が取得できるが、連続で大量の乱数を読み出そうとするとロックが発生することがある。それに、ハードウェアのノイズ自体に個体差や偏りがないとも言い切れない。基本的には疑似乱数とハードウェア乱数のいいとこ取りをしたライブラリ任せにしたほうがうまくいくだろう。
また、WebのAPIなどで不特定多数に対して乱数や乱数を使った結果が提供される場合もあまり気にしなくていいだろう。乱数生成器がスレッド間で共有されているか否かにも依存するし、誰かによって乱数が消費されているかもしれない、さらにサーバーレス環境では長期間使われないとインスタンスがシャットダウンしてしまうこともあり、乱数生成器の状態を窺い知ることができないので、一様分布であることが保証されていれば大きな問題にはならない。
乱数生成器の変遷
歴史的には長らくメルセンヌツイスターが使われてきたが、近年GoやRustといった言語ではフットプリントが小さく、高速で、強度があるChaChaが使われているようだ。しかし、両者とも利点と欠点があり、一概にどちらがいいとも言えない。
それに最近流行りの言語では、使い方さえ間違えなければ既定の乱数生成器で十分すぎるほどの性能が得られるようになっていて、乱数生成器自体を気にする必要がない。ゲームはもちろん、パスワードをハッシュ化するときのsaltを生成する場合でも、そのときの状況を再現できないので問題ないだろう。
少なくとも乱数生成器が返す乱数が実は線形合同法によるものなんじゃないかとビクビクすることがない時代になったのはいいことだ。
リソースに制限がある場合
組込みシステム開発等で、扱えるメモリ容量やコード容量が強く制限されているときにはXorShiftがおすすめだ。ただ、この乱数生成器は適切なパラメータを設定する必要があり、その場合であっても一工夫がないとうまく動作しない欠点がある。詳細は以下の参考文献を確認して欲しい。
Footnotes
-
現在はSa・GA COLLECTIONでプレイできる ↩
-
これは戦闘時に使われる乱数テーブルではない ↩