PythonでJCGのデッキ分布を自動集計する

# はじめに
**※この記事はPythonでのプログラミング経験がある方を対象としています。**

また、下記の知識を前提としています。
・ Pythonの基礎文法
・ matplotlibによるデータの可視化
# 目的
JCGサイトからカードリストを取得し、クラス分布、アーキタイプ分布の集計、およびグラフ化を自動的に行います。

# アプローチ

JCGサイトでは大会の当落情報、チェックイン状況、登録デッキなどの登録者情報がJSONという形式のテキストファイルでまとめられています。
このJSONファイルからカードリストを取得し、枚数構成からデッキタイプを振り分けます。

## JSONとは

JSON(JavaScript Object Notation)とは、構造化されたデータを表現するための記法です。
ディレクトリの階層構造のように、テキスト間の階層構造を表しているとイメージして下さい。
例えば、JCGのJSONファイルでは登録者のデータが下記のような形式で記述されています。

```json:example.json
{
	"result": "success",
	"participants": [
		{
			"id": "42155", 
			"chk": 1, #チェックイン状況
			"nm":  "somey", #登録名
			"dk": [ #デッキの情報
				{
					"cl": 3, #クラス1
					"hs": "3.3.6so2A.6so2A.5-gkQ.5-gkQ.5-gkQ.6_X7Q.6_X7Q.6_X7Q.6_djc.6_djc.6_djc.6turQ.6turQ.6turQ.6xnsw.6xnsw.6xnsw.6_ZZg.6_ZZg.6_ZZg.gHqNk.gHqNk.gHqNk.6ty_2.6ty_2.6ty_2.6q8sC.6q8sC.6q8sC.6twYy.6twYy.6twYy.6xpaI.6xpaI.5-glM.5-glM.5-glM.6t_RI.6t_RI.6t_RI"
				}, #デッキ1のカードリスト（正確にはポータルのURLの一部）
				{
					"cl": 8, #クラス2
					"hs": "3.8.71TeA.71TeA.71TeA.6zemS.6zemS.6zemS.71QTC.71QTC.71QTC.6zd2w.6zd2w.6zd2w.6s2wi.6zcK2.6zcK2.6zcK2.6s65g.6zcKC.6zcKC.6zcKC.6s5My.6s5My.6s5My.6zhCY.6zhxQ.6zhxQ.6zhxQ.6-UTo.6-UTo.71Xo6.71Xo6.71Xo6.6oEnY.6oEnY.6oEnY.6oHDo.6oHDo.6vvW6.6vvW6.6vvW6"
				} #デッキ2のカードリスト（正確にはポータルのURLの一部）
			],
			"en": 1527921725,
			"te": 1, #当落情報
			"pr": 1,
			"cu": 0
		},
```
## デッキ情報の取得

デッキに関する情報はJSONファイルの"hs"キーの部分に格納されています。

例えば、上記のJSONファイルでは
```6so2A.6so2A.5-gkQ.5-gkQ.5-gkQ.6_X7Q.6_X7Q.6_X7Q.6_djc.6_djc.6_djc.6turQ.6turQ.6turQ.6xnsw.6xnsw.6xnsw.6_ZZg.6_ZZg.6_ZZg.gHqNk.gHqNk.gHqNk.6ty_2.6ty_2.6ty_2.6q8sC.6q8sC.6q8sC.6twYy.6twYy.6twYy.6xpaI.6xpaI.5-glM.5-glM.5-glM.6t_RI.6t_RI.6t_RI```
がデッキ情報です。

ピリオドで区切られた5桁の英数字（```6so2A```など）が1枚のカードを表しており、これが40個集まって1つのデッキを表現しています。
この英数字とカード名の対応はShadowverse ポータルで確認します。
ポータルのURLを詳細に見てみると、JSONファイルと同様に40個の英数字から構成されていることがわかります。

（例：スペルウィッチ）
https://shadowverse-portal.com/deckbuilder/create/3?hash=3.3.6so2A.6so2A.5-gkQ.5-gkQ.5-gkQ.6_X7Q.6_X7Q.6_X7Q.6_djc.6_djc.6_djc.6turQ.6turQ.6turQ.6xnsw.6xnsw.6xnsw.6_ZZg.6_ZZg.6_ZZg.gHqNk.gHqNk.gHqNk.6ty_2.6ty_2.6ty_2.6q8sC.6q8sC.6q8sC.6twYy.6twYy.6twYy.6xpaI.6xpaI.5-glM.5-glM.5-glM.6t_RI.6t_RI.6t_RI&lang=ja

