LoginSignup
2
4

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

Last updated at Posted at 2023-08-16

前回からの続きです。

データ構造

USI プロトルの仕様

USI プロトコルでは SFEN(Shogi Forsyth-Edwards Notation)表記法により局面と指し手の情報をやり取りします。

SFEN 表記法の定義
先手 後手
歩兵 P p
香車 L l
桂馬 N n
銀将 S s
金将 G g
玉将 K k
飛車 R r
角行 B b

盤面を表現するとき,将棋では右上端(1一)を原点にとりますが,SFEN では左上端(9一)から右方向に駒の種類を書いていきます。段の区切りにはスラッシュ記号 / を用います。駒がない空白のマスは連続するマスの数を示します。以下は平手の初期配置の例です。

lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL

指し手を表現するとき,1~9筋はそのまま半角数字の 1~9,一~九段は半角アルファベット小文字の a~h で示します。たとえば7七の駒が7六に移動した場合,7g7f と表記して駒の種類は伝えません。駒が成るときは最後にプラス記号 + を追加します。たとえば8八の駒が2二に移動して成る場合 8h2b+ と表記します。持ち駒を打つときは最初に駒の種類を大文字で書いた後にアスタリスク記号 * を追加し,打った場所を追加します。金を5二に打つ場合は G*5b とします。

評価値バーのデータ構造

SFEN 表記法を参考にして下記のように定めました。分かりづらいですが,成駒は全角文字を使用しています。これは UNICODE では変換が簡単なのと,ソースコード中の表記が簡単になるからです。

評価値バーの駒の定義
先手 後手 先手 後手
歩兵 P p と金
香車 L l 成香
桂馬 N n 成桂
銀将 S s 成銀
金将 G g
玉将 K k
飛車 R r 竜王
角行 B b 竜馬

盤面情報は下記のように char 型の二次元配列で表します。一次元目は手数であり,1~999 手目までの盤面情報を格納します。二次元目は左上を起点とした駒の位置です。また,その手の盤面情報が有効か無効かを表すためのフラグも用意します。

int		  moveCount = 0;					// 手数
char[,]	   pieceMap = new char[1000, 81];	// 駒の配置情報
bool[]	  enableMap = new bool[1000];		// 駒の配置情報の有効フラグ

また指し手の修飾子や駒の位置(相対位置)なども半角アルファベット一文字で表すことにします。

記号 修飾子 位置 記号 修飾子 位置
@ 無印 I 左上 0
A -8 J +1
B 右引 -7 K +2
C 左引 -6 L +3
D -5 M +4
E 右寄 -4 N +5
F 左寄 -3 O +6
G -2 P +7
H 右上 -1 Q +8

移動元の相対位置と修飾子を定義する小駒用のクラスです。たとえば金将の場合,移動元の位置は6通りあり,移動先の位置を基準とした相対位置は以下のようになります。x座標とy座標をまとめて順にアルファベット文字列にして12文字の文字列 IHHIJIHJIJJJ になります。

小駒の相対位置
金種の場合 銀将の場合 桂馬の場合
   0,-1   
-1,0 1,0
-1,1 0,1 1,1
-1,-1    1,-1
     
-1,1 0,1 1,1
     
        
-1,2    1,2
//
// 小駒用のクラス
//
class SmallPiece {
	public	string		Position;	// 移動元と移動先との相対位置
	public	string[]	Select;		// 修飾子
}
//
// 金種(金将・成銀・成桂・成香・と)の場合
//
SmallPiece	goldPiece = new SmallPiece() {
	Position = "IHHIJIHJIJJJ",
	Select = new string[] {
		"@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "@DLLDDFFDDLLDDFFDDLLDDFFDDLLDDFF",
		"@DKKDDKKDDKKDDKKDDEEDDEEDDEEDDEE", "@GGGGGGGLLIILLIILLIILLIILLIILLII",
		"@GGGGGGGJJJJJJJJJJJJJJJJJJJJJJJJ", "@GGGGGGGKKKKHHHHKKKKHHHHKKKKHHHH"
	}
};
//
// 銀将の場合
//
SmallPiece	silverPiece = new SmallPiece() {
	Position = "HHJHHJIJJJ",
	Select = new string[] {
		"@LACALACALACALAC", "@KAKAKAKABABABAB", "@GGGLILILILILILI",
		"@GGGJJJJJJJJJJJJ", "@GGGKKHHKKHHKKHH"
	}
};
//
// 桂馬の場合
//
SmallPiece	knightPiece = new SmallPiece() {
	Position = "HKJK",
	Select = new string[] {
		"@L", "@K",
	}
};

