| #! /usr/bin/env python | |
| """Solitaire game, much like the one that comes with MS Windows. | |
| Limitations: | |
| - No cute graphical images for the playing cards faces or backs. | |
| - No scoring or timer. | |
| - No undo. | |
| - No option to turn 3 cards at a time. | |
| - No keyboard shortcuts. | |
| - Less fancy animation when you win. | |
| - The determination of which stack you drag to is more relaxed. | |
| Apology: | |
| I'm not much of a card player, so my terminology in these comments may | |
| at times be a little unusual. If you have suggestions, please let me | |
| know! | |
| """ | |
| # Imports | |
| import math | |
| import random | |
| from Tkinter import * | |
| from Canvas import Rectangle, CanvasText, Group, Window | |
| # Fix a bug in Canvas.Group as distributed in Python 1.4. The | |
| # distributed bind() method is broken. Rather than asking you to fix | |
| # the source, we fix it here by deriving a subclass: | |
| class Group(Group): | |
| def bind(self, sequence=None, command=None): | |
| return self.canvas.tag_bind(self.id, sequence, command) | |
| # Constants determining the size and lay-out of cards and stacks. We | |
| # work in a "grid" where each card/stack is surrounded by MARGIN | |
| # pixels of space on each side, so adjacent stacks are separated by | |
| # 2*MARGIN pixels. OFFSET is the offset used for displaying the | |
| # face down cards in the row stacks. | |
| CARDWIDTH = 100 | |
| CARDHEIGHT = 150 | |
| MARGIN = 10 | |
| XSPACING = CARDWIDTH + 2*MARGIN | |
| YSPACING = CARDHEIGHT + 4*MARGIN | |
| OFFSET = 5 | |
| # The background color, green to look like a playing table. The | |
| # standard green is way too bright, and dark green is way to dark, so | |
| # we use something in between. (There are a few more colors that | |
| # could be customized, but they are less controversial.) | |
| BACKGROUND = '#070' | |
| # Suits and colors. The values of the symbolic suit names are the | |
| # strings used to display them (you change these and VALNAMES to | |
| # internationalize the game). The COLOR dictionary maps suit names to | |
| # colors (red and black) which must be Tk color names. The keys() of | |
| # the COLOR dictionary conveniently provides us with a list of all | |
| # suits (in arbitrary order). | |
| HEARTS = 'Heart' | |
| DIAMONDS = 'Diamond' | |
| CLUBS = 'Club' | |
| SPADES = 'Spade' | |
| RED = 'red' | |
| BLACK = 'black' | |
| COLOR = {} | |
| for s in (HEARTS, DIAMONDS): | |
| COLOR[s] = RED | |
| for s in (CLUBS, SPADES): | |
| COLOR[s] = BLACK | |
| ALLSUITS = COLOR.keys() | |
| NSUITS = len(ALLSUITS) | |
| # Card values are 1-13. We also define symbolic names for the picture | |
| # cards. ALLVALUES is a list of all card values. | |
| ACE = 1 | |
| JACK = 11 | |
| QUEEN = 12 | |
| KING = 13 | |
| ALLVALUES = range(1, 14) # (one more than the highest value) | |
| NVALUES = len(ALLVALUES) | |
| # VALNAMES is a list that maps a card value to string. It contains a | |
| # dummy element at index 0 so it can be indexed directly with the card | |
| # value. | |
| VALNAMES = ["", "A"] + map(str, range(2, 11)) + ["J", "Q", "K"] | |
| # Solitaire constants. The only one I can think of is the number of | |
| # row stacks. | |
| NROWS = 7 | |
| # The rest of the program consists of class definitions. These are | |
| # further described in their documentation strings. | |
| class Card: | |
| """A playing card. | |
| A card doesn't record to which stack it belongs; only the stack | |
| records this (it turns out that we always know this from the | |
| context, and this saves a ``double update'' with potential for | |
| inconsistencies). | |
| Public methods: | |
| moveto(x, y) -- move the card to an absolute position | |
| moveby(dx, dy) -- move the card by a relative offset | |
| tkraise() -- raise the card to the top of its stack | |
| showface(), showback() -- turn the card face up or down & raise it | |
| Public read-only instance variables: | |
| suit, value, color -- the card's suit, value and color | |
| face_shown -- true when the card is shown face up, else false | |
| Semi-public read-only instance variables (XXX should be made | |
| private): | |
| group -- the Canvas.Group representing the card | |
| x, y -- the position of the card's top left corner | |
| Private instance variables: | |
| __back, __rect, __text -- the canvas items making up the card | |
| (To show the card face up, the text item is placed in front of | |
| rect and the back is placed behind it. To show it face down, this | |
| is reversed. The card is created face down.) | |
| """ | |
| def __init__(self, suit, value, canvas): | |
| """Card constructor. | |
| Arguments are the card's suit and value, and the canvas widget. | |
| The card is created at position (0, 0), with its face down | |
| (adding it to a stack will position it according to that | |
| stack's rules). | |
| """ | |
| self.suit = suit | |
| self.value = value | |
| self.color = COLOR[suit] | |
| self.face_shown = 0 | |
| self.x = self.y = 0 | |
| self.group = Group(canvas) | |
| text = "%s %s" % (VALNAMES[value], suit) | |
| self.__text = CanvasText(canvas, CARDWIDTH//2, 0, | |
| anchor=N, fill=self.color, text=text) | |
| self.group.addtag_withtag(self.__text) | |
| self.__rect = Rectangle(canvas, 0, 0, CARDWIDTH, CARDHEIGHT, | |
| outline='black', fill='white') | |
| self.group.addtag_withtag(self.__rect) | |
| self.__back = Rectangle(canvas, MARGIN, MARGIN, | |
| CARDWIDTH-MARGIN, CARDHEIGHT-MARGIN, | |
| outline='black', fill='blue') | |
| self.group.addtag_withtag(self.__back) | |
| def __repr__(self): | |
| """Return a string for debug print statements.""" | |
| return "Card(%r, %r)" % (self.suit, self.value) | |
| def moveto(self, x, y): | |
| """Move the card to absolute position (x, y).""" | |
| self.moveby(x - self.x, y - self.y) | |
| def moveby(self, dx, dy): | |
| """Move the card by (dx, dy).""" | |
| self.x = self.x + dx | |
| self.y = self.y + dy | |
| self.group.move(dx, dy) | |
| def tkraise(self): | |
| """Raise the card above all other objects in its canvas.""" | |
| self.group.tkraise() | |
| def showface(self): | |
| """Turn the card's face up.""" | |
| self.tkraise() | |
| self.__rect.tkraise() | |
| self.__text.tkraise() | |
| self.face_shown = 1 | |
| def showback(self): | |
| """Turn the card's face down.""" | |
| self.tkraise() | |
| self.__rect.tkraise() | |
| self.__back.tkraise() | |
| self.face_shown = 0 | |
| class Stack: | |
| """A generic stack of cards. | |
| This is used as a base class for all other stacks (e.g. the deck, | |
| the suit stacks, and the row stacks). | |
| Public methods: | |
| add(card) -- add a card to the stack | |
| delete(card) -- delete a card from the stack | |
| showtop() -- show the top card (if any) face up | |
| deal() -- delete and return the top card, or None if empty | |
| Method that subclasses may override: | |
| position(card) -- move the card to its proper (x, y) position | |
| The default position() method places all cards at the stack's | |
| own (x, y) position. | |
| userclickhandler(), userdoubleclickhandler() -- called to do | |
| subclass specific things on single and double clicks | |
| The default user (single) click handler shows the top card | |
| face up. The default user double click handler calls the user | |
| single click handler. | |
| usermovehandler(cards) -- called to complete a subpile move | |
| The default user move handler moves all moved cards back to | |
| their original position (by calling the position() method). | |
| Private methods: | |
| clickhandler(event), doubleclickhandler(event), | |
| motionhandler(event), releasehandler(event) -- event handlers | |
| The default event handlers turn the top card of the stack with | |
| its face up on a (single or double) click, and also support | |
| moving a subpile around. | |
| startmoving(event) -- begin a move operation | |
| finishmoving() -- finish a move operation | |
| """ | |
| def __init__(self, x, y, game=None): | |
| """Stack constructor. | |
| Arguments are the stack's nominal x and y position (the top | |
| left corner of the first card placed in the stack), and the | |
| game object (which is used to get the canvas; subclasses use | |
| the game object to find other stacks). | |
| """ | |
| self.x = x | |
| self.y = y | |
| self.game = game | |
| self.cards = [] | |
| self.group = Group(self.game.canvas) | |
| self.group.bind('<1>', self.clickhandler) | |
| self.group.bind('<Double-1>', self.doubleclickhandler) | |
| self.group.bind('<B1-Motion>', self.motionhandler) | |
| self.group.bind('<ButtonRelease-1>', self.releasehandler) | |
| self.makebottom() | |
| def makebottom(self): | |
| pass | |
| def __repr__(self): | |
| """Return a string for debug print statements.""" | |
| return "%s(%d, %d)" % (self.__class__.__name__, self.x, self.y) | |
| # Public methods | |
| def add(self, card): | |
| self.cards.append(card) | |
| card.tkraise() | |
| self.position(card) | |
| self.group.addtag_withtag(card.group) | |
| def delete(self, card): | |
| self.cards.remove(card) | |
| card.group.dtag(self.group) | |
| def showtop(self): | |
| if self.cards: | |
| self.cards[-1].showface() | |
| def deal(self): | |
| if not self.cards: | |
| return None | |
| card = self.cards[-1] | |
| self.delete(card) | |
| return card | |
| # Subclass overridable methods | |
| def position(self, card): | |
| card.moveto(self.x, self.y) | |
| def userclickhandler(self): | |
| self.showtop() | |
| def userdoubleclickhandler(self): | |
| self.userclickhandler() | |
| def usermovehandler(self, cards): | |
| for card in cards: | |
| self.position(card) | |
| # Event handlers | |
| def clickhandler(self, event): | |
| self.finishmoving() # In case we lost an event | |
| self.userclickhandler() | |
| self.startmoving(event) | |
| def motionhandler(self, event): | |
| self.keepmoving(event) | |
| def releasehandler(self, event): | |
| self.keepmoving(event) | |
| self.finishmoving() | |
| def doubleclickhandler(self, event): | |
| self.finishmoving() # In case we lost an event | |
| self.userdoubleclickhandler() | |
| self.startmoving(event) | |
| # Move internals | |
| moving = None | |
| def startmoving(self, event): | |
| self.moving = None | |
| tags = self.game.canvas.gettags('current') | |
| for i in range(len(self.cards)): | |
| card = self.cards[i] | |
| if card.group.tag in tags: | |
| break | |
| else: | |
| return | |
| if not card.face_shown: | |
| return | |
| self.moving = self.cards[i:] | |
| self.lastx = event.x | |
| self.lasty = event.y | |
| for card in self.moving: | |
| card.tkraise() | |
| def keepmoving(self, event): | |
| if not self.moving: | |
| return | |
| dx = event.x - self.lastx | |
| dy = event.y - self.lasty | |
| self.lastx = event.x | |
| self.lasty = event.y | |
| if dx or dy: | |
| for card in self.moving: | |
| card.moveby(dx, dy) | |
| def finishmoving(self): | |
| cards = self.moving | |
| self.moving = None | |
| if cards: | |
| self.usermovehandler(cards) | |
| class Deck(Stack): | |
| """The deck is a stack with support for shuffling. | |
| New methods: | |
| fill() -- create the playing cards | |
| shuffle() -- shuffle the playing cards | |
| A single click moves the top card to the game's open deck and | |
| moves it face up; if we're out of cards, it moves the open deck | |
| back to the deck. | |
| """ | |
| def makebottom(self): | |
| bottom = Rectangle(self.game.canvas, | |
| self.x, self.y, | |
| self.x+CARDWIDTH, self.y+CARDHEIGHT, | |
| outline='black', fill=BACKGROUND) | |
| self.group.addtag_withtag(bottom) | |
| def fill(self): | |
| for suit in ALLSUITS: | |
| for value in ALLVALUES: | |
| self.add(Card(suit, value, self.game.canvas)) | |
| def shuffle(self): | |
| n = len(self.cards) | |
| newcards = [] | |
| for i in randperm(n): | |
| newcards.append(self.cards[i]) | |
| self.cards = newcards | |
| def userclickhandler(self): | |
| opendeck = self.game.opendeck | |
| card = self.deal() | |
| if not card: | |
| while 1: | |
| card = opendeck.deal() | |
| if not card: | |
| break | |
| self.add(card) | |
| card.showback() | |
| else: | |
| self.game.opendeck.add(card) | |
| card.showface() | |
| def randperm(n): | |
| """Function returning a random permutation of range(n).""" | |
| r = range(n) | |
| x = [] | |
| while r: | |
| i = random.choice(r) | |
| x.append(i) | |
| r.remove(i) | |
| return x | |
| class OpenStack(Stack): | |
| def acceptable(self, cards): | |
| return 0 | |
| def usermovehandler(self, cards): | |
| card = cards[0] | |
| stack = self.game.closeststack(card) | |
| if not stack or stack is self or not stack.acceptable(cards): | |
| Stack.usermovehandler(self, cards) | |
| else: | |
| for card in cards: | |
| self.delete(card) | |
| stack.add(card) | |
| self.game.wincheck() | |
| def userdoubleclickhandler(self): | |
| if not self.cards: | |
| return | |
| card = self.cards[-1] | |
| if not card.face_shown: | |
| self.userclickhandler() | |
| return | |
| for s in self.game.suits: | |
| if s.acceptable([card]): | |
| self.delete(card) | |
| s.add(card) | |
| self.game.wincheck() | |
| break | |
| class SuitStack(OpenStack): | |
| def makebottom(self): | |
| bottom = Rectangle(self.game.canvas, | |
| self.x, self.y, | |
| self.x+CARDWIDTH, self.y+CARDHEIGHT, | |
| outline='black', fill='') | |
| def userclickhandler(self): | |
| pass | |
| def userdoubleclickhandler(self): | |
| pass | |
| def acceptable(self, cards): | |
| if len(cards) != 1: | |
| return 0 | |
| card = cards[0] | |
| if not self.cards: | |
| return card.value == ACE | |
| topcard = self.cards[-1] | |
| return card.suit == topcard.suit and card.value == topcard.value + 1 | |
| class RowStack(OpenStack): | |
| def acceptable(self, cards): | |
| card = cards[0] | |
| if not self.cards: | |
| return card.value == KING | |
| topcard = self.cards[-1] | |
| if not topcard.face_shown: | |
| return 0 | |
| return card.color != topcard.color and card.value == topcard.value - 1 | |
| def position(self, card): | |
| y = self.y | |
| for c in self.cards: | |
| if c == card: | |
| break | |
| if c.face_shown: | |
| y = y + 2*MARGIN | |
| else: | |
| y = y + OFFSET | |
| card.moveto(self.x, y) | |
| class Solitaire: | |
| def __init__(self, master): | |
| self.master = master | |
| self.canvas = Canvas(self.master, | |
| background=BACKGROUND, | |
| highlightthickness=0, | |
| width=NROWS*XSPACING, | |
| height=3*YSPACING + 20 + MARGIN) | |
| self.canvas.pack(fill=BOTH, expand=TRUE) | |
| self.dealbutton = Button(self.canvas, | |
| text="Deal", | |
| highlightthickness=0, | |
| background=BACKGROUND, | |
| activebackground="green", | |
| command=self.deal) | |
| Window(self.canvas, MARGIN, 3*YSPACING + 20, | |
| window=self.dealbutton, anchor=SW) | |
| x = MARGIN | |
| y = MARGIN | |
| self.deck = Deck(x, y, self) | |
| x = x + XSPACING | |
| self.opendeck = OpenStack(x, y, self) | |
| x = x + XSPACING | |
| self.suits = [] | |
| for i in range(NSUITS): | |
| x = x + XSPACING | |
| self.suits.append(SuitStack(x, y, self)) | |
| x = MARGIN | |
| y = y + YSPACING | |
| self.rows = [] | |
| for i in range(NROWS): | |
| self.rows.append(RowStack(x, y, self)) | |
| x = x + XSPACING | |
| self.openstacks = [self.opendeck] + self.suits + self.rows | |
| self.deck.fill() | |
| self.deal() | |
| def wincheck(self): | |
| for s in self.suits: | |
| if len(s.cards) != NVALUES: | |
| return | |
| self.win() | |
| self.deal() | |
| def win(self): | |
| """Stupid animation when you win.""" | |
| cards = [] | |
| for s in self.openstacks: | |
| cards = cards + s.cards | |
| while cards: | |
| card = random.choice(cards) | |
| cards.remove(card) | |
| self.animatedmoveto(card, self.deck) | |
| def animatedmoveto(self, card, dest): | |
| for i in range(10, 0, -1): | |
| dx, dy = (dest.x-card.x)//i, (dest.y-card.y)//i | |
| card.moveby(dx, dy) | |
| self.master.update_idletasks() | |
| def closeststack(self, card): | |
| closest = None | |
| cdist = 999999999 | |
| # Since we only compare distances, | |
| # we don't bother to take the square root. | |
| for stack in self.openstacks: | |
| dist = (stack.x - card.x)**2 + (stack.y - card.y)**2 | |
| if dist < cdist: | |
| closest = stack | |
| cdist = dist | |
| return closest | |
| def deal(self): | |
| self.reset() | |
| self.deck.shuffle() | |
| for i in range(NROWS): | |
| for r in self.rows[i:]: | |
| card = self.deck.deal() | |
| r.add(card) | |
| for r in self.rows: | |
| r.showtop() | |
| def reset(self): | |
| for stack in self.openstacks: | |
| while 1: | |
| card = stack.deal() | |
| if not card: | |
| break | |
| self.deck.add(card) | |
| card.showback() | |
| # Main function, run when invoked as a stand-alone Python program. | |
| def main(): | |
| root = Tk() | |
| game = Solitaire(root) | |
| root.protocol('WM_DELETE_WINDOW', root.quit) | |
| root.mainloop() | |
| if __name__ == '__main__': | |
| main() |