1#! /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
15Apology:
16
17I'm not much of a card player, so my terminology in these comments may
18at times be a little unusual.  If you have suggestions, please let me
19know!
20
21"""
22
23# Imports
24
25import math
26import random
27
28from Tkinter import *
29from 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
36class 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
47CARDWIDTH = 100
48CARDHEIGHT = 150
49MARGIN = 10
50XSPACING = CARDWIDTH + 2*MARGIN
51YSPACING = CARDHEIGHT + 4*MARGIN
52OFFSET = 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
59BACKGROUND = '#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
69HEARTS = 'Heart'
70DIAMONDS = 'Diamond'
71CLUBS = 'Club'
72SPADES = 'Spade'
73
74RED = 'red'
75BLACK = 'black'
76
77COLOR = {}
78for s in (HEARTS, DIAMONDS):
79    COLOR[s] = RED
80for s in (CLUBS, SPADES):
81    COLOR[s] = BLACK
82
83ALLSUITS = COLOR.keys()
84NSUITS = 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
90ACE = 1
91JACK = 11
92QUEEN = 12
93KING = 13
94ALLVALUES = range(1, 14) # (one more than the highest value)
95NVALUES = 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
102VALNAMES = ["", "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
108NROWS = 7
109
110
111# The rest of the program consists of class definitions.  These are
112# further described in their documentation strings.
113
114
115class 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
217class 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
387class 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
436def 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
447class 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
478class 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
502class 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
525class 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
630def main():
631    root = Tk()
632    game = Solitaire(root)
633    root.protocol('WM_DELETE_WINDOW', root.quit)
634    root.mainloop()
635
636if __name__ == '__main__':
637    main()
638