Commits

Sergey Astanin  committed 7e067f1

uligo 0.3

  • Participants

Comments (0)

Files changed (41)

+# file: board1.py
+
+##   This file is part of Kombilo, a go database program
+##   It contains classes implementing an abstract go board and a go
+##   board displayed on the screen.
+
+##   Copyright (C) 2001-3 Ulrich Goertz (u@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
+
+
+from Tkinter import *
+from whrandom import randint
+import math
+import sys
+import os
+
+try:   # PIL installed?
+    import GifImagePlugin
+    import Image
+    import ImageTk
+    import ImageEnhance
+    PILinstalled = 1
+except:
+    PILinstalled = 0
+
+
+class abstractBoard:
+    """ This class administrates a go board.
+        It keeps track of the stones currently on the board in the dictionary self.status,
+        and of the moves played so far in self.undostack
+
+        It has methods to clear the board, play a stone, undo a move. """
+
+    def __init__(self, boardSize = 19):
+        self.status = {}
+        self.undostack = []
+        self.boardSize = boardSize
+
+    def neighbors(self,x):
+        """ Returns the coordinates of the 4 (resp. 3 resp. 2 at the side / in the corner) intersections
+            adjacent to the given one. """
+        if   x[0]== 1              :     l0 = [2]
+        elif x[0]== self.boardSize :     l0 = [self.boardSize-1]
+        else:                            l0 = [x[0]-1, x[0]+1]
+
+        if   x[1]== 1              :     l1 = [2]
+        elif x[1]== self.boardSize :     l1 = [self.boardSize-1]
+        else:                            l1 = [x[1]-1, x[1]+1]
+
+        l = []
+        for i in l0: l.append((i,x[1]))
+        for j in l1: l.append((x[0],j))
+
+        return l
+
+    def clear(self):
+        """ Clear the board """
+        self.status = {}
+        self.undostack=[]        
+
+    def play(self,pos,color):
+        """ This plays a color=black/white stone at pos, if that is a legal move
+            (disregarding ko), and deletes stones captured by that move.
+            It returns 1 if the move has been played, 0 if not. """
+
+        if self.status.has_key(pos):                # check if empty
+            return 0
+
+        l = self.legal(pos,color)
+        if l:                                       # legal move?
+            captures = l[1]
+            for x in captures: del self.status[x]   # remove captured stones, if any
+            self.undostack.append((pos,color,captures))   # remember move + captured stones for easy undo
+            return 1
+        else: return 0
+
+    def legal(self, pos, color):
+        """ Check if a play by color at pos would be a legal move. """
+        c = [] # captured stones
+        for x in self.neighbors(pos):
+            if self.status.has_key(x) and self.status[x]==self.invert(color):
+                c = c + self.hasNoLibExcP(x, pos)        
+
+        self.status[pos]=color
+
+        if c:
+            captures = []
+            for x in c:
+                if not x in captures: captures.append(x)
+            return (1, captures)
+
+        if self.hasNoLibExcP(pos):
+            del self.status[pos]
+            return 0
+        else: return (1, [])
+
+    def hasNoLibExcP(self, pos, exc = None):
+        """ This function checks if the string (=solidly connected) of stones containing
+            the stone at pos has a liberty (resp. has a liberty besides that at exc).
+            If no liberties are found, a list of all stones in the string is returned.
+
+            The algorithm is a non-recursive  implementation of a simple flood-filling:
+            starting from the stone at pos, the main while-loop looks at the intersections
+            directly adjacent to the stones found so far, for liberties or other stones that belong
+            to the string. Then it looks at the neighbors of those newly found stones, and so
+            on, until it finds a liberty, or until it doesn't find any new stones belonging
+            to the string, which means that there are no liberties.
+            Once a liberty is found, the function returns immediately. """
+            
+        st = []            # in the end, this list will contain all stones solidly connected to the
+                           # one at pos, if this string has no liberties
+        newlyFound = [pos] # in the while loop, we will look at the neighbors of stones in newlyFound
+        foundNew = 1
+        
+        while foundNew:
+            foundNew = 0
+            n = []         # this will contain the stones found in this iteration of the loop
+            for x in newlyFound:
+                for y in self.neighbors(x):
+                    if not self.status.has_key(y) and y != exc:    # found a liberty
+                        return []
+                    elif self.status.has_key(y) and self.status[y]==self.status[x] \
+                         and not y in st and not y in newlyFound: # found another stone of same color
+                        n.append(y)
+                        foundNew = 1
+
+            st[:0] = newlyFound
+            newlyFound = n
+
+        return st     # no liberties found, return list of all stones connected to the original one
+
+    def undo(self, no=1):
+        """ Undo the last no moves. """
+        for i in range(no):
+            if self.undostack:
+                pos, color, captures = self.undostack.pop()
+                del self.status[pos]
+                for p in captures: self.status[p] = self.invert(color)
+
+    def remove(self, pos):
+        """ Remove a stone form the board, and store this action in undostack. """
+        
+        self.undostack.append(((-1,-1), self.invert(self.status[pos]), [pos]))
+        del self.status[pos]
+
+    def invert(self,color):
+        if color == 'B': return 'W'
+        else: return 'B'
+
+
+class Board(abstractBoard, Canvas):
+    """ This is a go board, displayed on the associated canvas.
+        canvasSize is a pair, the first entry is the size of the border, the second
+        entry is the distance between two go board lines, both in pixels.
+
+        The most important methods are:
+
+        - play: play a stone of some color at some position (if that is a legal move)
+        - undo: undo one (or several) moves
+        - state: activate (state("normal", f) - the function f is called when a stone
+                 is placed on the board) or disable (state("disabled")) the board;
+
+        - placeMark: place a colored label (slightly smaller than a stone) at some position
+        - delMarks: delete all these labels
+        - placeLabel: place a label (a letter, a circle, square, triangle or cross)
+    """
+
+    def __init__(self, master, boardSize = 19, canvasSize = (30,25), fuzzy=1, labelFont = None,
+                 focus=1, callOnChange=None):
+
+        self.focus = focus
+        self.coordinates = 0
+        
+        self.canvasSize = canvasSize
+        size = 2*canvasSize[0] + (boardSize-1)*canvasSize[1] # size of the canvas in pixel
+        Canvas.__init__(self, master, height = size, width =  size, highlightthickness = 0)
+
+        abstractBoard.__init__(self, boardSize)
+
+        self.changed = IntVar()  # this is set to 1 whenever a change occurs (placing stone, label etc.)
+        self.changed.set(0)      # this is used for Kombilo's 'back' method 
+
+        if callOnChange: self.callOnChange = callOnChange
+        else: self.callOnChange = lambda: None
+        self.noChanges = 0
+
+        self.fuzzy = IntVar()   # if self.fuzzy is true, the stones are not placed precisely
+        self.fuzzy.set(fuzzy)   # on the intersections, but randomly a pixel off
+
+        if labelFont:
+            self.labelFont = labelFont
+        else:
+            self.labelFont = (StringVar(), IntVar(), StringVar())
+            self.labelFont[0].set('Helvetica')
+            self.labelFont[1].set(5)
+            self.labelFont[2].set('bold')
+            
+        self.shadedStoneVar = IntVar()  # if this is true, there is a 'mouse pointer' showing
+        self.shadedStonePos = (-1,-1)   # where the next stone would be played, given the current
+                                        # mouse position
+
+        self.currentColor = 'B'     # the expected color of the next move
+
+        self.stones = {}            # references to the ovals placed on the canvas, used for removing stones
+        self.marks = {}             # references to the (colored) marks on the canvas
+        self.labels = {}
+        
+        self.bind("<Configure>", self.resize)
+        self.resizable = 1
+
+        global PILinstalled
+        gifpath = os.path.join(sys.path[0],'gifs')
+        
+        try:
+            self.img = PhotoImage(file=os.path.join(gifpath, 'board.gif'))
+        except TclError:
+            self.img = None
+
+        self.use3Dstones = IntVar()
+        self.use3Dstones.set(1)
+
+        if PILinstalled:
+            try:
+                self.blackStone = Image.open(os.path.join(gifpath, 'black.gif'))
+                self.whiteStone = Image.open(os.path.join(gifpath, 'white.gif'))
+            except IOError:
+                PILinstalled = 0
+
+        self.drawBoard()
+
+
+    def drawBoard(self):
+        """ Displays the background picture, and draws the lines and hoshi points of
+            the go board.
+            If PIL is installed, this also creates the PhotImages for black, white stones. """
+
+        self.delete('non-bg')     # delete everything except for background image
+        c0, c1 = self.canvasSize
+        size = 2*c0 + (self.boardSize-1)*c1
+        self.config(height=size, width=size)
+        
+        if self.img:
+            self.delete('board')
+            for i in range(size/100 + 1):
+                for j in range(size/100 + 1):
+                    self.create_image(100*i,100*j,image=self.img, tags='board')
+
+        color = 'black'
+
+        for i in range(self.boardSize):
+	    self.create_line(c0, c0 + c1*i, c0 + (self.boardSize-1)*c1, c0 + c1*i, fill=color, tags='non-bg')
+	    self.create_line(c0 + c1*i, c0, c0 + c1*i, c0+(self.boardSize-1)*c1, fill=color, tags='non-bg')
+
+        # draw hoshi's:
+
+        if c1 > 7:
+
+            if self.boardSize in [13,19]:
+                b = (self.boardSize-7)/2
+                for i in range(3):
+                    for j in range(3): 
+                        self.create_oval(c0 + (b*i+3)*c1 - 2, c0 + (b*j+3)*c1 - 2,
+                                         c0 + (b*i+3)*c1 + 2, c0 + (b*j+3)*c1 + 2, fill = 'black', tags='non-bg')
+            elif self.boardSize == 9:
+                self.create_oval(c0 + 4*c1 - 2, c0 + 4*c1 - 2,
+                                 c0 + 4*c1 + 2, c0 + 4*c1 + 2, fill = 'black', tags='non-bg')
+
+        # draw coordinates:
+
+        if self.coordinates:
+            for i in range(self.boardSize):
+                a = 'ABCDEFGHJKLMNOPQRST'[i]
+                self.create_text(c0 + c1*i, c1*self.boardSize+3*c0/4+4, text=a,
+                                 font = ('Helvetica', 5+c1/7, 'bold'))
+                self.create_text(c0 + c1*i, c0/4+1, text=a, font = ('Helvetica', 5+c1/7, 'bold'))
+                self.create_text(c0/4+1, c0+c1*i, text=`self.boardSize-i`,font = ('Helvetica', 5+c1/7, 'bold'))
+                self.create_text(c1*self.boardSize+3*c0/4+4, c0 + c1*i, text=`self.boardSize-i`, font = ('Helvetica', 5+c1/7, 'bold'))
+                
+
+
+        global PILinstalled
+        if PILinstalled:
+            try:
+                self.bStone = ImageTk.PhotoImage(self.blackStone.resize((c1,c1)))
+                self.wStone = ImageTk.PhotoImage(self.whiteStone.resize((c1,c1)))
+            except:
+                PILinstalled = 0
+
+
+
+
+    def resize(self, event = None):
+        """ This is called when the window containing the board is resized. """
+
+        if not self.resizable: return
+
+        self.noChanges = 1
+
+        if event: w, h = event.width, event.height
+        else:     w, h = int(self.cget('width')), int(self.cget('height'))
+        m = min(w,h)
+
+        self.canvasSize = (m/20 + 4, (m - 2*(m/20+4))/(self.boardSize-1))
+
+        self.drawBoard()
+            
+
+        # place a gray rectangle over the board background picture
+        # in order to make the board quadratic
+
+        self.create_rectangle(h+1, 0, h+1000, w+1000,
+                              fill ='grey88', outline='', tags='non-bg')     
+        self.create_rectangle(0, w+1, h+1000, w+1000,
+                              fill='grey88', outline='', tags='non-bg')
+
+            
+        for x in self.status.keys(): self.placeStone(x, self.status[x])
+        for x in self.marks.keys(): self.placeMark(x, self.marks[x])
+        for x in self.labels.keys(): self.placeLabel(x, '+'+self.labels[x][0], self.labels[x][1])
+
+        self.tkraise('sel') # this is for the list of previous search patterns ...
+
+        self.noChanges = 0
+
+
+    def play(self, pos, color=None):
+        """ Play a stone of color (default is self.currentColor) at pos. """
+
+        if color is None: color = self.currentColor
+        if abstractBoard.play(self, pos, color):                    # legal move?
+            captures = self.undostack[len(self.undostack)-1][2]     # retrieve list of captured stones
+            for x in captures:
+                self.delete(self.stones[x])
+                del self.stones[x]
+            self.placeStone(pos, color)
+            self.currentColor = self.invert(color)
+            self.delShadedStone()
+            return 1
+        else: return 0
+
+    def state(self, s, f=None):
+        """ s in "normal", "disabled": accepting moves or not
+            f the function to call if a move is entered 
+            [More elegant solution might be to replace this by an overloaded bind method,
+            for some event "Move"?!]  """
+
+        if s == "normal":
+            self.callOnMove = f
+            self.bound1 = self.bind("<Button-1>", self.onMove)  
+            self.boundm = self.bind("<Motion>", self.shadedStone)
+            self.boundl = self.bind("<Leave>", self.delShadedStone)
+        elif s == "disabled":
+            self.delShadedStone()
+            try:
+                self.unbind("<Button-1>", self.bound1)
+                self.unbind("<Motion>", self.boundm)
+                self.unbind("<Leave>", self.boundl)
+            except (TclError, AttributeError): pass                     # if board was already disabled, unbind will fail
+            
+    def onMove(self, event):
+        # compute board coordinates from the pixel coordinates of the mouse click
+
+        if self.focus:
+            self.master.focus()
+        x,y = self.getBoardCoord((event.x, event.y), self.shadedStoneVar.get())
+        if (not x*y): return
+
+        if abstractBoard.play(self,(x,y), self.currentColor): # would this be a legal move?
+            abstractBoard.undo(self)
+            self.callOnMove((x,y))
+
+    def onChange(self):
+        if self.noChanges: return
+        self.callOnChange()
+        self.changed.set(1)
+
+
+    def getPixelCoord(self, pos, nonfuzzy = 0):
+        """ transform go board coordinates into pixel coord. on the canvas of size canvSize """
+
+        fuzzy1 = randint(-1,1) * self.fuzzy.get() * (1-nonfuzzy)
+        fuzzy2 = randint(-1,1) * self.fuzzy.get() * (1-nonfuzzy)
+        c1 = self.canvasSize[1]
+        a = self.canvasSize[0] - self.canvasSize[1] - self.canvasSize[1]/2 
+        b = self.canvasSize[0] - self.canvasSize[1] + self.canvasSize[1]/2 
+        return (c1*pos[0]+a+fuzzy1, c1*pos[1]+a+fuzzy2, c1*pos[0]+b+fuzzy1, c1*pos[1]+b+fuzzy2) 
+
+    def getBoardCoord(self, pos, sloppy=1):
+        """ transform pixel coordinates on canvas into go board coord. in [1,..,boardSize]x[1,..,boardSize]
+            sloppy refers to how far the pixel may be from the intersection in order to
+            be accepted """
+
+        if sloppy: a, b = self.canvasSize[0]-self.canvasSize[1]/2, self.canvasSize[1]-1
+        else:      a, b = self.canvasSize[0]-self.canvasSize[1]/4, self.canvasSize[1]/2
+
+        if (pos[0]-a)%self.canvasSize[1] <= b: x = (pos[0]-a)/self.canvasSize[1] + 1
+        else:                                  x = 0
+        
+        if (pos[1]-a)%self.canvasSize[1] <= b: y = (pos[1]-a)/self.canvasSize[1] + 1
+        else:                  y = 0
+
+        if x<0 or y<0 or x>self.boardSize or y>self.boardSize: x = y = 0
+
+        return (x,y)    
+
+    def placeMark(self, pos, color):
+        """ Place colored mark at pos. """
+        x1, x2, y1, y2 = self.getPixelCoord(pos, 1)
+        self.create_oval(x1+2, x2+2, y1-2, y2-2, fill = color, tags=('marks','non-bg'))
+        self.marks[pos]=color
+        self.onChange()
+
+    def delMarks(self):
+        """ Delete all marks. """
+        if self.marks: self.onChange()
+        self.marks = {}
+        self.delete('marks')
+
+    def delLabels(self):
+        """ Delete all labels. """
+        if self.labels: self.onChange()
+        self.labels={}
+        self.delete('label')
+
+    def remove(self, pos):
+        """ Remove the stone at pos, append this as capture to undostack. """
+        if self.status.has_key(pos):
+            self.onChange()
+            self.delete(self.stones[pos])
+            del self.stones[pos]
+            abstractBoard.remove(self, pos)
+            self.update_idletasks()
+            return 1
+        else: return 0
+
+    def placeLabel(self, pos, type, text=None, color=None, override=None):
+        """ Place label of type type at pos; used to display labels
+            from SGF files. If type has the form +XX, add a label of type XX.
+            Otherwise, add or delete the label, depending on if there is no label at pos,
+            or if there is one."""
+
+        if type[0] != '+':
+
+            if self.labels.has_key(pos):
+                if self.labels[pos][0] == type:
+                    for item in self.labels[pos][2]: self.delete(item)
+                    del self.labels[pos]
+                    return
+                else:
+                    for item in self.labels[pos][2]: self.delete(item)
+                    del self.labels[pos]
+
+            self.onChange()
+
+        else: type = type[1:]
+
+        labelIDs = []
+
+        if override:
+            fcolor = override[0]
+            fcolor2 = override[1]
+        elif self.status.has_key(pos) and self.status[pos]=='B':
+            fcolor = 'white'
+            fcolor2 = '#FFBA59'
+        elif self.status.has_key(pos) and self.status[pos]=='W':
+            fcolor = 'black'
+            fcolor2 = ''
+        else:
+            fcolor = color or 'black'
+            fcolor2 = '#FFBA59'
+                    
+        x1, x2, y1, y2 = self.getPixelCoord(pos, 1)
+        if type == 'LB':
+            labelIDs.append(self.create_oval(x1+3, x2+3, y1-3, y2-3, fill=fcolor2, outline='',
+                                             tags=('label', 'non-bg')))
+            labelIDs.append(self.create_text((x1+y1)/2,(x2+y2)/2, text=text, fill=fcolor,
+                                             font = (self.labelFont[0].get(), self.labelFont[1].get() + self.canvasSize[1]/5,
+                                                     self.labelFont[2].get()),
+                                             tags=('label', 'non-bg')))
+        elif type == 'SQ':
+            labelIDs.append(self.create_rectangle(x1+6, x2+6, y1-6, y2-6, fill = fcolor, tags=('label','non-bg')))
+        elif type == 'CR':
+            labelIDs.append(self.create_oval(x1+5, x2+5, y1-5, y2-5, fill='', outline=fcolor, tags=('label','non-bg')))
+        elif type == 'TR':
+            labelIDs.append(self.create_polygon((x1+y1)/2, x2+5, x1+5, y2-5, y1-5, y2-5, fill = fcolor,
+                                                tags = ('label', 'non-bg')))
+        elif type == 'MA':
+            labelIDs.append(self.create_oval(x1+2, x2+2, y1-2, y2-2, fill=fcolor2, outline='',
+                             tags=('label', 'non-bg')))
+            labelIDs.append(self.create_text(x1+12,x2+12, text='X', fill=fcolor,
+                                             font = (self.labelFont[0].get(), self.labelFont[1].get() + 1 + self.canvasSize[1]/5,
+                                                     self.labelFont[2].get()),
+                                             tags=('label', 'non-bg')))
+            
+        self.labels[pos] = (type, text, labelIDs)
+
+
+            
+    def placeStone(self, pos, color):
+        self.onChange()
+        p = self.getPixelCoord(pos)
+        if not self.use3Dstones.get() or not PILinstalled or self.canvasSize[1] <= 7:
+            if color=='B':
+                self.stones[pos] = self.create_oval(p, fill='black', tags='non-bg')
+            elif color=='W':
+                self.stones[pos] = self.create_oval(p, fill='white', tags='non-bg')
+        else:
+            if color=='B': self.stones[pos] = self.create_image(((p[0]+p[2])/2, (p[1]+p[3])/2),
+                                                                image=self.bStone, tags='non-bg')
+            elif color=='W': self.stones[pos] = self.create_image(((p[0]+p[2])/2, (p[1]+p[3])/2),
+                                                                  image=self.wStone, tags='non-bg')
+            
+    def undo(self, no=1, changeCurrentColor=1):
+        """ Undo the last no moves. """
+
+        for i in range(no):
+            if self.undostack:
+                self.onChange()
+                pos, color, captures = self.undostack.pop()
+                if self.status.has_key(pos):
+                    del self.status[pos]
+                    self.delete(self.stones[pos])
+                    del self.stones[pos]
+                for p in captures:
+                    self.placeStone(p, self.invert(color)) 
+                    self.status[p] = self.invert(color)
+                # self.update_idletasks()
+                if changeCurrentColor:
+                    self.currentColor = self.invert(self.currentColor)
+
+    def clear(self):
+        """ Clear the board. """
+        abstractBoard.clear(self)
+        for x in self.stones.keys():
+            self.delete(self.stones[x])
+        self.stones = {}
+        self.onChange()
+
+    def ptOnCircle(self, size, degree):
+        radPerDeg = math.pi/180
+        r = size/2
+        x = int(r*math.cos((degree-90)*radPerDeg) + r)
+        y = int(r*math.sin((degree-90)*radPerDeg) + r)
+        return (x,y)
+
+    def shadedStone(self, event):
+        x, y = self.getBoardCoord((event.x, event.y), 1)
+        if (x,y) == self.shadedStonePos: return     # nothing changed
+
+        self.delShadedStone()
+        if self.currentColor == 'B': color = 'black'
+        else: color = 'white'
+
+        if (x*y) and self.shadedStoneVar.get() and abstractBoard.play(self, (x,y), self.currentColor):
+            abstractBoard.undo(self)
+
+            if sys.platform[:3]=='win':     # 'stipple' is ignored under windows for
+                                            # create_oval, so we'll draw a polygon ...
+                l = self.getPixelCoord((x,y),1)
+                m = []
+
+                for i in range(18):
+                    help = self.ptOnCircle(l[2]-l[0], i*360/18)
+                    m.append(help[0]+l[0])
+                    m.append(help[1]+l[1])
+                 
+                self.create_polygon(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9],
+                                    m[10], m[11], m[12], m[13], m[14], m[15], m[16], m[17],
+                                    m[18], m[19], m[20], m[21], m[22], m[23], m[24], m[25],
+                                    m[26], m[27], m[28], m[29], m[30], m[31], m[32], m[33],
+                                    m[34], m[35],
+                                    fill=color, stipple='gray50',
+                                    outline='', tags=('shaded','non-bg') )
+            else:
+                self.create_oval(self.getPixelCoord((x,y), 1), fill=color, stipple='gray50',
+                                 outline='', tags=('shaded','non-bg')) 
+
+            self.shadedStonePos = (x,y)
+    
+    def delShadedStone(self, event=None):
+        self.delete('shaded')
+        self.shadedStonePos = (-1,-1)
+
+    def fuzzyStones(self):
+        """ switch fuzzy/non-fuzzy stone placement according to self.fuzzy """
+        for p in self.status.keys():
+            self.delete(self.stones[p])
+            del self.stones[p]
+            self.placeStone(p, self.status[p])
+        self.tkraise('marks')
+        self.tkraise('label')
+# file: clock.py
+
+##   This file is part of uliGo, a program for exercising go problems.
+##   It contains a class that implements a simple stop watch.
+
+##   Copyright (C) 2001-3 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
+
+from Tkinter import *
+import time
+import math
+import os
+
+class Clock(Canvas):
+    """ This is a simple stop watch displayed on a Tkinter canvas.
+        It is given a size (diameter in pixel), a maxTime (this is how long it will take the hand
+        to go round the clock once; after that the clock stops, the clock's face becomes red and
+        'time is over'), and a function that is called when time is over.
+        The clock is started with Clock.start; it can be stopped with the Clock.stop function
+        (returns the elapsed time) and reset with Clock.reset.
+        The maxTime can be changed with the changeMaxTime function; it makes a new window with
+        a scale pop up, where the time in seconds can be chosen.
+        When the clock is running, the system clock is checked every 100 milliseconds, but the
+        clock hand is updated only once a second.
+        """
+    
+    def __init__(self, master, maxTime=0, f=None):
+        Canvas.__init__(self, master, height=120, width=120, highlightthickness=0)
+        self.size = 100
+        self.offset = 10
+        self.hand = None
+        gifpath = os.path.join(sys.path[0],'gifs')
+        
+        try:
+            self.img = PhotoImage(file=os.path.join(gifpath, 'clock.gif'))
+            self.create_image(60,60, image=self.img)
+        except TclError:
+            pass
+
+        self.drawClockface()
+        self.currentTime = None
+        self.running = 0
+        self.maxTime = IntVar()
+        self.maxTime.set(maxTime)
+        self.callOnMaxtime = f
+        self.bind("<Button-3>", self.changeMaxtime)
+        self.red = 0
+        
+        
+    
+    def start(self):
+        """ Start the clock. """
+        if not self.running and self.maxTime.get():
+            self.reset()
+            self.tick = 360.0/self.maxTime.get()
+   
+            self.running = 1
+            self.elapsedTime = 0
+
+            self.currentTime = time.localtime(time.time())[5]
+            self.updateClock()
+
+
+    def stop(self):
+        """ Stop the clock. """
+        if self.running:
+            self.running = 0
+            return self.elapsedTime
+
+
+    def reset(self):
+        """ Reset the hand."""
+        if not self.running:
+            self.drawHand()
+            if self.red: self.delete(self.red)
+
+    def updateClock(self):
+        """ This function is called every 100 milliseconds when the clock is running (first
+            by start(), and then by self.after(). If the time (as given by time.time())
+            jumped to a new second, the hand is drawn in its new position. """
+        
+        if self.running:
+            s = time.localtime(time.time())[5]
+            if s != self.currentTime:                 # second jumped
+                if s-self.currentTime<0 :
+                    self.elapsedTime = self.elapsedTime + s - self.currentTime + 60
+                else:
+                    self.elapsedTime = self.elapsedTime + s - self.currentTime
+                self.currentTime = s
+                
+                if self.maxTime.get() and self.elapsedTime >= self.maxTime.get():   # time over?
+                    self.drawHand()
+                    self.running = 0
+                    self.red = self.create_oval(1+self.offset,1*self.offset,
+                                                self.size+self.offset-1,self.size+self.offset-1, fill="red")
+                    self.tkraise('fg')
+                    self.callOnMaxtime()
+                    return
+                self.drawHand(self.elapsedTime*self.tick)
+                
+            self.after(100, self.updateClock)             # next update after 100 ms
+            
+
+    def changeMaxtime(self, event=None):
+        """ Change the maxTime; makes a new window with a scale where the new maxTime can be chosen.
+            Does not work while the clock is running. """
+        if not self.running:
+            window = Toplevel()
+            window.title("Change time settings")
+            sc = Scale(window, label='Pick time in seconds (0=off)', length=300,
+                       variable = self.maxTime, from_=0, to = 480,
+                       tickinterval = 60, showvalue=YES, orient='horizontal')
+            sc.pack()
+            b = Button(window, text="OK", command = window.destroy)
+            b.pack(side=RIGHT)
+            window.update_idletasks()  # grab_set won't work without that
+            window.focus()
+            window.grab_set()
+            window.wait_window()
+         
+
+    def drawClockface(self):
+        for i in range(12):
+            (x,y) = self.ptOnCircle(i*360/12)
+            self.create_oval(self.offset+x-2,self.offset+y-2,self.offset+x+2,self.offset+y+2,
+                             fill="blue", outline ="", tags='fg')
+        self.create_oval(self.offset+self.size/2-3, self.offset+self.size/2-3,
+                         self.offset+self.size/2+3, self.offset+self.size/2+3, fill="black", tags='fg')
+        self.drawHand()
+
+        
+    def drawHand(self, degree=0):
+        x,y = self.ptOnCircle(degree)
+        if self.hand: self.delete(self.hand)
+        self.hand = self.create_line(self.offset+self.size/2, self.offset+self.size/2,
+                                     self.offset+x, self.offset+y, fill="black", width=2, tags='fg')
+
+
+    def ptOnCircle(self, degree):
+        radPerDeg = math.pi/180
+        r = self.size/2
+        x = int((r-5)*math.cos((degree-90)*radPerDeg) + r)
+        y = int((r-5)*math.sin((degree-90)*radPerDeg) + r)
+        return (x,y)
+
+
+
+

