pgn2 1.1.0 → 1.2.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/CHANGELOG.md +23 -0
- data/README.md +11 -6
- data/TODO.md +33 -4
- data/bench/IMPROVEMENTS.md +24 -0
- data/bench/baseline_parse.txt +6 -6
- data/lib/pgn/game.rb +30 -30
- data/lib/pgn/lexer.rb +22 -25
- data/lib/pgn/move_calculator.rb +4 -6
- data/lib/pgn/pgn_parser.rb +12 -5
- data/lib/pgn/pgn_parser.y +12 -5
- data/lib/pgn/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a5ce857277684a9f89c02d35941d7c455df2ca8a18f557985f3ca92eed414b3
|
|
4
|
+
data.tar.gz: 5f126a3da7a6f314748495919168336bb7b97d07e5ad0c9f2ac9b9956a8bb4be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '079c4e3c1dd292c58fac802349c8d9d8054efa4a01288090bd0a38472029b8848a162cd4264fe83135c80d14fb9ea65d531009ddc6dbd6237e30c4f4bb9efda8'
|
|
7
|
+
data.tar.gz: e3ec9dbcf8dad46e7e8ec623642d468253a4ee6d5899dd9d1ac7486e60416b9069a49e34de092e08744439b9c40dce84074a6ddcaed91a584d88a65be6646b9a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0 (2026-08-13)
|
|
4
|
+
|
|
5
|
+
### Summary
|
|
6
|
+
|
|
7
|
+
Parse allocation quick win: collapse `PGN::Lexer#scan_one`'s per-token
|
|
8
|
+
3-element tuple. No public API changes; serialized PGN/FEN output stays
|
|
9
|
+
byte-identical. See `bench/IMPROVEMENTS.md` for the per-step deltas.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Lexer hot path**: `PGN::Lexer#scan_one` now returns the matched string
|
|
13
|
+
directly (stashing its type/discarded flag in ivars) instead of a
|
|
14
|
+
3-element `[type, m, discarded]` tuple, so the parser path (`next_token_pair`)
|
|
15
|
+
allocates only the single `[type, value]` array Racc requires per token.
|
|
16
|
+
Selected by per-line allocation profiling (`scan_one`'s tuple was the #1
|
|
17
|
+
parse allocation site).
|
|
18
|
+
- **Deferred**: the coordinate-only-board + piece-location-index work was
|
|
19
|
+
profiled and deferred to a single coherent board-representation rewrite
|
|
20
|
+
(see TODO) — coordinate-board alone measured only ~1.25× replay at medium
|
|
21
|
+
risk and would likely be superseded by the piece-index rewrite.
|
|
22
|
+
- Performance vs 1.1.0: parse-only allocations −42% objects (603537 → 347037 /
|
|
23
|
+
500 games); parse+replay −18% objects (1427586 → 1170586). Replay path
|
|
24
|
+
unchanged. All 182 specs pass; racc-sync OK.
|
|
25
|
+
|
|
3
26
|
## 1.1.0 (2026-08-13)
|
|
4
27
|
|
|
5
28
|
### Summary
|
data/README.md
CHANGED
|
@@ -181,12 +181,12 @@ Parser — 500 immortal games (`bench/profile_parse.rb`):
|
|
|
181
181
|
|
|
182
182
|
| Metric | original `pgn` | pgn2 | Δ |
|
|
183
183
|
|---|---:|---:|---:|
|
|
184
|
-
| Parse-only allocations (objects) | 1248065 |
|
|
185
|
-
| Parse-only allocations (bytes) | 120370470 |
|
|
186
|
-
| Parse + replay allocations (objects) | 3778073 |
|
|
187
|
-
| Parse + replay allocations (bytes) | 249570048 |
|
|
188
|
-
| Parse-only throughput | 1461 ms/i |
|
|
189
|
-
| Parse + replay throughput | 1938 ms/i |
|
|
184
|
+
| Parse-only allocations (objects) | 1248065 | 347037 | -901028 (-72.2%) |
|
|
185
|
+
| Parse-only allocations (bytes) | 120370470 | 17977414 | -102393056 (-85.1%) |
|
|
186
|
+
| Parse + replay allocations (objects) | 3778073 | 1170586 | -2607487 (-69.0%) |
|
|
187
|
+
| Parse + replay allocations (bytes) | 249570048 | 67804136 | -181765912 (-72.8%) |
|
|
188
|
+
| Parse-only throughput | 1461 ms/i | 305 ms/i | ~4.8x faster |
|
|
189
|
+
| Parse + replay throughput | 1938 ms/i | 816 ms/i | ~2.4x faster |
|
|
190
190
|
|
|
191
191
|
What changed to get there:
|
|
192
192
|
|
|
@@ -211,6 +211,11 @@ What changed to get there:
|
|
|
211
211
|
11. `PGN::MoveCalculator` — memoized `destination_coords`, frozen
|
|
212
212
|
`ROOK_RESTRICTIONS`, empty short-circuit in `castling_restrictions`;
|
|
213
213
|
`Move#pawn?` non-allocating.
|
|
214
|
+
12. `PGN::Lexer#scan_one` — returns the matched string directly (stashing
|
|
215
|
+
type/discarded in ivars) instead of a 3-element `[type, m, discarded]`
|
|
216
|
+
tuple, so the parser hot path now allocates only the single `[type, value]`
|
|
217
|
+
array Racc requires per token. Cuts parse allocations ~42% (603537 → 347037
|
|
218
|
+
objects for 500 games).
|
|
214
219
|
|
|
215
220
|
Public output (FEN, PGN) is byte-identical to the original gem; the full
|
|
216
221
|
suite (182 examples) stays green. See `bench/IMPROVEMENTS.md` for the per-step
|
data/TODO.md
CHANGED
|
@@ -10,12 +10,41 @@
|
|
|
10
10
|
|
|
11
11
|
- Support converting a game to pgn format
|
|
12
12
|
- Speed up parsing
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
- ✓ (done in 1.2.0) Removed `PGN::Lexer`'s per-token `Token` Struct
|
|
14
|
+
allocation on the parser hot path (`next_token_pair`), and collapsed
|
|
15
|
+
`scan_one`'s `[type, m, discarded]` tuple to a single returned string
|
|
16
|
+
(type/discarded stashed in ivars). The full `Token` is kept only for the
|
|
17
|
+
`#tokens` spec helper. Parse allocations −42% (603537 → 347037 / 500 games).
|
|
18
|
+
- Speed up replay via a board-representation rewrite (deferred "Approach B"):
|
|
19
|
+
per-line profiling shows the remaining replay allocations are architectural
|
|
20
|
+
— `MoveCalculator#first_piece` scan-return arrays (~5/ply, the #1 site) and
|
|
21
|
+
`Board#position_for` string joins (~3/ply). Do these as ONE coherent
|
|
22
|
+
rewrite (not separately, to avoid throwing away work):
|
|
23
|
+
(a) a piece-location index (piece → squares) so king/disambiguation/origin
|
|
24
|
+
lookups are O(1) instead of scanning 64 squares — kills `first_piece` scan
|
|
25
|
+
arrays AND the dominant replay compute (`valid_square?`/`at` ≈ 15/ply calls);
|
|
26
|
+
(b) a coordinate-only internal board (int square keys, no `"e4"` strings on
|
|
27
|
+
the hot path) — kills `position_for` strings + `changes` string keys.
|
|
28
|
+
Caveat: `change!`/`update`/`position_for`/`coordinates_for`/`squares` are
|
|
29
|
+
spec-tested public API, so the new representation must be additive (string
|
|
30
|
+
API kept). Realistic ceiling ~2× replay allocation + ~1.5–2× throughput;
|
|
31
|
+
medium-high risk (Board/MoveCalculator/Position/FEN). Only worth it given a
|
|
32
|
+
real hot-loop need (replay is already ~0.8 ms/ply).
|
|
17
33
|
- Replace the right-recursive `tag_section`/`variation_list` rules in
|
|
18
34
|
`pgn_parser.y` with ordinary left-recursion plus one explicit `.reverse`
|
|
19
35
|
at the point each list is consumed, so the legacy whittle-order
|
|
20
36
|
compatibility quirk is a single greppable line instead of implicit in
|
|
21
37
|
recursion direction.
|
|
38
|
+
- Make `MoveText#clean_text` idempotent (or run it exactly once, at
|
|
39
|
+
construction) so `Game#moves=`/`#standardize_castling` doesn't need to
|
|
40
|
+
sniff a comment for leftover `{`/`}` to decide whether a MoveText is safe
|
|
41
|
+
to reuse as-is. The brace check is a bandaid for `clean_text` not fully
|
|
42
|
+
normalizing multi-line/nested comments in one pass; fixing that at the
|
|
43
|
+
source would let `moves=` reuse unconditionally.
|
|
44
|
+
- `MoveCalculator#destination_coords` memoizes into `@dest_coords` based on
|
|
45
|
+
`board`/`move`/`origin` never changing after `#initialize` — true today,
|
|
46
|
+
but only by convention, since `board`, `move`, `origin` are all public
|
|
47
|
+
`attr_accessor`s with no cache invalidation tied to their setters. If a
|
|
48
|
+
future caller ever mutates and reuses a `MoveCalculator` instance, this
|
|
49
|
+
memo goes stale silently. Either drop the public setters or invalidate
|
|
50
|
+
`@dest_coords` when they're used.
|
data/bench/IMPROVEMENTS.md
CHANGED
|
@@ -141,3 +141,27 @@ byte-identical (covered by `spec/lexer_spec.rb`, `spec/parser_explicit_spec.rb`,
|
|
|
141
141
|
and the fixture round-trip in `spec/game_spec.rb`). Racc parser is in sync with
|
|
142
142
|
`pgn_parser.y` (CI racc-sync check passes). No public API or serialized-output
|
|
143
143
|
changes.
|
|
144
|
+
|
|
145
|
+
## Token-array collapse (2026-08-13, v1.2.0)
|
|
146
|
+
|
|
147
|
+
Collapse `PGN::Lexer#scan_one`'s per-token 3-element `[type, m, discarded]` tuple.
|
|
148
|
+
`scan_one` now returns the matched string directly and stashes its type /
|
|
149
|
+
discarded flag in `@scan_type` / `@scan_discarded` ivars, so the parser hot path
|
|
150
|
+
(`next_token_pair`) allocates only the single `[type, value]` array Racc requires.
|
|
151
|
+
Selected by per-line allocation profiling: `scan_one`'s tuple was the #1 parse
|
|
152
|
+
allocation site. The coordinate-only-board + piece-location-index work was
|
|
153
|
+
profiled and **deferred** — the board rewrite is a larger design (see TODO), and
|
|
154
|
+
coordinate-board alone measured only ~1.25× replay at medium risk and would
|
|
155
|
+
likely be superseded by the piece-index rewrite.
|
|
156
|
+
|
|
157
|
+
### bench/profile_parse.rb (500 immortal games)
|
|
158
|
+
|
|
159
|
+
| Metric | BEFORE | AFTER | Δ |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| Parse-only allocations (objects) | 603537 | 347037 | -256500 (-42.5%) |
|
|
162
|
+
| Parse-only allocations (bytes) | 28257414 | 17977414 | -10280000 (-36.4%) |
|
|
163
|
+
| Parse + replay allocations (objects) | 1427586 | 1170586 | -257000 (-18.0%) |
|
|
164
|
+
| Parse + replay allocations (bytes) | 78104136 | 67804136 | -10300000 (-13.2%) |
|
|
165
|
+
|
|
166
|
+
Replay (`profile_moves.rb`) is unchanged (1709 objects / 103720 bytes) — the
|
|
167
|
+
lexer edit does not touch the replay path. All 182 specs pass; racc-sync OK.
|
data/bench/baseline_parse.txt
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
Corpus: 500 copies of the immortal game
|
|
2
2
|
|
|
3
3
|
=== 1. Parse-only allocations (500 games) ===
|
|
4
|
-
total_allocated objects:
|
|
5
|
-
total_allocated bytes:
|
|
4
|
+
total_allocated objects: 347037
|
|
5
|
+
total_allocated bytes: 17977414
|
|
6
6
|
|
|
7
7
|
=== 2. Parse + replay allocations (500 games) ===
|
|
8
|
-
total_allocated objects:
|
|
9
|
-
total_allocated bytes:
|
|
8
|
+
total_allocated objects: 1170586
|
|
9
|
+
total_allocated bytes: 67804136
|
|
10
10
|
|
|
11
11
|
=== 3. Parse-only throughput (ips) ===
|
|
12
12
|
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
|
|
13
13
|
Warming up --------------------------------------
|
|
14
14
|
parse 500 games 1.000 i/100ms
|
|
15
15
|
Calculating -------------------------------------
|
|
16
|
-
parse 500 games
|
|
16
|
+
parse 500 games 4.086 (± 0.0%) i/s (244.74 ms/i) - 21.000 in 5.139596s
|
|
17
17
|
|
|
18
18
|
=== 4. Parse + replay throughput (ips) ===
|
|
19
19
|
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
|
|
20
20
|
Warming up --------------------------------------
|
|
21
21
|
parse+replay 500 games 1.000 i/100ms
|
|
22
22
|
Calculating -------------------------------------
|
|
23
|
-
parse+replay 500 games 1.
|
|
23
|
+
parse+replay 500 games 1.472 (± 0.0%) i/s (679.56 ms/i) - 8.000 in 5.436470s
|
|
24
24
|
|
|
25
25
|
Done. Compare this file against bench/baseline_parse.txt after optimizations.
|
data/lib/pgn/game.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'io/console'
|
|
2
4
|
|
|
3
5
|
module PGN
|
|
@@ -11,12 +13,12 @@ module PGN
|
|
|
11
13
|
@variations = variations
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
def ==(
|
|
15
|
-
to_s ==
|
|
16
|
+
def ==(other)
|
|
17
|
+
to_s == other.to_s
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def eql?(
|
|
19
|
-
self ==
|
|
20
|
+
def eql?(other)
|
|
21
|
+
self == other
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def hash
|
|
@@ -31,6 +33,7 @@ module PGN
|
|
|
31
33
|
text&.gsub(/{(.*)}/, '\1')&.gsub(/\s+/, ' ')&.strip
|
|
32
34
|
end
|
|
33
35
|
end
|
|
36
|
+
|
|
34
37
|
# {PGN::Game} holds all of the information about a game. It is either
|
|
35
38
|
# the result of parsing a PGN file, or created by hand.
|
|
36
39
|
#
|
|
@@ -57,9 +60,9 @@ module PGN
|
|
|
57
60
|
attr_accessor :tags, :result, :pgn, :comment
|
|
58
61
|
attr_reader :moves
|
|
59
62
|
|
|
60
|
-
LEFT = /(a|\x1B\[D)\z
|
|
61
|
-
RIGHT = /(d|\x1B\[C)\z
|
|
62
|
-
EXIT = /(q|\x03)\z
|
|
63
|
+
LEFT = /(a|\x1B\[D)\z/
|
|
64
|
+
RIGHT = /(d|\x1B\[C)\z/
|
|
65
|
+
EXIT = /(q|\x03)\z/
|
|
63
66
|
|
|
64
67
|
# @param moves [Array<String>] a list of moves in SAN
|
|
65
68
|
# @param tags [Hash<String, String>] metadata about the game
|
|
@@ -77,28 +80,8 @@ module PGN
|
|
|
77
80
|
#
|
|
78
81
|
# Standardize castling moves to use O's instead of 0's
|
|
79
82
|
#
|
|
80
|
-
# Reuse-safety invariant: when an element is already a MoveText we reuse
|
|
81
|
-
# it directly (sharing one object between the parser's move tree and
|
|
82
|
-
# Game#moves) whenever its comment is already fully cleaned. This is safe
|
|
83
|
-
# because nothing mutates a MoveText after this assignment returns. We
|
|
84
|
-
# still re-wrap when the comment carries braces: the parser's single-pass
|
|
85
|
-
# clean_text leaves braces on multi-line/nested comments, and the second
|
|
86
|
-
# clean_text that MoveText.new applies here is load-bearing for those
|
|
87
|
-
# (matches the legacy whittle byte-output). Skipping it would change
|
|
88
|
-
# serialized comments.
|
|
89
83
|
def moves=(moves)
|
|
90
|
-
@moves =
|
|
91
|
-
moves.map do |m|
|
|
92
|
-
if m.is_a?(String)
|
|
93
|
-
MoveText.new(m.include?('0') ? m.gsub('0', 'O') : m)
|
|
94
|
-
elsif m.notation.include?('0')
|
|
95
|
-
MoveText.new(m.notation.gsub('0', 'O'), m.annotation, m.comment, m.variations)
|
|
96
|
-
elsif m.comment.nil? || !m.comment.include?('{')
|
|
97
|
-
m
|
|
98
|
-
else
|
|
99
|
-
MoveText.new(m.notation, m.annotation, m.comment, m.variations)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
84
|
+
@moves = moves.map { |m| standardize_castling(m) }
|
|
102
85
|
end
|
|
103
86
|
|
|
104
87
|
# @return [String] a canonical PGN string for this game, ending with a
|
|
@@ -152,11 +135,11 @@ module PGN
|
|
|
152
135
|
loop do
|
|
153
136
|
puts "\e[H\e[2J"
|
|
154
137
|
puts positions[index].inspect
|
|
155
|
-
hist[0..2] = (hist[1..2] <<
|
|
138
|
+
hist[0..2] = (hist[1..2] << $stdin.getch)
|
|
156
139
|
|
|
157
140
|
case hist.join
|
|
158
141
|
when LEFT
|
|
159
|
-
index -= 1 if index
|
|
142
|
+
index -= 1 if index.positive?
|
|
160
143
|
when RIGHT
|
|
161
144
|
index += 1 if index < moves.length
|
|
162
145
|
when EXIT
|
|
@@ -164,5 +147,22 @@ module PGN
|
|
|
164
147
|
end
|
|
165
148
|
end
|
|
166
149
|
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
# A MoveText is reused as-is (no new object) when its notation needs no
|
|
154
|
+
# '0'->'O' fix and its comment has no braces left to clean; a braced
|
|
155
|
+
# comment still needs MoveText.new's second clean_text pass to match the
|
|
156
|
+
# legacy whittle byte-output.
|
|
157
|
+
def standardize_castling(entry)
|
|
158
|
+
return MoveText.new(entry.include?('0') ? entry.gsub('0', 'O') : entry) if entry.is_a?(String)
|
|
159
|
+
|
|
160
|
+
notation = entry.notation.include?('0') ? entry.notation.gsub('0', 'O') : entry.notation
|
|
161
|
+
if notation.equal?(entry.notation) && (entry.comment.nil? || !entry.comment.include?('{'))
|
|
162
|
+
return entry
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
MoveText.new(notation, entry.annotation, entry.comment, entry.variations)
|
|
166
|
+
end
|
|
167
167
|
end
|
|
168
168
|
end
|
data/lib/pgn/lexer.rb
CHANGED
|
@@ -146,29 +146,18 @@ module PGN
|
|
|
146
146
|
|
|
147
147
|
# Returns the next {Token}, or +nil+ at end of input.
|
|
148
148
|
def next_token
|
|
149
|
-
type, value
|
|
149
|
+
type, value = next_token_pair
|
|
150
150
|
return nil unless type
|
|
151
|
-
|
|
151
|
+
|
|
152
|
+
Token.new(type: type, value: value, offset: @last_offset, line: @line)
|
|
152
153
|
end
|
|
153
154
|
|
|
154
155
|
# Fast path for the parser: returns [type, value] for the next
|
|
155
156
|
# non-discarded token, or +nil+ at end of input. Does not allocate a
|
|
156
|
-
# {Token} Struct
|
|
157
|
-
#
|
|
158
|
-
#
|
|
157
|
+
# {Token} Struct, and +scan_one+ returns the matched string directly
|
|
158
|
+
# (stashing its type/discarded flag in ivars) so the only array
|
|
159
|
+
# allocated per token is the [type, value] pair Racc requires.
|
|
159
160
|
def next_token_pair
|
|
160
|
-
type, value, = scan_next
|
|
161
|
-
return nil unless type
|
|
162
|
-
[type, value]
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
private
|
|
166
|
-
|
|
167
|
-
# Scan to the next non-discarded token and return [type, value, offset],
|
|
168
|
-
# or nil at end of input. Performs the exact +advance_line+ and
|
|
169
|
-
# +note_token+ side effects that +next_token+ historically did, so
|
|
170
|
-
# +game_starts+ (used for verbatim +Game#pgn+ slicing) stays correct.
|
|
171
|
-
def scan_next
|
|
172
161
|
until @ss.eos?
|
|
173
162
|
off = @ss.pos
|
|
174
163
|
|
|
@@ -176,25 +165,33 @@ module PGN
|
|
|
176
165
|
type, value = lit
|
|
177
166
|
@ss.pos = off + 1
|
|
178
167
|
note_token(type, off)
|
|
179
|
-
|
|
168
|
+
@last_offset = off
|
|
169
|
+
return [type, value]
|
|
180
170
|
end
|
|
181
171
|
|
|
182
|
-
|
|
172
|
+
value = scan_one
|
|
183
173
|
advance_line(value)
|
|
184
|
-
next if
|
|
174
|
+
next if @scan_discarded
|
|
185
175
|
|
|
186
|
-
note_token(
|
|
187
|
-
|
|
176
|
+
note_token(@scan_type, off)
|
|
177
|
+
@last_offset = off
|
|
178
|
+
return [@scan_type, value]
|
|
188
179
|
end
|
|
189
180
|
nil
|
|
190
181
|
end
|
|
191
182
|
|
|
192
|
-
|
|
193
|
-
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
# Try each terminal rule in order; return the matched string for the
|
|
186
|
+
# first match (stashing its type and discarded flag in +@scan_type+ /
|
|
187
|
+
# +@scan_discarded+ so the caller avoids allocating a 3-element tuple),
|
|
188
|
+
# or raise if nothing matches at the current position.
|
|
194
189
|
def scan_one
|
|
195
190
|
RULES.each do |(type, re, discarded)|
|
|
196
191
|
if (m = @ss.scan(re))
|
|
197
|
-
|
|
192
|
+
@scan_type = type
|
|
193
|
+
@scan_discarded = discarded
|
|
194
|
+
return m
|
|
198
195
|
end
|
|
199
196
|
end
|
|
200
197
|
raise UnconsumedInputError,
|
data/lib/pgn/move_calculator.rb
CHANGED
|
@@ -118,15 +118,13 @@ module PGN
|
|
|
118
118
|
restrict += %w[K Q]
|
|
119
119
|
when 'k'
|
|
120
120
|
restrict += %w[k q]
|
|
121
|
-
when 'R'
|
|
122
|
-
restrict << ROOK_RESTRICTIONS[origin]
|
|
123
|
-
when 'r'
|
|
121
|
+
when 'R', 'r'
|
|
124
122
|
restrict << ROOK_RESTRICTIONS[origin]
|
|
125
123
|
end
|
|
126
124
|
|
|
127
125
|
# when castling occurs
|
|
128
|
-
restrict += %w[K Q] if %w[K Q].include?
|
|
129
|
-
restrict += %w[k q] if %w[k q].include?
|
|
126
|
+
restrict += %w[K Q] if %w[K Q].include?(move.castle)
|
|
127
|
+
restrict += %w[k q] if %w[k q].include?(move.castle)
|
|
130
128
|
|
|
131
129
|
# when a rook is taken
|
|
132
130
|
restrict << 'Q' if move.destination == 'a1'
|
|
@@ -337,7 +335,7 @@ module PGN
|
|
|
337
335
|
end
|
|
338
336
|
|
|
339
337
|
def destination_coords
|
|
340
|
-
@
|
|
338
|
+
@destination_coords ||= board.coordinates_for(move.destination)
|
|
341
339
|
end
|
|
342
340
|
end
|
|
343
341
|
end
|
data/lib/pgn/pgn_parser.rb
CHANGED
|
@@ -27,13 +27,20 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 97)
|
|
|
27
27
|
|
|
28
28
|
private
|
|
29
29
|
|
|
30
|
-
# Punctuation tokens are racc'd by literal
|
|
31
|
-
# the
|
|
32
|
-
#
|
|
33
|
-
|
|
30
|
+
# Punctuation tokens are racc'd by their literal character (taken from
|
|
31
|
+
# PGN::Lexer::LITERAL_BYTES, the single source of truth for those
|
|
32
|
+
# characters); everything else is racc'd by the upcased lexer symbol.
|
|
33
|
+
# A complete frozen lookup keeps this a plain hash read for every token
|
|
34
|
+
# instead of allocating two Strings (`to_s` + `upcase`) per token.
|
|
35
|
+
TOKEN_TRANSLATIONS = PGN::Lexer::LITERAL_BYTES.values.to_h.merge(
|
|
36
|
+
string: :STRING, comment: :COMMENT, game_termination: :GAME_TERMINATION,
|
|
37
|
+
san_move: :SAN_MOVE, nag: :NAG, move_number: :MOVE_NUMBER, tag_name: :TAG_NAME
|
|
38
|
+
).freeze
|
|
34
39
|
|
|
35
40
|
def translate_type(sym)
|
|
36
|
-
|
|
41
|
+
TOKEN_TRANSLATIONS.fetch(sym) do
|
|
42
|
+
raise "PGN::Lexer produced an unknown token type: #{sym.inspect}"
|
|
43
|
+
end
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
# Slice the verbatim raw PGN text per game out of the original input using
|
data/lib/pgn/pgn_parser.y
CHANGED
|
@@ -112,13 +112,20 @@ rule
|
|
|
112
112
|
|
|
113
113
|
private
|
|
114
114
|
|
|
115
|
-
# Punctuation tokens are racc'd by literal
|
|
116
|
-
# the
|
|
117
|
-
#
|
|
118
|
-
|
|
115
|
+
# Punctuation tokens are racc'd by their literal character (taken from
|
|
116
|
+
# PGN::Lexer::LITERAL_BYTES, the single source of truth for those
|
|
117
|
+
# characters); everything else is racc'd by the upcased lexer symbol.
|
|
118
|
+
# A complete frozen lookup keeps this a plain hash read for every token
|
|
119
|
+
# instead of allocating two Strings (`to_s` + `upcase`) per token.
|
|
120
|
+
TOKEN_TRANSLATIONS = PGN::Lexer::LITERAL_BYTES.values.to_h.merge(
|
|
121
|
+
string: :STRING, comment: :COMMENT, game_termination: :GAME_TERMINATION,
|
|
122
|
+
san_move: :SAN_MOVE, nag: :NAG, move_number: :MOVE_NUMBER, tag_name: :TAG_NAME
|
|
123
|
+
).freeze
|
|
119
124
|
|
|
120
125
|
def translate_type(sym)
|
|
121
|
-
|
|
126
|
+
TOKEN_TRANSLATIONS.fetch(sym) do
|
|
127
|
+
raise "PGN::Lexer produced an unknown token type: #{sym.inspect}"
|
|
128
|
+
end
|
|
122
129
|
end
|
|
123
130
|
|
|
124
131
|
# Slice the verbatim raw PGN text per game out of the original input using
|
data/lib/pgn/version.rb
CHANGED