Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

利きの計算

前提知識: 基本操作(マスク、シフト、pop_lsb() の使い方)

このページの要点

  • ステップ駒(歩・桂・銀・金・玉・成駒)は事前計算テーブルの参照だけで利きが確定する
  • 遠方駒(香・角・飛)は Occupancy 依存のため、Qugiy 方式(差分抽出+byte_reverse)で O(1) 検出する
  • 成駒の利きは小駒(と・成香・成桂・成銀)が金の利き、大駒は「元の駒の利き + 王の利き」(馬 = 角 + 王、龍 = 飛 + 王)で合成する
  • 駒打ちでは二歩・行き所のない駒・打ち歩詰めをビットマスクで早期フィルタする

このページでは、短い利き(歩・桂・銀・金・玉・成駒)と長い利き(香・角・飛)の両方について、ビットボードを使った利き計算の手法を解説します。

短い利き(ステップ駒)

ステップ駒は現在地の周囲の固定パターンで攻撃集合が決まります。1 したがって、事前計算済みの攻撃テーブルと集合演算だけで疑似合法手を列挙できます。

主要ステップ駒の利きパターン

以下のアニメーションで、各ステップ駒の利きパターンを矢印で確認できます。 5五に配置した駒の全利き方向が表示されます。

ステップ駒の攻撃テーブル参照

#[inline]
#[must_use]
pub fn pawn_attacks_unchecked(sq: Square, color: Color) -> Bitboard {
    PAWN_ATTACKS[sq][color.to_index()]
}

#[inline]
#[must_use]
pub fn knight_attacks_unchecked(sq: Square, color: Color) -> Bitboard {
    KNIGHT_ATTACKS[sq][color.to_index()]
}

#[inline]
#[must_use]
pub fn silver_attacks_unchecked(sq: Square, color: Color) -> Bitboard {
    SILVER_ATTACKS[sq][color.to_index()]
}

#[inline]
#[must_use]
pub fn gold_attacks_unchecked(sq: Square, color: Color) -> Bitboard {
    GOLD_ATTACKS[sq][color.to_index()]
}

#[inline]
#[must_use]
pub fn king_attacks_unchecked(sq: Square) -> Bitboard {
    KING_ATTACKS[sq]
}
/// 飛車の StepAttacks(空盤での利き = 4方向ビーム結合)
#[inline]
#[must_use]
pub fn rook_step_attacks(sq: Square) -> Bitboard {
    let b = ROOK_BEAMS[sq.to_index()];
    b.n | b.e | b.s | b.w
}

/// 角の StepAttacks(空盤での利き = 4方向ビーム結合)
#[inline]
#[must_use]
pub fn bishop_step_attacks(sq: Square) -> Bitboard {
    let b = BISHOP_BEAMS[sq.to_index()];
    b.ne | b.se | b.sw | b.nw
}

/// 香車の StepAttacks(空盤での片方向ビーム)
#[inline]
#[must_use]
pub fn lance_step_attacks(sq: Square, color: Color) -> Bitboard {
    let beams = LANCE_BEAMS[sq.to_index()];
    match color {
        Color::BLACK => beams.black,
        _ => beams.white,
    }
}

ソースコード

例として先手の歩を考えます。

let us = side_to_move;
let pawns = pieces_cp(us, PAWN);
let empty = !occupied();
let push = shift_forward(pawns, us) & empty;
for to in push { let from = back_from(to, us); emit_move(from, to, promote_if_needed(from, to)); }

銀や金、玉も同様に、attacks = ATTACKS[piece_type][from] を取り、自駒集合を引いて targets = attacks & !our_pieces を列挙すれば十分です。 桂馬は前方二つ×左右一つの固定オフセットで計算します。

成りは「移動元または移動先が成りゾーンに入るか」をビットテストし、必要に応じて 2 手(成・不成)を出力します。

この方式では、短い利きの駒は数命令で候補集合を作り、pop_lsb で列挙するだけです。 高速かつ実装が単純で、分岐も少なく保守性に優れます。

駒打ち(ドロップ)の生成

持ち駒の種類ごとに「打てる升集合 = 空升 − 禁止条件」を作ります。 二歩、行き所のない駒、打ち歩詰めといった将棋特有の制約は、この段階で早期にフィルタします。2

