|
1 #! /usr/bin/env python |
|
2 |
|
3 """Solitaire game, much like the one that comes with MS Windows. |
|
4 |
|
5 Limitations: |
|
6 |
|
7 - No cute graphical images for the playing cards faces or backs. |
|
8 - No scoring or timer. |
|
9 - No undo. |
|
10 - No option to turn 3 cards at a time. |
|
11 - No keyboard shortcuts. |
|
12 - Less fancy animation when you win. |
|
13 - The determination of which stack you drag to is more relaxed. |
|
14 |
|
15 Apology: |
|
16 |
|
17 I'm not much of a card player, so my terminology in these comments may |
|
18 at times be a little unusual. If you have suggestions, please let me |
|
19 know! |
|
20 |
|
21 """ |
|
22 |
|
23 # Imports |
|
24 |
|
25 import math |
|
26 import random |
|
27 |
|
28 from Tkinter import * |
|
29 from Canvas import Rectangle, CanvasText, Group, Window |
|
30 |
|
31 |
|
32 # Fix a bug in Canvas.Group as distributed in Python 1.4. The |
|
33 # distributed bind() method is broken. Rather than asking you to fix |
|
34 # the source, we fix it here by deriving a subclass: |
|
35 |
|
36 class Group(Group): |
|
37 def bind(self, sequence=None, command=None): |
|
38 return self.canvas.tag_bind(self.id, sequence, command) |
|
39 |
|
40 |
|
41 # Constants determining the size and lay-out of cards and stacks. We |
|
42 # work in a "grid" where each card/stack is surrounded by MARGIN |
|
43 # pixels of space on each side, so adjacent stacks are separated by |
|
44 # 2*MARGIN pixels. OFFSET is the offset used for displaying the |
|
45 # face down cards in the row stacks. |
|
46 |
|
47 CARDWIDTH = 100 |
|
48 CARDHEIGHT = 150 |
|
49 MARGIN = 10 |
|
50 XSPACING = CARDWIDTH + 2*MARGIN |
|
51 YSPACING = CARDHEIGHT + 4*MARGIN |
|
52 OFFSET = 5 |
|
53 |
|
54 # The background color, green to look like a playing table. The |
|
55 # standard green is way too bright, and dark green is way to dark, so |
|
56 # we use something in between. (There are a few more colors that |
|
57 # could be customized, but they are less controversial.) |
|
58 |
|
59 BACKGROUND = '#070' |
|
60 |
|
61 |
|
62 # Suits and colors. The values of the symbolic suit names are the |
|
63 # strings used to display them (you change these and VALNAMES to |
|
64 # internationalize the game). The COLOR dictionary maps suit names to |
|
65 # colors (red and black) which must be Tk color names. The keys() of |
|
66 # the COLOR dictionary conveniently provides us with a list of all |
|
67 # suits (in arbitrary order). |
|
68 |
|
69 HEARTS = 'Heart' |
|
70 DIAMONDS = 'Diamond' |
|
71 CLUBS = 'Club' |
|
72 SPADES = 'Spade' |
|
73 |
|
74 RED = 'red' |
|
75 BLACK = 'black' |
|
76 |
|
77 COLOR = {} |
|
78 for s in (HEARTS, DIAMONDS): |
|
79 COLOR[s] = RED |
|
80 for s in (CLUBS, SPADES): |
|
81 COLOR[s] = BLACK |
|
82 |
|
83 ALLSUITS = COLOR.keys() |
|
84 NSUITS = len(ALLSUITS) |
|
85 |
|
86 |
|
87 # Card values are 1-13. We also define symbolic names for the picture |
|
88 # cards. ALLVALUES is a list of all card values. |
|
89 |
|
90 ACE = 1 |
|
91 JACK = 11 |
|
92 QUEEN = 12 |
|
93 KING = 13 |
|
94 ALLVALUES = range(1, 14) # (one more than the highest value) |
|
95 NVALUES = len(ALLVALUES) |
|
96 |
|
97 |
|
98 # VALNAMES is a list that maps a card value to string. It contains a |
|
99 # dummy element at index 0 so it can be indexed directly with the card |
|
100 # value. |
|
101 |
|
102 VALNAMES = ["", "A"] + map(str, range(2, 11)) + ["J", "Q", "K"] |
|
103 |
|
104 |
|
105 # Solitaire constants. The only one I can think of is the number of |
|
106 # row stacks. |
|
107 |
|
108 NROWS = 7 |
|
109 |
|
110 |
|
111 # The rest of the program consists of class definitions. These are |
|
112 # further described in their documentation strings. |
|
113 |
|
114 |
|
115 class Card: |
|
116 |
|
117 """A playing card. |
|
118 |
|
119 A card doesn't record to which stack it belongs; only the stack |
|
120 records this (it turns out that we always know this from the |
|
121 context, and this saves a ``double update'' with potential for |
|
122 inconsistencies). |
|
123 |
|
124 Public methods: |
|
125 |
|
126 moveto(x, y) -- move the card to an absolute position |
|
127 moveby(dx, dy) -- move the card by a relative offset |
|
128 tkraise() -- raise the card to the top of its stack |
|
129 showface(), showback() -- turn the card face up or down & raise it |
|
130 |
|
131 Public read-only instance variables: |
|
132 |
|
133 suit, value, color -- the card's suit, value and color |
|
134 face_shown -- true when the card is shown face up, else false |
|
135 |
|
136 Semi-public read-only instance variables (XXX should be made |
|
137 private): |
|
138 |
|
139 group -- the Canvas.Group representing the card |
|
140 x, y -- the position of the card's top left corner |
|
141 |
|
142 Private instance variables: |
|
143 |
|
144 __back, __rect, __text -- the canvas items making up the card |
|
145 |
|
146 (To show the card face up, the text item is placed in front of |
|
147 rect and the back is placed behind it. To show it face down, this |
|
148 is reversed. The card is created face down.) |
|
149 |
|
150 """ |
|
151 |
|
152 def __init__(self, suit, value, canvas): |
|
153 """Card constructor. |
|
154 |
|
155 Arguments are the card's suit and value, and the canvas widget. |
|
156 |
|
157 The card is created at position (0, 0), with its face down |
|
158 (adding it to a stack will position it according to that |
|
159 stack's rules). |
|
160 |
|
161 """ |
|
162 self.suit = suit |
|
163 self.value = value |
|
164 self.color = COLOR[suit] |
|
165 self.face_shown = 0 |
|
166 |
|
167 self.x = self.y = 0 |
|
168 self.group = Group(canvas) |
|
169 |
|
170 text = "%s %s" % (VALNAMES[value], suit) |
|
171 self.__text = CanvasText(canvas, CARDWIDTH//2, 0, |
|
172 anchor=N, fill=self.color, text=text) |
|
173 self.group.addtag_withtag(self.__text) |
|
174 |
|
175 self.__rect = Rectangle(canvas, 0, 0, CARDWIDTH, CARDHEIGHT, |
|
176 outline='black', fill='white') |
|
177 self.group.addtag_withtag(self.__rect) |
|
178 |
|
179 self.__back = Rectangle(canvas, MARGIN, MARGIN, |
|
180 CARDWIDTH-MARGIN, CARDHEIGHT-MARGIN, |
|
181 outline='black', fill='blue') |
|
182 self.group.addtag_withtag(self.__back) |
|
183 |
|
184 def __repr__(self): |
|
185 """Return a string for debug print statements.""" |
|
186 return "Card(%r, %r)" % (self.suit, self.value) |
|
187 |
|
188 def moveto(self, x, y): |
|
189 """Move the card to absolute position (x, y).""" |
|
190 self.moveby(x - self.x, y - self.y) |
|
191 |
|
192 def moveby(self, dx, dy): |
|
193 """Move the card by (dx, dy).""" |
|
194 self.x = self.x + dx |
|
195 self.y = self.y + dy |
|
196 self.group.move(dx, dy) |
|
197 |
|
198 def tkraise(self): |
|
199 """Raise the card above all other objects in its canvas.""" |
|
200 self.group.tkraise() |
|
201 |
|
202 def showface(self): |
|
203 """Turn the card's face up.""" |
|
204 self.tkraise() |
|
205 self.__rect.tkraise() |
|
206 self.__text.tkraise() |
|
207 self.face_shown = 1 |
|
208 |
|
209 def showback(self): |
|
210 """Turn the card's face down.""" |
|
211 self.tkraise() |
|
212 self.__rect.tkraise() |
|
213 self.__back.tkraise() |
|
214 self.face_shown = 0 |
|
215 |
|
216 |
|
217 class Stack: |
|
218 |
|
219 """A generic stack of cards. |
|
220 |
|
221 This is used as a base class for all other stacks (e.g. the deck, |
|
222 the suit stacks, and the row stacks). |
|
223 |
|
224 Public methods: |
|
225 |
|
226 add(card) -- add a card to the stack |
|
227 delete(card) -- delete a card from the stack |
|
228 showtop() -- show the top card (if any) face up |
|
229 deal() -- delete and return the top card, or None if empty |
|
230 |
|
231 Method that subclasses may override: |
|
232 |
|
233 position(card) -- move the card to its proper (x, y) position |
|
234 |
|
235 The default position() method places all cards at the stack's |
|
236 own (x, y) position. |
|
237 |
|
238 userclickhandler(), userdoubleclickhandler() -- called to do |
|
239 subclass specific things on single and double clicks |
|
240 |
|
241 The default user (single) click handler shows the top card |
|
242 face up. The default user double click handler calls the user |
|
243 single click handler. |
|
244 |
|
245 usermovehandler(cards) -- called to complete a subpile move |
|
246 |
|
247 The default user move handler moves all moved cards back to |
|
248 their original position (by calling the position() method). |
|
249 |
|
250 Private methods: |
|
251 |
|
252 clickhandler(event), doubleclickhandler(event), |
|
253 motionhandler(event), releasehandler(event) -- event handlers |
|
254 |
|
255 The default event handlers turn the top card of the stack with |
|
256 its face up on a (single or double) click, and also support |
|
257 moving a subpile around. |
|
258 |
|
259 startmoving(event) -- begin a move operation |
|
260 finishmoving() -- finish a move operation |
|
261 |
|
262 """ |
|
263 |
|
264 def __init__(self, x, y, game=None): |
|
265 """Stack constructor. |
|
266 |
|
267 Arguments are the stack's nominal x and y position (the top |
|
268 left corner of the first card placed in the stack), and the |
|
269 game object (which is used to get the canvas; subclasses use |
|
270 the game object to find other stacks). |
|
271 |
|
272 """ |
|
273 self.x = x |
|
274 self.y = y |
|
275 self.game = game |
|
276 self.cards = [] |
|
277 self.group = Group(self.game.canvas) |
|
278 self.group.bind('<1>', self.clickhandler) |
|
279 self.group.bind('<Double-1>', self.doubleclickhandler) |
|
280 self.group.bind('<B1-Motion>', self.motionhandler) |
|
281 self.group.bind('<ButtonRelease-1>', self.releasehandler) |
|
282 self.makebottom() |
|
283 |
|
284 def makebottom(self): |
|
285 pass |
|
286 |
|
287 def __repr__(self): |
|
288 """Return a string for debug print statements.""" |
|
289 return "%s(%d, %d)" % (self.__class__.__name__, self.x, self.y) |
|
290 |
|
291 # Public methods |
|
292 |
|
293 def add(self, card): |
|
294 self.cards.append(card) |
|
295 card.tkraise() |
|
296 self.position(card) |
|
297 self.group.addtag_withtag(card.group) |
|
298 |
|
299 def delete(self, card): |
|
300 self.cards.remove(card) |
|
301 card.group.dtag(self.group) |
|
302 |
|
303 def showtop(self): |
|
304 if self.cards: |
|
305 self.cards[-1].showface() |
|
306 |
|
307 def deal(self): |
|
308 if not self.cards: |
|
309 return None |
|
310 card = self.cards[-1] |
|
311 self.delete(card) |
|
312 return card |
|
313 |
|
314 # Subclass overridable methods |
|
315 |
|
316 def position(self, card): |
|
317 card.moveto(self.x, self.y) |
|
318 |
|
319 def userclickhandler(self): |
|
320 self.showtop() |
|
321 |
|
322 def userdoubleclickhandler(self): |
|
323 self.userclickhandler() |
|
324 |
|
325 def usermovehandler(self, cards): |
|
326 for card in cards: |
|
327 self.position(card) |
|
328 |
|
329 # Event handlers |
|
330 |
|
331 def clickhandler(self, event): |
|
332 self.finishmoving() # In case we lost an event |
|
333 self.userclickhandler() |
|
334 self.startmoving(event) |
|
335 |
|
336 def motionhandler(self, event): |
|
337 self.keepmoving(event) |
|
338 |
|
339 def releasehandler(self, event): |
|
340 self.keepmoving(event) |
|
341 self.finishmoving() |
|
342 |
|
343 def doubleclickhandler(self, event): |
|
344 self.finishmoving() # In case we lost an event |
|
345 self.userdoubleclickhandler() |
|
346 self.startmoving(event) |
|
347 |
|
348 # Move internals |
|
349 |
|
350 moving = None |
|
351 |
|
352 def startmoving(self, event): |
|
353 self.moving = None |
|
354 tags = self.game.canvas.gettags('current') |
|
355 for i in range(len(self.cards)): |
|
356 card = self.cards[i] |
|
357 if card.group.tag in tags: |
|
358 break |
|
359 else: |
|
360 return |
|
361 if not card.face_shown: |
|
362 return |
|
363 self.moving = self.cards[i:] |
|
364 self.lastx = event.x |
|
365 self.lasty = event.y |
|
366 for card in self.moving: |
|
367 card.tkraise() |
|
368 |
|
369 def keepmoving(self, event): |
|
370 if not self.moving: |
|
371 return |
|
372 dx = event.x - self.lastx |
|
373 dy = event.y - self.lasty |
|
374 self.lastx = event.x |
|
375 self.lasty = event.y |
|
376 if dx or dy: |
|
377 for card in self.moving: |
|
378 card.moveby(dx, dy) |
|
379 |
|
380 def finishmoving(self): |
|
381 cards = self.moving |
|
382 self.moving = None |
|
383 if cards: |
|
384 self.usermovehandler(cards) |
|
385 |
|
386 |
|
387 class Deck(Stack): |
|
388 |
|
389 """The deck is a stack with support for shuffling. |
|
390 |
|
391 New methods: |
|
392 |
|
393 fill() -- create the playing cards |
|
394 shuffle() -- shuffle the playing cards |
|
395 |
|
396 A single click moves the top card to the game's open deck and |
|
397 moves it face up; if we're out of cards, it moves the open deck |
|
398 back to the deck. |
|
399 |
|
400 """ |
|
401 |
|
402 def makebottom(self): |
|
403 bottom = Rectangle(self.game.canvas, |
|
404 self.x, self.y, |
|
405 self.x+CARDWIDTH, self.y+CARDHEIGHT, |
|
406 outline='black', fill=BACKGROUND) |
|
407 self.group.addtag_withtag(bottom) |
|
408 |
|
409 def fill(self): |
|
410 for suit in ALLSUITS: |
|
411 for value in ALLVALUES: |
|
412 self.add(Card(suit, value, self.game.canvas)) |
|
413 |
|
414 def shuffle(self): |
|
415 n = len(self.cards) |
|
416 newcards = [] |
|
417 for i in randperm(n): |
|
418 newcards.append(self.cards[i]) |
|
419 self.cards = newcards |
|
420 |
|
421 def userclickhandler(self): |
|
422 opendeck = self.game.opendeck |
|
423 card = self.deal() |
|
424 if not card: |
|
425 while 1: |
|
426 card = opendeck.deal() |
|
427 if not card: |
|
428 break |
|
429 self.add(card) |
|
430 card.showback() |
|
431 else: |
|
432 self.game.opendeck.add(card) |
|
433 card.showface() |
|
434 |
|
435 |
|
436 def randperm(n): |
|
437 """Function returning a random permutation of range(n).""" |
|
438 r = range(n) |
|
439 x = [] |
|
440 while r: |
|
441 i = random.choice(r) |
|
442 x.append(i) |
|
443 r.remove(i) |
|
444 return x |
|
445 |
|
446 |
|
447 class OpenStack(Stack): |
|
448 |
|
449 def acceptable(self, cards): |
|
450 return 0 |
|
451 |
|
452 def usermovehandler(self, cards): |
|
453 card = cards[0] |
|
454 stack = self.game.closeststack(card) |
|
455 if not stack or stack is self or not stack.acceptable(cards): |
|
456 Stack.usermovehandler(self, cards) |
|
457 else: |
|
458 for card in cards: |
|
459 self.delete(card) |
|
460 stack.add(card) |
|
461 self.game.wincheck() |
|
462 |
|
463 def userdoubleclickhandler(self): |
|
464 if not self.cards: |
|
465 return |
|
466 card = self.cards[-1] |
|
467 if not card.face_shown: |
|
468 self.userclickhandler() |
|
469 return |
|
470 for s in self.game.suits: |
|
471 if s.acceptable([card]): |
|
472 self.delete(card) |
|
473 s.add(card) |
|
474 self.game.wincheck() |
|
475 break |
|
476 |
|
477 |
|
478 class SuitStack(OpenStack): |
|
479 |
|
480 def makebottom(self): |
|
481 bottom = Rectangle(self.game.canvas, |
|
482 self.x, self.y, |
|
483 self.x+CARDWIDTH, self.y+CARDHEIGHT, |
|
484 outline='black', fill='') |
|
485 |
|
486 def userclickhandler(self): |
|
487 pass |
|
488 |
|
489 def userdoubleclickhandler(self): |
|
490 pass |
|
491 |
|
492 def acceptable(self, cards): |
|
493 if len(cards) != 1: |
|
494 return 0 |
|
495 card = cards[0] |
|
496 if not self.cards: |
|
497 return card.value == ACE |
|
498 topcard = self.cards[-1] |
|
499 return card.suit == topcard.suit and card.value == topcard.value + 1 |
|
500 |
|
501 |
|
502 class RowStack(OpenStack): |
|
503 |
|
504 def acceptable(self, cards): |
|
505 card = cards[0] |
|
506 if not self.cards: |
|
507 return card.value == KING |
|
508 topcard = self.cards[-1] |
|
509 if not topcard.face_shown: |
|
510 return 0 |
|
511 return card.color != topcard.color and card.value == topcard.value - 1 |
|
512 |
|
513 def position(self, card): |
|
514 y = self.y |
|
515 for c in self.cards: |
|
516 if c == card: |
|
517 break |
|
518 if c.face_shown: |
|
519 y = y + 2*MARGIN |
|
520 else: |
|
521 y = y + OFFSET |
|
522 card.moveto(self.x, y) |
|
523 |
|
524 |
|
525 class Solitaire: |
|
526 |
|
527 def __init__(self, master): |
|
528 self.master = master |
|
529 |
|
530 self.canvas = Canvas(self.master, |
|
531 background=BACKGROUND, |
|
532 highlightthickness=0, |
|
533 width=NROWS*XSPACING, |
|
534 height=3*YSPACING + 20 + MARGIN) |
|
535 self.canvas.pack(fill=BOTH, expand=TRUE) |
|
536 |
|
537 self.dealbutton = Button(self.canvas, |
|
538 text="Deal", |
|
539 highlightthickness=0, |
|
540 background=BACKGROUND, |
|
541 activebackground="green", |
|
542 command=self.deal) |
|
543 Window(self.canvas, MARGIN, 3*YSPACING + 20, |
|
544 window=self.dealbutton, anchor=SW) |
|
545 |
|
546 x = MARGIN |
|
547 y = MARGIN |
|
548 |
|
549 self.deck = Deck(x, y, self) |
|
550 |
|
551 x = x + XSPACING |
|
552 self.opendeck = OpenStack(x, y, self) |
|
553 |
|
554 x = x + XSPACING |
|
555 self.suits = [] |
|
556 for i in range(NSUITS): |
|
557 x = x + XSPACING |
|
558 self.suits.append(SuitStack(x, y, self)) |
|
559 |
|
560 x = MARGIN |
|
561 y = y + YSPACING |
|
562 |
|
563 self.rows = [] |
|
564 for i in range(NROWS): |
|
565 self.rows.append(RowStack(x, y, self)) |
|
566 x = x + XSPACING |
|
567 |
|
568 self.openstacks = [self.opendeck] + self.suits + self.rows |
|
569 |
|
570 self.deck.fill() |
|
571 self.deal() |
|
572 |
|
573 def wincheck(self): |
|
574 for s in self.suits: |
|
575 if len(s.cards) != NVALUES: |
|
576 return |
|
577 self.win() |
|
578 self.deal() |
|
579 |
|
580 def win(self): |
|
581 """Stupid animation when you win.""" |
|
582 cards = [] |
|
583 for s in self.openstacks: |
|
584 cards = cards + s.cards |
|
585 while cards: |
|
586 card = random.choice(cards) |
|
587 cards.remove(card) |
|
588 self.animatedmoveto(card, self.deck) |
|
589 |
|
590 def animatedmoveto(self, card, dest): |
|
591 for i in range(10, 0, -1): |
|
592 dx, dy = (dest.x-card.x)//i, (dest.y-card.y)//i |
|
593 card.moveby(dx, dy) |
|
594 self.master.update_idletasks() |
|
595 |
|
596 def closeststack(self, card): |
|
597 closest = None |
|
598 cdist = 999999999 |
|
599 # Since we only compare distances, |
|
600 # we don't bother to take the square root. |
|
601 for stack in self.openstacks: |
|
602 dist = (stack.x - card.x)**2 + (stack.y - card.y)**2 |
|
603 if dist < cdist: |
|
604 closest = stack |
|
605 cdist = dist |
|
606 return closest |
|
607 |
|
608 def deal(self): |
|
609 self.reset() |
|
610 self.deck.shuffle() |
|
611 for i in range(NROWS): |
|
612 for r in self.rows[i:]: |
|
613 card = self.deck.deal() |
|
614 r.add(card) |
|
615 for r in self.rows: |
|
616 r.showtop() |
|
617 |
|
618 def reset(self): |
|
619 for stack in self.openstacks: |
|
620 while 1: |
|
621 card = stack.deal() |
|
622 if not card: |
|
623 break |
|
624 self.deck.add(card) |
|
625 card.showback() |
|
626 |
|
627 |
|
628 # Main function, run when invoked as a stand-alone Python program. |
|
629 |
|
630 def main(): |
|
631 root = Tk() |
|
632 game = Solitaire(root) |
|
633 root.protocol('WM_DELETE_WINDOW', root.quit) |
|
634 root.mainloop() |
|
635 |
|
636 if __name__ == '__main__': |
|
637 main() |