高専プロコン用のGUIを作ってたらSiv3DのJSONをデバッグした

# はじめに
全国高専プログラミングコンテスト(高専プロコン)という大会が2023年の10/14~15にありまして、それの競技部門に出場した時やってたことをずらーっと書く記事です。
なお、ソルバーの部分は後輩にぶん投げていたので、自分がOpenSiv3Dで作ったGUIのことだけ書こうと思います。
# 誰？
仙台高専広瀬キャンパス、#kosen20s
pic: 

https://www.sendai-nct.ac.jp/s20231024/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620524/9ddf598f-9577-a4c8-fa9f-f3b7476bb957.png)
真ん中の伝統を守る(？)人です。
# 高専プロコンって何
その名の通り、「全国」の「高専生」が集まる「プログラミング」の「コンテスト」で、
<dl>
    <dt>課題部門</dt>
    <dd>決められたテーマに沿った作品を作る</dd>
    <dt>自由部門</dt>
    <dd>自由に作品を作る(でも社会に役立つような何かじゃないと予選通らない)</dd>
    <dt>競技部門</dt>
    <dd>毎年違うルールに基づいてゲームをして競う、競プロ(AHC)っぽい感じ</dd>
</dl>

以上3部門が存在します。
で、今回自分は競技部門に出たわけですが、今年のゲームはこんな感じでした(超意訳)↓
- 1v1
- フィールド上のキャラ(それぞれ2~6体)を交互に動かして陣地を取るゲーム
- 1ターンで自キャラ全員を一気に動かせる
- キャラの行動は「移動(待機含め9方向)」「建築(上下左右)」「破壊(上下左右)」のいずれか
- 城壁を「建築」し、ぐるっと囲うと自陣地になる
- 一度陣地になった土地は城壁破壊により囲いが解除されても陣地のまま
    - 囲われている陣地を「閉鎖陣地」、囲われていない陣地を「開放陣地」と呼ぶ
- 「自**閉鎖**陣地 && 敵**開放**陣地」のとき、敵陣地判定は**消滅する**
    - 「自**閉鎖**陣地 && 敵**閉鎖**陣地」は**消滅しない**(どちらも点数にカウント)
- 別に敵の陣地にも入れるが、敵の城壁があるマスには入れない(通せんぼして詰ませれるので結構大事だった(過去形))
- フィールド上には **城** があり、城を陣地に入れると高得点(めちゃくちゃ大事)
- 城壁数x10 + 陣地数x30 + 自陣地内の城の数x100 が得点
- キャラの初期配置とかで先手後手の有利がふつーにあるので、1試合でどっちもやってその合計得点で勝敗が決まる
:::note info
フィールド情報(キャラ・城とかの初期配置)は事前に配布されています。
サイズは11x11から25x25で、フィールドに最初からあるのは
- 城(城壁は建てれない)
- 池(キャラは入れないが、城壁は建てれるし囲えば陣地にもなる)
- キャラ(2~6体、先手後手で分けて配置されてる)
です。

なお、別に先手後手のキャラが対称に置かれてたりはしません。
:::
# できたもの
最終的にはこんなん出来ました
![proconGUI_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620524/4f4ede13-5c8d-dbf9-faac-55ed970ce041.png)
## 説明
### UI・操作について
左側は現在のフィールド状況を描画しています。オレンジの数字が自キャラ、青が敵キャラです。池と城はSiv3Dデフォルトの絵文字です(池表すのにちょうどいい絵文字が無かった…)

右側のUIで行動を選択できます。
まず下のリストボックスから編集する行動を選び、その後キャラID、行動種別、方向を選びます。

矢印をクリックすると選択できるのですが、池や敵のキャラがいる場所、あるいはフィールド外といった行けない場所に対してはそもそも矢印のボタンを表示させないようにしています。
実際に職人4の左下と右下は池があるため、移動の矢印が表示されていないのが分かると思います。

