CodinGame でローカル対戦環境を作る
CodinGame / コドゲ というゲームAIを作って戦わせるコンペを定期的に開いているウェブサイトがあります。 この記事の執筆時点では Spring Challenge 2021 が開催中です。
コドゲはかなりリッチなサイトで、ブラウザ上のエディタで実装してテスト対戦実行・本番提出まで可能です。
ローカルにプログラミング環境がなくても実行できるのでめっちゃ便利で初心者にも優しいのですが、
サイトが混みあっているとき (終了間際とか) なんかはなかなか結果が返って来なくて厳しいですし、
何十回も対戦させて検証したいときなんかはちょっとやってらんないです。
多くの上位者はゲームのシミュレータを自分で実装してこれを解決していますが (多分)、
実はコドゲは GitHub 上でゲームのコードを公開していることがあるので、
これを利用して簡単にローカル対戦環境を作っちゃいましょう。
(当然ですが他ユーザーのコードは提供されないので、
サイト上での様に他ユーザーと戦わせるとかはできません。主に自己対戦用。
実行環境とかも違うと思うので注意。)
やること
今回の Spring Challenge 2021 では https://github.com/CodinGame/SpringChallenge2021 にコードが公開されています。
リポジトリに pom.xml
というファイルが見えます。
これは Maven という Java のプロジェクト管理ツールに使われているものなので、 Java (JDK) と Maven が必要です。
環境にない人はインストールしましょう。
僕は Windows 環境で、パッケージマネージャに Scoop を使っているのでそれで書きます (環境変数とかも設定してくれて便利)。 違う環境の人は適当に公式サイトからインストーラをダウンロードするとか apt とか packman とかを使うとか読み替えてください。
Java 開発環境
上述の通り Java 開発環境が必要なのでインストールします。 最近は openjdk とか何か色々派生してます。 過去の僕が何故か corretto11 を入れていましたが、最近のなら多分何でもいいと思います。 (どれがいいのかよくわかっていない)
$ scoop bucket add java
適当な JDK をインストール (なんでもいいと思う)
$ scoop install corretto11
Maven 環境
上述の通り Maven でプロジェクト管理しているようなので、依存パッケージを解決するのに使います。
$ scoop install maven
公式githubリポジトリからコードをダウンロード
コドゲ公式のリポジトリを clone します。 今回はこちら
https://github.com/CodinGame/SpringChallenge2021
$ git clone --depth 1 https://github.com/CodinGame/SpringChallenge2021 $ cd SpringChallenge2021
最新版だけあればいいので --depth 1
を指定してますが無くても。
git インストールしてなくても github から zip でダウンロードできると思います。
対戦のエントリポイント
src/main/java
配下にゲーム用のコードが入っているのですが、対戦用のエントリポイントは
src/test/java
の方に入っています。
今回の場合は これ https://github.com/CodinGame/SpringChallenge2021/blob/main/src/test/java/Spring2021.java 。
static String[] DEFAULT_AI = new String[] { "python3", "config/Boss.py" };
とかで AI の実行コマンドを指定していますね。 ここを書き換えて自分のに置き換えましょう。
この段階で VSCode (+Java拡張) 等 Maven 対応の IDE で開いて実行できます。
pom.xml
によると codingame-game-engine ver 4.0.2 を参照しているようです。
(過去回や未来回はバージョンが違うと思うので注意。 tag 付けてくれてるので対応したのを参照してね)
コードを追いかけたりドキュメントを見たりするとちょっと改造できます。
例えば Spring2021.java では MultiplayerGameRunner の MultiplayerGameRunner.start()
を呼ぶことで localhost:8888 に結果を表示していますが、
ビジュアライズ部分が不要な場合 MultiplayerGameRunner.simulate()
を呼べば良いです。
戻り値の GameResult 型にはスコア情報などが入っているので、これをコンソールに出力等すれば結果がわかります。
連戦させた結果が欲しい場合はこれのループを回せばいいです。
Maven で実行可能な jar をビルドして実行
さて、ここまででも IDE 経由の実行で対戦実行させることはできましたが、 対戦実行の度このプロジェクトを開きたくないので実行可能な jar ファイルをビルドします。 (オプションをめっちゃ指定してやるとかでもできるんだけど、長くて面倒)
[重要] Maven詳しくないんですが、 src/test
に入っているコードはビルド時に除外されてしまうみたいなので
エントリポイントのファイルを src/main/java
に移動しておきます。
src/test/java/Spring2021.java -> src/main/java/Spring2021.java
次のコマンドでビルド (不完全?) できます。
$ mvn assembly:assembly -DdescriptorId=jar-with-dependencies
これで target/spring-2021-1.0-SNAPSHOT-jar-with-dependencies.jar
のようなファイルをビルドしてくれます。
参考: Maven Assemblyとは?
実行するときは
$ java -cp 生成されたjarへのパス Spring2021
の様にして実行できます。お疲れさまでした。 (Spring2021 はエントリポイントのクラス名。 もし変えてたらここも変更)
なんか jar にパッケージしたときビジュアライズ用のファイルの相対位置がおかしくなるみたいで、 一部画像が表示されませんでした。(自分の環境だけ?謎。)
次のような assembly.xml
を別に用意して解決できました。
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd"> <!-- TODO: a jarjar format would be better --> <id>jar-with-dependencies</id> <formats> <format>jar</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <dependencySets> <dependencySet> <outputDirectory>/</outputDirectory> <useProjectArtifact>true</useProjectArtifact> <unpack>true</unpack> <scope>runtime</scope> </dependencySet> </dependencySets> <fileSets> <fileSet> <directory>${project.basedir}/src/main/resources/view</directory> <useDefaultExcludes>true</useDefaultExcludes> <outputDirectory>/view/assets</outputDirectory> </fileSet> </fileSets> </assembly>
ビルドするときは
$ mvn assembly:assembly -Ddescriptor=assembly.xml
に変わります。
tips
ビジュアライザ見ながらステップ実行でデバッグ、 みたいなことをしたくてもそもそも実行元が java のサブプロセス呼び出しなので (簡単には) できません。
間に何かを噛ませればできそうに思うんだけど (Spring2021 -> A <===> B with debugger, AB間はパイプとかファイルとか通信とか人力コピペとか)、実はタイムアウトの処理も提供されたコードに組み込まれているので 50-100ms のターン毎の時間制限に引っ掛かります。
// src/main/java/com/codingame/game/Referee.java public void init() { this.seed = gameManager.getSeed(); try { Config.load(gameManager.getGameParameters()); Config.export(gameManager.getGameParameters()); gameManager.setFirstTurnMaxTime(1000); gameManager.setTurnMaxTime(100); gameManager.setFrameDuration(500);
弄ればよさそうに見えるんですが、実はゲームエンジンの方で上下限 (50ms - 25s / turn, 30s / game) が決められていてできません。
src/main/java/com/codingame/gameengine/core/GameManager.java
をローカルにコピーして、以下のあたりを適宜コメントアウトしたり数値をいじればよさそう?(試してない)
// L38 private static final int GAME_DURATION_HARD_QUOTA = 30_000; private static final int GAME_DURATION_SOFT_QUOTA = 25_000; private static final int MAX_TURN_TIME = GAME_DURATION_SOFT_QUOTA; private static final int MIN_TURN_TIME = 50; private int turnMaxTime = 50; private int firstTurnMaxTime = 1000; // L456 public void setTurnMaxTime(int turnMaxTime) throws IllegalArgumentException { if (turnMaxTime < MIN_TURN_TIME) { throw new IllegalArgumentException("Invalid turn max time : stay above 50ms"); } else if (turnMaxTime > MAX_TURN_TIME) { throw new IllegalArgumentException("Invalid turn max time : stay under 25s"); } this.turnMaxTime = turnMaxTime; } // L473 public void setFirstTurnMaxTime(int firstTurnMaxTime) throws IllegalArgumentException { if (firstTurnMaxTime < MIN_TURN_TIME) { throw new IllegalArgumentException("Invalid turn max time : stay above 50ms"); } else if (firstTurnMaxTime > MAX_TURN_TIME) { throw new IllegalArgumentException("Invalid turn max time : stay under 25s"); } this.firstTurnMaxTime = firstTurnMaxTime; } // L579 private void addTurnTime() { totalTurnTime += turnMaxTime; if (totalTurnTime > GAME_DURATION_HARD_QUOTA) { throw new RuntimeException(String.format("Total game duration too long (>%dms)", GAME_DURATION_HARD_QUOTA)); } else if (totalTurnTime > GAME_DURATION_SOFT_QUOTA) { log.warn( String.format( "Warning: too many turns and/or too much time allocated to players per turn (%dms/%dms)", totalTurnTime, GAME_DURATION_HARD_QUOTA ) ); } }