28
30
40. Rd6 Kc5 41. Ra6 Nf2 42. g4 Bd3 43. Re6 1/2-1/2
33
RESULT_INCOMPLETE = '*'
34
RESULT_WHITE_WIN = '1-0'
35
RESULT_BLACK_WIN = '0-1'
36
RESULT_DRAW = '1/2-1/2'
37
results = {RESULT_INCOMPLETE: RESULT_INCOMPLETE,
38
RESULT_WHITE_WIN: RESULT_WHITE_WIN,
39
RESULT_BLACK_WIN: RESULT_BLACK_WIN,
40
RESULT_DRAW: RESULT_DRAW}
42
"""The required tags in a PGN file (the seven tag roster, STR)"""
54
TAG_WHITE_TYPE = 'WhiteType'
55
TAG_WHITE_ELO = 'WhiteElo'
56
TAG_BLACK_TYPE = 'BlackType'
57
TAG_BLACK_ELO = 'BlackElo'
58
TAG_TIME_CONTROL = 'TimeControl'
59
TAG_TERMINATION = 'Termination'
61
# Values for the WhiteType and BlackType tag
62
PLAYER_HUMAN = 'human'
65
# Values for the Termination tag
66
TERMINATE_ABANDONED = 'abandoned'
67
TERMINATE_ADJUDICATION = 'adjudication'
68
TERMINATE_DEATH = 'death'
69
TERMINATE_EMERGENCY = 'emergency'
70
TERMINATE_NORMAL = 'normal'
71
TERMINATE_RULES_INFRACTION = 'rules infraction'
72
TERMINATE_TIME_FORFEIT = 'time forfeit'
73
TERMINATE_UNTERMINATED = 'unterminated'
31
75
# Comments are bounded by ';' to '\n' or '{' to '}'
32
76
# Lines starting with '%' are ignored and are used as an extension mechanism
33
77
# Strings are bounded by '"' and '"' and quotes inside the strings are escaped with '\"'
80
TOKEN_LINE_COMMENT = 'Line comment'
81
TOKEN_COMMENT = 'Comment'
82
TOKEN_ESCAPED = 'Escaped data'
83
TOKEN_PERIOD = 'Period'
84
TOKEN_TAG_START = 'Tag start'
85
TOKEN_TAG_END = 'Tag end'
86
TOKEN_STRING = 'String'
87
TOKEN_SYMBOL = 'Symbol'
88
TOKEN_RAV_START = 'RAV start'
89
TOKEN_RAV_END = 'RAV end'
35
93
class Error(Exception):
36
94
"""PGN exception class"""
38
__errorType = 'Unknown'
40
# Text description of the error
43
# The file being opened
46
def __init__(self, description = '', fileName = ''):
49
self.description = description
50
self.fileName = fileName
51
Exception.__init__(self)
54
if self.fileName != '':
55
string = self.fileName + ': '
58
string += self.description
66
LINE_COMMENT = 'Line comment'
68
ESCAPED = 'Escaped data'
70
TAG_START = 'Tag start'
74
RAV_START = 'RAV start'
76
XML_START = 'XML start'
81
SYMBOL_START_CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + '*'
82
SYMBOL_CONTINUATION_CHARACTERS = SYMBOL_START_CHARACTERS + '_+#=:-' + '/' # Not in spec but required from game draw and imcomplete
83
NAG_CONTINUATION_CHARACTERS = '0123456789'
85
GAME_TERMINATE_INCOMPLETE = '*'
86
GAME_TERMINATE_WHITE_WIN = '1-0'
87
GAME_TERMINATE_BLACK_WIN = '0-1'
88
GAME_TERMINATE_DRAW = '1/2-1/2'
95
def __init__(self, lineNumber, characterNumber, tokenType, data = None):
100
self.lineNumber = lineNumber
101
self.characterNumber = characterNumber
105
if self.data is not None:
106
string += ': ' + self.data
110
return self.__str__()
121
self.tokens = {' ': (None, 1),
124
';': (PGNToken.LINE_COMMENT, self.__lineComment),
125
'{': (PGNToken.LINE_COMMENT, self.__collectComment),
126
'.': (PGNToken.PERIOD, 1),
127
'[': (PGNToken.TAG_START, 1),
128
']': (PGNToken.TAG_END, 1),
129
'"': (PGNToken.STRING, self.__extractPGNString),
130
'(': (PGNToken.RAV_START, 1),
131
')': (PGNToken.RAV_END, 1),
132
'<': (PGNToken.XML_START, 1),
133
'>': (PGNToken.XML_END, 1),
134
'$': (PGNToken.NAG, self.__extractNAG),
135
'*': (PGNToken.SYMBOL, 1)}
137
for c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ':
138
self.tokens[c] = (PGNToken.SYMBOL, self.__extractSymbol)
140
def __lineComment(self, data):
141
return (data, len(data))
143
def __collectComment(self, data):
144
index = data.find('}')
146
self.__inComment = True
147
index = data.find('\n')
148
self.__comment = self.__comment + data[:index+1]
149
return (data[:index+1], index+1)
151
self.__comment = self.__comment + data[:index+1]
152
self.__inComment = False
154
return (self.__comment, index+1)
156
def __extractSymbol(self, data):
157
for offset in xrange(1, len(data)):
158
if PGNToken.SYMBOL_CONTINUATION_CHARACTERS.find(data[offset]) < 0:
159
return (data[:offset], offset)
161
return (data, offset)
163
def __extractNAG(self, data):
164
index = PGNToken.NAG_CONTINUATION_CHARACTERS.find(data[1])
165
for offset in xrange(1, len(data)):
166
if PGNToken.NAG_CONTINUATION_CHARACTERS.find(data[offset]) < 0:
167
#FIXME: Should be at lest one character and less than $255
168
return (data[:offset], offset)
169
return (data, offset)
171
def __extractPGNString(self, data):
172
#"""Extract a PGN string.
174
#'data' is the data to extract the string from (string). It must start with a quote character '"'.
176
#Return a tuple containing the first PGN string and the number of characters of data it required.
177
#e.g. '"Mike \"Dog\" Smith"' -> ('Mike "Dog" Smith', 20).
178
#If no string is found a Error is raised.
181
raise Error('PGN string does not start with "')
183
for offset in xrange(1, len(data)):
185
escaped = (c == '\\')
186
if c == '"' and escaped is False:
187
pgnString = data[1:offset]
188
pgnString.replace('\\"', '"')
189
pgnString.replace('\\\\', '\\')
190
return (pgnString, offset + 1)
192
raise Error('Unterminated PGN string')
194
def parseLine(self, line, lineNumber):
197
Return an array of tokens extracted from the line.
199
# Ignore line if contains escaped data
201
return [PGNToken(lineNumber, self.__startOffset, PGNToken.ESCAPED, line[1:])]
205
while offset < len(line):
210
(tokenType, length) = self.tokens['{']
212
raise Error('Unknown character %s' % repr(c))
216
(tokenType, length) = self.tokens[c]
218
raise Error('Unknown character %s' % repr(c))
221
if type(length) is int:
222
data = line[offset:offset+length]
225
(data, o) = length(line[offset:])
228
if tokenType is not None and not self.__inComment:
229
tokens.append(PGNToken(lineNumber, startOffset, tokenType, data))
240
101
STATE_IDLE = 'IDLE'
241
102
STATE_TAG_NAME = 'TAG_NAME'
242
103
STATE_TAG_VALUE = 'TAG_VALUE'
244
105
STATE_MOVETEXT = 'MOVETEXT'
245
106
STATE_RAV = 'RAV'
246
107
STATE_XML = 'XML'
249
# The game being assembled
252
# The tag being assembled
256
# The move number being decoded
257
__expectedMoveNumber = 0
258
__prevTokenIsMoveNumber = False
260
__currentMoveNumber = 0
262
# The last white move
265
# The Recursive Annotation Variation (RAV) stack
268
def __parseTokenMovetext(self, token):
271
if token.type is PGNToken.RAV_START:
273
# FIXME: Check for RAV errors
276
elif token.type is PGNToken.RAV_END:
278
# FIXME: Check for RAV errors
281
# Ignore tokens inside RAV
282
if self.__ravDepth != 0:
285
if token.type is PGNToken.PERIOD:
286
if self.__prevTokenIsMoveNumber is False:
287
raise Error('Unexpected period on line %i:%i' % (token.lineNumber, token.characterNumber))
289
elif token.type is PGNToken.SYMBOL:
109
def __init__(self, maxGames = -1):
110
expressions = ['\%.*', # Escaped data
111
';.*', # Line comment
112
'\{', # Comment start
114
'[a-zA-Z0-9\*\_\+\#\=\:\-\/]+', # Symbol, '/' Not in spec but required from game draw and incomplete
117
'\$[0-9]{1,3}', # NAG
122
self.regexp = re.compile('|'.join(expressions))
124
self.tokens = {';': TOKEN_LINE_COMMENT,
126
'[': TOKEN_TAG_START,
131
'(': TOKEN_RAV_START,
135
for c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*':
136
self.tokens[c] = TOKEN_SYMBOL
139
self.maxGames = maxGames
142
self.state = self.STATE_IDLE
143
self.game = PGNGame() # Game being assembled
144
self.tagName = None # The tag being assembled
146
self.prevTokenIsMoveNumber = False
147
self.currentMoveNumber = 0
148
self.ravDepth = 0 # The Recursive Annotation Variation (RAV) stack
150
def _parseTokenMovetext(self, tokenType, data):
153
if tokenType is TOKEN_SYMBOL:
154
# Ignore tokens inside RAV
155
if self.ravDepth != 0:
290
158
# See if this is a game terminate
291
if token.data == PGNToken.GAME_TERMINATE_INCOMPLETE or \
292
token.data == PGNToken.GAME_TERMINATE_WHITE_WIN or \
293
token.data == PGNToken.GAME_TERMINATE_BLACK_WIN or \
294
token.data == PGNToken.GAME_TERMINATE_DRAW:
159
if results.has_key(data):
160
self.games.append(self.game)
161
self.game = PGNGame()
162
self.prevTokenIsMoveNumber = False
163
self.currentMoveNumber = 0
165
self.state = self.STATE_IDLE
300
167
# Otherwise it is a move number or a move
302
# See if this is a move number or a SAN move
304
moveNumber = int(token.data)
305
self.__prevTokenIsMoveNumber = True
306
if moveNumber != self.__expectedMoveNumber:
307
raise Error('Expected move number %i, got %i on line %i:%i' % (self.__expectedMoveNumber, moveNumber, token.lineNumber, token.characterNumber))
170
moveNumber = int(data)
308
171
except ValueError:
309
self.__prevTokenIsMoveNumber = False
310
if self.__whiteMove is None:
311
self.__whiteMove = token.data
313
move.number = self.__currentMoveNumber
314
move.move = self.__whiteMove
315
self.__game.addMove(move)
316
self.__currentMoveNumber += 1
320
move.move = token.data
321
move.number = self.__currentMoveNumber
322
self.__game.addMove(move)
323
self.__whiteMove = None
324
self.__currentMoveNumber += 1
325
self.__expectedMoveNumber += 1
327
elif token.type is PGNToken.NAG:
173
move.number = self.currentMoveNumber
175
self.game.addMove(move)
176
self.currentMoveNumber += 1
178
self.prevTokenIsMoveNumber = True
179
expected = (self.currentMoveNumber / 2) + 1
180
if moveNumber != expected:
181
raise Error('Expected move number %i, got %i' % (expected, moveNumber))
183
elif tokenType is TOKEN_NAG:
184
# Ignore tokens inside RAV
185
if self.ravDepth != 0:
188
move = self.game.getMove(self.currentMoveNumber)
191
elif tokenType is TOKEN_PERIOD:
192
# Ignore tokens inside RAV
193
if self.ravDepth != 0:
196
if self.prevTokenIsMoveNumber is False:
197
raise Error('Unexpected period')
199
elif tokenType is TOKEN_RAV_START:
201
# FIXME: Check for RAV errors
204
elif tokenType is TOKEN_RAV_END:
206
# FIXME: Check for RAV errors
331
raise Error('Unknown token %s in movetext on line %i:%i' % (str(token.type), token.lineNumber, token.characterNumber))
210
raise Error('Unknown token %s in movetext' % (str(tokenType)))
333
def parseToken(self, token):
336
Return a game object if a game is complete otherwise None.
212
def parseToken(self, tokenType, data):
338
215
# Ignore all comments at any time
339
if token.type is PGNToken.LINE_COMMENT or token.type is PGNToken.COMMENT:
340
if self.__currentMoveNumber > 0:
341
move = self.__game.getMove(self.__currentMoveNumber)
342
move.comment = token.data[1:-1]
345
if token.type is PGNToken.NAG:
346
if self.__currentMoveNumber > 0:
347
move = self.__game.getMove(self.__currentMoveNumber)
348
move.nag = token.data
216
if tokenType is TOKEN_LINE_COMMENT or tokenType is TOKEN_COMMENT:
217
if self.currentMoveNumber > 0:
218
move = self.game.getMove(self.currentMoveNumber)
219
move.comment = data[1:-1]
222
if self.state is self.STATE_MOVETEXT:
223
self._parseTokenMovetext(tokenType, data)
351
if self.__state is self.STATE_IDLE:
352
if self.__game is None:
353
self.__game = PGNGame()
355
if token.type is PGNToken.TAG_START:
356
self.__state = self.STATE_TAG_NAME
225
elif self.state is self.STATE_IDLE:
226
if tokenType is TOKEN_TAG_START:
227
self.state = self.STATE_TAG_NAME
359
elif token.type is PGNToken.SYMBOL:
360
self.__expectedMoveNumber = 1
361
self.__whiteMove = None
362
self.__prevTokenIsMoveNumber = False
364
self.__state = self.STATE_MOVETEXT
230
elif tokenType is TOKEN_SYMBOL:
231
self.whiteMove = None
232
self.prevTokenIsMoveNumber = False
234
self.state = self.STATE_MOVETEXT
235
self._parseTokenMovetext(tokenType, data)
366
elif token.type is PGNToken.ESCAPED:
237
elif tokenType is TOKEN_ESCAPED:
370
raise Error('Unexpected token %s on line %i:%i' % (str(token.type), token.lineNumber, token.characterNumber))
372
if self.__state is self.STATE_TAG_NAME:
373
if token.type is PGNToken.SYMBOL:
374
self.__tagName = token.data
375
self.__state = self.STATE_TAG_VALUE
377
raise Error('Not a valid file')
379
elif self.__state is self.STATE_TAG_VALUE:
380
if token.type is PGNToken.STRING:
381
self.__tagValue = token.data
382
self.__state = self.STATE_TAG_END
384
raise Error('Not a valid file')
386
elif self.__state is self.STATE_TAG_END:
387
if token.type is PGNToken.TAG_END:
388
self.__game.setTag(self.__tagName, self.__tagValue)
389
self.__state = self.STATE_IDLE
391
raise Error('Not a valid file')
241
raise Error('Unexpected token %s' % (str(tokenType)))
243
if self.state is self.STATE_TAG_NAME:
244
if tokenType is TOKEN_SYMBOL:
246
self.state = self.STATE_TAG_VALUE
248
raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_SYMBOL)))
250
elif self.state is self.STATE_TAG_VALUE:
251
if tokenType is TOKEN_STRING:
252
self.tagValue = data[1:-1]
253
self.state = self.STATE_TAG_END
255
raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_STRING)))
257
elif self.state is self.STATE_TAG_END:
258
if tokenType is TOKEN_TAG_END:
259
self.game.setTag(self.tagName, self.tagValue)
260
self.state = self.STATE_IDLE
262
raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_TAG_END)))
264
def parseLine(self, line):
265
"""Parse a line from a PGN file.
267
Return an array of tokens extracted from the line.
270
if self.comment is not None:
276
comment = self.comment + line[:end]
278
self.parseToken(TOKEN_COMMENT, comment)
282
for match in self.regexp.finditer(line):
283
text = line[match.start():match.end()]
285
line = line[match.end():]
289
tokenType = self.tokens[text[0]]
290
self.parseToken(tokenType, text)
393
elif self.__state is self.STATE_MOVETEXT:
394
game = self.__parseTokenMovetext(token)
396
self.__state = self.STATE_IDLE
292
if self.comment is None:
399
295
def complete(self):
403
# Raise an error if there was a partial game
296
if len(self.game.moves) > 0:
297
self.games.append(self.game)
422
"""The required tags in a PGN file (the seven tag roster, STR)"""
423
PGN_TAG_EVENT = 'Event'
424
PGN_TAG_SITE = 'Site'
425
PGN_TAG_DATE = 'Date'
426
PGN_TAG_ROUND = 'Round'
427
PGN_TAG_WHITE = 'White'
428
PGN_TAG_BLACK = 'Black'
429
PGN_TAG_RESULT = 'Result'
432
PGN_TAG_TIME = 'Time'
434
PGN_TAG_WHITE_TYPE = 'WhiteType'
435
PGN_TAG_WHITE_ELO = 'WhiteElo'
436
PGN_TAG_BLACK_TYPE = 'BlackType'
437
PGN_TAG_BLACK_ELO = 'BlackElo'
438
PGN_TAG_TIME_CONTROL = 'TimeControl'
439
PGN_TAG_TERMINATION = 'Termination'
441
# Values for the WhiteType and BlackType tag
442
PGN_TYPE_HUMAN = 'human'
443
PGN_TYPE_AI = 'program'
445
# Values for the Termination tag
446
PGN_TERMINATE_ABANDONED = 'abandoned'
447
PGN_TERMINATE_ADJUDICATION = 'adjudication'
448
PGN_TERMINATE_DEATH = 'death'
449
PGN_TERMINATE_EMERGENCY = 'emergency'
450
PGN_TERMINATE_NORMAL = 'normal'
451
PGN_TERMINATE_RULES_INFRACTION = 'rules infraction'
452
PGN_TERMINATE_TIME_FORFEIT = 'time forfeit'
453
PGN_TERMINATE_UNTERMINATED = 'unterminated'
455
315
# The seven tag roster in the required order (REFERENCE)
456
__strTags = [PGN_TAG_EVENT, PGN_TAG_SITE, PGN_TAG_DATE, PGN_TAG_ROUND, PGN_TAG_WHITE, PGN_TAG_BLACK, PGN_TAG_RESULT]
316
_strTags = [TAG_EVENT, TAG_SITE, TAG_DATE, TAG_ROUND, TAG_WHITE, TAG_BLACK, TAG_RESULT]
458
# The tags in this game
463
318
def __init__(self):
464
319
# Set the default STR tags
465
self.__tagsByName = {}
466
self.setTag(self.PGN_TAG_EVENT, '?')
467
self.setTag(self.PGN_TAG_SITE, '?')
468
self.setTag(self.PGN_TAG_DATE, '????.??.??')
469
self.setTag(self.PGN_TAG_ROUND, '?')
470
self.setTag(self.PGN_TAG_WHITE, '?')
471
self.setTag(self.PGN_TAG_BLACK, '?')
472
self.setTag(self.PGN_TAG_RESULT, '*')
321
self.setTag(TAG_EVENT, '?')
322
self.setTag(TAG_SITE, '?')
323
self.setTag(TAG_DATE, '????.??.??')
324
self.setTag(TAG_ROUND, '?')
325
self.setTag(TAG_WHITE, '?')
326
self.setTag(TAG_BLACK, '?')
327
self.setTag(TAG_RESULT, '*')
476
330
def getLines(self):
480
333
# Get the names of the non STR tags
481
otherTags = list(set(self.__tagsByName).difference(self.__strTags))
334
otherTags = list(set(self.tagsByName).difference(self._strTags))
483
336
# Write seven tag roster and the additional tags
484
for name in self.__strTags + otherTags:
485
value = self.__tagsByName[name]
486
lines.append('['+ name + ' ' + self.__makePGNString(value) + ']')
337
for name in self._strTags + otherTags:
338
value = self.tagsByName[name]
339
lines.append('['+ name + ' ' + self._makePGNString(value) + ']')
490
343
# Insert numbers in-between moves
493
for m in self.__moves:
494
347
if moveNumber % 2 == 0:
495
348
tokens.append('%i.' % (moveNumber / 2 + 1))