#!/usr/bin/env python

from Tkinter import *
from math import *
import threading, time, random, string
import tkFont,tkFileDialog,tkSimpleDialog
import tkMessageBox


info = dict(
    name     = "TkPySudoku",
    version  = "0.1",
    author   = "Roman Holenstein",
    email    = "holenstein@users.sourceforge.net",
    homepage = "http://sourceforge.net/projects/tkpysudoku/",
    )


try:
    set
except NameError:
    from sets import Set as set, ImmutableSet as frozenset
    
class Group(set):
    def __init__(self,s,name=''):
        set.__init__(s.copy())
        for x in s:
            self.add(x)
        self.members = set()
        self.name = name
        #print "defined group",self.name,self

class Box:
    def __init__(self, coord, labels_count, groups=None):
        self.coord = coord
        self.labels_count = labels_count
        self.val = None
        self.groups = []
        if groups!=None and len(groups)>0:
            for g in groups:
                self.addGroup(g)

    def addGroup(self,group):
        if not type(group) is Group:
            print "argument has wrong type",type(group)
            return
        
        self.groups.append(group)
        group.members.add(self)
        if not self.val is None:
            if self.val in group:
                group.discard(self.val)
            else:
                print "error: added group",group.name,\
                      "does not allow for currently set value",self.val
                self.remVal(self.val)
                self.val = None                
        
    def reset(self):
        return self.unsetVal()

    def getChoices(self):
        if len(self.groups)<=0:
            return set()
        s = self.groups[0]
        for i in range(1,len(self.groups)):
            s=s.intersection(self.groups[i])
        return s

    def getDependents(self):
        boxes = set()
        for grp in self.groups:
            boxes=boxes.union(grp.members)
        return boxes
    
    def unsetVal(self,test=False):
        if test:
            return True
        if self.val is None:
            return True
        self.labels_count[self.val] -= 1
        val,self.val = (self.val,None)
        return self.addVal(val)
        
    def setVal(self,val,test=False):
        #print "Box.setVal",self.coord,":",val,"test=",test
        if val in self.getChoices():
            if not test:
                self.unsetVal()
                self.val = val
                self.labels_count[self.val] += 1
                self.remVal(self.val)
            return True
        #print "  box",self.coord,":",val,"not in",self.choices
        return False

    def addVal(self,val,test=False):
        for b in self.getDependents():
            if b.val == val:
                return False
        if not test:
            for g in self.groups:
                g.add(val)
        return True

    def remVal(self,val,test=False):
        #print "Box.remVal",self.coord,":",val,"test=",test
        for g in self.groups:
            if len(g)==0:
                return False
        if not test:
            for g in self.groups:
                g.discard(val)
        return True


