root/05/release-0.5i/board1.py

Revision 41, 24.6 kB (checked in by ug, 5 years ago)

Updated copyright notice

Line 
1 # file: board1.py
2
3 ##   This file is part of Kombilo, a go database program
4 ##   It contains classes implementing an abstract go board and a go
5 ##   board displayed on the screen.
6
7 ##   Copyright (C) 2001-4 Ulrich Goertz (u@g0ertz.de)
8
9 ##   This program is free software; you can redistribute it and/or modify
10 ##   it under the terms of the GNU General Public License as published by
11 ##   the Free Software Foundation; either version 2 of the License, or
12 ##   (at your option) any later version.
13
14 ##   This program is distributed in the hope that it will be useful,
15 ##   but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ##   GNU General Public License for more details.
18
19 ##   You should have received a copy of the GNU General Public License
20 ##   along with this program (gpl.txt); if not, write to the Free Software
21 ##   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22 ##   The GNU GPL is also currently available at
23 ##   http://www.gnu.org/copyleft/gpl.html
24
25
26 from Tkinter import *
27 from whrandom import randint
28 import math
29 import sys
30 import os
31
32 try:   # PIL installed?
33     import GifImagePlugin
34     import Image
35     import ImageTk
36     import ImageEnhance
37 except:
38     pass
39
40
41 class abstractBoard:
42     """ This class administrates a go board.
43         It keeps track of the stones currently on the board in the dictionary self.status,
44         and of the moves played so far in self.undostack
45
46         It has methods to clear the board, play a stone, undo a move. """
47
48     def __init__(self, boardSize = 19):
49         self.status = {}
50         self.undostack = []
51         self.boardSize = boardSize
52
53     def neighbors(self,x):
54         """ Returns the coordinates of the 4 (resp. 3 resp. 2 at the side / in the corner) intersections
55             adjacent to the given one. """
56         if   x[0]== 1              :     l0 = [2]
57         elif x[0]== self.boardSize :     l0 = [self.boardSize-1]
58         else:                            l0 = [x[0]-1, x[0]+1]
59
60         if   x[1]== 1              :     l1 = [2]
61         elif x[1]== self.boardSize :     l1 = [self.boardSize-1]
62         else:                            l1 = [x[1]-1, x[1]+1]
63
64         l = []
65         for i in l0: l.append((i,x[1]))
66         for j in l1: l.append((x[0],j))
67
68         return l
69
70     def clear(self):
71         """ Clear the board """
72         self.status = {}
73         self.undostack=[]       
74
75     def play(self,pos,color):
76         """ This plays a color=black/white stone at pos, if that is a legal move
77             (disregarding ko), and deletes stones captured by that move.
78             It returns 1 if the move has been played, 0 if not. """
79
80         if self.status.has_key(pos):                # check if empty
81             return 0
82
83         l = self.legal(pos,color)
84         if l:                                       # legal move?
85             captures = l[1]
86             for x in captures: del self.status[x]   # remove captured stones, if any
87             self.undostack.append((pos,color,captures))   # remember move + captured stones for easy undo
88             return 1
89         else: return 0
90
91     def legal(self, pos, color):
92         """ Check if a play by color at pos would be a legal move. """
93         c = [] # captured stones
94         for x in self.neighbors(pos):
95             if self.status.has_key(x) and self.status[x]==self.invert(color):
96                 c = c + self.hasNoLibExcP(x, pos)       
97
98         self.status[pos]=color
99
100         if c:
101             captures = []
102             for x in c:
103                 if not x in captures: captures.append(x)
104             return (1, captures)
105
106         if self.hasNoLibExcP(pos):
107             del self.status[pos]
108             return 0
109         else: return (1, [])
110
111     def hasNoLibExcP(self, pos, exc = None):
112         """ This function checks if the string (=solidly connected) of stones containing
113             the stone at pos has a liberty (resp. has a liberty besides that at exc).
114             If no liberties are found, a list of all stones in the string is returned.
115
116             The algorithm is a non-recursive  implementation of a simple flood-filling:
117             starting from the stone at pos, the main while-loop looks at the intersections
118             directly adjacent to the stones found so far, for liberties or other stones that belong
119             to the string. Then it looks at the neighbors of those newly found stones, and so
120             on, until it finds a liberty, or until it doesn't find any new stones belonging
121             to the string, which means that there are no liberties.
122             Once a liberty is found, the function returns immediately. """
123            
124         st = []            # in the end, this list will contain all stones solidly connected to the
125                            # one at pos, if this string has no liberties
126         newlyFound = [pos] # in the while loop, we will look at the neighbors of stones in newlyFound
127         foundNew = 1
128        
129         while foundNew:
130             foundNew = 0
131             n = []         # this will contain the stones found in this iteration of the loop
132             for x in newlyFound:
133                 for y in self.neighbors(x):
134                     if not self.status.has_key(y) and y != exc:    # found a liberty
135                         return []
136                     elif self.status.has_key(y) and self.status[y]==self.status[x] \
137                          and not y in st and not y in newlyFound: # found another stone of same color
138                         n.append(y)
139                         foundNew = 1
140
141             st[:0] = newlyFound
142             newlyFound = n
143
144         return st     # no liberties found, return list of all stones connected to the original one
145
146     def undo(self, no=1):
147         """ Undo the last no moves. """
148         for i in range(no):
149             if self.undostack:
150                 pos, color, captures = self.undostack.pop()
151                 del self.status[pos]
152                 for p in captures: self.status[p] = self.invert(color)
153
154     def remove(self, pos, removeFromUndostack=0):
155         """Remove the stone at pos, if not removeFromUndostack, append this as capture to undostack.
156         Otherwise, find the placement of this stone in undostack, and remove it from there."""
157        
158         if removeFromUndostack:
159             for i in range(len(self.undostack), 0, -1):
160                 if pos == self.undostack[i-1][0]:
161                     self.undostack[i-1:i] = []
162         else:
163             self.undostack.append(((-1,-1), self.invert(self.status[pos]), [pos]))
164         del self.status[pos]
165
166
167     def invert(self,color):
168         if color == "black": return "white"
169         else:                return "black"
170
171
172 class Board(abstractBoard, Canvas):
173     """ This is a go board, displayed on the associated canvas.
174         canvasSize is a pair, the first entry is the size of the border, the second
175         entry is the distance between two go board lines, both in pixels.
176
177         The most important methods are:
178
179         - play: play a stone of some color at some position (if that is a legal move)
180         - undo: undo one (or several) moves
181         - state: activate (state("normal", f) - the function f is called when a stone
182                  is placed on the board) or disable (state("disabled")) the board;
183
184         - placeMark: place a colored label (slightly smaller than a stone) at some position
185         - delMarks: delete all these labels
186         - placeLabel: place a label (a letter, a circle, square, triangle or cross)
187     """
188
189     def __init__(self, master, boardSize = 19, canvasSize = (30,25), fuzzy=1, labelFont = None,
190                  focus=1, callOnChange=None, boardImg=None, blackImg=None, whiteImg=None):
191
192         self.focus = focus
193         self.coordinates = 0
194        
195         self.canvasSize = canvasSize
196         size = 2*canvasSize[0] + (boardSize-1)*canvasSize[1] # size of the canvas in pixel
197         Canvas.__init__(self, master, height = size, width =  size, highlightthickness = 0)
198
199         abstractBoard.__init__(self, boardSize)
200
201         self.changed = IntVar()  # this is set to 1 whenever a change occurs (placing stone, label etc.)
202         self.changed.set(0)      # this is used for Kombilo's 'back' method
203
204         if callOnChange: self.callOnChange = callOnChange
205         else: self.callOnChange = lambda: None
206         self.noChanges = 0
207
208         self.fuzzy = IntVar()   # if self.fuzzy is true, the stones are not placed precisely
209         self.fuzzy.set(fuzzy)   # on the intersections, but randomly a pixel off
210
211         if labelFont:
212             self.labelFont = labelFont
213         else:
214             self.labelFont = (StringVar(), IntVar(), StringVar())
215             self.labelFont[0].set('Helvetica')
216             self.labelFont[1].set(5)
217             self.labelFont[2].set('bold')
218            
219         self.shadedStoneVar = IntVar()  # if this is true, there is a 'mouse pointer' showing
220         self.shadedStonePos = (-1,-1)   # where the next stone would be played, given the current
221                                         # mouse position
222
223         self.currentColor = "black"     # the expected color of the next move
224
225         self.stones = {}            # references to the ovals placed on the canvas, used for removing stones
226         self.marks = {}             # references to the (colored) marks on the canvas
227         self.labels = {}
228        
229         self.boundConf = self.bind("<Configure>", self.resize)
230         self.resizable = 1
231
232         self.PILinstalled = 0
233
234         self.use3Dstones = IntVar()
235         self.use3Dstones.set(1)
236        
237         if boardImg: self.img = boardImg
238         else:        self.img = None
239
240         if blackImg and whiteImg:
241             self.PILinstalled = 1
242             self.blackStone = blackImg
243             self.whiteStone = whiteImg
244
245         self.drawBoard()
246
247
248     def drawBoard(self):
249         """ Displays the background picture, and draws the lines and hoshi points of
250             the go board.
251             If PIL is installed, this also creates the PhotImages for black, white stones. """
252
253         self.delete('non-bg')     # delete everything except for background image
254         c0, c1 = self.canvasSize
255         size = 2*c0 + (self.boardSize-1)*c1
256         self.config(height=size, width=size)
257        
258         if self.img:
259             self.delete('board')
260             for i in range(size/100 + 1):
261                 for j in range(size/100 + 1):
262                     self.create_image(100*i,100*j,image=self.img, tags='board')
263
264         color = 'black'
265
266         for i in range(self.boardSize):
267             self.create_line(c0, c0 + c1*i, c0 + (self.boardSize-1)*c1, c0 + c1*i, fill=color, tags='non-bg')
268             self.create_line(c0 + c1*i, c0, c0 + c1*i, c0+(self.boardSize-1)*c1, fill=color, tags='non-bg')
269
270         # draw hoshi's:
271
272         if c1 > 7:
273
274             if self.boardSize in [13,19]:
275                 b = (self.boardSize-7)/2
276                 for i in range(3):
277                     for j in range(3):
278                         self.create_oval(c0 + (b*i+3)*c1 - 2, c0 + (b*j+3)*c1 - 2,
279                                          c0 + (b*i+3)*c1 + 2, c0 + (b*j+3)*c1 + 2, fill = "black", tags='non-bg')
280             elif self.boardSize == 9:
281                 self.create_oval(c0 + 4*c1 - 2, c0 + 4*c1 - 2,
282                                  c0 + 4*c1 + 2, c0 + 4*c1 + 2, fill = "black", tags='non-bg')
283
284         # draw coordinates:
285
286         if self.coordinates:
287             for i in range(self.boardSize):
288                 a = 'ABCDEFGHJKLMNOPQRST'[i]
289                 self.create_text(c0 + c1*i, c1*self.boardSize+3*c0/4+4, text=a,
290                                  font = ('Helvetica', 5+c1/7, 'bold'))
291                 self.create_text(c0 + c1*i, c0/4+1, text=a, font = ('Helvetica', 5+c1/7, 'bold'))
292                 self.create_text(c0/4+1, c0+c1*i, text=`self.boardSize-i`,font = ('Helvetica', 5+c1/7, 'bold'))
293                 self.create_text(c1*self.boardSize+3*c0/4+4, c0 + c1*i, text=`self.boardSize-i`, font = ('Helvetica', 5+c1/7, 'bold'))
294                
295
296         if self.PILinstalled:
297             try:
298                 self.bStone = ImageTk.PhotoImage(self.blackStone.resize((c1+1,c1+1)))
299                 self.wStone = ImageTk.PhotoImage(self.whiteStone.resize((c1+1,c1+1)))
300             except:
301                 self.PILinstalled = 0
302
303
304
305
306     def resize(self, event = None):
307         """ This is called when the window containing the board is resized. """
308
309         if not self.resizable: return
310
311         self.noChanges = 1
312
313         if event: w, h = event.width, event.height
314         else:     w, h = int(self.cget('width')), int(self.cget('height'))
315         m = min(w,h)
316
317         self.canvasSize = (m/20 + 4, (m - 2*(m/20+4))/(self.boardSize-1))
318
319         self.drawBoard()
320            
321
322         # place a gray rectangle over the board background picture
323         # in order to make the board quadratic
324
325         self.create_rectangle(h+1, 0, h+1000, w+1000,
326                               fill ='grey88', outline='', tags='non-bg')     
327         self.create_rectangle(0, w+1, h+1000, w+1000,
328                               fill='grey88', outline='', tags='non-bg')
329
330            
331         for x in self.status.keys(): self.placeStone(x, self.status[x])
332         for x in self.marks.keys(): self.placeMark(x, self.marks[x])
333         for x in self.labels.keys(): self.placeLabel(x, '+'+self.labels[x][0], self.labels[x][1])
334
335         self.tkraise('sel') # this is for the list of previous search patterns ...
336
337         self.noChanges = 0
338
339     def play(self, pos, color=None):
340         """ Play a stone of color (default is self.currentColor) at pos. """
341
342         if color is None: color = self.currentColor
343         if abstractBoard.play(self, pos, color):                    # legal move?
344             captures = self.undostack[len(self.undostack)-1][2]     # retrieve list of captured stones
345             for x in captures:
346                 self.delete(self.stones[x])
347                 del self.stones[x]
348             self.placeStone(pos, color)
349             self.currentColor = self.invert(color)
350             self.delShadedStone()
351             return 1
352         else: return 0
353
354     def state(self, s, f=None):
355         """ s in "normal", "disabled": accepting moves or not
356             f the function to call if a move is entered
357             [More elegant solution might be to replace this by an overloaded bind method,
358             for some event "Move"?!]  """
359
360         if s == "normal":
361             self.callOnMove = f
362             self.bound1 = self.bind("<Button-1>", self.onMove) 
363             self.boundm = self.bind("<Motion>", self.shadedStone)
364             self.boundl = self.bind("<Leave>", self.delShadedStone)
365         elif s == "disabled":
366             self.delShadedStone()
367             try:
368                 self.unbind("<Button-1>", self.bound1)
369                 self.unbind("<Motion>", self.boundm)
370                 self.unbind("<Leave>", self.boundl)
371             except TclError: pass                     # if board was already disabled, unbind will fail
372             
373     def onMove(self, event):
374         # compute board coordinates from the pixel coordinates of the mouse click
375
376         if self.focus:
377             self.master.focus()
378         x,y = self.getBoardCoord((event.x, event.y), self.shadedStoneVar.get())
379         if (not x*y): return
380
381         if abstractBoard.play(self,(x,y), self.currentColor): # would this be a legal move?
382             abstractBoard.undo(self)
383             self.callOnMove((x,y))
384
385     def onChange(self):
386         if self.noChanges: return
387         self.callOnChange()
388         self.changed.set(1)
389
390
391     def getPixelCoord(self, pos, nonfuzzy = 0):
392         """ transform go board coordinates into pixel coord. on the canvas of size canvSize """
393
394         fuzzy1 = randint(-1,1) * self.fuzzy.get() * (1-nonfuzzy)
395         fuzzy2 = randint(-1,1) * self.fuzzy.get() * (1-nonfuzzy)
396         c1 = self.canvasSize[1]
397         a = self.canvasSize[0] - self.canvasSize[1] - self.canvasSize[1]/2
398         b = self.canvasSize[0] - self.canvasSize[1] + self.canvasSize[1]/2
399         return (c1*pos[0]+a+fuzzy1, c1*pos[1]+a+fuzzy2, c1*pos[0]+b+fuzzy1, c1*pos[1]+b+fuzzy2)
400
401     def getBoardCoord(self, pos, sloppy=1):
402         """ transform pixel coordinates on canvas into go board coord. in [1,..,boardSize]x[1,..,boardSize]
403             sloppy refers to how far the pixel may be from the intersection in order to
404             be accepted """
405
406         if sloppy: a, b = self.canvasSize[0]-self.canvasSize[1]/2, self.canvasSize[1]-1
407         else:      a, b = self.canvasSize[0]-self.canvasSize[1]/4, self.canvasSize[1]/2
408
409         if (pos[0]-a)%self.canvasSize[1] <= b: x = (pos[0]-a)/self.canvasSize[1] + 1
410         else:                                  x = 0
411        
412         if (pos[1]-a)%self.canvasSize[1] <= b: y = (pos[1]-a)/self.canvasSize[1] + 1
413         else:                  y = 0
414
415         if x<0 or y<0 or x>self.boardSize or y>self.boardSize: x = y = 0
416
417         return (x,y)   
418
419     def placeMark(self, pos, color):
420         """ Place colored mark at pos. """
421         x1, x2, y1, y2 = self.getPixelCoord(pos, 1)
422         self.create_oval(x1+2, x2+2, y1-2, y2-2, fill = color, tags=('marks','non-bg'))
423         self.marks[pos]=color
424         self.onChange()
425
426     def delMarks(self):
427         """ Delete all marks. """
428         if self.marks: self.onChange()
429         self.marks = {}
430         self.delete('marks')
431
432     def delLabels(self):
433         """ Delete all labels. """
434         if self.labels: self.onChange()
435         self.labels={}
436         self.delete('label')
437
438     def remove(self, pos, removeFromUndostack = 0):
439         """ Remove the stone at pos, if not removeFromUndostack, append this as capture to undostack.
440         Otherwise, find the placement of this stone in undostack, and remove it from there."""
441         if self.status.has_key(pos):
442             self.onChange()
443             self.delete(self.stones[pos])
444             del self.stones[pos]
445             abstractBoard.remove(self, pos, removeFromUndostack)
446             self.update_idletasks()
447             return 1
448         else: return 0
449
450     def placeLabel(self, pos, type, text=None, color=None, override=None):
451         """ Place label of type type at pos; used to display labels
452             from SGF files. If type has the form +XX, add a label of type XX.
453             Otherwise, add or delete the label, depending on if there is no label at pos,
454             or if there is one."""
455
456         if type[0] != '+':
457
458             if self.labels.has_key(pos):
459                 if self.labels[pos][0] == type:
460                     for item in self.labels[pos][2]: self.delete(item)
461                     del self.labels[pos]
462                     return
463                 else:
464                     for item in self.labels[pos][2]: self.delete(item)
465                     del self.labels[pos]
466
467             self.onChange()
468
469         else: type = type[1:]
470
471         labelIDs = []
472
473         if override:
474             fcolor = override[0]
475             fcolor2 = override[1]
476         elif self.status.has_key(pos) and self.status[pos]=='black':
477             fcolor = 'white'
478             fcolor2 = ''
479         elif self.status.has_key(pos) and self.status[pos]=='white':
480             fcolor = 'black'
481             fcolor2 = ''
482         else:
483             fcolor = color or 'black'
484             fcolor2 = '#D8A542'
485                    
486         x1, x2, y1, y2 = self.getPixelCoord(pos, 1)
487         if type == 'LB':
488             labelIDs.append(self.create_oval(x1+3, x2+3, y1-3, y2-3, fill=fcolor2, outline='',
489                                              tags=('label', 'non-bg')))
490             labelIDs.append(self.create_text((x1+y1)/2,(x2+y2)/2, text=text, fill=fcolor,
491                                              font = (self.labelFont[0].get(), self.labelFont[1].get() + self.canvasSize[1]/5,
492                                                      self.labelFont[2].get()),
493                                              tags=('label', 'non-bg')))
494         elif type == 'SQ':
495             labelIDs.append(self.create_rectangle(x1+6, x2+6, y1-6, y2-6,
496                                                   fill='', outline = fcolor, tags=('label','non-bg')))
497         elif type == 'CR':
498             labelIDs.append(self.create_oval(x1+5, x2+5, y1-5, y2-5, fill='',
499                                              outline=fcolor, tags=('label','non-bg')))
500         elif type == 'TR':
501             labelIDs.append(self.create_polygon((x1+y1)/2, x2+5, x1+5, y2-5, y1-5, y2-5,
502                                                 fill='', outline = fcolor,
503                                                 tags = ('label', 'non-bg')))
504         elif type == 'MA':
505             labelIDs.append(self.create_oval(x1+2, x2+2, y1-2, y2-2, fill=fcolor2, outline='',
506                              tags=('label', 'non-bg')))
507             labelIDs.append(self.create_text(x1+12,x2+12, text='X', fill=fcolor,
508                                              font = (self.labelFont[0].get(), self.labelFont[1].get() + 1 + self.canvasSize[1]/5,
509                                                      self.labelFont[2].get()),
510                                              tags=('label', 'non-bg')))
511            
512         self.labels[pos] = (type, text, labelIDs)
513
514
515            
516     def placeStone(self, pos, color):
517         self.onChange()
518         p = self.getPixelCoord(pos)
519         if not self.use3Dstones.get() or not self.PILinstalled or self.canvasSize[1] <= 7:
520             self.stones[pos] = self.create_oval(p, fill=color, tags='non-bg')
521         else:
522             if color=='black': self.stones[pos] = self.create_image(((p[0]+p[2])/2, (p[1]+p[3])/2),
523                                                                     image=self.bStone, tags='non-bg')
524             elif color=='white': self.stones[pos] = self.create_image(((p[0]+p[2])/2, (p[1]+p[3])/2),
525                                                                       image=self.wStone, tags='non-bg')
526            
527     def undo(self, no=1, changeCurrentColor=1):
528         """ Undo the last no moves. """
529
530         for i in range(no):
531             if self.undostack:
532                 self.onChange()
533                 pos, color, captures = self.undostack.pop()
534                 if self.status.has_key(pos):
535                     del self.status[pos]
536                     self.delete(self.stones[pos])
537                     del self.stones[pos]
538                 for p in captures:
539                     self.placeStone(p, self.invert(color))
540                     self.status[p] = self.invert(color)
541                 # self.update_idletasks()
542                 if changeCurrentColor:
543                     self.currentColor = self.invert(self.currentColor)
544
545     def clear(self):
546         """ Clear the board. """
547         abstractBoard.clear(self)
548         for x in self.stones.keys():
549             self.delete(self.stones[x])
550         self.stones = {}
551         self.onChange()
552
553     def ptOnCircle(self, size, degree):
554         radPerDeg = math.pi/180
555         r = size/2
556         x = int(r*math.cos((degree-90)*radPerDeg) + r)
557         y = int(r*math.sin((degree-90)*radPerDeg) + r)
558         return (x,y)
559
560     def shadedStone(self, event):
561         x, y = self.getBoardCoord((event.x, event.y), 1)
562         if (x,y) == self.shadedStonePos: return     # nothing changed
563
564         self.delShadedStone()
565
566         if (x*y) and self.shadedStoneVar.get() and abstractBoard.play(self, (x,y), self.currentColor):
567             abstractBoard.undo(self)
568
569             if sys.platform[:3]=='win':     # 'stipple' is ignored under windows for
570                                             # create_oval, so we'll draw a polygon ...
571                 l = self.getPixelCoord((x,y),1)
572                 m = []
573
574                 for i in range(18):
575                     help = self.ptOnCircle(l[2]-l[0], i*360/18)
576                     m.append(help[0]+l[0])
577                     m.append(help[1]+l[1])
578                  
579                 self.create_polygon(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9],
580                                     m[10], m[11], m[12], m[13], m[14], m[15], m[16], m[17],
581                                     m[18], m[19], m[20], m[21], m[22], m[23], m[24], m[25],
582                                     m[26], m[27], m[28], m[29], m[30], m[31], m[32], m[33],
583                                     m[34], m[35],
584                                     fill=self.currentColor, stipple='gray50',
585                                     outline='', tags=('shaded','non-bg') )
586             else:
587                 self.create_oval(self.getPixelCoord((x,y), 1), fill=self.currentColor, stipple='gray50',
588                                  outline='', tags=('shaded','non-bg'))
589
590             self.shadedStonePos = (x,y)
591    
592     def delShadedStone(self, event=None):
593         self.delete('shaded')
594         self.shadedStonePos = (-1,-1)
595
596     def fuzzyStones(self):
597         """ switch fuzzy/non-fuzzy stone placement according to self.fuzzy """
598         for p in self.status.keys():
599             self.delete(self.stones[p])
600             del self.stones[p]
601             self.placeStone(p, self.status[p])
602         self.tkraise('marks')
603         self.tkraise('label')
Note: See TracBrowser for help on using the browser.