pgn2 2.0.0 → 2.0.2
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/release-gems.yml +5 -2
- data/CHANGELOG.md +101 -2
- data/README.md +27 -0
- data/Rakefile +18 -0
- data/TODO.md +25 -107
- data/docs/superpowers/plans/2026-08-15-game-tree-api-plan.md +1014 -0
- data/docs/superpowers/plans/2026-08-15-small-medium-roadmap-plan.md +384 -0
- data/docs/superpowers/specs/2026-08-15-game-tree-api-design.md +387 -0
- data/ext/pgn2_native/pgn2-bitboard/src/board.rs +2 -10
- data/ext/pgn2_native/pgn2-bitboard/src/lib.rs +0 -2
- data/ext/pgn2_native/pgn2_native/src/lib.rs +5 -5
- data/lib/pgn/attack.rb +107 -0
- data/lib/pgn/epd.rb +54 -0
- data/lib/pgn/fen.rb +54 -44
- data/lib/pgn/game.rb +91 -7
- data/lib/pgn/move.rb +4 -4
- data/lib/pgn/node.rb +347 -0
- data/lib/pgn/notation.rb +8 -66
- data/lib/pgn/pgn_parser.rb +30 -31
- data/lib/pgn/pgn_parser.y +14 -15
- data/lib/pgn/position.rb +193 -0
- data/lib/pgn/version.rb +1 -1
- data/lib/pgn.rb +3 -0
- data/spec/castling_normalization_spec.rb +39 -0
- data/spec/comment_round_trip_spec.rb +35 -0
- data/spec/epd_spec.rb +44 -0
- data/spec/fen_spec.rb +6 -0
- data/spec/game_history_spec.rb +46 -0
- data/spec/game_spec.rb +46 -0
- data/spec/movetext_clean_spec.rb +44 -0
- data/spec/node_spec.rb +222 -0
- data/spec/outcome_spec.rb +93 -0
- data/spec/parser_left_recursion_spec.rb +37 -0
- data/spec/position_attack_spec.rb +56 -0
- data/spec/position_legal_spec.rb +123 -0
- data/spec/spec_helper.rb +17 -0
- metadata +18 -3
- data/ext/pgn2_native/pgn2-bitboard/src/moves.rs +0 -121
data/spec/spec_helper.rb
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
require 'pgn'
|
|
2
2
|
|
|
3
|
+
# Helpers for order-independent variation comparison. The parser's
|
|
4
|
+
# `variation_list` rule is right-recursive, so a move's variations are stored
|
|
5
|
+
# in reverse of their PGN-text order; round-tripping flips that order each
|
|
6
|
+
# parse. We therefore compare variation trees as sorted signatures, and
|
|
7
|
+
# reference variations by notation rather than by index.
|
|
8
|
+
def node_sig(move)
|
|
9
|
+
[move.notation, (move.variations || []).map { line_sig(_1) }.sort]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def line_sig(line)
|
|
13
|
+
line.map { node_sig(_1) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tree_sig(game)
|
|
17
|
+
game.moves.map { node_sig(_1) }
|
|
18
|
+
end
|
|
19
|
+
|
|
3
20
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
|
4
21
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
|
5
22
|
# Require this file using `require "spec_helper"` to ensure that it is only
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgn2
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stacey Touset
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-08-
|
|
12
|
+
date: 2026-08-18 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rb_sys
|
|
@@ -226,12 +226,15 @@ files:
|
|
|
226
226
|
- docs/superpowers/plans/2026-08-13-whittle-to-racc-migration.md
|
|
227
227
|
- docs/superpowers/plans/2026-08-14-chessie-migration.md
|
|
228
228
|
- docs/superpowers/plans/2026-08-14-rust-integration-plan.md
|
|
229
|
+
- docs/superpowers/plans/2026-08-15-game-tree-api-plan.md
|
|
230
|
+
- docs/superpowers/plans/2026-08-15-small-medium-roadmap-plan.md
|
|
229
231
|
- docs/superpowers/specs/2026-08-12-to-pgn-serialization-design.md
|
|
230
232
|
- docs/superpowers/specs/2026-08-13-attack-masks-design.md
|
|
231
233
|
- docs/superpowers/specs/2026-08-13-perf-internals-design.md
|
|
232
234
|
- docs/superpowers/specs/2026-08-13-pgn-performance-quick-wins-design.md
|
|
233
235
|
- docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md
|
|
234
236
|
- docs/superpowers/specs/2026-08-14-rust-integration-design.md
|
|
237
|
+
- docs/superpowers/specs/2026-08-15-game-tree-api-design.md
|
|
235
238
|
- examples/immortal_game.pgn
|
|
236
239
|
- ext/pgn2_native/Cargo.lock
|
|
237
240
|
- ext/pgn2_native/Cargo.toml
|
|
@@ -239,18 +242,20 @@ files:
|
|
|
239
242
|
- ext/pgn2_native/pgn2-bitboard/Cargo.toml
|
|
240
243
|
- ext/pgn2_native/pgn2-bitboard/src/board.rs
|
|
241
244
|
- ext/pgn2_native/pgn2-bitboard/src/lib.rs
|
|
242
|
-
- ext/pgn2_native/pgn2-bitboard/src/moves.rs
|
|
243
245
|
- ext/pgn2_native/pgn2-bitboard/src/perft.rs
|
|
244
246
|
- ext/pgn2_native/pgn2_native/Cargo.toml
|
|
245
247
|
- ext/pgn2_native/pgn2_native/src/lib.rs
|
|
246
248
|
- lib/pgn.rb
|
|
249
|
+
- lib/pgn/attack.rb
|
|
247
250
|
- lib/pgn/bitboard.rb
|
|
248
251
|
- lib/pgn/board.rb
|
|
252
|
+
- lib/pgn/epd.rb
|
|
249
253
|
- lib/pgn/fen.rb
|
|
250
254
|
- lib/pgn/game.rb
|
|
251
255
|
- lib/pgn/lexer.rb
|
|
252
256
|
- lib/pgn/move.rb
|
|
253
257
|
- lib/pgn/move_calculator.rb
|
|
258
|
+
- lib/pgn/node.rb
|
|
254
259
|
- lib/pgn/notation.rb
|
|
255
260
|
- lib/pgn/parser.rb
|
|
256
261
|
- lib/pgn/pgn_parser.rb
|
|
@@ -262,13 +267,21 @@ files:
|
|
|
262
267
|
- pgn2.gemspec
|
|
263
268
|
- spec/bitboard_spec.rb
|
|
264
269
|
- spec/board_spec.rb
|
|
270
|
+
- spec/castling_normalization_spec.rb
|
|
271
|
+
- spec/comment_round_trip_spec.rb
|
|
272
|
+
- spec/epd_spec.rb
|
|
265
273
|
- spec/fen_spec.rb
|
|
274
|
+
- spec/game_history_spec.rb
|
|
266
275
|
- spec/game_spec.rb
|
|
267
276
|
- spec/lexer_spec.rb
|
|
268
277
|
- spec/move_calculator_spec.rb
|
|
269
278
|
- spec/move_spec.rb
|
|
279
|
+
- spec/movetext_clean_spec.rb
|
|
280
|
+
- spec/node_spec.rb
|
|
270
281
|
- spec/notation_spec.rb
|
|
282
|
+
- spec/outcome_spec.rb
|
|
271
283
|
- spec/parser_explicit_spec.rb
|
|
284
|
+
- spec/parser_left_recursion_spec.rb
|
|
272
285
|
- spec/parser_spec.rb
|
|
273
286
|
- spec/pgn_files/alternate_castling.pgn
|
|
274
287
|
- spec/pgn_files/annotations.pgn
|
|
@@ -284,6 +297,8 @@ files:
|
|
|
284
297
|
- spec/pgn_files/two_annotations.pgn
|
|
285
298
|
- spec/pgn_files/two_games.pgn
|
|
286
299
|
- spec/pgn_files/variations.pgn
|
|
300
|
+
- spec/position_attack_spec.rb
|
|
301
|
+
- spec/position_legal_spec.rb
|
|
287
302
|
- spec/position_spec.rb
|
|
288
303
|
- spec/serializer_spec.rb
|
|
289
304
|
- spec/spec_helper.rb
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/// Adapter move carrying exactly what `to_uci` and `same_target` need:
|
|
2
|
-
/// from-square, to-square, and optional promotion kind. No flag bits,
|
|
3
|
-
/// so a position-free `uci_parse` can produce a comparable token — this
|
|
4
|
-
/// preserves the existing `same_target` semantics (match on
|
|
5
|
-
/// from + to + promo only), so `legal?("e1g1")` finds the castle and
|
|
6
|
-
/// `legal?("e7e8q")` finds that exact promotion.
|
|
7
|
-
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
8
|
-
pub struct Move {
|
|
9
|
-
from: u8, // 0..=63, index = rank * 8 + file
|
|
10
|
-
to: u8,
|
|
11
|
-
promo: Option<Promo>,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
15
|
-
enum Promo {
|
|
16
|
-
Knight,
|
|
17
|
-
Bishop,
|
|
18
|
-
Rook,
|
|
19
|
-
Queen,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
impl Move {
|
|
23
|
-
/// Build an adapter `Move` from a `chessie::Move` (a legal move).
|
|
24
|
-
///
|
|
25
|
-
/// chessie encodes castling internally as "King takes Rook"
|
|
26
|
-
/// (`to` = the rook's square, Chess960 style). `into_standard_castle`
|
|
27
|
-
/// rewrites the `to` square to the king's destination (`e1g1`/`e1c1`),
|
|
28
|
-
/// matching the standard-UCI output the previous engine produced and
|
|
29
|
-
/// that `uci_parse` / `bitboard_spec.rb` expect. No-op for non-castle
|
|
30
|
-
/// moves.
|
|
31
|
-
pub(crate) fn from_chessie(m: chessie::Move) -> Self {
|
|
32
|
-
let m = m.into_standard_castle();
|
|
33
|
-
Move {
|
|
34
|
-
from: m.from().index() as u8,
|
|
35
|
-
to: m.to().index() as u8,
|
|
36
|
-
promo: m.promotion().map(|kind| match kind {
|
|
37
|
-
chessie::PieceKind::Knight => Promo::Knight,
|
|
38
|
-
chessie::PieceKind::Bishop => Promo::Bishop,
|
|
39
|
-
chessie::PieceKind::Rook => Promo::Rook,
|
|
40
|
-
chessie::PieceKind::Queen => Promo::Queen,
|
|
41
|
-
// Pawns/Kings never appear as a promotion kind.
|
|
42
|
-
_ => unreachable!("non-promotion PieceKind in Move::promotion"),
|
|
43
|
-
}),
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/// UCI string: `"e2e4"`, `"e1g1"` (castle, king from→to),
|
|
48
|
-
/// `"e7e8q"` (promotion). Equal to `chessie::Move::to_uci` for
|
|
49
|
-
/// every legal move (castle and en-passant both reduce to from+to
|
|
50
|
-
/// in UCI), so the sorted-UCI output is unchanged.
|
|
51
|
-
pub fn to_uci(self) -> String {
|
|
52
|
-
let mut s = String::with_capacity(5);
|
|
53
|
-
s.push_str(&sq_name(self.from));
|
|
54
|
-
s.push_str(&sq_name(self.to));
|
|
55
|
-
if let Some(p) = self.promo {
|
|
56
|
-
s.push(match p {
|
|
57
|
-
Promo::Knight => 'n',
|
|
58
|
-
Promo::Bishop => 'b',
|
|
59
|
-
Promo::Rook => 'r',
|
|
60
|
-
Promo::Queen => 'q',
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
s
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/// Match on from + to + promo (the pre-existing semantics).
|
|
67
|
-
pub fn same_target(self, other: Move) -> bool {
|
|
68
|
-
self.from == other.from && self.to == other.to && self.promo == other.promo
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/// Thin wrapper around `Vec<Move>`; the binding calls `.iter()` on it.
|
|
73
|
-
pub struct MoveList(pub Vec<Move>);
|
|
74
|
-
|
|
75
|
-
impl MoveList {
|
|
76
|
-
pub fn iter(&self) -> std::slice::Iter<'_, Move> {
|
|
77
|
-
self.0.iter()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/// Parse a UCI string into a `Move` token **without** a position.
|
|
82
|
-
/// Only `to_uci`/`same_target` consume the result, so from + to + promo
|
|
83
|
-
/// is all that is needed. Returns `None` on any malformed input.
|
|
84
|
-
pub fn uci_parse(s: &str) -> Option<Move> {
|
|
85
|
-
let b = s.as_bytes();
|
|
86
|
-
if b.len() < 4 {
|
|
87
|
-
return None;
|
|
88
|
-
}
|
|
89
|
-
let from = parse_sq(&b[0..2])?;
|
|
90
|
-
let to = parse_sq(&b[2..4])?;
|
|
91
|
-
let promo = if b.len() >= 5 {
|
|
92
|
-
Some(match b[4] {
|
|
93
|
-
b'n' => Promo::Knight,
|
|
94
|
-
b'b' => Promo::Bishop,
|
|
95
|
-
b'r' => Promo::Rook,
|
|
96
|
-
b'q' => Promo::Queen,
|
|
97
|
-
_ => return None,
|
|
98
|
-
})
|
|
99
|
-
} else {
|
|
100
|
-
None
|
|
101
|
-
};
|
|
102
|
-
Some(Move { from, to, promo })
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
fn parse_sq(t: &[u8]) -> Option<u8> {
|
|
106
|
-
let f = t[0].checked_sub(b'a')?;
|
|
107
|
-
let r = t[1].checked_sub(b'1')?;
|
|
108
|
-
if f > 7 || r > 7 {
|
|
109
|
-
return None;
|
|
110
|
-
}
|
|
111
|
-
Some(r * 8 + f)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
fn sq_name(idx: u8) -> String {
|
|
115
|
-
let file = (b'a' + (idx & 7)) as char;
|
|
116
|
-
let rank = (b'1' + (idx >> 3)) as char;
|
|
117
|
-
let mut s = String::with_capacity(2);
|
|
118
|
-
s.push(file);
|
|
119
|
-
s.push(rank);
|
|
120
|
-
s
|
|
121
|
-
}
|