From 6c2cc134abf3f32d1d6ec172c6201f8d990c88ab Mon Sep 17 00:00:00 2001
From: Sébastien Dailly <sebastien@chimrod.com>
Date: Sun, 24 Aug 2014 12:52:10 +0200
Subject: Initial commit

---
 qml/python/__init__.py                   |   3 +
 qml/python/board.py                      |  57 +++
 qml/python/game.py                       | 145 ++++++++
 qml/python/sgfparser.py                  | 579 +++++++++++++++++++++++++++++++
 qml/python/tests/__init__.py             |   3 +
 qml/python/tests/test.sgf                |  13 +
 qml/python/tests/test2.sgf               |   6 +
 qml/python/tests/test_game.py            | 101 ++++++
 qml/python/tests/test_transformations.py | 134 +++++++
 qml/python/transformations.py            | 125 +++++++
 10 files changed, 1166 insertions(+)
 create mode 100644 qml/python/__init__.py
 create mode 100644 qml/python/board.py
 create mode 100644 qml/python/game.py
 create mode 100644 qml/python/sgfparser.py
 create mode 100644 qml/python/tests/__init__.py
 create mode 100644 qml/python/tests/test.sgf
 create mode 100644 qml/python/tests/test2.sgf
 create mode 100644 qml/python/tests/test_game.py
 create mode 100644 qml/python/tests/test_transformations.py
 create mode 100644 qml/python/transformations.py

(limited to 'qml/python')

