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 math
25
26from pysollib.game import Game
27from pysollib.gamedb import GI, GameInfo, registerGame
28from pysollib.hint import CautiousDefaultHint, DefaultHint
29from pysollib.layout import Layout
30from pysollib.mfxutil import kwdefault
31from pysollib.mygettext import _
32from pysollib.pysoltk import MfxCanvasText
33from pysollib.stack import \
34        AbstractFoundationStack, \
35        BasicRowStack, \
36        DealRowRedealTalonStack, \
37        OpenStack, \
38        ReserveStack, \
39        SS_FoundationStack, \
40        SS_RowStack, \
41        Stack, \
42        StackWrapper, \
43        WasteStack, \
44        WasteTalonStack
45from pysollib.util import ACE, KING, NO_RANK, RANKS, UNLIMITED_CARDS
46
47
48class Braid_Hint(DefaultHint):
49    # FIXME: demo is not too clever in this game
50    pass
51
52# ************************************************************************
53# *
54# ************************************************************************
55
56
57class Braid_Foundation(AbstractFoundationStack):
58    def __init__(self, x, y, game, suit, **cap):
59        kwdefault(cap, mod=13, dir=0, base_rank=NO_RANK, max_move=0)
60        AbstractFoundationStack.__init__(self, x, y, game, suit, **cap)
61
62    def acceptsCards(self, from_stack, cards):
63        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
64            return False
65        if not self.cards:
66            return True
67        stack_dir = self.game.getFoundationDir()
68        if stack_dir == 0:
69            card_dir = self.getRankDir(cards=(self.cards[-1], cards[0]))
70            return card_dir in (1, -1)
71        else:
72            return ((self.cards[-1].rank + stack_dir) %
73                    self.cap.mod == cards[0].rank)
74
75
76class Braid_BraidStack(OpenStack):
77    def __init__(self, x, y, game, sine=0):
78        OpenStack.__init__(self, x, y, game)
79        self.CARD_YOFFSET = self.game.app.images.CARD_YOFFSET
80        CW = self.game.app.images.CARDW
81        if sine:
82            # use a sine wave for the x offsets
83            self.CARD_XOFFSET = []
84            n = 9
85            dx = 0.4 * CW * (2*math.pi/n)
86            last_x = 0
87            for i in range(n):
88                x = int(round(dx * math.sin(i + 1)))
89                # print x, x - last_x
90                self.CARD_XOFFSET.append(x - last_x)
91                last_x = x
92        else:
93            self.CARD_XOFFSET = (-0.45*CW, 0.35*CW, 0.55*CW, -0.45*CW)
94
95
96class Braid_RowStack(ReserveStack):
97    def fillStack(self):
98        if not self.cards and self.game.s.braid.cards:
99            self.game.moveMove(1, self.game.s.braid, self)
100
101    getBottomImage = Stack._getBraidBottomImage
102
103
104class Braid_ReserveStack(ReserveStack):
105    def acceptsCards(self, from_stack, cards):
106        if from_stack is self.game.s.braid or from_stack in self.game.s.rows:
107            return False
108        return ReserveStack.acceptsCards(self, from_stack, cards)
109
110    getBottomImage = Stack._getTalonBottomImage
111
112
113# ************************************************************************
114# * Braid
115# ************************************************************************
116
117class Braid(Game):
118    Hint_Class = Braid_Hint
119    Foundation_Classes = [Braid_Foundation, Braid_Foundation]
120
121    BRAID_CARDS = 20
122    RANKS = RANKS           # pull into class Braid
123
124    #
125    # game layout
126    #
127
128    def createGame(self):
129        # create layout
130        l, s = Layout(self), self.s
131        font = self.app.getFont("canvas_default")
132
133        # set window
134        # (piles up to 20 cards are playable - needed for Braid_BraidStack)
135        decks = self.gameinfo.decks
136        h = max(4*l.YS + 30, l.YS+(self.BRAID_CARDS-1)*l.YOFFSET)
137        self.setSize(l.XM+(8+decks)*l.XS, l.YM+h)
138
139        # extra settings
140        self.base_card = None
141
142        # create stacks
143        s.addattr(braid=None)      # register extra stack variable
144        x, y = l.XM, l.YM
145        for i in range(2):
146            s.rows.append(Braid_RowStack(x + 0.5*l.XS, y, self))
147            s.rows.append(Braid_RowStack(x + 4.5*l.XS, y, self))
148            y = y + 3 * l.YS
149        y = l.YM + l.YS
150        for i in range(2):
151            s.rows.append(Braid_ReserveStack(x, y, self))
152            s.rows.append(Braid_ReserveStack(x + l.XS, y, self))
153            s.rows.append(Braid_ReserveStack(x, y + l.YS, self))
154            s.rows.append(Braid_ReserveStack(x + l.XS, y + l.YS, self))
155            x = x + 4 * l.XS
156        x, y = l.XM + l.XS * 5//2, l.YM
157        s.braid = Braid_BraidStack(x, y, self)
158        x, y = l.XM + 7 * l.XS, l.YM + l.YS * 3//2
159        s.talon = WasteTalonStack(x, y, self, max_rounds=3)
160        l.createText(s.talon, "s")
161        l.createRoundText(s.talon, 'nn')
162        x -= l.XS
163        s.waste = WasteStack(x, y, self)
164        l.createText(s.waste, "s")
165        y = l.YM
166        for i in range(4):
167            x = l.XM+8*l.XS
168            for cl in self.Foundation_Classes:
169                s.foundations.append(cl(x, y, self, suit=i))
170                x += l.XS
171            y = y + l.YS
172        x = l.XM+8*l.XS+decks*l.XS//2
173        self.texts.info = MfxCanvasText(self.canvas,
174                                        x, y, anchor="n", font=font)
175
176        # define stack-groups
177        self.sg.talonstacks = [s.talon] + [s.waste]
178        self.sg.openstacks = s.foundations + s.rows
179        self.sg.dropstacks = [s.braid] + s.rows + [s.waste]
180
181    #
182    # game overrides
183    #
184
185    def _shuffleHook(self, cards):
186        # do not play a trump as the base_card
187        n = m = -1 - self.BRAID_CARDS - len(self.s.rows)
188        while cards[n].suit >= len(self.gameinfo.suits):
189            n = n - 1
190        cards[n], cards[m] = cards[m], cards[n]
191        return cards
192
193    def startGame(self):
194        self.base_card = None
195        self.updateText()
196        self.startDealSample()
197        for i in range(self.BRAID_CARDS):
198            self.s.talon.dealRow(rows=[self.s.braid], frames=4)
199        self.s.talon.dealRow(frames=4)
200        # deal base_card to foundations
201        self.base_card = self.s.talon.cards[-1]
202        to_stack = self.s.foundations[self.gameinfo.decks*self.base_card.suit]
203        self.flipMove(self.s.talon)
204        self.moveMove(1, self.s.talon, to_stack)
205        self.updateText()
206        for s in self.s.foundations:
207            s.cap.base_rank = self.base_card.rank
208        # deal first card to WasteStack
209        self.s.talon.dealCards()
210
211    shallHighlightMatch = Game._shallHighlightMatch_SSW
212
213    def getHighlightPilesStacks(self):
214        return ()
215
216    def _restoreGameHook(self, game):
217        self.base_card = self.cards[game.loadinfo.base_card_id]
218        for s in self.s.foundations:
219            s.cap.base_rank = self.base_card.rank
220
221    def _loadGameHook(self, p):
222        self.loadinfo.addattr(base_card_id=None)    # register extra load var.
223        self.loadinfo.base_card_id = p.load()
224
225    def _saveGameHook(self, p):
226        p.dump(self.base_card.id)
227
228    #
229    # game extras
230    #
231
232    def updateText(self):
233        if self.preview > 1 or not self.texts.info:
234            return
235        if not self.base_card:
236            t = ""
237        else:
238            t = self.RANKS[self.base_card.rank]
239            dir = self.getFoundationDir()
240            if dir == 1:
241                t = t + _(" Ascending")
242            elif dir == -1:
243                t = t + _(" Descending")
244        self.texts.info.config(text=t)
245
246
247class LongBraid(Braid):
248    BRAID_CARDS = 24
249
250
251# ************************************************************************
252# * Fort
253# ************************************************************************
254
255class Fort(Braid):
256
257    Foundation_Classes = [SS_FoundationStack,
258                          StackWrapper(SS_FoundationStack, base_rank=KING,
259                                       dir=-1)]
260
261    BRAID_CARDS = 21
262
263    def _shuffleHook(self, cards):
264        # move 4 Kings and 4 Aces to top of the Talon
265        # (i.e. first cards to be dealt)
266        return self._shuffleHookMoveToTop(
267            cards,
268            lambda c: (c.rank in (ACE, KING) and c.deck == 0,
269                       (c.suit, c.rank)))
270
271    def _restoreGameHook(self, game):
272        pass
273
274    def _loadGameHook(self, p):
275        pass
276
277    def _saveGameHook(self, p):
278        pass
279
280    def startGame(self):
281        self.s.talon.dealRow(rows=self.s.foundations, frames=0)
282        self.startDealSample()
283        for i in range(self.BRAID_CARDS):
284            self.s.talon.dealRow(rows=[self.s.braid], frames=4)
285        self.s.talon.dealRow(frames=4)
286        self.s.talon.dealCards()
287
288
289# ************************************************************************
290# * Backbone
291# ************************************************************************
292
293class Backbone_BraidStack(OpenStack):
294    def __init__(self, x, y, game, **cap):
295        OpenStack.__init__(self, x, y, game, **cap)
296        self.CARD_YOFFSET = self.game.app.images.CARD_YOFFSET
297
298    def basicIsBlocked(self):
299        return len(self.game.s.reserves[2].cards) != 0
300
301
302class Backbone(Game):
303    RESERVE_CARDS = 11
304
305    Hint_Class = CautiousDefaultHint
306
307    def createGame(self, rows=8):
308        # create layout
309        l, s = Layout(self), self.s
310
311        # set window
312        w, h = l.XM+(rows+2)*l.XS, max(
313            l.YM + 3 * l.XS + 10 * l.YOFFSET,
314            l.YM + 2 * l.YS + self.RESERVE_CARDS * l.YOFFSET + l.TEXT_HEIGHT)
315        self.setSize(w, h)
316
317        # create stacks
318        y = l.YM
319        for i in range(4):
320            x = l.XM+(rows-8)*l.XS//2 + i*l.XS
321            s.foundations.append(SS_FoundationStack(x, y, self, suit=i))
322            x = l.XM+(rows//2+2)*l.XS + i*l.XS
323            s.foundations.append(SS_FoundationStack(x, y, self, suit=i))
324
325        x, y = l.XM+rows*l.XS//2, l.YM
326        s.reserves.append(Backbone_BraidStack(x, y, self, max_accept=0))
327        x += l.XS
328        s.reserves.append(Backbone_BraidStack(x, y, self, max_accept=0))
329        x, y = l.XM+(rows+1)*l.XS//2, l.YM+self.RESERVE_CARDS*l.YOFFSET
330        s.reserves.append(BasicRowStack(x, y, self, max_accept=0))
331
332        x, y = l.XM, l.YM+l.YS
333        for i in range(rows//2):
334            s.rows.append(SS_RowStack(x, y, self, max_move=1))
335            x += l.XS
336        x, y = l.XM+(rows//2+2)*l.XS, l.YM+l.YS
337        for i in range(rows//2):
338            s.rows.append(SS_RowStack(x, y, self, max_move=1))
339            x += l.XS
340
341        x, y = l.XM+rows*l.XS//2, h-l.YS
342        s.talon = WasteTalonStack(x, y, self, max_rounds=1)
343        l.createText(s.talon, "n")
344        x += l.XS
345        s.waste = WasteStack(x, y, self)
346        l.createText(s.waste, "n")
347
348        # define stack-groups
349        l.defaultStackGroups()
350
351    def startGame(self):
352        for i in range(self.RESERVE_CARDS - 1):
353            self.s.talon.dealRow(rows=self.s.reserves[:2], frames=0)
354        self.s.talon.dealRow(rows=self.s.reserves, frames=0)
355        self.startDealSample()
356        self.s.talon.dealRow()
357        self.s.talon.dealCards()
358
359    shallHighlightMatch = Game._shallHighlightMatch_SS
360
361
362class BackbonePlus(Backbone):
363    def createGame(self):
364        Backbone.createGame(self, rows=10)
365
366
367class SmallBackbone(Backbone):
368    RESERVE_CARDS = 9
369
370
371# ************************************************************************
372# * Big Braid
373# ************************************************************************
374
375class BigBraid(Braid):
376    Foundation_Classes = [Braid_Foundation, Braid_Foundation, Braid_Foundation]
377
378
379# ************************************************************************
380# * Casket
381# ************************************************************************
382
383class Casket_Hint(CautiousDefaultHint):
384    def computeHints(self):
385        CautiousDefaultHint.computeHints(self)
386        if self.hints:
387            return
388        if not self.game.s.waste.cards:
389            return
390        r = self.game.s.waste.cards[-1].rank
391        if 0 <= r <= 3:
392            to_stack = self.game.s.reserves[0]
393        elif 4 <= r <= 7:
394            to_stack = self.game.s.reserves[1]
395        else:
396            to_stack = self.game.s.reserves[2]
397        self.addHint(5000, 1, self.game.s.waste, to_stack)
398
399
400class JewelsStack(OpenStack):
401    def canFlipCard(self):
402        return False
403
404
405class Casket_RowStack(SS_RowStack):
406
407    getBottomImage = Stack._getReserveBottomImage
408
409    def acceptsCards(self, from_stack, cards):
410        if not SS_RowStack.acceptsCards(self, from_stack, cards):
411            return False
412        if not self.cards:
413            # don't accepts from lid
414            return from_stack not in self.game.s.lid
415        return True
416
417
418class Casket_Reserve(ReserveStack):
419    def acceptsCards(self, from_stack, cards):
420        if not ReserveStack.acceptsCards(self, from_stack, cards):
421            return False
422        return from_stack is self.game.s.waste
423
424
425class Casket(Game):
426    Hint_Class = Casket_Hint
427
428    def createGame(self):
429        # create layout
430        l, s = Layout(self), self.s
431
432        # set window
433        self.setSize(l.XM+10*l.XS, l.YM+4.5*l.YS)
434
435        # register extra stack variables
436        s.addattr(jewels=None)
437        s.addattr(lid=[])
438
439        # create stacks
440        # Lid
441        x0, y0 = l.XM+2.5*l.XS, l.YM
442        for xx, yy in ((0, 0.5),
443                       (1, 0.25),
444                       (2, 0),
445                       (3, 0.25),
446                       (4, 0.5),
447                       ):
448            x, y = x0+xx*l.XS, y0+yy*l.YS
449            s.lid.append(BasicRowStack(x, y, self, max_accept=0))
450
451        # Casket
452        x0, y0 = l.XM+3*l.XS, l.YM+1.5*l.YS
453        for xx, yy in ((0, 0),                 (3, 0),
454                       (0, 1),                 (3, 1),
455                       (0, 2), (1, 2), (2, 2), (3, 2),
456                       ):
457            x, y = x0+xx*l.XS, y0+yy*l.YS
458            stack = Casket_RowStack(x, y, self, max_move=1)
459            stack.CARD_YOFFSET = 0
460            s.rows.append(stack)
461
462        # Reserves
463        x, y = l.XM, l.YM+1.5*l.YS
464        for i in range(3):
465            stack = Casket_Reserve(x, y, self, max_cards=UNLIMITED_CARDS)
466            l.createText(stack, "ne")
467            s.reserves.append(stack)
468            y += l.YS
469
470        # Foundations
471        x = l.XM+8*l.XS
472        for i in range(2):
473            y = l.YM
474            for j in range(4):
475                s.foundations.append(SS_FoundationStack(x, y, self, suit=j))
476                y += l.YS
477            x += l.XS
478
479        # Jewels
480        x, y = l.XM+4.5*l.XS, l.YM+2*l.YS
481        s.jewels = JewelsStack(x, y, self)
482        l.createText(s.jewels, "s")
483
484        # waste & talon
485        x, y = l.XM, l.YM
486        s.talon = WasteTalonStack(x, y, self, max_rounds=1)
487        l.createText(s.talon, "s")
488        x += l.XS
489        s.waste = WasteStack(x, y, self, max_cards=1)
490
491        # define stack-groups
492        self.sg.talonstacks = [s.talon] + [s.waste]
493        self.sg.openstacks = s.foundations + s.rows + s.reserves
494        self.sg.dropstacks = s.lid + s.rows + [s.waste] + s.reserves
495
496    def startGame(self):
497        for i in range(13):
498            self.s.talon.dealRow(rows=[self.s.jewels], frames=0, flip=0)
499        self.startDealSample()
500        self.s.talon.dealToStacksOrFoundations(stacks=self.s.lid)
501        self.s.talon.dealToStacksOrFoundations(stacks=self.s.rows)
502        self.s.talon.dealCards()
503
504    def fillStack(self, stack):
505        if not stack.cards and stack in self.s.lid:
506            if self.s.jewels.cards:
507                old_state = self.enterState(self.S_FILL)
508                self.s.jewels.flipMove()
509                self.s.jewels.moveMove(1, stack)
510                self.leaveState(old_state)
511
512    shallHighlightMatch = Game._shallHighlightMatch_SS
513
514
515# ************************************************************************
516# * Well
517# ************************************************************************
518
519class Well_TalonStack(DealRowRedealTalonStack):
520
521    def canDealCards(self):
522        return DealRowRedealTalonStack.canDealCards(
523            self, rows=self.game.s.wastes)
524
525    def dealCards(self, sound=False):
526        num_cards = 0
527        if sound and self.game.app.opt.animations:
528            self.game.startDealSample()
529        if not self.cards:
530            # move all cards to talon
531            num_cards = self._redeal(rows=self.game.s.wastes, frames=3)
532            self.game.nextRoundMove(self)
533        wastes = self.game.s.wastes[:(6-self.round)]
534        num_cards += self.dealRowAvail(rows=wastes, frames=4, sound=False)
535        if sound:
536            self.game.stopSamples()
537        return num_cards
538
539
540class Well(Game):
541    Hint_Class = CautiousDefaultHint
542
543    def createGame(self):
544        # create layout
545        l, s = Layout(self), self.s
546
547        # set window
548        self.setSize(l.XM+6*l.XS, l.YM+6*l.YS+l.TEXT_HEIGHT)
549
550        # register extra stack variables
551        s.addattr(wastes=[])
552
553        # foundations
554        suit = 0
555        x0, y0 = l.XM+1.5*l.XS, l.YM+1.5*l.YS+l.TEXT_HEIGHT
556        for xx, yy in ((3, 0),
557                       (0, 3),
558                       (3, 3),
559                       (0, 0)):
560            x, y = x0+xx*l.XS, y0+yy*l.YS
561            s.foundations.append(SS_FoundationStack(x, y, self, suit=suit,
562                                 base_rank=KING, mod=13, max_cards=26,
563                                 dir=-1, max_move=0))
564            suit += 1
565
566        # rows
567        x0, y0 = l.XM+l.XS, l.YM+l.YS+l.TEXT_HEIGHT
568        for xx, yy in ((0, 2),
569                       (2, 0),
570                       (4, 2),
571                       (2, 4)):
572            x, y = x0+xx*l.XS, y0+yy*l.YS
573            stack = SS_RowStack(x, y, self, dir=1, mod=13, max_move=1)
574            stack.getBottomImage = stack._getReserveBottomImage
575            stack.CARD_YOFFSET = 0
576            s.rows.append(stack)
577
578        # left stack
579        x, y = l.XM, l.YM+l.YS+l.TEXT_HEIGHT
580        stack = SS_RowStack(
581            x, y, self, base_rank=ACE, dir=1, mod=13, max_move=1)
582        stack.getBottomImage = stack._getReserveBottomImage
583        stack.CARD_YOFFSET = 0
584        s.rows.append(stack)
585
586        # reserves
587        x0, y0 = l.XM+2*l.XS, l.YM+2*l.YS+l.TEXT_HEIGHT
588        for xx, yy, anchor in ((0, 1, 'e'),
589                               (1, 0, 's'),
590                               (2, 1, 'w'),
591                               (1, 2, 'n')):
592            x, y = x0+xx*l.XS, y0+yy*l.YS
593            stack = OpenStack(x, y, self)
594            l.createText(stack, anchor)
595            s.reserves.append(stack)
596
597        # wastes
598        x, y = l.XM+l.XS, l.YM
599        for i in range(5):
600            stack = WasteStack(x, y, self)
601            l.createText(stack, 's', text_format='%D')
602            s.wastes.append(stack)
603            x += l.XS
604
605        # talon
606        x, y = l.XM, l.YM
607        s.talon = Well_TalonStack(x, y, self, max_rounds=5)
608        l.createText(s.talon, "s")
609
610        # define stack-groups
611        self.sg.talonstacks = [s.talon] + s.wastes
612        self.sg.openstacks = s.foundations + s.rows
613        self.sg.dropstacks = s.rows + s.wastes + s.reserves
614
615    def startGame(self):
616        for i in range(10):
617            self.s.talon.dealRow(rows=self.s.reserves, frames=0)
618        self.startDealSample()
619        self.s.talon.dealRow(rows=self.s.rows[:4])
620        self.s.talon.dealCards()
621
622    def fillStack(self, stack):
623        if not stack.cards and stack in self.s.rows[:4]:
624            indx = list(self.s.rows).index(stack)
625            r = self.s.reserves[indx]
626            if r.cards:
627                old_state = self.enterState(self.S_FILL)
628                r.moveMove(1, stack)
629                self.leaveState(old_state)
630
631    shallHighlightMatch = Game._shallHighlightMatch_SSW
632
633
634# register the game
635registerGame(GameInfo(12, Braid, "Braid",
636                      GI.GT_NAPOLEON, 2, 2, GI.SL_BALANCED,
637                      altnames=("Der Zopf", "Plait", "Pigtail")))
638registerGame(GameInfo(175, LongBraid, "Long Braid",
639                      GI.GT_NAPOLEON, 2, 2, GI.SL_BALANCED,
640                      altnames=("Der lange Zopf",)))
641registerGame(GameInfo(358, Fort, "Fort",
642                      GI.GT_NAPOLEON, 2, 2, GI.SL_BALANCED))
643registerGame(GameInfo(376, Backbone, "Backbone",
644                      GI.GT_NAPOLEON, 2, 0, GI.SL_BALANCED))
645registerGame(GameInfo(377, BackbonePlus, "Backbone +",
646                      GI.GT_NAPOLEON, 2, 0, GI.SL_BALANCED))
647registerGame(GameInfo(510, BigBraid, "Big Braid",
648                      GI.GT_NAPOLEON | GI.GT_ORIGINAL, 3, 2, GI.SL_BALANCED))
649registerGame(GameInfo(694, Casket, "Casket",
650                      GI.GT_2DECK_TYPE, 2, 0, GI.SL_BALANCED))
651registerGame(GameInfo(717, Well, "Well",
652                      GI.GT_2DECK_TYPE, 2, 4, GI.SL_BALANCED))
653registerGame(GameInfo(824, SmallBackbone, "Small Backbone",
654                      GI.GT_NAPOLEON, 2, 0, GI.SL_BALANCED))
655