pyYGOBot

とりあえず概形は作った。
nnt339es.hatenablog.com

導入方法

意外と面倒くさくて、実用的な目途が立ったら追記する。

起動方法

  1. パッケージルートにDecksディレクトリを作成する
  2. Decksにydkファイルを用意する
  3. 引数を渡して app.pyを起動する。

引数確認方法

python app.py -h

起動コマンド例

python app.py --deck ThunderD --name myAI --port 7911

起動に成功するとDecksディレクトリにそのデッキ用のディレクトリが作成される

#ToDo
ydkファイルを読み込むディレクトリを指定できるようにする。

※注意※
python3.8が必要。ライブラリはrequirements.pyを参照。

モジュールとして使う

# YGOEnvとDuelAgentをインポート
from pyYGOenv import YGOEnv
from pyYGOagent import DuelAgent

def main():
    deck: str = 'deck_name'
    host: str = '127.0.0.1' # ADSを起動してあるIPアドレス
    port: int = 7911        # ADSのポート
    version: int = 38 | 1<<8 | 8<<16 # ADSのバージョン(この例では38.1.8)
    name: str = 'AI'  # ADSで表示する名前
    # 環境を作成
    collect_env = YGOEnv(deck, host, port, version, name+'_collect')
    eval_env = YGOEnv(deck, host, port+1, version, name+'_eval')

    # エージェントを作成
    agent = DuelAgent(collect_env, eval_env)

    # 訓練
    agent.train(1000)

    collect_env.close()
    eval_env.close()


if __name__ == '__main__':
    main()

簡単な解説

pyYGO

遊戯王一般のオブジェクトを扱うパッケージ。

pyYGOenv

tf-agents.environments.py_environment.PyEnvironment仕様の環境

pyYGOnetwork

通信関連のパッケージ

pyYGOagent

エージェント関連のパッケージ

agent.py

エージェント本体

AIパラメータ

AI関連でいじるべきパラメータ

パラメータ 説明 場所
学習率
レイヤー構造
活性化関数
報酬

※注意※

未コーディング箇所

カウンター関連、シンクロ召喚、宣言系の処理は未コーディングなためゲーム中に発生するとエラーを投げてプログラムは終了する。

学習成果

コメントの#unknown

私もよくわからない部分なため質問されても困る箇所。

コーディングポリシー

  • クラス名はキャピタライズ。
  • 変数名は小文字、区切り文字はアンダーバー_
  • 処理で一時的に作成するtemporaryな変数以外はできるだけ詳しい変数名を付ける。英単語の省略もできるだけ避ける。
  • 定数は全て大文字、列挙型Enumもすべて大文字。
  • 関数名は小文字、区切り文字はアンダーバー_
  • 関数処理が大きくなる場合は、空行で処理をブロックに分ける。
  • elif も複雑になって読みにくいようであれば空行で区切る。
  • メソッド間は空行2つ分空ける。
  • 型ヒントが必須の場所:引数、戻り値、初期化時のプロパティ
  • 型ヒントが推奨される場所:関数内で初めて出てきた変数
  • 型ヒントが非推奨な場所:プロパティへの代入時(selfうんぬんでコードが長くなって読みづらくなるから)
  • 数字に意味があるなら定数に適切な名前を付けてラップする。(コメントを付けるぐらいならの意)

ADS用遊戯王AIプロジェクト

目的

本プロジェクトの目的は、従来より強力な判断機構をもった遊戯王オフィシャルカードゲーム(以降、単にOCG)をシミュレートするエージェント(または遊戯王AI)を開発することである。

また、そのエージェントを用いた遊戯王AI提供WEBサービス、環境シミュレーションWEBサービス、およびエージェントのAPIを提供することである。

これらのサービスにより、マッチング待機時間、複数のデッキを用いた一人回しといったタスクの時間コスト削減を目指す。

コミュニティ

ソースリポジトリ

準備中

Discord

準備中

プロジェクト

全般

ユーザーインターフェース

ユーザーインターフェースには現在も開発が続けられているProjectIgnis(ADS)を使用する。

ユーザーはADS介して遊戯王AIと対戦することができる。

開発言語

開発言語には主にMLライブラリが充実しているPythonを使用する。
パフォーマンスが求められる処理には適宜、C/C++、Cythonによる拡張を行う。

プログラム概要

メインプロセスの中で、仮想デュエルとエージェントを立ち上げ、エージェントが仮想デュエルの状況に対してアクションを実行する。

ユーザーと対戦する際は、ADSが提供するTCPプロトコルを利用して通信を行い、仮想デュエルとユーザー画面を同期させる。

エージェント

概要

エージェントは深層学習を行い、アクションを要求される場面でアクションの良し悪しを評価し実行する。

ニューラルネットワーク

エージェントが評価値を算出するのにニューラルネットワーク(NN)を使用する。