File doc/hint.gif

Added
New image

File doc/left.gif

Added
New image

File doc/license.txt

+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.

File doc/manual.html

+<div style="width:600px">
+
+<h1>uliGo Documentation</h1>
+
+<p>
+This is the documentation for uliGo 0.3 - a program to practice Go problems. 
+It was written by Ulrich Goertz (<a href="mailto:u@g0ertz.de">u@g0ertz.de</a>).
+uliGo is published under the GNU General Public License (see the 
+file 'gpl.txt' or look at 
+<a href="http://www.gnu.org/copyleft/gpl.html">http://www.gnu.org/copyleft/gpl.html</a>).
+This program comes WITHOUT ANY WARRANTY.</p>
+
+<hr>
+
+<h2>Contents:</h2>
+<ul>
+<li> <a href="#gettingstarted">Getting Started</a>
+<li> <a href="#problems">Solving problems</a>
+<li> <a href="#stopclock">The Stop Clock</a>
+<li> <a href="#orderofproblems">The Order of Problems</a>
+<li> <a href="#ownproblemdatabase">Using Your Own Problem Database</a>
+<li> <a href="#replaygames">Replaing games ("guess next move")</a>
+<li> <a href="#menu">The Menu</a>
+<li> <a href="#miscellaneous">Miscellaneous</a>
+<li> <a href="#installation">Installation</a>
+<li> <a href="#history">History</a>
+</ul>
+
+
+<h2><a name="gettingstarted">Getting started:</a></h2>
+
+<p>
+From now on I assume that you installed the program, and that 
+the main window pops up when you start it. (Otherwise, read the 
+section about <a href="#installation">installing uliGo</a> first.)</p>
+
+<p>
+The first thing you have to do is to load a problem collection.
+Some example collections come with uliGo; see below how you
+can create your own ones. So, go to the 'File' menu, and select 'Open problem collection'.
+Then choose one of the sgf files that you are shown (they are in the 'sgf'
+subdirectory which should automatically be selected).
+Later, the program automatically loads the collection you used last. 
+Of course you can always load another collection with the 'Open problem collection'
+command in the File menu.</p>
+
+<p>
+I think that the user interface is pretty self-explanatory,
+so I suggest that you just play around a bit with it:
+press the right arrow to see the first problem. The problem (randomly
+chosen from the database) will be displayed, the stop clock 
+will be started (by default you have 2 min 30 sec to find the
+complete answer), and you can play your move. The indicator above the
+clock (and the 'cursor') shows whose turn it is. After you
+enter your move, the program automatically replies (unless
+your suggested move was wrong and no refutation is contained
+in the database). Then enter the next move ... when the correct
+solution is reached, the program shows a 'solved' indicator on the left.
+After entering a wrong move, you can still try to solve the
+problem, but you cannot get credit for it anymore, of course;
+instead of the green "solved" you will see a blue
+one when you get to a correct solution (similarly after
+using undo, the 'show solution' mode or the 'try variation' mode).</p>
+
+<p>
+With the right arrow you can go to the next problem (this works at
+any stage). Note that the arrow buttons do not serve to navigate within the SGF file 
+(use UNDO and HINT, respectively, to do that), but to go to the next problem, or 
+back to the previous one.</p>
+
+<p>Alternatively, if you choose 'Replay game', you can load any SGF file, and
+replay it by <a href="#replaygames">guessing the next move</a>. </p>
+
+
+<h2><a name="problems">Solving problems</a></h2>
+
+
+<h4>Next Problem</h4>
+
+<p>
+<img src="right.gif" align=RIGHT>
+This button discards the current problem, and shows the next one.
+This works at any stage, no matter if you solved the current problem
+correctly or not, or if you tried some variation etc.
+Of course, some problem collection must be open; otherwise nothing
+happens.</p>
+
+<h4>Previous problem</h4>
+<p>
+<img src="left.gif" align=RIGHT>
+With this button, you can go back to the previous problem. Clicking it more than once goes back further,
+like with the 'back' button in a web browser.
+Note though that you can't go back and forth: the 'next' button will not go back to the problem you came 
+from, but will give you a new problem.</p>
+
+<h4>Restart problem</h4>
+<p><img src="reload.gif" align=RIGHT>
+Go back to the beginning of the problem, and start over. 
+If you are at the beginning of the problem already,
+this sets up the problem again at a (possibly) different position and with different colors.</p>
+
+<h4>Hint</h4>
+<p><img src="hint.gif" align=RIGHT>
+Give a hint, i.e. show the next move (and the answer). Using the Hint button results in 
+this problem not being counted in the statistics, i.e. it is neither a wrong nor a correct answer.</p>
+
+<h4>Show Solution</h4>
+
+<p><img src="showsol.gif" align=RIGHT>
+This shows a solution of the current problem. (The button is
+disabled if you already solved the problem correctly yourself.)
+You can choose between two modes for displaying the solution
+(in the Options menu: 'Show solution mode'):</p>
+
+<ul>
+<li>animate solution: the moves of the solution are played out.
+  Choose the speed in the Options menu. If there are several correct 
+  solutions, one is chosen randomly.
+<li> navigate solution: You can 'navigate' the underlying SGF file
+  with the correct and wrong variations yourself. The possible
+  variations at a given move are marked by green (correct),
+  red (wrong) or blue (moves of the opponent) markers. Just click
+  on one of them to choose that variation. With the Undo button
+  you can undo the last move. If only red markers are displayed,
+  you cannot reach the correct resolution from this point anymore - 
+  you went wrong earlier. Use the Undo button to go back until
+  a green marker appears.
+</ul>
+
+<p>Using the Show Solution button results in 
+this problem not being counted in the statistics, i.e. it is neither a wrong nor a correct answer.
+</p>
+
+<h4>Try variation</h4>
+
+<p><img src="tryvar.gif" align=RIGHT>
+At any point, you can press this button, and then play out some
+variation of your own, e.g. to convince yourself that/why something
+does not work. Use the Undo button to undo a move. As long as
+you are in the 'Try variation' mode, the 'Show solution' mode
+is disabled - it wouldn't make sense to display the solution with
+your additional stones on the board. Press 'Try variation' again
+to leave this mode and remove all the stones of your variation.
+Once you enter this mode, you cannot get any credit for the current
+problem anymore.</p>
+
+<h4>Undo</h4>
+
+<p><img src="undo.gif" align=RIGHT>
+With this button you can undo the last two moves (the answer to your
+last move and your last move) or the last move (if there was no
+answer to your last move or in the Show solution/navigate mode
+or in 'Try variation' mode).
+If you use the undo feature, you cannot get any credit for the 
+current problem anymore.</p>
+
+
+<h2><a name="stopclock">The stop clock</a></h2>
+
+<p>
+The clock starts when you press the 'next problem' button.
+The default time is 150 seconds. You can change it by a
+right mouse click on the clock or by choosing the 'change
+clock settings' command in the options menu. This only works
+when the clock is not running.</p>
+
+<p>
+Set the clock to 0 seconds to turn it off.</p>
+
+<p>
+When the time for the current problem is over, it is counted
+as a wrong answer.</p>
+
+
+<h2><a name="orderofproblems">How the program chooses the next problem from the database
+   (in random order mode)</a></h2>
+
+<p>
+Apart from the database, the program maintains a list of all
+problems, together with information how often each problem
+has been asked already, and with which results (this list
+is stored in the xyz.dat file, where xyz is the name of the
+SGF file).</p>
+
+<p>
+When you request the next problem, a problem is chosen
+randomly from the first half of the list; problems from
+the first third are a little bit more likely to be chosen
+then others.</p>
+
+<p>
+When you answer a problem correctly, it will be moved to
+the very end of the list. So it will take some time until
+that problem can come up again. When you give a wrong answer,
+the problem will be moved to a random location in the 
+second half (more precisely: in the 4th sixth) of the list;
+so this problem cannot appear again immediately, but it
+could after a relatively short time, and the more problems you
+answer correctly, the more likely it is that you will asked 
+problems that you got wrong once for a second time.</p>
+
+<p>
+You can erase the information on your previous answers by
+deleting the .dat file corresponding to a database. A new
+.dat file (in which the order of problems is that of the 
+SGF file) will be created when you open the database.</p>
+
+<p>
+(In case you installed uliGo system-wide under Unix, the
+.dat files are in the .uligo subdirectory of your home
+directory. See the file install.txt for more details.)</p>
+
+
+<h2><a name="ownproblemdatabase">Using your own problem database</a></h2>
+
+<p>
+The format used for the problem database is just the SGF format.
+So in order to make your own database, just put a bunch of SGF
+games in one single file. Some conventions (explained below)
+have to be followed, but I think they are much or less
+common sense. So probably you can just enter a problem
+into any SGF editor, and everything will work.</p>
+
+<p>
+The following conventions have to be satisfied: </p>
+
+<ul>
+<li> The first node(s) of the SGF file may contain anything. If
+the first node contains a GN[gamename] item, the game name is
+displayed. Besides that the program ignores the nodes until
+a node with an AB[] (place black stone) and/or AW[] (place white stone)
+item comes up. All other AB's and AW's have to follow this node
+without any interruption (the program gets confused if there is
+an empty node in between). After that, the program expects
+nodes with a B[] ('play black stone') or W[] ('play white stone')
+item, and they must alternate properly. Two black plays in a row,
+for instance, are not allowed.
+
+<li> If you want to have 'wrong variations' in your problems (as
+a refutation to some answer), the first node of that variation has 
+to contain a WV ('Wrong Variation', this is not an official SGF tag) 
+item or a TR[] item ('triangle label').
+The triangle label option is there in order to make it
+easier for you to edit problems with an arbitrary SGF editor;
+just place triangle labels on the first move of a wrong variation.
+Of course, that also means that no other triangle labels should
+appear in the SGF files. (Other labels may appear, but are ignored
+at the moment.)
+
+<li> You can insert a general comment about the collection which
+is displayed after the file is loaded, but before you look at
+the first problem. Just place it at the very beginning of the
+SGF file. Anything before the first '(' is considered as a general
+comment. The only restriction is that your comment must not 
+contain a '('.
+</ul>
+
+<p>
+One final remark: since every move that is not in the SGF file
+is considered wrong, it is desirable to put every correct
+solution into the file. Unfortunately, it is easy to miss
+some alternative moves, especially after some moves have already
+been played. Certainly there are some correct alternatives
+missing in the problems that come with uliGo; so don't
+take it too seriously if your answer is counted as wrong
+although it is right ...</p>
+
+
+<h2><a name="replaygames">Replaying games ("guess next move")</a></h2>
+
+<p>
+One fun way to study go is to replay professional games by guessing the
+next move. You can load an SGF file with "Replay game" in the File menu. The
+ stop clock will then be replaced by a few buttons and a frame with a small 
+"go board".</p>
+
+<p>
+With the buttons, you can choose if you want to guess only black or only white
+moves, or both. Clicks on the board will be interpreted as guesses - if 
+you managed to guess the next
+move in the current SGF file, that move is played; otherwise no stone is
+placed on the board.</p>
+
+<p>
+In the frame below the buttons you get some feedback on your guesses. If your
+guess is right, it displays a green square (and the move is played on the 
+board). If the guess is wrong, it displays a red rectangle; the rectangle is
+roughly centered at the position of the next move, and the closer your
+guess was, the smaller, and more accurately positioned is that rectangle. 
+Furthermore the number of correct guesses and the number of all guesses, 
+as well as the success percentage are given.</p>
+
+<p>
+If you just can't find the next move, you can always use the
+'HINT' button, and the move will be played out. You can restart the game 
+with the middle button in the first row. </p>
+
+
+
+
+<h2><a name="menu">The menu</a></h2>
+
+<h4>File - Open</h4>
+
+<p>
+Load a new problem database. A database just consists of
+several SGF files. Some example databases are included in
+the uliGo distribution. See below for more information how
+to create your own databases.</p>
+
+<h4>File - Statistics</h4>
+
+<p>
+Open the statistics window. It shows the name of the 
+current database, how many problems are in it, how many problems 
+the program has asked you to answer, and how many right/wrong 
+answers you have given.</p>
+
+<h4>File - Clear Statistics</h4>
+
+<p>Delete all information about problems done so far, and about correct and
+wrong answers, and reload the problem collection from disk. In particular,
+this should be used after making changes to the SGF file with 
+your problem collection.
+
+<h4>File - Exit</h4>
+
+<p>
+Quit the program.</p>
+
+<h4>Options - Fuzzy stone placement</h4>
+
+<p>
+In order to make the board and stones look more like 'in real
+life', by default the stones are not placed precisely on the 
+intersections, but by a small, random amount off. 
+On a smaller board this doesn't well (and maybe some people
+don't like it at all?), so you can disable this fuzzy placement.</p>
+
+<h4>Options - Shaded stone mouse pointer</h4>
+
+<p>
+Disables the shaded stone cursor which shows where the next 
+move would be if you clicked at the current position.</p>
+
+<h4>Options - Allow color switch</h4>
+
+<p>
+In order to make sure that you don't just learn one particular
+problem, but rather a shape, uliGo randomly alters the position
+of the problem on the go board, and also the color of the stones.
+Because the latter could cause problems if your database contains
+comments referring to the colors ('good for black', 'white to move'),
+you can force uliGo to use the colors of the SGF file by disabling
+this option.</p>
+
+<h4>Options - Allow mirroring/rotating</h4>
+
+<p>
+With this checkbutton, you can switch off the automatical
+mirroring/rotating of the problems. That might be useful,
+for example, if there are comments referring to the "upper
+left" or the "right side".</p>
+
+<h4>Options - Show solution mode</h4>
+
+<p>
+Swich between animate and navigate mode. See the description
+of the 'Show solution' button above.</p>
+
+<h4>Options - Replay speed</h4>
+
+<p>
+Choose the speed for replaying the solution (in animate mode).</p>
+
+<h4>Options - Change clock settings</h4>
+
+<p>
+Change the maximal time for solving a problem. You can achieve
+the same by right clicking on the clock. (Also see below: The 
+stop clock)</p>
+
+<h4>Options - Wrong variations</h4>
+
+This determines what uliGo does with 'wrong variations', i.e. with wrong answers
+to which a refutation is given in the SGF file. You can choose if uliGo should 
+tell you your move was wrong immediately when entering the variation, or only at the end of the refutation,
+or if uliGo should not descend into wrong variations at all, i.e. 
+just show that the move was wrong and take it back.
+
+<h4>Options - Random/sequential order mode</h4>
+
+<p>
+Choose if the problems should be presented in</p>
+
+<ul>
+<li> random order: here, the problem is basically chosen at random,
+  but problems that you have been asked already are less likely
+  to be chosen, especially if your answer was correct.
+  (See below for more details.)
+<li> sequential order (keep track of solutions): The program 
+  maintains a list of all problems in the SGF file; in this
+  mode, it always presents the first problem from that list.
+  If you solve the problem, it is moved to the end of the list;
+  if your answer is wrong, it is moved somewhere to the second
+  half of the list (so it will reappear sooner).
+<li> sequential order (don't record results): In this mode,
+  the problems are presented in the same order as in the
+  SGF file. Correct or wrong answers are not recorded in
+  any way. You can specify the starting point. If you don't
+  specify it (or if the entry is invalid, e.g. not an integer),
+  it starts with the first problem in the SGF file.
+</ul>
+
+<p>
+The mode, together with the current position in 'sequential
+order, don't record results' mode, is stored in the .dat file;
+so basically each problem collection has its own mode.
+If you check the "use as default" option, then the current
+mode will be chosen for other collections which do not yet
+have a .dat file (i.e. you use then for the first time)
+or have a .dat file from version 0.1.</p>
+
+<h4>Options - Use 3D stones</h4>
+
+Toggle the use of the more beautiful 3D stones versus flat stones.
+The 3D stones were provided by Patrice Fontaine. (Thank you!)
+
+<h4>Help - About</h4>
+
+<p>
+Some basic information about uliGo.</p>
+
+<h4>Help - Documentation</h4>
+
+<p>
+Open this documentation in a web browser.</p>
+
+
+<h4>Help - License</h4>
+
+<p>
+The uliGo license.</p>
+
+
+
+<h2><a name="miscellaneous">Miscellaneous</a></h2>
+
+<p>
+That's it for the moment, I think. Feel free to contact me (at
+uligo@g0ertz.de) if you have any questions, or - in particular - 
+if you find any bugs in the program.</p>
+
+
+<h2><a name="installation">Installation</a></h2>
+
+<p>The program is written in <a href="http://www.python.org/">Python</a>,
+a high-level interpreted programming
+language. If you are using Windows, you can either use the uliGo installer
+which contains everything you need to run uliGo, or you install Python separately.
+On other systems, you need to install Python before you
+can run uliGo. Any version better than 2.0 should do; I tested
+the program with Python 2.0, 2.1 and 2.2. If you have to install
+Python, you should get the current version 2.2.
+
+
+<h3>Windows</h3>
+
+<p>Te easiest way is to use the installer. Since it contains the Python interpreter, there is no need to install
+Python separately.  </p>
+
+<p>
+If you want to install Python separately, download the Python installer for Windows 
+from the <a href="http://www.python.org/">Python website</a>. It will be very easy to install it.
+Then download the uligo03-win.zip file and unpack it. You should then be able to run uliGo
+by double-clicking the "uligo.pyw" file, or from the command line with "c:\python22\python uligo.pyw".</p>
+
+<h3>Linux/Unix</h3>
+
+<p>
+It is likely that Python is already included
+in your distribution. It is also easy to build it yourself with the
+source from the Python website. But be sure to install
+the Tkinter module which is needed for the GUI, too; look at the
+in the README file coming with Python for instructions how to do that.</p>
+
+<p>
+Once you have Python working, just download and unpack the
+uliGo file (uligo03.tar.gz). It will create 
+a subdirectory called uligo03 in the directory where you unzip
+it, and all files needed for uliGo will be placed in that
+subdirectory. Then just start uligo.py:
+change into the corresponding directory,
+and type 'python uligo.py'. (You can also make 'uligo.py'
+executable, possibly change the path in its first line to
+point to your Python installation, and run it as 'uligo.py'.)</p>
+
+<p>
+You can also install uliGo system-wide; see below.</p>
+
+<h3>Other operating systems</h3>
+
+<p>
+Python is available for many operating systems, so you should also be able to run
+uliGo. See the <a href="http://www.python.org/">Python website</a> for more 
+information.</p>
+
+<h3>Upgrade from uliGo 0.1, 0.2</h3>
+
+<p>
+Basically, you should just install uliGo 0.3 from scratch,
+and delete the old version (Make sure that you don't delete any sgf files
+with problems ...). In particular, you should not use
+the files uligo.def and uligo.opt from version 0.1 or 0.2 with
+version 0.3 (these files contain the default problem collection
+and the saved options, respectively).</p>
+
+<p>
+You can use the .dat files from uliGo 0.1, though (these files
+contain the information about right/wrong answers etc.; for
+each SGF file that you used with uliGo there is a corresponding
+.dat file). Just copy the .dat files from the sgf subdirectory
+of uligo01 to the sgf subdirectory of uligo03. (In case you
+installed uliGo system-wide under Unix, it is slightly more
+complicated; please see below.)</p>
+
+<h3>Systemwide installation under Unix/Linux</h3>
+
+<p>
+To install uligGo system-wide (in /usr/local/share, for instance),
+proceed as follows:</p>
+
+<p>
+Put the uliGo files in /usr/local/share/uligo03 (if you put them
+somewhere else, you have to adapt the unixinst.py script
+accordingly).</p>
+
+<p>
+Carefully read, and -if necessary- edit the script unixinst.py .
+(I think that you probably will not want to change much.)
+Basically, the unixinst.py script writes a 'global' uligo.def
+file (in the uligo03 directory) which tells uligo to look
+for individual .def files (in $HOME/.uligo ) when it is
+started. So for every user who uses uligo, a subdirectory
+called .uligo will be created in the user's home directory.
+In this directory, the individual .def file (which stores
+the path and name of the SGF file used last), the .opt
+file (which stored the saved options), and the .dat files
+(which store the number of correct/wrong answers for
+each problem in the corresponding SGF file) are stored.
+In order to avoid name conflicts between .dat files for
+.sgf files in different directories, the path is shadowed
+in the .uligo directory: for a .sgf file in 
+/usr/local/share/uligo/sgf, for example, the corresponding
+.dat file is in $HOME/.uligo/usr/local/share/uligo/sgf.</p>
+
+<p>
+Furthermore the unixinst.py script creates a link
+in /usr/local/bin, pointing to uligo.py.</p>
+
+<p>
+After you edited the unixinst.py script, execute it with
+'python unixinst.py'. The only other thing you might have 
+to do (if your python interpreter is not in /usr/bin),
+is to change the very first line of the file uligo.py,
+which must contain the location of the python interpreter,
+so that uligo can be started by 'uligo.py'.</p>
+
+<h2><a name="history">History</a></h2>
+<table>
+<tr>
+<td>May 2003:</td><td>uliGo 0.3, with a few new features, and a Windows installer.</td>
+</tr><tr>
+<td>June 2001:</td><td>	uliGo 0.2: some minor bugfixes, and the option
+                to change the order in which the problems are
+                presented (random vs. sequential)</td>
+</tr>
+<tr>
+<td>May 2001:</td><td>uliGo 0.1 is published.</td>
+</tr>
+<tr>
+<td>April 2001:</td><td>Started writing uliGo.</td>
+</tr>
+</table>
+
+</div>

File doc/readme.txt

+uliGo - Understanding, Learning, Inspiration for the game of GO
+
+uliGo 0.3 is a program to practice go problems. It was written by
+Ulrich Goertz (uliGo@g0ertz.de), and is published under the GNU
+General Public License.
+
+What follows is a description of the features of uliGo. To find information
+how to install the program, and how to use it, look at the manual, to be 
+found in the manual.html file.
+
+---------------------------------------------------------------------------
+
+Acknowledgments:
+
+I am grateful to everybody who pointed out bugs in previous versions,
+or made suggestions about new features.
+
+The images of the board and the stones were created by Patrice Fontaine.
+
+Disclaimer:
+
+I have thoroughly tested uliGo on one Linux box, and installed and briefly
+tested it on a Windows system (Win2000).
+There are no bugs that I know of, but since this is the very first published
+version, probably some bugs exist nevertheless. So let me state clearly that
+this program comes 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.
+
+Where to get uliGo:
+
+- You can download the uliGo distribution (as a .tar.gz for Linux/Unix or
+  as a .zip file for Windows/Macintosh) from
+
+    http://www.u-go.net/uligo/
+
+  On that page you can also find more information about uliGo, including some
+  screenshots.
+
+Give it a try and please send me your feedback! Any comments, and especially
+bug reports are welcome. 
+
+------------------------------------------------------------------------------
+
+To get stronger at go, it is essential to develop one's reading ability. That
+is why professionals recommend to study life and death or tesuji problems.
+uliGo is a program that allows you to do that: basically, the computer
+displays a problem, and asks for the answer. You enter the first move, the
+computer responds, and so on until you reach the final solution or enter a
+wrong move. 
+
+The main features of uliGo are
+
+* It is free. uliGo is published under the GNU General Public License 
+  (GPL), so you don't have to pay anything to use it. Moreover you may 
+  freely distribute it. You also get the full source code and may add your 
+  own features to the program, as long as you also release the changes
+  under the GPL.
+
+* It is cross-platform. uliGo is written in Python (with Tkinter for 
+  the graphical user interface). So you may have to install Python first; 
+  but it comes with most Linux distributions nowadays, can be installed on 
+  Windows machines in a minute with a comfortable installer, and is 
+  available on nearly any platform (Apple Macintosh, of course, but also 
+  other UNIX versions, etc.). Go to http://www.python.org/ for more 
+  information on Python (and how to install it; also see the uliGo 
+  documentation).
+
+* It uses the SGF format for storing the problems. That means that you can
+  easily (with any SGF editor, in fact) generate your own problem database.
+  Also, if you find an error in an existing database, you can fix it yourself.
+  Comments from the SGF files are automatically displayed, and you can 
+  also give 'wrong variations' that contain the refutation of a certain move.
+
+* In order to make sure that you do not only learn the answer in one particular
+  position, but rather the right move for a certain shape, uliGo randomly
+  switches the colors (black <-> white) and changes the position on the
+  board (by mirroring/rotating) of the problem. 
+
+* If you are stuck, you can look at the solution of the problem. You can
+  also, at any time, play out a variation of your own (for example when you
+  want to convince yourself why a certain move does not (or does) work.
+
+* There is a customizable stop clock; so you can set the time you have
+  for answering yourself. If the problems in the current database are easy,
+  try to solve them in thirty seconds (or less ...), if they hard, allow
+  yourself more time to think or turn off the clock.
+
+* It comes with two problem collections (easy & hard) with 80 problems 
+  altogether. Some of these problems I composed myself, the other
+  ones are taken from classical problem collections.
+  Certainly for some people the "easy" problems are not so easy, 
+  and others will not find the "hard" problems difficult at all. 
+  I hope you will find some problems that are suitable for you, anyway. 
+  Considering that only some of the problems will be useful for any 
+  particular level, 80 problems obviously are not much.
+  It would be desirable to make more problems available in SGF format;
+  of course, because of the copyright, it is not possibly to put a
+  whole book into SGF and give it away; also from an ethical point of view,
+  this would be undesirable, since I think it is important to support the 
+  authors and publishers of such books. I would find it ideal if 
+  some go publishers would sell problem collections in SGF format, at a 
+  reasonable price. It should be easy enough to find volunteers who put 
+  (parts of) a book into SGF.
+  If you own a copy of Cho's Encyclopedia of Life and Death on disk,
+  you can use the program cho2sgf.py (to be found on the same web page
+  as uliGo) to translate those problems into SGF format and use them
+  with uliGo. 
+
+

File doc/reload.gif

Added
New image

File doc/right.gif

Added
New image

File doc/showsol.gif

Added
New image

File doc/tryvar.gif

Added
New image

File doc/undo.gif

Added
New image

File gifs/BsTurn.gif

Added
New image

File gifs/WsTurn.gif

Added
New image

File gifs/b.gif

Added
New image

File gifs/bgd.gif

Added
New image

File gifs/black.gif

Added
New image

File gifs/board.gif

Added
New image

File gifs/bw.gif

Added
New image

File gifs/clock.gif

Added
New image

File gifs/empty.gif

Added
New image

File gifs/end.gif

Added
New image

File gifs/hint.gif

Added
New image

File gifs/left.gif

Added
New image

File gifs/logo.gif

Added
New image

File gifs/reload.gif

Added
New image

File gifs/right.gif

Added
New image

File gifs/showsol.gif

Added
New image

File gifs/solved1.gif

Added
New image

File gifs/solved2.gif

Added
New image

File gifs/tryvar.gif

Added
New image

File gifs/undo.gif

Added
New image

File gifs/w.gif

Added
New image

File gifs/white.gif

Added
New image

File gifs/wrong.gif

Added
New image

File sgf/easy.sgf

+A collection of 40 easy problems.
+(;GM[1]FF[3]
+;AW[oq][pq][qq][rq][sq][mr][or]
+AB[pr][qr][rr][sr][ps];B[rs]
+)
+(;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])
+)
+(;GM[1]FF[3]
+;AW[qo][qp][kq][nq][oq][pq]
+AB[qh][ol][nm][qm][op][pp][qq][rq][qr][sr][qs][rs]
+;W[no];B[oo];W[on];B[pn];W[po]
+)
+(;GM[1]FF[3]
+;AW[ok][qk][rl][mm][ln][pn][op][pp][rp][qq][rq][pr][qr][sr]
+AB[qn][qo][ro][np][qp][mq][oq][pq];B[on]
+(;W[po];B[pm];W[no];B[oo])
+(;W[oo];B[no];W[pm];B[po])
+)
+(;GM[1]FF[3]
+;AW[pq][qq][rq][gr][hr][ir][jr][kr][pr][is][ps][rs]
+AB[ql][op][pp][qp][rp][fq][gq][hq][iq][jq][kq][lq][mq][oq][dr][fr][gs]
+(;W[ns]
+(;B[nr]
+(;W[ls])
+(;W[ms])
+)
+(;B[ms];W[mr];B[lr];W[ls];B[nr];W[ms])
+(;B[lr];W[ls];B[nr];W[ms])
+)
+(;W[ms]WV[ms];B[ls];W[lr];B[ns];W[mr];B[nr])
+(;W[lr]WV[lr];B[mr])
+)
+(;GM[1]FF[3]
+;AW[iq][kq][lq][mq][nq][jr][kr][nr][is][ks]
+AB[mn][on][lo][hp][ip][jp][op][qp][hq][oq][gr][mr][or][ns]
+;W[ms];B[ls];W[lr];B[ms];W[os]
+)
+(;GM[1]FF[3]
+;AW[ip][jp][kp][lp][iq][lq][ir][kr][lr]
+AB[ho][io][jo][ko][lo][hp][mp][hq][mq][hr][mr][or]
+(;B[ls];W[is]
+(;B[ks])
+(;B[jq])
+)
+(;B[jq]WV[jq];W[ls];B[js];W[is];B[jr])
+(;B[is]WV[is];W[js])
+)
+(;GM[1]FF[3]
+;AW[jq][kq][gr][hr][ir][lr][gs][ls]
+AB[jp][kp][lp][fq][gq][hq][iq][lq][nq][fr][mr][fs]
+(;B[jr];W[kr];B[js])
+(;B[js]WV[js];W[jr];B[ks];W[is])
+)
+(;GM[1]FF[3]
+;AW[io][jo][ko][lo][jp][lp][jq][kq][ir][lr][js][ks]
+AB[lm][hn][in][jn][ho][mo][mp][hq][iq][lq][mq][hr][mr][ms]
+(;B[hs];W[is];B[kr];W[jr];B[ls])
+(;B[ls]WV[ls];W[kr])
+(;B[kr]WV[kr];W[ls])
+)
+(;GM[1]FF[3]
+;AW[jo][qp][fq][iq][jq][kq][mq][oq][gr][hr][lr][mr]
+AB[hp][lp][hq][lq][ir][jr][kr]
+;B[jp];W[ip];B[io];W[kp];B[ko]
+(;W[jn];B[jp])
+(;W[jp];B[jn])
+)
+(;GM[1]FF[3]
+;AW[fo][cp][dp][ep][cq][gq][hq][kq][br][dr][hr][ds]
+AB[dq][eq][cr][er][fr][gr][es]
+;B[bs]
+(;W[bq];B[gs])
+(;W[gs];B[bq];W[cs];B[cr];W[bp];B[ar])
+)
+(;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])
+)
+)
+(;GM[1]FF[3]
+;AW[rk][ol][pl][rl][om][qm][sm][qn][rn][sn][qo][pp][pq][pr][qr]
+AB[ri][qj][qk][sk][ql][pm][pn][oo][po][ro][so][qp][sp][qq][rq][rr];
+W[no];B[op];W[np];B[oq];W[or];B[nq];W[mq];B[nr];W[mr];B[os];W[on]
+)
+(;GM[1]FF[3]
+;AW[dl][cm][bn][ao][bo][ap][cp][dp][dq][dr][bs][ds]
+AB[cn][en][co][eo][bp][ep][bq][cq][eq][ar][cr][gr][cs]
+(;B[er]WV[er];W[aq])
+(;B[bm];W[bl]
+(;B[er])
+(;B[do])
+)
+)
+(;GM[1]FF[3]
+;AW[qq][rq][sq][pr][rs]
+AB[pp][qp][rp][nq][pq][or][rr][qs]
+(;W[sr];B[ss]C[Ko.])
+(;W[qr]WV[qr];B[ss])
+(;W[ps]WV[ps];B[ss])
+)
+(;GM[1]FF[3]
+;AW[qq][rq][pr][sr]
+AB[oo][qo][rp][sp][kq][oq][pq][sq][or]
+(;W[rs]
+(;B[qr];W[ps])
+(;B[ps];W[qr])
+)
+(;W[qr]WV[qr];B[rs])
+(;W[qs]WV[qs];B[rr];W[rs];B[ss]C[Ko.])
+)
+(;GM[1]FF[3]
+;AW[oq][qq][nr][pr][rr][sr][ns][ps][rs][ss]
+AB[ro][op][pp][qp][mq][nq][rq][sq][mr][qr]
+(;W[pq];B[qs];W[rr])
+(;W[qs]WV[qs];B[pq])
+)
+(;GM[1]FF[3]
+;AW[rl][qn][rn][po][so][pp][rp][pq][sq][pr]
+AB[qo][ro][qp][qq][rq][rr][sr];W[sp];B[sn];W[sp];B[so];W[rs]
+)
+(;GM[1]FF[3]
+;AW[po][qp][rp][mq][oq][pq][qr]
+AB[qq][rq][pr][rr]
+(;W[qs];B[rs];W[sq];B[ps];W[sr])
+(;W[or]WV[or];B[qs])
+)
+(;GM[1]FF[3]
+;AW[hp][ip][jp][kp][lp][mp][np][gq][kq][oq][pq][jr][pr][ps]
+AB[fp][gp][cq][fq][hq][iq][jq][lq][mq][nq][kr][mr][or][ls][ms][ns][os]
+(;W[hr]WV[hr];B[gr];W[ir];B[gq];W[js];B[hs])
+(;W[is]
+(;B[ks];W[gr])
+(;B[gr];W[ks])
+)
+)
+(;GM[1]FF[3]
+;AW[fp][gp][dq][hq][iq][er][hr][jr][hs][is]
+AB[hp][ip][jp][lp][gq][jq][gr][lr][gs]
+(;B[kr]WV[kr];W[fr])
+(;B[ks]
+(;W[fr];B[js])
+(;W[kr];B[ls];W[fq];B[js])
+)
+)
+(;GM[1]FF[3]
+;AW[hp][jp][kp][lp][iq][mq][pq][hr][jr][mr][hs][js]
+AB[co][ip][dq][gq][jq][kq][gr][kr][gs][ks]
+(;B[ir]WV[ir];W[hq])
+(;B[hq];W[io];B[ir];W[is];B[ir])
+)
+(;GM[1]FF[3]
+;AW[op][pp][qp][rp][sp][oq][or][os]
+AB[pq][qq][rq][sq][pr][ps]
+(;W[rr];B[rs];W[sr])
+(;W[rs]WV[rs];B[rr];W[qs];B[ss];W[sr]C[Ko.])
+)
+(;GM[1]FF[3]
+;AW[bp][cq][dq][eq][jq][mq][br][fr][gr][hr][ir]
+AB[fn][bo][co][ep][fp][hp][bq][fq][hq][cr][dr][er]
+(;B[aq];W[ap];B[cp];W[ar];B[bq];W[aq];B[dp];W[bq]
+(;B[ao])
+(;B[bs])
+)
+(;B[cp]WV[cp];W[aq];B[dp];W[bq];B[bs];W[cs];B[ds];W[fs])
+)
+(;GM[1]FF[3]
+;AW[gp][hp][fq][hq][fr][hr][ir][jr][lr][fs][ls]
+AB[go][ho][ko][ep][fp][ip][op][cq][gq][kq][lq][mq][dr][gr][kr][nr][gs][is][js]
+(;W[hs]WV[hs];B[gr];W[ks];B[js])
+(;W[ks];B[mr];W[hs];B[js];W[gr])
+)
+(;GM[1]FF[3]
+;AW[rp][pq][qq][rq][or][ps]
+AB[qn][ro][op][pp][qp][mq][oq][nr][ns]
+(;B[pr];W[qr];B[rs]
+(;W[sp];B[sr];W[os];B[qs])
+(;W[rr];B[os])
+)
+(;B[sp]WV[sp];W[rs];B[sr];W[sq];B[qr];W[rr];B[pr];W[qs];B[pr];W[os])
+)
+(;GM[1]FF[3]
+;AW[hq][iq][jq][gr][kr][gs]
+AB[hp][ip][jp][fq][gq][kq][mq][fr][lr];W[ks]
+(;B[ir];W[is])
+(;B[is];W[ir];B[ls];W[hs])
+)
+(;GM[1]FF[3]
+;AW[iq][jq][kq][hr][jr][lr][hs][ls]