1#! /usr/bin/env python 2 3"""Solitaire game, much like the one that comes with MS Windows. 4 5Limitations: 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 15Apology: 16 17I'm not much of a card player, so my terminology in these comments may 18at times be a little unusual. If you have suggestions, please let me 19know! 20 21""" 22 23# Imports 24 25import math 26import random 27 28from Tkinter import * 29from 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 36class 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 47CARDWIDTH = 100 48CARDHEIGHT = 150 49MARGIN = 10 50XSPACING = CARDWIDTH + 2*MARGIN 51YSPACING = CARDHEIGHT + 4*MARGIN 52OFFSET = 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 59BACKGROUND = '#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 69HEARTS = 'Heart' 70DIAMONDS = 'Diamond' 71CLUBS = 'Club' 72SPADES = 'Spade' 73 74RED = 'red' 75BLACK = 'black' 76 77COLOR = {} 78for s in (HEARTS, DIAMONDS): 79 COLOR[s] = RED 80for s in (CLUBS, SPADES): 81 COLOR[s] = BLACK 82 83ALLSUITS = COLOR.keys() 84NSUITS = 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 90ACE = 1 91JACK = 11 92QUEEN = 12 93KING = 13 94ALLVALUES = range(1, 14) # (one more than the highest value) 95NVALUES = 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 102VALNAMES = ["", "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 108NROWS = 7 109 110 111# The rest of the program consists of class definitions. These are 112# further described in their documentation strings. 113 114 115class 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 217class 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 387class 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 436def 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 447class 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 478class 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 502class 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 525class 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 630def main(): 631 root = Tk() 632 game = Solitaire(root) 633 root.protocol('WM_DELETE_WINDOW', root.quit) 634 root.mainloop() 635 636if __name__ == '__main__': 637 main() 638