ゲーム作者が普段何気なく使っている「乱数」が持つ性質について、「サイコロ」を例に、実際に Windows 標準環境で動かすことのできる PowerShell スクリプト付きで分かりやすく解説していきます。
目次
- まえがき (対象読者など)
- サイコロの数を増やしていくと…
- 実際にベル型カーブとの差異を確認してみる
- 6 面以外のダイス (=一般的な一様分布) でも確認してみる
- …で、これの何が重要なの?
- 補足 1 : サイコロの「分散」「標準偏差」「偏差値」について
- 補足 2 : サイコロの場合に特化した「正規分布」の式
- 補足 3 : 本記事で使用したサイコロの組合せ画像について
- おわりに
まえがき (対象読者など)
ゲーム作者が、普段、Unity やツクールなどの開発環境をとおして生成している乱数は、指定した範囲の数がどれも等確率で出現する「一様分布」という性質を持っています。
例えば、以下の API で取得できる乱数は、いずれも「一様分布」です。
- Unity (※ float, int いずれの API も該当)
Unity.Random.RandomRange(min, max)
- RPG ツクール (JavaScript)
Math.random()
TRPG などでも大活躍の「サイコロ」も一様分布の代表例の 1 つで、どの目も等確率です。
ところが、TRPG でもよく行われるように、複数個振って、その出目を「合計」した場合は話が変わってきます。
例えば、サイコロ 2 個の合計 (2D6) だと、7 の出現率が最も高く、2 と 12 の出現率が最も低いピラミッド型の分布となり、もはや「一様分布」ではありません。
既にゲームの確率論に詳しい作者なら、「ああ、中心極限定理の話か」と思われるでしょう。
本記事は、「中心極限定理? 何それ? おいしいの?」と思うゲーム作者向けに、実際に Windows で動かすことのできる PowerShell スクリプトとその実行結果を例に、なるべく分かりやすく説明していきます。
サイコロの数を増やしていくと…
「まえがき」からの続きになりますが、さらにサイコロを増やして 3 個 (3D6) にすると、ピラミッド型だった分布が「丸みを帯びる」ようになります。
4 個 (4D6) に増やすと、組合せが多すぎるため、1 枚の画像で表すのは困難になってきます。
(以下の画像に、拡大版の画像へのリンクを貼ってあります)
結論から言うと、これが中心極限定理と呼ばれているものです。
サイコロに限らず、「一様分布」な乱数であれば、数を増やしたときの「合計値」は、偏差値のグラフでおなじみの「正規分布」(いわゆるベル型カーブ) に収束していきます。
実際にベル型カーブとの差異を確認してみる
それでは、実際にベル型カーブである「正規分布」の式をもちいて、実際の分布 (=力技の総当たりで作った分布) と比較してみましょう。
唐突に数式が出てきましたが、心配は無用です。実際に本式をプログラミング済のスクリプトを用意してあります。
ちなみに、我々は「ゲーム作者」であって、「数学者」ではありませんから、この式そのものを理解する必要はありません。大事なのは、単純な現象である「一様分布」を重ね合わせるだけで、このような難しい式で表される曲線に変化するという観測的事実です。
(数式にアレルギーが無い方向け)
数式中に出てくる変数・定数を、今回のケースに当てはめると、以下の意味になります。
- x : サイコロの出目の合計 (範囲:サイコロ数 ~ 6×サイコロ数)
- μ : サイコロの出目の平均 (= 3.5 × サイコロ数)
- σ^2 : サイコロの出目の分散 (= 35 × サイコロ数 / 12)
- 分散については、難しい概念なので、後ほど詳細を説明します。
- π : 円周率 (= 3.14159265358979…)
実際に動かすことのできる PowerShell スクリプト
これから示す PowerShell スクリプトは、Windows 標準搭載機能ですので、Windows PC をお持ちの方は (古すぎなければ) 誰でも動かすことが可能です。
- Windows PC をお持ちでない方も、この後、いくつかの実行結果の例が説明とともに出てきますので、ご安心ください。
動かし方の詳細は、別記事「Windows 標準機能のみでグラフをプロットする方法」をご確認下さい。(※ 本例でも、実際にグラフのプロットを行っています)
clt.bat (スクリプト開始用バッチファイル)
PowerShell -NoProfile -ExecutionPolicy Unrestricted .\clt.ps1
clt.ps1 (PowerShell スクリプト本体)
# ■ グラフ描画関連の初期化 using namespace System.Drawing using namespace System.Drawing.Imaging using namespace System.Windows.Forms using namespace System.Windows.Forms.DataVisualization.Charting Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Windows.Forms.DataVisualization # ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $numOfSides = 6 # サイコロの面の数 $numOfDices = 1 # 一度に振る (=出目を合計する) サイコロの数 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■ # ■ サイコロの面の数と、一度に振る数から組合せの総数を計算 $numOfPerms = 1L # サイコロ数が多い場合も踏まえ long 型とする for ($diceID = 0; $diceID -lt $numOfDices; $diceID++) { $numOfPerms *= $numOfSides } # ■ 確率変数の個数を計算し、度数分布カウント用の配列変数を用意 $numOfVars = $numOfSides * $numOfDices - ($numOfDices - 1) $dist = New-Object int[] $numOfVars # ■ サイコロの組合せを総当たりで舐め、度数分布と分散を計算 # (計算単純化のため、出目の最小値を 1 ではなく 0 として計算) $avg = ($numOfVars - 1) / 2.0 # 平均値は必ず分布の中央 $vari = 0.0 # 分散は真面目に計算する for ($permID = 0L; $permID -lt $numOfPerms; $permID++) { # 現在の組合せ ID (順列の通し番号) から # 各サイコロの出目を求めて、その値を合計する $sum1st = [int]($permID % $numOfSides) if ($sum1st -eq 0) { # 計算処理高速化のため、2 個目以降のダイスの計算は # 1 個目のダイスが最小値のときのみ実施する $sumOther = 0 $currentPerm = [int][Math]::Floor($permID / $numOfSides) for ($diceID = 1; $diceID -lt $numOfDices; $diceID++) { $sumOther += [int]($currentPerm % $numOfSides) $currentPerm = [int][Math]::Floor($currentPerm / $numOfSides) } } $sum = $sum1st + $sumOther $diff = $sum - $avg # 分散計算用に平均からの差を出す $vari += $diff * $diff # 上記の 2 乗を総和 (Σ) $dist[$sum]++ # 度数分布を実測 } $vari /= $numOfPerms # 組合せ総数で割ることで分散値を確定 $stdev = [Math]::Sqrt($vari) # 分散から標準偏差を計算 (√) # ■ 求めた各種パラメーターを黒窓に出力 Write-Output ([String]::Format( "[サイコロ ({0} 面, {1} 個) の出目の合計]", $numOfSides, $numOfDices)) Write-Output ("組合せ総数 = " + $numOfPerms) Write-Output ("出目の平均 (期待値) = " + ($avg + $numOfDices)) Write-Output ("分散 = " + $vari) Write-Output ("標準偏差 = " + $stdev) Write-Output "----------------------------------------" # ■ 正規分布と、実測した度数分布を重ねたグラフデータを作成 # 実測した度数分布 $seriesDist = New-Object Series $seriesDist.ChartType = [SeriesChartType]::Column # 棒グラフ $seriesDist.LegendText = "実測分布" # 正規分布 $seriesNorm = New-Object Series $seriesNorm.ChartType = [SeriesChartType]::Line # 折れ線グラフ $seriesNorm.BorderWidth = 2 # 線の太さ 3 $seriesNorm.MarkerSize = 8 # マーカーの大きさ 12 $seriesNorm.MarkerStyle = [MarkerStyle]::Square # 四角形 $seriesNorm.LegendText = "正規分布" # グラフデータを作成 $vari2 = $vari * 2 $gain = $numOfPerms / [Math]::Sqrt($vari2 * [Math]::PI) for ($sum = 0; $sum -lt $numOfVars; $sum++) { $trueSum = $sum + $numOfDices $diff = $sum - $avg $norm = $gain * [Math]::Exp(-($diff * $diff) / $vari2) [void]$seriesDist.Points.AddXY($trueSum, $dist[$sum]) [void]$seriesNorm.Points.AddXY($trueSum, $norm) # 各確率変数 (出目合計) の生データも出力 Write-Output ([String]::Format( "{0:D3} : 偏差値{1,6:F2} 確率{2,11:F8}% (={3}/{4})", $trueSum, 50 + 10 * ($sum - $avg) / $stdev, 100 * $dist[$sum] / $numOfPerms, $dist[$sum], $numOfPerms)) } # ■ グラフ領域を作成 $chartArea = New-Object ChartArea $chartArea.AxisX.Title = "確率変数 (=出目の合計値)" $chartArea.AxisX.Minimum = $numOfDices - 1 $chartArea.AxisX.Maximum = $numOfDices + $numOfVars $chartArea.AxisX.Interval = 1 $chartArea.AxisY.Title = "度数 (=出目の組合せの総数)" $chartArea.Position = New-Object ElementPosition( 0, 6, 100, 94) # 横開始=0%, 縦開始=6%, 幅=100%, 高さ=94% # ■ 凡例を作成 $legend = New-Object Legend $legend.Position = New-Object ElementPosition( 60, 0, 40, 6) # 横開始=60%, 縦開始=0%, 幅=40%, 高さ=6% # ■ グラフタイトルを作成 $title = New-Object Title([String]::Format( "サイコロ ({0} 面, {1} 個) の出目の合計値の度数分布", $numOfSides, $numOfDices)) $title.Position = New-Object ElementPosition( 0, 0, 60, 6) # 横開始=0%, 縦開始=0%, 幅=60%, 高さ=6% # ■ グラフ本体を作成 $chart = New-Object Chart $chart.Series.Add($seriesDist) $chart.Series.Add($seriesNorm) $chart.ChartAreas.Add($chartArea) $chart.Legends.Add($legend) $chart.Titles.Add($title) $chart.Size = New-Object Size(640, 360) # ■ グラフの中身を画像ファイル (PNG) に保存 $bitmap = New-Object Bitmap( $chart.Size.Width, $chart.Size.Height) $chart.DrawToBitmap($bitmap, (New-Object Rectangle( 0, 0, $chart.Size.Width, $chart.Size.Height))) $bitmap.Save($PSScriptRoot + "\clt.png", [ImageFormat]::Png) $bitmap.Dispose() # ■ グラフの中身をウィンドウとして表示 $form = New-Object Form $form.ClientSize = $chart.Size $form.Controls.Add($chart) [void]$form.ShowDialog()
スクリプトを実際に動かしてみて結果を観測
初期状態では、サイコロ 1 個の場合を観測できるようになっていますので、まずはそのまま実行してみましょう。
サイコロ 1 個 (1D6) の分布
試すまでもなく、完全に一様分布です。
サイコロ 2 個の合計値 (2D6) の分布
コードの最初の方にある、以下の部分でサイコロの数を変えられます。(1 になっているのを 2 に変えれば OK です)
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $numOfSides = 6 # サイコロの面の数 $numOfDices = 1 # 一度に振る (=出目を合計する) サイコロの数 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
「まえがき」でも説明のとおり、ピラミッド型の分布になりました。
(まだ「ベル型カーブ」とは言えない形状です)
サイコロ 3 個の合計値 (3D6) の分布
ピラミッド型だった分布が曲線を描くようになり、ベル型カーブの形状に近づきました。
サイコロ 4 個の合計値 (4D6) の分布
正規分布の形状に近づいてきました。
中心部は実際の正規分布よりも低く、外側は若干高い傾向があるように見えます。
サイコロ 10 個の合計値 (10D6) の分布 (※ 時間がかかるので注意)
本スクリプトでは実際に力技で総当たりしているので、サイコロ 10 個だと 6^10 = 60466176 通りとなり、筆者の PC では 7~8 分ほどかかりました。
実際の正規分布との差は無くなった訳ではありませんが、比率的に見てもほぼ正規分布になったと見て問題ないでしょう。
6 面以外のダイス (=一般的な一様分布) でも確認してみる
本記事のスクリプトは、6 面以外の一般的な一様分布 (※ 厳密には「離散」一様分布と言います) のケースでも確認できるようになっています。
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $numOfSides = 6 # サイコロの面の数 $numOfDices = 1 # 一度に振る (=出目を合計する) サイコロの数 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
試しに、2 面ダイス (=コイン) と、10 面ダイスの場合を確認してみましょう。
2 面ダイス (=コイン) の場合
例えばコインを投げて表が出たら 2 点、裏が出たら 1 点とした場合に相当します。
コイン 1 枚 (1D2) の分布
試すまでもありませんが、やはり「表」(2 点) か「裏」(1 点) かの一様分布です。
コイン 2 枚の合計点 (2D2) の分布
かなり小さいですが、一応、ピラミッド型になっています。
コイン 18 枚の合計点 (18D2) の分布 (※ 所要時間やや大)
表と裏の 2 通りしかないコインでも、2^18 = 262144 通りもの組合せが集まれば、このとおり、正規分布に近づきます。
10 面ダイスの場合
一般的なゲーム (TRPG 等) において 10 面ダイスを複数振る場合、通常は、出目を合計せずに、各桁を独立であるものとして扱うため、一様分布のままであることに注意して下さい。
本ケースでは、あくまでも 1~10 の出目があるダイスを合計する (例えば、10 面ダイスを 2 個振って 4 と 5 が出たら「45」ではなく、「4 + 5 = 9」として扱う) ものとしての確率分布を扱います。
10 面ダイス 1 個 (1D10) の分布
試すまでもありませんが、やはり一様分布です。
10 面ダイス 2 個の合計値 (2D10) の分布
他のケースと同様、ピラミッド型の分布になります。
(最初の説明のとおり、ダイスの出目の「合計」であることに注意して下さい)
10 面ダイス 6 個の合計値 (6D10) の分布 (※ 所要時間やや大)
ダイス 1 個あたりの出目が多いため、比較的少ない試行数でも、それなりに分解能のある正規分布に近づいていきます。(6 個だけでも 10^6 = 1000000 通り)
…で、これの何が重要なの?
繰り返しになりますが、重要なのは、例えば Unity 使いが
Unity.Random.RandomRange(min, max)
とやったり、ツクラーの方が
Math.random()
とやったりすることを、単純に複数回繰り返して「合計」するだけで
という、なぜか円周率、平方根、ネイピア数を含む複雑な曲線式の分布に収束してしまうという事実です。
この事実が無ければ、このような「変態なだけ」の式、ガチの数学好きしか喜ばないはずです。
このような「変態」に見える式が、重宝される理由
逆を言うと、この難しい式が「偏差値教育」の場でも使われるほど有名でちやほやされる理由が、まさにこの性質 (=中心極限定理) にあるからです。
一様分布は、身近な「サイコロ」でも観測されるくらい、世の中で普遍的な現象です。だからこそ、世の中には、個体としては「一様分布」なバラツキを持つ者達 (物質、生物、etc ...) が重なり、集団が形成された結果、「正規分布」として観測される事象が数多く存在するのです。
それだけ普遍的な現象であったため、統計学の分野でも、一時期 (19 世紀頃)、正規分布万能論が流行ってしまったほどです。
- 現代では、正規分布に従わない、より複雑な現象も多数存在することが分かってきたため、正規分布万能論は下火になってきています…が、それでもまだまだ正規分布で近似しようとする「信仰」は、エンジニアリング分野や偏差値教育などで根強く残っているほどです。
ゲーム = 現実世界をモデル化したもの
ゲームも、世の中で普遍的な現象をモデル化したものだと言えますから、まさに「正規分布」はゲーム内における確率 (=運) をモデル化する際の有力な選択肢の 1 つだと言えます。
- もちろん、何でもかんでも正規分布にしてしまうのは、それこそ 19 世紀への逆戻りですから、あくまでも選択肢の 1 つです。
だからこそ TRPG では、サイコロを 2 個振ること (2D6) で、正規分布を「大雑把に」モデル化しているのだと言っても過言ではありません。
- 例えば、2D6 において、もっともレアな「2」と「12」は正規分布のベル型カーブの両端側に相当する稀な事象だからこそ、大失敗を意味するファンブルや、大成功を意味するクリティカルをモデル化するのに都合が良いと言えます。
- 逆に、2D6 において、もっとも普遍的な「7」は、某 TRPG のキャラメイク時のロールでは一般市民という、まさに「偏差値 50」の凡人的存在をモデル化したと言えます。
したがって、いちゲーム作者である筆者「鈴木YE」としては、この「中心極限定理」について、触り程度の理解でもいいから、乱数を扱うすべてのゲーム作者が知っておいた方が良い基礎知識だと考えています。
補足 1 : サイコロの「分散」「標準偏差」「偏差値」について
今回の事例のように確率が正規分布にしたがうとき、「分散」と、そこから求まる「標準偏差」が、確率計算を行ううえで非常に重要な意味を持ちますので、1 個 1 個解説していきます。
そもそも「分散」とは?
分散とは、データがどれだけばらついているかを数値化した値のことで、具体的には平均値 (期待値) からの差の 2 乗を平均したものとなります。
例えばサイコロ 1 個の場合は、平均値 (期待値) が (1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.5 ですから、分散 σ^2 は
- (1 - 3.5) ^ 2
- (2 - 3.5) ^ 2
- (3 - 3.5) ^ 2
- (4 - 3.5) ^ 2
- (5 - 3.5) ^ 2
- (6 - 3.5) ^ 2
の全 6 個の平均値です。
サイコロ 1 個の場合の分散 σ^2 の計算
分数で計算した方が後々便利ですので、分数に置き換えてみます。
- (2/2 - 7/2) ^ 2
- (4/2 - 7/2) ^ 2
- (6/2 - 7/2) ^ 2
- (8/2 - 7/2) ^ 2
- (10/2 - 7/2) ^ 2
- (12/2 - 7/2) ^ 2
先に括弧の中を計算するとシンプルになります。
(なぜなら、期待値がちょうど分布の中央にあり、左右対称になっているからです)
- (-5/2) ^ 2
- (-3/2) ^ 2
- (-1/2) ^ 2
- (+1/2) ^ 2
- (+3/2) ^ 2
- (+5/2) ^ 2
2 乗を計算します。分母が 4 に変わり、かつ符号が消え去ります。(これが 2 乗している理由の 1 つです)
- 25/4
- 9/4
- 1/4
- 1/4
- 9/4
- 25/4
全部合計すると、70/4 ですから、約分して 35/2 になります。
さらに平均化するため 6 で割りますから、サイコロ 1 個の場合の分散 σ^2 は「35/12」です。
サイコロが複数個 (n 個) の場合の合計値の分散
サイコロが複数個の場合でも、同じように全パターンを計算すれば、分散 σ^2 を計算できます。
しかし、真面目に計算する場合、例えばサイコロ 2 個だと 6 × 6 = 36 通りのパターンをすべて計算しなければなりません。
本記事の PowerShell スクリプトでは、この方法 (真面目に総当たりする方法) で分散 σ^2 を計算していますので、興味のある方はコードをよく読んでみて下さい。
簡単に計算する方法
証明は割愛しますが、実はサイコロが複数個 (n 個) になった場合の分散 σ^2 は、サイコロが 1 個のときの n 倍となることが知られています。
なので、n 個のサイコロの合計値の分散 σ^2 は、「35 * n / 12」です。
- 実際に、PowerShell スクリプト側が出した結果 (総当たりでの計算値) が、「35 * n / 12」にしたがっていることを検算できるはずです。
これは、サイコロの面数が 6 でない場合 (つまり、一般的な一様分布の乱数の場合) にも適用できるため、覚えておいて損はありません。
- 例えば n 個の 10 面ダイスの合計値の分散 σ^2 を調べたいときは、1 個の 10 面ダイスの分散 σ^2 さえ分かれば、あとは n 倍するだけです。
標準偏差 σ について
分散 σ^2 は、計算の過程で、平均値からの差分を「2 乗」していますので、元の分布のばらつきが大きいと σ^2 が非常に巨大な値になってしまいます。
そこで、計算時に行った「2 乗」をキャンセルするため、分散 σ^2 の「平方根」をとったものを「標準偏差 σ」といいます。
つまり、n 個のサイコロの出目の合計の標準偏差 σ は
- sqrt(35 * n / 12)
となります。
具体例
- サイコロ 1 個のとき、標準偏差 σ = sqrt(35 / 12) = 約 1.71
- サイコロ 2 個のとき、標準偏差 σ = sqrt(70 / 12) = 約 2.42
- サイコロ 3 個のとき、標準偏差 σ = sqrt(105 / 12) = 約 2.96
標準偏差 σ と「偏差値」の関係
みんな大好き(?)、受験教育でよく耳にする「偏差値」ですが、実は「標準偏差 σ」と密接に関わっています。
- 平均値とぴったり同じだった場合、偏差値 = 50
これは皆さん既にご存じかと思います。
では、標準偏差 σ との関係はどうなっているかというと、次のとおりです。
- 平均値から見て、標準偏差 σ の分だけ「低い」値になると偏差値 40
- 平均値から見て、標準偏差 σ の分だけ「高い」値になると偏差値 60
つまり、さらに言うと…
- 平均値から見て、標準偏差 σ の 2 倍「低い」値になると偏差値 30
- 平均値から見て、標準偏差 σ の 2 倍「高い」値になると偏差値 70
では、サイコロ 1 個の場合の具体例を見てみましょう。
実は、本記事の PowerShell スクリプトで、既にこの計算を行ったものをコンソール (黒窓) に出力していますので、皆さんの方でも見られると思います。
[サイコロ (6 面, 1 個) の出目の合計] 組合せ総数 = 6 出目の平均 (期待値) = 3.5 分散 = 2.91666666666667 標準偏差 = 1.70782512765993 ---------------------------------------- 001 : 偏差値 35.36 確率16.66666667% (=1/6) 002 : 偏差値 41.22 確率16.66666667% (=1/6) 003 : 偏差値 47.07 確率16.66666667% (=1/6) 004 : 偏差値 52.93 確率16.66666667% (=1/6) 005 : 偏差値 58.78 確率16.66666667% (=1/6) 006 : 偏差値 64.64 確率16.66666667% (=1/6)
偏差値が 40 と 60 になる値は、それぞれ 3.5 - 1.707... と 3.5 + 1.707... のポイントなので、サイコロの出目 2 と 5 は偏差値 40~60 の圏内に収まっていることが分かると思います。
「標準偏差」「偏差値」と「確率分布」の関係
実は、サイコロが 1 個の場合、「標準偏差」や「偏差値」は統計上の意味を持ちません。
これらが意味を持つのは、サイコロの個数が増えて、その分布が「正規分布」に近づいてきたときに、初めて意味を持ちます。
確率分布が「正規分布」に従うとき、「標準偏差」「偏差値」が持つ意味
具体的には、以下のようになることが知られています。
(確率について詳しくない方でも、この話だけは聞いたことのある方も居ると思います)
- 偏差値 40~60 の範囲 (±1σ) に収まる確率は、約 68.27 %
- 偏差値 30~70 の範囲 (±2σ) に収まる確率は、約 95.45 %
- 偏差値 20~80 の範囲 (±3σ) に収まる確率は、約 99.73 %
つまり、偏差値が 20 未満のものや、80 を超えるものは、100 - 99.73 より、全体の約 0.27% しかいないということになります。
サイコロの出目の合計の場合、サイコロの数が増えれば増えるほど正規分布に収束していきますから、サイコロの数がものすごく多い場合は、偏差値さえ計算すれば確率の見積ができることを意味します。(逆を言うと、サイコロの数が減るほど精度が落ちます)
ゲーム制作への応用について
もし自作ゲーム等で (乱数を複数回生成して合計する方法で) 正規分布の乱数を作った場合、ゲームバランスを調整する際に役に立ちますので、興味のある方は覚えておくと良いでしょう。
- 離散一様分布 (int 型の乱数や、float 型だけど整数に丸めた場合など) で作った場合は、厳密に確率計算が可能なので、正規分布の式を使わずに確率計算した方が良いかもしれませんが、float 型や double 型の乱数生成 API を使った場合は正規分布だとみなして確率計算を行った方が楽になるでしょう。
- 補足として、連続一様分布の場合、分散 σ^2 は (b - a)^2 / 12 となることが知られています。(b は最大値、a は最小値)
- 特に、0 ~ 1 の範囲で float 型または double 型の乱数を発生させる API を利用した場合、上記の式から分散 σ^2 がちょうど 1/12 となるため、この性質を逆手にとって 0~1 の範囲の乱数を 12 回生成した結果を合計 (つまり分散が 12 倍になる) することで、σ = σ^2 = 1 となる「非常に扱いやすい」正規分布の乱数を作るというテクニックが使われることもあります。
補足 2 : サイコロの場合に特化した「正規分布」の式
補足 1 の知識を活用して、正規分布に対する理解を深める目的で、具体的に、サイコロ (6 面ダイス) n 個の場合の正規分布の式を求めてみましょう。
これまでの説明で、「平均」や「分散」の意味や計算方法を理解できていれば、以下のように代入・変形できるはずです。(実際に読者の方で手元で計算してみて、次のように変形できることが確認できれば、良い勉強になると思います)
確率分布ではなく、度数分布としての曲線を求めたい場合
確率分布 (=出現「率」) ではなく、度数分布 (=出現「数」) に合わせたい場合は、サイコロの全パターンの組み合わせ数 (6^n) の分だけ乗算する必要があります。
本記事の PowerShell スクリプトも、度数分布 (=出現「数」) としてグラフをプロットしていますので、やはりこれに相当する計算処理を行っています。
サイコロ 10 個の場合の度数分布に合わせる例
n に 10 を代入することで、具体的に「サイコロ 10 個の場合」に合わせることができます。
ついでに、数学に苦手意識がある方のために、円周率 π も具体的な値 (途中で打ち切っていますが…) にしておきました。
実際に曲線を描きたい (計算したい) 場合
サイコロが 10 個のとき、その分布の範囲は 10 (全部 1 が出た場合) ~ 60 (全部 6 が出た場合) です。
つまり、例えば自前で Excel 等のグラフにしたい場合は、x の値を 10 ~ 60 の範囲で舐めるようにすれば、本記事の PowerShell スクリプトと同様のベル型カーブ (下記の「橙色」の方) になるはずです。
具体的に、どの関数を呼び出して計算すれば良いの?
C# (Unity も含む) の場合
- √ の部分 : Math.Sqrt() [Unity の場合は Mathf でも可]
- exp の部分 : Math.Exp() [Unity の場合は Mathf でも可]
- べき乗の部分 : Math.Pow() [Unity の場合は Mathf でも可]
- 2 乗なので、同じ値同士を掛け算でも可
Excel の場合
- √ の部分 : SQRT()
- exp の部分 : EXP()
- べき乗の部分 : POWER()
- 2 乗なので、同じ値同士を掛け算でも可
補足 3 : 本記事で使用したサイコロの組合せ画像について
実は、本記事の最初の方に掲載した次のような画像も PowerShell スクリプトを使って描画したものですので、おまけにこちらの PowerShell スクリプトも掲載しておきます。
サイコロの組合せ画像 (サイコロ数 1~4 限定) の描画スクリプト
dices_in.png (サイコロ画像)
下記を、ファイル名 dices_in.png のまま、スクリプト (bat および ps1) と同じ場所に保存して下さい。
dices.bat (スクリプト開始用バッチファイル)
PowerShell -NoProfile -ExecutionPolicy Unrestricted .\dices.ps1
dices.ps1 (PowerShell スクリプト本体)
# ■ サイコロ描画関連の初期化 using namespace System.Drawing using namespace System.Drawing.Imaging # ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $numOfDices = 4 # 一度に振るサイコロの数 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■ # ■ サイコロ画像の読み込み $bitmapIn = [Image]::FromFile( $PSScriptRoot + "\dices_in.png") # ■ 描画先画像の作成 $bitmapOut = New-Object Bitmap(2800, 378) # 最大 4 個を推定 $font = New-Object Font([FontFamily]::GenericMonospace, 12) $g = [Graphics]::FromImage($bitmapOut) $g.Clear([Color]::FromArgb(255, 255, 192)) # ■ サイコロの面の数と、一度に振る数から組合せの総数を計算 $numOfPerms = 1L # サイコロ数が多い場合も踏まえ long 型とする for ($diceID = 0; $diceID -lt $numOfDices; $diceID++) { $numOfPerms *= $numOfSides } # ■ 確率変数の個数を計算し、度数分布カウント用の配列変数を用意 $numOfVars = $numOfSides * $numOfDices - ($numOfDices - 1) $dist = New-Object int[] $numOfVars # ■ サイコロの組合せを総当たりで舐め、度数分布計算と描画を実施 for ($permID = 0L; $permID -lt $numOfPerms; $permID++) { # 現在の組合せ ID (順列の通し番号) から # 各サイコロの出目を求めて、その値を合計する $sum = 0 $currentPerm = $permID for ($diceID = 0; $diceID -lt $numOfDices; $diceID++) { $sum += [int]($currentPerm % $numOfSides) $currentPerm = [int][Math]::Floor($currentPerm / $numOfSides) } # 該当する合計値、かつ現在の度数の場所に合致する位置に # 各サイコロの出目を描画する $currentPerm = $permID for ($diceID = 0; $diceID -lt $numOfDices; $diceID++) { $destX = $dist[$sum] * 18 + ($diceID % 2) * 8 $destY = $sum * 18 + [int][Math]::Floor($diceID / 2) * 8 $srcX = [int]($currentPerm % $numOfSides) * 8 $g.DrawImage($bitmapIn, (New-Object Rectangle($destX, $destY, 9, 9)), (New-Object Rectangle($srcX, 0, 9, 9)), [GraphicsUnit]::Pixel) $currentPerm = [int][Math]::Floor($currentPerm / $numOfSides) } $dist[$sum]++ # 度数分布を実測 } # ■ 各度数分布のカウント結果を描画 for ($sum = 0; $sum -lt $numOfVars; $sum++) { $g.DrawString([String]::Format("[{0:D2}]{1}/{2}", $sum + $numOfDices, $dist[$sum], $numOfPerms), $font, [Brushes]::Black, $dist[$sum] * 18, $sum * 18) } # ■ 描画先画像をファイルに出力 $bitmapOut.Save( $PSScriptRoot + "\dices_out.png", [ImageFormat]::Png) $g.Dispose() $font.Dispose() $bitmapOut.Dispose() $bitmapIn.Dispose()
おわりに
特に後半になるほど、図が少なくなり数式も増えてきたため、数学が苦手な方にはやや難しい内容になってしまったかもしれません。
しかし、筆者も実は数学が苦手で、式そのものの背景 (理由や証明など) は全然理解できておらず、単に「ブラックボックス」「道具」として扱っているだけです。
ただし、幸い我々は「ゲーム作者」です。数学のテストに出る訳でもないし、証明問題を解く必要も無いのです。
数式の証明とか、導出なんてできなくていいんです。全然理解できていなくても構わないんです。
こういう数式を使えば、尤もらしいやり方で確率の計算・見積ができる。それが、我々ゲーム作者のように「応用する立場にあるソフトエンジニア」には重要な姿勢です。
丸暗記でもいいですし、重要な用語・数式だけ手元に残してメモしてもいいです。最悪、Google 先生に毎回聞いて、必要な数式を毎回手元に用意すれば済む話です。
日々当たり前のように乱数を利用するゲーム作者であれば、ゲームバランスの善し悪しにも影響する「確率論」を齧っておいて損はありません。
ゲーム作者同志、一緒に頑張りましょう!!!!