~ubuntu-branches/ubuntu/intrepid/gnome-games/intrepid-updates

« back to all changes in this revision

Viewing changes to glchess/src/lib/chess/pgn.py

  • Committer: Package Import Robot
  • Author(s): Sebastien Bacher
  • Date: 2007-11-21 13:43:08 UTC
  • mto: This revision was merged to the branch mainline in revision 54.
  • Revision ID: package-import@ubuntu.com-20071121134308-3m2zeh0776quevvk
Tags: upstream-2.21.2
ImportĀ upstreamĀ versionĀ 2.21.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
8
8
__license__ = 'GNU General Public License Version 2'
9
9
__copyright__ = 'Copyright 2005-2006  Robert Ancell'
10
10
 
 
11
import re
 
12
 
11
13
"""
12
14
; Example PGN file
13
15
 
28
30
40. Rd6 Kc5 41. Ra6 Nf2 42. g4 Bd3 43. Re6 1/2-1/2
29
31
"""
30
32
 
 
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}
 
41
 
 
42
"""The required tags in a PGN file (the seven tag roster, STR)"""
 
43
TAG_EVENT  = 'Event'
 
44
TAG_SITE   = 'Site'
 
45
TAG_DATE   = 'Date'
 
46
TAG_ROUND  = 'Round'
 
47
TAG_WHITE  = 'White'
 
48
TAG_BLACK  = 'Black'
 
49
TAG_RESULT = 'Result'
 
50
 
 
51
"""Optional tags"""
 
52
TAG_TIME         = 'Time'
 
53
TAG_FEN          = 'FEN'
 
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'
 
60
 
 
61
# Values for the WhiteType and BlackType tag
 
62
PLAYER_HUMAN     = 'human'
 
63
PLAYER_AI        = 'program'
 
64
 
 
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'
 
74
 
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 '\"'
34
78
 
 
79
# Token types
 
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'
 
90
TOKEN_XML          = 'XML'
 
91
TOKEN_NAG          = 'NAG'
 
92
 
35
93
class Error(Exception):
36
94
    """PGN exception class"""
37
 
    
38
 
    __errorType = 'Unknown'
39
 
    
40
 
    # Text description of the error
41
 
    description = ''
42
 
    
43
 
    # The file being opened
44
 
    fileName = ''
45
 
    
46
 
    def __init__(self, description = '', fileName = ''):
47
 
        """
48
 
        """
49
 
        self.description = description
50
 
        self.fileName = fileName
51
 
        Exception.__init__(self)
52
 
        
53
 
    def __str__(self):
54
 
        if self.fileName != '':
55
 
            string = self.fileName + ': '
56
 
        else:
57
 
            string = ''
58
 
        string += self.description
59
 
        return string
60
 
 
61
 
class PGNToken:
62
 
    """
63
 
    """
64
 
    
65
 
    # Token types
66
 
    LINE_COMMENT = 'Line comment'
67
 
    COMMENT      = 'Comment'
68
 
    ESCAPED      = 'Escaped data'
69
 
    PERIOD       = 'Period'
70
 
    TAG_START    = 'Tag start'
71
 
    TAG_END      = 'Tag end'
72
 
    STRING       = 'String'
73
 
    SYMBOL       = 'Symbol'
74
 
    RAV_START    = 'RAV start'
75
 
    RAV_END      = 'RAV end'
76
 
    XML_START    = 'XML start'
77
 
    XML_END      = 'XML end'
78
 
    NAG          = 'NAG'
79
 
    type = None
80
 
 
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'
84
 
    
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'
89
 
 
90
 
    data = None
91
 
    
92
 
    lineNumber = -1
93
 
    characterNumber = -1
94
 
    
95
 
    def __init__(self, lineNumber, characterNumber, tokenType, data = None):
96
 
        """
97
 
        """
98
 
        self.type = tokenType
99
 
        self.data = data
100
 
        self.lineNumber = lineNumber
101
 
        self.characterNumber = characterNumber
102
 
        
103
 
    def __str__(self):
104
 
        string = self.type
105
 
        if self.data is not None:
106
 
            string += ': ' + self.data
107
 
        return string
108
 
    
109
 
    def __repr__(self):
110
 
        return self.__str__()
111
 
    
 
95
    pass
 
96
 
112
97
class PGNParser:
113
98
    """
114
99
    """
115
100
    
