第II部 局面管理と合法手生成
第5章 攻撃テーブルと利き計算
前章で局面データの管理方法を学びました。本章では、そのデータを使って 「ある駒はどのマスに利いているか」「あるマスにはどの駒が利いているか」を 高速に求める仕組みを解説します。 利き計算は合法手生成、王手判定、ピン計算など、エンジンのあらゆる場面で使われる基盤技術です。
5.1 Tables 構造体の全体像
Hayanagi は起動時に事前計算テーブルを構築し、以降はそれを参照するだけで利き計算を行います。
テーブルは匿名名前空間内の Tables 構造体にまとめられています。
struct Tables {
Bitboard all_squares;
// 単一マスビットボード
std::array<Bitboard, kSquareCount> square_bb{};
// 筋ビットボード
std::array<Bitboard, kBoardSize> file_bb{};
// ステップ型駒の利き(色別×駒種別×マス)
std::array<std::array<std::array<Bitboard, kSquareCount>, 15>, 2> step_from{};
std::array<std::array<std::array<Bitboard, kSquareCount>, 15>, 2> step_to{};
// レイ(8方向の直線)
std::array<std::array<Bitboard, 8>, kSquareCount> rays{};
std::array<std::array<std::array<int, 8>, 8>, kSquareCount> ray_squares{};
std::array<std::array<int, 8>, kSquareCount> ray_lengths{};
// 2マス間のマス集合
std::array<std::array<Bitboard, kSquareCount>, kSquareCount> between{};
};
テーブルのインスタンスは static ローカル変数で保持され、初回アクセス時に一度だけ構築されます。
const Tables& tables() {
static const Tables instance = build_tables();
return instance;
}
5.2 build_tables() による事前計算
build_tables() は以下の順序でテーブルを構築します:
square_bbとfile_bbの初期化- 8方向のレイ(
rays/ray_squares/ray_lengths)の構築 betweenテーブルの構築- ステップ型駒の利きテーブル(
step_from/step_to)の構築
5.3 square_bb[81] — 単一マスビットボード
square_bb[sq] はマス sq のビットだけが立ったビットボードです。
集合演算でマスを追加・テストする際に使います。
for (int square = 0; square < kSquareCount; ++square) {
tables.square_bb[square].set(square);
tables.all_squares.set(square);
tables.file_bb[square_col(square)].set(square);
}
同じループで all_squares(全81マスのビットボード)と file_bb も初期化しています。
5.4 file_bb[9] — 筋ビットボード
file_bb[col] は指定した筋(col = 0..8)に属する全9マスのビットボードです。
主に二歩判定で使われます。歩の駒打ち時に、
打とうとしている筋に既に自分の歩があるかをビットボードの AND で瞬時に判定できます。
// 二歩判定の例(add_drop_moves 内)
if (piece == PieceType::Pawn &&
(piece_bb_[color][static_cast<int>(PieceType::Pawn)] & t.file_bb[file]).any()) {
targets &= ~t.file_bb[file]; // その筋を候補から除外
}
5.5 step_from / step_to — ステップ型駒の利きテーブル
将棋の駒は大きく2種類に分かれます:
- ステップ型 — 1マス(または桂馬のように決まったオフセット)だけ動く駒。歩、桂、銀、金、玉、全成駒。
- スライダー型 — 盤面の端か他の駒にぶつかるまで直線的に進む駒。香、角、飛。馬・龍はスライダー+ステップの複合。
ステップ型の利きは盤上の駒配置に依存しないため、完全に事前計算できます。
step_from と step_to の違い
step_from[color][type][sq]— マスsqにいる駒が利いているマスの集合step_to[color][type][sq]— マスsqに利いている駒がいうるマスの集合(逆引き)
step_to は「あるマスに利いている駒を探す」attackers_to() で使います。
step_from は「ある駒が動ける先を求める」attacks_from() で使います。
テーブル構築の実装
void add_step_attack(Tables& tables, Color color, PieceType type,
int square, int row_delta, int col_delta) {
const int sign = orientation(color); // Black=+1, White=-1
const int row = square_row(square) + row_delta * sign;
const int col = square_col(square) + col_delta * sign;
if (!is_on_board(row, col)) return;
const int to = make_square(row, col);
tables.step_from[static_cast<int>(color)]
[static_cast<int>(type)][square].set(to);
tables.step_to[static_cast<int>(color)]
[static_cast<int>(type)][to].set(square);
}
orientation() は先手なら +1、後手なら -1 を返します。
これにより、歩の「前方」が先手では北(row が減る方向)、後手では南(row が増える方向)になります。
1つの関数呼び出しで step_from と step_to の両方が同時にセットされる点が効率的です。
各駒種の移動パターン
static constexpr int kPawn[][2] = {{-1, 0}};
static constexpr int kKnight[][2] = {{-2, -1}, {-2, 1}};
static constexpr int kSilver[][2] = {{-1, -1}, {-1, 0}, {-1, 1},
{1, -1}, {1, 1}};
static constexpr int kGold[][2] = {{-1, -1}, {-1, 0}, {-1, 1},
{0, -1}, {0, 1}, {1, 0}};
これらのデルタ値は先手基準で定義され、orientation() の符号で後手の方向に反転されます。
成駒(と金・成香・成桂・成銀)はすべて金と同じ動きなので、同じデルタテーブルを共有します。
for (const auto& delta : kGold) {
add_step_attack(tables, color, PieceType::Gold, square, delta[0], delta[1]);
add_step_attack(tables, color, PieceType::ProPawn, square, delta[0], delta[1]);
add_step_attack(tables, color, PieceType::ProLance, square, delta[0], delta[1]);
add_step_attack(tables, color, PieceType::ProKnight, square, delta[0], delta[1]);
add_step_attack(tables, color, PieceType::ProSilver, square, delta[0], delta[1]);
}
玉は8方向すべてに1マス動けます。馬は角のスライダー利きに加えて十字方向のステップ利きを持ち、 龍は飛のスライダー利きに加えて斜め方向のステップ利きを持ちます。
// 玉: 8方向すべて
for (const auto& delta : kStepDirections) {
add_step_attack(tables, color, PieceType::King, square, delta[0], delta[1]);
}
// 馬: 十字方向のステップ(斜めスライダーは別途計算)
for (const auto& delta : kRookStep) {
add_step_attack(tables, color, PieceType::Horse, square, delta[0], delta[1]);
}
// 龍: 斜め方向のステップ(十字スライダーは別途計算)
for (const auto& delta : kBishopStep) {
add_step_attack(tables, color, PieceType::Dragon, square, delta[0], delta[1]);
}
5.6 rays / ray_squares / ray_lengths — レイ
スライダー型駒の利きを計算するためには、各マスから8方向に伸びる「レイ(光線)」の情報が必要です。
// 8方向の定義
enum Direction : int {
North = 0, South = 1, West = 2, East = 3,
NorthWest = 4, NorthEast = 5, SouthWest = 6, SouthEast = 7,
};
constexpr std::array<int, 8> kDirRow = {-1, 1, 0, 0, -1, -1, 1, 1};
constexpr std::array<int, 8> kDirCol = {0, 0, -1, 1, -1, 1, -1, 1};
3つの配列の役割:
rays[sq][dir]— マスsqから方向dirに伸びる全マスのビットボードray_squares[sq][dir][i]— 同じレイの i 番目のマス番号(近い順)ray_lengths[sq][dir]— レイ上のマス数
for (int square = 0; square < kSquareCount; ++square) {
const int row = square_row(square);
const int col = square_col(square);
for (int dir = 0; dir < 8; ++dir) {
int next_row = row + kDirRow[dir];
int next_col = col + kDirCol[dir];
int length = 0;
while (is_on_board(next_row, next_col)) {
const int to = make_square(next_row, next_col);
tables.rays[square][dir].set(to);
tables.ray_squares[square][dir][length++] = to;
next_row += kDirRow[dir];
next_col += kDirCol[dir];
}
tables.ray_lengths[square][dir] = length;
}
}
下図は 5五(square=40)からの各方向レイの概念図です:
9 8 7 6 5 4 3 2 1
+---+---+---+---+---+---+---+---+---+
a | . | . | . | . | N | . | . | . | NE| ← North, NorthEast
+---+---+---+---+---+---+---+---+---+
b | . | . | . | . | N | . | . | NE| . |
+---+---+---+---+---+---+---+---+---+
c | . | . | . | . | N | . | NE| . | . |
+---+---+---+---+---+---+---+---+---+
d | . | . | . | . | N | NE| . | . | . |
+---+---+---+---+---+---+---+---+---+
e | W | W | W | W |[*]| E | E | E | E | ← West, East
+---+---+---+---+---+---+---+---+---+
f | . | . | . | . | S | SW| . | . | . |
+---+---+---+---+---+---+---+---+---+
g | . | . | . | . | S | . | SW| . | . |
+---+---+---+---+---+---+---+---+---+
h | . | . | . | . | S | . | . | SW| . |
+---+---+---+---+---+---+---+---+---+
i | . | . | . | . | S | . | . | . | SW| ← South, SouthWest
+---+---+---+---+---+---+---+---+---+
[*] = 起点マス(5五) N=North E=East SW=SouthWest ...
ray_squares は起点に近い順にマスを格納します。
これはスライダー利き計算で「最初にぶつかる駒」を見つけるために重要です。
5.7 between テーブル — 2マス間のマス集合
between[from][to] は、マス from と to の間にある全マスのビットボードです
(from と to 自身は含みません)。
同一レイ上にないマスの組み合わせでは空のビットボードとなります。
for (int from = 0; from < kSquareCount; ++from) {
for (int dir = 0; dir < 8; ++dir) {
Bitboard between;
for (int index = 0; index < tables.ray_lengths[from][dir]; ++index) {
const int to = tables.ray_squares[from][dir][index];
tables.between[from][to] = between; // to より手前のマス集合
between |= tables.square_bb[to]; // to を追加して次へ
}
}
}
構築アルゴリズムは巧みです。レイに沿ってマスを順番に訪問し、
各マスに到達した時点での累積ビットボード(そのマス自身を含まない)を between テーブルに記録します。
下図は between の例です:
between[1a][1e] (1筋のaマスとeマスの間)
9 8 7 6 5 4 3 2 1
+---+---+---+---+---+---+---+---+---+
a | . | . | . | . | . | . | . | . | | ← from (含まない)
+---+---+---+---+---+---+---+---+---+
b | . | . | . | . | . | . | . | . | * | ← between に含まれる
+---+---+---+---+---+---+---+---+---+
c | . | . | . | . | . | . | . | . | * | ← between に含まれる
+---+---+---+---+---+---+---+---+---+
d | . | . | . | . | . | . | . | . | * | ← between に含まれる
+---+---+---+---+---+---+---+---+---+
e | . | . | . | . | . | . | . | . | | ← to (含まない)
+---+---+---+---+---+---+---+---+---+
between テーブルの主な用途は2つあります:
- 王手の合駒判定 — 王手をかけているスライダーと玉の間に合駒を打てるかを判定
- ピンライン — ピンされた駒が動ける範囲を制約する
5.8 attacks_from() — 任意の駒の利きビットボード取得
attacks_from() は、指定マスの指定駒種について、
利いているマスのビットボードを返す統合関数です。
Bitboard Position::attacks_from(int from, PieceType type, Color color,
const Bitboard& occupied) const {
const auto& t = tables();
Bitboard attacks = t.step_from[static_cast<int>(color)]
[static_cast<int>(type)][from];
switch (type) {
case PieceType::Lance:
return attacks | lance_attacks(from, color, occupied);
case PieceType::Bishop:
return attacks | bishop_attacks(from, occupied);
case PieceType::Rook:
return attacks | rook_attacks(from, occupied);
case PieceType::Horse:
return attacks | bishop_attacks(from, occupied);
case PieceType::Dragon:
return attacks | rook_attacks(from, occupied);
default:
return attacks;
}
}
まずステップ型の利きを事前計算テーブルから取得し、スライダー型の駒であればスライダー利きを追加します。 馬は角のスライダー利き + 十字ステップ利き、龍は飛のスライダー利き + 斜めステップ利きです。 純粋なステップ型駒(歩、桂、銀、金、玉、成駒)はテーブル参照のみで完結します。
5.9 rook_attacks(), bishop_attacks(), lance_attacks()
スライダー型駒の利きは、盤上の他の駒(occupied ビットボード)に依存するため、 実行時に計算する必要があります。
3つの関数はすべて ray_attacks_from() に処理を委譲しています。
Bitboard Position::rook_attacks(int from, const Bitboard& occupied) const {
return ray_attacks_from(from, occupied, {North, South, West, East});
}
Bitboard Position::bishop_attacks(int from, const Bitboard& occupied) const {
return ray_attacks_from(from, occupied,
{NorthWest, NorthEast, SouthWest, SouthEast});
}
Bitboard Position::lance_attacks(int from, Color color,
const Bitboard& occupied) const {
return ray_attacks_from(from, occupied,
{color == Color::Black ? North : South});
}
ray_attacks_from() の実装
Bitboard ray_attacks_from(int from, const Bitboard& occupied,
const std::initializer_list<int>& directions) {
const auto& t = tables();
Bitboard attacks;
for (const int dir : directions) {
for (int index = 0; index < t.ray_lengths[from][dir]; ++index) {
const int to = t.ray_squares[from][dir][index];
attacks.set(to);
if (occupied.test(to)) {
break; // 駒にぶつかったらこの方向は終了
}
}
}
return attacks;
}
各方向について、起点から順に ray_squares を辿り、マスを利き集合に追加していきます。
駒が存在するマス(occupied にビットが立っているマス)に到達したら、
そのマスまで利きに含めて(駒を取れる)、それ以降は遮られるため打ち切ります。
5.10 attackers_to() — あるマスへの全攻撃駒
attackers_to(square, by) は、色 by の駒のうち、マス square に利いている駒の集合を返します。
王手判定やピン計算の基礎となる重要な関数です。
Bitboard Position::attackers_to(int square, Color by) const {
const auto& t = tables();
const int color_index = static_cast<int>(by);
Bitboard attackers;
// ステップ型: 逆引きテーブルで候補を特定し、実際の駒種と AND
attackers |= piece_bb_[color_index][static_cast<int>(PieceType::Pawn)]
& t.step_to[color_index][static_cast<int>(PieceType::Pawn)][square];
attackers |= piece_bb_[color_index][static_cast<int>(PieceType::Knight)]
& t.step_to[color_index][static_cast<int>(PieceType::Knight)][square];
attackers |= piece_bb_[color_index][static_cast<int>(PieceType::Silver)]
& t.step_to[color_index][static_cast<int>(PieceType::Silver)][square];
// ... Gold, King, ProPawn, ProLance, ProKnight, ProSilver, Horse, Dragon
// スライダー型: 8方向にレイを飛ばし、最初の駒がスライダーか確認
for (int dir = 0; dir < 8; ++dir) {
for (int index = 0; index < t.ray_lengths[square][dir]; ++index) {
const int from = t.ray_squares[square][dir][index];
const int piece = board_[from];
if (is_empty(piece)) continue;
if (piece_color(piece) == by &&
is_slider_for_direction(piece_type(piece), by, dir)) {
attackers.set(from);
}
break; // 最初の駒で判定終了
}
}
return attackers;
}
ステップ型の攻撃者は step_to(逆引きテーブル)と piece_bb_ の AND で求めます。
たとえば「5五に利いている先手の歩」は、step_to[Black][Pawn][5五](5五に歩の利きが届くマスの集合)と
piece_bb_[Black][Pawn](先手の歩の位置集合)の AND で一発です。
スライダー型の攻撃者はレイを辿って判定します。対象マスから各方向に伸ばし、 最初にぶつかった駒がその方向のスライダーであれば攻撃者に追加します。
is_slider_for_direction()
特定の方向にスライドできる駒かを判定する補助関数です。
bool is_slider_for_direction(PieceType type, Color color, int direction) {
switch (direction) {
case North: case South: case West: case East:
if (type == PieceType::Rook || type == PieceType::Dragon)
return true;
if (type == PieceType::Lance)
return direction == (color == Color::Black ? South : North);
return false;
case NorthWest: case NorthEast: case SouthWest: case SouthEast:
return type == PieceType::Bishop || type == PieceType::Horse;
default:
return false;
}
}
注意点として、レイの方向は対象マスから外向きに定義されています。
つまり、対象マスから South 方向に飛車がいれば、その飛車は North 方向に利いて対象マスを攻撃しています。
しかし is_slider_for_direction では方向をレイの方向そのままで判定しているため、
飛車が North/South/West/East のいずれかの方向のレイ上にあれば攻撃者とみなす、
という対称性を利用しています(飛車は縦横いずれにも動けるため)。
香の場合だけは方向が非対称なので、先手は South 方向(対象マスの南側に先手香があれば北に利く)、
後手は North 方向で判定します。
5.11 is_square_attacked_after_move() — 移動後の被攻撃判定
「ある手を指した後に、特定のマスが攻撃されているか」を判定する関数です。
gives_check()(王手判定)や compute_pins_and_checks() の内部で使われます。
この関数は実際に手を適用するのではなく、piece_after_move_at() という
仮想的な駒配置を返すヘルパーを使って、盤面変更なしに判定を行います。
int Position::piece_after_move_at(int square, const Move& move,
Color mover, PieceType moved_type) const {
if (!move.drop && square == move.from) return 0; // 移動元は空に
if (square == move.to) return encode_piece(mover, moved_type); // 移動先は新しい駒
return board_[square]; // その他は変更なし
}
is_square_attacked_after_move() は attackers_to() と同じ構造ですが、
board_[] の代わりに piece_after_move_at() で仮想盤面を参照します。
bool Position::is_square_attacked_after_move(
int square, Color by, const Move& move, PieceType moved_type) const {
const auto& t = tables();
const int color_index = static_cast<int>(by);
// ステップ型: step_to の候補を仮想盤面で検証
const auto attacks_by_step_piece = [&](PieceType type) {
Bitboard candidates =
t.step_to[color_index][static_cast<int>(type)][square];
while (candidates.any()) {
const int from = candidates.pop_lsb();
const int piece = piece_after_move_at(from, move, by, moved_type);
if (!is_empty(piece) && piece_color(piece) == by
&& piece_type(piece) == type) {
return true;
}
}
return false;
};
if (attacks_by_step_piece(PieceType::Pawn) ||
attacks_by_step_piece(PieceType::Knight) ||
// ... 全ステップ型駒を列挙
attacks_by_step_piece(PieceType::Dragon)) {
return true;
}
// スライダー型: レイを辿って仮想盤面で判定
for (int dir = 0; dir < 8; ++dir) {
for (int index = 0; index < t.ray_lengths[square][dir]; ++index) {
const int from = t.ray_squares[square][dir][index];
const int piece = piece_after_move_at(from, move, by, moved_type);
if (is_empty(piece)) continue;
if (piece_color(piece) == by &&
is_slider_for_direction(piece_type(piece), by, dir)) {
return true;
}
break;
}
}
return false;
}
この手法により、指し手適用と巻き戻しのコストをかけずに「この手を指したら王手になるか」 「この手で玉が安全になるか」を高速に判定できます。 特に合法手生成で大量の候補手を検証する際に有効です。
5.12 まとめ
本章で解説した攻撃テーブルと利き計算の全体像を整理します。
| テーブル / 関数 | 内容 | 主な用途 |
|---|---|---|
square_bb[81] | 単一マスのビットボード | 集合演算の基本部品 |
file_bb[9] | 筋のビットボード | 二歩判定 |
step_from[2][15][81] | ステップ利き(順引き) | 駒の移動先計算 |
step_to[2][15][81] | ステップ利き(逆引き) | あるマスへの攻撃者検索 |
rays / ray_squares / ray_lengths | 8方向のレイ情報 | スライダー利き計算 |
between[81][81] | 2マス間のマス集合 | 合駒判定・ピンライン |
attacks_from() | 任意の駒の利きBB | 移動先生成 |
attackers_to() | あるマスへの攻撃者 | 王手判定・ピン計算 |
is_square_attacked_after_move() | 仮想的な移動後の利き判定 | 王手回避の合法性検証 |
これらの事前計算テーブルと利き計算関数を組み合わせることで、 次章で解説する合法手生成が効率的に実現されます。