机上のkuwa論

デュエルマスターズ考察 / 間違いがあればコメントでご指摘お願いします

シールド・トリガー確率をはじめからていねいに〜後編〜

f:id:kuWaan:20190811134754p:plain

つづき

前編を読んだ方は、シールド・トリガーの確率と期待値計算が、どう計算されているか理解してくれたと思います。

前編読んでない方は先にこちらをお読みください。

なお、今回の内容は、前編よりも数式多めかつ小難しい内容で、オリジナルの視点から考察していきます。ちょっとしんどいかもしれません。

そこで、「確率 ? は ? そんなん手を2回叩いて盾にお祈りしたらええやん。」って人に期待値が身近であると知ってもらいたいので、コーヒーブレイクとして少し雑談をします。興味ない方は飛ばして下さい。

索引

(雑談) DMGPで入賞して、やきにく代が稼げるのか簡単に計算してみた。

デュエル・マスターズプレイヤーは、よく「優勝して焼肉おごる」宣言をします。
界隈には、大型大会でプロモやゲーム機を獲得し、売却し、プレイヤーはよく一緒に調整する仲間、いわゆる身内とともに焼肉に行く風潮が存在します。

というわけでトーナメントガチ勢DMPはGPで稼いで焼肉をおごれるのか、はたまた、 そもそも参加費と交通費で焼肉にいったらいいのか知りたいので計算してみましょう。

今年の4月13日と4月14日に開催されたDMGP8thを例にあげましょう。DMGP8thは予選がスイスドロー式で9回戦、決勝トーナメントが勝ち抜け方式で7回戦で行われました。

つまり、予選+決勝で16回戦ありますが、決勝トーナメントで勝ち、ベスト8に入れば《超戦龍覇 モルトNEXT》 GP限定Verと《奇石 ミクセル/ジャミング・チャフ》GP限定Verを獲得することができます。

よって、予選+決勝で13回戦勝ち続ければ、そのGPプロモを獲得できるというわけです。

あるAさんとBさんが1回対戦すると、それぞれ勝つ確率が \displaystyle \frac{1}{2}、負ける確率が \displaystyle \frac{1}{2}です。

よって、予選+決勝で13回戦勝ち続ける確率は

 \displaystyle({\frac{1}{2} })^{13} = \frac{1}{8192} = 1.220703125 \times 10^{-4}



《奇石 ミクセル/ジャミング・チャフ》は、メルカリで約7万弱で取引されていました。

《超戦龍覇 モルトNEXT》 は、メルカリで約70万弱で取引されていました。

よって、

 \displaystyle({\frac{1}{2} })^{13} \cdot 770000 = 93.99414063・・・

結論:DMPがGP8thに出場し、ベスト8まで勝ち続けると、93円稼ぐことが期待されます。

93円では焼肉にいけないので、交通費と宿泊費で身内と焼肉にいきましょう。




すいません。 ↑の計算は実は真っ赤な嘘が入ってます。
対戦にはいろんな要素があるので、確率 \frac{1}{2}は厳密ではありません。当たり前ですけど、dm上手い人は勝つ確率が高いですから。また、予選のスイスドロー式はOMWが関わってくるので、予選を勝ち抜く確率が複雑です。今回の思考実験では「全て全勝する」と簡単化しています。
単純に勝率は求まらないんで、期待値計算できません。残念。もしもコイントス大会だった場合とかですかね。まあ、あんま意味ないですね。期待値に興味持ってくれれば幸いですというお話でしたー。


「初手シールドにおけるS・トリガー率、および期待値」を求める一般式

前編では、具体例で確率計算をしました。その計算を一般化(どの枚数であっても、当てはめて確率を求めることができる式に)しましょう。

前編の最後に、一般化しました。これですね。
期待値: E(x) = 1 \cdot p_1 +2 \cdot p_2+3 \cdot p_3 +4 \cdot p_4+5 \cdot p_5  \cdot \cdot \cdot (A)
この p_iについても一般化できることに気づいたので、示します。

