クラッシュ・ロワイヤル API を使って Google スプレッドシートへクラン対戦の戦績を記録する

## はじめに
エンジニアの [@Yasuhisa](https://github.com/Yasuhisa) です。

私はスマホのゲームアプリでよく遊びます。

最近は主に Supercell 社の `CLASH ROYALE（クラロワ）` や `BRAWL STARS（ブロスタ）` で遊んでいます。どちらもスマホゲームの手軽さがあり、ルールのシンプルさと競技性の高さを兼ね備えているため、飽きずに続いています。

この記事では `CLASH ROYALE（クラロワ）` の公開 API [Clash Royale API](https://developer.clashroyale.com/#/) からクラン対戦結果を取得して、Google スプレッドシートに戦績を記録する方法を紹介します。

注）この記事の執筆時点（2019/01/05）に動作確認をしたため、API のアップデート次第では動作しなくなる可能性があります。詳細は文末の API リファレンスを参考にしてください。

## クラッシュ・ロワイヤルおよびクラン対戦について
クラッシュ・ロワイヤル（通称クラロワ）は 2016年 1月4日 に Supercell 社からリリースされ、2018年 4月25日 にクラン対戦が追加されました。2018年からはプロリーグも活発で、YouTuber による生放送なども頻繁に行われています。

## クラロワ API をどう使うのか
本題の [Clash Royale API](https://developer.clashroyale.com/#/) の使い方については、[クラッシュロワイアル(ClashRoyale)のAPIが解放されたらしいので試してみる(その①) - Qiita](https://qiita.com/takahirono7/items/4d76d727945b0edd0657) の記事でわかりやすく紹介されています。

今回のスクリプトを作ったきっかけは、クラン対戦の戦績の閲覧に不便さを感じたからです。クラン対戦の戦績はゲーム内の `ソーシャル` > `クラン名` > `過去の対戦` の順にタップして見ることができます。しかしながら、**直近の5戦しか記録が確認できない（クラロワ API で 5 以上の Limit を指定しても見れない）**という問題があるため、私とクランメンバーの友人でスプレッドシートに戦績を記録するようになりました。

![01.png](https://qiita-image-store.s3.amazonaws.com/0/16539/4965afdb-93ae-d1cf-af14-6fa7f5c8ec5c.png)

筆者は以前からクラロワ API が試してみたかったので、年末年始の暇な時間に自動化しようと考えていました。試してみた結果、**クラロワ API は実行元の IP アドレスをあらかじめ登録しておく必要があった**ので、Google Apps Script の [トリガー](https://developers.google.com/apps-script/reference/script/trigger) などで完結することができませんでした。そのため、Python スクリプトをローカル端末から実行する方法で現在は記録しています。

## やりたいこと

手入力している Google スプレッドシートのクラン対戦戦績を Python スクリプトで一括登録します。

![02.png](https://qiita-image-store.s3.amazonaws.com/0/16539/8c36734d-ebb5-9986-7398-1845367ae875.png)

スプレッドシートへの煩わしい手入力やスマートフォン・タブレットでのクラン対戦戦績の照会数・記録ミスが減り、しっかりと記録しつつも遊べる時間が増えせます。

![03.png](https://qiita-image-store.s3.amazonaws.com/0/16539/a320d695-ce20-98b9-a187-9f62fb1b3ce0.png)

## 事前準備
後述の Python コードを実行するには以下の準備が必要です。

- 集計対象スプレッドシートの作成
    - スプレッドシート ID
    - 集計・記録対象セルの作成
- Clash Royale API キーの取得、およびグローバル変数 TOKEN へセット
- Google Spreadsheet API の有効化
- Google Spreadsheet API 認証情報を実行環境のルートディレクトリに配置
    - token.json
    - credentials.json

## Python コード
[Gist](https://gist.github.com/Yasuhisa/c29fb536bda48bc4f7f94dbd2a8fcf44) でも公開しています。

```python
import requests
import json
from googleapiclient.discovery import build
from httplib2 import Http
from oauth2client import file, client, tools

TOKEN = '{Your Clash Royale API Key}' # クラロワ API キー
URL = 'https://api.clashroyale.com/v1'

SCOPES = 'https://www.googleapis.com/auth/spreadsheets' # read/write
SPREADSHEET_ID = '{Your Spreadsheet ID}' # スプレッドシートの ID
DEFAULT_RANGE = '{Your Spreadsheet target rect}' # セルの集計対象範囲（「:」前後の矩形選択）
LAST_LOGGED_CELL = '{Your Spreadsheet CreatedDate cell identity}' # 最終更新日時セル
VALUE_INPUT_OPTION = 'RAW' # RAW 固定

NAME_COL = 0 # スプレッドシート A 列
WIN_COL = 1 # スプレッドシート B 列
LOSE_COL = 2 # スプレッドシート C 列

sheet = None

def get_current_clan_battle_results():
    """クラロワ API クラン対戦データを取得

    実行するためには事前にクラロワ API の API キーを `TOKEN` にセットする必要があります。

    Returns:
        List -- クラン対戦結果

    See Also:
        My Keys -- https://developer.clashroyale.com/#/account
        GET /clans/{clanTag}/warlog -- https://developer.clashroyale.com/#/documentation
    """

    endpoint = URL + "/clans/{your_clan_tag(need '#' to encode URL string '%23'}/warlog"

    headers = {
        "content-type": "application/json; charset=utf-8",
        "cache-control": "max-age=60",
        "authorization": "Bearer %s" % TOKEN
    }

    response = requests.get(endpoint, headers=headers)
    results = response.json()

    return results

def get_spreadsheet():
    """Google スプレッドシートを取得

    実行するためには事前に実行環境のルートディレクトリに「token.json」、および「credentials.json」を配置する必要があります。

    Raises:
        Exception -- スプレッドシートが取得できない場合

    Returns:
        Spreadsheet -- https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets?hl=ja#Spreadsheet
    
    See Also:
        Python Quickstart -- https://developers.google.com/sheets/api/quickstart/python
    """

    store = file.Storage('token.json')
    creds = store.get()

    if not creds or creds.invalid:
        flow = client.flow_from_clientsecrets('credentials.json', SCOPES)
        creds = tools.run_flow(flow, store)
    
    service = build('sheets', 'v4', http=creds.authorize(Http()))
    sheet = service.spreadsheets()

    if not sheet:
        raise Exception('スプレッドシートが正常に取得できませんでした。')

    return sheet

def get_sheetvalues_by_range(range):
    """スプレッドシートの指定した範囲を取得

    Parameters:
        range {string} -- セルの取得範囲。指定のセル番号(ex, A1)もしくは、「:」区切りのセル範囲(ex, A1:B2、矩形選択)を指定する。

    Raises:
        ValueError -- range の指定がない場合

    Returns:
        spreadsheets.values -- https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values?hl=ja
    """

    if not range:
        raise ValueError('範囲を設定してください')
    
    return sheet.values().get(spreadsheetId=SPREADSHEET_ID, range=range).execute()

def update_area(batch_update_values_request):
    """スプレッドシートの範囲を更新する
    
    Arguments:
        batch_update_values_request {BatchUpdateValuesRequest} -- https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate?hl=ja#request-body
    
    Raises:
        ValueError -- batch_update_values_request の指定がない場合
    
    See Also:
        POST spreadsheets.values.batchUpdate -- https://developers.google.com/sheets/guides/values?hl=ja#_8
    """

    if not batch_update_values_request:
        raise ValueError('batch_update_values_request を指定してください')

    body = {
        'valueInputOption': VALUE_INPUT_OPTION,
        'data': batch_update_values_request
    }

    sheet.values().batchUpdate(spreadsheetId=SPREADSHEET_ID, body=body).execute()

def update_battle_resutls():
    """
    クラロワ API のクラン対戦取得結果でスプレッドシートの最終更新日時より新しい記録があれば、スプレッドシートの戦績を更新する。
    """

    global sheet
    sheet = get_spreadsheet()

    # スプレッドシート情報を取得
    last_logged_cell = get_sheetvalues_by_range(range=LAST_LOGGED_CELL)
    # 最終記録時間を取得
    last_logged_date = last_logged_cell['values'][0][0]

    # クラン対戦結果を取得
    battle_results = get_current_clan_battle_results()
    items = battle_results['items']

    # クラン対戦結果を createdDate（対戦履歴）順に昇順ソート
    sorted_items = sorted(items, key=lambda items: items['createdDate'])

    for item in sorted_items:
        created_date = item['createdDate']

        # クラロワ API で取得した最終更新日時（createdDate）がスプレッドシートの記録以前のものであったら処理しない
        if created_date <= last_logged_date:
            continue

        # クラロワ API クラン対戦参加者情報
        participants = item['participants']
        # スプレッドシートセル情報
        values = get_sheetvalues_by_range(range=DEFAULT_RANGE)['values']

        for participant in participants:
            participant_name = participant['name']

            for value in values:
                cell_name = value[NAME_COL]

                # クラン対戦参加者名とスプレッドシートの名前列が一致するかどうか
                if participant_name == cell_name:
                    win_count = participant['wins']
                    # 対戦結果勝ち数を追加
                    value[WIN_COL] = str(int(value[WIN_COL]) + win_count)
                    # 対戦結果負け数を追加 (試合数 - 勝数)
                    value[LOSE_COL] = str(int(value[LOSE_COL]) + participant['battlesPlayed'] - win_count)

        # スプレッドシート API の `BatchUpdateValuesRequest` オブジェクト
        batch_update_values_request = [
            {
                'range': LAST_LOGGED_CELL,
                'values': [[created_date]]
            },
            {
                'range': DEFAULT_RANGE,
                'values': values
            },
        ]

        # LAST_LOGGED_CELL の最終更新日時（createdDate）、クラン対戦結果を更新
        update_area(batch_update_values_request)

if __name__ == '__main__':
    update_battle_resutls()
```

## 所属クランの宣伝
私の所属しているクランは少人数で、コアメンバーの高い勝率が特徴です。

クランのルールでは `クラン対戦本戦でカードレベルが 11 以上（ウルトラレアは 10 以上）でなければ許可なく参加しない` となっているため、対戦の参加人数が 10 人に満たないことが多々あります。

平均のカードレベルが 11 以上（理想は 12 以上）で、少数精鋭のクランに入ってみたいという方がいましたら、是非 `ツワキノクラロワ部` へ加入申請をお願いします。

## おわりに
クラロワのフレンド登録なども歓迎しますので、何か用があれば筆者の [Twitter](https://twitter.com/pakorepqu) へ気軽に連絡をください。

またクラロワ API を使う機会があれば記事にしていきたいです。

## 参考にしたドキュメント・記事
- [Clash Royale API](https://developer.clashroyale.com/#/documentation)
- [クラッシュロワイアル(ClashRoyale)のAPIが解放されたらしいので試してみる(その①) - Qiita](https://qiita.com/takahirono7/items/4d76d727945b0edd0657)
- [Python Quickstart  |  Sheets API  |  Google Developers](https://developers.google.com/sheets/api/quickstart/python?hl=ja)
- [Authorize Requests  |  Sheets API  |  Google Developers](https://developers.google.com/sheets/api/guides/authorizing)
- [セル値の読み取りと書き込み  |  Sheets  |  Google Developers](https://developers.google.com/sheets/guides/values?hl=ja#_6)