駒と持ち駒(PieceType / Piece / Hand)
将棋の駒には「成と不成」「先手と後手」「盤上と持ち駒」の状態遷移が存在する。
rsshogi では、これらを PieceType、Piece、HandPiece、Hand の 4 つの型で表現し、条件分岐やループを使わないビット演算で状態変換と枚数比較を実現している。
駒種(PieceType)
PieceType(#[repr(transparent)] pub struct PieceType(i8))は、先後の区別を除いた駒の種類を表す。
生駒(1..=8)に +8(ビット 3 の加算)を行うと対応する成駒(9..=14)になるよう設計されている。
| 内部値 | 定数名 | 意味 | 成り対応(+8) | 内部値 | 定数名 | 意味 |
|---|---|---|---|---|---|---|
0 | NONE | 駒なし | なし | 8 | KING | 玉 / 王 |
1 | PAWN | 歩 | PRO_PAWN(9) | 9 | PRO_PAWN | と金 |
2 | LANCE | 香 | PRO_LANCE(10) | 10 | PRO_LANCE | 成香 |
3 | KNIGHT | 桂 | PRO_KNIGHT(11) | 11 | PRO_KNIGHT | 成桂 |
4 | SILVER | 銀 | PRO_SILVER(12) | 12 | PRO_SILVER | 成銀 |
5 | BISHOP | 角 | HORSE(13) | 13 | HORSE | 馬 |
6 | ROOK | 飛 | DRAGON(14) | 14 | DRAGON | 龍 |
7 | GOLD | 金 | なし | 15 | GOLD_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 = 1516:無効値(後手ビットのみが立ち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..=4 | 5 bits | 0 | 0x1f | 31 | 18 | Bit 5 | 1 << 5(0x0000_0020) |
香(LANCE = 1) | 8..=10 | 3 bits | 8 | 0x07 | 7 | 4 | Bit 11 | 1 << 11(0x0000_0800) |
桂(KNIGHT = 2) | 12..=14 | 3 bits | 12 | 0x07 | 7 | 4 | Bit 15 | 1 << 15(0x0000_8000) |
銀(SILVER = 3) | 16..=18 | 3 bits | 16 | 0x07 | 7 | 4 | Bit 19 | 1 << 19(0x0008_0000) |
角(BISHOP = 4) | 20..=21 | 2 bits | 20 | 0x03 | 3 | 2 | Bit 22 | 1 << 22(0x0040_0000) |
飛(ROOK = 5) | 24..=25 | 2 bits | 24 | 0x03 | 3 | 2 | Bit 26 | 1 << 26(0x0400_0000) |
金(GOLD = 6) | 28..=30 | 3 bits | 28 | 0x07 | 7 | 4 | Bit 31 | 1 << 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;
}
self.0のすべてのボロービットをあらかじめ1にセット(self.0 | HAND_BORROW_MASK)してからother.0を減算する。- もしある駒種で
selfの枚数がotherより少なければ、その駒種のフィールドの上位にあるボロービットから桁借りが発生し、ボロービットが1から0に反転する。ボロービットが壁となるため、さらに上位の駒種フィールドへ桁借りが伝播することはない。 - したがって、減算結果
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であるため、いずれかの駒種で負になると対応するボロービットまたはその上位にボローが立つ性質を利用)によって判定を行っている。