diff --git a/qml/python/__init__.py b/qml/python/__init__.py
new file mode 100644
index 0000000..7847780
--- /dev/null
+++ b/qml/python/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
diff --git a/qml/python/board.py b/qml/python/board.py
new file mode 100644
index 0000000..48f3ff0
--- /dev/null
+++ b/qml/python/board.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import os
+try:
+    import pyotherside
+except:
+    print("no pyotherside module loaded")
+
+import sgfparser
+from game import Game
+
+counter = 0
+
+path = ""
+cursor = None
+
+def setPath(qtPath):
+    global path
+    path = qtPath
+
+def loadBoard(filename):
+    global cursor
+
+    sgfPath = os.path.join(path,"../content","sgf",filename);
+    pyotherside.send('log', sgfPath)
+    try:
+        f = open(sgfPath)
+        s = f.read()
+        f.close()
+    except IOError:
+        pyotherside.send('log', "Cannot open %s" % filename)
+        return
+
+    try:
+        cursor = sgfparser.Cursor(s)
+    except sgfparser.SGFError:
+        pyotherside.send('log', 'Error in SGF file!')
+        return
+    pyotherside.send('log', 'File %s loaded' % filename)
+    pyotherside.send('log', 'Found %d problems' % cursor.root.numChildren)
+    return cursor.root.numChildren
+
+def getGame(n):
+    global cursor
+
+    cursor.game(n)
+
+    game = Game(cursor)
+    game.normalize()
+    pyotherside.send('log', "Game loaded !!")
+
+    return {
+        "tree": game.tree,
+        "size": game.get_size(),
+        "side": game.side,
+    }
diff --git a/qml/python/game.py b/qml/python/game.py
new file mode 100644
index 0000000..7a91ae7
--- /dev/null
+++ b/qml/python/game.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import random
+from transformations import *
+
+
+class Game(object):
+    """" A game loaded from a sgf data source.
+    """
+
+    def __init__(self, cursor):
+        """ Create a new Game on the current cursor position.
+        :cursor: The cursor opened at the game.
+        """
+
+        node = cursor.currentNode()
+
+        # display problem name
+        name = ''
+        if 'GN' in node:
+            name = node['GN'][0][:15]
+        self.name = name
+
+        while not ('AG' in node or 'AW' in node \
+                   or 'B' in node or 'W' in node):
+            node = cursor.next()
+
+        self.min_x, self.min_y = 19, 19
+        self.max_x, self.max_y = 0, 0
+
+        # Get the board size from the whole possibles positions and create the
+        # game tree
+        self.tree = Game.create_tree(cursor, self.extend_board_size, [])
+
+        x_space = 2
+        y_space = 2
+
+        if self.min_y > y_space:
+            self.min_y -= y_space
+
+        if self.min_x > x_space:
+            self.min_x -= x_space
+
+        if self.max_y < 19 - y_space:
+            self.max_y += y_space
+
+        if self.max_x < 19 - x_space:
+            self.max_x += x_space
+
+        self.side = {
+            "TOP": self.min_y != 0,
+            "LEFT": self.min_x != 0,
+            "RIGHT": self.max_x != 19,
+            "BOTTOM": self.max_y != 19,
+        }
+
+
+    def extend_board_size(self, pos):
+        """ Extend the board size to include the position given.
+        """
+        x, y = Game.conv_coord(pos)
+        self.min_x = min(x, self.min_x)
+        self.max_x = max(x, self.max_x)
+        self.min_y = min(y, self.min_y)
+        self.max_y = max(y, self.max_y)
+        return (x, y)
+
+    @staticmethod
+    def create_tree(cursor, fun, acc=None):
+        """ Walk over the whole node in the game and call fun for each of them.
+        :cursor:    The cursor in the sgf parser.
+        :fun:       Function called for each position read
+        """
+
+        if acc is None:
+            acc = []
+
+        node = cursor.currentNode().copy()
+        for key in ['AB', 'AW', 'B', 'W']:
+            if key in node:
+                node[key] = [fun(pos) for pos in node[key]]
+
+        acc.append(node)
+        childs = cursor.noChildren()
+
+        if childs == 1:
+            # When there is only one child, we just add it to the current path
+            cursor.next()
+            Game.create_tree(cursor, fun, acc)
+            cursor.previous()
+        elif childs > 1:
+            # Create a new list containing each subtree
+            sub_nodes = []
+            for i in range(childs):
+                cursor.next(i)
+                sub_nodes.append(Game.create_tree(cursor, fun))
+                cursor.previous()
+            acc.append(sub_nodes)
+        return acc
+
+    def get_size(self):
+        #return self.max_x, self.max_y
+        x_size = self.max_x - self.min_x
+        y_size = self.max_y - self.min_y
+        return min(19, x_size + 1), min(19, y_size + 1)
+
+    @staticmethod
+    def conv_coord(x):
+        """ This takes coordinates in SGF style (aa - qq) and returns the
+        corresponding integer coordinates (between 1 and 19). """
+
+        print(x)
+
+        return tuple([ord(c) - 96 for c in x])
+
+    def parse_tree(self, fun, elements=None):
+        """" Parse the current tree, and apply fun to each element.
+        """
+
+        if elements is None:
+            elements = self.tree
+
+        for elem in elements:
+            if isinstance(elem, dict):
+                for key in ['AB', 'AW', 'B', 'W']:
+                    if key in elem:
+                        elem[key] = [fun(pos) for pos in elem[key]]
+#                for type, values in elem.items():
+#                    elem[type] = [fun(coord) for coord in values]
+            else:
+                for l in elem:
+                    self.parse_tree(fun, l)
+
+    def normalize(self):
+        """ Create a normalized board, translated on lower coord.
+        """
+
+        for transformation in [Translation(self), Rotation(self), Translation(self), Symmetry(self)]:
+            if not transformation.is_valid():
+                continue
+
+            self.parse_tree(transformation.apply_points)
+            self.min_x, self.min_y, self.max_x, self.max_y = transformation.get_new_size()
+            self.side = transformation.get_new_side()
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)
diff --git a/qml/python/tests/__init__.py b/qml/python/tests/__init__.py
new file mode 100644
index 0000000..7847780
--- /dev/null
+++ b/qml/python/tests/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
diff --git a/qml/python/tests/test.sgf b/qml/python/tests/test.sgf
new file mode 100644
index 0000000..bdbe898
--- /dev/null
+++ b/qml/python/tests/test.sgf
@@ -0,0 +1,13 @@
+(;GM[1]FF[3]
+;AW[ro][nq][oq][pq][qq][rq][mr]
+AB[or][pr][qr][rr][sr]
+    (;W[os]
+        (;B[ps];W[rs];B[ns];W[nr])
+        (;B[ns];W[nr];B[rs];W[ps];B[qs];W[os])
+    )
+(;W[rs]
+    (;B[os];W[qs])
+    (;B[qs];W[os];B[nr];W[ns])
+)
+)
+
diff --git a/qml/python/tests/test2.sgf b/qml/python/tests/test2.sgf
new file mode 100644
index 0000000..a11356c
--- /dev/null
+++ b/qml/python/tests/test2.sgf
@@ -0,0 +1,6 @@
+(;GM[1]FF[3]
+;AW[qn][mp][qp][rp][kq][mq][oq][pq][nr][pr][rs]
+AB[nn][mo][np][op][pp][nq][qq][rq][qr][sr][qs]
+(;B[or];W[os];B[ps];W[or];B[mr])
+(;B[ps]WV[ps];W[or];B[mr];W[lr])
+)
diff --git a/qml/python/tests/test_game.py b/qml/python/tests/test_game.py
new file mode 100644
index 0000000..de43909
--- /dev/null
+++ b/qml/python/tests/test_game.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import unittest
+from python import sgfparser, game
+
+def create_board(fic):
+   with open("python/tests/%s" % fic) as f:
+       cursor = sgfparser.Cursor(f.read())
+
+   return game.Game(cursor)
+
+class TestBoard(unittest.TestCase):
+    """ Test for the board management.
+    """
+
+    def testConvCoord(self):
+        """ Test the conversion in the coordinates system.
+        """
+
+        self.assertEqual((1,2),game.Game.conv_coord("ab"))
+        self.assertEqual((1,1),game.Game.conv_coord("aa"))
+
+    def test_createTree(self):
+
+        def fun(pos):
+            return pos
+
+        with open("python/tests/test.sgf") as f:
+            cursor = sgfparser.Cursor(f.read())
+
+        cursor.next()
+
+        expected_tee = \
+            [{'AB': ['or', 'pr', 'qr', 'rr', 'sr'], 'AW': ['ro', 'nq', 'oq', 'pq', 'qq', 'rq', 'mr']},
+                [
+                    [{'W': ['os']},
+                        [
+                            [{'B': ['ps']}, {'W': ['rs']}, {'B': ['ns']}, {'W': ['nr']}],
+                            [{'B': ['ns']}, {'W': ['nr']}, {'B': ['rs']}, {'W': ['ps']}, {'B': ['qs']}, {'W': ['os']}]
+                        ]
+                    ],
+                    [{'W': ['rs']},
+                        [
+                            [{'B': ['os']}, {'W': ['qs']}],
+                            [{'B': ['qs']}, {'W': ['os']}, {'B': ['nr']}, {'W': ['ns']}]
+                        ]
+                    ]
+                ]
+            ]
+        self.assertEqual(expected_tee, game.Game.create_tree(cursor, fun))
+
+    def test_init(self):
+
+        def fun(pos):
+            return (0,0)
+        currentgame = create_board("test.sgf")
+
+        self.assertEqual(11, currentgame.min_x)
+        self.assertEqual(13, currentgame.min_y)
+        self.assertEqual(19, currentgame.max_x)
+        self.assertEqual(19, currentgame.max_y)
+
+        self.assertEqual((9, 7), currentgame.get_size())
+
+        # There is only 2 state : initial board, and 2 possibilities.
+        self.assertEqual(2, len(currentgame.tree))
+
+        self.assertTrue(currentgame.side['TOP'])
+        self.assertTrue(currentgame.side['LEFT'])
+        self.assertFalse(currentgame.side['BOTTOM'])
+        self.assertFalse(currentgame.side['RIGHT'])
+
+        currentgame.normalize()
+
+    def test_level2(self):
+
+        def fun(pos):
+            return (0,0)
+        currentgame = create_board("test2.sgf")
+
+        print(currentgame.tree)
+
+        currentgame.normalize()
+
+        self.assertEqual(11, currentgame.min_x)
+        self.assertEqual(13, currentgame.min_y)
+        self.assertEqual(19, currentgame.max_x)
+        self.assertEqual(19, currentgame.max_y)
+
+        self.assertEqual((9, 7), currentgame.get_size())
+
+        # There is only 2 state : initial board, and 2 possibilities.
+        self.assertEqual(2, len(currentgame.tree))
+
+        self.assertTrue(currentgame.side['TOP'])
+        self.assertTrue(currentgame.side['LEFT'])
+        self.assertFalse(currentgame.side['BOTTOM'])
+        self.assertFalse(currentgame.side['RIGHT'])
+
+        currentgame.normalize()
diff --git a/qml/python/tests/test_transformations.py b/qml/python/tests/test_transformations.py
new file mode 100644
index 0000000..b802b9f
--- /dev/null
+++ b/qml/python/tests/test_transformations.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import unittest
+
+from python.transformations import Rotation, Translation, Symmetry
+
+class FakeBoard():
+
+    def __init__(self, min_x, min_y, max_x, max_y):
+        self.min_x = min_x
+        self.min_y = min_y
+        self.max_x = max_x
+        self.max_y = max_y
+
+    def get_size(self):
+        x_size = self.max_x - self.min_x
+        y_size = self.max_y - self.min_y
+        return min(19, x_size), min(19, y_size)
+
+class TestRotation(unittest.TestCase):
+    """ Test the rotation transformation
+    """
+
+    def test_apply_points(self):
+        """ Test the points rotation.
+        """
+        rotation = Rotation(FakeBoard(2, 1, 8, 5))
+        self.assertEqual((4, 2), rotation.apply_points((2, 1)))
+
+        rotation = Rotation(FakeBoard(0, 0, 6, 4))
+        self.assertEqual((4, 0), rotation.apply_points((0, 0)))
+        self.assertEqual((4, 3), rotation.apply_points((3, 0)))
+        self.assertEqual((1, 0), rotation.apply_points((0, 3)))
+        self.assertEqual((1, 3), rotation.apply_points((3, 3)))
+
+        rotation = Rotation(FakeBoard(1, 1, 6, 4))
+        self.assertEqual((4, 1), rotation.apply_points((1, 0)))
+
+
+
+    def test_get_new_size(self):
+        """ Test the goban size rotation.
+        """
+        rotation = Rotation(FakeBoard(2, 1, 8, 5))
+        self.assertEqual((0, 2, 4, 8), rotation.get_new_size())
+
+        rotation = Rotation(FakeBoard(0, 0, 6, 4))
+        self.assertEqual((0, 0, 4, 6), rotation.get_new_size())
+
+    def test_is_valid(self):
+        """ Test the is_valid method.
+        """
+
+        # Do not rotate if height > width
+        rotation = Rotation(FakeBoard(0, 0, 6, 4))
+        self.assertTrue(rotation.is_valid())
+
+        # Always rotate if width > height
+        rotation = Rotation(FakeBoard(0, 0, 4, 6))
+        self.assertFalse(rotation.is_valid())
+
+        # May rotate if width = height (not tested here…)
+
+class TestTranslation(unittest.TestCase):
+    """ Test the translation transformation.
+    """
+
+    def test_apply_points(self):
+        """ Test the points translation.
+        """
+        translation = Translation(FakeBoard(2, 1, 8, 5))
+        self.assertEqual((0, 0), translation.apply_points((2, 1)))
+        self.assertEqual((6, 4), translation.apply_points((8, 5)))
+
+    def test_get_new_size(self):
+        """ Test the goban size translation.
+        """
+        translation = Translation(FakeBoard(2, 1, 8, 5))
+
+        self.assertEqual((0, 0, 6, 4), translation.get_new_size())
+
+    def test_is_valid(self):
+        """ Test the is_valid method.
+        """
+
+        translation = Translation(FakeBoard(1, 0, 6, 4))
+        self.assertTrue(translation.is_valid())
+
+        translation = Translation(FakeBoard(0, 1, 6, 4))
+        self.assertTrue(translation.is_valid())
+
+        translation = Translation(FakeBoard(0, 0, 4, 6))
+        self.assertFalse(translation.is_valid())
+
+class TestRotation(unittest.TestCase):
+    """ Test the simetry transformation.
+    """
+
+    def test_apply_points(self):
+        """ Test the points Symmetry.
+        """
+        symmetry = Symmetry(FakeBoard(2, 1, 8, 5))
+        symmetry.x_flip = True
+        symmetry.y_flip = False
+        self.assertEqual((8, 1), symmetry.apply_points((2, 1)))
+        self.assertEqual((2, 5), symmetry.apply_points((8, 5)))
+
+        symmetry.x_flip = False
+        symmetry.y_flip = True
+        self.assertEqual((2, 5), symmetry.apply_points((2, 1)))
+        self.assertEqual((8, 1), symmetry.apply_points((8, 5)))
+
+    def test_get_new_size(self):
+        """ Test the goban size Symmetry.
+        """
+        symmetry = Symmetry(FakeBoard(2, 1, 8, 5))
+
+        self.assertEqual((2, 1, 8, 5), symmetry.get_new_size())
+
+    def test_is_valid(self):
+        """ Test the is_valid method.
+        """
+
+        symmetry = Symmetry(FakeBoard(1, 0, 6, 4))
+        symmetry.x_flip = True
+        self.assertTrue(symmetry.is_valid())
+
+
+        symmetry = Symmetry(FakeBoard(0, 0, 4, 6))
+        symmetry.x_flip = False
+        symmetry.y_flip = False
+        self.assertFalse(symmetry.is_valid())
+
diff --git a/qml/python/transformations.py b/qml/python/transformations.py
new file mode 100644
index 0000000..380c30c
--- /dev/null
+++ b/qml/python/transformations.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import random
+random.seed()
+
+class Translation(object):
+    """ A translation transformation.
+    """
+
+    def __init__(self, board):
+        """ Create a new translation from the board.
+        """
+        self.board = board
+
+    def is_valid(self):
+        """ Apply if a translation if the goban is not maximized.
+        """
+        return self.board.min_x != 0 or self.board.min_y != 0
+
+    def apply_points(self, coord):
+        """ Move the points to the lower position.
+        """
+        x, y = coord
+        return (x - self.board.min_x, y - self.board.min_y)
+
+    def get_new_size(self):
+        """ The goban size does not change.
+        """
+        return 0, 0, self.board.max_x - self.board.min_x, self.board.max_y - self.board.min_y
+
+    def get_new_side(self):
+        """ There is no changes on the sides.
+        """
+        return self.board.side
+
+class Rotation(object):
+    """ A rotation transformation.
+    """
+
+    def __init__(self, board):
+        """ Create a new roation from the board.
+        """
+        self.board = board
+
+    def is_valid(self):
+        """ Apply the rotation in following cases :
+        * if the board is widther than heigther
+        * randomly is width == heigth
+        * never if the board is heigther than widther
+        """
+
+        width, heigth = self.board.get_size()
+        should = heigth - width
+        if should == 0:
+            return random.randint(0, 1) == 1
+        return should < 0
+
+    def apply_points(self, coord):
+        """ Apply the transformation on a point.
+        """
+        x, y = coord
+        return (self.board.max_y - y, x)
+
+    def get_new_size(self):
+        """ Apply the transformation on the goban size.
+        """
+        max_x = self.board.max_y - self.board.min_y
+        return 0, self.board.min_x, max_x, self.board.max_x
+
+    def get_new_side(self):
+        """ Apply the transformations on the sides.
+        """
+        return {
+            "TOP":   self.board.side["RIGHT"],
+            "LEFT":  self.board.side["TOP"],
+            "RIGHT": self.board.side["BOTTOM"],
+            "BOTTOM":self.board.side["LEFT"]
+        }
+
+class Symmetry(object):
+    """ A translation transformation.
+    """
+
+    def __init__(self, board):
+        self.board = board
+
+        self.x_flip = random.randint(0, 1) == 1
+        self.y_flip = random.randint(0, 1) == 1
+
+    def is_valid(self):
+        """ The transformation is valid if one flip is required in one
+        direction.  """
+        return self.x_flip or self.y_flip
+
+    def apply_points(self, coord):
+        """ Flip in both directions.
+        """
+        x, y = coord
+        if self.x_flip:
+            new_x = self.board.max_x - x + self.board.min_x
+        else:
+            new_x = x
+
+        if self.y_flip:
+            new_y = self.board.max_y - y + self.board.min_y
+        else:
+            new_y = y
+
+        return (new_x, new_y)
+
+    def get_new_size(self):
+        """ The size is not changed.
+        """
+        return self.board.min_x, self.board.min_y, self.board.max_x, self.board.max_y
+
+    def get_new_side(self):
+        """ There is no changes on the sides.
+        """
+        return {
+            "TOP":   self.board.side["BOTTOM" if self.y_flip else "TOP"],
+            "LEFT":  self.board.side["RIGHT" if self.x_flip else "LEFT"],
+            "RIGHT": self.board.side["LEFT" if self.x_flip else "RIGHT"],
+            "BOTTOM":self.board.side["TOP" if self.y_flip else "BOTTOM"]
+        }
-- 
cgit v1.2.3