例えば末尾には```6t_RI```という英数字が3つ続けて出現していますが、これは「陰陽の開祖・クオン」が3枚含まれていることを表しています。

## アーキタイプの分類

デッキに含まれるカードの種類と枚数の情報を用い、アーキタイプを分類する判定式を作成します。
判定式には、なるべくアーキタイプを一意に特定できるカードを利用しましょう。

例：```6q95g```（魔道具専門店）が3回含まれる→専門店ウィッチ

## 解析手順
1. jsonファイルをリクエスト
（以下jsonファイルの分析）
2. 当落情報の確認（当選している参加者が見つかったら3へ）
3. カードリストの取得
4. アーキタイプ分類（✕✕が○枚以上なら□□エルフ、など）
5. 2~4を繰り返し
6. 全参加者のアーキタイプ分類が終了したら、集計しグラフ化

## 集計結果の一例

**JCG Shadowverse Open 14th Season Vol.29 8月22日 ローテーション大会 グループ予選**

**クラス分布**
<img width="480" alt="class_pie_2328.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693609/00e0c41e-4f70-ed10-627e-105cd08fa0f1.png">
**アーキタイプ分布**
<img width="480" alt="class_bar_2328.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693609/14f786cb-6953-989a-89cf-5bbaa30cc21d.png">

# コーディング
※プログラミングに関しては独学なため、冗長なコードや慣習に従ってない場合があります。
下記に紹介するコーディングの例は参考程度に留めておいて下さい。
## ライブラリのインポート
最初に必要なライブラリをインポートします。

```python:import.py
import requests
import json
```

## カウンタの定義
クラス数とアーキタイプ数の集計に用いる変数を定義します。
また、クラス数、アーキタイプ数、アーキタイプ名を関連付けるために二次元の辞書型変数```arche_dict```を定義します。

下記に示すコードのアーキタイプ変数やアーキタイプ名は記事執筆時点（2020年8月22日）での一例です。
新弾やアディショナルカード実装などのタイミングで環境が大きく変わる場合には、新しい環境に合わせ適宜書き換えを行って下さい。

```python:counter.py
#クラスカウンタの定義
E = R = W = D = Nc = V = B = Nm = 0
#アーキタイプカウンタの定義
E1 = E2 = R1 = R2 = W1 = W2 = W3 = D1 = D2 = Nc1 = Nc2 = V1 = V2 = B1 = B2 = Nm1 = 0
#その他のアーキタイプのカウンタ
OE = OR = OW = OD = ONc = OV = OB = ONm = 0

#クラスカウンタ、アーキタイプ名、アーキタイプカウンタの辞書
arche_dict = {"E":{"リノセウスE":E1, "アマツE":E2, "その他E":OE},"R": {"進化R":R1, "連携R":R2, "その他R":OR},"W": {"スペルW":W1, "専門店W":W2, "秘術W":W3, "その他W":OW},"D": {"ディスカードD":D1, "ホエールD":D2, "その他D":OD},"Nc": {"冥府Nc":Nc1, "葬送Nc":Nc2,  "その他Nc":ONc},"V": {"コントロールV":V1, "狂乱V":V2, "その他V":OV},"B": {"エイラB":B1, "コントロールB":B2, "その他B":OB},"Nm": {"AFNm":Nm1, "その他Nm":ONm}}

```

## アーキタイプの判定式

アーキタイプの判定を行う関数を定義します。
引数としてクラス情報と40枚のカードリストを受け取り、クラス分類→アーキタイプ分類の順に振り分けを行います。

下記に判定式の一例を示します。
分類の精度を高めるために、試行錯誤を重ねながら適切な判定式を設定しましょう。


