プリパラのコーデをコンプするための期待値

プリパラにハマっています。課金が前提のゲームに熱中するのは初めてです。

12月末にプリパラ筐体デビューを果たしたのですが、ファイルやらグミやら付録目当ての関連書籍やらを含めると既に万単位でお金をつぎ込んでいる気がします。

射幸心を煽るというやつなのでしょうか。コーデを全身揃えたくなって次から次へとは百円玉を筐体に入れてしまいます。その時の自分は何も考えていないのでとても幸せです。

このまま無計画に百円玉を入れていてはいつか破産するかもしれません。プリパラのために破産するなら本望です。しかし、どれくらい百円玉を入れればコーデが揃うのか見通しを立てておいたほうが借金の計画も立てやすいでしょう。

というわけでプリパラのコーデを揃えるには何枚百円玉を入れればいいのか計算してみることにしました。


http://members3.jcom.home.ne.jp/zakii/enumeration/30_capsuletoy.htm

http://doryokujin.hatenablog.jp/entry/2012/05/09/034209



「ガチャ コンプ 期待値」などでググるとこういう記事がヒットします。こういう記事によるとこれはCoupon Collector's Problem と呼ばれている問題であり、ガチャをコンプする期待値はガチャの種類をn個とすると

n\Sigma_{i=1}^{n} 1/ i

だそうです。これで終わりです。ぱちぱちぱち。

ここで東堂シオンが「一件落着!」と野太い声で叫び、すかさずレズの南委員長から「私の計算では云々」ツッコミが入ります。

上の式はガチャがn種類で、その全てが等確率で出現する場合の期待値です。

プリパラはシチュエーションがだいぶ違います。

プリパラ概論

プリパラのシステムについて簡単に説明しましょう。

プリチケをプレイすると貰えるキラキラした可愛らしい代物、プリチケはパキるために上部と下部に分かれています。(「トモチケをパキる」は本当に美しい日本語だと思います。)


プリチケの部品は大きく分けてマイチケ(下部)、トモチケ(上部)、ガチャチケ(上部)の3つがあります。

マイチケは百円玉を入れると必ず貰えます。

しかし、トモチケはライブをした時だけ、ガチャチケは逆にライブをしなかった時にしか貰えません。



さらにコレクションの対象となるのは

マイチケ(下部)に印刷されるコーデ

ガチャチケ(上部)に印刷されるヘアアクセ

ガチャチケ(上部)に印刷されるアゲアゲアイテム

の3つです。ややこしいですね。ガチャチケにはさらに二つの種類があるのです。

コーデとヘアアクセは月替りで、アゲアゲアイテムは数ヶ月のシーズンごとにアップデートされるようです。

トモチケは自分が印刷されているものであり、アイテムが貰えるわけではないのでコレクションの対象にはならないでしょう。


ライブをする、しないの二つの場合にわけて何が貰えるのか整理します。

1.ライブをする

ライブ後にプリズムストーンに行くことができトモチケとマイチケがもらえる。

トモチケは自分が印刷されているもので、友達と交換して遊ぶ。

プリズムストーンに行くとコーデ(トップス、ワンピース、ボトムス、シューズ)がランダムで二種類表示され、そこから好きな片方を選んでマイチケにできる。

コーデにはレア度がありN、R、SR、PRの順で出にくくなる。



2.ライブをしない

ライブ後にさらに百円玉を入れる、もしくはライブをせずに直接プリズムストーンに行くと、ガチャチケとマイチケがもらえる。

マイチケに関してはライブ時と同じ。

ガチャチケは 所謂普通のガチャで、アゲアゲアイテムかヘアアクセが貰える。

アゲアゲアイテムとヘアアクセにはレア度がなく、変わりにレベルがある。レベルが高いと出にくいのか、それとは別にアイテム別に出にくさが設定されているのか不明。


ややこしいので後者に絞って考えます。レオナの気持ちになってめがにぃに貢ぎまくることを想定します。

問題設定


想定するシチュエーションを整理すると

