読者です 読者をやめる 読者になる 読者になる

おいしいとこはすこしだけ

文系出身SE見習いの備忘録。

MTGと機械学習で遊びたいので準備中

Python マジック・ザ・ギャザリング 機械学習 自然言語処理

文系出身SE見習いの北白川キリコです。


自然言語処理系のDeepLearningが相変わらず楽しいので、MTGでもできないかなーと思い最近ちまちま取り組んでいます。

言語はPythonで、とりあえずgensimでなんかできないかなーと思ってますが、他に面白そうなライブラリ見つけたらそっちに行くかも。


何がしたいの?

オリカのテキストを入力したらいい感じのマナコストをサジェストしてくれるプログラムを作りたい。

既存のデータベースを使って、カードテキストとマナコストを学習させたりできないかなー?

というのが、今回の目標です。


正直、どういうモデルを使えばそういうことができるのか、まだよく分かってない……色々試してみようかなという段階。

gensimはとりあえずword2vecで遊んでみたいので入れておきます。


前準備

  • WinPython(2.7.10)

WinPython - Browse /WinPython_2.7/2.7.10.3 at SourceForge.net

numpyとかscipyとかの主だったライブラリとか、あとIDEのSpyderとかもまるっと入っていて便利。


  • smart_open

smart_open 1.3.3 : Python Package Index

これがないとgensimの実行ができないようなので。

WinPythonにはインストール管理ができるコンパネが付いているので、手軽にパッケージからインストールしていきます。

(別にpipでもいいはずなんだけど、pipの導入自体が面倒くさくてやってないという)


↓のサイトに分かりやすく導入方法が書いてあります。

MinGW(gcc) の Windows へのインストールと使い方 | プログラマーズ雑記帳

ちょっと面倒くさいけど、MinGWみたいなLinuxライク環境入れとくと今後も便利そうですね。


  • gensim

gensim 0.13.3 : Python Package Index

ここまできてようやく満を持してgensim。

smart_openと同様にWinPythonのコンパネから。


Janome 0.2.8 : Python Package Index

あとは形態素解析が必要かなーと思うのでJanomeを導入。


データベース作成

データベースはWisdom Guildさんのカードデータベースより、出力形式を「テキスト」にして全データを検索することで取得。

WHISPER CARD DATABASE - Wisdom Guild


とはいえこのままだと、↓みたいな感じのベタ書きのテキストなので、

 英語名:Abandoned Outpost

日本語名:見捨てられた前哨地(みすてられたぜんしょうち)

 コスト:

 タイプ:土地

見捨てられた前哨地はタップ状態で戦場に出る。

(T):あなたのマナ・プールに(白)を加える。

(T),見捨てられた前哨地を生け贄に捧げる:あなたのマナ・プールに好きな色1色のマナ1点を加える。

イラスト:Edward P. Beard, Jr.

 セット:Odyssey

 稀少度:コモン

ここから必要な情報を切り出していく必要があります。


とりあえず欲しい情報としては、

  • カードの名前

  • マナコスト(色拘束とシンボル両方)

  • カードタイプ

  • テキスト

ぐらいかなー。


