LoginSignup
3
6

More than 3 years have passed since last update.

将棋エンジンを作ってみる【ルール実装編(2)】USIプロトコル

Last updated at Posted at 2021-02-13

将棋エンジンを解説する上で避けては通れない仕組みが「USIプロトコル」である。
この仕組みについては紹介している資料がいくつかはあるので、将棋エンジンに触れたことがある方の中には知っている読者も多いと思う。
そういった読者は本ページを読み飛ばしてもらっても構わないと思う。
(読み飛ばす先があれば、の話だが)

USIプロトコルとは

一言でいうと「将棋エンジンと会話するための言語」である。
対局だけでなく、各種設定や思考中に情報をやりとりするための「言葉」もある。

将棋エンジンを動かすソフトウェアとして有名(と私は思っている)「将棋所」や「ShogiGUI」は、
ユーザの操作(対局開始ボタンクリック、駒を動かす、など)に応じてこれらの「言葉」に翻訳してエンジンに渡す、またはエンジンから受け取った「言葉」を翻訳してソフトウェア上に表示しているわけである。

USIプロトコルにおける決まりごとは以下のページにまとめられており、将棋エンジンはこの決まりに従って作られている必要がある。
USIプロトコル原案

原案は英語でとっつきにくいという方もいると思う。
将棋所の開発者さんが日本語でわかりやすく解説しているので、そちらも紹介しておく。
将棋所さんのUSIプロトコル解説

では、USIプロトコルの中で使われる「言葉」について、
・将棋エンジン設定
・対局準備
・対局
の3段階にわけて重要なものをピックアップして見ていく。
USIプロトコルにおける「言葉」は「コマンド」と言うので、今後はこの表現を使っていく。

将棋エンジン設定

usiコマンド

エンジン使用者から将棋エンジンに使用開始したことを知らせるコマンド。よって、将棋エンジン起動後最初に送る必要がある。
将棋エンジンに「usi」と送るとUSIプロトコルの決まりで、後述するidコマンド(必須)、optionコマンド(こっちは必要に応じて)、最後に設定完了を示すusiokコマンド(必須)が将棋エンジンから送られる。

idコマンド

将棋エンジンからエンジン使用者に送られるコマンドで、エンジン使用者にエンジンの名称と作者を通知するためのコマンド。
「id name (エンジン名)」または「id author (作者)」の形式で将棋エンジンから送られてくる。

optionコマンド

将棋エンジンからエンジン使用者に各種設定を通知するコマンド。
「option name <id> (設定値)」の形式で将棋エンジンから送られてくる。<id>にも種類があるがここではスルーする。

usiokコマンド

将棋エンジンからエンジン使用者に全ての設定が完了したことを通知するコマンド。
エンジン使用者がusiコマンドを実行したあと、何回かのidコマンドおよびoptionコマンドの通知を受け取ったあと、「usiok」と送られたら将棋エンジン設定は完了である。

ここまでまとめ

エンジン使用者が将棋エンジンを起動してから設定完了するまでに使用するコマンドを紹介した。
ここまでの流れを図にまとめてみた。

また、やねうら王さんエンジンを使って設定完了するまでのやりとりを以下に示す。
ここまで文字ばかりだったので、この方がイメージがしやすいと思う。

一番上の行の「usi」と入力した結果、
やねうら王さんから2つのidコマンド(エンジン名と作者)といくつかのoptionコマンドが送られてくる。
エンジン使用者側は↑これらの情報を必要に応じて処理する。
最後にやねうら王さんからusiokコマンドが送られてくるので設定完了となる。

対局準備

isreadyコマンド

エンジン使用者から将棋エンジンに対局が始まることを通知するコマンド。
将棋エンジンはこのコマンドを受け取ったら、対局開始のための準備を行う。

readyokコマンド

isreadyコマンドを受け取った将棋エンジンの対局準備が完了したことを示すコマンド。
将棋エンジン内で、盤面の情報などが必要なく時間がかかる処理などはこのコマンドを実行する前に済ませておくのがよいとされている。

ここまでまとめ

エンジン使用者が将棋エンジンを設定完了してから対局準備完了するまでに使用するコマンドを紹介した。
ここまでの流れを図にまとめてみた。

やねうら王さんエンジンを使って対局準備完了するまでのやりとりを以下に示す。

エンジン使用者が「isready」と入力した結果、
infoコマンドを1つ返したあと、対局準備完了を示すreadyokコマンドが返ってきている
(ここでのinfoコマンドについてはデバッグ情報に近いと思うので解説は割愛する)。
これで対局準備は完了である。

対局

usinewgameコマンド

