こんにちは。くろこんです。今回はオンラインゲーム原神のガチャ回数の分析です。不規則な天井があるガチャシステムは、高校数学では追いつきません。絶望の世界をご案内します。
計算には Python を使っていきます。前提条件の参考などに、以下のサイト様を利用させていただきました。
不規則な天井システム
ランダムなアイテムが排出される通称ガチャ。レアアイテムが一定の確率で出現するのが通例です。オンラインゲーム原神では少し確率に偏りがあります。
- 通常時の最高レア度の出現率は 0.6 %である
- 前の73回のガチャで最高レア度のアイテムが出現していない場合、以降1回のガチャごとに最高レア度の出現率が 6 %上昇する
特に2つ目の「 6 %ずつ上昇」する条件が非常に問題を難しくしています。
参考にしたサイト様の結果なども含めて検討しましたが、おそらく、高校数学では太刀打ちできないものだと思われます。
今回は、大学で習うマルコフ連鎖をつかって、高精度に確率を計算したいと思います。
マルコフ連鎖
マルコフ連鎖は簡単に言うと前の状態だけに依存する確率の表現となります。今回の場合、前のx回のガチャで最高レア度のアイテムが出現していないという状態を定義して、確率を考えます。
上の図は、1回ハズレまで状態遷移を記載したマルコフ連鎖です。89回外れると必ず最高レア度が出現するため、89回ハズレまで状態は存在します。
この状態遷移を0回から89回のハズレについて考えます。例えば、73回ハズレた状態だと出現率の上昇が起こり 6.6 %の当選確率となります。
マルコフ連鎖を考えたら、行列へ変換します。
\begin{pmatrix}
0.006 & 0.006 & 0.006 & \cdots & 0.906 & 0.966 & 1.0 \\
0.994 & & \\
& 0.994 & \\
& & 0.994 \\
& & & \ddots \\
& & & & 0.094 \\
& & & & & 0.034
\end{pmatrix}
各列が状態の番号で、要素が遷移確率になっています。このように表現すると、状態遷移を行列積に置き換えることができます。
もし初期状態で0回外れた状態であれば、0.006 の確率で最高レア度が当選して外れた回数はリセットされ、0.994 の確率で1回外れた状態になります。
一方で、ここで行列積の例として、$(1, 0, 0,\ \ldots \ ,0)$ のような初期状態が与えられた場合、この行列をかけることで、$(0.006, 0.994, 0, \ \ldots \ ,0)$ に変化します。
行列積に置き換えるというのは、この行列積の結果と、先ほどの状態遷移後の確率が表現として一致しているということです。
ここで少し考えます。行列積が状態遷移になるなら、行列積を何度も繰り返すことで、極限の確率を得られるのではないか? そんな気がしなくもないです。
この直感は結果的には正しく、$(1, 0, 0,\ \ldots \ ,0)$ に先ほどの行列を何回もかけると、極限の確率が現れます。(極限なので細かいいろいろな条件はありますが、今回は問題ないのでこのまま計算をします。数学的な細かい部分は定常状態などを調べるといろいろ出てくると思います)
この時、0回ハズレにいる確率がそのまま最高レア度の当選確率になります。極限における0回ハズレということは、無限のガチャをしてランダムに止めて直前に当たっている確率を表しています。
Python で計算
def getPr_from_i_to_j(i, j, p = 0.006, th = 72):
if j != 0 and j != i+1: # 当選するか、ハズレ回数が1増えるか、以外は確率 0
return 0.0
a = 1.0 # p にかかる倍率
b = 10.0 # 元の確率の 10 倍ずつ上がっていく
if i > th: # th = 72 なので本文と見た目の整合性が弱くてすみません
a = (i - th) * b + 1.0
pr = p * a # 当たる確率
if pr > 1.0:
pr = 1.0
if j == 0:
return pr # j == 0 は当たる確率
else:
return 1.0-pr # j == i+1 はハズレる確率
先ほどの通りに行列の要素を計算します。
def calc():
size = 90
mat = np.zeros((size, size))
for i in range(size):
for j in range(size):
# i から j への確率なので引数がまぎらわしい
mat[i, j] = getPr_from_i_to_j(j, i)
p = np.zeros((size, 1))
p[0,0] = 1.0
s = 10000
for i in range(s):
p = np.matmul(mat, p)
print(p[0,0])
0.01605205178552114
10000 回も行列積をすれば十分に収束します。参考にしたサイト様と同等の値を算出できました。今回は、外部サイト様と値が一致したということで、検算もできているとしています。
最高レア度のうちのレア
いわゆるピックアップの出現確率です。以降ではピックアップと表します。
また、最高レア度の出現時にピックアップが出現しないことを、すり抜けと表記することが多いです。この記事内でも同じように表現します。
- ピックアップは最高レア度の出現に対して 50 %である
- 前回の最高レア度の出現時にピックアップが出現せずすり抜けた場合、次の最高レア度のアイテムはピックアップになる
- すり抜けの判定を3回連続で満たした場合、次のすり抜け判定時の最高レア度のアイテムはピックアップになる(掴みし明光)
3つ目の条件は言い換えれば、すり抜けの判定を3回連続で満たした場合、次の最高レア度は2つ目の条件によりピックアップが出現し、さらにその次の最高レア度は3つ目の条件によりピックアップが出現する、ということです。
参考にしたサイト様では、3つ目の条件は他にも発動することがあるらしいです。しかし、今回は確定の発動条件だけ考慮します。従って、求める確率は下限の値で、実際のピックアップ出現率はもっと高い可能性があります。また、以降では3つ目の条件を掴みし明光と表します。
Python で計算 その2
先ほどの条件から、すり抜けに関することも合わせて状態に表記します。
- ハズレの回数(0~89)
- 前回すり抜けたか(0~1)
- すり抜け後ピックアップ確定の連続回数(0~3)
掴みし明光により、すり抜け後ピックアップ確定が3回連続するとピックアップが出現するので、「ピックアップ確定の3回連続」のときだけは「前回すり抜け」となりません。従って状態数は、90×7=630状態になります。
存在する状態の例です。630と数は多いですが、頑張って記述します。
def getPr_from_i_to_j_large(i, j, p = 0.006, th = 72, ceil = 90):
if j != 0 and j != i+1 and ( j % ceil != 0 or j // ceil != i // ceil + 1 ):
return 0.0
# パターン1:すり抜けせずにピックアップが当たる
# パターン2:ハズレの回数が1増える
# パターン3:パターン1ではないが最高レア度が当選
# どれかにも該当しないなら 0 を返す
a = 1.0
b = 10.0
if i % ceil > th:
a = (i % ceil - th) * b + 1.0
pr = p * a
if pr > 1.0:
pr = 1.0
# ハズレの場合(掴みし明光ではない89回ハズレの☆5確定の場合もここ)
if j == i+1:
if j % ceil != 0:
return 1.0-pr # 通常時 ハズレ
# ☆5確定の場合
if j % (ceil * 2) == 0:
return 1.0 # ピックアップ確定 かつ ☆5確定
return 0.5 # ☆5確定
# すり抜けせずにピックアップが当たる場合
if j == 0:
if i == ceil * 7 - 1:
return 1.0 # 掴みし明光 かつ ☆5確定
if i // ceil == 6:
return pr # 掴みし明光
if (i // ceil) % 2 == 1:
return 0.0 # 通常時 ピックアップ確定で j == 0 へ遷移はしない
return pr / 2 # 通常時 ☆5ピックアップ
if (j // ceil) % 2 == 1:
return pr / 2 # 通常時 ☆5すり抜け
return pr # 通常時 ピックアップ確定
def calc_large():
size = 90 * 7
mat = np.zeros((size, size))
for i in range(size):
for j in range(size):
mat[i, j] = getPr_from_i_to_j_large(j, i)
p = np.zeros((size, 1))
p[0,0] = 1.0
s = 10000
for i in range(s):
p = np.matmul(mat, p)
print(p[0,0] + p[180,0] + p[360,0] + p[540,0])
print(p[0,0] + p[90,0] + p[180,0] + p[270,0] + p[360,0] + p[450,0] + p[540,0])
# この数値や近似値などを載せる場合は、この記事の URL などを併記してください
0.010944580762855424
0.016052051785521285
かなり面倒ですが、頑張って行列を生成することで、マルコフ連鎖として計算ができます。(個人的には十分に絶望だと思います)
出力の1つ目は、0 180 360 540 番目の確率の和を表します。0 はすり抜けずにピックアップが出現する確率、180 360 540 はすり抜け直後のピックアップ確定の確率なので、この和はピックアップの出現率になります。
出力の2つ目は、0 90 180 270 360 450 540 番目の確率の和を表します。90 の倍数の状態は、☆5が当たった状態であるため、この和は☆5の出現率になります。これは、前に計算した☆5の出現率と一致するため、行列の計算は正しかったことが確認できます。
今回、頑張って確率計算をしました。生成した行列は630次正方行列で、要素数10万越えとすごい規模なので、間違いなどあるかもしれません。
この記事の内容で、恐ろしい数学の世界の一部でも知ってもらえればと思います。
原神プレーヤーさんをビビらせるおまけ
上記のピックアップ出現率は現実の多くの原神プレーヤーさんにとっては間違いです。
……え? こんなに計算したのにウソなの? と思われそうですが、計算は正しく進めているつもりです。間違いなのは現実の原神プレーヤーさんは極限までガチャをしないという当たり前の条件を使っていないことです。
例えば、ガチャ1回でのピックアップの出現率は 0.003 です。一方で、無限回ガチャすると 0.010944580762855424 になります。つまり、任意のガチャ回数でこれらの値の中間や近い確率になっているということです。
以上を含めて、上記のプログラムだと s = 10000 がガチャ回数で、これだと収束しているので、PC による計算の精度では10000回は無限回と見なせることになります。一方で、例えば s = 1000 では収束しないので PC の小数処理で確認できるくらい異なる確率になります。( 10000 回ガチャをするのが普通だったらすみません)
「これらの値の中間や近い確率」というのは極限値より高い確率を含みます。しかし、極限値より高い確率というのはあくまでも確率です。
例えば、77 回目は極限値より当たりやすく、0.056116185249394594 まで上昇します。
正確に書くと新規でアカウントを作った状態から 76 回のガチャの記憶が消し飛び 77 回目のガチャをすると極限値より高い確率で当たります。一方で、普通の人は 76 回のガチャの記憶がいくらかあるので、その記憶による状態の確率を正しく認識してしまいます。
今回の計算で得られた値は、全原神プレーヤーの新規でアカウントを作った状態からのガチャ回数ごとのレア出現の期待値と言うべき値です。
補足として極限値より高くなる理由は、73回外れた場合の確率上昇の波と、すり抜け判定による確率上昇の波と、掴みし明光による確率上昇の波の3つがあり、73回外れた場合の確率上昇の波が最も強い影響となるためです。
例えば、77 回目が 0.056116185249394594 なのは、73 回目まで 0.003 だった分の埋め合わせということになります。
おまけ 検算
def sim(itr):
pickup = 0
common = 0
state = 0
ceil = 90
th = 72
p = 0.006
for i in range(itr):
x = random.random()
a = 1.0
if state % ceil > th:
a = (state % ceil - th) * 10 + 1.0
pr = p * a
if pr > 1.0:
pr = 1.0
if x > pr:
state += 1
continue
if state // ceil == 6:
state = 0
pickup += 1
continue
if (state // ceil) % 2 == 1:
state = (state // ceil + 1) * ceil
pickup += 1
continue
y = random.random()
if y > 0.5:
state = (state // ceil + 1) * ceil
common += 1
continue
state = 0
pickup += 1
print(pickup, common, (pickup + common) / itr, pickup / itr)
109706 50884 0.016059 0.0109706
10000000 回ガチャすることで、大体確率通りの出現数であることが確認できます。
(あくまでも確率が 10000 回で収束するだけなので、実際に当たった割合はこの程度では収束しないということですね……)