root/04/release-0.4e/board1.py

Revision 21, 20.3 kB (checked in by ug, 5 years ago)

Import release 0.4d

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-2 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
33 class abstractBoard:
34     """ This class administrates a go board.
35         It keeps track of the stones currently on the board in the dictionary self.status,
36         and of the moves played so far in self.undostack
37
38         It has methods to clear the board, play a stone, undo a move. """
39
40     def __init__(self, boardSize = 19):
41         self.status = {}
42         self.undostack = []
43         self.boardSize = boardSize
44
45     def neighbors(self,x):
46         """ Returns the coordinates of the 4 (resp. 3 resp. 2 at the side / in the corner) intersections
47             adjacent to the given one. """
48         if   x[0]== 1              :     l0 = [2]
49         elif x[0]== self.boardSize :     l0 = [self.boardSize-1]
50         else:                            l0 = [x[0]-1, x[0]+1]
51
52         if   x[1]== 1              :     l1 = [2]
53         elif x[1]== self.boardSize :     l1 = [self.boardSize-1]
54         else:                            l1 = [x[1]-1, x[1]+1]
55
56         l = []
57         for i in l0: l.append((i,x[1]))
58         for j in l1: l.append((x[0],j))
59
60         return l
61
62     def clear(self):
63         """ Clear the board """
64         self.status = {}
65         self.undostack=[]       
66
67     def play(self,pos,color):
68         """ This plays a color=black/white stone at pos, if that is a legal move
69             (disregarding ko), and deletes stones captured by that move.
70             It returns 1 if the move has been played, 0 if not. """
71
72         if self.status.has_key(pos):                # check if empty
73             return 0
74
75         l = self.legal(pos,color)
76         if l:                                       # legal move?
77             captures = l[1]
78             for x in captures: del self.status[x]   # remove captured stones, if any
79             self.undostack.append((pos,color,captures))   # remember move + captured stones for easy undo
80             return 1
81         else: return 0
82
83     def legal(self, pos, color):
84         """ Check if a play by color at pos would be a legal move. """
85         c = [] # captured stones
86         for x in self.neighbors(pos):
87             if self.status.has_key(x) and self.status[x]==self.invert(color):
88                 c = c + self.hasNoLibExcP(x, pos)       
89
90         self.status[pos]=color
91
92         if c:
93             captures = []
94             for x in c:
95                 if not x in captures: captures.append(x)
96             return (1, captures)
97
98         if self.hasNoLibExcP(pos):
99             del self.status[pos]
100             return 0
101         else: return (1, [])
102
103     def hasNoLibExcP(self, pos, exc = None):
104         """ This function checks if the string (=solidly connected) of stones containing
105             the stone at pos has a liberty (resp. has a liberty besides that at exc).
106             If no liberties are found, a list of all stones in the string is returned.
107
108             The algorithm is a non-recursive  implementation of a simple flood-filling:
109             starting from the stone at pos, the main while-loop looks at the intersections
110             directly adjacent to the stones found so far, for liberties or other stones that belong
111             to the string. Then it looks at the neighbors of those newly found stones, and so
112             on, until it finds a liberty, or until it doesn't find any new stones belonging
113             to the string, which means that there are no liberties.
114             Once a liberty is found, the function returns immediately. """
115            
116         st = []            # in the end, this list will contain all stones solidly connected to the
117                            # one at pos, if this string has no liberties
118         newlyFound = [pos] # in the while loop, we will look at the neighbors of stones in newlyFound
119         foundNew = 1
120        
121         while foundNew:
122             foundNew = 0
123             n = []         # this will contain the stones found in this iteration of the loop
124             for x in newlyFound:
125                 for y in self.neighbors(x):
126                     if not self.status.has_key(y) and y != exc:    # found a liberty
127                         return []
128                     elif self.status.has_key(y) and self.status[y]==self.status[x] \
129                          and not y in st and not y in newlyFound: # found another stone of same color
130                         n.append(y)
131                         foundNew = 1
132
133             st[:0] = newlyFound
134             newlyFound = n
135
136         return st     # no liberties found, return list of all stones connected to the original one
137
138     def undo(self, no=1):
139         """ Undo the last no moves. """
140         for i in range(no):
141             if self.undostack:
142                 pos, color, captures = self.undostack.pop()
143                 del self.status[pos]
144                 for p in captures: self.status[p] = self.invert(color)
145
146     def remove(self, pos):
147         """ Remove a stone form the board, and store this action in undostack. """
148        
149         self.undostack.append(((-1,-1), self.invert(self.status[pos]), [pos]))
150         del self.status[pos]
151
152     def invert(self,color):
153         if color == "black": return "white"
154         else:                return "black"
155
156
157 class Board(abstractBoard, Canvas):
158     """ This is a go board, displayed on the associated canvas.
159         canvasSize is a pair, the first entry is the size of the border, the second
160         entry is the distance between two go board lines, both in pixels.
161
162         The most important methods are:
163
164         - play: play a stone of some color at some position (if that is a legal move)
165         - undo: undo one (or several) moves
166         - state: activate (state("normal", f) - the function f is called when a stone
167                  is placed on the board) or disable (state("disabled")) the board;
168
169         - placeMark: place a colored label (slightly smaller than a stone) at some position
170         - delMarks: delete all these labels
171         - placeLabel: place a label (a letter, a circle, square, triangle or cross)
172     """
173
174     def __init__(self, master, boardSize = 19, canvasSize = (30,25), fuzzy=1, labelFont = None, focus=1):
175
176         self.focus = focus
177        
178         self.canvasSize = canvasSize
179        
180         size = 2*canvasSize[0] + (boardSize-1)*canvasSize[1] # size of the canvas in pixel
181         Canvas.__init__(self, master, height = size, width =  size)
182
183         abstractBoard.__init__(self, boardSize)
184
185         self.changed = IntVar()  # this is set to 1 whenever a change occurs (placing stone, label etc.)
186         self.changed.set(0)      # this is used for Kombilo's 'back' method
187
188         self.fuzzy = IntVar()   # if self.fuzzy is true, the stones are not placed precisely
189         self.fuzzy.set(fuzzy)   # on the intersections, but randomly a pixel off
190
191         if labelFont:
192             self.labelFont = labelFont
193         else:
194             self.labelFont = (StringVar(), IntVar(), StringVar())
195             self.labelFont[0].set('Helvetica')
196             self.labelFont[1].set(5)
197             self.labelFont[2].set('bold')
198            
199         self.shadedStoneVar = IntVar()  # if this is true, there is a 'mouse pointer' showing
200         self.shadedStonePos = (-1,-1)   # where the next stone would be played, given the current
201                                         # mouse position
202
203         self.currentColor = "black"     # the expected color of the next move
204
205         self.stones = {}            # references to the ovals placed on the canvas, used for removing stones
206         self.marks = {}             # references to the (colored) marks on the canvas
207         self.labels = {}
208        
209         self.bind("<Configure>", self.resize)
210         self.resize = 0
211
212         try:
213             gifpath = os.path.join(os.path.join(sys.path[0],'gifs'),'board.gif')
214             self.img = PhotoImage(file=gifpath)
215         except TclError:
216             self.img = None
217
218         self.drawBoard()
219
220
221     def drawBoard(self):
222         """ Displays the background picture, and draws the lines and hoshi points of
223             the go board. """
224
225         self.delete('non-bg')     # delete everything except for background image
226         c0, c1 = self.canvasSize
227
228         if self.img:
229             size = 2*c0 + (self.boardSize-1)*c1
230             self.delete('board')
231             for i in range(size/100 + 1):
232                 for j in range(size/100 + 1):
233                     self.create_image(100*i,100*j,image=self.img, tags='board')
234        
235         for i in range(self.boardSize):
236             self.create_line(c0, c0 + c1*i, c0 + (self.boardSize-1)*c1, c0 + c1*i, tags='non-bg')
237             self.create_line(c0 + c1*i, c0, c0 + c1*i, c0+(self.boardSize-1)*c1, tags='non-bg')
238
239         # draw hoshi's:
240
241         if self.boardSize in [13,19]:
242             b = (self.boardSize-7)/2
243             for i in range(3):
244                 for j in range(3):
245                     self.create_oval(c0 + (b*i+3)*c1 - 2, c0 + (b*j+3)*c1 - 2,
246                                           c0 + (b*i+3)*c1 + 2, c0 + (b*j+3)*c1 + 2, fill = "black", tags='non-bg')
247         elif self.boardSize == 9:
248             self.create_oval(c0 + 4*c1 - 2, c0 + 4*c1 - 2,
249                                   c0 + 4*c1 + 2, c0 + 4*c1 + 2, fill = "black", tags='non-bg')
250
251
252
253     def resize(self, event):
254         """ This is called when the window containing the board is resized. """
255        
256         m = min(event.width, event.height)
257      
258         self.canvasSize = (m/20 + 5, (m - 2*(m/20+5))/(self.boardSize-1))
259         self.drawBoard()
260
261         # place a gray rectangle over the board background picture
262         # in order to make the board quadratic
263
264         if event.width >= event.height:
265             self.create_rectangle(event.height+1,0, event.width,event.width,
266                                   fill ='grey88', outline='', tags='non-bg')     
267         else:
268             self.create_rectangle(0,event.width+1, event.height, event.height,
269                                   fill='grey88', outline='', tags='non-bg')
270            
271         for x in self.status.keys(): self.placeStone(x, self.status[x])
272         for x in self.marks.keys(): self.placeMark(x, self.marks[x])
273         for x in self.labels.keys(): self.placeLabel(x, self.labels[x][0], self.labels[x][1])
274
275
276     def play(self, pos, color=None):
277         """ Play a stone of color (default is self.currentColor) at pos. """
278
279         if color is None: color = self.currentColor
280         if abstractBoard.play(self, pos, color):                    # legal move?
281             captures = self.undostack[len(self.undostack)-1][2]     # retrieve list of captured stones
282             for x in captures:
283                 self.delete(self.stones[x])
284                 del self.stones[x]
285             self.placeStone(pos, color)
286             self.currentColor = self.invert(color)
287             self.delShadedStone()
288             return 1
289         else: return 0
290
291     def state(self, s, f=None):
292         """ s in "normal", "disabled": accepting moves or not
293             f the function to call if a move is entered
294             [More elegant solution might be to replace this by an overloaded bind method,
295             for some event "Move"?!]  """
296
297         if s == "normal":
298             self.callOnMove = f
299             self.bind("<Button-1>", self.onMove) 
300             self.bind("<Motion>", self.shadedStone)
301             self.bind("<Leave>", self.delShadedStone)
302         elif s == "disabled":
303             self.delShadedStone()
304             self.unbind("<Button-1>")
305             self.unbind("<Motion>")
306             self.unbind("<Leave>")
307            
308     def onMove(self, event):
309         # compute board coordinates from the pixel coordinates of the mouse click
310
311         if self.focus:
312             self.master.focus()
313         x,y = self.getBoardCoord((event.x, event.y), self.shadedStoneVar.get())
314         if (not x*y): return
315
316         if abstractBoard.play(self,(x,y), self.currentColor): # would this be a legal move?
317             abstractBoard.undo(self)
318             self.callOnMove((x,y))
319
320     def getPixelCoord(self, pos, nonfuzzy = 0):
321         """ transform go board coordinates into pixel coord. on the canvas of size canvSize """
322
323         fuzzy1 = randint(-1,1) * self.fuzzy.get() * (1-nonfuzzy)
324         fuzzy2 = randint(-1,1) * self.fuzzy.get() * (1-nonfuzzy)
325         c1 = self.canvasSize[1]
326         a = self.canvasSize[0] - self.canvasSize[1] - self.canvasSize[1]/2
327         b = self.canvasSize[0] - self.canvasSize[1] + self.canvasSize[1]/2
328         return (c1*pos[0]+a+fuzzy1, c1*pos[1]+a+fuzzy2, c1*pos[0]+b+fuzzy1, c1*pos[1]+b+fuzzy2)
329
330     def getBoardCoord(self, pos, sloppy=1):
331         """ transform pixel coordinates on canvas into go board coord. in [1,..,boardSize]x[1,..,boardSize]
332             sloppy refers to how far the pixel may be from the intersection in order to
333             be accepted """
334
335         if sloppy: a, b = self.canvasSize[0]-self.canvasSize[1]/2, self.canvasSize[1]-1
336         else:      a, b = self.canvasSize[0]-self.canvasSize[1]/4, self.canvasSize[1]/2
337
338         if (pos[0]-a)%self.canvasSize[1] <= b: x = (pos[0]-a)/self.canvasSize[1] + 1
339         else:                                  x = 0
340        
341         if (pos[1]-a)%self.canvasSize[1] <= b: y = (pos[1]-a)/self.canvasSize[1] + 1
342         else:                  y = 0
343
344         if x<0 or y<0 or x>self.boardSize or y>self.boardSize: x = y = 0
345
346         return (x,y)   
347
348     def placeMark(self, pos, color):
349         """ Place colored mark at pos. """
350         x1, x2, y1, y2 = self.getPixelCoord(pos, 1)
351         self.create_oval(x1+2, x2+2, y1-2, y2-2, fill = color, tags=('marks','non-bg'))
352         self.marks[pos]=color
353         self.changed.set(1)
354
355     def delMarks(self):
356         """ Delete all marks. """
357         if self.marks: self.changed.set(1)
358         self.marks = {}
359         self.delete('marks')
360
361     def delLabels(self):
362         """ Delete all labels. """
363         if self.labels: self.changed.set(1)
364         self.labels={}
365         self.delete('label')
366
367     def remove(self, pos):
368         """ Remove the stone at pos, append this as capture to undostack. """
369         if self.status.has_key(pos):
370             self.changed.set(1)
371             self.delete(self.stones[pos])
372             del self.stones[pos]
373             abstractBoard.remove(self, pos)
374             self.update_idletasks()
375             return 1
376         else: return 0
377
378     def placeLabel(self, pos, type, text=None, color=None):
379         """ Place label of type type at pos; used to display labels
380             from SGF files. """
381
382         self.changed.set(1)
383         self.labels[pos] = (type, text)
384
385         if self.status.has_key(pos) and self.status[pos]=='black':
386             fcolor = 'white'
387             fcolor2 = ''
388         elif self.status.has_key(pos) and self.status[pos]=='white':
389             fcolor = 'black'
390             fcolor2 = ''
391         else:
392             fcolor = color or 'black'
393             fcolor2 = '#FFBA59'
394                    
395         x1, x2, y1, y2 = self.getPixelCoord(pos, 1)
396         if type == 'LB':
397             self.create_oval(x1+3, x2+3, y1-3, y2-3, fill=fcolor2, outline='',
398                              tags=('label', 'non-bg'))
399             self.create_text((x1+y1)/2,(x2+y2)/2, text=text, fill=fcolor,
400                              font = (self.labelFont[0].get(), self.labelFont[1].get() + self.canvasSize[1]/5,
401                                      self.labelFont[2].get()),
402                              tags=('label', 'non-bg'))
403         elif type == 'SQ':
404             self.create_rectangle(x1+6, x2+6, y1-6, y2-6, fill = fcolor, tags=('label','non-bg'))
405         elif type == 'CR':
406             self.create_oval(x1+5, x2+5, y1-5, y2-5, fill = fcolor, tags=('label','non-bg'))
407         elif type == 'TR':
408             self.create_polygon((x1+y1)/2, x2+5, x1+5, y2-5, y1-5, y2-5, fill = fcolor,
409                                 tags = ('label', 'non-bg'))
410         elif type == 'MA':
411             self.create_oval(x1+2, x2+2, y1-2, y2-2, fill=fcolor2, outline='',
412                              tags=('label', 'non-bg'))
413             self.create_text(x1+12,x2+12, text='X', fill=fcolor,
414                              font = (self.labelFont[0].get(), self.labelFont[1].get() + 1 + self.canvasSize[1]/5,
415                                      self.labelFont[2].get()),
416                              tags=('label', 'non-bg'))
417            
418     def placeStone(self, pos, color):
419         self.changed.set(1)
420         p = self.getPixelCoord(pos)
421         self.stones[pos] = self.create_oval(p, fill=color, tags='non-bg')
422
423            
424     def undo(self, no=1, changeCurrentColor=1):
425         """ Undo the last no moves. """
426
427         for i in range(no):
428             if self.undostack:
429                 self.changed.set(1)
430                 pos, color, captures = self.undostack.pop()
431                 if self.status.has_key(pos):
432                     del self.status[pos]
433                     self.delete(self.stones[pos])
434                     del self.stones[pos]
435                 for p in captures:
436                     self.placeStone(p, self.invert(color))
437                     self.status[p] = self.invert(color)
438                 self.update_idletasks()
439                 if changeCurrentColor:
440                     self.currentColor = self.invert(self.currentColor)
441
442     def clear(self):
443         """ Clear the board. """
444         abstractBoard.clear(self)
445         for x in self.stones.keys():
446             self.delete(self.stones[x])
447         self.stones = {}
448         self.changed.set(1)
449
450     def ptOnCircle(self, size, degree):
451         radPerDeg = math.pi/180
452         r = size/2
453         x = int(r*math.cos((degree-90)*radPerDeg) + r)
454         y = int(r*math.sin((degree-90)*radPerDeg) + r)
455         return (x,y)
456
457     def shadedStone(self, event):
458         x, y = self.getBoardCoord((event.x, event.y), 1)
459         if (x,y) == self.shadedStonePos: return     # nothing changed
460
461         self.delShadedStone()
462
463         if (x*y) and self.shadedStoneVar.get() and abstractBoard.play(self, (x,y), self.currentColor):
464             abstractBoard.undo(self)
465
466             if sys.platform[:3]=='win':     # 'stipple' is ignored under windows for
467                                             # create_oval, so we'll draw a polygon ...
468                 l = self.getPixelCoord((x,y),1)
469                 m = []
470
471                 for i in range(18):
472                     help = self.ptOnCircle(l[2]-l[0], i*360/18)
473                     m.append(help[0]+l[0])
474                     m.append(help[1]+l[1])
475                  
476                 self.create_polygon(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9],
477                                     m[10], m[11], m[12], m[13], m[14], m[15], m[16], m[17],
478                                     m[18], m[19], m[20], m[21], m[22], m[23], m[24], m[25],
479                                     m[26], m[27], m[28], m[29], m[30], m[31], m[32], m[33],
480                                     m[34], m[35],
481                                     fill=self.currentColor, stipple='gray50',
482                                     outline='', tags=('shaded','non-bg') )
483             else:
484                 self.create_oval(self.getPixelCoord((x,y), 1), fill=self.currentColor, stipple='gray50',
485                                  outline='', tags=('shaded','non-bg'))
486
487             self.shadedStonePos = (x,y)
488    
489     def delShadedStone(self, event=None):
490         self.delete('shaded')
491         self.shadedStonePos = (-1,-1)
492
493     def fuzzyStones(self):
494         """ switch fuzzy/non-fuzzy stone placement according to self.fuzzy """
495         for p in self.status.keys():
496             self.delete(self.stones[p])
497             del self.stones[p]
498             self.placeStone(p, self.status[p])
499         self.tkraise('marks')
500         self.tkraise('label')
Note: See TracBrowser for help on using the browser.