pgn3 0.0.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 +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/pgn3.gemspec +27 -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 +169 -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
|