■ データ構造 (1)

● 駒種別

将棋の駒には 歩、香車、桂馬、銀、金、角、飛車、王 の8種類がある。これらには 1〜8の値を割り振るのが無難だ。

王、金以外は相手の陣地に入ると成ることができ、と、成香、成桂、成銀、馬、龍になる。
0x08 を成っているかどうかのフラグにすれば、0x09 〜 0x0f で成り駒を表すことができる。(0x0d は欠番)

相手の駒か自分の駒を区別するには、0x10、0x20 をフラグとすればよい。
あと、盤面の各マスの状態としてはマスに駒が無い状態があるので、これを 0 とするとよい。

以上をまとめると、以下のように駒種別を enum で定義することができる。

//	駒値定義:
enum {
	EMPTY = 0,
	FU = 1,
	KYOU,
	KEI,
	GIN,
	KIN,
	KAKU,
	HISHA,
	OU,
	TO,
	NARI_KYOU,
	NARI_KEI,
	NARI_GIN,
	DUMMY,
	UMA,
	RYUU,
	PROMOTED = TO - FU,
	KOMA_MASK = 0x0f,
	OWN_MASK = 0x10,
	OWN_FU = FU + OWN_MASK,
	OWN_KYOU,
	OWN_KEI,
	OWN_GIN,
	OWN_KIN,
	OWN_KAKU,
	OWN_HISHA,
	OWN_OU,
	OWN_TO,
	OWN_NARI_KYOU,
	OWN_NARI_KEI,
	OWN_NARI_GIN,
	OWN_DUMMY,
	OWN_UMA,
	OWN_RYUU,
	OPPONENT_MASK = 0x20,
	OPP_FU = FU + OPPONENT_MASK,
	OPP_KYOU,
	OPP_KEI,
	OPP_GIN,
	OPP_KIN,
	OPP_KAKU,
	OPP_HISHA,
	OPP_OU,
	OPP_TO,
	OPP_NARI_KYOU,
	OPP_NARI_KEI,
	OPP_NARI_GIN,
	OPP_DUMMY,
	OPP_UMA,
	OPP_RYUU,
	WALL = 0x80		//	壁
};

enum の最後にある WALL は、利きのチェックなどで使用される番人である。 これがあると座標チェックを省略できて、プログラムを高速化できる。

上記の定義では、駒種別に6ビット、壁に1ビットしか使用していないので、駒のためには char で十分である。 (残りの1ビットは将来何かに使うかもしれない)

typedef unsigned char TKoma;

上記のように駒種別を表す型 TKoma を定義しておくとよい。

enum TKoma { ... }; と定義してもよいのだが、C++ では enum のサイズが自然なレジスタサイズ(通常は32ビット)になるので、 メモリ効率が悪くなる可能性があるので、このようにした。

駒種別関連のインライン関数を以下のように定義しておくと便利である。

inline bool isOwnKoma(TKoma k) { return (k & OWN_MASK) != 0; }
inline bool isOppKoma(TKoma k) { return (k & OPPONENT_MASK) != 0; }
inline TKoma toRawKoma(TKoma k) { return k & KOMA_MASK; }
inline TKoma toOwnKoma(TKoma k) { return k | OWN_MASK; }
inline TKoma toOppKoma(TKoma k) { return k | OPPONENT_MASK; }
inline TKoma toPromote(TKoma k) { return k | PROMOTED; }     //  成り駒変換
inline TKoma unPromote(TKoma k) { return k & ~PROMOTED; }    //  成り駒を元の駒に変換

自分相手の駒を示すために2ビット使ったが、それらが同時にONになることはないので、
WALL = OWN_MASK | OPPONENT_MASK
としてもよいのだが、ビットはまだ余っているし isOwnKoma(WALL), isOppKoma(WALL) が false になる方が都合がよいと考えた。

● 盤面座標系

将棋盤は右上が原点で、横方向:1〜9、縦方向:一〜九 の2次元配列で表現できるる。

TKoma koma[9][9];

しかし、2次元配列はアドレス計算に時間がかかるので、1次元配列で表現するのが普通である。 このとき、1行目に番人の壁を配置し、横方向の盤面サイズを 16(16進数で 0x10)にしておくと、アドレス計算が高速になるし、 5二の位置が 0x52 となるのでデバッグ時にわかりやすい。

//	壁を含む盤配列サイズ
#define		BAN_EX_WD	0x10
#define		BAN_EX_HT	(9+2)
#define		BAN_EX_SIZE	(BAN_EX_WD*BAN_EX_HT)
TKoma koma[BAN_EX_SIZE];

盤面位置を示す型を TBanIX で定義し、盤面位置と筋・段(1..9)の変換インライン関数を以下のように定義しておくとよい。

typedef unsigned char TBanIX

//	dan, suji (1..9) 座標 ←→ 盤配列インデックス変換
//	右上: (s = 1, d = 1) 、左上(9, 1)
inline TBanIX sd2banIX(uchar s, uchar d) { return d + s * BAN_EX_WD; }
inline TBanIX ds2banIX(uchar d, uchar s) { return d + s * BAN_EX_WD; }
inline uchar banIX2dan(TBanIX ix) { return ix % BAN_EX_WD; }
inline uchar banIX2suji(TBanIX ix) { return ix / BAN_EX_WD; }
inline int sd2ix(int s, int d) { return d + s * BAN_EX_WD; }
inline int ds2ix(int d, int s) { return d + s * BAN_EX_WD; }

● 着手

前節の画面座標系を用いるとひとつの着手(駒を動かす手、駒を打つ手)は以下のように定義することができる。

struct SMove
{
    TBanIX  m_src;              //  移動元、0 for 駒打ち
    TBanIX  m_dst;              //  移動先
    TKoma   m_cptKoma;          //  取る駒(OPP_FU..OPP_RYUU)、0 ならば無し
    TKoma   m_uchiKoma : 7;     //  打つ駒(FU..HISHA)
    bool    m_nari : 1;         //  成りフラグ
public:
    SMove(TBanIX src, TBanIX dst, TKoma cptKoma, bool nari, TKoma koma=0)
        : m_src(src), m_dst(dst), m_cptKoma(cptKoma), m_nari(nari), m_koma(koma) {};
};

上記のコメントは先手番の場合で、後手番であれば、取る駒は FU..RYUU、打つ駒は OPP_FU..OPP_HISHA となる。

着手情報は配列に格納されるので、4バイト単位の方がメモリ効率がよい。 そこで打つ駒情報を7ビット、成るかどうかの情報を1ビットにして4バイトに収まるようパックしている。
なお、VC6 では先に宣言した方が下位ビットに割り付けられるので 打ち駒 の方を先に宣言し、 よけいなビット演算が生成されないようにしている。

● 利きテーブル等

後で詳しく説明するが、合法着手生成において、自玉に王手がかかっている場合、王手を避ける手を指さなくてはいけない (それが存在しない場合は「詰み」でゲーム終了となる)。 また、王が移動する場合、相手の利きがあるマスには移動することができないし、飛車角の王手に対する合駒(ピンされていると言う)は、 合駒でなくなる位置に移動することはできない。
このように合法着手生成生成においては駒の利きを計算しておく必要があるが、 毎回新規に計算するより着手実行時にインクリメンタルに計算する方が処理速度が速い。