・百円玉を一枚入れるとマイチケとガチャチケが一つずつ貰える

・マイチケはランダムに出現した二つのコーデから選択する自由がある。出現率はレア度によって変わる

・ガチャチケは選択の自由がない。出現率がどうやって決まるのか不明
・全身コーデを揃えるにはヘアアクセをガチャチケとして、それ以外をマイチケとして入手する必要がある

となります。


冒頭で紹介したシンプルな設定でも結構ややこしいので、この場合の期待値をきちんと数学を使って求めるのは難しそうです。
というか不明点が多々あります。

そこで不明点を妄想で補ったシミュレーションを行って、コンプまでの平均プレイ回数を計算してみます。


コンプするのは現在行われている2015 3rdライブ 2月のアイテムのうち、見た目に関わるコーデとヘアアクセを対象とします。

また、ヘアアクセのレベルは問わないことにします。

アゲアゲアイテムはシーズン替りだし、見た目に反映されないので血眼になって集める必要はないんじゃないかと思います。

シミュレーションの方針

シミュレーションの方向性は2つあると思います。

・コーデが2つでてきて片方を選びプリチケ化するというプロセスをそのままシミュレーションする
・2つから片方を選ぶプロセスを無視し、最終的にプリチケ化するところだけをシミュレーションする

今回は素直に前者を採用しました。

前者を採用するとなると、2つのコーデから片方を選択する方針を決める必要があります。

選択方針としては、「持っていないほうを選ぶ。両方とも持っていない場合にはレア度が高いほうを選ぶ。両方とも持っておらずさらにレア度が等しい場合は、ランダムに片方を選ぶ」という方針を採用します。
実際には、持っていないPRとSRが出てきてSRの全身コーデを揃えたいからSRを選ぶということも考えられるでしょうが、今回はシンプルな方針を採用しました。

わかっていること

http://pripara.jp/item/pdf/2015_3rd02_itemlist.pdf

http://pripara.jp/item/2015_3rd_01.html

プリパラの公式サイトをみれば


・2015 3rdライブ 2月のレア度別のコーデの総数

N:6種類

R:6種類

SR :7種類

PR:6種類


・2015 3rdライブ のアゲアゲアイテムの総数:9種


・2015 3rdライブ 2月のヘアアクセの総数:12種


がわかります

わからないこと

・レア度とコーデの出現率の関係
仕方がないので手持ちのプリチケから最尤推定します。最尤推定という言葉がかっこいいので使いますがただ数えてるだけです。

N:24枚(スターランク差分:1、重複:2を含む)

R:19枚(スターランク差分:1)

SR :10枚(スターランク差分:1)

PR:7枚(スターランク差分:1)

月次ライブで獲得した手持ちのマイチケを数えると以上でした。
サイリウムコーデやパラダイスコーデ等、貰えるコーデが確定しているライブで手に入れたプリチケは無視しています。

しかし、コーデは同時に2つでてきます。実際にプリズムストーンに出現したコーデ数は手元にあるマイチケの倍であるはずです。

今回のシミュレーションを行う上では、マイチケにする確率ではなく、プリズムストーンにコーデが出現する確率が必要になります。
このままではプリズムストーンにコーデが出現する確率は推定できません。
そこで、基本的にレア度が高いほうを選んでいるはずという仮定とあいまいな記憶にもとづいて、マイチケにしなかったコーデの枚数を補完します。

