ウタカゼ 判定の達成値

 ウタカゼというTRPGがある。人類が滅んだ後の世界に現れたコビット族という身長20cm前後のコロポックルみたいな小さな種族となって冒険するというメルヘンなゲームだ。一緒に遊んだ人曰く、ガンバの冒険みたいだとのこと。僕はガンバの冒険知らないのでこれとかこれなイメージ。まあ、確かにそんな感じだと思う。
 そんなウタカゼなんだけど、独特な判定ルールとなっている。能力値と技能値を足した数だけサイコロを振って、同じ出目のサイコロの数を達成値とする。そんなウタカゼの判定の確率を計算してみた。
 一応、ウタカゼの判定は代数解が求められないか少し頑張ってみたのだけど、うまい方法が見つからなかった。求められる部分もあるにはあるのだけど、全く役に立たず諦めた。ちょこっとエクセルで計算を試みたりもしたが、6サイコロの数個の組み合わせをカウントしなければならないので、6Dくらいが限界だった。エクセルは表計算ソフトであり、ガッツリとした計算には向いてないことを再認識させられた。
 そんなわけで、面倒臭くなって放置していたのだけど、先日ちょっとC++でプログラムを書く機会があったので、これに便乗して改めて計算した。コンパイラVisual C++ 6.0を使ったんだけど、Windows8に対応していなかったのでVMware PlayerでWindowsXPを走らせてその上で計算を行った。WMwareから出してWindows8の上で走らせたほうが計算は早くなるかなと思ったんだけど、比較したところ優劣は感じられなかったのでVMware上で計算した。

 計算方法は確率の定義どおり、(起こる事象/すべての事象)とした。すべての事象と気軽に言うけど、上に書いたとおり、6サイコロの数個の組み合わせがあるので楽ではない。たとえば615=4.7×1011であり、この数字を見るだけですごくやる気がなくなる。
 とはいえ、計算するのはPCであって人間様はPCにあれやっといてと放り投げるだけなので、時間さえあればどうにでもなる。エクセルみたいにフリーズしてOSごと落ちるとかいうこともないし。
 そんなわけで出来たのが以下のソースコード


#include <math.h>
#include <iostream>
#include <fstream.h>
#include <conio.h>
#include <iomanip.h>

