# Quick-and-dirty implementation of the familiar freecell game. # # Michael L. Scott, Jan. 2010 # # Uses John Zelle's graphics.py package. # This is nice and simple, but has some limitations. In particular, # in the current version (4.1), the graphics window will not be "live" # (meaning it won't respond to certain normal GUI events like rising # to the front when clicked on) unless you're in the middle of a # getMouse() call. Sorry about that. # # Meant to be called into from the interpreter. # Launch with 'execfile("freecell.py")' # Then call one or more of the following: # # play() Start game with current cards. Reset to starting # positions if any of them have been moved. # shuffle(s) Switch to shuffle number s. # If s is absent, pick a new random shuffle. # clear() Erase all the cards from the field. # deal() Deal out the cards, using the current shuffle. # close() Close the game window. Other entry points will no # longer work. # # To stop the game in the middle, type ^C (control-C) in the console # window. ######################################################## # Search for all occurrences of "YOUR CODE HERE" and # replace with code that makes the program work. ######################################################## import graphics from graphics import * import random, time # Space to hold a card, in pixels: cellWd = 50 cellHt = 40 # Close any previous instance of the playing field: try: w.close() except: pass # Create the playing field: w = GraphWin("FreeCell", cellWd*8+2, cellHt*20+2) w.setBackground("yellow") # Upper left area: "nooks" for <= 4 cards: r1 = Rectangle(Point(3,3), Point(cellWd*4+3, cellHt+3)) r1.setFill("orange") r1.draw(w) # Subdivide: l1 = Line(Point(cellWd+3,3), Point(cellWd+3,cellHt+3)) l2 = Line(Point(cellWd*2+3,3), Point(cellWd*2+3,cellHt+3)) l3 = Line(Point(cellWd*3+3,3), Point(cellWd*3+3,cellHt+3)) l1.draw(w); l2.draw(w); l3.draw(w) # Upper right area: "homes" for suits: r2 = Rectangle(Point(cellWd*4+3,3), Point(cellWd*8+3,cellHt+3)) r2.setFill("red") r2.draw(w) # Subdivide: l4 = Line(Point(cellWd*5+3,3), Point(cellWd*5+3,cellHt+3)) l5 = Line(Point(cellWd*6+3,3), Point(cellWd*6+3,cellHt+3)) l6 = Line(Point(cellWd*7+3,3), Point(cellWd*7+3,cellHt+3)) l4.draw(w); l5.draw(w); l6.draw(w) # Flash the field, e.g. in response to an invalid move: def flash(): w.setBackground("black") time.sleep(0.2) w.setBackground("yellow") # The graphics package implicitly stacks objects in the order they were # drawn, so the above will be under all the cards, permanently. # Cards will (see below) be created from the integers 0..52. # The low two bits will index into suitNames; the rest (/4) will index # into cardNames to give us the value. cardNames = ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K") suitNames = ("H", "C", "D", "S") # Three exceptions are used for nonlocal control flow. # We raise InvalidMove when a click doesn't indicate anything currently # allowed in the game; we raise WonGame when all cards have been placed # in the homes. class InvalidMove(Exception): pass # 'pass' means 'this space intentionally left blank class WonGame(Exception): pass class card: # color "red" or "black" -- used for printing label # and for comparing for equality/inequality # key 0..53 used for sorting # suit 0..3 == key % 4 # val 0..12 == key / 4 # image, label -- graphical elements to display on playing field def __init__(self, n): self.key = n assert n >= 0 and n < 52 # Assertions are for debugging. They should never be true. # If they ever are, there's something I don't understand, and # I want Python to stop the program and tell me about it. self.suit = n % 4 if n % 2 == 0: self.color = "red" else: self.color = "black" self.val = n/4 self.image = None self.label = None def __cmp__(self, other): # The special method __cmp__ is used for equality and ordering # comparisons. It should return 1 if I'm bigger than other, # zero if I'm the same, and -1 if I'm smaller. if other.__class__ == None.__class__: return 1 # any card is bigger than nothing if other.__class__ != self.__class__: assert False # I don't expect to compare to anything # other than another card or None. return self.key.__cmp__(other.key) def undisplay(self): # erase me from the field if self.image != None: self.image.undraw() self.label.undraw() self.image = None def display(self, col, row): # display me in the col-th column of the row-th row of the field self.undisplay() if row == 0: offset = 0 else: offset = 10 self.image = Rectangle(Point(col*cellWd+9, row*cellHt+8+offset), Point((col+1)*cellWd-2, (row+1)*cellHt-2+offset)) self.image.setFill("white") self.image.setWidth(4) self.label = Text(Point(col*cellWd+cellWd/2+3, row*cellHt+cellHt/2+3+offset), cardNames[self.val] + suitNames[self.suit]) self.label.setSize(16) # point size self.label.setStyle("bold") self.label.setTextColor(self.color) self.image.draw(w) self.label.draw(w) def select(self): # Highlight me, to indicate I'm the card about to be moved. assert self.image != None self.image.setFill("gray") def unselect(self): assert self.image != None self.image.setFill("white") # nooks, homes, and piles support the following methods: # pick Highlight and return the top card; raise InvalidMove if # there isn't one, or if I'm not allowed to move it. # pop Remove the top card; return nothing. # Should be called only when pick was successful. # push Add the given card on top, if allowed; ow raise InvalidMove. # clear Remove all cards. class nook: # One of the upper-left areas, when a single card can # temporarily reside. def __init__(self, a): self.col = a self.card = None def pick(self): pass # YOUR CODE HERE def pop(self): pass # YOUR CODE HERE def push(self, c): pass # YOUR CODE HERE def clear(self): self.card = None class home: # One of the upper-right areas, where individual suits are stacked, # ace-first. def __init__(self, a): self.col = a+4 self.suit = None self.topval = None def pick(self): raise InvalidMove # not allowed to pick home card def pop(self): assert False # not allowed to pick home card def push(self, c): pass # YOUR CODE HERE def full(self): # This method is supported by homes but not nooks or piles. # It's used to check whether the game is over. return self.topval == 12 def clear(self): self.topval = None class pile: # One of the lower-area columns, in which cards start. # Moved cards must be in numeric order, and of alternating colors. def __init__(self, a): self.col = a self.stack = [ ] def insert(self, c): # This method is supported by piles but not nooks or homes. # It's unchecked, and is used for initialization. self.stack.append(c) c.display(self.col, len(self.stack)) def pick(self): pass # YOUR CODE HERE def pop(self): pass # YOUR CODE HERE def push(self, c): pass # YOUR CODE HERE def clear(self): self.stack = [ ] nooks = [nook(i) for i in range(4)] homes = [home(i) for i in range(4)] piles = [pile(i) for i in range(8)] # Wait for a mouse click and return the object on which it occurred: def getClickedObject(): p = w.getMouse() col = min(max(p.getX()-3, 0) / cellWd, 7) if p.getY() < cellHt+3: if col < 4: return nooks[col] else: return homes[col-4] else: return piles[col] # Game-winning message: congrats = Text(Point(cellWd*4+3, cellHt*8), "You won!") congrats.setSize(24) congrats.setStyle("bold") congrats.setTextColor("brown") # (don't display this yet!) # Raise exception WonGame if we've run the game; ow do nothing. def checkWin(): for i in range(4): if not homes[i].full(): return congrats.draw(w) raise WonGame # Create the cards; keep a list of them in variable 'cards'. cards = [ ] for i in range(52): cards.append(card(i)) # Variable 'clean' indicates whether we have to re-deal the cards before # begnining a game. 'False' means we do. clean = False # Wipe the field; empty the nooks, homes, and piles. def clear(): global clean # This tells Python to use the global variable 'clean', # rather than creating a local one for this funciton. for c in cards: c.undisplay() for p in nooks: p.clear() for p in homes: p.clear() for p in piles: p.clear() congrats.undraw() clean = False # Deal the (already-shuffled) cards, unless clean, in which case just # return. def deal(): global clean if clean: return clear() i = 0 # First four piles get 7 cards each: for j in range(4): for k in range(7): piles[j].insert(cards[i]) i += 1 # Last four piles get 6 cards each: for j in range(4, 8): for k in range(6): piles[j].insert(cards[i]) i += 1 clean = True # Shuffle the cards. With no argument, do a random shuffle. # Otherwise, use shuffle number 's' (this will allow the TA to # try your code with a desired arrangement of cards). def shuffle(s = None): clear() random.seed(s) # This primes the random number generator. cards.sort() # This puts the cards back in order. random.shuffle(cards) # This uses the random number generator to shuffle them. deal() # Start with the cards showing, in a random shuffle: shuffle() # This is the main entry point of the program: def play(): global clean deal() clean = False try: # YOUR CODE HERE except (KeyboardInterrupt, WonGame): # This catches ^C (control-C) typed in the console, as well as # the WonGame exception (which your code above may raise). pass # Close the playing field (game window): def close(): w.close()