| #! /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() |