歩の打てるマス(二歩フィルタ)

先手が歩を持っている状態で、5筋に既に歩がある場合の打てるマスを可視化します。 黄色ハイライトが実際に歩を打てる空きマス、赤い丸が 5 筋の自歩によって二歩で除外されるマスです。 緑の矢印は、打てるマスの一例として 3五への歩打ちを示しています。

let empties = !occupied();
let files_without_our_pawn = FILE_MASKS & !our_pawns;
let pawn_drop_targets = empties & files_without_our_pawn & not_last_rank(us);
for to in pawn_drop_targets { emit_drop(PAWN, to); }

長い利き(飛び利き)の難しさ

香・角・飛は途中の駒の有無で利きが動的に変わります。 単純なテーブル参照では済まず、占有(Occupancy)依存の計算が必要です。 つまり、短い利きと同じ感覚では実装できません。

この問題に対しては複数の定番アプローチが知られています。 どれも「占有から一次元の線形表現を取り出し、最近接遮蔽駒までの区間を切り出す」ことを、高速に行う工夫です。

  • Rotated Bitboards(90°/45° 回転):ファイルや対角線を一次元化し、index = rotated_occ & mask でテーブル参照します。
  • PEXT Bitboards(BMI2):pext 命令で疎なビット列を圧縮してインデックス化します。
  • Magic Bitboards:乗算シフトによる完全ハッシュでテーブル参照します。
  • LZ/TZ(Leading/Trailing Zeros)法:線形に並ぶ各方向で「最近接遮蔽駒」を ctz/clz 相当で直接求めます。
  • Qugiy 方式(差分抽出+byte_reverse):減算の借位伝播で遮蔽駒までのビーム区間を一括抽出し、byte_reverse で逆方向も処理します。rsshogi はこの手法を採用しています。

各手法の詳細な比較は 飛び利きアルゴリズム比較 を参照してください。

rsshogi の選択: Qugiy 方式(差分抽出+byte_reverse)

rsshogi は WCSC31(2021)で Qugiy が発表した差分抽出法を採用しています。 減算の借位伝播と byte_reverse を組み合わせ、事前テーブルなしで遠方駒の利きを O(1) で算出します。

角の対角線に遮蔽駒がある場合

角の斜め利きが味方駒(6四の歩)で遮られる例を表示します。 4方向の対角線ビームのうち、左上方向だけが歩で遮断されています。

  • 🔵 青い矢印: 通過可能な対角線ビーム(遮蔽駒なし)
  • 🔴 赤い矢印/丸: 遮蔽駒(味方歩)で遮断される方向

概要

Qugiy 方式は減算の借位伝播byte_reverse(バイト反転) の 2 つを組み合わせます。3 やねうら王の開発者による完全解説記事では、このアルゴリズムが 4 段階のアイデアの積み上げであることが解説されています。4

  1. 加算による繰り上がり: occ + pawnStepEffect(sq)pawnStepEffect はやねうら王の呼称。rsshogi の *_step_attacks に相当)で最近接遮蔽駒まで繰り上がりが伝播する性質を利用
  2. 減算への改良: (occ − 1) ^ occ に書き換えることで、参照テーブルが不要になる
  3. byte_reverse で逆方向処理: SSSE3 の pshufb でバイト順を反転し、同じ減算トリックで逆方向の利きも一括処理
  4. 128bit decrement の並列化: AVX2 の unpack 命令で lo/hi を分離し、Bitboard256 で 4 方向を同時に decrement 処理

CPU 依存性が小さく、AMD Zen2 の PEXT 問題を回避しつつ多様な環境で安定して高速です。 Magic Bitboard のようなテーブル探索が不要で、実装が簡潔な点も採用理由です。

ビーム&遮蔽駒検出の流れ

飛車の4方向ビームを段階的に可視化するアニメーションです。 各フレームで1方向ずつビームを表示し、遮蔽駒の検出を示します。

Qugiy 方式のステップ(飛車の場合):

  1. 縦方向: 香車の利き(lance_attacks)を先手・後手の両方向で呼び出して合成
  2. 横方向: 占有を byte_reverse して順方向・逆方向を揃え、減算(decrement_pair)→ XOR で区間を抽出
  3. 4方向を OR で合成 → 最終的な飛車の利き

