GraphicalGoBoards: sgfparser.py

File sgfparser.py, 15.8 kB (added by arend, 3 years ago)

Ulrich Goertz' GPL'ed python sgf parser (taken from kombilo 0.5)

Line 
1# File: sgfparser.py
2
3##   Copyright (C) 2001-4 Ulrich Goertz (u@g0ertz.de)
4
5##   This is part of Kombilo, a go database program.
6
7##   This program is free software; you can redistribute it and/or modify
8##   it under the terms of the GNU General Public License as published by
9##   the Free Software Foundation; either version 2 of the License, or
10##   (at your option) any later version.
11
12##   This program is distributed in the hope that it will be useful,
13##   but WITHOUT ANY WARRANTY; without even the implied warranty of
14##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15##   GNU General Public License for more details.
16
17##   You should have received a copy of the GNU General Public License
18##   along with this program (see doc/license.txt); if not, write to the Free Software
19##   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20##   The GNU GPL is also currently available at
21##   http://www.gnu.org/copyleft/gpl.html
22
23
24import re
25import string
26
27class SGFError(Exception): pass
28
29reGameStart = re.compile(r'\(\s*;')
30reRelevant = re.compile(r'[\[\]\(\);]')
31reStartOfNode = re.compile(r'\s*;\s*')
32
33def SGFescape(s):
34    t = string.replace(s, '\\', '\\\\')
35    t = string.replace(t, ']', '\\]')
36    return t
37
38class Node:
39    def __init__(self, previous=None, SGFstring = '', level=0):
40        self.previous = previous
41        self.next = None
42        self.up = None
43        self.down = None
44        self.level = level # self == self.previous.next[self.level]
45
46        self.numChildren = 0
47       
48        self.SGFstring = SGFstring
49        self.parsed = 0
50        if self.SGFstring:
51            self.parseNode()
52        else:
53            self.data = {}
54
55        self.posyD = 0
56
57
58    def getData(self):
59        if not self.parsed: self.parseNode()
60        return self.data
61
62    def pathToNode(self):
63        l = []
64        n = self
65
66        while n.previous:
67            l.append(n.level)
68            n = n.previous
69           
70        l.reverse()
71        return l
72
73
74    def parseNode(self):
75
76        if self.parsed: return
77
78        s = self.SGFstring
79        i = 0
80
81        match = reStartOfNode.search(s, i)
82        if not match:
83            raise SGFError('No node found')
84        i = match.end()
85
86        node = {}
87        while i < len(s):
88
89            while i < len(s) and s[i] in string.whitespace: i += 1
90            if i >= len(s): break
91           
92            ID = []
93           
94            while not s[i] == '[':
95                if s[i] in string.uppercase:
96                    ID.append(s[i])
97                elif not s[i] in string.lowercase + string.whitespace:
98                    raise SGFError('Invalid Property ID')
99                   
100                i += 1
101
102                if i >= len(s):
103                    raise SGFError('Property ID does not have any value')
104
105            i += 1
106
107            key = string.join(ID, '')
108
109            if key == '': raise SGFError('Property does not have a correct ID')
110
111
112            if node.has_key(key):
113                if not Node.sloppy:
114                    raise SGFError('Multiple occurrence of SGF tag')
115            else:
116                node[key] = []
117               
118            propertyValueList = []
119            while 1:
120                propValue = []
121                while s[i] != ']':
122                    if s[i] == '\t':      # convert whitespace to ' '
123                        propValue.append(' ')
124                        i += 1
125                        continue
126                    if s[i] == '\\':
127                        i += 1            # ignore escaped characters, throw away backslash
128                        if s[i:i+2] in ['\n\r', '\r\n']:
129                            i += 2
130                            continue
131                        elif s[i] in ['\n', '\r']:
132                            i += 1
133                            continue
134                    propValue.append(s[i])
135                    i += 1
136                   
137                    if i >= len(s):
138                        raise SGFError('Property value does not end')
139
140                propertyValueList.append(string.join(propValue, ''))
141
142                i += 1
143                       
144                while i < len(s) and s[i] in string.whitespace:
145                    i += 1
146                   
147                if i >= len(s) or s[i] != '[': break   
148                else: i += 1
149
150            if key in ['B', 'W', 'AB', 'AW']:
151                for N in range(len(propertyValueList)):
152                    en = propertyValueList[N]
153                    if Node.sloppy:
154                        en = string.replace(en, '\n', '')
155                        en = string.replace(en, '\r', '')
156                    if not (len(en) == 2 or (len(en) == 0 and key in ['B', 'W'])):
157                        raise SGFError('')
158                    propertyValueList[N] = en
159                                           
160            node[key].extend(propertyValueList)
161           
162        self.data = node
163        self.parsed = 1
164
165
166Node.sloppy = 1
167
168# ------------------------------------------------------------------------------------
169
170class Cursor:
171
172    """ Initialized with an SGF file. Then use game(n); next(n), previous to navigate.
173    self.collection is list of Nodes, namely of the root nodes of the game trees.
174   
175    self.currentN is the current Node
176    self.currentNode() returns self.currentN.data
177
178    The sloppy option for __init__ determines if the following things, which are not allowed
179    according to the SGF spec, are accepted nevertheless:
180     - multiple occurrences of a tag in one node
181     - line breaks in AB[]/AW[]/B[]/W[] tags (e.g. "B[a\nb]")
182    """
183
184
185    def __init__(self, sgf, sloppy):
186        Node.sloppy = sloppy
187
188        self.height = 0
189        self.width = 0
190        self.posx = 0
191        self.posy = 0
192
193        self.root = Node(None, '', 0)
194       
195        self.parse(sgf)
196        self.currentN = self.root.next
197        self.setFlags()
198
199        if self.currentNode().has_key('CA') and self.currentNode()['CA']:
200            self.encoding = self.currentNode()['CA'][0]
201        else:
202            self.encoding = ''
203
204       
205    def setFlags(self):
206        if self.currentN.next: self.atEnd = 0
207        else: self.atEnd = 1
208        if self.currentN.previous: self.atStart = 0
209        else: self.atStart = 1
210       
211    def noChildren(self):
212        return self.currentN.numChildren
213
214    def currentNode(self):
215        if not self.currentN.parsed:
216            self.currentN.parseNode()
217        return self.currentN.data
218
219
220
221    def parse(self, sgf):
222
223        curr = self.root
224       
225        p = -1           # start of the currently parsed node
226        c = []           # list of nodes from which variations started
227        last = ')'       # type of last aprsed bracked ('(' or ')')
228        inbrackets = 0   # are the currently parsed characters in []'s?
229
230        height_previous = 0
231        width_currentVar = 0
232
233        i = 0 # current parser position
234
235        # skip everything before first (; :
236
237        match = reGameStart.search(sgf, i)
238        if not match:
239            raise SGFError('No game found')
240
241        i = match.start()
242       
243        while i < len(sgf):
244
245            match = reRelevant.search(sgf, i)
246            if not match:
247                break
248            i = match.end() - 1
249           
250            if inbrackets:
251                if sgf[i]==']':
252                    numberBackslashes = 0
253                    j = i-1
254                    while sgf[j] == '\\':
255                        numberBackslashes += 1
256                        j -= 1
257                    if not (numberBackslashes % 2):
258                        inbrackets = 0
259                i = i + 1
260                continue
261
262            if sgf[i] == '[':
263                inbrackets = 1
264       
265            if sgf[i] == '(':
266                if last != ')':       # start of first variation of previous node
267                    if p != -1: curr.SGFstring = sgf[p:i]
268
269                nn = Node()
270                nn.previous = curr
271
272                width_currentVar += 1
273                if width_currentVar > self.width: self.width = width_currentVar
274               
275                if curr.next:
276                    last = curr.next
277                    while last.down: last = last.down
278                    nn.up = last
279                    last.down = nn
280                    nn.level = last.level + 1
281                    self.height += 1
282                    nn.posyD = self.height - height_previous
283                else:
284                    curr.next = nn
285                    nn.posyD = 0
286                    height_previous = self.height
287
288                curr.numChildren += 1
289               
290                c.append((curr, width_currentVar-1, self.height))
291               
292                curr = nn
293               
294                p = -1
295                last = '('
296       
297            if sgf[i] == ')':
298                if last != ')' and p != -1:
299                    curr.SGFstring = sgf[p:i]
300                try:
301                    curr, width_currentVar, height_previous = c.pop()
302                except IndexError:
303                    raise SGFError('Game tree parse error')
304                last = ')'
305
306            if sgf[i] == ';':
307                if p != -1:
308                    curr.SGFstring = sgf[p:i]
309                    nn = Node()
310                    nn.previous = curr
311                    width_currentVar += 1
312                    if width_currentVar > self.width: self.width = width_currentVar
313                    nn.posyD = 0
314                    curr.next = nn
315                    curr.numChildren = 1
316                    curr = nn
317                p = i
318
319            i = i + 1
320
321        if inbrackets or c:
322            raise SGFError('Game tree parse error')
323
324        n = curr.next
325        n.previous = None
326        n.up = None
327
328        while n.down:
329            n = n.down
330            n.previous = None
331
332
333    def game(self, n):
334        if n < self.root.numChildren:
335            self.posx = 0
336            self.posy = 0
337            self.currentN = self.root.next
338            for i in range(n): self.currentN = self.currentN.down
339            self.setFlags()
340        else:
341            raise SGFError('Game not found')
342
343
344    def delVariation(self, c):
345
346        if c.previous:
347            self.delVar(c)
348        else:
349            if c.next:
350                node = c.next
351                while node.down:
352                    node = node.down
353                    self.delVar(node.up)
354     
355                self.delVar(node)
356   
357            c.next = None
358
359        self.setFlags()
360       
361
362    def delVar(self, node):
363        if node.up: node.up.down = node.down
364        else: node.previous.next = node.down
365 
366        if node.down:
367            node.down.up = node.up
368            node.down.posyD = node.posyD
369            n = node.down
370            while n: 
371                n.level -= 1
372                n = n.down
373
374        h = 0
375        n = node
376        while n.next:
377            n = n.next
378            while n.down:
379                n = n.down
380                h += n.posyD
381
382        if node.up or node.down: h += 1
383
384        p = node.previous
385        p.numChildren -= 1
386
387        while p:
388            if p.down: p.down.posyD -= h
389            p = p.previous
390 
391
392        self.height -= h
393
394
395
396
397    def add(self, st):
398        node = Node(self.currentN,st,0)
399
400        node.down = None
401        node.next = None
402        node.numChildren = 0
403
404        if not self.currentN.next:
405            node.level = 0
406            node.posyD = 0
407            node.up = 0
408
409            self.currentN.next = node
410            self.currentN.numChildren = 1
411        else:
412            n = self.currentN.next
413            while n.down:
414                n = n.down
415                self.posy += n.posyD
416               
417   
418            n.down = node
419            node.up = n
420            node.level = n.level + 1
421            node.next = None
422            self.currentN.numChildren += 1
423
424            node.posyD = 1
425            while n.next:
426                n = n.next
427                while n.down:
428                    n = n.down
429                    node.posyD += n.posyD
430     
431            self.posy += node.posyD
432
433            self.height += 1
434
435            n = node
436            while n.previous:
437                n = n.previous
438                if n.down: n.down.posyD += 1
439
440        self.currentN = node
441
442        self.posx += 1
443        self.setFlags()
444
445        if self.posx > self.width: self.width += 1
446
447
448
449
450
451
452
453    def next(self, n=0):
454        if n >= self.noChildren():
455            raise SGFError('Variation not found')
456
457        self.posx += 1
458
459        self.currentN = self.currentN.next
460        for i in range(n):
461            self.currentN = self.currentN.down
462            self.posy += self.currentN.posyD
463        self.setFlags()
464        return self.currentNode()
465   
466    def previous(self):
467        if self.currentN.previous:
468            while self.currentN.up:
469                self.posy -= self.currentN.posyD
470                self.currentN = self.currentN.up
471            self.currentN = self.currentN.previous
472            self.posx -= 1
473        else: raise SGFError('No previous node')
474        self.setFlags()
475        return self.currentNode()
476
477    def getRootNode(self, n):
478        if not self.root: return
479        if n >= self.root.numChildren: raise SGFError('Game not found')
480
481        nn = self.root.next
482        for i in range(n): nn = nn.down
483
484        if not nn.parsed: nn.parseNode()
485
486        return nn.data
487
488
489    def updateCurrentNode(self):
490        """ Put the data in self.currentNode into the corresponding string in self.collection.
491        This will be called from an application which may have modified self.currentNode."""
492
493        self.currentN.SGFstring = self.nodeToString(self.currentN.data)
494       
495
496
497
498    def updateRootNode(self, data, n=0):
499        if n >= self.root.numChildren:
500            raise SGFError('Game not found')
501
502        nn = self.root.next
503        for i in range(n): nn = nn.down
504       
505        nn.SGFstring = self.rootNodeToString(data)
506        nn.parsed = 0
507        nn.parseNode()
508
509
510    def rootNodeToString(self, node):
511       
512        result = [';']
513        keylist = ['GM', 'FF', 'SZ', 'PW', 'WR', 'PB', 'BR',
514                   'EV', 'RO', 'DT', 'PC', 'KM', 'RE', 'US', 'GC']
515        for key in keylist:
516            if node.has_key(key):
517                result.append(key)
518                result.append('[' + SGFescape(node[key][0]) + ']\n')
519               
520        l = 0
521        for key in node.keys():
522            if not key in keylist:
523                result.append(key)
524                l += len(key)
525                for item in node[key]:
526                    result.append('[' + SGFescape(item) + ']\n')
527                    l += len(item) + 2
528                    if l > 72:
529                        result.append('\n')
530                        l = 0
531                       
532        return string.join(result, '')
533
534    def nodeToString(self, node):
535        l = 0
536        result = [';']
537        for k in node.keys():
538            if l + len(k) > 72:
539                result.append('\n')
540                l = 0
541            if not node[k]: continue
542            result.append(k)
543            l += len(k) 
544            for item in node[k]:
545                if l + len(item) > 72:
546                    result.append('\n')
547                    l = 0
548                l += len(item) + 2
549                result.append('[' + SGFescape(item) + ']')
550
551        return string.join(result, '')
552
553
554    def outputVar(self, node):
555
556        result = []
557       
558        result.append(node.SGFstring)
559
560        while node.next:
561            node = node.next
562
563            if node.down:
564                while node.down:
565                    result.append('(' + self.outputVar(node) + ')' )
566                    node = node.down
567
568                result.append('(' + self.outputVar(node) + ')' )
569                return string.join(result, '')
570
571            else:
572                result.append(node.SGFstring)
573
574        return string.join(result, '')
575
576
577
578    def output(self):
579        result = []
580
581        n = self.root.next
582
583        while n:
584            result.append('(' + self.outputVar(n)+ ')\n')
585            n = n.down
586
587        return string.join(result, '')
588