#!/usr/bin/env python """This is a favorite card game of mine called 'Montana'. """ helpText = """Object: To arrange all of the cards into 4 rows in increasing order from 2 to King, with one suit per row. Starting a game The cards are dealt out into 4 rows of 13 cards each. The aces are then removed, leaving 4 gaps. Playing the game: Move cards into the gaps, which will create new gaps in their old location. The only card that can be moved into any gap is determined by the card to the immediate left of the gap. The moved card must be the same suit, and one rank higher. Example: if there is a gap, and the card to left of it is 4C, only 5C can be moved to the gap. If the gap is located in the leftmost column, any 2 card can be moved there. If the card to the left of the gap is a King, no card can be moved there. When all 4 gaps are located to the right of Kings, no further moves are possible, and the hand ends. If there are any re-deals remaining, the cards are re-dealt, and another hand is played. When all re-deals are used, the game ends. The number of re-deals can be set in the game preferences (default=2 re-deals). Scoring: When a card is placed "in order", it scores a point. "In order" is defined as any cards arranged with a 2 of that suit in the leftmost column of a row, followed by other cards of that suit in sequence. Since Aces are not played, you can score a maximum of 48 points per level. Completing a level starts you over again, adding an additional re-deal to your remaining re-deal status. Re-deals: All ordered cards (i.e., those that in sequence and have scored a point) remain where they are. All unordered cards (i.e., those not in sequence) are picked up, shuffled with the Aces, and dealt into the open spaces. The Aces are then removed, and play resumes. """ import random import dabo from dabo.dLocalize import _ dabo.ui.loadUI("wx") class Card(dabo.ui.dBitmapButton): def afterInit(self): self._suit = None self._rank = None self.scored = False self.bindEvent(dabo.dEvents.Hit, self.onClick) self.bindEvent(dabo.dEvents.MouseLeftDown, self.onMDown) self.bindEvent(dabo.dEvents.MouseLeftUp, self.onMUp) # Turn auto-resize on self.AutoSize = True # These help when re-scaling baseBmp = dabo.ui.dBitmap(self.Parent, Picture="cards/blank") self._baseWd = baseBmp.Width self._baseHt = baseBmp.Height baseBmp.release() # Base the size on the ImageScale self.ImageScale = 1.0 def onClick(self, evt): self.Parent.cardClick(self) def onMDown(self, evt): self.Parent.cardMDown(self) def onMUp(self, evt): self.Parent.cardMUp(self) def updPic(self): """Sets the Picture property for this card to match its Suit and Rank. """ if not self.Rank or not self.Suit: # Card isn't set yet pic = "cards/blank" else: rank = self.Rank suit = self.Suit.lower() if rank == 1: # Ace, hide it pic = "cards/blank" else: pic = "cards/%s%s" % (suit, str(rank)) self.Picture = pic def setDeadPic(self): """Mark the card as dead""" self.Picture = "cards/x" def setLivePic(self): """Mark the card as active""" self.Picture = "cards/blank" def _getBaseHt(self): return self._baseHt def _getBaseWd(self): return self._baseWd def _getDesc(self): rank = self._rank suit = self._suit if rank == 1: ret = "Empty Space" else: if rank == 11: ret = "Jack" elif rank == 12: ret = "Queen" elif rank == 13: ret = "King" else: ret = str(rank) suitNames = {"S" : "Spades", "D" : "Diamonds", "H" : "Hearts", "C" : "Clubs"} ret += " of %s" % suitNames[suit] return ret def _getRank(self): return self._rank def _setRank(self, val): if self._rank != val: self._rank = val self.updPic() def _getSuit(self): return self._suit def _setSuit(self, val): suit = val[0].upper() if self._suit != suit: if suit in ("H", "D", "S", "C"): self._suit = suit self.updPic() BaseHeight = property(_getBaseHt, None, None, _("Normal (100%) height of the card (int)") ) BaseWidth = property(_getBaseWd, None, None, _("Normal (100%) width of the card (int)") ) Description = property(_getDesc, None, None, _("Descriptive name for this card, such as 'King of Clubs' (str)") ) Rank = property(_getRank, _setRank, None, _("Rank for this card (int)") ) Suit = property(_getSuit, _setSuit, None, _("Suit for this card (Spades, Hearts, Diamonds, Clubs)") ) class Board(dabo.ui.dPanel): def afterInit(self): self.BackColor = "gold" self._redeals = 2 self._priorHandScore = 0 self._score = 0 self.isStuck = True # Controls card flashing self.cardTimer = dabo.ui.dTimer(self, Interval=100) self.cardTimer.bindEvent(dabo.dEvents.Hit, self.onCardTimer) self.flashCard = None # Holds a reference to all the aces in the deck. self.aces = [] # Flag that indicates we need to resize the cards self.needResize = False self.gridSizer = None # Border around the cards self._border = 15 self.createSizer() # Create the deck self.createDeck() def initEvents(self): self.bindEvent(dabo.dEvents.Resize, self.onResize) self.bindEvent(dabo.dEvents.Idle, self.onIdle) def onResize(self, evt): """Resize the cards to fit the board.""" self.needResize = True def onIdle(self, evt): if not self.needResize: return if not self.deck: # No deck info yet return self.needResize = False sampleCard = self.deck[0] # Calculate available wd, ht wd = self.Width - (2 * self._border) - (12 * self.gridSizer.hgap) ht = self.Height - (2 * self._border) - (3 * self.gridSizer.vgap) # Calculate wd/ht per card cdWd = wd / 13.0 cdHt = ht / 4.0 # Calculte bitmap wd/ht per card bmpWd = cdWd - sampleCard.BitmapBorder bmpHt = cdHt - sampleCard.BitmapBorder # Card base wd, ht for bmp cdBaseWd = sampleCard.BaseWidth cdBaseHt = sampleCard.BaseHeight # Ratio of available card size to base size wdRatio = bmpWd / cdBaseWd htRatio = bmpHt / cdBaseHt # Use the smaller of the two, to ensure that the cards fit ratio = min(wdRatio, htRatio) self.setAll("ImageScale", ratio) self.layout() def sizeToNormal(self): """Reset the cards to 100% size""" self.setAll("ImageScale", 1.0) self.layout() def createSizer(self): if not self.Sizer: self.Sizer = dabo.ui.dSizer("v") self.Sizer.Border = self._border self.Sizer.BorderAll = True if self.gridSizer: self.Sizer.remove(self.gridSizer) for card in self.deck: self.gridSizer.remove(card) self.gridSizer.release() self.gridSizer = dabo.ui.dGridSizer(maxCols=13, hgap=2, vgap=2) self.Sizer.append1x(self.gridSizer) def newGame(self): # Change this to use preference setting! self._redeals = 2 self._priorHandScore = 0 self._score = 0 # Contains the current layout of the cards. self.cardLayout = () # This holds the history, enabling undo. Most recent positions # are at the end. self.historyStack = [] # This holds the redo stack self.redoStack = [] random.shuffle(self.deck) self.createSizer() self.gridSizer.appendItems(self.deck) self.updateCardLayout() self.updateStatus() def createDeck(self): """Creates a dict representing a 52-card deck.""" self.deck = [] for suit in "SHDC": for rank in range(1, 14): card = Card(self, Suit=suit, Rank=rank) self.deck.append(card) if rank == 1: self.aces.append(card) def redeal(self): """ Gather all the non-scored cards, shuffle 'em, and place them back into the layout. """ sz = self.gridSizer redo = [] for card in self.deck: if not card.scored: sz.remove(card) redo.append(card) random.shuffle(redo) sz.appendItems(redo) self._redeals -= 1 self.historyStack = [] self.updateStatus() def cardClick(self, card): """Called from a card when it is clicked.""" # First, stop the flashing. MouseLeftUp doesn't seem to fire self.cardTimer.stop() if self.flashCard is not None: self.flashCard.Visible = True self.flashCard = None rank, suit = card.Rank, card.Suit if rank == 1: # Ace; nothing to do return if rank == 2: # See if there are any aces in the first column. for row in range(4): firstColCard = self.gridSizer.getItemByRowCol(row, 0) if firstColCard is not None: if firstColCard.Rank == 1: self.switchCards(card, firstColCard) self.updateStatus() break else: # See if the card below it in sequence has an ace to its right prevCard = self.getCard(rank-1, suit) rCard = self.gridSizer.getNeighbor(prevCard, "right") if rCard is not None: if rCard.Rank == 1: # We can move it self.switchCards(card, rCard) self.updateStatus() def cardMDown(self, card): rank, suit = card.Rank, card.Suit if rank == 1: # Ace; get the card before it leftCard = self.gridSizer.getNeighbor(card, "left") if leftCard is None: # We're at the left column, so there's nothing to flash return lRank, lSuit = leftCard.Rank, leftCard.Suit if lRank == 1: # Another ace; do nothing return elif lRank == 13: # King; card is dead, so do nothing return else: # Flash the card next in sequence target = self.getCard(lRank+1, lSuit) self.flashCard = target self.cardTimer.start() def cardMUp(self, card): self.cardClick(card) def updateStatus(self): """Several things need to be determined: - update the score and the scored property of the cards - mark and count any dead aces - if all aces are dead, enable re-deal button """ # Calculate the score self._score = 0 for row in range(4): start = row*13 cards = self.cardLayout[start:start+12] if cards[0].Rank == 2: # There is scoring in this row suit = cards[0].Suit seq = 1 for card in cards: if card.Suit == suit: if card.Rank == seq+1: card.scored = True self._score += 1 seq += 1 else: break else: break # update the form self.Form.setScore(self.TotalScore) # Mark the dead aces. These are all the aces to the right # of Kings. sz = self.gridSizer for ace in self.aces: ace.setLivePic() dead = 0 self.isStuck = False for suit in "SHDC": king = self.getCard(13, suit) rtCard = sz.getNeighbor(king, "right") while (rtCard is not None) and (rtCard.Rank == 1): rtCard.setDeadPic() dead += 1 rtCard = sz.getNeighbor(rtCard, "right") if dead == 4: # No more moves possible! self.isStuck = True self.stuck() def stuck(self): """Called when no more moves are possible. If there are re-deals left, enable the re-deal button. Otherwise, flash 'em the Game Over message. """ # see if we've completed the deck outOfPlace = [cd for cd in self.deck if cd.Rank != 1 and not cd.scored] if not outOfPlace: self._priorHandScore += self._score msg = "Congratulations! You completed the board!\n\n" + \ "You have earned an extra re-deal!" dabo.ui.exclaim(message=msg, title="We have a winner!") # We have to add 2 here, since the redeal count will be decreased # by one when we call redeal(). self._redeals += 2 # Mark all the cards as not 'scorded' so that they are all shuffled. self.setAll("scored", False) self.redeal() return if self._redeals: self.Form.showRedeal() else: msg = "Game over! Your final score was %s" % self.TotalScore dabo.ui.info(message=msg, title="Game Over!") def switchCards(self, c1, c2): """Change the position of the two cards.""" sz = self.gridSizer c1.lockDisplay() c2.lockDisplay() row1, col1 = sz.getGridPos(c1) row2, col2 = sz.getGridPos(c2) # Move the first out of the way tempRow, tempCol = sz.findFirstEmptyCell() sz.moveObject(c1, tempRow, tempCol, delay=True) # Now move the second to the first position sz.moveObject(c2, row1, col1, delay=True) # Now move the first to the second position sz.moveObject(c1, row2, col2) # Since this is a move forward, clear the redo stack self.redoStack = [] # Update the card layout and history self.updateCardLayout() dabo.ui.callAfter(c1.unlockDisplay) dabo.ui.callAfter(c2.unlockDisplay) def updateCardLayout(self, addToHistory=True): if addToHistory: if self.cardLayout: # Push the old layout onto the history stack self.historyStack.append(self.cardLayout) # Set the new card layout layout = [] sz = self.gridSizer for row in range(4): for col in range(13): layout.append(sz.getItemByRowCol(row, col)) self.cardLayout = tuple(layout) def undo(self): self.undoRedo(self.historyStack, self.redoStack) def redo(self): self.undoRedo(self.redoStack, self.historyStack) def undoRedo(self, fromStack, toStack): if fromStack: turn = fromStack.pop() toStack.append(self.cardLayout) self.createSizer() self.gridSizer.appendItems(turn) self.layout() self.updateCardLayout(False) self.updateStatus() def getCard(self, rank, suit): """Returns a reference to the card that has the specified rank and suit. """ try: ret = [cd for cd in self.deck if (cd.Rank == rank) and (cd.Suit == suit)][0] except: ret = None return ret def onCardTimer(self, evt): fc = self.flashCard if fc is None: self.cardTimer.stop() else: vis = fc.Visible fc.Visible = not vis fc.refresh() self.cardTimer.start() def _getReDeals(self): return self._redeals def _setReDeals(self, val): self._redeals = val def _getScore(self): return self._score def _setScore(self, val): self._score = val self.Parent.updateScore(self._score) def _getTotScore(self): return self._priorHandScore + self._score ReDeals = property(_getReDeals, _setReDeals, None, _("Number of remaining re-deals (int)") ) Score = property(_getScore, _setScore, None, _("Score of the game for the current hand (int)") ) TotalScore = property(_getTotScore, None, None, _("Total score of the game, including prior hands (int)") ) class MontanaForm(dabo.ui.dForm): def afterInit(self): #self.Centered = True self.Caption = "Montana" # Add the board, score display and re-deal button self.Sizer.Border = 5 self.Sizer.BorderAll = True self.gameBoard = Board(self) self.Sizer.append1x(self.gameBoard) self.layout() self.fillMenu() dabo.ui.callAfter(self.startGame) #dabo.ui.callAfter(self.fitToSizer) def fillMenu(self): mb = self.MenuBar fileMenu = mb.getMenu(_("File")) fileMenu.prependSeparator() fileMenu.prepend(_("&New Game\tCtrl+N"), help=_("Start a new game"), bindfunc=self.onNewGame, bmp="new") viewMenu = mb.getMenu(_("View")) viewMenu.appendSeparator() viewMenu.append(_("&Resize to 100%"), help=_("Reset the board to normal size"), bindfunc=self.onDisplayNormal) helpMenu = mb.getMenu(_("Help")) helpMenu.append(_("&How to Play\tCtrl+I"), help=_("Rules of the game"), bindfunc=self.onRules, bmp="info") tb = self.ToolBar = dabo.ui.dToolBar(self) tb.appendButton("New", pic="new", toggle=False, bindfunc=self.onNewGame, tip="New Game", help="Start a new game") tb.appendButton("Preferences", pic="configure", toggle=False, bindfunc=self.onEditPreferences, tip="Preferences", help="Edit preferences") tb.appendSeparator() btn = self.btnRedeal = tb.appendControl(dabo.ui.dButton(tb, Enabled=False)) btn.Caption = "Redeals left: %s" % self.gameBoard.ReDeals btn.Width = dabo.ui.fontMetric(btn.Caption, wind=btn)[0] + 24 btn.Height += 6 self.btnRedeal.bindEvent(dabo.dEvents.Hit, self.onRedeal) tb.appendSeparator() lbl = dabo.ui.dLabel(tb, Caption="Score: ", FontSize=12) tb.appendControl(lbl) self.lblScore = tb.appendControl(dabo.ui.dLabel(tb, Caption="0", FontBold=True, FontSize=12, ForeColor="blue")) self.lblScore.Width += 20 tb.Realize() def onEditUndo(self, evt): self.gameBoard.undo() def onEditRedo(self, evt): self.gameBoard.redo() def onEditPreferences(self, evt): print "PREF EDIT" def onRules(self, evt): win = dabo.ui.dForm(self, Caption="Montana Rules", Centered=True) pnl = dabo.ui.dScrollPanel(win) win.Sizer.append1x(pnl) txt = dabo.ui.dLabel(pnl, Caption=helpText) sz = dabo.ui.dSizer("v") sz.append1x(txt, border=10) pnl.Sizer = sz btn = dabo.ui.dButton(win, Caption="OK") btn.bindEvent(dabo.dEvents.Hit, win.close) win.Sizer.append(btn, border=10, halign="right") win.layout() pnl.fitToSizer() win.Visible = True def onNewGame(self, evt): # Check for a game in progress. if not self.gameBoard.isStuck or self.gameBoard.ReDeals: if not dabo.ui.areYouSure(message="Your game is not over. Are " + "you sure you want to end it and start a new game?"): return self.startGame() def onDisplayNormal(self, evt): self.gameBoard.sizeToNormal() dabo.ui.callAfter(self.fitToSizer) def startGame(self): self.gameBoard.newGame() self.btnRedeal.Caption = "Redeals left: %s" % self.gameBoard.ReDeals def showRedeal(self): self.btnRedeal.Enabled = True def onRedeal(self, evt): self.gameBoard.redeal() self.btnRedeal.Enabled = False self.btnRedeal.Caption = "Redeals left: %s" % self.gameBoard.ReDeals def setScore(self, score): self.lblScore.Caption = str(score) self.layout() if __name__ == "__main__": app = dabo.dApp() app.MainFormClass = MontanaForm app.start()