summaryrefslogtreecommitdiff
path: root/qml/python/sgfparser.py
diff options
context:
space:
mode:
authorSébastien Dailly <sebastien@chimrod.com>2014-08-24 12:52:10 +0200
committerSébastien Dailly <sebastien@chimrod.com>2014-08-24 12:52:10 +0200
commit6c2cc134abf3f32d1d6ec172c6201f8d990c88ab (patch)
tree98b03151505f8fc058977f906e93e9a799b02217 /qml/python/sgfparser.py
Initial commit
Diffstat (limited to 'qml/python/sgfparser.py')
-rw-r--r--qml/python/sgfparser.py579
1 files changed, 579 insertions, 0 deletions
diff --git a/qml/python/sgfparser.py b/qml/python/sgfparser.py
new file mode 100644
index 0000000..2ad91c9
--- /dev/null
+++ b/qml/python/sgfparser.py
@@ -0,0 +1,579 @@
+# File: sgfparser.py
+
+## This is part of uliGo 0.4, a program for practicing
+## go problems. For more information, see http://www.g0ertz.de/uligo/
+
+## Copyright (C) 2001-12 Ulrich Goertz (uligo@g0ertz.de)
+
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+## You should have received a copy of the GNU General Public License
+## along with this program (gpl.txt); if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+## The GNU GPL is also currently available at
+## http://www.gnu.org/copyleft/gpl.html
+
+import re
+import string
+
+class SGFError(Exception): pass
+
+reGameStart = re.compile(r'\(\s*;')
+reRelevant = re.compile(r'[\[\]\(\);]')
+reStartOfNode = re.compile(r'\s*;\s*')
+
+def SGFescape(s):
+ t = s.replace('\\', '\\\\')
+ t = t.replace(']', '\\]')
+ return t
+
+class Node:
+ def __init__(self, previous=None, SGFstring = '', level=0):
+ self.previous = previous
+ self.next = None
+ self.up = None
+ self.down = None
+ self.level = level # self == self.previous.next[self.level]
+
+ self.numChildren = 0
+
+ self.SGFstring = SGFstring
+ self.parsed = 0
+ if self.SGFstring:
+ self.parseNode()
+ else:
+ self.data = {}
+
+ self.posyD = 0
+
+
+ def getData(self):
+ if not self.parsed: self.parseNode()
+ return self.data
+
+ def pathToNode(self):
+ l = []
+ n = self
+
+ while n.previous:
+ l.append(n.level)
+ n = n.previous
+
+ l.reverse()
+ return l
+
+
+ def parseNode(self):
+
+ if self.parsed: return
+
+ s = self.SGFstring
+ i = 0
+
+ match = reStartOfNode.search(s, i)
+ if not match:
+ raise SGFError('No node found')
+ i = match.end()
+
+ node = {}
+ while i < len(s):
+
+ while i < len(s) and s[i] in string.whitespace: i += 1
+ if i >= len(s): break
+
+ ID = []
+
+ while not s[i] == '[':
+ if s[i] in string.ascii_uppercase:
+ ID.append(s[i])
+ elif not s[i] in string.ascii_lowercase + string.whitespace:
+ raise SGFError('Invalid Property ID')
+
+ i += 1
+
+ if i >= len(s):
+ raise SGFError('Property ID does not have any value')
+
+ i += 1
+
+ key = ''.join(ID)
+
+ if key == '': raise SGFError('Property does not have a correct ID')
+
+
+ if key in node:
+ if not Node.sloppy:
+ raise SGFError('Multiple occurrence of SGF tag')
+ else:
+ node[key] = []
+
+ propertyValueList = []
+ while 1:
+ propValue = []
+ while s[i] != ']':
+ if s[i] == '\t': # convert whitespace to ' '
+ propValue.append(' ')
+ i += 1
+ continue
+ if s[i] == '\\':
+ i += 1 # ignore escaped characters, throw away backslash
+ if s[i:i+2] in ['\n\r', '\r\n']:
+ i += 2
+ continue
+ elif s[i] in ['\n', '\r']:
+ i += 1
+ continue
+ propValue.append(s[i])
+ i += 1
+
+ if i >= len(s):
+ raise SGFError('Property value does not end')
+
+ propertyValueList.append(''.join(propValue))
+
+ i += 1
+
+ while i < len(s) and s[i] in string.whitespace:
+ i += 1
+
+ if i >= len(s) or s[i] != '[': break
+ else: i += 1
+
+ if key in ['B', 'W', 'AB', 'AW']:
+ for N in range(len(propertyValueList)):
+ en = propertyValueList[N]
+ if Node.sloppy:
+ en = en.replace('\n', '')
+ en = en.replace('\r', '')
+ if not (len(en) == 2 or (len(en) == 0 and key in ['B', 'W'])):
+ raise SGFError('')
+ propertyValueList[N] = en
+
+ node[key].extend(propertyValueList)
+
+ self.data = node
+ self.parsed = 1
+
+
+Node.sloppy = 1
+
+# ------------------------------------------------------------------------------------
+
+class Cursor:
+
+ """ Initialized with an SGF file. Then use game(n); next(n), previous to navigate.
+ self.collection is list of Nodes, namely of the root nodes of the game trees.
+
+ self.currentN is the current Node
+ self.currentNode() returns self.currentN.data
+
+ The sloppy option for __init__ determines if the following things, which are not allowed
+ according to the SGF spec, are accepted nevertheless:
+ - multiple occurrences of a tag in one node
+ - line breaks in AB[]/AW[]/B[]/W[] tags (e.g. "B[a\nb]")
+ """
+
+
+ def __init__(self, sgf, sloppy = 1):
+ Node.sloppy = sloppy
+
+ self.height = 0
+ self.width = 0
+ self.posx = 0
+ self.posy = 0
+
+ self.root = Node(None, '', 0)
+
+ self.parse(sgf)
+ self.currentN = self.root.next
+ self.setFlags()
+
+ def setFlags(self):
+ if self.currentN.next: self.atEnd = 0
+ else: self.atEnd = 1
+ if self.currentN.previous: self.atStart = 0
+ else: self.atStart = 1
+
+ def noChildren(self):
+ return self.currentN.numChildren
+
+ def currentNode(self):
+ if not self.currentN.parsed:
+ self.currentN.parseNode()
+ return self.currentN.data
+
+ def parse(self, sgf):
+
+ curr = self.root
+
+ p = -1 # start of the currently parsed node
+ c = [] # list of nodes from which variations started
+ last = ')' # type of last aprsed bracked ('(' or ')')
+ inbrackets = 0 # are the currently parsed characters in []'s?
+
+ height_previous = 0
+ width_currentVar = 0
+
+ i = 0 # current parser position
+
+ # skip everything before first (; :
+
+ match = reGameStart.search(sgf, i)
+ if not match:
+ raise SGFError('No game found')
+
+ i = match.start()
+
+ while i < len(sgf):
+
+ match = reRelevant.search(sgf, i)
+ if not match:
+ break
+ i = match.end() - 1
+
+ if inbrackets:
+ if sgf[i]==']':
+ numberBackslashes = 0
+ j = i-1
+ while sgf[j] == '\\':
+ numberBackslashes += 1
+ j -= 1
+ if not (numberBackslashes % 2):
+ inbrackets = 0
+ i = i + 1
+ continue
+
+ if sgf[i] == '[':
+ inbrackets = 1
+
+ if sgf[i] == '(':
+ if last != ')': # start of first variation of previous node
+ if p != -1: curr.SGFstring = sgf[p:i]
+
+ nn = Node()
+ nn.previous = curr
+
+ width_currentVar += 1
+ if width_currentVar > self.width: self.width = width_currentVar
+
+ if curr.next:
+ last = curr.next
+ while last.down: last = last.down
+ nn.up = last
+ last.down = nn
+ nn.level = last.level + 1
+ self.height += 1
+ nn.posyD = self.height - height_previous
+ else:
+ curr.next = nn
+ nn.posyD = 0
+ height_previous = self.height
+
+ curr.numChildren += 1
+
+ c.append((curr, width_currentVar-1, self.height))
+
+ curr = nn
+
+ p = -1
+ last = '('
+
+ if sgf[i] == ')':
+ if last != ')' and p != -1:
+ curr.SGFstring = sgf[p:i]
+ try:
+ curr, width_currentVar, height_previous = c.pop()
+ except IndexError:
+ raise SGFError('Game tree parse error')
+ last = ')'
+
+ if sgf[i] == ';':
+ if p != -1:
+ curr.SGFstring = sgf[p:i]
+ nn = Node()
+ nn.previous = curr
+ width_currentVar += 1
+ if width_currentVar > self.width: self.width = width_currentVar
+ nn.posyD = 0
+ curr.next = nn
+ curr.numChildren = 1
+ curr = nn
+ p = i
+
+ i = i + 1
+
+ if inbrackets or c:
+ raise SGFError('Game tree parse error')
+
+ n = curr.next
+ n.previous = None
+ n.up = None
+
+ while n.down:
+ n = n.down
+ n.previous = None
+
+
+ def game(self, n):
+ if n < self.root.numChildren:
+ self.posx = 0
+ self.posy = 0
+ self.currentN = self.root.next
+ for i in range(n): self.currentN = self.currentN.down
+ self.setFlags()
+ else:
+ raise SGFError('Game not found')
+
+
+ def delVariation(self, c):
+
+ if c.previous:
+ self.delVar(c)
+ else:
+ if c.next:
+ node = c.next
+ while node.down:
+ node = node.down
+ self.delVar(node.up)
+
+ self.delVar(node)
+
+ c.next = None
+
+ self.setFlags()
+
+
+ def delVar(self, node):
+ if node.up: node.up.down = node.down
+ else: node.previous.next = node.down
+
+ if node.down:
+ node.down.up = node.up
+ node.down.posyD = node.posyD
+ n = node.down
+ while n:
+ n.level -= 1
+ n = n.down
+
+ h = 0
+ n = node
+ while n.next:
+ n = n.next
+ while n.down:
+ n = n.down
+ h += n.posyD
+
+ if node.up or node.down: h += 1
+
+ p = node.previous
+ p.numChildren -= 1
+
+ while p:
+ if p.down: p.down.posyD -= h
+ p = p.previous
+
+
+ self.height -= h
+
+
+
+
+ def add(self, st):
+ node = Node(self.currentN,st,0)
+
+ node.down = None
+ node.next = None
+ node.numChildren = 0
+
+ if not self.currentN.next:
+ node.level = 0
+ node.posyD = 0
+ node.up = 0
+
+ self.currentN.next = node
+ self.currentN.numChildren = 1
+ else:
+ n = self.currentN.next
+ while n.down:
+ n = n.down
+ self.posy += n.posyD
+
+
+ n.down = node
+ node.up = n
+ node.level = n.level + 1
+ node.next = None
+ self.currentN.numChildren += 1
+
+ node.posyD = 1
+ while n.next:
+ n = n.next
+ while n.down:
+ n = n.down
+ node.posyD += n.posyD
+
+ self.posy += node.posyD
+
+ self.height += 1
+
+ n = node
+ while n.previous:
+ n = n.previous
+ if n.down: n.down.posyD += 1
+
+ self.currentN = node
+
+ self.posx += 1
+ self.setFlags()
+
+ if self.posx > self.width: self.width += 1
+
+
+
+
+
+
+
+ def next(self, n=0):
+ if n >= self.noChildren():
+ raise SGFError('Variation not found')
+
+ self.posx += 1
+
+ self.currentN = self.currentN.next
+ for i in range(n):
+ self.currentN = self.currentN.down
+ self.posy += self.currentN.posyD
+ self.setFlags()
+ return self.currentNode()
+
+ def previous(self):
+ if self.currentN.previous:
+ while self.currentN.up:
+ self.posy -= self.currentN.posyD
+ self.currentN = self.currentN.up
+ self.currentN = self.currentN.previous
+ self.posx -= 1
+ else: raise SGFError('No previous node')
+ self.setFlags()
+ return self.currentNode()
+
+ def getRootNode(self, n):
+ if not self.root: return
+ if n >= self.root.numChildren: raise SGFError('Game not found')
+
+ nn = self.root.next
+ for i in range(n): nn = nn.down
+
+ if not nn.parsed: nn.parseNode()
+
+ return nn.data
+
+
+ def updateCurrentNode(self):
+ """ Put the data in self.currentNode into the corresponding string in self.collection.
+ This will be called from an application which may have modified self.currentNode."""
+
+ self.currentN.SGFstring = self.nodeToString(self.currentN.data)
+
+
+
+
+ def updateRootNode(self, data, n=0):
+ if n >= self.root.numChildren:
+ raise SGFError('Game not found')
+
+ nn = self.root.next
+ for i in range(n): nn = nn.down
+
+ nn.SGFstring = self.rootNodeToString(data)
+ nn.parsed = 0
+ nn.parseNode()
+
+
+ def rootNodeToString(self, node):
+
+ result = [';']
+ keylist = ['GM', 'FF', 'SZ', 'PW', 'WR', 'PB', 'BR',
+ 'EV', 'RO', 'DT', 'PC', 'KM', 'RE', 'US', 'GC']
+ for key in keylist:
+ if key in node:
+ result.append(key)
+ result.append('[' + SGFescape(node[key][0]) + ']\n')
+
+ l = 0
+ for key in node.keys():
+ if not key in keylist:
+ result.append(key)
+ l += len(key)
+ for item in node[key]:
+ result.append('[' + SGFescape(item) + ']\n')
+ l += len(item) + 2
+ if l > 72:
+ result.append('\n')
+ l = 0
+
+ return ''.join(result)
+
+ def nodeToString(self, node):
+ l = 0
+ result = [';']
+ for k in node.keys():
+ if l + len(k) > 72:
+ result.append('\n')
+ l = 0
+ if not node[k]: continue
+ result.append(k)
+ l += len(k)
+ for item in node[k]:
+ if l + len(item) > 72:
+ result.append('\n')
+ l = 0
+ l += len(item) + 2
+ result.append('[' + SGFescape(item) + ']')
+
+ return ''.join(result)
+
+
+ def outputVar(self, node):
+
+ result = []
+
+ result.append(node.SGFstring)
+
+ while node.next:
+ node = node.next
+
+ if node.down:
+ while node.down:
+ result.append('(' + self.outputVar(node) + ')' )
+ node = node.down
+
+ result.append('(' + self.outputVar(node) + ')' )
+ return ''.join(result)
+
+ else:
+ result.append(node.SGFstring)
+
+ return ''.join(result)
+
+
+
+ def output(self):
+ result = []
+
+ n = self.root.next
+
+ while n:
+ result.append('(' + self.outputVar(n)+ ')\n')
+ n = n.down
+
+ return ''.join(result)