TRPG紹介の置き場所

遊んだTRPGを紹介してアフィで儲けて新しいTRPGを買うためのブログ(赤字)

TRPGとランダマイザとプログラム(メルセンヌ・ツイスタを添えて)

TRPGといえばダイス。ダイスを使わないTRPGは(あんまり)ありません。

ダイスを使うのは「ランダムな数値を必要とする」からです。こういう、何らかの方法論に基づいてランダムな結果を得るための道具を「ランダマイザ」と呼びます。日本語だと「乱数器」ですね。

ダイスは物理的なランダマイザですが、他にもたくさんのランダマイザがあります。また、オンラインで行うセッションでは、プログラムで作成されたランダマイザを使うこともあるでしょう。(ダイスロールしてくれるチャットボットなど)

このエントリでは、プログラムにおけるランダマイザの妥当性について解説します。けどめっちゃ長くなっちゃったしあんまわかりやすくないので悲しい。

f:id:shino_gamer:20191224212315p:plain

優秀なランダマイザとそうでないランダマイザの差

当たり前のことですが、ランダマイザは結果の偏りが小さいほど優秀です。

……当たり前ですか? では、「結果の偏りが小さい」とは、どういうことか、厳密に考えてみましょう。

連続した出目

例えば、ある六面ダイスを無作為に3度ロールした時、結果が「4、5、6」だったとします。このダイスは優秀なランダマイザか、あるいはそうでないランダマイザか、どちらでしょうか?

答えは「わからない」です。

え? でも、明らかに出目が偏っているように思います。4よりも大きな目が3回も連続で出たのですから、その確率は 1/8 ですよね。直感的には、このダイスは「出目が偏ったダイス」のように思えます。

しかしこれが無作為な10回のロールの一部だったらどうでしょうか。例えば「1、2、4、5、6、2、3、4、1、6」こういう出目だったらどうでしょう。これは、直感的にも偏ってるとは感じられませんね。

六面ダイスをロールした時に、3回連続して4より大きな出目が出る確率は 1/8 です。しかし、それが10回の連続したダイスロールの一部に含まれる確率はそう低くはありません。計算してみたら、50%くらいでした。つまり、無作為に10回ロールすれば、その中にはそこそこの確率で「高い出目が連続するタイミング」が含まれているわけです。したがって、たまたま3回連続で高い出目が出たからといって、全体としてそのダイスの出目が偏っているとは言い切れません。

ではどんな結果なら、そのダイスが偏っている=優秀なランダマイザではない、と言えるのでしょうか。

基準を適当に決めるしかありません。例えば「100回ロールした時に、1〜6までのそれぞれの出目が15%~17%に収まらなかったら、偏っている」という具合です。少なくとも「たった3回の結果」では偏っているとも偏っていないとも言えてしまうのです。もちろん、100回のダイスロールで調べるよりも、1000回のダイスロールで調べたほうが、より確かな結果が得られるでしょう。

「どどんとふで3回連続して90以上の出目だったので、偏っている!」とは言えないわけです。

偏りの予測性

もう一つ、ランダマイザの優秀さの指標があります。それは、「それまでの結果から次の結果が予想しにくいこと」です。言い換えれば、「それまでの結果と次の結果の間に、規則が存在しないこと」となります。

ここに2つの六面ダイスがあります。片方は普通のダイスで、もう片方は「奇数回目のロールでは奇数が、偶数回目のロールでは偶数が出る」という魔法がエンチャントされたダイスです。この二つのダイスのうち、どちらがより優秀なランダマイザかというと、もちろん普通のダイスのほうが優秀ということになります。しかしそれも、場合によります。

どちらのダイスについても、その場でのロールの記録が残っていないならば、この2つのダイスの性能は同じに見えます。ダイスロールする人が出目を当てるゲームをする時に、普通のダイスであれ、魔法のダイスであれ、次の出目はわからないからです。(魔法のダイスが作られてからその場に置かれるまでに、何度ダイスロールされたのかは、未知の情報であるという点に注意してください。奇数回ロールされたダイスなのか、偶数回ロールされたダイスなのかは、わかりません)

