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
24
25import math
26import time
27import traceback
28from pickle import Pickler, Unpickler, UnpicklingError
29
30import attr
31
32from pysol_cards.cards import ms_rearrange
33from pysol_cards.random import random__int2str
34
35from pysollib.game.dump import pysolDumpGame
36from pysollib.gamedb import GI
37from pysollib.help import help_about
38from pysollib.hint import DefaultHint
39from pysollib.mfxutil import Image, ImageTk, USE_PIL
40from pysollib.mfxutil import Struct, SubclassResponsibility, destruct
41from pysollib.mfxutil import format_time, print_err
42from pysollib.mfxutil import uclock, usleep
43from pysollib.move import AFlipAllMove
44from pysollib.move import AFlipAndMoveMove
45from pysollib.move import AFlipMove
46from pysollib.move import AMoveMove
47from pysollib.move import ANextRoundMove
48from pysollib.move import ASaveSeedMove
49from pysollib.move import ASaveStateMove
50from pysollib.move import AShuffleStackMove
51from pysollib.move import ASingleCardMove
52from pysollib.move import ASingleFlipMove
53from pysollib.move import ATurnStackMove
54from pysollib.move import AUpdateStackMove
55from pysollib.mygettext import _
56from pysollib.mygettext import ungettext
57from pysollib.pysolrandom import LCRandom31, PysolRandom, construct_random
58from pysollib.pysoltk import CURSOR_WATCH
59from pysollib.pysoltk import Card
60from pysollib.pysoltk import EVENT_HANDLED, EVENT_PROPAGATE
61from pysollib.pysoltk import MfxCanvasLine, MfxCanvasRectangle, MfxCanvasText
62from pysollib.pysoltk import MfxExceptionDialog, MfxMessageDialog
63from pysollib.pysoltk import after, after_cancel, after_idle
64from pysollib.pysoltk import bind, wm_map
65from pysollib.settings import DEBUG
66from pysollib.settings import PACKAGE, TITLE, TOOLKIT, TOP_SIZE
67from pysollib.settings import VERSION, VERSION_TUPLE
68from pysollib.struct_new import NewStruct
69
70import random2
71
72import six
73from six import BytesIO
74from six.moves import range
75
76if TOOLKIT == 'tk':
77    from pysollib.ui.tktile.solverdialog import reset_solver_dialog
78else:
79    from pysollib.pysoltk import reset_solver_dialog
80
81# See: https://github.com/shlomif/PySolFC/issues/159 .
82# 'factory=' is absent from older versions.
83assert getattr(attr, '__version_info__', (0, 0, 0)) >= (18, 2, 0), (
84        "Newer version of https://pypi.org/project/attrs/ is required.")
85
86
87PLAY_TIME_TIMEOUT = 200
88S_PLAY = 0x40
89
90# ************************************************************************
91# * Base class for all solitaire games
92# *
93# * Handles:
94# *   load/save
95# *   undo/redo (using a move history)
96# *   hints/demo
97# ************************************************************************
98
99
100def _updateStatus_process_key_val(tb, sb, k, v):
101    if k == "gamenumber":
102        if v is None:
103            if sb:
104                sb.updateText(gamenumber="")
105            # self.top.wm_title("%s - %s"
106            # % (TITLE, self.getTitleName()))
107            return
108        if isinstance(v, six.string_types):
109            if sb:
110                sb.updateText(gamenumber=v)
111            # self.top.wm_title("%s - %s %s" % (TITLE,
112            # self.getTitleName(), v))
113            return
114    if k == "info":
115        # print 'updateStatus info:', v
116        if v is None:
117            if sb:
118                sb.updateText(info="")
119            return
120        if isinstance(v, str):
121            if sb:
122                sb.updateText(info=v)
123            return
124    if k == "moves":
125        if v is None:
126            # if tb: tb.updateText(moves="Moves\n")
127            if sb:
128                sb.updateText(moves="")
129            return
130        if isinstance(v, tuple):
131            # if tb: tb.updateText(moves="Moves\n%d/%d" % v)
132            if sb:
133                sb.updateText(moves="%d/%d" % v)
134            return
135        if isinstance(v, int):
136            # if tb: tb.updateText(moves="Moves\n%d" % v)
137            if sb:
138                sb.updateText(moves="%d" % v)
139            return
140        if isinstance(v, str):
141            # if tb: tb.updateText(moves=v)
142            if sb:
143                sb.updateText(moves=v)
144            return
145    if k == "player":
146        if v is None:
147            if tb:
148                tb.updateText(player=_("Player\n"))
149            return
150        if isinstance(v, six.string_types):
151            if tb:
152                # if self.app.opt.toolbar_size:
153                if tb.getSize():
154                    tb.updateText(player=_("Player\n") + v)
155                else:
156                    tb.updateText(player=v)
157            return
158    if k == "stats":
159        if v is None:
160            if sb:
161                sb.updateText(stats="")
162            return
163        if isinstance(v, tuple):
164            t = "%d: %d/%d" % (v[0]+v[1], v[0], v[1])
165            if sb:
166                sb.updateText(stats=t)
167            return
168    if k == "time":
169        if v is None:
170            if sb:
171                sb.updateText(time='')
172        if isinstance(v, six.string_types):
173            if sb:
174                sb.updateText(time=v)
175        return
176    if k == 'stuck':
177        if sb:
178            sb.updateText(stuck=v)
179        return
180    raise AttributeError(k)
181
182
183def _stats__is_perfect(stats):
184    """docstring for _stats__is_perfect"""
185    return (stats.undo_moves == 0 and
186            stats.goto_bookmark_moves == 0 and
187            # stats.quickplay_moves == 0 and
188            stats.highlight_piles == 0 and
189            stats.highlight_cards == 0 and
190            stats.shuffle_moves == 0)
191
192
193def _highlightCards__calc_item(canvas, delta, cw, ch, s, c1, c2, color):
194    assert c1 in s.cards and c2 in s.cards
195    tkraise = False
196    if c1 is c2:
197        # highlight single card
198        sx0, sy0 = s.getOffsetFor(c1)
199        x1, y1 = s.getPositionFor(c1)
200        x2, y2 = x1, y1
201        if c1 is s.cards[-1]:
202            # last card in the stack (for Pyramid-like games)
203            tkraise = True
204    else:
205        # highlight pile
206        if len(s.CARD_XOFFSET) > 1:
207            sx0 = 0
208        else:
209            sx0 = s.CARD_XOFFSET[0]
210        if len(s.CARD_YOFFSET) > 1:
211            sy0 = 0
212        else:
213            sy0 = s.CARD_YOFFSET[0]
214        x1, y1 = s.getPositionFor(c1)
215        x2, y2 = s.getPositionFor(c2)
216    if sx0 != 0 and sy0 == 0:
217        # horizontal stack
218        y2 += ch
219        if c2 is s.cards[-1]:  # top card
220            x2 += cw
221        else:
222            if sx0 > 0:
223                # left to right
224                x2 += sx0
225            else:
226                # right to left
227                x1 += cw
228                x2 += cw + sx0
229    elif sx0 == 0 and sy0 != 0:
230        # vertical stack
231        x2 += cw
232        if c2 is s.cards[-1]:  # top card
233            y2 += ch
234        else:
235            if sy0 > 0:
236                # up to down
237                y2 = y2 + sy0
238            else:
239                # down to up
240                y1 += ch
241                y2 += ch + sy0
242    else:
243        x2 += cw
244        y2 += ch
245        tkraise = True
246    # print c1, c2, x1, y1, x2, y2
247    x1, x2 = x1-delta[0], x2+delta[1]
248    y1, y2 = y1-delta[2], y2+delta[3]
249    if TOOLKIT == 'tk':
250        r = MfxCanvasRectangle(canvas, x1, y1, x2, y2,
251                               width=4, fill=None, outline=color)
252        if tkraise:
253            r.tkraise(c2.item)
254    elif TOOLKIT == 'kivy':
255        r = MfxCanvasRectangle(canvas, x1, y1, x2, y2,
256                               width=4, fill=None, outline=color)
257        if tkraise:
258            r.tkraise(c2.item)
259    elif TOOLKIT == 'gtk':
260        r = MfxCanvasRectangle(canvas, x1, y1, x2, y2,
261                               width=4, fill=None, outline=color,
262                               group=s.group)
263        if tkraise:
264            i = s.cards.index(c2)
265            for c in s.cards[i+1:]:
266                c.tkraise(1)
267    return r
268
269
270@attr.s
271class StackGroups(NewStruct):
272    dropstacks = attr.ib(factory=list)
273    hp_stacks = attr.ib(factory=list)  # for getHightlightPilesStacks()
274    openstacks = attr.ib(factory=list)
275    reservestacks = attr.ib(factory=list)
276    talonstacks = attr.ib(factory=list)
277
278    def to_tuples(self):
279        """docstring for to_tuples"""
280        self.openstacks = [s for s in self.openstacks
281                           if s.cap.max_accept >= s.cap.min_accept]
282        self.hp_stacks = [s for s in self.dropstacks
283                          if s.cap.max_move >= 2]
284        self.openstacks = tuple(self.openstacks)
285        self.talonstacks = tuple(self.talonstacks)
286        self.dropstacks = tuple(self.dropstacks)
287        self.reservestacks = tuple(self.reservestacks)
288        self.hp_stacks = tuple(self.hp_stacks)
289
290
291@attr.s
292class StackRegions(NewStruct):
293    # list of tuples(stacks, rect)
294    info = attr.ib(factory=list)
295    # list of stacks in no region
296    remaining = attr.ib(factory=list)
297    data = attr.ib(factory=list)
298    # init info (at the start)
299    init_info = attr.ib(factory=list)
300
301    def calc_info(self, xf, yf, widthpad=0, heightpad=0):
302        """docstring for calc_info"""
303        info = []
304        for stacks, rect in self.init_info:
305            newrect = (int(round((rect[0] + widthpad) * xf)),
306                       int(round((rect[1] + heightpad) * yf)),
307                       int(round((rect[2] + widthpad) * xf)),
308                       int(round((rect[3] + heightpad) * yf)))
309            info.append((stacks, newrect))
310        self.info = tuple(info)
311
312    def optimize(self, remaining):
313        """docstring for optimize"""
314        # sort data by priority
315        self.data.sort()
316        self.data.reverse()
317        # copy (stacks, rect) to info
318        self.info = []
319        for d in self.data:
320            self.info.append((d[2], d[3]))
321        self.info = tuple(self.info)
322        # determine remaining stacks
323        for stacks, rect in self.info:
324            for stack in stacks:
325                while stack in remaining:
326                    remaining.remove(stack)
327        self.remaining = tuple(remaining)
328        self.init_info = self.info
329
330
331@attr.s
332class GameStacks(NewStruct):
333    talon = attr.ib(default=None)
334    waste = attr.ib(default=None)
335    foundations = attr.ib(factory=list)
336    rows = attr.ib(factory=list)  # for getHightlightPilesStacks()
337    reserves = attr.ib(factory=list)
338    internals = attr.ib(factory=list)
339
340    def to_tuples(self):
341        self.foundations = tuple(self.foundations)
342        self.rows = tuple(self.rows)
343        self.reserves = tuple(self.reserves)
344        self.internals = tuple(self.internals)
345
346
347@attr.s
348class GameDrag(NewStruct):
349    event = attr.ib(default=None)
350    timer = attr.ib(default=None)
351    start_x = attr.ib(default=0)
352    start_y = attr.ib(default=0)
353    index = attr.ib(default=-1)
354    stack = attr.ib(default=None)
355    shade_stack = attr.ib(default=None)
356    shade_img = attr.ib(default=None)
357    cards = attr.ib(factory=list)
358    canshade_stacks = attr.ib(factory=list)
359    noshade_stacks = attr.ib(factory=list)
360    shadows = attr.ib(factory=list)
361
362
363@attr.s
364class GameTexts(NewStruct):
365    info = attr.ib(default=None)
366    help = attr.ib(default=None)
367    misc = attr.ib(default=None)
368    score = attr.ib(default=None)
369    base_rank = attr.ib(default=None)
370    list = attr.ib(factory=list)
371
372
373@attr.s
374class GameHints(NewStruct):
375    list = attr.ib(default=None)
376    index = attr.ib(default=-1)
377    level = attr.ib(default=-1)
378
379
380@attr.s
381class GameStatsStruct(NewStruct):
382    hints = attr.ib(default=0)                  # number of hints consumed
383    # number of highlight piles consumed
384    highlight_piles = attr.ib(default=0)
385    # number of highlight matching cards consumed
386    highlight_cards = attr.ib(default=0)
387    # number of highlight same rank consumed
388    highlight_samerank = attr.ib(default=0)
389    undo_moves = attr.ib(default=0)             # number of undos
390    redo_moves = attr.ib(default=0)             # number of redos
391    # number of total moves in this game
392    total_moves = attr.ib(default=0)
393    player_moves = attr.ib(default=0)           # number of moves
394    # number of moves while in demo mode
395    demo_moves = attr.ib(default=0)
396    autoplay_moves = attr.ib(default=0)         # number of moves
397    quickplay_moves = attr.ib(default=0)        # number of quickplay moves
398    goto_bookmark_moves = attr.ib(default=0)    # number of goto bookmark
399    shuffle_moves = attr.ib(default=0)          # number of shuffles (Mahjongg)
400    # did this game already update the demo stats ?
401    demo_updated = attr.ib(default=0)
402    update_time = attr.ib()
403
404    @update_time.default
405    def _foofoo(self):
406        return time.time()  # for updateTime()
407    elapsed_time = attr.ib(default=0.0)
408    pause_start_time = attr.ib(default=0.0)
409
410    def _reset_statistics(self):
411        """docstring for _reset_stats"""
412        self.undo_moves = 0
413        self.redo_moves = 0
414        self.player_moves = 0
415        self.demo_moves = 0
416        self.total_moves = 0
417        self.quickplay_moves = 0
418        self.goto_bookmark_moves = 0
419
420
421_GLOBAL_U_PLAY = 0
422
423
424@attr.s
425class GameGlobalStatsStruct(NewStruct):
426    holded = attr.ib(default=0)                 # is this a holded game
427    # number of times this game was loaded
428    loaded = attr.ib(default=0)
429    # number of times this game was saved
430    saved = attr.ib(default=0)
431    # number of times this game was restarted
432    restarted = attr.ib(default=0)
433    goto_bookmark_moves = attr.ib(default=0)    # number of goto bookmark
434    # did this game already update the player stats ?
435    updated = attr.ib(default=_GLOBAL_U_PLAY)
436    start_time = attr.ib()
437
438    @start_time.default
439    def _foofoo(self):
440        return time.time()  # for updateTime()
441    total_elapsed_time = attr.ib(default=0.0)
442    start_player = attr.ib(default=None)
443
444
445@attr.s
446class GameWinAnimation(NewStruct):
447    timer = attr.ib(default=None)
448    images = attr.ib(factory=list)
449    tk_images = attr.ib(factory=list)             # saved tk images
450    saved_images = attr.ib(factory=dict)          # saved resampled images
451    canvas_images = attr.ib(factory=list)         # ids of canvas images
452    frame_num = attr.ib(default=0)              # number of the current frame
453    width = attr.ib(default=0)
454    height = attr.ib(default=0)
455
456
457@attr.s
458class GameMoves(NewStruct):
459    current = attr.ib(factory=list)
460    history = attr.ib(factory=list)
461    index = attr.ib(default=0)
462    state = attr.ib(default=S_PLAY)
463
464
465# used when loading a game
466@attr.s
467class GameLoadInfo(NewStruct):
468    ncards = attr.ib(default=0)
469    stacks = attr.ib(factory=list)
470    talon_round = attr.ib(default=1)
471
472
473# global saveinfo survives a game restart
474@attr.s
475class GameGlobalSaveInfo(NewStruct):
476    bookmarks = attr.ib(factory=dict)
477    comment = attr.ib(default="")
478
479
480# Needed for saving a game
481@attr.s
482class GameSaveInfo(NewStruct):
483    stack_caps = attr.ib(factory=list)
484
485
486_Game_LOAD_CLASSES = [GameGlobalSaveInfo, GameGlobalStatsStruct, GameMoves,
487                      GameSaveInfo, GameStatsStruct, ]
488
489
490class Game(object):
491    # for self.gstats.updated
492    U_PLAY = _GLOBAL_U_PLAY
493    U_WON = -2
494    U_LOST = -3
495    U_PERFECT = -4
496
497    # for self.moves.state
498    S_INIT = 0x00
499    S_DEAL = 0x10
500    S_FILL = 0x20
501    S_RESTORE = 0x30
502    S_UNDO = 0x50
503    S_PLAY = S_PLAY
504    S_REDO = 0x60
505
506    # for loading and saving - subclasses should override if
507    # the format for a saved game changed (see also canLoadGame())
508    GAME_VERSION = 1
509
510    # only basic initialization here
511    def __init__(self, gameinfo):
512        self.preview = 0
513        self.random = None
514        self.gameinfo = gameinfo
515        self.id = gameinfo.id
516        assert self.id > 0
517        self.busy = 0
518        self.pause = False
519        self.finished = False
520        self.version = VERSION
521        self.version_tuple = VERSION_TUPLE
522        self.cards = []
523        self.stackmap = {}              # dict with (x,y) tuples as key
524        self.allstacks = []
525        self.sn_groups = []  # snapshot groups; list of list of similar stacks
526        self.snapshots = []
527        self.failed_snapshots = []
528        self.stackdesc_list = []
529        self.demo_logo = None
530        self.pause_logo = None
531        self.s = GameStacks()
532        self.sg = StackGroups()
533        self.regions = StackRegions()
534        self.init_size = (0, 0)
535        self.center_offset = (0, 0)
536        self.event_handled = False      # if click event handled by Stack (???)
537        self.reset()
538
539    # main constructor
540    def create(self, app):
541        # print 'Game.create'
542        old_busy = self.busy
543        self.__createCommon(app)
544        self.setCursor(cursor=CURSOR_WATCH)
545        # print 'gameid:', self.id
546        self.top.wm_title(TITLE + " - " + self.getTitleName())
547        self.top.wm_iconname(TITLE + " - " + self.getTitleName())
548        # create the game
549        if self.app.intro.progress:
550            self.app.intro.progress.update(step=1)
551        self.createGame()
552        # set some defaults
553        self.createSnGroups()
554        # convert stackgroups to tuples (speed)
555        self.allstacks = tuple(self.allstacks)
556        self.sg.to_tuples()
557        self.s.to_tuples()
558        # init the stack view
559        for stack in self.allstacks:
560            stack.prepareStack()
561            stack.assertStack()
562        if self.s.talon:
563            assert hasattr(self.s.talon, "round")
564            assert hasattr(self.s.talon, "max_rounds")
565        if DEBUG:
566            self._checkGame()
567        self.optimizeRegions()
568        # create cards
569        if not self.cards:
570            self.cards = self.createCards(progress=self.app.intro.progress)
571        self.initBindings()
572        # self.top.bind('<ButtonPress>', self.top._sleepEvent)
573        # self.top.bind('<3>', self.top._sleepEvent)
574        # update display properties
575        self.canvas.busy = True
576        # geometry
577        mycond = (self.app.opt.save_games_geometry and
578                  self.id in self.app.opt.games_geometry)
579        if mycond:
580            # restore game geometry
581            w, h = self.app.opt.games_geometry[self.id]
582            self.canvas.config(width=w, height=h)
583        if True and USE_PIL:
584            if self.app.opt.auto_scale:
585                w, h = self.app.opt.game_geometry
586                self.canvas.setInitialSize(w, h, margins=False,
587                                           scrollregion=False)
588                # self.canvas.config(width=w, height=h)
589                # dx, dy = self.canvas.xmargin, self.canvas.ymargin
590                # self.canvas.config(scrollregion=(-dx, -dy, dx, dy))
591            else:
592                if not mycond:
593                    w = int(round(self.width * self.app.opt.scale_x))
594                    h = int(round(self.height * self.app.opt.scale_y))
595                    self.canvas.setInitialSize(w, h)
596                self.top.wm_geometry("")    # cancel user-specified geometry
597            # preserve texts positions
598            for t in ('info', 'help', 'misc', 'score', 'base_rank'):
599                item = getattr(self.texts, t)
600                if item:
601                    coords = self.canvas.coords(item)
602                    setattr(self.init_texts, t, coords)
603            #
604            for item in self.texts.list:
605                coords = self.canvas.coords(item)
606                self.init_texts.list.append(coords)
607            # resize
608            self.resizeGame()
609            # fix coords of cards (see self.createCards)
610            x, y = self.s.talon.x, self.s.talon.y
611            for c in self.cards:
612                c.moveTo(x, y)
613        else:
614            # no PIL
615            self.canvas.setInitialSize(self.width, self.height)
616            self.top.wm_geometry("")    # cancel user-specified geometry
617        # done; update view
618        self.top.update_idletasks()
619        self.canvas.busy = False
620        if DEBUG >= 4:
621            MfxCanvasRectangle(self.canvas, 0, 0, self.width, self.height,
622                               width=2, fill=None, outline='green')
623        #
624        self.stats.update_time = time.time()
625        self.showHelp()                 # just in case
626        hint_class = self.getHintClass()
627        if hint_class is not None:
628            self.Stuck_Class = hint_class(self, 0)
629        self.busy = old_busy
630
631    def _checkGame(self):
632        class_name = self.__class__.__name__
633        if self.s.foundations:
634            ncards = 0
635            for stack in self.s.foundations:
636                ncards += stack.cap.max_cards
637            if ncards != self.gameinfo.ncards:
638                print_err('invalid sum of foundations.max_cards: '
639                          '%s: %s %s' %
640                          (class_name, ncards, self.gameinfo.ncards),
641                          2)
642        if self.s.rows:
643            from pysollib.stack import AC_RowStack, UD_AC_RowStack, \
644                 SS_RowStack, UD_SS_RowStack, \
645                 RK_RowStack, UD_RK_RowStack, \
646                 Spider_AC_RowStack, Spider_SS_RowStack
647            r = self.s.rows[0]
648            for c, f in (
649                ((Spider_AC_RowStack, Spider_SS_RowStack),
650                 (self._shallHighlightMatch_RK,
651                  self._shallHighlightMatch_RKW)),
652                ((AC_RowStack, UD_AC_RowStack),
653                 (self._shallHighlightMatch_AC,
654                  self._shallHighlightMatch_ACW)),
655                ((SS_RowStack, UD_SS_RowStack),
656                 (self._shallHighlightMatch_SS,
657                  self._shallHighlightMatch_SSW)),
658                ((RK_RowStack, UD_RK_RowStack),
659                 (self._shallHighlightMatch_RK,
660                  self._shallHighlightMatch_RKW)),):
661                if isinstance(r, c):
662                    if self.shallHighlightMatch not in f:
663                        print_err('shallHighlightMatch is not valid: '
664                                  ' %s, %s' % (class_name, r.__class__), 2)
665                    if r.cap.mod == 13 and self.shallHighlightMatch != f[1]:
666                        print_err('shallHighlightMatch is not valid (wrap): '
667                                  ' %s, %s' % (class_name, r.__class__), 2)
668                    break
669        if self.s.talon.max_rounds > 1 and self.s.talon.texts.rounds is None:
670            print_err('max_rounds > 1, but talon.texts.rounds is None: '
671                      '%s' % class_name, 2)
672        elif (self.s.talon.max_rounds <= 1 and
673              self.s.talon.texts.rounds is not None):
674            print_err('max_rounds <= 1, but talon.texts.rounds is not None: '
675                      '%s' % class_name, 2)
676
677    def _calcMouseBind(self, binding_format):
678        """docstring for _calcMouseBind"""
679        return self.app.opt.calcCustomMouseButtonsBinding(binding_format)
680
681    def initBindings(self):
682        # note: a Game is only allowed to bind self.canvas and not to self.top
683        # bind(self.canvas, "<Double-1>", self.undoHandler)
684        bind(self.canvas,
685             self._calcMouseBind("<{mouse_button1}>"), self.undoHandler)
686        bind(self.canvas,
687             self._calcMouseBind("<{mouse_button2}>"), self.dropHandler)
688        bind(self.canvas,
689             self._calcMouseBind("<{mouse_button3}>"), self.redoHandler)
690        bind(self.canvas, '<Unmap>', self._unmapHandler)
691        bind(self.canvas, '<Configure>', self._configureHandler, add=True)
692
693    def __createCommon(self, app):
694        self.busy = 1
695        self.app = app
696        self.top = app.top
697        self.canvas = app.canvas
698        self.filename = ""
699        self.drag = GameDrag()
700        if self.gstats.start_player is None:
701            self.gstats.start_player = self.app.opt.player
702        # optional MfxCanvasText items
703        self.texts = GameTexts()
704        # initial position of the texts
705        self.init_texts = GameTexts()
706
707    def createPreview(self, app):
708        old_busy = self.busy
709        self.__createCommon(app)
710        self.preview = max(1, self.canvas.preview)
711        # create game
712        self.createGame()
713        # set some defaults
714        self.sg.openstacks = [s for s in self.sg.openstacks
715                              if s.cap.max_accept >= s.cap.min_accept]
716        self.sg.hp_stacks = [s for s in self.sg.dropstacks
717                             if s.cap.max_move >= 2]
718        # init the stack view
719        for stack in self.allstacks:
720            stack.prepareStack()
721            stack.assertStack()
722        self.optimizeRegions()
723        # create cards
724        self.cards = self.createCards()
725        #
726        self.canvas.setInitialSize(self.width, self.height)
727        self.busy = old_busy
728
729    def destruct(self):
730        # help breaking circular references
731        for obj in self.cards:
732            destruct(obj)
733        for obj in self.allstacks:
734            obj.destruct()
735            destruct(obj)
736
737    # Do not destroy game structure (like stacks and cards) here !
738    def reset(self, restart=0):
739        self.filename = ""
740        self.demo = None
741        self.solver = None
742        self.hints = GameHints()
743        self.saveinfo = GameSaveInfo()
744        self.loadinfo = GameLoadInfo()
745        self.snapshots = []
746        self.failed_snapshots = []
747        # local statistics are reset on each game restart
748        self.stats = GameStatsStruct()
749        self.startMoves()
750        if restart:
751            return
752        # global statistics survive a game restart
753        self.gstats = GameGlobalStatsStruct()
754        self.gsaveinfo = GameGlobalSaveInfo()
755        # some vars for win animation
756        self.win_animation = GameWinAnimation()
757
758    def getTitleName(self):
759        return self.app.getGameTitleName(self.id)
760
761    def getGameNumber(self, format):
762        s = self.random.getSeedAsStr()
763        if format:
764            return "# " + s
765        return s
766
767    # this is called from within createGame()
768    def setSize(self, w, h):
769        self.width, self.height = int(round(w)), int(round(h))
770        dx, dy = self.canvas.xmargin, self.canvas.ymargin
771        self.init_size = self.width+2*dx, self.height+2*dy
772
773    def setCursor(self, cursor):
774        if self.canvas:
775            self.canvas.config(cursor=cursor)
776            # self.canvas.update_idletasks()
777        # if self.app and self.app.toolbar:
778        # self.app.toolbar.setCursor(cursor=cursor)
779
780    def newGame(self, random=None, restart=0, autoplay=1, shuffle=True,
781                dealer=None):
782        self.finished = False
783        old_busy, self.busy = self.busy, 1
784        self.setCursor(cursor=CURSOR_WATCH)
785        self.stopWinAnimation()
786        self.disableMenus()
787        if shuffle:
788            self.redealAnimation()
789        self.reset(restart=restart)
790        self.resetGame()
791        self.createRandom(random)
792        if shuffle:
793            self.shuffle()
794            assert len(self.s.talon.cards) == self.gameinfo.ncards
795        for stack in self.allstacks:
796            stack.updateText()
797        self.updateText()
798        self.updateStatus(
799            player=self.app.opt.player,
800            gamenumber=self.getGameNumber(format=1),
801            moves=(0, 0),
802            stats=self.app.stats.getStats(
803                self.app.opt.player,
804                self.id),
805            stuck='')
806        reset_solver_dialog()
807        # unhide toplevel when we use a progress bar
808        if not self.preview:
809            wm_map(self.top, maximized=self.app.opt.wm_maximized)
810            self.top.busyUpdate()
811        if TOOLKIT == 'gtk':
812            # FIXME
813            if self.top:
814                self.top.update_idletasks()
815                self.top.show_now()
816        self.stopSamples()
817        self.moves.state = self.S_INIT
818        if dealer:
819            dealer()
820        else:
821            if not self.preview:
822                self.resizeGame()
823            self.startGame()
824        self.startMoves()
825        for stack in self.allstacks:
826            stack.updateText()
827        self.updateSnapshots()
828        self.updateText()
829        self.updateStatus(moves=(0, 0))
830        self.updateMenus()
831        self.stopSamples()
832        if autoplay:
833            self.autoPlay()
834            self.stats.player_moves = 0
835        self.setCursor(cursor=self.app.top_cursor)
836        self.stats.update_time = time.time()
837        if not self.preview:
838            self.startPlayTimer()
839        self.busy = old_busy
840
841    def restoreGame(self, game, reset=1):
842        old_busy, self.busy = self.busy, 1
843        if reset:
844            self.reset()
845        self.resetGame()
846        # 1) copy loaded variables
847        self.filename = game.filename
848        self.version = game.version
849        self.version_tuple = game.version_tuple
850        self.random = game.random
851        self.moves = game.moves
852        self.stats = game.stats
853        self.gstats = game.gstats
854        # 2) copy extra save-/loadinfo
855        self.saveinfo = game.saveinfo
856        self.gsaveinfo = game.gsaveinfo
857        self.s.talon.round = game.loadinfo.talon_round
858        self.finished = game.finished
859        self.snapshots = game.snapshots
860        # 3) move cards to stacks
861        assert len(self.allstacks) == len(game.loadinfo.stacks)
862        old_state = game.moves.state
863        game.moves.state = self.S_RESTORE
864        for i in range(len(self.allstacks)):
865            for t in game.loadinfo.stacks[i]:
866                card_id, face_up = t
867                card = self.cards[card_id]
868                if face_up:
869                    card.showFace()
870                else:
871                    card.showBack()
872                self.allstacks[i].addCard(card)
873        game.moves.state = old_state
874        # 4) update settings
875        for stack_id, cap in self.saveinfo.stack_caps:
876            # print stack_id, cap
877            self.allstacks[stack_id].cap.update(cap.__dict__)
878        # 5) subclass settings
879        self._restoreGameHook(game)
880        # 6) update view
881        for stack in self.allstacks:
882            stack.updateText()
883        self.updateText()
884        self.updateStatus(
885            player=self.app.opt.player,
886            gamenumber=self.getGameNumber(format=1),
887            moves=(self.moves.index, self.stats.total_moves),
888            stats=self.app.stats.getStats(self.app.opt.player, self.id))
889        if not self.preview:
890            self.updateMenus()
891            wm_map(self.top, maximized=self.app.opt.wm_maximized)
892        self.setCursor(cursor=self.app.top_cursor)
893        self.stats.update_time = time.time()
894        self.busy = old_busy
895        # wait for canvas is mapped
896        after(self.top, 200, self._configureHandler)
897        if TOOLKIT == 'gtk':
898            # FIXME
899            if self.top:
900                self.top.update_idletasks()
901                self.top.show_now()
902        self.startPlayTimer()
903
904    def restoreGameFromBookmark(self, bookmark):
905        old_busy, self.busy = self.busy, 1
906        file = BytesIO(bookmark)
907        p = Unpickler(file)
908        game = self._undumpGame(p, self.app)
909        assert game.id == self.id
910        self.restoreGame(game, reset=0)
911        destruct(game)
912        self.busy = old_busy
913
914    def resetGame(self):
915        self.hints.list = None
916        self.s.talon.removeAllCards()
917        for stack in self.allstacks:
918            stack.resetGame()
919            if TOOLKIT == 'gtk':
920                # FIXME (pyramid like games)
921                stack.group.tkraise()
922        if self.preview <= 1:
923            for t in (self.texts.score, self.texts.base_rank,):
924                if t:
925                    t.config(text="")
926
927    def nextGameFlags(self, id, random=None):
928        f = 0
929        if id != self.id:
930            f |= 1
931        if self.app.nextgame.cardset is not self.app.cardset:
932            f |= 2
933        if random is not None:
934            if ((random.__class__ is not self.random.__class__) or
935                    random.initial_seed != self.random.initial_seed):
936                f |= 16
937        return f
938
939    # quit to outer mainloop in class App, possibly restarting
940    # with another game from there
941    def quitGame(self, id=0, random=None, loadedgame=None,
942                 startdemo=0, bookmark=0, holdgame=0):
943        self.updateTime()
944        if bookmark:
945            id, random = self.id, self.random
946            f = BytesIO()
947            self._dumpGame(Pickler(f, 1), bookmark=1)
948            self.app.nextgame.bookmark = f.getvalue()
949        if id > 0:
950            self.setCursor(cursor=CURSOR_WATCH)
951        self.app.nextgame.id = id
952        self.app.nextgame.random = random
953        self.app.nextgame.loadedgame = loadedgame
954        self.app.nextgame.startdemo = startdemo
955        self.app.nextgame.holdgame = holdgame
956        self.updateStatus(time=None, moves=None, gamenumber=None, stats=None)
957        self.top.mainquit()
958
959    # This should be called directly before newGame(),
960    # restoreGame(), restoreGameFromBookmark() and quitGame().
961    def endGame(self, restart=0, bookmark=0, holdgame=0):
962        if self.preview:
963            return
964        self.app.wm_save_state()
965        if self.pause:
966            self.doPause()
967        if holdgame:
968            return
969        if bookmark:
970            return
971        if restart:
972            if self.moves.index > 0 and self.getPlayerMoves() > 0:
973                self.gstats.restarted += 1
974            return
975        self.updateStats()
976        stats = self.app.stats
977        if self.shallUpdateBalance():
978            b = self.getGameBalance()
979            if b:
980                stats.total_balance[self.id] = \
981                    stats.total_balance.get(self.id, 0) + b
982                stats.session_balance[self.id] = \
983                    stats.session_balance.get(self.id, 0) + b
984                stats.gameid_balance = stats.gameid_balance + b
985
986    def restartGame(self):
987        self.endGame(restart=1)
988        self.newGame(restart=1, random=self.random)
989
990    def resizeImages(self, manually=False):
991        if self.canvas.winfo_ismapped():
992            # apparent size of canvas
993            vw = self.canvas.winfo_width()
994            vh = self.canvas.winfo_height()
995        else:
996            # we have no a real size of canvas
997            # (winfo_width / winfo_reqwidth)
998            # so we use a saved size
999            vw, vh = self.app.opt.game_geometry
1000            if not vw:
1001                # first run of the game
1002                return 1, 1, 1, 1, 0, 0
1003        # requested size of canvas (createGame -> setSize)
1004        iw, ih = self.init_size
1005
1006        # resizing images and cards
1007        if (self.app.opt.auto_scale or
1008                (self.app.opt.spread_stacks and not manually)):
1009            # calculate factor of resizing
1010            xf = float(vw)/iw
1011            yf = float(vh)/ih
1012            if (self.app.opt.preserve_aspect_ratio
1013                    and not self.app.opt.spread_stacks):
1014                xf = yf = min(xf, yf)
1015        else:
1016            xf, yf = self.app.opt.scale_x, self.app.opt.scale_y
1017        cw, ch = self.getCenterOffset(vw, vh, iw, ih, xf, yf)
1018        self.center_offset = (cw, ch)
1019        if (not self.app.opt.spread_stacks or manually):
1020            # images
1021            self.app.images.resize(xf, yf)
1022        # cards
1023        for card in self.cards:
1024            card.update(card.id, card.deck, card.suit, card.rank, self)
1025        return xf, yf, self.app.images._xfactor, self.app.images._yfactor, \
1026            cw, ch
1027
1028    def getCenterOffset(self, vw, vh, iw, ih, xf, yf):
1029        if (not self.app.opt.center_layout or self.app.opt.spread_stacks or
1030                (self.app.opt.auto_scale and not
1031                 self.app.opt.preserve_aspect_ratio)):
1032            return 0, 0
1033        if ((vw > iw and vh > ih) or self.app.opt.auto_scale):
1034            return (vw / xf - iw) / 2, (vh / yf - ih) / 2
1035        elif (vw >= iw and vh < ih):
1036            return (vw / xf - iw) / 2, 0
1037        elif (vw < iw and vh >= ih):
1038            return 0, (vh / yf - ih) / 2
1039        else:
1040            return 0, 0
1041
1042    def resizeGame(self, card_size_manually=False):
1043        # if self.busy:
1044        # return
1045        if not USE_PIL:
1046            return
1047        self.deleteStackDesc()
1048        xf, yf, xf0, yf0, cw, ch = \
1049            self.resizeImages(manually=card_size_manually)
1050        self.center_offset = (cw, ch)
1051        for stack in self.allstacks:
1052
1053            if (self.app.opt.spread_stacks):
1054                # Do not move Talons
1055                # (because one would need to reposition
1056                # 'empty cross' and 'redeal' figures)
1057                # But in that case,
1058                # games with talon not placed top-left corner
1059                # will get it misplaced when auto_scale
1060                # e.g. Suit Elevens
1061                # => player can fix that issue by setting auto_scale false
1062                if stack is self.s.talon:
1063                    # stack.init_coord=(x, y)
1064                    if card_size_manually:
1065                        stack.resize(xf, yf0, widthpad=cw, heightpad=ch)
1066                    else:
1067                        stack.resize(xf0, yf0, widthpad=cw, heightpad=ch)
1068                else:
1069                    stack.resize(xf, yf0, widthpad=cw, heightpad=ch)
1070            else:
1071                stack.resize(xf, yf, widthpad=cw, heightpad=ch)
1072            stack.updatePositions()
1073        self.regions.calc_info(xf, yf, widthpad=cw, heightpad=ch)
1074        # texts
1075        for t in ('info', 'help', 'misc', 'score', 'base_rank'):
1076            init_coord = getattr(self.init_texts, t)
1077            if init_coord:
1078                item = getattr(self.texts, t)
1079                x, y = int(round((init_coord[0] + cw) * xf)), \
1080                    int(round((init_coord[1] + ch) * yf))
1081                self.canvas.coords(item, x, y)
1082        for i in range(len(self.texts.list)):
1083            init_coord = self.init_texts.list[i]
1084            item = self.texts.list[i]
1085            x, y = int(round((init_coord[0] + cw) * xf)), \
1086                int(round((init_coord[1] + ch) * yf))
1087            self.canvas.coords(item, x, y)
1088
1089    def createRandom(self, random):
1090        if random is None:
1091            if isinstance(self.random, PysolRandom):
1092                state = self.random.getstate()
1093                self.app.gamerandom.setstate(state)
1094            # we want at least 17 digits
1095            seed = self.app.gamerandom.randrange(
1096                int('10000000000000000'),
1097                PysolRandom.MAX_SEED
1098            )
1099            self.random = PysolRandom(seed)
1100            self.random.origin = self.random.ORIGIN_RANDOM
1101        else:
1102            self.random = random
1103            self.random.reset()
1104
1105    def enterState(self, state):
1106        old_state = self.moves.state
1107        if state < old_state:
1108            self.moves.state = state
1109        return old_state
1110
1111    def leaveState(self, old_state):
1112        self.moves.state = old_state
1113
1114    def getSnapshot(self):
1115        # generate hash (unique string) of current move
1116        sn = []
1117        for stack in self.allstacks:
1118            s = []
1119            for card in stack.cards:
1120                s.append('%d%03d%d' % (card.suit, card.rank, card.face_up))
1121            sn.append(''.join(s))
1122        sn = '-'.join(sn)
1123        # optimisation
1124        sn = hash(sn)
1125        return sn
1126
1127    def createSnGroups(self):
1128        # group stacks by class and cap
1129        sg = {}
1130        for s in self.allstacks:
1131            for k in sg:
1132                if s.__class__ is k.__class__ and \
1133                       s.cap.__dict__ == k.cap.__dict__:
1134                    g = sg[k]
1135                    g.append(s.id)
1136                    break
1137            else:
1138                # new group
1139                sg[s] = [s.id]
1140        sg = list(sg.values())
1141        self.sn_groups = sg
1142
1143    def updateSnapshots(self):
1144        sn = self.getSnapshot()
1145        if sn in self.snapshots:
1146            # self.updateStatus(snapshot=True)
1147            pass
1148        else:
1149            self.snapshots.append(sn)
1150            # self.updateStatus(snapshot=False)
1151
1152    # Create all cards for the game.
1153    def createCards(self, progress=None):
1154        gi = self.gameinfo
1155        pstep = 0
1156        if progress:
1157            pstep = (100.0 - progress.percent) / gi.ncards
1158        cards = []
1159        id = [0]
1160        x, y = self.s.talon.x, self.s.talon.y
1161        for deck in range(gi.decks):
1162            def _iter_ranks(ranks, suit):
1163                for rank in ranks:
1164                    card = self._createCard(id[0], deck, suit, rank, x=x, y=y)
1165                    if card is None:
1166                        continue
1167                    cards.append(card)
1168                    id[0] += 1
1169                    if progress:
1170                        progress.update(step=pstep)
1171            for suit in gi.suits:
1172                _iter_ranks(gi.ranks, suit)
1173            _iter_ranks(gi.trumps, len(gi.suits))
1174        if progress:
1175            progress.update(percent=100)
1176        assert len(cards) == gi.ncards
1177        return cards
1178
1179    def _createCard(self, id, deck, suit, rank, x, y):
1180        return Card(id, deck, suit, rank, game=self, x=x, y=y)
1181
1182    # shuffle cards
1183    def shuffle(self):
1184        # get a fresh copy of the original game-cards
1185        cards = list(self.cards)
1186        # init random generator
1187        if isinstance(self.random, LCRandom31):
1188            cards = ms_rearrange(cards)
1189        self.random.reset()         # reset to initial seed
1190        # shuffle
1191        self.random.shuffle(cards)
1192        # subclass hook
1193        cards = self._shuffleHook(cards)
1194        # finally add the shuffled cards to the Talon
1195        for card in cards:
1196            self.s.talon.addCard(card, update=0)
1197            card.showBack(unhide=0)
1198
1199    # shuffle cards, but keep decks together
1200    def shuffleSeparateDecks(self):
1201        cards = []
1202        self.random.reset()
1203        n = self.gameinfo.ncards // self.gameinfo.decks
1204        for deck in range(self.gameinfo.decks):
1205            i = deck * n
1206            deck_cards = list(self.cards)[i:i+n]
1207            self.random.shuffle(deck_cards)
1208            cards.extend(deck_cards)
1209        cards = self._shuffleHook(cards)
1210        for card in cards:
1211            self.s.talon.addCard(card, update=0)
1212            card.showBack(unhide=0)
1213
1214    # subclass overrideable (must use self.random)
1215    def _shuffleHook(self, cards):
1216        return cards
1217
1218    # utility for use by subclasses
1219    def _shuffleHookMoveToTop(self, cards, func, ncards=999999):
1220        # move cards to top of the Talon (i.e. first cards to be dealt)
1221        cards, scards = self._shuffleHookMoveSorter(cards, func, ncards)
1222        return cards + scards
1223
1224    def _shuffleHookMoveToBottom(self, cards, func, ncards=999999):
1225        # move cards to bottom of the Talon (i.e. last cards to be dealt)
1226        cards, scards = self._shuffleHookMoveSorter(cards, func, ncards)
1227        return scards + cards
1228
1229    def _shuffleHookMoveSorter(self, cards, cb, ncards):
1230        extracted, i, new = [], len(cards), []
1231        for c in cards:
1232            select, ord_ = cb(c)
1233            if select:
1234                extracted.append((ord_, i, c))
1235                if len(extracted) >= ncards:
1236                    new += cards[(len(cards)-i+1):]
1237                    break
1238            else:
1239                new.append(c)
1240            i -= 1
1241        return new, [x[2] for x in reversed(sorted(extracted))]
1242
1243    def _finishDrag(self):
1244        if self.demo:
1245            self.stopDemo()
1246        if self.busy:
1247            return 1
1248        if self.drag.stack:
1249            self.drag.stack.finishDrag()
1250        return 0
1251
1252    def _cancelDrag(self, break_pause=True):
1253        self.stopWinAnimation()
1254        if self.demo:
1255            self.stopDemo()
1256        if break_pause and self.pause:
1257            self.doPause()
1258        self.interruptSleep()
1259        self.deleteStackDesc()
1260        if self.busy:
1261            return 1
1262        if self.drag.stack:
1263            self.drag.stack.cancelDrag()
1264        return 0
1265
1266    def updateMenus(self):
1267        if not self.preview:
1268            self.app.menubar.updateMenus()
1269
1270    def disableMenus(self):
1271        if not self.preview:
1272            self.app.menubar.disableMenus()
1273
1274    def _defaultHandler(self, event):
1275        if not self.app:
1276            return True                 # FIXME (GTK)
1277        if not self.app.opt.mouse_undo:
1278            return True
1279        if self.pause:
1280            self.app.menubar.mPause()
1281            return True
1282        if not self.event_handled and self.stopWinAnimation():
1283            return True
1284        self.interruptSleep()
1285        if self.deleteStackDesc():
1286            # delete piles descriptions
1287            return True
1288        if self.demo:
1289            self.stopDemo()
1290            return True
1291        if not self.event_handled and self.drag.stack:
1292            self.drag.stack.cancelDrag(event)
1293            return True
1294        return False                    # continue this event
1295
1296    def dropHandler(self, event):
1297        if not self._defaultHandler(event) and not self.event_handled:
1298            self.app.menubar.mDrop()
1299        self.event_handled = False
1300        return EVENT_PROPAGATE
1301
1302    def undoHandler(self, event):
1303        if not self._defaultHandler(event) and not self.event_handled:
1304            self.app.menubar.mUndo()
1305        self.event_handled = False
1306        return EVENT_PROPAGATE
1307
1308    def redoHandler(self, event):
1309        if not self._defaultHandler(event) and not self.event_handled:
1310            self.app.menubar.mRedo()
1311        self.event_handled = False
1312        return EVENT_PROPAGATE
1313
1314    def updateStatus(self, **kw):
1315        if self.preview:
1316            return
1317        tb, sb = self.app.toolbar, self.app.statusbar
1318        for k, v in six.iteritems(kw):
1319            _updateStatus_process_key_val(tb, sb, k, v)
1320
1321    def _unmapHandler(self, event):
1322        # pause game if root window has been iconified
1323        if self.app and not self.pause:
1324            self.app.menubar.mPause()
1325
1326    _resizeHandlerID = None
1327
1328    def _resizeHandler(self):
1329        self._resizeHandlerID = None
1330        self.resizeGame()
1331
1332    def _configureHandler(self, event=None):
1333        if False:  # if not USE_PIL:
1334            return
1335        if not self.app:
1336            return
1337        if not self.canvas:
1338            return
1339        if (not self.app.opt.auto_scale and
1340                not self.app.opt.spread_stacks and
1341                not self.app.opt.center_layout):
1342            return
1343        if self.preview:
1344            return
1345        if self._resizeHandlerID:
1346            self.canvas.after_cancel(self._resizeHandlerID)
1347        self._resizeHandlerID = self.canvas.after(250, self._resizeHandler)
1348
1349    def playSample(self, name, priority=0, loop=0):
1350
1351        if name.startswith('deal'):
1352            sampleopt = 'deal'
1353        elif name not in self.app.opt.sound_samples:
1354            sampleopt = 'extra'
1355        else:
1356            sampleopt = name
1357
1358        if sampleopt in self.app.opt.sound_samples and \
1359           not self.app.opt.sound_samples[sampleopt]:
1360            return 0
1361        if self.app.audio:
1362            return self.app.audio.playSample(
1363                name,
1364                priority=priority,
1365                loop=loop)
1366        return 0
1367
1368    def stopSamples(self):
1369        if self.app.audio:
1370            self.app.audio.stopSamples()
1371
1372    def stopSamplesLoop(self):
1373        if self.app.audio:
1374            self.app.audio.stopSamplesLoop()
1375
1376    def startDealSample(self, loop=999999):
1377        a = self.app.opt.animations
1378        if a and not self.preview:
1379            self.canvas.update_idletasks()
1380        if self.app.audio and self.app.opt.sound:
1381            if a in (1, 2, 3, 10):
1382                self.playSample("deal01", priority=100, loop=loop)
1383            elif a == 4:
1384                self.playSample("deal04", priority=100, loop=loop)
1385            elif a == 5:
1386                self.playSample("deal08", priority=100, loop=loop)
1387
1388    def areYouSure(self, title=None, text=None, confirm=-1, default=0):
1389        if TOOLKIT == 'kivy':
1390            return True
1391        if self.preview:
1392            return True
1393        if confirm < 0:
1394            confirm = self.app.opt.confirm
1395        if confirm:
1396            if not title:
1397                title = TITLE
1398            if not text:
1399                text = _("Discard current game?")
1400            self.playSample("areyousure")
1401            d = MfxMessageDialog(self.top, title=title, text=text,
1402                                 bitmap="question",
1403                                 strings=(_("&OK"), _("&Cancel")))
1404            if d.status != 0 or d.button != 0:
1405                return False
1406        return True
1407
1408    def notYetImplemented(self):
1409        MfxMessageDialog(self.top, title="Not yet implemented",
1410                         text="This function is\nnot yet implemented.",
1411                         bitmap="error")
1412
1413    # main animation method
1414    def animatedMoveTo(self, from_stack, to_stack, cards, x, y,
1415                       tkraise=1, frames=-1, shadow=-1):
1416        # available values of app.opt.animations:
1417        # 0 - without animations
1418        # 1 - very fast (without timer)
1419        # 2 - fast (default)
1420        # 3 - medium (2/3 of fast speed)
1421        # 4 - slow (1/4 of fast speed)
1422        # 5 - very slow (1/8 of fast speed)
1423        # 10 - used internally in game preview
1424        if self.app.opt.animations == 0 or frames == 0:
1425            return
1426        # init timer - need a high resolution for this to work
1427        clock, delay, skip = None, 1, 1
1428        if self.app.opt.animations >= 2:
1429            clock = uclock
1430        SPF = 0.15 / 8          # animation speed - seconds per frame
1431        if frames < 0:
1432            frames = 8
1433        assert frames >= 2
1434        if self.app.opt.animations == 3:        # medium
1435            frames *= 3
1436            SPF /= 2
1437        elif self.app.opt.animations == 4:      # slow
1438            frames *= 8
1439            SPF /= 2
1440        elif self.app.opt.animations == 5:      # very slow
1441            frames *= 16
1442            SPF /= 2
1443        elif self.app.opt.animations == 10:
1444            # this is used internally in game preview to speed up
1445            # the initial dealing
1446            # if self.moves.state == self.S_INIT and frames > 4:
1447            #     frames //= 2
1448            return
1449        if shadow < 0:
1450            shadow = self.app.opt.shadow
1451        shadows = ()
1452        # start animation
1453        if TOOLKIT == 'kivy':
1454            c0 = cards[0]
1455            dx, dy = (x - c0.x), (y - c0.y)
1456            for card in cards:
1457                base = float(self.app.opt.animations)
1458                duration = base*0.1
1459                card.animatedMove(dx, dy, duration)
1460            return
1461
1462        if tkraise:
1463            for card in cards:
1464                card.tkraise()
1465        c0 = cards[0]
1466        dx, dy = (x - c0.x) / float(frames), (y - c0.y) / float(frames)
1467        tx, ty = 0, 0
1468        i = 1
1469        if clock:
1470            starttime = clock()
1471        while i < frames:
1472            mx, my = int(round(dx * i)) - tx, int(round(dy * i)) - ty
1473            tx, ty = tx + mx, ty + my
1474            if i == 1 and shadow and from_stack:
1475                # create shadows in the first frame
1476                sx, sy = self.app.images.SHADOW_XOFFSET, \
1477                    self.app.images.SHADOW_YOFFSET
1478                shadows = from_stack.createShadows(cards, sx, sy)
1479            for s in shadows:
1480                s.move(mx, my)
1481            for card in cards:
1482                card.moveBy(mx, my)
1483            self.canvas.update_idletasks()
1484            step = 1
1485            if clock:
1486                endtime = starttime + i*SPF
1487                sleep = endtime - clock()
1488                if delay and sleep >= 0.005:
1489                    # we're fast - delay
1490                    # print "Delay frame", i, sleep
1491                    usleep(sleep)
1492                elif skip and sleep <= -0.75*SPF:
1493                    # we're slow - skip 1 or 2 frames
1494                    # print "Skip frame", i, sleep
1495                    step += 1
1496                    if frames > 4 and sleep < -1.5*SPF:
1497                        step += 1
1498                # print i, step, mx, my; time.sleep(0.5)
1499            i += step
1500        # last frame: delete shadows, move card to final position
1501        for s in shadows:
1502            s.delete()
1503        dx, dy = x - c0.x, y - c0.y
1504        for card in cards:
1505            card.moveBy(dx, dy)
1506        self.canvas.update_idletasks()
1507
1508    def doAnimatedFlipAndMove(self, from_stack, to_stack=None, frames=-1):
1509        if self.app.opt.animations == 0 or frames == 0:
1510            return False
1511        if not from_stack.cards:
1512            return False
1513        if TOOLKIT == 'gtk':
1514            return False
1515        if not Image:
1516            return False
1517
1518        canvas = self.canvas
1519        card = from_stack.cards[-1]
1520        im1 = card._active_image._pil_image
1521        if card.face_up:
1522            im2 = card._back_image._pil_image
1523        else:
1524            im2 = card._face_image._pil_image
1525        w, h = im1.size
1526        id = card.item.id
1527
1528        SPF = 0.1/8                     # animation speed - seconds per frame
1529        frames = 4.0                    # num frames for each step
1530        if self.app.opt.animations == 3:  # medium
1531            SPF = 0.1/8
1532            frames = 7.0
1533        elif self.app.opt.animations == 4:  # slow
1534            SPF = 0.1/8
1535            frames = 12.0
1536        elif self.app.opt.animations == 5:  # very slow
1537            SPF = 0.1/8
1538            frames = 24.0
1539
1540        if to_stack is None:
1541            x0, y0 = from_stack.getPositionFor(card)
1542            x1, y1 = x0, y0
1543            dest_x, dest_y = 0, 0
1544        else:
1545            x0, y0 = from_stack.getPositionFor(card)
1546            x1, y1 = to_stack.getPositionForNextCard()
1547            dest_x, dest_y = x1-x0, y1-y0
1548
1549        if dest_x == 0 and dest_y == 0:
1550            # flip
1551            # ascent_dx, ascent_dy = 0, self.app.images.SHADOW_YOFFSET/frames
1552            ascent_dx, ascent_dy = 0, h/10.0/frames
1553            min_size = w/10
1554            shrink_dx = (w-min_size) / (frames-1)
1555            shrink_dy = 0
1556        elif dest_y == 0:
1557            # move to left/right waste
1558            # ascent_dx, ascent_dy = 0, self.app.images.SHADOW_YOFFSET/frames
1559            ascent_dx, ascent_dy = 0, h/10.0/frames
1560            min_size = w/10
1561            shrink_dx = (w-min_size) / (frames-1)
1562            shrink_dy = 0
1563        elif dest_x == 0:
1564            # move to top/bottom waste
1565            if 0:
1566                ascent_dx, ascent_dy = 0, h/10.0/frames
1567                min_size = w/10
1568                shrink_dx = (w-min_size) / (frames-1)
1569                shrink_dy = 0
1570            elif 0:
1571                ascent_dx, ascent_dy = 0, 0
1572                min_size = h/10
1573                shrink_dx = 0
1574                shrink_dy = (h-min_size) / (frames-1)
1575            else:
1576                return False
1577        else:
1578            # dest_x != 0 and dest_y != 0
1579            return False
1580
1581        move_dx = dest_x / frames / 2
1582        move_dy = dest_y / frames / 2
1583        xpos, ypos = float(x0), float(y0)
1584
1585        card.tkraise()
1586
1587        # step 1
1588        d_x = shrink_dx/2+move_dx-ascent_dx
1589        d_y = shrink_dy/2+move_dy-ascent_dy
1590        nframe = 0
1591        while nframe < frames:
1592            starttime = uclock()
1593            # resize img
1594            ww = w - nframe*shrink_dx
1595            hh = h - nframe*shrink_dy
1596            tmp = im1.resize((int(ww), int(hh)))
1597            tk_tmp = ImageTk.PhotoImage(image=tmp)
1598            canvas.itemconfig(id, image=tk_tmp)
1599            # move img
1600            xpos += d_x
1601            ypos += d_y
1602            card.moveTo(int(round(xpos)), int(round(ypos)))
1603            canvas.update_idletasks()
1604
1605            nframe += 1
1606            t = (SPF-(uclock()-starttime))*1000   # milliseconds
1607            if t > 0:
1608                usleep(t/1000)
1609                # else:
1610                # nframe += 1
1611                # xpos += d_x
1612                # ypos += d_y
1613
1614        # step 2
1615        d_x = -shrink_dx/2+move_dx+ascent_dx
1616        d_y = -shrink_dy/2+move_dy+ascent_dy
1617        nframe = 0
1618        while nframe < frames:
1619            starttime = uclock()
1620            # resize img
1621            ww = w - (frames-nframe-1)*shrink_dx
1622            hh = h - (frames-nframe-1)*shrink_dy
1623            tmp = im2.resize((int(ww), int(hh)))
1624            tk_tmp = ImageTk.PhotoImage(image=tmp)
1625            canvas.itemconfig(id, image=tk_tmp)
1626            # move img
1627            xpos += d_x
1628            ypos += d_y
1629            card.moveTo(int(round(xpos)), int(round(ypos)))
1630            canvas.update_idletasks()
1631
1632            nframe += 1
1633            t = (SPF-(uclock()-starttime))*1000  # milliseconds
1634            if t > 0:
1635                usleep(t/1000)
1636                # else:
1637                # nframe += 1
1638                # xpos += d_x
1639                # ypos += d_y
1640
1641        card.moveTo(x1, y1)
1642        # canvas.update_idletasks()
1643        return True
1644
1645    def animatedFlip(self, stack):
1646        if not self.app.opt.flip_animation:
1647            return False
1648        return self.doAnimatedFlipAndMove(stack)
1649
1650    def animatedFlipAndMove(self, from_stack, to_stack, frames=-1):
1651        if not self.app.opt.flip_animation:
1652            return False
1653        return self.doAnimatedFlipAndMove(from_stack, to_stack, frames)
1654
1655    def winAnimationEvent(self):
1656        # based on code from pygtk-demo
1657        FRAME_DELAY = 80
1658        CYCLE_LEN = 60
1659        starttime = uclock()
1660        images = self.win_animation.images
1661        saved_images = self.win_animation.saved_images  # cached images
1662        canvas = self.canvas
1663        canvas.delete(*self.win_animation.canvas_images)
1664        self.win_animation.canvas_images = []
1665
1666        x0 = int(int(canvas.cget('width'))*(canvas.xview()[0]))
1667        y0 = int(int(canvas.cget('height'))*(canvas.yview()[0]))
1668        width, height = self.win_animation.width, self.win_animation.height
1669        cw = self.canvas.winfo_width()
1670        ch = self.canvas.winfo_height()
1671        x0 -= (width-cw)/2
1672        y0 -= (height-ch)/2
1673
1674        tmp_tk_images = []
1675        raised_images = []
1676        n_images = len(images)
1677        xmid = width / 2.0
1678        ymid = height / 2.0
1679        radius = min(xmid, ymid) / 2.0
1680
1681        f = float(self.win_animation.frame_num % CYCLE_LEN) / float(CYCLE_LEN)
1682        r = radius + (radius / 3.0) * math.sin(f * 2.0 * math.pi)
1683        img_index = 0
1684
1685        for im in images:
1686
1687            iw, ih = im.size
1688
1689            ang = 2.0 * math.pi * img_index / n_images - f * 2.0 * math.pi
1690            xpos = x0 + int(xmid + r * math.cos(ang) - iw / 2.0)
1691            ypos = y0 + int(ymid + r * math.sin(ang) - ih / 2.0)
1692
1693            k = (math.sin if img_index & 1 else math.cos)(f * 2.0 * math.pi)
1694            k = max(0.4, k ** 2)
1695            round_k = int(round(k*100))
1696            if img_index not in saved_images:
1697                saved_images[img_index] = {}
1698            if round_k in saved_images[img_index]:
1699                tk_tmp = saved_images[img_index][round_k]
1700            else:
1701                new_size = (int(iw*k), int(ih*k))
1702                if round_k == 100:
1703                    tmp = im
1704                else:
1705                    tmp = im.resize(new_size, resample=Image.BILINEAR)
1706                tk_tmp = ImageTk.PhotoImage(image=tmp)
1707                saved_images[img_index][round_k] = tk_tmp
1708
1709            id = canvas.create_image(xpos, ypos, image=tk_tmp, anchor='nw')
1710            self.win_animation.canvas_images.append(id)
1711            if k > 0.6:
1712                raised_images.append(id)
1713            tmp_tk_images.append(tk_tmp)
1714
1715            img_index += 1
1716
1717        for id in raised_images:
1718            canvas.tag_raise(id)
1719        self.win_animation.frame_num = \
1720            (self.win_animation.frame_num+1) % CYCLE_LEN
1721        self.win_animation.tk_images = tmp_tk_images
1722        canvas.update_idletasks()
1723        # loop
1724        t = FRAME_DELAY-int((uclock()-starttime)*1000)
1725        if t > 0:
1726            self.win_animation.timer = after(canvas, t, self.winAnimationEvent)
1727        else:
1728            self.win_animation.timer = after_idle(
1729                canvas,
1730                self.winAnimationEvent)
1731
1732    def stopWinAnimation(self):
1733        if self.win_animation.timer:
1734            after_cancel(self.win_animation.timer)  # stop loop
1735            self.win_animation.timer = None
1736            self.canvas.delete(*self.win_animation.canvas_images)
1737            self.win_animation.canvas_images = []
1738            self.win_animation.tk_images = []  # delete all images
1739            self.saved_images = {}
1740            self.canvas.showAllItems()
1741            return True
1742        return False
1743
1744    def winAnimation(self, perfect=0):
1745        if self.preview:
1746            return
1747        if not self.app.opt.win_animation:
1748            return
1749        if TOOLKIT == 'gtk':
1750            return
1751        if not Image:
1752            return
1753        self.canvas.hideAllItems()
1754        # select some random cards
1755        cards = self.cards[:]
1756        scards = []
1757        ncards = min(10, len(cards))
1758        for i in range(ncards):
1759            c = self.app.miscrandom.choice(cards)
1760            scards.append(c)
1761            cards.remove(c)
1762        for c in scards:
1763            self.win_animation.images.append(c._face_image._pil_image)
1764        # compute visible geometry
1765        self.win_animation.width = self.canvas.winfo_width()
1766        self.win_animation.height = self.canvas.winfo_height()
1767        # run win animation in background
1768        # after_idle(self.canvas, self.winAnimationEvent)
1769        after(self.canvas, 200, self.winAnimationEvent)
1770        return
1771
1772    def redealAnimation(self):
1773        if self.preview:
1774            return
1775        if not self.app.opt.animations or not self.app.opt.redeal_animation:
1776            return
1777        cards = []
1778        for s in self.allstacks:
1779            if s is not self.s.talon:
1780                for c in s.cards:
1781                    cards.append((c, s))
1782        if not cards:
1783            return
1784        self.setCursor(cursor=CURSOR_WATCH)
1785        self.top.busyUpdate()
1786        self.canvas.update_idletasks()
1787        old_a = self.app.opt.animations
1788        if old_a == 0:
1789            self.app.opt.animations = 1     # very fast
1790        elif old_a == 3:                    # medium
1791            self.app.opt.animations = 2     # fast
1792        elif old_a == 4:                    # very slow
1793            self.app.opt.animations = 3     # slow
1794        # select some random cards
1795        acards = []
1796        scards = cards[:]
1797        for i in range(8):
1798            c, s = self.app.miscrandom.choice(scards)
1799            if c not in acards:
1800                acards.append(c)
1801                scards.remove((c, s))
1802                if not scards:
1803                    break
1804        # animate
1805        sx, sy = self.s.talon.x, self.s.talon.y
1806        w, h = self.width, self.height
1807        while cards:
1808            # get and un-tuple a random card
1809            t = self.app.miscrandom.choice(cards)
1810            c, s = t
1811            s.removeCard(c, update=0)
1812            # animation
1813            if c in acards or len(cards) <= 2:
1814                self.animatedMoveTo(
1815                    s, None, [c], w//2, h//2, tkraise=0, shadow=0)
1816                self.animatedMoveTo(s, None, [c], sx, sy, tkraise=0, shadow=0)
1817            else:
1818                c.moveTo(sx, sy)
1819            cards.remove(t)
1820        self.app.opt.animations = old_a
1821
1822    def sleep(self, seconds):
1823        # if 0 and self.canvas:
1824        # self.canvas.update_idletasks()
1825        if seconds > 0:
1826            if self.top:
1827                self.top.interruptSleep()
1828                self.top.sleep(seconds)
1829            else:
1830                time.sleep(seconds)
1831
1832    def interruptSleep(self):
1833        if self.top:
1834            self.top.interruptSleep()
1835
1836    def getCardFaceImage(self, deck, suit, rank):
1837        return self.app.images.getFace(deck, suit, rank)
1838
1839    def getCardBackImage(self, deck, suit, rank):
1840        return self.app.images.getBack()
1841
1842    def getCardShadeImage(self):
1843        return self.app.images.getShade()
1844
1845    def _getClosestStack(self, cx, cy, stacks, dragstack):
1846        closest, cdist = None, 999999999
1847        # Since we only compare distances,
1848        # we don't bother to take the square root.
1849        for stack in stacks:
1850            dist = (stack.x - cx)**2 + (stack.y - cy)**2
1851            if dist < cdist:
1852                closest, cdist = stack, dist
1853        return closest
1854
1855    def getClosestStack(self, card, dragstack):
1856        cx, cy = card.x, card.y
1857        for stacks, rect in self.regions.info:
1858            if cx >= rect[0] and cx < rect[2] \
1859                    and cy >= rect[1] and cy < rect[3]:
1860                return self._getClosestStack(cx, cy, stacks, dragstack)
1861        return self._getClosestStack(cx, cy, self.regions.remaining, dragstack)
1862
1863    # define a region for use in getClosestStack()
1864    def setRegion(self, stacks, rect, priority=0):
1865        assert len(stacks) > 0
1866        assert len(rect) == 4 and rect[0] < rect[2] and rect[1] < rect[3]
1867        if DEBUG >= 2:
1868            xf, yf = self.app.images._xfactor, self.app.images._yfactor
1869            MfxCanvasRectangle(self.canvas,
1870                               xf*rect[0], yf*rect[1], xf*rect[2], yf*rect[3],
1871                               width=2, fill=None, outline='red')
1872        for s in stacks:
1873            assert s and s in self.allstacks
1874            # verify that the stack lies within the rectangle
1875            r = rect
1876            if USE_PIL:
1877                x, y = s.init_coord
1878            else:
1879                x, y = s.x, s.y
1880            assert r[0] <= x <= r[2] and r[1] <= y <= r[3]
1881            # verify that the stack is not already in another region
1882            # with the same priority
1883            for d in self.regions.data:
1884                if priority == d[0]:
1885                    assert s not in d[2]
1886        # add to regions
1887        self.regions.data.append(
1888            (priority, -len(self.regions.data), tuple(stacks), tuple(rect)))
1889
1890    # as getClosestStack() is called within the mouse motion handler
1891    # event it is worth optimizing a little bit
1892    def optimizeRegions(self):
1893        return self.regions.optimize(list(self.sg.openstacks))
1894
1895    def getInvisibleCoords(self):
1896        # for InvisibleStack, etc
1897        # x, y = -500, -500 - len(game.allstacks)
1898        cardw, cardh = self.app.images.CARDW, self.app.images.CARDH
1899        xoffset = self.app.images.CARD_XOFFSET
1900        yoffset = self.app.images.CARD_YOFFSET
1901        x = cardw + xoffset + self.canvas.xmargin
1902        y = cardh + yoffset + self.canvas.ymargin
1903        return -x-10, -y-10
1904
1905    #
1906    # Game - subclass overridable actions - IMPORTANT FOR GAME LOGIC
1907    #
1908
1909    # create the game (create stacks, texts, etc.)
1910    def createGame(self):
1911        raise SubclassResponsibility
1912
1913    # start the game (i.e. deal initial cards)
1914    def startGame(self):
1915        raise SubclassResponsibility
1916
1917    # can we deal cards ?
1918    def canDealCards(self):
1919        # default: ask the Talon
1920        return self.s.talon and self.s.talon.canDealCards()
1921
1922    # deal cards - return number of cards dealt
1923    def dealCards(self, sound=True):
1924        # default: set state to deal and pass dealing to Talon
1925        if self.s.talon and self.canDealCards():
1926            self.finishMove()
1927            old_state = self.enterState(self.S_DEAL)
1928            n = self.s.talon.dealCards(sound=sound)
1929            self.leaveState(old_state)
1930            self.finishMove()
1931            if not self.checkForWin():
1932                self.autoPlay()
1933            return n
1934        return 0
1935
1936    # fill a stack if rules require it (e.g. Picture Gallery)
1937    def fillStack(self, stack):
1938        pass
1939
1940    # redeal cards (used in RedealTalonStack; all cards already in talon)
1941    def redealCards(self):
1942        pass
1943
1944    # the actual hint class (or None)
1945    Hint_Class = DefaultHint
1946    Solver_Class = None
1947    Stuck_Class = None
1948
1949    def getHintClass(self):
1950        return self.Hint_Class
1951
1952    def getStrictness(self):
1953        return 0
1954
1955    def canSaveGame(self):
1956        return True
1957
1958    def canLoadGame(self, version_tuple, game_version):
1959        return self.GAME_VERSION == game_version
1960
1961    def canSetBookmark(self):
1962        return self.canSaveGame()
1963
1964    def canUndo(self):
1965        return True
1966
1967    def canRedo(self):
1968        return self.canUndo()
1969
1970    # Mahjongg
1971    def canShuffle(self):
1972        return False
1973
1974    # game changed - i.e. should we ask the player to discard the game
1975    def changed(self, restart=False):
1976        if self.gstats.updated < 0:
1977            return 0                    # already won or lost
1978        # if self.gstats.loaded > 0:
1979        #     return 0                    # loaded games account for no stats
1980        if not restart:
1981            if self.gstats.restarted > 0:
1982                return 1                # game was restarted - always ask
1983            if self.gstats.goto_bookmark_moves > 0:
1984                return 1
1985        if self.moves.index == 0 or self.getPlayerMoves() == 0:
1986            return 0
1987        return 2
1988
1989    def getWinStatus(self):
1990        won = self.isGameWon() != 0
1991        if not won or self.stats.hints > 0 or self.stats.demo_moves > 0:
1992            # sorry, you lose
1993            return won, 0, self.U_LOST
1994        if _stats__is_perfect(self.stats):
1995            return won, 2, self.U_PERFECT
1996        return won, 1, self.U_WON
1997
1998    # update statistics when a game was won/ended/canceled/...
1999    def updateStats(self, demo=0):
2000        if self.preview:
2001            return ''
2002        if not demo:
2003            self.stopPlayTimer()
2004        won, status, updated = self.getWinStatus()
2005        if demo and self.getPlayerMoves() == 0:
2006            if not self.stats.demo_updated:
2007                # a pure demo game - update demo stats
2008                self.stats.demo_updated = updated
2009                self.app.stats.updateStats(None, self, won)
2010            return ''
2011        elif self.changed():
2012            # must update player stats
2013            self.gstats.updated = updated
2014            if self.app.opt.update_player_stats:
2015                ret = self.app.stats.updateStats(
2016                    self.app.opt.player, self, status)
2017                self.updateStatus(
2018                    stats=self.app.stats.getStats(
2019                        self.app.opt.player, self.id))
2020                top_msg = ''
2021                if ret:
2022                    if ret[0] and ret[1]:
2023                        top_msg = _(
2024                            '\nYou have reached\n# %(timerank)d in the top ' +
2025                            '%(tops)d of playing time\nand # %(movesrank)d ' +
2026                            'in the top %(tops)d of moves.') % {
2027                                'timerank': ret[0],
2028                                'movesrank': ret[1],
2029                                'tops': TOP_SIZE}
2030                    elif ret[0]:        # playing time
2031                        top_msg = _(
2032                            '\nYou have reached\n# %(timerank)d in the top ' +
2033                            '%(tops)d of playing time.') % {
2034                                'timerank': ret[0],
2035                                'tops': TOP_SIZE}
2036                    elif ret[1]:        # moves
2037                        top_msg = _(
2038                            '\nYou have reached\n# %(movesrank)d in the top ' +
2039                            '%(tops)s of moves.') % {
2040                                'movesrank': ret[1],
2041                                'tops': TOP_SIZE}
2042                return top_msg
2043        elif not demo:
2044            # only update the session log
2045            if self.app.opt.update_player_stats:
2046                if self.gstats.loaded:
2047                    self.app.stats.updateStats(self.app.opt.player, self, -2)
2048                elif self.gstats.updated == 0 and self.stats.demo_updated == 0:
2049                    self.app.stats.updateStats(self.app.opt.player, self, -1)
2050        return ''
2051
2052    def checkForWin(self):
2053        won, status, updated = self.getWinStatus()
2054        if not won:
2055            return False
2056        self.finishMove()       # just in case
2057        if self.preview:
2058            return True
2059        if self.finished:
2060            return True
2061        if self.demo:
2062            return status
2063        if TOOLKIT == 'kivy':
2064            if not self.app.opt.display_win_message:
2065                return True
2066            self.top.waitAnimation()
2067        if status == 2:
2068            top_msg = self.updateStats()
2069            time = self.getTime()
2070            self.finished = True
2071            self.playSample("gameperfect", priority=1000)
2072            self.winAnimation(perfect=1)
2073            text = ungettext('Your playing time is %(time)s\nfor %(n)d move.',
2074                             'Your playing time is %(time)s\nfor %(n)d moves.',
2075                             self.moves.index)
2076            text = text % {'time': time, 'n': self.moves.index}
2077            congrats = _('Congratulations, this\nwas a truly perfect game!')
2078            d = MfxMessageDialog(
2079                self.top, title=_("Game won"),
2080                text='\n' + congrats + '\n\n' + text + '\n' + top_msg + '\n',
2081                strings=(_("&New game"), None, _("&Back to game"),
2082                         _("&Cancel")),
2083                image=self.app.gimages.logos[5])
2084        elif status == 1:
2085            top_msg = self.updateStats()
2086            time = self.getTime()
2087            self.finished = True
2088            self.playSample("gamewon", priority=1000)
2089            self.winAnimation()
2090            text = ungettext('Your playing time is %(time)s\nfor %(n)d move.',
2091                             'Your playing time is %(time)s\nfor %(n)d moves.',
2092                             self.moves.index)
2093            text = text % {'time': time, 'n': self.moves.index}
2094            congrats = _('Congratulations, you did it!')
2095            d = MfxMessageDialog(
2096                self.top, title=_("Game won"),
2097                text='\n' + congrats + '\n\n' + text + '\n' + top_msg + '\n',
2098                strings=(_("&New game"), None, _("&Back to game"),
2099                         _("&Cancel")),
2100                image=self.app.gimages.logos[4])
2101        elif self.gstats.updated < 0:
2102            self.finished = True
2103            self.playSample("gamefinished", priority=1000)
2104            d = MfxMessageDialog(
2105                self.top, title=_("Game finished"), bitmap="info",
2106                text=_("\nGame finished\n"),
2107                strings=(_("&New game"), None, None, _("&Close")))
2108        else:
2109            self.finished = True
2110            self.playSample("gamelost", priority=1000)
2111            d = MfxMessageDialog(
2112                self.top, title=_("Game finished"), bitmap="info",
2113                text=_("\nGame finished, but not without my help...\n"),
2114                strings=(_("&New game"), _("&Restart"), None, _("&Cancel")))
2115        self.updateMenus()
2116        if TOOLKIT == 'kivy':
2117            return True
2118        if d.status == 0 and d.button == 0:
2119            # new game
2120            self.endGame()
2121            self.newGame()
2122        elif d.status == 0 and d.button == 1:
2123            # restart game
2124            self.restartGame()
2125        elif d.status == 0 and d.button == 2:
2126            self.stopWinAnimation()
2127        return True
2128
2129    #
2130    # Game - subclass overridable methods (but usually not)
2131    #
2132
2133    def isGameWon(self):
2134        # default: all Foundations must be filled
2135        return sum([len(s.cards) for s in self.s.foundations]) == \
2136            len(self.cards)
2137
2138    def getFoundationDir(self):
2139        for s in self.s.foundations:
2140            if len(s.cards) >= 2:
2141                return s.getRankDir()
2142        return 0
2143
2144    # determine the real number of player_moves
2145    def getPlayerMoves(self):
2146        return self.stats.player_moves
2147
2148    def updateTime(self):
2149        if self.finished or self.pause:
2150            return
2151        t = time.time()
2152        d = t - self.stats.update_time
2153        if d > 0:
2154            self.stats.elapsed_time += d
2155            self.gstats.total_elapsed_time += d
2156        self.stats.update_time = t
2157
2158    def getTime(self):
2159        self.updateTime()
2160        t = int(self.stats.elapsed_time)
2161        return format_time(t)
2162
2163    #
2164    # Game - subclass overridable intelligence
2165    #
2166
2167    def getAutoStacks(self, event=None):
2168        # returns (flipstacks, dropstacks, quickplaystacks)
2169        # default: sg.dropstacks
2170        return (self.sg.dropstacks, self.sg.dropstacks, self.sg.dropstacks)
2171
2172    # handles autofaceup, autodrop and autodeal
2173    def autoPlay(self, autofaceup=-1, autodrop=-1, autodeal=-1, sound=True):
2174        if self.demo:
2175            return 0
2176        old_busy, self.busy = self.busy, 1
2177        if autofaceup < 0:
2178            autofaceup = self.app.opt.autofaceup
2179        if autodrop < 0:
2180            autodrop = self.app.opt.autodrop
2181        if autodeal < 0:
2182            autodeal = self.app.opt.autodeal
2183        moves = self.stats.total_moves
2184        n = self._autoPlay(autofaceup, autodrop, autodeal, sound=sound)
2185        self.finishMove()
2186        self.stats.autoplay_moves += (self.stats.total_moves - moves)
2187        self.busy = old_busy
2188        return n
2189
2190    def _autoPlay(self, autofaceup, autodrop, autodeal, sound):
2191        flipstacks, dropstacks, quickstacks = self.getAutoStacks()
2192        done_something = 1
2193        while done_something:
2194            done_something = 0
2195            # a) flip top cards face-up
2196            if autofaceup and flipstacks:
2197                for s in flipstacks:
2198                    if s.canFlipCard():
2199                        if sound:
2200                            self.playSample("autoflip", priority=5)
2201                        # ~s.flipMove()
2202                        s.flipMove(animation=True)
2203                        done_something = 1
2204                        # each single flip is undo-able unless opt.autofaceup
2205                        self.finishMove()
2206                        if self.checkForWin():
2207                            return 1
2208            # b) drop cards
2209            if autodrop and dropstacks:
2210                for s in dropstacks:
2211                    to_stack, ncards = s.canDropCards(self.s.foundations)
2212                    if to_stack:
2213                        # each single drop is undo-able (note that this call
2214                        # is before the actual move)
2215                        self.finishMove()
2216                        if sound:
2217                            self.playSample("autodrop", priority=30)
2218                        s.moveMove(ncards, to_stack)
2219                        done_something = 1
2220                        if self.checkForWin():
2221                            return 1
2222            # c) deal
2223            if autodeal:
2224                if self._autoDeal(sound=sound):
2225                    done_something = 1
2226                    self.finishMove()
2227                    if self.checkForWin():
2228                        return 1
2229        return 0
2230
2231    def _autoDeal(self, sound=True):
2232        # default: deal a card to the waste if the waste is empty
2233        w = self.s.waste
2234        if w and len(w.cards) == 0 and self.canDealCards():
2235            return self.dealCards(sound=sound)
2236        return 0
2237
2238    def autoDrop(self, autofaceup=-1):
2239        old_a = self.app.opt.animations
2240        if old_a == 3:                   # medium
2241            self.app.opt.animations = 2  # fast
2242        self.autoPlay(autofaceup=autofaceup, autodrop=1)
2243        self.app.opt.animations = old_a
2244
2245    # for find_card_dialog
2246    def highlightCard(self, suit, rank):
2247        if not self.app:
2248            return None
2249        col = self.app.opt.colors['samerank_1']
2250        info = []
2251        for s in self.allstacks:
2252            for c in s.cards:
2253                if c.suit == suit and c.rank == rank:
2254                    if s.basicShallHighlightSameRank(c):
2255                        info.append((s, c, c, col))
2256        return self._highlightCards(info, 0)
2257
2258    # highlight all moveable piles
2259    def getHighlightPilesStacks(self):
2260        # default: dropstacks with min pile length = 2
2261        if self.sg.hp_stacks:
2262            return ((self.sg.hp_stacks, 2),)
2263        return ()
2264
2265    def _highlightCards(self, info, sleep=1.5, delta=(1, 1, 1, 1)):
2266        if not info:
2267            return 0
2268        if self.pause:
2269            return 0
2270        self.stopWinAnimation()
2271        cw, ch = self.app.images.getSize()
2272        items = []
2273        for s, c1, c2, color in info:
2274            items.append(
2275                _highlightCards__calc_item(
2276                    self.canvas, delta, cw, ch, s, c1, c2, color))
2277        if not items:
2278            return 0
2279        self.canvas.update_idletasks()
2280        if sleep:
2281            self.sleep(sleep)
2282            items.reverse()
2283            for r in items:
2284                r.delete()
2285            self.canvas.update_idletasks()
2286            return EVENT_HANDLED
2287        else:
2288            # remove items later (find_card_dialog)
2289            return items
2290
2291    def highlightNotMatching(self):
2292        if self.demo:
2293            return
2294        if not self.app.opt.highlight_not_matching:
2295            return
2296        # compute visible geometry
2297        x = int(int(self.canvas.cget('width'))*(self.canvas.xview()[0]))
2298        y = int(int(self.canvas.cget('height'))*(self.canvas.yview()[0]))
2299        w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
2300
2301        color = self.app.opt.colors['not_matching']
2302        width = 6
2303        xmargin, ymargin = self.canvas.xmargin, self.canvas.ymargin
2304        if self.preview:
2305            width = 4
2306            xmargin, ymargin = 0, 0
2307        x0, y0 = x+width//2-xmargin, y+width//2-ymargin
2308        x1, y1 = x+w-width//2-xmargin, y+h-width//2-ymargin
2309        r = MfxCanvasRectangle(self.canvas, x0, y0, x1, y1,
2310                               width=width, fill=None, outline=color)
2311
2312        if TOOLKIT == "kivy":
2313            r.canvas.canvas.ask_update()
2314            r.delete_deferred(self.app.opt.timeouts['highlight_cards'])
2315            return
2316
2317        self.canvas.update_idletasks()
2318        self.sleep(self.app.opt.timeouts['highlight_cards'])
2319        r.delete()
2320        self.canvas.update_idletasks()
2321
2322    def highlightPiles(self, sleep=1.5):
2323        stackinfo = self.getHighlightPilesStacks()
2324        if not stackinfo:
2325            self.highlightNotMatching()
2326            return 0
2327        col = self.app.opt.colors['piles']
2328        hi = []
2329        for si in stackinfo:
2330            for s in si[0]:
2331                pile = s.getPile()
2332                if pile and len(pile) >= si[1]:
2333                    hi.append((s, pile[0], pile[-1], col))
2334        if not hi:
2335            self.highlightNotMatching()
2336            return 0
2337        return self._highlightCards(hi, sleep)
2338
2339    #
2340    # highlight matching cards
2341    #
2342
2343    def shallHighlightMatch(self, stack1, card1, stack2, card2):
2344        return False
2345
2346    def _shallHighlightMatch_AC(self, stack1, card1, stack2, card2):
2347        # by alternate color
2348        return card1.color != card2.color and abs(card1.rank-card2.rank) == 1
2349
2350    def _shallHighlightMatch_ACW(self, stack1, card1, stack2, card2):
2351        # by alternate color with wrapping (only for french games)
2352        return (card1.color != card2.color and
2353                ((card1.rank + 1) % 13 == card2.rank or
2354                 (card2.rank + 1) % 13 == card1.rank))
2355
2356    def _shallHighlightMatch_SS(self, stack1, card1, stack2, card2):
2357        # by same suit
2358        return card1.suit == card2.suit and abs(card1.rank-card2.rank) == 1
2359
2360    def _shallHighlightMatch_SSW(self, stack1, card1, stack2, card2):
2361        # by same suit with wrapping (only for french games)
2362        return (card1.suit == card2.suit and
2363                ((card1.rank + 1) % 13 == card2.rank or
2364                 (card2.rank + 1) % 13 == card1.rank))
2365
2366    def _shallHighlightMatch_RK(self, stack1, card1, stack2, card2):
2367        # by rank
2368        return abs(card1.rank-card2.rank) == 1
2369
2370    def _shallHighlightMatch_RKW(self, stack1, card1, stack2, card2):
2371        # by rank with wrapping (only for french games)
2372        return ((card1.rank + 1) % 13 == card2.rank or
2373                (card2.rank + 1) % 13 == card1.rank)
2374
2375    def _shallHighlightMatch_BO(self, stack1, card1, stack2, card2):
2376        # by any suit but own
2377        return card1.suit != card2.suit and abs(card1.rank-card2.rank) == 1
2378
2379    def _shallHighlightMatch_BOW(self, stack1, card1, stack2, card2):
2380        # by any suit but own with wrapping (only for french games)
2381        return (card1.suit != card2.suit and
2382                ((card1.rank + 1) % 13 == card2.rank or
2383                 (card2.rank + 1) % 13 == card1.rank))
2384
2385    def _shallHighlightMatch_SC(self, stack1, card1, stack2, card2):
2386        # by same color
2387        return card1.color == card2.color and abs(card1.rank-card2.rank) == 1
2388
2389    def _shallHighlightMatch_SCW(self, stack1, card1, stack2, card2):
2390        # by same color with wrapping (only for french games)
2391        return (card1.color == card2.color and
2392                ((card1.rank + 1) % 13 == card2.rank or
2393                 (card2.rank + 1) % 13 == card1.rank))
2394
2395    def getQuickPlayScore(self, ncards, from_stack, to_stack):
2396        if to_stack in self.s.reserves:
2397            # if to_stack in reserves prefer empty stack
2398            # return 1000 - len(to_stack.cards)
2399            return 1000 - int(len(to_stack.cards) != 0)
2400        # prefer non-empty piles in to_stack
2401        return 1001 + int(len(to_stack.cards) != 0)
2402
2403    def _getSpiderQuickPlayScore(self, ncards, from_stack, to_stack):
2404        if to_stack in self.s.reserves:
2405            # if to_stack in reserves prefer empty stack
2406            return 1000-len(to_stack.cards)
2407        # for spider-type stacks
2408        if to_stack.cards:
2409            # check suit
2410            same_suit = (from_stack.cards[-ncards].suit ==
2411                         to_stack.cards[-1].suit)
2412            return int(same_suit)+1002
2413        return 1001
2414
2415    #
2416    # Score (I really don't like scores in Patience games...)
2417    #
2418
2419    # update game-related canvas texts (i.e. self.texts)
2420    def updateText(self):
2421        pass
2422
2423    def getGameScore(self):
2424        return None
2425
2426    # casino type scoring
2427    def getGameScoreCasino(self):
2428        v = -len(self.cards)
2429        for s in self.s.foundations:
2430            v = v + 5 * len(s.cards)
2431        return v
2432
2433    def shallUpdateBalance(self):
2434        # Update the balance unless this is a loaded game or
2435        # a manually selected game number.
2436        if self.gstats.loaded:
2437            return False
2438        if self.random.origin == self.random.ORIGIN_SELECTED:
2439            return False
2440        return True
2441
2442    def getGameBalance(self):
2443        return 0
2444
2445    # compute all hints for the current position
2446    # this is the only method that actually uses class Hint
2447    def getHints(self, level, taken_hint=None):
2448        if level == 3:
2449            # if self.solver is None:
2450            # return None
2451            return self.solver.getHints(taken_hint)
2452        hint_class = self.getHintClass()
2453        if hint_class is None:
2454            return None
2455        hint = hint_class(self, level)      # call constructor
2456        return hint.getHints(taken_hint)    # and return all hints
2457
2458    # give a hint
2459    def showHint(self, level=0, sleep=1.5, taken_hint=None):
2460        if self.getHintClass() is None:
2461            self.highlightNotMatching()
2462            return None
2463        # reset list if level has changed
2464        if level != self.hints.level:
2465            self.hints.level = level
2466            self.hints.list = None
2467        # compute all hints
2468        if self.hints.list is None:
2469            self.hints.list = self.getHints(level, taken_hint)
2470            # print self.hints.list
2471            self.hints.index = 0
2472        # get next hint from list
2473        if not self.hints.list:
2474            self.highlightNotMatching()
2475            return None
2476        h = self.hints.list[self.hints.index]
2477        self.hints.index = self.hints.index + 1
2478        if self.hints.index >= len(self.hints.list):
2479            self.hints.index = 0
2480        # paranoia - verify hint
2481        score, pos, ncards, from_stack, to_stack, text_color, forced_move = h
2482        assert from_stack and len(from_stack.cards) >= ncards
2483        if ncards == 0:
2484            # a deal move, should not happen with level=0/1
2485            assert level >= 2
2486            assert from_stack is self.s.talon
2487            return h
2488        elif from_stack == to_stack:
2489            # a flip move, should not happen with level=0/1
2490            assert level >= 2
2491            assert ncards == 1 and len(from_stack.cards) >= ncards
2492            return h
2493        else:
2494            # a move move
2495            assert to_stack
2496            assert 1 <= ncards <= len(from_stack.cards)
2497            if DEBUG:
2498                if not to_stack.acceptsCards(
2499                        from_stack, from_stack.cards[-ncards:]):
2500                    print('*fail accepts cards*', from_stack, to_stack, ncards)
2501                if not from_stack.canMoveCards(from_stack.cards[-ncards:]):
2502                    print('*fail move cards*', from_stack, ncards)
2503            # assert from_stack.canMoveCards(from_stack.cards[-ncards:])
2504            # FIXME: Pyramid
2505            assert to_stack.acceptsCards(
2506                from_stack, from_stack.cards[-ncards:])
2507        if sleep <= 0.0:
2508            return h
2509        info = (level == 1) or (level > 1 and DEBUG)
2510        if info and self.app.statusbar and self.app.opt.statusbar:
2511            self.app.statusbar.configLabel(
2512                "info", text=_("Score %6d") % (score), fg=text_color)
2513        else:
2514            info = 0
2515        self.drawHintArrow(from_stack, to_stack, ncards, sleep)
2516        if info:
2517            self.app.statusbar.configLabel("info", text="", fg="#000000")
2518        return h
2519
2520    def drawHintArrow(self, from_stack, to_stack, ncards, sleep):
2521        # compute position for arrow
2522        images = self.app.images
2523        x1, y1 = from_stack.getPositionFor(from_stack.cards[-ncards])
2524        x2, y2 = to_stack.getPositionFor(to_stack.getCard())
2525        cw, ch = images.getSize()
2526        dx, dy = images.getDelta()
2527        x1, y1 = x1 + dx, y1 + dy
2528        x2, y2 = x2 + dx, y2 + dy
2529        if ncards == 1:
2530            x1 += cw // 2
2531            y1 += ch // 2
2532        elif from_stack.CARD_XOFFSET[0]:
2533            x1 += from_stack.CARD_XOFFSET[0] // 2
2534            y1 += ch // 2
2535        else:
2536            x1 += cw // 2
2537            y1 += from_stack.CARD_YOFFSET[0] // 2
2538        x2 += cw // 2
2539        y2 += ch // 2
2540        # draw the hint
2541        arrow = MfxCanvasLine(self.canvas, x1, y1, x2, y2, width=7,
2542                              fill=self.app.opt.colors['hintarrow'],
2543                              arrow="last", arrowshape=(30, 30, 10))
2544        self.canvas.update_idletasks()
2545        # wait
2546        if TOOLKIT == "kivy":
2547            arrow.delete_deferred(sleep)
2548            return
2549        # wait
2550        self.sleep(sleep)
2551        # delete the hint
2552        if arrow is not None:
2553            arrow.delete()
2554        self.canvas.update_idletasks()
2555
2556    #
2557    # Demo - uses showHint()
2558    #
2559
2560    def startDemo(self, mixed=1, level=2):
2561        assert level >= 2               # needed for flip/deal hints
2562        if not self.top:
2563            return
2564        self.demo = Struct(
2565            level=level,
2566            mixed=mixed,
2567            sleep=self.app.opt.timeouts['demo'],
2568            last_deal=[],
2569            snapshots=[],
2570            hint=None,
2571            keypress=None,
2572            start_demo_moves=self.stats.demo_moves,
2573            info_text=None,
2574        )
2575        self.hints.list = None
2576        self.createDemoInfoText()
2577        self.createDemoLogo()
2578        after_idle(self.top, self.demoEvent)  # schedule first move
2579
2580    def stopDemo(self, event=None):
2581        if not self.demo:
2582            return
2583        self.canvas.setTopImage(None)
2584        self.demo_logo = None
2585        self.demo = None
2586        self.updateMenus()
2587
2588    # demo event - play one demo move and check for win/loss
2589    def demoEvent(self):
2590        # note: other events are allowed to stop self.demo at any time
2591        if not self.demo or self.demo.keypress:
2592            self.stopDemo()
2593            # self.updateMenus()
2594            return
2595        finished = self.playOneDemoMove(self.demo)
2596        self.finishMove()
2597        self.top.update_idletasks()
2598        self.hints.list = None
2599        player_moves = self.getPlayerMoves()
2600        d, status = None, 0
2601        bitmap = "info"
2602        timeout = 10000
2603        if 1 and player_moves == 0:
2604            timeout = 5000
2605        if self.demo and self.demo.level == 3:
2606            timeout = 0
2607        if self.isGameWon():
2608            self.updateTime()
2609            finished = 1
2610            self.finished = True
2611            self.stopPlayTimer()
2612            if not self.top.winfo_ismapped():
2613                status = 2
2614            elif player_moves == 0:
2615                self.playSample("autopilotwon", priority=1000)
2616                s = self.app.miscrandom.choice((_("&Great"), _("&Cool"),
2617                                                _("&Yeah"),  _("&Wow")))
2618                text = ungettext('\nGame solved in %d move.\n',
2619                                 '\nGame solved in %d moves.\n',
2620                                 self.moves.index)
2621                text = text % self.moves.index
2622                d = MfxMessageDialog(self.top,
2623                                     title=_("%s Autopilot") % TITLE,
2624                                     text=text,
2625                                     image=self.app.gimages.logos[4],
2626                                     strings=(s,),
2627                                     separator=True,
2628                                     timeout=timeout)
2629                status = d.status
2630            else:
2631                # s = self.app.miscrandom.choice((_("&OK"), _("&OK")))
2632                s = _("&OK")
2633                text = _("\nGame finished\n")
2634                if DEBUG:
2635                    text += "\nplayer_moves: %d\ndemo_moves: %d\n" % \
2636                        (self.stats.player_moves, self.stats.demo_moves)
2637                d = MfxMessageDialog(self.top,
2638                                     title=_("%s Autopilot") % TITLE,
2639                                     text=text, bitmap=bitmap, strings=(s,),
2640                                     padx=30, timeout=timeout)
2641                status = d.status
2642        elif finished:
2643            # self.stopPlayTimer()
2644            if not self.top.winfo_ismapped():
2645                status = 2
2646            else:
2647                if player_moves == 0:
2648                    self.playSample("autopilotlost", priority=1000)
2649                s = self.app.miscrandom.choice(
2650                        (_("&Oh well"), _("&That's life"), _("&Hmm")))
2651                # ??? accelerators
2652                d = MfxMessageDialog(self.top,
2653                                     title=_("%s Autopilot") % TITLE,
2654                                     text=_("\nThis won't come out...\n"),
2655                                     bitmap=bitmap, strings=(s,),
2656                                     padx=30, timeout=timeout)
2657                status = d.status
2658        if finished:
2659            self.updateStats(demo=1)
2660            if not DEBUG and self.demo and status == 2:
2661                # timeout in dialog
2662                if self.stats.demo_moves > self.demo.start_demo_moves:
2663                    # we only increase the splash-screen counter if the last
2664                    # demo actually made a move
2665                    self.app.demo_counter += 1
2666                    if self.app.demo_counter % 3 == 0:
2667                        if self.top.winfo_ismapped():
2668                            status = help_about(self.app, timeout=10000)
2669            if self.demo and status == 2:
2670                # timeout in dialog - start another demo
2671                demo = self.demo
2672                id = self.id
2673                if 1 and demo.mixed and DEBUG:
2674                    # debug - advance game id to make sure we hit all games
2675                    gl = self.app.gdb.getGamesIdSortedById()
2676                    # gl = self.app.gdb.getGamesIdSortedByName()
2677                    gl = list(gl)
2678                    index = (gl.index(self.id) + 1) % len(gl)
2679                    id = gl[index]
2680                elif demo.mixed:
2681                    # choose a random game
2682                    gl = self.app.gdb.getGamesIdSortedById()
2683                    while len(gl) > 1:
2684                        id = self.app.getRandomGameId()
2685                        if 0 or id != self.id:      # force change of game
2686                            break
2687                if self.nextGameFlags(id) == 0:
2688                    self.endGame()
2689                    self.newGame(autoplay=0)
2690                    self.startDemo(mixed=demo.mixed)
2691                else:
2692                    self.endGame()
2693                    self.stopDemo()
2694                    self.quitGame(id, startdemo=1)
2695            else:
2696                self.stopDemo()
2697                if DEBUG >= 10:
2698                    # debug - only for testing winAnimation()
2699                    self.endGame()
2700                    self.winAnimation()
2701                    self.newGame()
2702        else:
2703            # game not finished yet
2704            self.top.busyUpdate()
2705            if self.demo:
2706                after_idle(self.top, self.demoEvent)  # schedule next move
2707
2708    # play one demo move while in the demo event
2709    def playOneDemoMove(self, demo):
2710        if self.moves.index > 2000:
2711            # we're probably looping because of some bug in the hint code
2712            return 1
2713        sleep = demo.sleep
2714        # first try to deal cards to the Waste (unless there was a forced move)
2715        if not demo.hint or not demo.hint[6]:
2716            if self._autoDeal(sound=False):
2717                return 0
2718        # display a hint
2719        h = self.showHint(demo.level, sleep, taken_hint=demo.hint)
2720        demo.hint = h
2721        if not h:
2722            return 1
2723        # now actually play the hint
2724        score, pos, ncards, from_stack, to_stack, text_color, forced_move = h
2725        if ncards == 0:
2726            # a deal-move
2727            # do not let games like Klondike and Canfield deal forever
2728            if self.dealCards() == 0:
2729                return 1
2730            if 0:                       # old version, based on dealing card
2731                c = self.s.talon.getCard()
2732                if c in demo.last_deal:
2733                    # We went through the whole Talon. Give up.
2734                    return 1
2735                # Note that `None' is a valid entry in last_deal[]
2736                # (this means that all cards are on the Waste).
2737                demo.last_deal.append(c)
2738            else:                       # new version, based on snapshots
2739                # check snapshot
2740                sn = self.getSnapshot()
2741                if sn in demo.snapshots:
2742                    # not unique
2743                    return 1
2744                demo.snapshots.append(sn)
2745        elif from_stack == to_stack:
2746            # a flip-move
2747            from_stack.flipMove(animation=True)
2748            demo.last_deal = []
2749        else:
2750            # a move-move
2751            from_stack.moveMove(ncards, to_stack, frames=-1)
2752            demo.last_deal = []
2753        return 0
2754
2755    def createDemoInfoText(self):
2756        # TODO - the text placement is not fully ok
2757        if DEBUG:
2758            self.showHelp('help', self.getDemoInfoText())
2759        return
2760        if not self.demo or self.demo.info_text or self.preview:
2761            return
2762        tinfo = [
2763            ("sw", 8, self.height - 8),
2764            ("se", self.width - 8, self.height - 8),
2765            ("nw", 8, 8),
2766            ("ne", self.width - 8, 8),
2767        ]
2768        ta = self.getDemoInfoTextAttr(tinfo)
2769        if ta:
2770            # font = self.app.getFont("canvas_large")
2771            font = self.app.getFont("default")
2772            self.demo.info_text = MfxCanvasText(self.canvas, ta[1], ta[2],
2773                                                anchor=ta[0], font=font,
2774                                                text=self.getDemoInfoText())
2775
2776    def getDemoInfoText(self):
2777        h = self.Hint_Class is None and 'None' or self.Hint_Class.__name__
2778        return '%s (%s)' % (self.gameinfo.short_name, h)
2779
2780    def getDemoInfoTextAttr(self, tinfo):
2781        items1, items2 = [], []
2782        for s in self.allstacks:
2783            if s.is_visible:
2784                items1.append(s)
2785                items1.extend(list(s.cards))
2786                if not s.cards and s.cap.max_accept > 0:
2787                    items2.append(s)
2788                else:
2789                    items2.extend(list(s.cards))
2790        ti = self.__checkFreeSpaceForDemoInfoText(items1)
2791        if ti < 0:
2792            ti = self.__checkFreeSpaceForDemoInfoText(items2)
2793        if ti < 0:
2794            return None
2795        return tinfo[ti]
2796
2797    def __checkFreeSpaceForDemoInfoText(self, items):
2798        CW, CH = self.app.images.CARDW, self.app.images.CARDH
2799        # note: these are translated by (-CW/2, -CH/2)
2800        x1, x2 = 3*CW//2, self.width - 5*CW//2
2801        y1, y2 = CH//2, self.height - 3*CH//2
2802        #
2803        m = [1, 1, 1, 1]
2804        for c in items:
2805            cx, cy = c.x, c.y
2806            if cy >= y2:
2807                if cx <= x1:
2808                    m[0] = 0
2809                elif cx >= x2:
2810                    m[1] = 0
2811            elif cy <= y1:
2812                if cx <= x1:
2813                    m[2] = 0
2814                elif cx >= x2:
2815                    m[3] = 0
2816        for mm in m:
2817            if mm:
2818                return mm
2819        return -1
2820
2821    def createDemoLogo(self):
2822        if not self.app.gimages.demo:
2823            return
2824        if self.demo_logo or not self.app.opt.demo_logo:
2825            return
2826        if self.width <= 100 or self.height <= 100:
2827            return
2828        # self.demo_logo = self.app.miscrandom.choice(self.app.gimages.demo)
2829        n = self.random.initial_seed % len(self.app.gimages.demo)
2830        self.demo_logo = self.app.gimages.demo[int(n)]
2831        self.canvas.setTopImage(self.demo_logo)
2832
2833    def getStuck(self):
2834        h = self.Stuck_Class.getHints(None)
2835        if h:
2836            self.failed_snapshots = []
2837            return True
2838        if not self.canDealCards():
2839            return False
2840        # can deal cards: do we have any hints in previous deals ?
2841        sn = self.getSnapshot()
2842        if sn in self.failed_snapshots:
2843            return False
2844        self.failed_snapshots.append(sn)
2845        return True
2846
2847    def updateStuck(self):
2848        # stuck
2849        if self.finished:
2850            return
2851        if self.Stuck_Class is None:
2852            return
2853        if self.getStuck():
2854            text = ''
2855        else:
2856            text = 'x'
2857            # self.playSample("autopilotlost", priority=1000)
2858        self.updateStatus(stuck=text)
2859
2860    #
2861    # Handle moves (with move history for undo/redo)
2862    # Actual move is handled in a subclass of AtomicMove.
2863    #
2864    # Note:
2865    # All playing moves (user actions, demo games) must get routed
2866    # to Stack.moveMove() because the stack may add important
2867    # triggers to a move (most notably fillStack and updateModel).
2868    #
2869    # Only low-level game (Game.startGame, Game.dealCards, Game.fillStack)
2870    # or stack methods (Stack.moveMove) should call the functions below
2871    # directly.
2872    #
2873
2874    def startMoves(self):
2875        self.moves = GameMoves()
2876        self.stats._reset_statistics()
2877
2878    def __storeMove(self, am):
2879        if self.S_DEAL <= self.moves.state <= self.S_PLAY:
2880            self.moves.current.append(am)
2881
2882    # move type 1
2883    def moveMove(self, ncards, from_stack, to_stack, frames=-1, shadow=-1):
2884        assert from_stack and to_stack and from_stack is not to_stack
2885        assert 0 < ncards <= len(from_stack.cards)
2886        am = AMoveMove(ncards, from_stack, to_stack, frames, shadow)
2887        self.__storeMove(am)
2888        am.do(self)
2889        self.hints.list = None
2890
2891    # move type 2
2892    def flipMove(self, stack):
2893        assert stack
2894        am = AFlipMove(stack)
2895        self.__storeMove(am)
2896        am.do(self)
2897        self.hints.list = None
2898
2899    def singleFlipMove(self, stack):
2900        # flip with animation (without "moveMove" in this move)
2901        assert stack
2902        am = ASingleFlipMove(stack)
2903        self.__storeMove(am)
2904        am.do(self)
2905        self.hints.list = None
2906
2907    def flipAndMoveMove(self, from_stack, to_stack, frames=-1):
2908        assert from_stack and to_stack and (from_stack is not to_stack)
2909        am = AFlipAndMoveMove(from_stack, to_stack, frames)
2910        self.__storeMove(am)
2911        am.do(self)
2912        self.hints.list = None
2913
2914    # move type 3
2915    def turnStackMove(self, from_stack, to_stack):
2916        assert from_stack and to_stack and (from_stack is not to_stack)
2917        assert len(to_stack.cards) == 0
2918        am = ATurnStackMove(from_stack, to_stack)
2919        self.__storeMove(am)
2920        am.do(self)
2921        self.hints.list = None
2922
2923    # move type 4
2924    def nextRoundMove(self, stack):
2925        assert stack
2926        am = ANextRoundMove(stack)
2927        self.__storeMove(am)
2928        am.do(self)
2929        self.hints.list = None
2930
2931    # move type 5
2932    def saveSeedMove(self):
2933        am = ASaveSeedMove(self)
2934        self.__storeMove(am)
2935        am.do(self)
2936        # self.hints.list = None
2937
2938    # move type 6
2939    def shuffleStackMove(self, stack):
2940        assert stack
2941        am = AShuffleStackMove(stack, self)
2942        self.__storeMove(am)
2943        am.do(self)
2944        self.hints.list = None
2945
2946    # move type 7
2947    def updateStackMove(self, stack, flags):
2948        assert stack
2949        am = AUpdateStackMove(stack, flags)
2950        self.__storeMove(am)
2951        am.do(self)
2952        # #self.hints.list = None
2953
2954    # move type 8
2955    def flipAllMove(self, stack):
2956        assert stack
2957        am = AFlipAllMove(stack)
2958        self.__storeMove(am)
2959        am.do(self)
2960        self.hints.list = None
2961
2962    # move type 9
2963    def saveStateMove(self, flags):
2964        am = ASaveStateMove(self, flags)
2965        self.__storeMove(am)
2966        am.do(self)
2967        # self.hints.list = None
2968
2969    # for ArbitraryStack
2970    def singleCardMove(self, from_stack, to_stack, position,
2971                       frames=-1, shadow=-1):
2972        am = ASingleCardMove(from_stack, to_stack, position, frames, shadow)
2973        self.__storeMove(am)
2974        am.do(self)
2975        self.hints.list = None
2976
2977    # Finish the current move.
2978    def finishMove(self):
2979        current, moves, stats = self.moves.current, self.moves, self.stats
2980        if not current:
2981            return 0
2982        # invalidate hints
2983        self.hints.list = None
2984        # update stats
2985        if self.demo:
2986            stats.demo_moves += 1
2987            if moves.index == 0:
2988                stats.player_moves = 0  # clear all player moves
2989        else:
2990            stats.player_moves += 1
2991            if moves.index == 0:
2992                stats.demo_moves = 0    # clear all demo moves
2993        stats.total_moves += 1
2994
2995        # try to detect a redo move in order to keep our history
2996        redo = 0
2997        if moves.index + 1 < len(moves.history):
2998            mylen, m = len(current), moves.history[moves.index]
2999            if mylen == len(m):
3000                for i in range(mylen):
3001                    a1 = current[i]
3002                    a2 = m[i]
3003                    if a1.__class__ is not a2.__class__ or \
3004                            a1.cmpForRedo(a2) != 0:
3005                        break
3006                else:
3007                    redo = 1
3008        # add current move to history (which is a list of lists)
3009        if redo:
3010            # print "detected redo:", current
3011            # overwrite existing entry because minor things like
3012            # shadow/frames may have changed
3013            moves.history[moves.index] = current
3014            moves.index += 1
3015        else:
3016            # resize (i.e. possibly shorten list from previous undos)
3017            moves.history[moves.index:] = [current]
3018            moves.index += 1
3019            assert moves.index == len(moves.history)
3020
3021        moves.current = []
3022        self.updateSnapshots()
3023        # update view
3024        self.updateText()
3025        self.updateStatus(moves=(moves.index, self.stats.total_moves))
3026        self.updateMenus()
3027        self.updatePlayTime(do_after=0)
3028        self.updateStuck()
3029        reset_solver_dialog()
3030
3031        return 1
3032
3033    def undo(self):
3034        assert self.canUndo()
3035        assert self.moves.state == self.S_PLAY and len(self.moves.current) == 0
3036        assert 0 <= self.moves.index <= len(self.moves.history)
3037        if self.moves.index == 0:
3038            return
3039        self.moves.index -= 1
3040        self.moves.state = self.S_UNDO
3041        for atomic_move in reversed(self.moves.history[self.moves.index]):
3042            atomic_move.undo(self)
3043        self.moves.state = self.S_PLAY
3044        self.stats.undo_moves += 1
3045        self.stats.total_moves += 1
3046        self.hints.list = None
3047        self.updateSnapshots()
3048        self.updateText()
3049        self.updateStatus(moves=(self.moves.index, self.stats.total_moves))
3050        self.updateMenus()
3051        self.updateStatus(stuck='')
3052        self.failed_snapshots = []
3053        reset_solver_dialog()
3054
3055    def redo(self):
3056        assert self.canRedo()
3057        assert self.moves.state == self.S_PLAY and len(self.moves.current) == 0
3058        assert 0 <= self.moves.index <= len(self.moves.history)
3059        if self.moves.index == len(self.moves.history):
3060            return
3061        m = self.moves.history[self.moves.index]
3062        self.moves.index += 1
3063        self.moves.state = self.S_REDO
3064        for atomic_move in m:
3065            atomic_move.redo(self)
3066        self.moves.state = self.S_PLAY
3067        self.stats.redo_moves += 1
3068        self.stats.total_moves += 1
3069        self.hints.list = None
3070        self.updateSnapshots()
3071        self.updateText()
3072        self.updateStatus(moves=(self.moves.index, self.stats.total_moves))
3073        self.updateMenus()
3074        self.updateStuck()
3075        reset_solver_dialog()
3076
3077    #
3078    # subclass hooks
3079    #
3080
3081    def setState(self, state):
3082        # restore saved vars (from undo/redo)
3083        pass
3084
3085    def getState(self):
3086        # save vars (for undo/redo)
3087        return []
3088
3089    #
3090    # bookmarks
3091    #
3092
3093    def setBookmark(self, n, confirm=1):
3094        self.finishMove()       # just in case
3095        if not self.canSetBookmark():
3096            return 0
3097        if confirm < 0:
3098            confirm = self.app.opt.confirm
3099        if confirm and self.gsaveinfo.bookmarks.get(n):
3100            if not self.areYouSure(
3101                    _("Set bookmark"),
3102                    _("Replace existing bookmark %d?") % (n+1)):
3103                return 0
3104        f = BytesIO()
3105        try:
3106            self._dumpGame(Pickler(f, 1), bookmark=2)
3107            bm = (f.getvalue(), self.moves.index)
3108        except Exception:
3109            pass
3110        else:
3111            self.gsaveinfo.bookmarks[n] = bm
3112            return 1
3113        return 0
3114
3115    def gotoBookmark(self, n, confirm=-1, update_stats=1):
3116        self.finishMove()       # just in case
3117        bm = self.gsaveinfo.bookmarks.get(n)
3118        if not bm:
3119            return
3120        if confirm < 0:
3121            confirm = self.app.opt.confirm
3122        if confirm:
3123            if not self.areYouSure(_("Goto bookmark"),
3124                                   _("Goto bookmark %d?") % (n+1)):
3125                return
3126        try:
3127            s, moves_index = bm
3128            self.setCursor(cursor=CURSOR_WATCH)
3129            file = BytesIO(s)
3130            p = Unpickler(file)
3131            game = self._undumpGame(p, self.app)
3132            assert game.id == self.id
3133            # save state for undoGotoBookmark
3134            self.setBookmark(-1, confirm=0)
3135        except Exception:
3136            del self.gsaveinfo.bookmarks[n]
3137            self.setCursor(cursor=self.app.top_cursor)
3138        else:
3139            if update_stats:
3140                self.stats.goto_bookmark_moves += 1
3141                self.gstats.goto_bookmark_moves += 1
3142            self.restoreGame(game, reset=0)
3143            destruct(game)
3144
3145    def undoGotoBookmark(self):
3146        self.gotoBookmark(-1, update_stats=0)
3147
3148    def loadGame(self, filename):
3149        if self.changed():
3150            if not self.areYouSure(_("Open game")):
3151                return
3152        self.finishMove()       # just in case
3153        game = None
3154        self.setCursor(cursor=CURSOR_WATCH)
3155        self.disableMenus()
3156        try:
3157            game = self._loadGame(filename, self.app)
3158            game.gstats.holded = 0
3159        except AssertionError:
3160            self.updateMenus()
3161            self.setCursor(cursor=self.app.top_cursor)
3162            MfxMessageDialog(
3163                self.top, title=_("Load game error"), bitmap="error",
3164                text=_(
3165                    "Error while loading game.\n\n" +
3166                    "Probably the game file is damaged,\n" +
3167                    "but this could also be a bug you might want to report."))
3168            traceback.print_exc()
3169        except UnpicklingError as ex:
3170            self.updateMenus()
3171            self.setCursor(cursor=self.app.top_cursor)
3172            MfxExceptionDialog(self.top, ex, title=_("Load game error"),
3173                               text=_("Error while loading game"))
3174        except Exception:
3175            self.updateMenus()
3176            self.setCursor(cursor=self.app.top_cursor)
3177            MfxMessageDialog(
3178                self.top, title=_("Load game error"),
3179                bitmap="error", text=_(
3180                    """Internal error while loading game.\n\n""" +
3181                    "Please report this bug."))
3182            traceback.print_exc()
3183        else:
3184            if self.pause:
3185                # unselect pause-button
3186                self.app.menubar.mPause()
3187            self.filename = filename
3188            game.filename = filename
3189            # now start the new game
3190            # print game.__dict__
3191            if self.nextGameFlags(game.id) == 0:
3192                self.endGame()
3193                self.restoreGame(game)
3194                destruct(game)
3195            else:
3196                self.endGame()
3197                self.quitGame(game.id, loadedgame=game)
3198
3199    def saveGame(self, filename, protocol=-1):
3200        self.finishMove()       # just in case
3201        self.setCursor(cursor=CURSOR_WATCH)
3202        try:
3203            self._saveGame(filename, protocol)
3204        except Exception as ex:
3205            self.setCursor(cursor=self.app.top_cursor)
3206            MfxExceptionDialog(self.top, ex, title=_("Save game error"),
3207                               text=_("Error while saving game"))
3208        else:
3209            self.filename = filename
3210            self.setCursor(cursor=self.app.top_cursor)
3211
3212    #
3213    # low level load/save
3214    #
3215
3216    def _loadGame(self, filename, app):
3217        game = None
3218        with open(filename, "rb") as f:
3219            game = self._undumpGame(Unpickler(f), app)
3220            game.gstats.loaded += 1
3221        return game
3222
3223    def _undumpGame(self, p, app):
3224        self.updateTime()
3225        #
3226        err_txt = _("Invalid or damaged %s save file") % PACKAGE
3227        #
3228
3229        def pload(t=None, p=p):
3230            obj = p.load()
3231            if isinstance(t, type):
3232                if not isinstance(obj, t):
3233                    # accept old storage format in case:
3234                    if t in _Game_LOAD_CLASSES:
3235                        assert isinstance(obj, Struct), err_txt
3236                    else:
3237                        assert False, err_txt
3238            return obj
3239
3240        def validate(v, txt):
3241            if not v:
3242                raise UnpicklingError(txt)
3243        #
3244        package = pload(str)
3245        validate(package == PACKAGE, err_txt)
3246        version = pload(str)
3247        # validate(isinstance(version, str) and len(version) <= 20, err_txt)
3248        version_tuple = pload(tuple)
3249        validate(
3250            version_tuple >= (1, 0),
3251            _('Cannot load games saved with\n%(app)s version %(ver)s') % {
3252                'app': PACKAGE,
3253                'ver': version})
3254        game_version = 1
3255        bookmark = pload(int)
3256        validate(0 <= bookmark <= 2, err_txt)
3257        game_version = pload(int)
3258        validate(game_version > 0, err_txt)
3259        #
3260        id = pload(int)
3261        validate(id > 0, err_txt)
3262        if id not in GI.PROTECTED_GAMES:
3263            game = app.constructGame(id)
3264            if game:
3265                if not game.canLoadGame(version_tuple, game_version):
3266                    destruct(game)
3267                    game = None
3268        validate(
3269            game is not None,
3270            _('Cannot load this game from version %s\n' +
3271              'as the game rules have changed\n' +
3272              'in the current implementation.') % version)
3273        game.version = version
3274        game.version_tuple = version_tuple
3275        #
3276        initial_seed = random__int2str(pload(int))
3277        game.random = construct_random(initial_seed)
3278        state = pload()
3279        if (game.random is not None and
3280                not isinstance(game.random, random2.Random) and
3281                isinstance(state, int)):
3282            game.random.setstate(state)
3283        # if not hasattr(game.random, "origin"):
3284        # game.random.origin = game.random.ORIGIN_UNKNOWN
3285        game.loadinfo.stacks = []
3286        game.loadinfo.ncards = 0
3287        nstacks = pload(int)
3288        validate(1 <= nstacks, err_txt)
3289        for i in range(nstacks):
3290            stack = []
3291            ncards = pload(int)
3292            validate(0 <= ncards <= 1024, err_txt)
3293            for j in range(ncards):
3294                card_id = pload(int)
3295                face_up = pload(int)
3296                stack.append((card_id, face_up))
3297            game.loadinfo.stacks.append(stack)
3298            game.loadinfo.ncards = game.loadinfo.ncards + ncards
3299        validate(game.loadinfo.ncards == game.gameinfo.ncards, err_txt)
3300        game.loadinfo.talon_round = pload()
3301        game.finished = pload()
3302        if 0 <= bookmark <= 1:
3303            saveinfo = pload(GameSaveInfo)
3304            game.saveinfo.__dict__.update(saveinfo.__dict__)
3305            gsaveinfo = pload(GameGlobalSaveInfo)
3306            game.gsaveinfo.__dict__.update(gsaveinfo.__dict__)
3307        moves = pload(GameMoves)
3308        game.moves.__dict__.update(moves.__dict__)
3309        snapshots = pload(list)
3310        game.snapshots = snapshots
3311        if 0 <= bookmark <= 1:
3312            gstats = pload(GameGlobalStatsStruct)
3313            game.gstats.__dict__.update(gstats.__dict__)
3314            stats = pload(GameStatsStruct)
3315            game.stats.__dict__.update(stats.__dict__)
3316        game._loadGameHook(p)
3317        dummy = pload(str)
3318        validate(dummy == "EOF", err_txt)
3319        if bookmark == 2:
3320            # copy back all variables that are not saved
3321            game.stats = self.stats
3322            game.gstats = self.gstats
3323            game.saveinfo = self.saveinfo
3324            game.gsaveinfo = self.gsaveinfo
3325        return game
3326
3327    def _saveGame(self, filename, protocol=-1):
3328        if self.canSaveGame():
3329            with open(filename, "wb") as f:
3330                self._dumpGame(Pickler(f, protocol))
3331
3332    def _dumpGame(self, p, bookmark=0):
3333        return pysolDumpGame(self, p, bookmark)
3334
3335    def startPlayTimer(self):
3336        self.updateStatus(time=None)
3337        self.stopPlayTimer()
3338        self.play_timer = after(
3339            self.top, PLAY_TIME_TIMEOUT, self.updatePlayTime)
3340
3341    def stopPlayTimer(self):
3342        if hasattr(self, 'play_timer') and self.play_timer:
3343            after_cancel(self.play_timer)
3344            self.play_timer = None
3345            self.updatePlayTime(do_after=0)
3346
3347    def updatePlayTime(self, do_after=1):
3348        if not self.top:
3349            return
3350        if self.pause or self.finished:
3351            return
3352        if do_after:
3353            self.play_timer = after(
3354                self.top, PLAY_TIME_TIMEOUT, self.updatePlayTime)
3355        d = time.time() - self.stats.update_time + self.stats.elapsed_time
3356        self.updateStatus(time=format_time(d))
3357
3358    def doPause(self):
3359        if self.finished:
3360            return
3361        if self.demo:
3362            self.stopDemo()
3363        if not self.pause:
3364            self.updateTime()
3365        self.pause = not self.pause
3366        if self.pause:
3367            # self.updateTime()
3368            self.canvas.hideAllItems()
3369            n = self.random.initial_seed % len(self.app.gimages.pause)
3370            self.pause_logo = self.app.gimages.pause[int(n)]
3371            self.canvas.setTopImage(self.pause_logo)
3372        else:
3373            self.stats.update_time = time.time()
3374            self.updatePlayTime()
3375            self.canvas.setTopImage(None)
3376            self.pause_logo = None
3377            self.canvas.showAllItems()
3378
3379    def showHelp(self, *args):
3380        if self.preview:
3381            return
3382        kw = dict([(args[i], args[i+1]) for i in range(0, len(args), 2)])
3383        if not kw:
3384            kw = {'info': '', 'help': ''}
3385        if 'info' in kw and self.app.opt.statusbar and self.app.opt.num_cards:
3386            self.app.statusbar.updateText(info=kw['info'])
3387        if 'help' in kw and self.app.opt.helpbar:
3388            self.app.helpbar.updateText(info=kw['help'])
3389
3390    #
3391    # Piles descriptions
3392    #
3393
3394    def showStackDesc(self):
3395        from pysollib.pysoltk import StackDesc
3396        from pysollib.stack import InitialDealTalonStack
3397        sd_list = []
3398        for s in self.allstacks:
3399            sd = (s.__class__.__name__, s.cap.base_rank, s.cap.dir)
3400            if sd in sd_list:
3401                # one of each uniq pile
3402                continue
3403            if isinstance(s, InitialDealTalonStack):
3404                continue
3405            self.stackdesc_list.append(StackDesc(self, s))
3406            sd_list.append(sd)
3407
3408    def deleteStackDesc(self):
3409        if self.stackdesc_list:
3410            for sd in self.stackdesc_list:
3411                sd.delete()
3412            self.stackdesc_list = []
3413            return True
3414        return False
3415
3416    # for find_card_dialog
3417    def canFindCard(self):
3418        return self.gameinfo.category == GI.GC_FRENCH
3419
3420    #
3421    # subclass hooks
3422    #
3423
3424    def _restoreGameHook(self, game):
3425        pass
3426
3427    def _loadGameHook(self, p):
3428        pass
3429
3430    def _saveGameHook(self, p):
3431        pass
3432
3433    def _dealNumRows(self, n):
3434        for i in range(n):
3435            self.s.talon.dealRow(frames=0)
3436
3437    def _startDealNumRows(self, n):
3438        self._dealNumRows(n)
3439        self.startDealSample()
3440
3441    def _startDealNumRowsAndDealSingleRow(self, n):
3442        self._startDealNumRows(n)
3443        self.s.talon.dealRow()
3444
3445    def _startAndDealRow(self):
3446        self._startDealNumRowsAndDealSingleRow(0)
3447
3448    def _startDealNumRowsAndDealRowAndCards(self, n):
3449        self._startDealNumRowsAndDealSingleRow(n)
3450        self.s.talon.dealCards()
3451
3452    def _startAndDealRowAndCards(self):
3453        self._startAndDealRow()
3454        self.s.talon.dealCards()
3455
3456
3457class StartDealRowAndCards(object):
3458    def startGame(self):
3459        self._startAndDealRowAndCards()
3460