第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() は以下の順序でテーブルを構築します:

  1. square_bb と file_bb の初期化
  2. 8方向のレイ(rays / ray_squares / ray_lengths)の構築
  3. between テーブルの構築
  4. ステップ型駒の利きテーブル(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種類に分かれます:

ステップ型の利きは盤上の駒配置に依存しないため、完全に事前計算できます。

step_from と step_to の違い

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つの配列の役割:

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_lengths8方向のレイ情報スライダー利き計算
between[81][81]2マス間のマス集合合駒判定・ピンライン
attacks_from()任意の駒の利きBB移動先生成
attackers_to()あるマスへの攻撃者王手判定・ピン計算
is_square_attacked_after_move()仮想的な移動後の利き判定王手回避の合法性検証

これらの事前計算テーブルと利き計算関数を組み合わせることで、 次章で解説する合法手生成が効率的に実現されます。