前回からの続きです。
評価値バーの起動処理
C# のプログラムはコンソールアプリと Windows アプリに大別されます。評価値バーというウィンドウを開くのでWindows アプリかと思いきや,標準入出力を介して将棋 GUI や将棋エンジンとやり取りをしなくてはならないので,コンソールアプリとしてコンパイルする必要があります。とはいえ,Windows 内蔵の C# コンパイラのデフォルトはコンソールアプリなので特に何も指定する必要はありません。
評価値バーの起動処理は以下のようになります。コンソールアプリといいつつ,通常の Windows Form アプリと大差ありませんが,メインウィンドウを立ち上げる前に初期設定ファイルの読み込みや,先手・後手プレイヤー名の取得を行います。
static public string enginePath = "Suisho5.exe"; // 将棋エンジンのパス名
static public string engineName = "水匠5"; // 将棋エンジン名
static private string configFile = "settings.ini"; // 初期設定ファイル名
[STAThread]
static int Main( string[] args ) {
Application.EnableVisualStyles(); // Visual Style
LoadConfigFile(); // 初期設定ファイルの読み込み
if( !File.Exists( enginePath ) ) { // 将棋エンジンの存在チェック
MessageBox.Show( string.Format( "{0} が存在しません!!", enginePath ),
"評価値バー", MessageBoxButtons.OK, MessageBoxIcon.Error );
return( -1 );
}
GetPlayersName(); // 先手・後手プレイヤー名の取得
MainWindow mainWindow = new MainWindow(); // メインウィンドウを立ち上げる
mainWindow.StartEngine(); // 将棋エンジンの起動
Application.Run( mainWindow ); // メインウィンドウの表示
return( 0 );
}
評価値バーの実行ファイルと同じフォルダに初期設定ファイルが存在すれば,これを読み出します。
//--------------------------------------------------------------------------
// 初期設定ファイルの読み込み
// ・行頭のシャープ記号(#)はコメント行とする。
// ・文字エンコードは OS デフォルト値とする。おそらく Shift-JIS になる。
// ・ただし BOM がある場合はソチラを優先する。
//--------------------------------------------------------------------------
private static void LoadConfigFile() {
string filename = Application.StartupPath + "\\" + configFile;
if( !File.Exists( filename ) ) return;
StreamReader reader = new StreamReader( filename, Encoding.Default, true );
while( !reader.EndOfStream ) {
string s = reader.ReadLine();
if( s == "" || s[0] == '#' ) continue;
string[] a = s.Split('=');
string key = a[0].Trim();
string val = a[1].Trim();
switch( key ) {
case "EngineName": engineName = val; break;
case "EnginePath": enginePath = val; break;
}
}
reader.Close();
}
初期設定ファイルの内容はこんな感じです。
EngineName=水匠5
EnginePath=C:\SHOGI\SUISHO5\YaneuraOu_NNUE-tournament-clang++-sse42.exe
将棋エンジンの起動処理
将棋エンジンの起動処理を以下に示します。将棋 GUI と将棋エンジンの間では標準入出力を介したテキストで情報のやり取りを行いますが,これらの送受信を非同期で処理できなくてはならないのでマルチタスク,あるいはマルチスレッドで処理する必要があり,将棋エンジンの出力を非同期で読み出すイベントハンドラ,将棋エンジンの終了時に呼び出されるイベントハンドラを登録します。また将棋 GUI の出力を読み出すタスクを起動します。
Process process = new Process(); // 将棋エンジンのプロセス
//--------------------------------------------------------------------------
// 将棋エンジン起動
//--------------------------------------------------------------------------
void StartEngine() {
string path = Path.GetFullPath( EvalBar.enginePath );
process.StartInfo.FileName = path;
process.StartInfo.WorkingDirectory = Path.GetDirectoryName( path );
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.EnableRaisingEvents = true;
process.OutputDataReceived += EventDataReceived; // 将棋エンジンの出力を非同期で読み出す
process.Exited += EventProcessExited; // 将棋エンジンの終了時に呼び出される
process.Start();
process.BeginOutputReadLine();
Task.Run( () => TaskReadInput() ); // 将棋 GUI の出力を読み出すタスク
}
将棋エンジンの出力を読み出すイベントハンドラは以下のようになります。このハンドラは将棋エンジンが出力したときに呼び出され,将棋エンジンの出力した文字列を読み出し,そのまま将棋GUIに引き渡しますが,このうちUSIプロトコルのinfoコマンドを解析して評価値や最善手などの情報を抽出します。
//--------------------------------------------------------------------------
// 将棋エンジンの標準出力読み出しイベント
//--------------------------------------------------------------------------
void EventDataReceived( object sender, DataReceivedEventArgs e ) {
string line = e.Data; // 将棋エンジンの出力を読み出す
ParseUSIInfo( line ); // USI プロトコルの info コマンドの解析
Console.Out.WriteLine( line ); // 将棋 GUI に引き渡す
}
一方,将棋 GUI の出力を読み出すタスクは以下のようになります。こちらはループ処理になっており,将棋 GUI の出力を読み出すと,そのまま将棋エンジンに引き渡しますが,このうち USI プロトコルの position コマンドを解析して盤面情報を得ます。将棋エンジンの終了コマンドを受け取るまでループを繰り返します。終了コマンドを受け取るループを抜け出し,これ以降の読み込みをキャンセルします。
//--------------------------------------------------------------------------
// 評価値バーの標準入力の読み出しタスク
//--------------------------------------------------------------------------
void TaskReadInput() {
for(;;) {
string line = Console.In.ReadLine(); // 将棋 GUI の出力を読み出す
ParseUSIPosition( line ); // USI プロトコルの position コマンドの解析
process.StandardInput.WriteLine( line ); // 将棋エンジンに引き渡す
process.StandardInput.Flush();
if( line == "quit" ) break; // 終了コマンドを受け取るとループを抜ける
}
process.CancelOutputRead(); // 読み出しのキャンセル
}
将棋エンジンの終了時に呼び出されるイベントハンドラは以下のようになります。子プロセスである将棋エンジンが終了したら,親プロセスである自身も終了します。
//--------------------------------------------------------------------------
// 将棋エンジンの終了イベント
//--------------------------------------------------------------------------
void EventProcessExited( object sender, EventArgs e ) {
this.Close();
}
USI プロトコルの解析
info コマンドの解析
将棋エンジンから送られてくる info コマンドの解析です。最後に Invalidate() を実行してペイントイベント(ウィンドウ描画)を呼び出します。ここで呼び出している GetMoveString() は指し手を人間の分かり易い表現に変換する処理ですが,この開発に多大な苦労を要しました。
//--------------------------------------------------------------------------
// 最善手,評価値などの情報
//--------------------------------------------------------------------------
string moveBest = ""; // 最善手
long searchNodes = 0; // 探索局面数
int searchDepth = 0; // 探索深さ
int evalValue = 0; // 評価値
int mateCount = 0; // 詰み手数
//--------------------------------------------------------------------------
// USI info コマンドの解析
//--------------------------------------------------------------------------
void ParseUSIInfo( string line ) {
if( !line.Include( "info" ) ) return;
string[] a = line.Split(' ');
string multipv = "", pv = "", depth = "", nodes = "", cp = "", mate = "";
for( int i = 1; i < a.Length; i++ ) {
switch( a[i] ) {
case "multipv": multipv = a[++i]; break;
case "pv": pv = a[++i]; break;
case "depth": depth = a[++i]; break;
case "nodes": nodes = a[++i]; break;
case "cp": cp = a[++i]; break;
case "mate": mate = a[++i]; break;
default: break;
}
}
if( multipv != "" && multipv != "1" ) return;
if( pv != "" ) moveBest = GetMoveString( pv );
if( depth != "" ) searchDepth = int.Parse( depth );
if( nodes != "" ) searchNodes = long.Parse( nodes );
if( cp != "" ) evalValue = int.Parse( cp );
if( mate != "" ) mateCount = int.Parse( mate ); else mateCount = 0;
Invalidate();
}
position コマンドの解析
一方,将棋 GUI から送られてくる position コマンドの解析です。持ち駒の情報は評価値バーには必要ないので廃棄し,現在の手数,手番,盤面内の情報のみ格納します。将棋では指し手を「同歩」とか「同飛車」と表現するときがありますが,このような表現をするためには直前の指し手を記憶しておく必要があるため,駒の配置情報は 999 手まで格納できるようにしています。
//--------------------------------------------------------------------------
// 指し手情報
//--------------------------------------------------------------------------
int moveCount = 0; // 手数
bool turnFlag = true; // 手番(先手:true,後手:false)
char[,] pieceMap = new char[1000, 81]; // 駒の配置情報
bool[] enableMap = new bool[1000]; // 駒の配置情報の有効フラグ
//--------------------------------------------------------------------------
// USI position コマンドの解析
//--------------------------------------------------------------------------
void ParseUSIPosition( string line ) {
if( !line.Include( "position" ) ) return;
string[] a = line.Split(' ');
int n;
if( a[1] == "startpos" ) {
turnFlag = true;
moveCount = 1;
SetPosition( "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL" );
n = 2;
} else if( a[1] == "sfen" ) {
turnFlag = ( a[3] == "b" ) ? true : false;
moveCount = int.Parse( a[5] );
SetPosition( a[2] );
n = 6;
} else {
return;
}
if( n >= a.Length || a[n] != "moves" ) return;
for( int i = n + 1, j = 0; i < a.Length; i++, j++ ) {
turnFlag = !turnFlag;
MovePiece( a[i] );
}
}
こちらは,たとえば lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL のように一括して盤面情報を初期化するコマンドの処理です。
//--------------------------------------------------------------------------
// 駒の配置
//--------------------------------------------------------------------------
void SetPosition( string sfen ) {
enableMap[moveCount] = true;
for( int i = 0; i < 81; i++ ) pieceMap[moveCount, i] = ' ';
string[] a = sfen.Split('/');
for( int i = 0, k = 0; i < a.Length; i++ ) {
string s = a[i];
for( int j = 0; j < s.Length; j++ ) {
char c = s[j];
if( '1' <= c && c <= '9' ) {
k += c - '0';
} else if( c == '+' ) {
c = s[++j];
pieceMap[moveCount, k++] = (char)( c - 'A' + 'A' );
} else {
pieceMap[moveCount, k++] = c;
}
}
}
}
こちらは,たとえば 7g7f のように盤面を一手進めるコマンドの処理です。
//--------------------------------------------------------------------------
// 駒の移動
//--------------------------------------------------------------------------
void MovePiece( string sfen ) {
moveCount++;
enableMap[moveCount] = true;
for( int i = 0; i < 81; i++ ) pieceMap[moveCount, i] = pieceMap[moveCount - 1, i];
char c;
if( sfen[1] == '*' ) {
c = sfen[0];
if( moveCount % 2 != 0 ) c = (char)( c - 'A' + 'a' );
} else {
int x0 = '9' - sfen[0]; // 0~8
int y0 = sfen[1] - 'a'; // 0~8
int p0 = 9 * y0 + x0; // 0~80
c = pieceMap[moveCount, p0];
pieceMap[moveCount, p0] = ' ';
if( sfen.Length > 4 && sfen[4] == '+' ) c = (char)( c - 'A' + 'A' );
}
int x1 = '9' - sfen[2]; // 0~8
int y1 = sfen[3] - 'a'; // 0~8
int p1 = 9 * y1 + x1; // 0~80
pieceMap[moveCount, p1] = c;
}
長くなり過ぎたので次回に続きます。