Home | History | Annotate | Download | only in guido
      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()
    638