class DrawableBox(Box):
    def __init__(self, canvas, labels, labels_count, nrow, ncol,
                 x0, y0, width, height, coord):
        Box.__init__(self,coord,labels_count)

        self.pos = (x0,y0)
        self.dim = (width,height)
        
        self.canvas=canvas
        self.size=nrow*ncol
        self.labels = labels
        while len(self.labels)<self.size:
            x = len(self.labels)
            while x in self.labels or repr(x) in self.labels:
                x += 1
            self.labels.append(repr(x))

        self.tag = 'box(%s,%s)'%self.coord
        self.frame_outline = "black"
        self.frame_width = 1
        self.frame = canvas.create_rectangle(x0-width/2,y0-height/2,
                                             x0+width/2,y0+height/2,
                                             outline=self.frame_outline,
                                             width=self.frame_width,
                                             tags=self.tag)
        
        dx=float(width)/nrow
        dy=float(height)/ncol
        scale=0.7
        self.smallFont = tkFont.Font(size=-int(scale*min(dx,dy)))
        self.bigFont   = tkFont.Font(size=-int(scale*min(width,height)))
        
        self.id = [[] for i in range(self.size)]
        for i in range(nrow):
            for j in range(ncol):
                x = x0+int(i*dx-(nrow-1)*dx/2.0);
                y = y0+int(j*dy-(ncol-1)*dy/2.0);
                v = self.labels[i*ncol+j]
                self.id[i*ncol+j] = canvas.create_text(x,y,text=v,
                                                       justify="center",
                                                       font=self.smallFont,
                                                       tags=self.tag)                
        self.t = canvas.create_text(x0,y0,text="",
                                    justify="center",
                                    font=self.bigFont,
                                    tags=self.tag)

    def reset(self):
        Box.reset(self)
        self.redraw()
        
    def redraw(self):
        if self.val is None:
            self.canvas.itemconfig(self.t, text="")
        else:
            self.canvas.itemconfig(self.t, text=self.labels[self.val])

        choices = self.getChoices()
        for val in range(self.size):
            if self.val is None and val in choices:
                s = self.labels[val]
                self.canvas.itemconfig(self.id[val], text=self.labels[val])
            else:
                self.canvas.itemconfig(self.id[val], text="")

    def redrawDependents(self):
        for box in self.getDependents():
            box.redraw()
        return True
    
    def unsetVal(self,test=False):
        if test:
            return Box.unsetVal(self,test=True)
        return Box.unsetVal(self) and self.redrawDependents()
    
    def setVal(self,val,test=False):
        #print "DrawableBox.setVal",self.coord,":",val
        if test:
            return (0 <= val < self.size and
                    Box.setVal(self,val,test=True))
        return (0 <= val < self.size and
                Box.setVal(self,val,test=False) and
                self.redrawDependents())
        
    def addVal(self,val,test=False):
        if test:
            return (0 <= val < self.size and
                    Box.addVal(self,val,test=True))
        return (0 <= val < self.size and
                Box.addVal(self,val,test=False) and
                self.redrawDependents())
    
    def remVal(self,val,test=False):
        #print "DrawableBox.remVal",self.coord,":",val
        if test:
            return Box.remVal(self,val,test=True)
        if Box.remVal(self,val,test=False):
            for box in self.getDependents():
                box.canvas.itemconfig(box.id[val], text="")
            return True
        return False


    def formatSet(self,lSet):
        st = '{'
        for x in lSet:
            st = '%s %s'%(st,x)
        st = '%s }'%st
        return st
        
    def getLabels(self,indSet,raw=False):
        s = set()
        for i in indSet:
            s.add(self.labels[i])
        if raw:
            return s
        return self.formatSet(s)
    
    def printInfo(self):
        s=''
        for g in self.groups:
            s = '%s %s'%(s,g.name)
        ch=self.getChoices()
        print 'box',self.coord,'depends on groups:',s,":",self.getLabels(ch)
        for g in self.groups:
            print '    ',g.name,':\t',self.getLabels(g)

        print " |labels:",self.formatSet(self.labels)
        print " |count: ",self.formatSet(self.labels_count)

    def raiseBox(self):
        self.canvas.tag_raise(self.frame)
        for item in self.id:
            self.canvas.tag_raise(item)
        self.canvas.tag_raise(self.t)
        
    def lowerBox(self):
        self.canvas.tag_lower(self.t)
        for item in self.id:
            self.canvas.tag_lower(item)
        self.canvas.tag_lower(self.frame)

    def highlight(self,verbose=False):
        self.canvas.itemconfig(self.frame,outline="red",width=3)
        self.raiseBox()
        for box in self.getDependents():
            self.canvas.itemconfig(box.frame,fill="gray")
        
        if verbose:
            self.printInfo()
        
    def unhighlight(self):
        self.canvas.itemconfig(self.frame,
                               outline=self.frame_outline,
                               width=self.frame_width)
        self.lowerBox()
        for box in self.getDependents():
            box.canvas.itemconfig(box.frame,fill="")