116
 
    __inComment = False
117
 
    __comment = ''
118
 
    __startOffset = -1
119
 
    
120
 
    def __init__(self):
121
 
        self.tokens = {' ':  (None,                  1),
122
 
                       '\t': (None,                  1),
123
 
                       '\n': (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)}
136
 
 
137
 
        for c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ':
138
 
            self.tokens[c] = (PGNToken.SYMBOL, self.__extractSymbol)
139
 
            
140
 
    def __lineComment(self, data):
141
 
        return (data, len(data))
142
 
 
143
 
    def __collectComment(self, data):
144
 
        index = data.find('}')
145
 
        if index < 0:
146
 
           self.__inComment = True
147
 
           index = data.find('\n')
148
 
           self.__comment = self.__comment + data[:index+1]
149
 
           return (data[:index+1], index+1)
150
 
        else:
151
 
            self.__comment = self.__comment + data[:index+1]
152
 
            self.__inComment = False
153
 
                 
154
 
        return (self.__comment, index+1)
155
 
            
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)
160
 
 
161
 
        return (data, offset)
162
 
            
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)
170
 
    
171
 
    def __extractPGNString(self, data):
172
 
        #"""Extract a PGN string.
173
 
        
174
 
        #'data' is the data to extract the string from (string). It must start with a quote character '"'.
175
 
        
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.
179
 
        #"""
180
 
        if data[0] != '"':
181
 
            raise Error('PGN string does not start with "')
182
 
        
183
 
        for offset in xrange(1, len(data)):
184
 
            c = data[offset]
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)
191
 
 
192
 
        raise Error('Unterminated PGN string')
193
 
            
194
 
    def parseLine(self, line, lineNumber):
195
 
        """TODO
196
 
        
197
 
        Return an array of tokens extracted from the line.
198
 
        """
199
 
        # Ignore line if contains escaped data
200
 
        if line[0] == '%':
201
 
            return [PGNToken(lineNumber, self.__startOffset, PGNToken.ESCAPED, line[1:])]
202
 
        
203
 
        offset = 0
204
 
        tokens = []
205
 
        while offset < len(line):
206
 
            c = line[offset]
207
 
 
208
 
            if self.__inComment:
209
 
                try:
210
 
                    (tokenType, length) = self.tokens['{']
211
 
                except KeyError:
212
 
                    raise Error('Unknown character %s' % repr(c))
213
 
            else:
214
 
                self.__comment = ''
215
 
                try:
216
 
                    (tokenType, length) = self.tokens[c]
217
 
                except KeyError:
218
 
                    raise Error('Unknown character %s' % repr(c))
219
 
 
220
 
            startOffset = offset
221
 
            if type(length) is int:
222
 
                data = line[offset:offset+length]
223
 
                offset += length
224
 
            else:
225
 
                (data, o) = length(line[offset:])
226
 
                offset += o
227
 
            
228
 
            if tokenType is not None and not self.__inComment:
229
 
                tokens.append(PGNToken(lineNumber, startOffset, tokenType, data))
230
 
 
231
 
        return tokens
232
 
            
233
 
    def endParse(self):
234
 
        pass
235
 
    
236
 
class PGNGameParser:
237
 
    """
238
 
    """
239
 
    
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'
247
 
    __state = STATE_IDLE
248
 
    
249
 
    # The game being assembled
250
 
    __game = None
251
 
    
252
 
    # The tag being assembled
253
 
    __tagName = None
254
 
    __tagValue = None
255
 
    
256
 
    # The move number being decoded
257
 
    __expectedMoveNumber = 0
258
 
    __prevTokenIsMoveNumber = False
259
 
    
260
 
    __currentMoveNumber = 0
261
 
 
262
 
    # The last white move
263
 
    __whiteMove = None
264
 
    
265
 
    # The Recursive Annotation Variation (RAV) stack
266
 
    __ravDepth = 0
267
 
    
268
 
    def __parseTokenMovetext(self, token):
269
 
        """
270
 
        """
271
 
        if token.type is PGNToken.RAV_START:
272
 
            self.__ravDepth += 1
273
 
            # FIXME: Check for RAV errors
274
 
            return
275
 
                
276
 
        elif token.type is PGNToken.RAV_END:
277
 
            self.__ravDepth -= 1
278
 
            # FIXME: Check for RAV errors
279
 
            return
280
 
 
281
 
        # Ignore tokens inside RAV
