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