大駒用のクラスです。大駒用のクラスでは相対位置を示すメンバが文字列の配列になりました。小駒と大駒でデータ構造を変えているのは,小駒の場合,相対位置を示す文字列を全探索する必要があるのに対し,大駒の場合,同方向の探索は途中で打ち切ることができるからです。このため大駒用のデータは同じ方向ごとに(つまり探索を途中で打ち切れる単位で)文字列を区切って文字列の配列にしています。たとえば飛車の場合,4方向に最大8マスまで進めるので,方向別に16文字の文字列を4個持ちます。

大駒の相対位置(±2の範囲まで)
飛車の場合 角行の場合
      0,-2      
      0,-1      
-2,0 -1,0 1,0 2,0
      0,1      
      0,2      
-2,-2          2,-2
-1,-1 1,-1
-1,1 1,1
-2,2 2,2
竜王の場合 竜馬の場合
      0,-2      
   -1,-1 0,-1 1,-1   
-2,0 -1,0 1,0 2,0
   -1,1 0,1 1,1   
      0,2      
-2,-2          2,-2
   -1,-1 0,-1 1,-1   
   -1,0 1,0   
   -1,1 0,1 1,1   
-2,2          2,2
//
// 大駒用のクラス
//
class LargePiece {
	public	string[]	Position;	// 移動元と移動先との相対位置
	public	string[]	Select;		// 修飾子
}
//
// 飛車の場合
//
LargePiece	rookPiece = new LargePiece() {
	Position = new string[] {
		"IHIGIFIEIDICIBIA", "HIGIFIEIDICIBIAI", "JIKILIMINIOIPIQI", "IJIKILIMINIOIPIQ"
	},
	Select = new string[] {
		"@AAA", "@DLD", "@DKD", "@GGG"
	}
};
//
// 角行の場合
//
LargePiece	bishopPiece = new LargePiece() {
	Position = new string[] {
		"HHGGFFEEDDCCBBAA", "JHKGLFMENDOCPBQA", "HJGKFLEMDNCOBPAQ", "JJKKLLMMNNOOPPQQ"
	},
	Select = new string[] {
		"@LAA", "@KAA", "@GGL", "@GGK"
	}
};
//
// 竜王の場合
//
LargePiece	dragonPiece = new LargePiece() {
	Position = new string[] {
		"HH", "IHIGIFIEIDICIBIA", "JH", "HIGIFIEIDICIBIAI",
		"JIKILIMINIOIPIQI", "HJ", "IJIKILIMINIOIPIQ", "JJ"
	},
	Select = new string[] {
		"@LLAAAAA", "@KLAAAAA", "@KKAAAAA", "@DDDLDDD",
		"@DDDKDDD", "@GGGGGLL", "@GGGGGKL", "@GGGGGKK",
	}
};
//
// 竜馬の場合
//
LargePiece	horsePiece = new LargePiece() {
	Position = new string[] {
		"HHGGFFEEDDCCBBAA", "IH", "JHKGLFMENDOCPBQA", "HI",
		"JI", "HJGKFLEMDNCOBPAQ", "IJ",	"JJKKLLMMNNOOPPQQ"
	},
	Select = new string[] {
		"@LLAAAAA", "@KLAAAAA", "@KKAAAAA", "@DDDLDDD",
		"@DDDKDDD", "@GGGGGLL", "@GGGGGKL", "@GGGGGKK",
	}
};

評価値バーでの実装

SFEN 表記による盤面情報を解析する処理は下記のようになります。

//
// 駒の配置,手数 moveCount は既に与えられている
//
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;
			}
		}
	}
}

SFEN 表記による指し手を人間の分かり易い文字列に変換する処理は下記のようになります。

