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