概要
第34回高専プロコンの競技部門1に参加しました。その際、公式のサーバーと通信しフィールドの状態を表示するビジュアライザを、OpenSiv3D2を用いて実装しました。
今年のルールは基本的には陣取り合戦で、フィールドをより広く囲ったチームが勝ちです。加えて、城を囲むと得点が高いなどのルールがあります。詳しいルールは高専プロコンのホームページを参照してください。
特に「競技プログラミングでしかコードを書いたことがなくて、できれば全部C++で書きたい!」という人におすすめしたいです。
良かった点
実装量が少ない
例えばボタンを座標(400,300)の位置に出して、そのボタンがマウスカーソルによって押されたかどうか判定するコードは次のように書くことができます。
if (SimpleGUI::Button(U"ボタンの名前",Vec2{400,300}){
//処理
}
HTTPのGETリクエストも一行で書けます。
if (const auto response = SimpleHTTP::Get(url, headers, saveFilePath)) {
//例として、ステータスコードが200(OK)かどうかの判定
if (response.isOK()) {
}
}
簡単で助かりますね。
競技部門に必要な機能が全て含まれている
競技部門のビジュアライザを製作するにあたって、実装しなければならない機能は大まかにはこれくらいです。
- ボタンなどの基本的なUI
- HTTP通信(GET,POSTリクエスト)
- JSONファイル操作
- 子プロセスとの通信
OpenSiv3Dを導入すればこれらすべてを簡単に書けるため、競技部門におけるフレームワークの選択肢としてかなり魅力的です。
また、サンプルコードも豊富に存在します。HTTP通信やJSONファイルの構造などは全く知らない状態だったのですが、ちょっとコピぺしてきて改造すれば思い通りの処理を簡単に実装することができました。ここなどを眺めると良いです。
サポートが手厚い
Discordにあるコミュニティ3で質問をすると、1時間以内には返信が来ます(!?)。個人的に作者さんは3人いて交代で活動していると思っています。
ただし、質問の仕方には気を付けましょう。意図がはっきりしている良い質問をすれば、答える側も答えやすいです。例えば、エラーの原因を知りたい場合は、そのエラーが起こる最小のコードも併せて質問すると良いでしょう(その過程で、自己解決することも多いです)。
注意した方が良い点
MacとWindowsの両方で開発する場合注意が必要
OpenSiv3Dを使用したコードは、WindowsとMacの両方でコンパイルすることができます。しかし、Windowsで動いていたコードをそのままMacでコンパイルするとかなりエラーが出ます。結局最後までエラーを解決することができず、チームメイトには迷惑を掛けました。
これは私がMacの開発環境であるXCodeを触ったことがないのもあると思います。しかし、同じチーム内でOpenSiv3Dでの開発経験のない別プラットフォームの人がいる場合は注意が必要です。
子プロセスで呼び出したPythonが入力を受け付けない
子プロセスとの通信ができるChildProcessクラスは、ソルバとの通信を実装するにあたって非常に便利です。しかし、子プロセスとしてPython.exeを起動した場合、コード中でinput()を使うと強制終了してしまい、入力を読むことができなかったので注意が必要です。
KerasやPyTorchを使ってAIを実装したい!という人はPythonですべて書くのが良さそうです。
高専プロコンでOpenSiv3Dを使いたくなった人へ向けて
私が今年書いたコードを例として、よく使う処理を説明します。
試合一覧の取得や試合情報の取得
SimpleHTTPというnamespace内にGet関数があるのでそれを使いましょう。
URLにトークン(プロコン公式から1チームごとに与えられるパスワードみたいなもの)を足さないといけないので気を付けましょう。
また、プロコン公式がリクエストやレスポンスを保存しておけというので、保存しておきましょう。公式のサーバーがバグっていたときに文句を言うことができます。
const String jsonFoldername = DateTime::Now().format(U"yyyy-MM-dd HH-mm-ss");
const String jsonsaveFolder = U"json/" + jsonFoldername + U"/";
//参加する試合の一覧を取得するリクエストをGETで送信する
bool APIManager::sendGetMatches() {
const URL url = urlbase + U"?token=" + token;
const HashTable<String, String> headers = { {U"procon-token",token} };
const FilePath saveFilePath = jsonsaveFolder + U"get_matches_response.json";
//GETリクエストの送信に成功した場合
if (const auto response = SimpleHTTP::Get(url, headers, saveFilePath)) {
Print << response.getStatusLine();
if (response.isOK()) {
const JSON json = JSON::Load(saveFilePath);
if (not json) {
throw Error{ U"Failed to load " + saveFilePath };
}
this->matches_response = json;
}
else {
return false;
}
}
//GETリクエストの送信に失敗した場合
else {
Print << U"Failed to send getMatches.";
return false;
}
return true;
}
ソルバーを起動する
ChildProcessというクラスで子プロセスを作ることができます。ソルバーから送られてきたコマンドを常に読み込むために別スレッドを作成すると良いでしょう。(もしかしたら、しなくても良いかもしれません?)
//子プロセスの作成
void Solver::create(FilePath mappath, bool is1P,std::mutex& mtx) {
this->child = ChildProcess{ this->solver_path.value() ,Pipe::StdInOut};
if (!child) {
throw Error{ U"Failed to create a process." };
}
this->readyok = false;
this->is1P = is1P;
this->is2P = not is1P;
//常にソルバーの標準入出力を読みたいので、別スレッドを作成する
//これはあまりSiv3Dっぽくなく、この処理をする標準的な書き方が分からないので教えてほしいです
this->receiveOK.store(true);
std::promise<bool> p;
this->receiver = p.get_future();
this->receiver_thread = std::thread([&](std::promise<bool> p) {
while (this->receiveOK.load()) {
if (not this->receive(mtx)) {
p.set_value(false);
return;
}
}
p.set_value(true);
return;
}, std::move(p));
}
ソルバー側は標準入出力(C++でのstd::cin,std::cout)を使うだけで通信ができます。プログラミングを始めたばかりの人がチームにいる場合も安心ですね。
ソルバーへ試合情報を送信する
子プロセスへの出力は、C++のstd::coutのようにoperator<<が実装されているので簡単に実装できます。
void Solver::send_command_solver() {
this->child.ostream() << "solver?" << '\n';
this->child.ostream() << Procon::Config::pipe_protocolVersion << endl;
}
サーバーへ着手を送信する
SimpleHTTP::Post()を使用します。
//現在のターンに対する行動計画を送信する
void APIManager::sendPostAction(const int32 matchID, const Procon::Actions& actions) {
const URL url = urlbase + U"/{}"_fmt(matchID) + U"?token=" + token;
const HashTable<String, String> headers = { {U"procon-token",token} ,{ U"Content-Type", U"application/json" } };
const FilePath saveFilePath = jsonsaveFolder + U"post_action_response{}.json"_fmt(actions.current_turn);
//送信するJSONデータの準備
JSON sendjson;
sendjson[U"turn"] = actions.current_turn + 1;
Array<JSON> actions_json(Procon::Config::masons_count);
for (const int i : step(Procon::Config::masons_count)) {
//こんな感じでJSONファイルにアクセスできるので便利
actions_json[i][U"type"] = actions[i].action.type;
actions_json[i][U"dir"] = actions[i].dir.toNumber();
}
sendjson[U"actions"] = actions_json;
//例によってリクエストは保存しておきましょう
sendjson.save(jsonsaveFolder + U"post_action_turn{}.json"_fmt(actions.current_turn));
const std::string data = sendjson.formatUTF8();
//サーバーにPOSTリクエストを送信する
if (auto response = SimpleHTTP::Post(url, headers, data.data(), data.size(), saveFilePath)) {
Print << response.getStatusLine();
if (response.isOK()){
const JSON json = JSON::Load(saveFilePath);
if (not json) {
throw Error{ U"Failed to load " + saveFilePath };
}
this->action_response = json;
}
}
else {
Print << U"Failed to post actions.";
}
}
その他いろいろ
-
タイマーはターンごとにリセットしないようにしましょう。累積された誤差が50ターンくらいから誤差が無視できなくなってきます。
-
試合はアナウンスと同時に開始されず、3秒程度早めに始まります。手動でプログラムを開始するとそれだけ時間を無駄にすることになるので気を付けましょう。
-
盤面が回転対称の場合、1Pと2Pがひっくり返っていても思っているより気づきません。ちゃんと合っているか確認しないと試合中にプログラムが異常終了することになります(第一試合前半での出来事)。
最後に
OpenSiv3Dは手軽なビジュアライズの手段として強力なフレームワークだと感じました。ぜひ皆さんも来年の競技部門で使いましょう!