def gen_wg_card_list(path):
    
    import codecs
    import re
    
    white_str = u'白'
    blue_str = u'青'
    black_str = u'黒'
    red_str = u'赤'
    green_str = u'緑'
    none_str = u'◇'
    
    eng_header = u' 英語名:'
    jan_header = u'日本語名:'
    cost_header = u' コスト:'
    color_header = u' 色指標:'
    type_header = u' タイプ:'
    illust_header = u'イラスト:'
    set_header = u' セット:'
    rarity_header = u' 稀少度:'
    
    card_list = []
    
    with codecs.open(path, encoding = 'utf_8_sig') as f:        
        # カードの情報は、
        # 日本語名・マナシンボル・点数で見たマナコスト・カードタイプ・カードテキスト
        # の5要素の配列で管理
        card_info = [u'', [0]*6, 0, u'', u'']
        info_set = [False]*5
        for line in f.readlines():
            
            # 日本語名・コスト・色指標・タイプ・カードテキスト以外は飛ばす
            if line.count(eng_header) or \
            line.count(illust_header) or \
            line.count(set_header) or \
            line.count(rarity_header) or \
            line == u'\r\n':
                # この直前までに必要事項が全て埋まっていたらリストに加える
                if False not in info_set:
                    info_set = [False]*5
                    card_list.append(card_info)
                    card_info = [u'', [0]*6, 0, u'', u'']
                continue
            
            # 日本語名は()内の読みがなを削除して登録
            elif line.count(jan_header):
                info_set[0] = True
                card_info[0] = re.sub(u'\r\n', u'', re.sub(
                u'(.*)', u'', re.sub(jan_header, u'', line)))
            
            # マナコストはWUBRG+Nの6要素の配列で管理
            elif line.count(cost_header):
                info_set[1] = True
                card_info[1][0] = line.count(white_str)
                card_info[1][1] = line.count(blue_str)
                card_info[1][2] = line.count(black_str)
                card_info[1][3] = line.count(red_str)
                card_info[1][4] = line.count(green_str)
                card_info[1][5] = line.count(none_str) 
               
                # 点数で見たマナコストは()の数でカウント
                info_set[2] = True
                card_info[2] = line.count(u'(')
            
            # 色指標しかない場合はマナコスト-1とする
            elif line.count(color_header):
                info_set[1] = True
                card_info[1][0] = line.count(white_str)
                card_info[1][1] = line.count(blue_str)
                card_info[1][2] = line.count(black_str)
                card_info[1][3] = line.count(red_str)
                card_info[1][4] = line.count(green_str)
                card_info[1][5] = line.count(none_str)
    
                info_set[2] = True
                card_info[2] = -1
            
            # カード・タイプからサブタイプや部族を削除して登録
            elif line.count(type_header):
                info_set[3] = True
                card_info[3] = re.sub(u'\r\n', u'', re.sub(
                u' --- .*', u'', re.sub(type_header, u'', line)))
            
            # その他の部分は全てカードテキストとして登録
            else:
                info_set[4] = True
                card_info[4] += line
            
    return card_list

if __name__ == '__main__':
    card_list = gen_wg_card_list('C:\\Python\\Projects\\mtg\\resource\\mtgdata.txt')

ってこれ、今気づいたけどマナコストのカウントの仕方おかしいですね……

(の数を数える方法だと(5)でも1マナと判定されてしまうので、()内にある数字を正規表現かなんかで抽出してintにするとかそういうのになるかなあ。


ともあれこれで一旦データベースの形にはなった(あとでマナコスト周り修正しなくちゃだけど)。

あとはカードテキストを形態素解析してー……と思ったら、問題発生。

MTG用語は一般辞書に含まれていないものが多いので、変な分割をされてしまったりするんですね。


形態素解析用辞書作成

というわけで、Janome用のユーザ辞書を作らないといけない。


はじめはMTG Wikiから単語を拾ってこられないかなーと思ったけど、結構面倒くさそう。

というわけで、

専門用語(キーワード)自動抽出システム”のページ

こちらのTermExtractをお借りすることで強引に解決することにしました。


TermExtractは、コーパス中の単語の語順や出現頻度から、連語による重要単語を抽出するというスグレモノperlモジュールです。

python版もあったんですが3.x対応だったのでやめておきました。

こいつに、先ほど抽出したカードテキストを食わせてやると……

クリーチャー

ターン終了

カード

呪文

土地

1つ

ターン

呪文1つ

ダメージ

土地1つ

カウンター

対象

プレイヤー

対戦相手

ちょっと変なのもありますが、いい感じではないでしょうか。

「絆魂」とか「呪禁」とか、「ツリーフォーク」なんかもちゃんと取れています。


これをJanomeユーザ辞書形式にすれば、形態素解析はできそうかなー。

というところで、今日は終わり。まだ機械学習のきの字にもかかってない。先は長いですが、頑張ろう。


……とりあえずword2vecで適当に遊んでみるところからやってみても良いかもしれないですね。