# !/usr/bin/env python # -*- mode: python; coding: utf-8; -*- # --------------------------------------------------------------------------- # # Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer # Copyright (C) 2003 Mt. Hood Playing Card Co. # Copyright (C) 2005-2009 Skomoroh # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # --------------------------------------------------------------------------- # imports import math import time import traceback from pickle import Pickler, Unpickler, UnpicklingError from pysollib.gamedb import GI from pysollib.mfxutil import Image, ImageTk, USE_PIL from pysollib.mfxutil import Struct, SubclassResponsibility, destruct from pysollib.mfxutil import format_time, print_err from pysollib.mfxutil import uclock, usleep from pysollib.mygettext import _ from pysollib.mygettext import ungettext from pysollib.pysolrandom import LCRandom31, PysolRandom, constructRandom, \ random__long2str, random__str2long from pysollib.pysoltk import CURSOR_WATCH from pysollib.pysoltk import Card from pysollib.pysoltk import EVENT_HANDLED, EVENT_PROPAGATE from pysollib.pysoltk import MfxCanvasLine, MfxCanvasRectangle, MfxCanvasText from pysollib.pysoltk import MfxExceptionDialog, MfxMessageDialog from pysollib.pysoltk import after, after_cancel, after_idle from pysollib.pysoltk import bind, wm_map from pysollib.settings import DEBUG from pysollib.settings import PACKAGE, TITLE, TOOLKIT, TOP_TITLE from pysollib.settings import VERSION, VERSION_TUPLE import six from six import BytesIO from six.moves import range if TOOLKIT == 'tk': from pysollib.ui.tktile.solverdialog import reset_solver_dialog else: from pysollib.pysoltk import reset_solver_dialog if True: # This prevents from travis 'error' E402. from pysollib.move import AMoveMove, AFlipMove, AFlipAndMoveMove from pysollib.move import ASingleFlipMove, ATurnStackMove from pysollib.move import ANextRoundMove, ASaveSeedMove, AShuffleStackMove from pysollib.move import AUpdateStackMove, AFlipAllMove, ASaveStateMove from pysollib.move import ASingleCardMove from pysollib.hint import DefaultHint from pysollib.help import help_about PLAY_TIME_TIMEOUT = 200 # ************************************************************************ # * Base class for all solitaire games # * # * Handles: # * load/save # * undo/redo (using a move history) # * hints/demo # ************************************************************************ class Game(object): # for self.gstats.updated U_PLAY = 0 U_WON = -2 U_LOST = -3 U_PERFECT = -4 # for self.moves.state S_INIT = 0x00 S_DEAL = 0x10 S_FILL = 0x20 S_RESTORE = 0x30 S_PLAY = 0x40 S_UNDO = 0x50 S_REDO = 0x60 # for loading and saving - subclasses should override if # the format for a saved game changed (see also canLoadGame()) GAME_VERSION = 1 # # game construction # # only basic initialization here def __init__(self, gameinfo): self.preview = 0 self.random = None self.gameinfo = gameinfo self.id = gameinfo.id assert self.id > 0 self.busy = 0 self.pause = False self.finished = False self.version = VERSION self.version_tuple = VERSION_TUPLE self.cards = [] self.stackmap = {} # dict with (x,y) tuples as key self.allstacks = [] self.sn_groups = [] # snapshot groups; list of list of similar stacks self.snapshots = [] self.failed_snapshots = [] self.stackdesc_list = [] self.demo_logo = None self.pause_logo = None self.s = Struct( # stacks talon=None, waste=None, foundations=[], rows=[], reserves=[], internals=[], ) self.sg = Struct( # stack-groups openstacks=[], # for getClosestStack(): only on # these stacks the player can place a card talonstacks=[], # for Hint dropstacks=[], # for Hint & getAutoStacks() reservestacks=[], # for Hint # hint=Struct(), # extra info for class Hint hp_stacks=[], # for getHightlightPilesStacks() ) self.regions = Struct( # for getClosestStack() # set by optimizeRegions(): info=[], # list of tuples(stacks, rect) remaining=[], # list of stacks in no region # data=[], # raw data init_info=[], # init info (at the start) ) self.init_size = (0, 0) self.event_handled = False # if click event handled by Stack (???) self.reset() # main constructor def create(self, app): # print 'Game.create' old_busy = self.busy self.__createCommon(app) self.setCursor(cursor=CURSOR_WATCH) # print 'gameid:', self.id self.top.wm_title(TITLE + " - " + self.getTitleName()) self.top.wm_iconname(TITLE + " - " + self.getTitleName()) # create the game if self.app.intro.progress: self.app.intro.progress.update(step=1) self.createGame() # set some defaults self.sg.openstacks = [s for s in self.sg.openstacks if s.cap.max_accept >= s.cap.min_accept] self.sg.hp_stacks = [s for s in self.sg.dropstacks if s.cap.max_move >= 2] self.createSnGroups() # convert stackgroups to tuples (speed) self.allstacks = tuple(self.allstacks) self.s.foundations = tuple(self.s.foundations) self.s.rows = tuple(self.s.rows) self.s.reserves = tuple(self.s.reserves) self.s.internals = tuple(self.s.internals) self.sg.openstacks = tuple(self.sg.openstacks) self.sg.talonstacks = tuple(self.sg.talonstacks) self.sg.dropstacks = tuple(self.sg.dropstacks) self.sg.reservestacks = tuple(self.sg.reservestacks) self.sg.hp_stacks = tuple(self.sg.hp_stacks) # init the stack view for stack in self.allstacks: stack.prepareStack() stack.assertStack() if self.s.talon: assert hasattr(self.s.talon, "round") assert hasattr(self.s.talon, "max_rounds") if DEBUG: self._checkGame() # optimize regions self.optimizeRegions() # create cards if not self.cards: self.cards = self.createCards(progress=self.app.intro.progress) self.initBindings() # self.top.bind('', self.top._sleepEvent) # self.top.bind('<3>', self.top._sleepEvent) # update display properties self.canvas.busy = True # geometry mycond = (not USE_PIL and self.app.opt.save_games_geometry and self.id in self.app.opt.games_geometry) if mycond: # restore game geometry w, h = self.app.opt.games_geometry[self.id] self.canvas.config(width=w, height=h) if USE_PIL: if self.app.opt.auto_scale: w, h = self.app.opt.game_geometry self.canvas.setInitialSize(w, h, margins=False, scrollregion=False) # self.canvas.config(width=w, height=h) # dx, dy = self.canvas.xmargin, self.canvas.ymargin # self.canvas.config(scrollregion=(-dx, -dy, dx, dy)) else: w = int(round(self.width * self.app.opt.scale_x)) h = int(round(self.height * self.app.opt.scale_y)) self.canvas.setInitialSize(w, h) self.top.wm_geometry("") # cancel user-specified geometry # preserve texts positions for t in ('info', 'help', 'misc', 'score', 'base_rank'): item = getattr(self.texts, t) if item: coords = self.canvas.coords(item) setattr(self.init_texts, t, coords) # for item in self.texts.list: coords = self.canvas.coords(item) self.init_texts.list.append(coords) # resize self.resizeGame() # fix coords of cards (see self.createCards) x, y = self.s.talon.x, self.s.talon.y for c in self.cards: c.moveTo(x, y) else: # no PIL self.canvas.setInitialSize(self.width, self.height) self.top.wm_geometry("") # cancel user-specified geometry # done; update view self.top.update_idletasks() self.canvas.busy = False if DEBUG >= 4: MfxCanvasRectangle(self.canvas, 0, 0, self.width, self.height, width=2, fill=None, outline='green') # self.stats.update_time = time.time() self.showHelp() # just in case hint_class = self.getHintClass() if hint_class is not None: self.Stuck_Class = hint_class(self, 0) self.busy = old_busy def _checkGame(self): class_name = self.__class__.__name__ if self.s.foundations: ncards = 0 for stack in self.s.foundations: ncards += stack.cap.max_cards if ncards != self.gameinfo.ncards: print_err('invalid sum of foundations.max_cards: ' '%s: %s %s' % (class_name, ncards, self.gameinfo.ncards), 2) if self.s.rows: from pysollib.stack import AC_RowStack, UD_AC_RowStack, \ SS_RowStack, UD_SS_RowStack, \ RK_RowStack, UD_RK_RowStack, \ Spider_AC_RowStack, Spider_SS_RowStack r = self.s.rows[0] for c, f in ( ((Spider_AC_RowStack, Spider_SS_RowStack), (self._shallHighlightMatch_RK, self._shallHighlightMatch_RKW)), ((AC_RowStack, UD_AC_RowStack), (self._shallHighlightMatch_AC, self._shallHighlightMatch_ACW)), ((SS_RowStack, UD_SS_RowStack), (self._shallHighlightMatch_SS, self._shallHighlightMatch_SSW)), ((RK_RowStack, UD_RK_RowStack), (self._shallHighlightMatch_RK, self._shallHighlightMatch_RKW)),): if isinstance(r, c): if self.shallHighlightMatch not in f: print_err('shallHighlightMatch is not valid: ' ' %s, %s' % (class_name, r.__class__), 2) if r.cap.mod == 13 and self.shallHighlightMatch != f[1]: print_err('shallHighlightMatch is not valid (wrap): ' ' %s, %s' % (class_name, r.__class__), 2) break if self.s.talon.max_rounds > 1 and self.s.talon.texts.rounds is None: print_err('max_rounds > 1, but talon.texts.rounds is None: ' '%s' % class_name, 2) elif (self.s.talon.max_rounds <= 1 and self.s.talon.texts.rounds is not None): print_err('max_rounds <= 1, but talon.texts.rounds is not None: ' '%s' % class_name, 2) def initBindings(self): # note: a Game is only allowed to bind self.canvas and not to self.top # bind(self.canvas, "", self.undoHandler) bind(self.canvas, "<1>", self.undoHandler) bind(self.canvas, "<2>", self.dropHandler) bind(self.canvas, "<3>", self.redoHandler) bind(self.canvas, '', self._unmapHandler) bind(self.canvas, '', self._configureHandler, add=True) def __createCommon(self, app): self.busy = 1 self.app = app self.top = app.top self.canvas = app.canvas self.filename = "" self.drag = Struct( event=None, # current event timer=None, # current event timer start_x=0, # X coord of initial drag event start_y=0, # Y coord of initial drag event stack=None, # cards=[], # index=-1, # shadows=[], # list of canvas images shade_stack=None, # stack currently shaded shade_img=None, # canvas image canshade_stacks=[], # list of stacks already tested noshade_stacks=[], # for this drag ) if self.gstats.start_player is None: self.gstats.start_player = self.app.opt.player # optional MfxCanvasText items self.texts = Struct( info=None, # misc info text help=None, # a static help text misc=None, # score=None, # for displaying the score base_rank=None, # for displaying the base_rank list=[], # list of texts (if we use many text) ) # initial position of the texts self.init_texts = Struct( info=None, # misc info text help=None, # a static help text misc=None, # score=None, # for displaying the score base_rank=None, # for displaying the base_rank list=[], ) def createPreview(self, app): old_busy = self.busy self.__createCommon(app) self.preview = max(1, self.canvas.preview) # create game self.createGame() # set some defaults self.sg.openstacks = [s for s in self.sg.openstacks if s.cap.max_accept >= s.cap.min_accept] self.sg.hp_stacks = [s for s in self.sg.dropstacks if s.cap.max_move >= 2] # init the stack view for stack in self.allstacks: stack.prepareStack() stack.assertStack() # optimize regions self.optimizeRegions() # create cards self.cards = self.createCards() # self.canvas.setInitialSize(self.width, self.height) self.busy = old_busy def destruct(self): # help breaking circular references for obj in self.cards: destruct(obj) for obj in self.allstacks: obj.destruct() destruct(obj) # Do not destroy game structure (like stacks and cards) here ! def reset(self, restart=0): self.filename = "" self.demo = None self.solver = None self.hints = Struct( list=None, # list of hints for the current move index=-1, level=-1, ) self.saveinfo = Struct( # needed for saving a game stack_caps=[], ) self.loadinfo = Struct( # used when loading a game stacks=None, talon_round=1, ncards=0, ) self.snapshots = [] self.failed_snapshots = [] # local statistics are reset on each game restart self.stats = Struct( hints=0, # number of hints consumed highlight_piles=0, # number of highlight piles consumed # number of highlight matching cards consumed highlight_cards=0, highlight_samerank=0, # number of highlight same rank consumed undo_moves=0, # number of undos redo_moves=0, # number of redos total_moves=0, # number of total moves in this game player_moves=0, # number of moves demo_moves=0, # number of moves while in demo mode autoplay_moves=0, # number of moves quickplay_moves=0, # number of quickplay moves goto_bookmark_moves=0, # number of goto bookmark shuffle_moves=0, # number of shuffles (Mahjongg) # did this game already update the demo stats ? demo_updated=0, update_time=time.time(), # for updateTime() elapsed_time=0.0, pause_start_time=0.0, ) self.startMoves() if restart: return # global statistics survive a game restart self.gstats = Struct( holded=0, # is this a holded game loaded=0, # number of times this game was loaded saved=0, # number of times this game was saved restarted=0, # number of times this game was restarted goto_bookmark_moves=0, # number of goto bookmark # did this game already update the player stats ? updated=self.U_PLAY, start_time=time.time(), # game start time total_elapsed_time=0.0, start_player=None, ) # global saveinfo survives a game restart self.gsaveinfo = Struct( bookmarks={}, comment="", ) # some vars for win animation self.win_animation = Struct( timer=None, images=[], tk_images=[], # saved tk images saved_images={}, # saved resampled images canvas_images=[], # ids of canvas images frame_num=0, # number of the current frame width=0, height=0, ) def getTitleName(self): return self.app.getGameTitleName(self.id) def getGameNumber(self, format): s = self.random.getSeedAsStr() if format: return "# " + s return s # this is called from within createGame() def setSize(self, w, h): self.width, self.height = int(round(w)), int(round(h)) dx, dy = self.canvas.xmargin, self.canvas.ymargin self.init_size = self.width+2*dx, self.height+2*dy def setCursor(self, cursor): if self.canvas: self.canvas.config(cursor=cursor) # self.canvas.update_idletasks() # if self.app and self.app.toolbar: # self.app.toolbar.setCursor(cursor=cursor) # # game creation # # start a new name def newGame(self, random=None, restart=0, autoplay=1, shuffle=True, dealer=None): # print 'Game.newGame' self.finished = False old_busy, self.busy = self.busy, 1 self.setCursor(cursor=CURSOR_WATCH) self.stopWinAnimation() self.disableMenus() if shuffle: self.redealAnimation() self.reset(restart=restart) self.resetGame() self.createRandom(random) # print self.random, self.random.__dict__ if shuffle: self.shuffle() assert len(self.s.talon.cards) == self.gameinfo.ncards for stack in self.allstacks: stack.updateText() self.updateText() self.updateStatus( player=self.app.opt.player, gamenumber=self.getGameNumber(format=1), moves=(0, 0), stats=self.app.stats.getStats( self.app.opt.player, self.id), stuck='') reset_solver_dialog() # unhide toplevel when we use a progress bar if not self.preview: wm_map(self.top, maximized=self.app.opt.wm_maximized) self.top.busyUpdate() if TOOLKIT == 'gtk': # FIXME if self.top: self.top.update_idletasks() self.top.show_now() # self.stopSamples() # let's go self.moves.state = self.S_INIT if dealer: dealer() else: self.startGame() self.startMoves() for stack in self.allstacks: stack.updateText() self.updateSnapshots() self.updateText() self.updateStatus(moves=(0, 0)) self.updateMenus() self.stopSamples() if autoplay: self.autoPlay() self.stats.player_moves = 0 self.setCursor(cursor=self.app.top_cursor) self.stats.update_time = time.time() if not self.preview: self.startPlayTimer() self.busy = old_busy # restore a loaded game (see load/save below) def restoreGame(self, game, reset=1): old_busy, self.busy = self.busy, 1 if reset: self.reset() self.resetGame() # 1) copy loaded variables self.filename = game.filename self.version = game.version self.version_tuple = game.version_tuple self.random = game.random self.moves = game.moves self.stats = game.stats self.gstats = game.gstats # 2) copy extra save-/loadinfo self.saveinfo = game.saveinfo self.gsaveinfo = game.gsaveinfo self.s.talon.round = game.loadinfo.talon_round self.finished = game.finished self.snapshots = game.snapshots # 3) move cards to stacks assert len(self.allstacks) == len(game.loadinfo.stacks) old_state = game.moves.state game.moves.state = self.S_RESTORE for i in range(len(self.allstacks)): for t in game.loadinfo.stacks[i]: card_id, face_up = t card = self.cards[card_id] if face_up: card.showFace() else: card.showBack() self.allstacks[i].addCard(card) game.moves.state = old_state # 4) update settings for stack_id, cap in self.saveinfo.stack_caps: # print stack_id, cap self.allstacks[stack_id].cap.update(cap.__dict__) # 5) subclass settings self._restoreGameHook(game) # 6) update view for stack in self.allstacks: stack.updateText() self.updateText() self.updateStatus( player=self.app.opt.player, gamenumber=self.getGameNumber(format=1), moves=(self.moves.index, self.stats.total_moves), stats=self.app.stats.getStats(self.app.opt.player, self.id)) if not self.preview: self.updateMenus() wm_map(self.top, maximized=self.app.opt.wm_maximized) self.setCursor(cursor=self.app.top_cursor) self.stats.update_time = time.time() self.busy = old_busy # # self._configureHandler() # reallocateCards # wait for canvas is mapped after(self.top, 200, self._configureHandler) # if TOOLKIT == 'gtk': # FIXME if self.top: self.top.update_idletasks() self.top.show_now() # self.startPlayTimer() # restore a bookmarked game (e.g. after changing the cardset) def restoreGameFromBookmark(self, bookmark): old_busy, self.busy = self.busy, 1 file = BytesIO(bookmark) p = Unpickler(file) game = self._undumpGame(p, self.app) assert game.id == self.id self.restoreGame(game, reset=0) destruct(game) self.busy = old_busy def resetGame(self): # print 'Game.resetGame' self.hints.list = None self.s.talon.removeAllCards() for stack in self.allstacks: # print stack stack.resetGame() if TOOLKIT == 'gtk': # FIXME (pyramid like games) stack.group.tkraise() if self.preview <= 1: for t in (self.texts.score, self.texts.base_rank,): if t: t.config(text="") def nextGameFlags(self, id, random=None): f = 0 if id != self.id: f |= 1 if self.app.nextgame.cardset is not self.app.cardset: f |= 2 if random is not None: if ((random.__class__ is not self.random.__class__) or random.initial_seed != self.random.initial_seed): f |= 16 return f # quit to outer mainloop in class App, possibly restarting # with another game from there def quitGame(self, id=0, random=None, loadedgame=None, startdemo=0, bookmark=0, holdgame=0): self.updateTime() if bookmark: id, random = self.id, self.random f = BytesIO() self._dumpGame(Pickler(f, 1), bookmark=1) self.app.nextgame.bookmark = f.getvalue() if id > 0: self.setCursor(cursor=CURSOR_WATCH) self.app.nextgame.id = id self.app.nextgame.random = random self.app.nextgame.loadedgame = loadedgame self.app.nextgame.startdemo = startdemo self.app.nextgame.holdgame = holdgame self.updateStatus(time=None, moves=None, gamenumber=None, stats=None) self.top.mainquit() # This should be called directly before newGame(), # restoreGame(), restoreGameFromBookmark() and quitGame(). def endGame(self, restart=0, bookmark=0, holdgame=0): if self.preview: return self.app.wm_save_state() if self.pause: self.doPause() if holdgame: return if bookmark: return if restart: if self.moves.index > 0 and self.getPlayerMoves() > 0: self.gstats.restarted = self.gstats.restarted + 1 return self.updateStats() stats = self.app.stats if self.shallUpdateBalance(): b = self.getGameBalance() if b: stats.total_balance[self.id] = \ stats.total_balance.get(self.id, 0) + b stats.session_balance[self.id] = \ stats.session_balance.get(self.id, 0) + b stats.gameid_balance = stats.gameid_balance + b # restart the current game def restartGame(self): self.endGame(restart=1) self.newGame(restart=1, random=self.random) def resizeImages(self): # resizing images and cards if self.app.opt.auto_scale: if self.canvas.winfo_ismapped(): # apparent size of canvas vw = self.canvas.winfo_width() vh = self.canvas.winfo_height() else: # we have no a real size of canvas # (winfo_width / winfo_reqwidth) # so we use a saved size vw, vh = self.app.opt.game_geometry if not vw: # first run of the game return 1, 1 # requested size of canvas (createGame -> setSize) iw, ih = self.init_size # calculate factor of resizing xf = float(vw)/iw yf = float(vh)/ih if self.app.opt.preserve_aspect_ratio: xf = yf = min(xf, yf) else: xf, yf = self.app.opt.scale_x, self.app.opt.scale_y # images self.app.images.resize(xf, yf) # cards for card in self.cards: card.update(card.id, card.deck, card.suit, card.rank, self) return xf, yf def resizeGame(self): # if self.busy: # return if not USE_PIL: return self.deleteStackDesc() xf, yf = self.resizeImages() # print 'resizeGame', xf, yf # stacks for stack in self.allstacks: x0, y0 = stack.init_coord x, y = int(round(x0*xf)), int(round(y0*yf)) # if x == stack.x and y == stack.y: # continue stack.resize(xf, yf) stack.updatePositions() # regions info = [] for stacks, rect in self.regions.init_info: rect = (int(round(rect[0]*xf)), int(round(rect[1]*yf)), int(round(rect[2]*xf)), int(round(rect[3]*yf))) info.append((stacks, rect)) self.regions.info = tuple(info) # texts for t in ('info', 'help', 'misc', 'score', 'base_rank'): init_coord = getattr(self.init_texts, t) if init_coord: item = getattr(self.texts, t) x, y = int(round(init_coord[0]*xf)), \ int(round(init_coord[1]*yf)) self.canvas.coords(item, x, y) for i in range(len(self.texts.list)): init_coord = self.init_texts.list[i] item = self.texts.list[i] x, y = int(round(init_coord[0]*xf)), int(round(init_coord[1]*yf)) self.canvas.coords(item, x, y) # # self.canvas.update() # self.canvas.update_idletasks() def createRandom(self, random): if random is None: if isinstance(self.random, PysolRandom): state = self.random.getstate() self.app.gamerandom.setstate(state) # we want at least 17 digits seed = self.app.gamerandom.randrange( int('10000000000000000'), PysolRandom.MAX_SEED ) self.random = PysolRandom(seed) self.random.origin = self.random.ORIGIN_RANDOM else: self.random = random self.random.reset() # print 'createRandom:', self.random # print "createRandom: origin =", self.random.origin def enterState(self, state): old_state = self.moves.state if state < old_state: self.moves.state = state return old_state def leaveState(self, old_state): self.moves.state = old_state def getSnapshot(self): # generate hash (unique string) of current move sn = [] for stack in self.allstacks: s = [] for card in stack.cards: s.append('%d%03d%d' % (card.suit, card.rank, card.face_up)) sn.append(''.join(s)) sn = '-'.join(sn) # optimisation sn = hash(sn) return sn def createSnGroups(self): # group stacks by class and cap sg = {} for s in self.allstacks: for k in sg: if s.__class__ is k.__class__ and \ s.cap.__dict__ == k.cap.__dict__: g = sg[k] g.append(s.id) break else: # new group sg[s] = [s.id] sg = list(sg.values()) self.sn_groups = sg # print sg def updateSnapshots(self): sn = self.getSnapshot() if sn in self.snapshots: # self.updateStatus(snapshot=True) pass else: self.snapshots.append(sn) # self.updateStatus(snapshot=False) # # card creation & shuffling # # Create all cards for the game. def createCards(self, progress=None): gi = self.gameinfo pstep = 0 if progress: pstep = (100.0 - progress.percent) / gi.ncards cards = [] id = [0] x, y = self.s.talon.x, self.s.talon.y for deck in range(gi.decks): def _iter_ranks(ranks, suit): for rank in ranks: card = self._createCard(id[0], deck, suit, rank, x=x, y=y) if card is None: continue cards.append(card) id[0] += 1 if progress: progress.update(step=pstep) for suit in gi.suits: _iter_ranks(gi.ranks, suit) _iter_ranks(gi.trumps, len(gi.suits)) if progress: progress.update(percent=100) assert len(cards) == gi.ncards return cards def _createCard(self, id, deck, suit, rank, x, y): return Card(id, deck, suit, rank, game=self, x=x, y=y) # shuffle cards def shuffle(self): # get a fresh copy of the original game-cards cards = list(self.cards) # init random generator if isinstance(self.random, LCRandom31) and len(cards) == 52: # FreeCell mode fcards = [] for i in range(13): for j in (0, 39, 26, 13): fcards.append(cards[i + j]) cards = fcards self.random.reset() # reset to initial seed # shuffle self.random.shuffle(cards) # subclass hook cards = self._shuffleHook(cards) # finally add the shuffled cards to the Talon for card in cards: self.s.talon.addCard(card, update=0) card.showBack(unhide=0) # shuffle cards, but keep decks together def shuffleSeparateDecks(self): cards = [] self.random.reset() n = self.gameinfo.ncards // self.gameinfo.decks for deck in range(self.gameinfo.decks): i = deck * n deck_cards = list(self.cards)[i:i+n] self.random.shuffle(deck_cards) cards.extend(deck_cards) cards = self._shuffleHook(cards) for card in cards: self.s.talon.addCard(card, update=0) card.showBack(unhide=0) # subclass overrideable (must use self.random) def _shuffleHook(self, cards): return cards # utility for use by subclasses def _shuffleHookMoveToTop(self, cards, func, ncards=999999): # move cards to top of the Talon (i.e. first cards to be dealt) cards, scards = self._shuffleHookMoveSorter(cards, func, ncards) return cards + scards def _shuffleHookMoveToBottom(self, cards, func, ncards=999999): # move cards to bottom of the Talon (i.e. last cards to be dealt) cards, scards = self._shuffleHookMoveSorter(cards, func, ncards) return scards + cards def _shuffleHookMoveSorter(self, cards, cb, ncards): extracted, i, new = [], len(cards), [] for c in cards: select, ord_ = cb(c) if select: extracted.append((ord_, i, c)) if len(extracted) >= ncards: new += cards[(len(cards)-i+1):] break else: new.append(c) i -= 1 return new, [x[2] for x in reversed(sorted(extracted))] # # menu support # def _finishDrag(self): if self.demo: self.stopDemo() if self.busy: return 1 if self.drag.stack: self.drag.stack.finishDrag() return 0 def _cancelDrag(self, break_pause=True): self.stopWinAnimation() if self.demo: self.stopDemo() if break_pause and self.pause: self.doPause() self.interruptSleep() self.deleteStackDesc() if self.busy: return 1 if self.drag.stack: self.drag.stack.cancelDrag() return 0 def updateMenus(self): if not self.preview: self.app.menubar.updateMenus() def disableMenus(self): if not self.preview: self.app.menubar.disableMenus() # # UI & graphics support # def _defaultHandler(self, event): if not self.app: return True # FIXME (GTK) if not self.app.opt.mouse_undo: return True if self.pause: self.app.menubar.mPause() return True # stop animation if not self.event_handled and self.stopWinAnimation(): return True self.interruptSleep() if self.deleteStackDesc(): # delete piles descriptions return True if self.demo: # stop demo self.stopDemo() return True if not self.event_handled and self.drag.stack: # cancel drag self.drag.stack.cancelDrag(event) return True return False # continue this event def dropHandler(self, event): if not self._defaultHandler(event) and not self.event_handled: self.app.menubar.mDrop() self.event_handled = False return EVENT_PROPAGATE def undoHandler(self, event): if not self._defaultHandler(event) and not self.event_handled: self.app.menubar.mUndo() self.event_handled = False return EVENT_PROPAGATE def redoHandler(self, event): if not self._defaultHandler(event) and not self.event_handled: self.app.menubar.mRedo() self.event_handled = False return EVENT_PROPAGATE def updateStatus(self, **kw): if self.preview: return tb, sb = self.app.toolbar, self.app.statusbar for k, v in kw.items(): if k == "gamenumber": if v is None: if sb: sb.updateText(gamenumber="") # self.top.wm_title("%s - %s" # % (TITLE, self.getTitleName())) continue if isinstance(v, six.string_types): if sb: sb.updateText(gamenumber=v) # self.top.wm_title("%s - %s %s" % (TITLE, # self.getTitleName(), v)) continue if k == "info": # print 'updateStatus info:', v if v is None: if sb: sb.updateText(info="") continue if isinstance(v, str): if sb: sb.updateText(info=v) continue if k == "moves": if v is None: # if tb: tb.updateText(moves="Moves\n") if sb: sb.updateText(moves="") continue if isinstance(v, tuple): # if tb: tb.updateText(moves="Moves\n%d/%d" % v) if sb: sb.updateText(moves="%d/%d" % v) continue if isinstance(v, int): # if tb: tb.updateText(moves="Moves\n%d" % v) if sb: sb.updateText(moves="%d" % v) continue if isinstance(v, str): # if tb: tb.updateText(moves=v) if sb: sb.updateText(moves=v) continue if k == "player": if v is None: if tb: tb.updateText(player=_("Player\n")) continue if isinstance(v, six.string_types): if tb: # if self.app.opt.toolbar_size: if self.app.toolbar.getSize(): tb.updateText(player=_("Player\n") + v) else: tb.updateText(player=v) continue if k == "stats": if v is None: if sb: sb.updateText(stats="") continue if isinstance(v, tuple): t = "%d: %d/%d" % (v[0]+v[1], v[0], v[1]) if sb: sb.updateText(stats=t) continue if k == "time": if v is None: if sb: sb.updateText(time='') if isinstance(v, six.string_types): if sb: sb.updateText(time=v) continue if k == 'stuck': if sb: sb.updateText(stuck=v) continue raise AttributeError(k) def _unmapHandler(self, event): # pause game if root window has been iconified if self.app and not self.pause: self.app.menubar.mPause() _resizeHandlerID = None def _resizeHandler(self): # print '_resizeHandler' self._resizeHandlerID = None self.resizeGame() def _configureHandler(self, event=None): # print 'configureHandler' if not USE_PIL: return if not self.app: return if not self.canvas: return if not self.app.opt.auto_scale: return if self.preview: return if self._resizeHandlerID: self.canvas.after_cancel(self._resizeHandlerID) self._resizeHandlerID = self.canvas.after(250, self._resizeHandler) # # sound support # def playSample(self, name, priority=0, loop=0): # print "Game.playSample:", name, priority, loop if name in self.app.opt.sound_samples and \ not self.app.opt.sound_samples[name]: return 0 if self.app.audio: return self.app.audio.playSample( name, priority=priority, loop=loop) return 0 def stopSamples(self): if self.app.audio: self.app.audio.stopSamples() def stopSamplesLoop(self): if self.app.audio: self.app.audio.stopSamplesLoop() def startDealSample(self, loop=999999): a = self.app.opt.animations if a and not self.preview: self.canvas.update_idletasks() if self.app.audio and self.app.opt.sound \ and self.app.opt.sound_samples['deal']: if a in (1, 2, 3, 10): self.playSample("deal01", priority=100, loop=loop) elif a == 4: self.playSample("deal04", priority=100, loop=loop) elif a == 5: self.playSample("deal08", priority=100, loop=loop) # # misc. methods # def areYouSure(self, title=None, text=None, confirm=-1, default=0): if TOOLKIT == 'kivy': return True if self.preview: return True if confirm < 0: confirm = self.app.opt.confirm if confirm: if not title: title = TITLE if not text: text = _("Discard current game ?") self.playSample("areyousure") d = MfxMessageDialog(self.top, title=title, text=text, bitmap="question", strings=(_("&OK"), _("&Cancel"))) if d.status != 0 or d.button != 0: return False return True def notYetImplemented(self): # don't used MfxMessageDialog(self.top, title="Not yet implemented", text="This function is\nnot yet implemented.", bitmap="error") # main animation method def animatedMoveTo(self, from_stack, to_stack, cards, x, y, tkraise=1, frames=-1, shadow=-1): # available values of app.opt.animations: # 0 - without animations # 1 - very fast (without timer) # 2 - fast (default) # 3 - medium (2/3 of fast speed) # 4 - slow (1/4 of fast speed) # 5 - very slow (1/8 of fast speed) # 10 - used internally in game preview if self.app.opt.animations == 0 or frames == 0: return # init timer - need a high resolution for this to work clock, delay, skip = None, 1, 1 if self.app.opt.animations >= 2: clock = uclock SPF = 0.15 / 8 # animation speed - seconds per frame if frames < 0: frames = 8 assert frames >= 2 if self.app.opt.animations == 3: # medium frames *= 3 SPF /= 2 elif self.app.opt.animations == 4: # slow frames *= 8 SPF /= 2 elif self.app.opt.animations == 5: # very slow frames *= 16 SPF /= 2 elif self.app.opt.animations == 10: # this is used internally in game preview to speed up # the initial dealing if self.moves.state == self.S_INIT and frames > 4: frames //= 2 if shadow < 0: shadow = self.app.opt.shadow shadows = () # start animation if TOOLKIT == 'kivy': c0 = cards[0] dx, dy = (x - c0.x), (y - c0.y) for card in cards: base = float(self.app.opt.animations) duration = base*0.1 card.animatedMove(dx, dy, duration) return if tkraise: for card in cards: card.tkraise() c0 = cards[0] dx, dy = (x - c0.x) / float(frames), (y - c0.y) / float(frames) tx, ty = 0, 0 i = 1 if clock: starttime = clock() while i < frames: mx, my = int(round(dx * i)) - tx, int(round(dy * i)) - ty tx, ty = tx + mx, ty + my if i == 1 and shadow and from_stack: # create shadows in the first frame sx, sy = self.app.images.SHADOW_XOFFSET, \ self.app.images.SHADOW_YOFFSET shadows = from_stack.createShadows(cards, sx, sy) for s in shadows: s.move(mx, my) for card in cards: card.moveBy(mx, my) self.canvas.update_idletasks() step = 1 if clock: endtime = starttime + i*SPF sleep = endtime - clock() if delay and sleep >= 0.005: # we're fast - delay # print "Delay frame", i, sleep usleep(sleep) elif skip and sleep <= -0.75*SPF: # we're slow - skip 1 or 2 frames # print "Skip frame", i, sleep step += 1 if frames > 4 and sleep < -1.5*SPF: step += 1 # print i, step, mx, my; time.sleep(0.5) i += step # last frame: delete shadows, move card to final position for s in shadows: s.delete() dx, dy = x - c0.x, y - c0.y for card in cards: card.moveBy(dx, dy) self.canvas.update_idletasks() def doAnimatedFlipAndMove(self, from_stack, to_stack=None, frames=-1): if self.app.opt.animations == 0 or frames == 0: return False if not from_stack.cards: return False if TOOLKIT == 'gtk': return False if not Image: return False canvas = self.canvas card = from_stack.cards[-1] im1 = card._active_image._pil_image if card.face_up: im2 = card._back_image._pil_image else: im2 = card._face_image._pil_image w, h = im1.size id = card.item.id # SPF = 0.1/8 # animation speed - seconds per frame frames = 4.0 # num frames for each step if self.app.opt.animations == 3: # medium SPF = 0.1/8 frames = 7.0 elif self.app.opt.animations == 4: # slow SPF = 0.1/8 frames = 12.0 elif self.app.opt.animations == 5: # very slow SPF = 0.1/8 frames = 24.0 if to_stack is None: x0, y0 = from_stack.getPositionFor(card) x1, y1 = x0, y0 dest_x, dest_y = 0, 0 else: x0, y0 = from_stack.getPositionFor(card) x1, y1 = to_stack.getPositionForNextCard() dest_x, dest_y = x1-x0, y1-y0 if dest_x == 0 and dest_y == 0: # flip # ascent_dx, ascent_dy = 0, self.app.images.SHADOW_YOFFSET/frames ascent_dx, ascent_dy = 0, h/10.0/frames min_size = w/10 shrink_dx = (w-min_size) / (frames-1) shrink_dy = 0 elif dest_y == 0: # move to left/right waste # ascent_dx, ascent_dy = 0, self.app.images.SHADOW_YOFFSET/frames ascent_dx, ascent_dy = 0, h/10.0/frames min_size = w/10 shrink_dx = (w-min_size) / (frames-1) shrink_dy = 0 elif dest_x == 0: # move to top/bottom waste if 0: ascent_dx, ascent_dy = 0, h/10.0/frames min_size = w/10 shrink_dx = (w-min_size) / (frames-1) shrink_dy = 0 elif 0: ascent_dx, ascent_dy = 0, 0 min_size = h/10 shrink_dx = 0 shrink_dy = (h-min_size) / (frames-1) else: return False else: # dest_x != 0 and dest_y != 0 return False move_dx = dest_x / frames / 2 move_dy = dest_y / frames / 2 xpos, ypos = float(x0), float(y0) card.tkraise() # step 1 d_x = shrink_dx/2+move_dx-ascent_dx d_y = shrink_dy/2+move_dy-ascent_dy nframe = 0 while nframe < frames: starttime = uclock() # resize img ww = w - nframe*shrink_dx hh = h - nframe*shrink_dy tmp = im1.resize((int(ww), int(hh))) tk_tmp = ImageTk.PhotoImage(image=tmp) canvas.itemconfig(id, image=tk_tmp) # move img xpos += d_x ypos += d_y card.moveTo(int(round(xpos)), int(round(ypos))) canvas.update_idletasks() nframe += 1 t = (SPF-(uclock()-starttime))*1000 # milliseconds if t > 0: usleep(t/1000) # else: # nframe += 1 # xpos += d_x # ypos += d_y # step 2 d_x = -shrink_dx/2+move_dx+ascent_dx d_y = -shrink_dy/2+move_dy+ascent_dy nframe = 0 while nframe < frames: starttime = uclock() # resize img ww = w - (frames-nframe-1)*shrink_dx hh = h - (frames-nframe-1)*shrink_dy tmp = im2.resize((int(ww), int(hh))) tk_tmp = ImageTk.PhotoImage(image=tmp) canvas.itemconfig(id, image=tk_tmp) # move img xpos += d_x ypos += d_y card.moveTo(int(round(xpos)), int(round(ypos))) canvas.update_idletasks() nframe += 1 t = (SPF-(uclock()-starttime))*1000 # milliseconds if t > 0: usleep(t/1000) # else: # nframe += 1 # xpos += d_x # ypos += d_y card.moveTo(x1, y1) # canvas.update_idletasks() return True def animatedFlip(self, stack): if not self.app.opt.flip_animation: return False return self.doAnimatedFlipAndMove(stack) def animatedFlipAndMove(self, from_stack, to_stack, frames=-1): if not self.app.opt.flip_animation: return False return self.doAnimatedFlipAndMove(from_stack, to_stack, frames) def winAnimationEvent(self): # based on code from pygtk-demo FRAME_DELAY = 80 CYCLE_LEN = 60 starttime = uclock() images = self.win_animation.images saved_images = self.win_animation.saved_images # cached images canvas = self.canvas canvas.delete(*self.win_animation.canvas_images) self.win_animation.canvas_images = [] x0 = int(int(canvas.cget('width'))*(canvas.xview()[0])) y0 = int(int(canvas.cget('height'))*(canvas.yview()[0])) width, height = self.win_animation.width, self.win_animation.height cw = self.canvas.winfo_width() ch = self.canvas.winfo_height() x0 -= (width-cw)/2 y0 -= (height-ch)/2 tmp_tk_images = [] raised_images = [] n_images = len(images) xmid = width / 2.0 ymid = height / 2.0 radius = min(xmid, ymid) / 2.0 f = float(self.win_animation.frame_num % CYCLE_LEN) / float(CYCLE_LEN) r = radius + (radius / 3.0) * math.sin(f * 2.0 * math.pi) img_index = 0 for im in images: iw, ih = im.size ang = 2.0 * math.pi * img_index / n_images - f * 2.0 * math.pi xpos = x0 + int(xmid + r * math.cos(ang) - iw / 2.0) ypos = y0 + int(ymid + r * math.sin(ang) - ih / 2.0) k = (math.sin if img_index & 1 else math.cos)(f * 2.0 * math.pi) k = max(0.4, k ** 2) round_k = int(round(k*100)) if img_index not in saved_images: saved_images[img_index] = {} if round_k in saved_images[img_index]: tk_tmp = saved_images[img_index][round_k] else: new_size = (int(iw*k), int(ih*k)) if round_k == 100: tmp = im else: tmp = im.resize(new_size, resample=Image.BILINEAR) tk_tmp = ImageTk.PhotoImage(image=tmp) saved_images[img_index][round_k] = tk_tmp id = canvas.create_image(xpos, ypos, image=tk_tmp, anchor='nw') self.win_animation.canvas_images.append(id) if k > 0.6: raised_images.append(id) tmp_tk_images.append(tk_tmp) img_index += 1 for id in raised_images: canvas.tag_raise(id) self.win_animation.frame_num = \ (self.win_animation.frame_num+1) % CYCLE_LEN self.win_animation.tk_images = tmp_tk_images canvas.update_idletasks() # loop t = FRAME_DELAY-int((uclock()-starttime)*1000) if t > 0: self.win_animation.timer = after(canvas, t, self.winAnimationEvent) else: self.win_animation.timer = after_idle( canvas, self.winAnimationEvent) def stopWinAnimation(self): if self.win_animation.timer: after_cancel(self.win_animation.timer) # stop loop self.win_animation.timer = None self.canvas.delete(*self.win_animation.canvas_images) self.win_animation.canvas_images = [] self.win_animation.tk_images = [] # delete all images self.saved_images = {} self.canvas.showAllItems() return True return False def winAnimation(self, perfect=0): if self.preview: return # if not self.app.opt.animations: # return if not self.app.opt.win_animation: return if TOOLKIT == 'gtk': return if not Image: return self.canvas.hideAllItems() # select some random cards cards = self.cards[:] scards = [] ncards = min(10, len(cards)) for i in range(ncards): c = self.app.miscrandom.choice(cards) scards.append(c) cards.remove(c) for c in scards: self.win_animation.images.append(c._face_image._pil_image) # compute visible geometry self.win_animation.width = self.canvas.winfo_width() self.win_animation.height = self.canvas.winfo_height() # run win animation in background # after_idle(self.canvas, self.winAnimationEvent) after(self.canvas, 200, self.winAnimationEvent) return def redealAnimation(self): if self.preview: return if not self.app.opt.animations or not self.app.opt.redeal_animation: return cards = [] for s in self.allstacks: if s is not self.s.talon: for c in s.cards: cards.append((c, s)) if not cards: return self.setCursor(cursor=CURSOR_WATCH) self.top.busyUpdate() self.canvas.update_idletasks() old_a = self.app.opt.animations if old_a == 0: self.app.opt.animations = 1 # very fast elif old_a == 3: # medium self.app.opt.animations = 2 # fast elif old_a == 4: # very slow self.app.opt.animations = 3 # slow # select some random cards acards = [] scards = cards[:] for i in range(8): c, s = self.app.miscrandom.choice(scards) if c not in acards: acards.append(c) scards.remove((c, s)) if not scards: break # animate sx, sy = self.s.talon.x, self.s.talon.y w, h = self.width, self.height while cards: # get and un-tuple a random card t = self.app.miscrandom.choice(cards) c, s = t s.removeCard(c, update=0) # animation if c in acards or len(cards) <= 2: self.animatedMoveTo( s, None, [c], w//2, h//2, tkraise=0, shadow=0) self.animatedMoveTo(s, None, [c], sx, sy, tkraise=0, shadow=0) else: c.moveTo(sx, sy) cards.remove(t) self.app.opt.animations = old_a def sleep(self, seconds): # if 0 and self.canvas: # self.canvas.update_idletasks() if seconds > 0: if self.top: self.top.interruptSleep() self.top.sleep(seconds) else: time.sleep(seconds) def interruptSleep(self): if self.top: self.top.interruptSleep() # # card image support # def getCardFaceImage(self, deck, suit, rank): return self.app.images.getFace(deck, suit, rank) def getCardBackImage(self, deck, suit, rank): return self.app.images.getBack() def getCardShadeImage(self): return self.app.images.getShade() # # layout support # def _getClosestStack(self, cx, cy, stacks, dragstack): closest, cdist = None, 999999999 # Since we only compare distances, # we don't bother to take the square root. for stack in stacks: dist = (stack.x - cx)**2 + (stack.y - cy)**2 if dist < cdist: closest, cdist = stack, dist return closest def getClosestStack(self, card, dragstack): cx, cy = card.x, card.y for stacks, rect in self.regions.info: if cx >= rect[0] and cx < rect[2] \ and cy >= rect[1] and cy < rect[3]: return self._getClosestStack(cx, cy, stacks, dragstack) return self._getClosestStack(cx, cy, self.regions.remaining, dragstack) # define a region for use in getClosestStack() def setRegion(self, stacks, rect, priority=0): assert len(stacks) > 0 assert len(rect) == 4 and rect[0] < rect[2] and rect[1] < rect[3] if DEBUG >= 2: xf, yf = self.app.images._xfactor, self.app.images._yfactor MfxCanvasRectangle(self.canvas, xf*rect[0], yf*rect[1], xf*rect[2], yf*rect[3], width=2, fill=None, outline='red') for s in stacks: assert s and s in self.allstacks # verify that the stack lies within the rectangle r = rect if USE_PIL: x, y = s.init_coord # print 'setRegion:', x, y, r else: x, y = s.x, s.y # print 'setRegion:', x, y, r assert r[0] <= x <= r[2] and r[1] <= y <= r[3] # verify that the stack is not already in another region # with the same priority for d in self.regions.data: if priority == d[0]: assert s not in d[2] # add to regions self.regions.data.append( (priority, -len(self.regions.data), tuple(stacks), tuple(rect))) # as getClosestStack() is called within the mouse motion handler # event it is worth optimizing a little bit def optimizeRegions(self): # sort regions.data by priority self.regions.data.sort() self.regions.data.reverse() # copy (stacks, rect) to regions.info self.regions.info = [] for d in self.regions.data: self.regions.info.append((d[2], d[3])) self.regions.info = tuple(self.regions.info) # determine remaining stacks remaining = list(self.sg.openstacks) for stacks, rect in self.regions.info: for stack in stacks: while stack in remaining: remaining.remove(stack) self.regions.remaining = tuple(remaining) self.regions.init_info = self.regions.info def getInvisibleCoords(self): # for InvisibleStack, etc # x, y = -500, -500 - len(game.allstacks) cardw, cardh = self.app.images.CARDW, self.app.images.CARDH x, y = cardw + self.canvas.xmargin, cardh + self.canvas.ymargin return -x-10, -y-10 # # Game - subclass overridable actions - IMPORTANT FOR GAME LOGIC # # create the game (create stacks, texts, etc.) def createGame(self): raise SubclassResponsibility # start the game (i.e. deal initial cards) def startGame(self): raise SubclassResponsibility # can we deal cards ? def canDealCards(self): # default: ask the Talon return self.s.talon and self.s.talon.canDealCards() # deal cards - return number of cards dealt def dealCards(self, sound=True): # default: set state to deal and pass dealing to Talon if self.s.talon and self.canDealCards(): self.finishMove() old_state = self.enterState(self.S_DEAL) n = self.s.talon.dealCards(sound=sound) self.leaveState(old_state) self.finishMove() if not self.checkForWin(): self.autoPlay() return n return 0 # fill a stack if rules require it (e.g. Picture Gallery) def fillStack(self, stack): pass # redeal cards (used in RedealTalonStack; all cards already in talon) def redealCards(self): pass # the actual hint class (or None) Hint_Class = DefaultHint Solver_Class = None Stuck_Class = None def getHintClass(self): return self.Hint_Class def getStrictness(self): return 0 # can we save outself ? def canSaveGame(self): return True # can we load this game ? def canLoadGame(self, version_tuple, game_version): return self.GAME_VERSION == game_version # can we set a bookmark ? def canSetBookmark(self): return self.canSaveGame() # can we undo/redo ? def canUndo(self): return True def canRedo(self): return self.canUndo() # Mahjongg def canShuffle(self): return False # # Game - stats handlers # # game changed - i.e. should we ask the player to discard the game def changed(self, restart=0): if self.gstats.updated < 0: return 0 # already won or lost if self.gstats.loaded > 0: return 0 # loaded games account for no stats if not restart: if self.gstats.restarted > 0: return 1 # game was restarted - always ask if self.gstats.goto_bookmark_moves > 0: return 1 if self.moves.index == 0 or self.getPlayerMoves() == 0: return 0 return 2 def getWinStatus(self): won = self.isGameWon() != 0 if not won or self.stats.hints > 0 or self.stats.demo_moves > 0: # sorry, you lose return won, 0, self.U_LOST if (self.stats.undo_moves == 0 and self.stats.goto_bookmark_moves == 0 and # self.stats.quickplay_moves == 0 and self.stats.highlight_piles == 0 and self.stats.highlight_cards == 0 and self.stats.shuffle_moves == 0): # perfect ! return won, 2, self.U_PERFECT return won, 1, self.U_WON # update statistics when a game was won/ended/canceled/... def updateStats(self, demo=0): if self.preview: return '' if not demo: self.stopPlayTimer() won, status, updated = self.getWinStatus() if demo and self.getPlayerMoves() == 0: if not self.stats.demo_updated: # a pure demo game - update demo stats self.stats.demo_updated = updated self.app.stats.updateStats(None, self, won) return '' elif self.changed(): # must update player stats self.gstats.updated = updated if self.app.opt.update_player_stats: ret = self.app.stats.updateStats( self.app.opt.player, self, status) self.updateStatus( stats=self.app.stats.getStats( self.app.opt.player, self.id)) top_msg = '' if ret: if ret[0] and ret[1]: top_msg = _(''' You have reached # %d in the %s of playing time and # %d in the %s of moves.''') % (ret[0], TOP_TITLE, ret[1], TOP_TITLE) elif ret[0]: # playing time top_msg = _(''' You have reached # %d in the %s of playing time.''') % (ret[0], TOP_TITLE) elif ret[1]: # moves top_msg = _(''' You have reached # %d in the %s of moves.''') % (ret[1], TOP_TITLE) return top_msg elif not demo: # only update the session log if self.app.opt.update_player_stats: if self.gstats.loaded: self.app.stats.updateStats(self.app.opt.player, self, -2) elif self.gstats.updated == 0 and self.stats.demo_updated == 0: self.app.stats.updateStats(self.app.opt.player, self, -1) return '' def checkForWin(self): won, status, updated = self.getWinStatus() if not won: return 0 self.finishMove() # just in case if self.preview: return 1 if self.finished: return 1 if self.demo: return status if TOOLKIT == 'kivy': if not self.app.opt.display_win_message: return 1 self.top.waitAnimation() if status == 2: top_msg = self.updateStats() time = self.getTime() self.finished = True self.playSample("gameperfect", priority=1000) self.winAnimation(perfect=1) text = ungettext('''Your playing time is %s\nfor %d move.''', '''Your playing time is %s\nfor %d moves.''', self.moves.index) text = text % (time, self.moves.index) d = MfxMessageDialog(self.top, title=_("Game won"), text=_(''' Congratulations, this was a truly perfect game ! %s %s ''') % (text, top_msg), strings=(_("&New game"), None, _("&Cancel")), image=self.app.gimages.logos[5]) elif status == 1: top_msg = self.updateStats() time = self.getTime() self.finished = True self.playSample("gamewon", priority=1000) self.winAnimation() text = ungettext('''Your playing time is %s\nfor %d move.''', '''Your playing time is %s\nfor %d moves.''', self.moves.index) text = text % (time, self.moves.index) d = MfxMessageDialog(self.top, title=_("Game won"), text=_(''' Congratulations, you did it ! %s %s ''') % (text, top_msg), strings=(_("&New game"), None, _("&Cancel")), image=self.app.gimages.logos[4]) elif self.gstats.updated < 0: self.finished = True self.playSample("gamefinished", priority=1000) d = MfxMessageDialog( self.top, title=_("Game finished"), bitmap="info", text=_("\nGame finished\n"), strings=(_("&New game"), None, _("&Cancel"))) else: self.finished = True self.playSample("gamelost", priority=1000) d = MfxMessageDialog( self.top, title=_("Game finished"), bitmap="info", text=_("\nGame finished, but not without my help...\n"), strings=(_("&New game"), _("&Restart"), _("&Cancel"))) self.updateMenus() if TOOLKIT == 'kivy': return 1 if d.status == 0 and d.button == 0: # new game self.endGame() self.newGame() elif d.status == 0 and d.button == 1: # restart game self.restartGame() return 1 # # Game - subclass overridable methods (but usually not) # def isGameWon(self): # default: all Foundations must be filled c = 0 for s in self.s.foundations: c = c + len(s.cards) return c == len(self.cards) def getFoundationDir(self): for s in self.s.foundations: if len(s.cards) >= 2: return s.getRankDir() return 0 # determine the real number of player_moves def getPlayerMoves(self): player_moves = self.stats.player_moves # if self.moves.index > 0 and # self.stats.demo_moves == self.moves.index: # player_moves = 0 return player_moves def updateTime(self): if self.finished: return if self.pause: return t = time.time() d = t - self.stats.update_time if d > 0: self.stats.elapsed_time += d self.gstats.total_elapsed_time += d self.stats.update_time = t def getTime(self): self.updateTime() t = int(self.stats.elapsed_time) return format_time(t) # # Game - subclass overridable intelligence # def getAutoStacks(self, event=None): # returns (flipstacks, dropstacks, quickplaystacks) # default: sg.dropstacks return (self.sg.dropstacks, self.sg.dropstacks, self.sg.dropstacks) # handles autofaceup, autodrop and autodeal def autoPlay(self, autofaceup=-1, autodrop=-1, autodeal=-1, sound=True): if self.demo: return 0 old_busy, self.busy = self.busy, 1 if autofaceup < 0: autofaceup = self.app.opt.autofaceup if autodrop < 0: autodrop = self.app.opt.autodrop if autodeal < 0: autodeal = self.app.opt.autodeal moves = self.stats.total_moves n = self._autoPlay(autofaceup, autodrop, autodeal, sound=sound) self.finishMove() self.stats.autoplay_moves += (self.stats.total_moves - moves) self.busy = old_busy return n def _autoPlay(self, autofaceup, autodrop, autodeal, sound): flipstacks, dropstacks, quickstacks = self.getAutoStacks() done_something = 1 while done_something: done_something = 0 # a) flip top cards face-up if autofaceup and flipstacks: for s in flipstacks: if s.canFlipCard(): if sound: self.playSample("autoflip", priority=5) # ~s.flipMove() s.flipMove(animation=True) done_something = 1 # each single flip is undo-able unless opt.autofaceup self.finishMove() if self.checkForWin(): return 1 # b) drop cards if autodrop and dropstacks: for s in dropstacks: to_stack, ncards = s.canDropCards(self.s.foundations) if to_stack: # each single drop is undo-able (note that this call # is before the actual move) self.finishMove() if sound: self.playSample("autodrop", priority=30) s.moveMove(ncards, to_stack) done_something = 1 if self.checkForWin(): return 1 # c) deal if autodeal: if self._autoDeal(sound=sound): done_something = 1 self.finishMove() if self.checkForWin(): return 1 return 0 def _autoDeal(self, sound=True): # default: deal a card to the waste if the waste is empty w = self.s.waste if w and len(w.cards) == 0 and self.canDealCards(): return self.dealCards(sound=sound) return 0 def autoDrop(self, autofaceup=-1): old_a = self.app.opt.animations if old_a == 3: # medium self.app.opt.animations = 2 # fast self.autoPlay(autofaceup=autofaceup, autodrop=1) self.app.opt.animations = old_a # for find_card_dialog def highlightCard(self, suit, rank): if not self.app: return None col = self.app.opt.colors['samerank_1'] info = [] for s in self.allstacks: for c in s.cards: if c.suit == suit and c.rank == rank: if s.basicShallHighlightSameRank(c): info.append((s, c, c, col)) return self._highlightCards(info, 0) # highlight all moveable piles def getHighlightPilesStacks(self): # default: dropstacks with min pile length = 2 if self.sg.hp_stacks: return ((self.sg.hp_stacks, 2),) return () def _highlightCards(self, info, sleep=1.5, delta=(1, 1, 1, 1)): if not info: return 0 if self.pause: return 0 self.stopWinAnimation() cw, ch = self.app.images.getSize() items = [] for s, c1, c2, color in info: assert c1 in s.cards and c2 in s.cards tkraise = False if c1 is c2: # highlight single card sx0, sy0 = s.getOffsetFor(c1) x1, y1 = s.getPositionFor(c1) x2, y2 = x1, y1 if c1 is s.cards[-1]: # last card in the stack (for Pyramid-like games) tkraise = True else: # highlight pile if len(s.CARD_XOFFSET) > 1: sx0 = 0 else: sx0 = s.CARD_XOFFSET[0] if len(s.CARD_YOFFSET) > 1: sy0 = 0 else: sy0 = s.CARD_YOFFSET[0] x1, y1 = s.getPositionFor(c1) x2, y2 = s.getPositionFor(c2) if sx0 != 0 and sy0 == 0: # horizontal stack y2 += ch if c2 is s.cards[-1]: # top card x2 += cw else: if sx0 > 0: # left to right x2 += sx0 else: # right to left x1 += cw x2 += cw + sx0 elif sx0 == 0 and sy0 != 0: # vertical stack x2 += cw if c2 is s.cards[-1]: # top card y2 += ch else: if sy0 > 0: # up to down y2 = y2 + sy0 else: # down to up y1 += ch y2 += ch + sy0 else: x2 += cw y2 += ch tkraise = True # print c1, c2, x1, y1, x2, y2 x1, x2 = x1-delta[0], x2+delta[1] y1, y2 = y1-delta[2], y2+delta[3] if TOOLKIT == 'tk': r = MfxCanvasRectangle(self.canvas, x1, y1, x2, y2, width=4, fill=None, outline=color) if tkraise: r.tkraise(c2.item) elif TOOLKIT == 'kivy': r = MfxCanvasRectangle(self.canvas, x1, y1, x2, y2, width=4, fill=None, outline=color) if tkraise: r.tkraise(c2.item) elif TOOLKIT == 'gtk': r = MfxCanvasRectangle(self.canvas, x1, y1, x2, y2, width=4, fill=None, outline=color, group=s.group) if tkraise: i = s.cards.index(c2) for c in s.cards[i+1:]: c.tkraise(1) items.append(r) if not items: return 0 self.canvas.update_idletasks() if sleep: self.sleep(sleep) items.reverse() for r in items: r.delete() self.canvas.update_idletasks() return EVENT_HANDLED else: # remove items later (find_card_dialog) return items def highlightNotMatching(self): if self.demo: return if not self.app.opt.highlight_not_matching: return # compute visible geometry x = int(int(self.canvas.cget('width'))*(self.canvas.xview()[0])) y = int(int(self.canvas.cget('height'))*(self.canvas.yview()[0])) w, h = self.canvas.winfo_width(), self.canvas.winfo_height() # color = self.app.opt.colors['not_matching'] width = 6 xmargin, ymargin = self.canvas.xmargin, self.canvas.ymargin if self.preview: width = 4 xmargin, ymargin = 0, 0 x0, y0 = x+width//2-xmargin, y+width//2-ymargin x1, y1 = x+w-width//2-xmargin, y+h-width//2-ymargin r = MfxCanvasRectangle(self.canvas, x0, y0, x1, y1, width=width, fill=None, outline=color) if TOOLKIT == "kivy": r.canvas.canvas.ask_update() r.delete_deferred(self.app.opt.timeouts['highlight_cards']) return self.canvas.update_idletasks() self.sleep(self.app.opt.timeouts['highlight_cards']) r.delete() self.canvas.update_idletasks() def highlightPiles(self, sleep=1.5): stackinfo = self.getHighlightPilesStacks() if not stackinfo: self.highlightNotMatching() return 0 col = self.app.opt.colors['piles'] hi = [] for si in stackinfo: for s in si[0]: pile = s.getPile() if pile and len(pile) >= si[1]: hi.append((s, pile[0], pile[-1], col)) if not hi: self.highlightNotMatching() return 0 return self._highlightCards(hi, sleep) # # highlight matching cards # def shallHighlightMatch(self, stack1, card1, stack2, card2): return False def _shallHighlightMatch_AC(self, stack1, card1, stack2, card2): # by alternate color return card1.color != card2.color and abs(card1.rank-card2.rank) == 1 def _shallHighlightMatch_ACW(self, stack1, card1, stack2, card2): # by alternate color with wrapping (only for french games) return (card1.color != card2.color and ((card1.rank + 1) % 13 == card2.rank or (card2.rank + 1) % 13 == card1.rank)) def _shallHighlightMatch_SS(self, stack1, card1, stack2, card2): # by same suit return card1.suit == card2.suit and abs(card1.rank-card2.rank) == 1 def _shallHighlightMatch_SSW(self, stack1, card1, stack2, card2): # by same suit with wrapping (only for french games) return (card1.suit == card2.suit and ((card1.rank + 1) % 13 == card2.rank or (card2.rank + 1) % 13 == card1.rank)) def _shallHighlightMatch_RK(self, stack1, card1, stack2, card2): # by rank return abs(card1.rank-card2.rank) == 1 def _shallHighlightMatch_RKW(self, stack1, card1, stack2, card2): # by rank with wrapping (only for french games) return ((card1.rank + 1) % 13 == card2.rank or (card2.rank + 1) % 13 == card1.rank) def _shallHighlightMatch_BO(self, stack1, card1, stack2, card2): # by any suit but own return card1.suit != card2.suit and abs(card1.rank-card2.rank) == 1 def _shallHighlightMatch_BOW(self, stack1, card1, stack2, card2): # by any suit but own with wrapping (only for french games) return (card1.suit != card2.suit and ((card1.rank + 1) % 13 == card2.rank or (card2.rank + 1) % 13 == card1.rank)) def _shallHighlightMatch_SC(self, stack1, card1, stack2, card2): # by same color return card1.color == card2.color and abs(card1.rank-card2.rank) == 1 def _shallHighlightMatch_SCW(self, stack1, card1, stack2, card2): # by same color with wrapping (only for french games) return (card1.color == card2.color and ((card1.rank + 1) % 13 == card2.rank or (card2.rank + 1) % 13 == card1.rank)) # # quick-play # def getQuickPlayScore(self, ncards, from_stack, to_stack): if to_stack in self.s.reserves: # if to_stack in reserves prefer empty stack # return 1000 - len(to_stack.cards) return 1000 - int(len(to_stack.cards) != 0) # prefer non-empty piles in to_stack return 1001 + int(len(to_stack.cards) != 0) def _getSpiderQuickPlayScore(self, ncards, from_stack, to_stack): if to_stack in self.s.reserves: # if to_stack in reserves prefer empty stack return 1000-len(to_stack.cards) # for spider-type stacks if to_stack.cards: # check suit same_suit = (from_stack.cards[-ncards].suit == to_stack.cards[-1].suit) return int(same_suit)+1002 return 1001 # # Score (I really don't like scores in Patience games...) # # update game-related canvas texts (i.e. self.texts) def updateText(self): pass def getGameScore(self): return None # casino type scoring def getGameScoreCasino(self): v = -len(self.cards) for s in self.s.foundations: v = v + 5 * len(s.cards) return v def shallUpdateBalance(self): # Update the balance unless this is a loaded game or # a manually selected game number. if self.gstats.loaded: return False if self.random.origin == self.random.ORIGIN_SELECTED: return False return True def getGameBalance(self): return 0 # # Hint - uses self.getHintClass() # # compute all hints for the current position # this is the only method that actually uses class Hint def getHints(self, level, taken_hint=None): if level == 3: # if self.solver is None: # return None return self.solver.getHints(taken_hint) hint_class = self.getHintClass() if hint_class is None: return None hint = hint_class(self, level) # call constructor return hint.getHints(taken_hint) # and return all hints # give a hint def showHint(self, level=0, sleep=1.5, taken_hint=None): if self.getHintClass() is None: self.highlightNotMatching() return None # reset list if level has changed if level != self.hints.level: self.hints.level = level self.hints.list = None # compute all hints if self.hints.list is None: self.hints.list = self.getHints(level, taken_hint) # print self.hints.list self.hints.index = 0 # get next hint from list if not self.hints.list: self.highlightNotMatching() return None h = self.hints.list[self.hints.index] self.hints.index = self.hints.index + 1 if self.hints.index >= len(self.hints.list): self.hints.index = 0 # paranoia - verify hint score, pos, ncards, from_stack, to_stack, text_color, forced_move = h assert from_stack and len(from_stack.cards) >= ncards if ncards == 0: # a deal move, should not happen with level=0/1 assert level >= 2 assert from_stack is self.s.talon return h elif from_stack == to_stack: # a flip move, should not happen with level=0/1 assert level >= 2 assert ncards == 1 and len(from_stack.cards) >= ncards return h else: # a move move assert to_stack assert 1 <= ncards <= len(from_stack.cards) if DEBUG: if not to_stack.acceptsCards( from_stack, from_stack.cards[-ncards:]): print('*fail accepts cards*', from_stack, to_stack, ncards) if not from_stack.canMoveCards(from_stack.cards[-ncards:]): print('*fail move cards*', from_stack, ncards) # assert from_stack.canMoveCards(from_stack.cards[-ncards:]) # FIXME: Pyramid assert to_stack.acceptsCards( from_stack, from_stack.cards[-ncards:]) if sleep <= 0.0: return h info = (level == 1) or (level > 1 and DEBUG) if info and self.app.statusbar and self.app.opt.statusbar: self.app.statusbar.configLabel( "info", text=_("Score %6d") % (score), fg=text_color) else: info = 0 self.drawHintArrow(from_stack, to_stack, ncards, sleep) if info: self.app.statusbar.configLabel("info", text="", fg="#000000") return h def drawHintArrow(self, from_stack, to_stack, ncards, sleep): # compute position for arrow images = self.app.images x1, y1 = from_stack.getPositionFor(from_stack.cards[-ncards]) x2, y2 = to_stack.getPositionFor(to_stack.getCard()) cw, ch = images.getSize() dx, dy = images.getDelta() x1, y1 = x1 + dx, y1 + dy x2, y2 = x2 + dx, y2 + dy if ncards == 1: x1 += cw // 2 y1 += ch // 2 elif from_stack.CARD_XOFFSET[0]: x1 += from_stack.CARD_XOFFSET[0] // 2 y1 += ch // 2 else: x1 += cw // 2 y1 += from_stack.CARD_YOFFSET[0] // 2 x2 += cw // 2 y2 += ch // 2 # draw the hint arrow = MfxCanvasLine(self.canvas, x1, y1, x2, y2, width=7, fill=self.app.opt.colors['hintarrow'], arrow="last", arrowshape=(30, 30, 10)) self.canvas.update_idletasks() # wait if TOOLKIT == "kivy": arrow.delete_deferred(sleep) return # wait self.sleep(sleep) # delete the hint if arrow is not None: arrow.delete() self.canvas.update_idletasks() # # Demo - uses showHint() # # start a demo def startDemo(self, mixed=1, level=2): assert level >= 2 # needed for flip/deal hints if not self.top: return self.demo = Struct( level=level, mixed=mixed, sleep=self.app.opt.timeouts['demo'], last_deal=[], snapshots=[], hint=None, keypress=None, start_demo_moves=self.stats.demo_moves, info_text=None, ) self.hints.list = None self.createDemoInfoText() self.createDemoLogo() after_idle(self.top, self.demoEvent) # schedule first move # stop the demo def stopDemo(self, event=None): if not self.demo: return self.canvas.setTopImage(None) self.demo_logo = None self.demo = None self.updateMenus() # demo event - play one demo move and check for win/loss def demoEvent(self): # note: other events are allowed to stop self.demo at any time if not self.demo or self.demo.keypress: self.stopDemo() # self.updateMenus() return finished = self.playOneDemoMove(self.demo) self.finishMove() self.top.update_idletasks() self.hints.list = None player_moves = self.getPlayerMoves() d, status = None, 0 bitmap = "info" timeout = 10000 if 1 and player_moves == 0: timeout = 5000 if self.demo and self.demo.level == 3: timeout = 0 if self.isGameWon(): self.updateTime() finished = 1 self.finished = True self.stopPlayTimer() if not self.top.winfo_ismapped(): status = 2 elif player_moves == 0: self.playSample("autopilotwon", priority=1000) s = self.app.miscrandom.choice((_("&Great"), _("&Cool"), _("&Yeah"), _("&Wow"))) text = ungettext('\nGame solved in %d move.\n', '\nGame solved in %d moves.\n', self.moves.index) text = text % self.moves.index d = MfxMessageDialog(self.top, title=TITLE+_(" Autopilot"), text=text, image=self.app.gimages.logos[4], strings=(s,), separator=True, timeout=timeout) status = d.status else: # s = self.app.miscrandom.choice((_("&OK"), _("&OK"))) s = _("&OK") text = _("\nGame finished\n") if DEBUG: text += "\nplayer_moves: %d\ndemo_moves: %d\n" % \ (self.stats.player_moves, self.stats.demo_moves) d = MfxMessageDialog(self.top, title=TITLE+_(" Autopilot"), text=text, bitmap=bitmap, strings=(s,), padx=30, timeout=timeout) status = d.status elif finished: # self.stopPlayTimer() if not self.top.winfo_ismapped(): status = 2 else: if player_moves == 0: self.playSample("autopilotlost", priority=1000) s = self.app.miscrandom.choice( (_("&Oh well"), _("&That's life"), _("&Hmm"))) # ??? accelerators d = MfxMessageDialog(self.top, title=TITLE+_(" Autopilot"), text=_("\nThis won't come out...\n"), bitmap=bitmap, strings=(s,), padx=30, timeout=timeout) status = d.status if finished: self.updateStats(demo=1) if not DEBUG and self.demo and status == 2: # timeout in dialog if self.stats.demo_moves > self.demo.start_demo_moves: # we only increase the splash-screen counter if the last # demo actually made a move self.app.demo_counter += 1 if self.app.demo_counter % 3 == 0: if self.top.winfo_ismapped(): status = help_about(self.app, timeout=10000) if self.demo and status == 2: # timeout in dialog - start another demo demo = self.demo id = self.id if 1 and demo.mixed and DEBUG: # debug - advance game id to make sure we hit all games gl = self.app.gdb.getGamesIdSortedById() # gl = self.app.gdb.getGamesIdSortedByName() gl = list(gl) index = (gl.index(self.id) + 1) % len(gl) id = gl[index] elif demo.mixed: # choose a random game gl = self.app.gdb.getGamesIdSortedById() while len(gl) > 1: id = self.app.getRandomGameId() if 0 or id != self.id: # force change of game break if self.nextGameFlags(id) == 0: self.endGame() self.newGame(autoplay=0) self.startDemo(mixed=demo.mixed) else: self.endGame() self.stopDemo() self.quitGame(id, startdemo=1) else: self.stopDemo() if DEBUG >= 10: # debug - only for testing winAnimation() self.endGame() self.winAnimation() self.newGame() else: # game not finished yet self.top.busyUpdate() if self.demo: after_idle(self.top, self.demoEvent) # schedule next move # play one demo move while in the demo event def playOneDemoMove(self, demo): if self.moves.index > 2000: # we're probably looping because of some bug in the hint code return 1 sleep = demo.sleep # first try to deal cards to the Waste (unless there was a forced move) if not demo.hint or not demo.hint[6]: if self._autoDeal(sound=False): return 0 # display a hint h = self.showHint(demo.level, sleep, taken_hint=demo.hint) demo.hint = h if not h: return 1 # now actually play the hint score, pos, ncards, from_stack, to_stack, text_color, forced_move = h if ncards == 0: # a deal-move # do not let games like Klondike and Canfield deal forever if self.dealCards() == 0: return 1 if 0: # old version, based on dealing card c = self.s.talon.getCard() if c in demo.last_deal: # We went through the whole Talon. Give up. return 1 # Note that `None' is a valid entry in last_deal[] # (this means that all cards are on the Waste). demo.last_deal.append(c) else: # new version, based on snapshots # check snapshot sn = self.getSnapshot() if sn in demo.snapshots: # not unique return 1 demo.snapshots.append(sn) elif from_stack == to_stack: # a flip-move from_stack.flipMove(animation=True) demo.last_deal = [] else: # a move-move from_stack.moveMove(ncards, to_stack, frames=-1) demo.last_deal = [] return 0 def createDemoInfoText(self): # TODO - the text placement is not fully ok if DEBUG: self.showHelp('help', self.getDemoInfoText()) return if not self.demo or self.demo.info_text or self.preview: return tinfo = [ ("sw", 8, self.height - 8), ("se", self.width - 8, self.height - 8), ("nw", 8, 8), ("ne", self.width - 8, 8), ] ta = self.getDemoInfoTextAttr(tinfo) if ta: # font = self.app.getFont("canvas_large") font = self.app.getFont("default") self.demo.info_text = MfxCanvasText(self.canvas, ta[1], ta[2], anchor=ta[0], font=font, text=self.getDemoInfoText()) def getDemoInfoText(self): h = self.Hint_Class is None and 'None' or self.Hint_Class.__name__ return '%s (%s)' % (self.gameinfo.short_name, h) def getDemoInfoTextAttr(self, tinfo): items1, items2 = [], [] for s in self.allstacks: if s.is_visible: items1.append(s) items1.extend(list(s.cards)) if not s.cards and s.cap.max_accept > 0: items2.append(s) else: items2.extend(list(s.cards)) ti = self.__checkFreeSpaceForDemoInfoText(items1) if ti < 0: ti = self.__checkFreeSpaceForDemoInfoText(items2) if ti < 0: return None return tinfo[ti] def __checkFreeSpaceForDemoInfoText(self, items): CW, CH = self.app.images.CARDW, self.app.images.CARDH # note: these are translated by (-CW/2, -CH/2) x1, x2 = 3*CW//2, self.width - 5*CW//2 y1, y2 = CH//2, self.height - 3*CH//2 # m = [1, 1, 1, 1] for c in items: cx, cy = c.x, c.y if cy >= y2: if cx <= x1: m[0] = 0 elif cx >= x2: m[1] = 0 elif cy <= y1: if cx <= x1: m[2] = 0 elif cx >= x2: m[3] = 0 for mm in m: if mm: return mm return -1 def createDemoLogo(self): if not self.app.gimages.demo: return if self.demo_logo or not self.app.opt.demo_logo: return if self.width <= 100 or self.height <= 100: return # self.demo_logo = self.app.miscrandom.choice(self.app.gimages.demo) n = self.random.initial_seed % len(self.app.gimages.demo) self.demo_logo = self.app.gimages.demo[int(n)] self.canvas.setTopImage(self.demo_logo) # # stuck # def getStuck(self): h = self.Stuck_Class.getHints(None) if h: self.failed_snapshots = [] return True if not self.canDealCards(): return False # can deal cards: do we have any hints in previous deals ? sn = self.getSnapshot() if sn in self.failed_snapshots: return False self.failed_snapshots.append(sn) return True def updateStuck(self): # stuck if self.finished: return if self.Stuck_Class is None: return if self.getStuck(): text = '' else: text = 'x' # self.playSample("autopilotlost", priority=1000) self.updateStatus(stuck=text) # # Handle moves (with move history for undo/redo) # Actual move is handled in a subclass of AtomicMove. # # Note: # All playing moves (user actions, demo games) must get routed # to Stack.moveMove() because the stack may add important # triggers to a move (most notably fillStack and updateModel). # # Only low-level game (Game.startGame, Game.dealCards, Game.fillStack) # or stack methods (Stack.moveMove) should call the functions below # directly. # def startMoves(self): self.moves = Struct( state=self.S_PLAY, history=[], # list of lists of atomic moves index=0, current=[], # atomic moves for the current move ) # reset statistics self.stats.undo_moves = 0 self.stats.redo_moves = 0 self.stats.player_moves = 0 self.stats.demo_moves = 0 self.stats.total_moves = 0 self.stats.quickplay_moves = 0 self.stats.goto_bookmark_moves = 0 def __storeMove(self, am): if self.S_DEAL <= self.moves.state <= self.S_PLAY: self.moves.current.append(am) # move type 1 def moveMove(self, ncards, from_stack, to_stack, frames=-1, shadow=-1): assert from_stack and to_stack and from_stack is not to_stack assert 0 < ncards <= len(from_stack.cards) am = AMoveMove(ncards, from_stack, to_stack, frames, shadow) self.__storeMove(am) am.do(self) self.hints.list = None # move type 2 def flipMove(self, stack): assert stack am = AFlipMove(stack) self.__storeMove(am) am.do(self) self.hints.list = None def singleFlipMove(self, stack): # flip with animation (without "moveMove" in this move) assert stack am = ASingleFlipMove(stack) self.__storeMove(am) am.do(self) self.hints.list = None def flipAndMoveMove(self, from_stack, to_stack, frames=-1): assert from_stack and to_stack and (from_stack is not to_stack) am = AFlipAndMoveMove(from_stack, to_stack, frames) self.__storeMove(am) am.do(self) self.hints.list = None # move type 3 def turnStackMove(self, from_stack, to_stack): assert from_stack and to_stack and (from_stack is not to_stack) assert len(to_stack.cards) == 0 am = ATurnStackMove(from_stack, to_stack) self.__storeMove(am) am.do(self) self.hints.list = None # move type 4 def nextRoundMove(self, stack): assert stack am = ANextRoundMove(stack) self.__storeMove(am) am.do(self) self.hints.list = None # move type 5 def saveSeedMove(self): am = ASaveSeedMove(self) self.__storeMove(am) am.do(self) # self.hints.list = None # move type 6 def shuffleStackMove(self, stack): assert stack am = AShuffleStackMove(stack, self) self.__storeMove(am) am.do(self) self.hints.list = None # move type 7 def updateStackMove(self, stack, flags): assert stack am = AUpdateStackMove(stack, flags) self.__storeMove(am) am.do(self) # #self.hints.list = None # move type 8 def flipAllMove(self, stack): assert stack am = AFlipAllMove(stack) self.__storeMove(am) am.do(self) self.hints.list = None # move type 9 def saveStateMove(self, flags): am = ASaveStateMove(self, flags) self.__storeMove(am) am.do(self) # self.hints.list = None # for ArbitraryStack def singleCardMove(self, from_stack, to_stack, position, frames=-1, shadow=-1): am = ASingleCardMove(from_stack, to_stack, position, frames, shadow) self.__storeMove(am) am.do(self) self.hints.list = None # Finish the current move. def finishMove(self): current, moves, stats = self.moves.current, self.moves, self.stats if not current: return 0 # invalidate hints self.hints.list = None # update stats if self.demo: stats.demo_moves += 1 if moves.index == 0: stats.player_moves = 0 # clear all player moves else: stats.player_moves += 1 if moves.index == 0: stats.demo_moves = 0 # clear all demo moves stats.total_moves += 1 # try to detect a redo move in order to keep our history redo = 0 if moves.index + 1 < len(moves.history): mylen, m = len(current), moves.history[moves.index] if mylen == len(m): for i in range(mylen): a1 = current[i] a2 = m[i] if a1.__class__ is not a2.__class__ or \ a1.cmpForRedo(a2) != 0: break else: redo = 1 # add current move to history (which is a list of lists) if redo: # print "detected redo:", current # overwrite existing entry because minor things like # shadow/frames may have changed moves.history[moves.index] = current moves.index += 1 else: # resize (i.e. possibly shorten list from previous undos) moves.history[moves.index:] = [current] moves.index += 1 assert moves.index == len(moves.history) moves.current = [] self.updateSnapshots() # update view self.updateText() self.updateStatus(moves=(moves.index, self.stats.total_moves)) self.updateMenus() self.updatePlayTime(do_after=0) self.updateStuck() reset_solver_dialog() return 1 # # undo/redo layer # def undo(self): assert self.canUndo() assert self.moves.state == self.S_PLAY and len(self.moves.current) == 0 assert 0 <= self.moves.index <= len(self.moves.history) if self.moves.index == 0: return self.moves.index -= 1 self.moves.state = self.S_UNDO for atomic_move in reversed(self.moves.history[self.moves.index]): atomic_move.undo(self) self.moves.state = self.S_PLAY self.stats.undo_moves += 1 self.stats.total_moves += 1 self.hints.list = None self.updateSnapshots() self.updateText() self.updateStatus(moves=(self.moves.index, self.stats.total_moves)) self.updateMenus() self.updateStatus(stuck='') self.failed_snapshots = [] reset_solver_dialog() def redo(self): assert self.canRedo() assert self.moves.state == self.S_PLAY and len(self.moves.current) == 0 assert 0 <= self.moves.index <= len(self.moves.history) if self.moves.index == len(self.moves.history): return m = self.moves.history[self.moves.index] self.moves.index += 1 self.moves.state = self.S_REDO for atomic_move in m: atomic_move.redo(self) self.moves.state = self.S_PLAY self.stats.redo_moves += 1 self.stats.total_moves += 1 self.hints.list = None self.updateSnapshots() self.updateText() self.updateStatus(moves=(self.moves.index, self.stats.total_moves)) self.updateMenus() self.updateStuck() reset_solver_dialog() # # subclass hooks # def setState(self, state): # restore saved vars (from undo/redo) pass def getState(self): # save vars (for undo/redo) return [] # # bookmarks # def setBookmark(self, n, confirm=1): self.finishMove() # just in case if not self.canSetBookmark(): return 0 if confirm < 0: confirm = self.app.opt.confirm if confirm and self.gsaveinfo.bookmarks.get(n): if not self.areYouSure( _("Set bookmark"), _("Replace existing bookmark %d ?") % (n+1)): return 0 f = BytesIO() try: self._dumpGame(Pickler(f, 1), bookmark=2) bm = (f.getvalue(), self.moves.index) except Exception: pass else: self.gsaveinfo.bookmarks[n] = bm return 1 return 0 def gotoBookmark(self, n, confirm=-1, update_stats=1): self.finishMove() # just in case bm = self.gsaveinfo.bookmarks.get(n) if not bm: return if confirm < 0: confirm = self.app.opt.confirm if confirm: if not self.areYouSure(_("Goto bookmark"), _("Goto bookmark %d ?") % (n+1)): return try: s, moves_index = bm self.setCursor(cursor=CURSOR_WATCH) file = BytesIO(s) p = Unpickler(file) game = self._undumpGame(p, self.app) assert game.id == self.id # save state for undoGotoBookmark self.setBookmark(-1, confirm=0) except Exception: del self.gsaveinfo.bookmarks[n] self.setCursor(cursor=self.app.top_cursor) else: if update_stats: self.stats.goto_bookmark_moves += 1 self.gstats.goto_bookmark_moves += 1 self.restoreGame(game, reset=0) destruct(game) def undoGotoBookmark(self): self.gotoBookmark(-1, update_stats=0) # # load/save # def loadGame(self, filename): if self.changed(): if not self.areYouSure(_("Open game")): return self.finishMove() # just in case game = None self.setCursor(cursor=CURSOR_WATCH) self.disableMenus() try: game = self._loadGame(filename, self.app) game.gstats.holded = 0 except AssertionError: self.updateMenus() self.setCursor(cursor=self.app.top_cursor) MfxMessageDialog( self.top, title=_("Load game error"), bitmap="error", text=_("""\ Error while loading game. Probably the game file is damaged, but this could also be a bug you might want to report.""")) traceback.print_exc() except UnpicklingError as ex: self.updateMenus() self.setCursor(cursor=self.app.top_cursor) MfxExceptionDialog(self.top, ex, title=_("Load game error"), text=_("Error while loading game")) except Exception: self.updateMenus() self.setCursor(cursor=self.app.top_cursor) MfxMessageDialog(self.top, title=_("Load game error"), bitmap="error", text=_("""\ Internal error while loading game. Please report this bug.""")) traceback.print_exc() else: if self.pause: # unselect pause-button self.app.menubar.mPause() self.filename = filename game.filename = filename # now start the new game # print game.__dict__ if self.nextGameFlags(game.id) == 0: self.endGame() self.restoreGame(game) destruct(game) else: self.endGame() self.quitGame(game.id, loadedgame=game) def saveGame(self, filename, protocol=-1): self.finishMove() # just in case self.setCursor(cursor=CURSOR_WATCH) try: self._saveGame(filename, protocol) except Exception as ex: self.setCursor(cursor=self.app.top_cursor) MfxExceptionDialog(self.top, ex, title=_("Save game error"), text=_("Error while saving game")) else: self.filename = filename self.setCursor(cursor=self.app.top_cursor) # # low level load/save # def _loadGame(self, filename, app): game = None f = None try: f = open(filename, "rb") game = self._undumpGame(Unpickler(f), app) game.gstats.loaded = game.gstats.loaded + 1 finally: if f: f.close() return game def _undumpGame(self, p, app): self.updateTime() # err_txt = _("Invalid or damaged %s save file") % PACKAGE # def pload(t=None, p=p): obj = p.load() if isinstance(t, type): assert isinstance(obj, t), err_txt return obj def validate(v, txt): if not v: raise UnpicklingError(txt) # package = pload(str) validate(package == PACKAGE, err_txt) version = pload(str) # validate(isinstance(version, str) and len(version) <= 20, err_txt) version_tuple = pload(tuple) validate(version_tuple >= (1, 0), _('''\ Cannot load games saved with %s version %s''') % (PACKAGE, version)) game_version = 1 bookmark = pload(int) validate(0 <= bookmark <= 2, err_txt) game_version = pload(int) validate(game_version > 0, err_txt) # id = pload(int) validate(id > 0, err_txt) if id not in GI.PROTECTED_GAMES: game = app.constructGame(id) if game: if not game.canLoadGame(version_tuple, game_version): destruct(game) game = None validate(game is not None, _('''\ Cannot load this game from version %s as the game rules have changed in the current implementation.''') % version) game.version = version game.version_tuple = version_tuple # initial_seed = random__long2str(pload(int)) game.random = constructRandom(initial_seed) state = pload() game.random.setstate(state) # if not hasattr(game.random, "origin"): # game.random.origin = game.random.ORIGIN_UNKNOWN game.loadinfo.stacks = [] game.loadinfo.ncards = 0 nstacks = pload(int) validate(1 <= nstacks, err_txt) for i in range(nstacks): stack = [] ncards = pload(int) validate(0 <= ncards <= 1024, err_txt) for j in range(ncards): card_id = pload(int) face_up = pload(int) stack.append((card_id, face_up)) game.loadinfo.stacks.append(stack) game.loadinfo.ncards = game.loadinfo.ncards + ncards validate(game.loadinfo.ncards == game.gameinfo.ncards, err_txt) game.loadinfo.talon_round = pload() game.finished = pload() if 0 <= bookmark <= 1: saveinfo = pload(Struct) game.saveinfo.__dict__.update(saveinfo.__dict__) gsaveinfo = pload(Struct) game.gsaveinfo.__dict__.update(gsaveinfo.__dict__) moves = pload(Struct) game.moves.__dict__.update(moves.__dict__) snapshots = pload(list) game.snapshots = snapshots if 0 <= bookmark <= 1: gstats = pload(Struct) game.gstats.__dict__.update(gstats.__dict__) stats = pload(Struct) game.stats.__dict__.update(stats.__dict__) game._loadGameHook(p) dummy = pload(str) validate(dummy == "EOF", err_txt) if bookmark == 2: # copy back all variables that are not saved game.stats = self.stats game.gstats = self.gstats game.saveinfo = self.saveinfo game.gsaveinfo = self.gsaveinfo return game def _saveGame(self, filename, protocol=-1): f = None try: if not self.canSaveGame(): raise Exception("Cannot save this game.") f = open(filename, "wb") self._dumpGame(Pickler(f, protocol)) finally: if f: f.close() def _dumpGame(self, p, bookmark=0): self.updateTime() assert 0 <= bookmark <= 2 p.dump(PACKAGE) p.dump(VERSION) p.dump(VERSION_TUPLE) p.dump(bookmark) p.dump(self.GAME_VERSION) p.dump(self.id) # p.dump(random__str2long(self.random.getSeedStr())) p.dump(self.random.getstate()) # p.dump(len(self.allstacks)) for stack in self.allstacks: p.dump(len(stack.cards)) for card in stack.cards: p.dump(card.id) p.dump(card.face_up) p.dump(self.s.talon.round) p.dump(self.finished) if 0 <= bookmark <= 1: p.dump(self.saveinfo) p.dump(self.gsaveinfo) p.dump(self.moves) p.dump(self.snapshots) if 0 <= bookmark <= 1: if bookmark == 0: self.gstats.saved = self.gstats.saved + 1 p.dump(self.gstats) p.dump(self.stats) self._saveGameHook(p) p.dump("EOF") # # Playing time # def startPlayTimer(self): self.updateStatus(time=None) self.stopPlayTimer() self.play_timer = after( self.top, PLAY_TIME_TIMEOUT, self.updatePlayTime) def stopPlayTimer(self): if hasattr(self, 'play_timer') and self.play_timer: after_cancel(self.play_timer) self.play_timer = None self.updatePlayTime(do_after=0) def updatePlayTime(self, do_after=1): if not self.top: return if self.pause or self.finished: return if do_after: self.play_timer = after( self.top, PLAY_TIME_TIMEOUT, self.updatePlayTime) d = time.time() - self.stats.update_time + self.stats.elapsed_time self.updateStatus(time=format_time(d)) # # Pause # def doPause(self): if self.finished: return if self.demo: self.stopDemo() if not self.pause: self.updateTime() self.pause = not self.pause if self.pause: # self.updateTime() self.canvas.hideAllItems() n = self.random.initial_seed % len(self.app.gimages.pause) self.pause_logo = self.app.gimages.pause[int(n)] self.canvas.setTopImage(self.pause_logo) else: self.stats.update_time = time.time() self.updatePlayTime() self.canvas.setTopImage(None) self.pause_logo = None self.canvas.showAllItems() # # Help # def showHelp(self, *args): if self.preview: return kw = dict([(args[i], args[i+1]) for i in range(0, len(args), 2)]) if not kw: kw = {'info': '', 'help': ''} if 'info' in kw and self.app.opt.statusbar and self.app.opt.num_cards: self.app.statusbar.updateText(info=kw['info']) if 'help' in kw and self.app.opt.helpbar: self.app.helpbar.updateText(info=kw['help']) # # Piles descriptions # def showStackDesc(self): from pysollib.pysoltk import StackDesc from pysollib.stack import InitialDealTalonStack sd_list = [] for s in self.allstacks: sd = (s.__class__.__name__, s.cap.base_rank, s.cap.dir) if sd in sd_list: # one of each uniq pile continue if isinstance(s, InitialDealTalonStack): continue self.stackdesc_list.append(StackDesc(self, s)) sd_list.append(sd) def deleteStackDesc(self): if self.stackdesc_list: for sd in self.stackdesc_list: sd.delete() self.stackdesc_list = [] return True return False # for find_card_dialog def canFindCard(self): return self.gameinfo.category == GI.GC_FRENCH # # subclass hooks # def _restoreGameHook(self, game): pass def _loadGameHook(self, p): pass def _saveGameHook(self, p): pass def _dealNumRows(self, n): for i in range(n): self.s.talon.dealRow(frames=0) def _startDealNumRows(self, n): self._dealNumRows(n) self.startDealSample() def _startDealNumRowsAndDealSingleRow(self, n): self._startDealNumRows(n) self.s.talon.dealRow() def _startAndDealRow(self): self._startDealNumRowsAndDealSingleRow(0) def _startDealNumRowsAndDealRowAndCards(self, n): self._startDealNumRowsAndDealSingleRow(n) self.s.talon.dealCards() def _startAndDealRowAndCards(self): self._startAndDealRow() self.s.talon.dealCards() class StartDealRowAndCards(object): def startGame(self): self._startAndDealRowAndCards()