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.
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.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-14 00:00:00.000000000 Z
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
- }