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
data/lib/pgn/node.rb
CHANGED
|
@@ -141,14 +141,7 @@ module PGN
|
|
|
141
141
|
@line.push(*new_line)
|
|
142
142
|
else
|
|
143
143
|
normalize_branch_point(cont)
|
|
144
|
-
|
|
145
|
-
pos = @index + 1
|
|
146
|
-
old_tail = @line[(pos + 1)..] || []
|
|
147
|
-
n0 = new_line[0]
|
|
148
|
-
@line[pos] = n0
|
|
149
|
-
@line[(pos + 1)..] = (new_line[1..] || [])
|
|
150
|
-
cont.variations = []
|
|
151
|
-
n0.variations = [[cont, *old_tail], *vars]
|
|
144
|
+
make_mainline(@line, @index + 1, cont, new_line, cont.variations, old_main_position: :first)
|
|
152
145
|
end
|
|
153
146
|
@game.root
|
|
154
147
|
end
|
|
@@ -157,16 +150,9 @@ module PGN
|
|
|
157
150
|
# No-op for the mainline (index 0) or the first variation (index 1).
|
|
158
151
|
# Returns a fresh +game.root+.
|
|
159
152
|
def promote
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return @game.root if i.nil? || i <= 1
|
|
164
|
-
|
|
165
|
-
cont = parent_continuation
|
|
166
|
-
normalize_branch_point(cont)
|
|
167
|
-
vars = cont.variations
|
|
168
|
-
vars[i - 1], vars[i - 2] = vars[i - 2], vars[i - 1]
|
|
169
|
-
@game.root
|
|
153
|
+
mutate_sibling(noop: ->(i) { i <= 1 }) do |i, _cont, vars|
|
|
154
|
+
vars[i - 1], vars[i - 2] = vars[i - 2], vars[i - 1]
|
|
155
|
+
end
|
|
170
156
|
end
|
|
171
157
|
|
|
172
158
|
# Move this node one slot toward the end among its siblings. At index 0
|
|
@@ -174,83 +160,54 @@ module PGN
|
|
|
174
160
|
# mainline and the old mainline becomes variation #1. No-op at the last
|
|
175
161
|
# index. Returns a fresh +game.root+.
|
|
176
162
|
def demote
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
normalize_branch_point(cont)
|
|
185
|
-
vars = cont.variations
|
|
186
|
-
if i.zero?
|
|
187
|
-
v1 = vars.shift
|
|
188
|
-
make_mainline(cont, v1, vars, old_main_position: :first)
|
|
189
|
-
else
|
|
190
|
-
vars[i - 1], vars[i] = vars[i], vars[i - 1]
|
|
163
|
+
mutate_sibling(noop: ->(i) { i == @parent.children.size - 1 }) do |i, cont, vars|
|
|
164
|
+
if i.zero?
|
|
165
|
+
v1 = vars.shift
|
|
166
|
+
make_mainline(@parent.line, @parent.index + 1, cont, v1, vars, old_main_position: :first)
|
|
167
|
+
else
|
|
168
|
+
vars[i - 1], vars[i] = vars[i], vars[i - 1]
|
|
169
|
+
end
|
|
191
170
|
end
|
|
192
|
-
@game.root
|
|
193
171
|
end
|
|
194
172
|
|
|
195
173
|
# Make this node the mainline at its branching point (the old mainline
|
|
196
174
|
# becomes variation #1). No-op if already the mainline. Returns a
|
|
197
175
|
# fresh +game.root+.
|
|
198
176
|
def promote_to_main
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
cont = parent_continuation
|
|
205
|
-
normalize_branch_point(cont)
|
|
206
|
-
vars = cont.variations
|
|
207
|
-
vk = vars.delete_at(i - 1)
|
|
208
|
-
make_mainline(cont, vk, vars, old_main_position: :first)
|
|
209
|
-
@game.root
|
|
177
|
+
mutate_sibling(noop: lambda(&:zero?)) do |i, cont, vars|
|
|
178
|
+
vk = vars.delete_at(i - 1)
|
|
179
|
+
make_mainline(@parent.line, @parent.index + 1, cont, vk, vars, old_main_position: :first)
|
|
180
|
+
end
|
|
210
181
|
end
|
|
211
182
|
|
|
212
183
|
# Move this node to the last position among its siblings. At index 0
|
|
213
184
|
# the last variation becomes the new mainline and the old mainline
|
|
214
185
|
# becomes the last variation. Returns a fresh +game.root+.
|
|
215
186
|
def demote_to_last
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
last = vars.pop
|
|
228
|
-
make_mainline(cont, last, vars, old_main_position: :last)
|
|
229
|
-
else
|
|
230
|
-
el = vars.delete_at(i - 1)
|
|
231
|
-
vars.push(el)
|
|
187
|
+
mutate_sibling do |i, cont, vars|
|
|
188
|
+
if i.zero?
|
|
189
|
+
unless vars.empty?
|
|
190
|
+
last = vars.pop
|
|
191
|
+
make_mainline(@parent.line, @parent.index + 1, cont, last, vars, old_main_position: :last)
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
el = vars.delete_at(i - 1)
|
|
195
|
+
vars.push(el)
|
|
196
|
+
end
|
|
232
197
|
end
|
|
233
|
-
@game.root
|
|
234
198
|
end
|
|
235
199
|
|
|
236
200
|
# Remove this node and its subtree. If it is the mainline continuation,
|
|
237
201
|
# the first remaining variation (if any) takes its place; otherwise the
|
|
238
202
|
# line is truncated at this point. Returns a fresh +game.root+.
|
|
239
203
|
def delete
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
normalize_branch_point(cont)
|
|
247
|
-
vars = cont.variations
|
|
248
|
-
if i.zero?
|
|
249
|
-
delete_mainline_continuation(cont, vars)
|
|
250
|
-
else
|
|
251
|
-
vars.delete_at(i - 1)
|
|
204
|
+
mutate_sibling do |i, cont, vars|
|
|
205
|
+
if i.zero?
|
|
206
|
+
delete_mainline_continuation(cont, vars)
|
|
207
|
+
else
|
|
208
|
+
vars.delete_at(i - 1)
|
|
209
|
+
end
|
|
252
210
|
end
|
|
253
|
-
@game.root
|
|
254
211
|
end
|
|
255
212
|
|
|
256
213
|
private
|
|
@@ -295,11 +252,25 @@ module PGN
|
|
|
295
252
|
# SANs, applying the same castling 0->O normalization as Game#moves=.
|
|
296
253
|
def build_movetexts(move_or_moves)
|
|
297
254
|
sans = move_or_moves.is_a?(String) ? [move_or_moves] : move_or_moves
|
|
298
|
-
sans.map { |s| PGN::MoveText.new(normalize_castling(s)) }
|
|
255
|
+
sans.map { |s| PGN::MoveText.new(PGN::MoveText.normalize_castling(s)) }
|
|
299
256
|
end
|
|
300
257
|
|
|
301
|
-
|
|
302
|
-
|
|
258
|
+
# Shared prologue/epilogue for the sibling-reordering mutators (promote,
|
|
259
|
+
# demote, promote_to_main, demote_to_last, delete): no-op at the root or
|
|
260
|
+
# per +noop+ (checked against the sibling index before any mutation),
|
|
261
|
+
# otherwise normalize the branching point and yield the sibling index,
|
|
262
|
+
# the parent's mainline continuation, and its (now flat) variations to
|
|
263
|
+
# +block+ for the mutation proper. Always returns a fresh +game.root+.
|
|
264
|
+
def mutate_sibling(noop: nil)
|
|
265
|
+
return @game.root if root?
|
|
266
|
+
|
|
267
|
+
i = sibling_index
|
|
268
|
+
return @game.root if i.nil? || noop&.call(i)
|
|
269
|
+
|
|
270
|
+
cont = parent_continuation
|
|
271
|
+
normalize_branch_point(cont)
|
|
272
|
+
yield i, cont, cont.variations
|
|
273
|
+
@game.root
|
|
303
274
|
end
|
|
304
275
|
|
|
305
276
|
# Hoist every variation first-move branching at the position before
|
|
@@ -331,18 +302,24 @@ module PGN
|
|
|
331
302
|
end
|
|
332
303
|
end
|
|
333
304
|
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
305
|
+
# Splice +movetexts+ (an Array<MoveText>) into +line+ starting at +pos+:
|
|
306
|
+
# its first entry replaces +line[pos]+, and the rest replace the tail
|
|
307
|
+
# after it (so a shorter +movetexts+ truncates +line+).
|
|
308
|
+
def splice_line!(line, pos, movetexts)
|
|
309
|
+
line[pos] = movetexts[0]
|
|
310
|
+
line[(pos + 1)..] = (movetexts[1..] || [])
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Make +vk+ (= [vk0, *tail]) the new mainline continuation at +line+/+pos+
|
|
314
|
+
# (the position before +cont+), replacing +cont+. The old mainline
|
|
315
|
+
# (+cont+ and its tail) becomes a variation appended to +others+ either
|
|
316
|
+
# before (+old_main_position == :first+) or after (+:last+).
|
|
317
|
+
# +cont.variations+ is cleared (its at-point variations are now +vk0+'s
|
|
318
|
+
# siblings).
|
|
319
|
+
def make_mainline(line, pos, cont, variation, others, old_main_position:)
|
|
342
320
|
old_tail = line[(pos + 1)..] || []
|
|
343
321
|
variation0 = variation[0]
|
|
344
|
-
line
|
|
345
|
-
line[(pos + 1)..] = (variation[1..] || [])
|
|
322
|
+
splice_line!(line, pos, variation)
|
|
346
323
|
cont.variations = []
|
|
347
324
|
old_var = [cont, *old_tail]
|
|
348
325
|
variation0.variations =
|
|
@@ -361,11 +338,9 @@ module PGN
|
|
|
361
338
|
line[pos..] = []
|
|
362
339
|
else
|
|
363
340
|
first_variation = vars.shift
|
|
364
|
-
|
|
365
|
-
line[pos] = first_variation_move
|
|
366
|
-
line[(pos + 1)..] = (first_variation[1..] || [])
|
|
341
|
+
splice_line!(line, pos, first_variation)
|
|
367
342
|
cont.variations = []
|
|
368
|
-
|
|
343
|
+
first_variation[0].variations = vars
|
|
369
344
|
end
|
|
370
345
|
end
|
|
371
346
|
end
|
data/lib/pgn/pgn_parser.rb
CHANGED
|
@@ -8,7 +8,7 @@ require 'racc/parser.rb'
|
|
|
8
8
|
module PGN
|
|
9
9
|
class PgnParser < Racc::Parser
|
|
10
10
|
|
|
11
|
-
module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y',
|
|
11
|
+
module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 87)
|
|
12
12
|
|
|
13
13
|
def parse(input)
|
|
14
14
|
@lexer = PGN::Lexer.new(input)
|
|
@@ -29,6 +29,26 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 96)
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
+
# Fold a move's trailing suffix (comments, NAGs, variations, in any
|
|
33
|
+
# order) onto a MoveText. Multiple comments are concatenated with a
|
|
34
|
+
# space after each is individually cleaned; NAGs are merged in order;
|
|
35
|
+
# variations are collected in source order and then reversed, preserving
|
|
36
|
+
# the legacy order the serializer and round-trip specs depend on.
|
|
37
|
+
def build_move(notation, suffix)
|
|
38
|
+
comment_parts = []
|
|
39
|
+
nags = nil
|
|
40
|
+
variations = []
|
|
41
|
+
suffix.each do |kind, payload|
|
|
42
|
+
case kind
|
|
43
|
+
when :comment then comment_parts << MoveText.clean_text(payload)
|
|
44
|
+
when :nags then nags = nags ? nags + payload : payload
|
|
45
|
+
when :variation then variations << payload
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
comment = comment_parts.empty? ? nil : comment_parts.join(' ')
|
|
49
|
+
MoveText.new(notation, nags, comment, variations.reverse)
|
|
50
|
+
end
|
|
51
|
+
|
|
32
52
|
# Punctuation tokens are racc'd by their literal character (taken from
|
|
33
53
|
# PGN::Lexer::LITERAL_BYTES, the single source of truth for those
|
|
34
54
|
# characters); everything else is racc'd by the upcased lexer symbol.
|
|
@@ -66,42 +86,44 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 96)
|
|
|
66
86
|
##### State transition tables begin ###
|
|
67
87
|
|
|
68
88
|
racc_action_table = [
|
|
69
|
-
2, 16, 7, 17,
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
2, 16, 7, 17, 21, 14, 6, 24, 21, 6,
|
|
90
|
+
30, 24, 25, 16, 12, 17, 25, 14, 11, 18,
|
|
91
|
+
26, 28 ]
|
|
72
92
|
|
|
73
93
|
racc_action_check = [
|
|
74
|
-
1,
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
1, 29, 2, 29, 17, 29, 4, 17, 19, 1,
|
|
95
|
+
29, 19, 17, 10, 10, 10, 19, 10, 6, 11,
|
|
96
|
+
18, 22 ]
|
|
77
97
|
|
|
78
98
|
racc_action_pointer = [
|
|
79
|
-
nil, 0, 2, nil, -3, nil,
|
|
80
|
-
|
|
81
|
-
nil, nil, nil,
|
|
82
|
-
nil
|
|
99
|
+
nil, 0, 2, nil, -3, nil, 10, nil, nil, nil,
|
|
100
|
+
10, 17, nil, nil, nil, nil, nil, 1, 10, 5,
|
|
101
|
+
nil, nil, 15, nil, nil, nil, nil, nil, nil, -2,
|
|
102
|
+
nil ]
|
|
83
103
|
|
|
84
104
|
racc_action_default = [
|
|
85
|
-
-1, -
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
105
|
+
-1, -23, -23, -2, -8, -4, -23, 31, -3, -5,
|
|
106
|
+
-23, -23, -7, -9, -10, -11, -12, -13, -23, -14,
|
|
107
|
+
-15, -17, -18, -19, -20, -8, -6, -16, -21, -23,
|
|
108
|
+
-22 ]
|
|
89
109
|
|
|
90
110
|
racc_goto_table = [
|
|
91
|
-
10,
|
|
92
|
-
|
|
111
|
+
10, 5, 1, 20, 9, 27, 3, 4, 8, 19,
|
|
112
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
113
|
+
nil, 29 ]
|
|
93
114
|
|
|
94
115
|
racc_goto_check = [
|
|
95
|
-
6,
|
|
96
|
-
|
|
116
|
+
6, 5, 1, 10, 5, 10, 2, 3, 4, 9,
|
|
117
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
118
|
+
nil, 6 ]
|
|
97
119
|
|
|
98
120
|
racc_goto_pointer = [
|
|
99
|
-
nil,
|
|
100
|
-
|
|
121
|
+
nil, 2, 5, 6, 4, 0, -4, nil, nil, -8,
|
|
122
|
+
-14, nil, nil ]
|
|
101
123
|
|
|
102
124
|
racc_goto_default = [
|
|
103
125
|
nil, nil, nil, nil, nil, nil, nil, 13, 15, nil,
|
|
104
|
-
nil,
|
|
126
|
+
nil, 22, 23 ]
|
|
105
127
|
|
|
106
128
|
racc_reduce_table = [
|
|
107
129
|
0, 0, :racc_error,
|
|
@@ -116,23 +138,21 @@ racc_reduce_table = [
|
|
|
116
138
|
2, 19, :_reduce_9,
|
|
117
139
|
1, 20, :_reduce_10,
|
|
118
140
|
1, 20, :_reduce_none,
|
|
119
|
-
|
|
120
|
-
1,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
1, 20, :_reduce_12,
|
|
142
|
+
1, 21, :_reduce_13,
|
|
143
|
+
2, 21, :_reduce_14,
|
|
144
|
+
1, 22, :_reduce_15,
|
|
145
|
+
2, 22, :_reduce_16,
|
|
124
146
|
1, 23, :_reduce_17,
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
1, 23, :_reduce_18,
|
|
148
|
+
1, 23, :_reduce_19,
|
|
127
149
|
1, 24, :_reduce_20,
|
|
128
150
|
2, 24, :_reduce_21,
|
|
129
|
-
|
|
130
|
-
2, 22, :_reduce_23,
|
|
131
|
-
3, 25, :_reduce_24 ]
|
|
151
|
+
3, 25, :_reduce_22 ]
|
|
132
152
|
|
|
133
|
-
racc_reduce_n =
|
|
153
|
+
racc_reduce_n = 23
|
|
134
154
|
|
|
135
|
-
racc_shift_n =
|
|
155
|
+
racc_shift_n = 31
|
|
136
156
|
|
|
137
157
|
racc_token_table = {
|
|
138
158
|
false => 0,
|
|
@@ -193,8 +213,8 @@ Racc_token_to_s_table = [
|
|
|
193
213
|
"element_sequence",
|
|
194
214
|
"element",
|
|
195
215
|
"san_move_annotated",
|
|
196
|
-
"
|
|
197
|
-
"
|
|
216
|
+
"move_suffix",
|
|
217
|
+
"suffix_item",
|
|
198
218
|
"annotation_list",
|
|
199
219
|
"variation" ]
|
|
200
220
|
Ractor.make_shareable(Racc_token_to_s_table) if defined?(Ractor)
|
|
@@ -293,101 +313,80 @@ module_eval(<<'.,.,', 'pgn_parser.y', 50)
|
|
|
293
313
|
|
|
294
314
|
module_eval(<<'.,.,', 'pgn_parser.y', 54)
|
|
295
315
|
def _reduce_12(val, _values, result)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
316
|
+
# A standalone comment (not attached to a move) becomes the game
|
|
317
|
+
# comment; the last such comment in the game wins.
|
|
318
|
+
@game_comment = val[0]
|
|
319
|
+
result = nil
|
|
299
320
|
|
|
300
321
|
result
|
|
301
322
|
end
|
|
302
323
|
.,.,
|
|
303
324
|
|
|
304
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
325
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 61)
|
|
305
326
|
def _reduce_13(val, _values, result)
|
|
306
|
-
|
|
307
|
-
# comment; the last such comment in the game wins.
|
|
308
|
-
@game_comment = val[0]
|
|
309
|
-
result = nil
|
|
310
|
-
|
|
327
|
+
result = MoveText.new(val[0])
|
|
311
328
|
result
|
|
312
329
|
end
|
|
313
330
|
.,.,
|
|
314
331
|
|
|
315
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
332
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 62)
|
|
316
333
|
def _reduce_14(val, _values, result)
|
|
317
|
-
result =
|
|
334
|
+
result = build_move(val[0], val[1])
|
|
318
335
|
result
|
|
319
336
|
end
|
|
320
337
|
.,.,
|
|
321
338
|
|
|
322
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
339
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 70)
|
|
323
340
|
def _reduce_15(val, _values, result)
|
|
324
|
-
result =
|
|
341
|
+
result = [val[0]]
|
|
325
342
|
result
|
|
326
343
|
end
|
|
327
344
|
.,.,
|
|
328
345
|
|
|
329
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
346
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 71)
|
|
330
347
|
def _reduce_16(val, _values, result)
|
|
331
|
-
result = [
|
|
348
|
+
result = val[0] << val[1]
|
|
332
349
|
result
|
|
333
350
|
end
|
|
334
351
|
.,.,
|
|
335
352
|
|
|
336
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
353
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 74)
|
|
337
354
|
def _reduce_17(val, _values, result)
|
|
338
|
-
result = [val[0]
|
|
355
|
+
result = [:comment, val[0]]
|
|
339
356
|
result
|
|
340
357
|
end
|
|
341
358
|
.,.,
|
|
342
359
|
|
|
343
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
360
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 75)
|
|
344
361
|
def _reduce_18(val, _values, result)
|
|
345
|
-
result = [
|
|
362
|
+
result = [:nags, val[0]]
|
|
346
363
|
result
|
|
347
364
|
end
|
|
348
365
|
.,.,
|
|
349
366
|
|
|
350
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
367
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 76)
|
|
351
368
|
def _reduce_19(val, _values, result)
|
|
352
|
-
result = [
|
|
369
|
+
result = [:variation, val[0]]
|
|
353
370
|
result
|
|
354
371
|
end
|
|
355
372
|
.,.,
|
|
356
373
|
|
|
357
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
374
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 79)
|
|
358
375
|
def _reduce_20(val, _values, result)
|
|
359
376
|
result = [val[0]]
|
|
360
377
|
result
|
|
361
378
|
end
|
|
362
379
|
.,.,
|
|
363
380
|
|
|
364
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
381
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 80)
|
|
365
382
|
def _reduce_21(val, _values, result)
|
|
366
383
|
result = val[0] << val[1]
|
|
367
384
|
result
|
|
368
385
|
end
|
|
369
386
|
.,.,
|
|
370
387
|
|
|
371
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
388
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 83)
|
|
372
389
|
def _reduce_22(val, _values, result)
|
|
373
|
-
result = [val[0]]
|
|
374
|
-
result
|
|
375
|
-
end
|
|
376
|
-
.,.,
|
|
377
|
-
|
|
378
|
-
module_eval(<<'.,.,', 'pgn_parser.y', 85)
|
|
379
|
-
def _reduce_23(val, _values, result)
|
|
380
|
-
# Left-recursive; the legacy variation-order reversal is applied as
|
|
381
|
-
# a single explicit `.reverse` where the list is consumed in
|
|
382
|
-
# {element}, instead of being implicit in recursion direction.
|
|
383
|
-
result = val[0] << val[1]
|
|
384
|
-
|
|
385
|
-
result
|
|
386
|
-
end
|
|
387
|
-
.,.,
|
|
388
|
-
|
|
389
|
-
module_eval(<<'.,.,', 'pgn_parser.y', 92)
|
|
390
|
-
def _reduce_24(val, _values, result)
|
|
391
390
|
result = val[1]
|
|
392
391
|
result
|
|
393
392
|
end
|
data/lib/pgn/pgn_parser.y
CHANGED
|
@@ -50,12 +50,6 @@ rule
|
|
|
50
50
|
element:
|
|
51
51
|
MOVE_NUMBER { result = nil }
|
|
52
52
|
| san_move_annotated
|
|
53
|
-
| san_move_annotated variation_list
|
|
54
|
-
{
|
|
55
|
-
result = val[0]
|
|
56
|
-
result.variations = val[1].reverse
|
|
57
|
-
result
|
|
58
|
-
}
|
|
59
53
|
| COMMENT
|
|
60
54
|
{
|
|
61
55
|
# A standalone comment (not attached to a move) becomes the game
|
|
@@ -66,28 +60,25 @@ rule
|
|
|
66
60
|
|
|
67
61
|
san_move_annotated:
|
|
68
62
|
SAN_MOVE { result = MoveText.new(val[0]) }
|
|
69
|
-
| SAN_MOVE
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
| SAN_MOVE move_suffix { result = build_move(val[0], val[1]) }
|
|
64
|
+
|
|
65
|
+
# The trailing material after a move: any number of comments, NAGs, and
|
|
66
|
+
# variations, in any order (PGN spec §3.2.5). Each suffix item is tagged
|
|
67
|
+
# [:comment, c] | [:nags, a] | [:variation, v]; {build_move} folds them
|
|
68
|
+
# onto the MoveText. This generalizes the old single-comment move_trailer,
|
|
69
|
+
# which rejected two comments before a variation (`{a} {b} (...)`).
|
|
70
|
+
move_suffix:
|
|
71
|
+
suffix_item { result = [val[0]] }
|
|
72
|
+
| move_suffix suffix_item { result = val[0] << val[1] }
|
|
73
|
+
|
|
74
|
+
suffix_item:
|
|
75
|
+
COMMENT { result = [:comment, val[0]] }
|
|
76
|
+
| annotation_list { result = [:nags, val[0]] }
|
|
77
|
+
| variation { result = [:variation, val[0]] }
|
|
77
78
|
|
|
78
79
|
annotation_list:
|
|
79
|
-
NAG
|
|
80
|
-
| annotation_list NAG
|
|
81
|
-
|
|
82
|
-
variation_list:
|
|
83
|
-
variation { result = [val[0]] }
|
|
84
|
-
| variation_list variation
|
|
85
|
-
{
|
|
86
|
-
# Left-recursive; the legacy variation-order reversal is applied as
|
|
87
|
-
# a single explicit `.reverse` where the list is consumed in
|
|
88
|
-
# {element}, instead of being implicit in recursion direction.
|
|
89
|
-
result = val[0] << val[1]
|
|
90
|
-
}
|
|
80
|
+
NAG { result = [val[0]] }
|
|
81
|
+
| annotation_list NAG { result = val[0] << val[1] }
|
|
91
82
|
|
|
92
83
|
variation:
|
|
93
84
|
'(' element_sequence ')' { result = val[1] }
|
|
@@ -113,6 +104,26 @@ rule
|
|
|
113
104
|
|
|
114
105
|
private
|
|
115
106
|
|
|
107
|
+
# Fold a move's trailing suffix (comments, NAGs, variations, in any
|
|
108
|
+
# order) onto a MoveText. Multiple comments are concatenated with a
|
|
109
|
+
# space after each is individually cleaned; NAGs are merged in order;
|
|
110
|
+
# variations are collected in source order and then reversed, preserving
|
|
111
|
+
# the legacy order the serializer and round-trip specs depend on.
|
|
112
|
+
def build_move(notation, suffix)
|
|
113
|
+
comment_parts = []
|
|
114
|
+
nags = nil
|
|
115
|
+
variations = []
|
|
116
|
+
suffix.each do |kind, payload|
|
|
117
|
+
case kind
|
|
118
|
+
when :comment then comment_parts << MoveText.clean_text(payload)
|
|
119
|
+
when :nags then nags = nags ? nags + payload : payload
|
|
120
|
+
when :variation then variations << payload
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
comment = comment_parts.empty? ? nil : comment_parts.join(' ')
|
|
124
|
+
MoveText.new(notation, nags, comment, variations.reverse)
|
|
125
|
+
end
|
|
126
|
+
|
|
116
127
|
# Punctuation tokens are racc'd by their literal character (taken from
|
|
117
128
|
# PGN::Lexer::LITERAL_BYTES, the single source of truth for those
|
|
118
129
|
# characters); everything else is racc'd by the upcased lexer symbol.
|
data/lib/pgn/position.rb
CHANGED
|
@@ -179,8 +179,10 @@ module PGN
|
|
|
179
179
|
#
|
|
180
180
|
# @return [Boolean]
|
|
181
181
|
def in_check?
|
|
182
|
+
return @in_check if defined?(@in_check)
|
|
183
|
+
|
|
182
184
|
king = PGN::Attack.king_idx(board, mover_color)
|
|
183
|
-
!king.nil? && PGN::Attack.attacked?(board, king, opponent_color)
|
|
185
|
+
@in_check = !king.nil? && PGN::Attack.attacked?(board, king, opponent_color)
|
|
184
186
|
end
|
|
185
187
|
|
|
186
188
|
# The algebraic squares of every piece of the given color that attacks
|
data/lib/pgn/version.rb
CHANGED
data/lib/pgn.rb
CHANGED
|
@@ -26,6 +26,10 @@ module PGN
|
|
|
26
26
|
# @see http://www.chessclub.com/help/PGN-spec PGN Specification
|
|
27
27
|
#
|
|
28
28
|
def self.parse(pgn, encoding = Encoding::ISO_8859_1)
|
|
29
|
+
# Operate on a private copy so a frozen caller string is never mutated
|
|
30
|
+
# (force_encoding mutates its receiver; under frozen-string literals
|
|
31
|
+
# this would otherwise raise FrozenError on the target Ruby).
|
|
32
|
+
pgn = pgn.dup
|
|
29
33
|
pgn.force_encoding(encoding) if encoding
|
|
30
34
|
|
|
31
35
|
PGN::Parser.new.parse(pgn).map do |game|
|
data/spec/game_spec.rb
CHANGED
|
@@ -133,20 +133,6 @@ describe PGN::Game do
|
|
|
133
133
|
describe 'node mutation round-trip' do
|
|
134
134
|
non_round_trip = %w[doublequotes.pgn specialcharacters.pgn].freeze
|
|
135
135
|
|
|
136
|
-
# A node-sig tree with variations sorted, so the comparison is
|
|
137
|
-
# order-independent (the parser reverses variation order on each parse).
|
|
138
|
-
def node_sig(move)
|
|
139
|
-
[move.notation, (move.variations || []).map { line_sig(_1) }.sort]
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def line_sig(line)
|
|
143
|
-
line.map { node_sig(_1) }
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def tree_sig(game)
|
|
147
|
-
game.moves.map { node_sig(_1) }
|
|
148
|
-
end
|
|
149
|
-
|
|
150
136
|
# First node (DFS over children) that has at least one variation. Such a
|
|
151
137
|
# node always has a non-nil continuation, so add_variation can branch.
|
|
152
138
|
def first_branch_with_variations(node)
|
data/spec/node_spec.rb
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
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 (the same
|
|
7
|
-
# approach the existing game_spec round-trip gate uses), and reference
|
|
8
|
-
# variations by notation rather than by index.
|
|
9
|
-
def node_sig(move)
|
|
10
|
-
[move.notation, (move.variations || []).map { line_sig(_1) }.sort]
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def line_sig(line)
|
|
14
|
-
line.map { node_sig(_1) }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def tree_sig(game)
|
|
18
|
-
game.moves.map { node_sig(_1) }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
3
|
describe PGN::Node do
|
|
22
4
|
let(:game) { PGN.parse(File.read(File.expand_path('pgn_files/variations.pgn', __dir__), encoding: Encoding::ISO_8859_1)).first }
|
|
23
5
|
let(:root) { game.root }
|