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
24import pysollib.game
25from pysollib.game import Game
26from pysollib.gamedb import GI, GameInfo, registerGame
27from pysollib.games.pileon import FourByFour_Hint
28from pysollib.hint import AbstractHint, CautiousDefaultHint, DefaultHint
29from pysollib.hint import BlackHoleSolverWrapper
30from pysollib.layout import Layout
31from pysollib.mfxutil import kwdefault
32from pysollib.mygettext import _
33from pysollib.pysoltk import MfxCanvasText
34from pysollib.stack import \
35        AbstractFoundationStack, \
36        BasicRowStack, \
37        DealRowTalonStack, \
38        InitialDealTalonStack, \
39        OpenStack, \
40        RK_FoundationStack, \
41        RK_RowStack, \
42        ReserveStack, \
43        SS_FoundationStack, \
44        SS_RowStack, \
45        Stack, \
46        StackWrapper, \
47        TalonStack, \
48        UD_RK_RowStack, \
49        WasteStack, \
50        WasteTalonStack, \
51        isSameSuitSequence
52from pysollib.util import ACE, ANY_RANK, ANY_SUIT, DIAMOND, KING, NO_RANK,\
53        RANKS, SUITS, \
54        UNLIMITED_REDEALS
55
56
57class Golf_Hint(AbstractHint):
58    # FIXME: this is very simple
59
60    def computeHints(self):
61        game = self.game
62        # for each stack
63        for r in game.sg.dropstacks:
64            # try if we can drop a card to the Waste
65            w, ncards = r.canDropCards(game.s.foundations)
66            if not w:
67                continue
68            # this assertion must hold for Golf
69            assert ncards == 1
70            # clone the Waste (including the card that will be dropped) to
71            # form our new foundations
72            ww = (self.ClonedStack(w, stackcards=w.cards+[r.cards[-1]]), )
73            # now search for a stack that would benefit from this card
74            score, color = 10000 + r.id, None
75            for t in game.sg.dropstacks:
76                if not t.cards:
77                    continue
78                if t is r:
79                    t = self.ClonedStack(r, stackcards=r.cards[:-1])
80                if t.canFlipCard():
81                    score = score + 100
82                elif t.canDropCards(ww)[0]:
83                    score = score + 100
84            # add hint
85            self.addHint(score, ncards, r, w, color)
86
87
88# ************************************************************************
89# *
90# ************************************************************************
91
92class Golf_Talon(WasteTalonStack):
93    def canDealCards(self):
94        if not WasteTalonStack.canDealCards(self):
95            return False
96        return not self.game.isGameWon()
97
98
99class Golf_Waste(WasteStack):
100    def __init__(self, x, y, game, **cap):
101        kwdefault(cap, max_move=0, max_accept=1)
102        WasteStack.__init__(self, x, y, game, **cap)
103
104    def acceptsCards(self, from_stack, cards):
105        if from_stack is self.game.s.talon:
106            return True
107        if not WasteStack.acceptsCards(self, from_stack, cards):
108            return False
109        # check cards
110        r1, r2 = self.cards[-1].rank, cards[0].rank
111        if self.game.getStrictness() == 1:
112            # nothing on a King
113            if r1 == KING:
114                return False
115        return (r1 + 1) % self.cap.mod == r2 or (r2 + 1) % self.cap.mod == r1
116
117    def getHelp(self):
118        return _('Waste. Build up or down regardless of suit.')
119
120
121class Golf_RowStack(BasicRowStack):
122    def clickHandler(self, event):
123        return self.doubleclickHandler(event)
124
125    def getHelp(self):
126        return _('Tableau. No building.')
127
128
129# ************************************************************************
130# * Golf
131# ************************************************************************
132
133class Golf(Game):
134    Solver_Class = BlackHoleSolverWrapper(preset='golf', base_rank=0,
135                                          queens_on_kings=True)
136    Waste_Class = Golf_Waste
137    Hint_Class = Golf_Hint
138
139    #
140    # game layout
141    #
142
143    def createGame(self, columns=7):
144        # create layout
145        layout, s = Layout(self), self.s
146
147        # set window
148        playcards = 5
149        w1, w2 = (columns + 1) * layout.XS + layout.XM, 2 * layout.XS
150
151        totalcards = 52 * self.gameinfo.decks
152        if w2 + totalcards * layout.XOFFSET > w1:
153            layout.XOFFSET = int((w1 - w2) / totalcards)
154        self.setSize(w1, layout.YM + 3 * layout.YS +
155                     (playcards - 1) * layout.YOFFSET + layout.TEXT_HEIGHT)
156
157        # create stacks
158        x, y = layout.XM + layout.XS // 2, layout.YM
159        for i in range(columns):
160            s.rows.append(Golf_RowStack(x, y, self))
161            x = x + layout.XS
162        x, y = layout.XM, self.height - layout.YS
163        s.talon = Golf_Talon(x, y, self, max_rounds=1)
164        layout.createText(s.talon, "n")
165        x = x + layout.XS
166        s.waste = self.Waste_Class(x, y, self)
167        s.waste.CARD_XOFFSET = layout.XOFFSET
168        layout.createText(s.waste, "n")
169        # the Waste is also our only Foundation in this game
170        s.foundations.append(s.waste)
171
172        # define stack-groups (non default)
173        self.sg.openstacks = [s.waste]
174        self.sg.talonstacks = [s.talon]
175        self.sg.dropstacks = s.rows
176
177    #
178    # game overrides
179    #
180
181    def startGame(self, num_rows=5):
182        self._startDealNumRows(num_rows - 1)
183        self.s.talon.dealRow()
184        self.s.talon.dealCards()          # deal first card to WasteStack
185
186    def isGameWon(self):
187        for r in self.s.rows:
188            if r.cards:
189                return False
190        return True
191
192    shallHighlightMatch = Game._shallHighlightMatch_RK
193
194    def getHighlightPilesStacks(self):
195        return ()
196
197    def getAutoStacks(self, event=None):
198        if event is None:
199            # disable auto drop - this would ruin the whole gameplay
200            return (self.sg.dropstacks, (), ())
201        else:
202            # rightclickHandler
203            return (self.sg.dropstacks, self.sg.dropstacks, ())
204
205
206# ************************************************************************
207# * Double Golf
208# ************************************************************************
209
210class DoubleGolf(Golf):
211
212    def createGame(self):
213        Golf.createGame(self, 9)
214
215    def startGame(self):
216        Golf.startGame(self, 7)
217
218
219# ************************************************************************
220# *
221# ************************************************************************
222
223class DeadKingGolf(Golf):
224    Solver_Class = BlackHoleSolverWrapper(preset='golf', base_rank=0)
225
226    def getStrictness(self):
227        return 1
228
229    def shallHighlightMatch(self, stack1, card1, stack2, card2):
230        if card1.rank == KING:
231            return False
232        return Golf.shallHighlightMatch(self, stack1, card1, stack2, card2)
233
234
235class RelaxedGolf(Golf):
236    Solver_Class = BlackHoleSolverWrapper(preset='golf', base_rank=0,
237                                          wrap_ranks=True)
238    Waste_Class = StackWrapper(Golf_Waste, mod=13)
239
240    shallHighlightMatch = Game._shallHighlightMatch_RKW
241
242
243# ************************************************************************
244# * Elevator - Relaxed Golf in a Pyramid layout
245# ************************************************************************
246
247class Elevator_RowStack(Golf_RowStack):
248    STEP = (1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6)
249
250    def basicIsBlocked(self):
251        r, step = self.game.s.rows, self.STEP
252        i, n, mylen = self.id, 1, len(step)
253        while i < mylen:
254            i = i + step[i]
255            n = n + 1
256            for j in range(i, i+n):
257                if r[j].cards:
258                    return True
259        return False
260
261
262class Elevator(RelaxedGolf):
263
264    #
265    # game layout
266    #
267
268    def createGame(self):
269        # create layout
270        layout, s = Layout(self), self.s
271
272        # set window
273        self.setSize(9*layout.XS+layout.XM, 4*layout.YS+layout.YM)
274
275        # create stacks
276        for i in range(7):
277            x = layout.XM + (8-i) * layout.XS // 2
278            y = layout.YM + i * layout.YS // 2
279            for j in range(i+1):
280                s.rows.append(Elevator_RowStack(x, y, self))
281                x = x + layout.XS
282        x, y = layout.XM, layout.YM
283        s.talon = Golf_Talon(x, y, self, max_rounds=1)
284        layout.createText(s.talon, "s")
285        x = x + layout.XS
286        s.waste = self.Waste_Class(x, y, self)
287        layout.createText(s.waste, "s")
288        # the Waste is also our only Foundation in this game
289        s.foundations.append(s.waste)
290
291        # define stack-groups (non default)
292        self.sg.openstacks = [s.waste]
293        self.sg.talonstacks = [s.talon]
294        self.sg.dropstacks = s.rows
295
296    #
297    # game overrides
298    #
299
300    def startGame(self):
301        self.startDealSample()
302        self.s.talon.dealRow(rows=self.s.rows[:21], flip=0)
303        self.s.talon.dealRow(rows=self.s.rows[21:])
304        self.s.talon.dealCards()          # deal first card to WasteStack
305
306
307class Escalator(pysollib.game.StartDealRowAndCards, Elevator):
308    pass
309
310
311# ************************************************************************
312# * Black Hole
313# ************************************************************************
314
315class BlackHole_Foundation(AbstractFoundationStack):
316    def acceptsCards(self, from_stack, cards):
317        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
318            return False
319        # check the rank
320        if self.cards:
321            r1, r2 = self.cards[-1].rank, cards[0].rank
322            return (r1 + 1) % self.cap.mod == r2 or \
323                (r2 + 1) % self.cap.mod == r1
324        return True
325
326    def getHelp(self):
327        return _('Foundation. Build up or down regardless of suit.')
328
329
330class BlackHole_RowStack(ReserveStack):
331    def clickHandler(self, event):
332        return self.doubleclickHandler(event)
333
334    def getHelp(self):
335        return _('Tableau. No building.')
336
337
338class BlackHole(Game):
339    RowStack_Class = StackWrapper(
340        BlackHole_RowStack, max_accept=0, max_cards=3)
341    Hint_Class = Golf_Hint
342    Solver_Class = BlackHoleSolverWrapper(preset='black_hole')
343
344    #
345    # game layout
346    #
347
348    def createGame(self, playcards=5):
349        # create layout
350        layout, s = Layout(self), self.s
351
352        # set window
353        w = max(2*layout.XS, layout.XS+(playcards-1)*layout.XOFFSET)
354        self.setSize(layout.XM + 5*w, layout.YM + 4*layout.YS)
355
356        # create stacks
357        y = layout.YM
358        for i in range(5):
359            x = layout.XM + i*w
360            s.rows.append(self.RowStack_Class(x, y, self))
361        for i in range(2):
362            y = y + layout.YS
363            for j in (0, 1, 3, 4):
364                x = layout.XM + j*w
365                s.rows.append(self.RowStack_Class(x, y, self))
366        y = y + layout.YS
367        for i in range(4):
368            x = layout.XM + i*w
369            s.rows.append(self.RowStack_Class(x, y, self))
370        for r in s.rows:
371            r.CARD_XOFFSET = layout.XOFFSET
372            r.CARD_YOFFSET = 0
373        x, y = layout.XM + 2*w, layout.YM + 3*layout.YS//2
374        s.foundations.append(BlackHole_Foundation(x, y, self, suit=ANY_SUIT,
375                             dir=0, mod=13, max_move=0, max_cards=52))
376        layout.createText(s.foundations[0], "s")
377        x, y = layout.XM + 4*w, self.height - layout.YS
378        s.talon = InitialDealTalonStack(x, y, self)
379
380        # define stack-groups
381        layout.defaultStackGroups()
382
383    #
384    # game overrides
385    #
386
387    def _shuffleHook(self, cards):
388        # move Ace to bottom of the Talon (i.e. last cards to be dealt)
389        return self._shuffleHookMoveToBottom(
390            cards, lambda c: (c.id == 13, c.suit), 1)
391
392    def startGame(self):
393        self._startDealNumRows(2)
394        self.s.talon.dealRow()
395        self.s.talon.dealRow(rows=self.s.foundations)
396
397    def getAutoStacks(self, event=None):
398        if event is None:
399            # disable auto drop - this would ruin the whole gameplay
400            return ((), (), self.sg.dropstacks)
401        else:
402            # rightclickHandler
403            return ((), self.sg.dropstacks, self.sg.dropstacks)
404
405
406# ************************************************************************
407# * Four Leaf Clovers
408# ************************************************************************
409
410class FourLeafClovers_Foundation(AbstractFoundationStack):
411    def acceptsCards(self, from_stack, cards):
412        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
413            return False
414        # check the rank
415        if self.cards:
416            r1, r2 = self.cards[-1].rank, cards[0].rank
417            return (r1 + 1) % self.cap.mod == r2
418        return True
419
420    def getHelp(self):
421        return _('Foundation. Build up regardless of suit.')
422
423
424class FourLeafClovers(Game):
425
426    Hint_Class = CautiousDefaultHint
427
428    #
429    # game layout
430    #
431
432    def createGame(self):
433        # create layout
434        layout, s = Layout(self), self.s
435
436        # set window
437        h = layout.YS + 6*layout.YOFFSET
438        self.setSize(layout.XM + 7*layout.XS, layout.YM + 2*h)
439
440        # create stacks
441        y = layout.YM
442        for i in range(7):
443            x = layout.XM + i*layout.XS
444            s.rows.append(
445                UD_RK_RowStack(x, y, self, mod=13, base_rank=NO_RANK))
446        y = layout.YM+h
447        for i in range(6):
448            x = layout.XM + i*layout.XS
449            s.rows.append(
450                UD_RK_RowStack(x, y, self, mod=13, base_rank=NO_RANK))
451        stack = FourLeafClovers_Foundation(
452            layout.XM+6*layout.XS, self.height-layout.YS, self,
453            suit=ANY_SUIT, dir=0, mod=13,
454            max_move=0, max_cards=52)
455        s.foundations.append(stack)
456        layout.createText(stack, 'n')
457        x, y = layout.XM + 7*layout.XS, self.height - layout.YS
458        s.talon = InitialDealTalonStack(x, y, self)
459
460        # define stack-groups
461        layout.defaultStackGroups()
462
463    def startGame(self):
464        self._startDealNumRowsAndDealSingleRow(3)
465
466    shallHighlightMatch = Game._shallHighlightMatch_RKW
467
468
469# ************************************************************************
470# * All in a Row
471# ************************************************************************
472
473class AllInARow(BlackHole):
474
475    Solver_Class = BlackHoleSolverWrapper(preset='all_in_a_row')
476
477    def createGame(self):
478        # create layout
479        layout, s = Layout(self), self.s
480
481        # set window
482        h = layout.YM+layout.YS+4*layout.YOFFSET
483        self.setSize(layout.XM+7*layout.XS, 3*layout.YM+2*h+layout.YS)
484
485        # create stacks
486        x, y = layout.XM, layout.YM
487        for i in range(7):
488            s.rows.append(self.RowStack_Class(x, y, self))
489            x += layout.XS
490        x, y = layout.XM, layout.YM+h
491        for i in range(6):
492            s.rows.append(self.RowStack_Class(x, y, self))
493            x += layout.XS
494        for r in s.rows:
495            r.CARD_XOFFSET, r.CARD_YOFFSET = 0, layout.YOFFSET
496
497        x, y = layout.XM, self.height-layout.YS
498        stack = BlackHole_Foundation(
499            x, y, self, ANY_SUIT, dir=0, mod=13, max_move=0, max_cards=52,
500            base_rank=ANY_RANK)
501        s.foundations.append(stack)
502        stack.CARD_XOFFSET, stack.CARD_YOFFSET = (self.width-layout.XS)//51, 0
503        layout.createText(stack, 'n')
504        x = self.width-layout.XS
505        s.talon = InitialDealTalonStack(x, y, self)
506
507        # define stack-groups
508        layout.defaultStackGroups()
509
510    def startGame(self):
511        self._startDealNumRowsAndDealSingleRow(3)
512
513
514# ************************************************************************
515# * Robert
516# * Wasatch
517# ************************************************************************
518
519class Robert(Game):
520
521    def createGame(self, max_rounds=3, num_deal=1):
522        layout, s = Layout(self), self.s
523        self.setSize(layout.XM+4*layout.XS, layout.YM+2*layout.YS)
524        x, y = layout.XM+3*layout.XS//2, layout.YM
525        stack = BlackHole_Foundation(x, y, self, ANY_SUIT,
526                                     dir=0, mod=13, max_move=0, max_cards=52)
527        s.foundations.append(stack)
528        layout.createText(stack, 'ne')
529        x, y = layout.XM+layout.XS, layout.YM+layout.YS
530        s.talon = WasteTalonStack(x, y, self,
531                                  max_rounds=max_rounds, num_deal=num_deal)
532        layout.createText(s.talon, 'nw')
533        if max_rounds > 0:
534            layout.createRoundText(self.s.talon, 'se', dx=layout.XS)
535        x += layout.XS
536        s.waste = WasteStack(x, y, self)
537        layout.createText(s.waste, 'ne')
538
539        # define stack-groups
540        layout.defaultStackGroups()
541
542    def startGame(self):
543        self.startDealSample()
544        self.s.talon.dealRow(rows=self.s.foundations)
545        self.s.talon.dealCards()
546
547
548class Wasatch(Robert):
549
550    def createGame(self):
551        Robert.createGame(self, max_rounds=UNLIMITED_REDEALS, num_deal=3)
552
553    def startGame(self):
554        Robert.startGame(self)
555
556# ************************************************************************
557# * Uintah
558# ************************************************************************
559
560
561class Uintah_Foundation(AbstractFoundationStack):
562    def acceptsCards(self, from_stack, cards):
563        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
564            return False
565        if (self.cards[-1].color != cards[0].color):
566            return False
567        # check the rank
568        if self.cards:
569            r1, r2 = self.cards[-1].rank, cards[0].rank
570            return (r1 + 1) % self.cap.mod == r2 or \
571                (r2 + 1) % self.cap.mod == r1
572        return True
573
574    def getHelp(self):
575        return _('Foundation. Build up or down by same color.')
576
577
578class Uintah(Game):
579
580    def createGame(self):
581        layout, s = Layout(self), self.s
582        self.setSize(layout.XM + 4 * layout.XS, layout.YM + 2 * layout.YS)
583        x, y = layout.XM, layout.YM
584        for i in range(4):
585            s.foundations.append(Uintah_Foundation(x, y, self,
586                                 suit=ANY_SUIT, dir=0, mod=13,
587                                 max_cards=52, max_move=0))
588            x += layout.XS
589        x, y = layout.XM + layout.XS, layout.YM + layout.YS
590        s.talon = WasteTalonStack(x, y, self,
591                                  max_rounds=UNLIMITED_REDEALS, num_deal=3)
592        layout.createText(s.talon, 'nw')
593        x += layout.XS
594        s.waste = WasteStack(x, y, self)
595        layout.createText(s.waste, 'ne')
596
597        # define stack-groups
598        layout.defaultStackGroups()
599
600    def _shuffleHook(self, cards):
601        suits = []
602        top_cards = []
603        for c in cards[:]:
604            if c.suit not in suits:
605                suits.append(c.suit)
606                top_cards.append(c)
607                cards.remove(c)
608            if len(suits) == 4:
609                break
610        top_cards.sort(key=lambda x: -x.suit)  # sort by suit
611        return cards + top_cards
612
613    def startGame(self):
614        self.startDealSample()
615        self.s.talon.dealRow(rows=self.s.foundations)
616        self.s.talon.dealCards()
617
618
619# ************************************************************************
620# * Diamond Mine
621# ************************************************************************
622
623class DiamondMine_RowStack(RK_RowStack):
624    def acceptsCards(self, from_stack, cards):
625        if not RK_RowStack.acceptsCards(self, from_stack, cards):
626            return False
627        if cards[0].suit == DIAMOND:
628            return False
629        if self.cards:
630            return self.cards[-1].suit != DIAMOND
631        return True
632
633
634class DiamondMine(Game):
635
636    def createGame(self):
637        layout, s = Layout(self), self.s
638        self.setSize(
639            layout.XM+13*layout.XS,
640            layout.YM+2*layout.YS+15*layout.YOFFSET)
641
642        x, y = layout.XM+6*layout.XS, layout.YM
643        s.foundations.append(SS_FoundationStack(x, y, self,
644                             base_rank=ANY_RANK, suit=DIAMOND, mod=13))
645        x, y = layout.XM, layout.YM+layout.YS
646        for i in range(13):
647            s.rows.append(DiamondMine_RowStack(x, y, self))
648            x += layout.XS
649        s.talon = InitialDealTalonStack(layout.XM, self.height-layout.YS, self)
650
651        layout.defaultAll()
652
653    def startGame(self):
654        for i in range(3):
655            self.s.talon.dealRow(flip=0, frames=0)
656        self._startAndDealRow()
657
658    def isGameWon(self):
659        if len(self.s.foundations[0].cards) != 13:
660            return False
661        for s in self.s.rows:
662            if len(s.cards) == 0:
663                continue
664            if len(s.cards) != 13:
665                return False
666            if not isSameSuitSequence(s.cards):
667                return False
668        return True
669
670    shallHighlightMatch = Game._shallHighlightMatch_RK
671
672
673# ************************************************************************
674# * Dolphin
675# ************************************************************************
676
677class Dolphin(Game):
678
679    def createGame(self, rows=8, reserves=4, playcards=6):
680        layout, s = Layout(self), self.s
681        self.setSize(
682            layout.XM+rows*layout.XS,
683            layout.YM+3*layout.YS+playcards*layout.YOFFSET)
684
685        dx = (self.width-layout.XM-(reserves+1)*layout.XS)//3
686        x, y = layout.XM+dx, layout.YM
687        for i in range(reserves):
688            s.reserves.append(ReserveStack(x, y, self))
689            x += layout.XS
690        x += dx
691        max_cards = 52*self.gameinfo.decks
692        s.foundations.append(RK_FoundationStack(x, y, self,
693                             base_rank=ANY_RANK, mod=13, max_cards=max_cards))
694        layout.createText(s.foundations[0], 'ne')
695        x, y = layout.XM, layout.YM+layout.YS
696        for i in range(rows):
697            s.rows.append(BasicRowStack(x, y, self))
698            x += layout.XS
699        s.talon = InitialDealTalonStack(layout.XM, self.height-layout.YS, self)
700
701        layout.defaultAll()
702
703    def startGame(self):
704        self._startDealNumRows(5)
705        self.s.talon.dealRow()
706        self.s.talon.dealRowAvail()
707
708
709class DoubleDolphin(Dolphin):
710
711    def createGame(self):
712        Dolphin.createGame(self, rows=10, reserves=5, playcards=10)
713
714    def startGame(self):
715        self._startDealNumRows(9)
716        self.s.talon.dealRow()
717        self.s.talon.dealRowAvail()
718
719
720# ************************************************************************
721# * Waterfall
722# ************************************************************************
723
724class Waterfall_Foundation(AbstractFoundationStack):
725    def acceptsCards(self, from_stack, cards):
726        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
727            return False
728        c1 = cards[0]
729        if not self.cards:
730            return c1.rank == ACE and c1.suit == 0
731        c2 = self.cards[-1]
732        if c2.rank == KING:
733            suit = (c2.suit+1) % 4
734            rank = ACE
735        else:
736            suit = c2.suit
737            rank = c2.rank+1
738        return c1.suit == suit and c1.rank == rank
739
740
741class Waterfall(Game):
742
743    def createGame(self):
744        rows = 8
745        layout, s = Layout(self), self.s
746        self.setSize(
747            layout.XM+rows*layout.XS, layout.YM+2*layout.YS+20*layout.YOFFSET)
748
749        x, y = layout.XM, layout.YM
750        for i in range(rows):
751            s.rows.append(RK_RowStack(x, y, self))
752            x += layout.XS
753        x, y = layout.XM+(rows-1)*layout.XS//2, self.height-layout.YS
754        s.foundations.append(Waterfall_Foundation(x, y, self, suit=ANY_SUIT,
755                                                  max_cards=104))
756        stack = s.foundations[0]
757        tx, ty, ta, tf = layout.getTextAttr(stack, 'se')
758        font = self.app.getFont('canvas_default')
759        stack.texts.misc = MfxCanvasText(self.canvas, tx, ty,
760                                         anchor=ta, font=font)
761        x, y = self.width-layout.XS, self.height-layout.YS
762        s.talon = DealRowTalonStack(x, y, self)
763        layout.createText(s.talon, 'sw')
764
765        layout.defaultStackGroups()
766
767    def startGame(self):
768        self._startDealNumRowsAndDealSingleRow(3)
769
770    def updateText(self):
771        if self.preview > 1:
772            return
773        f = self.s.foundations[0]
774        if len(f.cards) == 104:
775            t = ''
776        elif len(f.cards) == 0:
777            t = SUITS[0]
778        else:
779            c = f.cards[-1]
780            if c.rank == KING:
781                suit = (c.suit+1) % 4
782            else:
783                suit = c.suit
784            t = SUITS[suit]
785        f.texts.misc.config(text=t)
786
787    shallHighlightMatch = Game._shallHighlightMatch_RK
788
789
790# ************************************************************************
791# * Vague
792# * Thirty Two Cards
793# ************************************************************************
794
795class Vague_RowStack(BasicRowStack):
796    clickHandler = BasicRowStack.doubleclickHandler
797
798
799class Vague(Game):
800    Foundation_Classes = [StackWrapper(SS_FoundationStack,
801                                       base_rank=ANY_RANK, mod=13)]
802
803    SEPARATE_FOUNDATIONS = True
804    SPREAD_FOUNDATION = False
805
806    def createGame(self, rows=3, columns=6):
807        layout, s = Layout(self), self.s
808        decks = self.gameinfo.decks
809        maxrows = max(columns, 2+decks*4)
810        self.setSize(layout.XM+maxrows*layout.XS, layout.YM+(rows+1)*layout.YS)
811
812        x, y = layout.XM, layout.YM
813        s.talon = TalonStack(x, y, self)
814        layout.createText(s.talon, 'ne')
815
816        x, y = layout.XM+2*layout.XS, layout.YM
817        for found in self.Foundation_Classes:
818            if self.SEPARATE_FOUNDATIONS:
819                for i in range(4):
820                    s.foundations.append(found(x, y, self, suit=i))
821                    x += layout.XS
822            else:
823                s.foundations.append(found(x, y, self, suit=ANY_SUIT))
824                if self.SPREAD_FOUNDATION:
825                    w1, w2 = 6 * layout.XS + layout.XM, 2 * layout.XS
826
827                    totalcards = self.gameinfo.ncards
828                    if w2 + totalcards * layout.XOFFSET > w1:
829                        layout.XOFFSET = int((w1 - w2) / totalcards)
830                    s.foundations[0].CARD_XOFFSET = layout.XOFFSET
831
832        y = layout.YM+layout.YS
833        for i in range(rows):
834            x = layout.XM + (maxrows-columns)*layout.XS//2
835            for j in range(columns):
836                s.rows.append(Vague_RowStack(x, y, self))
837                x += layout.XS
838            y += layout.YS
839
840        layout.defaultStackGroups()
841
842    def startGame(self):
843        self.startDealSample()
844        self.s.talon.dealRow()
845        self.s.talon.flipMove()
846
847    def fillStack(self, stack):
848        if stack in self.s.rows and not stack.cards:
849            if self.s.talon.cards:
850                old_state = self.enterState(self.S_FILL)
851                if not self.s.talon.cards[-1].face_up:
852                    self.s.talon.flipMove()
853                self.s.talon.moveMove(1, stack)
854                self.leaveState(old_state)
855
856    def getAutoStacks(self, event=None):
857        if event is None:
858            # disable auto drop - this would ruin the whole gameplay
859            return ((), (), self.sg.dropstacks)
860        else:
861            # rightclickHandler
862            return ((), self.sg.dropstacks, self.sg.dropstacks)
863
864
865class ThirtyTwoCards(Vague):
866    Foundation_Classes = [
867        SS_FoundationStack,
868        StackWrapper(SS_FoundationStack, base_rank=KING, dir=-1)]
869
870    def createGame(self):
871        Vague.createGame(self, rows=4, columns=8)
872
873    def startGame(self):
874        self._startAndDealRow()
875
876
877# ************************************************************************
878# * Sticko
879# ************************************************************************
880
881class Sticko_Foundation(AbstractFoundationStack):
882    def acceptsCards(self, from_stack, cards):
883        r1, r2 = self.cards[-1].rank, cards[0].rank
884        s1, s2 = self.cards[-1].suit, cards[0].suit
885        c1, c2 = self.cards[-1].color, cards[0].color
886
887        # Increase rank, same suit
888        if ((r2 == r1 + 1 or (r2 == ACE and r1 == KING)
889             or (r2 == 6 and r1 == ACE)) and s1 == s2):
890            return True
891
892        # Decrease rank, different suit but same color
893        if ((r1 == r2 + 1 or (r1 == ACE and r2 == KING)
894             or (r1 == 6 and r2 == ACE)) and s1 != s2 and c1 == c2):
895            return True
896
897        # Same rank, different color
898        if r1 == r2 and c1 != c2:
899            return True
900
901        return False
902
903
904class Sticko(Vague):
905    Foundation_Classes = [StackWrapper(Sticko_Foundation,
906                                       max_cards=32, )]
907    SEPARATE_FOUNDATIONS = False
908    SPREAD_FOUNDATION = True
909
910    def createGame(self):
911        Vague.createGame(self, rows=2, columns=8)
912
913    def startGame(self):
914        self._startAndDealRow()
915        self.s.talon.flipMove()
916        self.s.talon.moveMove(1, self.s.foundations[0])
917
918
919# ************************************************************************
920# * Devil's Solitaire
921# ************************************************************************
922
923class DevilsSolitaire_Foundation(RK_FoundationStack):
924    def acceptsCards(self, from_stack, cards):
925        if not RK_FoundationStack.acceptsCards(self, from_stack, cards):
926            return False
927        if self.cards:
928            return True
929        if self.game.s.reserves[0].cards:
930            c = self.game.s.reserves[0].cards[-1]
931            return (c.rank+1) % 13 == cards[-1].rank
932        return True
933
934
935class DevilsSolitaire_WasteStack(WasteStack):
936    clickHandler = WasteStack.doubleclickHandler
937
938
939class DevilsSolitaire(Game):
940
941    def createGame(self):
942        layout, s = Layout(self), self.s
943        self.setSize(
944            layout.XM+9*layout.XS,
945            layout.YM+3*layout.YS+7*layout.YOFFSET+2*layout.TEXT_HEIGHT)
946
947        x, y = layout.XM+4*layout.XS, layout.YM
948        stack = DevilsSolitaire_Foundation(
949            x, y, self, suit=ANY_SUIT, base_rank=ANY_RANK, mod=13)
950        tx, ty, ta, tf = layout.getTextAttr(stack, 'nw')
951        font = self.app.getFont('canvas_default')
952        stack.texts.misc = MfxCanvasText(self.canvas, tx, ty,
953                                         anchor=ta, font=font)
954        s.foundations.append(stack)
955
956        x, y = self.width-layout.XS, layout.YM
957        stack = AbstractFoundationStack(
958            x, y, self,
959            suit=ANY_SUIT, max_move=0, max_cards=104,
960            max_accept=0, base_rank=ANY_RANK)
961        layout.createText(stack, 'nw')
962        s.foundations.append(stack)
963
964        x, y = layout.XM, layout.YM+layout.YS
965        for i in range(4):
966            s.rows.append(Vague_RowStack(x, y, self))
967            x += layout.XS
968        x += layout.XS
969        for i in range(4):
970            s.rows.append(Vague_RowStack(x, y, self))
971            x += layout.XS
972
973        x, y = layout.XM+4*layout.XS, layout.YM+layout.YS
974        stack = OpenStack(x, y, self)
975        stack.CARD_YOFFSET = layout.YOFFSET
976        s.reserves.append(stack)
977
978        x, y = layout.XM+4.5*layout.XS, self.height-layout.YS
979        s.talon = WasteTalonStack(x, y, self, max_rounds=3)
980        layout.createText(s.talon, 'n')
981        layout.createRoundText(s.talon, 'nnn')
982        x -= layout.XS
983        s.waste = DevilsSolitaire_WasteStack(x, y, self)
984        layout.createText(s.waste, 'n')
985
986        layout.defaultStackGroups()
987
988    def startGame(self):
989        for i in range(8):
990            self.s.talon.dealRow(rows=self.s.reserves, frames=0)
991        self.startDealSample()
992        self.s.talon.dealRow()
993        self.s.talon.dealCards()
994
995    def fillStack(self, stack):
996        old_state = self.enterState(self.S_FILL)
997        if stack in self.s.rows and not stack.cards:
998            if not self.s.waste.cards:
999                self.s.talon.dealCards()
1000            if self.s.waste.cards:
1001                self.s.waste.moveMove(1, stack)
1002        f0 = self.s.foundations[0]
1003        if len(f0.cards) == 12:
1004            self.moveMove(1, self.s.reserves[0], f0, frames=4)
1005            f1 = self.s.foundations[1]
1006            for i in range(13):
1007                self.moveMove(1, f0, f1, frames=4)
1008        self.leaveState(old_state)
1009
1010    def updateText(self):
1011        if self.preview > 1:
1012            return
1013        f = self.s.foundations[0]
1014        r = self.s.reserves[0]
1015        if not r.cards:
1016            t = ''
1017        else:
1018            c = r.cards[-1]
1019            t = RANKS[(c.rank+1) % 13]
1020        f.texts.misc.config(text=t)
1021
1022
1023# ************************************************************************
1024# * Three Fir-trees
1025# ************************************************************************
1026
1027class ThreeFirTrees_RowStack(Golf_RowStack):
1028    def __init__(self, x, y, game):
1029        Golf_RowStack.__init__(self, x, y, game, max_accept=0, max_cards=1)
1030        self.CARD_YOFFSET = 0
1031        self.blockmap = []
1032
1033    def basicIsBlocked(self):
1034        for r in self.blockmap:
1035            if r.cards:
1036                return True
1037        return False
1038
1039    getBottomImage = Stack._getNoneBottomImage
1040
1041
1042class FirTree_GameMethods:
1043    def _createFirTree(self, layout, x0, y0):
1044        rows = []
1045        # create stacks
1046        for i in range(11):
1047            x = x0 + ((i+1) % 2) * layout.XS // 2
1048            y = y0 + i * layout.YS // 4
1049            for j in range((i % 2) + 1):
1050                rows.append(ThreeFirTrees_RowStack(x, y, self))
1051                x += layout.XS
1052        # compute blocking
1053        n = 0
1054        for i in range(10):
1055            if i % 2:
1056                rows[n].blockmap = [rows[n+2]]
1057                rows[n+1].blockmap = [rows[n+2]]
1058                n += 2
1059            else:
1060                rows[n].blockmap = [rows[n+1], rows[n+2]]
1061                n += 1
1062        return rows
1063
1064
1065class ThreeFirTrees(Golf, FirTree_GameMethods):
1066    Hint_Class = CautiousDefaultHint
1067    Waste_Class = Golf_Waste
1068
1069    def createGame(self):
1070
1071        layout, s = Layout(self), self.s
1072        self.setSize(
1073            layout.XM+max(7*layout.XS, 2*layout.XS+26*layout.XOFFSET),
1074            layout.YM+5*layout.YS)
1075
1076        x0, y0 = (self.width-7*layout.XS)//2, layout.YM
1077        for i in range(3):
1078            s.rows += self._createFirTree(layout, x0, y0)
1079            x0 += 2.5*layout.XS
1080
1081        x, y = layout.XM, self.height - layout.YS
1082        s.talon = Golf_Talon(x, y, self, max_rounds=1)
1083        layout.createText(s.talon, 'n')
1084        x += layout.XS
1085        s.waste = self.Waste_Class(x, y, self)
1086        s.waste.CARD_XOFFSET = layout.XOFFSET//4
1087        layout.createText(s.waste, 'n')
1088        # the Waste is also our only Foundation in this game
1089        s.foundations.append(s.waste)
1090
1091        # define stack-groups (non default)
1092        self.sg.openstacks = [s.waste]
1093        self.sg.talonstacks = [s.talon]
1094        self.sg.dropstacks = s.rows
1095
1096    def startGame(self):
1097        self.startDealSample()
1098        self.s.talon.dealRow(frames=4)
1099        self.s.talon.dealCards()
1100
1101
1102class RelaxedThreeFirTrees(ThreeFirTrees):
1103    Waste_Class = StackWrapper(Golf_Waste, mod=13)
1104
1105
1106# ************************************************************************
1107# * Napoleon Takes Moscow
1108# * Napoleon Leaves Moscow
1109# ************************************************************************
1110
1111class NapoleonTakesMoscow(Game, FirTree_GameMethods):
1112    RowStack_Class = StackWrapper(SS_RowStack, base_rank=KING, max_move=1)
1113    Hint_Class = CautiousDefaultHint
1114
1115    def createGame(self):
1116
1117        layout, s = Layout(self), self.s
1118        self.setSize(
1119            layout.XM+10*layout.XS, layout.YM+3*layout.YS+15*layout.YOFFSET)
1120
1121        x, y = layout.XM+layout.XS, layout.YM
1122        for i in range(8):
1123            s.foundations.append(SS_FoundationStack(x, y, self, suit=i//2))
1124            x += layout.XS
1125
1126        x, y = layout.XM, layout.YM+layout.YS
1127        for i in range(2):
1128            for j in range(4):
1129                s.rows.append(self.RowStack_Class(x, y, self))
1130                x += layout.XS
1131            x += 2*layout.XS
1132
1133        x, y = layout.XM+4*layout.XS, layout.YM+layout.YS
1134        s.reserves += self._createFirTree(layout, x, y)
1135
1136        x, y = layout.XM, self.height-layout.YS
1137        s.talon = WasteTalonStack(x, y, self, max_rounds=3)
1138        layout.createText(s.talon, 'n')
1139        layout.createRoundText(s.talon, 'nnn')
1140        x += layout.XS
1141        s.waste = WasteStack(x, y, self)
1142        layout.createText(s.waste, 'n')
1143
1144        # define stack-groups
1145        layout.defaultStackGroups()
1146
1147    def startGame(self):
1148        self.s.talon.dealRow(rows=self.s.reserves, frames=0)
1149        self._startDealNumRowsAndDealRowAndCards(3)
1150
1151    shallHighlightMatch = Game._shallHighlightMatch_SS
1152
1153
1154class NapoleonLeavesMoscow(NapoleonTakesMoscow):
1155    RowStack_Class = StackWrapper(SS_RowStack, base_rank=KING)
1156    Hint_Class = DefaultHint
1157
1158    def startGame(self):
1159        self.s.talon.dealRow(rows=self.s.reserves, frames=0)
1160        self._startDealNumRowsAndDealRowAndCards(4)
1161
1162
1163# ************************************************************************
1164# * Flake
1165# * Flake (2 decks)
1166# ************************************************************************
1167
1168
1169class Flake(Game):
1170    Hint_Class = FourByFour_Hint  # CautiousDefaultHint
1171
1172    def createGame(self, rows=6, playcards=18):
1173        # create layout
1174        layout, s = Layout(self), self.s
1175
1176        # set window
1177        self.setSize(
1178            layout.XM + rows*layout.XS,
1179            layout.YM + 2*layout.YS + playcards*layout.XOFFSET)
1180
1181        # create stacks
1182        x, y, = layout.XM, layout.YM+layout.YS
1183        for i in range(rows):
1184            s.rows.append(UD_RK_RowStack(x, y, self, mod=13))
1185            x += layout.XS
1186
1187        x, y = layout.XM + (rows-1)*layout.XS//2, layout.YM
1188        stack = BlackHole_Foundation(x, y, self, max_move=0, suit=ANY_SUIT,
1189                                     base_rank=ANY_RANK, dir=0, mod=13,
1190                                     max_cards=52*self.gameinfo.decks)
1191        s.foundations.append(stack)
1192        layout.createText(stack, 'ne')
1193
1194        x, y = layout.XM, self.height-layout.YS
1195        s.talon = InitialDealTalonStack(x, y, self)
1196
1197        # define stack-groups
1198        layout.defaultStackGroups()
1199
1200    def startGame(self):
1201        self._startDealNumRows(7)
1202        self.s.talon.dealRow()
1203        self.s.talon.dealRowAvail()
1204
1205    shallHighlightMatch = Game._shallHighlightMatch_RKW
1206
1207
1208class Flake2Decks(Flake):
1209    def createGame(self):
1210        Flake.createGame(self, rows=8, playcards=22)
1211
1212    def startGame(self):
1213        self._startDealNumRowsAndDealSingleRow(12)
1214
1215
1216# ************************************************************************
1217# * Beacon
1218# ************************************************************************
1219
1220class Beacon(Game):
1221
1222    def createGame(self, rows=8):
1223        # create layout
1224        layout, s = Layout(self), self.s
1225
1226        # set window
1227        playcards = 12
1228        self.setSize(
1229            layout.XM+rows*layout.XS,
1230            layout.YM+3*layout.YS+playcards*layout.YOFFSET)
1231
1232        # create stacks
1233        x, y = layout.XM + (rows-1)*layout.XS//2, layout.YM
1234        stack = RK_FoundationStack(x, y, self, base_rank=ANY_RANK,
1235                                   max_cards=52, mod=13)
1236        s.foundations.append(stack)
1237        layout.createText(stack, 'ne')
1238
1239        x, y = layout.XM, layout.YM+layout.YS
1240        for i in range(rows):
1241            s.rows.append(RK_RowStack(x, y, self, base_rank=NO_RANK, mod=13))
1242            x += layout.XS
1243
1244        x, y = layout.XM, self.height-layout.YS
1245        s.talon = TalonStack(x, y, self)
1246        layout.createText(s.talon, 'se')
1247
1248        # define stack-groups
1249        layout.defaultStackGroups()
1250
1251    def startGame(self):
1252        self._startDealNumRowsAndDealSingleRow(3)
1253
1254    def fillStack(self, stack):
1255        if stack in self.s.rows and not stack.cards:
1256            if self.s.talon.cards:
1257                old_state = self.enterState(self.S_FILL)
1258                self.s.talon.flipMove()
1259                self.s.talon.moveMove(1, stack)
1260                self.leaveState(old_state)
1261
1262    shallHighlightMatch = Game._shallHighlightMatch_RKW
1263
1264
1265# register the game
1266registerGame(GameInfo(36, Golf, "Golf",
1267                      GI.GT_GOLF, 1, 0, GI.SL_BALANCED))
1268registerGame(GameInfo(259, DeadKingGolf, "Dead King Golf",
1269                      GI.GT_GOLF, 1, 0, GI.SL_BALANCED))
1270registerGame(GameInfo(260, RelaxedGolf, "Relaxed Golf",
1271                      GI.GT_GOLF | GI.GT_RELAXED, 1, 0, GI.SL_BALANCED,
1272                      altnames=("Putt Putt",)))
1273registerGame(GameInfo(40, Elevator, "Elevator",
1274                      GI.GT_GOLF, 1, 0, GI.SL_BALANCED,
1275                      altnames=("Egyptian Solitaire", "Pyramid Golf")))
1276registerGame(GameInfo(98, BlackHole, "Black Hole",
1277                      GI.GT_GOLF | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL))
1278registerGame(GameInfo(267, FourLeafClovers, "Four Leaf Clovers",
1279                      GI.GT_GOLF | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL))
1280registerGame(GameInfo(281, Escalator, "Escalator",
1281                      GI.GT_GOLF, 1, 0, GI.SL_BALANCED))
1282registerGame(GameInfo(405, AllInARow, "All in a Row",
1283                      GI.GT_GOLF | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL))
1284registerGame(GameInfo(432, Robert, "Robert",
1285                      GI.GT_GOLF, 1, 2, GI.SL_LUCK))
1286registerGame(GameInfo(551, DiamondMine, "Diamond Mine",
1287                      GI.GT_1DECK_TYPE, 1, 0, GI.SL_BALANCED))
1288registerGame(GameInfo(661, Dolphin, "Dolphin",
1289                      GI.GT_GOLF | GI.GT_OPEN | GI.GT_ORIGINAL, 1, 0,
1290                      GI.SL_MOSTLY_SKILL))
1291registerGame(GameInfo(662, DoubleDolphin, "Double Dolphin",
1292                      GI.GT_GOLF | GI.GT_OPEN | GI.GT_ORIGINAL, 2, 0,
1293                      GI.SL_MOSTLY_SKILL))
1294registerGame(GameInfo(709, Waterfall, "Waterfall",
1295                      GI.GT_2DECK_TYPE | GI.GT_ORIGINAL, 2, 0,
1296                      GI.SL_MOSTLY_SKILL))
1297registerGame(GameInfo(720, Vague, "Vague",
1298                      GI.GT_1DECK_TYPE, 1, 0, GI.SL_MOSTLY_LUCK))
1299registerGame(GameInfo(723, DevilsSolitaire, "Devil's Solitaire",
1300                      GI.GT_2DECK_TYPE, 2, 2, GI.SL_BALANCED,
1301                      altnames=('Banner',)))
1302registerGame(GameInfo(728, ThirtyTwoCards, "Thirty Two Cards",
1303                      GI.GT_2DECK_TYPE, 2, 0, GI.SL_LUCK))
1304registerGame(GameInfo(731, ThreeFirTrees, "Three Fir-trees",
1305                      GI.GT_GOLF, 2, 0, GI.SL_BALANCED))
1306registerGame(GameInfo(733, NapoleonTakesMoscow, "Napoleon Takes Moscow",
1307                      GI.GT_2DECK_TYPE, 2, 2, GI.SL_BALANCED))
1308registerGame(GameInfo(734, NapoleonLeavesMoscow, "Napoleon Leaves Moscow",
1309                      GI.GT_2DECK_TYPE, 2, 2, GI.SL_BALANCED,
1310                      altnames=("Napoleon at Friedland",)))
1311registerGame(GameInfo(749, Flake, "Flake",
1312                      GI.GT_GOLF | GI.GT_OPEN | GI.GT_ORIGINAL,
1313                      1, 0, GI.SL_MOSTLY_SKILL))
1314registerGame(GameInfo(750, Flake2Decks, "Flake (2 decks)",
1315                      GI.GT_GOLF | GI.GT_OPEN | GI.GT_ORIGINAL,
1316                      2, 0, GI.SL_MOSTLY_SKILL))
1317registerGame(GameInfo(763, Wasatch, "Wasatch",
1318                      GI.GT_GOLF, 1, UNLIMITED_REDEALS,
1319                      GI.SL_MOSTLY_LUCK))
1320registerGame(GameInfo(764, Beacon, "Beacon",
1321                      GI.GT_1DECK_TYPE | GI.GT_ORIGINAL, 1, 0,
1322                      GI.SL_MOSTLY_SKILL))
1323registerGame(GameInfo(768, RelaxedThreeFirTrees, "Relaxed Three Fir-trees",
1324                      GI.GT_GOLF | GI.GT_RELAXED, 2, 0, GI.SL_BALANCED))
1325registerGame(GameInfo(777, DoubleGolf, "Double Golf",
1326                      GI.GT_GOLF, 2, 0, GI.SL_BALANCED))
1327registerGame(GameInfo(783, Uintah, "Uintah",
1328                      GI.GT_GOLF, 1, UNLIMITED_REDEALS,
1329                      GI.SL_MOSTLY_LUCK))
1330registerGame(GameInfo(812, Sticko, "Sticko",
1331                      GI.GT_1DECK_TYPE, 1, 0, GI.SL_BALANCED,
1332                      ranks=(0, 6, 7, 8, 9, 10, 11, 12)))
1333