ちなみに、リストボックスや行動ボタンが2つあるのは、先生方から「同時に出して比較できるようにしたらいいんじゃない？」というアドバイスを受け実装したものです。
選んだ行動はマップ上でも確認・比較できるようになっていて、自キャラから出ているオレンジの矢印と、水色の点線の四角がそれにあたります。

真ん中下部のラジオボタンは自動送信用です。自ターン残り1秒で未送信の場合、選択されたIDの行動が自動で送信されます。ソルバーの方が2パターンだけ用意しているという話だったので、じゃAとBってID振っといてと頼み、この機能が出来上がりました。どちらかというとトラブル回避というより放置用を想定。
planってのは事前に計画したファイルを読み込みます。1日目が不調に終わり、敗者復活戦を控えた夜のこと、GUIの行動編集コードを流用してプランナーソフトを爆速で作り上げました(正直その日の記憶がありません)
~~なお3日目の朝に無事寝坊~~

### 内部のデータ管理
競技APIがJSONでデータの送受信を行うので、内部でもそのままJSONで管理しています。
キャラの行動情報は、行動部分のaction配列だけを管理して、実際に送信する時に形式を整え`formatUTF8Minimum`しています。

### 競技サーバーとの通信部
通信はs3d::SimpleHTTPを使っています。
ターンの同期は、s3d::Stopwatchで時間を計り、ターン終了1秒前からAsyncでGETリクエストを投げまくり試合情報の中のturnが変わったタイミングでタイマーをリセットするという力技で同期しています。(1日目はAsyncですらありませんでした)
ちなみにログはUNIX時間とHTTPステータスコード(行動の送信だけは受領時間を競技サーバーが返してくれるのでそれも)をすべてtxtファイルに保存しています。トラブル対策もバッチリ。

# バグって何したの
きっかけはソルバー担当の後輩との話で出たこんなアイデアでした。


**ソルバーは数ターン後まで行動をまとめて出せるからそれもマップに描画しよう**


まあ深層学習AIくんが考えていることを見るためには割と欲しい機能ではあります。
この頃、ソルバーとの連携方法はいたってシンプルで、「行動に加えモデルIDとターン数を埋め込んだJSONをTCPで送ってもらい、GUIでそれを読み込む」ということをしていました。
ターン数が違うもの(今より先の行動)は読み込みの際に弾いていましたが、これをArrayに突っ込んでソートしてやればいいと思ったわけです。
## で、こうなった
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620524/00f4aee0-859e-5046-e701-87ecc97b527d.png)
やったことは、`Array::sort_by`にJSONの中のturnキーで順序を決定するラムダを渡しただけです(たしか)。
なーんかランタイムエラー出るなと思ってスタックトレースを眺めてみると、**これ`s3d::JSON`が死んでね？** と気づきます。
そこで、これとは別に再現コードを書いてみることにしました。そしたらものの見事に***CRITICAL HIT.***
```cpp:Main.cpp
# include <Siv3D.hpp> // Siv3D v0.6.12

class inJSON
{
public:
	int32 num;
	JSON json;
	inJSON() {
		json = JSON();
	}
};

void Main()
{
	Array<inJSON> ar;
	ar << inJSON() << inJSON();
	ar[0].num = 0;
	ar[1].num = 2;

	ar.sort_by([](const inJSON& a, const inJSON& b) {return (a.num < b.num); });

	while (System::Update()) {}
}
```
全く同じスタックトレースが出てきたので、discordに投げてみました(当時出発前日の23:55)
すぐRyoさんに見てもらい、応急処置も教えてもらいました(結局時間が足らず未来描画は断念しましたが...)
最終的には`s3d::JSON`を`swap`すると出るエラーだったようです(コピーコンストラクタ絡み)

https://github.com/Siv3D/OpenSiv3D/issues/1117

# おしまい
**#procon35 仙台高専広瀬競技部門3位🥉、自由部門特別賞🏅おめでとう！！！！！！！！！！！！！！！！！！！！！！！！！！！**