MTGをハックする技術


みなさんこんにちは。LAPRASでデータベースエンジニアをしている[@denzowill](https://twitter.com/denzowill)です。
突然ですが、社会人のスケジュールで最も多くの時間を占める予定はなんでしょうか?

そうです。"MTG"です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44722/959beca1-2e50-4210-9376-bb13f3fcb71c.png)


社会人のスケージュルには多くのMTGが入っています。私も、LAPRASで勤務する中では業務時間中はもちろん、時にはランチMTG、業務時間終了後の残業MTGをすることもあります。

パフォーマンスチューニングでは、最も大きなボトルネックから着手するのが定石です。そのため、予定の多くを占めるMTGをハックすることは、労働効率を上げる上で効果的と言えます。

私はエンジニアなのでハックするなら精神論ではなくシステムを使うなり作るなりして対処したいです。そこで今回はMTGをハックするための技術を見ていきます。

### MTGの定義

まずはハック対象であるMTGの定義を確認します。今回対象としているMTGは

- 集会
- 会合
- 報告会

などではなく、[Magic: The Gathering](https://mtg-jp.com/)です。ということで、ミィーティングの効率化を知りたい方はこれ以降有用な情報は一切書かれていないことをお伝えしておきます。


### MTG関連のAPI等

MTGは歴史が長いだけあって、色々データが整っています。利用できるメジャー所のサービスも色々特徴がありますのでそれぞれ紹介していきます。

### Magic: The Gathering API(`api.magicthegathering.io`)

MTGのカードやフォーマット等の情報を取得することが出来るAPIが提供されています。Qiitaでもいくつか記事で紹介されています。

* [Magic: The Gathering API and Developer SDKs](https://docs.magicthegathering.io/)

こちらは `https://api.magicthegathering.io/<version>/<resource>` と言った形式のREST APIを提供している上、各種主要言語に対応したSDKを提供しています。

![範囲を選択_183.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44722/acc09f05-5997-400f-7d72-a31e1a70ed5f.png)

ちなみにドキュメント外ではありますが、直接[GitHub](https://github.com/MagicTheGathering)を見に行くと`Elixir`や`Rust`等のSDKも提供されています。

ちなみにこの手のSDKは単にAPIのラッパーなだけなことが多いのですが、こいつは`QueryBuilder` を持っていておしゃれに使えます。例えば、以下はカード名の`オーコ`を日本語表記内で含み、カードタイプが`Plainswalker`を含むカードを探し出します。


```python:sample
cards = Card.where(name='オーコ').where(language='japanese').where(types='Planeswalker').all()
for card in cards:
    original_name = card.name
    japanese_name = [f for f in card.foreign_names if f['language'] == 'Japanese'][0]['name']
    print(f'English: {original_name}, Japanese: {japanese_name}')
```

```:実行例
English: Oko, Thief of Crowns, Japanese: 王冠泥棒、オーコ
English: Oko, the Trickster, Japanese: トリックスター、オーコ
```

このAPIのRate limitは 5000 request/1時間 で特に引き上げる方法もなさそうなのでおとなしめに使いましょう。また、検索系のパフォーマンスはかなり悪いです。おそらく全文検索エンジン等を使用しない素直なRDBMSをバックエンドに持っているのでしょう。先のサンプルコードでも5秒くらいかかります。

なお、このAPIは決して公式(ウィザーズオブザコースト)が提供しているものではありません。有志の方が管理しているAPIのようです。

### MTGJSON

先のAPIのデータとして利用されているのが[mtgjson](https://mtgjson.com)です。


![範囲を選択_184.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44722/9ce8e152-3035-b830-930a-7f8d022c8a72.png)

mtgjsonは、有志によってMTGに存在するカード等の全てを取り回しの効くフォーマットとしてメンテナンス・提供をするプロジェクトです。これらのデータはJSONやSQLファイル、sqliteのデータベースファイルとして誰でもダウンロード可能になっています。

https://mtgjson.com/downloads/all-files/

これらを利用して、先のAPIのように独自のMTG関連のサービスを提供することが出来ます。逆に、カード情報に関するサービスを自作するのであれば先のAPIを使うのではなくこちらのデータベースを直接利用する方が楽です。

なお、mtgjsonはGitHubリポジトリもあります。

https://github.com/mtgjson/mtgjson

これは、mtgjsonが提供しているデータをビルドするためのコードが提供されています。そのため、このリポジトリをローカルで動かすことで同様のデータを手に入れることが出来ます。じゃあその元データは何処？と思って見てましたが https://scryfall.com/ から取得しているようです。

```python:mtgjson4/provider/scryfall.py
SCRYFALL_API_SETS: str = "https://api.scryfall.com/sets/"
SCRYFALL_API_CARD: str = "https://api.scryfall.com/cards/"
SCRYFALL_API_CATALOG: str = "https://api.scryfall.com/catalog/{0}-types"
SCRYFALL_VARIATIONS: str = "https://api.scryfall.com/cards/search?q=is%3Avariation%20set%3A{0}&unique=prints"
SCRYFALL_SET_SIZE: str = "https://api.scryfall.com/cards/search?order=set&q=set:{0}%20is:booster%20unique:prints"
SCRYFALL_API_SEARCH: str = "https://api.scryfall.com/cards/search?q=(o:deck%20o:any%20o:number%20o:cards%20o:named)"
```

### scryfall

MTGのカードの検索を提供している https://scryfall.com/ にもREST APIが提供されています。

https://scryfall.com/docs/api

こちらも`api.magicthegathering.io` 同様にカードの検索等が提供されており、更に`api.magicthegathering.io`よりも高速に動作します。残念ながら公式に提供されているSDKはありませんが、REST APIなのでラップしたSDKを作ることはそれほど難しくないでしょう。

気になるRateLimitは明確な値は書かれていませんが、10 request/秒程度とのことですので`api.magicthegathering.io` より少し厳し目ですが、APIとしてのスペック的にはこちらのほうが良さそうです。

### まとめ

結局何使えばいいのかという点でいうと以下の様な形になります。

* とにかくSDKがほしい
    * `api.magicthegathering.io`
* 高性能な検索APIがほしい
    * scryfall
* データベースごとほしい
    * mtgjson

自身でデータベースを持たない場合はscryfall, データベースを持った上で何か作るのであればmtgjsonを利用するのが良さそうです。MTGが好きなエンジニアさんが自分でなにか面白いことをする時のデータの取得先として参考になれば幸いです。