CODINGAME FALL CHALLENGE 2020 参加記
CodinGame で 2020/11/13 ~ 23 に開催された CODINGAME FALL CHALLENGE 2020 に参加しました。
目次
コンテスト概要
素材からポーションを作ってルピー(お金)を稼ぐゲームがあって、それを動かす AI を書いて競い合うコンテスト。
ルールなどはこちらのツカモさんの記事を、参照!(丸投げ)
結果
世界: 33位 / 7011人 日本:6位 / 320人 でした。初参加でこの結果はうれしい!
やるべきこと全部ぶん投げて参加した甲斐がありました!!(論文さん......)
やったこと
ビームサーチ (幅 500 深さ 8) で盤面の評価値と各操作によるボーナスの和を最大化するような操作の探索を行い、見つかった最善手を採用していくという方法を取りました。 探索の方法はコンテスト中に ビームサーチ → chokudai サーチ → ビームサーチ という変化を遂げました。 ビームサーチ / chokudai サーチ の実装は下のツカモさんの記事を参考にしました。ありがとうございます!
盤面の評価値
盤面の評価値は以下のように定義しました。
ルピー
1ルピー = 1000 点
素材
tier-i = 900 × (i + 1) 点 / 個
ポーションの素材とルピーの関係を見ると tier-i 1つから (i + 1) ルピー が生じていることが分かるため、素材の価値はそれから生じるルピーとほぼ同じにすべきだと考えました。
魔法
] である非 repeatable 魔法の価値は
で計算し、 repeatable 魔法の価値は として
で計算しました。ただし、 はその魔法が castable なら 10、そうでなければ 0 となるものです。repeatable の倍率が切り上げになっているのは「repeatable、強いだろ」の気持ちが入っているためです(ふつうに切り下げの方が良かったかもしれない)。係数の 40 という値はパラメータガチャで見つけました。
各操作へのボーナス
盤面の評価値に加え、各操作にボーナスを与えることも考えました。各操作のボーナスは次のように決めました。
BREW
BREW へのボーナスは、「近い BREW を優先したい」、「相手の既得ポーション数が大きくなってきたら BREW の優先度を上げたい」という気持ちから次のように決めました。
は現在からのターン数です。はじめは相手のポーション数に合わせてもっと細分化してたんですが、そうするよりこっちの方が安定しました。途中で評価関数がガチャガチャ変化するのはあまり良くないのでしょうか?
LEARN
LEARN へのボーナスは「はじめは LEARN を優先したい」、「近い LEARN を若干優先したい」という気持ちから次のように決めました。
は現在からのターン数、 は tome の index です。100 という数字はゲーム中盤以降で LEARN される回数のバランスをみて決めました。 には、tax として取られてしまう tier-0 の評価値を補償したいという気持ちが込められています。
CAST, REST
ターンロスとして -400 を与えました。
勝利の確定と負け確の回避
今現在の自分の既得ポーション数が 5 のときは、 相手の次ターンのルピー数としてあり得るものの最大値を求めておいて、それを次のターンで超えられるような手があればそれを確定的に選んで、逆にそれを下回るような手は探索から外すようにしました。
探索の高速化
priority_queue に Node のポインタを乗せる
Visual Studio のパフォーマンスプロファイラを見ると、やはり実行時間の大部分を priority_queue の push/pop が占めていました。 そこで、priority_queue に Node 構造体をそのまま載せるのではなく、Node のポインタを乗せるようにしました。これでだいたい 1.5 倍くらい速くなりました(ほんとはもっと速くなるとおもってたけど......)。
具体的にどのようにしたかというと、このようにしました ↓ (雑) (もっとこうした方が良いよ!とかありましたら教えていただけると嬉しいです!)
struct Node { ActionType initialActionType; int initialActionId; int score; int bonus; // etc... bool operator<(const Node& r) const noexcept { return this->eval() < r.eval(); } int eval() const noexcept { // Node の評価値を返す } }; class NodePointer { private: Node* nodePtr = nullptr; public: explicit NodePointer(const Node& node) noexcept { nodePtr = new Node(node); } NodePointer(const NodePointer& x) noexcept { nodePtr = new Node(x.get()); } NodePointer(NodePointer&& x) noexcept { nodePtr = x.nodePtr; x.nodePtr = nullptr; } ~NodePointer() noexcept { if (nodePtr != nullptr) memoryFree(); } int eval() const noexcept { return nodePtr->eval(); } Node& get() const noexcept { return *nodePtr; } void memoryFree() noexcept { delete nodePtr; nodePtr = nullptr; } NodePointer& operator=(const NodePointer& x) noexcept { if (nodePtr != nullptr) memoryFree(); nodePtr = new Node(x.get()); return *this; } NodePointer& operator=(NodePointer&& x) noexcept { if (nodePtr != nullptr) memoryFree(); nodePtr = x.nodePtr; x.nodePtr = nullptr; return *this; } bool operator<(const NodePointer& r) const noexcept { return this->nodePtr->eval() < r.nodePtr->eval(); } }; // ビームサーチ priority_queue<NodePointer> nexts; for (int depth = 0; depth < DEPTH_LIM; depth++) { priority_queue<NodePointer> nextsTmp; for (int i = 0; i < BEAM_WIDTH && !nexts.empty(); i++) { nextsTmp.emplace(move(nexts.top())); nexts.pop(); } nexts = priority_queue<NodePointer>(); while (!nextsTmp.empty()) { const Node& node = nextsTmp.top().get(); for (int i = 0; i < n; i++) { Node newNode; // hogehoge nexts.emplace(newNode); } nextsTmp.pop(); } }
QCFium 法
QCFium 法 というのは次のような pragma を書いて、コードからコンパイラオプションを指定するというもの ↓
#pragma GCC optimize("O3", "unroll-loops", "omit-frame-pointer", "inline")
これは劇的で 10 倍くらい高速化されました。
えーQCFium法したら探索速度10倍になったが
<QCFium 法を試そうと思ったいきさつ>
(これ合法なのか??) pic.twitter.com/qyMqyfWaoY
CodinGame の Discord でモデレーターが こう 言ってるんで大丈夫じゃないですかね(たぶん)。
<合法?>
試したけどあんまりよくなかったこと
ビームサーチの幅や深さを動的に変化させる
たとえば、自分や相手の既得ポーション数が大きくなるにつれて幅を広く、深さを浅くするといったことを試してみました。 しかし、これをすると安定性がなくなって格上にときどき勝てる一方で格下に度々負けたりするようになり、総合的にはあんまりよくなかったです。 うまくやる方法とかあるんでしょうか。
ビームサーチ / chokudai サーチ の深さを超深くする (40 ターンとか)
よわよわになりました。
ビームサーチ / chokudai サーチ の深さを超浅くする (3 ターンとか)
よわよわになりました。
序盤の LEARN しまくるときだけ深さを深くする
こうするといい感じにデッキが組まれたりするかな~~とか思いましたが、不安定になりました。
相手の方が確実に速く取れるポーションは探索から除外する
これをすると明らかに弱くなりました。多様性が下がるとかそういうのが原因でしょうか? まぁ確かに相手が取らなかった場合の選択がなくなってしまうのは痛い気がします。
ルピーの評価値をめちゃくちゃ大きくする
「今作れるポーションをあえて作らず、より効率のいい行動をする」とかの賢い行動が減って弱くなりました。
やらなかったこと
感想
頑張って改善して実際にオババが強くなってるところを見られたりするのが超楽しかったです。結果も出たし本当に参加してよかったです。1週間吹っ飛んだけど。