Receita: TkShisenSho
Em uma noite de insônia brincando com os joguinhos do KDE, achei o Shisen-Sho, uma variação do Mahjongg. O problema é que fiquei viciado nisso, um dia viajei, em um lugar com computador só com Windows, acabei resolvendo aproveitar melhor algumas horas de insônia e fazer um em Python e Tkinter. Tem alguns bugs, não marca tempo nem placar, mas funciona. O algoritmo para encontrar a rota correta (baseado no A*) pode servir para alguém, e o código é um exemplo razoável do poder do Canvas da Tkinter.
Código
1 #!/usr/bin/env python
2
3
4 import random
5 import math
6
7 from Tkinter import *
8
9 import Image
10 import ImageTk
11 import ImageEnhance
12 import itertools
13
14
15 SIZE = [(16, 8), (20, 10), (26, 14), (28, 16), (32, 18)]
16 CELLSIZE = 40, 56
17 IMAGE = 'kmahjongg.bmp'
18 PATHTIME = 200
19 GRAVITY = True
20
21
22 def load_images():
23 w, h = 40, 56
24 base = Image.open(IMAGE)
25 cells = []
26 for x in range(0, 9*w, w):
27 for y in range(0, 3*h, h):
28 im = base.crop((x, y, x+w, y+h))
29 cells.append(im)
30 for x in range(0, 8*w, w):
31 for y in range(3*h, 4*h, h):
32 im = base.crop((x, y, x+w, y+h))
33 cells.append(im)
34 for x in range(0, 7*w, w):
35 for y in range(4*h, 5*h, h):
36 im = base.crop((x, y, x+w, y+h))
37 cells.append(im)
38 assert len(cells) == 42
39 return cells
40
41
42 class Cell(object):
43 def __init__(self, board, x, y):
44 self.board = board
45 self.x = x
46 self.y = y
47 self.content = None
48 self.tag = None
49
50 w, h = self.board.cellsize
51 self.center = (x*w + w/2), (y*h + h/2)
52 x0 = self.x * w
53 y0 = self.y * h
54 x1 = (self.x+1) * w
55 y1 = (self.y+1) * h
56 self.coords = (x0, y0, x1, y1)
57
58 def __repr__(self):
59 return '<%r, %r>'%(self.x, self.y)
60
61 def set(self, content):
62 self.content = content
63 self.board.itemconfig(self.tag, image=content[0])
64
65 def clear(self):
66 self.content = None
67 self.board.itemconfig(self.tag, image='')
68
69 def light(self):
70 if self.content is not None:
71 self.board.itemconfig(self.tag, image=self.content[1])
72
73 def dark(self):
74 if self.content is not None:
75 self.board.itemconfig(self.tag, image=self.content[0])
76
77 def draw(self):
78 if self.tag is None:
79 self.tag = self.board.create_image(self.center)
80
81 def neighbors(self):
82 # return adjacent cells
83 for v in (-1, 1):
84 x = self.x + v
85 y = self.y + v
86
87 if 0 <= x < self.board.w:
88 cell = self.board[x, self.y]
89 yield cell
90
91 if 0 <= y < self.board.h:
92 cell = self.board[self.x, y]
93 yield cell
94
95 def routes(self, dest):
96 # get route from self to dest
97
98 #create a list paths
99 paths = []
100 #add the start node self to paths giving it one element
101 paths.append((self,))
102
103 #Until first path of paths ends with dest, or paths is empty
104 while paths and paths[0][-1] is not dest:
105 #extract the first path from paths
106 first = paths.pop(0)
107 #extend the path one step to all empty neighbors
108 #create X new paths
109 #reject all paths with loops
110 new = [first+(n,) for n in first[-1].neighbors() if n not in first and (n.content is None or n is dest)]
111
112 #filter out paths with more than n turns
113 new = filter(self.lines, new)
114
115 #add each remaining new path to paths
116 paths.extend(new)
117
118 #sort paths by distance to goal
119 paths.sort(key=lambda p: self.dist(p[-1], dest))
120
121
122
123 return paths
124
125 def dist(self, s, d):
126 x = abs(s.x - d.x)
127 y = abs(s.y - d.y)
128 h = math.sqrt(x**2 + y**2)
129 return h
130
131
132 def lines(self, path):
133 n = 3
134 i = 0
135
136 b = path[0]
137 x = False
138 y = False
139 for a in path[1:]:
140 if a.x == b.x:
141 if not x:
142 x = True
143 i += 1
144 else:
145 x = False
146
147 if a.y == b.y:
148 if not y:
149 y = True
150 i += 1
151 else:
152 y = False
153
154 b = a
155
156 if i > n:
157 return False
158 return True
159
160
161
162
163 class Board(Canvas):
164 def __init__(self, parent):
165 Canvas.__init__(self, parent)
166 self.parent = parent
167 self.size = w, h = SIZE[3]
168 self.w = w
169 self.h = h
170 self.cellsize = CELLSIZE
171
172 self.cells = [Cell(self, x, y) for x in range(w) for y in range(h)]
173 self.tags = {}
174
175 self.src = None
176
177 self._images = None
178 self.images = None
179 self.aimages = None
180
181 self.setup()
182 self.reset()
183
184 def setup(self):
185 self._images = load_images()
186 self.images = [ImageTk.PhotoImage(img) for img in self._images]
187 self.aimages = [ImageTk.PhotoImage(ImageEnhance.Brightness(img).enhance(1.2)) for img in self._images]
188
189 self['width'] = self.cellsize[0] * self.w
190 self['height'] = self.cellsize[1] * self.h
191 self['bg'] = '#00aa00'
192
193 for c in self.cells:
194 c.draw()
195 self.tags[c.tag] = c
196
197 self.bind('<Button-1>', self.lclick)
198 self.bind('<Button-3>', self.rclick)
199 self.parent.bind('h', self.givehint)
200
201 def reset(self):
202 ncells = (self.w-2) * (self.h-2)
203 nimgs = ncells/4
204
205 simgs = itertools.cycle(zip(self.images, self.aimages)[:nimgs])
206
207 imgs = []
208 while len(imgs) < ncells:
209 x = simgs.next()
210 imgs.append(x)
211 imgs.append(x)
212
213 random.shuffle(imgs)
214
215 assert len(imgs) == ncells, (nimgs, len(imgs), ncells)
216
217 for x in range(1, self.w-1):
218 for y in range(1, self.h-1):
219 self[x, y].set(imgs.pop())
220
221 assert len(imgs) == 0
222
223 def __getitem__(self, i):
224 x, y = i
225 if not x < self.w:
226 raise IndexError('x=%s'%x)
227 if not y < self.h:
228 raise IndexError('y=%s'%y)
229 c = y + (self.h * x)
230 return self.cells[c]
231
232 def draw_path(self, path, fill='red'):
233 coords = [cell.center for cell in path]
234 return self.create_line(coords, fill=fill, width=2)
235
236 def clear_cells(self, path, src, dest):
237 print 'clear'
238 self.delete(path)
239 src.dark()
240 dest.dark()
241 src.clear()
242 dest.clear()
243 if GRAVITY:
244 self.drop_cells(src.x)
245 self.drop_cells(dest.x)
246
247 def drop_cells(self, col):
248 for row in reversed(range(1, self.h-1)):
249 x, y = col, row
250 a = self[x, y]
251 b = self[x, y-1]
252 if a.content is None and b.content is not None:
253 a.set(b.content)
254 b.clear()
255
256 def lclick(self, event=None):
257 x = self.canvasx(event.x)
258 y = self.canvasy(event.y)
259 w, h = self.cellsize
260 rx, ry = int(x/w), int(y/h)
261 print rx, ry
262 cell = self[rx, ry]
263 if cell.content is not None:
264 self.clickcell(cell)
265
266 def clickcell(self, cell):
267
268 cell.light()
269
270 if self.src is None:
271 self.src = cell
272
273 elif self.src is cell:
274 self.src.dark()
275 self.src = None
276
277 elif self.src.content != cell.content:
278 print "Contents don't match!"
279 self.src.dark()
280 cell.dark()
281 self.src = None
282
283 else:
284 paths = self.src.routes(cell)
285 if not paths:
286 print 'No path!'
287 self.src.dark()
288 cell.dark()
289 else:
290 path = paths[0]
291 tag = self.draw_path(path)
292 self.after(PATHTIME, self.clear_cells, tag, self.src, cell)
293 self.src = None
294
295 def rclick(self, event=None):
296 x = self.canvasx(event.x)
297 y = self.canvasy(event.y)
298 w, h = self.cellsize
299 rx, ry = int(x/w), int(y/h)
300 print rx, ry
301 cell = self[rx, ry]
302 self.cellhint(cell, show=True)
303
304 def cellhint(self, cell, delay=0.5, show=False):
305 if cell.content is not None:
306 # find a cell with same contents
307 for other in self.cells:
308 if other is cell:
309 continue
310 if cell.content == other.content:
311 paths = cell.routes(other)
312 if paths:
313 if show:
314 p = paths[0]
315 tag = self.draw_path(p, fill='blue')
316 self.after(int(delay*1000), self.delete, tag)
317 return other
318 else:
319 return False
320
321 def givehint(self, event=None, show=True):
322 for cell in self.cells:
323 if cell.content is not None:
324 other = self.cellhint(cell, delay=1, show=show)
325 if other:
326 return cell, other
327 else:
328 print 'No more moves possible. Game Over!'
329
330
331
332
333
334 ### Gui
335
336 class MainWindow(Tk):
337 def __init__(self):
338 Tk.__init__(self)
339
340 self.build()
341
342
343 def build(self):
344 self.board = Board(self)
345 self.board.pack(expand=1, fill=BOTH)
346
347
348
349
350
351 if __name__ == '__main__':
352 root = MainWindow()
353 root.mainloop()
354
355
356
Imagem com as peças, do jogo original para o KDE, liberada sob GPL:
Volta para CookBook.