282
 
        if self.__ravDepth != 0:
283
 
            return
284
 
                
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))
288
 
 
289
 
        elif token.type is PGNToken.SYMBOL:
 
108
    
 
109
    def __init__(self, maxGames = -1):
 
110
        expressions = ['\%.*',         # Escaped data
 
111
                       ';.*',          # Line comment
 
112
                       '\{',           # Comment start
 
113
                       '\".*\"',       # String
 
114
                       '[a-zA-Z0-9\*\_\+\#\=\:\-\/]+', # Symbol, '/' Not in spec but required from game draw and incomplete
 
115
                       '\[',           # Tag start
 
116
                       '\]',           # Tag end
 
117
                       '\$[0-9]{1,3}', # NAG
 
118
                       '\(',           # RAV start
 
119
                       '\)',           # RAV end
 
120
                       '\<.*\>',       # XML
 
121
                       '[.]+']         # Period(s)
 
122
        self.regexp = re.compile('|'.join(expressions))
 
123
 
 
124
        self.tokens = {';':  TOKEN_LINE_COMMENT,
 
125
                       '{':  TOKEN_COMMENT,
 
126
                       '[':  TOKEN_TAG_START,
 
127
                       ']':  TOKEN_TAG_END,
 
128
                       '"':  TOKEN_STRING,
 
129
                       '.':  TOKEN_PERIOD,
 
130
                       '$':  TOKEN_NAG,
 
131
                       '(':  TOKEN_RAV_START,
 
132
                       ')':  TOKEN_RAV_END,
 
133
                       '<':  TOKEN_XML,
 
134
                       '%':  TOKEN_ESCAPED}
 
135
        for c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*':
 
136
            self.tokens[c] = TOKEN_SYMBOL
 
137
 
 
138
        self.games = []
 
139
        self.maxGames = maxGames
 
140
        self.comment = None
 
141
 
 
142
        self.state = self.STATE_IDLE
 
143
        self.game = PGNGame() # Game being assembled       
 
144
        self.tagName = None # The tag being assembled
 
145
        self.tagValue = None
 
146
        self.prevTokenIsMoveNumber = False
 
147
        self.currentMoveNumber = 0    
 
148
        self.ravDepth = 0     # The Recursive Annotation Variation (RAV) stack
 
149
 
 
150
    def _parseTokenMovetext(self, tokenType, data):
 
151
        """
 
152
        """
 
153
        if tokenType is TOKEN_SYMBOL:
 
154
            # Ignore tokens inside RAV
 
155
            if self.ravDepth != 0:
 
156
                return
 
157
 
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:
295
 
                game = self.__game
296
 
                self.__game = None
297
 
                
298
 
                return game
 
159
            if results.has_key(data):
 
160
                self.games.append(self.game)
 
161
                self.game = PGNGame()
 
162
                self.prevTokenIsMoveNumber = False
 
163
                self.currentMoveNumber = 0    
 
164
                self.ravDepth = 0
 
165
                self.state = self.STATE_IDLE
299
166
            
300
167
            # Otherwise it is a move number or a move
301
168
            else:
302
 
                # See if this is a move number or a SAN move
303
169
                try:
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
312
 
                        move = PGNMove()
313
 
                        move.number = self.__currentMoveNumber
314
 
                        move.move = self.__whiteMove
315
 
                        self.__game.addMove(move)
316
 
                        self.__currentMoveNumber += 1
317
 
 
318
 
                    else:
319
 
                        move = PGNMove()
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
326
 
 
327
 
        elif token.type is PGNToken.NAG:
328
 
            pass
 
172
                    move = PGNMove()
 
173
                    move.number = self.currentMoveNumber
 
174
                    move.move = data
 
175
                    self.game.addMove(move)
 
176
                    self.currentMoveNumber += 1
 
177
                else:
 
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))
 
182
 
 
183
        elif tokenType is TOKEN_NAG:
 
184
            # Ignore tokens inside RAV
 
185
            if self.ravDepth != 0:
 
186
                return
 
187
            
 
188
            move = self.game.getMove(self.currentMoveNumber)
 
189
            move.nag = data
 
190
            
 
191
        elif tokenType is TOKEN_PERIOD:
 
192
            # Ignore tokens inside RAV
 
193
            if self.ravDepth != 0:
 
194
                return           
 
