pgn2 1.0.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/.github/workflows/release.yml +103 -0
- data/.gitignore +2 -1
- data/CHANGELOG.md +55 -0
- data/README.md +38 -15
- data/TODO.md +38 -0
- data/bench/IMPROVEMENTS.md +92 -0
- data/bench/baseline_moves.pre-quickwins.txt +23 -0
- data/bench/baseline_moves.txt +6 -6
- data/bench/baseline_parse.pre-quickwins.txt +26 -0
- data/bench/baseline_parse.txt +6 -6
- data/docs/superpowers/specs/2026-08-13-pgn-performance-quick-wins-design.md +227 -0
- data/lib/pgn/game.rb +30 -19
- data/lib/pgn/lexer.rb +56 -37
- data/lib/pgn/move.rb +5 -2
- data/lib/pgn/move_calculator.rb +11 -8
- data/lib/pgn/parser.rb +5 -15
- data/lib/pgn/pgn_parser.rb +84 -76
- data/lib/pgn/pgn_parser.y +25 -20
- data/lib/pgn/position.rb +3 -2
- data/lib/pgn/version.rb +1 -1
- data/spec/parser_spec.rb +6 -38
- metadata +5 -1
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', 97)
|
|
12
12
|
|
|
13
13
|
def parse(input)
|
|
14
14
|
@lexer = PGN::Lexer.new(input)
|
|
@@ -19,27 +19,26 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 91)
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def next_token
|
|
22
|
-
|
|
23
|
-
return [false, false] unless
|
|
24
|
-
|
|
22
|
+
pair = @lexer.next_token_pair
|
|
23
|
+
return [false, false] unless pair
|
|
24
|
+
type, value = pair
|
|
25
|
+
[translate_type(type), value]
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
private
|
|
28
29
|
|
|
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
|
|
39
|
+
|
|
29
40
|
def translate_type(sym)
|
|
30
|
-
|
|
31
|
-
when :string then :STRING
|
|
32
|
-
when :comment then :COMMENT
|
|
33
|
-
when :game_termination then :GAME_TERMINATION
|
|
34
|
-
when :san_move then :SAN_MOVE
|
|
35
|
-
when :nag then :NAG
|
|
36
|
-
when :move_number then :MOVE_NUMBER
|
|
37
|
-
when :tag_name then :TAG_NAME
|
|
38
|
-
when :lbracket then '['
|
|
39
|
-
when :rbracket then ']'
|
|
40
|
-
when :lparen then '('
|
|
41
|
-
when :rparen then ')'
|
|
42
|
-
else
|
|
41
|
+
TOKEN_TRANSLATIONS.fetch(sym) do
|
|
43
42
|
raise "PGN::Lexer produced an unknown token type: #{sym.inspect}"
|
|
44
43
|
end
|
|
45
44
|
end
|
|
@@ -65,42 +64,42 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 91)
|
|
|
65
64
|
##### State transition tables begin ###
|
|
66
65
|
|
|
67
66
|
racc_action_table = [
|
|
68
|
-
2, 16, 7, 17,
|
|
69
|
-
|
|
70
|
-
21,
|
|
67
|
+
2, 16, 7, 17, 23, 14, 6, 25, 11, 6,
|
|
68
|
+
32, 16, 12, 17, 30, 14, 18, 31, 21, 26,
|
|
69
|
+
21, 25, 31 ]
|
|
71
70
|
|
|
72
71
|
racc_action_check = [
|
|
73
|
-
1,
|
|
74
|
-
|
|
75
|
-
20,
|
|
72
|
+
1, 28, 2, 28, 17, 28, 5, 17, 6, 1,
|
|
73
|
+
28, 9, 9, 9, 24, 9, 11, 24, 15, 18,
|
|
74
|
+
20, 23, 29 ]
|
|
76
75
|
|
|
77
76
|
racc_action_pointer = [
|
|
78
77
|
nil, 0, 2, nil, nil, -3, 0, nil, nil, 8,
|
|
79
78
|
nil, 14, nil, nil, nil, 7, nil, 1, 9, nil,
|
|
80
|
-
9, nil, 15, 11, nil, nil, nil, -2, 16,
|
|
81
|
-
nil, nil ]
|
|
79
|
+
9, nil, nil, 15, 11, nil, nil, nil, -2, 16,
|
|
80
|
+
nil, nil, nil ]
|
|
82
81
|
|
|
83
82
|
racc_action_default = [
|
|
84
|
-
-1, -
|
|
85
|
-
-5, -
|
|
86
|
-
-
|
|
87
|
-
-
|
|
83
|
+
-1, -25, -25, -2, -8, -4, -25, 33, -3, -25,
|
|
84
|
+
-5, -25, -7, -9, -10, -11, -13, -14, -25, -12,
|
|
85
|
+
-22, -8, -15, -16, -17, -20, -6, -23, -25, -19,
|
|
86
|
+
-18, -21, -24 ]
|
|
88
87
|
|
|
89
88
|
racc_goto_table = [
|
|
90
|
-
9,
|
|
91
|
-
|
|
89
|
+
9, 24, 4, 19, 1, 3, 10, 29, 27, 8,
|
|
90
|
+
22, nil, nil, nil, nil, nil, nil, 28 ]
|
|
92
91
|
|
|
93
92
|
racc_goto_check = [
|
|
94
|
-
6, 9,
|
|
95
|
-
|
|
93
|
+
6, 11, 3, 9, 1, 2, 3, 11, 9, 4,
|
|
94
|
+
10, nil, nil, nil, nil, nil, nil, 6 ]
|
|
96
95
|
|
|
97
96
|
racc_goto_pointer = [
|
|
98
|
-
nil,
|
|
99
|
-
-
|
|
97
|
+
nil, 4, 4, 1, 5, nil, -4, nil, nil, -12,
|
|
98
|
+
-7, -16, nil ]
|
|
100
99
|
|
|
101
100
|
racc_goto_default = [
|
|
102
101
|
nil, nil, nil, nil, nil, 5, nil, 13, 15, nil,
|
|
103
|
-
nil, 20 ]
|
|
102
|
+
nil, nil, 20 ]
|
|
104
103
|
|
|
105
104
|
racc_reduce_table = [
|
|
106
105
|
0, 0, :racc_error,
|
|
@@ -119,18 +118,19 @@ racc_reduce_table = [
|
|
|
119
118
|
1, 20, :_reduce_13,
|
|
120
119
|
1, 21, :_reduce_14,
|
|
121
120
|
2, 21, :_reduce_15,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
1, 23, :_reduce_16,
|
|
122
|
+
1, 23, :_reduce_17,
|
|
123
|
+
2, 23, :_reduce_18,
|
|
124
|
+
2, 23, :_reduce_19,
|
|
125
|
+
1, 24, :_reduce_20,
|
|
126
|
+
2, 24, :_reduce_21,
|
|
127
|
+
1, 22, :_reduce_22,
|
|
128
|
+
2, 22, :_reduce_23,
|
|
129
|
+
3, 25, :_reduce_24 ]
|
|
130
130
|
|
|
131
|
-
racc_reduce_n =
|
|
131
|
+
racc_reduce_n = 25
|
|
132
132
|
|
|
133
|
-
racc_shift_n =
|
|
133
|
+
racc_shift_n = 33
|
|
134
134
|
|
|
135
135
|
racc_token_table = {
|
|
136
136
|
false => 0,
|
|
@@ -192,6 +192,7 @@ Racc_token_to_s_table = [
|
|
|
192
192
|
"element",
|
|
193
193
|
"san_move_annotated",
|
|
194
194
|
"variation_list",
|
|
195
|
+
"move_trailer",
|
|
195
196
|
"annotation_list",
|
|
196
197
|
"variation" ]
|
|
197
198
|
Ractor.make_shareable(Racc_token_to_s_table) if defined?(Ractor)
|
|
@@ -202,21 +203,21 @@ Racc_debug_parser = false
|
|
|
202
203
|
|
|
203
204
|
# reduce 0 omitted
|
|
204
205
|
|
|
205
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
206
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 9)
|
|
206
207
|
def _reduce_1(val, _values, result)
|
|
207
208
|
result = []
|
|
208
209
|
result
|
|
209
210
|
end
|
|
210
211
|
.,.,
|
|
211
212
|
|
|
212
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
213
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 10)
|
|
213
214
|
def _reduce_2(val, _values, result)
|
|
214
215
|
result = val[0] << val[1]
|
|
215
216
|
result
|
|
216
217
|
end
|
|
217
218
|
.,.,
|
|
218
219
|
|
|
219
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
220
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 15)
|
|
220
221
|
def _reduce_3(val, _values, result)
|
|
221
222
|
result = val[1].pop
|
|
222
223
|
result = {
|
|
@@ -231,14 +232,14 @@ module_eval(<<'.,.,', 'pgn_parser.y', 13)
|
|
|
231
232
|
end
|
|
232
233
|
.,.,
|
|
233
234
|
|
|
234
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
235
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 26)
|
|
235
236
|
def _reduce_4(val, _values, result)
|
|
236
237
|
result = val[0]
|
|
237
238
|
result
|
|
238
239
|
end
|
|
239
240
|
.,.,
|
|
240
241
|
|
|
241
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
242
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 29)
|
|
242
243
|
def _reduce_5(val, _values, result)
|
|
243
244
|
# Right-recursive with section.merge(pair) reproduces the legacy
|
|
244
245
|
# whittle parser's tag semantics exactly: reverse source insertion
|
|
@@ -250,28 +251,28 @@ module_eval(<<'.,.,', 'pgn_parser.y', 27)
|
|
|
250
251
|
end
|
|
251
252
|
.,.,
|
|
252
253
|
|
|
253
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
254
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 37)
|
|
254
255
|
def _reduce_6(val, _values, result)
|
|
255
256
|
result = { val[1] => val[2][1...-1] }
|
|
256
257
|
result
|
|
257
258
|
end
|
|
258
259
|
.,.,
|
|
259
260
|
|
|
260
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
261
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 40)
|
|
261
262
|
def _reduce_7(val, _values, result)
|
|
262
263
|
result = val[0] << val[1]
|
|
263
264
|
result
|
|
264
265
|
end
|
|
265
266
|
.,.,
|
|
266
267
|
|
|
267
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
268
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 43)
|
|
268
269
|
def _reduce_8(val, _values, result)
|
|
269
270
|
result = []
|
|
270
271
|
result
|
|
271
272
|
end
|
|
272
273
|
.,.,
|
|
273
274
|
|
|
274
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
275
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 46)
|
|
275
276
|
def _reduce_9(val, _values, result)
|
|
276
277
|
result = val[1].nil? ? val[0] : val[0] << val[1]
|
|
277
278
|
|
|
@@ -279,7 +280,7 @@ module_eval(<<'.,.,', 'pgn_parser.y', 44)
|
|
|
279
280
|
end
|
|
280
281
|
.,.,
|
|
281
282
|
|
|
282
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
283
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 50)
|
|
283
284
|
def _reduce_10(val, _values, result)
|
|
284
285
|
result = nil
|
|
285
286
|
result
|
|
@@ -288,7 +289,7 @@ module_eval(<<'.,.,', 'pgn_parser.y', 48)
|
|
|
288
289
|
|
|
289
290
|
# reduce 11 omitted
|
|
290
291
|
|
|
291
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
292
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 54)
|
|
292
293
|
def _reduce_12(val, _values, result)
|
|
293
294
|
result = val[0]
|
|
294
295
|
result.variations = val[1]
|
|
@@ -298,7 +299,7 @@ module_eval(<<'.,.,', 'pgn_parser.y', 52)
|
|
|
298
299
|
end
|
|
299
300
|
.,.,
|
|
300
301
|
|
|
301
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
302
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 60)
|
|
302
303
|
def _reduce_13(val, _values, result)
|
|
303
304
|
# A standalone comment (not attached to a move) becomes the game
|
|
304
305
|
# comment; the last such comment in the game wins.
|
|
@@ -309,64 +310,71 @@ module_eval(<<'.,.,', 'pgn_parser.y', 58)
|
|
|
309
310
|
end
|
|
310
311
|
.,.,
|
|
311
312
|
|
|
312
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
313
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 67)
|
|
313
314
|
def _reduce_14(val, _values, result)
|
|
314
315
|
result = MoveText.new(val[0])
|
|
315
316
|
result
|
|
316
317
|
end
|
|
317
318
|
.,.,
|
|
318
319
|
|
|
319
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
320
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 68)
|
|
320
321
|
def _reduce_15(val, _values, result)
|
|
321
|
-
result = MoveText.new(val[0],
|
|
322
|
+
result = MoveText.new(val[0], *val[1])
|
|
322
323
|
result
|
|
323
324
|
end
|
|
324
325
|
.,.,
|
|
325
326
|
|
|
326
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
327
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 72)
|
|
327
328
|
def _reduce_16(val, _values, result)
|
|
328
|
-
result =
|
|
329
|
+
result = [nil, val[0]]
|
|
329
330
|
result
|
|
330
331
|
end
|
|
331
332
|
.,.,
|
|
332
333
|
|
|
333
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
334
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 73)
|
|
334
335
|
def _reduce_17(val, _values, result)
|
|
335
|
-
result =
|
|
336
|
+
result = [val[0], nil]
|
|
336
337
|
result
|
|
337
338
|
end
|
|
338
339
|
.,.,
|
|
339
340
|
|
|
340
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
341
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 74)
|
|
341
342
|
def _reduce_18(val, _values, result)
|
|
342
|
-
result =
|
|
343
|
+
result = [val[0], val[1]]
|
|
343
344
|
result
|
|
344
345
|
end
|
|
345
346
|
.,.,
|
|
346
347
|
|
|
347
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
348
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 75)
|
|
348
349
|
def _reduce_19(val, _values, result)
|
|
349
|
-
result = [val[0]]
|
|
350
|
+
result = [val[1], val[0]]
|
|
350
351
|
result
|
|
351
352
|
end
|
|
352
353
|
.,.,
|
|
353
354
|
|
|
354
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
355
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 78)
|
|
355
356
|
def _reduce_20(val, _values, result)
|
|
356
|
-
result = val[0]
|
|
357
|
+
result = [val[0]]
|
|
357
358
|
result
|
|
358
359
|
end
|
|
359
360
|
.,.,
|
|
360
361
|
|
|
361
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
362
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 79)
|
|
362
363
|
def _reduce_21(val, _values, result)
|
|
363
|
-
result =
|
|
364
|
+
result = val[0] << val[1]
|
|
364
365
|
result
|
|
365
366
|
end
|
|
366
367
|
.,.,
|
|
367
368
|
|
|
368
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
369
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 82)
|
|
369
370
|
def _reduce_22(val, _values, result)
|
|
371
|
+
result = [val[0]]
|
|
372
|
+
result
|
|
373
|
+
end
|
|
374
|
+
.,.,
|
|
375
|
+
|
|
376
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 85)
|
|
377
|
+
def _reduce_23(val, _values, result)
|
|
370
378
|
# Right-recursive prepend reproduces the legacy whittle parser's
|
|
371
379
|
# variation-order reversal on every parse. This keeps parsed-game
|
|
372
380
|
# serialization byte-compatible with the legacy behavior; the quirk
|
|
@@ -377,8 +385,8 @@ module_eval(<<'.,.,', 'pgn_parser.y', 79)
|
|
|
377
385
|
end
|
|
378
386
|
.,.,
|
|
379
387
|
|
|
380
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
381
|
-
def
|
|
388
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 93)
|
|
389
|
+
def _reduce_24(val, _values, result)
|
|
382
390
|
result = val[1]
|
|
383
391
|
result
|
|
384
392
|
end
|
data/lib/pgn/pgn_parser.y
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
class PGN::PgnParser
|
|
2
4
|
|
|
3
5
|
token STRING COMMENT GAME_TERMINATION SAN_MOVE NAG MOVE_NUMBER TAG_NAME
|
|
@@ -64,10 +66,14 @@ rule
|
|
|
64
66
|
|
|
65
67
|
san_move_annotated:
|
|
66
68
|
SAN_MOVE { result = MoveText.new(val[0]) }
|
|
67
|
-
| SAN_MOVE
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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]] }
|
|
71
77
|
|
|
72
78
|
annotation_list:
|
|
73
79
|
NAG { result = [val[0]] }
|
|
@@ -98,27 +104,26 @@ rule
|
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
def next_token
|
|
101
|
-
|
|
102
|
-
return [false, false] unless
|
|
103
|
-
|
|
107
|
+
pair = @lexer.next_token_pair
|
|
108
|
+
return [false, false] unless pair
|
|
109
|
+
type, value = pair
|
|
110
|
+
[translate_type(type), value]
|
|
104
111
|
end
|
|
105
112
|
|
|
106
113
|
private
|
|
107
114
|
|
|
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
|
|
124
|
+
|
|
108
125
|
def translate_type(sym)
|
|
109
|
-
|
|
110
|
-
when :string then :STRING
|
|
111
|
-
when :comment then :COMMENT
|
|
112
|
-
when :game_termination then :GAME_TERMINATION
|
|
113
|
-
when :san_move then :SAN_MOVE
|
|
114
|
-
when :nag then :NAG
|
|
115
|
-
when :move_number then :MOVE_NUMBER
|
|
116
|
-
when :tag_name then :TAG_NAME
|
|
117
|
-
when :lbracket then '['
|
|
118
|
-
when :rbracket then ']'
|
|
119
|
-
when :lparen then '('
|
|
120
|
-
when :rparen then ')'
|
|
121
|
-
else
|
|
126
|
+
TOKEN_TRANSLATIONS.fetch(sym) do
|
|
122
127
|
raise "PGN::Lexer produced an unknown token type: #{sym.inspect}"
|
|
123
128
|
end
|
|
124
129
|
end
|
data/lib/pgn/position.rb
CHANGED
|
@@ -81,7 +81,8 @@ module PGN
|
|
|
81
81
|
move = PGN::Move.new(str, player)
|
|
82
82
|
calculator = PGN::MoveCalculator.new(board, move)
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
restrictions = calculator.castling_restrictions
|
|
85
|
+
new_castling = restrictions.empty? ? castling : castling - restrictions
|
|
85
86
|
new_halfmove = if calculator.increment_halfmove?
|
|
86
87
|
halfmove + 1
|
|
87
88
|
else
|
|
@@ -106,7 +107,7 @@ module PGN
|
|
|
106
107
|
# @return [Symbol] the next player to move
|
|
107
108
|
#
|
|
108
109
|
def next_player
|
|
109
|
-
|
|
110
|
+
player == :white ? :black : :white
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def inspect
|
data/lib/pgn/version.rb
CHANGED
data/spec/parser_spec.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'spec_helper'
|
|
2
4
|
require 'pry'
|
|
3
5
|
|
|
@@ -25,7 +27,7 @@ describe PGN do
|
|
|
25
27
|
games.each do |game|
|
|
26
28
|
expect(game.tags['White']).to eq 'Fool'
|
|
27
29
|
expect(game.result).to eq '0-1'
|
|
28
|
-
expect(game.moves.last).to eq
|
|
30
|
+
expect(game.moves.last).to eq 'Qh4#'
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -39,7 +41,7 @@ describe PGN do
|
|
|
39
41
|
game = games.first
|
|
40
42
|
expect(game.tags['White']).to eq 'Scholar'
|
|
41
43
|
expect(game.result).to eq '1-0'
|
|
42
|
-
expect(game.moves.last).to eq
|
|
44
|
+
expect(game.moves.last).to eq 'Qxf7#'
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
it 'should handle multiline comments' do
|
|
@@ -47,7 +49,7 @@ describe PGN do
|
|
|
47
49
|
game = games.first
|
|
48
50
|
expect(game.tags['White']).to eq 'Scholar'
|
|
49
51
|
expect(game.result).to eq '1-0'
|
|
50
|
-
expect(game.moves.last).to eq
|
|
52
|
+
expect(game.moves.last).to eq 'Qxf7#'
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
it 'should handle nested comments' do
|
|
@@ -57,12 +59,6 @@ describe PGN do
|
|
|
57
59
|
expect(game.moves.last).to eq 'Nf6'
|
|
58
60
|
end
|
|
59
61
|
|
|
60
|
-
it 'handles two annotations' do
|
|
61
|
-
games = PGN.parse(File.read('./spec/pgn_files/two_annotations.pgn'))
|
|
62
|
-
game = games.first
|
|
63
|
-
expect(game.moves[1].annotation).to eq ['$2', '$11']
|
|
64
|
-
end
|
|
65
|
-
|
|
66
62
|
it 'returns empty array when no variations' do
|
|
67
63
|
games = PGN.parse(File.read('./spec/pgn_files/variations.pgn'))
|
|
68
64
|
game = games.first
|
|
@@ -84,13 +80,6 @@ describe PGN do
|
|
|
84
80
|
expect(game.moves[-2].variations.first).to eq %w[f4 exf4]
|
|
85
81
|
end
|
|
86
82
|
|
|
87
|
-
it 'should handle empty variation moves' do
|
|
88
|
-
games = PGN.parse(File.read('./spec/pgn_files/empty_variation_move.pgn'))
|
|
89
|
-
game = games.first
|
|
90
|
-
expect(game.result).to eq '*'
|
|
91
|
-
expect(game.moves.last).to eq 'Ng5'
|
|
92
|
-
end
|
|
93
|
-
|
|
94
83
|
it 'should handle complex files' do
|
|
95
84
|
games = PGN.parse(File.read('./spec/pgn_files/test.pgn'))
|
|
96
85
|
game = games.first
|
|
@@ -103,7 +92,7 @@ describe PGN do
|
|
|
103
92
|
expect(game.moves[35].variations.size).to eq 1
|
|
104
93
|
variation = game.moves[35].variations[0]
|
|
105
94
|
expect(variation.size).to eq 2
|
|
106
|
-
expect(variation[0]).to eq
|
|
95
|
+
expect(variation[0]).to eq 'exf3'
|
|
107
96
|
end
|
|
108
97
|
|
|
109
98
|
it 'should handle files with starting position' do
|
|
@@ -133,26 +122,5 @@ describe PGN do
|
|
|
133
122
|
expect { game.positions }.not_to raise_error
|
|
134
123
|
expect(game.moves).not_to be_empty
|
|
135
124
|
end
|
|
136
|
-
|
|
137
|
-
it 'gets game comment' do
|
|
138
|
-
games = PGN.parse(File.read('./spec/pgn_files/no_moves.pgn'))
|
|
139
|
-
game = games.last
|
|
140
|
-
expect(game.comment).to eq('{game comment}')
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
it 'tag_values with double quotes' do
|
|
144
|
-
games = PGN.parse(File.read('./spec/pgn_files/doublequotes.pgn'))
|
|
145
|
-
game = games.last
|
|
146
|
-
expect(game.tags['Event']).to eq('IRT BLITZ "Sub Zonal"')
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it 'set encoding' do
|
|
150
|
-
file = File.read('./spec/pgn_files/specialcharacters.pgn')
|
|
151
|
-
games = PGN.parse(file, Encoding::UTF_8)
|
|
152
|
-
game1 = games.first
|
|
153
|
-
game2 = games.last
|
|
154
|
-
expect(game1.tags['WhiteTeam']).to eq('NARIÑO')
|
|
155
|
-
expect(game2.tags['Site']).to eq('HOTEL CAFEIRA Calle 18 Nro. 5 – 38 Centro de Pereira – Risaralda')
|
|
156
|
-
end
|
|
157
125
|
end
|
|
158
126
|
end
|
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: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stacey Touset
|
|
@@ -133,6 +133,7 @@ extra_rdoc_files: []
|
|
|
133
133
|
files:
|
|
134
134
|
- ".github/workflows/ci.yml"
|
|
135
135
|
- ".github/workflows/publish.yml"
|
|
136
|
+
- ".github/workflows/release.yml"
|
|
136
137
|
- ".gitignore"
|
|
137
138
|
- ".rspec"
|
|
138
139
|
- ".rubocop.yml"
|
|
@@ -145,8 +146,10 @@ files:
|
|
|
145
146
|
- bench/.keep
|
|
146
147
|
- bench/IMPROVEMENTS.md
|
|
147
148
|
- bench/baseline_moves.pre-optimization.txt
|
|
149
|
+
- bench/baseline_moves.pre-quickwins.txt
|
|
148
150
|
- bench/baseline_moves.txt
|
|
149
151
|
- bench/baseline_parse.pre-optimization.txt
|
|
152
|
+
- bench/baseline_parse.pre-quickwins.txt
|
|
150
153
|
- bench/baseline_parse.racc.txt
|
|
151
154
|
- bench/baseline_parse.txt
|
|
152
155
|
- bench/profile_moves.rb
|
|
@@ -156,6 +159,7 @@ files:
|
|
|
156
159
|
- docs/superpowers/plans/2026-08-12-to-pgn-serialization.md
|
|
157
160
|
- docs/superpowers/plans/2026-08-13-whittle-to-racc-migration.md
|
|
158
161
|
- docs/superpowers/specs/2026-08-12-to-pgn-serialization-design.md
|
|
162
|
+
- docs/superpowers/specs/2026-08-13-pgn-performance-quick-wins-design.md
|
|
159
163
|
- examples/immortal_game.pgn
|
|
160
164
|
- lib/pgn.rb
|
|
161
165
|
- lib/pgn/board.rb
|