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

駒と持ち駒(PieceType / Piece / Hand)

将棋の駒には「成と不成」「先手と後手」「盤上と持ち駒」の状態遷移が存在する。 rsshogi では、これらを PieceType、Piece、HandPiece、Hand の 4 つの型で表現し、条件分岐やループを使わないビット演算で状態変換と枚数比較を実現している。


駒種(PieceType)

PieceType(#[repr(transparent)] pub struct PieceType(i8))は、先後の区別を除いた駒の種類を表す。 生駒(1..=8)に +8(ビット 3 の加算)を行うと対応する成駒(9..=14)になるよう設計されている。

内部値定数名意味成り対応(+8)内部値定数名意味
0NONE駒なしなし8KING玉 / 王
1PAWN歩PRO_PAWN(9)9PRO_PAWNと金
2LANCE香PRO_LANCE(10)10PRO_LANCE成香
3KNIGHT桂PRO_KNIGHT(11)11PRO_KNIGHT成桂
4SILVER銀PRO_SILVER(12)12PRO_SILVER成銀
5BISHOP角HORSE(13)13HORSE馬
6ROOK飛DRAGON(14)14DRAGON龍
7GOLD金なし15GOLD_LIKE金相当の合成識別子

このビット配置により、成り判定と生駒への復元は単一のビットマスク演算で行える。

  • 成駒判定(is_promoted):(self.0 & 8) != 0 && self.0 != PieceType::KING.0 && self.0 != PieceType::GOLD_LIKE.0
  • 生駒への復元(unpromote):KING(8)を除き、self.0 & 7 で生駒の ID(1..=7)が得られる。捕獲した駒を持ち駒に戻す際にテーブル参照を必要としない。

なお、PieceType::COUNT = 16、PieceType::HAND_TABLE_SIZE = 8(NONE + 生駒 7 種)が配列サイズ定数として定義されている。


先後付きの駒(Piece)

Piece(#[repr(transparent)] pub struct Piece(i8))は、PieceType の下位 4 ビットに、ビット 4(COLOR_BIT = 16)の先後フラグを合成した 5 ビット値である。

Bit:   7   6   5    4      3      2   1   0
     +---+---+---+------+------+---+---+---+
     | 0 | 0 | 0 |Color |Promot| Base Piece|
     +---+---+---+------+------+---+---+---+
                  0:先手 0:生駒   1..=7 (生駒)
                  1:後手 1:成駒/玉 8 (玉: Promot=1, Base=0)

具体的な数値範囲は以下の通りとなる。

  • 0:Piece::NONE(空きマス)
  • 1..=14:先手の実駒(B_PAWN = 1 〜 B_DRAGON = 14)、および合成識別子 B_GOLD_LIKE = 15
  • 16:無効値(後手ビットのみが立ち PieceType が 0 の状態であり、from_raw では弾かれる)
  • 17..=30:後手の実駒(W_PAWN = 17 〜 W_DRAGON = 30)、および合成識別子 W_GOLD_LIKE = 31

この構造により、Piece から各属性への分解はすべて 1〜2 命令のビット演算で完了する。

  • 駒種の抽出(piece_type):PieceType(self.0 & 0x0f)
  • 手番の抽出(color):Color((self.0 >> 4) as u8)(self != Piece::NONE のとき)
  • 成駒への変換(promote):成れる駒に対して Piece(self.0 | 8)

持ち駒のビットパッキング(Hand / HandPiece)

持ち駒の駒種インデックスには、0..=6 の連続値を持つ HandPiece(PAWN = 0、LANCE = 1、KNIGHT = 2、SILVER = 3、BISHOP = 4、ROOK = 5、GOLD = 6、COUNT = 7)を用いる。 片方の陣営が持つ全 7 種の持ち駒枚数は、32 ビット符号なし整数を包む Hand(u32) 1 個にすべてパックされる。

Hand(u32) のビットレイアウト

各駒種の枚数フィールドの間には、減算時の桁借り(ボロー)を受け止めるための ボロービット(Borrow Bit) が 1 ビットずつ配置されている(歩と香の間のみビット 6〜7 の 2 ビットが未使用のパディングとなっている)。

Bit:  31  30..28  27  26  25..24  23  22  21..20  19  18..16  15  14..12  11  10..8  7..6   5   4..0
     +---+-------+---+---+-------+---+---+-------+---+-------+---+-------+---+------+-----+---+------+
     | B | Gold  | 0 | B | Rook  | 0 | B |Bishop | B |Silver | B |Knight | B |Lance | 0 0 | B | Pawn |
     +---+-------+---+---+-------+---+---+-------+---+-------+---+-------+---+------+-----+---+------+
      (1)  (3b)       (1)  (2b)       (1)  (2b)   (1)  (3b)   (1)  (3b)   (1)  (3b)        (1)  (5b)

全 7 駒種の正確なビット範囲、シフト量、マスク値、およびボロービットの位置は以下の通りである。

駒種(HandPiece)枚数ビット範囲ビット幅シフト量(SHIFT)値マスク最大表現値将棋の上限枚数ボロービット位置ボローマスク(1 << bit)
歩(PAWN = 0)0..=45 bits00x1f3118Bit 51 << 5(0x0000_0020)
香(LANCE = 1)8..=103 bits80x0774Bit 111 << 11(0x0000_0800)
桂(KNIGHT = 2)12..=143 bits120x0774Bit 151 << 15(0x0000_8000)
銀(SILVER = 3)16..=183 bits160x0774Bit 191 << 19(0x0008_0000)
角(BISHOP = 4)20..=212 bits200x0332Bit 221 << 22(0x0040_0000)
飛(ROOK = 5)24..=252 bits240x0332Bit 261 << 26(0x0400_0000)
金(GOLD = 6)28..=303 bits280x0774Bit 311 << 31(0x8000_0000)

これらを束ねるマスク定数として、枚数ビット全体の HAND_BIT_MASK とボロービット全体の HAND_BORROW_MASK が定義されている。

#![allow(unused)]
fn main() {
const HAND_BIT_MASK: u32 = 0x1f
    | (0x7 << 8)
    | (0x7 << 12)
    | (0x7 << 16)
    | (0x3 << 20)
    | (0x3 << 24)
    | (0x7 << 28);

const HAND_BORROW_MASK: u32 = (1 << 5)
    | (1 << 11)
    | (1 << 15)
    | (1 << 19)
    | (1 << 22)
    | (1 << 26)
    | (1 << 31);
}

基本操作と Zobrist テーブルへの対応

各駒種の 1 枚分に相当する重み HAND_INCREMENT[hp] = 1 << SHIFT[hp] を用いることで、持ち駒の増減は通常の 32 ビット加減算だけで完了する。

  • 1 枚追加(add_one):Hand(self.0 + (1 << shift))
  • 1 枚削除(remove_one):Hand(self.0 - (1 << shift))
  • 枚数取得(count):(self.0 >> shift) & mask
  • 所持判定(has):(self.0 & (mask << shift)) != 0
  • オーバーフロー検査(has_overflow):(self.0 & HAND_BORROW_MASK) != 0

さらに、各駒種に割り当てられたビット幅(最大表現値 + 1:歩 32、香/桂/銀/金 各 8、角/飛 各 4)の合計は 32 + 8 + 8 + 8 + 4 + 4 + 8 = 72 となる。 この構造は Zobrist ハッシュの持ち駒テーブル(HAND_SLOT_COUNT = 72)のフラット配列レイアウトと 1 対 1 で対応しており、count で取り出した枚数をそのままベースオフセットに加算するだけでハッシュキーを引くことができる。

ボロービットを用いた一括優劣比較(SWAR 比較)

探索中の千日手判定では、同一盤面で手番側の持ち駒が過去局面より優越しているか(優等局面:RepetitionState::Superior)あるいは劣後しているか(劣等局面:RepetitionState::Inferior)を判定する必要がある。 すなわち「全 7 種の持ち駒すべてについて self.count(hp) >= other.count(hp) が成り立つか」を検査しなければならない。

Hand ではボロービットを活用する SWAR(SIMD Within A Register)技法により、7 回のループや分岐を一切行わず、以下の数命令で全 7 駒種の一括比較を実現している。

#![allow(unused)]
fn main() {
let diff = (self.0 | HAND_BORROW_MASK).wrapping_sub(other.0);
let is_superior_or_equal = (diff & HAND_BORROW_MASK) == HAND_BORROW_MASK;
}
  1. self.0 のすべてのボロービットをあらかじめ 1 にセット(self.0 | HAND_BORROW_MASK)してから other.0 を減算する。
  2. もしある駒種で self の枚数が other より少なければ、その駒種のフィールドの上位にあるボロービットから桁借りが発生し、ボロービットが 1 から 0 に反転する。ボロービットが壁となるため、さらに上位の駒種フィールドへ桁借りが伝播することはない。
  3. したがって、減算結果 diff のすべてのボロービットが 1 のまま残っていれば(diff & HAND_BORROW_MASK == HAND_BORROW_MASK)、全 7 駒種すべてにおいて self >= other であることが 1 回のマスク比較で証明される。 なお、crates/rsshogi/src/types/hand.rs の実装では、これと数学的に同値で命令数のさらに少ない ((self.0 - other.0) & HAND_BORROW_MASK) == 0(正常な Hand ではボロービットが 0 であるため、いずれかの駒種で負になると対応するボロービットまたはその上位にボローが立つ性質を利用)によって判定を行っている。