```python:json_request.py
#クラス、アーキタイプ分析
def deck_arche_analysis(sv_deck, sv_class):
    global E,R,W,D,Nc,V,B,Nm
    global E1,E2,R1,R2,W1,W2,W3,D1,D2,Nc1,Nc2,V1,V2,B1,B2,Nm1
    global OE,OR,OW,OD,ONc,OV,OB,ONm
    if sv_class == 1: #エルフ
        E += 1
        if sv_deck.count("6lZu2") == 3:
            E1 += 1
        elif sv_deck.count("6pQTI") == 3:
            E2 += 1
        else:
            OE += 1
    elif sv_class == 2: #ロイヤル
        R += 1
        if sv_deck.count("6td16") > 1:
            R1 += 1
        elif sv_deck.count("6_B9A") == 3:
            R2 += 1
        else:
            OR += 1
    elif sv_class == 3: #ウィッチ
        W += 1
        if sv_deck.count("6_djc") == 3:
            W1 += 1
        elif sv_deck.count("6q95g") == 3:
            W2 += 1
        elif sv_deck.count("6t_Rc") == 3:
            W3 += 1
        else:
            OW += 1
    elif sv_class == 4: #ドラゴン
        D += 1
        if sv_deck.count("6yB-y") == 3:
            D1 += 1
        elif sv_deck.count("6_zhY") == 3:
            D2 += 1
        else:
            OD += 1
    elif sv_class == 5: #ネクロマンサー
        Nc += 1
        if sv_deck.count("6n7-I") > 1:
            Nc1 += 1
        elif sv_deck.count("70OYI") == 3:
            Nc2 += 1
        else:
            ONc += 1
    elif sv_class == 6: #ヴァンパイア
        V += 1
        if sv_deck.count("6rGOA") == 3:
            V1 += 1
        elif sv_deck.count("6v1MC") ==3:
            V2 += 1
        else:
            OV += 1
    elif sv_class == 7: #ビショップ
        B += 1
        if sv_deck.count("6nupS") == 3:
            B1 += 1
        elif sv_deck.count("6nsN2") == 3:
            B2 += 1
        else:
            OB += 1
    elif sv_class == 8: #ネメシス
        Nm += 1
        if sv_deck.count("6zcK2") == 3:
            Nm1 += 1
        else:
            ONm += 1
```

## JSONファイルの取得

```python:json_request.py
# JCGの大会番号を入力
compe_num = input("調べたいJCGの大会番号を入力して下さい")
#大会のjsonファイルのURL
jcg_url = "https://sv.j-cg.com/compe/view/entrylist/" +  str(compe_num) + "/json"
#jsonファイルをリクエスト
res_jcg = requests.get(jcg_url)
#json形式のデータとして保存
j_txt = json.loads(res_jcg.text)
```

## JSONファイルを解析し、クラス、アーキタイプを集計

```python:data-processing.py
for i in range(len(j_txt["participants"])):
    #当落情報の確認
    if j_txt["participants"][i]["te"] == 0: #落選
        continue
    elif j_txt["participants"][i]["te"] == 1: #当選
        for j in range(2):
            #クラス情報の取得
            class_ij = j_txt["participants"][i]["dk"][j]["cl"]
            #カード情報の取得
            deck_ij = j_txt["participants"][i]["dk"][j]["hs"]
            #クラス・カード情報からアーキタイプを判別
            deck_arche_analysis(deck_ij, class_ij)
    else:
        continue
```
## グラフ用にデータを整形

```python:data.py
#集計結果の反映
arche_dict = {"E":{"リノセウスE":E1, "アマツE":E2, "その他E":OE},"R": {"進化R":R1, "連携R":R2, "その他R":OR},"W": {"スペルW":W1, "専門店W":W2, "秘術W":W3, "その他W":OW},"D": {"ディスカードD":D1, "ホエールD":D2, "その他D":OD},"Nc": {"冥府Nc":Nc1, "葬送Nc":Nc2,  "その他Nc":ONc},"V": {"コントロールV":V1, "狂乱V":V2, "その他V":OV},"B": {"エイラB":B1, "コントロールB":B2, "その他B":OB},"Nm": {"AFNm":Nm1, "その他Nm":ONm}}

#クラスのカウント、ラベルの配列化
class_count = [E, R, W, D, Nc, V, B, Nm]
class_name = ["E", "R", "W", "D", "Nc", "V", "B", "Nm"]

#アーキタイプのカウント、ラベルの配列化
count = [list(arche_dict[key].values()) for key in arche_dict]
arche_count = sum(count,[])
label = [list(arche_dict[key].keys()) for key in arche_dict]
arche_name = sum(label,[])
```

