# rshogi

Rust で実装された将棋ライブラリです。[YaneuraOu](https://github.com/yaneurao/YaneuraOu) の
Rust 版を目指し、高速かつ安全な将棋プリミティブを提供します。

Python バインディング ([rshogi-py](https://pypi.org/project/rshogi-py/)) も提供しています。

## 設計思想

### 1. 安全性優先

- パフォーマンス上必要な箇所に限り `unsafe` を使用し、SAFETY コメントとテストで妥当性を担保
- コンパイル時に検出可能なエラーは型システムで表現
- 実行時エラーは `Result` 型で明示的に扱う

### 2. 正確性

- YaneuraOu の合法手生成との互換性テストで正確性を担保
- Perft テストによる手生成の網羅的検証
- 千日手・入玉宣言勝ちなど将棋固有のルールを正しく実装

### 3. パフォーマンス

- ビットボードによる高速な盤面表現
- Magic Bitboard / PEXT による効率的なスライダー攻撃計算
- 差分更新による Zobrist ハッシュ計算
- SIMD 最適化（AVX2 対応）

### 4. 再利用性

- Rust と Python の両方から利用可能
- エンジンや評価関数とは分離し、純粋な局面管理に専念
- 明確な API 境界でダウンストリームプロジェクトに組み込みやすい

## アーキテクチャ

```text
rshogi
├── types     - 基本型（Color, Square, Move, Bitboard, Hand, ...）
├── labels    - 学習や推論で利用するラベル変換
├── board     - 盤面管理と合法手生成
│   ├── Position      - 盤面状態の管理
│   ├── movegen       - 合法手生成
│   ├── attack_tables - 利きテーブル
│   └── ...
├── records   - 棋譜フォーマット（KIF, CSA, JKF, SFEN, sbinpack, ...）
├── book      - 定跡管理（MemoryBook, StaticBook）
└── mate      - 詰み判定（1手詰め）
```

## クイックスタート

### 盤面操作

```rust
use rshogi::board::{self, Position};
use rshogi::types::Move;

// 初期化（プログラム起動時に1回）
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 描画

```rust
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

```rust
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()` で明示的に検証できます。

### 合法手生成

```rust
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 ラベル変換

```rust
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);
```

### 棋譜の読み込み

```rust
use rshogi::records::formats::kif;
use rshogi::types::GameResult;

let kif_text = r#"
手合割：平手
手数----指手---------消費時間--
   1 ７六歩(77)
   2 ３四歩(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());
```

### 棋譜のエンコードと走査

```rust
use rshogi::records::formats::{kif, traversal, TextEncoding};
use rshogi::records::record::GameRecordEntry;

let kif_text = "手合割：平手\n手数----指手---------消費時間--\n   1 ７六歩(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();
```

### 定跡の利用

```rust
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 ハッシュキー |

### `board::movegen` - 合法手生成

ジェネリクスによる型安全な手生成 API を提供します。

```rust
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);

// 王手回避手（王手されている場合）
generate_moves::<Evasions>(&pos, &mut moves);
```

`Move32`（駒情報付き 32bit 指し手）のリストを直接生成することもできます。

```rust
use rshogi::board::{self, Move32List};
use rshogi::movegen::generate_legal_all_move32;

board::init();
let pos = board::hirate_position();

let mut moves = Move32List::new();
generate_legal_all_move32(&pos, &mut moves);
```

**手生成タイプ:**

| タイプ | 説明 |
|---|---|
| `Legal` | 全合法手 |
| `Captures` | 駒を取る手 |
| `Quiets` | 駒を取らない手 |
| `Evasions` | 王手回避手 |
| `Checks` | 王手をかける手 |
| `Recaptures` | 取り返し手 |

### `board::attack_tables` - 利き/王手候補テーブル

低レベル API として、王手候補マスの取得関数を提供します。

```rust
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 は `'*...` 形式でコメントを出力しつつ、parser 側では `'*...` と `'...` の
両方をコメントとして受理します（`'CSA encoding=...` は除く）。
KIF parser は `1 ７六歩(77) (0:27/00:00:27)` のような指し手行末尾の消費時間も
`MoveRecord.time_ms` に取り込みます。
KIF/KI2 parser は連続するコメント行を `\n` 区切りで保持し、初手前コメントも
最初の `MoveRecord.comment` に引き継ぎます。

各指し手のエンジン解析情報は `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）を計測した参考値です。

### 実行方法

```bash
RUSTFLAGS="-C target-feature=+avx2" \
  scripts/bench_perft_avg.sh --depth 4 --repeat 5
```

### 参考結果

x86_64 / AVX2 環境、PGO なし、5 回平均の結果です。

| エンジン | avg NPS |
|---|---|
| rshogi (AVX2) | 約 248M |
| YaneuraOu MATERIAL (AVX2) | 約 289M |

詳細は [`perf/results.md`](../../perf/results.md) を参照してください。

結果はハードウェアや OS によって変動します。

## ライセンス

MIT License
