From 6c2cc134abf3f32d1d6ec172c6201f8d990c88ab Mon Sep 17 00:00:00 2001 From: Sébastien Dailly Date: Sun, 24 Aug 2014 12:52:10 +0200 Subject: Initial commit --- qml/python/sgfparser.py | 579 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 579 insertions(+) create mode 100644 qml/python/sgfparser.py (limited to 'qml/python/sgfparser.py') 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) -- cgit v1.2.3