デッキに、シールドトリガーを x枚採用したとき、


 p_1 p_1 =  \displaystyle \frac{x}{40}  \cdot \frac{40-x}{39}  \displaystyle \cdot \frac{39-x}{38} \cdot \frac{38-x}{37} \cdot \frac{37-x}{36} \cdot {}_5C_1  = {}_5C_1 \cdot \displaystyle \frac{x(40-x)(39-x)(38-x)(37-x)}{{} _{40}P_5}
 = {}_5C_1 \cdot \displaystyle \frac{{}_{x}P_1  \cdot  {}_{40-x}P_4}{{} _{40}P_5}


 p_2 p_2 =  \displaystyle \frac{x}{40}  \cdot \frac{x-1}{39}  \displaystyle \cdot \frac{40-x}{38} \cdot \frac{39-x}{37} \cdot \frac{38-x}{36} \cdot {}_5C_2  = {}_5C_2 \cdot \displaystyle \frac{x(x-1)(40-x)(39-x)(38-x)}{{} _{40}P_5}
 = {}_5C_2 \cdot \displaystyle \frac{{}_{x}P_2  \cdot  {}_{40-x}P_3}{{} _{40}P_5}


 p_3 p_3 =  \displaystyle \frac{x}{40}  \cdot \frac{x-1}{39}  \displaystyle \cdot \frac{x-2}{38} \cdot \frac{40-x}{37} \cdot \frac{39-x}{36} \cdot {}_5C_3 =  {}_5C_3 \cdot \displaystyle \frac{x(x-1)(x-2)(40-x)(39-x)}{{} _{40}P_5}
 = {}_5C_3 \cdot \displaystyle \frac{{}_{x}P_3  \cdot  {}_{40-x}P_2}{{} _{40}P_5}


 p_4 p_4 =  \displaystyle \frac{x}{40}  \cdot \frac{x-1}{39}  \displaystyle \cdot \frac{x-2}{38} \cdot \frac{x-3}{37} \cdot \frac{40-x}{36} \cdot {}_5C_4 =  {}_5C_4 \cdot \displaystyle \frac{x(x-1)(x-2)(x-3)(40-x)}{{} _{40}P_5}
 = {}_5C_4 \cdot \displaystyle \frac{{}_{x}P_4  \cdot  {}_{40-x}P_1}{{} _{40}P_5}


 p_5 p_5 =  \displaystyle \frac{x}{40}  \cdot \frac{x-1}{39}  \displaystyle \cdot \frac{x-2}{38} \cdot \frac{x-3}{37} \cdot \frac{x-4}{36} \cdot {}_5C_5 =  {}_5C_5 \cdot \displaystyle \frac{x(x-1)(x-2)(x-3)(x-4)}{{} _{40}P_5}
 = \displaystyle {}_5C_5 \cdot \frac{{}_{x}P_5 }{{} _{40}P_5}



(A)に上記 p_iを代入して、式を整えると、
期待値:

 E(x) =  \displaystyle  \frac{5}{{}_{40}P_5}( {}_{x}P_1 \cdot {}_{40-x}P_4 + 4 \cdot {}_{x}P_2 \cdot {}_{40-x}P_3 + 6 \cdot {}_{x}P_3 \cdot {}_{40-x}P_2 + 4 \cdot {}_{x}P_4 \cdot {}_{40-x}P_1+{}_{x}P_5 \cdot {}_{40-x}P_0 )


さらにΣで書くと、

「初手シールドにおけるS・トリガー率、および期待値」を求める一般式は、

デッキに、シールドトリガーを x枚採用したとき、



期待値: E(x) = \displaystyle \sum_{k=1}^5 \frac{k \cdot {}_{5}C_k \cdot {}_{x}P_k \cdot {}_{40-x}P_{5-k}}{{}_{40}P_5}


ときれいに整えることができました。
数学得意な人、数式チェックお願いします。もっときれいにできるかもしれません。リプorコメント待ってます。

「なぜ、計算式の一般化にこだわるのか?」という人がいるかもしれません。それは、式の抽象度をあげると、プログラムで期待値計算をコンピュータに任せることができるからです。上で定義した式をPythonプログラムで書いてみました。参考程度にどうぞ。

import math
print('シールド・トリガー期待値計算プログラムを起動しました.')
print('何枚デッキにシールド・トリガーを入れますか?、入力してください.')
x = input('シールド・トリガーの枚数: ') #任意の枚数を入力 
x = int(x)

def permutations_count(n, r): #順列計算用
	if n -r <= 0 or n <= 0: 
		return 1
	else:
		return math.factorial(n) // math.factorial(n - r)
    
