#!/usr/bin/env python # -*- mode: python; coding: utf-8; -*- # --------------------------------------------------------------------------- # # Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer # Copyright (C) 2003 Mt. Hood Playing Card Co. # Copyright (C) 2005-2009 Skomoroh # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # --------------------------------------------------------------------------- from pysollib.mfxutil import Image, ImageTk, USE_PIL from pysollib.mfxutil import Struct, SubclassResponsibility, kwdefault from pysollib.mygettext import _ from pysollib.pysoltk import ANCHOR_NW, ANCHOR_SE from pysollib.pysoltk import CURSOR_DOWN_ARROW, CURSOR_DRAG from pysollib.pysoltk import EVENT_HANDLED, EVENT_PROPAGATE from pysollib.pysoltk import MfxCanvasGroup, MfxCanvasImage from pysollib.pysoltk import MfxCanvasRectangle, MfxCanvasText from pysollib.pysoltk import after_cancel, after_idle from pysollib.pysoltk import bind, unbind_destroy from pysollib.pysoltk import get_text_width from pysollib.pysoltk import markImage from pysollib.settings import DEBUG from pysollib.settings import TOOLKIT from pysollib.util import ACE, KING from pysollib.util import ANY_RANK, ANY_SUIT, NO_RANK # ************************************************************************ # * Let's start with some test methods for cards. # * Empty card-lists return false. # ************************************************************************ # check that all cards are face-up def cardsFaceUp(cards): if not cards: return False for c in cards: if not c.face_up: return False return True # check that all cards are face-down def cardsFaceDown(cards): if not cards: return False for c in cards: if c.face_up: return False return True # check that cards are face-up and build down by rank def isRankSequence(cards, mod=8192, dir=-1): if not cardsFaceUp(cards): return False c1 = cards[0] for c2 in cards[1:]: if (c1.rank + dir) % mod != c2.rank: return False c1 = c2 return True # check that cards are face-up and build down by alternate color def isAlternateColorSequence(cards, mod=8192, dir=-1): if not cardsFaceUp(cards): return False c1 = cards[0] for c2 in cards[1:]: if (c1.rank + dir) % mod != c2.rank or c1.color == c2.color: return False c1 = c2 return True # check that cards are face-up and build down by same color def isSameColorSequence(cards, mod=8192, dir=-1): if not cardsFaceUp(cards): return False c1 = cards[0] for c2 in cards[1:]: if (c1.rank + dir) % mod != c2.rank or c1.color != c2.color: return False c1 = c2 return True # check that cards are face-up and build down by same suit def isSameSuitSequence(cards, mod=8192, dir=-1): if not cardsFaceUp(cards): return False c1 = cards[0] for c2 in cards[1:]: if (c1.rank + dir) % mod != c2.rank or c1.suit != c2.suit: return False c1 = c2 return True # check that cards are face-up and build down by any suit but own def isAnySuitButOwnSequence(cards, mod=8192, dir=-1): if not cardsFaceUp(cards): return False c1 = cards[0] for c2 in cards[1:]: if (c1.rank + dir) % mod != c2.rank or c1.suit == c2.suit: return False c1 = c2 return True def getNumberOfFreeStacks(stacks): return len([s for s in stacks if not s.cards]) # collect the top cards of several stacks into a pile def getPileFromStacks(stacks, reverse=False): cards = [] for s in stacks: if not s.cards or not s.cards[-1].face_up: return None cards.append(s.cards[-1]) return (reversed(cards) if reverse else cards) class Stack: # A generic stack of cards. # # This is used as a base class for all other stacks (e.g. the talon, # the foundations and the row stacks). # # 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. # constants MIN_VISIBLE_XOFFSET = 3 MIN_VISIBLE_YOFFSET = 3 SHRINK_FACTOR = 2. def __init__(self, x, y, game, cap={}): # 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). # # link back to game # id = len(game.allstacks) game.allstacks.append(self) x = int(round(x)) y = int(round(y)) mapkey = (x, y) # assert not game.stackmap.has_key(mapkey) ## can happen in PyJonngg game.stackmap[mapkey] = id # # setup our pseudo MVC scheme # model, view = self, self # # model # model.id = id model.game = game model.cards = [] # model.is_filled = False # capabilites - the game logic model.cap = Struct( suit=-1, # required suit for this stack (-1 is ANY_SUIT) color=-1, # required color for this stack (-1 is ANY_COLOR) rank=-1, # required rank for this stack (-1 is ANY_RANK) base_suit=-1, # base suit for this stack (-1 is ANY_SUIT) base_color=-1, # base color for this stack (-1 is ANY_COLOR) base_rank=-1, # base rank for this stack (-1 is ANY_RANK) dir=0, # direction - stack builds up/down mod=8192, # modulo for wrap around (typically 13 or 8192) max_move=0, # can move at most # cards at a time max_accept=0, # can accept at most # cards at a time max_cards=999999, # total number of cards may not exceed this # not commonly used: min_move=1, # must move at least # cards at a time min_accept=1, # must accept at least # cards at a time # total number of cards this stack at least requires min_cards=0, ) model.cap.update(cap) assert isinstance(model.cap.suit, int) assert isinstance(model.cap.color, int) assert isinstance(model.cap.rank, int) assert isinstance(model.cap.base_suit, int) assert isinstance(model.cap.base_color, int) assert isinstance(model.cap.base_rank, int) # # view # self.init_coord = (x, y) view.x = x view.y = y view.canvas = game.canvas view.CARD_XOFFSET = 0 view.CARD_YOFFSET = 0 view.INIT_CARD_OFFSETS = (0, 0) view.INIT_CARD_YOFFSET = 0 # for reallocateCards view.group = MfxCanvasGroup(view.canvas) if (TOOLKIT == 'kivy'): if hasattr(view.group, 'stack'): view.group.stack = self view.shrink_face_down = 1 # image items view.images = Struct( bottom=None, # canvas item redeal=None, # canvas item redeal_img=None, # the corresponding PhotoImage shade_img=None, ) # other canvas items view.items = Struct( bottom=None, # dummy canvas item shade_item=None, ) # text items view.texts = Struct( ncards=None, # canvas item # by default only used by Talon: rounds=None, # canvas item redeal=None, # canvas item redeal_str=None, # the corresponding string # for use by derived stacks: misc=None, # canvas item ) view.top_bottom = None # the highest of all bottom items cardw, cardh = game.app.images.CARDW, game.app.images.CARDH dx, dy = cardw+view.canvas.xmargin, cardh+view.canvas.ymargin view.is_visible = view.x >= -dx and view.y >= -dy view.is_open = -1 view.can_hide_cards = -1 view.max_shadow_cards = -1 view.current_cursor = '' view.cursor_changed = False def destruct(self): # help breaking circular references unbind_destroy(self.group) def prepareStack(self): self.prepareView() if self.is_visible: self.initBindings() def _calcMouseBind(self, binding_format): return self.game.app.opt.calcCustomMouseButtonsBinding(binding_format) # bindings {view widgets bind to controller} def initBindings(self): group = self.group bind(group, self._calcMouseBind("<{mouse_button1}>"), self.__clickEventHandler) # bind(group, "", self.__motionEventHandler) bind(group, "", self.__motionEventHandler) bind(group, self._calcMouseBind(""), self.__releaseEventHandler) bind(group, self._calcMouseBind(""), self.__controlclickEventHandler) bind(group, self._calcMouseBind(""), self.__shiftclickEventHandler) bind(group, self._calcMouseBind(""), self.__doubleclickEventHandler) bind(group, self._calcMouseBind("<{mouse_button3}>"), self.__rightclickEventHandler) bind(group, self._calcMouseBind("<{mouse_button2}>"), self.__middleclickEventHandler) bind(group, self._calcMouseBind(""), self.__middleclickEventHandler) # bind(group, self._calcMouseBind( # ""), self.__controlmiddleclickEventHandler) # bind(group, self._calcMouseBind(""), # self.__shiftrightclickEventHandler) # bind(group, self._calcMouseBind(""), "") bind(group, "", self.__enterEventHandler) bind(group, "", self.__leaveEventHandler) def prepareView(self): # assertView(self) if (self.CARD_XOFFSET == 0 and self.CARD_YOFFSET == 0): assert self.cap.max_move <= 1 # prepare some variables ox, oy = self.CARD_XOFFSET, self.CARD_YOFFSET if isinstance(ox, (int, float)): self.CARD_XOFFSET = (ox,) else: self.CARD_XOFFSET = tuple([int(round(x)) for x in ox]) if isinstance(oy, (int, float)): self.CARD_YOFFSET = (oy,) else: self.CARD_YOFFSET = tuple([int(round(y)) for y in oy]) # preserve offsets # for resize() self.INIT_CARD_OFFSETS = (self.CARD_XOFFSET, self.CARD_YOFFSET) self.INIT_CARD_YOFFSET = self.CARD_YOFFSET # for reallocateCards if self.can_hide_cards < 0: self.can_hide_cards = self.is_visible if self.cap.max_cards < 3: self.can_hide_cards = 0 elif [_f for _f in self.CARD_XOFFSET if _f]: self.can_hide_cards = 0 elif [_f for _f in self.CARD_YOFFSET if _f]: self.can_hide_cards = 0 elif self.canvas.preview: self.can_hide_cards = 0 if self.is_open < 0: self.is_open = False if (self.is_visible and (abs(self.CARD_XOFFSET[0]) >= self.MIN_VISIBLE_XOFFSET or abs(self.CARD_YOFFSET[0]) >= self.MIN_VISIBLE_YOFFSET)): self.is_open = True if self.max_shadow_cards < 0: self.max_shadow_cards = 999999 # if abs(self.CARD_YOFFSET[0]) # != self.game.app.images.CARD_YOFFSET: # # don't display a shadow if the YOFFSET of the stack # # and the images don't match # self.max_shadow_cards = 1 if (self.game.app.opt.shrink_face_down and isinstance(ox, (int, float)) and isinstance(oy, (int, float))): # no shrink if xoffset/yoffset too small f = self.SHRINK_FACTOR if ((ox == 0 and oy >= self.game.app.images.CARD_YOFFSET//f) or (oy == 0 and ox >= self.game.app.images.CARD_XOFFSET//f)): self.shrink_face_down = f # bottom image if self.is_visible: self.prepareBottom() # stack bottom image def prepareBottom(self): assert self.is_visible and self.images.bottom is None img = self.getBottomImage() if img is not None: self.images.bottom = MfxCanvasImage(self.canvas, self.x, self.y, image=img, anchor=ANCHOR_NW, group=self.group) self.top_bottom = self.images.bottom # invisible stack bottom # We need this if we want to get any events for an empty stack (which # is needed by the quickPlayHandler in some games like Montana) def prepareInvisibleBottom(self): assert self.is_visible and self.items.bottom is None images = self.game.app.images self.items.bottom = MfxCanvasRectangle(self.canvas, self.x, self.y, self.x + images.CARDW, self.y + images.CARDH, fill="", outline="", width=0, group=self.group) self.top_bottom = self.items.bottom # sanity checks def assertStack(self): assert self.cap.min_move > 0 assert self.cap.min_accept > 0 assert not hasattr(self, "suit") # # Core access methods {model -> view} # # Add a card add the top of a stack. Also update display. {model -> view} def addCard(self, card, unhide=1, update=1): model, view = self, self model.cards.append(card) card.tkraise(unhide=unhide) if view.can_hide_cards and len(model.cards) >= 3: # we only need to display the 2 top cards model.cards[-3].hide(self) card.item.addtag(view.group) view._position(card) if update: view.updateText() self.closeStack() return card def insertCard(self, card, position, unhide=1, update=1): model, view = self, self model.cards.insert(position, card) for c in model.cards[position:]: c.tkraise(unhide=unhide) if (view.can_hide_cards and len(model.cards) >= 3 and len(model.cards)-position <= 2): # we only need to display the 2 top cards model.cards[-3].hide(self) card.item.addtag(view.group) for c in model.cards[position:]: view._position(c) if update: view.updateText() self.closeStack() return card # Remove a card from the stack. Also update display. {model -> view} def removeCard(self, card=None, unhide=1, update=1, update_positions=0): model, view = self, self assert len(model.cards) > 0 if card is None: card = model.cards[-1] # optimized a little bit (compare with the else below) card.item.dtag(view.group) if unhide and self.can_hide_cards: card.unhide() if len(self.cards) >= 3: model.cards[-3].unhide() del model.cards[-1] else: card.item.dtag(view.group) if unhide and view.can_hide_cards: # Note: the 2 top cards ([-1] and [-2]) are already unhidden. card.unhide() if len(model.cards) >= 3: if card is model.cards[-1] or model is self.cards[-2]: # Make sure that 2 top cards will be un-hidden. model.cards[-3].unhide() card_index = model.cards.index(card) model.cards.remove(card) if update_positions: for c in model.cards[card_index:]: view._position(c) if update: view.updateText() self.unshadeStack() self.is_filled = False return card # Get the top card {model} def getCard(self): if self.cards: return self.cards[-1] return None # get the largest moveable pile {model} - uses canMoveCards() def getPile(self): if self.cap.max_move > 0: cards = self.cards[-self.cap.max_move:] while len(cards) >= self.cap.min_move: if self.canMoveCards(cards): return cards del cards[0] return None # Position the card on the canvas {view} def _position(self, card): x, y = self.getPositionFor(card) card.moveTo(x, y) # find card def _findCard(self, event): model, view = self, self if event is not None and model.cards: # ask the canvas return view.canvas.findCard(self, event) return -1 # find card def _findCardXY(self, x, y, cards=None): model = self if cards is None: cards = model.cards images = self.game.app.images cw, ch = images.getSize() index = -1 for i in range(len(cards)): c = cards[i] r = (c.x, c.y, c.x + cw, c.y + ch) if r[0] <= x < r[2] and r[1] <= y < r[3]: index = i return index # generic model update (can be used for undo/redo - see move.py) def updateModel(self, undo, flags): pass # copy model data - see Hint.AClonedStack def copyModel(self, clone): clone.id = self.id clone.game = self.game clone.cap = self.cap def getRankDir(self, cards=None): if cards is None: cards = self.cards[-2:] if len(cards) < 2: return 0 dir = (cards[-1].rank - cards[-2].rank) % self.cap.mod if dir > self.cap.mod // 2: return dir - self.cap.mod return dir # # Basic capabilities {model} # Used by various subclasses. # def basicIsBlocked(self): # Check if the stack is blocked (e.g. Pyramid or Mahjongg) return False def basicAcceptsCards(self, from_stack, cards): # Check that the limits are ok and that the cards are face up if from_stack is self or self.basicIsBlocked(): return False cap = self.cap mylen = len(cards) if mylen < cap.min_accept or mylen > cap.max_accept: return False mylen += len(self.cards) # note: we don't check cap.min_cards here if mylen > cap.max_cards: return False def _check(c, suit, color, rank): return ((suit >= 0 and c.suit != suit) or (color >= 0 and c.color != color) or (rank >= 0 and c.rank != rank)) for c in cards: if not c.face_up or _check(c, cap.suit, cap.color, cap.rank): return False if self.cards: # top card of our stack must be face up return self.cards[-1].face_up # check required base return not _check(cards[0], cap.base_suit, cap.base_color, cap.base_rank) def basicCanMoveCards(self, cards): # Check that the limits are ok and the cards are face up if self.basicIsBlocked(): return False cap = self.cap mylen = len(cards) if mylen < cap.min_move or mylen > cap.max_move: return False mylen = len(self.cards) - mylen # note: we don't check cap.max_cards here if mylen < cap.min_cards: return False return cardsFaceUp(cards) # # Capabilities - important for game logic {model} # def acceptsCards(self, from_stack, cards): # Do we accept receiving `cards' from `from_stack' ? return False def canMoveCards(self, cards): # Can we move these cards when assuming they are our top-cards ? return False def canFlipCard(self): # Can we flip our top card ? return False def canDropCards(self, stacks): # Can we drop the top cards onto one of the foundation stacks ? return (None, 0) # return the stack and the number of cards # # State {model} # def resetGame(self): # Called when starting a new game. self.CARD_YOFFSET = self.INIT_CARD_YOFFSET self.items.shade_item = None self.images.shade_img = None # self.items.bottom = None # self.images.bottom = None def __repr__(self): # Return a string for debug print statements. return "%s(%d)" % (self.__class__.__name__, self.id) # # Atomic move actions {model -> view} # def flipMove(self, animation=False): # Flip the top card. if animation: self.game.singleFlipMove(self) else: self.game.flipMove(self) def moveMove(self, ncards, to_stack, frames=-1, shadow=-1): # Move the top n cards. self.game.moveMove( ncards, self, to_stack, frames=frames, shadow=shadow) self.fillStack() def fillStack(self): self.game.fillStack(self) def closeStack(self): pass # # Playing move actions. Better not override. # def playFlipMove(self, sound=True, animation=False): if sound: self.game.playSample("flip", 5) self.flipMove(animation=animation) if not self.game.checkForWin(): self.game.autoPlay() self.game.finishMove() def playMoveMove(self, ncards, to_stack, frames=-1, shadow=-1, sound=True): if sound: if to_stack in self.game.s.foundations: self.game.playSample("drop", priority=30) else: self.game.playSample("move", priority=10) self.moveMove(ncards, to_stack, frames=frames, shadow=shadow) if not self.game.checkForWin(): # let the player put cards back from the foundations if self not in self.game.s.foundations: self.game.autoPlay() self.game.finishMove() # # Appearance {view} # def _getBlankBottomImage(self): return self.game.app.images.getBlankBottom() def _getReserveBottomImage(self): return self.game.app.images.getReserveBottom() def _getSuitBottomImage(self): return self.game.app.images.getSuitBottom(self.cap.base_suit) def _getNoneBottomImage(self): return None def _getTalonBottomImage(self): return self.game.app.images.getTalonBottom() def _getBraidBottomImage(self): return self.game.app.images.getBraidBottom() def _getLetterImage(self): return self.game.app.images.getLetter(self.cap.base_rank) getBottomImage = _getBlankBottomImage def getPositionFor(self, card): model, view = self, self x, y = view.x, view.y if view.can_hide_cards: return x, y ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) d = self.shrink_face_down for c in model.cards: if c is card: break if c.face_up: x += self.CARD_XOFFSET[ix] y += self.CARD_YOFFSET[iy] else: x += self.CARD_XOFFSET[ix]//d y += self.CARD_YOFFSET[iy]//d ix = (ix + 1) % lx iy = (iy + 1) % ly return int(x), int(y) def getPositionForNextCard(self): model, view = self, self x, y = view.x, view.y if view.can_hide_cards: return x, y if not self.cards: return x, y ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) d = self.shrink_face_down for c in model.cards: if c.face_up: x += self.CARD_XOFFSET[ix] y += self.CARD_YOFFSET[iy] else: x += self.CARD_XOFFSET[ix]//d y += self.CARD_YOFFSET[iy]//d ix = (ix + 1) % lx iy = (iy + 1) % ly return int(x), int(y) def getOffsetFor(self, card): model, view = self, self if view.can_hide_cards: return 0, 0 lx, ly = len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) i = list(model.cards).index(card) return view.CARD_XOFFSET[i % lx], view.CARD_YOFFSET[i % ly] # Fully update the view of a stack - updates # hiding, card positions and stacking order. # Avoid calling this as it is rather slow. def refreshView(self): model, view = self, self cards = model.cards if not view.is_visible or len(cards) < 2: return if view.can_hide_cards: # hide all lower cards for c in cards[:-2]: # print "refresh hide", c, c.hide_stack c.hide(self) # unhide the 2 top cards for c in cards[-2:]: # print "refresh unhide 1", c, c.hide_stack c.unhide() # print "refresh unhide 1", c, c.hide_stack # update the card postions and stacking order item = cards[0].item x, y = view.x, view.y ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) for c in cards[1:]: c.item.tkraise(item) item = c.item if not view.can_hide_cards: d = self.shrink_face_down if c.face_up: x += self.CARD_XOFFSET[ix] y += self.CARD_YOFFSET[iy] else: x += int(self.CARD_XOFFSET[ix]/d) y += int(self.CARD_YOFFSET[iy]/d) ix = (ix + 1) % lx iy = (iy + 1) % ly c.moveTo(x, y) def updateText(self): if self.game.preview > 1 or self.texts.ncards is None: return t = "" format = "%d" if self.texts.ncards.text_format is not None: format = self.texts.ncards.text_format if format == "%D": format = "" if self.cards: format = "%d" if format: t = format % len(self.cards) # if 0: # visible = 0 # for c in self.cards: # if c.isHidden(): # assert c.hide_stack is not None # else: # visible = visible + 1 # assert c.hide_stack is None # t = t + " (%d)" % visible self.texts.ncards.config(text=t) def updatePositions(self): # compact the stack when a cards goes off screen if self.reallocateCards(): for c in self.cards: self._position(c) def reallocateCards(self): # change CARD_YOFFSET if a cards is off-screen # returned False if CARD_YOFFSET is not changed, otherwise True if not self.game.app.opt.compact_stacks: return False if TOOLKIT != 'tk': return False if self.CARD_XOFFSET != (0,): return False if len(self.CARD_YOFFSET) != 1: return False if self.CARD_YOFFSET[0] <= 0: return False if len(self.cards) <= 1: return False if not self.canvas.winfo_ismapped(): return False yoffset = self.CARD_YOFFSET[0] # 1/2 of a card is visible cardh = self.game.app.images.getSize()[0] // 2 num_face_up = len([c for c in self.cards if c.face_up]) num_face_down = len(self.cards) - num_face_up stack_height = int(self.y + num_face_down * yoffset // self.shrink_face_down + num_face_up * yoffset + cardh) visible_height = self.canvas.winfo_height() if USE_PIL and self.game.app.opt.auto_scale: # use visible_height only game_height = 0 else: game_height = self.game.height + 2*self.canvas.ymargin height = max(visible_height, game_height) # print 'reallocateCards:', stack_height, height, \ # visible_height, game_height if stack_height > height: # compact stack n = num_face_down // self.shrink_face_down + num_face_up dy = float(height - self.y - cardh) / n if dy < yoffset: # print 'compact:', dy self.CARD_YOFFSET = (dy,) return True elif stack_height < height: # expande stack if self.CARD_YOFFSET == self.INIT_CARD_YOFFSET: return False n = num_face_down // self.shrink_face_down + num_face_up dy = float(height - self.y - cardh) / n dy = min(dy, self.INIT_CARD_YOFFSET[0]) # print 'expande:', dy self.CARD_YOFFSET = (dy,) return True return False def resize(self, xf, yf, widthpad=0, heightpad=0): # resize and move stack # xf, yf - a multiplicative factor (from the original values) # print 'Stack.resize:', self, self.is_visible, xf, yf x0, y0 = self.init_coord if (x0 > 0): x0 += widthpad if (y0 > 0): y0 += heightpad x, y = int(round(x0*xf)), int(round(y0*yf)) self.x, self.y = x, y # offsets xoffset = tuple(int(round(i*xf)) for i in self.INIT_CARD_OFFSETS[0]) yoffset = tuple(int(round(i*yf)) for i in self.INIT_CARD_OFFSETS[1]) self.CARD_XOFFSET = xoffset self.CARD_YOFFSET = yoffset self.INIT_CARD_YOFFSET = yoffset # print '* resize offset:', self.INIT_CARD_XOFFSET, # move cards for c in self.cards: cx, cy = self.getPositionFor(c) c.moveTo(cx, cy) # --- if not self.is_visible: return # bottom and shade if self.images.bottom: img = self.getBottomImage() self.images.bottom['image'] = img self.images.bottom.moveTo(x, y) if self.items.bottom: c = self.items.bottom.coords() c = ((int(round(c[0]*xf)), int(round(c[1]*yf))), (int(round(c[2]*xf)), int(round(c[3]*yf)))) self.items.bottom.coords(c) if self.items.shade_item: c = self.cards[-1] img = self.game.app.images.getHighlightedCard( c.deck, c.suit, c.rank) if img: self.items.shade_item['image'] = img self.items.shade_item.moveTo(x, y) # move the items def move(item): ix, iy = item.init_coord x = int(round((ix + widthpad) * xf)) y = int(round((iy + heightpad) * yf)) item.moveTo(x, y) # images if self.images.redeal: move(self.images.redeal) # texts if self.texts.ncards: move(self.texts.ncards) if self.texts.rounds: move(self.texts.rounds) if self.texts.redeal: move(self.texts.redeal) if self.texts.misc: move(self.texts.misc) def basicShallHighlightSameRank(self, card): # by default all open stacks are available for highlighting assert card in self.cards if not self.is_visible or not card.face_up: return False if card is self.cards[-1]: return True if not self.is_open: return False # dx, dy = self.getOffsetFor(card) # if ((dx == 0 and dy <= self.MIN_VISIBLE_XOFFSET) or # (dx <= self.MIN_VISIBLE_YOFFSET and dy == 0)): # return False return True def basicShallHighlightMatch(self, card): # by default all open stacks are available for highlighting return self.basicShallHighlightSameRank(card) def highlightSameRank(self, event): i = self._findCard(event) if i < 0: return 0 card = self.cards[i] if not self.basicShallHighlightSameRank(card): return 0 col_1 = self.game.app.opt.colors['samerank_1'] col_2 = self.game.app.opt.colors['samerank_2'] info = [(self, card, card, col_1)] for s in self.game.allstacks: for c in s.cards: if c is card: continue # check the rank if c.rank != card.rank: continue # ask the target stack if s.basicShallHighlightSameRank(c): info.append((s, c, c, col_2)) self.game.stats.highlight_samerank += 1 return self.game._highlightCards( info, self.game.app.opt.timeouts['highlight_samerank']) def highlightMatchingCards(self, event): i = self._findCard(event) if i < 0: return 0 card = self.cards[i] if not self.basicShallHighlightMatch(card): return 0 col_1 = self.game.app.opt.colors['cards_1'] col_2 = self.game.app.opt.colors['cards_2'] c1 = c2 = card info = [] found = 0 for s in self.game.allstacks: # continue if both stacks are foundations if (self in self.game.s.foundations and s in self.game.s.foundations): continue # for all cards for c in s.cards: if c is card: continue # ask the target stack if not s.basicShallHighlightMatch(c): continue # ask the game if self.game.shallHighlightMatch(self, card, s, c): found = 1 if s is self: # enlarge rectangle for neighbours j = self.cards.index(c) if i - 1 == j: c1 = c continue if i + 1 == j: c2 = c continue info.append((s, c, c, col_1)) if found: if info: self.game.stats.highlight_cards += 1 info.append((self, c1, c2, col_2)) return self.game._highlightCards( info, self.game.app.opt.timeouts['highlight_cards']) if not self.basicIsBlocked(): self.game.highlightNotMatching() return 0 # # Subclass overridable handlers {contoller -> model -> view} # def clickHandler(self, event): return 0 def middleclickHandler(self, event): # default action: show the card if it is overlapped by other cards if not self.is_open: return 0 i = self._findCard(event) positions = len(self.cards) - i - 1 peeked = False if i < 0: return 0 if not self.cards[i].face_up: if not self.game.app.opt.peek_facedown: return 0 else: self.game.stats.peeks += 1 self.cards[i].showFace() peeked = True elif positions <= 0: return 0 # print self.cards[i] self.cards[i].item.tkraise() self.canvas.update_idletasks() self.game.sleep(self.game.app.opt.timeouts['raise_card']) if peeked: self.cards[i].showBack() if positions > 0: if TOOLKIT == 'tk': self.cards[i].item.lower(self.cards[i+1].item) elif TOOLKIT == 'gtk': for c in self.cards[i+1:]: c.tkraise() self.canvas.update_idletasks() return 1 def controlmiddleclickHandler(self, event): # cheating: show face-down card if not self.is_open: return 0 i = self._findCard(event) positions = len(self.cards) - i - 1 if i < 0 or positions < 0: return 0 # print self.cards[i] face_up = self.cards[i].face_up if not face_up: self.cards[i].showFace() self.cards[i].item.tkraise() self.canvas.update_idletasks() self.game.sleep(self.game.app.opt.timeouts['raise_card']) if not face_up: self.cards[i].showBack() if TOOLKIT == 'tk': if positions > 0: self.cards[i].item.lower(self.cards[i+1].item) elif TOOLKIT == 'gtk': for c in self.cards[i+1:]: c.tkraise() self.canvas.update_idletasks() return 1 def rightclickHandler(self, event): return 0 def doubleclickHandler(self, event): return self.clickHandler(event) def controlclickHandler(self, event): return 0 def shiftclickHandler(self, event): # default action: highlight all cards of the same rank if self.game.app.opt.highlight_samerank: return self.highlightSameRank(event) return 0 def shiftrightclickHandler(self, event): return 0 def releaseHandler(self, event, drag, sound=True): # default action: move cards back to their origin position if drag.cards: if sound: self.game.playSample("nomove") if self.game.app.opt.mouse_type == 'point-n-click': drag.stack.moveCardsBackHandler(event, drag) else: self.moveCardsBackHandler(event, drag) def moveCardsBackHandler(self, event, drag): if self.game.app.opt.animations: if drag.cards: c = drag.cards[0] x0, y0 = drag.stack.getPositionFor(c) x1, y1 = c.x, c.y dx, dy = abs(x0-x1), abs(y0-y1) w, h = self.game.app.images.getSize() if dx > 2*w or dy > 2*h: self.game.animatedMoveTo(drag.stack, drag.stack, drag.cards, x0, y0, frames=-1) elif dx > w or dy > h: self.game.animatedMoveTo(drag.stack, drag.stack, drag.cards, x0, y0, frames=4) for card in drag.cards: self._position(card) if self.is_filled and self.items.shade_item: self.items.shade_item.show() self.items.shade_item.tkraise() # # Event handlers {controller} # def __defaultClickEventHandler(self, event, handler, start_drag=0, cancel_drag=1): self.game.event_handled = True # for Game.undoHandler if self.game.demo: self.game.stopDemo(event) return EVENT_HANDLED self.game.interruptSleep() if self.game.busy: return EVENT_HANDLED if self.game.drag.stack and cancel_drag: # in case we lost an event self.game.drag.stack.cancelDrag(event) if start_drag: # this handler may start a drag operation r = handler(event) if r is not None and r <= 0: sound = r == 0 self.startDrag(event, sound=sound) else: handler(event) return EVENT_HANDLED if (TOOLKIT == 'kivy'): def _motionEventHandler(self, event): return self.__motionEventHandler(event) def __clickEventHandler(self, event): if self.game.app.opt.mouse_type == 'drag-n-drop': cancel_drag = 1 start_drag = 1 handler = self.clickHandler else: # sticky-mouse or point-n-click cancel_drag = 0 start_drag = not self.game.drag.stack if start_drag: handler = self.clickHandler else: handler = self.finishDrag return self.__defaultClickEventHandler( event, handler, start_drag, cancel_drag) def __doubleclickEventHandler(self, event): return self.__defaultClickEventHandler(event, self.doubleclickHandler) def __middleclickEventHandler(self, event): return self.__defaultClickEventHandler(event, self.middleclickHandler) def __controlmiddleclickEventHandler(self, event): return self.__defaultClickEventHandler( event, self.controlmiddleclickHandler) def __rightclickEventHandler(self, event): return self.__defaultClickEventHandler(event, self.rightclickHandler) def __controlclickEventHandler(self, event): return self.__defaultClickEventHandler(event, self.controlclickHandler) def __shiftclickEventHandler(self, event): return self.__defaultClickEventHandler(event, self.shiftclickHandler) def __shiftrightclickEventHandler(self, event): return self.__defaultClickEventHandler( event, self.shiftrightclickHandler) def __motionEventHandler(self, event): if not self.game.drag.stack or self is not self.game.drag.stack: return EVENT_PROPAGATE if self.game.demo: self.game.stopDemo(event) if self.game.busy: return EVENT_HANDLED if self.game.app.opt.mouse_type == 'point-n-click': return EVENT_HANDLED self.keepDrag(event) # if self.game.app.opt.mouse_type == 'drag-n-drop' \ # and TOOLKIT == 'tk': # # use a timer to update the drag # # this allows us to skip redraws on slow machines # drag = self.game.drag # if drag.timer is None: # drag.timer = after_idle(self.canvas, self.keepDragTimer) # drag.event = event # else: # # update now # self.keepDrag(event) return EVENT_HANDLED def __releaseEventHandler(self, event): if self.game.demo: self.game.stopDemo(event) self.game.interruptSleep() if self.game.busy: return EVENT_HANDLED if self.game.app.opt.mouse_type == 'drag-n-drop': if TOOLKIT == 'kivy': drag = self.game.drag if drag and drag.stack: drag.stack.keepDrag(event) drag.stack.finishDrag(event) return EVENT_HANDLED self.keepDrag(event) self.finishDrag(event) return EVENT_HANDLED def __enterEventHandler(self, event): if self.game.drag.stack: if self.game.app.opt.mouse_type == 'point-n-click': if self.acceptsCards(self.game.drag.stack, self.game.drag.cards): self.canvas.config(cursor=CURSOR_DOWN_ARROW) self.current_cursor = CURSOR_DOWN_ARROW self.cursor_changed = True else: help = self.getHelp() # +' '+self.getBaseCard(), if DEBUG: help = repr(self) after_idle(self.canvas, self.game.showHelp, 'help', help, 'info', self.getNumCards()) return EVENT_HANDLED def __leaveEventHandler(self, event): if not self.game.drag.stack: after_idle(self.canvas, self.game.showHelp) if self.game.app.opt.mouse_type == 'drag-n-drop': return EVENT_HANDLED if self.cursor_changed: self.canvas.config(cursor='') self.current_cursor = '' self.cursor_changed = False drag_stack = self.game.drag.stack if self is drag_stack: x, y = event.x, event.y w, h = self.canvas.winfo_width(), self.canvas.winfo_height() if x < 0 or y < 0 or x >= w or y >= h: # cancel drag if mouse leave canvas drag_stack.cancelDrag(event) after_idle(self.canvas, self.game.showHelp) return EVENT_HANDLED else: # continue drag return self.__motionEventHandler(event) else: return EVENT_PROPAGATE # # Drag internals {controller -> model -> view} # def getDragCards(self, index): return self.cards[index:] # begin a drag operation def startDrag(self, event, sound=True): # print event.x, event.y assert self.game.drag.stack is None # import pdb # pdb.set_trace() i = self._findCard(event) if i < 0 or not self.canMoveCards(self.cards[i:]): return if self.is_filled and self.items.shade_item: self.items.shade_item.hide() x_offset, y_offset = self.cards[i].x, self.cards[i].y if sound: self.game.playSample("startdrag") self.lastx = event.x self.lasty = event.y game = self.game drag = game.drag drag.start_x = event.x drag.start_y = event.y drag.stack = self drag.noshade_stacks = [self] drag.cards = self.getDragCards(i) drag.index = i if self.game.app.opt.mouse_type == 'point-n-click': self._markCards(drag) return # if TOOLKIT == 'gtk': # drag.stack.group.tkraise() images = game.app.images drag.shadows = self.createShadows(drag.cards) # sx, sy = 0, 0 sx, sy = -images.SHADOW_XOFFSET, -images.SHADOW_YOFFSET dx, dy = 0, 0 cw, ch = images.getSize() if game.app.opt.mouse_type == 'sticky-mouse': # return cards under mouse dx = event.x - (x_offset+cw+sx) - game.canvas.xmargin dy = event.y - (y_offset+ch+sy) - game.canvas.ymargin if dx < 0: dx = 0 if dy < 0: dy = 0 for s in drag.shadows: if dx > 0 or dy > 0: s.move(dx, dy) if TOOLKIT == 'gtk': s.addtag(drag.stack.group) s.tkraise() for card in drag.cards: card.tkraise() card.moveBy(sx+dx, sy+dy) if game.app.opt.dragcursor: game.canvas.config(cursor=CURSOR_DRAG) # continue a drag operation def keepDrag(self, event): drag = self.game.drag if not drag.cards: return assert self is drag.stack dx = event.x - self.lastx dy = event.y - self.lasty if dx or dy: self.lastx = event.x self.lasty = event.y if self.game.app.opt.shade: self._updateShade() for s in drag.shadows: s.move(dx, dy) for card in drag.cards: card.moveBy(dx, dy) drag.event = None def keepDragTimer(self): drag = self.game.drag after_cancel(drag.timer) drag.timer = None if drag.event: self.keepDrag(drag.event) self.canvas.update_idletasks() # create shadows, return a tuple of MfxCanvasImages def createShadows(self, cards, dx=0, dy=0): if not self.game.app.opt.shadow or self.canvas.preview > 1: return () mylen = len(cards) if mylen == 0 or mylen > self.max_shadow_cards: return () images = self.game.app.images cx, cy = cards[0].x, cards[0].y ddx, ddy = cx-cards[-1].x, cy-cards[-1].y cw, ch = images.getSize() if USE_PIL: c0 = cards[-1] if self.CARD_XOFFSET[0] < 0: c0 = cards[0] if self.CARD_YOFFSET[0] < 0: c0 = cards[0] img = images.getShadowPIL(self, cards) cx, cy = c0.x + cw + dx, c0.y + ch + dy s = MfxCanvasImage(self.canvas, cx, cy, image=img, anchor=ANCHOR_SE) s.lower(c0.item) return (s,) if ddx == 0: # vertical for c in cards[1:]: if c.x != cx or abs(c.y - cy) != images.CARD_YOFFSET: return () cy = c.y img0, img1 = images.getShadow(0), images.getShadow(mylen) c0 = cards[-1] if self.CARD_YOFFSET[0] < 0: c0 = cards[0] elif ddy == 0: # horizontal for c in cards[1:]: if c.y != cy or abs(c.x - cx) != images.CARD_XOFFSET: return () cx = c.x img0, img1 = images.getShadow(-mylen), images.getShadow(1) c0 = cards[-1] if self.CARD_XOFFSET[0] < 0: c0 = cards[0] else: return () if img0 and img1: cx, cy = c0.x + cw + dx, c0.y + ch + dy if TOOLKIT == 'kivy': height0 = img0.getHeight() else: height0 = img0.height() s1 = MfxCanvasImage(self.game.canvas, cx, cy - height0, image=img1, anchor=ANCHOR_SE) s2 = MfxCanvasImage(self.canvas, cx, cy, image=img0, anchor=ANCHOR_SE) if TOOLKIT == 'tk': s1.lower(c0.item) s2.lower(c0.item) # elif TOOLKIT == 'gtk': # positions = 2 ## FIXME # s1.lower(positions) # s2.lower(positions) return (s1, s2) return () # handle shade within a drag operation def _deleteShade(self): if self.game.drag.shade_img: self.game.drag.shade_img.delete() self.game.drag.shade_img = None self.game.drag.shade_stack = None def _updateShade(self): # optimized for speed - we use lots of local variables game = self.game images = game.app.images CW, CH = images.CARDW, images.CARDH drag = game.drag # stacks = game.allstacks c = drag.cards[0] stacks = (game.getClosestStack(c, drag.stack), ) r1_0, r1_1, r1_2, r1_3 = c.x, c.y, c.x + CW, c.y + CH sstack, sdiff, sx, sy = None, 999999999, 0, 0 for s in stacks: if s is None or s in drag.noshade_stacks: continue if s.cards: c = s.cards[-1] r2 = (c.x, c.y, c.x + CW, c.y + CH) else: r2 = (s.x, s.y, s.x + CW, s.y + CH) if (r1_2 <= r2[0] or r1_3 <= r2[1] or r2[2] <= r1_0 or r2[3] <= r1_1): # rectangles do not intersect continue if s in drag.canshade_stacks: pass elif s.acceptsCards(drag.stack, drag.cards): drag.canshade_stacks.append(s) else: drag.noshade_stacks.append(s) continue diff = (r1_0 - r2[0])**2 + (r1_1 - r2[1])**2 if diff < sdiff: sstack, sdiff, sx, sy = s, diff, r2[0], r2[1] if sstack is drag.shade_stack: return if sstack is None: self._deleteShade() return if drag.shade_img: self._deleteShade() # create the shade image drag.shade_stack = sstack if sstack.cards: card = sstack.cards[-1] if card.face_up: img = images.getHighlightedCard( card.deck, card.suit, card.rank) else: img = images.getHighlightedBack() else: img = images.getShade() if not img: return img = MfxCanvasImage(game.canvas, sx, sy, image=img, anchor=ANCHOR_NW) drag.shade_img = img # raise/lower the shade image to the correct stacking order if TOOLKIT == 'tk': if drag.shadows: img.lower(drag.shadows[0]) else: img.lower(drag.cards[0].item) elif TOOLKIT == 'gtk': img.tkraise() drag.stack.group.tkraise() # for closeStack def _shadeStack(self): if not self.game.app.opt.shade_filled_stacks: return # if (self.CARD_XOFFSET != (0,) or # self.CARD_YOFFSET != (0,)): # return card = self.cards[-1] img = self.game.app.images.getHighlightedCard( card.deck, card.suit, card.rank) if img is None: return # self.canvas.update_idletasks() if TOOLKIT == 'kivy': self.game.top.waitAnimation() item = MfxCanvasImage(self.canvas, card.x, card.y, image=img, anchor=ANCHOR_NW, group=self.group) # item.tkraise() self.items.shade_item = item def unshadeStack(self): if self.items.shade_item: self.items.shade_item.delete() self.items.shade_item = None def _markCards(self, drag): cards = drag.cards drag.stack.group.tkraise() # x0, y0 = self.getPositionFor(cards[0]) x1, y1 = self.getPositionFor(cards[-1]) x0, x1 = min(x1, x0), max(x1, x0) y0, y1 = min(y1, y0), max(y1, y0) cw, ch = self.game.app.images.getSize() x1 += cw y1 += ch xx0, yy0 = x0, y0 w, h = x1-x0, y1-y0 # if TOOLKIT == 'gtk' or not Image: color = self.game.app.opt.colors['cards_1'] r = MfxCanvasRectangle(self.canvas, xx0, yy0, xx0+w, yy0+h, fill="", outline=color, width=4, group=self.group) drag.shadows.append(r) # mylen = MfxCanvasLine(self.canvas, xx0, yy0, xx0+w, yy0+h, # fill=color, width=4) # drag.shadows.append(mylen) # mylen = MfxCanvasLine(self.canvas, xx0, yy0+h, xx0+w, yy0, # fill=color, width=4) # drag.shadows.append(mylen) return # shade = Image.new('RGBA', (w, h)) for c in cards: x, y = self.getPositionFor(c) x, y = x-xx0, y-yy0 im = c._active_image._pil_image shade.paste(im, (x, y), im) # shade = markImage(shade) tkshade = ImageTk.PhotoImage(shade) im = MfxCanvasImage(self.canvas, xx0, yy0, image=tkshade, anchor=ANCHOR_NW, group=self.group) drag.shadows.append(im) def _stopDrag(self): drag = self.game.drag after_cancel(drag.timer) drag.timer = None self._deleteShade() drag.canshade_stacks = [] drag.noshade_stacks = [] for s in drag.shadows: s.delete() drag.shadows = [] drag.stack = None drag.cards = [] # finish a drag operation def finishDrag(self, event=None): if self.game.app.opt.dragcursor: self.canvas.config(cursor='') drag = self.game.drag.copy() if self.game.app.opt.mouse_type == 'point-n-click': drag.stack._stopDrag() else: self._stopDrag() if drag.cards: if self.game.app.opt.mouse_type == 'point-n-click': self.releaseHandler(event, drag) else: assert drag.stack is self self.releaseHandler(event, drag) # cancel a drag operation def cancelDrag(self, event=None): if self.game.app.opt.dragcursor: self.canvas.config(cursor='') drag = self.game.drag.copy() if self.game.app.opt.mouse_type == 'point-n-click': drag.stack._stopDrag() else: self._stopDrag() if drag.cards: assert drag.stack is self self.moveCardsBackHandler(event, drag) def getHelp(self): return str(self) # debug def getBaseCard(self): return '' def _getBaseCard(self, rank=None): # FIXME: no-french games if self.cap.max_accept == 0: return '' if rank is None: br = self.cap.base_rank else: br = rank s = _('Base card - %s.') if br == NO_RANK: s = _('Empty row cannot be filled.') elif br == -1: s = s % _('any card') elif br == 10: s = s % _('Jack') elif br == 11: s = s % _('Queen') elif br == 12: s = s % _('King') elif br == 0: s = s % _('Ace') else: s = s % str(br+1) return s def getNumCards(self): from pysollib.mygettext import ungettext n = len(self.cards) if n == 0: return _('No cards') else: return ungettext('%d card', '%d cards', n) % n # ************************************************************************ # * Abstract interface that supports a concept of dealing. # ************************************************************************ class DealRow_StackMethods: # Deal a card to each of the RowStacks. Return number of cards dealt. def dealRow(self, rows=None, flip=1, reverse=0, frames=-1, sound=False): if rows is None: rows = self.game.s.rows if sound and frames and self.game.app.opt.animations: self.game.startDealSample() n = self.dealToStacks(rows, flip, reverse, frames) if sound: self.game.stopSamples() return n # Same, but no error if not enough cards are available. def dealRowAvail(self, rows=None, flip=1, reverse=0, frames=-1, sound=False): if rows is None: rows = self.game.s.rows if sound and frames and self.game.app.opt.animations: self.game.startDealSample() if len(self.cards) < len(rows): rows = rows[:len(self.cards)] n = self.dealToStacks(rows, flip, reverse, frames) if sound: self.game.stopSamples() return n def dealToStacks(self, stacks, flip=1, reverse=0, frames=-1): if not self.cards or not stacks: return 0 assert len(self.cards) >= len(stacks) old_state = self.game.enterState(self.game.S_DEAL) if reverse: stacks = list(stacks) stacks.reverse() for r in stacks: assert not self.getCard().face_up assert r is not self if flip: self.game.flipMove(self) self.game.moveMove(1, self, r, frames=frames) if frames > 0 and self.game.top is not None: self.game.top.update_idletasks() self.game.top.busyUpdate() self.game.leaveState(old_state) if TOOLKIT == 'kivy': self.game.top.waitAnimation() return len(stacks) # all Aces go to the Foundations def dealToStacksOrFoundations(self, stacks, flip=1, reverse=0, frames=-1, rank=-1): if rank < 0: rank = self.game.s.foundations[0].cap.base_rank if not self.cards or not stacks: return 0 old_state = self.game.enterState(self.game.S_DEAL) if reverse: stacks = list(stacks) stacks.reverse() n = 0 for r in stacks: assert r is not self while self.cards: n += 1 if flip: self.game.flipMove(self) if flip and self.cards[-1].rank == rank: for s in self.game.s.foundations: assert s is not self if s.acceptsCards(self, self.cards[-1:]): self.game.moveMove(1, self, s, frames=frames) break else: self.game.moveMove(1, self, r, frames=frames) break self.game.leaveState(old_state) if TOOLKIT == 'kivy': self.game.top.waitAnimation() return n class DealBaseCard_StackMethods: def dealSingleBaseCard(self, frames=-1, update_saveinfo=1): c = self.cards[-1] self.dealBaseCards(ncards=1, frames=frames, update_saveinfo=0) for s in self.game.s.foundations: s.cap.base_rank = c.rank if update_saveinfo: cap = Struct(base_rank=c.rank) self.game.saveinfo.stack_caps.append((s.id, cap)) return c def dealBaseCards(self, ncards=1, frames=-1, update_saveinfo=1): assert self.game.moves.state == self.game.S_INIT assert not self.base_cards while ncards > 0: assert self.cards c = self.cards[-1] for s in self.game.s.foundations: if (not s.cards and (s.cap.base_suit < 0 or s.cap.base_suit == c.suit)): break else: assert 0 s = None s.cap.base_rank = c.rank if update_saveinfo: cap = Struct(base_rank=c.rank) self.game.saveinfo.stack_caps.append((s.id, cap)) if not c.face_up: self.game.flipMove(self) self.game.moveMove(1, self, s, frames=frames) ncards -= 1 class RedealCards_StackMethods: def _redeal(self, rows=None, reverse=False, frames=0): # move all cards to the Talon num_cards = 0 assert len(self.cards) == 0 if rows is None: rows = self.game.s.rows rows = list(rows) if reverse: rows.reverse() for r in rows: for i in range(len(r.cards)): num_cards += 1 self.game.moveMove(1, r, self, frames=frames, shadow=0) if self.cards[-1].face_up: self.game.flipMove(self) assert len(self.cards) == num_cards return num_cards def redealCards(self, rows=None, sound=False, shuffle=False, reverse=False, frames=0): if sound and self.game.app.opt.animations: self.game.startDealSample() num_cards = self._redeal(rows=rows, reverse=reverse, frames=frames) if num_cards == 0: # game already finished return 0 if shuffle: # shuffle self.game.shuffleStackMove(self) # redeal self.game.nextRoundMove(self) self.game.redealCards() if sound: self.game.stopSamples() return num_cards # ************************************************************************ # * The Talon is a stack with support for dealing. # ************************************************************************ class TalonStack(Stack, DealRow_StackMethods, DealBaseCard_StackMethods, ): def __init__(self, x, y, game, max_rounds=1, num_deal=1, **cap): Stack.__init__(self, x, y, game, cap=cap) self.max_rounds = max_rounds self.num_deal = num_deal self.init_redeal = Struct( top_bottom=None, img_coord=None, txt_coord=None, ) self.resetGame() def resetGame(self): Stack.resetGame(self) self.round = 1 self.base_cards = [] # for DealBaseCard_StackMethods def assertStack(self): Stack.assertStack(self) n = self.game.gameinfo.redeals if n < 0: assert self.max_rounds == n else: assert self.max_rounds == n + 1 # Control of dealing is transferred to the game which usually # transfers it back to the Talon - see dealCards() below. def clickHandler(self, event): return self.game.dealCards(sound=True) def rightclickHandler(self, event): return self.clickHandler(event) # Usually called by Game.canDealCards() def canDealCards(self): return len(self.cards) > 0 # Actual dealing, usually called by Game.dealCards(). # Either deal all cards in Game.startGame(), or subclass responsibility. def dealCards(self, sound=False): pass # remove all cards from all stacks def removeAllCards(self): for stack in self.game.allstacks: while stack.cards: stack.removeCard(update=0) # stack.removeCard(unhide=0, update=0) for stack in self.game.allstacks: stack.updateText() def updateRedealImage(self): deal = self.canDealCards() != 0 if self.images.redeal is not None: img = (self.getRedealImages())[deal] if img is not None and img is not self.images.redeal_img: self.images.redeal.config(image=img) self.images.redeal_img = img def updateText(self, update_rounds=1, update_redeal=1): # assertView(self) Stack.updateText(self) if update_rounds and self.game.preview <= 1: if self.texts.rounds is not None: t = _("Round %d") % self.round self.texts.rounds.config(text=t) if update_redeal: deal = self.canDealCards() != 0 if self.images.redeal is not None: img = (self.getRedealImages())[deal] if img is not None and img is not self.images.redeal_img: self.images.redeal.config(image=img) self.images.redeal_img = img t = ("", _("Redeal"))[deal] else: t = (_("Stop"), _("Redeal"))[deal] if self.texts.redeal is not None and self.game.preview <= 1: if t != self.texts.redeal_str: self.texts.redeal.config(text=t) self.texts.redeal_str = t def _addRedealImage(self): # add or remove the redeal image/text if not self.is_visible or self.images.bottom is None: return if self.game.preview > 1: return images = self.game.app.images cw, ch = images.getSize() cx, cy = self.init_redeal.img_coord ca = 'center' tx, ty = self.init_redeal.txt_coord if self.images.redeal: self.canvas.delete(self.images.redeal) self.images.redeal = None self.images.redeal_img = None if self.texts.redeal: self.canvas.delete(self.texts.redeal) self.texts.redeal = None self.texts.redeal_str = '' self.top_bottom = self.init_redeal.top_bottom if cw >= 60 and ch >= 60: # add a redeal image above the bottom image img = (self.getRedealImages())[self.max_rounds != 1] if img is not None: self.images.redeal_img = img if TOOLKIT == 'tk': self.images.redeal = MfxCanvasImage(self.canvas, cx, cy, image=img, anchor="center", group=self.group) self.images.redeal.tkraise(self.top_bottom) elif TOOLKIT == 'kivy': self.images.redeal = MfxCanvasImage(self.canvas, cx, cy, image=img, anchor="center", group=self.group, hint="redeal_image") self.images.redeal.tkraise(self.top_bottom) elif TOOLKIT == 'gtk': # FIXME pass self.top_bottom = self.images.redeal if ch >= 90: cy, ca = ty, "s" else: ca = None font = self.game.app.getFont("canvas_default") text_width = get_text_width(_('Redeal'), font=font, root=self.canvas) if cw >= text_width+4 and ca: # add a redeal text below the bottom image if self.max_rounds != 1: # FIXME: sometimes canvas do not show the text # print 'add txt', cx, cy self.texts.redeal_str = "" images = self.game.app.images self.texts.redeal = MfxCanvasText(self.canvas, cx, cy, anchor=ca, font=font, group=self.group) if TOOLKIT == 'tk': self.texts.redeal.tkraise(self.top_bottom) elif TOOLKIT == 'kivy': self.texts.redeal.tkraise(self.top_bottom) elif TOOLKIT == 'gtk': # FIXME pass self.top_bottom = self.texts.redeal def prepareView(self): Stack.prepareView(self) if 0: if not self.is_visible or self.images.bottom is None: return if self.images.redeal is not None or self.texts.redeal is not None: return if self.game.preview > 1: return images = self.game.app.images self.init_redeal.top_bottom = self.top_bottom cx, cy = self.x + images.CARDW//2, self.y + images.CARDH//2 ty = self.y + images.CARDH - 4 self.init_redeal.img_coord = cx, cy self.init_redeal.txt_coord = cx, ty # At least display a redealImage at start, if USE_PIL is not set. if USE_PIL is False: self._addRedealImage() getBottomImage = Stack._getTalonBottomImage def getRedealImages(self): # returns a tuple of two PhotoImages return self.game.app.gimages.redeal def getHelp(self): from pysollib.mygettext import ungettext if self.max_rounds == -2: nredeals = _('Variable redeals.') elif self.max_rounds == -1: nredeals = _('Unlimited redeals.') else: n = self.max_rounds-1 nredeals = ungettext('%d redeal', '%d redeals', n) % n # round = _('Round #%d.') % self.round return _('Talon.')+' '+nredeals # +' '+round # def getBaseCard(self): # return self._getBaseCard() def resize(self, xf, yf, widthpad=0, heightpad=0): self._addRedealImage() Stack.resize(self, xf, yf, widthpad=widthpad, heightpad=heightpad) # Use for games that do not allow manual dealing from the talon. class AutoDealTalonStack(TalonStack): def canDealCards(self): return False # A single click deals one card to each of the RowStacks. class DealRowTalonStack(TalonStack): def dealCards(self, sound=False): return self.dealRowAvail(sound=sound) # A single click deals one card to each of the RowStacks. class DealFirstRowTalonStack(TalonStack): def dealCards(self, sound=False): return self.dealRowAvail(sound=sound, rows=(self.game.s.rows[0],)) # For games where the Talon is only used for the initial dealing. class InitialDealTalonStack(TalonStack): # no bindings def initBindings(self): pass # no bottom getBottomImage = Stack._getNoneBottomImage class RedealTalonStack(TalonStack, RedealCards_StackMethods): def canDealCards(self): if self.round == self.max_rounds: return False return not self.game.isGameWon() def dealCards(self, sound=False): RedealCards_StackMethods.redealCards(self, sound=sound) class DealRowRedealTalonStack(TalonStack, RedealCards_StackMethods): def canDealCards(self, rows=None): if rows is None: rows = self.game.s.rows r_cards = sum([len(r.cards) for r in rows]) if self.cards: return True elif r_cards and self.round != self.max_rounds: return True return False def dealCards(self, sound=False, rows=None, shuffle=False): num_cards = 0 if rows is None: rows = self.game.s.rows if sound and self.game.app.opt.animations: self.game.startDealSample() if not self.cards: # move all cards to talon num_cards = self._redeal(rows=rows, frames=4) if shuffle: # shuffle self.game.shuffleStackMove(self) self.game.nextRoundMove(self) num_cards += self.dealRowAvail(rows=rows, sound=False) if sound: self.game.stopSamples() return num_cards def shuffleAndDealCards(self, sound=False, rows=None): DealRowRedealTalonStack.dealCards(self, sound=sound, rows=rows, shuffle=True) class DealFirstRowRedealTalonStack(DealRowRedealTalonStack): def canDealCards(self, rows=None): return DealRowRedealTalonStack.canDealCards( self, rows=self.game.s.reserves) def dealCards(self, sound=False, rows=None): return DealRowRedealTalonStack.dealCards( self, sound=sound, rows=(self.game.s.rows[0],)) class DealReserveRedealTalonStack(DealRowRedealTalonStack): def canDealCards(self, rows=None): return DealRowRedealTalonStack.canDealCards( self, rows=self.game.s.reserves) def dealCards(self, sound=False, rows=None): return DealRowRedealTalonStack.dealCards( self, sound=sound, rows=self.game.s.reserves) # Spider Talons class SpiderTalonStack(DealRowRedealTalonStack): def canDealCards(self): if not DealRowRedealTalonStack.canDealCards(self): return False # no row may be empty for r in self.game.s.rows: if not r.cards: return False return True class GroundsForADivorceTalonStack(DealRowRedealTalonStack): # A single click deals a new cards to each non-empty row. def dealCards(self, sound=True): if self.cards: rows = [r for r in self.game.s.rows if r.cards] # if not rows: # # deal one card to first row if all rows are empty # rows = self.game.s.rows[:1] return DealRowRedealTalonStack.dealRowAvail(self, rows=rows, sound=sound) return 0 # ************************************************************************ # * An OpenStack is a stack where cards can be placed and dragged # * (i.e. FoundationStack, RowStack, ReserveStack, ...) # * # * Note that it defaults to max_move=1 and max_accept=0. # ************************************************************************ class OpenStack(Stack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=1, max_accept=0, max_cards=999999) Stack.__init__(self, x, y, game, cap=cap) # # Capabilities {model} # def acceptsCards(self, from_stack, cards): # default for OpenStack: we cannot accept # cards (max_accept defaults to 0) return self.basicAcceptsCards(from_stack, cards) def canMoveCards(self, cards): # import pdb # pdb.set_trace() # print('OpenStack.canMoveCards()', cards) # default for OpenStack: we can move the top card # (max_move defaults to 1) return self.basicCanMoveCards(cards) def canFlipCard(self): # default for OpenStack: we can flip the top card if self.basicIsBlocked() or not self.cards: return False return not self.cards[-1].face_up def canDropCards(self, stacks): if self.basicIsBlocked() or not self.cards: return (None, 0) cards = self.cards[-1:] if self.canMoveCards(cards): for s in stacks: if s is not self and s.acceptsCards(self, cards): return (s, 1) return (None, 0) # # Mouse handlers {controller} # def clickHandler(self, event): flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event) if self in flipstacks and self.canFlipCard(): self.playFlipMove(animation=True) # return -1 # continue this event (start a drag) return 1 # break return 0 def rightclickHandler(self, event): if self.doubleclickHandler(event): return 1 if self.game.app.opt.quickplay: flipstacks, dropstacks, quickstacks = \ self.game.getAutoStacks(event) if self in quickstacks: n = self.quickPlayHandler(event) self.game.stats.quickplay_moves += n return n return 0 def doubleclickHandler(self, event): # flip or drop a card flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event) if self in flipstacks and self.canFlipCard(): self.playFlipMove(animation=True) return -1 # continue this event (start a drag) if self in dropstacks: to_stack, ncards = self.canDropCards(self.game.s.foundations) if to_stack: self.game.playSample("autodrop", priority=30) self.playMoveMove(ncards, to_stack, sound=False) return 1 return 0 def controlclickHandler(self, event): # highlight matching cards if self.game.app.opt.highlight_cards: return self.highlightMatchingCards(event) return 0 def dragMove(self, drag, stack, sound=True): if self.game.app.opt.mouse_type == 'point-n-click': self.playMoveMove(len(drag.cards), stack, sound=sound) else: # self.playMoveMove(len(drag.cards), stack, frames=0, sound=sound) self.playMoveMove(len(drag.cards), stack, frames=-2, sound=sound) def releaseHandler(self, event, drag, sound=True): cards = drag.cards # check if we moved the card by at least 10 pixels if event is not None: dx, dy = event.x - drag.start_x, event.y - drag.start_y if abs(dx) < 10 and abs(dy) < 10: # move cards back to their origin stack Stack.releaseHandler(self, event, drag, sound=sound) return # print dx, dy # get destination stack if self.game.app.opt.mouse_type == 'point-n-click': from_stack = drag.stack to_stack = self else: from_stack = self to_stack = self.game.getClosestStack(cards[0], self) # move cards if (not to_stack or from_stack is to_stack or not to_stack.acceptsCards(from_stack, cards)): # move cards back to their origin stack Stack.releaseHandler(self, event, drag, sound=sound) else: # this code actually moves the cards to the new stack # self.playMoveMove(len(cards), stack, frames=0, sound=sound) from_stack.dragMove(drag, to_stack, sound=sound) def quickPlayHandler(self, event, from_stacks=None, to_stacks=None): # from_stacks and to_stacks are meant for possible # use in a subclasses if from_stacks is None: from_stacks = self.game.sg.dropstacks if to_stacks is None: # to_stacks = self.game.s.rows + self.game.s.reserves # to_stacks = self.game.sg.dropstacks to_stacks = self.game.s.foundations + self.game.sg.dropstacks # from pprint import pprint; pprint(to_stacks) moves = [] # if not self.cards: for s in from_stacks: if s is not self and s.cards: pile = s.getPile() if pile and self.acceptsCards(s, pile): score = self.game.getQuickPlayScore(len(pile), s, self) moves.append((score, -len(moves), len(pile), s, self)) else: pile1, pile2 = None, self.getPile() if pile2: i = self._findCard(event) if i >= 0: pile = self.cards[i:] if len(pile) != len(pile2) and self.canMoveCards(pile): pile1 = pile for pile in (pile1, pile2): if not pile: continue for s in to_stacks: if s is not self and s.acceptsCards(self, pile): score = self.game.getQuickPlayScore(len(pile), self, s) moves.append((score, -len(moves), len(pile), self, s)) # if moves: moves.sort() # from pprint import pprint; pprint(moves) score, len_moves, ncards, from_stack, to_stack = moves[-1] if score >= 0: # self.game.playSample("startdrag") from_stack.playMoveMove(ncards, to_stack) return 1 return 0 def getHelp(self): if self.cap.max_accept == 0: return _('Reserve. No building.') return 'Reserve.' # ************************************************************************ # * Foundations stacks # ************************************************************************ class AbstractFoundationStack(OpenStack): def __init__(self, x, y, game, suit, **cap): kwdefault(cap, suit=suit, base_suit=suit, base_rank=ACE, dir=1, max_accept=1, max_cards=13) OpenStack.__init__(self, x, y, game, **cap) def canDropCards(self, stacks): return (None, 0) def clickHandler(self, event): return 0 def rightclickHandler(self, event): # return 0 if self.game.app.opt.quickplay: n = self.quickPlayHandler(event) self.game.stats.quickplay_moves += n return n return 0 def quickPlayHandler(self, event): # return 0 from_stacks = self.game.sg.dropstacks + self.game.s.foundations # to_stacks = self.game.sg.dropstacks to_stacks = from_stacks return OpenStack.quickPlayHandler(self, event, from_stacks, to_stacks) getBottomImage = Stack._getSuitBottomImage def getBaseCard(self): return self._getBaseCard() def closeStack(self): if len(self.cards) == self.cap.max_cards: self.is_filled = True self._shadeStack() def getHelp(self): return _('Foundation.') def varyAcceptsCards(self, from_stack, cards): # if base rank of foundations is vary subclass = self.__class__ # derived class (SS_FoundationStack, etc) assert subclass is not AbstractFoundationStack if self.cards: return subclass.acceptsCards(self, from_stack, cards) if not subclass.acceptsCards(self, from_stack, cards): return False # this stack don't have cards: check base rank of other stacks for s in self.game.s.foundations: if s.cards: base_card = s.cards[0] return base_card.rank == cards[0].rank return True # all foundations is empty def varyGetBaseCard(self): rank = None for s in self.game.s.foundations: if s.cards: rank = s.cards[0].rank return self._getBaseCard(rank=rank) # A SameSuit_FoundationStack is the typical Foundation stack. # It builds up in rank and suit. class SS_FoundationStack(AbstractFoundationStack): def acceptsCards(self, from_stack, cards): if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): return False if self.cards: # check the rank if ((self.cards[-1].rank + self.cap.dir) % self.cap.mod != cards[0].rank): return False return True def getHelp(self): if self.cap.dir > 0: return _('Foundation. Build up by suit.') elif self.cap.dir < 0: return _('Foundation. Build down by suit.') else: return _('Foundation. Build by same rank.') # A Rank_FoundationStack builds up in rank and ignores color and suit. class RK_FoundationStack(SS_FoundationStack): def __init__(self, x, y, game, suit=ANY_SUIT, **cap): SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap) def getHelp(self): if self.cap.dir > 0: return _('Foundation. Build up regardless of suit.') elif self.cap.dir < 0: return _('Foundation. Build down regardless of suit.') else: return _('Foundation. Build by same rank.') # A AlternateColor_FoundationStack builds up in rank and alternate color. # It is used in only a few games. class AC_FoundationStack(SS_FoundationStack): def __init__(self, x, y, game, suit, **cap): kwdefault(cap, base_suit=suit) SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap) def acceptsCards(self, from_stack, cards): if not SS_FoundationStack.acceptsCards(self, from_stack, cards): return False if self.cards: # check the color if cards[0].color == self.cards[-1].color: return False return True def getHelp(self): if self.cap.dir > 0: return _('Foundation. Build up by alternate color.') elif self.cap.dir < 0: return _('Foundation. Build down by alternate color.') else: return _('Foundation. Build by same rank.') # A SameColor_FoundationStack builds up in rank and alternate color. # It is used in only a few games. class SC_FoundationStack(SS_FoundationStack): def __init__(self, x, y, game, suit, **cap): kwdefault(cap, base_suit=suit) SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap) def acceptsCards(self, from_stack, cards): if not SS_FoundationStack.acceptsCards(self, from_stack, cards): return False if self.cards: # check the color if cards[0].color != self.cards[-1].color: return False return True def getHelp(self): if self.cap.dir > 0: return _('Foundation. Build up by color.') elif self.cap.dir < 0: return _('Foundation. Build down by color.') else: return _('Foundation. Build by same rank.') # A ButOwn_FoundationStack builds up in rank and any suit but the same. # It is used in only a few games. class BO_FoundationStack(SS_FoundationStack): def __init__(self, x, y, game, suit, **cap): kwdefault(cap, base_suit=suit) SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap) def acceptsCards(self, from_stack, cards): if not SS_FoundationStack.acceptsCards(self, from_stack, cards): return False if self.cards: # check the suit if cards[0].suit == self.cards[-1].suit: return False return True def getHelp(self): if self.cap.dir > 0: return _('Foundation. Build up in any suit but the same.') elif self.cap.dir < 0: return _('Foundation. Build down in any suit but the same.') else: return _('Foundation. Build by same rank.') # Spider-type foundations class Spider_SS_Foundation(AbstractFoundationStack): def __init__(self, x, y, game, suit=ANY_SUIT, **cap): kwdefault(cap, dir=-1, base_rank=KING, min_accept=13, max_accept=13, max_move=0) AbstractFoundationStack.__init__(self, x, y, game, suit, **cap) def acceptsCards(self, from_stack, cards): if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): return False # now check the cards return isSameSuitSequence(cards, self.cap.mod, self.cap.dir) class Spider_AC_Foundation(Spider_SS_Foundation): def acceptsCards(self, from_stack, cards): if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): return False # now check the cards return isAlternateColorSequence(cards, self.cap.mod, self.cap.dir) class Spider_SC_Foundation(Spider_SS_Foundation): def acceptsCards(self, from_stack, cards): if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): return False # now check the cards return isSameColorSequence(cards, self.cap.mod, self.cap.dir) class Spider_RK_Foundation(Spider_SS_Foundation): def acceptsCards(self, from_stack, cards): if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): return False # now check the cards return isRankSequence(cards, self.cap.mod, self.cap.dir) class Spider_BO_Foundation(Spider_SS_Foundation): def acceptsCards(self, from_stack, cards): if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): return False # now check the cards return isAnySuitButOwnSequence(cards, self.cap.mod, self.cap.dir) # ************************************************************************ # * Abstract classes for row stacks. # ************************************************************************ # Abstract class. class SequenceStack_StackMethods: def _isSequence(self, cards): # Are the cards in a basic sequence for our stack ? raise SubclassResponsibility def _isAcceptableSequence(self, cards): return self._isSequence(cards) def _isMoveableSequence(self, cards): # import pdb; pdb.set_trace() return self._isSequence(cards) def acceptsCards(self, from_stack, cards): if not self.basicAcceptsCards(from_stack, cards): return False # cards must be an acceptable sequence if not self._isAcceptableSequence(cards): return False # [topcard + cards] must be an acceptable sequence if (self.cards and not self._isAcceptableSequence([self.cards[-1]] + cards)): return False return True def canMoveCards(self, cards): return self.basicCanMoveCards(cards) and \ self._isMoveableSequence(cards) # Abstract class. class BasicRowStack(OpenStack): def __init__(self, x, y, game, **cap): kwdefault(cap, dir=-1, base_rank=ANY_RANK) OpenStack.__init__(self, x, y, game, **cap) self.CARD_YOFFSET = game.app.images.CARD_YOFFSET def getHelp(self): if self.cap.max_accept == 0: return _('Tableau. No building.') return '' # def getBaseCard(self): # return self._getBaseCard() def spiderCanDropCards(self, stacks): # print('spiderCanDropCards()', stacks) # drop whole sequence if len(self.cards) < 13: return (None, 0) cards = self.cards[-13:] for s in stacks: if s is not self and s.acceptsCards(self, cards): return (s, 13) return (None, 0) def getReserveBottomImage(self): return self.game.app.images.getReserveBottom() # Abstract class. class SequenceRowStack(SequenceStack_StackMethods, BasicRowStack): # canMoveCards = OpenStack.canMoveCards def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=999999, max_accept=999999) BasicRowStack.__init__(self, x, y, game, **cap) def getBaseCard(self): return self._getBaseCard() # ************************************************************************ # * Row stacks (the main playing stacks on the Tableau). # ************************************************************************ # # Implementation of common row stacks follows here. # # An AlternateColor_RowStack builds down by rank and alternate color. # e.g. Klondike class AC_RowStack(SequenceRowStack): def _isSequence(self, cards): return isAlternateColorSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up by alternate color.') elif self.cap.dir < 0: return _('Tableau. Build down by alternate color.') else: return _('Tableau. Build by same rank.') # A SameColor_RowStack builds down by rank and same color. # e.g. Klondike class SC_RowStack(SequenceRowStack): def _isSequence(self, cards): return isSameColorSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up by color.') elif self.cap.dir < 0: return _('Tableau. Build down by color.') else: return _('Tableau. Build by same rank.') # A SameSuit_RowStack builds down by rank and suit. class SS_RowStack(SequenceRowStack): def _isSequence(self, cards): return isSameSuitSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up by suit.') elif self.cap.dir < 0: return _('Tableau. Build down by suit.') else: return _('Tableau. Build by same rank.') # A Rank_RowStack builds down by rank ignoring suit. class RK_RowStack(SequenceRowStack): def _isSequence(self, cards): return isRankSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up regardless of suit.') elif self.cap.dir < 0: return _('Tableau. Build down regardless of suit.') else: return _('Tableau. Build by same rank.') # ButOwn_RowStack class BO_RowStack(SequenceRowStack): def _isSequence(self, cards): return isAnySuitButOwnSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up in any suit but the same.') elif self.cap.dir < 0: return _('Tableau. Build down in any suit but the same.') else: return _('Tableau. Build by same rank.') # A Freecell_AlternateColor_RowStack class FreeCell_AC_RowStack(AC_RowStack): def canMoveCards(self, cards): max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 return len(cards) <= max_move and AC_RowStack.canMoveCards(self, cards) # A Freecell_SameSuit_RowStack (i.e. Baker's Game) class FreeCell_SS_RowStack(SS_RowStack): def canMoveCards(self, cards): max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 return len(cards) <= max_move and SS_RowStack.canMoveCards(self, cards) # A Freecell_Rank_RowStack class FreeCell_RK_RowStack(RK_RowStack): def canMoveCards(self, cards): max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 return len(cards) <= max_move and RK_RowStack.canMoveCards(self, cards) # A Spider_AlternateColor_RowStack builds down by rank and alternate color, # but accepts sequences that match by rank only. class Spider_AC_RowStack(AC_RowStack): def _isAcceptableSequence(self, cards): return isRankSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up regardless of suit. ' 'Sequences of cards in alternate color ' 'can be moved as a unit.') elif self.cap.dir < 0: return _('Tableau. Build down regardless of suit. ' 'Sequences of cards in alternate color can be moved ' 'as a unit.') else: return _('Tableau. Build by same rank.') # A Spider_SameSuit_RowStack builds down by rank and suit, # but accepts sequences that match by rank only. class Spider_SS_RowStack(SS_RowStack): def _isAcceptableSequence(self, cards): return isRankSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up regardless of suit. ' 'Sequences of cards in the same suit can be moved ' 'as a unit.') elif self.cap.dir < 0: return _('Tableau. Build down regardless of suit. ' 'Sequences of cards in the same suit can be moved ' 'as a unit.') else: return _('Tableau. Build by same rank.') # A Spider_SameColor_RowStack builds down by rank and color, # but accepts sequences that match by rank only. class Spider_SC_RowStack(SC_RowStack): def _isAcceptableSequence(self, cards): return isRankSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up regardless of suit. ' 'Sequences of cards in the same color can be moved ' 'as a unit.') elif self.cap.dir < 0: return _('Tableau. Build down regardless of suit. ' 'Sequences of cards in the same color can be moved ' 'as a unit.') else: return _('Tableau. Build by same rank.') # A Spider_ButOwn_RowStack builds down by rank and any suit but own, # but accepts sequences that match by rank only. class Spider_BO_RowStack(BO_RowStack): def _isAcceptableSequence(self, cards): return isRankSequence(cards, self.cap.mod, self.cap.dir) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up regardless of suit. ' 'Sequences of cards in any suit but the same ' 'can be moved as a unit.') elif self.cap.dir < 0: return _('Tableau. Build down regardless of suit. ' 'Sequences of cards in any suit but the same ' 'can be moved as a unit.') else: return _('Tableau. Build by same rank.') # A Yukon_AlternateColor_RowStack builds down by rank and alternate color, # but can move any face-up cards regardless of sequence. class Yukon_AC_RowStack(BasicRowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=999999, max_accept=999999) BasicRowStack.__init__(self, x, y, game, **cap) def _isYukonSequence(self, c1, c2): # print('Yukon_AC_RowStack._isYukonSequence()', c1, c2) return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and c1.color != c2.color) def acceptsCards(self, from_stack, cards): # print('Yukon_AC_RowStack.acceptsCards()', from_stack, cards) if not self.basicAcceptsCards(from_stack, cards): return False # [topcard + card[0]] must be acceptable if self.cards and not self._isYukonSequence(self.cards[-1], cards[0]): return False return True def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up by alternate color, ' 'can move any face-up cards regardless of sequence.') elif self.cap.dir < 0: return _('Tableau. Build down by alternate color, ' 'can move any face-up cards regardless of sequence.') else: return _('Tableau. Build by same rank, can move ' 'any face-up cards regardless of sequence.') def getBaseCard(self): return self._getBaseCard() # A Yukon_SameSuit_RowStack builds down by rank and suit, # but can move any face-up cards regardless of sequence. class Yukon_SS_RowStack(Yukon_AC_RowStack): def _isYukonSequence(self, c1, c2): return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and c1.suit == c2.suit) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up by suit, can move any face-up cards ' 'regardless of sequence.') elif self.cap.dir < 0: return _('Tableau. Build down by suit, can move any ' 'face-up cards regardless of sequence.') else: return _('Tableau. Build by same rank, can move any ' 'face-up cards regardless of sequence.') # A Yukon_SameColor_RowStack builds down by rank and color, # but can move any face-up cards regardless of sequence. class Yukon_SC_RowStack(Yukon_AC_RowStack): def _isYukonSequence(self, c1, c2): return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and c1.color == c2.color) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up by color, can move any face-up cards ' 'regardless of sequence.') elif self.cap.dir < 0: return _('Tableau. Build down by color, can move any ' 'face-up cards regardless of sequence.') else: return _('Tableau. Build by same rank, can move any ' 'face-up cards regardless of sequence.') # A Yukon_Rank_RowStack builds down by rank # but can move any face-up cards regardless of sequence. class Yukon_RK_RowStack(Yukon_AC_RowStack): def _isYukonSequence(self, c1, c2): return (c1.rank + self.cap.dir) % self.cap.mod == c2.rank def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up regardless of suit, ' 'can move any face-up cards regardless of sequence.') elif self.cap.dir < 0: return _('Tableau. Build up regardless of suit, can move any ' 'face-up cards regardless of sequence.') else: return _('Tableau. Build by same rank, can move any ' 'face-up cards regardless of sequence.') # A Yukon_ButOwn_RowStack builds down by rank and suit, # but can move any face-up cards regardless of sequence. class Yukon_BO_RowStack(Yukon_AC_RowStack): def _isYukonSequence(self, c1, c2): return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and c1.suit != c2.suit) def getHelp(self): if self.cap.dir > 0: return _('Tableau. Build up by any suit but the same, ' 'can move any face-up cards regardless of sequence.') elif self.cap.dir < 0: return _('Tableau. Build down by any suit but the same, ' 'can move any face-up cards regardless of sequence.') else: return _('Tableau. Build by same rank, can move any ' 'face-up cards regardless of sequence.') # # King-versions of some of the above stacks: they accepts only Kings or # sequences starting with a King as base_rank cards (i.e. when empty). # class KingAC_RowStack(AC_RowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, base_rank=KING) AC_RowStack.__init__(self, x, y, game, **cap) class KingSS_RowStack(SS_RowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, base_rank=KING) SS_RowStack.__init__(self, x, y, game, **cap) class KingRK_RowStack(RK_RowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, base_rank=KING) RK_RowStack.__init__(self, x, y, game, **cap) # up or down by color class UD_SC_RowStack(SequenceRowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=1, max_accept=1) SequenceRowStack.__init__(self, x, y, game, **cap) def _isSequence(self, cards): return (isSameColorSequence(cards, self.cap.mod, 1) or isSameColorSequence(cards, self.cap.mod, -1)) def getHelp(self): return _('Tableau. Build up or down by color.') # up or down by alternate color class UD_AC_RowStack(SequenceRowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=1, max_accept=1) SequenceRowStack.__init__(self, x, y, game, **cap) def _isSequence(self, cards): return (isAlternateColorSequence(cards, self.cap.mod, 1) or isAlternateColorSequence(cards, self.cap.mod, -1)) def getHelp(self): return _('Tableau. Build up or down by alternate color.') # up or down by suit class UD_SS_RowStack(SequenceRowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=1, max_accept=1) SequenceRowStack.__init__(self, x, y, game, **cap) def _isSequence(self, cards): return (isSameSuitSequence(cards, self.cap.mod, 1) or isSameSuitSequence(cards, self.cap.mod, -1)) def getHelp(self): return _('Tableau. Build up or down by suit.') # up or down by rank ignoring suit class UD_RK_RowStack(SequenceRowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=1, max_accept=1) SequenceRowStack.__init__(self, x, y, game, **cap) def _isSequence(self, cards): return (isRankSequence(cards, self.cap.mod, 1) or isRankSequence(cards, self.cap.mod, -1)) def getHelp(self): return _('Tableau. Build up or down regardless of suit.') # up or down by rank ignoring suit class UD_BO_RowStack(SequenceRowStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=1, max_accept=1) SequenceRowStack.__init__(self, x, y, game, **cap) def _isSequence(self, cards): return (isAnySuitButOwnSequence(cards, self.cap.mod, 1) or isAnySuitButOwnSequence(cards, self.cap.mod, -1)) def getHelp(self): return _('Tableau. Build up or down in any suit but the same.') # To simplify playing we also consider the number of free rows. # See also the "SuperMove" section in the FreeCell FAQ. class SuperMoveStack_StackMethods: def _getMaxMove(self, to_stack_ncards): max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 if self.cap.base_rank != ANY_RANK: return max_move n = getNumberOfFreeStacks(self.game.s.rows) if to_stack_ncards == 0: n -= 1 return max_move << max(n, 0) def _getNumSSSeq(self, cards): # num of same-suit sequences (for SuperMoveSpider_RowStack) if not cards: return 0 n = 1 suit = cards[-1].suit for c in cards[-2::-1]: if c.suit != suit: suit = c.suit n += 1 return n class SuperMoveSS_RowStack(SuperMoveStack_StackMethods, SS_RowStack): def canMoveCards(self, cards): if not SS_RowStack.canMoveCards(self, cards): return False return len(cards) <= self._getMaxMove(1) def acceptsCards(self, from_stack, cards): if not SS_RowStack.acceptsCards(self, from_stack, cards): return False return len(cards) <= self._getMaxMove(len(self.cards)) class SuperMoveAC_RowStack(SuperMoveStack_StackMethods, AC_RowStack): def canMoveCards(self, cards): if not AC_RowStack.canMoveCards(self, cards): return False return len(cards) <= self._getMaxMove(1) def acceptsCards(self, from_stack, cards): if not AC_RowStack.acceptsCards(self, from_stack, cards): return False return len(cards) <= self._getMaxMove(len(self.cards)) class SuperMoveRK_RowStack(SuperMoveStack_StackMethods, RK_RowStack): def canMoveCards(self, cards): if not RK_RowStack.canMoveCards(self, cards): return False return len(cards) <= self._getMaxMove(1) def acceptsCards(self, from_stack, cards): if not RK_RowStack.acceptsCards(self, from_stack, cards): return False return len(cards) <= self._getMaxMove(len(self.cards)) class SuperMoveSC_RowStack(SuperMoveStack_StackMethods, SC_RowStack): def canMoveCards(self, cards): if not SC_RowStack.canMoveCards(self, cards): return False return len(cards) <= self._getMaxMove(1) def acceptsCards(self, from_stack, cards): if not SC_RowStack.acceptsCards(self, from_stack, cards): return False return len(cards) <= self._getMaxMove(len(self.cards)) class SuperMoveBO_RowStack(SuperMoveStack_StackMethods, BO_RowStack): def canMoveCards(self, cards): if not BO_RowStack.canMoveCards(self, cards): return False return len(cards) <= self._getMaxMove(1) def acceptsCards(self, from_stack, cards): if not BO_RowStack.acceptsCards(self, from_stack, cards): return False return len(cards) <= self._getMaxMove(len(self.cards)) # ************************************************************************ # * WasteStack (a helper stack for the Talon, e.g. in Klondike) # ************************************************************************ class WasteStack(OpenStack): def getHelp(self): return _('Waste.') class WasteTalonStack(TalonStack): # A single click moves the top cards to the game's waste and # moves it face up; if we're out of cards, it moves the waste # back to the talon and increases the number of rounds (redeals). def __init__(self, x, y, game, max_rounds, num_deal=1, waste=None, **cap): TalonStack.__init__(self, x, y, game, max_rounds, num_deal, **cap) self.waste = waste def prepareStack(self): TalonStack.prepareStack(self) if self.waste is None: self.waste = self.game.s.waste def canDealCards(self): waste = self.waste if self.cards: num_cards = min(len(self.cards), self.num_deal) return len(waste.cards) + num_cards <= waste.cap.max_cards elif waste.cards and self.round != self.max_rounds: return True return False def dealCards(self, sound=False, shuffle=False): old_state = self.game.enterState(self.game.S_DEAL) num_cards = 0 waste = self.waste if self.cards: if sound and not self.game.demo: self.game.playSample("dealwaste") num_cards = min(len(self.cards), self.num_deal) assert len(waste.cards) + num_cards <= waste.cap.max_cards for i in range(num_cards): if not self.cards[-1].face_up: if 1: self.game.flipAndMoveMove(self, waste) else: self.game.flipMove(self) self.game.moveMove(1, self, waste, frames=4, shadow=0) else: self.game.moveMove(1, self, waste, frames=4, shadow=0) self.fillStack() if TOOLKIT == 'kivy': self.game.top.waitAnimation() elif waste.cards and self.round != self.max_rounds: if sound: self.game.playSample("turnwaste", priority=20) num_cards = len(waste.cards) self.game.turnStackMove(waste, self) if shuffle: # shuffle self.game.shuffleStackMove(self) self.game.nextRoundMove(self) self.game.leaveState(old_state) return num_cards def shuffleAndDealCards(self, sound=False): WasteTalonStack.dealCards(self, sound=sound, shuffle=True) class FaceUpWasteTalonStack(WasteTalonStack): def canFlipCard(self): return len(self.cards) > 0 and not self.cards[-1].face_up def fillStack(self): if self.canFlipCard(): self.game.singleFlipMove(self) self.game.fillStack(self) def dealCards(self, sound=False): retval = WasteTalonStack.dealCards(self, sound=sound) if self.canFlipCard(): self.flipMove() return retval class OpenTalonStack(TalonStack, OpenStack): canMoveCards = OpenStack.canMoveCards canDropCards = OpenStack.canDropCards releaseHandler = OpenStack.releaseHandler def __init__(self, x, y, game, **cap): kwdefault(cap, max_move=1) TalonStack.__init__(self, x, y, game, **cap) def canDealCards(self): return False def canFlipCard(self): return len(self.cards) > 0 and not self.cards[-1].face_up def fillStack(self): if self.canFlipCard(): self.game.singleFlipMove(self) self.game.fillStack(self) def clickHandler(self, event): if self.canDealCards(): return TalonStack.clickHandler(self, event) else: return OpenStack.clickHandler(self, event) # ************************************************************************ # * ReserveStack (free cell) # ************************************************************************ class ReserveStack(OpenStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_accept=1, max_cards=1) OpenStack.__init__(self, x, y, game, **cap) getBottomImage = Stack._getReserveBottomImage def getHelp(self): if self.cap.max_accept == 0: return _('Reserve. No building.') return _('Free cell.') # ************************************************************************ # * InvisibleStack (an internal off-screen stack to hold cards) # ************************************************************************ class InvisibleStack(Stack): def __init__(self, game, **cap): x, y = game.getInvisibleCoords() kwdefault(cap, max_move=0, max_accept=0) Stack.__init__(self, x, y, game, cap=cap) def assertStack(self): Stack.assertStack(self) assert not self.is_visible # no bindings def initBindings(self): pass # no bottom getBottomImage = Stack._getNoneBottomImage # ************************************************************************ # * ArbitraryStack (stack with arbitrary access) # * # * NB: don't support hint and demo for non-top cards # * NB: this stack only for CARD_XOFFSET == 0 # ************************************************************************ class ArbitraryStack(OpenStack): def __init__(self, x, y, game, **cap): kwdefault(cap, max_accept=0) OpenStack.__init__(self, x, y, game, **cap) self.CARD_YOFFSET = game.app.images.CARD_YOFFSET def canMoveCards(self, cards): return True def getDragCards(self, index): return [self.cards[index]] def startDrag(self, event, sound=True): OpenStack.startDrag(self, event, sound=sound) if self.game.app.opt.mouse_type == 'point-n-click': self.cards[self.game.drag.index].tkraise() self.game.drag.shadows[0].tkraise() else: for c in self.cards[self.game.drag.index+1:]: c.moveBy(0, -self.CARD_YOFFSET[0]) def doubleclickHandler(self, event): # flip or drop a card flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event) if self in flipstacks and self.canFlipCard(): self.playFlipMove(animation=True) return -1 # continue this event (start a drag) if self in dropstacks: i = self._findCard(event) if i < 0: return 0 cards = [self.cards[i]] for s in self.game.s.foundations: if s is not self and s.acceptsCards(self, cards): self.game.playSample("autodrop", priority=30) self.playSingleCardMove(i, s, sound=False) return 1 return 0 def moveCardsBackHandler(self, event, drag): i = self.cards.index(drag.cards[0]) for card in self.cards[i:]: self._position(card) card.tkraise() def singleCardMove(self, index, to_stack, frames=-1, shadow=-1): self.game.singleCardMove( self, to_stack, index, frames=frames, shadow=shadow) self.fillStack() def dragMove(self, drag, to_stack, sound=True): self.playSingleCardMove(drag.index, to_stack, frames=0, sound=sound) def playSingleCardMove(self, index, to_stack, frames=-1, shadow=-1, sound=True): if sound: if to_stack in self.game.s.foundations: self.game.playSample("drop", priority=30) else: self.game.playSample("move", priority=10) self.singleCardMove(index, to_stack, frames=frames, shadow=shadow) if not self.game.checkForWin(): # let the player put cards back from the foundations if self not in self.game.s.foundations: self.game.autoPlay() self.game.finishMove() def quickPlayHandler(self, event, from_stacks=None, to_stacks=None): if to_stacks is None: to_stacks = self.game.s.foundations + self.game.sg.dropstacks if not self.cards: return 0 # moves = [] i = self._findCard(event) if i < 0: return 0 pile = [self.cards[i]] for s in to_stacks: if s is not self and s.acceptsCards(self, pile): score = self.game.getQuickPlayScore(1, self, s) moves.append((score, -len(moves), i, s)) # if moves: moves.sort() # from pprint import pprint; pprint(moves) score, len_moves, index, to_stack = moves[-1] if score >= 0: # self.game.playSample("startdrag") self.playSingleCardMove(index, to_stack) return 1 return 0 # ************************************************************************ # * A StackWrapper is a functor (function object) that creates a # * new stack when called, i.e. it wraps the constructor. # * # * "cap" are the capabilites, see class Stack above. # ************************************************************************ # self.cap override any call-time cap class StackWrapper: def __init__(self, stack_class, **cap): assert issubclass(stack_class, Stack) self.stack_class = stack_class self.cap = cap # return a new stack (an instance of the stack class) def __call__(self, x, y, game, **cap): # must preserve self.cap, so create a shallow copy # import pdb # pdb.set_trace() c = self.cap.copy() kwdefault(c, **cap) return self.stack_class(x, y, game, **c) # call-time cap override self.cap class WeakStackWrapper(StackWrapper): def __call__(self, x, y, game, **cap): kwdefault(cap, **self.cap) return self.stack_class(x, y, game, **cap) # self.cap only, call-time cap is completely ignored class FullStackWrapper(StackWrapper): def __call__(self, x, y, game, **cap): return self.stack_class(x, y, game, **self.cap)