pgn2 2.0.1 → 2.0.3
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/CHANGELOG.md +73 -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 +18 -8
- data/lib/pgn/epd.rb +3 -30
- data/lib/pgn/fen.rb +48 -45
- data/lib/pgn/game.rb +26 -15
- data/lib/pgn/node.rb +66 -91
- data/lib/pgn/pgn_parser.rb +77 -78
- data/lib/pgn/pgn_parser.y +37 -26
- data/lib/pgn/position.rb +3 -1
- data/lib/pgn/version.rb +1 -1
- data/lib/pgn.rb +4 -0
- data/spec/game_spec.rb +0 -14
- data/spec/node_spec.rb +0 -18
- data/spec/parser_explicit_spec.rb +39 -0
- data/spec/spec_helper.rb +17 -0
- metadata +2 -3
- data/ext/pgn2_native/pgn2-bitboard/src/moves.rs +0 -121
|
@@ -176,12 +176,51 @@ RSpec.describe PGN, '.parse (racc parser)' do
|
|
|
176
176
|
end
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
+
describe 'move suffix (comments, NAGs, variations in any order)' do
|
|
180
|
+
it 'attaches two comments before a variation to the move' do
|
|
181
|
+
game = PGN.parse('[White "A"] 1. Bxh7+ Kxh7 {a} {b} (20... Kf8) 2. Ng5+ *').first
|
|
182
|
+
move = game.moves[1]
|
|
183
|
+
expect(move.notation).to eq('Kxh7')
|
|
184
|
+
expect(move.comment).to eq('a b')
|
|
185
|
+
expect(variations_as(move)).to eq([%w[Kf8]])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'attaches a comment after a variation to the same move' do
|
|
189
|
+
game = PGN.parse('[White "A"] 1. e4 (1... c5) {a} (2... f5) 2. Nf3 *').first
|
|
190
|
+
move = game.moves.first
|
|
191
|
+
expect(move.comment).to eq('a')
|
|
192
|
+
expect(variations_as(move).sort).to eq([%w[c5], %w[f5]].sort)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'merges an arbitrary mix of comments, NAGs, and variations' do
|
|
196
|
+
game = PGN.parse('[White "A"] 1. e4 {c1} $1 (1... d5) {c2} $2 *').first
|
|
197
|
+
move = game.moves.first
|
|
198
|
+
expect(move.comment).to eq('c1 c2')
|
|
199
|
+
expect(move.annotation).to eq(%w[$1 $2])
|
|
200
|
+
expect(variations_as(move)).to eq([%w[d5]])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'round-trips a multi-comment move through parse -> serialize -> parse' do
|
|
204
|
+
input = +"[White \"A\"]\n\n1. e4 {a} {b} (1... d5) *\n"
|
|
205
|
+
once = PGN.parse(input, Encoding::UTF_8).first.to_pgn
|
|
206
|
+
twice = PGN.parse(once, Encoding::UTF_8).first.to_pgn
|
|
207
|
+
expect(twice).to eq(once)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
179
211
|
describe 'pgn (verbatim raw text)' do
|
|
180
212
|
it 'returns the verbatim text for a single game' do
|
|
181
213
|
input = "[White \"A\"] 1. e4 1-0\n"
|
|
182
214
|
expect(PGN.parse(input).first.pgn).to eq(input)
|
|
183
215
|
end
|
|
184
216
|
|
|
217
|
+
it 'does not mutate a frozen input string' do
|
|
218
|
+
input = "[White \"A\"] 1. e4 1-0\n".freeze
|
|
219
|
+
expect { PGN.parse(input, Encoding::UTF_8) }.not_to raise_error
|
|
220
|
+
expect(input.encoding).to eq(Encoding::UTF_8)
|
|
221
|
+
expect(PGN.parse(input, Encoding::UTF_8).first.pgn).to eq(input)
|
|
222
|
+
end
|
|
223
|
+
|
|
185
224
|
it 'partitions consecutive games contiguously' do
|
|
186
225
|
input = "[White \"A\"] 1. e4 1-0\n\n[White \"B\"] 1. d4 0-1\n"
|
|
187
226
|
games = PGN.parse(input)
|
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.3
|
|
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-20 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rb_sys
|
|
@@ -242,7 +242,6 @@ files:
|
|
|
242
242
|
- ext/pgn2_native/pgn2-bitboard/Cargo.toml
|
|
243
243
|
- ext/pgn2_native/pgn2-bitboard/src/board.rs
|
|
244
244
|
- ext/pgn2_native/pgn2-bitboard/src/lib.rs
|
|
245
|
-
- ext/pgn2_native/pgn2-bitboard/src/moves.rs
|
|
246
245
|
- ext/pgn2_native/pgn2-bitboard/src/perft.rs
|
|
247
246
|
- ext/pgn2_native/pgn2_native/Cargo.toml
|
|
248
247
|
- ext/pgn2_native/pgn2_native/src/lib.rs
|
|
@@ -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
|
-
}
|