Flask でレアガチャ・シミュレーターを作る

[昨日](http://qiita.com/ynakayama/items/2cc0b1d3cf1a2da612e4)は Python のウェブフレームワーク [Flask](http://flask.pocoo.org/) で簡単な応答システムを作りました。これだけだとあまりに退屈なので、せっかくですから [NumPy](http://www.numpy.org/) を利用して少しは楽しめるものを作ってみたいと思います。

「ガチャ」とは、最近流行のソーシャルゲームやオンラインゲームにおいて 1 回課金することによってアイテム等をランダムに入手できる仕組みのことです。 10 連ガチャとは 1 回の試行で 10 回分のレアガチャを引く仕組みのことです。基本無料を謳うソーシャルゲームなどでは、このようなアイテム課金で利益を得る仕組みになっています。

シミュレーションの目的としては、運営サイドとしては[プレイヤーがじゃぶじゃぶ課金したくなるような射幸心を煽りまくる](http://netgeek.biz/archives/28690)ガチャが提供できているかどうか確認する、プレイヤーサイドとしてはリアルマネーいわゆる実際のお金を投入する前にあらかじめどれくらいの確率で成功するのかを体感する (結果によっては頭を冷やす) といった狙いがあります。

今まで[推測統計](http://qiita.com/ynakayama/items/1d9d2d894a4fc2a60f2c)や[可視化](http://qiita.com/ynakayama/items/94779651d09e1004fd46)などで一部の標本から母集団を推し量ることをしてきましたが、理論だけではなかなか想像できない部分をシミュレーションによって体感として試してみるのも十分に価値のあることです。

## ガチャの仕様

実装するガチャの仕様は次の通りとします。

#### 種類と値段

ガチャには「レアガチャ」と「 10 連レアガチャ」の 2 種類があります。レアガチャは 1 回 300 円、 10 連レアガチャは 1 回 3,000 円です。これを 1 回引くごとにカードが 1 枚入手できるものとします。

#### レアリティ

レアリティというのは希少度と訳されます。希少度の高いほうが価値も高くなります。

余談ですが rarity という単語は正確にはレリティと発音するようです。

|種類|説明|
|:--|:--|
|R|単なるレア。最も価値が低い。|
|SR|スーパーレア。それなりに価値が高い。|
|UR|アルティメットレア。最も価値が高く、これを入手するのがプレイヤーの目的である。|

#### 当選確率

レアガチャの当選確率は次の通りです。

|R|SR|UR
|:--|:--|:--|
|94.85%|5.04%|0.12%|

10 連レアガチャの当選確率は次の通りです。

|R|SR|UR|
|:--|:--|:--|
|90.28%|9.29%|0.45%|

ただし 10 連レアガチャではボーナスとして最後の 1 回は必ず SR が当選するものとします。一見これは優遇措置のようですが、逆に言えば最後の 1 回では決して UR は当選しないということを意味します。

また確率表記は合計が 100 パーセントになっていません。これは小数点第三位以下が存在しそれを「切り上げ」ているからです。なぜ四捨五入ではなく切り上げを仮定するのかと言うと 10 連のほうは合計が 100.02% になりますが 3 種類のレアリティの小数点第三位を仮に四捨五入する前提だとこのような合計値は求まらないからです。

#### 景品の種類

UR を入手することがユーザーの目的です。この UR のカードは景品 1 から景品 12 までの 12 種類あります。 UR が当選した場合、この景品のいずれか 1 枚が[等確率](http://ja.wikipedia.org/wiki/%E7%AD%89%E7%A2%BA%E7%8E%87%E3%81%AE%E5%8E%9F%E7%90%86)で入手できるものとします。

## ガチャの実装

#### 乱数生成

NumPy は[メルセンヌ・ツイスタ](http://random.ism.ac.jp/info01/random_number_generation/node6.html)による乱数を生成します。 [NumPy での乱数生成](http://docs.scipy.org/doc/numpy/reference/routines.random.html)については[ここ](http://qiita.com/airtoxin/items/25b284309aa8fb450883)や[ここ](http://qiita.com/yubais/items/bf9ce0a8fefdcc0b0c97)に日本語の記事があります。

ひとまずレアリティごとの抽出機能を作ります。重み付けによる抽出器を次の通り実装します。

#### 重み付けによるカード排出処理

```py3
def turn_rare():
    """レアガチャを回す"""
    result = []
    # 小数点第三位を切り上げて 94.85%, 5.04%, 0.12%
    weight = [0.94849, 0.0504, 0.00111]
    result.append(pickup_rare(weight))
    return result

def turn_10rare():
    """10 連レアガチャを回す"""
    result = []
    # 小数点第三位を切り上げて 90.28%, 9.29%, 0.45%
    weight = [0.90278, 0.09281, 0.00441]
    # 9 回抽選する
    for v in range(0, 9):
        result.append(pickup_rare(weight))
    # 最後の 1 回は必ず SR が当選する
    result.append("SR")
    return result
```

上記は小数点第三位を切り上げにした値のうち、最も運営サイドが有利になる値を仮定しました。

あとは与えられた重みに応じてカードを排出する処理を記述します。

```py3
def pickup_rare(weight):
    """重みに応じてレアガチャを排出する"""
    rarities = ["R", "SR", "UR"]
    picked_rarity = np.random.choice(rarities, p=weight)

    # UR が当選した場合はどの景品にするのか決定する
    if picked_rarity == "UR":
        picked_rarity = "".join((picked_rarity, "(", pickup_premium(), ")"))

    return picked_rarity

def pickup_premium():
    """UR の景品を等確率を仮定して排出する"""
    ur = ["景品1", "景品2", "景品3", "景品4", "景品5", "景品6", "景品7",
          "景品8", "景品9", "景品10", "景品11", "景品12"]
    return np.random.choice(ur)
```

#### 値段や回数を保持するバリューオブジェクト

バリューオブジェクトは[値を保持するオブジェクトのこと](http://d.hatena.ne.jp/asakichy/20091007/1254888424)です。

今回はガチャを回した回数や課金した金額を保持します。面倒なので一つのオブジェクトにまとめます。

```py3
class VO(object):
    def __init__(self):
        self._count = 0 # 回数
        self._price = 0 # 課金額

    def getcount(self):
        return self._count

    def setcount(self, count):
        self._count = count

    def getprice(self):
        return self._price

    def setprice(self, price):
        self._price = price

    count = property(getcount, setcount)
    price = property(getprice, setprice)
```

#### ガチャを回す処理のルーティング

あとは Flask のルーティングを実装します。

```py3
@app.route('/')
def index():
    title = "ようこそ"
    message = "ガチャを回すにはボタンをクリックしてください"
    return render_template('index.html',
                           message=message, title=title)

@app.route('/post', methods=['POST', 'GET'])
def post():
    time = datetime.datetime.today().strftime("%H:%M:%S")
    message = ""
    if request.method == 'POST':
        result = []
        if 'rare' in request.form:
            title = "ガチャを回しました！"
            vo.price = vo.price + 300
            vo.count = vo.count + 1
            result = turn_rare()
        if '10rare' in request.form:
            title = "ガチャを回しました！"
            vo.price = vo.price + 3000
            vo.count = vo.count + 1
            result = turn_10rare()
        if 'reset' in request.form:
            title = "リセットしました"
            vo.price = 0
            vo.count = 0
            result = ""
            message = "リセットしました"
        return render_template('index.html',
                               result=result, title=title,
                               time=time, vo=vo,
                               message=message)
    else:
        return redirect(url_for('index'))
```

#### ビュー

最後に画面を用意します。

```html+jinja
<div class="form">
  <div class="container">
    <div class="row">
      <div class="col-md-12">
        <p class="lead">
          {% if result %}
            {{ time }} ガチャを回しました！<br>
            {{ vo.count }} 回目 合計金額 {{ vo.price }} 円<br>
            結果は
            {% for v in result %}
              {{ v }}
            {% endfor %}
            でした！
          {% else %}
            {{ message }}
          {% endif %}
        </p>
        <form action="/post" method="post" name="rare" class="form-inline">
          <button type="submit" name="rare" class="btn btn-default">レアガチャを回す</button>
        </form>
        <form action="/post" method="post" name="10rare" class="form-inline">
          <button type="submit" name="10rare" class="btn btn-default">10 連レアガチャを回す</button>
        </form>
        <form action="/post" method="post" name="reset" class="form-inline">
          <button type="submit" name="reset" class="btn btn-default">リセットする</button>
        </form>
      </div>
    </div>
  </div>
</div>
```

これで完成です。

## ガチャをまわしてみる

[前回](http://qiita.com/ynakayama/items/2cc0b1d3cf1a2da612e4)と同じく python app.py としてアプリケーションを起動します。

ブラウザで localhost の 5000 番ポートにアクセスします。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/38501/33c7b4e7-1ce6-f849-a9dd-cbad4ac0efbe.png)

この状態でレアガチャを押すと 300 円、 10 連レアガチャを押すと 3,000 円払ってガチャを回すことができます。試しに 10 連を押すと以下のように SR が 2 枚獲得できました。

![2.png](https://qiita-image-store.s3.amazonaws.com/0/38501/2ab57df2-1c37-193d-eaf7-4b406dc53115.png)

はたして何回の試行で UR を入手できるでしょうか。みなさんもぜひ試してみてください。

ちなみに筆者はこのシミュレーションで 10 連レアガチャを 16 回、課金額合計 48,000 円で UR を入手できました。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/38501/39cfc3e1-db58-fbd2-a857-07488d548772.png)

果たしてこの課金額が本当に景品に見合う額なのか、あらためてよく考えてみるのも良いでしょう。

以前に[コンプガチャ](http://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%83%97%E3%83%AA%E3%83%BC%E3%83%88%E3%82%AC%E3%83%81%E3%83%A3)というものが問題になりましたが、たとえばこのシミュレーションを利用してガチャ産 UR 全 12 種類をそろえるために果たしていくらかかるのか試してみるのも面白いかもしれません。

なお今回掲載したガチャの確率は任意に設定した値であり実在のサービス等とは一切関係がございません。

## まとめ

今回はソーシャルゲームのガチャという遊びを題材にしましたが、このように手軽に **経済理論** に基づいた計算を検証できることは大変有意義です。

経済理論はたとえばミクロ経済理論では人間の行動について仮説的に導かれます。理論が実体経済をうまく説明しているかを客観的に検証するには数式を用いて **経済モデル** を構築します。

数量化されたモデルは **計量経済モデル (econometric model)** と呼ばれ、実証分析の対象となります。

実証分析を進める上で、シミュレーションシステムを手軽かつ高機能に実装できる Python + Flask の組み合わせは強力な手助けとなるでしょう。

今回の記事のソースコードは[こちら](https://github.com/ynakayama/flask-hello/tree/gacha-simulator)に公開してあります。