class SelectBox:
    def __init__(self,board,i,j):
        self.board = board
        self.ind = (i,j)
        self.box = board.board[i][j]
        self.inpstr = ''
        self.box.highlight()

    def cancel(self):
        self.box.unhighlight()
        self.box.redraw()

    def done(self):
        if len(self.inpstr)>0:
            self.board.setChar(self.ind[0],self.ind[1],self.inpstr)
        self.inpstr = ''
        self.box.redraw()
        
    def finish(self):
        self.done()
        self.cancel()
        
    def addChar(self,char):
        if char in ('\r','\n'):
            self.done()
            return
        if char == '\x08':
            if len(self.inpstr)>0:
                self.inpstr = self.inpstr[0:(len(self.inpstr)-1)]
        else:
            self.inpstr += char
        #print "input string =",self.inpstr
        self.box.canvas.itemconfig(self.box.t, text=self.inpstr)

class Board(Canvas):
    def __init__(self, master=None, nrow=3, ncol=3, width=400, height=400):
        Canvas.__init__(self, master, width=width, height=height)

        lw=3;
        self.size=nrow*ncol
        self.ncol=ncol
        self.nrow=nrow

        self.labels = []
        for i in range(0,self.size):
            self.labels.append(repr(i+1))
        self.labels_count = [0]*len(self.labels)

        self.board = [[] for i in range(self.size)]
        self.dx=(width-lw)/self.size
        self.dy=(height-lw)/self.size
        self.x0=(width+lw-self.size*self.dx)/2
        self.y0=(height+lw-self.size*self.dy)/2
        y=self.y0-self.dy/2
        for i in range(self.size):
            y=y+self.dy
            x=self.x0-self.dx/2
            for j in range(self.size):
                x = x+self.dx;
                t = DrawableBox(self,self.labels,self.labels_count,
                                self.nrow,self.ncol,x,y,self.dx,self.dy,(i,j))
                self.board[i].append(t)
                self.create_rectangle(x-self.dx/2,y-self.dy/2,x+self.dx/2,y+self.dy/2)

        self.makeGroups()

        dx=self.nrow*self.dx # size/ncol = nrow
        dy=self.ncol*self.dy # size/nrow = ncol
        x0=self.x0-lw/2
        y0=self.y0-lw/2
        y=y0-dy/2
        for i in range(self.nrow):
            y=y+dy
            x=x0-dx/2
            for j in range(self.ncol):
                x = x+dx
                self.create_rectangle(x-dx/2+1,y-dy/2+1,\
                                      x+dx/2-1,y+dy/2-1,width=lw)
        self.selected=None
        self.actionstack=[]
        
        self.do_mouse()
        #self.bind_all('<Key>',self.key)
        

    def makeGroups(self):
        choices = set(range(len(self.labels)))
        #print "choices=",choices
        # row groups
        for k in range(self.size):
            group = Group(choices,'row-%r'%k)
            for j in range(self.size):
                self.board[k][j].addGroup(group)
        # column groups
        for k in range(self.size):
            group = Group(choices,'col-%r'%k)
            for i in range(self.size):
                self.board[i][k].addGroup(group)
        # block groups
        for bi in range(self.nrow):
            for bj in range(self.ncol):
                group = Group(choices,'blk-%r-%r'%(bi,bj))
                for i in range(bi*self.ncol,bi*self.ncol+self.ncol):
                    for j in range(bj*self.nrow,bj*self.nrow+self.nrow):
                        #print "adding group to box",(i,j)
                        self.board[i][j].addGroup(group)

    def do_mouse(self):
        self.bind_all('<Button-1>',self.mouseButton1)
        self.bind_all('<Button-2>',self.mouseButton2)
        self.bind_all('<Button-3>',self.mouseButton3)

    def mouseButton1(self,event):
        if event.widget != self:
            return
        #print "mouse button 1 clicked",(event.x,event.y)
        self.selectBoxByCoord(event.x,event.y)
        
    def mouseButton2(self,event):
        if event.widget != self:
            return
        #print "mouse button 2 clicked",(event.x,event.y)
        self.selectBoxByCoord(event.x,event.y)
        if not self.selected is None:
            i,j = self.selected.ind
            self.unsetVal(i,j)
            
    def mouseButton3(self,event):
        if event.widget != self:
            return
        #print "mouse button 3 clicked",(event.x,event.y)
        self.unselectBox()

    def selectBoxByKey(self,char):
        #print "selectBoxByKey(",char,")"
        if self.selected is None:
            i=j=0
        else:
            movement = {'Tab':(0,1),
                        'Right':(0,1),
                        'Left':(0,-1),
                        'Down':(1,0),
                        'Up':(-1,0)}
            
            if not movement.has_key(char):
                print "undefined key",char
                return
            
            di,dj = movement[char]
            i,j = self.selected.ind
            #print "pos=",(i,j)," move=",(di,dj)
            i = (i+di)%self.size
            j = (j+dj)%self.size
            self.selected.finish()
        #print "selecting",(i,j)
        self.selected = SelectBox(self,i,j)
        
    def selectBoxByCoord(self,x,y):
        j = (x-self.x0)/self.dx
        i = (y-self.y0)/self.dy
        #print "selecting",(i,j)
        if 0<=i<self.size and 0<=j<self.size:
            if not self.selected is None:
                self.selected.finish()
            self.selected = SelectBox(self,i,j)

    def unselectBox(self):
        if not self.selected is None:
            self.selected.cancel()
            self.selected = None

    def redraw(self):
        for i in range(self.size):
            for j in range(self.size):
                self.board[i][j].redraw()

    def reset(self):
        for i in range(self.size):
            for j in range(self.size):
                self.board[i][j].reset()

    def remVal(self,i,j,val):
        return self.board[i][j].remVal(val)

    def addVal(self,i,j,val):
        return self.board[i][j].addVal(val)

    def setVal(self,i,j,val):
        return self.board[i][j].setVal(val,test=False) and \
               self.actionstack.append(("set",i,j,val))


    def unsetVal(self,i,j):
        val = self.board[i][j].val
        return self.board[i][j].unsetVal(test=False) and \
               self.actionstack.append(("unset",i,j,val))

    def addLabel(self,c):
        for i in range(len(self.labels)):
            if not self.labels_count[i]:
                while len(self.labels_count)<len(self.labels):
                    labels_count.append(0)
                self.labels[i] = c
                self.redraw()
                #print "added new label:",(i,c)
                return i
        return -1
    
    def getVal(self,c):
        try:
            return self.labels.index(c)
        except ValueError:
            return self.addLabel(c)

    def getChar(self,i,j):
        if self.board[i][j].val is None:
            return '.'
        else:
            return self.labels[self.board[i][j].val]
    
    def setChar(self,i,j,c):
        val = self.getVal(c)
        if val>=0:
            return self.setVal(i,j,val)
        #print "cannot use label",c
        tkMessageBox.showerror("Cannot use label",
                               "Cannot add new label '%s'.  The maximum number of "
                               "distinct labels for this puzzle is %d.  "
                               "If you want to use a new label you must replace one "
                               "of the existing labels by first removing all occurances "
                               "of that label."%(c,self.size));
        return False
    
    def undo(self):
        if len(self.actionstack)>0:
            (action,i,j,val) = self.actionstack.pop()
            if action=="set":
                self.unsetVal(i,j)
            elif action=="unset":
                self.setVal(i,j,val)
            self.actionstack.pop()

    
    def findMostConstrainedBox(self):
        num = self.size+1
        choices = None
        cbox = None
        for row in self.board:
            for box in row:
                if box.val is not None:
                    continue
                c = box.getChoices()
                n = len(c)
                if n<num:
                    cbox,choices,num = (box,c,n)
        return (cbox,choices)

    def solve(self):
        stack = []
        (box,choices) = self.findMostConstrainedBox()
        tried = set()
        while box is not None:
            if choices is None or len(choices)==0:
                if len(stack)<=0:
                    tkMessageBox.showerror("No Solution",
                                           "No solution exists for this puzzle");
                    return False
                (box,tried) = stack.pop()
                box.unsetVal()
                choices = box.getChoices().difference(tried)
                continue
            c = choices.pop() # pick a random element
            tried.add(c)
            box.setVal(c)
            stack.append( (box,tried) )
            (box,choices) = self.findMostConstrainedBox()
            tried = set()
            
            