出力層
アクションの評価値を出力するパーセプトロンである。

評価値

エージェントから見て相手ターン終了時にお互いのプレーヤーの価値を計算する。
 (アクションの評価値) = (自分の価値) - (相手の価値)とし、この値に近づくようにNNを学習させる。

したがって、1ターン中の全てのアクションは同じ評価値となる。
デュエルは1アクションの報酬を最大化しても勝てるわけではないので、長期的なアクションの評価算出タイミングとして相手ターン終了時という指標を設けた。
また、「悪いアクションは必ず悪い結果を招く」を仮定しているので、1ターンに悪いアクションと良いアクションが混じっていても確率的に悪いアクションの方が評価が低くなることを期待している。

お互いのプレイヤーの価値は、フィールド上のカード枚数、手札枚数、デッキ枚数(デッキを多く掘っている方が基本的に有利)、ライフポイント等から定量的に算出する。
計算式は設計中である。

カードの埋め込みベクトル

一部が欠けたテキストからカード名を予測する3層構造のNNを学習させることによって、各カードを低次元のベクトル表現を抽出できる。

自然言語処理の分野で言う、単語の分散表現であり、そのアルゴリズムはCBoWを模している。
つまり、中心語がカード名であり、周辺語がそのテキストである。

実際、この方法で作成したベクトルでコサイン類似度を計算すると、《ブラックホール》と《激流葬》、《サンダーボルト》と《ハーピィの羽根帚》、《サイクロン》と《コズミック・サイクロン》などは大きい値を取る。
これは似たテキストを持つカードが空間上に上手く配置されていることを示唆している。

テキストの分散表現とカードステータス(種類、属性、ATK/DEF、etc)をどのように同時に扱うかは検討中である。

学習の工夫

環境で想定されるデッキに対していくつかエージェントを作成し、エージェント同士で対戦させる。

対戦の全てのアクションは棋譜のようにすべて記録しておく。

対戦終了後、エージェントは "棋譜" を参照して学習を行う。

また、学習済みとして有限個の "棋譜"を適当なディレクトリに保存しておき、定期的(数対戦後)にその中からランダムに選択した "棋譜" を参照して再び学習させる。

"棋譜" 保存数が上限を超えた場合、最も古いものから削除する。
削除されるタイミングは環境の変わり目であることが望ましい。
また、どの環境でも通用する基本的な戦術はどの環境の "棋譜" にも含まれると予想されるので、貴重なデータが失われることに過度に反応する必要はないと思われる。

ある程度学習したエージェントはさらにユーザーと対戦させることでNNを強化する。

仮想デュエル

既存のBotなどを参考にしながらPythonに焼き直す。
既存のソースコードを使用しない理由は、NNをPythonで実装するからである。
エージェントにクエリを投げる際に、Pythonオブジェクトへ変換するオーバーヘッドを削減するのが目的である。(1デュエル中にクエリは相当数投げられるはずである。)

各種WEBサービスについて

WEBを通して学習済みエージェントを提供し、利用してもらう。

また、ADSサーバーを介して対戦する方法も提供することでより手軽に利用してもらう。
この方法ならば我々も対戦データを取得できるため、より良いエージェントに繋げられる。

さらにエージェント開発者向けとして、ソースコードを公開、あるいはライブラリとして提供することでプロジェクトの発展、コミュニティの拡大を目指す。

弱AIから脱却したいという思い付きで練ったプロジェクトでありますので、もし実生活に影響が出るようでしたら突然投げ出す可能性がございます。

ADSというグレーなツールを利用する関係上見返りはございません。

WindBotをコマンドラインから起動

今日は半日 「WindBot」のソースを読んで、半日「WindBot」の起動に苦しんだ。

WindBotの概要

C#

開発言語はC#

どこからインクルードしてきた関数かわかりにくい点(これはもうC共通だが。。。)を除けば、非常に読みやすい言語。

ただちょっと過剰に継承・委譲し過ぎな感じはする

ADSとの関係性

UDPで双方向通信をしていて(よく読んだらTCPだったけど、本体はUDPだった気がするしもうわからん)、パケット情報をもとにお互い(ADSとWindBot)のプロセスが内部でデュエルの処理(カードの移動、LPの増減etc...)を行っている。

エントリポイント

Program.cs
そんなに大きなプロジェクトでもないので読みたいところから読んでも追えると思う。

大まかな仕組み

予めExecutorにアクション(summon, activate, etc...)とその発動条件(bool関数)を登録してコンパイルしておく。

受け取ったパケット情報をもとにPhaseやChainを進め、そのたびにExecutors(Listオブジェクト)をiterateして、発動条件が真を返したらその処理が行い、パケットを送信する。

WindBotの起動

起動に必要な引数たち

