3 """Solitaire game, much like the one that comes with MS Windows.
7 - No cute graphical images for the playing cards faces or backs.
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.
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
29 from Canvas
import Rectangle
, CanvasText
, Group
, Window
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:
37 def bind(self
, sequence
=None, command
=None):
38 return self
.canvas
.tag_bind(self
.id, sequence
, command
)
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.
50 XSPACING
= CARDWIDTH
+ 2*MARGIN
51 YSPACING
= CARDHEIGHT
+ 4*MARGIN
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.)
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).
78 for s
in (HEARTS
, DIAMONDS
):
80 for s
in (CLUBS
, SPADES
):
83 ALLSUITS
= COLOR
.keys()
84 NSUITS
= len(ALLSUITS
)
87 # Card values are 1-13. We also define symbolic names for the picture
88 # cards. ALLVALUES is a list of all card values.
94 ALLVALUES
= range(1, 14) # (one more than the highest value)
95 NVALUES
= len(ALLVALUES
)
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
102 VALNAMES
= ["", "A"] + map(str, range(2, 11)) + ["J", "Q", "K"]
105 # Solitaire constants. The only one I can think of is the number of
111 # The rest of the program consists of class definitions. These are
112 # further described in their documentation strings.
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
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
131 Public read-only instance variables:
133 suit, value, color -- the card's suit, value and color
134 face_shown -- true when the card is shown face up, else false
136 Semi-public read-only instance variables (XXX should be made
139 group -- the Canvas.Group representing the card
140 x, y -- the position of the card's top left corner
142 Private instance variables:
144 __back, __rect, __text -- the canvas items making up the card
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.)
152 def __init__(self
, suit
, value
, canvas
):
155 Arguments are the card's suit and value, and the canvas widget.
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
164 self
.color
= COLOR
[suit
]
168 self
.group
= Group(canvas
)
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
)
175 self
.__rect
= Rectangle(canvas
, 0, 0, CARDWIDTH
, CARDHEIGHT
,
176 outline
='black', fill
='white')
177 self
.group
.addtag_withtag(self
.__rect
)
179 self
.__back
= Rectangle(canvas
, MARGIN
, MARGIN
,
180 CARDWIDTH
-MARGIN
, CARDHEIGHT
-MARGIN
,
181 outline
='black', fill
='blue')
182 self
.group
.addtag_withtag(self
.__back
)
185 """Return a string for debug print statements."""
186 return "Card(%s, %s)" % (`self
.suit`
, `self
.value`
)
188 def moveto(self
, x
, y
):
189 """Move the card to absolute position (x, y)."""
190 self
.moveby(x
- self
.x
, y
- self
.y
)
192 def moveby(self
, dx
, dy
):
193 """Move the card by (dx, dy)."""
196 self
.group
.move(dx
, dy
)
199 """Raise the card above all other objects in its canvas."""
203 """Turn the card's face up."""
205 self
.__rect
.tkraise()
206 self
.__text
.tkraise()
210 """Turn the card's face down."""
212 self
.__rect
.tkraise()
213 self
.__back
.tkraise()
219 """A generic stack of cards.
221 This is used as a base class for all other stacks (e.g. the deck,
222 the suit stacks, and the row stacks).
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
231 Method that subclasses may override:
233 position(card) -- move the card to its proper (x, y) position
235 The default position() method places all cards at the stack's
238 userclickhandler(), userdoubleclickhandler() -- called to do
239 subclass specific things on single and double clicks
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.
245 usermovehandler(cards) -- called to complete a subpile move
247 The default user move handler moves all moved cards back to
248 their original position (by calling the position() method).
252 clickhandler(event), doubleclickhandler(event),
253 motionhandler(event), releasehandler(event) -- event handlers
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.
259 startmoving(event) -- begin a move operation
260 finishmoving() -- finish a move operation
264 def __init__(self
, x
, y
, game
=None):
265 """Stack constructor.
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).
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
)
284 def makebottom(self
):
288 """Return a string for debug print statements."""
289 return "%s(%d, %d)" % (self
.__class
__.__name
__, self
.x
, self
.y
)
294 self
.cards
.append(card
)
297 self
.group
.addtag_withtag(card
.group
)
299 def delete(self
, card
):
300 self
.cards
.remove(card
)
301 card
.group
.dtag(self
.group
)
305 self
.cards
[-1].showface()
310 card
= self
.cards
[-1]
314 # Subclass overridable methods
316 def position(self
, card
):
317 card
.moveto(self
.x
, self
.y
)
319 def userclickhandler(self
):
322 def userdoubleclickhandler(self
):
323 self
.userclickhandler()
325 def usermovehandler(self
, cards
):
331 def clickhandler(self
, event
):
332 self
.finishmoving() # In case we lost an event
333 self
.userclickhandler()
334 self
.startmoving(event
)
336 def motionhandler(self
, event
):
337 self
.keepmoving(event
)
339 def releasehandler(self
, event
):
340 self
.keepmoving(event
)
343 def doubleclickhandler(self
, event
):
344 self
.finishmoving() # In case we lost an event
345 self
.userdoubleclickhandler()
346 self
.startmoving(event
)
352 def startmoving(self
, event
):
354 tags
= self
.game
.canvas
.gettags('current')
355 for i
in range(len(self
.cards
)):
357 if card
.group
.tag
in tags
:
361 if not card
.face_shown
:
363 self
.moving
= self
.cards
[i
:]
366 for card
in self
.moving
:
369 def keepmoving(self
, event
):
372 dx
= event
.x
- self
.lastx
373 dy
= event
.y
- self
.lasty
377 for card
in self
.moving
:
380 def finishmoving(self
):
384 self
.usermovehandler(cards
)
389 """The deck is a stack with support for shuffling.
393 fill() -- create the playing cards
394 shuffle() -- shuffle the playing cards
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
402 def makebottom(self
):
403 bottom
= Rectangle(self
.game
.canvas
,
405 self
.x
+CARDWIDTH
, self
.y
+CARDHEIGHT
,
406 outline
='black', fill
=BACKGROUND
)
407 self
.group
.addtag_withtag(bottom
)
410 for suit
in ALLSUITS
:
411 for value
in ALLVALUES
:
412 self
.add(Card(suit
, value
, self
.game
.canvas
))
417 for i
in randperm(n
):
418 newcards
.append(self
.cards
[i
])
419 self
.cards
= newcards
421 def userclickhandler(self
):
422 opendeck
= self
.game
.opendeck
426 card
= opendeck
.deal()
432 self
.game
.opendeck
.add(card
)
437 """Function returning a random permutation of range(n)."""
447 class OpenStack(Stack
):
449 def acceptable(self
, cards
):
452 def usermovehandler(self
, cards
):
454 stack
= self
.game
.closeststack(card
)
455 if not stack
or stack
is self
or not stack
.acceptable(cards
):
456 Stack
.usermovehandler(self
, cards
)
463 def userdoubleclickhandler(self
):
466 card
= self
.cards
[-1]
467 if not card
.face_shown
:
468 self
.userclickhandler()
470 for s
in self
.game
.suits
:
471 if s
.acceptable([card
]):
478 class SuitStack(OpenStack
):
480 def makebottom(self
):
481 bottom
= Rectangle(self
.game
.canvas
,
483 self
.x
+CARDWIDTH
, self
.y
+CARDHEIGHT
,
484 outline
='black', fill
='')
486 def userclickhandler(self
):
489 def userdoubleclickhandler(self
):
492 def acceptable(self
, cards
):
497 return card
.value
== ACE
498 topcard
= self
.cards
[-1]
499 return card
.suit
== topcard
.suit
and card
.value
== topcard
.value
+ 1
502 class RowStack(OpenStack
):
504 def acceptable(self
, cards
):
507 return card
.value
== KING
508 topcard
= self
.cards
[-1]
509 if not topcard
.face_shown
:
511 return card
.color
!= topcard
.color
and card
.value
== topcard
.value
- 1
513 def position(self
, card
):
522 card
.moveto(self
.x
, y
)
527 def __init__(self
, master
):
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
)
537 self
.dealbutton
= Button(self
.canvas
,
539 highlightthickness
=0,
540 background
=BACKGROUND
,
541 activebackground
="green",
543 Window(self
.canvas
, MARGIN
, 3*YSPACING
+ 20,
544 window
=self
.dealbutton
, anchor
=SW
)
549 self
.deck
= Deck(x
, y
, self
)
552 self
.opendeck
= OpenStack(x
, y
, self
)
556 for i
in range(NSUITS
):
558 self
.suits
.append(SuitStack(x
, y
, self
))
564 for i
in range(NROWS
):
565 self
.rows
.append(RowStack(x
, y
, self
))
568 self
.openstacks
= [self
.opendeck
] + self
.suits
+ self
.rows
575 if len(s
.cards
) != NVALUES
:
581 """Stupid animation when you win."""
583 for s
in self
.openstacks
:
584 cards
= cards
+ s
.cards
586 card
= random
.choice(cards
)
588 self
.animatedmoveto(card
, self
.deck
)
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
594 self
.master
.update_idletasks()
596 def closeststack(self
, card
):
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
611 for i
in range(NROWS
):
612 for r
in self
.rows
[i
:]:
613 card
= self
.deck
.deal()
619 for stack
in self
.openstacks
:
628 # Main function, run when invoked as a stand-alone Python program.
632 game
= Solitaire(root
)
633 root
.protocol('WM_DELETE_WINDOW', root
.quit
)
636 if __name__
== '__main__':