しかし魔法のダイスをすでに1回でもロールしていて、その記録が残っている場合、魔法のダイスのランダム性は損なわれます。出目を当てるゲームの勝率は 1/6 から 1/3 に上昇するでしょう。一つ前の出目が奇数なら、次は偶数(1,4,6)が出ますし、一つ前の出目が偶数なら、次は奇数(1,3,5)になります。

魔法のダイスを優秀なランダマイザにすることもできます。魔法のダイスを例えば100個ほど用意して、箱の中にでも入れておきます。そして、使う前にいつもその箱から無作為に取り出してロールします。そうすれば、ダイスロールを行う人物にとっては、取り出したダイスが次に偶数を出すのか奇数を出すのか予想できなくなり、十分に優秀なランダマイザと言えるようになるでしょう。

物理的ランダマイザ

物理的なランダマイザは数多く存在します。TRPGボードゲームでおなじみのダイスはもちろんランダマイザの一つです。他には「コイン」「カード」「ルーレット」などもありますね。

ランダマイザとしての優秀さは、そのランダマイザの物理的な特性に依存します。もっと言えば、形や重心によって決まります。現在のようにほぼ均質なダイスであれば、それぞれの面の出る確率は十分に等しいと言えるでしょう。

しかし物理的なランダマイザも、実際に「無作為に」結果を示すわけではありません。

「ある時点において作用している全ての力学的・物理的な状態を完全に把握・解析する能力を持つ」知性を仮定しましょう。物理シュミレーション内部のダイスロールをイメージしてもいいですし、知っている人はラプラスの悪魔を思い浮かべるかもしれません。このエントリではラプラスの悪魔に因んで、この知性を「悪魔」と呼ぶことにします。

さて、悪魔はすべての力学的・物理的な状態を完全に把握できるので、ある人物(ただの人間です)がダイスロールする時に、その手の動き、何秒後に手を離すのか、ダイスにかかる風や空気抵抗、テーブルの摩擦係数や振動、そういったすべてを考慮して結果をすでに知っていることができます。人間にはとても真似できない計算量ですが、悪魔にとってそれは造作も無いことです。したがって、六面ダイスは十分なランダマイザではないと言えます。

(厳密には、この悪魔の存在は量子論によって否定されています。未来を完全に予想することはできません。しかし、この悪魔はたかだか一回のダイスロールの結果であれば、かなり高い確率で言い当てることができるでしょう)

人間にとって優秀なランダマイザであっても、悪魔にとっては全く優秀ではないランダマイザである。果たしてこのランダマイザは、TRPGにおいて十分なランダマイザでしょうか?

答えは「YES」です。なぜなら、私達は人間ですから、人間にとって十分ランダムなら、それで良いわけです。

プログラムされたランダマイザ

さて、ランダマイザをプログラムしましょう。実は簡単ではありません。

コンピュータは「計算を行う装置」ですが、ランダムな数値を作る物理的な能力を持っているわけではありません。なので、なんとか計算の組み合わせによってランダマイザを作る必要があります。(そういう能力を持っているコンピュータもあります)

その前に、「どのくらい優秀なランダマイザだったらTRPGで十分使える」のかを検討しておきましょう。……といってもこれは単純で、「使っている人間が次の出目を予測できない」という程度になるでしょう。更に優秀なランダマイザを目指すなら、「コンピュータにすら次の出目を予測できない」くらいになりますが、ここまでの性能は不要です。

時刻を使ってみた

ダイスロールする瞬間(つまりプログラムが動作した瞬間)のコンピュータ時刻を採取し、例えばマイクロ秒(1秒を1,000,000分割した秒数)を使って 1 〜 6 の結果を出すプログラムを書いてみます。

先に書いておきますが、このプログラムには欠陥があり、うんこです。

(Time.now.usec.to_f / 1000000 * 6).to_i + 1