195
 
 
196
            if self.prevTokenIsMoveNumber is False:
 
197
                raise Error('Unexpected period')
 
198
 
 
199
        elif tokenType is TOKEN_RAV_START:
 
200
            self.ravDepth += 1
 
201
            # FIXME: Check for RAV errors
 
202
            return
329
203
                
 
204
        elif tokenType is TOKEN_RAV_END:
 
205
            self.ravDepth -= 1
 
206
            # FIXME: Check for RAV errors
 
207
            return
 
208
               
330
209
        else:
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)))
332
211
    
333
 
    def parseToken(self, token):
334
 
        """TODO
335
 
        
336
 
        Return a game object if a game is complete otherwise None.
 
212
    def parseToken(self, tokenType, data):
 
213
        """
337
214
        """
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]
343
 
            return None
344
 
 
345
 
        if token.type is PGNToken.NAG:
346
 
            if self.__currentMoveNumber > 0:
347
 
                move = self.__game.getMove(self.__currentMoveNumber)
348
 
                move.nag = token.data               
349
 
            return None
 
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]
 
220
            return
 
221
       
 
222
        if self.state is self.STATE_MOVETEXT:
 
223
            self._parseTokenMovetext(tokenType, data)
350
224
            
351
 
        if self.__state is self.STATE_IDLE:
352
 
            if self.__game is None:
353
 
                self.__game = PGNGame()
354
 
                
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
357
228
                return
358
229
 
359
 
            elif token.type is PGNToken.SYMBOL:
360
 
                self.__expectedMoveNumber = 1
361
 
                self.__whiteMove = None
362
 
                self.__prevTokenIsMoveNumber = False
363
 
                self.__ravDepth = 0
364
 
                self.__state = self.STATE_MOVETEXT
 
230
            elif tokenType is TOKEN_SYMBOL:
 
231
                self.whiteMove = None
 
232
                self.prevTokenIsMoveNumber = False
 
233
                self.ravDepth = 0
 
234
                self.state = self.STATE_MOVETEXT
 
235
                self._parseTokenMovetext(tokenType, data)
365
236
                
366
 
            elif token.type is PGNToken.ESCAPED:
 
237
            elif tokenType is TOKEN_ESCAPED:
367
238
                pass
368
 
            
369
 
            else:
370
 
                raise Error('Unexpected token %s on line %i:%i' % (str(token.type), token.lineNumber, token.characterNumber))
371
 
            
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
376
 
            else:
377
 
                raise Error('Not a valid file')
378
 
 
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
383
 
            else:
384
 
                raise Error('Not a valid file')
385
 
            
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
390
 
            else:
391
 
                raise Error('Not a valid file')
 
239
 
 
240
            else:
 
241
                raise Error('Unexpected token %s' % (str(tokenType)))
 
242
 
 
243
        if self.state is self.STATE_TAG_NAME:
 
244
            if tokenType is TOKEN_SYMBOL:
 
245
                self.tagName = data
 
246
                self.state = self.STATE_TAG_VALUE
 
247
            else:
 
248
                raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_SYMBOL)))
 
249
 
 
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
 
254
            else:
 
255
                raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_STRING)))
 
256
 
 
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
 
261
            else:
 
262
                raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_TAG_END)))
 
263
 
 
264
    def parseLine(self, line):
 
265
        """Parse a line from a PGN file.
 
266
        
 
267
        Return an array of tokens extracted from the line.
 
268
        """
 
269
        while len(line) > 0:
 
270
            if self.comment is not None:
 
271
                end = line.find('}')
 
272
                if end < 0:
 
273
                    self.comment += line
 
274
                    return True
 
275
                else:
 
276
                    comment = self.comment + line[:end]
 
277
                    self.comment = None
 
278
                    self.parseToken(TOKEN_COMMENT, comment)
 
279
                    line = line[end+1:]
 
280
                continue
 
281
            
 
282
            for match in self.regexp.finditer(line):
 
283
                text = line[match.start():match.end()]
 
284
                if text == '{':
 
285
                    line = line[match.end():]
 
286
                    self.comment = ''
 
287
                    break
 
288
                else:
 
289
                    tokenType = self.tokens[text[0]]
 
290
                    self.parseToken(tokenType, text)
392
291
                    
393
 
        elif self.__state is self.STATE_MOVETEXT:
394
 
            game = self.__parseTokenMovetext(token)
395
 
            if game is not None:
396
 
                self.__state = self.STATE_IDLE
397
 
                return game
398
 
                
 
292
            if self.comment is None:
 
293
                return True
 
294
            
399
295
    def complete(self):
400
 
        """
