638 lines
17 KiB
Python
Executable File
638 lines
17 KiB
Python
Executable File
#! /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 = list(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"] + list(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()
|