ruby というプログラム言語を使っています。 Time.now.usec.to_f というのは、現在時刻のマイクロ秒を小数点形式で取得するという意味です。https://apidock.com/ruby/Time/usec

マイクロ秒を取得すれば 0 〜 1,000,000 までの間の数値が取れるので、それを 0.0 〜 1.0 に変換(1000000で割る)して、6倍にした結果から小数点以下を切り捨て、1を加算します。小数点周りにすごい雑さが見えますが、とりあえず目をつむってください。

このプログラムを例えば1000回繰り返し実行すると、すべての出目が同じか、まあ2通りとか3通りの出目に収まります。

f:id:shino_gamer:20191224184727p:plain

実際の実行画面

何が起こっているのか把握するために、プログラムを少し修正してみます。具体的には、マイクロ秒と計算した出目を併記してみます。

f:id:shino_gamer:20191224185107p:plain

左の数字がマイクロ秒、右の数字が出目

マイクロ秒の数値は 0 〜 1,000,000 までの整数ですが、これは1秒に対応しています。したがって、最終的な出目は1/6秒ごとにしか変化しないわけです。1回のダイスロールの計算にかかる時間がめちゃくちゃ短い(1秒の1万分の1くらい)ので、コンピュータ1000回ダイスロールすると結果が全部同じになってしまうわけですね。

これでも、1個しかダイスロールしないなら、人間がダイスロールの指示をコンピュータに送るタイミング(例えばチャットボットにダイスコードを打ち込んでエンターを押すタイミング)がある程度ランダムである以上、人間にとってはそこそこ十分な乱数と言えるかもしれません。しかし格闘ゲーマーはFPS60の世界(1秒に60回画面が切り替わる。当たり判定も1秒に60回行われる)の世界で生きていたりしますし、スロットの目押しができる人というのも居ます。はっきり行って、1/6秒で出目を確定できる六面ダイスは、あまり優秀なランダマイザとは言えないでしょう。

これをもうちょっとマシにするのなら、こんな感じです。

(Time.now.usec % * 6) + 1

% は剰余、つまり「割ったあまり」を求める演算子です。マイクロ秒を6で割った余りは、必ず 0 〜 5 の間になります。これは 1/1000000秒 ごとに次の出目に切り替わることを意味しているので、目押しをするには人智を超えた能力が必要になってくるでしょう。コンピュータの処理能力がどのように割り振られるのか未知数であるという点を考慮しても、出目の予測はほぼ不可能と言えるでしょう。(低レイヤで実装すれば出目に規則性が出ると思いますが、ruby ではそんなことはありません)

f:id:shino_gamer:20191224190457p:plain

6の剰余を使ってみる

(1,000,000は6で割り切れないので、実際にはそれぞれの出目ごとに出現頻度の差がありますが、最大でも 1/1000000 の確率なので、人間には無視できるレベルです)

これで TRPG で使うくらいなら十分な乱数生成ができそうです。(とはいえ問題はあるので、実践レベルで使うのはやめてね)

数学的なアプローチ

TRPG で適当に乱数を使うのならマイクロ秒なんかを利用してもぶっちゃけ問題ないと思います。(なんとなく乱数っぽいものがアレば俺たちは楽しく遊べるぜ)思いますが、どうせならちゃんとしたものを使いたいと思うのも人情でしょう。また、プログラムの世界においては、マイクロ秒を使った乱数というのはぜんぜん使い物にならないので、当然もっと良いものが普通に使われています。(例えば1,000,000より桁の大きい乱数は生成できないので困ったりする)

多くのプログラム言語でははじめから優秀な乱数生成のためのコードが用意されています。ここでよく使われているのがメルセンヌ・ツイスタです。

かなり疲れてきたので、詳細はこちらのスライドを御覧ください。メルセンヌ・ツイスタの考案者の一人である松本さんの作ったものです。

http://www.math.sci.hiroshima-u.ac.jp/~m-mat/TEACH/ichimura-sho-koen.pdf