実装の仕組み

rsshogi の遠方駒の利き計算は 3 つの関数で構成されます。

香車(lance_attacks

1方向のみに利く直線の遠方駒(香)で、先手(北向き)と後手(南向き)で処理が異なります。 香車は 128 ビットの Bitboard 単体で処理するため、SSE2 の基本命令のみで完結します。

// 後手(南向き): 減算の借位伝播
let em = occ & mask;           // ビーム上の占有ビットを抽出
let t = em.wrapping_sub(1);    // 最下位の遮蔽駒まで借位が伝播
let result = (em ^ t) & mask;  // XOR で遮蔽駒までの区間を得る

// 先手(北向き): leading_zeros で MSB を検出
//   BMI1 の LZCNT 命令で 1 サイクル実行
let msb = 63 - (mocc | 1).leading_zeros();
let fill = (!0u64) << msb;     // MSB 以上を全て 1 に
let result = fill & mask;       // ビームでマスクして利きを得る

lance_attacks のソースコード

角(bishop_attacks)の Bitboard256 並列処理

4方向の対角線を Bitboard256AVX2__m256i、256 ビット SIMD レジスタ)で並列に処理します。 Bitboard256 は 128 ビットの Bitboard を 2 つ束ねたラッパーで、AVX2 対応 CPU では単一の 256 ビットレジスタで演算し、非対応環境では [u64; 4] のスカラーフォールバックに切り替わります(SIMD 概論参照)。

fn bishop_attacks(sq: Square, occupied: Bitboard) -> Bitboard {
    let mask_lo = QUGIY_BISHOP_MASK[sq][0];  // 事前計算マスク
    let mask_hi = QUGIY_BISHOP_MASK[sq][1];

    // splat: 同じ Bitboard を 256 ビットレジスタの上下に複製
    //   AVX2 では _mm256_broadcastsi128_si256 に相当
    let occ2 = Bitboard256::splat(occupied);
    // byte_reverse: SSSE3 の pshufb でバイト順を反転し、逆方向の処理を実現
    let rocc2 = Bitboard256::splat(occupied.byte_reverse());
    let (hi, lo) = Bitboard256::unpack(rocc2, occ2);

    // マスク適用 → 減算(decrement)→ XOR → マスク
    //   AVX2 では _mm256_and_si256, _mm256_sub_epi64, _mm256_xor_si256 を使用
    let hi = hi.and(mask_hi);
    let lo = lo.and(mask_lo);
    let (t1, t0) = Bitboard256::decrement(hi, lo);
    let t1 = t1.xor(hi).and(mask_hi);
    let t0 = t0.xor(lo).and(mask_lo);

    // 逆方向の結果を byte_reverse で戻して合成
    let (hi, lo) = Bitboard256::unpack(t1, t0);
    hi.byte_reverse().or(lo).merge()
}

bishop_attacks のソースコード

飛車(rook_attacks

縦方向は香車 2 方向の合成、横方向は byte_reverseSSSE3 pshufb)+ decrement_pair で処理します。 角と異なり Bitboard256 は使わず、128 ビットの Bitboard で完結します。

fn rook_attacks(sq: Square, occupied: Bitboard) -> Bitboard {
    // 縦方向: 香車の先手・後手ビームを合成
    let mut attacks = lance_attacks(sq, occupied, Color::BLACK);
    attacks |= lance_attacks(sq, occupied, Color::WHITE);

    // 横方向: byte_reverse + 差分抽出
    let mask_lo = QUGIY_ROOK_MASK[sq][0];
    let mask_hi = QUGIY_ROOK_MASK[sq][1];
    let occ_rev = occupied.byte_reverse();
    let (hi, lo) = Bitboard::unpack(occ_rev, occupied);
    let hi = hi.and_raw(mask_hi);
    let lo = lo.and_raw(mask_lo);
    let (t1, t0) = Bitboard::decrement_pair(hi, lo);
    let t1 = t1.xor_raw(hi).and_raw(mask_hi);
    let t0 = t0.xor_raw(lo).and_raw(mask_lo);
    let (hi, lo) = Bitboard::unpack(t1, t0);
    attacks | hi.byte_reverse() | lo
}

rook_attacks のソースコード

事前計算マスク(build.rs)

