39 releases (8 breaking)
Uses new Rust 2024
| 0.10.0 | May 3, 2026 |
|---|---|
| 0.8.0 | Apr 27, 2026 |
| 0.7.6 | Mar 16, 2026 |
#61 in Games
1.5MB
31K
SLoC
rshogi
Rust で実装された将棋ライブラリです。YaneuraOu の Rust 版を目指し、高速かつ安全な将棋プリミティブを提供します。
Python バインディング (rshogi-py) も提供しています。
設計思想
1. 安全性優先
- パフォーマンス上必要な箇所に限り
unsafeを使用し、SAFETY コメントとテストで妥当性を担保 - コンパイル時に検出可能なエラーは型システムで表現
- 実行時エラーは
Result型で明示的に扱う
2. 正確性
- YaneuraOu の合法手生成との互換性テストで正確性を担保
- Perft テストによる手生成の網羅的検証
- 千日手・入玉宣言勝ちなど将棋固有のルールを正しく実装
3. パフォーマンス
- ビットボードによる高速な盤面表現
- Magic Bitboard / PEXT による効率的なスライダー攻撃計算
- 差分更新による Zobrist ハッシュ計算
- SIMD 最適化(ビルド時に
target-feature=+avx2が有効な x86_64 では AVX2 実装を使用)
4. 再利用性
- Rust と Python の両方から利用可能
- エンジンや評価関数とは分離し、純粋な局面管理に専念
- 明確な API 境界でダウンストリームプロジェクトに組み込みやすい
アーキテクチャ
rshogi
├── types - 基本型(Color, Square, Move, Bitboard, Hand, ...)
├── labels - 学習や推論で利用するラベル変換
├── board - 盤面管理と合法手生成
│ ├── Position - 盤面状態の管理
│ ├── movegen - 合法手生成
│ ├── attack_tables - 利きテーブル
│ └── ...
├── records - 棋譜フォーマット(KIF, CSA, JKF, SFEN, sbinpack, ...)
├── book - 定跡管理(MemoryBook, StaticBook)
└── mate - 詰み判定(1手詰め)
クイックスタート
盤面操作
use rshogi::board::{self, Position};
use rshogi::types::Move;
// 任意: 初回アクセス前にテーブルを warm-up する
board::init();
// 平手初期局面
let mut pos = board::hirate_position();
// SFEN から局面を構築
let mut pos = board::position_from_sfen(
"lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1"
).unwrap();
// 指し手の適用
let mv16 = Move::from_usi("7g7f").unwrap();
let m = pos.move32_from_move(mv16);
pos.apply_move32(m);
// 指し手の取り消し
pos.undo_move32(m);
// SFEN 出力
println!("{}", pos.to_sfen(None));
SVG 描画
use rshogi::board;
use rshogi::types::Move;
board::init();
let mut pos = board::hirate_position();
let mv = pos.move32_from_move(Move::from_usi("7g7f").unwrap());
pos.apply_move32(mv);
let svg = pos.to_svg(Some(mv), 1.0);
assert!(svg.starts_with("<svg"));
raw-state import/export
use rshogi::board::{self, position_from_position_state};
board::init();
let pos = board::hirate_position();
let mut state = board::generate_position_state(&pos);
state.ply = 42;
let restored = position_from_position_state(&state);
assert_eq!(board::generate_position_state(&restored), state);
assert!(restored.validate().is_ok());
PositionState は SFEN 文字列を経由しない raw-state 境界です。
外部で編集したデータを import した後は、必要に応じて validate() /
validate_all() で明示的に検証できます。
合法手生成
use rshogi::board::{self, Position, MoveList};
use rshogi::movegen::{Legal, generate_moves};
board::init();
let pos = board::hirate_position();
// 合法手を生成
let mut moves = MoveList::new();
generate_moves::<Legal>(&pos, &mut moves);
for m in moves.iter() {
println!("{}", m.to_usi());
}
// 合法手の数
println!("合法手数: {}", moves.len());
policy ラベル変換
use rshogi::labels::policy::{CompactMoveLabel, MoveLabel, MoveLabelClass};
use rshogi::types::{Color, Move, Square};
let mv = Move::from_usi("7g7f").unwrap();
let label = MoveLabel::from_move(mv, Color::BLACK).unwrap();
assert_eq!(label.class(), MoveLabelClass::Up);
assert_eq!(label.to_sq(), Square::from_usi("7f").unwrap());
let compact = CompactMoveLabel::from_move(mv, Color::BLACK).unwrap();
assert_eq!(compact.expand(), label);
棋譜の読み込み
use rshogi::records::formats::kif;
use rshogi::types::GameResult;
let kif_text = r#"
手合割:平手
手数----指手---------消費時間--
1 7六歩(77)
2 3四歩(33)
まで2手で中断
"#;
let record = kif::parse_kif_str(kif_text).unwrap();
println!("手数: {}", record.move_count());
println!("結果: {:?}", record.result());
assert_eq!(record.result(), GameResult::Paused);
assert!(record.main_terminal().is_some());
棋譜のエンコードと走査
use rshogi::records::formats::{kif, traversal, TextEncoding};
use rshogi::records::record::GameRecordEntry;
let kif_text = "手合割:平手\n手数----指手---------消費時間--\n 1 7六歩(77)\nまで1手で中断\n";
let record = kif::parse_kif_str(kif_text).unwrap();
let encoded = kif::export_kif_bytes(&record, TextEncoding::Utf8).unwrap();
assert!(!encoded.had_unmappable_chars());
traversal::traverse_with_position(&record, |node| {
if let GameRecordEntry::Move(mv_record) = node.entry {
println!("{}: {}", node.ply, mv_record.mv().to_usi());
}
true
})
.unwrap();
定跡の利用
use rshogi::board::{self, Position};
use rshogi::book::{Book, MemoryBook, BookBuilder, book_key_from_position};
board::init();
let pos = board::hirate_position();
// 定跡を構築(GameRecord から)
let mut builder = BookBuilder::default();
// builder.add_record(&record);
let book = builder.into_memory();
// 定跡を検索
let key = book_key_from_position(&pos);
if let Some(entry) = book.get(key) {
for mv in entry.moves() {
println!("{}: {}", mv.mv().to_usi(), mv.score());
}
}
モジュール詳細
types - 基本型
将棋で使用する基本的な型を定義します。
| 型 | 説明 |
|---|---|
Color |
手番(先手 BLACK / 後手 WHITE) |
Square |
マス(0-80、9x9 盤面) |
File |
筋(1-9) |
Rank |
段(一-九) |
PieceType |
駒種(歩、香、桂、銀、金、角、飛、玉、と、... ) |
Piece |
駒(駒種 + 手番) |
Hand |
持ち駒(各駒種の枚数をビットパック) |
Move32 |
32bit 指し手(移動元・移動先・成り・駒打ちなど) |
Move |
16bit 指し手(コンパクト版) |
Bitboard |
81bit ビットボード |
GameResult |
対局結果(勝敗、千日手、入玉宣言など) |
labels - 学習向けラベル
局面非依存で使える policy ラベル変換を提供します。
policy::MoveLabel: 手番を先手視点に正規化した 2187 クラス(27x81)policy::CompactMoveLabel: 構造的に現れうる 1496 クラスに圧縮したラベルpolicy::MoveLabelClass: 通常移動 20 種 + 駒打ち 7 種のラベルクラスpolicy::MOVE_LABEL_TO_COMPACT/policy::COMPACT_TO_MOVE_LABEL: 変換テーブル
CompactMoveLabel は「局面で合法」ではなく、空盤面の移動・成り・駒打ち制約から見て
構造的に現れうるラベルだけを残したものです。
board - 盤面管理
Position 構造体で盤面状態を管理します。
SFEN 文字列を経由しない raw-state の import/export には
PositionState, generate_position_state(), generate_sfen_from_position_state(),
position_from_position_state(), Position::set_position_state() を使用します。
外部入力をそのまま反映した場合は validate() / validate_all() で
ルール妥当性を明示的に確認できます。
主要メソッド:
| メソッド | 説明 |
|---|---|
apply_move32(m) |
指し手を適用(Move32) |
undo_move32(m) |
指し手を取り消し(Move32) |
is_legal_move32(m) |
合法手か判定(Move32) |
checkers() |
王手している駒のビットボード |
check_squares(pt) |
指定駒種で王手可能な候補マス(キャッシュ) |
blockers_for_king(color) |
玉を守るブロッカー(pin候補を含む) |
is_in_check() |
王手されているか |
is_mated() |
詰んでいるか |
validate() / validate_all() |
盤面の妥当性を検証 |
to_sfen(ply) |
SFEN 文字列を生成 |
board_key() |
盤上配置 + 手番の Zobrist キー |
board_key_after(m) |
指し手適用後の board_key を計算 |
key() |
Zobrist ハッシュキー |
探索ホットパス向け cache view
王手判定やピン判定で複数の tactical cache をまとめて読みたい場合は、
Position::current_state_cache() から StateCacheView を取得できます。
check_squares() は配列全体をコピーせず共有参照を返すため、探索ホットパスで
checkers / check_squares / blockers_for_king / pinners を軽く扱えます。
use rshogi::board;
use rshogi::types::{Color, PieceType};
board::init();
let pos = board::hirate_position();
let cache = pos.current_state_cache();
let gives_rook_check = cache.check_square(PieceType::ROOK);
let blockers = cache.blockers_for_king(Color::BLACK);
let pinners = cache.pinners(Color::WHITE);
assert_eq!(cache.checkers(), pos.checkers());
assert_eq!(cache.check_squares().get(PieceType::ROOK), gives_rook_check);
assert_eq!(blockers, pos.blockers_for_king(Color::BLACK));
assert_eq!(pinners, pos.pinners(Color::WHITE));
StateCacheView は取得時点の current state を表す共有 borrow です。
apply_move32() / apply_null_move() / undo_*() を挟んだ後は再取得してください。
board::movegen - 指し手生成
ジェネリクスによる型安全な手生成 API を提供します。
use rshogi::board::{self, MoveList};
use rshogi::movegen::{generate_moves, Legal, Captures, Evasions};
board::init();
let pos = board::hirate_position();
let mut moves = MoveList::new();
// 全合法手
generate_moves::<Legal>(&pos, &mut moves);
// 駒を取る手のみ
generate_moves::<Captures>(&pos, &mut moves);
// 王手回避の pseudo-legal 手(王手されている場合)
generate_moves::<Evasions>(&pos, &mut moves);
generate_moves::<Evasions>() / generate_moves::<EvasionsAll>() は legal-only ではなく、
split legality 後段で落ちる回避手を含みうります。王手回避を legal-only で直接取りたい場合は
generate_legal_evasions_move32() または generate_legal_evasions_all_move32() を使います。
Move32(駒情報付き 32bit 指し手)のリストを直接生成することもできます。
また、独自コンテナへ直接流し込む Move32Sink と、分岐数だけ数える
count_moves::<T>() も使えます。
use rshogi::board::{self, Move32List};
use rshogi::movegen::{count_moves, generate_legal_all_move32, generate_legal_evasions_all_move32};
board::init();
let pos = board::hirate_position();
let mut moves = Move32List::new();
generate_legal_all_move32(&pos, &mut moves);
let branching = count_moves::<rshogi::movegen::Legal>(&pos);
assert_eq!(branching, moves.len());
let mut legal_evasions = Move32List::new();
if !pos.checkers().is_empty() {
generate_legal_evasions_all_move32(&pos, &mut legal_evasions);
}
手生成タイプ:
| タイプ | 説明 |
|---|---|
Legal |
全合法手 |
Captures |
駒を取る手 |
Quiets |
駒を取らない手 |
Evasions |
王手回避の pseudo-legal 手 |
Checks |
王手をかける手 |
Recaptures |
取り返し手 |
board::attack_tables - 利き/王手候補テーブル
低レベル API として、王手候補マスの取得関数を提供します。
use rshogi::board::{self, check_candidate_bb};
use rshogi::types::{Color, PieceType, Square};
board::init();
let king_sq = Square::from_usi("5a").unwrap();
let candidates = check_candidate_bb(Color::BLACK, PieceType::ROOK, king_sq);
assert!(candidates.count() > 0);
records - 棋譜フォーマット
複数の棋譜フォーマットの読み書きをサポートします。
| フォーマット | パース | 出力 |
|---|---|---|
| KIF | ○ | ○ |
| KI2 | ○ | ○ |
| CSA | ○ | ○ |
| JKF | - | ○ |
| SFEN | ○ | ○ |
| SBINPACK | ○ | ○ |
GameRecord の終局情報は result() -> GameResult で取得します。
終局行そのもの(投了・千日手・最大手数などの種別、終局コメント、終局消費時間)は
main_terminal() -> Option<&SpecialMoveRecord> に保持されます。
終局特殊手が存在しない棋譜も保持でき、その場合 result() は GameResult::Invalid、
main_terminal() は None になります。
KIF/KI2 の exporter は、ShogiHome/tsshogi 系の読み手と shogi-validator を
意識した実務互換を優先しています。変化手順と終局要約 (まで...) が両方ある
棋譜では、変化を先に出して終局要約を最後に出力するため、
export_* → parse_* でも main_terminal() を保持しやすくなっています。
終局特殊手がない場合、KIF/KI2/CSA exporter は終局行を省略して
本手順のみを書き出します。
CSA は CSA 3.0 の定義に合わせて '*... を「プログラムが読むコメント」として扱い、
plain な '... は読み飛ばします('CSA encoding=... は文字コード宣言)。
KIF parser は 1 7六歩(77) (0:27/00:00:27) のような指し手行末尾の消費時間も
MoveRecord.time_ms に取り込みます。
KIF/KI2 parser は連続するコメント行を \n 区切りで保持し、初手前コメントは
GameRecord.initial_comment() に保持します。exporter はこのコメントを
1 手目の前に書き戻し、JKF exporter ではルート moves[0].comments に出力します。
CSA parser も '*... コメントを GameRecord.initial_comment() に保持し、exporter は
そのコメントを手番行直後へ '*... として書き戻します。互換性のため、手番行より前の
'*... も受理して開始局面コメントへ正規化します。
GameRecordMetadata::comment() は自由コメントではなく、KIF の 備考 / CSA の $NOTE /
JKF header の 備考 に対応するメタ情報として扱います。
各指し手のエンジン解析情報は MoveEngineInfo 構造体で管理されます。
評価値・探索深度・ノード数のほか、extras フィールドで任意のキー/値ペア
(MoveEngineExtraValue: String / Int / Float / Bool)を保持できます。
Rust core では追加で以下の補助 API を提供します。
TextEncoding,ExportOptions,EncodedText: KIF / KI2 / CSA を UTF-8 または Shift_JIS のバイト列として出力traversal::traverse_with_position():GameRecordを DFS で走査し、各ノードの局面を計算traversal::position_at(): 任意ノード時点の局面を復元
book - 定跡
Zobrist ハッシュによる高速な定跡検索を提供します。
MemoryBook: メモリ内で定跡を管理StaticBook: 静的にコンパイルされた定跡BookBuilder:GameRecordから定跡を構築
mate - 詰み判定
テーブル駆動の 1 手詰め判定ルーチンを提供します。
solve_mate_in_one(): 1手詰め判定
互換性
YaneuraOu 互換
- 内部のビットボード表現、Zobrist ハッシュ、手のエンコーディングは YaneuraOu と互換
Move(16bit)は YaneuraOu のMove16と同一のビットレイアウト- Perft 結果が YaneuraOu と一致することを検証済み
policy ラベル互換
labels::policy::MoveLabelは、cshogi/dlshogi 系で広く使われる 27x81 の policy ラベル配置と互換- 後手番の手は 180 度回転してからラベル化し、常に先手視点で比較できる
CompactMoveLabelは 2187 クラスから構造的に無効な 691 クラスを除いた 1496 クラス
ベンチマーク(perft)
80 局面で perft(depth=4)を計測した参考値です。
実行方法
RUSTFLAGS="-C target-feature=+avx2" \
scripts/bench_perft_avg.sh --depth 4 --repeat 5
AVX2 実装は runtime dispatch ではなくビルド時の target feature で選択されます。
AVX2 非対応環境で実行する配布物には target-feature=+avx2 を付けないでください。
参考結果
x86_64 / AVX2 環境、PGO なし、5 回平均の結果です。
| エンジン | avg NPS |
|---|---|
| rshogi (AVX2) | 約 248M |
| YaneuraOu MATERIAL (AVX2) | 約 289M |
詳細は perf/results.md を参照してください。
結果はハードウェアや OS によって変動します。
ライセンス
MIT License
Dependencies
~3.5–5MB
~155K SLoC