class Sudoku(Frame):

    class DialogNew(tkSimpleDialog.Dialog):

        def __init__(self,master,title,nrow=3,ncol=3,maxDim=10):
            self.nrow,self.ncol,self.maxDim = nrow,ncol,maxDim
            tkSimpleDialog.Dialog.__init__(self,master,title=title)

        def body(self, master):
            Label(master, text="Blocks rows:").grid(row=0)
            Label(master, text="Blocks columns:").grid(row=1)

            self.e1 = Spinbox(master,from_=1,to=self.maxDim)
            self.e2 = Spinbox(master,from_=1,to=self.maxDim)
            for i in range(self.nrow-1):
                self.e1.invoke("buttonup")
            for i in range(self.ncol-1):
                self.e2.invoke("buttonup")

            self.e1.grid(row=0, column=1)
            self.e2.grid(row=1, column=1)
            self.result=None
            return self.e1 # initial focus

        def apply(self):
            self.nrow = string.atoi(self.e1.get())
            self.ncol = string.atoi(self.e2.get())
            self.result = self.nrow,self.ncol

    class Buttons(Frame):
        def __init__(self,master):
            Frame.__init__(self,master)
            self.sudoku = master
            c=0
            self.load = Button(master=self,text="New",command=master.new)
            self.load.grid(row=0,column=c); c=c+1
            self.load = Button(master=self,text="Load",command=master.load)
            self.load.grid(row=0,column=c); c=c+1
            self.save = Button(master=self,text="Save",command=master.save)
            self.save.grid(row=0,column=c); c=c+1
            self.solve = Button(master=self,text="Solve",command=master.solve)
            self.solve.grid(row=0,column=c); c=c+1
            Label(master=self,text="   ").grid(row=0,column=c); c=c+1
            self.quit = Button(master=self,text="About",command=master.about)
            self.quit.grid(row=0,column=c); c=c+1
            self.quit = Button(master=self,text="Help",command=master.help)
            self.quit.grid(row=0,column=c); c=c+1
            Label(master=self,text="   ").grid(row=0,column=c); c=c+1
            self.quit = Button(master=self,text="Quit",command=master.quit)
            self.quit.grid(row=0,column=c); c=c+1

            self.grid()


    def __init__(self, master, nrow=3, ncol=3):
        Frame.__init__(self, master)

        self.master = master
        self.master.title(info["name"])
        
        self.nrow = nrow
        self.ncol = ncol
        self.size = nrow*ncol
        self.cellWidth = 65
        self.cellHeight = 60
        self.buttons = self.Buttons(self)
        self.board = Board(self,nrow,ncol,
                           width=self.size*self.cellWidth,
                           height=self.size*self.cellHeight)
        self.board.grid()
        self.grid()

        self.bind_all('<Key>',self.key)
        self.focus = True

        self.lastLoadDir = self.lastSaveDir = ''

    def reset(self):
        self.board.reset()

    def new(self,nrow=None,ncol=None):
        if nrow is None or ncol is None:
            self.focus=False
            x = self.DialogNew(self,title="New puzzle",nrow=self.nrow,ncol=self.ncol)
            self.focus=True
            if x.result is None:
                return
            nrow,ncol = x.result
        maxDim = 10
        if 0<nrow<=maxDim and 0<nrow<=maxDim:
            if self.board is not None:
                self.board.grid_remove()
            self.nrow = nrow
            self.ncol = ncol
            self.size = self.nrow*self.ncol
            self.board = Board(self, nrow, ncol,
                               width=self.size*self.cellWidth,
                               height=self.size*self.cellHeight)
            self.board.grid()
            self.grid()
        
    def save(self,filename=None):
        if filename is None:
            if self.lastSaveDir!="":
                lastdir = self.lastSaveDir
            elif self.lastLoadDir!="":
                lastdir = self.lastLoadDir
            else:
                lastdir = ""
            f = tkFileDialog.asksaveasfile(parent=self,mode='w',
                                           title='Save puzzle to file',
                                           initialdir=lastdir,
                                           filetypes=[('TXT Files','*.txt'),
                                                      ('all files','*.*')])
        else:
            try:
                f = open(filename,'w')
            except:
                print "could not open file", filename
                return False
        if f is None:
            return False

        self.lastSaveDir=f.name

        for i in range(self.size):
            if i>0 and i%self.nrow==0:
                f.write('\n')
            s=''
            for j in range(self.size):
                if j>0 and j%self.ncol==0:
                    s = '%s '%s
                s = '%s%s'%(s,self.board.getChar(i,j))
            f.write('%s\n'%s)
        
    def load(self,filename=None):
        if filename is None:
            if self.lastLoadDir!="":
                lastdir = self.lastLoadDir
            elif self.lastSaveDir!="":
                lastdir = self.lastSaveDir
            else:
                lastdir = ""
            f = tkFileDialog.askopenfile(parent=self,mode='r',
                                         title='Load puzzle from file',
                                         initialdir=lastdir,
                                         filetypes=[('TXT Files','*.txt'),
                                                    ('all files','*.*')])
        else:
            try:
                f = open(filename,'r')
            except:
                print "could not open file", filename
                return False
        if f is None:
            return False

        self.lastLoadDir=f.name

        cells = []
        ncblocks = None
        nrblocks = 0
        ncol = None
        nrow = None
        size = None
        nr = 0
        for line in f:
            blocks=line.split()
            if len(blocks) == 0:
                if (not nrow is None) and nrow != nr:
                    tkMessageBox.showerror("Cannot parse file",
                                           "Cannot parse file: file contains "
                                           "inconsistent number of rows in blocks")
                    return False
                nrow = nr
                if nr > 0:
                    nrblocks += 1
                nr = 0
                continue

            for b in blocks:
                if (not ncol is None) and ncol != len(b):
                    tkMessageBox.showerror("Cannot parse file",
                                           "Cannot parse file: file contains "
                                           "inconsistent number of columns in blocks")
                    return False
                ncol = len(b)
                
            if (not ncblocks is None) and ncblocks != len(blocks):
                tkMessageBox.showerror("Cannot parse file",
                                       "Cannot parse file: file contains "
                                       "inconsistent number of column blocks")
                return False
            
            ncblocks = len(blocks)
            nr += 1
            s = ''.join(blocks)
            if (not size is None) and size != len(s):
                tkMessageBox.showerror("Cannot parse file",
                                       "Cannot parse file: file contains "
                                       "inconsistent number of cells")
                return False
            size = len(s)
            cells.append(s)

        if nr>0:
            if (not nrow is None) and nrow != nr:
                tkMessageBox.showerror("Cannot parse file",
                                       "Cannot parse file: file contains "
                                       "inconsistent number of rows in blocks")
            nrow = nr
            nrblocks += 1

        if size is None or len(cells) != size:
            tkMessageBox.showerror("Cannot parse file",
                                   "Cannot parse file: number of rows and columns "
                                   "don't match: %sx%s"%(len(cells),size))
            return False

        if nrblocks*ncblocks != size:
            tkMessageBox.showerror("Cannot parse file",
                                   "Cannot parse file: inconsistent dimensions: "
                                   "%sx%s!=%s"%(nrblocks,ncblocks,size))

        #print "blocks:",nrblocks,"x",ncblocks
        self.new(nrblocks, ncblocks)
            
        #for cell in cells:
        #    print cell
        for i in range(size):
            for j in range(size):
                if cells[i][j] != '.':
                    self.board.setChar(i,j,cells[i][j])
        self.board.redraw()
        self.board.actionstack = []
            
    def key(self,event):
        #print "key pressed:",event.keysym," char=%r"%event.char

        if not self.focus:
            return

        if event.char in ('\x03','\x11'): # CTRL-C or CTRL-Q
            self.quit()

        if event.char == '\x0e': # CTRL-N
            self.new()
            return
        if event.char == '\x0c': # CTRL-L
            self.load()
            return
        if event.char == '\x13': # CTRL-S
            self.save()
            return
        if event.char == '\x07': # CTRL-G
            self.solve()
            return
        if event.char == '\x12': # CTRL-R
            self.reset()
            return
        if event.char == '\x01': # CTRL-A
            self.about()
            return
        if event.char == '\x08' and event.keysym != 'BackSpace': # CTRL-H
            self.help()
            return
        if event.char == '\x1a': # CTRL-Z
            self.board.undo()
            return

        if self.board.selected is not None:
            if event.keysym == 'Escape':
                self.board.unselectBox()
                return
            elif event.keysym == 'Delete':
                i,j = self.board.selected.ind
                self.board.unsetVal(i,j)
            elif event.char == '\t': # CTRL-I
                self.board.selected.box.printInfo()
                return
            elif ((len(event.char)==1 and event.char!='.')
                  or event.keysym == 'Return'
                  or event.keysym == 'BackSpace'):
                self.board.selected.addChar(event.char)
                return
                return

        if event.keysym in ('Tab','Left','Right','Up','Down'):
            self.board.selectBoxByKey(event.keysym)
            return
                
        if event.keysym in ('Escape','q','Q'):
            self.quit()

    def quit(self):
        sys.exit()
        
    def solve(self):
        self.board.solve()

    def about(self):
        tkMessageBox.showinfo(
            "About",
            "%s v%s\n\n"
            "Code written by %s (%s).\n"
            "This program is published under the GNU public licence "
            "and can be obtained at %s.  "
            "If you make any modifications/improvements I would "
            "appreciate if you could email me a copy of the new version.  "
            "Thanks! :)\n\n"%(info["name"],info["version"],info["author"],
                              info["email"],info["homepage"])
            )
        
    def help(self):
        tkMessageBox.showinfo(
            "Help",
            "An overview of Sudoku and how it is played can be found at "
            "http://en.wikipedia.org/wiki/Sudoku\n\n"
            "Using the buttons at the top you can create new Sudoku games "
            "of various sizes, load and save a game to a text file, and "
            "solve the current puzzle (only one solution is found even "
            "if multiple solutions exist).\n\n"
            "To enter a value (numbers or letters) into a cell or change "
            "the value, you simply select a box/cell "
            "using the left mouse button or the arrow "
            "keys, type the value you wish to enter, and hit the Return/Enter "
            "key or select a different box using the mouse or one of "
            "the arrow keys.  If the value you entered does not meet the "
            "constraints of the game then the value is not entered.  The "
            "possible values (alphabet) that can be entered are shown in "
            "small font in the cell.  By default only numbers are shown, "
            "but if letters are entered and the alphabet (the set of possible "
            "values) is not fully defined yet by the values already entered "
            "on the board then the new value is allowed and the alphabet is "
            "adjusted.\n\n"
            "To delete a value you select the cell using the middle mouse "
            "button or once the box is selected press the Delete key\n\n"
            "To unselect a box you can press the Escape key or use the "
            "right mouse button."
            )

root = Tk()
app = Sudoku(root, 3,3)
root.mainloop()
