#shogi

rshogi

Rust shogi library inspired by the Python cshogi API

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

MIT license

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::Invalidmain_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