root/06/devel-old/board.py

Revision 164, 20.3 kB (checked in by ug, 4 years ago)

Back to prev. searches.

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