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.
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
- vars = cont.variations
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
- return @game.root if root?
161
-
162
- i = sibling_index
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
- return @game.root if root?
178
-
179
- i = sibling_index
180
- sibs = @parent.children
181
- return @game.root if i.nil? || i == sibs.size - 1
182
-
183
- cont = parent_continuation
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
- return @game.root if root?
200
-
201
- i = sibling_index
202
- return @game.root if i.nil? || i.zero?
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
- return @game.root if root?
217
-
218
- i = sibling_index
219
- return @game.root if i.nil?
220
-
221
- cont = parent_continuation
222
- normalize_branch_point(cont)
223
- vars = cont.variations
224
- if i.zero?
225
- return @game.root if vars.empty?
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
- return @game.root if root?
241
-
242
- i = sibling_index
243
- return @game.root if i.nil?
244
-
245
- cont = parent_continuation
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
- def normalize_castling(san)
302
- san.include?('0') ? san.gsub('0', 'O') : san
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
- # Make +vk+ (= [vk0, *tail]) the new mainline continuation at the
335
- # parent's position, replacing +cont+. The old mainline (+cont+ and
336
- # its tail) becomes a variation appended to +others+ either before
337
- # (+old_main_position == :first+) or after (+:last+). +cont.variations+
338
- # is cleared (its at-point variations are now +vk0+'s siblings).
339
- def make_mainline(cont, variation, others, old_main_position:)
340
- line = @parent.line
341
- pos = @parent.index + 1
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[pos] = variation0
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
- first_variation_move = first_variation[0]
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
- first_variation_move.variations = vars
343
+ first_variation[0].variations = vars
369
344
  end
370
345
  end
371
346
  end
@@ -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', 96)
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, 23, 14, 6, 25, 11, 6,
70
- 32, 16, 12, 17, 30, 14, 18, 31, 21, 26,
71
- 21, 25, 31 ]
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, 28, 2, 28, 17, 28, 4, 17, 6, 1,
75
- 28, 10, 10, 10, 24, 10, 11, 24, 15, 18,
76
- 19, 23, 29 ]
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, 0, nil, nil, nil,
80
- 8, 14, nil, nil, nil, 7, nil, 1, 9, 9,
81
- nil, nil, nil, 15, 11, nil, nil, nil, -2, 16,
82
- nil, nil, 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, -25, -25, -2, -8, -4, -25, 33, -3, -5,
86
- -25, -25, -7, -9, -10, -11, -13, -14, -25, -12,
87
- -22, -8, -15, -16, -17, -20, -6, -23, -25, -19,
88
- -18, -21, -24 ]
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, 24, 20, 1, 3, 5, 27, 29, 9, 4,
92
- 8, 19, 22, nil, nil, nil, nil, 28 ]
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, 11, 12, 1, 2, 5, 12, 11, 5, 3,
96
- 4, 9, 10, nil, nil, nil, nil, 6 ]
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, 3, 3, 8, 6, 4, -4, nil, nil, -4,
100
- -5, -16, -13 ]
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, nil, 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
- 2, 20, :_reduce_12,
120
- 1, 20, :_reduce_13,
121
- 1, 21, :_reduce_14,
122
- 2, 21, :_reduce_15,
123
- 1, 23, :_reduce_16,
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
- 2, 23, :_reduce_18,
126
- 2, 23, :_reduce_19,
147
+ 1, 23, :_reduce_18,
148
+ 1, 23, :_reduce_19,
127
149
  1, 24, :_reduce_20,
128
150
  2, 24, :_reduce_21,
129
- 1, 22, :_reduce_22,
130
- 2, 22, :_reduce_23,
131
- 3, 25, :_reduce_24 ]
151
+ 3, 25, :_reduce_22 ]
132
152
 
133
- racc_reduce_n = 25
153
+ racc_reduce_n = 23
134
154
 
135
- racc_shift_n = 33
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
- "variation_list",
197
- "move_trailer",
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
- result = val[0]
297
- result.variations = val[1].reverse
298
- result
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', 60)
325
+ module_eval(<<'.,.,', 'pgn_parser.y', 61)
305
326
  def _reduce_13(val, _values, result)
306
- # A standalone comment (not attached to a move) becomes the game
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', 67)
332
+ module_eval(<<'.,.,', 'pgn_parser.y', 62)
316
333
  def _reduce_14(val, _values, result)
317
- result = MoveText.new(val[0])
334
+ result = build_move(val[0], val[1])
318
335
  result
319
336
  end
320
337
  .,.,
321
338
 
322
- module_eval(<<'.,.,', 'pgn_parser.y', 68)
339
+ module_eval(<<'.,.,', 'pgn_parser.y', 70)
323
340
  def _reduce_15(val, _values, result)
324
- result = MoveText.new(val[0], *val[1])
341
+ result = [val[0]]
325
342
  result
326
343
  end
327
344
  .,.,
328
345
 
329
- module_eval(<<'.,.,', 'pgn_parser.y', 72)
346
+ module_eval(<<'.,.,', 'pgn_parser.y', 71)
330
347
  def _reduce_16(val, _values, result)
331
- result = [nil, val[0]]
348
+ result = val[0] << val[1]
332
349
  result
333
350
  end
334
351
  .,.,
335
352
 
336
- module_eval(<<'.,.,', 'pgn_parser.y', 73)
353
+ module_eval(<<'.,.,', 'pgn_parser.y', 74)
337
354
  def _reduce_17(val, _values, result)
338
- result = [val[0], nil]
355
+ result = [:comment, val[0]]
339
356
  result
340
357
  end
341
358
  .,.,
342
359
 
343
- module_eval(<<'.,.,', 'pgn_parser.y', 74)
360
+ module_eval(<<'.,.,', 'pgn_parser.y', 75)
344
361
  def _reduce_18(val, _values, result)
345
- result = [val[0], val[1]]
362
+ result = [:nags, val[0]]
346
363
  result
347
364
  end
348
365
  .,.,
349
366
 
350
- module_eval(<<'.,.,', 'pgn_parser.y', 75)
367
+ module_eval(<<'.,.,', 'pgn_parser.y', 76)
351
368
  def _reduce_19(val, _values, result)
352
- result = [val[1], val[0]]
369
+ result = [:variation, val[0]]
353
370
  result
354
371
  end
355
372
  .,.,
356
373
 
357
- module_eval(<<'.,.,', 'pgn_parser.y', 78)
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', 79)
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', 82)
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 move_trailer { result = MoveText.new(val[0], *val[1]) }
70
-
71
- # [annotation_list, comment], in whichever order they followed the move.
72
- move_trailer:
73
- COMMENT { result = [nil, val[0]] }
74
- | annotation_list { result = [val[0], nil] }
75
- | annotation_list COMMENT { result = [val[0], val[1]] }
76
- | COMMENT annotation_list { result = [val[1], val[0]] }
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 { result = [val[0]] }
80
- | annotation_list NAG { result = val[0] << val[1] }
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
@@ -1,3 +1,3 @@
1
1
  module PGN
2
- VERSION = '2.0.1'.freeze
2
+ VERSION = '2.0.3'.freeze
3
3
  end
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 }