def combinations_count(n, r): #組み合わせ計算用
	if n - r  <= 0  or n <= 0:
		return 1
	else:
		return math.factorial(n) // (math.factorial(n - r) * math.factorial(r))

def func1(k): #k
	return k

def func2(k): # 5Ck
	return combinations_count(5,k)

def func3(x, k): #xPk
	return permutations_count(x,k)

def func4(x,k): #40-xP5-k
	return permutations_count(40-x,5-k)

def func5(): #40P5
	return permutations_count(40,5)

def sigma(func1,func2,func3,func4,func5): 
    result = 0
    for k in range(1, 6):
        result += (func1(k)*func2(k)*func3(x,k)*func4(x,k))/func5() 
        if k == 1:
        	p1 =  (func1(k)*func2(k)*func3(x,k)*func4(x,k))/func5()
        elif k == 2:
        	p2 =  (func1(k)*func2(k)*func3(x,k)*func4(x,k))/func5()
        elif k == 3:
        	p3 =  (func1(k)*func2(k)*func3(x,k)*func4(x,k))/func5()
        elif k ==4:
        	p4 =  (func1(k)*func2(k)*func3(x,k)*func4(x,k))/func5()
        elif k == 5:
        	p5 = (func1(k)*func2(k)*func3(x,k)*func4(x,k))/func5()    
        	
    print()
    print('シールド・トリガーがシールドに1枚入っている確率は{}%です.'.format(round(p1*100,2)))
    print('シールド・トリガーがシールドに2枚入っている確率は{}%です.'.format(round(p2*100/2,3)))
    print('シールド・トリガーがシールドに3枚入っている確率は{}%です.'.format(round(p3*100/3,3)))
    print('シールド・トリガーがシールドに4枚入っている確率は{}%です.'.format(round(p4*100/4,3)))
    print('シールド・トリガーがシールドに5枚入っている確率は{}%です.'.format(round(p5*100/5,3)))
    print()
    print('シールドに{}枚入っていると期待されます.'.format(round(result,2)))

sigma(func1,func2,func3,func4,func5) 


実行結果はこんな感じです。



f:id:kuWaan:20190816005410p:plain

デッキに積むシールド・トリガーの枚数を1~40で入力すると、期待値を自動的にプログラム通り計算機が計算してくれます。Python3.x系がある読者の方は、コピペして、動かしてみてくださいね。

この期待値はゲームで本当に役に立つのか? 実際に検証してみた。

ここまでは、先人たちが計算してきたことでした。ここからは私のオリジナルの視点から
期待値計算について考察していこうと思います。

果たしてこの期待値はデュエルマスターズ の試合で役に立つのでしょうか?
つまり、「期待値は理論値で一応出てるけど、本当にその枚数積めば安心なの?」「期待値なんてただの数字で、実際のDMの試合に何にも関係ないでしょ」「実際のゲームじゃ、計算値のズレがあるのでは?」という議題を検証、証明しようということです。

では検証しましょう。検証方法として、カードを40枚用意して、シールドトリガーを入れて、シャッフルして、机の上に並べて、実際に何枚シールドトリガーが入っているか人力でデータをとるという手段が考えられます。それを何百回、何千回、何万回と繰り返せば、確率(期待値)がいい感じに収束しそうです。

しかし、私はそれはめんどくさいのでPythonプログラムで上記の手法を仮想上で再現しました。コンピュータ(計算機)は、何百回、何千回、何万回のシャッフル処理やシールド・トリガーの確認処理も電気信号が伝わる速度、クロックが立ち上がる時間で、計算が可能です。便利で、楽ですよね。

さあ、デッキにシールド・トリガーを8枚積んだとき、実際に何枚ほど入っているのかを見てみましょう。

試行「 デッキは40枚で構成し、シールド・トリガーを8枚デッキに積んでいるとする。このとき、シャッフルして完全にデッキがランダムになっている状態で、
デッキの上から5枚を確認し、シールド・トリガーが入っている枚数のデータをとる。確認し終わったら、再びシャッフルする。」

上記の試行を1千万回繰り返して、データをとって期待値を計算してみます。さて、これまで理論的に求めていた期待値は、どれくらい信用ができる、つまり実際に試行して得た値と近い値が出るか、外れた値が出るのでしょうか ?


結果



f:id:kuWaan:20190817001106p:plain:w1000

