1#!/usr/bin/env python 2# -*- mode: python; coding: utf-8; -*- 3# --------------------------------------------------------------------------- 4# 5# Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer 6# Copyright (C) 2003 Mt. Hood Playing Card Co. 7# Copyright (C) 2005-2009 Skomoroh 8# 9# This program is free software: you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation, either version 3 of the License, or 12# (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program. If not, see <http://www.gnu.org/licenses/>. 21# 22# --------------------------------------------------------------------------- 23 24from pysollib.mfxutil import Image, ImageTk, USE_PIL 25from pysollib.mfxutil import Struct, SubclassResponsibility, kwdefault 26from pysollib.mygettext import _ 27from pysollib.pysoltk import ANCHOR_NW, ANCHOR_SE 28from pysollib.pysoltk import CURSOR_DOWN_ARROW, CURSOR_DRAG 29from pysollib.pysoltk import EVENT_HANDLED, EVENT_PROPAGATE 30from pysollib.pysoltk import MfxCanvasGroup, MfxCanvasImage 31from pysollib.pysoltk import MfxCanvasRectangle, MfxCanvasText 32from pysollib.pysoltk import after_cancel, after_idle 33from pysollib.pysoltk import bind, unbind_destroy 34from pysollib.pysoltk import get_text_width 35from pysollib.pysoltk import markImage 36from pysollib.settings import DEBUG 37from pysollib.settings import TOOLKIT 38from pysollib.util import ACE, KING 39from pysollib.util import ANY_RANK, ANY_SUIT, NO_RANK 40 41# ************************************************************************ 42# * Let's start with some test methods for cards. 43# * Empty card-lists return false. 44# ************************************************************************ 45 46 47# check that all cards are face-up 48def cardsFaceUp(cards): 49 if not cards: 50 return False 51 for c in cards: 52 if not c.face_up: 53 return False 54 return True 55 56 57# check that all cards are face-down 58def cardsFaceDown(cards): 59 if not cards: 60 return False 61 for c in cards: 62 if c.face_up: 63 return False 64 return True 65 66 67# check that cards are face-up and build down by rank 68def isRankSequence(cards, mod=8192, dir=-1): 69 if not cardsFaceUp(cards): 70 return False 71 c1 = cards[0] 72 for c2 in cards[1:]: 73 if (c1.rank + dir) % mod != c2.rank: 74 return False 75 c1 = c2 76 return True 77 78 79# check that cards are face-up and build down by alternate color 80def isAlternateColorSequence(cards, mod=8192, dir=-1): 81 if not cardsFaceUp(cards): 82 return False 83 c1 = cards[0] 84 for c2 in cards[1:]: 85 if (c1.rank + dir) % mod != c2.rank or c1.color == c2.color: 86 return False 87 c1 = c2 88 return True 89 90 91# check that cards are face-up and build down by same color 92def isSameColorSequence(cards, mod=8192, dir=-1): 93 if not cardsFaceUp(cards): 94 return False 95 c1 = cards[0] 96 for c2 in cards[1:]: 97 if (c1.rank + dir) % mod != c2.rank or c1.color != c2.color: 98 return False 99 c1 = c2 100 return True 101 102 103# check that cards are face-up and build down by same suit 104def isSameSuitSequence(cards, mod=8192, dir=-1): 105 if not cardsFaceUp(cards): 106 return False 107 c1 = cards[0] 108 for c2 in cards[1:]: 109 if (c1.rank + dir) % mod != c2.rank or c1.suit != c2.suit: 110 return False 111 c1 = c2 112 return True 113 114 115# check that cards are face-up and build down by any suit but own 116def isAnySuitButOwnSequence(cards, mod=8192, dir=-1): 117 if not cardsFaceUp(cards): 118 return False 119 c1 = cards[0] 120 for c2 in cards[1:]: 121 if (c1.rank + dir) % mod != c2.rank or c1.suit == c2.suit: 122 return False 123 c1 = c2 124 return True 125 126 127def getNumberOfFreeStacks(stacks): 128 return len([s for s in stacks if not s.cards]) 129 130 131# collect the top cards of several stacks into a pile 132def getPileFromStacks(stacks, reverse=False): 133 cards = [] 134 for s in stacks: 135 if not s.cards or not s.cards[-1].face_up: 136 return None 137 cards.append(s.cards[-1]) 138 return (reversed(cards) if reverse else cards) 139 140 141class Stack: 142 # A generic stack of cards. 143 # 144 # This is used as a base class for all other stacks (e.g. the talon, 145 # the foundations and the row stacks). 146 # 147 # The default event handlers turn the top card of the stack with 148 # its face up on a (single or double) click, and also support 149 # moving a subpile around. 150 151 # constants 152 MIN_VISIBLE_XOFFSET = 3 153 MIN_VISIBLE_YOFFSET = 3 154 SHRINK_FACTOR = 2. 155 156 def __init__(self, x, y, game, cap={}): 157 # Arguments are the stack's nominal x and y position (the top 158 # left corner of the first card placed in the stack), and the 159 # game object (which is used to get the canvas; subclasses use 160 # the game object to find other stacks). 161 162 # 163 # link back to game 164 # 165 id = len(game.allstacks) 166 game.allstacks.append(self) 167 x = int(round(x)) 168 y = int(round(y)) 169 mapkey = (x, y) 170 # assert not game.stackmap.has_key(mapkey) ## can happen in PyJonngg 171 game.stackmap[mapkey] = id 172 173 # 174 # setup our pseudo MVC scheme 175 # 176 model, view = self, self 177 178 # 179 # model 180 # 181 model.id = id 182 model.game = game 183 model.cards = [] 184 # 185 model.is_filled = False 186 187 # capabilites - the game logic 188 model.cap = Struct( 189 suit=-1, # required suit for this stack (-1 is ANY_SUIT) 190 color=-1, # required color for this stack (-1 is ANY_COLOR) 191 rank=-1, # required rank for this stack (-1 is ANY_RANK) 192 base_suit=-1, # base suit for this stack (-1 is ANY_SUIT) 193 base_color=-1, # base color for this stack (-1 is ANY_COLOR) 194 base_rank=-1, # base rank for this stack (-1 is ANY_RANK) 195 dir=0, # direction - stack builds up/down 196 mod=8192, # modulo for wrap around (typically 13 or 8192) 197 max_move=0, # can move at most # cards at a time 198 max_accept=0, # can accept at most # cards at a time 199 max_cards=999999, # total number of cards may not exceed this 200 # not commonly used: 201 min_move=1, # must move at least # cards at a time 202 min_accept=1, # must accept at least # cards at a time 203 # total number of cards this stack at least requires 204 min_cards=0, 205 ) 206 model.cap.update(cap) 207 assert isinstance(model.cap.suit, int) 208 assert isinstance(model.cap.color, int) 209 assert isinstance(model.cap.rank, int) 210 assert isinstance(model.cap.base_suit, int) 211 assert isinstance(model.cap.base_color, int) 212 assert isinstance(model.cap.base_rank, int) 213 # 214 # view 215 # 216 self.init_coord = (x, y) 217 view.x = x 218 view.y = y 219 view.canvas = game.canvas 220 view.CARD_XOFFSET = 0 221 view.CARD_YOFFSET = 0 222 view.INIT_CARD_OFFSETS = (0, 0) 223 view.INIT_CARD_YOFFSET = 0 # for reallocateCards 224 view.group = MfxCanvasGroup(view.canvas) 225 226 if (TOOLKIT == 'kivy'): 227 if hasattr(view.group, 'stack'): 228 view.group.stack = self 229 230 view.shrink_face_down = 1 231 # image items 232 view.images = Struct( 233 bottom=None, # canvas item 234 redeal=None, # canvas item 235 redeal_img=None, # the corresponding PhotoImage 236 shade_img=None, 237 ) 238 # other canvas items 239 view.items = Struct( 240 bottom=None, # dummy canvas item 241 shade_item=None, 242 ) 243 # text items 244 view.texts = Struct( 245 ncards=None, # canvas item 246 # by default only used by Talon: 247 rounds=None, # canvas item 248 redeal=None, # canvas item 249 redeal_str=None, # the corresponding string 250 # for use by derived stacks: 251 misc=None, # canvas item 252 ) 253 view.top_bottom = None # the highest of all bottom items 254 cardw, cardh = game.app.images.CARDW, game.app.images.CARDH 255 dx, dy = cardw+view.canvas.xmargin, cardh+view.canvas.ymargin 256 view.is_visible = view.x >= -dx and view.y >= -dy 257 view.is_open = -1 258 view.can_hide_cards = -1 259 view.max_shadow_cards = -1 260 view.current_cursor = '' 261 view.cursor_changed = False 262 263 def destruct(self): 264 # help breaking circular references 265 unbind_destroy(self.group) 266 267 def prepareStack(self): 268 self.prepareView() 269 if self.is_visible: 270 self.initBindings() 271 272 def _calcMouseBind(self, binding_format): 273 return self.game.app.opt.calcCustomMouseButtonsBinding(binding_format) 274 275 # bindings {view widgets bind to controller} 276 def initBindings(self): 277 group = self.group 278 bind(group, self._calcMouseBind("<{mouse_button1}>"), 279 self.__clickEventHandler) 280 # bind(group, "<B1-Motion>", self.__motionEventHandler) 281 bind(group, "<Motion>", self.__motionEventHandler) 282 bind(group, self._calcMouseBind("<ButtonRelease-{mouse_button1}>"), 283 self.__releaseEventHandler) 284 bind(group, self._calcMouseBind("<Control-{mouse_button1}>"), 285 self.__controlclickEventHandler) 286 bind(group, self._calcMouseBind("<Shift-{mouse_button1}>"), 287 self.__shiftclickEventHandler) 288 bind(group, self._calcMouseBind("<Double-{mouse_button1}>"), 289 self.__doubleclickEventHandler) 290 bind(group, self._calcMouseBind("<{mouse_button3}>"), 291 self.__rightclickEventHandler) 292 bind(group, self._calcMouseBind("<{mouse_button2}>"), 293 self.__middleclickEventHandler) 294 bind(group, self._calcMouseBind("<Control-{mouse_button3}>"), 295 self.__middleclickEventHandler) 296 # bind(group, self._calcMouseBind( 297 # "<Control-{mouse_button2}>"), self.__controlmiddleclickEventHandler) 298 # bind(group, self._calcMouseBind("<Shift-{mouse_button3}>"), 299 # self.__shiftrightclickEventHandler) 300 # bind(group, self._calcMouseBind("<Double-{mouse_button2}>"), "") 301 bind(group, "<Enter>", self.__enterEventHandler) 302 bind(group, "<Leave>", self.__leaveEventHandler) 303 304 def prepareView(self): 305 # assertView(self) 306 if (self.CARD_XOFFSET == 0 and self.CARD_YOFFSET == 0): 307 assert self.cap.max_move <= 1 308 # prepare some variables 309 ox, oy = self.CARD_XOFFSET, self.CARD_YOFFSET 310 if isinstance(ox, (int, float)): 311 self.CARD_XOFFSET = (ox,) 312 else: 313 self.CARD_XOFFSET = tuple([int(round(x)) for x in ox]) 314 if isinstance(oy, (int, float)): 315 self.CARD_YOFFSET = (oy,) 316 else: 317 self.CARD_YOFFSET = tuple([int(round(y)) for y in oy]) 318 319 # preserve offsets 320 # for resize() 321 self.INIT_CARD_OFFSETS = (self.CARD_XOFFSET, self.CARD_YOFFSET) 322 self.INIT_CARD_YOFFSET = self.CARD_YOFFSET # for reallocateCards 323 324 if self.can_hide_cards < 0: 325 self.can_hide_cards = self.is_visible 326 if self.cap.max_cards < 3: 327 self.can_hide_cards = 0 328 elif [_f for _f in self.CARD_XOFFSET if _f]: 329 self.can_hide_cards = 0 330 elif [_f for _f in self.CARD_YOFFSET if _f]: 331 self.can_hide_cards = 0 332 elif self.canvas.preview: 333 self.can_hide_cards = 0 334 if self.is_open < 0: 335 self.is_open = False 336 if (self.is_visible and 337 (abs(self.CARD_XOFFSET[0]) >= self.MIN_VISIBLE_XOFFSET or 338 abs(self.CARD_YOFFSET[0]) >= self.MIN_VISIBLE_YOFFSET)): 339 self.is_open = True 340 if self.max_shadow_cards < 0: 341 self.max_shadow_cards = 999999 342 # if abs(self.CARD_YOFFSET[0]) 343 # != self.game.app.images.CARD_YOFFSET: 344 # # don't display a shadow if the YOFFSET of the stack 345 # # and the images don't match 346 # self.max_shadow_cards = 1 347 if (self.game.app.opt.shrink_face_down and 348 isinstance(ox, (int, float)) and 349 isinstance(oy, (int, float))): 350 # no shrink if xoffset/yoffset too small 351 f = self.SHRINK_FACTOR 352 if ((ox == 0 and oy >= self.game.app.images.CARD_YOFFSET//f) or 353 (oy == 0 and 354 ox >= self.game.app.images.CARD_XOFFSET//f)): 355 self.shrink_face_down = f 356 # bottom image 357 if self.is_visible: 358 self.prepareBottom() 359 360 # stack bottom image 361 def prepareBottom(self): 362 assert self.is_visible and self.images.bottom is None 363 img = self.getBottomImage() 364 if img is not None: 365 self.images.bottom = MfxCanvasImage(self.canvas, self.x, self.y, 366 image=img, anchor=ANCHOR_NW, 367 group=self.group) 368 self.top_bottom = self.images.bottom 369 370 # invisible stack bottom 371 # We need this if we want to get any events for an empty stack (which 372 # is needed by the quickPlayHandler in some games like Montana) 373 def prepareInvisibleBottom(self): 374 assert self.is_visible and self.items.bottom is None 375 images = self.game.app.images 376 self.items.bottom = MfxCanvasRectangle(self.canvas, self.x, self.y, 377 self.x + images.CARDW, 378 self.y + images.CARDH, 379 fill="", outline="", width=0, 380 group=self.group) 381 self.top_bottom = self.items.bottom 382 383 # sanity checks 384 def assertStack(self): 385 assert self.cap.min_move > 0 386 assert self.cap.min_accept > 0 387 assert not hasattr(self, "suit") 388 389 # 390 # Core access methods {model -> view} 391 # 392 393 # Add a card add the top of a stack. Also update display. {model -> view} 394 def addCard(self, card, unhide=1, update=1): 395 model, view = self, self 396 model.cards.append(card) 397 card.tkraise(unhide=unhide) 398 if view.can_hide_cards and len(model.cards) >= 3: 399 # we only need to display the 2 top cards 400 model.cards[-3].hide(self) 401 card.item.addtag(view.group) 402 view._position(card) 403 if update: 404 view.updateText() 405 self.closeStack() 406 return card 407 408 def insertCard(self, card, position, unhide=1, update=1): 409 model, view = self, self 410 model.cards.insert(position, card) 411 for c in model.cards[position:]: 412 c.tkraise(unhide=unhide) 413 if (view.can_hide_cards and len(model.cards) >= 3 and 414 len(model.cards)-position <= 2): 415 # we only need to display the 2 top cards 416 model.cards[-3].hide(self) 417 card.item.addtag(view.group) 418 for c in model.cards[position:]: 419 view._position(c) 420 if update: 421 view.updateText() 422 self.closeStack() 423 return card 424 425 # Remove a card from the stack. Also update display. {model -> view} 426 def removeCard(self, card=None, unhide=1, update=1, update_positions=0): 427 model, view = self, self 428 assert len(model.cards) > 0 429 if card is None: 430 card = model.cards[-1] 431 # optimized a little bit (compare with the else below) 432 card.item.dtag(view.group) 433 if unhide and self.can_hide_cards: 434 card.unhide() 435 if len(self.cards) >= 3: 436 model.cards[-3].unhide() 437 del model.cards[-1] 438 else: 439 card.item.dtag(view.group) 440 if unhide and view.can_hide_cards: 441 # Note: the 2 top cards ([-1] and [-2]) are already unhidden. 442 card.unhide() 443 if len(model.cards) >= 3: 444 if card is model.cards[-1] or model is self.cards[-2]: 445 # Make sure that 2 top cards will be un-hidden. 446 model.cards[-3].unhide() 447 card_index = model.cards.index(card) 448 model.cards.remove(card) 449 if update_positions: 450 for c in model.cards[card_index:]: 451 view._position(c) 452 453 if update: 454 view.updateText() 455 self.unshadeStack() 456 self.is_filled = False 457 return card 458 459 # Get the top card {model} 460 def getCard(self): 461 if self.cards: 462 return self.cards[-1] 463 return None 464 465 # get the largest moveable pile {model} - uses canMoveCards() 466 def getPile(self): 467 if self.cap.max_move > 0: 468 cards = self.cards[-self.cap.max_move:] 469 while len(cards) >= self.cap.min_move: 470 if self.canMoveCards(cards): 471 return cards 472 del cards[0] 473 return None 474 475 # Position the card on the canvas {view} 476 def _position(self, card): 477 x, y = self.getPositionFor(card) 478 card.moveTo(x, y) 479 480 # find card 481 def _findCard(self, event): 482 model, view = self, self 483 if event is not None and model.cards: 484 # ask the canvas 485 return view.canvas.findCard(self, event) 486 return -1 487 488 # find card 489 def _findCardXY(self, x, y, cards=None): 490 model = self 491 if cards is None: 492 cards = model.cards 493 images = self.game.app.images 494 cw, ch = images.getSize() 495 index = -1 496 for i in range(len(cards)): 497 c = cards[i] 498 r = (c.x, c.y, c.x + cw, c.y + ch) 499 if r[0] <= x < r[2] and r[1] <= y < r[3]: 500 index = i 501 return index 502 503 # generic model update (can be used for undo/redo - see move.py) 504 def updateModel(self, undo, flags): 505 pass 506 507 # copy model data - see Hint.AClonedStack 508 def copyModel(self, clone): 509 clone.id = self.id 510 clone.game = self.game 511 clone.cap = self.cap 512 513 def getRankDir(self, cards=None): 514 if cards is None: 515 cards = self.cards[-2:] 516 if len(cards) < 2: 517 return 0 518 dir = (cards[-1].rank - cards[-2].rank) % self.cap.mod 519 if dir > self.cap.mod // 2: 520 return dir - self.cap.mod 521 return dir 522 523 # 524 # Basic capabilities {model} 525 # Used by various subclasses. 526 # 527 528 def basicIsBlocked(self): 529 # Check if the stack is blocked (e.g. Pyramid or Mahjongg) 530 return False 531 532 def basicAcceptsCards(self, from_stack, cards): 533 # Check that the limits are ok and that the cards are face up 534 if from_stack is self or self.basicIsBlocked(): 535 return False 536 cap = self.cap 537 mylen = len(cards) 538 if mylen < cap.min_accept or mylen > cap.max_accept: 539 return False 540 mylen += len(self.cards) 541 # note: we don't check cap.min_cards here 542 if mylen > cap.max_cards: 543 return False 544 545 def _check(c, suit, color, rank): 546 return ((suit >= 0 and c.suit != suit) or 547 (color >= 0 and c.color != color) or 548 (rank >= 0 and c.rank != rank)) 549 for c in cards: 550 if not c.face_up or _check(c, cap.suit, cap.color, cap.rank): 551 return False 552 if self.cards: 553 # top card of our stack must be face up 554 return self.cards[-1].face_up 555 # check required base 556 return not _check(cards[0], cap.base_suit, cap.base_color, 557 cap.base_rank) 558 559 def basicCanMoveCards(self, cards): 560 # Check that the limits are ok and the cards are face up 561 if self.basicIsBlocked(): 562 return False 563 cap = self.cap 564 mylen = len(cards) 565 if mylen < cap.min_move or mylen > cap.max_move: 566 return False 567 mylen = len(self.cards) - mylen 568 # note: we don't check cap.max_cards here 569 if mylen < cap.min_cards: 570 return False 571 return cardsFaceUp(cards) 572 573 # 574 # Capabilities - important for game logic {model} 575 # 576 577 def acceptsCards(self, from_stack, cards): 578 # Do we accept receiving `cards' from `from_stack' ? 579 return False 580 581 def canMoveCards(self, cards): 582 # Can we move these cards when assuming they are our top-cards ? 583 return False 584 585 def canFlipCard(self): 586 # Can we flip our top card ? 587 return False 588 589 def canDropCards(self, stacks): 590 # Can we drop the top cards onto one of the foundation stacks ? 591 return (None, 0) # return the stack and the number of cards 592 593 # 594 # State {model} 595 # 596 597 def resetGame(self): 598 # Called when starting a new game. 599 self.CARD_YOFFSET = self.INIT_CARD_YOFFSET 600 self.items.shade_item = None 601 self.images.shade_img = None 602 # self.items.bottom = None 603 # self.images.bottom = None 604 605 def __repr__(self): 606 # Return a string for debug print statements. 607 return "%s(%d)" % (self.__class__.__name__, self.id) 608 609 # 610 # Atomic move actions {model -> view} 611 # 612 613 def flipMove(self, animation=False): 614 # Flip the top card. 615 if animation: 616 self.game.singleFlipMove(self) 617 else: 618 self.game.flipMove(self) 619 620 def moveMove(self, ncards, to_stack, frames=-1, shadow=-1): 621 # Move the top n cards. 622 self.game.moveMove( 623 ncards, self, to_stack, frames=frames, shadow=shadow) 624 self.fillStack() 625 626 def fillStack(self): 627 self.game.fillStack(self) 628 629 def closeStack(self): 630 pass 631 632 # 633 # Playing move actions. Better not override. 634 # 635 636 def playFlipMove(self, sound=True, animation=False): 637 if sound: 638 self.game.playSample("flip", 5) 639 self.flipMove(animation=animation) 640 if not self.game.checkForWin(): 641 self.game.autoPlay() 642 self.game.finishMove() 643 644 def playMoveMove(self, ncards, to_stack, frames=-1, shadow=-1, sound=True): 645 if sound: 646 if to_stack in self.game.s.foundations: 647 self.game.playSample("drop", priority=30) 648 else: 649 self.game.playSample("move", priority=10) 650 self.moveMove(ncards, to_stack, frames=frames, shadow=shadow) 651 if not self.game.checkForWin(): 652 # let the player put cards back from the foundations 653 if self not in self.game.s.foundations: 654 self.game.autoPlay() 655 self.game.finishMove() 656 657 # 658 # Appearance {view} 659 # 660 661 def _getBlankBottomImage(self): 662 return self.game.app.images.getBlankBottom() 663 664 def _getReserveBottomImage(self): 665 return self.game.app.images.getReserveBottom() 666 667 def _getSuitBottomImage(self): 668 return self.game.app.images.getSuitBottom(self.cap.base_suit) 669 670 def _getNoneBottomImage(self): 671 return None 672 673 def _getTalonBottomImage(self): 674 return self.game.app.images.getTalonBottom() 675 676 def _getBraidBottomImage(self): 677 return self.game.app.images.getBraidBottom() 678 679 def _getLetterImage(self): 680 return self.game.app.images.getLetter(self.cap.base_rank) 681 682 getBottomImage = _getBlankBottomImage 683 684 def getPositionFor(self, card): 685 model, view = self, self 686 x, y = view.x, view.y 687 if view.can_hide_cards: 688 return x, y 689 ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) 690 d = self.shrink_face_down 691 for c in model.cards: 692 if c is card: 693 break 694 if c.face_up: 695 x += self.CARD_XOFFSET[ix] 696 y += self.CARD_YOFFSET[iy] 697 else: 698 x += self.CARD_XOFFSET[ix]//d 699 y += self.CARD_YOFFSET[iy]//d 700 ix = (ix + 1) % lx 701 iy = (iy + 1) % ly 702 return int(x), int(y) 703 704 def getPositionForNextCard(self): 705 model, view = self, self 706 x, y = view.x, view.y 707 if view.can_hide_cards: 708 return x, y 709 if not self.cards: 710 return x, y 711 ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) 712 d = self.shrink_face_down 713 for c in model.cards: 714 if c.face_up: 715 x += self.CARD_XOFFSET[ix] 716 y += self.CARD_YOFFSET[iy] 717 else: 718 x += self.CARD_XOFFSET[ix]//d 719 y += self.CARD_YOFFSET[iy]//d 720 ix = (ix + 1) % lx 721 iy = (iy + 1) % ly 722 return int(x), int(y) 723 724 def getOffsetFor(self, card): 725 model, view = self, self 726 if view.can_hide_cards: 727 return 0, 0 728 lx, ly = len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) 729 i = list(model.cards).index(card) 730 return view.CARD_XOFFSET[i % lx], view.CARD_YOFFSET[i % ly] 731 732 # Fully update the view of a stack - updates 733 # hiding, card positions and stacking order. 734 # Avoid calling this as it is rather slow. 735 def refreshView(self): 736 model, view = self, self 737 cards = model.cards 738 if not view.is_visible or len(cards) < 2: 739 return 740 if view.can_hide_cards: 741 # hide all lower cards 742 for c in cards[:-2]: 743 # print "refresh hide", c, c.hide_stack 744 c.hide(self) 745 # unhide the 2 top cards 746 for c in cards[-2:]: 747 # print "refresh unhide 1", c, c.hide_stack 748 c.unhide() 749 # print "refresh unhide 1", c, c.hide_stack 750 # update the card postions and stacking order 751 item = cards[0].item 752 x, y = view.x, view.y 753 ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET) 754 for c in cards[1:]: 755 c.item.tkraise(item) 756 item = c.item 757 if not view.can_hide_cards: 758 d = self.shrink_face_down 759 if c.face_up: 760 x += self.CARD_XOFFSET[ix] 761 y += self.CARD_YOFFSET[iy] 762 else: 763 x += int(self.CARD_XOFFSET[ix]/d) 764 y += int(self.CARD_YOFFSET[iy]/d) 765 ix = (ix + 1) % lx 766 iy = (iy + 1) % ly 767 c.moveTo(x, y) 768 769 def updateText(self): 770 if self.game.preview > 1 or self.texts.ncards is None: 771 return 772 t = "" 773 format = "%d" 774 if self.texts.ncards.text_format is not None: 775 format = self.texts.ncards.text_format 776 if format == "%D": 777 format = "" 778 if self.cards: 779 format = "%d" 780 if format: 781 t = format % len(self.cards) 782 # if 0: 783 # visible = 0 784 # for c in self.cards: 785 # if c.isHidden(): 786 # assert c.hide_stack is not None 787 # else: 788 # visible = visible + 1 789 # assert c.hide_stack is None 790 # t = t + " (%d)" % visible 791 self.texts.ncards.config(text=t) 792 793 def updatePositions(self): 794 # compact the stack when a cards goes off screen 795 if self.reallocateCards(): 796 for c in self.cards: 797 self._position(c) 798 799 def reallocateCards(self): 800 # change CARD_YOFFSET if a cards is off-screen 801 # returned False if CARD_YOFFSET is not changed, otherwise True 802 if not self.game.app.opt.compact_stacks: 803 return False 804 if TOOLKIT != 'tk': 805 return False 806 if self.CARD_XOFFSET != (0,): 807 return False 808 if len(self.CARD_YOFFSET) != 1: 809 return False 810 if self.CARD_YOFFSET[0] <= 0: 811 return False 812 if len(self.cards) <= 1: 813 return False 814 if not self.canvas.winfo_ismapped(): 815 return False 816 yoffset = self.CARD_YOFFSET[0] 817 # 1/2 of a card is visible 818 cardh = self.game.app.images.getSize()[0] // 2 819 num_face_up = len([c for c in self.cards if c.face_up]) 820 num_face_down = len(self.cards) - num_face_up 821 stack_height = int(self.y + 822 num_face_down * yoffset // self.shrink_face_down + 823 num_face_up * yoffset + 824 cardh) 825 visible_height = self.canvas.winfo_height() 826 if USE_PIL and self.game.app.opt.auto_scale: 827 # use visible_height only 828 game_height = 0 829 else: 830 game_height = self.game.height + 2*self.canvas.ymargin 831 height = max(visible_height, game_height) 832 # print 'reallocateCards:', stack_height, height, \ 833 # visible_height, game_height 834 if stack_height > height: 835 # compact stack 836 n = num_face_down // self.shrink_face_down + num_face_up 837 dy = float(height - self.y - cardh) / n 838 if dy < yoffset: 839 # print 'compact:', dy 840 self.CARD_YOFFSET = (dy,) 841 return True 842 elif stack_height < height: 843 # expande stack 844 if self.CARD_YOFFSET == self.INIT_CARD_YOFFSET: 845 return False 846 n = num_face_down // self.shrink_face_down + num_face_up 847 dy = float(height - self.y - cardh) / n 848 dy = min(dy, self.INIT_CARD_YOFFSET[0]) 849 # print 'expande:', dy 850 self.CARD_YOFFSET = (dy,) 851 return True 852 return False 853 854 def resize(self, xf, yf, widthpad=0, heightpad=0): 855 # resize and move stack 856 # xf, yf - a multiplicative factor (from the original values) 857 # print 'Stack.resize:', self, self.is_visible, xf, yf 858 x0, y0 = self.init_coord 859 if (x0 > 0): 860 x0 += widthpad 861 if (y0 > 0): 862 y0 += heightpad 863 x, y = int(round(x0*xf)), int(round(y0*yf)) 864 self.x, self.y = x, y 865 # offsets 866 xoffset = tuple(int(round(i*xf)) for i in self.INIT_CARD_OFFSETS[0]) 867 yoffset = tuple(int(round(i*yf)) for i in self.INIT_CARD_OFFSETS[1]) 868 self.CARD_XOFFSET = xoffset 869 self.CARD_YOFFSET = yoffset 870 self.INIT_CARD_YOFFSET = yoffset 871 # print '* resize offset:', self.INIT_CARD_XOFFSET, 872 # move cards 873 for c in self.cards: 874 cx, cy = self.getPositionFor(c) 875 c.moveTo(cx, cy) 876 # --- 877 if not self.is_visible: 878 return 879 # bottom and shade 880 if self.images.bottom: 881 img = self.getBottomImage() 882 self.images.bottom['image'] = img 883 self.images.bottom.moveTo(x, y) 884 if self.items.bottom: 885 c = self.items.bottom.coords() 886 c = ((int(round(c[0]*xf)), int(round(c[1]*yf))), 887 (int(round(c[2]*xf)), int(round(c[3]*yf)))) 888 self.items.bottom.coords(c) 889 if self.items.shade_item: 890 c = self.cards[-1] 891 img = self.game.app.images.getHighlightedCard( 892 c.deck, c.suit, c.rank) 893 if img: 894 self.items.shade_item['image'] = img 895 self.items.shade_item.moveTo(x, y) 896 897 # move the items 898 def move(item): 899 ix, iy = item.init_coord 900 x = int(round((ix + widthpad) * xf)) 901 y = int(round((iy + heightpad) * yf)) 902 item.moveTo(x, y) 903 # images 904 if self.images.redeal: 905 move(self.images.redeal) 906 # texts 907 if self.texts.ncards: 908 move(self.texts.ncards) 909 if self.texts.rounds: 910 move(self.texts.rounds) 911 if self.texts.redeal: 912 move(self.texts.redeal) 913 if self.texts.misc: 914 move(self.texts.misc) 915 916 def basicShallHighlightSameRank(self, card): 917 # by default all open stacks are available for highlighting 918 assert card in self.cards 919 if not self.is_visible or not card.face_up: 920 return False 921 if card is self.cards[-1]: 922 return True 923 if not self.is_open: 924 return False 925 # dx, dy = self.getOffsetFor(card) 926 # if ((dx == 0 and dy <= self.MIN_VISIBLE_XOFFSET) or 927 # (dx <= self.MIN_VISIBLE_YOFFSET and dy == 0)): 928 # return False 929 return True 930 931 def basicShallHighlightMatch(self, card): 932 # by default all open stacks are available for highlighting 933 return self.basicShallHighlightSameRank(card) 934 935 def highlightSameRank(self, event): 936 i = self._findCard(event) 937 if i < 0: 938 return 0 939 card = self.cards[i] 940 if not self.basicShallHighlightSameRank(card): 941 return 0 942 col_1 = self.game.app.opt.colors['samerank_1'] 943 col_2 = self.game.app.opt.colors['samerank_2'] 944 info = [(self, card, card, col_1)] 945 for s in self.game.allstacks: 946 for c in s.cards: 947 if c is card: 948 continue 949 # check the rank 950 if c.rank != card.rank: 951 continue 952 # ask the target stack 953 if s.basicShallHighlightSameRank(c): 954 info.append((s, c, c, col_2)) 955 self.game.stats.highlight_samerank += 1 956 return self.game._highlightCards( 957 info, self.game.app.opt.timeouts['highlight_samerank']) 958 959 def highlightMatchingCards(self, event): 960 i = self._findCard(event) 961 if i < 0: 962 return 0 963 card = self.cards[i] 964 if not self.basicShallHighlightMatch(card): 965 return 0 966 col_1 = self.game.app.opt.colors['cards_1'] 967 col_2 = self.game.app.opt.colors['cards_2'] 968 c1 = c2 = card 969 info = [] 970 found = 0 971 for s in self.game.allstacks: 972 # continue if both stacks are foundations 973 if (self in self.game.s.foundations and 974 s in self.game.s.foundations): 975 continue 976 # for all cards 977 for c in s.cards: 978 if c is card: 979 continue 980 # ask the target stack 981 if not s.basicShallHighlightMatch(c): 982 continue 983 # ask the game 984 if self.game.shallHighlightMatch(self, card, s, c): 985 found = 1 986 if s is self: 987 # enlarge rectangle for neighbours 988 j = self.cards.index(c) 989 if i - 1 == j: 990 c1 = c 991 continue 992 if i + 1 == j: 993 c2 = c 994 continue 995 info.append((s, c, c, col_1)) 996 if found: 997 if info: 998 self.game.stats.highlight_cards += 1 999 info.append((self, c1, c2, col_2)) 1000 return self.game._highlightCards( 1001 info, self.game.app.opt.timeouts['highlight_cards']) 1002 if not self.basicIsBlocked(): 1003 self.game.highlightNotMatching() 1004 return 0 1005 1006 # 1007 # Subclass overridable handlers {contoller -> model -> view} 1008 # 1009 1010 def clickHandler(self, event): 1011 return 0 1012 1013 def middleclickHandler(self, event): 1014 # default action: show the card if it is overlapped by other cards 1015 if not self.is_open: 1016 return 0 1017 i = self._findCard(event) 1018 positions = len(self.cards) - i - 1 1019 if i < 0 or positions <= 0 or not self.cards[i].face_up: 1020 return 0 1021 # print self.cards[i] 1022 self.cards[i].item.tkraise() 1023 self.canvas.update_idletasks() 1024 self.game.sleep(self.game.app.opt.timeouts['raise_card']) 1025 if TOOLKIT == 'tk': 1026 self.cards[i].item.lower(self.cards[i+1].item) 1027 elif TOOLKIT == 'gtk': 1028 for c in self.cards[i+1:]: 1029 c.tkraise() 1030 self.canvas.update_idletasks() 1031 return 1 1032 1033 def controlmiddleclickHandler(self, event): 1034 # cheating: show face-down card 1035 if not self.is_open: 1036 return 0 1037 i = self._findCard(event) 1038 positions = len(self.cards) - i - 1 1039 if i < 0 or positions < 0: 1040 return 0 1041 # print self.cards[i] 1042 face_up = self.cards[i].face_up 1043 if not face_up: 1044 self.cards[i].showFace() 1045 self.cards[i].item.tkraise() 1046 self.canvas.update_idletasks() 1047 self.game.sleep(self.game.app.opt.timeouts['raise_card']) 1048 if not face_up: 1049 self.cards[i].showBack() 1050 if TOOLKIT == 'tk': 1051 if positions > 0: 1052 self.cards[i].item.lower(self.cards[i+1].item) 1053 elif TOOLKIT == 'gtk': 1054 for c in self.cards[i+1:]: 1055 c.tkraise() 1056 self.canvas.update_idletasks() 1057 return 1 1058 1059 def rightclickHandler(self, event): 1060 return 0 1061 1062 def doubleclickHandler(self, event): 1063 return self.clickHandler(event) 1064 1065 def controlclickHandler(self, event): 1066 return 0 1067 1068 def shiftclickHandler(self, event): 1069 # default action: highlight all cards of the same rank 1070 if self.game.app.opt.highlight_samerank: 1071 return self.highlightSameRank(event) 1072 return 0 1073 1074 def shiftrightclickHandler(self, event): 1075 return 0 1076 1077 def releaseHandler(self, event, drag, sound=True): 1078 # default action: move cards back to their origin position 1079 if drag.cards: 1080 if sound: 1081 self.game.playSample("nomove") 1082 if self.game.app.opt.mouse_type == 'point-n-click': 1083 drag.stack.moveCardsBackHandler(event, drag) 1084 else: 1085 self.moveCardsBackHandler(event, drag) 1086 1087 def moveCardsBackHandler(self, event, drag): 1088 if self.game.app.opt.animations: 1089 if drag.cards: 1090 c = drag.cards[0] 1091 x0, y0 = drag.stack.getPositionFor(c) 1092 x1, y1 = c.x, c.y 1093 dx, dy = abs(x0-x1), abs(y0-y1) 1094 w, h = self.game.app.images.getSize() 1095 if dx > 2*w or dy > 2*h: 1096 self.game.animatedMoveTo(drag.stack, drag.stack, 1097 drag.cards, x0, y0, frames=-1) 1098 elif dx > w or dy > h: 1099 self.game.animatedMoveTo(drag.stack, drag.stack, 1100 drag.cards, x0, y0, frames=4) 1101 for card in drag.cards: 1102 self._position(card) 1103 if self.is_filled and self.items.shade_item: 1104 self.items.shade_item.show() 1105 self.items.shade_item.tkraise() 1106 1107 # 1108 # Event handlers {controller} 1109 # 1110 1111 def __defaultClickEventHandler(self, event, handler, 1112 start_drag=0, cancel_drag=1): 1113 self.game.event_handled = True # for Game.undoHandler 1114 if self.game.demo: 1115 self.game.stopDemo(event) 1116 return EVENT_HANDLED 1117 self.game.interruptSleep() 1118 if self.game.busy: 1119 return EVENT_HANDLED 1120 if self.game.drag.stack and cancel_drag: 1121 # in case we lost an event 1122 self.game.drag.stack.cancelDrag(event) 1123 if start_drag: 1124 # this handler may start a drag operation 1125 r = handler(event) 1126 if r <= 0: 1127 sound = r == 0 1128 self.startDrag(event, sound=sound) 1129 else: 1130 handler(event) 1131 return EVENT_HANDLED 1132 1133 if (TOOLKIT == 'kivy'): 1134 def _motionEventHandler(self, event): 1135 return self.__motionEventHandler(event) 1136 1137 def __clickEventHandler(self, event): 1138 if self.game.app.opt.mouse_type == 'drag-n-drop': 1139 cancel_drag = 1 1140 start_drag = 1 1141 handler = self.clickHandler 1142 else: # sticky-mouse or point-n-click 1143 cancel_drag = 0 1144 start_drag = not self.game.drag.stack 1145 if start_drag: 1146 handler = self.clickHandler 1147 else: 1148 handler = self.finishDrag 1149 return self.__defaultClickEventHandler( 1150 event, handler, start_drag, cancel_drag) 1151 1152 def __doubleclickEventHandler(self, event): 1153 return self.__defaultClickEventHandler(event, self.doubleclickHandler) 1154 1155 def __middleclickEventHandler(self, event): 1156 return self.__defaultClickEventHandler(event, self.middleclickHandler) 1157 1158 def __controlmiddleclickEventHandler(self, event): 1159 return self.__defaultClickEventHandler( 1160 event, self.controlmiddleclickHandler) 1161 1162 def __rightclickEventHandler(self, event): 1163 return self.__defaultClickEventHandler(event, self.rightclickHandler) 1164 1165 def __controlclickEventHandler(self, event): 1166 return self.__defaultClickEventHandler(event, self.controlclickHandler) 1167 1168 def __shiftclickEventHandler(self, event): 1169 return self.__defaultClickEventHandler(event, self.shiftclickHandler) 1170 1171 def __shiftrightclickEventHandler(self, event): 1172 return self.__defaultClickEventHandler( 1173 event, self.shiftrightclickHandler) 1174 1175 def __motionEventHandler(self, event): 1176 if not self.game.drag.stack or self is not self.game.drag.stack: 1177 return EVENT_PROPAGATE 1178 if self.game.demo: 1179 self.game.stopDemo(event) 1180 if self.game.busy: 1181 return EVENT_HANDLED 1182 if self.game.app.opt.mouse_type == 'point-n-click': 1183 return EVENT_HANDLED 1184 self.keepDrag(event) 1185 # if self.game.app.opt.mouse_type == 'drag-n-drop' \ 1186 # and TOOLKIT == 'tk': 1187 # # use a timer to update the drag 1188 # # this allows us to skip redraws on slow machines 1189 # drag = self.game.drag 1190 # if drag.timer is None: 1191 # drag.timer = after_idle(self.canvas, self.keepDragTimer) 1192 # drag.event = event 1193 # else: 1194 # # update now 1195 # self.keepDrag(event) 1196 return EVENT_HANDLED 1197 1198 def __releaseEventHandler(self, event): 1199 if self.game.demo: 1200 self.game.stopDemo(event) 1201 self.game.interruptSleep() 1202 if self.game.busy: 1203 return EVENT_HANDLED 1204 if self.game.app.opt.mouse_type == 'drag-n-drop': 1205 1206 if TOOLKIT == 'kivy': 1207 drag = self.game.drag 1208 if drag and drag.stack: 1209 drag.stack.keepDrag(event) 1210 drag.stack.finishDrag(event) 1211 return EVENT_HANDLED 1212 1213 self.keepDrag(event) 1214 self.finishDrag(event) 1215 return EVENT_HANDLED 1216 1217 def __enterEventHandler(self, event): 1218 if self.game.drag.stack: 1219 if self.game.app.opt.mouse_type == 'point-n-click': 1220 if self.acceptsCards(self.game.drag.stack, 1221 self.game.drag.cards): 1222 self.canvas.config(cursor=CURSOR_DOWN_ARROW) 1223 self.current_cursor = CURSOR_DOWN_ARROW 1224 self.cursor_changed = True 1225 else: 1226 help = self.getHelp() # +' '+self.getBaseCard(), 1227 if DEBUG: 1228 help = repr(self) 1229 after_idle(self.canvas, self.game.showHelp, 1230 'help', help, 1231 'info', self.getNumCards()) 1232 return EVENT_HANDLED 1233 1234 def __leaveEventHandler(self, event): 1235 if not self.game.drag.stack: 1236 after_idle(self.canvas, self.game.showHelp) 1237 if self.game.app.opt.mouse_type == 'drag-n-drop': 1238 return EVENT_HANDLED 1239 if self.cursor_changed: 1240 self.canvas.config(cursor='') 1241 self.current_cursor = '' 1242 self.cursor_changed = False 1243 drag_stack = self.game.drag.stack 1244 if self is drag_stack: 1245 x, y = event.x, event.y 1246 w, h = self.canvas.winfo_width(), self.canvas.winfo_height() 1247 if x < 0 or y < 0 or x >= w or y >= h: 1248 # cancel drag if mouse leave canvas 1249 drag_stack.cancelDrag(event) 1250 after_idle(self.canvas, self.game.showHelp) 1251 return EVENT_HANDLED 1252 else: 1253 # continue drag 1254 return self.__motionEventHandler(event) 1255 else: 1256 return EVENT_PROPAGATE 1257 1258 # 1259 # Drag internals {controller -> model -> view} 1260 # 1261 1262 def getDragCards(self, index): 1263 return self.cards[index:] 1264 1265 # begin a drag operation 1266 def startDrag(self, event, sound=True): 1267 # print event.x, event.y 1268 assert self.game.drag.stack is None 1269 # import pdb 1270 # pdb.set_trace() 1271 i = self._findCard(event) 1272 if i < 0 or not self.canMoveCards(self.cards[i:]): 1273 return 1274 if self.is_filled and self.items.shade_item: 1275 self.items.shade_item.hide() 1276 x_offset, y_offset = self.cards[i].x, self.cards[i].y 1277 if sound: 1278 self.game.playSample("startdrag") 1279 self.lastx = event.x 1280 self.lasty = event.y 1281 game = self.game 1282 drag = game.drag 1283 drag.start_x = event.x 1284 drag.start_y = event.y 1285 drag.stack = self 1286 drag.noshade_stacks = [self] 1287 drag.cards = self.getDragCards(i) 1288 drag.index = i 1289 if self.game.app.opt.mouse_type == 'point-n-click': 1290 self._markCards(drag) 1291 return 1292 # if TOOLKIT == 'gtk': 1293 # drag.stack.group.tkraise() 1294 images = game.app.images 1295 drag.shadows = self.createShadows(drag.cards) 1296 # sx, sy = 0, 0 1297 sx, sy = -images.SHADOW_XOFFSET, -images.SHADOW_YOFFSET 1298 dx, dy = 0, 0 1299 cw, ch = images.getSize() 1300 if game.app.opt.mouse_type == 'sticky-mouse': 1301 # return cards under mouse 1302 dx = event.x - (x_offset+cw+sx) - game.canvas.xmargin 1303 dy = event.y - (y_offset+ch+sy) - game.canvas.ymargin 1304 if dx < 0: 1305 dx = 0 1306 if dy < 0: 1307 dy = 0 1308 for s in drag.shadows: 1309 if dx > 0 or dy > 0: 1310 s.move(dx, dy) 1311 if TOOLKIT == 'gtk': 1312 s.addtag(drag.stack.group) 1313 s.tkraise() 1314 for card in drag.cards: 1315 card.tkraise() 1316 card.moveBy(sx+dx, sy+dy) 1317 if game.app.opt.dragcursor: 1318 game.canvas.config(cursor=CURSOR_DRAG) 1319 1320 # continue a drag operation 1321 def keepDrag(self, event): 1322 drag = self.game.drag 1323 if not drag.cards: 1324 return 1325 assert self is drag.stack 1326 dx = event.x - self.lastx 1327 dy = event.y - self.lasty 1328 if dx or dy: 1329 self.lastx = event.x 1330 self.lasty = event.y 1331 if self.game.app.opt.shade: 1332 self._updateShade() 1333 for s in drag.shadows: 1334 s.move(dx, dy) 1335 for card in drag.cards: 1336 card.moveBy(dx, dy) 1337 drag.event = None 1338 1339 def keepDragTimer(self): 1340 drag = self.game.drag 1341 after_cancel(drag.timer) 1342 drag.timer = None 1343 if drag.event: 1344 self.keepDrag(drag.event) 1345 self.canvas.update_idletasks() 1346 1347 # create shadows, return a tuple of MfxCanvasImages 1348 def createShadows(self, cards, dx=0, dy=0): 1349 if not self.game.app.opt.shadow or self.canvas.preview > 1: 1350 return () 1351 mylen = len(cards) 1352 if mylen == 0 or mylen > self.max_shadow_cards: 1353 return () 1354 images = self.game.app.images 1355 cx, cy = cards[0].x, cards[0].y 1356 ddx, ddy = cx-cards[-1].x, cy-cards[-1].y 1357 cw, ch = images.getSize() 1358 if USE_PIL: 1359 c0 = cards[-1] 1360 if self.CARD_XOFFSET[0] < 0: 1361 c0 = cards[0] 1362 if self.CARD_YOFFSET[0] < 0: 1363 c0 = cards[0] 1364 img = images.getShadowPIL(self, cards) 1365 cx, cy = c0.x + cw + dx, c0.y + ch + dy 1366 s = MfxCanvasImage(self.canvas, cx, cy, 1367 image=img, anchor=ANCHOR_SE) 1368 s.lower(c0.item) 1369 return (s,) 1370 1371 if ddx == 0: # vertical 1372 for c in cards[1:]: 1373 if c.x != cx or abs(c.y - cy) != images.CARD_YOFFSET: 1374 return () 1375 cy = c.y 1376 img0, img1 = images.getShadow(0), images.getShadow(mylen) 1377 c0 = cards[-1] 1378 if self.CARD_YOFFSET[0] < 0: 1379 c0 = cards[0] 1380 elif ddy == 0: # horizontal 1381 for c in cards[1:]: 1382 if c.y != cy or abs(c.x - cx) != images.CARD_XOFFSET: 1383 return () 1384 cx = c.x 1385 img0, img1 = images.getShadow(-mylen), images.getShadow(1) 1386 c0 = cards[-1] 1387 if self.CARD_XOFFSET[0] < 0: 1388 c0 = cards[0] 1389 else: 1390 return () 1391 if img0 and img1: 1392 cx, cy = c0.x + cw + dx, c0.y + ch + dy 1393 1394 if TOOLKIT == 'kivy': 1395 height0 = img0.getHeight() 1396 else: 1397 height0 = img0.height() 1398 1399 s1 = MfxCanvasImage(self.game.canvas, cx, cy - height0, 1400 image=img1, anchor=ANCHOR_SE) 1401 s2 = MfxCanvasImage(self.canvas, cx, cy, 1402 image=img0, anchor=ANCHOR_SE) 1403 if TOOLKIT == 'tk': 1404 s1.lower(c0.item) 1405 s2.lower(c0.item) 1406 # elif TOOLKIT == 'gtk': 1407 # positions = 2 ## FIXME 1408 # s1.lower(positions) 1409 # s2.lower(positions) 1410 return (s1, s2) 1411 return () 1412 1413 # handle shade within a drag operation 1414 def _deleteShade(self): 1415 if self.game.drag.shade_img: 1416 self.game.drag.shade_img.delete() 1417 self.game.drag.shade_img = None 1418 self.game.drag.shade_stack = None 1419 1420 def _updateShade(self): 1421 # optimized for speed - we use lots of local variables 1422 game = self.game 1423 images = game.app.images 1424 CW, CH = images.CARDW, images.CARDH 1425 drag = game.drag 1426 # stacks = game.allstacks 1427 c = drag.cards[0] 1428 stacks = (game.getClosestStack(c, drag.stack), ) 1429 r1_0, r1_1, r1_2, r1_3 = c.x, c.y, c.x + CW, c.y + CH 1430 sstack, sdiff, sx, sy = None, 999999999, 0, 0 1431 for s in stacks: 1432 if s is None or s in drag.noshade_stacks: 1433 continue 1434 if s.cards: 1435 c = s.cards[-1] 1436 r2 = (c.x, c.y, c.x + CW, c.y + CH) 1437 else: 1438 r2 = (s.x, s.y, s.x + CW, s.y + CH) 1439 if (r1_2 <= r2[0] or r1_3 <= r2[1] or 1440 r2[2] <= r1_0 or r2[3] <= r1_1): 1441 # rectangles do not intersect 1442 continue 1443 if s in drag.canshade_stacks: 1444 pass 1445 elif s.acceptsCards(drag.stack, drag.cards): 1446 drag.canshade_stacks.append(s) 1447 else: 1448 drag.noshade_stacks.append(s) 1449 continue 1450 diff = (r1_0 - r2[0])**2 + (r1_1 - r2[1])**2 1451 if diff < sdiff: 1452 sstack, sdiff, sx, sy = s, diff, r2[0], r2[1] 1453 if sstack is drag.shade_stack: 1454 return 1455 if sstack is None: 1456 self._deleteShade() 1457 return 1458 if drag.shade_img: 1459 self._deleteShade() 1460 # create the shade image 1461 drag.shade_stack = sstack 1462 if sstack.cards: 1463 card = sstack.cards[-1] 1464 if card.face_up: 1465 img = images.getHighlightedCard( 1466 card.deck, card.suit, card.rank) 1467 else: 1468 img = images.getHighlightedBack() 1469 else: 1470 img = images.getShade() 1471 if not img: 1472 return 1473 img = MfxCanvasImage(game.canvas, sx, sy, image=img, anchor=ANCHOR_NW) 1474 drag.shade_img = img 1475 # raise/lower the shade image to the correct stacking order 1476 if TOOLKIT == 'tk': 1477 if drag.shadows: 1478 img.lower(drag.shadows[0]) 1479 else: 1480 img.lower(drag.cards[0].item) 1481 elif TOOLKIT == 'gtk': 1482 img.tkraise() 1483 drag.stack.group.tkraise() 1484 1485 # for closeStack 1486 def _shadeStack(self): 1487 if not self.game.app.opt.shade_filled_stacks: 1488 return 1489 # if (self.CARD_XOFFSET != (0,) or 1490 # self.CARD_YOFFSET != (0,)): 1491 # return 1492 card = self.cards[-1] 1493 img = self.game.app.images.getHighlightedCard( 1494 card.deck, card.suit, card.rank) 1495 if img is None: 1496 return 1497 # self.canvas.update_idletasks() 1498 if TOOLKIT == 'kivy': 1499 self.game.top.waitAnimation() 1500 item = MfxCanvasImage(self.canvas, card.x, card.y, 1501 image=img, anchor=ANCHOR_NW, group=self.group) 1502 # item.tkraise() 1503 self.items.shade_item = item 1504 1505 def unshadeStack(self): 1506 if self.items.shade_item: 1507 self.items.shade_item.delete() 1508 self.items.shade_item = None 1509 1510 def _markCards(self, drag): 1511 cards = drag.cards 1512 drag.stack.group.tkraise() 1513 # 1514 x0, y0 = self.getPositionFor(cards[0]) 1515 x1, y1 = self.getPositionFor(cards[-1]) 1516 x0, x1 = min(x1, x0), max(x1, x0) 1517 y0, y1 = min(y1, y0), max(y1, y0) 1518 cw, ch = self.game.app.images.getSize() 1519 x1 += cw 1520 y1 += ch 1521 xx0, yy0 = x0, y0 1522 w, h = x1-x0, y1-y0 1523 # 1524 if TOOLKIT == 'gtk' or not Image: 1525 color = self.game.app.opt.colors['cards_1'] 1526 r = MfxCanvasRectangle(self.canvas, xx0, yy0, xx0+w, yy0+h, 1527 fill="", outline=color, width=4, 1528 group=self.group) 1529 drag.shadows.append(r) 1530 # mylen = MfxCanvasLine(self.canvas, xx0, yy0, xx0+w, yy0+h, 1531 # fill=color, width=4) 1532 # drag.shadows.append(mylen) 1533 # mylen = MfxCanvasLine(self.canvas, xx0, yy0+h, xx0+w, yy0, 1534 # fill=color, width=4) 1535 # drag.shadows.append(mylen) 1536 return 1537 # 1538 shade = Image.new('RGBA', (w, h)) 1539 for c in cards: 1540 x, y = self.getPositionFor(c) 1541 x, y = x-xx0, y-yy0 1542 im = c._active_image._pil_image 1543 shade.paste(im, (x, y), im) 1544 # 1545 shade = markImage(shade) 1546 tkshade = ImageTk.PhotoImage(shade) 1547 im = MfxCanvasImage(self.canvas, xx0, yy0, 1548 image=tkshade, anchor=ANCHOR_NW, 1549 group=self.group) 1550 drag.shadows.append(im) 1551 1552 def _stopDrag(self): 1553 drag = self.game.drag 1554 after_cancel(drag.timer) 1555 drag.timer = None 1556 self._deleteShade() 1557 drag.canshade_stacks = [] 1558 drag.noshade_stacks = [] 1559 for s in drag.shadows: 1560 s.delete() 1561 drag.shadows = [] 1562 drag.stack = None 1563 drag.cards = [] 1564 1565 # finish a drag operation 1566 def finishDrag(self, event=None): 1567 if self.game.app.opt.dragcursor: 1568 self.canvas.config(cursor='') 1569 drag = self.game.drag.copy() 1570 if self.game.app.opt.mouse_type == 'point-n-click': 1571 drag.stack._stopDrag() 1572 else: 1573 self._stopDrag() 1574 if drag.cards: 1575 if self.game.app.opt.mouse_type == 'point-n-click': 1576 self.releaseHandler(event, drag) 1577 else: 1578 assert drag.stack is self 1579 self.releaseHandler(event, drag) 1580 1581 # cancel a drag operation 1582 def cancelDrag(self, event=None): 1583 if self.game.app.opt.dragcursor: 1584 self.canvas.config(cursor='') 1585 drag = self.game.drag.copy() 1586 if self.game.app.opt.mouse_type == 'point-n-click': 1587 drag.stack._stopDrag() 1588 else: 1589 self._stopDrag() 1590 if drag.cards: 1591 assert drag.stack is self 1592 self.moveCardsBackHandler(event, drag) 1593 1594 def getHelp(self): 1595 return str(self) # debug 1596 1597 def getBaseCard(self): 1598 return '' 1599 1600 def _getBaseCard(self, rank=None): 1601 # FIXME: no-french games 1602 if self.cap.max_accept == 0: 1603 return '' 1604 if rank is None: 1605 br = self.cap.base_rank 1606 else: 1607 br = rank 1608 s = _('Base card - %s.') 1609 if br == NO_RANK: 1610 s = _('Empty row cannot be filled.') 1611 elif br == -1: 1612 s = s % _('any card') 1613 elif br == 10: 1614 s = s % _('Jack') 1615 elif br == 11: 1616 s = s % _('Queen') 1617 elif br == 12: 1618 s = s % _('King') 1619 elif br == 0: 1620 s = s % _('Ace') 1621 else: 1622 s = s % str(br+1) 1623 return s 1624 1625 def getNumCards(self): 1626 from pysollib.mygettext import ungettext 1627 n = len(self.cards) 1628 if n == 0: 1629 return _('No cards') 1630 else: 1631 return ungettext('%d card', '%d cards', n) % n 1632 1633 1634# ************************************************************************ 1635# * Abstract interface that supports a concept of dealing. 1636# ************************************************************************ 1637 1638class DealRow_StackMethods: 1639 # Deal a card to each of the RowStacks. Return number of cards dealt. 1640 def dealRow(self, rows=None, flip=1, reverse=0, frames=-1, sound=False): 1641 if rows is None: 1642 rows = self.game.s.rows 1643 if sound and frames and self.game.app.opt.animations: 1644 self.game.startDealSample() 1645 n = self.dealToStacks(rows, flip, reverse, frames) 1646 if sound: 1647 self.game.stopSamples() 1648 return n 1649 1650 # Same, but no error if not enough cards are available. 1651 def dealRowAvail(self, rows=None, flip=1, 1652 reverse=0, frames=-1, sound=False): 1653 if rows is None: 1654 rows = self.game.s.rows 1655 if sound and frames and self.game.app.opt.animations: 1656 self.game.startDealSample() 1657 if len(self.cards) < len(rows): 1658 rows = rows[:len(self.cards)] 1659 n = self.dealToStacks(rows, flip, reverse, frames) 1660 if sound: 1661 self.game.stopSamples() 1662 return n 1663 1664 def dealToStacks(self, stacks, flip=1, reverse=0, frames=-1): 1665 if not self.cards or not stacks: 1666 return 0 1667 assert len(self.cards) >= len(stacks) 1668 old_state = self.game.enterState(self.game.S_DEAL) 1669 if reverse: 1670 stacks = list(stacks) 1671 stacks.reverse() 1672 for r in stacks: 1673 assert not self.getCard().face_up 1674 assert r is not self 1675 if flip: 1676 self.game.flipMove(self) 1677 self.game.moveMove(1, self, r, frames=frames) 1678 self.game.leaveState(old_state) 1679 if TOOLKIT == 'kivy': 1680 self.game.top.waitAnimation() 1681 return len(stacks) 1682 1683 # all Aces go to the Foundations 1684 def dealToStacksOrFoundations(self, stacks, flip=1, 1685 reverse=0, frames=-1, rank=-1): 1686 if rank < 0: 1687 rank = self.game.s.foundations[0].cap.base_rank 1688 if not self.cards or not stacks: 1689 return 0 1690 old_state = self.game.enterState(self.game.S_DEAL) 1691 if reverse: 1692 stacks = list(stacks) 1693 stacks.reverse() 1694 n = 0 1695 for r in stacks: 1696 assert r is not self 1697 while self.cards: 1698 n += 1 1699 if flip: 1700 self.game.flipMove(self) 1701 if flip and self.cards[-1].rank == rank: 1702 for s in self.game.s.foundations: 1703 assert s is not self 1704 if s.acceptsCards(self, self.cards[-1:]): 1705 self.game.moveMove(1, self, s, frames=frames) 1706 break 1707 else: 1708 self.game.moveMove(1, self, r, frames=frames) 1709 break 1710 self.game.leaveState(old_state) 1711 if TOOLKIT == 'kivy': 1712 self.game.top.waitAnimation() 1713 return n 1714 1715 1716class DealBaseCard_StackMethods: 1717 def dealSingleBaseCard(self, frames=-1, update_saveinfo=1): 1718 c = self.cards[-1] 1719 self.dealBaseCards(ncards=1, frames=frames, update_saveinfo=0) 1720 for s in self.game.s.foundations: 1721 s.cap.base_rank = c.rank 1722 if update_saveinfo: 1723 cap = Struct(base_rank=c.rank) 1724 self.game.saveinfo.stack_caps.append((s.id, cap)) 1725 return c 1726 1727 def dealBaseCards(self, ncards=1, frames=-1, update_saveinfo=1): 1728 assert self.game.moves.state == self.game.S_INIT 1729 assert not self.base_cards 1730 while ncards > 0: 1731 assert self.cards 1732 c = self.cards[-1] 1733 for s in self.game.s.foundations: 1734 if (not s.cards and 1735 (s.cap.base_suit < 0 or s.cap.base_suit == c.suit)): 1736 break 1737 else: 1738 assert 0 1739 s = None 1740 s.cap.base_rank = c.rank 1741 if update_saveinfo: 1742 cap = Struct(base_rank=c.rank) 1743 self.game.saveinfo.stack_caps.append((s.id, cap)) 1744 if not c.face_up: 1745 self.game.flipMove(self) 1746 self.game.moveMove(1, self, s, frames=frames) 1747 ncards -= 1 1748 1749 1750class RedealCards_StackMethods: 1751 1752 def _redeal(self, rows=None, reverse=False, frames=0): 1753 # move all cards to the Talon 1754 num_cards = 0 1755 assert len(self.cards) == 0 1756 if rows is None: 1757 rows = self.game.s.rows 1758 rows = list(rows) 1759 if reverse: 1760 rows.reverse() 1761 for r in rows: 1762 for i in range(len(r.cards)): 1763 num_cards += 1 1764 self.game.moveMove(1, r, self, frames=frames, shadow=0) 1765 if self.cards[-1].face_up: 1766 self.game.flipMove(self) 1767 assert len(self.cards) == num_cards 1768 return num_cards 1769 1770 def redealCards(self, rows=None, sound=False, 1771 shuffle=False, reverse=False, frames=0): 1772 if sound and self.game.app.opt.animations: 1773 self.game.startDealSample() 1774 num_cards = self._redeal(rows=rows, reverse=reverse, frames=frames) 1775 if num_cards == 0: # game already finished 1776 return 0 1777 if shuffle: 1778 # shuffle 1779 self.game.shuffleStackMove(self) 1780 # redeal 1781 self.game.nextRoundMove(self) 1782 self.game.redealCards() 1783 if sound: 1784 self.game.stopSamples() 1785 return num_cards 1786 1787 1788# ************************************************************************ 1789# * The Talon is a stack with support for dealing. 1790# ************************************************************************ 1791 1792class TalonStack(Stack, 1793 DealRow_StackMethods, 1794 DealBaseCard_StackMethods, 1795 ): 1796 def __init__(self, x, y, game, max_rounds=1, num_deal=1, **cap): 1797 Stack.__init__(self, x, y, game, cap=cap) 1798 self.max_rounds = max_rounds 1799 self.num_deal = num_deal 1800 self.init_redeal = Struct( 1801 top_bottom=None, 1802 img_coord=None, 1803 txt_coord=None, 1804 ) 1805 self.resetGame() 1806 1807 def resetGame(self): 1808 Stack.resetGame(self) 1809 self.round = 1 1810 self.base_cards = [] # for DealBaseCard_StackMethods 1811 1812 def assertStack(self): 1813 Stack.assertStack(self) 1814 n = self.game.gameinfo.redeals 1815 if n < 0: 1816 assert self.max_rounds == n 1817 else: 1818 assert self.max_rounds == n + 1 1819 1820 # Control of dealing is transferred to the game which usually 1821 # transfers it back to the Talon - see dealCards() below. 1822 def clickHandler(self, event): 1823 return self.game.dealCards(sound=True) 1824 1825 def rightclickHandler(self, event): 1826 return self.clickHandler(event) 1827 1828 # Usually called by Game.canDealCards() 1829 def canDealCards(self): 1830 return len(self.cards) > 0 1831 1832 # Actual dealing, usually called by Game.dealCards(). 1833 # Either deal all cards in Game.startGame(), or subclass responsibility. 1834 def dealCards(self, sound=False): 1835 pass 1836 1837 # remove all cards from all stacks 1838 def removeAllCards(self): 1839 for stack in self.game.allstacks: 1840 while stack.cards: 1841 stack.removeCard(update=0) 1842 # stack.removeCard(unhide=0, update=0) 1843 for stack in self.game.allstacks: 1844 stack.updateText() 1845 1846 def updateText(self, update_rounds=1, update_redeal=1): 1847 # assertView(self) 1848 Stack.updateText(self) 1849 if update_rounds and self.game.preview <= 1: 1850 if self.texts.rounds is not None: 1851 t = _("Round %d") % self.round 1852 self.texts.rounds.config(text=t) 1853 if update_redeal: 1854 deal = self.canDealCards() != 0 1855 if self.images.redeal is not None: 1856 img = (self.getRedealImages())[deal] 1857 if img is not None and img is not self.images.redeal_img: 1858 self.images.redeal.config(image=img) 1859 self.images.redeal_img = img 1860 t = ("", _("Redeal"))[deal] 1861 else: 1862 t = (_("Stop"), _("Redeal"))[deal] 1863 if self.texts.redeal is not None and self.game.preview <= 1: 1864 if t != self.texts.redeal_str: 1865 self.texts.redeal.config(text=t) 1866 self.texts.redeal_str = t 1867 1868 def _addRedealImage(self): 1869 # add or remove the redeal image/text 1870 if not self.is_visible or self.images.bottom is None: 1871 return 1872 if self.game.preview > 1: 1873 return 1874 images = self.game.app.images 1875 cw, ch = images.getSize() 1876 cx, cy = self.init_redeal.img_coord 1877 ca = 'center' 1878 tx, ty = self.init_redeal.txt_coord 1879 1880 if self.images.redeal: 1881 self.canvas.delete(self.images.redeal) 1882 self.images.redeal = None 1883 self.images.redeal_img = None 1884 if self.texts.redeal: 1885 self.canvas.delete(self.texts.redeal) 1886 self.texts.redeal = None 1887 self.texts.redeal_str = '' 1888 self.top_bottom = self.init_redeal.top_bottom 1889 1890 if cw >= 60 and ch >= 60: 1891 # add a redeal image above the bottom image 1892 img = (self.getRedealImages())[self.max_rounds != 1] 1893 if img is not None: 1894 self.images.redeal_img = img 1895 self.images.redeal = MfxCanvasImage(self.canvas, 1896 cx, cy, image=img, 1897 anchor="center", 1898 group=self.group) 1899 if TOOLKIT == 'tk': 1900 self.images.redeal.tkraise(self.top_bottom) 1901 elif TOOLKIT == 'kivy': 1902 self.images.redeal.tkraise(self.top_bottom) 1903 elif TOOLKIT == 'gtk': 1904 # FIXME 1905 pass 1906 self.top_bottom = self.images.redeal 1907 if ch >= 90: 1908 cy, ca = ty, "s" 1909 else: 1910 ca = None 1911 font = self.game.app.getFont("canvas_default") 1912 text_width = get_text_width(_('Redeal'), font=font, 1913 root=self.canvas) 1914 if cw >= text_width+4 and ca: 1915 # add a redeal text below the bottom image 1916 if self.max_rounds != 1: 1917 # FIXME: sometimes canvas do not show the text 1918 # print 'add txt', cx, cy 1919 self.texts.redeal_str = "" 1920 images = self.game.app.images 1921 self.texts.redeal = MfxCanvasText(self.canvas, cx, cy, 1922 anchor=ca, font=font, 1923 group=self.group) 1924 if TOOLKIT == 'tk': 1925 self.texts.redeal.tkraise(self.top_bottom) 1926 elif TOOLKIT == 'kivy': 1927 self.texts.redeal.tkraise(self.top_bottom) 1928 elif TOOLKIT == 'gtk': 1929 # FIXME 1930 pass 1931 self.top_bottom = self.texts.redeal 1932 1933 def prepareView(self): 1934 Stack.prepareView(self) 1935 if 0: 1936 if not self.is_visible or self.images.bottom is None: 1937 return 1938 if self.images.redeal is not None or self.texts.redeal is not None: 1939 return 1940 if self.game.preview > 1: 1941 return 1942 images = self.game.app.images 1943 self.init_redeal.top_bottom = self.top_bottom 1944 cx, cy = self.x + images.CARDW//2, self.y + images.CARDH//2 1945 ty = self.y + images.CARDH - 4 1946 self.init_redeal.img_coord = cx, cy 1947 self.init_redeal.txt_coord = cx, ty 1948 1949 # At least display a redealImage at start, if USE_PIL is not set. 1950 if USE_PIL is False: 1951 self._addRedealImage() 1952 1953 getBottomImage = Stack._getTalonBottomImage 1954 1955 def getRedealImages(self): 1956 # returns a tuple of two PhotoImages 1957 return self.game.app.gimages.redeal 1958 1959 def getHelp(self): 1960 from pysollib.mygettext import ungettext 1961 if self.max_rounds == -2: 1962 nredeals = _('Variable redeals.') 1963 elif self.max_rounds == -1: 1964 nredeals = _('Unlimited redeals.') 1965 else: 1966 n = self.max_rounds-1 1967 nredeals = ungettext('%d redeal', '%d redeals', n) % n 1968 # round = _('Round #%d.') % self.round 1969 return _('Talon.')+' '+nredeals # +' '+round 1970 1971 # def getBaseCard(self): 1972 # return self._getBaseCard() 1973 1974 def resize(self, xf, yf, widthpad=0, heightpad=0): 1975 self._addRedealImage() 1976 Stack.resize(self, xf, yf, widthpad=widthpad, heightpad=heightpad) 1977 1978 1979# A single click deals one card to each of the RowStacks. 1980class DealRowTalonStack(TalonStack): 1981 def dealCards(self, sound=False): 1982 return self.dealRowAvail(sound=sound) 1983 1984 1985# For games where the Talon is only used for the initial dealing. 1986class InitialDealTalonStack(TalonStack): 1987 # no bindings 1988 def initBindings(self): 1989 pass 1990 # no bottom 1991 getBottomImage = Stack._getNoneBottomImage 1992 1993 1994class RedealTalonStack(TalonStack, RedealCards_StackMethods): 1995 def canDealCards(self): 1996 if self.round == self.max_rounds: 1997 return False 1998 return not self.game.isGameWon() 1999 2000 def dealCards(self, sound=False): 2001 RedealCards_StackMethods.redealCards(self, sound=sound) 2002 2003 2004class DealRowRedealTalonStack(TalonStack, RedealCards_StackMethods): 2005 2006 def canDealCards(self, rows=None): 2007 if rows is None: 2008 rows = self.game.s.rows 2009 r_cards = sum([len(r.cards) for r in rows]) 2010 if self.cards: 2011 return True 2012 elif r_cards and self.round != self.max_rounds: 2013 return True 2014 return False 2015 2016 def dealCards(self, sound=False, rows=None, shuffle=False): 2017 num_cards = 0 2018 if rows is None: 2019 rows = self.game.s.rows 2020 if sound and self.game.app.opt.animations: 2021 self.game.startDealSample() 2022 if not self.cards: 2023 # move all cards to talon 2024 num_cards = self._redeal(rows=rows, frames=4) 2025 if shuffle: 2026 # shuffle 2027 self.game.shuffleStackMove(self) 2028 self.game.nextRoundMove(self) 2029 num_cards += self.dealRowAvail(rows=rows, sound=False) 2030 if sound: 2031 self.game.stopSamples() 2032 return num_cards 2033 2034 def shuffleAndDealCards(self, sound=False, rows=None): 2035 DealRowRedealTalonStack.dealCards(self, sound=sound, 2036 rows=rows, shuffle=True) 2037 2038 2039class DealReserveRedealTalonStack(DealRowRedealTalonStack): 2040 2041 def canDealCards(self, rows=None): 2042 return DealRowRedealTalonStack.canDealCards( 2043 self, rows=self.game.s.reserves) 2044 2045 def dealCards(self, sound=False, rows=None): 2046 return DealRowRedealTalonStack.dealCards( 2047 self, sound=sound, rows=self.game.s.reserves) 2048 2049 2050# Spider Talons 2051class SpiderTalonStack(DealRowRedealTalonStack): 2052 def canDealCards(self): 2053 if not DealRowRedealTalonStack.canDealCards(self): 2054 return False 2055 # no row may be empty 2056 for r in self.game.s.rows: 2057 if not r.cards: 2058 return False 2059 return True 2060 2061 2062class GroundsForADivorceTalonStack(DealRowRedealTalonStack): 2063 # A single click deals a new cards to each non-empty row. 2064 def dealCards(self, sound=True): 2065 if self.cards: 2066 rows = [r for r in self.game.s.rows if r.cards] 2067 # if not rows: 2068 # # deal one card to first row if all rows are empty 2069 # rows = self.game.s.rows[:1] 2070 return DealRowRedealTalonStack.dealRowAvail(self, rows=rows, 2071 sound=sound) 2072 return 0 2073 2074 2075# ************************************************************************ 2076# * An OpenStack is a stack where cards can be placed and dragged 2077# * (i.e. FoundationStack, RowStack, ReserveStack, ...) 2078# * 2079# * Note that it defaults to max_move=1 and max_accept=0. 2080# ************************************************************************ 2081 2082class OpenStack(Stack): 2083 def __init__(self, x, y, game, **cap): 2084 kwdefault(cap, max_move=1, max_accept=0, max_cards=999999) 2085 Stack.__init__(self, x, y, game, cap=cap) 2086 2087 # 2088 # Capabilities {model} 2089 # 2090 2091 def acceptsCards(self, from_stack, cards): 2092 # default for OpenStack: we cannot accept 2093 # cards (max_accept defaults to 0) 2094 return self.basicAcceptsCards(from_stack, cards) 2095 2096 def canMoveCards(self, cards): 2097 # import pdb 2098 # pdb.set_trace() 2099 # print('OpenStack.canMoveCards()', cards) 2100 # default for OpenStack: we can move the top card 2101 # (max_move defaults to 1) 2102 return self.basicCanMoveCards(cards) 2103 2104 def canFlipCard(self): 2105 # default for OpenStack: we can flip the top card 2106 if self.basicIsBlocked() or not self.cards: 2107 return False 2108 return not self.cards[-1].face_up 2109 2110 def canDropCards(self, stacks): 2111 if self.basicIsBlocked() or not self.cards: 2112 return (None, 0) 2113 cards = self.cards[-1:] 2114 if self.canMoveCards(cards): 2115 for s in stacks: 2116 if s is not self and s.acceptsCards(self, cards): 2117 return (s, 1) 2118 return (None, 0) 2119 2120 # 2121 # Mouse handlers {controller} 2122 # 2123 2124 def clickHandler(self, event): 2125 flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event) 2126 if self in flipstacks and self.canFlipCard(): 2127 self.playFlipMove(animation=True) 2128 # return -1 # continue this event (start a drag) 2129 return 1 # break 2130 return 0 2131 2132 def rightclickHandler(self, event): 2133 if self.doubleclickHandler(event): 2134 return 1 2135 if self.game.app.opt.quickplay: 2136 flipstacks, dropstacks, quickstacks = \ 2137 self.game.getAutoStacks(event) 2138 if self in quickstacks: 2139 n = self.quickPlayHandler(event) 2140 self.game.stats.quickplay_moves += n 2141 return n 2142 return 0 2143 2144 def doubleclickHandler(self, event): 2145 # flip or drop a card 2146 flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event) 2147 if self in flipstacks and self.canFlipCard(): 2148 self.playFlipMove(animation=True) 2149 return -1 # continue this event (start a drag) 2150 if self in dropstacks: 2151 to_stack, ncards = self.canDropCards(self.game.s.foundations) 2152 if to_stack: 2153 self.game.playSample("autodrop", priority=30) 2154 self.playMoveMove(ncards, to_stack, sound=False) 2155 return 1 2156 return 0 2157 2158 def controlclickHandler(self, event): 2159 # highlight matching cards 2160 if self.game.app.opt.highlight_cards: 2161 return self.highlightMatchingCards(event) 2162 return 0 2163 2164 def dragMove(self, drag, stack, sound=True): 2165 if self.game.app.opt.mouse_type == 'point-n-click': 2166 self.playMoveMove(len(drag.cards), stack, sound=sound) 2167 else: 2168 # self.playMoveMove(len(drag.cards), stack, frames=0, sound=sound) 2169 self.playMoveMove(len(drag.cards), stack, frames=-2, sound=sound) 2170 2171 def releaseHandler(self, event, drag, sound=True): 2172 cards = drag.cards 2173 # check if we moved the card by at least 10 pixels 2174 if event is not None: 2175 dx, dy = event.x - drag.start_x, event.y - drag.start_y 2176 if abs(dx) < 10 and abs(dy) < 10: 2177 # move cards back to their origin stack 2178 Stack.releaseHandler(self, event, drag, sound=sound) 2179 return 2180 # print dx, dy 2181 # get destination stack 2182 if self.game.app.opt.mouse_type == 'point-n-click': 2183 from_stack = drag.stack 2184 to_stack = self 2185 else: 2186 from_stack = self 2187 to_stack = self.game.getClosestStack(cards[0], self) 2188 # move cards 2189 if (not to_stack or from_stack is to_stack or 2190 not to_stack.acceptsCards(from_stack, cards)): 2191 # move cards back to their origin stack 2192 Stack.releaseHandler(self, event, drag, sound=sound) 2193 else: 2194 # this code actually moves the cards to the new stack 2195 # self.playMoveMove(len(cards), stack, frames=0, sound=sound) 2196 from_stack.dragMove(drag, to_stack, sound=sound) 2197 2198 def quickPlayHandler(self, event, from_stacks=None, to_stacks=None): 2199 # from_stacks and to_stacks are meant for possible 2200 # use in a subclasses 2201 if from_stacks is None: 2202 from_stacks = self.game.sg.dropstacks 2203 if to_stacks is None: 2204 # to_stacks = self.game.s.rows + self.game.s.reserves 2205 # to_stacks = self.game.sg.dropstacks 2206 to_stacks = self.game.s.foundations + self.game.sg.dropstacks 2207 # from pprint import pprint; pprint(to_stacks) 2208 moves = [] 2209 # 2210 if not self.cards: 2211 for s in from_stacks: 2212 if s is not self and s.cards: 2213 pile = s.getPile() 2214 if pile and self.acceptsCards(s, pile): 2215 score = self.game.getQuickPlayScore(len(pile), s, self) 2216 moves.append((score, -len(moves), len(pile), s, self)) 2217 else: 2218 pile1, pile2 = None, self.getPile() 2219 if pile2: 2220 i = self._findCard(event) 2221 if i >= 0: 2222 pile = self.cards[i:] 2223 if len(pile) != len(pile2) and self.canMoveCards(pile): 2224 pile1 = pile 2225 for pile in (pile1, pile2): 2226 if not pile: 2227 continue 2228 for s in to_stacks: 2229 if s is not self and s.acceptsCards(self, pile): 2230 score = self.game.getQuickPlayScore(len(pile), self, s) 2231 moves.append((score, -len(moves), len(pile), self, s)) 2232 # 2233 if moves: 2234 moves.sort() 2235 # from pprint import pprint; pprint(moves) 2236 score, len_moves, ncards, from_stack, to_stack = moves[-1] 2237 if score >= 0: 2238 # self.game.playSample("startdrag") 2239 from_stack.playMoveMove(ncards, to_stack) 2240 return 1 2241 return 0 2242 2243 def getHelp(self): 2244 if self.cap.max_accept == 0: 2245 return _('Reserve. No building.') 2246 return '' 2247 2248 2249# ************************************************************************ 2250# * Foundations stacks 2251# ************************************************************************ 2252 2253class AbstractFoundationStack(OpenStack): 2254 def __init__(self, x, y, game, suit, **cap): 2255 kwdefault(cap, suit=suit, base_suit=suit, base_rank=ACE, 2256 dir=1, max_accept=1, max_cards=13) 2257 OpenStack.__init__(self, x, y, game, **cap) 2258 2259 def canDropCards(self, stacks): 2260 return (None, 0) 2261 2262 def clickHandler(self, event): 2263 return 0 2264 2265 def rightclickHandler(self, event): 2266 # return 0 2267 if self.game.app.opt.quickplay: 2268 n = self.quickPlayHandler(event) 2269 self.game.stats.quickplay_moves += n 2270 return n 2271 return 0 2272 2273 def quickPlayHandler(self, event): 2274 # return 0 2275 from_stacks = self.game.sg.dropstacks + self.game.s.foundations 2276 # to_stacks = self.game.sg.dropstacks 2277 to_stacks = from_stacks 2278 return OpenStack.quickPlayHandler(self, event, from_stacks, to_stacks) 2279 2280 getBottomImage = Stack._getSuitBottomImage 2281 2282 def getBaseCard(self): 2283 return self._getBaseCard() 2284 2285 def closeStack(self): 2286 if len(self.cards) == self.cap.max_cards: 2287 self.is_filled = True 2288 self._shadeStack() 2289 2290 def getHelp(self): 2291 return _('Foundation.') 2292 2293 def varyAcceptsCards(self, from_stack, cards): 2294 # if base rank of foundations is vary 2295 subclass = self.__class__ # derived class (SS_FoundationStack, etc) 2296 assert subclass is not AbstractFoundationStack 2297 if self.cards: 2298 return subclass.acceptsCards(self, from_stack, cards) 2299 if not subclass.acceptsCards(self, from_stack, cards): 2300 return False 2301 # this stack don't have cards: check base rank of other stacks 2302 for s in self.game.s.foundations: 2303 if s.cards: 2304 base_card = s.cards[0] 2305 return base_card.rank == cards[0].rank 2306 return True # all foundations is empty 2307 2308 def varyGetBaseCard(self): 2309 rank = None 2310 for s in self.game.s.foundations: 2311 if s.cards: 2312 rank = s.cards[0].rank 2313 return self._getBaseCard(rank=rank) 2314 2315 2316# A SameSuit_FoundationStack is the typical Foundation stack. 2317# It builds up in rank and suit. 2318class SS_FoundationStack(AbstractFoundationStack): 2319 def acceptsCards(self, from_stack, cards): 2320 if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): 2321 return False 2322 if self.cards: 2323 # check the rank 2324 if ((self.cards[-1].rank + self.cap.dir) % self.cap.mod != 2325 cards[0].rank): 2326 return False 2327 return True 2328 2329 def getHelp(self): 2330 if self.cap.dir > 0: 2331 return _('Foundation. Build up by suit.') 2332 elif self.cap.dir < 0: 2333 return _('Foundation. Build down by suit.') 2334 else: 2335 return _('Foundation. Build by same rank.') 2336 2337 2338# A Rank_FoundationStack builds up in rank and ignores color and suit. 2339class RK_FoundationStack(SS_FoundationStack): 2340 def __init__(self, x, y, game, suit=ANY_SUIT, **cap): 2341 SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap) 2342 2343 def getHelp(self): 2344 if self.cap.dir > 0: 2345 return _('Foundation. Build up regardless of suit.') 2346 elif self.cap.dir < 0: 2347 return _('Foundation. Build down regardless of suit.') 2348 else: 2349 return _('Foundation. Build by same rank.') 2350 2351 2352# A AlternateColor_FoundationStack builds up in rank and alternate color. 2353# It is used in only a few games. 2354class AC_FoundationStack(SS_FoundationStack): 2355 def __init__(self, x, y, game, suit, **cap): 2356 kwdefault(cap, base_suit=suit) 2357 SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap) 2358 2359 def acceptsCards(self, from_stack, cards): 2360 if not SS_FoundationStack.acceptsCards(self, from_stack, cards): 2361 return False 2362 if self.cards: 2363 # check the color 2364 if cards[0].color == self.cards[-1].color: 2365 return False 2366 return True 2367 2368 def getHelp(self): 2369 if self.cap.dir > 0: 2370 return _('Foundation. Build up by alternate color.') 2371 elif self.cap.dir < 0: 2372 return _('Foundation. Build down by alternate color.') 2373 else: 2374 return _('Foundation. Build by same rank.') 2375 2376 2377# A SameColor_FoundationStack builds up in rank and alternate color. 2378# It is used in only a few games. 2379class SC_FoundationStack(SS_FoundationStack): 2380 def __init__(self, x, y, game, suit, **cap): 2381 kwdefault(cap, base_suit=suit) 2382 SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap) 2383 2384 def acceptsCards(self, from_stack, cards): 2385 if not SS_FoundationStack.acceptsCards(self, from_stack, cards): 2386 return False 2387 if self.cards: 2388 # check the color 2389 if cards[0].color != self.cards[-1].color: 2390 return False 2391 return True 2392 2393 def getHelp(self): 2394 if self.cap.dir > 0: 2395 return _('Foundation. Build up by color.') 2396 elif self.cap.dir < 0: 2397 return _('Foundation. Build down by color.') 2398 else: 2399 return _('Foundation. Build by same rank.') 2400 2401 2402# Spider-type foundations 2403class Spider_SS_Foundation(AbstractFoundationStack): 2404 def __init__(self, x, y, game, suit=ANY_SUIT, **cap): 2405 kwdefault(cap, dir=-1, base_rank=KING, 2406 min_accept=13, max_accept=13, max_move=0) 2407 AbstractFoundationStack.__init__(self, x, y, game, suit, **cap) 2408 2409 def acceptsCards(self, from_stack, cards): 2410 if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): 2411 return False 2412 # now check the cards 2413 return isSameSuitSequence(cards, self.cap.mod, self.cap.dir) 2414 2415 2416class Spider_AC_Foundation(Spider_SS_Foundation): 2417 def acceptsCards(self, from_stack, cards): 2418 if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): 2419 return False 2420 # now check the cards 2421 return isAlternateColorSequence(cards, self.cap.mod, self.cap.dir) 2422 2423 2424class Spider_RK_Foundation(Spider_SS_Foundation): 2425 def acceptsCards(self, from_stack, cards): 2426 if not AbstractFoundationStack.acceptsCards(self, from_stack, cards): 2427 return False 2428 # now check the cards 2429 return isRankSequence(cards, self.cap.mod, self.cap.dir) 2430 2431 2432# ************************************************************************ 2433# * Abstract classes for row stacks. 2434# ************************************************************************ 2435 2436 2437# Abstract class. 2438class SequenceStack_StackMethods: 2439 def _isSequence(self, cards): 2440 # Are the cards in a basic sequence for our stack ? 2441 raise SubclassResponsibility 2442 2443 def _isAcceptableSequence(self, cards): 2444 return self._isSequence(cards) 2445 2446 def _isMoveableSequence(self, cards): 2447 # import pdb; pdb.set_trace() 2448 return self._isSequence(cards) 2449 2450 def acceptsCards(self, from_stack, cards): 2451 if not self.basicAcceptsCards(from_stack, cards): 2452 return False 2453 # cards must be an acceptable sequence 2454 if not self._isAcceptableSequence(cards): 2455 return False 2456 # [topcard + cards] must be an acceptable sequence 2457 if (self.cards and not 2458 self._isAcceptableSequence([self.cards[-1]] + cards)): 2459 return False 2460 return True 2461 2462 def canMoveCards(self, cards): 2463 return self.basicCanMoveCards(cards) and \ 2464 self._isMoveableSequence(cards) 2465 2466 2467# Abstract class. 2468class BasicRowStack(OpenStack): 2469 def __init__(self, x, y, game, **cap): 2470 kwdefault(cap, dir=-1, base_rank=ANY_RANK) 2471 OpenStack.__init__(self, x, y, game, **cap) 2472 self.CARD_YOFFSET = game.app.images.CARD_YOFFSET 2473 2474 def getHelp(self): 2475 if self.cap.max_accept == 0: 2476 return _('Tableau. No building.') 2477 return '' 2478 2479 # def getBaseCard(self): 2480 # return self._getBaseCard() 2481 2482 def spiderCanDropCards(self, stacks): 2483 # print('spiderCanDropCards()', stacks) 2484 # drop whole sequence 2485 if len(self.cards) < 13: 2486 return (None, 0) 2487 cards = self.cards[-13:] 2488 for s in stacks: 2489 if s is not self and s.acceptsCards(self, cards): 2490 return (s, 13) 2491 return (None, 0) 2492 2493 def getReserveBottomImage(self): 2494 return self.game.app.images.getReserveBottom() 2495 2496 2497# Abstract class. 2498class SequenceRowStack(SequenceStack_StackMethods, BasicRowStack): 2499 # canMoveCards = OpenStack.canMoveCards 2500 2501 def __init__(self, x, y, game, **cap): 2502 kwdefault(cap, max_move=999999, max_accept=999999) 2503 BasicRowStack.__init__(self, x, y, game, **cap) 2504 2505 def getBaseCard(self): 2506 return self._getBaseCard() 2507 2508 2509# ************************************************************************ 2510# * Row stacks (the main playing stacks on the Tableau). 2511# ************************************************************************ 2512 2513# 2514# Implementation of common row stacks follows here. 2515# 2516 2517# An AlternateColor_RowStack builds down by rank and alternate color. 2518# e.g. Klondike 2519class AC_RowStack(SequenceRowStack): 2520 def _isSequence(self, cards): 2521 return isAlternateColorSequence(cards, self.cap.mod, self.cap.dir) 2522 2523 def getHelp(self): 2524 if self.cap.dir > 0: 2525 return _('Tableau. Build up by alternate color.') 2526 elif self.cap.dir < 0: 2527 return _('Tableau. Build down by alternate color.') 2528 else: 2529 return _('Tableau. Build by same rank.') 2530 2531 2532# A SameColor_RowStack builds down by rank and same color. 2533# e.g. Klondike 2534class SC_RowStack(SequenceRowStack): 2535 def _isSequence(self, cards): 2536 return isSameColorSequence(cards, self.cap.mod, self.cap.dir) 2537 2538 def getHelp(self): 2539 if self.cap.dir > 0: 2540 return _('Tableau. Build up by color.') 2541 elif self.cap.dir < 0: 2542 return _('Tableau. Build down by color.') 2543 else: 2544 return _('Tableau. Build by same rank.') 2545 2546 2547# A SameSuit_RowStack builds down by rank and suit. 2548class SS_RowStack(SequenceRowStack): 2549 def _isSequence(self, cards): 2550 return isSameSuitSequence(cards, self.cap.mod, self.cap.dir) 2551 2552 def getHelp(self): 2553 if self.cap.dir > 0: 2554 return _('Tableau. Build up by suit.') 2555 elif self.cap.dir < 0: 2556 return _('Tableau. Build down by suit.') 2557 else: 2558 return _('Tableau. Build by same rank.') 2559 2560 2561# A Rank_RowStack builds down by rank ignoring suit. 2562class RK_RowStack(SequenceRowStack): 2563 def _isSequence(self, cards): 2564 return isRankSequence(cards, self.cap.mod, self.cap.dir) 2565 2566 def getHelp(self): 2567 if self.cap.dir > 0: 2568 return _('Tableau. Build up regardless of suit.') 2569 elif self.cap.dir < 0: 2570 return _('Tableau. Build down regardless of suit.') 2571 else: 2572 return _('Tableau. Build by same rank.') 2573 2574 2575# ButOwn_RowStack 2576class BO_RowStack(SequenceRowStack): 2577 def _isSequence(self, cards): 2578 return isAnySuitButOwnSequence(cards, self.cap.mod, self.cap.dir) 2579 2580 def getHelp(self): 2581 if self.cap.dir > 0: 2582 return _('Tableau. Build up in any suit but the same.') 2583 elif self.cap.dir < 0: 2584 return _('Tableau. Build down in any suit but the same.') 2585 else: 2586 return _('Tableau. Build by same rank.') 2587 2588 2589# A Freecell_AlternateColor_RowStack 2590class FreeCell_AC_RowStack(AC_RowStack): 2591 def canMoveCards(self, cards): 2592 max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 2593 return len(cards) <= max_move and AC_RowStack.canMoveCards(self, cards) 2594 2595 2596# A Freecell_SameSuit_RowStack (i.e. Baker's Game) 2597class FreeCell_SS_RowStack(SS_RowStack): 2598 def canMoveCards(self, cards): 2599 max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 2600 return len(cards) <= max_move and SS_RowStack.canMoveCards(self, cards) 2601 2602 2603# A Freecell_Rank_RowStack 2604class FreeCell_RK_RowStack(RK_RowStack): 2605 def canMoveCards(self, cards): 2606 max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 2607 return len(cards) <= max_move and RK_RowStack.canMoveCards(self, cards) 2608 2609 2610# A Spider_AlternateColor_RowStack builds down by rank and alternate color, 2611# but accepts sequences that match by rank only. 2612class Spider_AC_RowStack(AC_RowStack): 2613 def _isAcceptableSequence(self, cards): 2614 return isRankSequence(cards, self.cap.mod, self.cap.dir) 2615 2616 def getHelp(self): 2617 if self.cap.dir > 0: 2618 return _('Tableau. Build up regardless of suit. ' 2619 'Sequences of cards in alternate color ' 2620 'can be moved as a unit.') 2621 elif self.cap.dir < 0: 2622 return _('Tableau. Build down regardless of suit. ' 2623 'Sequences of cards in alternate color can be moved ' 2624 'as a unit.') 2625 else: 2626 return _('Tableau. Build by same rank.') 2627 2628 2629# A Spider_SameSuit_RowStack builds down by rank and suit, 2630# but accepts sequences that match by rank only. 2631class Spider_SS_RowStack(SS_RowStack): 2632 def _isAcceptableSequence(self, cards): 2633 return isRankSequence(cards, self.cap.mod, self.cap.dir) 2634 2635 def getHelp(self): 2636 if self.cap.dir > 0: 2637 return _('Tableau. Build up regardless of suit. ' 2638 'Sequences of cards in the same suit can be moved ' 2639 'as a unit.') 2640 elif self.cap.dir < 0: 2641 return _('Tableau. Build down regardless of suit. ' 2642 'Sequences of cards in the same suit can be moved ' 2643 'as a unit.') 2644 else: 2645 return _('Tableau. Build by same rank.') 2646 2647 2648# A Yukon_AlternateColor_RowStack builds down by rank and alternate color, 2649# but can move any face-up cards regardless of sequence. 2650class Yukon_AC_RowStack(BasicRowStack): 2651 def __init__(self, x, y, game, **cap): 2652 kwdefault(cap, max_move=999999, max_accept=999999) 2653 BasicRowStack.__init__(self, x, y, game, **cap) 2654 2655 def _isYukonSequence(self, c1, c2): 2656 # print('Yukon_AC_RowStack._isYukonSequence()', c1, c2) 2657 return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and 2658 c1.color != c2.color) 2659 2660 def acceptsCards(self, from_stack, cards): 2661 # print('Yukon_AC_RowStack.acceptsCards()', from_stack, cards) 2662 if not self.basicAcceptsCards(from_stack, cards): 2663 return False 2664 # [topcard + card[0]] must be acceptable 2665 if self.cards and not self._isYukonSequence(self.cards[-1], cards[0]): 2666 return False 2667 return True 2668 2669 def getHelp(self): 2670 if self.cap.dir > 0: 2671 return _('Tableau. Build up by alternate color, ' 2672 'can move any face-up cards regardless of sequence.') 2673 elif self.cap.dir < 0: 2674 return _('Tableau. Build down by alternate color, ' 2675 'can move any face-up cards regardless of sequence.') 2676 else: 2677 return _('Tableau. Build by same rank, can move ' 2678 'any face-up cards regardless of sequence.') 2679 2680 def getBaseCard(self): 2681 return self._getBaseCard() 2682 2683 2684# A Yukon_SameSuit_RowStack builds down by rank and suit, 2685# but can move any face-up cards regardless of sequence. 2686class Yukon_SS_RowStack(Yukon_AC_RowStack): 2687 def _isYukonSequence(self, c1, c2): 2688 return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and 2689 c1.suit == c2.suit) 2690 2691 def getHelp(self): 2692 if self.cap.dir > 0: 2693 return _('Tableau. Build up by suit, can move any face-up cards ' 2694 'regardless of sequence.') 2695 elif self.cap.dir < 0: 2696 return _('Tableau. Build down by suit, can move any ' 2697 'face-up cards regardless of sequence.') 2698 else: 2699 return _('Tableau. Build by same rank, can move any ' 2700 'face-up cards regardless of sequence.') 2701 2702 2703# A Yukon_Rank_RowStack builds down by rank 2704# but can move any face-up cards regardless of sequence. 2705class Yukon_RK_RowStack(Yukon_AC_RowStack): 2706 def _isYukonSequence(self, c1, c2): 2707 return (c1.rank + self.cap.dir) % self.cap.mod == c2.rank 2708 2709 def getHelp(self): 2710 if self.cap.dir > 0: 2711 return _('Tableau. Build up regardless of suit, ' 2712 'can move any face-up cards regardless of sequence.') 2713 elif self.cap.dir < 0: 2714 return _('Tableau. Build up regardless of suit, can move any ' 2715 'face-up cards regardless of sequence.') 2716 else: 2717 return _('Tableau. Build by same rank, can move any ' 2718 'face-up cards regardless of sequence.') 2719 2720# 2721# King-versions of some of the above stacks: they accepts only Kings or 2722# sequences starting with a King as base_rank cards (i.e. when empty). 2723# 2724 2725 2726class KingAC_RowStack(AC_RowStack): 2727 def __init__(self, x, y, game, **cap): 2728 kwdefault(cap, base_rank=KING) 2729 AC_RowStack.__init__(self, x, y, game, **cap) 2730 2731 2732class KingSS_RowStack(SS_RowStack): 2733 def __init__(self, x, y, game, **cap): 2734 kwdefault(cap, base_rank=KING) 2735 SS_RowStack.__init__(self, x, y, game, **cap) 2736 2737 2738class KingRK_RowStack(RK_RowStack): 2739 def __init__(self, x, y, game, **cap): 2740 kwdefault(cap, base_rank=KING) 2741 RK_RowStack.__init__(self, x, y, game, **cap) 2742 2743 2744# up or down by color 2745class UD_SC_RowStack(SequenceRowStack): 2746 def __init__(self, x, y, game, **cap): 2747 kwdefault(cap, max_move=1, max_accept=1) 2748 SequenceRowStack.__init__(self, x, y, game, **cap) 2749 2750 def _isSequence(self, cards): 2751 return (isSameColorSequence(cards, self.cap.mod, 1) or 2752 isSameColorSequence(cards, self.cap.mod, -1)) 2753 2754 def getHelp(self): 2755 return _('Tableau. Build up or down by color.') 2756 2757 2758# up or down by alternate color 2759class UD_AC_RowStack(SequenceRowStack): 2760 def __init__(self, x, y, game, **cap): 2761 kwdefault(cap, max_move=1, max_accept=1) 2762 SequenceRowStack.__init__(self, x, y, game, **cap) 2763 2764 def _isSequence(self, cards): 2765 return (isAlternateColorSequence(cards, self.cap.mod, 1) or 2766 isAlternateColorSequence(cards, self.cap.mod, -1)) 2767 2768 def getHelp(self): 2769 return _('Tableau. Build up or down by alternate color.') 2770 2771 2772# up or down by suit 2773class UD_SS_RowStack(SequenceRowStack): 2774 def __init__(self, x, y, game, **cap): 2775 kwdefault(cap, max_move=1, max_accept=1) 2776 SequenceRowStack.__init__(self, x, y, game, **cap) 2777 2778 def _isSequence(self, cards): 2779 return (isSameSuitSequence(cards, self.cap.mod, 1) or 2780 isSameSuitSequence(cards, self.cap.mod, -1)) 2781 2782 def getHelp(self): 2783 return _('Tableau. Build up or down by suit.') 2784 2785 2786# up or down by rank ignoring suit 2787class UD_RK_RowStack(SequenceRowStack): 2788 def __init__(self, x, y, game, **cap): 2789 kwdefault(cap, max_move=1, max_accept=1) 2790 SequenceRowStack.__init__(self, x, y, game, **cap) 2791 2792 def _isSequence(self, cards): 2793 return (isRankSequence(cards, self.cap.mod, 1) or 2794 isRankSequence(cards, self.cap.mod, -1)) 2795 2796 def getHelp(self): 2797 return _('Tableau. Build up or down regardless of suit.') 2798 2799 2800# To simplify playing we also consider the number of free rows. 2801# See also the "SuperMove" section in the FreeCell FAQ. 2802class SuperMoveStack_StackMethods: 2803 def _getMaxMove(self, to_stack_ncards): 2804 max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1 2805 if self.cap.base_rank != ANY_RANK: 2806 return max_move 2807 n = getNumberOfFreeStacks(self.game.s.rows) 2808 if to_stack_ncards == 0: 2809 n -= 1 2810 return max_move << max(n, 0) 2811 2812 def _getNumSSSeq(self, cards): 2813 # num of same-suit sequences (for SuperMoveSpider_RowStack) 2814 if not cards: 2815 return 0 2816 n = 1 2817 suit = cards[-1].suit 2818 for c in cards[-2::-1]: 2819 if c.suit != suit: 2820 suit = c.suit 2821 n += 1 2822 return n 2823 2824 2825class SuperMoveSS_RowStack(SuperMoveStack_StackMethods, SS_RowStack): 2826 def canMoveCards(self, cards): 2827 if not SS_RowStack.canMoveCards(self, cards): 2828 return False 2829 return len(cards) <= self._getMaxMove(1) 2830 2831 def acceptsCards(self, from_stack, cards): 2832 if not SS_RowStack.acceptsCards(self, from_stack, cards): 2833 return False 2834 return len(cards) <= self._getMaxMove(len(self.cards)) 2835 2836 2837class SuperMoveAC_RowStack(SuperMoveStack_StackMethods, AC_RowStack): 2838 def canMoveCards(self, cards): 2839 if not AC_RowStack.canMoveCards(self, cards): 2840 return False 2841 return len(cards) <= self._getMaxMove(1) 2842 2843 def acceptsCards(self, from_stack, cards): 2844 if not AC_RowStack.acceptsCards(self, from_stack, cards): 2845 return False 2846 return len(cards) <= self._getMaxMove(len(self.cards)) 2847 2848 2849class SuperMoveRK_RowStack(SuperMoveStack_StackMethods, RK_RowStack): 2850 def canMoveCards(self, cards): 2851 if not RK_RowStack.canMoveCards(self, cards): 2852 return False 2853 return len(cards) <= self._getMaxMove(1) 2854 2855 def acceptsCards(self, from_stack, cards): 2856 if not RK_RowStack.acceptsCards(self, from_stack, cards): 2857 return False 2858 return len(cards) <= self._getMaxMove(len(self.cards)) 2859 2860 2861class SuperMoveSC_RowStack(SuperMoveStack_StackMethods, SC_RowStack): 2862 def canMoveCards(self, cards): 2863 if not SC_RowStack.canMoveCards(self, cards): 2864 return False 2865 return len(cards) <= self._getMaxMove(1) 2866 2867 def acceptsCards(self, from_stack, cards): 2868 if not SC_RowStack.acceptsCards(self, from_stack, cards): 2869 return False 2870 return len(cards) <= self._getMaxMove(len(self.cards)) 2871 2872 2873class SuperMoveBO_RowStack(SuperMoveStack_StackMethods, BO_RowStack): 2874 def canMoveCards(self, cards): 2875 if not BO_RowStack.canMoveCards(self, cards): 2876 return False 2877 return len(cards) <= self._getMaxMove(1) 2878 2879 def acceptsCards(self, from_stack, cards): 2880 if not BO_RowStack.acceptsCards(self, from_stack, cards): 2881 return False 2882 return len(cards) <= self._getMaxMove(len(self.cards)) 2883 2884 2885# ************************************************************************ 2886# * WasteStack (a helper stack for the Talon, e.g. in Klondike) 2887# ************************************************************************ 2888 2889class WasteStack(OpenStack): 2890 def getHelp(self): 2891 return _('Waste.') 2892 2893 2894class WasteTalonStack(TalonStack): 2895 # A single click moves the top cards to the game's waste and 2896 # moves it face up; if we're out of cards, it moves the waste 2897 # back to the talon and increases the number of rounds (redeals). 2898 def __init__(self, x, y, game, max_rounds, num_deal=1, waste=None, **cap): 2899 TalonStack.__init__(self, x, y, game, max_rounds, num_deal, **cap) 2900 self.waste = waste 2901 2902 def prepareStack(self): 2903 TalonStack.prepareStack(self) 2904 if self.waste is None: 2905 self.waste = self.game.s.waste 2906 2907 def canDealCards(self): 2908 waste = self.waste 2909 if self.cards: 2910 num_cards = min(len(self.cards), self.num_deal) 2911 return len(waste.cards) + num_cards <= waste.cap.max_cards 2912 elif waste.cards and self.round != self.max_rounds: 2913 return True 2914 return False 2915 2916 def dealCards(self, sound=False, shuffle=False): 2917 old_state = self.game.enterState(self.game.S_DEAL) 2918 num_cards = 0 2919 waste = self.waste 2920 if self.cards: 2921 if sound and not self.game.demo: 2922 self.game.playSample("dealwaste") 2923 num_cards = min(len(self.cards), self.num_deal) 2924 assert len(waste.cards) + num_cards <= waste.cap.max_cards 2925 for i in range(num_cards): 2926 if not self.cards[-1].face_up: 2927 if 1: 2928 self.game.flipAndMoveMove(self, waste) 2929 else: 2930 self.game.flipMove(self) 2931 self.game.moveMove(1, self, waste, frames=4, shadow=0) 2932 else: 2933 self.game.moveMove(1, self, waste, frames=4, shadow=0) 2934 self.fillStack() 2935 if TOOLKIT == 'kivy': 2936 self.game.top.waitAnimation() 2937 elif waste.cards and self.round != self.max_rounds: 2938 if sound: 2939 self.game.playSample("turnwaste", priority=20) 2940 num_cards = len(waste.cards) 2941 self.game.turnStackMove(waste, self) 2942 if shuffle: 2943 # shuffle 2944 self.game.shuffleStackMove(self) 2945 self.game.nextRoundMove(self) 2946 self.game.leaveState(old_state) 2947 return num_cards 2948 2949 def shuffleAndDealCards(self, sound=False): 2950 WasteTalonStack.dealCards(self, sound=sound, shuffle=True) 2951 2952 2953class FaceUpWasteTalonStack(WasteTalonStack): 2954 def canFlipCard(self): 2955 return len(self.cards) > 0 and not self.cards[-1].face_up 2956 2957 def fillStack(self): 2958 if self.canFlipCard(): 2959 self.game.singleFlipMove(self) 2960 self.game.fillStack(self) 2961 2962 def dealCards(self, sound=False): 2963 retval = WasteTalonStack.dealCards(self, sound=sound) 2964 if self.canFlipCard(): 2965 self.flipMove() 2966 return retval 2967 2968 2969class OpenTalonStack(TalonStack, OpenStack): 2970 canMoveCards = OpenStack.canMoveCards 2971 canDropCards = OpenStack.canDropCards 2972 releaseHandler = OpenStack.releaseHandler 2973 2974 def __init__(self, x, y, game, **cap): 2975 kwdefault(cap, max_move=1) 2976 TalonStack.__init__(self, x, y, game, **cap) 2977 2978 def canDealCards(self): 2979 return False 2980 2981 def canFlipCard(self): 2982 return len(self.cards) > 0 and not self.cards[-1].face_up 2983 2984 def fillStack(self): 2985 if self.canFlipCard(): 2986 self.game.singleFlipMove(self) 2987 self.game.fillStack(self) 2988 2989 def clickHandler(self, event): 2990 if self.canDealCards(): 2991 return TalonStack.clickHandler(self, event) 2992 else: 2993 return OpenStack.clickHandler(self, event) 2994 2995 2996# ************************************************************************ 2997# * ReserveStack (free cell) 2998# ************************************************************************ 2999 3000class ReserveStack(OpenStack): 3001 def __init__(self, x, y, game, **cap): 3002 kwdefault(cap, max_accept=1, max_cards=1) 3003 OpenStack.__init__(self, x, y, game, **cap) 3004 3005 getBottomImage = Stack._getReserveBottomImage 3006 3007 def getHelp(self): 3008 if self.cap.max_accept == 0: 3009 return _('Reserve. No building.') 3010 return _('Free cell.') 3011 3012 3013# ************************************************************************ 3014# * InvisibleStack (an internal off-screen stack to hold cards) 3015# ************************************************************************ 3016 3017class InvisibleStack(Stack): 3018 def __init__(self, game, **cap): 3019 x, y = game.getInvisibleCoords() 3020 kwdefault(cap, max_move=0, max_accept=0) 3021 Stack.__init__(self, x, y, game, cap=cap) 3022 3023 def assertStack(self): 3024 Stack.assertStack(self) 3025 assert not self.is_visible 3026 3027 # no bindings 3028 def initBindings(self): 3029 pass 3030 3031 # no bottom 3032 getBottomImage = Stack._getNoneBottomImage 3033 3034 3035# ************************************************************************ 3036# * ArbitraryStack (stack with arbitrary access) 3037# * 3038# * NB: don't support hint and demo for non-top cards 3039# * NB: this stack only for CARD_XOFFSET == 0 3040# ************************************************************************ 3041 3042class ArbitraryStack(OpenStack): 3043 3044 def __init__(self, x, y, game, **cap): 3045 kwdefault(cap, max_accept=0) 3046 OpenStack.__init__(self, x, y, game, **cap) 3047 self.CARD_YOFFSET = game.app.images.CARD_YOFFSET 3048 3049 def canMoveCards(self, cards): 3050 return True 3051 3052 def getDragCards(self, index): 3053 return [self.cards[index]] 3054 3055 def startDrag(self, event, sound=True): 3056 OpenStack.startDrag(self, event, sound=sound) 3057 if self.game.app.opt.mouse_type == 'point-n-click': 3058 self.cards[self.game.drag.index].tkraise() 3059 self.game.drag.shadows[0].tkraise() 3060 else: 3061 for c in self.cards[self.game.drag.index+1:]: 3062 c.moveBy(0, -self.CARD_YOFFSET[0]) 3063 3064 def doubleclickHandler(self, event): 3065 # flip or drop a card 3066 flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event) 3067 if self in flipstacks and self.canFlipCard(): 3068 self.playFlipMove(animation=True) 3069 return -1 # continue this event (start a drag) 3070 if self in dropstacks: 3071 i = self._findCard(event) 3072 if i < 0: 3073 return 0 3074 cards = [self.cards[i]] 3075 for s in self.game.s.foundations: 3076 if s is not self and s.acceptsCards(self, cards): 3077 self.game.playSample("autodrop", priority=30) 3078 self.playSingleCardMove(i, s, sound=False) 3079 return 1 3080 return 0 3081 3082 def moveCardsBackHandler(self, event, drag): 3083 i = self.cards.index(drag.cards[0]) 3084 for card in self.cards[i:]: 3085 self._position(card) 3086 card.tkraise() 3087 3088 def singleCardMove(self, index, to_stack, frames=-1, shadow=-1): 3089 self.game.singleCardMove( 3090 self, to_stack, index, frames=frames, shadow=shadow) 3091 self.fillStack() 3092 3093 def dragMove(self, drag, to_stack, sound=True): 3094 self.playSingleCardMove(drag.index, to_stack, frames=0, sound=sound) 3095 3096 def playSingleCardMove(self, index, to_stack, frames=-1, shadow=-1, 3097 sound=True): 3098 if sound: 3099 if to_stack in self.game.s.foundations: 3100 self.game.playSample("drop", priority=30) 3101 else: 3102 self.game.playSample("move", priority=10) 3103 self.singleCardMove(index, to_stack, frames=frames, shadow=shadow) 3104 if not self.game.checkForWin(): 3105 # let the player put cards back from the foundations 3106 if self not in self.game.s.foundations: 3107 self.game.autoPlay() 3108 self.game.finishMove() 3109 3110 def quickPlayHandler(self, event, from_stacks=None, to_stacks=None): 3111 if to_stacks is None: 3112 to_stacks = self.game.s.foundations + self.game.sg.dropstacks 3113 if not self.cards: 3114 return 0 3115 # 3116 moves = [] 3117 i = self._findCard(event) 3118 if i < 0: 3119 return 0 3120 pile = [self.cards[i]] 3121 for s in to_stacks: 3122 if s is not self and s.acceptsCards(self, pile): 3123 score = self.game.getQuickPlayScore(1, self, s) 3124 moves.append((score, -len(moves), i, s)) 3125 # 3126 if moves: 3127 moves.sort() 3128 # from pprint import pprint; pprint(moves) 3129 score, len_moves, index, to_stack = moves[-1] 3130 if score >= 0: 3131 # self.game.playSample("startdrag") 3132 self.playSingleCardMove(index, to_stack) 3133 return 1 3134 return 0 3135 3136 3137# ************************************************************************ 3138# * A StackWrapper is a functor (function object) that creates a 3139# * new stack when called, i.e. it wraps the constructor. 3140# * 3141# * "cap" are the capabilites, see class Stack above. 3142# ************************************************************************ 3143 3144# self.cap override any call-time cap 3145class StackWrapper: 3146 def __init__(self, stack_class, **cap): 3147 assert issubclass(stack_class, Stack) 3148 self.stack_class = stack_class 3149 self.cap = cap 3150 3151 # return a new stack (an instance of the stack class) 3152 def __call__(self, x, y, game, **cap): 3153 # must preserve self.cap, so create a shallow copy 3154 # import pdb 3155 # pdb.set_trace() 3156 c = self.cap.copy() 3157 kwdefault(c, **cap) 3158 return self.stack_class(x, y, game, **c) 3159 3160 3161# call-time cap override self.cap 3162class WeakStackWrapper(StackWrapper): 3163 def __call__(self, x, y, game, **cap): 3164 kwdefault(cap, **self.cap) 3165 return self.stack_class(x, y, game, **cap) 3166 3167 3168# self.cap only, call-time cap is completely ignored 3169class FullStackWrapper(StackWrapper): 3170 def __call__(self, x, y, game, **cap): 3171 return self.stack_class(x, y, game, **self.cap) 3172