## グラフ化
最後に解析結果をグラフ化します。以下にプロットの一例を示しますが、好みのレイアウトでグラフ化してみて下さい。

**※下記のコードをそのまま実行する場合はmatplotlibにて日本語フォントの設定が必要です。**
そのまま実行する場合は下記の記事を参考にフォントをダウンロードし、このpythonファイルと同じディレクトリに配置してください。


https://tech-k-labs.xyz/post/others/matplotlib_with_heroku/
（Heroku上でもmatplotlibのグラフ描写をする）

```python:graphs.py
#matplotlibのインポート
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
#下記の2行は日本語設定用
from matplotlib.font_manager import FontProperties
fontprop = FontProperties(fname="ipaexg.ttf")

#カラーの設定
class_colors = ["palegreen", "peachpuff", "mediumslateblue", "sienna","darkmagenta", "crimson", "wheat", "lightsteelblue"]
arche_colors = ["palegreen"]*len(arche_dict["E"]) +["peachpuff"]*len(arche_dict["R"]) +  ["mediumslateblue"] * len(arche_dict["W"]) + ["sienna"] * len(arche_dict["D"]) + ["darkmagenta"] * len(arche_dict["Nc"]) + ["crimson"] * len(arche_dict["V"]) + ["wheat"] * len(arche_dict["B"]) + ["lightsteelblue"] * len(arche_dict["Nm"])

#クラス分布の円グラフ
fig1 = plt.figure()
plt.pie(class_count, labels=class_name, colors=class_colors, autopct="%.1f%%",pctdistance=1.35,wedgeprops={'linewidth': 2, 'edgecolor':"white"})
fig1.savefig("class_pie_"+compe_num+".png")

#アーキタイプ分布の棒グラフ
fig2 = plt.figure()
#x = np.array(list(range(len(arche_name))))
x = list(range(len(arche_name)))
plt.bar(x, arche_count, color=arche_colors)
plt.ylabel("使用数",font_properties=fontprop)
plt.xticks(x,arche_name,rotation=90,font_properties=fontprop)
plt.subplots_adjust(left=0.1, right=0.95, bottom=0.25, top=0.95)
for x, y in zip(x, arche_count):
    plt.text(x, y, y, ha='center', va='bottom')

fig2.savefig("class_bar_"+compe_num+".png")
```

# 実行結果
JCGのURLの末尾にある4桁の大会番号を入力し、実行。
（今回調べた大会URL：https://sv.j-cg.com/compe/2328 ）

**コマンドライン**
![execute.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693609/08d01a14-3a9e-c575-4a7c-2164928fa575.png)
**クラス分布**
<img width="480" alt="class_pie_2328.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693609/00e0c41e-4f70-ed10-627e-105cd08fa0f1.png">
**アーキタイプ分布**
<img width="480" alt="class_bar_2328.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693609/14f786cb-6953-989a-89cf-5bbaa30cc21d.png">
# おわりに
今回はローテーション環境のアーキタイプ分析を例に説明しました。
判定ロジックを変更すればアンリミテッド環境の分析も可能なので、興味がある方は試してみて下さい。

# 参考1：（発展）カードの採用枚数の比較

Webスクレイピングでshadowverseポータルからカード名と枚数を取得することにより、採用枚数の比較表を自動作成することもできます。

**例：JCG Shadowverse Open 14th Season Vol.29 8月22日 ローテーション大会 決勝トーナメント**
**スペルウィッチ**
![list_W_2356.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693609/516315d3-86b7-5f81-f3c6-c33776231919.png)

この詳細に関しては機会があれば別の記事で。
（2020年8月29日 追記）記事を執筆しました。
[【Python】 JCG決勝のデッキリスト比較表を自動作成する](https://qiita.com/somey/items/6f96bbbffd49bf020ad8)

# 参考2：（発展）Discord上から実行

Discord botを用いることでDiscord上からプログラムを実行することも可能です。
<img width="480" alt="discord.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693609/4280553d-fa95-07d1-fa15-d7531332ef13.png">