pgn2 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +34 -3
- data/.github/workflows/native.yml +32 -0
- data/.github/workflows/publish.yml +44 -2
- data/.github/workflows/release-gems.yml +40 -0
- data/.github/workflows/release.yml +52 -5
- data/.gitignore +9 -1
- data/.rubocop.yml +46 -6
- data/CHANGELOG.md +148 -0
- data/Gemfile +3 -0
- data/NOTICE.md +21 -0
- data/README.md +107 -5
- data/Rakefile +35 -10
- data/TODO.md +64 -0
- data/bench/baseline_moves.txt +38 -4
- data/bench/baseline_parse.txt +4 -4
- data/bench/cross_check.rb +61 -0
- data/bench/legal_moves.rb +38 -0
- data/bench/perft.rb +32 -0
- data/bench/profile_moves.rb +93 -0
- data/docs/superpowers/plans/2026-08-13-attack-masks-plan.md +51 -0
- data/docs/superpowers/plans/2026-08-13-perf-internals-plan.md +883 -0
- data/docs/superpowers/plans/2026-08-13-rust-bitboard-perft-plan.md +2442 -0
- data/docs/superpowers/plans/2026-08-14-chessie-migration.md +722 -0
- data/docs/superpowers/plans/2026-08-14-rust-integration-plan.md +444 -0
- data/docs/superpowers/specs/2026-08-13-attack-masks-design.md +57 -0
- data/docs/superpowers/specs/2026-08-13-perf-internals-design.md +111 -0
- data/docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md +270 -0
- data/docs/superpowers/specs/2026-08-14-rust-integration-design.md +217 -0
- data/ext/pgn2_native/Cargo.lock +321 -0
- data/ext/pgn2_native/Cargo.toml +19 -0
- data/ext/pgn2_native/extconf.rb +8 -0
- data/ext/pgn2_native/pgn2-bitboard/Cargo.toml +10 -0
- data/ext/pgn2_native/pgn2-bitboard/src/board.rs +32 -0
- data/ext/pgn2_native/pgn2-bitboard/src/lib.rs +12 -0
- data/ext/pgn2_native/pgn2-bitboard/src/moves.rs +121 -0
- data/ext/pgn2_native/pgn2-bitboard/src/perft.rs +81 -0
- data/ext/pgn2_native/pgn2_native/Cargo.toml +11 -0
- data/ext/pgn2_native/pgn2_native/src/lib.rs +54 -0
- data/lib/pgn/bitboard.rb +13 -0
- data/lib/pgn/board.rb +64 -0
- data/lib/pgn/fen.rb +35 -46
- data/lib/pgn/game.rb +22 -13
- data/lib/pgn/move.rb +19 -15
- data/lib/pgn/move_calculator.rb +13 -1
- data/lib/pgn/notation.rb +24 -29
- data/lib/pgn/position.rb +60 -19
- data/lib/pgn/serializer.rb +13 -18
- data/lib/pgn/version.rb +1 -1
- data/lib/pgn/zobrist.rb +53 -0
- data/lib/pgn.rb +2 -0
- data/pgn2.gemspec +17 -10
- data/spec/bitboard_spec.rb +54 -0
- data/spec/board_spec.rb +53 -0
- data/spec/fen_spec.rb +65 -65
- data/spec/game_spec.rb +52 -15
- data/spec/lexer_spec.rb +5 -5
- data/spec/notation_spec.rb +5 -0
- data/spec/parser_spec.rb +8 -1
- data/spec/position_spec.rb +128 -27
- data/spec/serializer_spec.rb +4 -4
- data/spec/zobrist_spec.rb +46 -0
- metadata +97 -35
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
# Chessie Backend Migration Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Replace the hand-rolled `pgn2-bitboard` chess engine on `main`
|
|
6
|
+
with a thin adapter over the `chessie` crate (MPL-2.0), keeping the
|
|
7
|
+
`pgn2_native` magnus binding and the Ruby `PGN::Bitboard::Engine`
|
|
8
|
+
surface byte-for-byte identical and ~4–6× faster.
|
|
9
|
+
|
|
10
|
+
**Architecture:** `pgn2-bitboard` becomes a ~3-file adapter crate
|
|
11
|
+
(`board.rs`, `moves.rs`, `perft.rs` tests) that wraps `chessie::Game` and
|
|
12
|
+
exposes exactly the public API the `pgn2_native` binding already
|
|
13
|
+
consumes (`Board::from_fen`/`perft`/`legal_moves`, `moves::uci_parse`,
|
|
14
|
+
`Move::to_uci`/`same_target`, `MoveList`). The five hand-rolled
|
|
15
|
+
chess-logic modules (`square`, `piece`, `attacks`, `magics`, the old
|
|
16
|
+
`moves`/`legality`/`perft` impls) are deleted. The `pgn2_native`
|
|
17
|
+
binding crate is **not modified** — it keeps importing
|
|
18
|
+
`pgn2_bitboard::Board` and `pgn2_bitboard::moves::uci_parse`. The Ruby
|
|
19
|
+
surface (`PGN::Bitboard::Engine#perft`/`#legal_moves`/`#legal?`,
|
|
20
|
+
`PGN::Position#perft`/`#legal_moves` delegations, `bench/perft.rb`) is
|
|
21
|
+
unchanged. License compliance is handled by a `NOTICE`/license
|
|
22
|
+
declaration addition.
|
|
23
|
+
|
|
24
|
+
**Tech Stack:** Rust 2021, `chessie` 2.0 (MPL-2.0, pure-Rust, no C deps),
|
|
25
|
+
`magnus` 0.8, `rb_sys`, RSpec, `cargo test` (perft oracle).
|
|
26
|
+
|
|
27
|
+
## Global Constraints
|
|
28
|
+
|
|
29
|
+
- The `pgn2_native` binding crate source (`ext/pgn2_native/pgn2_native/src/lib.rs`)
|
|
30
|
+
is **not modified** — the adapter must preserve its consumed API
|
|
31
|
+
verbatim: `pgn2_bitboard::Board` (with `Board::from_fen(&str) -> Result<Board, String>`,
|
|
32
|
+
`Board::perft(&self, depth: u32) -> u64`, `Board::legal_moves(&self) -> MoveList`,
|
|
33
|
+
and `#[derive]` of at least `Default`), `pgn2_bitboard::moves::uci_parse(&str) -> Option<Move>`,
|
|
34
|
+
and `Move` (with `Move::to_uci(self) -> String`, `Move::same_target(self, Move) -> bool`,
|
|
35
|
+
`Move: Copy`).
|
|
36
|
+
- The pure-Ruby suite and the byte-identical FEN/PGN guarantees stay intact;
|
|
37
|
+
no change to `lib/pgn/board.rb`, `notation.rb`, `move_calculator.rb`,
|
|
38
|
+
`position.rb` delegations, or `bench/perft.rb`.
|
|
39
|
+
- The published perft values remain the oracle; `spec/bitboard_spec.rb`
|
|
40
|
+
and `ext/.../pgn2-bitboard/src/perft.rs` both stay green unchanged.
|
|
41
|
+
- `chessie` is MPL-2.0; the gem stays MIT, but MPL attribution is added
|
|
42
|
+
(Task 2). The gem's own source is not relicensed.
|
|
43
|
+
- Commit per task. `Cargo.lock` is committed (gemspec uses `git ls-files`).
|
|
44
|
+
- Only strings and integers cross the Ruby↔Rust boundary (unchanged).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## File Structure
|
|
49
|
+
|
|
50
|
+
**Deleted** (hand-rolled chess logic, replaced by `chessie`):
|
|
51
|
+
- `ext/pgn2_native/pgn2-bitboard/src/square.rs`
|
|
52
|
+
- `ext/pgn2_native/pgn2-bitboard/src/piece.rs`
|
|
53
|
+
- `ext/pgn2_native/pgn2-bitboard/src/attacks.rs`
|
|
54
|
+
- `ext/pgn2_native/pgn2-bitboard/src/magics.rs`
|
|
55
|
+
|
|
56
|
+
**Rewritten** (adapter):
|
|
57
|
+
- `ext/pgn2_native/pgn2-bitboard/src/lib.rs` — re-exports only.
|
|
58
|
+
- `ext/pgn2_native/pgn2-bitboard/src/board.rs` — `Board { game: chessie::Game }`
|
|
59
|
+
+ `from_fen`/`perft`/`legal_moves`.
|
|
60
|
+
- `ext/pgn2_native/pgn2-bitboard/src/moves.rs` — `Move`/`MoveList` adapter +
|
|
61
|
+
`uci_parse`.
|
|
62
|
+
- `ext/pgn2_native/pgn2-bitboard/src/perft.rs` — perft oracle tests only
|
|
63
|
+
(no engine impl, no `attacks::init`).
|
|
64
|
+
|
|
65
|
+
**Modified:**
|
|
66
|
+
- `ext/pgn2_native/pgn2-bitboard/Cargo.toml` — add `chessie = "2.0"`.
|
|
67
|
+
- `ext/pgn2_native/Cargo.lock` — regenerated (chessie + chessie_types + anyhow + arrayvec).
|
|
68
|
+
- `ext/pgn2_native/Cargo.toml` — update the stale `[profile.test]` comment.
|
|
69
|
+
- `pgn2.gemspec` — declare MPL-2.0 alongside MIT.
|
|
70
|
+
- `NOTICE.md` (new) — chessie MPL-2.0 attribution + source pointer.
|
|
71
|
+
- `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`,
|
|
72
|
+
`README.md`, `CHANGELOG.md` — reflect chessie backend.
|
|
73
|
+
- `lib/pgn/bitboard.rb`, `.github/workflows/native.yml` — comment cleanup.
|
|
74
|
+
|
|
75
|
+
**Unchanged (verified, not edited):**
|
|
76
|
+
- `ext/pgn2_native/pgn2_native/src/lib.rs` (binding), `pgn2_native/Cargo.toml`,
|
|
77
|
+
`lib/pgn/position.rb`, `bench/perft.rb`, `spec/bitboard_spec.rb`,
|
|
78
|
+
`.github/workflows/release-gems.yml`.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Task 1: Replace `pgn2-bitboard` with a `chessie`-backed adapter
|
|
83
|
+
|
|
84
|
+
The migration core. One cohesive change — the adapter's pieces can't
|
|
85
|
+
be independently rejected (Board/Move/legal_moves are one contract). The
|
|
86
|
+
existing perft oracle (`perft.rs`) and `spec/bitboard_spec.rb` are the
|
|
87
|
+
regression guards; they stay green.
|
|
88
|
+
|
|
89
|
+
**Files:**
|
|
90
|
+
- Create: `ext/pgn2_native/pgn2-bitboard/src/lib.rs` (rewrite)
|
|
91
|
+
- Create: `ext/pgn2_native/pgn2-bitboard/src/board.rs` (rewrite)
|
|
92
|
+
- Create: `ext/pgn2_native/pgn2-bitboard/src/moves.rs` (rewrite)
|
|
93
|
+
- Create: `ext/pgn2_native/pgn2-bitboard/src/perft.rs` (rewrite, tests only)
|
|
94
|
+
- Delete: `square.rs`, `piece.rs`, `attacks.rs`, `magics.rs`
|
|
95
|
+
- Modify: `ext/pgn2_native/pgn2-bitboard/Cargo.toml`
|
|
96
|
+
- Regenerate: `ext/pgn2_native/Cargo.lock`
|
|
97
|
+
|
|
98
|
+
**Interfaces:**
|
|
99
|
+
- Consumes: `chessie 2.0` — `chessie::Game` (`Game::from_fen(&str) -> Result<Game>`,
|
|
100
|
+
`Game::perft(&self, usize) -> u64`, `Game: Clone + Copy + PartialEq + Eq + Default`),
|
|
101
|
+
`chessie::MoveGenIter` (`MoveGenIter::new(&Game) -> impl Iterator<Item = chessie::Move>`),
|
|
102
|
+
`chessie::Move` (`Move::from(&self) -> Square`, `Move::to(&self) -> Square`,
|
|
103
|
+
`Move::promotion(&self) -> Option<PieceKind>`), `chessie::Square` (`Square::index(&self) -> usize`),
|
|
104
|
+
`chessie::PieceKind` (`{Pawn, Knight, Bishop, Rook, Queen, King}`).
|
|
105
|
+
- Produces (preserved verbatim for the binding): `Board` (`from_fen`, `perft`, `legal_moves`,
|
|
106
|
+
`Default`), `Move` (`to_uci`, `same_target`, `Copy`), `MoveList(pub Vec<Move>)` with `iter()`,
|
|
107
|
+
`moves::uci_parse(&str) -> Option<Move>`.
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 1: Add the `chessie` dependency**
|
|
110
|
+
|
|
111
|
+
`ext/pgn2_native/pgn2-bitboard/Cargo.toml`:
|
|
112
|
+
```toml
|
|
113
|
+
[package]
|
|
114
|
+
name = "pgn2-bitboard"
|
|
115
|
+
version = "0.1.0"
|
|
116
|
+
edition = "2021"
|
|
117
|
+
|
|
118
|
+
[lib]
|
|
119
|
+
crate-type = ["lib"]
|
|
120
|
+
|
|
121
|
+
[dependencies]
|
|
122
|
+
chessie = "2.0"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- [ ] **Step 2: Rewrite `lib.rs` to re-export only the adapter**
|
|
126
|
+
|
|
127
|
+
`ext/pgn2_native/pgn2-bitboard/src/lib.rs`:
|
|
128
|
+
```rust
|
|
129
|
+
//! Thin adapter over the `chessie` crate exposing the small surface the
|
|
130
|
+
//! `pgn2_native` magnus binding consumes. No chess logic lives here —
|
|
131
|
+
//! `chessie` is the engine. Kept as a separate crate so the perft oracle
|
|
132
|
+
//! stays testable in pure Rust (`cargo test -p pgn2-bitboard`) with no
|
|
133
|
+
//! Ruby in the loop.
|
|
134
|
+
|
|
135
|
+
pub mod board;
|
|
136
|
+
pub mod moves;
|
|
137
|
+
pub mod perft;
|
|
138
|
+
|
|
139
|
+
pub use board::Board;
|
|
140
|
+
pub use moves::{Move, MoveList};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- [ ] **Step 3: Write the `Board` adapter**
|
|
144
|
+
|
|
145
|
+
`ext/pgn2_native/pgn2-bitboard/src/board.rs`:
|
|
146
|
+
```rust
|
|
147
|
+
use crate::moves::{Move, MoveList};
|
|
148
|
+
|
|
149
|
+
/// A chess position backed by `chessie::Game`. Holds no chess logic of
|
|
150
|
+
/// its own; every operation delegates. `Copy` because `chessie::Game`
|
|
151
|
+
/// is `Copy`; `Default` because the magnus `Engine` wraps it in a
|
|
152
|
+
/// `RefCell` and `#[derive(Default)]`s it.
|
|
153
|
+
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
|
154
|
+
pub struct Board {
|
|
155
|
+
game: chessie::Game,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
impl Board {
|
|
159
|
+
pub fn from_fen(fen: &str) -> Result<Board, String> {
|
|
160
|
+
chessie::Game::from_fen(fen)
|
|
161
|
+
.map(|game| Board { game })
|
|
162
|
+
.map_err(|e| e.to_string())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub fn perft(&self, depth: u32) -> u64 {
|
|
166
|
+
self.game.perft(depth as usize)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub fn legal_moves(&self) -> MoveList {
|
|
170
|
+
use chessie::MoveGenIter;
|
|
171
|
+
let moves: Vec<Move> = MoveGenIter::new(&self.game)
|
|
172
|
+
.map(Move::from_chessie)
|
|
173
|
+
.collect();
|
|
174
|
+
MoveList(moves)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> `chessie::Game` derives `Clone, Copy, PartialEq, Eq` (verified in
|
|
180
|
+
> `chessie-2.0.0/src/game.rs:28`) but **not** `Debug`, so `Board` does
|
|
181
|
+
> not derive `Debug` either — nothing the binding or the oracle uses
|
|
182
|
+
> requires `Board: Debug`.
|
|
183
|
+
|
|
184
|
+
- [ ] **Step 4: Write the `Move` / `MoveList` adapter + `uci_parse`**
|
|
185
|
+
|
|
186
|
+
`ext/pgn2_native/pgn2-bitboard/src/moves.rs`:
|
|
187
|
+
```rust
|
|
188
|
+
/// Adapter move carrying exactly what `to_uci` and `same_target` need:
|
|
189
|
+
/// from-square, to-square, and optional promotion kind. No flag bits,
|
|
190
|
+
/// so a position-free `uci_parse` can produce a comparable token — this
|
|
191
|
+
/// preserves the existing `same_target` semantics (match on
|
|
192
|
+
/// from + to + promo only), so `legal?("e1g1")` finds the castle and
|
|
193
|
+
/// `legal?("e7e8q")` finds that exact promotion.
|
|
194
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
195
|
+
pub struct Move {
|
|
196
|
+
from: u8, // 0..=63, index = rank * 8 + file
|
|
197
|
+
to: u8,
|
|
198
|
+
promo: Option<Promo>,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
202
|
+
enum Promo { Knight, Bishop, Rook, Queen }
|
|
203
|
+
|
|
204
|
+
impl Move {
|
|
205
|
+
/// Build an adapter `Move` from a `chessie::Move` (a legal move).
|
|
206
|
+
pub(crate) fn from_chessie(m: chessie::Move) -> Self {
|
|
207
|
+
Move {
|
|
208
|
+
from: m.from().index() as u8,
|
|
209
|
+
to: m.to().index() as u8,
|
|
210
|
+
promo: m.promotion().map(|kind| match kind {
|
|
211
|
+
chessie::PieceKind::Knight => Promo::Knight,
|
|
212
|
+
chessie::PieceKind::Bishop => Promo::Bishop,
|
|
213
|
+
chessie::PieceKind::Rook => Promo::Rook,
|
|
214
|
+
chessie::PieceKind::Queen => Promo::Queen,
|
|
215
|
+
// Pawns/Kings never appear as a promotion kind.
|
|
216
|
+
_ => unreachable!("non-promotion PieceKind in Move::promotion"),
|
|
217
|
+
}),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// UCI string: `"e2e4"`, `"e1g1"` (castle, king from→to),
|
|
222
|
+
/// `"e7e8q"` (promotion). Equal to `chessie::Move::to_uci` for
|
|
223
|
+
/// every legal move (castle and en-passant both reduce to from+to
|
|
224
|
+
/// in UCI), so the sorted-UCI output is unchanged.
|
|
225
|
+
pub fn to_uci(self) -> String {
|
|
226
|
+
let mut s = String::with_capacity(5);
|
|
227
|
+
s.push_str(&sq_name(self.from));
|
|
228
|
+
s.push_str(&sq_name(self.to));
|
|
229
|
+
if let Some(p) = self.promo {
|
|
230
|
+
s.push(match p {
|
|
231
|
+
Promo::Knight => 'n',
|
|
232
|
+
Promo::Bishop => 'b',
|
|
233
|
+
Promo::Rook => 'r',
|
|
234
|
+
Promo::Queen => 'q',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
s
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Match on from + to + promo (the pre-existing semantics).
|
|
241
|
+
pub fn same_target(self, other: Move) -> bool {
|
|
242
|
+
self.from == other.from && self.to == other.to && self.promo == other.promo
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Thin wrapper around `Vec<Move>`; the binding calls `.iter()` on it.
|
|
247
|
+
pub struct MoveList(pub Vec<Move>);
|
|
248
|
+
impl MoveList {
|
|
249
|
+
pub fn iter(&self) -> std::slice::Iter<'_, Move> {
|
|
250
|
+
self.0.iter()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Parse a UCI string into a `Move` token **without** a position.
|
|
255
|
+
/// Only `to_uci`/`same_target` consume the result, so from + to + promo
|
|
256
|
+
/// is all that is needed. Returns `None` on any malformed input.
|
|
257
|
+
pub fn uci_parse(s: &str) -> Option<Move> {
|
|
258
|
+
let b = s.as_bytes();
|
|
259
|
+
if b.len() < 4 { return None; }
|
|
260
|
+
let from = parse_sq(&b[0..2])?;
|
|
261
|
+
let to = parse_sq(&b[2..4])?;
|
|
262
|
+
let promo = if b.len() >= 5 {
|
|
263
|
+
Some(match b[4] {
|
|
264
|
+
b'n' => Promo::Knight,
|
|
265
|
+
b'b' => Promo::Bishop,
|
|
266
|
+
b'r' => Promo::Rook,
|
|
267
|
+
b'q' => Promo::Queen,
|
|
268
|
+
_ => return None,
|
|
269
|
+
})
|
|
270
|
+
} else { None };
|
|
271
|
+
Some(Move { from, to, promo })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn parse_sq(t: &[u8]) -> Option<u8> {
|
|
275
|
+
let f = t[0].checked_sub(b'a')?;
|
|
276
|
+
let r = t[1].checked_sub(b'1')?;
|
|
277
|
+
if f > 7 || r > 7 { return None; }
|
|
278
|
+
Some(r * 8 + f)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn sq_name(idx: u8) -> String {
|
|
282
|
+
let file = (b'a' + (idx & 7)) as char;
|
|
283
|
+
let rank = (b'1' + (idx >> 3)) as char;
|
|
284
|
+
let mut s = String::with_capacity(2);
|
|
285
|
+
s.push(file);
|
|
286
|
+
s.push(rank);
|
|
287
|
+
s
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- [ ] **Step 5: Rewrite `perft.rs` as the oracle tests (no engine impl)**
|
|
292
|
+
|
|
293
|
+
`ext/pgn2_native/pgn2-bitboard/src/perft.rs`:
|
|
294
|
+
```rust
|
|
295
|
+
//! Perft oracle — the published node counts are the test. The chess
|
|
296
|
+
//! logic lives in `chessie`; this file only exercises the adapter's
|
|
297
|
+
//! `Board::from_fen` + `Board::perft` end-to-end in pure Rust (no Ruby).
|
|
298
|
+
|
|
299
|
+
#[cfg(test)]
|
|
300
|
+
mod tests {
|
|
301
|
+
use crate::board::Board;
|
|
302
|
+
|
|
303
|
+
struct Case { fen: &'static str, depth: u32, nodes: u64 }
|
|
304
|
+
|
|
305
|
+
fn run(c: Case) {
|
|
306
|
+
let b = Board::from_fen(c.fen).unwrap();
|
|
307
|
+
assert_eq!(b.perft(c.depth), c.nodes, "fen={} depth={}", c.fen, c.depth);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[test]
|
|
311
|
+
fn perft_startpos() {
|
|
312
|
+
let f = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
313
|
+
run(Case { fen: f, depth: 1, nodes: 20 });
|
|
314
|
+
run(Case { fen: f, depth: 2, nodes: 400 });
|
|
315
|
+
run(Case { fen: f, depth: 3, nodes: 8902 });
|
|
316
|
+
run(Case { fen: f, depth: 4, nodes: 197281 });
|
|
317
|
+
run(Case { fen: f, depth: 5, nodes: 4865609 });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[test]
|
|
321
|
+
fn perft_kiwipete() {
|
|
322
|
+
let f = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
|
|
323
|
+
run(Case { fen: f, depth: 1, nodes: 48 });
|
|
324
|
+
run(Case { fen: f, depth: 2, nodes: 2039 });
|
|
325
|
+
run(Case { fen: f, depth: 3, nodes: 97862 });
|
|
326
|
+
run(Case { fen: f, depth: 4, nodes: 4085603 });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn perft_pos3() {
|
|
331
|
+
let f = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1";
|
|
332
|
+
run(Case { fen: f, depth: 1, nodes: 14 });
|
|
333
|
+
run(Case { fen: f, depth: 4, nodes: 43238 });
|
|
334
|
+
run(Case { fen: f, depth: 5, nodes: 674624 });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn perft_pos4() {
|
|
339
|
+
let f = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1";
|
|
340
|
+
run(Case { fen: f, depth: 1, nodes: 6 });
|
|
341
|
+
run(Case { fen: f, depth: 3, nodes: 9467 });
|
|
342
|
+
run(Case { fen: f, depth: 4, nodes: 422333 });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[test]
|
|
346
|
+
fn perft_pos5() {
|
|
347
|
+
let f = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8";
|
|
348
|
+
run(Case { fen: f, depth: 3, nodes: 62379 });
|
|
349
|
+
run(Case { fen: f, depth: 4, nodes: 2103487 });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn perft_pos6() {
|
|
354
|
+
let f = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10";
|
|
355
|
+
run(Case { fen: f, depth: 3, nodes: 89890 });
|
|
356
|
+
run(Case { fen: f, depth: 4, nodes: 3894594 });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#[test]
|
|
360
|
+
#[ignore] // slow; run with `cargo test -- --ignored`
|
|
361
|
+
fn perft_deep() {
|
|
362
|
+
let s = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
363
|
+
run(Case { fen: s, depth: 6, nodes: 119060324 });
|
|
364
|
+
let k = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
|
|
365
|
+
run(Case { fen: k, depth: 5, nodes: 193690690 });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// The old `make_unmake_symmetry_via_perft` test is intentionally
|
|
369
|
+
// removed: it exercised the hand-rolled make/unmake internals, which
|
|
370
|
+
// no longer exist. chessie's own test suite covers its make/unmake.
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- [ ] **Step 6: Delete the hand-rolled chess-logic modules**
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
cd ext/pgn2_native/pgn2-bitboard
|
|
378
|
+
git rm src/square.rs src/piece.rs src/attacks.rs src/magics.rs
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- [ ] **Step 7: Regenerate `Cargo.lock` and run the perft oracle (pure Rust)**
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
cd ext/pgn2_native
|
|
385
|
+
cargo update -p pgn2-bitboard
|
|
386
|
+
cargo test -p pgn2-bitboard
|
|
387
|
+
```
|
|
388
|
+
Expected: PASS — `perft_startpos`, `perft_kiwipete`, `perft_pos3`,
|
|
389
|
+
`perft_pos4`, `perft_pos5`, `perft_pos6` all green. (These are the same
|
|
390
|
+
canonical counts chessie was verified against in the `exp/chessie-smoke`
|
|
391
|
+
run; pos5 d3 = 62379 is the repo's own canonical value.)
|
|
392
|
+
`perft_deep` is `#[ignore]`; optionally `cargo test -p pgn2-bitboard -- --ignored`.
|
|
393
|
+
|
|
394
|
+
- [ ] **Step 8: Build the whole workspace (binding compiles unchanged)**
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
cd ext/pgn2_native
|
|
398
|
+
cargo build
|
|
399
|
+
```
|
|
400
|
+
Expected: the `pgn2_native` cdylib compiles with **no edits** to
|
|
401
|
+
`pgn2_native/src/lib.rs` — it still imports `pgn2_bitboard::Board` and
|
|
402
|
+
`pgn2_bitboard::moves::uci_parse`, both preserved by the adapter.
|
|
403
|
+
|
|
404
|
+
- [ ] **Step 9: Compile the extension and run the full Ruby suite**
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
bundle exec rake compile
|
|
408
|
+
bundle exec rspec
|
|
409
|
+
```
|
|
410
|
+
Expected: all green, including `spec/bitboard_spec.rb` (perft values,
|
|
411
|
+
ArgumentError on bad FEN, sorted UCI legal_moves incl. `e2e4`/`g1f3`/`d2d4`,
|
|
412
|
+
`legal?` true/false, promotions `a7a8q/r/b/n`, castling `e1g1`/`e1c1`).
|
|
413
|
+
|
|
414
|
+
- [ ] **Step 10: Commit**
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
git add ext/pgn2_native/pgn2-bitboard/src ext/pgn2_native/pgn2-bitboard/Cargo.toml ext/pgn2_native/Cargo.lock
|
|
418
|
+
git commit -m "refactor(native): replace hand-rolled bitboard engine with chessie adapter
|
|
419
|
+
|
|
420
|
+
pgn2-bitboard becomes a ~3-file adapter over the chessie 2.0 crate
|
|
421
|
+
(MPL-2.0): Board wraps chessie::Game; perft/legal_moves delegate;
|
|
422
|
+
Move/MoveList/uci_parse preserve the exact API the pgn2_native magnus
|
|
423
|
+
binding consumes, so the binding source is unchanged. Deletes
|
|
424
|
+
square/piece/attacks/magics and the old moves/legality/perft impls.
|
|
425
|
+
|
|
426
|
+
Verified: cargo test -p pgn2-bitboard (perft oracle, all 6 positions)
|
|
427
|
+
+ cargo build (workspace) + rake compile + rspec (spec/bitboard_spec.rb)
|
|
428
|
+
all green. Head-to-head on znver5/BMI2: ~4.4-6.1x faster than the
|
|
429
|
+
hand-rolled engine (startpos d6 21.6->95.7 Mnps; kiwipete d5
|
|
430
|
+
23.2->142.6 Mnps; pos3 d6 18.8->90.7 Mnps)."
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Task 2: MPL-2.0 license compliance
|
|
436
|
+
|
|
437
|
+
`chessie` (and `chessie_types`) are MPL-2.0. The gem stays MIT; this
|
|
438
|
+
task only adds the required attribution and declares the aggregated
|
|
439
|
+
component. No source is relicensed.
|
|
440
|
+
|
|
441
|
+
**Files:**
|
|
442
|
+
- Create: `NOTICE.md`
|
|
443
|
+
- Modify: `pgn2.gemspec`
|
|
444
|
+
|
|
445
|
+
**Interfaces:** none.
|
|
446
|
+
|
|
447
|
+
- [ ] **Step 1: Add the attribution notice**
|
|
448
|
+
|
|
449
|
+
`NOTICE.md`:
|
|
450
|
+
```markdown
|
|
451
|
+
# Third-Party Notices
|
|
452
|
+
|
|
453
|
+
This gem bundles a compiled Rust extension (`pgn2_native`) that links
|
|
454
|
+
the `chessie` crate.
|
|
455
|
+
|
|
456
|
+
## chessie
|
|
457
|
+
|
|
458
|
+
- Source: https://crates.io/crates/chessie
|
|
459
|
+
- Repository: https://github.com/duck2/chessie
|
|
460
|
+
- Version: 2.0.x (see `ext/pgn2_native/Cargo.lock` for the exact pinned
|
|
461
|
+
version)
|
|
462
|
+
- License: Mozilla Public License 2.0 (MPL-2.0)
|
|
463
|
+
|
|
464
|
+
`chessie` and its dependency `chessie_types` are MPL-2.0. They are used
|
|
465
|
+
unmodified. The MPL-2.0 license is file-level copyleft: it applies to
|
|
466
|
+
`chessie`'s own source files only and does not change the license of
|
|
467
|
+
this gem's code (MIT). Per MPL-2.0 §3.3, the source of the MPL-licensed
|
|
468
|
+
files is available at the repository URL above (and is reproducibly
|
|
469
|
+
pinned in `ext/pgn2_native/Cargo.lock`).
|
|
470
|
+
|
|
471
|
+
pgn2's own code remains MIT-licensed; see `LICENSE.txt`.
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
- [ ] **Step 2: Declare the aggregated license in the gemspec**
|
|
475
|
+
|
|
476
|
+
In `pgn2.gemspec`, replace the single `spec.license = 'MIT'` line with a
|
|
477
|
+
plural declaration covering the bundled component:
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
spec.licenses = ['MIT', 'MPL-2.0']
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
> RubyGems accepts `spec.licenses` (plural) for aggregated works. The
|
|
484
|
+
> gem's own code is MIT; the bundled `chessie` component is MPL-2.0
|
|
485
|
+
> (documented in `NOTICE.md`). If a single-string license is required by
|
|
486
|
+
> an older toolchain, keep `spec.license = 'MIT'` and rely on
|
|
487
|
+
> `NOTICE.md` for the MPL declaration — but prefer the plural form.
|
|
488
|
+
|
|
489
|
+
- [ ] **Step 3: Verify the gemspec still loads and `bundle install` is clean**
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
bundle exec ruby -e "puts Gem::Specification.load('pgn2.gemspec').licenses.inspect"
|
|
493
|
+
```
|
|
494
|
+
Expected: prints `["MIT", "MPL-2.0"]`. Then:
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
bundle install
|
|
498
|
+
bundle exec rake compile
|
|
499
|
+
bundle exec rspec
|
|
500
|
+
```
|
|
501
|
+
Expected: all green (no behavioral change from Task 1).
|
|
502
|
+
|
|
503
|
+
- [ ] **Step 4: Commit**
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
git add NOTICE.md pgn2.gemspec
|
|
507
|
+
git commit -m "docs(license): add MPL-2.0 notice for bundled chessie; declare aggregated license
|
|
508
|
+
|
|
509
|
+
chessie/chessie_types are MPL-2.0 (file-level copyleft). The gem stays
|
|
510
|
+
MIT; NOTICE.md credits chessie and points to its source (pinned in
|
|
511
|
+
Cargo.lock per MPL-2.0 §3.3). gemspec declares both licenses."
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Task 3: Update design spec, README, and CHANGELOG
|
|
517
|
+
|
|
518
|
+
The design doc and README describe the hand-rolled magic/pext engine;
|
|
519
|
+
that is now superseded by the chessie adapter. This task records the
|
|
520
|
+
change honestly without rewriting every stale line — a status banner +
|
|
521
|
+
targeted section updates + a CHANGELOG entry.
|
|
522
|
+
|
|
523
|
+
**Files:**
|
|
524
|
+
- Modify: `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`
|
|
525
|
+
- Modify: `README.md`
|
|
526
|
+
- Modify: `CHANGELOG.md`
|
|
527
|
+
|
|
528
|
+
**Interfaces:** none.
|
|
529
|
+
|
|
530
|
+
- [ ] **Step 1: Prepend a status banner to the design spec**
|
|
531
|
+
|
|
532
|
+
At the very top of `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`,
|
|
533
|
+
above the `**Goal:**` line, insert:
|
|
534
|
+
|
|
535
|
+
```markdown
|
|
536
|
+
> **Status (2026-08-14):** The hand-rolled magic/pext engine described
|
|
537
|
+
> below has been replaced by a thin adapter over the `chessie` crate
|
|
538
|
+
> (MPL-2.0). See `docs/superpowers/plans/2026-08-14-chessie-migration.md`.
|
|
539
|
+
> The "Engine details (magic bitboards)" section is retained as the
|
|
540
|
+
> historical design rationale for the since-removed hand-rolled engine;
|
|
541
|
+
> the shipped engine is now `chessie` via the `pgn2-bitboard` adapter.
|
|
542
|
+
> The Ruby API surface, FEN-keyed boundary, and global constraints
|
|
543
|
+
> below are unchanged.
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
- [ ] **Step 2: Update the Architecture section's crate descriptions**
|
|
547
|
+
|
|
548
|
+
In `docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md`, in
|
|
549
|
+
the `## Architecture` numbered list, replace the `pgn2-bitboard` item
|
|
550
|
+
(item 1) with:
|
|
551
|
+
|
|
552
|
+
```markdown
|
|
553
|
+
1. `pgn2-bitboard` (**lib**, no Ruby dependency): a thin adapter over
|
|
554
|
+
the `chessie` crate (MPL-2.0). `Board` wraps `chessie::Game`; `perft`
|
|
555
|
+
and `legal_moves` delegate; `Move`/`MoveList`/`uci_parse` expose the
|
|
556
|
+
small surface the binding consumes. No chess logic lives in this
|
|
557
|
+
crate — `chessie` is the engine. **Unit-testable in pure Rust**
|
|
558
|
+
(`cargo test`) against published perft values — no Ruby in the loop.
|
|
559
|
+
```
|
|
560
|
+
Leave item 2 (`pgn2_native`) and the boundary principle unchanged.
|
|
561
|
+
|
|
562
|
+
- [ ] **Step 3: Add a CHANGELOG entry**
|
|
563
|
+
|
|
564
|
+
In `CHANGELOG.md`, under the next-unreleased section at the top, add:
|
|
565
|
+
|
|
566
|
+
```markdown
|
|
567
|
+
### Changed (native)
|
|
568
|
+
|
|
569
|
+
- The native bitboard engine is now a thin adapter over the `chessie`
|
|
570
|
+
crate (MPL-2.0) instead of a hand-rolled magic/pext engine. The Ruby
|
|
571
|
+
`PGN::Bitboard::Engine` surface (`#perft`, `#legal_moves`, `#legal?`)
|
|
572
|
+
and `PGN::Position#perft`/`#legal_moves` delegations are unchanged.
|
|
573
|
+
~4–6× faster perft on x86-64/BMI2 (e.g. Kiwipete d5: ~23 → ~143 Mnps).
|
|
574
|
+
- The gem now bundles an MPL-2.0 component (`chessie`); the gem's own
|
|
575
|
+
code remains MIT. See `NOTICE.md`. `pgn2.gemspec` declares both
|
|
576
|
+
licenses.
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
- [ ] **Step 4: Update the README's native-engine description**
|
|
580
|
+
|
|
581
|
+
In `README.md`, find the native-engine section (the one added in commit
|
|
582
|
+
`1940baa` "ships with the gem, not 'optional'") and replace any phrase
|
|
583
|
+
describing the engine as "magic bitboard"/"pext"/"hand-rolled" with
|
|
584
|
+
"`chessie`-backed bitboard engine (MPL-2.0)". Add one line: "The engine
|
|
585
|
+
is the `chessie` crate; see `NOTICE.md` for license attribution."
|
|
586
|
+
|
|
587
|
+
> If the README has no engine-internals paragraph, just ensure it does
|
|
588
|
+
> not claim "magic bitboards" or "pext"; the public API description
|
|
589
|
+
> stays valid.
|
|
590
|
+
|
|
591
|
+
- [ ] **Step 5: Verify docs render and the suite still passes**
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
bundle exec rake compile && bundle exec rspec
|
|
595
|
+
```
|
|
596
|
+
Expected: green (docs-only change; no code touched).
|
|
597
|
+
|
|
598
|
+
- [ ] **Step 6: Commit**
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
git add docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md README.md CHANGELOG.md
|
|
602
|
+
git commit -m "docs: record chessie backend migration (spec banner, README, CHANGELOG)"
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Task 4: Clean up stale references and verify CI
|
|
608
|
+
|
|
609
|
+
Comments and a workspace-manifest note still describe the removed
|
|
610
|
+
hand-rolled engine ("magic-bitboard search", "optional"). This task
|
|
611
|
+
fixes the stale strings and runs the full verification matrix the CI
|
|
612
|
+
uses, so the tree is release-ready.
|
|
613
|
+
|
|
614
|
+
**Files:**
|
|
615
|
+
- Modify: `ext/pgn2_native/Cargo.toml`
|
|
616
|
+
- Modify: `lib/pgn/bitboard.rb`
|
|
617
|
+
- Modify: `.github/workflows/native.yml`
|
|
618
|
+
|
|
619
|
+
**Interfaces:** none.
|
|
620
|
+
|
|
621
|
+
- [ ] **Step 1: Fix the workspace manifest's stale comment**
|
|
622
|
+
|
|
623
|
+
In `ext/pgn2_native/Cargo.toml`, replace the comment above
|
|
624
|
+
`[profile.test]`:
|
|
625
|
+
|
|
626
|
+
```toml
|
|
627
|
+
# The perft oracle does real work; optimize tests so `cargo test` runs
|
|
628
|
+
# in release-like time instead of minutes.
|
|
629
|
+
[profile.test]
|
|
630
|
+
opt-level = 2
|
|
631
|
+
```
|
|
632
|
+
(The old comment referenced "magic-bitboard search", which no longer
|
|
633
|
+
exists.)
|
|
634
|
+
|
|
635
|
+
- [ ] **Step 2: Fix the Ruby shim's stale comment**
|
|
636
|
+
|
|
637
|
+
In `lib/pgn/bitboard.rb`, change the header comment
|
|
638
|
+
"Load the native bitboard engine" / any "magic/pext" wording to
|
|
639
|
+
"Load the `chessie`-backed native bitboard engine". Do not change the
|
|
640
|
+
`require` line or the `rescue` behavior.
|
|
641
|
+
|
|
642
|
+
- [ ] **Step 3: Fix the CI workflow's stale framing**
|
|
643
|
+
|
|
644
|
+
In `.github/workflows/native.yml`, change the top comment block and the
|
|
645
|
+
job name/comment that say "optional Rust bitboard perft backend" /
|
|
646
|
+
"pure-Rust engine's `cargo test`" to reflect that the backend is
|
|
647
|
+
required and `chessie`-backed, e.g.:
|
|
648
|
+
|
|
649
|
+
```yaml
|
|
650
|
+
# Builds and tests the required Rust bitboard perft backend (a thin
|
|
651
|
+
# adapter over the `chessie` crate): runs the perft oracle via
|
|
652
|
+
# `cargo test`, compiles the native extension via `rake compile`, and
|
|
653
|
+
# runs the full RSpec suite (which loads the ext).
|
|
654
|
+
```
|
|
655
|
+
Do not change the step commands (`cargo test --manifest-path ...`,
|
|
656
|
+
`bundle exec rake compile`, `bundle exec rspec`) — only the comments.
|
|
657
|
+
|
|
658
|
+
- [ ] **Step 4: Run the full CI-equivalent verification matrix**
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
cargo test --manifest-path ext/pgn2_native/Cargo.toml
|
|
662
|
+
bundle exec rake compile
|
|
663
|
+
bundle exec rspec
|
|
664
|
+
bundle exec rubocop
|
|
665
|
+
```
|
|
666
|
+
Expected: `cargo test` green (perft oracle); `rake compile` builds the
|
|
667
|
+
cdylib; `rspec` all green; `rubocop` clean (no Ruby code changed, so no
|
|
668
|
+
new offenses).
|
|
669
|
+
|
|
670
|
+
- [ ] **Step 5: Sanity-check the cross-compile path is unaffected**
|
|
671
|
+
|
|
672
|
+
`chessie` is pure Rust (`chessie_types`, `anyhow`, `arrayvec` — no C
|
|
673
|
+
deps), so `rake-compiler-dock` cross-compilation in
|
|
674
|
+
`.github/workflows/release-gems.yml` is unaffected. Confirm no
|
|
675
|
+
workspace/Cargo.toml change re-introduces a C dependency:
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
cd ext/pgn2_native && cargo tree | grep -iE "\[build-dependencies\]|cc |cmake |libc " | head
|
|
679
|
+
```
|
|
680
|
+
Expected: empty (no C/link build-deps introduced).
|
|
681
|
+
|
|
682
|
+
- [ ] **Step 6: Commit**
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
git add ext/pgn2_native/Cargo.toml lib/pgn/bitboard.rb .github/workflows/native.yml
|
|
686
|
+
git commit -m "chore(native): drop stale magic/pext references in comments; verify CI matrix
|
|
687
|
+
|
|
688
|
+
Workspace manifest, Ruby shim, and native.yml comments now describe the
|
|
689
|
+
chessie-backed (required) backend instead of the removed hand-rolled
|
|
690
|
+
magic/pext engine. chessie is pure Rust, so cross-compile via
|
|
691
|
+
rake-compiler-dock is unaffected."
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Self-Review
|
|
697
|
+
|
|
698
|
+
**1. Spec coverage.** The migration decision (accept MPL-2.0, adopt
|
|
699
|
+
chessie) is fully covered: Task 1 = the adapter (the engine swap),
|
|
700
|
+
Task 2 = the MPL-2.0 obligation the decision accepted, Task 3 = docs
|
|
701
|
+
honesty, Task 4 = release-readiness. The binding crate and Ruby surface
|
|
702
|
+
are explicitly unchanged (Global Constraints + each task's "unchanged"
|
|
703
|
+
list). The perft oracle (`perft.rs`) and `spec/bitboard_spec.rb` are
|
|
704
|
+
the regression guards and are referenced in Task 1's verification.
|
|
705
|
+
|
|
706
|
+
**2. Placeholder scan.** No TBD/TODO/"implement later". Every code step
|
|
707
|
+
contains full file contents. The two conditional phrasings ("If the
|
|
708
|
+
README has no engine-internals paragraph…", "If a single-string license
|
|
709
|
+
is required…") give concrete fallbacks, not placeholders.
|
|
710
|
+
|
|
711
|
+
**3. Type consistency.** `Board` (`from_fen`, `perft(&self, u32)`,
|
|
712
|
+
`legal_moves(&self) -> MoveList`, `Default`) — matches the binding's
|
|
713
|
+
`Board::from_fen(&fen)`, `self.0.borrow().perft(depth)`,
|
|
714
|
+
`self.0.borrow().legal_moves()`, and `RefCell<Board>` `Default`. `Move`
|
|
715
|
+
(`to_uci(self)`, `same_target(self, Move)`, `Copy`) — matches the
|
|
716
|
+
binding's `m.to_uci()` and `m.same_target(parsed)` called on `&Move`
|
|
717
|
+
from `MoveList::iter()` (works because `Move: Copy`). `MoveList(pub
|
|
718
|
+
Vec<Move>)` with `iter()` — matches the binding's
|
|
719
|
+
`legal_moves().iter()`. `moves::uci_parse(&str) -> Option<Move>` —
|
|
720
|
+
matches `pgn2_bitboard::moves::uci_parse(&uci)`. Adapter `Move` uses
|
|
721
|
+
`from`/`to`/`promo` (not the old `pub u16` field); the binding never
|
|
722
|
+
touches `.0`, so the representation change is safe.
|