USIプロトコルへの様々な疑問にお答えします

USIに文句を言うだけ言う
http://d.hatena.ne.jp/merom686/20111217


まあ、USIプロトコルがいろいろアレであることは、いまさら言うまでもないことなのですが、なぜこんなプロトコルがコンピューター将棋の標準になっているかと言うと、将棋所という素晴らしいUIが採用したからに他なりません。


その将棋所の作者はコンピューター将棋を自らは作っておらず、何が必要かあまりよくわかっていないのだと私は理解しています。


まず、将棋所のサンプルについているLesserKaiのソースもかなりでたらめなコードです。例えば、将棋所のサンプルのLesserKaiのソースで、時間をparseしているコードは次の部分です。

void USIUtil::ParseAllTimes(const char *buf, int SorE)
{
	string goStr = buf;
	if (goStr.find("infinite") != string::npos) {
		isInfinite = true;
	} else {
		isInfinite = false;
		int pos = goStr.find(" btime ") + 7; // 7 = strlen(" btime ")
		int senteTime = ParseTime(buf + pos);
		pos = goStr.find(" wtime ") + 7; // 7 = strlen(" wtime ")
		int goteTime = ParseTime(buf + pos);
		remainTime = SorE == SELF ? senteTime : goteTime;
		pos = goStr.find(" byoyomi ") + 9; // 9 = strlen(" byoyomi ")
		byoyomiTime = ParseTime(buf + pos);
	}
}

驚くべきことに"byoyomi"という文字列が必ず書かれていることを期待しており、"byoyomi"という文字列が書かれていない場合のことは全く考えてもありません。"byoyomi"という文字列がない場合、findは-1を返し、-1 + 9でpos == 8となり、なんと"btime"の直後の数字を"byoyomi"の数値として解釈してしまいます。


まるで、「"byoyomi"は必ず書くものであり、書かなかったときの動作はどうなっても知らんよ」と言いたげです。


将棋所もこれと同じぐらいひどいコードが書かれていることは想像に難くなく、思考エンジン側から少し変な文字列を送るだけで将棋所がクラッシュするのは、このようなエラーチェックの甘さに起因するものでしょう。


解釈できない文字列が来たら、警告を出した上でデバッグ用のログには書き出すだとか、そういう配慮がないので、思考エンジンのデバッグ時に非常に使いにくいです。将棋所はユーザーフレンドリーではありますが、思考エンジンの開発者フレンドリーでは決してありません。


自分で思考エンジンを書いている人ならば、こんなひどい仕様にはしないと思うのですが。


さて、以下では冒頭で挙げたmerom686さんのUSIプロトコルへの不満に対して私の理解を書いておきます。

ponder(先読み)は、それ自体をUSIの仕様から削除すべきだと思う。
予測読みのやり方は、エンジン作者が考えるべき問題だ。
そのやり方を1種類に決めてGUIがサポートしてはならないと思う。
これは使いやすさ以前の問題だ。