QUGIY_BISHOP_MASKQUGIY_ROOK_MASKbuild.rs でビルド時に生成されます。 各マスごとに、対角線や段方向のビームを byte_reverse 対応形式でパックしたマスクです。 LANCE_BEAMS は筋方向の先手・後手ビームを事前計算しています。

将棋盤を用いた可視化

理解を助けるため、任意の局面を表示できる簡易な将棋盤レンダラを用意しています。 docs/book/src/assets/shogi-board.ts を参照してください。

このレンダラは画像の作成やアニメーションの作成に適しており、ドキュメント内の図示を容易にします。

落とし穴

香車の方向間違い

香車は先手なら上方向、後手なら下方向にのみ移動します。 lance_attacks(sq, occ, color) に渡す color を間違えると、逆方向に利きが生成され、合法手が大量に誤生成されます。 テストでは先手・後手の両方の香車を必ず検証してください。

桂馬の成りゾーン

桂馬は先手なら 1-2 段目に移動した場合必ず成る必要があります(行き所のない駒になるため)。 3 段目への移動は成り/不成の選択があります。 成り判定を忘れると、1-2 段目に不成の桂馬が生成される不正な状態になります。

成駒の利き合成

馬の利きは「角の利き + 王の利き」、龍の利きは「飛の利き + 王の利き」です。 角の利きだけで馬の利きとしてしまうと、隣接 4 マスへの移動が欠落します。

龍の利き = 飛車 + 王

龍(成り飛車)の利きは飛車の直線利きと王の隣接8マスの OR 合成です。 青い矢印が飛車成分(直線利き)、オレンジの矢印が王成分(隣接斜め4マス)です。

// 龍の利き = 飛車の利き | 王の利き
let dragon_attacks = rook_attacks(sq, occ) | KING_ATTACKS[sq];
// 馬の利き = 角の利き | 王の利き(直交4マス)
let horse_attacks = bishop_attacks(sq, occ) | KING_ATTACKS[sq];

SIMD 命令の利用マップ

利きの計算で使われる主な SIMD/拡張命令の対応関係です。 各命令の詳細は 拡張命令リファレンス を参照してください。

処理使用命令ISA備考
Bitboard の AND/OR/XOR_mm_and_si128SSE2全演算の基盤
byte_reverse_mm_shuffle_epi8SSSE3Qugiy 方式の逆方向処理
pop_lsb()tzcntBMI1ビット列挙
count()popcntPOPCNT駒数カウント
Bitboard256 演算_mm256_*AVX2角の 4 方向並列
lance_attacks の MSB 検出lzcntBMI1先手香の遮蔽駒検出

フォールバック: AVX2 非対応環境では Bitboard256 がスカラー実装([u64; 4])に切り替わります。 SSE2 は x86-64 の必須仕様のため、基本的な 128 ビット演算は常に利用可能です。

まとめ

  • ステップ駒: テーブル参照 1 回 + マスク演算 → 数命令で完了
  • 遠方駒: Qugiy 方式(差分抽出+byte_reverse)で方向ごとの利きを O(1) 算出
  • 駒打ち: 空マスから二歩・行き所のない駒をビットマスクで除外
  • 成駒の利きは元の駒 + 王の利きで合成(見落としやすいバグ源)
  • SIMD は利き計算の全レイヤーに関与: SSE2(基本演算)、SSSE3(byte_reverse)、AVX2(角の並列処理)

次に読む

飛び利きアルゴリズム比較: Qugiy, Magic, PEXT, Rotated, LZ/TZ の詳細比較に進みます。

参考文献


  1. psilord, “Representation of a Chess Board with a Bitboard” — ステップ駒の攻撃テーブル参照

  2. やねうら王, “縦型Bitboardの唯一の弱点を克服する” (2015) — 二歩判定のための PEXT + 加算トリック。縦型 Bitboard で歩の打てるマスを高速判定する手法

  3. すぎゃーんメモ, “Bitboardでleading/trailing zerosを使って遠方駒の利きを求める” — LZ/TZ 法と差分抽出法の日本語解説

  4. やねうら王, “Qugiyの飛び利きのコード、完全解説” (2021) — 加算→減算→byte_reverse→128bit decrement の 4 段階アイデア発展と、やねうら王への統合経緯