なんと、ほぼほぼ理論値と一致しました ! つまり、期待値は信用できるということです !
面白いですね。桁数のオーダーによりますが、四捨五入すると理論値と一致すると
結論づけて問題ないでしょう。以上の結果より、デュエル・マスターズ公式が推すように、
デッキにシールド・トリガーを最低8枚は積みたいですね。
そうすれば、シールドにシールド・トリガーが1枚入っていることが期待できます。

プログラム解説

そんなに長いコードじゃないので、あんまり解説することないのですが、せっかくなので乗っけておきます。
Python 3.x系の環境があれば、ぜひご自身のマシンで試してみてください。「シャッフルするランダム関数は本当にランダムシャッフルになっているかい?、所詮計算機の乱数はランダムでないでしょ?」という疑問が湧いて当然だと思います。実はPythonにおけるrandom関数の乱数発生はメルセンヌ・ツイスタというアルゴリズムが用いられていて、かなり精度の高い、独立な乱数を発生させます。この2つの文献がとても勉強になりました。詳しくはここを読んでみてください。
http://www.math.sci.hiroshima-u.ac.jp/~m-mat/TEACH/ichimura-sho-koen.pdf
random --- 擬似乱数を生成する — Python 3.7.4 ドキュメント

n =10000000 #何回、試してみるか 
import random
deck = []
c1 = 0 #トリガーが盾に1枚のときのカウンタc1 
c2 = 0 #トリガーが盾に2枚のときのカウンタc2
c3 = 0 #トリガーが盾に3枚のときのカウンタc3
c4 = 0 #トリガーが盾に4枚のときのカウンタc4
c5 = 0 #トリガーが盾に5枚のときのカウンタc5

for i in range(32):  #デッキに32枚入れる
	deck.append('N')
for i in range(8): #デッキに8枚シールド・トリガーを入れる
	deck.append('ST')

for i in range(n): #n回繰り返して、データをとる
	random.shuffle(deck) #シャッフル
	indexD = deck[0:5] #デッキから上5枚とる
	STnum = indexD.count('ST') #STの枚数をカウント
	
	#期待値計算のために各事象をカウント
	if STnum == 1:
		c1+=1
	elif STnum ==2 :
		c2+=1
	elif STnum ==3 :
		c3+=1
	elif STnum ==4 :
		c4+=1
	elif STnum == 5:
		c5+=1

EV = (1*c1+2*c2+3*c3+4*c4+5*c5)/n #期待値計算

print()
print('シールド・トリガーがシールドに1枚入っている確率は{}%です.'.format(c1/n*100))
print('シールド・トリガーがシールドに2枚入っている確率は{}%です.'.format(c2/n*100))
print('シールド・トリガーがシールドに3枚入っている確率は{}%です.'.format(c3/n*100))
print('シールド・トリガーがシールドに4枚入っている確率は{}%です.'.format(c4/n*100))
print('シールド・トリガーがシールドに5枚入っている確率は{}%です.'.format(c5/n*100))
print()
print('シールドに{}枚入っていると期待されます.'.format(EV))

デッキをリストで管理して、
1番目のfor文で、シールド・トリガーでない任意のカードを'N'として、リストに追加します。
2番目のfor文で、シールド・トリガーであるカードを'ST'として、リストに追加します。
3番目のfor文でシールドチェックとその都度ランダムシャッフルを1千万回繰り返しています。

最後に、各事象でのカウントした値をnで割ることで、確率を求めることができ、それぞれ1,2,3,4,5を掛けて足すと期待値が計算できます。

プログラムを立ちあげるごとに、乱数によって、若干、期待値や確率が異なりますが、ほぼ同じ値に収束します。

あとがき

この内容についてご意見がある方は、コメントで書いてもらうか、リプをTwitterにください。(見てないことがあるかもしれないけど、
厳密性にかけている場合があるかもしれないので、今後の考察に活かしたいです! )。またこの記事の内容をもとに自身のブログ記事で発展されることや、批判されることを大いに期待しています 。数学得意な読者のかた、一般式を整えてください。DMブログ大好き人間なので、他の人の意見を聞いてみたいです。
当ブログの全記事は完全にリンクフリーで、かつ内容も全て勝手に引用しても構わないです。何も声かけずやっちゃってください。ブログ名とURL貼ってくれると、主はとても喜びます。