- No keyboard shortcuts.
- Less fancy animation when you win.
- The determination of which stack you drag to is more relaxed.
-
-Bugs:
-
-- When you double-click a card on a temp stack to move it to the suit
-stack, if the next card is face down, you have to wait until the
-double-click time-out expires before you can click it to turn it.
-I think this has to do with Tk's multiple-click detection, which means
-it's hard to work around.
Apology:
import random
from Tkinter import *
-from Canvas import Rectangle, CanvasText, Group
+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. This is what should be used:
+# 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):
# 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.
+# 2*MARGIN pixels. OFFSET is the offset used for displaying the
+# face down cards in the row stacks.
CARDWIDTH = 100
CARDHEIGHT = 150
NSUITS = len(ALLSUITS)
-# Card values are 1-13, with symbolic names for the picture cards.
-# ALLVALUES is a list of all card values.
+# 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
NROWS = 7
-# The rest of the program consists of class definitions. Read their
-# doc strings.
-
-class Bottom:
-
- """A "card-like" object to serve as the bottom for some stacks.
-
- Specifically, this is used by the deck and the suit stacks.
-
- """
-
- def __init__(self, stack):
-
- """Constructor, taking the stack as an argument.
-
- We displays ourselves as a gray rectangle the size of a
- playing card, positioned at the stack's x and y location.
-
- We register the stack's bottomhandler to handle clicks.
-
- No other behavior.
-
- """
-
- self.rect = Rectangle(stack.game.canvas,
- stack.x, stack.y,
- stack.x+CARDWIDTH, stack.y+CARDHEIGHT,
- outline='black', fill='gray')
- self.rect.bind('<ButtonRelease-1>', stack.bottomhandler)
+# 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
- turnover() -- turn the card (face up or down) & raise it
- onclick(handler), ondouble(handler), onmove(handler),
- onrelease(handler) -- set various mount event handlers
- reset() -- move the card out of sight, face down, and reset all
- event handlers
- Public instance variables:
+ Public read-only instance variables:
- color, suit, value -- the card's color, suit and value
+ suit, value, color -- the card's suit, value and color
face_shown -- true when the card is shown face up, else false
- Semi-public instance variables (XXX should be made private):
+ 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
(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.)
+ is reversed. The card is created face down.)
"""
- def __init__(self, game, suit, value):
+ 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.color = COLOR[suit]
self.value = value
- canvas = game.canvas
+ self.color = COLOR[suit]
+ self.face_shown = 0
+
self.x = self.y = 0
- self.__back = Rectangle(canvas, MARGIN, MARGIN,
- CARDWIDTH-MARGIN, CARDHEIGHT-MARGIN,
- outline='black', fill='blue')
- self.__rect = Rectangle(canvas, 0, 0, CARDWIDTH, CARDHEIGHT,
- outline='black', fill='white')
+ 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 = Group(canvas)
- self.group.addtag_withtag(self.__back)
- self.group.addtag_withtag(self.__rect)
self.group.addtag_withtag(self.__text)
- self.reset()
+
+ 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 "Card(game, %s, %s)" % (`self.suit`, `self.value`)
+ """Return a string for debug print statements."""
+ return "Card(%s, %s)" % (`self.suit`, `self.value`)
def moveto(self, x, y):
- dx = x - self.x
- dy = y - self.y
- self.group.move(dx, dy)
- self.x = x
- self.y = y
+ """Move the card to absolute position (x, y)."""
+ self.moveby(x - self.x, y - self.y)
def moveby(self, dx, dy):
- self.moveto(self.x + dx, self.y + 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
- def turnover(self):
- if self.face_shown:
- self.showback()
- else:
- self.showface()
- def onclick(self, handler):
- self.group.bind('<1>', handler)
+class Stack:
- def ondouble(self, handler):
- self.group.bind('<Double-1>', handler)
+ """A generic stack of cards.
- def onmove(self, handler):
- self.group.bind('<B1-Motion>', handler)
+ This is used as a base class for all other stacks (e.g. the deck,
+ the suit stacks, and the row stacks).
- def onrelease(self, handler):
- self.group.bind('<ButtonRelease-1>', handler)
+ Public methods:
- def reset(self):
- self.moveto(-1000, -1000) # Out of sight
- self.onclick('')
- self.ondouble('')
- self.onmove('')
- self.onrelease('')
- self.showback()
+ 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
-class Deck:
+ Method that subclasses may override:
- def __init__(self, game):
- self.game = game
- self.allcards = []
- for suit in ALLSUITS:
- for value in ALLVALUES:
- self.allcards.append(Card(self.game, suit, value))
- self.reset()
+ position(card) -- move the card to its proper (x, y) position
- def shuffle(self):
- n = len(self.cards)
- newcards = []
- for i in randperm(n):
- newcards.append(self.cards[i])
- self.cards = newcards
+ The default position() method places all cards at the stack's
+ own (x, y) position.
- def deal(self):
- # Raise IndexError when no more cards
- card = self.cards[-1]
- del self.cards[-1]
- return card
+ userclickhandler(), userdoubleclickhandler() -- called to do
+ subclass specific things on single and double clicks
- def accept(self, card):
- if card not in self.cards:
- self.cards.append(card)
+ The default user (single) click handler shows the top card
+ face up. The default user double click handler calls the user
+ single click handler.
- def reset(self):
- self.cards = self.allcards[:]
- for card in self.cards:
- card.reset()
+ usermovehandler(cards) -- called to complete a subpile move
-def randperm(n):
- r = range(n)
- x = []
- while r:
- i = random.choice(r)
- x.append(i)
- r.remove(i)
- return x
+ The default user move handler moves all moved cards back to
+ their original position (by calling the position() method).
-class Stack:
+ Private methods:
- x = MARGIN
- y = MARGIN
+ clickhandler(event), doubleclickhandler(event),
+ motionhandler(event), releasehandler(event) -- event handlers
- def __init__(self, game):
+ 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 "<Stack at (%d, %d)>" % (self.x, self.y)
+ """Return a string for debug print statements."""
+ return "%s(%d, %d)" % (self.__class__.__name__, self.x, self.y)
- def reset(self):
- self.cards = []
-
- def acceptable(self, cards):
- return 1
+ # Public methods
- def accept(self, card):
+ def add(self, card):
self.cards.append(card)
- card.onclick(self.clickhandler)
- card.onmove(self.movehandler)
- card.onrelease(self.releasehandler)
- card.ondouble(self.doublehandler)
card.tkraise()
- self.placecard(card)
+ self.position(card)
+ self.group.addtag_withtag(card.group)
- def placecard(self, card):
- card.moveto(self.x, self.y)
+ def delete(self, card):
+ self.cards.remove(card)
+ card.group.dtag(self.group)
def showtop(self):
if self.cards:
self.cards[-1].showface()
- def clickhandler(self, event):
- pass
+ def deal(self):
+ if not self.cards:
+ return None
+ card = self.cards[-1]
+ self.delete(card)
+ return card
- def movehandler(self, event):
- pass
+ # Subclass overridable methods
- def releasehandler(self, event):
- pass
+ def position(self, card):
+ card.moveto(self.x, self.y)
- def doublehandler(self, event):
- pass
+ def userclickhandler(self):
+ self.showtop()
+
+ def userdoubleclickhandler(self):
+ self.userclickhandler()
-class PoolStack(Stack):
+ def usermovehandler(self, cards):
+ for card in cards:
+ self.position(card)
- def __init__(self, game):
- Stack.__init__(self, game)
- self.bottom = Bottom(self)
+ # 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):
- if not self.cards:
- return
- card = self.cards[-1]
- self.game.turned.accept(card)
- del self.cards[-1]
- card.showface()
+ self.keepmoving(event)
+ self.finishmoving()
- def bottomhandler(self, event):
- cards = self.game.turned.cards
- cards.reverse()
- for card in cards:
- card.showback()
- self.accept(card)
- self.game.turned.reset()
+ def doubleclickhandler(self, event):
+ self.finishmoving() # In case we lost an event
+ self.userdoubleclickhandler()
+ self.startmoving(event)
-class MovingStack(Stack):
+ # Move internals
- thecards = None
- theindex = None
+ moving = None
- def clickhandler(self, event):
- self.thecards = self.theindex = None # Just in case
+ def startmoving(self, event):
+ self.moving = None
tags = self.game.canvas.gettags('current')
- if not tags:
- return
- tag = tags[0]
for i in range(len(self.cards)):
card = self.cards[i]
- if tag == str(card.group):
+ if card.group.tag in tags:
break
else:
return
- self.theindex = i
- self.thecards = Group(self.game.canvas)
- for card in self.cards[i:]:
- self.thecards.addtag_withtag(card.group)
- self.thecards.tkraise()
- self.lastx = self.firstx = event.x
- self.lasty = self.firsty = event.y
-
- def movehandler(self, event):
- if not self.thecards:
- return
- card = self.cards[self.theindex]
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.thecards.move(dx, dy)
self.lastx = event.x
self.lasty = event.y
+ if dx or dy:
+ for card in self.moving:
+ card.moveby(dx, dy)
- def releasehandler(self, event):
- cards = self._endmove()
- if not cards:
- return
+ 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 usermovehandler(self, cards):
card = cards[0]
- if not card.face_shown:
- if len(cards) == 1:
- card.showface()
- self.thecards = self.theindex = None
- return
- stack = self.game.closeststack(cards[0])
- if stack and stack is not self and stack.acceptable(cards):
- for card in cards:
- stack.accept(card)
- self.cards.remove(card)
+ 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.placecard(card)
+ self.delete(card)
+ stack.add(card)
+ self.game.wincheck()
- def doublehandler(self, event):
- cards = self._endmove()
- if not cards:
+ def userdoubleclickhandler(self):
+ if not self.cards:
return
- for stack in self.game.suits:
- if stack.acceptable(cards):
- break
- else:
+ card = self.cards[-1]
+ if not card.face_shown:
+ self.userclickhandler()
return
- for card in cards:
- stack.accept(card)
- del self.cards[self.theindex:]
- self.thecards = self.theindex = None
-
- def _endmove(self):
- if not self.thecards:
- return []
- self.thecards.move(self.firstx - self.lastx,
- self.firsty - self.lasty)
- self.thecards.dtag()
- cards = self.cards[self.theindex:]
- if not cards:
- return []
- card = cards[0]
- card.moveby(self.lastx - self.firstx, self.lasty - self.firsty)
- self.lastx = self.firstx
- self.lasty = self.firsty
- return cards
-
-class TurnedStack(MovingStack):
-
- x = XSPACING + MARGIN
- y = MARGIN
+ for s in self.game.suits:
+ if s.acceptable([card]):
+ self.delete(card)
+ s.add(card)
+ self.game.wincheck()
+ break
-class SuitStack(MovingStack):
- y = MARGIN
+class SuitStack(OpenStack):
- def __init__(self, game, i):
- self.index = i
- self.x = MARGIN + XSPACING * (i+3)
- Stack.__init__(self, game)
- self.bottom = Bottom(self)
+ def makebottom(self):
+ bottom = Rectangle(self.game.canvas,
+ self.x, self.y,
+ self.x+CARDWIDTH, self.y+CARDHEIGHT,
+ outline='black', fill='')
- bottomhandler = ""
+ def userclickhandler(self):
+ pass
- def __repr__(self):
- return "SuitStack(game, %d)" % self.index
+ def userdoubleclickhandler(self):
+ pass
def acceptable(self, cards):
if len(cards) != 1:
return 0
card = cards[0]
- if not card.face_shown:
- return 0
if not self.cards:
return card.value == ACE
topcard = self.cards[-1]
- if not topcard.face_shown:
- return 0
return card.suit == topcard.suit and card.value == topcard.value + 1
- def doublehandler(self, event):
- pass
-
- def accept(self, card):
- MovingStack.accept(self, card)
- if card.value == KING:
- # See if we won
- for s in self.game.suits:
- card = s.cards[-1]
- if card.value != KING:
- return
- self.game.win()
- self.game.deal()
-
-class RowStack(MovingStack):
-
- def __init__(self, game, i):
- self.index = i
- self.x = MARGIN + XSPACING * i
- self.y = MARGIN + YSPACING
- Stack.__init__(self, game)
- def __repr__(self):
- return "RowStack(game, %d)" % self.index
-
- def placecard(self, card):
- offset = 0
- for c in self.cards:
- if c is card:
- break
- if c.face_shown:
- offset = offset + 2*MARGIN
- else:
- offset = offset + OFFSET
- card.moveto(self.x, self.y + offset)
+class RowStack(OpenStack):
def acceptable(self, cards):
card = cards[0]
- if not card.face_shown:
- return 0
if not self.cards:
return card.value == KING
topcard = self.cards[-1]
if not topcard.face_shown:
return 0
- if card.value != topcard.value - 1:
- return 0
- if card.color == topcard.color:
- return 0
- return 1
+ 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.buttonframe = Frame(self.master, background=BACKGROUND)
- self.buttonframe.pack(fill=X)
+ 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.buttonframe,
+ self.dealbutton = Button(self.canvas,
text="Deal",
highlightthickness=0,
background=BACKGROUND,
activebackground="green",
command=self.deal)
- self.dealbutton.pack(side=LEFT)
+ Window(self.canvas, MARGIN, 3*YSPACING + 20,
+ window=self.dealbutton, anchor=SW)
- self.canvas = Canvas(self.master,
- background=BACKGROUND,
- highlightthickness=0,
- width=NROWS*XSPACING,
- height=3*YSPACING)
- self.canvas.pack(fill=BOTH, expand=TRUE)
+ x = MARGIN
+ y = MARGIN
- self.deck = Deck(self)
-
- self.pool = PoolStack(self)
- self.turned = TurnedStack(self)
+ 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):
- self.suits.append(SuitStack(self, i))
+ 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(self, i))
+ self.rows.append(RowStack(x, y, self))
+ x = x + XSPACING
+ 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 = self.deck.allcards
+ cards = []
+ for s in self.suits:
+ cards = cards + s.cards
+ if not cards:
+ return
for i in range(1000):
card = random.choice(cards)
dx = random.randint(-50, 50)
cdist = dist
return closest
- def reset(self):
- self.pool.reset()
- self.turned.reset()
- for stack in self.rows + self.suits:
- stack.reset()
- self.deck.reset()
-
def deal(self):
self.reset()
self.deck.shuffle()
for i in range(NROWS):
for r in self.rows[i:]:
card = self.deck.deal()
- r.accept(card)
+ r.add(card)
for r in self.rows:
r.showtop()
- try:
+
+ def reset(self):
+ for stack in [self.opendeck] + self.suits + self.rows:
while 1:
- self.pool.accept(self.deck.deal())
- except IndexError:
- pass
+ card = stack.deal()
+ if not card:
+ break
+ self.deck.add(card)
+ card.showback()
# Main function, run when invoked as a stand-alone Python program.