Legends of Code & Magic(LOCM)でLegend Leagueに入った
概要
Legends of Code & Magicは、2人対戦のカードゲームを行うボットを作成し、その強さを競うプログラミングコンテストです。コンテストはCodinGameで常設されています。
前回の記事では、ルールの紹介と、カードゲームを統計的に分析するときに何に注目するか、何が読み取れるか等を書きました。
上の記事で述べた分析を活用し、Legend Leagueに入ることができたので、どういう方針で実装を行ったか、工夫した点などについて振り返りたいと思います。
- 実装の方針
- 細かいテクニック
実装の方針
まず、実装の流れを書きます。
- 対戦フェーズのボットを実装する。
- ランダムにドラフトを行う自己対局を繰り返してデータを収集する。
- 収集したデータを利用したドラフトフェーズのボットを実装する。
- 自己対局を繰り返して更にデータを追加する。
このように、対戦フェーズのボットを実装し終わってから、ドラフトフェーズのボットを実装しました。[1]
対戦フェーズとドラフトフェーズの開発を別々に行った理由は、ドラフトのボットに統計データを利用することを考えた場合、質の良いログを集められないと、正しく評価を行うことができないためです。
対戦フェーズ
自分は、ゲームの情報を評価する評価関数を設計し、ゲーム木を探索することで、最適な行動を計算することにしました。
他によくあるゲームの探索アルゴリズムとしては、モンテカルロ木探索が挙げられます。ただ、不確定要素が多く、実行時間制限(
また、カードゲームと同じく不確定情報ゲームである麻雀のAIで、Lucky Jというものがあります。Tencentによる公式の紹介記事(その翻訳記事)によると、強化学習を拡張した方法を使って作成しているそうです。なので、LOCMでも強化学習を使った強いボットを作ることは可能なのかもしれません。
評価関数・探索アルゴリズムの設計
まずは、評価関数の設計についてです。
以下のような要素を考慮すればよい、というのはすぐに分かります。
- 自分と相手のHP
- 自分と相手の盤面にいるクリーチャー
- クリーチャーの攻撃力、体力、キーワード能力
- 次のターンのドロー枚数
- 残っている手札
これらの要素を適当に重み付けしながら関数を設計すればよいです。
例えば、自分の盤面に攻撃力
Wardを失わせるためには、一度0より大きいダメージを与える必要があります。このとき、相手のクリーチャーは
このような考察は、他の要素、キーワード能力についても同様に行うことができます。
例えば、以下のような質問にどう答えればよいかを考えてみると分かりやすいかもしれません。[2]
- 「自分の盤面に攻撃力
、体力a のクリーチャーが2体いる」と「自分の盤面に攻撃力h 、体力2a のクリーチャーが1体いる」はどちらが嬉しいか?2h - 「自分の盤面に攻撃力
、体力a のクリーチャーが2体いる」と「自分の盤面に攻撃力h 、体力a のクリーチャーが1体いる」はどちらが嬉しいか?2h + 1 - 「自分の盤面に攻撃力
、体力a のクリーチャーが2体いる」と「自分の盤面に攻撃力h 、体力2a + 1 のクリーチャーが1体いる」はどちらが嬉しいか?h - 「自分の盤面に攻撃力
、体力1 Lethal のクリーチャーが2体いる」と「自分の盤面に攻撃力h 、体力1 Lethal のクリーチャーが1体いる」はどちらが嬉しいか?2 h
次に、探索アルゴリズムの設計についてです。
標準入力でターン開始時の状態が与えられるので、ここから自分のターンでできる合法手をプレイして到達できる状態を列挙します。列挙されたそれぞれの状態に対して、相手のターンに進みます。
相手の手札は分からないので、相手の盤面に残っているクリーチャーの攻撃を合法手として、到達できる状態を列挙します。
そうして自分のターン、次の相手のターンまでを終えて到達できる状態を評価し、自分にとって最も評価の高い状態を実現できる合法手を探索します。
下は状態遷移のイメージ図です。始状態
相手のクリーチャーの攻撃を考慮するのは、自分から見えている確定情報の中で、最も深い探索になります。探索をより深く行っていれば、評価関数の設計が多少悪くても、それなりに良いプレイングを探索できるようになります。
例えば、自分の盤面に3/3のクリーチャー
自分でクリーチャー
相手のクリーチャーの攻撃を探索しない場合、以下の2パターンの比較になります。
- 自分のクリーチャー
でクリーチャーA を攻撃して、両方破壊されることB - 相手を攻撃して、クリーチャー
はそのままで相手の体力が3減ることA, B
上の評価が高いとき、クリーチャー
相手のクリーチャーの攻撃を探索する場合、以下の2パターンの比較になります。
- 自分のクリーチャー
でクリーチャーA を攻撃して、両方破壊されることB - 相手を攻撃してクリーチャー
はそのままで相手の体力が3減り、その後相手のターンにクリーチャーA, B でクリーチャーB を攻撃し、両方破壊されることA
この比較であれば、評価関数がそれなりに真っ当な場合、後者を選択できます。
高速化の工夫
手札の枚数が多く、お互いの盤面にクリーチャーが多く並んでいるときは、ゲーム木を構築するのに時間がかかってしまいます。
盤面に6体ずつクリーチャーがいるときは、攻撃の順番と攻撃対象の選択で最悪
LOCMでは、クリーチャーの攻撃、クリーチャーの召喚、カードの使用は順番を入れ替えても結果が変わらないことが多くあります。そのため、一度探索し終えた状態と同じ状態を探索すると無駄が大きく、重複した状態を見ないようにすることの効果が大きいです。
重複した状態を見ないようにするには、探索の順番を制御することも重要です。例えば、クリーチャーの召喚を後回しにするように探索すれば、クリーチャーの攻撃のパターンを取り尽くしたあとで、クリーチャーの召喚のパターンを取り尽くすように探索でき、ランダムな場合と比べて実行時間が短くなることが期待されます。[3]
ドラフトフェーズ
結論から言うと、3つの中からDrawnWRが高いものを選び続けるだけで、Legend Leagueに入れました。(DrawnWR: あるカードがドローされた試合における勝率)
データベースを作成し、ボットはそこからDrawnWRを計算して、ドラフトを行うようにします。
試合が終了したら、その結果をもとにデータベースを更新します。これを繰り返して、DrawnWRをより正確にします。
コンテストに提出するときは、単独のソースコードを提出するので、データベースから計算したすべてのカードのDranwWRの値を埋め込んで利用します。
更に戦略を発展させる場合、マナカーブを考慮してドラフトを行うような戦略は妥当だと考えられます。
ドラフトがある程度済んだ状況を考えると、データベースから計算されるDrawnWRは、現時点でのデッキに加えたときに期待されるDrawnWRと異なることは明らかなので、より強いボットを作成する上ではマナカーブを考慮する必要があります。
実際、1位の方と対戦させて振る舞いを観察していましたが、そのような戦略を取っていることが確認できました。
ただ、Legend Leagueに到達する程度であれば、DrawnWRだけを見てドラフトしても十分戦えます。
LOCMでDrawnWRが指標として優れている理由
LOCMは、デッキにあるすべてのカードは同じ頻度でドローされます。
これは当たり前のように感じるかもしれませんが、一般的なカードゲームでは、デッキから自分で選んでカードをドローできる効果をもったカードはよくあります。そのような場合、カードごとに引かれる頻度が異なるので、カードの強さの指標としてDrawnWRを単純に使うのは難しいです。
例えば、カード
- 普通にカード
をドローする。B - カード
を使用してカードA をドローする。B - 普通にカード
をドローする頻度は、普通にカードA をドローする頻度と大して変わらないと考えます。B
- 普通にカード
ここで、2のパターンでカード
LOCMでは、上のようなカードは実装されていないので、DrawnWRをカードの強さの指標として利用しやすいです。
あとがき
Legend Leagueに到達して一区切りついたので、戦略や実装についても書いておきたいと思い、そこに焦点を当てて記事を書きました。
当初はもう少し工夫しないとLegend Leagueには行けないかなと考えていましたが、DrawnWRを利用したドラフト戦略がかなり効果的で、そこまで苦労せずに到達できました。
ドラフト戦略はまだ工夫の余地があるので、それを分析しつつ、もう少し上位が狙えると良いなと考えています。
CodinGameのCode of Conductによれば、
Sharing of code is encouraged, as part of the CodinGame's solution system. Please refrain from sharing "ready to copy & paste" code in the forum or on GitHub for example.
とあります。実装の詳細に触れない範囲で、振り返りを行うようにしました。
Discussion