//
// 指し手の文字列変換
//
public	string	GetMoveString( string sfen ) {
    // 移動後の駒の座標を得る
	int		x1 = '9' - sfen[2];		// 0~8
	int		y1 = sfen[3] - 'a';		// 0~8
	int		p1 = 9 * y1 + x1;		// 0~80

	string	piece, sel, opt;
	if( sfen[1] == '*' ) {
        // 持ち駒を打つとき
		piece = GetPieceName( sfen[0] );
		sel = "";
		opt = "打";
	} else {
        // 盤面上の駒を移動するとき,移動元の座標を得る
		int		x0 = '9' - sfen[0];	// 0~8
		int		y0 = sfen[1] - 'a';	// 0~8
		int		p0 = 9 * y0 + x0;	// 0~80
		char	c0 = pieceMap[moveCount, p0];
		piece = GetPieceName( c0 );
        // 「引・寄・上・右・左・直」などの修飾子を得る
		sel = GetSelectString( c0, x0, y0, x1, y1 );
        // 駒が成れるかどうか?
		bool	promo = ( "PLNSBR".Contains( c0 ) && ( y1 < 3 || y0 < 3 ) ) ||
						( "plnsbr".Contains( c0 ) && ( y1 > 5 || y0 > 5 ) )
					  ? true : false;
        // 駒が成れるのに成らない場合は不成とする
		opt = ( sfen.Length > 4 && sfen[4] == '+' ) ? "成" : promo ? "不成" : "";
	}
	char	mark = "☖☗"[moveCount % 2];
    // 移動先が前局面の移動先と同じ場合,移動先の駒の種類が異なるはず(空白以外)
	string	pos;
	if( moveCount > 1 && enableMap[moveCount] && 
		pieceMap[moveCount, p1] != ' ' &&
		pieceMap[moveCount, p1] != pieceMap[moveCount - 1, p1] )
	{
		pos = "同";
    } else {
		char	col = "987654321"[x1];
		char	row = "一二三四五六七八九"[y1];
		pos = string.Format( "{0}{1}", col, row );
	}
	return string.Format( "{0}{1}{2}{3}{4}", mark, pos, piece, sel, opt );
}

駒の名前を得る処理は下記のようになります。

//
// 駒の名前を得る
//
string	GetPieceName( char c ) {
	switch( c ) {
	  case 'P':		case 'p':	return "歩";
	  case 'L':		case 'l':	return "香";
	  case 'N':		case 'n':	return "桂";
	  case 'S':		case 's':	return "銀";
 	  case 'G':		case 'g':	return "金";
	  case 'K':		case 'k':	return "玉";
	  case 'B':		case 'b':	return "角";
	  case 'R':		case 'r':	return "飛";
	  case 'P':	case 'p':	return "と";
	  case 'L':	case 'l':	return "成香";
	  case 'N':	case 'n':	return "成桂";
	  case 'S':	case 's':	return "成銀";
	  case 'B':	case 'b':	return "馬";
	  case 'R':	case 'r':	return "竜";
	  default:					return "?";
	}
}

指し手の修飾子を求める処理です。

//
// 修飾子「引・寄・上・左・右・直」及びこれらの組み合わせを返す
//      c:駒の種類
// x0, y0:移動元の座標
// x1, y1:移動先の座標
//
string	GetSelectString( char c, int x0, int y0, int x1, int y1 ) {
	char	d;
	switch( c ) {
	  case 'P': case 'L': case 'K': case 'p': case 'l': case 'k':
					d = '@';													break;
	  case 'N':		d = SelectSmallPiece( c, x0, y0, x1, y1,  1, knightPiece );	break;
	  case 'n':		d = SelectSmallPiece( c, x0, y0, x1, y1, -1, knightPiece );	break;
	  case 'S':		d = SelectSmallPiece( c, x0, y0, x1, y1,  1, silverPiece );	break;
	  case 's':		d = SelectSmallPiece( c, x0, y0, x1, y1, -1, silverPiece );	break;
	  case 'G': case 'P': case 'L': case 'N': case 'S': 
					d = SelectSmallPiece( c, x0, y0, x1, y1,  1, goldPiece   );	break;
	  case 'g': case 'p': case 'l': case 'n': case 's': 
					d = SelectSmallPiece( c, x0, y0, x1, y1, -1, goldPiece   );	break;
	  case 'B':		d = SelectLargePiece( c, x0, y0, x1, y1,  1, bishopPiece );	break;
	  case 'b':		d = SelectLargePiece( c, x0, y0, x1, y1, -1, bishopPiece );	break;
	  case 'R':		d = SelectLargePiece( c, x0, y0, x1, y1,  1, rookPiece   );	break;
	  case 'r':		d = SelectLargePiece( c, x0, y0, x1, y1, -1, rookPiece   );	break;
	  case 'B':	d = SelectLargePiece( c, x0, y0, x1, y1,  1, horsePiece  );	break;
	  case 'b':	d = SelectLargePiece( c, x0, y0, x1, y1, -1, horsePiece  );	break;
	  case 'R':	d = SelectLargePiece( c, x0, y0, x1, y1,  1, dragonPiece );	break;
	  case 'r':	d = SelectLargePiece( c, x0, y0, x1, y1, -1, dragonPiece );	break;
	  default:		d = '@';													break;
	}
	string[]	table = new string[] {
		"", "引", "右引", "左引", "寄", "右寄", "左寄", "上", "右上", "左上", "直", "右", "左"
	};
	return table[d - '@'];
}