メルセンヌ・ツイスタのランダムっぽさがわかる部分を引用しておきます。まずは古いランダム生成関数の結果をプロットしたもの。

f:id:shino_gamer:20191224191305p:plain

線形合同法rand

そしてこっちがメルセンヌ・ツイスタの結果をプロットしたもの。

f:id:shino_gamer:20191224191343p:plain

メルセンヌ・ツイスター

ぜんぜん違う。

疑似乱数とはなにか

このメルセンヌ・ツイスタを紹介する時に、よく「疑似乱数生成器」と呼ばれます。

この「疑似乱数」という部分に反応して「メルセンヌ・ツイスタはランダムじゃない!」という人もいますが、これは数学用語なので、直感的な意味とは異なります。

「疑似乱数」をこのエントリの言葉で表現するなら「悪魔にとっては乱数ではないが、人間にとっては乱数」というくらいの意味だと思ってください。

メルセンヌ・ツイスタの計算

メルセンヌ・ツイスタも計算の結果として乱数を生み出すものです。先程の例のように計算の内部でマイクロ秒をつかったりはしませんから、初期条件が同じなら全く同じ乱数列を取り出すことができます。ですので、メルセンヌ・ツイスタを使うときは、初期条件として用いるシード値は十分不規則なものにしておく必要があります。

メルセンヌ・ツイスタの「同じ初期条件(シード値)からは同じ乱数列を取り出せる」という性質は、科学実験の際などに非常に有用だそうです。ゲームの世界だと、例えばマインクラフトで「ワールドのシード値が同じなら同じ地形が生成される」という場面で使われていますね。

メルセンヌ・ツイスタで「初期条件(シード値)」と「その乱数が全体で何番目の乱数か」が分かれば、次の乱数を計算することは出来ます。しかしこの計算はとても人間の脳でできるレベルのものではありません。また、次の乱数を予測する法則もありません(正確に計算できることと、大雑把に予測できることは、全く違うことです)

したがって、メルセンヌ・ツイスタを使っていれば、そのランダマイザは「十分優秀なランダマイザ」であると言えるのです。

メルセンヌ・ツイスタの実証

ここではメルセンヌ・ツイスタが実際に乱数として振る舞うのかを実証していきたいとおもいます。

ケース1:出目が腐ってる!

錯覚です

セッションをしているとなんだかしらないけど高い出目や低い出目が連発することがあります。クトゥルフ神話TRPGで80以上がやたら多いとか、ソード・ワールドで5以下がめっちゃ出るとか、そういうことですね。果たしてこういうことがメルセンヌ・ツイスタだと起こりやすかったりするでしょうか?

「出目が腐っている状態」は個人によって色々と感じ方がありますが、ここではざっくりと「10回ロールしたうち、7回以上が悪い出目である」ものとしましょう。そして、悪い出目と良い出目は半々と仮定します。例えばクトゥルフ神話TRPGなら、50以下は良い出目、51以上は悪い出目としておきます。

良い出目と悪い出目の確率を常に半々だとして、他の情報を無視するなら、これはコインを使って表が出るかどうかと同じ問題だと言えるでしょう。つまり「10回コイントスをして、7回以上裏が出る」と置き換えます。

10回コイントスをした時の結果のパターンは1024通りで、そのうち7回以上裏が含まれるパターンは176通りなので、その確率は 17.18% です。

メルセンヌ・ツイスタを使って作ったコイントスで、適当に1,000,000回くらい「10回コイントスした」結果を出してみます。そのうち、7回以上裏が出ているものが全体の17%程度だったなら、メルセンヌ・ツイスタはたしかに理想的なコインと統計的に似た振る舞いをしている=十分にランダムである、と言えるでしょう。

以下はその結果です

N N回以上裏が出たケース 全体の割合
0 965 0.10%
1 9763 0.98%
2 44266 4.43%
3 117889 11.79%
4 204749 20.47%
5 245365 24.54%
6 205226 20.52%
7 117274 11.73%
8 43780 4.38%
9 9718 0.97%
10 1005 0.10%