レア度 手持ちのマイチケ枚数 プリチケ化したNと同時に出現した気がする回数 プリチケ化したRと同時に出現した気がする回数 プリチケ化したSRと同時に出現した気がする回数 プリチケ化したPRと同時に出現した気がする回数 プリズムストーンに出現した想定回数(合計) `推定出現確率
N 24 24 10 6 3 67 67/120
R 19 0 9 3 3 34 34/120
SR 10 0 0 1 1 12 12/120
PR 7 0 0 0 0 7 7/120

水増しした頻度に基づきレア度ごとの出現確率を推定しています。
さらにシミュレーションを行う上で、同じレア度のコーデは同じ確率で出ることにします。これはそれほどまずい仮定ではないと思います。

私は出現したコーデが両方とも持っていた場合に、スターランクアップあるいは手持ちと重複するコーデを選んでいます。
このへんも考慮したほうが良さそうですが、今回は無視します。



・ガチャチケの出現確率

全然情報がないので、一様分布を仮定します。つまり、どのアゲアゲアイテムもガチャチケも同じ確率で出るということにします。レア度が高いコーデのヘアアクセが出にくいような気がするのですが、そういうところを推測でやるのはやめます。



ここまで情報を集めるとコードが書けます。

長いので最後に載せます。

シミュレーション結果

コーデコンプ、ヘアアクセコンプ、コーデとヘアアクセコンプ、PRコーデ(ヘアアクセ含む)の片方をコンプ、PRの両方をコンプ
について、100000回試行してコンプまでの平均プレイ回数と標準偏差を出しました。

PRコーデについて計算したのはやっぱりPRコーデを揃えたいからです。今月のPRはドロシーとレオナのノクターンスカイアイドルコーデです。とても可愛いのでとてもよいと思います。

コンプ対象 コンプまでの平均プレイ回数 標準偏差
コーデとヘアアクセ 136.8 55.62
コーデ 134.7 56.93
ヘアアクセ 65.0 24.91
PRの片方 64.3 33.34
PR両方 126.0 60.65


標準偏差がデカいですね。
百円玉が140枚くらいあれば2月のコーデが揃えられるみたいです。もっとあくどいかと思いましたが、こんなもんなんですね。しかし、女児がコンプを目指すのは厳しいと思います。
最近ドロシーのノクターンスカイアイドルコーデが揃ったのですが、この結果を見て自分は運がよかったほうなのかなと思いました。


この結果はわからないことを推測して出したので大体あってる保証すらもありません、一応。

コード

最後にコードです。
pythonですが、無駄にpython3です。

#! coding:UTF-8

import random
from itertools import accumulate

class coordInfo:
    def __init__(self, number, rate):
        self.number = number
        self.rate = rate

#コーデはレア度によって重みをつける
def createCoordDistribution(coords):
    z = 0.0
    dist = []
    #レア度が同じコーデは全て同じ確率で出る
    for coord in coords:
        dist += [coord.rate] * coord.number
        z += coord.rate * coord.number
    #正規化
    dist = [d/z for d in dist]
    #累積分布の計算
    accum = list(accumulate(dist))
    return dist, accum

#ガチャは一様分布
def createGachaDistribution(ageage, hairAcce):
    dist = [1] * (ageage + hairAcce)
    z = sum(dist)
    dist = [d/z for d in dist]
    accum = list(accumulate(dist))
    return dist, accum

#離散累積分布から(0,1)の一様分布を使ってサンプリングする
def sampling(accum):
    r = random.random()
    for i, a in enumerate(accum):
        if r < a:
            return i

def isCompleted(gotten):
    return all(gotten)

#後半acceNum個をヘアアクセに、残りの前半をアゲアゲアイテムに割り当てる
def isCompletedHairAcce(gotten, acceNum):
    return isCompleted(gotten[-acceNum:])

#コーデは出現率の高い順から並んでいる。
#PR最後3つをドロシーのノクターンスカイアイドルコーデに、PR残りの3つをレオナに割り当てる。
#ヘアアクセも動揺。
def isCompletedPrareOne(coordGotten, gachaGotten):
    dorothy = isCompleted(coordGotten[-3:]) and gachaGotten[-1]
    reona = isCompleted(coordGotten[-6:-3]) and gachaGotten[-2]
    return dorothy or reona

def isCompletedPrareTwo(coordGotten, gachaGotten):
    dorothy = isCompleted(coordGotten[-3:]) and gachaGotten[-1]
    reona = isCompleted(coordGotten[-6:-3]) and gachaGotten[-2]
    return dorothy and reona

def updateCoordGotten(coordAccum, coordGotten):
    s1 = sampling(coordAccum)
    s2 = sampling(coordAccum)
    #違うコーデが出るまで粘る
    while s1 == s2:
        s2 = sampling(coordAccum)
    #持っていないほうをとる
    if coordGotten[s1] and not coordGotten[s2]:
        coordGotten[s2] = True
    elif not coordGotten[s1] and coordGotten[s2]:
        coordGotten[s1] = True
    elif  not coordGotten[s1] and not coordGotten[s2]:
        #両方持っていない場合には確率が低い=レア度が高いほうをとる
        if coordDist[s1] < coordDist[s2]:
            coordGotten[s1] = True
        else:
            coordGotten[s2] = True
    else:
        #両方持っている場合は何もしない
        pass


def updateGachaGotten(gachaAccum, gachaGotten):
    s = sampling(gachaAccum)
    gachaGotten[s] = True


def calc(values):
    average = sum(values) / len(values)
    stdev = 0
    for v in values:
        stdev += (v - average)**2
    stdev = (stdev / len(values)) ** 0.5
    return average, stdev

def printResult(message, average, stdev=None):
    if stdev is None:
        stdev = average[1]
        average = average[0]
    print("{0:<23}".format(message), end="")
    print("{0:.1f}".format(average), "{0:.2f}".format(stdev))

if __name__ == '__main__':
    #手元にあるプリチケ枚数 + Nと同時に出た気がする回数 + Rと~ + SRと~ + PRと~
    pRareNum =  7 +  0 +  0 +  0 +  0 #スターランク差分1、重複0
    sRareNum = 10 +  0 +  0 +  1 +  1 #スターランク差分1、重複0
    rRareNum = 19 +  0 +  9 +  3 +  3 #スターランク差分1、重複0
    nRareNum = 24 + 24 + 10 +  6 +  3 #スターランク差分1、重複2

    pRare = coordInfo(6, pRareNum)
    sRare = coordInfo(7, sRareNum)
    rRare = coordInfo(6, rRareNum)
    nRare = coordInfo(6, nRareNum)

    ageage = 9
    hairAcce = 12

    expRun = 100000

    coordinates = [nRare, rRare, sRare, pRare]

    coordDist, coordAccum = createCoordDistribution(coordinates)
    gachaDist, gachaAccum = createGachaDistribution(ageage, hairAcce)

    coordComp, gachaComp, allComp, pRareOneComp, pRareTwoComp = [], [], [], [], []

    for i in range(expRun):
        run, coordRun, gachaRun, pRareOneRun, pRareTwoRun = [0] * 5
        coordFlag, gachaFlag, pRareOneFlag, pRareTwoFlag = [True] * 4
        coordGotten = [False] * len(coordAccum)
        gachaGotten = [False] * len(gachaAccum)
        while not isCompleted(coordGotten) or not isCompletedHairAcce(gachaGotten, hairAcce):
            updateCoordGotten(coordAccum, coordGotten)
            updateGachaGotten(gachaAccum, gachaGotten)
            run += 1
            if coordFlag and isCompleted(coordGotten):
                coordRun = run
                coordFlag = False
            if gachaFlag and isCompletedHairAcce(gachaGotten, hairAcce):
                gachaRun = run
                gachaFlag = False
            #PRのどちらか片方が揃ったら
            if pRareOneFlag and isCompletedPrareOne(coordGotten, gachaGotten):
                pRareOneRun = run
                pRareOneFlag = False
            #PRのどちらか両方が揃ったら
            if pRareTwoFlag and isCompletedPrareTwo(coordGotten, gachaGotten):
                pRareTwoRun = run
                pRareTwoFlag = False

        allComp.append(run)
        coordComp.append(coordRun)
        gachaComp.append(gachaRun)
        pRareOneComp.append(pRareOneRun)
        pRareTwoComp.append(pRareTwoRun)

    printResult("all items", calc(allComp))
    printResult("all coodinates", calc(coordComp))
    printResult("all hair accessories", calc(gachaComp))
    print()
    printResult("one PR", calc(pRareOneComp))
    printResult("two PRs", calc(pRareTwoComp))