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