int main(void){

__int64 bangou;
__int64 bangou_2;
__int64 bangou_saidai;
__int64 zorome_kosuu[16][16]={0};
int c=1;
int f;
int g;
int h;
//zorome_kosuu[a][b] a個のダイスを振ったとき、同じ目がb回出る数
int dice_kosuu;
int deme[16]={0};
//deme[a] a個目のサイコロの出目
int dice_no;
int one_six_no_kosuu[7]={0};
int max;

ofstream fo("utakaze.txt");
if(!fo){
cout<<"ファイルが開けません!!"<<endl;

return 1;
}
fo<<setiosflags(ios::fixed);


for(dice_kosuu = 2 ; dice_kosuu <= 15 ; dice_kosuu++){
//13~14個のダイスを振るという設定
deme[dice_kosuu]=0;
bangou_saidai = pow(6,dice_kosuu-1);
for(bangou = 1 ; bangou <= bangou_saidai ; bangou++){
//6^dice_no個の結果を評価する
//同じ出目が何個あるかを6^dice_no通り調べてカウントする。
//1つは必ず0があるとすることで、計算量は1/6となる。最後にすべての値を6倍する。
//
for(int d=0; d<= 5; d++){
one_six_no_kosuu[d]=0;
//one_six_no_kosuuをすべて0にする。
}
bangou_2 = bangou;
//bangouは変化させると都合が悪いので、bangou_2という変数を使う。
for(dice_no = 1 ; dice_no <= dice_kosuu-1 ; dice_no++){
//dice_noを6でdice_kosuu回割って、それぞれの余りを出目とする
deme[dice_no] = bangou_2%6;
bangou_2 = (bangou_2 - deme[dice_no])/6;
//deme[dice_kosuu]を必ず0とすることで計算を1/6に減らす。
}

//deme[dice_no]の中身について同じ目が最大で何個あるかをカウントする
for(int a=1; a<= dice_kosuu; a++){
one_six_no_kosuu[deme[a]&#93;++;
//one_six_no_kosuu[0]には0の個数。
//one_six_no_kosuu[1]には1の個数。
//などなど
}
max = 0;
for(int b=0; b<= 5 ; b++){
//0~5の数をカウントする。
if(max < one_six_no_kosuu[b]){
max = one_six_no_kosuu[b];
//max:同じ出目の最大個数
}
}
zorome_kosuu[dice_kosuu][max]++;
}

cout << dice_kosuu << "個のダイスを振ったとき"<<endl;

fo << dice_kosuu << "個のダイスを振ったとき"<<endl;


for(c=1; c<=dice_kosuu; c++){
cout <<" "<< c << " " ;

fo <<" "<< c << " " ;

f=zorome_kosuu[dice_kosuu][c]*6/100000000;
g=zorome_kosuu[dice_kosuu][c]*6%100000000;
if(f>0){
cout<<f;

fo<<f;

for(h=0;h<=6-floor(log10(g));h++){
cout<<"0";

fo<<"0";


}
}
cout << g << endl;
fo << g << endl;

}

}
getch();

return 0;
}

 ソースをHTMLにするとタブが反映されないので読みづらくなってしまったけど、他人のコードなんて誰も読まないだろうからこのままにしておく。
 n個のサイコロを振った時に同じ1~6の目がそれぞれ何個あるかをカウントし、その中で一番たくさんあるものを数えていくというもの。なのだが、ウタカゼの判定はダイスの目の数字自体に意味はなく、最初に振ったサイコロの出目を固定しておくという設定にするだけで計算量を1/6に減らすことができる。そんなわけで、n個目の出目を固定してn-1個のサイコロを振ったときの同じ目の個数をカウントしてそれを6倍した。
 n個全てのサイコロを振らなくても、大抵は何回か振った時点で達成値が決まるのでその点を考慮することでもう少し計算が早くなるかもしれない。こういったところのアルゴリズムの改善を考えるのもなかなか楽しいものなのだけど、そんなに気合を入れてやることでもないので、あんまり面倒なことはせずにマシンパワーに任せて計算した。
 今回扱う数が大きいので__int64というパラメータを使った。通常、intだと±232/2までの数値を扱うことができるのだけど、今回はこの範囲を超えているので、倍の±264/2で定義する必要があった。このパラメータは初めて使ったんだけど、すごく使い勝手が悪い。というのはcoutで出力することが出来ない。仕方がないので、出力する際は1億で割り、1億より上の桁と下の桁2つの数字に分けて順に出力するという手間を掛けることになった。
 しかし、この1億で割って商と余りを並べて数値を表記するというのが曲者で、余りが1000万よりも小さいときに問題が起こった。例えば、6207201000という値を表記したいとき、6207201000の商は62で余りは7201000なので、627201000という数値を出力してしまう。このことに気付くのに片手間ながらも3日かかった。何せ、デバッグの際にプログラムを走らせて調べようにも612回の計算を行わなければならないのでなかなか迂遠な問題だった。とにかく、足りない桁を0で埋めてしまうことで解決した。プログラムを生業としてる方とかにはこういうのは常識なのかな。そもそも__int64なんて変なパラメータを使わずにlongとかdoubleとか使っておけばよかったのだろうか。この辺り、ちゃんと教科書を参照してクリアするべきなんだけど、ネットが便利なのと僕自身が不精なのでgoogleさんに尋ねて終了としてしまう傾向があってよくない。
 ちなみに教科書は学生のためのC++C++プログラミングを使っている。学生のためのC++の方がキャッチーで実例とかが分かりやすくて参考になるけど、C++プログラミングの方は隙がなくて教科書としてしっかりとしている。
 計算結果は次のようになった。

2個のダイスを振ったとき
1 30
2 6
3個のダイスを振ったとき
1 120
2 90
3 6
4個のダイスを振ったとき
1 360
2 810
3 120
4 6
5個のダイスを振ったとき
1 720
2 5400
3 1500
4 150
5 6
6個のダイスを振ったとき
1 720
2 28800
3 14700
4 2250
5 180
6 6
7個のダイスを振ったとき
1 0
2 128520
3 121800
4 26250
5 3150
6 210
7 6
8個のダイスを振ったとき
1 0
2 491400
3 880320
4 261450
5 42000
6 4200
7 240
8 6
9個のダイスを振ったとき
1 0
2 1587600
3 5628000
4 2320920
5 472500
6 63000
7 5400
8 270
9 6
10個のダイスを振ったとき
1 0
2 4082400
3 32004000
4 18774000
5 4721220
6 787500
7 90000
8 6750
9 300
10 6
11個のダイスを振ったとき
1 0
2 7484400
3 162162000
4 139986000
5 43132320
6 8662500
7 1237500
8 123750
9 8250
10 330
11 6
12個のダイスを振ったとき
1 0
2 7484400
3 731808000
4 967428000
5 366569280
6 86611140
7 14850000
8 1856250
9 165000
10 9900
11 360
12 6
13個のダイスを振ったとき
1 0
2 0
3 2933330400
4 6207201000
5 2928645720
6 803602800
7 160875000
8 24131250
9 2681250
10 214500
11 11700
12 390
13 6
14個のダイスを振ったとき
1 0
2 0
3 10342332000
4 36942305400
5 22132590480
6 7015128120
7 1608698520
8 281531250
9 37537500
10 3753750
11 273000
12 13650
13 420
14 6
15個のダイスを振ったとき
1 0
2 0
3 31279248000
4 203378175000
5 158744706120
6 58156698600
7 15078749400
8 3016406250
9 469218750
10 56306250
11 5118750
12 341250
13 15750
14 450
15 6

 計算時間は10時間ほど。この6倍ほど時間をかければ16個のダイスを振ったときも求められるんだけど、まあいいや。もっと知りたいという方は、上のソースコードを書き換えて自分で計算してください。

 ウタカゼの判定は2個以上のサイコロを振ったり、能力値に上限があったりした気がするのだけど、グラフの見栄えとか表の統一感とかを求めて、また、純粋に数学の問題に落としこむためにそこら辺の制約はところどころ目を瞑った。
 結果を6nで割って確率を求め、グラフに表すと次の通りになる。

 実はこのグラフはそれぞれの個数のダイスを振った時に出やすい達成値がわかるという程度で、あんまり意味がない。それぞれの値を積算して、目標値ごとの成功確率を描くことで使えるグラフとなる。あるいは、期待値を求めるのも良いかもしれない。


 見ての通り、特に感動的な結果が待っていたりするわけではない。
 期待値は真っ直ぐなラインとはならず、どこか歪な直線を描くようにして上昇していく。確率の代数解が求められていればこの歪みについて考察ができるのだけど、いま手元には力ずくで求めた計算結果があるだけなので、こういう結果になったとしか言うことが出来ない。

・クリティカルコール
 ウタカゼにはクリティカルコールという判定方法がある。龍のダイスという特定の出目が出たサイコロの数を2倍にして達成値とする判定方法であり、プレイヤーが判定前に好きに通常判定とクリティカルコールを選ぶことができる。
 クリティカルコールの確率は通常の判定と違って簡単に求めることができる。ダイスの数をn個、龍のダイスの数をm個とすると、nCm(1/6)m(5/6)n-m×100%で表現することができる。これを積算すれば目標値ごとの成功確率が得られる。

 全体的に通常判定よりも傾きが小さく、より高達成値に有利な感じがする。2倍にするという部分が効いているため横に長くなる。通常判定とクリティカルコールの成功確率を並べたグラフも作ってみたけど、ちょっとごちゃごちゃして見づらいのでリンクするだけにとどめておく。
 期待値は次のようになる。

 期待値は通常判定と違ってサイコロ1個につき1/3ずつピッタリ上昇していく。たぶん、数学的に証明できるのだけど面倒なのでやらない。結果を見たら1/3ずつ増えてるんだからいいじゃん。
 11Dの期待値が通常判定で3.69、クリティカルコールで3.6667となっている。サイコロ11個までは通常判定が有利で、12個からはクリティカルコールが有利ということになる。ただし、これはより多い達成値を求める場合なので、成功失敗を論じるのなら期待値ではなく、上の成功確率を見なければならない。通常判定とクリティカルコールを並べたグラフを見ればよいのだけど、見づらいので、サイコロの数と目標値を決めた時にどちらが成功確率が高くなるかを一覧表にしてみた。
 横軸がサイコロの数、縦軸が目標値である。「クリティカルコール」と書くと場所をとるのでCCと略した。

1 2 3 4 5 6 7 8
0 同じ 同じ 同じ 同じ 同じ 同じ 同じ 同じ
1 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定
2 CC CC 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定
3 失敗確定 CC CC CC 通常判定 通常判定 通常判定 通常判定
4 失敗確定 CC CC CC CC CC CC CC
5 失敗確定 失敗確定 CC CC CC CC CC CC
6 失敗確定 失敗確定 CC CC CC CC CC CC
7 失敗確定 失敗確定 失敗確定 CC CC CC CC CC
8 失敗確定 失敗確定 失敗確定 CC CC CC CC CC
9 失敗確定 失敗確定 失敗確定 失敗確定 CC CC CC CC
10 失敗確定 失敗確定 失敗確定 失敗確定 CC CC CC CC
11 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC CC CC
12 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC CC CC
13 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC CC
14 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC CC
15 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC
16 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC
17 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定
18 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定
19 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定
9 10 11 12 13 14 15
0 同じ 同じ 同じ 同じ 同じ 同じ 同じ
1 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定
2 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定
3 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定 通常判定
4 CC CC CC 通常判定 通常判定 通常判定 通常判定
5 CC CC CC CC CC CC 通常判定
6 CC CC CC CC CC CC CC
7 CC CC CC CC CC CC CC
8 CC CC CC CC CC CC CC
9 CC CC CC CC CC CC CC
10 CC CC CC CC CC CC CC
11 CC CC CC CC CC CC CC
12 CC CC CC CC CC CC CC
13 CC CC CC CC CC CC CC
14 CC CC CC CC CC CC CC
15 CC CC CC CC CC CC CC
16 CC CC CC CC CC CC CC
17 CC CC CC CC CC CC CC
18 CC CC CC CC CC CC CC
19 失敗確定 CC CC CC CC CC CC
20 失敗確定 CC CC CC CC CC CC
21 失敗確定 失敗確定 CC CC CC CC CC
22 失敗確定 失敗確定 CC CC CC CC CC
23 失敗確定 失敗確定 失敗確定 CC CC CC CC
24 失敗確定 失敗確定 失敗確定 CC CC CC CC
25 失敗確定 失敗確定 失敗確定 失敗確定 CC CC CC
26 失敗確定 失敗確定 失敗確定 失敗確定 CC CC CC
27 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC CC
28 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC CC
29 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC
30 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 失敗確定 CC

 この表を見れば通常判定とクリティカルコールのどちらを選べばよいか一目瞭然である。
 この一覧表はエクセルでif構文で作った。本当はもっとスマートな書式で書いたんだけど、どういうわけかエクセルが暴走して0を0として認識してくれなくなってどうにもならなかったので野暮ったいif構文を使わざるを得なくなった。
 いつものように図などを作ったエクセルを上げておく。
ウタカゼ判定.xls

 最後に重要なこと。
 ウタカゼはこういうギリギリのところで優劣を求めて戦うルールではありません。ほのぼのした雰囲気でTRPGを楽しみましょう。

関連エントリー
 20150807 ドラスレ確率計算
 20150722 ドラスレ遊んだよ
 20150516 でたとこサーガ 判定の達成値と確率について
 20150505 でたとこサーガ遊んだよ
 20140531 複数のダイスを振ったときの出目の合計が任意の値となる確率
 20120929 アサルトエンジン リロールの確率について