1
0
Fork 0
mirror of https://github.com/shlomif/PySolFC.git synced 2025-04-05 00:02:29 -04:00
PySolFC/pysollib/game.py
skomoroh dfe790a101 * changed GPL version to 3
* added config-file option sound_sample_buffer_size
* bugfixes


git-svn-id: file:///home/shlomif/Backup/svn-dumps/PySolFC/svnsync-repos/pysolfc/PySolFC/trunk@234 efabe8c0-fbe8-4139-b769-b5e6d273206e
2009-06-12 09:32:43 +00:00

3166 lines
111 KiB
Python

#!/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 <http://www.gnu.org/licenses/>.
##
##---------------------------------------------------------------------------##
# imports
import time
import math
import traceback
from gettext import ungettext
from cStringIO import StringIO
# PySol imports
from mfxutil import Pickler, Unpickler, UnpicklingError
from mfxutil import Image, ImageTk
from mfxutil import destruct, Struct, SubclassResponsibility
from mfxutil import uclock, usleep
from mfxutil import format_time, print_err
from settings import PACKAGE, TITLE, TOOLKIT, TOP_TITLE
from settings import VERSION, VERSION_TUPLE
from settings import DEBUG
from gamedb import GI
from pysolrandom import PysolRandom, LCRandom31
from pysoltk import EVENT_HANDLED, EVENT_PROPAGATE
from pysoltk import CURSOR_WATCH
from pysoltk import bind, wm_map
from pysoltk import after, after_idle, after_cancel
from pysoltk import MfxMessageDialog, MfxExceptionDialog
from pysoltk import MfxCanvasText, MfxCanvasLine, MfxCanvasRectangle
from pysoltk import Card
from pysoltk import reset_solver_dialog
from move import AMoveMove, AFlipMove, AFlipAndMoveMove
from move import ASingleFlipMove, ATurnStackMove
from move import ANextRoundMove, ASaveSeedMove, AShuffleStackMove
from move import AUpdateStackMove, AFlipAllMove, ASaveStateMove
from move import ASingleCardMove
from hint import DefaultHint
from 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:
# 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_PLAY = 0x30
S_UNDO = 0x40
S_REDO = 0x50
# 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.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
)
self.event_handled = False # if click event handled by Stack (???)
self.reset()
# main constructor
def create(self, app):
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('<ButtonPress>', self.top._sleepEvent)
##self.top.bind('<3>', self.top._sleepEvent)
# update display properties
self.canvas.busy = True
self.canvas.setInitialSize(self.width, self.height)
if self.app.opt.save_games_geometry and \
self.id in self.app.opt.games_geometry:
# restore game geometry
w, h = self.app.opt.games_geometry[self.id]
self.canvas.config(width=w, height=h)
self.top.wm_geometry("") # cancel user-specified geometry
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.busy = old_busy
self.showHelp() # just in case
##self.reallocateStacks()
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 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, "<Double-1>", self.undoHandler)
bind(self.canvas, "<1>", self.undoHandler)
bind(self.canvas, "<2>", self.dropHandler)
bind(self.canvas, "<3>", self.redoHandler)
bind(self.canvas, '<Unmap>', self._unmapHandler)
bind(self.canvas, '<Configure>', 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
)
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 = []
# 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
highlight_cards = 0, # number of highlight matching cards consumed
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)
demo_updated = 0, # did this game already update the demo stats ?
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
updated = self.U_PLAY, # did this game already update the player stats ?
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 = str(self.random)
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))
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):
self.finished = False
old_busy, self.busy = self.busy, 1
self.setCursor(cursor=CURSOR_WATCH)
self.stopWinAnimation()
self.disableMenus()
self.redealAnimation()
self.reset(restart=restart)
self.resetGame()
self.createRandom(random)
##print self.random, self.random.__dict__
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))
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
self.startGame()
if self.gameinfo.si.game_flags & GI.GT_OPEN:
if self.s.talon:
assert len(self.s.talon.cards) == 0
for stack in self.allstacks:
if stack.is_visible:
for c in stack.cards:
assert c.face_up
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)
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)
# 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
after(self.top, 200, self.configureHandler) # wait for canvas is mapped
#
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 = StringIO(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):
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 = f | 1
if self.app.nextgame.cardset is not self.app.cardset:
f = f | 2
if random is not None:
if random.__class__ is not self.random.__class__:
f = f | 16
elif random.initial_seed != self.random.initial_seed:
f = 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
file = StringIO()
p = Pickler(file, 1)
self._dumpGame(p, bookmark=1)
self.app.nextgame.bookmark = file.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 reallocateStacks(self):
w0, h0 = self.width, self.height
iw = int(self.canvas.cget('width'))
ih = int(self.canvas.cget('height'))
vw = self.canvas.winfo_width()
vh = self.canvas.winfo_height()
if vw <= iw or vh <= ih:
return
xf = float(vw)/iw
yf = float(vh)/ih
for stack in self.allstacks:
x0, y0 = stack.init_coord
x, y = int(x0*xf), int(y0*yf)
if x == stack.x and y == stack.y:
continue
stack.moveTo(x, y)
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(10000000000000000L,
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 = 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):
for suit in gi.suits:
for rank in gi.ranks:
card = self._createCard(id, deck, suit, rank, x=x, y=y)
if card is None:
continue
cards.append(card)
id = id + 1
if progress: progress.update(step=pstep)
trump_suit = len(gi.suits)
for rank in gi.trumps:
card = self._createCard(id, deck, trump_suit, rank, x=x, y=y)
if card is None:
continue
cards.append(card)
id = id + 1
if progress: progress.update(step=pstep)
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, func, ncards):
# note that we reverse the cards, so that smaller sort_orders
# will be nearer to the top of the Talon
sitems, i = [], len(cards)
for c in cards[:]:
select, sort_order = func(c)
if select:
cards.remove(c)
sitems.append((sort_order, i, c))
if len(sitems) >= ncards:
break
i = i - 1
sitems.sort()
sitems.reverse()
scards = [item[2] for item in sitems]
return cards, scards
#
# 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, basestring):
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, basestring):
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, basestring):
if sb: sb.updateText(time=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()
def configureHandler(self, event=None):
if not self.canvas:
return
for stack in self.allstacks:
stack.updatePositions()
#
# 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 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
d = 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 = frames * 3
SPF = SPF / 2
elif self.app.opt.animations == 4: # slow
frames = frames * 8
SPF = SPF / 2
elif self.app.opt.animations == 5: # very slow
frames = frames * 16
SPF = 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 = frames / 2
if shadow < 0:
shadow = self.app.opt.shadow
shadows = ()
# start animation
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 = step + 1
if frames > 4 and sleep < -1.5*SPF: step = step + 1
##print i, step, mx, my; time.sleep(0.5)
i = 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)
if img_index & 1:
k = math.sin(f * 2.0 * math.pi)
else:
k = math.cos(f * 2.0 * math.pi)
k = k * k
k = max(0.4, k)
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 xrange(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:
MfxCanvasRectangle(self.canvas, rect[0], rect[1], rect[2], 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
x, y, r = s.x, s.y, rect
##print 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)
##print 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
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 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 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.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 acutal 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()
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 = y2 + self.app.images.CARDH
if c2 is s.cards[-1]: # top card
x2 = x2 + self.app.images.CARDW
else:
if sx0 > 0:
# left to right
x2 = x2 + sx0
else:
# right to left
x1 = x1 + self.app.images.CARDW
x2 = x2 + self.app.images.CARDW + sx0
elif sx0 == 0 and sy0 != 0:
# vertical stack
x2 = x2 + self.app.images.CARDW
if c2 is s.cards[-1]: # top card
y2 = y2 + self.app.images.CARDH
else:
if sy0 > 0:
# up to down
y2 = y2 + sy0
else:
# down to up
y1 = y1 + self.app.images.CARDH
y2 = y2 + self.app.images.CARDH + sy0
else:
x2 = x2 + self.app.images.CARDW
y2 = y2 + self.app.images.CARDH
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 == '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)
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())
x1, y1 = x1 + images.CARD_DX, y1 + images.CARD_DY
x2, y2 = x2 + images.CARD_DX, y2 + images.CARD_DY
if ncards == 1:
x1 = x1 + images.CARDW / 2
y1 = y1 + images.CARDH / 2
elif from_stack.CARD_XOFFSET[0]:
x1 = x1 + from_stack.CARD_XOFFSET[0] / 2
y1 = y1 + images.CARDH / 2
else:
x1 = x1 + images.CARDW / 2
y1 = y1 + from_stack.CARD_YOFFSET[0] / 2
x2 = x2 + images.CARDW / 2
y2 = y2 + images.CARDH / 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
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 = 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)
#
# 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):
l, m = len(current), moves.history[moves.index]
if l == len(m):
for i in range(l):
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)
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
m = self.moves.history[self.moves.index]
m = m[:]
m.reverse()
self.moves.state = self.S_UNDO
for atomic_move in m:
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()
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
if self.moves.index == len(self.moves.history):
mm = self.moves.current
else:
mm = self.moves.history[self.moves.index]
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()
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
file = StringIO()
p = Pickler(file, 1)
try:
self._dumpGame(p, bookmark=2)
bm = (file.getvalue(), self.moves.index)
except:
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 = StringIO(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:
del self.gsaveinfo.bookmarks[n]
self.setCursor(cursor=self.app.top_cursor)
else:
if update_stats:
self.stats.goto_bookmark_moves = self.stats.goto_bookmark_moves + 1
self.gstats.goto_bookmark_moves = 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, ex:
self.updateMenus()
self.setCursor(cursor=self.app.top_cursor)
d = 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, ex:
self.updateMenus()
self.setCursor(cursor=self.app.top_cursor)
d = MfxExceptionDialog(self.top, ex, title=_("Load game error"),
text=_("Error while loading game"))
except:
self.updateMenus()
self.setCursor(cursor=self.app.top_cursor)
d = 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, ex:
self.setCursor(cursor=self.app.top_cursor)
d = 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")
p = Unpickler(f)
game = self._undumpGame(p, 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 = pload(long)
if initial_seed <= 32000:
game.random = LCRandom31(initial_seed)
else:
game.random = PysolRandom(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")
p = Pickler(f, protocol)
self._dumpGame(p)
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(self.random.initial_seed)
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 pysoltk import StackDesc
from 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