ビット圧縮(PackedSfen / HuffmanCodedPos)
ディープラーニング将棋の自己対局データや数億局面の教師データでは、1 局面あたりのデータサイズが入出力帯域とストレージ容量のボトルネックとなる。
rsshogi は、position-serialization Cargo feature のもとで、任意の合法局面を 正確に 256 ビット(32 バイト) の固定長バイト配列 [u8; 32] に可逆圧縮する PackedSfen(YaneuraOu 形式)と HuffmanCodedPos(Apery / dlshogi 形式)の 2 種類を実装している。
先後反転した局面の出力
Position::to_sfen_flipped() は、盤面を 180 度回転し、駒色、持ち駒、手番を入れ替えた SFEN を返す。
Position::to_packed_sfen_flipped() は同じ反転規則を使って PackedSfen を返す。
表現不能な局面をエラーとして扱う場合は try_to_packed_sfen_flipped() を使う。
元の局面と履歴は変更しない。
PackedSfen の反転出力は SFEN 文字列の生成と再解析を経由しない。
出力用の局面には盤面、持ち駒、手番と玉位置だけを設定し、bitboard や戦術キャッシュの再計算を行わない。
Python では Board.to_sfen_flipped() と Board.to_packed_sfen_flipped(out=None) を使う。
1. ビットストリーム I/O(BitReader / BitWriter)
ハフマン符号化では、1 ビットのフラグから 7 ビットの座標、2〜8 ビットの可変長符号までがバイト境界(8 ビット単位)を跨いで隙間なく詰め込まれる。
crates/rsshogi/src/board/position/bit_io.rs の BitReader と BitWriter は、[u8; 32] バッファに対する LSB-first(各バイトの最下位ビット Bit 0 から最上位ビット Bit 7 の順) のビット読み書きを担う。
#![allow(unused)]
fn main() {
// BitWriter::write_one_bit のビット位置計算 (cursor: 0..256)
self.buf[self.cursor >> 3] |= (b & 1) << (self.cursor & 7);
self.cursor += 1;
}
cursor >> 3で書き込み先のバイトインデックス(0..32)を特定し、cursor & 7でそのバイト内のビットオフセット(0..8)を決定する。- 複数ビットの読み書きでは、コード定義側で「LSB から順に送出するビット列(Pre-reversed bit pattern)」を保持しておくことで、1 ビットずつシフトしながらそのままストリームへ流し込める設計となっている。
2. 将棋の駒数不変条件と 256 ビット固定長への収束原理
可変長符号(ハフマン符号)を用いているにもかかわらず、なぜどんな局面でも必ず 256 ビット(32 バイト)以内に収まるのか。 その理由は、「盤上の駒が空きマスに変わって盤面ビット数が減った分だけ、持ち駒が増えて持ち駒ビット数が増える(逆もまた然り)」という将棋の総駒数(40 枚)不変条件 にある。
- 盤上の空きマス:1 ビット(
0)で表現される。 - 盤上の駒(玉を除く):移動元、先後、成フラグを含め、駒種に応じたビット数が割り当てられる。
- 持ち駒:同一駒種の盤上駒よりも常に 2 ビット少ない符号長(先後 1 ビットと成フラグ 1 ビットが不要なため、あるいは空きマス
0の 1 ビット分との差し引きで盤上配置と同等の合計ビット数)が割り当てられている。
具体的には、ある駒が盤上から捕獲されて持ち駒に移ると、盤面側ではそのマスが「駒(N ビット)」から「空マス(1 ビット)」に変わって N - 1 ビット減少し、持ち駒側では N - 2 ビット増加するため、局面全体の合計ビット数はむしろ 1 ビット減少 する。
その結果、全 38 枚の非玉駒がすべて盤上に存在し、かつ成駒が混じる最悪ケースでも、合計ビット数は必ず 最大 256 ビット以下 に収まることが数学的に保証される。
3. PackedSfen(YaneuraOu 形式)のビット構造と符号表
PackedSfen(crates/rsshogi/src/board/position/packed_sfen.rs)は、以下の 4 セクションを先頭(Bit 0)から順番に書き込む。
Bit: 0 1..=7 (7b) 8..=14 (7b) 15.. (可変長) ..255
+------+--------------+--------------+-----------------------------+------------------+
| Turn | Black King | White King | 81 Squares Board Encoding | Hand Pieces |
| (1b) | Square (0..80| Square (0..80| (sq = 0..=80, King = Empty) | (R->B->G->S->N->L|
+------+--------------+--------------+-----------------------------+------------------+
- 手番(1 ビット):先手(
BLACK)なら0、後手(WHITE)なら1。 - 先手玉と後手玉の座標(7 ビット × 2 = 14 ビット):先手玉の
Square(0..=80、玉不在時は81)と後手玉のSquareをそれぞれ 7 ビット整数で書き込む。 - 盤面 81 マスの符号化(
sq = 0..=80):先手玉および後手玉がいるマスも含めて全 81 マスを走査するが、玉がいるマスはすでに先頭 14 ビットで記録済みであるため、盤面ストリーム上では「空きマス(0、1 ビット)」として出力する。それ以外の駒があるマスは、駒種プレフィックスに続けて 先後(1b) と 成フラグ(金以外のとき 1b、後置) を書き込む。 - 持ち駒の符号化:先手 → 後手の順、かつ飛 → 角 → 金 → 銀 → 桂 → 香 → 歩の順で、所持枚数分だけ持ち駒符号を繰り返し書き込む(玉が 2 枚とも盤上にある通常の局面では、最初の 2 枚の玉スロットをスキップして残りのビット領域に詰め込む)。
PackedSfen の符号定義表(LSB-first 送出順)
| 対象 | プレフィックス(ビット幅) | 付加ビット(送出順) | 盤上合計ビット数 | 持ち駒の符号(プレフィックス + 先後 1b) | 持ち駒合計ビット数 |
|---|---|---|---|---|---|
| 空きマス(または玉) | 0(1b) | なし | 1 bit | なし | なし |
歩(PAWN) | 01(2b, 値 0x01) | 先後 1b + 成 1b | 4 bits | 0(1b, 値 0x00)+ 先後 1b | 2 bits |
香(LANCE) | 0011(4b, 値 0x03) | 先後 1b + 成 1b | 6 bits | 001(3b, 値 0x01)+ 先後 1b | 4 bits |
桂(KNIGHT) | 1011(4b, 値 0x0b) | 先後 1b + 成 1b | 6 bits | 101(3b, 値 0x05)+ 先後 1b | 4 bits |
銀(SILVER) | 0111(4b, 値 0x07) | 先後 1b + 成 1b | 6 bits | 011(3b, 値 0x03)+ 先後 1b | 4 bits |
金(GOLD) | 1111(4b, 値 0x0f) | 先後 1b(成なし) | 5 bits | 111(3b, 値 0x07)+ 先後 1b | 4 bits |
角(BISHOP) | 011111(6b, 値 0x1f) | 先後 1b + 成 1b | 8 bits | 01111(5b, 値 0x0f)+ 先後 1b | 6 bits |
飛(ROOK) | 111111(6b, 値 0x3f) | 先後 1b + 成 1b | 8 bits | 11111(5b, 値 0x1f)+ 先後 1b | 6 bits |
4. HuffmanCodedPos(Apery / dlshogi 形式)のビット構造と PackedSfen との違い
HuffmanCodedPos(crates/rsshogi/src/board/position/huffman_coded_pos.rs)も同じく 256 ビットのハフマン符号化形式であるが、YaneuraOu の PackedSfen とは以下の 3 つの重要な非互換点 があるため、両者のバイト列を直接混用することはできない。
| 相違点 | PackedSfen(YaneuraOu 形式) | HuffmanCodedPos(Apery / dlshogi 形式) |
|---|---|---|
| 1. 玉が存在するマスの扱い | 81 マスすべてを走査し、玉のマスにも空きマスビット 0(1b × 2 = 2b)を出力する。 | 玉が存在する 2 マスを盤面ループから完全にスキップ し、残りの 79 マスのみ を符号化する(2 ビット節約)。 |
| 2. 成フラグと先後ビットの配置 | すべての成れる駒で [プレフィックス] -> [先後 1b] -> [成 1b] の順で固定。 | 桂、銀、金は [プレフィックス 3b] -> [成 1b(金は 0 固定)] -> [先後 1b] の順で、成フラグが先後ビットより前に置かれる。 |
| 3. プレフィックスのビット値 | 香=0x03、桂=0x0b、銀=0x07、金=0x0f 等。 | 香=0x03、桂=0x03(ビット幅で区別)、銀=0x05、金=0x0f 等。 |
HuffmanCodedPos の符号定義表(盤上 79 マスと持ち駒)
| 駒種 | 盤上符号(合計ビット数、カッコ内は BitWriter 送出値) | 持ち駒符号(合計ビット数、カッコ内は BitWriter 送出値) |
|---|---|---|
| 空きマス | 1 bit(0x00) | なし |
歩(PAWN) | 4 bits(プレフィックス 0x01 [2b] + 成 1b + 先後 1b) | 2 bits(プレフィックス 0x00 [1b] + 先後 1b) |
香(LANCE) | 6 bits(プレフィックス 0x03 [4b] + 成 1b + 先後 1b) | 4 bits(プレフィックス 0x01 [3b] + 先後 1b) |
桂(KNIGHT) | 6 bits(プレフィックス 0x0b [4b] + 成 1b + 先後 1b) | 4 bits(プレフィックス 0x05 [3b] + 先後 1b) |
銀(SILVER) | 6 bits(プレフィックス 0x07 [4b] + 成 1b + 先後 1b) | 4 bits(プレフィックス 0x03 [3b] + 先後 1b) |
金(GOLD) | 6 bits(プレフィックス 0x0f [5b, 成ビット相当が 0 固定] + 先後 1b) | 4 bits(プレフィックス 0x07 [3b] + 先後 1b) |
角(BISHOP) | 8 bits(プレフィックス 0x1f [6b] + 成 1b + 先後 1b) | 6 bits(プレフィックス 0x0f [5b] + 先後 1b) |
飛(ROOK) | 8 bits(プレフィックス 0x3f [6b] + 成 1b + 先後 1b) | 6 bits(プレフィックス 0x1f [5b] + 先後 1b) |
PackedSfen では盤上の金が 5 ビットであるのに対し、HuffmanCodedPos では金も成ビット位置に 0 を持つ 6 ビット符号となっており、代わりに玉のいる 2 マス分の空きマスビット(2 ビット)を完全にスキップすることで、全駒配置時の最大ビット数がちょうど 256 ビットに収まるよう設計されている。
rsshogi は両フォーマットのデコーダとエンコーダを独立して備えているため、YaneuraOu 系データセットと Apery / dlshogi 系データセットをロスレスに相互変換できる。