ponderの仕様が非常にまずいことは私が以前書きました。( http://d.hatena.ne.jp/LS3600/20111029 ) 結論としては、ponderを使うなってことです。USIプロトコルから削除すべきは私もそう思います。

setoptionで、エンジン側で設定を保存する必要があるのがおかしい。
ファイルに設定を保存するというのは、それだけで相当な手間である。
USI_PonderとUSI_Hashだけ保存しなくていいというのも一貫性がなく面倒だ。
そもそもハッシュサイズを1MB単位で指定するという仕様も変だ。

1MB単位ではなく1byte単位で指定できるようになっているべきということですかね?
単位がMBになっているのは文字数を少しでも減らす、みたいな意図はあるのかも知れません。

1MB単位ではなく2**N byteのNを指定させろという意味でしたら、それは私は反対です。置換表は、通常探索用・詰将棋探索用など複数の置換表を持つことがあり、それぞれを適当な配分で確保したいことがあるので2**Nしか指定できないと困ります。

position

毎回全ての指し手を書く必要があるのは糞。
N手の将棋でやり取りされるpositionのデータ量がO(N^2)になる時点でキモい。

これはまあ、そうですね。入玉模様で1000手ぐらいの将棋ですと特に…。

USIでは、棋譜をSFENという方式で表す。
これは、USIがUCI(チェス用のプロトコル)を元にしているので仕方ないのだが、
日本人は8hがどの升を表すかわからないし、パーサを書くのもプチめんどくさい。
飛車がRookのrで表されるのも、チェスの知識を要求される。

Bonanzaソースコード上で飛車はRookとなってて、Bonanzaソースコードに慣れ親しんでいるとそのへんはどうってことはないですが、升の表記がわかりにくいのは同感です。思考エンジンをデバッグしているときに、すごく困ります。

merom686 「対局ごとに必要な初期化」はgameover直後にするのがいいと思うのです。最初のisreadyでは「起動時」「対局ごと」両方やるとして。

http://d.hatena.ne.jp/merom686/20111217/1324123889#c

対局ごとに必要な初期化はisreadyでやって全然構いませんけどね。isreadyに数時間かかろうと問題ないようなので。isreadyは対局のたびに送られてきますし、連続対戦であればgameoverのあと即座にisreadyが送られてきますので。

go infinite

次にstopが送られてくる前にbestmoveを返してはいけません。
将棋所:USIプロトコルとは
細かいところだけど、不安になるエンジン作者がいたらいけないので。
勝ちを読み切った場合などは当然bestmoveを返していいはず。

返してはいけません。

例えば、次の問題です。(将棋所付属のLesserKaiのソースコードのコメントより抜粋。)

// 人間対エンジンで、エンジンが予想した手を人間が指すとponderhitが送られ、
// その後、まだエンジンが継続して思考している時に「すぐ指させる」ボタンを押すと、
// stopが送られてくる。このように、ponderhitとstopが続けて送られてきた場合、
// ここでは何も返さず、その次のstopに入ったところで手を返す。

実は、上記のコメントに書かれているようなことはうまく実装できません。

ponderhitが起きたときに、ponderのときに指定されていた思考時間設定のまま思考を継続しているわけで、ちょうど思考が終了してbestmoveを返す瞬間にstopが送られてきたからと言って、「何も返さず」というのが無理な話で、「もう返してしまっている」こともありますし、次のstopでもbestmoveを返すのですが、それがそのstopに呼応するbestmoveを返していることにはならないケースがあります。

たとえば次のようなシーケンスを考えてみましょう。

ホスト側     思考エンジン側
go ponder
ponderhit

※ 思考エンジン側はponderhitして、思考が終了したのでbestmoveを返す
       bestmove XXXX
stop

※ ホスト側には思考エンジン側が返したbestmove XXXXが、stopに呼応したもののように見える。

position XXXX
go
       bestmove XXXX

※ 思考エンジン側はstopコマンドが来たようなのでbestmoveを返すが、これが、ホスト側からすればgoコマンドに対する応手のように見える。

とまあ、bestmoveを返して良いコマンド(ponderhit,stop)を思考エンジン側からの応答を待たずにホスト側が2つ続けて送信してしまうのは、プロトコル上の欠陥であり、シーケンス的に見て破綻しています。

要するにponderhitに続けてstopを送ることがあるというのは仕様上の欠陥だと私は思います。


同様の理由により、"go infinite"で「次にstopが送られてくる前にbestmoveを返して」しまいますと、bestmove返すのと同時にstopが送られてくるとホスト側にはstopに呼応するbestmoveのように見え、次の局面図を思考エンジン側に送信するかも知れないですが、思考エンジン側はそのあとstopコマンドを解釈し、bestmoveを再度返しまして、これがホスト側からすると次の局面図に対するbestmoveのように見えます。

ゆえに、いかなるときであっても「次にstopが送られてくる前にbestmoveを返して」は、いけません。

usinewgame

このコマンドの意味するところがよくわからない。
異なる対局を始めるからハッシュをクリアしたほうがいいとか、
対局が始まったからsetoptionなどのコマンドを受け付ける義務がなくなったとか、
いくつか考えられるが、仕様に明示されていないと使えない。
ぶっちゃけ、なくてもいいコマンドなので、その辺りの説明は欲しい。


対局中に(思考中に)setoptionで置換表サイズを変更されても困りますし、ponder有りから無しに変更されても困ります。対局中に、それらのコマンドが来ないことを保証するために、対局中と非対局中とでは別の状態(決定性有限オートマトン的な意味で)でなくてはなりません。usinewgame〜gameoverはそういう意図だと思います。このへんはまあよく考えてあるなぁとは思うのですが。USIのドキュメントに有限オートマトンで状態遷移図を書いて、この仕様についてもう少し明確にしてあったほうがいいような気は私もします。


以上、ざっとUSIプロトコルについての疑問に答えてみました。思考エンジンを実装する人の参考になれば幸いです。