ADS/gframe/windbot.cppより(リポジトリ

namespace ygo {

#if defined(_WIN32) || defined(__ANDROID__)
bool WindBot::Launch(int port, const std::wstring& pass, bool chat, int hand) const {
#else
pid_t WindBot::Launch(int port, const std::wstring& pass, bool chat, int hand) const {
#endif
#ifdef _WIN32
	auto args = fmt::format(
		L"./WindBot/WindBot.exe HostInfo=\"{}\" Deck=\"{}\" Port={} Version={} name=\"[AI] {}\" Chat={} {}",
		pass,
		deck,
		port,
		version,
		name,
		chat,
		hand ? fmt::format(L"Hand={}", hand) : L"");

	STARTUPINFO si = {};
	PROCESS_INFORMATION pi = {};
	si.cb = sizeof(si);
	si.dwFlags = STARTF_USESHOWWINDOW;
	si.wShowWindow = SW_HIDE;
	if (CreateProcess(NULL, (TCHAR*)Utils::ToPathString(args).data(), NULL, NULL, FALSE, 0, NULL, executablePath.data(), &si, &pi)) {
		CloseHandle(pi.hProcess);
		CloseHandle(pi.hThread);
		return true;
	}
	return false;
...

ADS自身がコマンドでコールしているので使えそう。

引数がわからない

HostInfoVersionがわからな~い。

からADSをデバッグして引数を抜き取る。

なぜか上手くいかないビルド

過去の自分にすがってもなかなか上手くいかない。

最終的にリポジトリからクローンするところからやり直した。
ついでに記事も編集した。

無駄な数時間

VS2019でデバッグすればいいのにprintf()で出力しようとして、しかも文字化けして上手くいかない落とし穴。

デバッグ実行

例として、オルターガイストを起動するには

  • HostInfo="" (まさかの空文字列)
  • Deck="Altergeist"
  • Port=7911
  • Version=590188
  • Name="AI"
  • Chat=False
  • Hand=0

の引数でWindBotを起動すればよい。

無事ホスト側のADSに「ピンポーン」とAIが入ってきて、めでたしめでたし。

Versionの謎

windbot.cppのどこを探してもversionが見当たらない。
ヘッダを見るとどうやらWindBotクラスのインスタンス変数だったので、VSCodebot.versionを検索したら見事ヒット。

// gframe/game.cpp line 2017
bot.version = CLIENT_VERSION;

マクロの展開先を調べることで無事Versionを得られたとさ。めでたしめでたし。

WindBotの活用法

遊戯王システムのプログラミングをしなければならないと思い、個人の限界を感じてプロジェクトをなかったことにしようと思った所、以前途中でやめたWindBotのソース解析を思い出した。

要は、各カードに対するbool判定を学習可能な関数で置き換えれば良いので、私の仕事はAIの脳みそを作るだけになった。やったね。

既存Botの問題点

変化しない条件分岐

状況が変わろうとも、何回対戦しようとも分岐条件が変わらない。

そりゃ人間が勝つに決まってる。

決まったカードしか扱えない

何度も対戦したらデッキ構成がバレて、駆け引きが減少する。

デッキの動的な変化にも対応できる汎用AIを作る予定。

おわりに

強化学習の基本は、

  • エージェント
  • 環境
  • アクション
  • 報酬

の4つ。

報酬を上手く設計すれば、学習するはず!

という感じで頑張って組み立てていきますよ。

過去の遺物(リプレイ集)

過去の遺物(リプレイ)をここで閲覧できます。

クラウド上に保存するという発想が無くて、容量確保のために多くのリプレイ動画を削除してしまったことが悔やまれます。
(ちなみに過去にブログやtwitterに挙げたものは全て削除されています)

【サンダードラゴン】

vs【転生炎獣】(2019年4月環境終盤)

止まったら負けなのでキルしにいくパターンです。
罠型が流行りだったので手札誘発は無視できます。

vs【転生炎獣】(2019年4月環境終盤)

罠型をメタった構築がはまる瞬間です。気持ち良いですね。

vs【オルフェゴール】(2019年4月環境中盤)

止まったら負けるのでワンキルしにいくパターンです。
当時の【オルフェゴール】は初動貫通重視の構築が主流で《増殖するG》で引いて《超弩級砲塔列車ジャガーノート・リーベ》を止められるカードは無視できる程度でした。

vs【サブテラー】(2019年1月環境終盤)

永続いっぱい開かれたけど捲れました。

vs【オルターガイスト】(2019年7月環境序盤)

《神の宣告》3枚開かれましたが、最後妨害しても打点が減らないことを相手が悟り、負けを認めました。
このように妨害されてもリソースが続くようにプレイするのがコツです。

vs【オルターガイスト】(2019年7月環境序盤)

《ハーピィの羽根箒》を引いたターンに使っていませんか?
《幽鬼うさぎ》を相手ターンに使っていませんか?(自分のターンに表の《オルターガイスト・プロトコル》に使うつもりでした。)
最後は《闇の誘惑》で突き放してから《混沌の戦士カオス・ソルジャー》で《オルターガイスト・シルキタス》を処理して勝ちです。相手の手札に《オルターガイスト・クンティエリ》がないことは確定しています。

vs【プランキッズ】(2019年9月環境終盤)

手札3枚の破壊力に見えますか?
《闇の誘惑》は神カードです。

vs【エンディミオン】(2019年9月環境終盤)

プラン勝ちです。

vs【SPYRAL】(2020年1月環境序盤)

対【SPYRAL】は研究していたので何とか《増殖するG》を通すことに成功しました。詰めの一手のはずの相手の《王宮の勅命》のおかげで奇跡的に捲ることが出来ました。

トリックスター

後日(と言いつつ、既に使用できないテクニックが多数あるのでやらないかも)

【転生炎獣】(ストラク情報公開後から発売前まで)

後日(と言いつつ、ry)

【魔術師】

後日


カテゴリー

全部見るのは大変でしょうし、関係ないデッキのは見たくもないでしょう。

大まかに分類すると次の通りです。

  • 2018.5~2018.6 【忍者(という名のFWDリンク)】
  • 2018.6~2018.9【トリックスター(ときどきFWDリンク)】
  • 2018.8~2019.4【魔術師】
  • 2018.10~2018.12 【転生炎獣(発売前)】
  • 2018.11~2020 【サンダードラゴン】

削除したということは、ある意味では選別したということなので結構楽しめるんじゃないかと思います。

研究メモとしての一人回し(一人対戦)も含まれるため、神視点の判断があったらそう思ってください。




遊戯王カードの分散表現その1

遊戯王AIプロジェクトの進捗状況です。

以前、カードテキスト分散表現すればより汎用的な状況を理解できるはずだという仮定を述べました。
計算機に遊戯王を理解させる - Just a Note for Hobby


現在はその分散表現を得る機構を、ニューラルネットワーク(NN)で実現させようと四苦八苦しております。

追記(2020/10/04)

もしかして重みの修正法が間違っているのか?、と損失関数から勾配を計算したら実装しているNNのそれとは違う結果になったため、計算結果の式で訓練したら原因がそれだとはっきりしました。。。

その結果が次のグラフです。

1日で解決できたから良かったけど、なんてアホなミスをしてたんだ。。。

実装にあたり

Bag of Words

文書分類NNでは、NNに入力する情報形式も様々な工夫がなされていますが、今回私は最も単純な "Bag of Words"という手法を取りました。
理由は単純、実装が簡単だからです。

"Bag of Words" とは文書内の語を分解して、その全ての語を一つのベクトルに詰め込む手法です。
語を1次元のベクトルに圧縮するため、語の順序情報が失われますが文書分類においてはそのような単純な方法でも上手くいくことがあるそうです。

判別するカード

いきなり遊戯王カード10000種を全て判別するのは難しいと考えたため、以下の採用基準を定めました。

  1. 魔法・トラップ
  2. カテゴリーに属さない
  3. 以下20種の効果のいずれかを有する
    1. ドロー
    2. ライフダメージ
    3. ライフ回復
    4. モンスター破壊
    5. 魔法罠破壊
    6. 手札に戻す
    7. 除外
    8. 攻撃反応
    9. 召喚反応
    10. 召喚無効
    11. 効果無効
    12. 発動無効
    13. コントロール奪取
    14. デッキから手札に加える
    15. 墓地から手札に加える
    16. デッキから特殊召喚
    17. 墓地・除外から特殊召喚
    18. 攻撃力アップ
    19. 表示形式変更

そして各効果から10枚ずつ、計200枚を訓練データとして選びました。
詳細はこちら

stopwordの除去

ここからデータの前処理の話になります。

テキストにはそれ単体では意味が広すぎて、カード特定に役立たない単語が含まれます。
本来の意味とは異なりますが、それらを "stopword"と呼ぶことにします。

今回わたしはこれらの語を "stopword" としてテキストから除去しました。

同義語の処理

遊戯王は20年の歴史をもつカードゲームですから、ルール上同じでも表現が異なる場合があります。
"banish" は嘗て "remove from play" でした。
”Warrior-Type monster""Warrior monster" は同じ意味ですからそれらも統一します。
割と最近変わった表現としては、”GY"などの省略形が挙げられます。

実装

まだ開発途中で、まとまっていませんがソースをここに置いておきます。
言語はPython3で、必要な外部ライブラリはnumpyだけです。

上手く完成したらGitHubに挙げるなり、PyPIにあげるなりしてライブラリとして提供したいです。

  • 隠れ層の活性化関数 - tanh
  • 出力層の活性化関数 - softmax

訓練結果

初めは良くて正答率10%とかで、当てずっぽうも良いところだったんですけど、活性化関数をsigmoid関数からtanh関数に変えたら学習し始めました!

しかし、画像をよく見ると気が付くのですが、正答率が40~50%で頭打ちになっています。
当てずっぽうでもないが、識別しているとは言い難い状況です。

今後の課題

当分の目標は50%の壁を破ることです(過去最高は47%)。
パラメータをいじって調整するのも一つの手ですが、まだ奥の手としてこの単純なNNをRNNに進化させるというのがあるので、じっくり仕上げていきたいです。

※冒頭の追記にも書きましたが、重みの補正式が間違ってました。
自力で計算できるくらいには理解が進んだため、その内記事にしてまとめたいと思います。

OCGの話

そう言えば、久しぶりにOCG(の話題に)触れますと、《ドラグーン》が禁止になったり、《指名者》が一枚減っていたり、《混沌領域》というとんでもない【サンダードラゴン】の初動を貰えたりで呼ばれている気がします。

気はするんですけど、復帰は難しいでしょう。

都心へ向かうための往復3時間が毎日浮いたにも関わらず、やりたい事とやらねばならぬ事を成すのに時間が足らず、時々睡眠時間を削っては睡眠不足でストレスを抱えながら日々を送っている状況です。

私は知っています、メタゲーム調整に莫大な時間が必要なことを。そして、その犠牲となるのは「やらねばならぬ事」である事を(私の性格による)。

何で1日は24時間で、人生は高々100年なんでしょうね。
どっかの株主さんみたいに、1日1時間の睡眠でパフォーマンスが出せるような体であってほしかったです。

そしてカードがありません。
去年の10月以降のカードは皆無です。《原始生命態ニビル》も《冥王結界波》もありません。
むしろ《強欲で金満な壺》は売りたいです。

おわりに

まだまだ正答率50%未満の未学習NNですが、ここまでするのにさえ多くの苦労がありました。

重みが発散したり、勾配が消えて学習しなくなったり、オーバーフローしてNaNで埋め尽くされたりetc...

アルゴリズムが勝手に最適化してくれると思っていたら大間違いで、とても奥が深い機構だなと感じました。

NN自体が世に活用されて出てきたのが5年ほど前くらいで、その時に食らいつけなかったのが少し悔しく思います。(当時は微分積分・古文...(と遊戯王)でいっぱいいっぱいだった...)

計算機に遊戯王を理解させる

さて、コンピュータと遊戯王その2です。

ADSのコードは順調に解読できていて、メインフレームやコア、イベント管理など把握はできました。

どうやらADSはソケット通信でサーバーとクライアント間のイベントをやり取りして、それをサーバー側のコアが処理しているようなのですが、クライアントからサーバー側にどんな情報を送れば良いのかで詰まってしまい解析は中断中です。
(誰か知ってたら教えてください...)※追記:解決済み※

ADSと繋げなくてもAI開発はできるんですけど、やはり人間をぶっ潰してこそAIの価値があると思うので何とかホスト・クライアント通信を実現させたいです。

今日の話題は「計算機に遊戯王を理解させる」ということで、頭に浮かびつつあるアルゴリズムを書き起こして問題点を洗い出してみようと思います。

前回の案

https://nnt339es.hatenablog.com/entry/2020/09/27/020625nnt339es.hatenablog.com
前回の案は環境が変わろうとも永久に学習を続けていくため手堅いアルゴリズムだと考えていたのですが、やはり探索空間の広さがネックです。

グラフ理論を使って、デュエル状況をノード、タグをエッジとすればエッジによって連結空間が作成されるため、UnionFindアルゴリズムを使えばある程度高速化できるかもしれませんね。
ただ、その場合プログラム起動時にデータベースをすべてメモリに読み込むことになるので莫大なメモリ空間が必要になるでしょう。

私の環境では12GB積んでるんで不可能でもないかもしれませんが、複数のプログラムを立ち上げるのは無理そうです。
データベースにクエリ投げるだけですね。レスポンスは結構速く感じますが、100万データとかだとどれくらいなんでしょう?

そして、このアルゴリズムのもう一つの弱点は同じ名前のカードしか検索できないということです!

今回の案

単語の分散表現

先日「PythonB’zの歌詞を機械学習させる」(うろ覚え)のようなブログを見つけて、単語の分散表現というのを知りました。

単語の分散表現(distributed representation)とは簡単に説明すると、単語を意味空間の成分にベクトル分解します。

単語を意味空間ベクトルで表現することで、"king" - "man" + "woman" = "queen"のように単語の意味演算が可能となります。

遊戯王への応用

実装アルゴリズムは置いておいて、私のやりたいこととしては
"エアーマン" - "HERO" + "光天使" = "セプター" です。

つまり、遊戯王の効果を分散表現してコンピュータに理解させたいわけです。

そこで先ほどの単語分散表現のアルゴリズムを参考にしますと、単語の周りの単語を入力として元の単語の予測させるという方法で学習させるそうです。
データセットの文書それ自体が教師となるわけです。

しかし、遊戯王カード名を使った文書なんて数が知れてますし、そもそもそのアルゴリズムでは"エアーマン" - "HERO" + "光天使" = "セプター"は実現できないと思うのですよ。
効果テキストを教師として学習させても効果の意味までは理解できないのがまた。

と、そのままでは使い物にはならないのです。

発想の整理

改めて私の目標を書きますと、"エアーマン" - "HERO" + "光天使" = "セプター"、です。

つまり、効果テキストを意味空間にベクトル分解してその演算を行いたいのです。
いや、もはや "エアーマン"="セプター" ができればもう良いです。

単なる効果分けではダメなのか?

私はこれはダメだと考えています。
理由を以下に述べます。

次元数が膨大

  1. デッキから手札を加える
  2. フィールドのモンスターを1体破壊する
  3. ...

と効果を列挙した場合、細かな違いも1次元とカウントされるため次元数が膨れ上がります。

また、この列挙は人為的なものなので漏れがないことを証明するのにコストがかかります。

抽象的な概念を抽出できない
例えば、「ドロー」と「サーチ」は同じ「デッキから手札に加える」操作ですが、単純分けした場合この二つは全く異なるものとして扱うため関連性は失われます。

「ドロー」と「サーチ」、「ドロー」と「破壊」の違いを等価に扱うのは人間の思考とはかけ離れていると思いませんか?

新効果に対応できない
かつて「裏側除外」という処理を行うカードは存在しませんでした。

しかし、「裏側表示」と「除外する」というワードはプレイヤーの中にあったため、我々人間は公式に問い合わせることなくカードを使用することができました。

ところが効果分けの場合、新たに「裏側で除外する」の次元を設けなければなりません。

デュエルの状況

どうやって効果のベクトル表示を得るかは置いておいて、ベクトル表示によって得られる展望を述べたいと思います

効果のベクトル表示が強力なのは盤面の全てのカードについてベクトルの和を取ることで、デュエルの状況を効果空間(結果空間?)上に配置できるということです!

例えば、相手フィールド上に効果未使用の《サイバードラゴン・インフィニティ》または《召喚獣メルカバー》がいる状況は共通点があります。
これらの状況は効果空間上の同じ方向に配置されるはずなので、類似状況として検出可能です。

また、デュエル状況をベクトル空間に配置するとK平均クラスタリングによってカテゴリ分けができるため、探索空間を大幅に削ることができます。


効果空間の問題点

ステータスの扱い

《神の宣告》と《神の警告》が似ていると感じるのは、効果ベクトルの類似性はその一因でしょうが、それ以前にどちらもカウンター罠だからというのがあると思います。
《神の警告》と《エヴォルカイザー・ラギア》のどちらがより《神の宣告》に似ていると感じるでしょうか。

問題点はステータスを効果ベクトルの成分と同等に扱ってよいのかどうかです。

同等に扱うというのは同じだけ類似度に寄与するということです。
エアーマン》と《セプター》が似ていると感じるのは、そのステータスもよく似ているからだと思います。
しかし、同じ光属性・レベル4・天使族の《オネスト》は《セプター》に似ているでしょうか?
光属性・レベル4・戦士族・サーチ効果持ちの《星輝士 デネブ》は《エアーマン》に似ているでしょうか?

ステータスの一致不一致は0か1で白黒はっきり出ます。
ところが《エアーマン》と《セプター》の守備力100の違いは0と1の間にある気がします。

このように重みが異なるようなステータスのパラメータを効果ベクトルと同等に扱って良いのでしょうか?
もし分別するのならばその重みは何によって決定すれば良いでしょうか?

終わりに

とりあえずデュエル状況の分類の案を書き起こしてみましたが、実現はまだまだ遠いです。

ADSのビルドに21時間かけた話

AI作るにあたりADSと連携できないかとファイル漁ってたら、どうやらGithubに公開されているらしいので見に行くと、C++Luaで書かれていたのでとりあえずソースからコンパイルして実行できないか試してみた。
github.com

プロジェクトになれていない私には睡眠3時間のハードタスクだった。

OSはwindows10

手順概要

ソースコードに書いてある。

という不親切なことはやめておいて、未来の自分のためにメモしておこう。
ざっくりとした手順は次の通り。

  1. 必要なツール、ライブラリをダウンロードして環境を整える。
  1. プロジェクトチームが作成したビルド用のシェルスクリプトを読み解きながら、コマンド入力を行う。
  1. エラーを処理しながらビルドする。

シェルスクリプト解読に気付くまでに17時間くらい使い、そこから4時間程度で完成。
Githubwiki欄を参照すると古い情報ゆえに私のように無駄な時間を過ごすので、スクリプトの意味がわからなかった時にだけ参考にするように。

参照コード

.travis.ymltravisフォルダ
travisCIというサービスが自動でビルドしてくれるらしく、その命令用のソースがこれら。
即ちビルド手順が全てここに詰め込まれているのである。

次の三行を実行すればよい。
f:id:nnt339:20200928011750p:plain
(そのままコマンドラインに入力しても無意味なことに注意!)

インストールしたツールたち

それぞれビルドとダウンロードに使う。

VisualStudioには「C++によるデスクトップ開発」と「個別コンポーネント/VS 2017 (v141) ツールの C++ Windows XP サポート [非推奨]」をインストールしておく必要がある。

ソースのダウンロード

Unixコマンドを使用するためターミナルにはGit Bashを使用する。
(Cの講義取ってなかったらUnixコマンド一生知らなかっただろうな...)
ダウンロードしたいディレクトリに移動して

#ソースのダウンロード
$ git clone https://github.com/edo9300/ygopro.git ads
$ cd ads
#ocgcoreのダウンロード
$ git clone https://github.com/edo9300/ygopro-core ocgcore

例の3行を実行

1行目
$ ./travis/install-premake5.sh windows

/ads/直下にpremake5.exeが追加される。
MacLinuxユーザーはソース見ながら適宜変更して。

2行目

ソースの中身を見ると次の三つを実行すべきだとわかる

#!/usr/bin/env bash

set -euxo pipefail

./travis/install-local-dependencies.sh
if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then
    ./travis/get-windows-vcpkg-cache.sh
    ./travis/get-windows-d3d9sdk.sh
fi

ちなみに$TRAVIS_OS_NAME.travis.ymlで定義されている変数なのでファイルをそのまま実行しても機能しない。

./travis/install-local-dependencies.sh

上から一行一行処理していけばよい。
やっていることは3dゲームエンジンサウンドエンジンなどのダウンロード、解凍、しかるべき場所への配置である。
Unixコマンドなので調べなくても感覚で読めるかもしれない。

./travis/get-windows-vcpkg-cache.sh(vcpkg)
#!/usr/bin/env bash

set -euxo pipefail

mkdir -p "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
curl --retry 5 --connect-timeout 30 --location --remote-header-name --output installed.7z $VCPKG_CACHE_7Z_URL
7z x installed.7z
mv irrlicht/include /usr/local/include/irrlicht
mv irrlicht/lib/* /usr/local/lib

モジュールのダウンロード。
数行の処理だが変数が噛んでいるため、そのままは実行できない。
適当な場所(Cドライブ直下とか)にvcpkgディレクトリを作成して、zipファイルを解凍すればよい。
その後mvコマンドに従ってフォルダを移動させる。
移動させなくても2回目のビルドができたが、それが以前何かしらの操作をしていたからなのかは不明。

./travis/get-windows-d3d9sdk(dirextX9 SDK)
#!/usr/bin/env bash

set -euxo pipefail

git clone --depth=1 https://github.com/kevinlul/DXSDK201006.git "$DXSDK_DIR"

"$DXSDK_DIR" はCドライブ直下がおすすめ。(元がそうであるため。)

3行目

とりあえずソースコードを参考に次のコマンドを打ち込む

$ ./premake5 vs2019 --sound=sdl-mixer
#使用しているvsによってvs2017のように変更

そうすると/adsbuildフォルダが追加される。
そのままソースコードの手順に従っても良いが、私はygo.slnVisual Studio 2019で開いてビルドを行った。

ビルドを行うとエラーを吐きまくって、まず成功はしない。
そこで主要なエラーに対する処置を記載しておく。

  • includeエラー

各プロジェクトは名前が一致するフォルダ内に配置されたpremake5.lua(数字は異なり得る)に従って構成されている。
例えば、次のコードはirrlicht/premake5.luaであるが$(DXSDK_DIR)という変数が使われているのがわかる。

project "Irrlicht"
	kind "StaticLib"
	includedirs "include"
	links "imm32"
	dofile("defines.lua")
	exceptionhandling "Off"
	rtti "Off"
	files { "**.cpp", "**.c", "**.cxx", "**.hpp", "**.h" }

	filter "options:no-direct3d"
		defines "NO_IRR_COMPILE_WITH_DIRECT3D_9_"

	filter "options:not no-direct3d"
		defines "IRR_COMPILE_WITH_DX9_DEV_PACK"
		includedirs "$(DXSDK_DIR)Include"
		libdirs "$(DXSDK_DIR)Lib/x86"

これを先ほどダウンロードしたdirectX9 SDKのパスを直接書き込んでやればよい。
私はそれでも解決しなかったため、Include内全部をirrlicht/includeにコピペした。

2回目もここで詰まったので、最初からIncludeの中身をirrlicht/includeにコピペするのが良いかもしれない。


以下のエラーはedoprohttps://github.com/edo9300/ygopro-coreの公開しているCoreを使用することで回避可能。

  • back_inserter' 'std' のメンバーではありません。

VC++10の仕様変更の影響。
相当古いソースを使っているらしい。

該当ファイル(ocgcore/operations.cpp)のインクルードに#include < iterator >を追加する。
流石にソースからビルドするような人にインクルードの説明は要らないだろう。

  • 'uint32_t' から 'int' への変換には縮小変換が必要です

MicroSoftのドキュメントによるとキャストすれば直るらしいが、直らなかったのでそのままexeファイルを実行したら動いたのでとりあえず無視することにした。
いつかエラー吐くのが楽しみですね。


と、全てのコンパイルエラーを抜ければ起動できるようになるので、予めインストーラー等で導入したEDOproのカード画像やフォントを使えばADSが動くわけである。
(そもそもADSをインストールできない人はダメ元で./travis/quickrun.shを試してみるとか?)

まとめ

手順の流れを1つのシェルスクリプトにしてみたので、Windows10環境ならコピペでも動くかも?

#githubからクローンしてきた後、そのディレクトリ(eg. c/ads)で実行する

# install core engine
git clone https://github.com/edo9300/ygopro-core ocgcore

# install premake5.exe
./travis/install-premake5.sh windows

# install irrklang64
curl --retry 5 --connect-timeout 30 --location --remote-header-name --remote-name http://www.ambiera.at/downloads/irrKlang-64bit-1.6.0.zip
echo Extracting irrKlang64...
unzip -uo irrKlang-64bit-1.6.0.zip > /dev/null
rm -rf irrKlang
mv irrKlang-64bit-1.6.0 irrKlang
rm irrKlang-64bit-1.6.0.zip


# install freetype
curl --retry 5 --connect-timeout 30 --location --remote-header-name --remote-name http://downloads.sourceforge.net/freetype/freetype-2.6.5.tar.bz2
echo Extracting FreeType...
tar xf freetype-2.6.5.tar.bz2 > /dev/null
mv freetype-2.6.5/builds freetype
mv freetype-2.6.5/include freetype
mv freetype-2.6.5/src freetype
rm -rf freetype-2.6.5
rm freetype-2.6.5.tar.bz2


# install irrklang32
# We wrap irrKlang32's extract because its zip has a hidden macOS directory that we don't want
curl --retry 5 --connect-timeout 30 --location --remote-header-name --remote-name https://www.ambiera.at/downloads/irrKlang-32bit-1.6.0.zip
echo Extracting irrKlang32...
unzip -uo irrKlang-32bit-1.6.0.zip -d irrKlang-tmp > /dev/null
# Merge 32-bit binaries into folder
mv irrKlang-tmp/irrKlang-1.6.0/bin/win32-gcc irrKlang/bin/win32-gcc
mv irrKlang-tmp/irrKlang-1.6.0/bin/win32-visualStudio irrKlang/bin/win32-visualStudio
mv irrKlang-tmp/irrKlang-1.6.0/lib/Win32-gcc irrKlang/lib/Win32-gcc
mv irrKlang-tmp/irrKlang-1.6.0/lib/Win32-visualStudio irrKlang/lib/Win32-visualStudio
rm -rf irrKlang-tmp
rm irrKlang-32bit-1.6.0.zip


# install irrlicht
curl --retry 5 --connect-timeout 30 --location --remote-header-name --remote-name https://github.com/edo9300/irrlicht1-8-4/archive/master.zip
echo Extracting irrlicht... this may take some time.
unzip -uo irrlicht1-8-4-master.zip > /dev/null
mv irrlicht1-8-4-master/include irrlicht/include
# Technically, only code files need to be moved, and code files in lzma and aesGladman, but this is easier
mv irrlicht1-8-4-master/source/Irrlicht irrlicht/src
rm -rf irrlicht1-8-4-master
rm irrlicht1-8-4-master.zip
# We will build against vcpkg-provided versions of these libs
rm -rf irrlicht/src/bzip2 irrlicht/src/jpeglib irrlicht/src/libpng irrlicht/src/zlib irrlicht/src/MacOSX

# ※install vcpkg-cache and directX9 sdk here if you need※

# irrlicht/premake5.luaの"$(DXSDK_SDK)"を書き換えるコード

# build for vs2019
./premake5 vs2019 --sound=sdl-mixer 

成功した後、わずかに書き換えたため不備があれば連絡求む

終わりに

私はもう疲れたので休みますが、また時間ができたら再度同じ手順を実行して書き漏れを補充したいと思います。

とはいえ、これでADSをc++で拡張できるわけですからPythonと組み合わせてAI開発に励みたいと思います。

2回目のビルドで気が付いたが、もしかしてirrklang使ってない?