pgn2 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +123 -0
- data/Rakefile +1 -0
- data/TODO.md +12 -0
- data/examples/immortal_game.pgn +17 -0
- data/lib/pgn/board.rb +163 -0
- data/lib/pgn/fen.rb +148 -0
- data/lib/pgn/game.rb +148 -0
- data/lib/pgn/move.rb +163 -0
- data/lib/pgn/move_calculator.rb +337 -0
- data/lib/pgn/parser.rb +208 -0
- data/lib/pgn/position.rb +129 -0
- data/lib/pgn/version.rb +3 -0
- data/lib/pgn.rb +26 -0
- data/pgn2.gemspec +26 -0
- data/spec/fen_spec.rb +100 -0
- data/spec/game_spec.rb +21 -0
- data/spec/parser_spec.rb +143 -0
- data/spec/pgn_files/alternate_castling.pgn +4 -0
- data/spec/pgn_files/annotations.pgn +9 -0
- data/spec/pgn_files/comments.pgn +8 -0
- data/spec/pgn_files/empty_variation_move.pgn +11 -0
- data/spec/pgn_files/fen.pgn +5 -0
- data/spec/pgn_files/multiline_comments.pgn +5 -0
- data/spec/pgn_files/nested_comments.pgn +4 -0
- data/spec/pgn_files/no_moves.pgn +18 -0
- data/spec/pgn_files/test.pgn +55 -0
- data/spec/pgn_files/two_annotations.pgn +4 -0
- data/spec/pgn_files/two_games.pgn +9 -0
- data/spec/pgn_files/variations.pgn +4 -0
- data/spec/position_spec.rb +54 -0
- data/spec/spec_helper.rb +19 -0
- metadata +167 -0
data/lib/pgn/move.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
module PGN
|
2
|
+
# {PGN::Move} knows how to parse a move string in standard algebraic
|
3
|
+
# notation to extract all relevant information.
|
4
|
+
#
|
5
|
+
# @see http://en.wikipedia.org/wiki/Algebraic_notation_(chess) Standard
|
6
|
+
# Algebraic Notation
|
7
|
+
#
|
8
|
+
# @!attribute san
|
9
|
+
# @return [String] the move string
|
10
|
+
# @example
|
11
|
+
# move1.san #=> "O-O-O+"
|
12
|
+
# move2.san #=> "Raxe1"
|
13
|
+
# move3.san #=> "e8=Q#"
|
14
|
+
#
|
15
|
+
# @!attribute player
|
16
|
+
# @return [Symbol] the current player
|
17
|
+
# @example
|
18
|
+
# move.player #=> :white
|
19
|
+
#
|
20
|
+
# @!attribute piece
|
21
|
+
# @return [String, nil] the piece being moved
|
22
|
+
# @example
|
23
|
+
# move1.piece #=> "Q"
|
24
|
+
# move2.piece #=> "r"
|
25
|
+
# @note this is nil for castling
|
26
|
+
# @note uppercase represents white, lowercase represents black
|
27
|
+
#
|
28
|
+
# @!attribute destination
|
29
|
+
# @return [String, nil] the destination square of the piece
|
30
|
+
# @example
|
31
|
+
# move.destination #=> "e4"
|
32
|
+
#
|
33
|
+
# @!attribute promotion
|
34
|
+
# @return [String, nil] the promotion piece, if applicable
|
35
|
+
#
|
36
|
+
# @!attribute check
|
37
|
+
# @return [String, nil] whether the move results in check or mate
|
38
|
+
# @example
|
39
|
+
# move1.check #=> "+"
|
40
|
+
# move2.check #=> "#"
|
41
|
+
#
|
42
|
+
# @!attribute capture
|
43
|
+
# @return [String, nil] whether the move is a capture
|
44
|
+
# @example
|
45
|
+
# move.capture #=> "x"
|
46
|
+
#
|
47
|
+
# @!attribute disambiguation
|
48
|
+
# @return [String, nil] the disambiguation string if there is one
|
49
|
+
# @example
|
50
|
+
# move.disambiguation #=> "3"
|
51
|
+
#
|
52
|
+
# @!attribute castle
|
53
|
+
# @return [String, nil] the castle string if applicable
|
54
|
+
# @example
|
55
|
+
# move1.castle #=> "O-O-O"
|
56
|
+
# move2.castle #=> "O-O"
|
57
|
+
#
|
58
|
+
|
59
|
+
class Move
|
60
|
+
attr_accessor :san, :player
|
61
|
+
attr_accessor :piece, :destination, :promotion, :check, :capture, :disambiguation, :castle
|
62
|
+
# A regular expression for matching moves in standard algebraic
|
63
|
+
# notation
|
64
|
+
#
|
65
|
+
SAN_REGEX = /
|
66
|
+
(?<piece> [BKNQR] ){0}
|
67
|
+
(?<destination> [a-h][1-8] ){0}
|
68
|
+
(?<promotion> =[BNQR] ){0}
|
69
|
+
(?<check> [#+] ){0}
|
70
|
+
(?<capture> x ){0}
|
71
|
+
(?<disambiguation> [a-h]?[1-8]? ){0}
|
72
|
+
|
73
|
+
(?<castle> O-O(-O)? ){0}
|
74
|
+
|
75
|
+
(?<normal>
|
76
|
+
\g<piece>?
|
77
|
+
\g<disambiguation>
|
78
|
+
\g<capture>?
|
79
|
+
\g<destination>
|
80
|
+
\g<promotion>?
|
81
|
+
){0}
|
82
|
+
|
83
|
+
\A (\g<castle> | \g<normal>) \g<check>? \z
|
84
|
+
/x.freeze
|
85
|
+
|
86
|
+
# @param move [String] the move in SAN
|
87
|
+
# @param player [Symbol] the player making the move
|
88
|
+
# @example
|
89
|
+
# PGN::Move.new("e4", :white)
|
90
|
+
#
|
91
|
+
def initialize(move, player)
|
92
|
+
self.player = player
|
93
|
+
self.san = move
|
94
|
+
|
95
|
+
match = move.match(SAN_REGEX)
|
96
|
+
return if match.nil?
|
97
|
+
|
98
|
+
match.names.each do |name|
|
99
|
+
send("#{name}=", match[name]) if respond_to?(name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def piece=(val)
|
104
|
+
return if san.match('O-O')
|
105
|
+
|
106
|
+
val ||= 'P'
|
107
|
+
@piece = black? ? val.downcase : val
|
108
|
+
end
|
109
|
+
|
110
|
+
def promotion=(val)
|
111
|
+
if val
|
112
|
+
val.downcase! if black?
|
113
|
+
@promotion = val.delete('=')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def capture=(val)
|
118
|
+
@capture = !!val
|
119
|
+
end
|
120
|
+
|
121
|
+
def disambiguation=(val)
|
122
|
+
@disambiguation = (val == '' ? nil : val)
|
123
|
+
end
|
124
|
+
|
125
|
+
def castle=(val)
|
126
|
+
if val
|
127
|
+
@castle = 'K' if val == 'O-O'
|
128
|
+
@castle = 'Q' if val == 'O-O-O'
|
129
|
+
@castle.downcase! if black?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Boolean] whether the move results in check
|
134
|
+
#
|
135
|
+
def check?
|
136
|
+
check == '+'
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return [Boolean] whether the move results in checkmate
|
140
|
+
#
|
141
|
+
def checkmate?
|
142
|
+
check == '#'
|
143
|
+
end
|
144
|
+
|
145
|
+
# @return [Boolean] whether it's white's turn
|
146
|
+
#
|
147
|
+
def white?
|
148
|
+
player == :white
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Boolean] whether it's black's turn
|
152
|
+
#
|
153
|
+
def black?
|
154
|
+
player == :black
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Boolean] whether the piece being moved is a pawn
|
158
|
+
#
|
159
|
+
def pawn?
|
160
|
+
%w[P p].include?(piece)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,337 @@
|
|
1
|
+
module PGN
|
2
|
+
# {PGN::MoveCalculator} is responsible for computing all of the ways that a
|
3
|
+
# specific move changes the current position. This includes which squares on
|
4
|
+
# the board need to be updated, new castling restrictions, the en passant
|
5
|
+
# square and whether to update fullmove and halfmove counters.
|
6
|
+
#
|
7
|
+
# @!attribute board
|
8
|
+
# @return [PGN::Board] the current board
|
9
|
+
#
|
10
|
+
# @!attribute move
|
11
|
+
# @return [PGN::Move] the current move
|
12
|
+
#
|
13
|
+
# @!attribute origin
|
14
|
+
# @return [String, nil] the origin square in SAN
|
15
|
+
#
|
16
|
+
class MoveCalculator
|
17
|
+
# Specifies the movement of pieces who are allowed to move in a
|
18
|
+
# given direction until they reach an obstacle or the end of the
|
19
|
+
# board.
|
20
|
+
#
|
21
|
+
DIRECTIONS = {
|
22
|
+
'b' => [[1, 1], [-1, 1], [-1, -1], [1, -1]],
|
23
|
+
'r' => [[-1, 0], [1, 0], [0, -1], [0, 1]],
|
24
|
+
'q' => [[1, 1], [-1, 1], [-1, -1], [1, -1],
|
25
|
+
[-1, 0], [1, 0], [0, -1], [0, 1]]
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
# Specifies the movement of pieces that have a limited set of moves
|
29
|
+
# they are allowed to make.
|
30
|
+
#
|
31
|
+
MOVES = {
|
32
|
+
'k' => [[-1, -1], [0, -1], [1, -1], [1, 0],
|
33
|
+
[1, 1], [0, 1], [-1, 1], [-1, 0]],
|
34
|
+
'n' => [[-1, -2], [-1, 2], [1, -2], [1, 2],
|
35
|
+
[-2, -1], [2, -1], [-2, 1], [2, 1]]
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
# Specifies possible pawn movements. It may seem backwards since it is
|
39
|
+
# used to compute the origin square and not the destination.
|
40
|
+
#
|
41
|
+
PAWN_MOVES = {
|
42
|
+
'P' => {
|
43
|
+
capture: [[-1, -1], [1, -1]],
|
44
|
+
normal: [[0, -1]],
|
45
|
+
double: [[0, -2]]
|
46
|
+
},
|
47
|
+
'p' => {
|
48
|
+
capture: [[-1, 1], [1, 1]],
|
49
|
+
normal: [[0, 1]],
|
50
|
+
double: [[0, 2]]
|
51
|
+
}
|
52
|
+
}.freeze
|
53
|
+
|
54
|
+
# The squares to update for each possible castling move.
|
55
|
+
#
|
56
|
+
CASTLING = {
|
57
|
+
'Q' => {
|
58
|
+
'a1' => nil,
|
59
|
+
'c1' => 'K',
|
60
|
+
'd1' => 'R',
|
61
|
+
'e1' => nil
|
62
|
+
},
|
63
|
+
'K' => {
|
64
|
+
'e1' => nil,
|
65
|
+
'f1' => 'R',
|
66
|
+
'g1' => 'K',
|
67
|
+
'h1' => nil
|
68
|
+
},
|
69
|
+
'q' => {
|
70
|
+
'a8' => nil,
|
71
|
+
'c8' => 'k',
|
72
|
+
'd8' => 'r',
|
73
|
+
'e8' => nil
|
74
|
+
},
|
75
|
+
'k' => {
|
76
|
+
'e8' => nil,
|
77
|
+
'f8' => 'r',
|
78
|
+
'g8' => 'k',
|
79
|
+
'h8' => nil
|
80
|
+
}
|
81
|
+
}.freeze
|
82
|
+
|
83
|
+
attr_accessor :board
|
84
|
+
attr_accessor :move
|
85
|
+
attr_accessor :origin
|
86
|
+
|
87
|
+
# @param board [PGN::Board] the current board
|
88
|
+
# @param move [PGN::Move] the current move
|
89
|
+
#
|
90
|
+
def initialize(board, move)
|
91
|
+
self.board = board
|
92
|
+
self.move = move
|
93
|
+
self.origin = compute_origin
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [PGN::Board] the board after the move is made
|
97
|
+
#
|
98
|
+
def result_board
|
99
|
+
new_board = board.dup
|
100
|
+
new_board.change!(changes)
|
101
|
+
|
102
|
+
new_board
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [Array<String>] which castling moves are no longer available
|
106
|
+
#
|
107
|
+
def castling_restrictions
|
108
|
+
restrict = []
|
109
|
+
|
110
|
+
# when a king or rook is moved
|
111
|
+
case move.piece
|
112
|
+
when 'K'
|
113
|
+
restrict += %w[K Q]
|
114
|
+
when 'k'
|
115
|
+
restrict += %w[k q]
|
116
|
+
when 'R'
|
117
|
+
restrict << { 'a1' => 'Q', 'h1' => 'K' }[origin]
|
118
|
+
when 'r'
|
119
|
+
restrict << { 'a8' => 'q', 'h8' => 'k' }[origin]
|
120
|
+
end
|
121
|
+
|
122
|
+
# when castling occurs
|
123
|
+
restrict += %w[K Q] if %w[K Q].include? move.castle
|
124
|
+
restrict += %w[k q] if %w[k q].include? move.castle
|
125
|
+
|
126
|
+
# when a rook is taken
|
127
|
+
restrict << 'Q' if move.destination == 'a1'
|
128
|
+
restrict << 'q' if move.destination == 'a8'
|
129
|
+
restrict << 'K' if move.destination == 'h1'
|
130
|
+
restrict << 'k' if move.destination == 'h8'
|
131
|
+
|
132
|
+
restrict.compact.uniq
|
133
|
+
end
|
134
|
+
|
135
|
+
# @return [Boolean] whether to increment the halfmove clock
|
136
|
+
#
|
137
|
+
def increment_halfmove?
|
138
|
+
!(move.capture || move.pawn?)
|
139
|
+
end
|
140
|
+
|
141
|
+
# @return [Boolean] whether to increment the fullmove counter
|
142
|
+
#
|
143
|
+
def increment_fullmove?
|
144
|
+
move.black?
|
145
|
+
end
|
146
|
+
|
147
|
+
# @return [String, nil] the en passant square if applicable
|
148
|
+
#
|
149
|
+
def en_passant_square
|
150
|
+
return nil if move.castle
|
151
|
+
|
152
|
+
if move.pawn? && (origin[1].to_i - move.destination[1].to_i).abs == 2
|
153
|
+
if move.white?
|
154
|
+
origin[0] + '3'
|
155
|
+
else
|
156
|
+
origin[0] + '6'
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def changes
|
164
|
+
changes = {}
|
165
|
+
changes.merge!(CASTLING[move.castle]) if move.castle
|
166
|
+
changes.merge!(
|
167
|
+
origin => nil,
|
168
|
+
move.destination => move.piece,
|
169
|
+
en_passant_capture => nil
|
170
|
+
)
|
171
|
+
changes[move.destination] = move.promotion if move.promotion
|
172
|
+
|
173
|
+
changes.reject! { |key, _| key.nil? or key.empty? }
|
174
|
+
|
175
|
+
changes
|
176
|
+
end
|
177
|
+
|
178
|
+
# Using the current position and move, figure out where the piece
|
179
|
+
# came from.
|
180
|
+
#
|
181
|
+
def compute_origin
|
182
|
+
return nil if move.castle
|
183
|
+
|
184
|
+
possibilities = case move.piece
|
185
|
+
when /[brq]/i then direction_origins
|
186
|
+
when /[kn]/i then move_origins
|
187
|
+
when /p/i then pawn_origins
|
188
|
+
else # don't care move, used in variations
|
189
|
+
return nil
|
190
|
+
end
|
191
|
+
|
192
|
+
possibilities = disambiguate(possibilities) if possibilities.length > 1
|
193
|
+
|
194
|
+
board.position_for(possibilities.first)
|
195
|
+
end
|
196
|
+
|
197
|
+
# From the destination square, move in each direction stopping if we
|
198
|
+
# reach the end of the board. If we encounter a piece, add it to the
|
199
|
+
# list of origin possibilities if it is the moving piece, or else
|
200
|
+
# check the next direction.
|
201
|
+
#
|
202
|
+
def direction_origins
|
203
|
+
directions = DIRECTIONS[move.piece.downcase]
|
204
|
+
possibilities = []
|
205
|
+
|
206
|
+
directions.each do |dir|
|
207
|
+
piece, square = first_piece(destination_coords, dir)
|
208
|
+
possibilities << square if piece == move.piece
|
209
|
+
end
|
210
|
+
|
211
|
+
possibilities
|
212
|
+
end
|
213
|
+
|
214
|
+
# From the destination square, make each move. If it is a valid
|
215
|
+
# square and matches the moving piece, add it to the list of origin
|
216
|
+
# possibilities.
|
217
|
+
#
|
218
|
+
def move_origins(moves = nil)
|
219
|
+
moves ||= MOVES[move.piece.downcase]
|
220
|
+
possibilities = []
|
221
|
+
file, rank = destination_coords
|
222
|
+
|
223
|
+
moves.each do |i, j|
|
224
|
+
f = file + i
|
225
|
+
r = rank + j
|
226
|
+
|
227
|
+
possibilities << [f, r] if valid_square?(f, r) && board.at(f, r) == move.piece
|
228
|
+
end
|
229
|
+
|
230
|
+
possibilities
|
231
|
+
end
|
232
|
+
|
233
|
+
# Computes the possbile pawn origins based on the destination square
|
234
|
+
# and whether or not the move is a capture.
|
235
|
+
#
|
236
|
+
def pawn_origins
|
237
|
+
_, rank = destination_coords
|
238
|
+
double_rank = (rank == 3 && move.white?) || (rank == 4 && move.black?)
|
239
|
+
|
240
|
+
pawn_moves = PAWN_MOVES[move.piece]
|
241
|
+
|
242
|
+
moves = move.capture ? pawn_moves[:capture] : pawn_moves[:normal]
|
243
|
+
moves += pawn_moves[:double] if double_rank
|
244
|
+
|
245
|
+
move_origins(moves)
|
246
|
+
end
|
247
|
+
|
248
|
+
def disambiguate(possibilities)
|
249
|
+
possibilities = disambiguate_san(possibilities)
|
250
|
+
possibilities = disambiguate_pawns(possibilities) if possibilities.length > 1
|
251
|
+
possibilities = disambiguate_discovered_check(possibilities) if possibilities.length > 1
|
252
|
+
|
253
|
+
possibilities
|
254
|
+
end
|
255
|
+
|
256
|
+
# Try to disambiguate based on the standard algebraic notation.
|
257
|
+
#
|
258
|
+
def disambiguate_san(possibilities)
|
259
|
+
if move.disambiguation
|
260
|
+
possibilities.select { |p| board.position_for(p).match(move.disambiguation) }
|
261
|
+
else
|
262
|
+
possibilities
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# A pawn can't move two spaces if there is a pawn in front of it.
|
267
|
+
#
|
268
|
+
def disambiguate_pawns(possibilities)
|
269
|
+
if move.piece.match(/p/i) && !move.capture
|
270
|
+
possibilities.reject { |p| board.position_for(p).match(/2|7/) }
|
271
|
+
else
|
272
|
+
possibilities
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# A piece can't move if it would result in a discovered check.
|
277
|
+
#
|
278
|
+
def disambiguate_discovered_check(possibilities)
|
279
|
+
DIRECTIONS.each do |attacking_piece, directions|
|
280
|
+
attacking_piece = attacking_piece.upcase if move.black?
|
281
|
+
|
282
|
+
directions.each do |dir|
|
283
|
+
piece, square = first_piece(king_position, dir)
|
284
|
+
next unless piece == move.piece && possibilities.include?(square)
|
285
|
+
|
286
|
+
piece, = first_piece(square, dir)
|
287
|
+
possibilities.reject! { |p| p == square } if piece == attacking_piece
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
possibilities
|
292
|
+
end
|
293
|
+
|
294
|
+
def first_piece(from, direction)
|
295
|
+
file, rank = from
|
296
|
+
i, j = direction
|
297
|
+
|
298
|
+
piece = nil
|
299
|
+
|
300
|
+
while valid_square?(file += i, rank += j)
|
301
|
+
break if piece = board.at(file, rank)
|
302
|
+
end
|
303
|
+
|
304
|
+
[piece, [file, rank]]
|
305
|
+
end
|
306
|
+
|
307
|
+
# If the move is a capture and there is no piece on the
|
308
|
+
# destination square, it must be an en passant capture.
|
309
|
+
#
|
310
|
+
def en_passant_capture
|
311
|
+
return nil if move.castle
|
312
|
+
|
313
|
+
move.destination[0] + origin[1] if !board.at(move.destination) && move.capture
|
314
|
+
end
|
315
|
+
|
316
|
+
def king_position
|
317
|
+
king = move.white? ? 'K' : 'k'
|
318
|
+
|
319
|
+
coords = nil
|
320
|
+
0.upto(7) do |file|
|
321
|
+
0.upto(7) do |rank|
|
322
|
+
coords = [file, rank] if board.at(file, rank) == king
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
coords
|
327
|
+
end
|
328
|
+
|
329
|
+
def valid_square?(file, rank)
|
330
|
+
(0..7) === file && (0..7) === rank
|
331
|
+
end
|
332
|
+
|
333
|
+
def destination_coords
|
334
|
+
board.coordinates_for(move.destination)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
data/lib/pgn/parser.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'whittle'
|
2
|
+
|
3
|
+
module PGN
|
4
|
+
# {PGN::Parser} uses the whittle gem to parse pgn files based on their
|
5
|
+
# context free grammar.
|
6
|
+
#
|
7
|
+
class Parser < Whittle::Parser
|
8
|
+
def lex(input)
|
9
|
+
line = 1
|
10
|
+
offset = 0
|
11
|
+
ending = input.length
|
12
|
+
@@pgn ||= ''
|
13
|
+
@@game_comment ||= nil
|
14
|
+
|
15
|
+
until offset == ending
|
16
|
+
next_token(input, offset, line).tap do |token|
|
17
|
+
if !token.nil?
|
18
|
+
token[:offset] = offset
|
19
|
+
line, token[:line] = token[:line], line
|
20
|
+
yield token unless token[:discarded]
|
21
|
+
@@pgn += token[:value]
|
22
|
+
offset += token[:value].length
|
23
|
+
else
|
24
|
+
raise Whittle::UnconsumedInputError,
|
25
|
+
"Unmatched input #{input[offset..-1].inspect} on line #{line}"
|
26
|
+
# offset += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
yield ({ name: :$end, line: line, value: nil, offset: offset })
|
32
|
+
end
|
33
|
+
|
34
|
+
rule(wsp: /\s+/).skip!
|
35
|
+
rule(
|
36
|
+
pgn_comment: /\A% .*/x
|
37
|
+
).skip!
|
38
|
+
|
39
|
+
rule('[')
|
40
|
+
rule(']')
|
41
|
+
rule('(')
|
42
|
+
rule(')')
|
43
|
+
|
44
|
+
start(:pgn_database)
|
45
|
+
|
46
|
+
rule(:pgn_database) do |r|
|
47
|
+
r[].as { [] }
|
48
|
+
r[:pgn_comment, :pgn_database].as { |_title, db| db }
|
49
|
+
r[:pgn_database, :pgn_game].as { |database, game| database << game }
|
50
|
+
end
|
51
|
+
|
52
|
+
rule(:pgn_game) do |r|
|
53
|
+
r[:tag_section, :movetext_section].as do |tags, moves|
|
54
|
+
old_pgn = @@pgn
|
55
|
+
@@pgn = ''
|
56
|
+
comment = @@game_comment
|
57
|
+
@@game_comment = nil
|
58
|
+
{ tags: tags, result: moves.pop, moves: moves, pgn: old_pgn, comment: comment }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
rule(:tag_section) do |r|
|
63
|
+
r[:tag_pair, :tag_section].as { |pair, section| section.merge(pair) }
|
64
|
+
r[:tag_pair]
|
65
|
+
end
|
66
|
+
|
67
|
+
rule(:tag_pair) do |r|
|
68
|
+
r['[', :tag_name, :tag_value, ']'].as { |_, a, b, _| { a => b } }
|
69
|
+
end
|
70
|
+
|
71
|
+
rule(:tag_value) do |r|
|
72
|
+
r[:string].as { |value| value[1...-1] }
|
73
|
+
end
|
74
|
+
|
75
|
+
rule(:movetext_section) do |r|
|
76
|
+
r[:element_sequence, :game_termination].as { |a, b| a << b }
|
77
|
+
end
|
78
|
+
|
79
|
+
rule(:element_sequence) do |r|
|
80
|
+
r[:element_sequence, :element].as do |sequence, element|
|
81
|
+
element.nil? ? sequence : sequence << element
|
82
|
+
end
|
83
|
+
r[].as { [] }
|
84
|
+
end
|
85
|
+
|
86
|
+
rule(:element) do |r|
|
87
|
+
r[:move_number_indication].as { nil }
|
88
|
+
r[:san_move_annotated]
|
89
|
+
r[:san_move_annotated, :variation_list].as do |move, variations|
|
90
|
+
move.variations = variations
|
91
|
+
move
|
92
|
+
end
|
93
|
+
r[:comment].as { |c| @@game_comment = c; nil }
|
94
|
+
end
|
95
|
+
|
96
|
+
rule(:san_move_annotated) do |r|
|
97
|
+
r[:san_move].as { |move| MoveText.new(move) }
|
98
|
+
r[:san_move, :comment].as do |move, comment|
|
99
|
+
MoveText.new(move, nil, comment)
|
100
|
+
end
|
101
|
+
r[:san_move, :annotation_list].as do |move, annotation|
|
102
|
+
MoveText.new(move, annotation)
|
103
|
+
end
|
104
|
+
r[:san_move, :annotation_list, :comment].as do |move, annotation, comment|
|
105
|
+
MoveText.new(move, annotation, comment)
|
106
|
+
end
|
107
|
+
r[:san_move, :comment, :annotation_list].as do |move, comment, annotation|
|
108
|
+
MoveText.new(move, annotation, comment)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
rule(:annotation_list) do |r|
|
113
|
+
r[:annotation_list, :numeric_annotation_glyph].as do |sequence, element|
|
114
|
+
element.nil? ? sequence : sequence << element
|
115
|
+
end
|
116
|
+
r[:numeric_annotation_glyph].as { |v| [v] }
|
117
|
+
end
|
118
|
+
|
119
|
+
rule(:variation_list) do |r|
|
120
|
+
r[:variation, :variation_list].as do |variation, sequence|
|
121
|
+
sequence << variation
|
122
|
+
end
|
123
|
+
r[:variation].as { |v| [v] }
|
124
|
+
end
|
125
|
+
|
126
|
+
rule(:variation) do |r|
|
127
|
+
r['(', :element_sequence, ')'].as { |_, sequence, _| sequence }
|
128
|
+
end
|
129
|
+
|
130
|
+
rule(
|
131
|
+
string: /
|
132
|
+
" # beginning of string
|
133
|
+
(
|
134
|
+
[[:print:]&&[^\\"]] | # printing characters except quote and backslash
|
135
|
+
\\\\ | # escaped backslashes
|
136
|
+
\\" # escaped quotation marks
|
137
|
+
)* # zero or more of the above
|
138
|
+
" # end of string
|
139
|
+
/x
|
140
|
+
)
|
141
|
+
|
142
|
+
rule(
|
143
|
+
comment: /
|
144
|
+
(
|
145
|
+
\{ # beginning of comment
|
146
|
+
(
|
147
|
+
[[:print:]&&[^\\\{\}]] | # printing characters except brace and backslash
|
148
|
+
\n |
|
149
|
+
\\\\ | # escaped backslashes
|
150
|
+
\\\{|\\\} | # escaped braces
|
151
|
+
\n | # newlines
|
152
|
+
\g<1> # recursive
|
153
|
+
)* # zero or more of the above
|
154
|
+
\} # end of comment
|
155
|
+
)
|
156
|
+
/x
|
157
|
+
)
|
158
|
+
|
159
|
+
rule(
|
160
|
+
game_termination: %r{
|
161
|
+
1-0 | # white wins
|
162
|
+
0-1 | # black wins
|
163
|
+
1\/2-1\/2 | # draw
|
164
|
+
\* # ?
|
165
|
+
}x
|
166
|
+
)
|
167
|
+
|
168
|
+
rule(
|
169
|
+
move_number_indication: /
|
170
|
+
[[:digit:]]+\.* # one or more digits followed by zero or more periods
|
171
|
+
/x
|
172
|
+
)
|
173
|
+
|
174
|
+
rule(
|
175
|
+
san_move: %r{
|
176
|
+
(
|
177
|
+
-- | # "don't care" move (used in variations)
|
178
|
+
[O0](-[O0]){1,2} | # castling (O-O, O-O-O)
|
179
|
+
[a-h][1-8] | # pawn moves (e4, d7)
|
180
|
+
[BKNQR][a-h1-8]?x?[a-h][1-8] | # major piece moves w/ optional specifier
|
181
|
+
# and capture
|
182
|
+
# (Bd2, N4c3, Raxc1)
|
183
|
+
[a-h][1-8]?x[a-h][1-8] # pawn captures
|
184
|
+
)
|
185
|
+
(
|
186
|
+
=[BNQR] # optional promotion (d8=Q)
|
187
|
+
)?
|
188
|
+
(
|
189
|
+
\+ | # check (g5+)
|
190
|
+
\# # checkmate (Qe7#)
|
191
|
+
)?
|
192
|
+
}x
|
193
|
+
)
|
194
|
+
|
195
|
+
rule(
|
196
|
+
tag_name: /
|
197
|
+
[A-Za-z0-9_]+ # letters, digits and underscores only
|
198
|
+
/x
|
199
|
+
)
|
200
|
+
|
201
|
+
rule(
|
202
|
+
numeric_annotation_glyph: /
|
203
|
+
\$\d+ | # dollar sign followed by an integer from 0 to 255
|
204
|
+
[\?!][\?!]? # support the most used annotations directly
|
205
|
+
/x
|
206
|
+
)
|
207
|
+
end
|
208
|
+
end
|