エンジン使用者が将棋エンジンに対して、対局開始を通知するコマンド。
isreadyコマンドの役割と変わらないと思ったが、やねうら王さんのコメントによると、
このコマンドが送られたら、対局終了までsetoptionコマンドなどを送ってはならないことを宣言しているらしい。

positionコマンド

エンジン使用者が将棋エンジンに局面の情報を通知するためのコマンド。
「position sfen (局面のSFEN文字列) moves (指し手のSFEN文字列1) (指し手のSFEN文字列2) (・・・)」の形式で将棋エンジンに通知する。
指し手がない場合はmovesは不要。
また、平手初期鏡面に限り、「sfen (局面のSFEN文字列)」を「startpos」と置き換えることもできる。

「SFEN文字列」ってなに?
と言われそうだが、これも将棋エンジンにおいて重要な仕組みなので別ページで解説するつもりである。
ここでは「将棋エンジン(コンピュータ)に局面および指し手を伝えるための文字列」と認識してくれればよい。

例えば「初期局面から2六歩」と指した以下の局面を将棋エンジンに通知したい場合を考える。

この局面を将棋エンジンに通知する方法は2通り考えられる。(他にも実現する方法はあるが、あえてそうする必要はないだろう)

  1. 「position startpos moves (2六歩のSFEN文字列)」を将棋エンジンに送る。
  2. 「position sfen (2六歩と指した局面のSFEN文字列)」を将棋エンジンに送る。

どちらを送っても構わない。
有名な将棋ソフトウェアの「ShogiGUI」は1の方式を取っていることを確認したことがある。
理由の仔細は不明だが、待ったの機能が実装しやすいといったメリットを取ったのだろうと考えている。

goコマンド

エンジン使用者から将棋エンジンに指し手を要求するコマンド。
将棋エンジン側で思考が完了したらbestmoveコマンドで指し手を返してくる。
なお、対局によっては持ち時間があるので、なんらかの方法で将棋エンジンに通知する必要があるはず。
だが、現状その方法はわかっていない。

bestmoveコマンド

将棋エンジンが選択した指し手をエンジン使用者に通知するコマンド。
「bestmove (指し手のSFEN文字列)」の形式で通知される。

上記のUSIプロトコル原案には記載がないようだが、
将棋エンジンが投了を選択した場合は「bestmove resign」を通知するのが通例となっているようだ。
もう1つ、将棋エンジンが入玉宣言を選択した場合は「bestmove win」を通知する。

gameoverコマンド

これもUSIプロトコル原案には記載がないようだが、エンジン使用者から将棋エンジンに対して対局終了を通知するコマンド。
勝敗情報(引き分け含む)を渡すこともできる。
将棋エンジンはこのコマンドを受け取ったら対局待ち状態となる。
その後、エンジン使用者からisreadyコマンドとusinewgameコマンドを受け取ると再度対局開始となる。

ここまでまとめ

エンジン使用者が将棋エンジンとの対局を完了するまでのコマンドを紹介した。
ここまでの流れを図にまとめた。

USIプロトコルの実装

例によってここからはソフトウェア開発の基本的な知見のある読者を想定する。
それに関連してここまであえて触れてこなかったが、
エンジン使用者と将棋エンジンの通信はすべて標準入出力を用いて行われる。
printf関数、scanf関数やcout、cinを用いる、と書いたほうがピンと来るだろうか。

よって、世の中の将棋エンジンを用いる将棋ソフトウェアは自身の標準入力、標準出力をそれぞれ、
将棋エンジンの標準出力、標準入力にリダイレクトしているわけである。
この辺りは意味がわからない読者はスルーしてもらって構わない。
将棋ソフトウェア(将棋アプリ)も別途開発しているので、そっちの記事で解説したいと思っている。

クラス定義

usi.h
#pragma once

#include <string>

const std::string ENGINE_NAME    = "shogi_neuron";
const std::string ENGINE_AUTHOR  = "samakusa";

エンジン名と作者を定義する。
もちろんこれを写経する人は独自に名前を定義してもらって構わない。
エンジン名は私が開発中の将棋アプリにちなんでいる。

usi.h
class USI {
    public:
        USI();
        void Loop();

    private:
        enum ID { ID_NAME, ID_AUTHOR, ID_NB };
        void SendId(ID id);
        void SendUsiok();
        void SendReadyOk();
        void SendBestmove();
};

こっちがクラス定義のメインディッシュ。
詳細は実装側で説明する。

コマンド受け付け処理

クラス実装のコードは以下。
例によって長いので、分割して解説していく。
全体のコードはここを参照されたし。