401
 
        """
402
 
        pass
403
 
        # Raise an error if there was a partial game
404
 
        #raise Error()
405
 
        
 
296
        if len(self.game.moves) > 0:
 
297
            self.games.append(self.game)
 
298
   
406
299
class PGNMove:
407
300
    """
408
301
    """
418
311
class PGNGame:
419
312
    """
420
313
    """
421
 
    
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'
430
 
    
431
 
    """Optional tags"""
432
 
    PGN_TAG_TIME         = 'Time'
433
 
    PGN_TAG_FEN          = 'FEN'
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'
440
 
    
441
 
    # Values for the WhiteType and BlackType tag
442
 
    PGN_TYPE_HUMAN       = 'human'
443
 
    PGN_TYPE_AI          = 'program'
444
 
    
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'
454
 
    
 
314
 
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]
457
317
 
458
 
    # The tags in this game
459
 
    __tagsByName = None
460
 
    
461
 
    __moves = None
462
 
    
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, '*')
473
 
        
474
 
        self.__moves = []
 
320
        self.tagsByName = {}
 
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, '*')
 
328
        self.moves = []
475
329
        
476
330
    def getLines(self):
477
 
    
478
331
        lines = []
479
332
        
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))
482
335
 
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) + ']')
487
340
 
488
341
        lines.append('')
489
342
        
490
343
        # Insert numbers in-between moves
491
344
        tokens = []
492
345
        moveNumber = 0
493
 
        for m in self.__moves:
 
346
        for m in self.moves:
494
347
            if moveNumber % 2 == 0:
495
348
                tokens.append('%i.' % (moveNumber / 2 + 1))
496
349
            moveNumber += 1
501
354
                tokens.append('{' + m.comment + '}')
502
355
                
503
356
        # Add result token to the end
504
 
        tokens.append(self.__tagsByName[self.PGN_TAG_RESULT])
 
357
        tokens.append(self.tagsByName[TAG_RESULT])
505
358
 
506
359
        # Print moves keeping the line length to less than 256 characters (PGN requirement)
507
360
        line = ''
531
384
        
532
385
        Deleting a STR tag or setting one to an invalid value will raise an Error exception.
533
386
        """
534
 
        if self.__isValidTagName(name) is False:
 
387
        if self._isValidTagName(name) is False:
535
388
            raise Error('%s is an invalid tag name' % str(name))
536
389
        
537
390
        # If no value delete
538
391
        if value is None:
539
392
            # If is a STR tag throw an exception
540
 
            if self.__strTags.has_key(name):
 
393
            if self._strTags.has_key(name):
541
394
                raise Error('%s is a PGN STR tag and cannot be deleted' % name)
542
395
            
543
396
            # Delete the tag
544
397
            try:
545
 
                self.__strTags.pop(name)
 
398
                self._strTags.pop(name)
546
399
            except KeyError:
547
400
                pass
548
401
        
550
403
        else:
551
404
            # FIXME: Validate if it is a STR tag
552
405
            
553
 
            self.__tagsByName[name] = value
 
406
            self.tagsByName[name] = value
554
407
    
555
408
    def getTag(self, name, default = None):
556
409
        """Get a PGN tag.
561
414
        Return the value of the tag (string) or the default if the tag does not exist.
562
415
        """
563
416
        try:
564
 
            return self.__tagsByName[name]
 
417
            return self.tagsByName[name]
565
418
        except KeyError:
566
419
            return default
567
420
        
568
421
    def addMove(self, move):
569
 
        self.__moves.append(move)
 
422
        self.moves.append(move)
570
423
 
571
424
    def getMove(self, moveNumber):
572
 
        return self.__moves[moveNumber - 1]
 
425
        return self.moves[moveNumber - 1]
573
426
    
574
427
    def getMoves(self):
575
 
        return self.__moves
 
428
        return self.moves
576
429
 
577
430
    def __str__(self):
578
 
        
579
431
        string = ''
580
 
        for tag, value in self.__tagsByName.iteritems():
581
 
            string += tag + ' = ' + value + '\n'
 
432
        for tag, value in self.tagsByName.iteritems():
 