小駒の修飾子を求める処理です。

//
// 小駒の修飾子を求める
//     c:駒の種類
// x0,y0:移動元の座標
// x1,y1:移動先の座標
//  turn:手番,先手は +1, 後手は -1
//    sp:小駒用クラス
//
char	SelectSmallPiece( char c, int x0, int y0, int x1, int y1, int turn, SmallPiece sp ) {
	int		sx = ( x0 - x1 ) * turn;
	int		sy = ( y0 - y1 ) * turn;
	int		m = -1;
	int		n = 0;
	for( int i = 0, j = 0, bit = 1; i < sp.Position.Length; i += 2, j++ ) {
		int		dx = sp.Position[i]     - 'I';
		int		dy = sp.Position[i + 1] - 'I';
		if( dx == sx && dy == sy ) {
			m = j;
			continue;
		}
		int		x = x1 + dx * turn;
		int		y = y1 + dy * turn;
		int		p = y * 9 + x;
		if( 0 <= x && x < 9 && 0 <= y && y < 9 && pieceMap[moveCount, p] == c )
			n |= bit;
		bit = bit << 1;
	}
	return sp.Select[m][n];
}

大駒の修飾子を求める処理です。

//
// 大駒の修飾子を求める
//     c:駒の種類
// x0,y0:移動元の座標
// x1,y1:移動先の座標
//  turn:手番,先手は +1, 後手は -1
//    lp:大駒用クラス
//
char	SelectLargePiece( char c, int x0, int y0, int x1, int y1, int turn, LargePiece lp ) {
	int		sx = ( x0 - x1 ) * turn;
	int		sy = ( y0 - y1 ) * turn;
	int		m = -1;
	int		n = 0;
	for( int i = 0, j = 1; i < lp.Position.Length; i++ ) {
		for( int k = 0; k < lp.Position[i].Length; k += 2 ) {
			int		dx = lp.Position[i][k]     - 'I';
			int		dy = lp.Position[i][k + 1] - 'I';
			if( dx == sx && dy == sy ) {
				m = i;
    			goto BREAK;
			}
			int		x = x1 + dx * turn;
			int		y = y1 + dy * turn;
			int		p = y * 9 + x;
			if( x < 0 || x >= 9 || y < 0 || y >= 9 ) break;
			char	d = pieceMap[moveCount, p];
			if( d == c ) {
				n = j;
				break;
			} else if( d != ' ' ) {
				break;
			}
		}
		j++;
BREAK:	;
	}
	return lp.Select[m][n];
}

おまけ

なぜ相対位置をわざわざ文字列,または文字列の配列で保持するようにしたのか?

文字列⇔二次元配列くらいなら大差ないように思えます。

// 文字列の場合
string	Position = "IHHIJIHJIJJJ";

// 二次元配列の場合
int[,]	Position = new int[,] {
	{0,-1},{-1,0},{1,0},{-1,1},{0,1},{1,1}
};

しかし,文字列の配列⇔二次元配列のジャグ配列となると,結構コード量が増えるのが我慢ならなくて・・・C# は多次元配列もジャグ配列もサポートしている素晴らしい言語なんですけどね。

// 文字列の配列の場合
string[] Position = new string[] {
	"HH", "IHIGIFIEIDICIBIA", "JH", "HIGIFIEIDICIBIAI",
	"JIKILIMINIOIPIQI", "HJ", "IJIKILIMINIOIPIQ", "JJ"
};

// 二次元配列のジャグ配列の場合
int[][,] Position = new int[][,] {
	new int[,] {{-1,-1}},
	new int[,] {{ 0,-1},{ 0,-2},{ 0,-3},{ 0,-4},{ 0,-5},{ 0,-6},{ 0,-7},{ 0,-8}},
	new int[,] {{ 1,-1}},
	new int[,] {{-1, 0},{-2, 0},{-3, 0},{-4, 0},{-5, 0},{-6, 0},{-7, 0},{-8, 0}},
	new int[,] {{ 1, 0},{ 2, 0},{ 3, 0},{ 4, 0},{ 5, 0},{ 6, 0},{ 7, 0},{ 8, 0}},
	new int[,] {{-1, 1}},
	new int[,] {{ 0, 1},{ 0, 2},{ 0, 3},{ 0, 4},{ 0, 5},{ 0, 6},{ 0, 7},{ 0, 8}},
	new int[,] {{ 1, 1}}
};

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

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