1from pysollib.game import Game
2from pysollib.gamedb import GI, GameInfo, registerGame
3from pysollib.hint import DefaultHint
4from pysollib.layout import Layout
5from pysollib.mygettext import _
6from pysollib.pysoltk import MfxCanvasText, get_text_width
7from pysollib.stack import \
8        BasicRowStack, \
9        DealRowTalonStack, \
10        InitialDealTalonStack, \
11        RK_FoundationStack, \
12        Stack, \
13        StackWrapper, \
14        WasteStack, \
15        WasteTalonStack
16from pysollib.util import ANY_SUIT, KING, RANKS
17
18
19class Calculation_Hint(DefaultHint):
20    # FIXME: demo logic is a complete nonsense
21    def _getMoveWasteScore(self, score, color, r, t, pile, rpile):
22        assert r is self.game.s.waste and len(pile) == 1
23        score = 30000
24        if len(t.cards) == 0:
25            score = score - (KING - r.cards[0].rank) * 1000
26        elif t.cards[-1].rank < r.cards[0].rank:
27            score = 10000 + t.cards[-1].rank - len(t.cards)
28        elif t.cards[-1].rank == r.cards[0].rank:
29            score = 20000
30        else:
31            score = score - (t.cards[-1].rank - r.cards[0].rank) * 1000
32        return score, color
33
34
35# ************************************************************************
36# *
37# ************************************************************************
38
39class BetsyRoss_Foundation(RK_FoundationStack):
40    def updateText(self, update_empty=True):
41        if self.game.preview > 1:
42            return
43        if self.texts.misc:
44            if len(self.cards) == 0:
45                if update_empty:
46                    rank = self.cap.base_rank
47                    self.texts.misc.config(text=RANKS[rank])
48                else:
49                    self.texts.misc.config(text="")
50            elif len(self.cards) == self.cap.max_cards:
51                self.texts.misc.config(text="")
52            else:
53                rank = (self.cards[-1].rank + self.cap.dir) % self.cap.mod
54                self.texts.misc.config(text=RANKS[rank])
55
56
57class Calculation_Foundation(BetsyRoss_Foundation):
58    getBottomImage = Stack._getLetterImage
59
60
61class Calculation_RowStack(BasicRowStack):
62    def acceptsCards(self, from_stack, cards):
63        if not BasicRowStack.acceptsCards(self, from_stack, cards):
64            return False
65        # this stack accepts any one card from the Waste pile
66        return from_stack is self.game.s.waste and len(cards) == 1
67
68    getBottomImage = Stack._getReserveBottomImage
69
70    def getHelp(self):
71        return _('Tableau. Build regardless of rank and suit.')
72
73
74# ************************************************************************
75# * Calculation
76# ************************************************************************
77
78class Calculation(Game):
79    Hint_Class = Calculation_Hint
80    Foundation_Class = Calculation_Foundation
81    RowStack_Class = StackWrapper(
82        Calculation_RowStack, max_move=1, max_accept=1)
83
84    #
85    # game layout
86    #
87
88    def _getHelpText(self):
89        help = (_('''\
901: 2 3 4 5 6 7 8 9 T J Q K
912: 4 6 8 T Q A 3 5 7 9 J K
923: 6 9 Q 2 5 8 J A 4 7 T K
934: 8 Q 3 7 J 2 6 T A 5 9 K'''))
94        # calculate text_width
95        lines = help.split('\n')
96        lines.sort(key=len)
97        max_line = lines[-1]
98        text_width = get_text_width(max_line,
99                                    font=self.app.getFont("canvas_fixed"))
100        return help, text_width
101
102    def createGame(self):
103
104        # create layout
105        l, s = Layout(self, TEXT_HEIGHT=40), self.s
106        help, text_width = self._getHelpText()
107        text_width += 2*l.XM
108
109        # set window
110        w = l.XM+5.5*l.XS+text_width
111        h = max(2*l.YS, 20*l.YOFFSET)
112        self.setSize(w, l.YM + l.YS + l.TEXT_HEIGHT + h)
113
114        # create stacks
115        x0 = l.XM + l.XS * 3 // 2
116        x, y = x0, l.YM
117        for i in range(4):
118            stack = self.Foundation_Class(x, y, self,
119                                          mod=13, dir=i+1, base_rank=i)
120            s.foundations.append(stack)
121            tx, ty, ta, tf = l.getTextAttr(stack, "s")
122            font = self.app.getFont("canvas_default")
123            stack.texts.misc = MfxCanvasText(self.canvas, tx, ty,
124                                             anchor=ta, font=font)
125            x = x + l.XS
126        self.texts.help = MfxCanvasText(
127            self.canvas, x + l.XM, y + l.CH // 2, text=help,
128            anchor="w", font=self.app.getFont("canvas_fixed"))
129        x = x0
130        y = l.YM + l.YS + l.TEXT_HEIGHT
131        for i in range(4):
132            s.rows.append(self.RowStack_Class(x, y, self))
133            x = x + l.XS
134        self.setRegion(s.rows, (-999, y-l.CH//2, 999999, 999999))
135        x = l.XM
136        s.talon = WasteTalonStack(x, y, self, max_rounds=1)
137        l.createText(s.talon, "n")
138        y = y + l.YS
139        s.waste = WasteStack(x, y, self, max_cards=1)
140
141        # define stack-groups
142        l.defaultStackGroups()
143
144    #
145    # game overrides
146    #
147
148    def _shuffleHook(self, cards):
149        # prepare first cards
150        topcards = [None] * 4
151        for c in cards[:]:
152            if c.rank <= 3 and topcards[c.rank] is None:
153                topcards[c.rank] = c
154                cards.remove(c)
155        topcards.reverse()
156        return cards + topcards
157
158    def startGame(self):
159        self.startDealSample()
160        self.s.talon.dealRow(rows=self.s.foundations)
161        self.s.talon.dealCards()          # deal first card to WasteStack
162
163    def getHighlightPilesStacks(self):
164        return ()
165
166
167# ************************************************************************
168# * Hopscotch
169# ************************************************************************
170
171class Hopscotch(Calculation):
172    def _shuffleHook(self, cards):
173        # prepare first cards
174        topcards = [None] * 4
175        for c in cards[:]:
176            if c.suit == 0 and c.rank <= 3 and topcards[c.rank] is None:
177                topcards[c.rank] = c
178                cards.remove(c)
179        topcards.reverse()
180        return cards + topcards
181
182
183# ************************************************************************
184# * Betsy Ross
185# ************************************************************************
186
187class BetsyRoss(Calculation):
188
189    #
190    # game layout
191    #
192
193    def createGame(self):
194        # create layout
195        l, s = Layout(self), self.s
196        help, text_width = self._getHelpText()
197        text_width += 2*l.XM
198
199        # set window
200        self.setSize(5.5*l.XS+l.XM+text_width, l.YM+3*l.YS+l.TEXT_HEIGHT)
201
202        # create stacks
203        x0 = l.XM + l.XS * 3 // 2
204        x, y = x0, l.YM
205        for i in range(4):
206            stack = BetsyRoss_Foundation(x, y, self, base_rank=i,
207                                         max_cards=1, max_move=0, max_accept=0)
208            s.foundations.append(stack)
209            x += l.XS
210        x = x0
211        y = l.YM + l.YS
212        for i in range(4):
213            stack = BetsyRoss_Foundation(x, y, self, base_rank=2*i+1,
214                                         mod=13, dir=i+1,
215                                         max_cards=12, max_move=0)
216            tx, ty, ta, tf = l.getTextAttr(stack, "s")
217            font = self.app.getFont("canvas_default")
218            stack.texts.misc = MfxCanvasText(self.canvas, tx, ty,
219                                             anchor=ta, font=font)
220            s.foundations.append(stack)
221            x += l.XS
222        self.texts.help = MfxCanvasText(self.canvas, x + l.XM, y + l.CH // 2,
223                                        text=help, anchor="w",
224                                        font=self.app.getFont("canvas_fixed"))
225        x = l.XM
226        s.talon = WasteTalonStack(x, y, self, max_rounds=3)
227        l.createText(s.talon, "n")
228        l.createRoundText(s.talon, 'nnn')
229        y += l.YS
230        s.waste = WasteStack(x, y, self)
231        l.createText(s.waste, "s")
232
233        # define stack-groups
234        l.defaultStackGroups()
235
236    #
237    # game overrides
238    #
239
240    def _shuffleHook(self, cards):
241        # prepare first cards
242        topcards = [None] * 8
243        for c in cards[:]:
244            if c.rank <= 3 and topcards[c.rank] is None:
245                topcards[c.rank] = c
246                cards.remove(c)
247            elif c.rank in (1, 3, 5, 7):
248                i = 4 + (c.rank - 1) // 2
249                if topcards[i] is None:
250                    topcards[i] = c
251                    cards.remove(c)
252        topcards.reverse()
253        return cards + topcards
254
255
256# ************************************************************************
257# * One234
258# ************************************************************************
259
260class One234_Foundation(BetsyRoss_Foundation):
261    def canMoveCards(self, cards):
262        if not BetsyRoss_Foundation.canMoveCards(self, cards):
263            return False
264        return len(self.cards) > 1
265
266    def updateText(self):
267        BetsyRoss_Foundation.updateText(self, update_empty=False)
268
269
270class One234_RowStack(BasicRowStack):
271    # clickHandler = BasicRowStack.doubleclickHandler
272    pass
273
274
275class One234(Calculation):
276    Foundation_Class = One234_Foundation
277    RowStack_Class = StackWrapper(One234_RowStack, max_move=1, max_accept=0)
278
279    def createGame(self):
280        # create layout
281        l, s = Layout(self, TEXT_HEIGHT=40), self.s
282        help, text_width = self._getHelpText()
283        text_width += 2*l.XM
284
285        # set window
286        # (piles up to 20 cards are playable in default window size)
287        w = l.XM+max(4*l.XS+text_width, 8*l.XS)
288        h = l.YM+2*l.YS+5*l.YOFFSET+l.TEXT_HEIGHT+l.YS
289        self.setSize(w, h)
290
291        # create stacks
292        x, y = l.XM, l.YM
293        for i in range(4):
294            stack = self.Foundation_Class(x, y, self,
295                                          mod=13, dir=i+1, base_rank=i)
296            s.foundations.append(stack)
297            tx, ty, ta, tf = l.getTextAttr(stack, "s")
298            font = self.app.getFont("canvas_default")
299            stack.texts.misc = MfxCanvasText(self.canvas, tx, ty,
300                                             anchor=ta, font=font)
301            x = x + l.XS
302        self.texts.help = MfxCanvasText(
303            self.canvas, x + l.XM, y + l.CH // 2, text=help,
304            anchor="w", font=self.app.getFont("canvas_fixed"))
305        x, y = l.XM, l.YM+l.YS+l.TEXT_HEIGHT
306        for i in range(8):
307            s.rows.append(self.RowStack_Class(x, y, self))
308            x = x + l.XS
309
310        s.talon = InitialDealTalonStack(l.XM, self.height-l.YS, self)
311
312        # define stack-groups
313        l.defaultStackGroups()
314
315    def _shuffleHook(self, cards):
316        return cards
317
318    def startGame(self):
319        self._startDealNumRows(4)
320        self.s.talon.dealRow()
321        self.s.talon.dealRow()
322        self.s.talon.dealRow(rows=self.s.foundations)
323
324
325# ************************************************************************
326# * Senior Wrangler
327# ************************************************************************
328
329class SeniorWrangler_Talon(DealRowTalonStack):
330
331    def canDealCards(self):
332        if self.round == self.max_rounds:
333            return False
334        return not self.game.isGameWon()
335
336    def dealCards(self, sound=False):
337        num_cards = 0
338        r = self.game.s.rows[self.round-1]
339        if not r.cards:
340            self.game.nextRoundMove(self)
341            return 1
342        if sound:
343            self.game.startDealSample()
344        old_state = self.game.enterState(self.game.S_DEAL)
345        while r.cards:
346            self.game.flipMove(r)
347            self.game.moveMove(1, r, self, frames=4, shadow=0)
348        self.dealRowAvail(rows=self.game.s.rows[self.round-1:], sound=False)
349        while self.cards:
350            num_cards += self.dealRowAvail(sound=False)
351        self.game.nextRoundMove(self)
352        self.game.leaveState(old_state)
353        if sound:
354            self.game.stopSamples()
355        return num_cards
356
357
358class SeniorWrangler_RowStack(BasicRowStack):
359    # clickHandler = BasicRowStack.doubleclickHandler
360    pass
361
362
363class SeniorWrangler(Game):
364
365    def createGame(self):
366        l, s = Layout(self), self.s
367        self.setSize(l.XM+9.5*l.XS, l.YM+3*l.YS)
368
369        x, y = l.XM+1.5*l.XS, l.YM
370        for i in range(8):
371            stack = BetsyRoss_Foundation(x, y, self, base_rank=i,
372                                         mod=13, dir=i+1, max_move=0)
373            tx, ty, ta, tf = l.getTextAttr(stack, "s")
374            font = self.app.getFont("canvas_default")
375            stack.texts.misc = MfxCanvasText(self.canvas, tx, ty,
376                                             anchor=ta, font=font)
377            s.foundations.append(stack)
378            x = x + l.XS
379        x, y = l.XM+1.5*l.XS, l.YM+2*l.YS
380        for i in range(8):
381            stack = SeniorWrangler_RowStack(x, y, self, max_accept=0)
382            s.rows.append(stack)
383            stack.CARD_YOFFSET = 0
384            x += l.XS
385        x, y = l.XM, l.YM+l.YS
386        s.talon = SeniorWrangler_Talon(x, y, self, max_rounds=9)
387        l.createRoundText(s.talon, 'nn')
388
389        # define stack-groups
390        l.defaultStackGroups()
391
392    def _shuffleHook(self, cards):
393        top = []
394        ranks = []
395        for c in cards[:]:
396            if c.rank in range(8) and c.rank not in ranks:
397                ranks.append(c.rank)
398                cards.remove(c)
399                top.append(c)
400        top.sort(key=lambda x: -x.rank)
401        return cards+top
402
403    def startGame(self):
404        self.s.talon.dealRow(rows=self.s.foundations[:8], frames=0)
405        self._startDealNumRowsAndDealSingleRow(11)
406
407
408# ************************************************************************
409# * S Patience
410# ************************************************************************
411
412class SPatience(Game):
413    Hint_Class = Calculation_Hint
414
415    def createGame(self):
416        l, s = Layout(self), self.s
417        self.setSize(l.XM+7.5*l.XS, l.YM+3.8*l.YS)
418
419        x0, y0 = l.XM, l.YM
420        for xx, yy in ((4, 0.4),
421                       (3, 0.2),
422                       (2, 0.0),
423                       (1, 0.2),
424                       (0, 0.7),
425                       (1, 1.2),
426                       (2, 1.4),
427                       (3, 1.6),
428                       (4, 2.0),
429                       (3, 2.6),
430                       (2, 2.8),
431                       (1, 2.6),
432                       (0, 2.4),
433                       ):
434            x, y = x0+xx*l.XS, y0+yy*l.YS
435            s.foundations.append(RK_FoundationStack(x, y, self, suit=ANY_SUIT,
436                                 max_cards=8, mod=13, max_move=0))
437
438        x, y = l.XM+5.5*l.XS, l.YM+2*l.YS
439        for i in (0, 1):
440            stack = Calculation_RowStack(x, y, self, max_move=1, max_accept=1)
441            stack.CARD_YOFFSET = 0
442            s.rows.append(stack)
443            l.createText(stack, 's')
444            x += l.XS
445        x, y = l.XM+5.5*l.XS, l.YM+l.YS
446        s.talon = WasteTalonStack(x, y, self, max_rounds=1)
447        l.createText(s.talon, 'nw')
448        x += l.XS
449        s.waste = WasteStack(x, y, self, max_cards=1)
450
451        l.defaultStackGroups()
452
453    def _shuffleHook(self, cards):
454        top = []
455        ranks = []
456        for c in cards[:]:
457            if c.rank not in ranks:
458                ranks.append(c.rank)
459                cards.remove(c)
460                top.append(c)
461        top.sort(key=lambda x: -x.rank)
462        return cards+top[7:]+top[:7]
463
464    def startGame(self):
465        self.startDealSample()
466        self.s.talon.dealRow(rows=self.s.foundations)
467        self.s.talon.dealCards()
468
469
470# register the game
471registerGame(GameInfo(256, Calculation, "Calculation",
472                      GI.GT_1DECK_TYPE, 1, 0, GI.SL_MOSTLY_SKILL,
473                      altnames=("Progression",)))
474registerGame(GameInfo(94, Hopscotch, "Hopscotch",
475                      GI.GT_1DECK_TYPE, 1, 0, GI.SL_MOSTLY_SKILL))
476registerGame(GameInfo(134, BetsyRoss, "Betsy Ross",
477                      GI.GT_1DECK_TYPE, 1, 2, GI.SL_MOSTLY_LUCK,
478                      altnames=("Fairest", "Four Kings", "Musical Patience",
479                                "Quadruple Alliance", "Plus Belle")))
480registerGame(GameInfo(550, One234, "One234",
481                      GI.GT_1DECK_TYPE | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL))
482registerGame(GameInfo(653, SeniorWrangler, "Senior Wrangler",
483                      GI.GT_2DECK_TYPE, 2, 8, GI.SL_BALANCED))
484registerGame(GameInfo(704, SPatience, "S Patience",
485                      GI.GT_2DECK_TYPE, 2, 0, GI.SL_BALANCED))
486