433
            string += '%s = %s\n' % (tag, value)
582
434
        string += '\n'
583
435
        
584
436
        number = 1
585
 
        for move in self.__moves:
586
 
            string += '%3i. ' % number + str(move[0]) + ' ' + str(move[1]) + '\n'
 
437
        moves = self.moves
 
438
        while len(moves) >= 2:
 
439
            string += '%3i. %s %s\n' % (number, moves[0].move, moves[1].move)
587
440
            number += 1
 
441
            moves = moves[2:]
 
442
        if len(moves) > 0:
 
443
            string += '%3i. %s\n' % (number, moves[0].move)
588
444
            
589
445
        return string
590
446
    
591
447
    # Private methods    
592
 
    def __makePGNString(self, string):
 
448
    def _makePGNString(self, string):
593
449
        """Make a PGN string.
594
450
        
595
451
        'string' is the string to convert to a PGN string (string).
602
458
        pgnString.replace('"', '\\"')
603
459
        return '"' + pgnString + '"'    
604
460
 
605
 
    def __isValidTagName(self, name):
 
461
    def _isValidTagName(self, name):
606
462
        """Valid a PGN tag name.
607
463
        
608
464
        'name' is the tag name to validate (string).
690
546
        """
691
547
        # Convert the file into PGN tokens
692
548
        f = file(fileName, 'r')
693
 
        p = PGNParser()
694
 
        gp = PGNGameParser()
695
 
        lineNumber = 1
696
 
        gameCount = 0
697
 
        while True:
698
 
            # Read a line from the file
699
 
            line = f.readline()
700
 
            if line == '':
701
 
                break
702
 
            
703
 
            # Parse the line into tokens
704
 
            tokens = p.parseLine(line, lineNumber)
705
 
 
706
 
            # Decode the tokens into PGN games
707
 
            for token in tokens:
708
 
                game = gp.parseToken(token)
709
 
                
710
 
                # Store this game and stop if only required to parse a certain number
711
 
                if game is not None:
712
 
                    gp = PGNGameParser()
713
 
                    self.__games.append(game)
714
 
                    gameCount += 1
715
 
 
716
 
                    if maxGames is not None and gameCount >= maxGames:
717
 
                        break
718
 
                    
719
 
            # YUCK... FIXME
720
 
            if maxGames is not None and gameCount >= maxGames:
721
 
                break
722
 
            
723
 
            lineNumber += 1
724
 
            
 
549
        p = PGNParser(maxGames)
 
550
        lineNumber = 0
 
551
        try:
 
552
            for line in f.readlines():
 
553
                lineNumber += 1                
 
554
                p.parseLine(line)
 
555
            p.complete()
 
556
        except Error, e:
 
557
            raise Error('Error on line %d: %s' % (lineNumber, e.args[0]))
 
558
 
725
559
        # Must be at least one game in the PGN file
726
 
        if gameCount == 0:
 
560
        self.__games = p.games
 
561
        if len(self.__games) == 0:
727
562
            raise Error('Empty PGN file')
728
563
 
729
564
        # Tidy up
730
 
        gp.complete()
731
 
        p.endParse()
732
565
        f.close()
733
566
 
734
567
if __name__ == '__main__':
 
568
    import time
 
569
 
735
570
    def test(fileName, maxGames = None):
 
571
        s = time.time()
736
572
        p = PGN(fileName, maxGames)
 
573
        print time.time() - s
737
574
        number = 1
738
575
        games = p[:]
739
 
        for game in games:
740
 
            print 'Game ' + str(number)
741
 
            print game
742
 
            print 
743
 
            number += 1
744
 
 
745
 
    test('example.pgn')
746
 
    test('rav.pgn')
747
 
    test('wolga-benko.pgn', 3)
748
 
 
749
 
    p = PGN('example.pgn')
750
 
    p.save('out.pgn')
 
576
        #for game in games:
 
577
        #    print 'Game ' + str(number)
 
578
        #    print game
 
579
        #    print 
 
580
        #    number += 1
 
581
 
 
582
    #test('example.pgn')
 
583
    #test('rav.pgn')
 
584
    #test('wolga-benko.pgn', 3)
 
585
    
 
586
    #test('wolga-benko.pgn')
 
587
    #test('yahoo_chess.pgn')
 
588
 
 
589
    #p = PGN('example.pgn')
 
590
    #p.save('out.pgn')