コンストラクタは説明不要だろう。
コマンド受け付け処理が以下となる(すでに長いがガマンしてほしい)。
このページで紹介したコマンドで対応する必要があるのは以下である。
要件は以下となる。
・usiコマンド受け取り:idコマンドを送ってエンジン名と作者を通知する。
・isreadyコマンド受け取り:対局開始のための準備を行い(ここでは何もしない)、readyokコマンドを送る。
・usinewgameコマンド受け取り:対局開始までsetoptionコマンドを送ってはならない。何もしない。
・positionコマンド:受け取った局面に応じて必要な処理を行う(ここでは何もしない)。
・goコマンド:最善手を返す(ここでは局面に関係なく、即投了を返してみる)。
・その他:何もしなくてもいいが、対応していないコマンドであることがわかるようにした。

usi.cpp
#include "usi.h"

#include <iostream>
#include <string>
#include <cassert>

#include "string_ex.h"

USI::USI() {}

void USI::Loop() {
    StringEx cmd;

    while (std::getline(std::cin, cmd)) {
        std::vector<std::string> cmds = cmd.Split();

        if (cmds[0] == "quit") break;

        else if (cmds[0] == "usi") {
            SendId(ID_NAME);
            SendId(ID_AUTHOR);
            SendUsiok();
        }

        else if (cmds[0] == "isready")
            SendReadyOk();

        else if (cmds[0] == "usinewgame")
            continue;

        else if (cmds[0] == "position")
            continue;

        else if (cmds[0] == "go")
            SendBestmove();

        else std::cout << "invalid comannd: " + cmd << std::endl;
    }
}

(文字列の分割について)

1つだけ、↓この処理について。
std::vector<std::string> cmds = cmd.Split();
コマンドは半角スペース区切りで送られ、最初の単語でコマンドの種類を判別する必要がある。
簡単なのは受け取ったコマンドを半角スペース区切ってしまう方法なのだが、
宗教の問題なのか、std::stringクラスには指定した文字で文字列を区切る処理が実装されていない(例えばPythonのstrクラスには実装されている、それくらい汎用性のある処理だ)。
なので、私の方でstd::stringクラスを拡張したStringExクラスを定義しSplitメソッドを実装した。
この実装は将棋エンジンとは関係ないのと、地味に重要な要素があるので別記事にまとめることにした。

2021/02/15追記:まとめた。
https://qiita.com/samakusa/items/d6f0bf48a3f1e72edae1

写経のために実装を参照したい方は先程のリンクからアクセスできるので、そちらをを参照されたし。

各種コマンド送信処理

コマンド送信処理は特に難しいことはないと思う。
idコマンドだけ引数を取るようにしてエンジン名か作者、どちらを送るか選択するようにしている。

usi.cpp
void USI::SendId(ID id) {
    if (id == ID_NAME)
        std::cout << "id name " + ENGINE_NAME << std::endl;
    else if (id == ID_AUTHOR)
        std::cout << "id author " + ENGINE_AUTHOR << std::endl;
    else
        assert(false);
}

void USI::SendUsiok() {
    std::cout << "usiok" << std::endl;
}

void USI::SendReadyOk() {
    std::cout << "readyok" << std::endl;
}

void USI::SendBestmove() {
    std::cout << "bestmove resign" << std::endl;
}

単体試験

例によって本来のmain関数は別で定義したいので、プリプロセッサ定義でUSIクラスの単体試験時のみビルドするようにする。
処理については難しいことはないだろう。

usi.cpp
#ifdef UNIT_TEST_USI
int main(int argc, char **argv) {
    USI usi = USI();
    usi.Loop();

    return 0;
}
#endif // UNIT_TEST_USI

実行結果は以下になると思う。
★の行は自分で入力する。

実行結果
usi ★
id name shogi_neuron
id author samakusa
usiok
isready ★
readyok
usinewgame ★
position startpos ★
go
bestmove resign ★
quit ★

ShogiGUIで動作確認

最後にShogiGUIにエンジン登録して対局開始後即投了するかを確認する。
ShogiGUIにおけるエンジン登録や設定の詳細な手順の解説はここではしないことにする。
ビルドした将棋エンジンをShogiGUIに追加すると↓の画面が表示される。
「USI_Hash」や「USI_Ponder」の設定ができるが、よく意味はわかっていない。

上記画面でOKをクリックすると、エンジン一覧に実装した将棋エンジンが追加されたことがわかる。

実際に対局(?)してみようと思う。
対局設定画面で後手を実装したエンジンに設定してみる。

初手で▲2六歩と指すとすぐに投了したので、想定通り実装できているようだ。

これで将棋エンジンを対局させる上でのUSIプロトコルの最低限のことは確認できた。
それと同時に将棋エンジンの骨組みをつくることができた。
今後、このエンジンに将棋のルールの実装していく(予定)。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6