LoginSignup
1
2

将棋評価値バーを作る(第5回)

Last updated at Posted at 2023-08-17

前回からの続きです。

評価値バーの起動処理

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;
}

長くなり過ぎたので次回に続きます。

1
2
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
1
2