7回以上裏が出たケースは合計で171,777回、これは全体の17.17%となり、理論値とほぼ同じ数値になっています。(試行回数を増やせば、より近づいていきます)

というわけで、「理想的なダイスで出目が腐る確率」と「メルセンヌ・ツイスタをダイス化して出目が腐る確率」は同じようです。ちなみに17%ってけっこう高い確率だと思うんですけど、どうですかね?

rand = Random.new Time.now.usec
result = (0...1000000).map { (0...10).map { rand.rand > 0.5 ? 1 : 0 } }.group_by(&:sum)
(0..10).each { |k| puts "#{k}: #{result[k].count}" }

 ケース2:私の時だけ出目が悪い!

そんなことないです

実際にはそういうことはありませんが、プレイヤー4人がみんな順番にダイスロールするものと仮定しましょう。そうすると、ある人物にとって、1度目と2度目のダイスロールの間には常に3回のダイスロールが行われていることになります。一定周期で出目が高い/低い傾向が確認できる場合、これは明確な偏りと言わざるを得ません。

ちなみに、実はメルセンヌ・ツイスタは周期性を持ちます。ただしその周期は「4回」とかではなくて、「219937-1回」だそうです。えーっと、計算した人がいるみたいなので、こちらの記事を御覧ください。「メルセンヌ素数2^19,937 - 1を計算してみました - GameSprit

今回は「4人の人物が順番に1回ずつ、それぞれ合計100回のダイスロールを行った時、悪い出目が出た回数」を10000回出してみて、それぞれのプレイヤーの悪い出目が出た回数の標準偏差を出します。

10000回分のデータを掲載するのはちょっとすごいことになるので、結果だけ載せておきます。

1: 4.99880302072406

2: 4.93716598465152

3: 4.98267375512385

4: 5.0451554733229

大きな偏りはないですね。

rand = Random.new Time.now.usec
(0...10000).each do
  result = [[], [], [], []]
  (0...100).each { 4.times { |i| result[i] << ( rand.rand >= 0.5 ? 1 : 0 ) } }
  puts result.map(&:sum).join("\t")
end

ケース3:偏ってきたらいっぱいダイスロールすれば解消される

言いがかり

こういうことを言う人が稀に居ますが、数学的根拠は一切ありません。すでに述べたとおり、「ある出目から次の出目の傾向が予測できる」という性質をメルセンヌ・ツイスタは持ちません。「ある出目から次の出目を確定できる」性質は持っていますが、これは「計算すればわかる」というものに過ぎず、また個々の乱数の数値にまつわる類似性ではありません。

メルセンヌ・ツイスタにおいては、「前の出目が大きいので、次の出目も大きい」とは言えないし、「前の出目が大きいので、次の出目は小さい」とも言えないのです。ただし、「シード値 3453456321 の 101 番目の乱数は 0.04286415295761392」ということがわかるだけです。

また、公式のどどんとふサーバーのような共有環境だと、数百人というたくさんのプレイヤーが同時に一つの乱数生成器を共有しています。そのため、同じ部屋では連続した乱数の取得に見えても、その間で他の部屋を使っている人が何十回とダイスロールを行っているはずです。もし仮にダイスをたくさん振れば偏りが解消されるというのが正しいとしても、わざわざ 100D100 なんて打ち込む必要は無いのです。

ケース1で述べたとおり、「10回のダイスロールを行った時に、7回以上悪い目が出る」確率は 17% もあります。直感的には、7回以上悪い出目が出る確率は 1/2^7 で 0.7% くらいに思えますが、ぜんぜん違うんですね。

確率の世界で直感ほどあてにならないものはありません。

メルセンヌ・ツイスタを使っているTRPGのツール

最後に、メジャーなオンラインセッションツールでどういう乱数生成器が使われているのかを調べてまとめておこうかと思ったんですが、ちょっと他人のコードを読むのがつらすぎたのでやめました。

まあ普通に実装してれば普通にメルセンヌ・ツイスタを使っていると思います。