mirror of
https://github.com/shlomif/PySolFC.git
synced 2025-04-05 00:02:29 -04:00
This is artisanal manual craftwork :-) def mDone(self, button): if button == 0: # "OK" or double click - if isinstance(self.tree.selection_key, six.string_types): - self.key = str(self.tree.selection_key) - else: - self.key = self.tree.selection_key + self.key = self.tree.selection_key
1056 lines
36 KiB
Python
1056 lines
36 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/>.
|
|
#
|
|
# ---------------------------------------------------------------------------
|
|
|
|
import re
|
|
import time
|
|
|
|
from pysollib.game import Game
|
|
from pysollib.gamedb import GI, GameInfo, registerGame
|
|
from pysollib.hint import AbstractHint
|
|
from pysollib.layout import Layout
|
|
from pysollib.mfxutil import Image, Struct, kwdefault
|
|
from pysollib.mygettext import _
|
|
from pysollib.mygettext import ungettext
|
|
from pysollib.pysoltk import ANCHOR_NW, EVENT_HANDLED, bind
|
|
from pysollib.pysoltk import MfxCanvasImage, MfxCanvasText
|
|
from pysollib.pysoltk import MfxMessageDialog
|
|
from pysollib.settings import DEBUG, TOOLKIT
|
|
from pysollib.stack import \
|
|
InitialDealTalonStack, \
|
|
OpenStack
|
|
from pysollib.util import ANY_SUIT, NO_RANK
|
|
|
|
|
|
def factorial(x):
|
|
if x <= 1:
|
|
return 1
|
|
a = 1
|
|
for i in range(x):
|
|
a *= (i+1)
|
|
return a
|
|
|
|
|
|
# ************************************************************************
|
|
# *
|
|
# ************************************************************************
|
|
|
|
class Mahjongg_Hint(AbstractHint):
|
|
# FIXME: no intelligence whatsoever is implemented here
|
|
def computeHints(self):
|
|
game = self.game
|
|
# get free stacks
|
|
stacks = []
|
|
for r in game.s.rows:
|
|
if r.cards and not r.basicIsBlocked():
|
|
stacks.append(r)
|
|
# find matching tiles
|
|
i = 0
|
|
for r in stacks:
|
|
for t in stacks[i+1:]:
|
|
if game.cardsMatch(r.cards[0], t.cards[0]):
|
|
# simple scoring...
|
|
# score = 10000 + r.id + t.id
|
|
rb = r.blockmap
|
|
tb = t.blockmap
|
|
score = \
|
|
10000 + \
|
|
1000 * (len(rb.below) + len(tb.below)) + \
|
|
len(rb.all_left) + len(rb.all_right) + \
|
|
len(tb.all_left) + len(tb.all_right)
|
|
self.addHint(score, 1, r, t)
|
|
i += 1
|
|
|
|
|
|
# ************************************************************************
|
|
# *
|
|
# ************************************************************************
|
|
|
|
# class Mahjongg_Foundation(AbstractFoundationStack):
|
|
class Mahjongg_Foundation(OpenStack):
|
|
|
|
def __init__(self, x, y, game, suit=ANY_SUIT, **cap):
|
|
kwdefault(cap, max_move=0, max_accept=0, max_cards=game.NCARDS)
|
|
OpenStack.__init__(self, x, y, game, **cap)
|
|
|
|
def acceptsCards(self, from_stack, cards):
|
|
# We do not accept any cards - pairs will get
|
|
# delivered by _dropPairMove() below.
|
|
return 0
|
|
|
|
def basicIsBlocked(self):
|
|
return 1
|
|
|
|
# def initBindings(self):
|
|
# pass
|
|
|
|
def _position(self, card):
|
|
# AbstractFoundationStack._position(self, card)
|
|
OpenStack._position(self, card)
|
|
|
|
fnds = self.game.s.foundations
|
|
|
|
cols = (3, 2, 1, 0)
|
|
for i in cols:
|
|
for j in range(9):
|
|
n = i*9+j
|
|
if fnds[n].cards:
|
|
fnds[n].group.tkraise()
|
|
return
|
|
|
|
def getHelp(self):
|
|
return ''
|
|
|
|
|
|
# ************************************************************************
|
|
# *
|
|
# ************************************************************************
|
|
|
|
class Mahjongg_RowStack(OpenStack):
|
|
def __init__(self, x, y, game, **cap):
|
|
kwdefault(cap, max_move=1, max_accept=1, max_cards=2,
|
|
base_rank=NO_RANK)
|
|
OpenStack.__init__(self, x, y, game, **cap)
|
|
|
|
def basicIsBlocked(self):
|
|
# any of above blocks
|
|
for stack in self.blockmap.above:
|
|
if stack.cards:
|
|
return 1
|
|
# any of left blocks - but we can try right as well
|
|
for stack in self.blockmap.left:
|
|
if stack.cards:
|
|
break
|
|
else:
|
|
return 0
|
|
# any of right blocks
|
|
for stack in self.blockmap.right:
|
|
if stack.cards:
|
|
return 1
|
|
return 0
|
|
|
|
def acceptsCards(self, from_stack, cards):
|
|
if not OpenStack.acceptsCards(self, from_stack, cards):
|
|
return 0
|
|
return self.game.cardsMatch(self.cards[0], cards[-1])
|
|
|
|
def canFlipCard(self):
|
|
return 0
|
|
|
|
def canDropCards(self, stacks):
|
|
return (None, 0)
|
|
|
|
def moveMove(self, ncards, to_stack, frames=-1, shadow=-1):
|
|
self._dropPairMove(ncards, to_stack, frames=-1, shadow=shadow)
|
|
|
|
def _dropPairMove(self, n, other_stack, frames=-1, shadow=-1):
|
|
# print 'drop:', self.id, other_stack.id
|
|
assert n == 1 and self.acceptsCards(
|
|
other_stack, [other_stack.cards[-1]])
|
|
if not self.game.demo:
|
|
self.game.playSample("droppair", priority=200)
|
|
old_state = self.game.enterState(self.game.S_FILL)
|
|
c = self.cards[0]
|
|
if c.suit == 3:
|
|
if c.rank >= 8:
|
|
i = 35
|
|
elif c.rank >= 4:
|
|
i = 34
|
|
else:
|
|
i = 30+c.rank
|
|
elif c.rank == 9:
|
|
i = 27+c.suit
|
|
else:
|
|
i = c.suit*9+c.rank
|
|
f = self.game.s.foundations[i]
|
|
self.game.moveMove(n, self, f, frames=frames, shadow=shadow)
|
|
self.game.moveMove(n, other_stack, f, frames=frames, shadow=shadow)
|
|
self.game.leaveState(old_state)
|
|
self.fillStack()
|
|
other_stack.fillStack()
|
|
|
|
#
|
|
# Mahjongg special overrides
|
|
#
|
|
|
|
# Mahjongg special: we must preserve the relative stacking order
|
|
# to keep our pseudo 3D look.
|
|
def _position(self, card):
|
|
OpenStack._position(self, card)
|
|
#
|
|
if TOOLKIT == 'tk':
|
|
rows = [s for s in self.game.s.rows[:self.id] if s.cards]
|
|
if rows:
|
|
self.group.tkraise(rows[-1].group)
|
|
return
|
|
rows = [s for s in self.game.s.rows[self.id+1:] if s.cards]
|
|
if rows:
|
|
self.group.lower(rows[0].group)
|
|
return
|
|
elif TOOLKIT == 'kivy':
|
|
rows = [s for s in self.game.s.rows[:self.id] if s.cards]
|
|
if rows:
|
|
self.group.tkraise(rows[-1].group)
|
|
return
|
|
rows = [s for s in self.game.s.rows[self.id+1:] if s.cards]
|
|
if rows:
|
|
self.group.lower(rows[0].group)
|
|
return
|
|
elif TOOLKIT == 'gtk':
|
|
# FIXME (this is very slow)
|
|
for s in self.game.s.rows[self.id+1:]:
|
|
s.group.tkraise()
|
|
|
|
def _calcMouseBind(self, binding_format):
|
|
return self.game.app.opt.calcCustomMouseButtonsBinding(binding_format)
|
|
|
|
# In Mahjongg games type there are a lot of stacks, so we optimize
|
|
# and don't create bindings that are not used anyway.
|
|
def initBindings(self):
|
|
group = self.group
|
|
# FIXME: dirty hack to access the Stack's private methods
|
|
# bind(group, "<1>", self._Stack__clickEventHandler)
|
|
# bind(group, "<3>", self._Stack__controlclickEventHandler)
|
|
# bind(group, "<Control-1>", self._Stack__controlclickEventHandler)
|
|
#
|
|
bind(
|
|
group,
|
|
self._calcMouseBind("<{mouse_button1}>"),
|
|
self.__clickEventHandler
|
|
)
|
|
bind(
|
|
group,
|
|
self._calcMouseBind("<{mouse_button3}>"),
|
|
self.__controlclickEventHandler
|
|
)
|
|
bind(
|
|
group,
|
|
self._calcMouseBind("<Control-{mouse_button1}>"),
|
|
self.__controlclickEventHandler
|
|
)
|
|
# bind(group, "<Enter>", self._Stack__enterEventHandler)
|
|
# bind(group, "<Leave>", self._Stack__leaveEventHandler)
|
|
|
|
def __defaultClickEventHandler(self, event, handler):
|
|
self.game.event_handled = True # for Game.undoHandler
|
|
if self.game.demo:
|
|
self.game.stopDemo(event)
|
|
if self.game.busy:
|
|
return EVENT_HANDLED
|
|
handler(event)
|
|
return EVENT_HANDLED
|
|
|
|
def __clickEventHandler(self, event):
|
|
# print 'click:', self.id
|
|
return self.__defaultClickEventHandler(event, self.clickHandler)
|
|
|
|
def __controlclickEventHandler(self, event):
|
|
return self.__defaultClickEventHandler(event, self.controlclickHandler)
|
|
|
|
def clickHandler(self, event):
|
|
game = self.game
|
|
drag = game.drag
|
|
# checks
|
|
if not self.cards:
|
|
return 1
|
|
card = self.cards[-1]
|
|
from_stack = drag.stack
|
|
if from_stack is self:
|
|
# remove selection
|
|
self.game.playSample("nomove")
|
|
self._stopDrag()
|
|
return 1
|
|
if self.basicIsBlocked():
|
|
# remove selection
|
|
# self.game.playSample("nomove")
|
|
return 1
|
|
# possible move
|
|
if from_stack:
|
|
if self.acceptsCards(from_stack, from_stack.cards):
|
|
self._stopDrag()
|
|
# this code actually moves the tiles
|
|
from_stack.playMoveMove(1, self, frames=0, sound=True)
|
|
if TOOLKIT == 'kivy':
|
|
if drag.shade_img:
|
|
# drag.shade_img.dtag(drag.shade_stack.group)
|
|
drag.shade_img.delete()
|
|
# game.canvas.delete(drag.shade_img)
|
|
drag.shade_img = None
|
|
return 1
|
|
drag.stack = self
|
|
self.game.playSample("startdrag")
|
|
# create the shade image (see stack.py, _updateShade)
|
|
if drag.shade_img:
|
|
# drag.shade_img.dtag(drag.shade_stack.group)
|
|
drag.shade_img.delete()
|
|
# game.canvas.delete(drag.shade_img)
|
|
drag.shade_img = None
|
|
img = game.app.images.getHighlightedCard(
|
|
card.deck, card.suit, card.rank)
|
|
if img is None:
|
|
return 1
|
|
img = MfxCanvasImage(game.canvas, self.x, self.y, image=img,
|
|
anchor=ANCHOR_NW, group=self.group)
|
|
drag.shade_img = img
|
|
# raise/lower the shade image to the correct stacking order
|
|
img.tkraise(card.item)
|
|
drag.shade_stack = self
|
|
return 1
|
|
|
|
def cancelDrag(self, event=None):
|
|
if event is None:
|
|
self._stopDrag()
|
|
|
|
def _findCard(self, event):
|
|
# we need to override this because the shade may be hiding
|
|
# the tile (from Tk's stacking view)
|
|
return len(self.cards) - 1
|
|
|
|
def getBottomImage(self):
|
|
return None
|
|
|
|
|
|
# ************************************************************************
|
|
# *
|
|
# ************************************************************************
|
|
|
|
class AbstractMahjonggGame(Game):
|
|
Hint_Class = Mahjongg_Hint
|
|
RowStack_Class = Mahjongg_RowStack
|
|
|
|
GAME_VERSION = 3
|
|
|
|
NCARDS = 144
|
|
|
|
def getTiles(self):
|
|
# decode tile positions
|
|
L = self.L
|
|
|
|
assert L[0] == "0"
|
|
assert (len(L) - 1) % 3 == 0
|
|
|
|
tiles = []
|
|
max_tl, max_tx, max_ty = -1, -1, -1
|
|
t = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
for i in range(1, len(L), 3):
|
|
n = t.find(L[i])
|
|
level, height = n // 7, n % 7 + 1
|
|
tx = t.find(L[i+1])
|
|
ty = t.find(L[i+2])
|
|
assert n >= 0 and tx >= 0 and ty >= 0
|
|
max_tl = max(level + height - 1, max_tl)
|
|
max_tx = max(tx, max_tx)
|
|
max_ty = max(ty, max_ty)
|
|
for tl in range(level, level + height):
|
|
tiles.append((tl, tx, ty))
|
|
assert len(tiles) == self.NCARDS
|
|
# tiles.sort()
|
|
# tiles = tuple(tiles)
|
|
return tiles, max_tl, max_tx, max_ty
|
|
|
|
#
|
|
# game layout
|
|
#
|
|
|
|
def createGame(self):
|
|
tiles, max_tl, max_tx, max_ty = self.getTiles()
|
|
|
|
# start layout
|
|
l, s = Layout(self), self.s
|
|
show_removed = self.app.opt.mahjongg_show_removed
|
|
|
|
# dx, dy = 2, -2
|
|
# dx, dy = 3, -3
|
|
cs = self.app.images.cs
|
|
if cs.version == 6 or cs.mahjongg3d:
|
|
dx = l.XOFFSET
|
|
dy = -l.YOFFSET
|
|
d_x = cs.SHADOW_XOFFSET
|
|
d_y = cs.SHADOW_YOFFSET
|
|
if self.preview:
|
|
size_cap, r = 100, 2
|
|
if l.CW // r > size_cap or l.CH // r > size_cap:
|
|
r = max(l.CW, l.CH) // size_cap
|
|
|
|
# Fixme
|
|
dx, dy, d_x, d_y = dx // r, dy // r, d_x // r, d_y // r
|
|
|
|
self._delta_x, self._delta_y = dx, -dy
|
|
else:
|
|
dx = 3
|
|
dy = -3
|
|
d_x = 0
|
|
d_y = 0
|
|
self._delta_x, self._delta_y = 0, 0
|
|
# print dx, dy, d_x, d_y, cs.version
|
|
|
|
font = self.app.getFont("canvas_default")
|
|
|
|
# width of self.texts.info
|
|
# ti_width = Font(self.canvas, font).measure(_('Remaining'))
|
|
ti_width = 80
|
|
|
|
# set window size
|
|
dxx, dyy = abs(dx) * (max_tl+1), abs(dy) * (max_tl+1)
|
|
# foundations dxx dyy
|
|
if self.NCARDS > 144:
|
|
fdxx = abs(dx)*8
|
|
fdyy = abs(dy)*8
|
|
else:
|
|
fdxx = abs(dx)*4
|
|
fdyy = abs(dy)*4
|
|
cardw, cardh = l.CW - d_x, l.CH - d_y
|
|
if show_removed:
|
|
left_margin = l.XM + 4*cardw+fdxx+d_x + l.XM
|
|
else:
|
|
left_margin = l.XM
|
|
tableau_width = (max_tx+2)*cardw//2+dxx+d_x
|
|
right_margin = l.XM+ti_width+l.XM
|
|
w = left_margin + tableau_width + right_margin
|
|
h = l.YM + dyy + (max_ty + 2) * cardh // 2 + d_y + l.YM
|
|
if show_removed:
|
|
h = max(h, l.YM+fdyy+cardh*9+d_y+l.YM)
|
|
self.setSize(w, h)
|
|
|
|
# set game extras
|
|
self.check_dist = l.CW*l.CW + l.CH*l.CH # see _getClosestStack()
|
|
|
|
# sort tiles (for 3D)
|
|
tiles.sort(key=lambda x: (x[0], x[2]-x[1]))
|
|
|
|
# create a row stack for each tile and compute the tilemap
|
|
tilemap = {}
|
|
x0 = left_margin
|
|
y0 = l.YM + dyy
|
|
for level, tx, ty in tiles:
|
|
# print level, tx, ty
|
|
x = x0 + (tx * cardw) // 2 + level * dx
|
|
y = y0 + (ty * cardh) // 2 + level * dy
|
|
stack = self.RowStack_Class(x, y, self)
|
|
# stack.G = (level, tx, ty)
|
|
stack.CARD_XOFFSET = dx
|
|
stack.CARD_YOFFSET = dy
|
|
s.rows.append(stack)
|
|
# tilemap - each tile covers 4 positions
|
|
tilemap[(level, tx, ty)] = stack
|
|
tilemap[(level, tx+1, ty)] = stack
|
|
tilemap[(level, tx, ty+1)] = stack
|
|
tilemap[(level, tx+1, ty+1)] = stack
|
|
|
|
# compute blockmap
|
|
for stack in s.rows:
|
|
level, tx, ty = tiles[stack.id]
|
|
above, below, left, right = {}, {}, {}, {}
|
|
# above blockers
|
|
for tl in range(level+1, level+2):
|
|
above[tilemap.get((tl, tx, ty))] = 1
|
|
above[tilemap.get((tl, tx+1, ty))] = 1
|
|
above[tilemap.get((tl, tx, ty+1))] = 1
|
|
above[tilemap.get((tl, tx+1, ty+1))] = 1
|
|
#
|
|
for tl in range(level):
|
|
below[tilemap.get((tl, tx, ty))] = 1
|
|
below[tilemap.get((tl, tx+1, ty))] = 1
|
|
below[tilemap.get((tl, tx, ty+1))] = 1
|
|
below[tilemap.get((tl, tx+1, ty+1))] = 1
|
|
# left blockers
|
|
left[tilemap.get((level, tx-1, ty))] = 1
|
|
left[tilemap.get((level, tx-1, ty+1))] = 1
|
|
# right blockers
|
|
right[tilemap.get((level, tx+2, ty))] = 1
|
|
right[tilemap.get((level, tx+2, ty+1))] = 1
|
|
# up blockers
|
|
# up[tilemap.get((level, tx, ty-1))] = 1
|
|
# up[tilemap.get((level, tx+1, ty-1))] = 1
|
|
# bottom blockers
|
|
# bottom[tilemap.get((level, tx, ty+2))] = 1
|
|
# bottom[tilemap.get((level, tx+1, ty+2))] = 1
|
|
# sanity check - assert that there are no overlapping tiles
|
|
assert tilemap.get((level, tx, ty)) is stack
|
|
assert tilemap.get((level, tx+1, ty)) is stack
|
|
assert tilemap.get((level, tx, ty+1)) is stack
|
|
assert tilemap.get((level, tx+1, ty+1)) is stack
|
|
#
|
|
above = tuple([_f for _f in above.keys() if _f])
|
|
below = tuple([_f for _f in below.keys() if _f])
|
|
left = tuple([_f for _f in left.keys() if _f])
|
|
right = tuple([_f for _f in right.keys() if _f])
|
|
# up = tuple(filter(None, up.keys()))
|
|
# bottom = tuple(filter(None, bottom.keys()))
|
|
|
|
# assemble
|
|
stack.blockmap = Struct(
|
|
above=above,
|
|
below=below,
|
|
left=left,
|
|
right=right,
|
|
# up=up,
|
|
# bottom=bottom,
|
|
all_left=None,
|
|
all_right=None,
|
|
)
|
|
|
|
def get_all_left(s):
|
|
if s.blockmap.all_left is None:
|
|
s.blockmap.all_left = {}
|
|
for t in s.blockmap.left:
|
|
if t.blockmap.all_left is None:
|
|
get_all_left(t)
|
|
s.blockmap.all_left.update(t.blockmap.all_left)
|
|
s.blockmap.all_left[t] = 1
|
|
|
|
def get_all_right(s):
|
|
if s.blockmap.all_right is None:
|
|
s.blockmap.all_right = {}
|
|
for t in s.blockmap.right:
|
|
if t.blockmap.all_right is None:
|
|
get_all_right(t)
|
|
s.blockmap.all_right.update(t.blockmap.all_right)
|
|
s.blockmap.all_right[t] = 1
|
|
|
|
for r in s.rows:
|
|
get_all_left(r)
|
|
get_all_right(r)
|
|
for r in s.rows:
|
|
r.blockmap.all_left = tuple(r.blockmap.all_left.keys())
|
|
r.blockmap.all_right = tuple(r.blockmap.all_right.keys())
|
|
|
|
# create other stacks
|
|
for i in range(4):
|
|
for j in range(9):
|
|
if show_removed:
|
|
x = l.XM+i*cardw
|
|
y = l.YM+fdyy+j*cardh
|
|
else:
|
|
if TOOLKIT == 'tk':
|
|
x = -l.XS-self.canvas.xmargin
|
|
y = l.YM+dyy
|
|
elif TOOLKIT == 'kivy':
|
|
x = -1000
|
|
y = l.YM+dyy
|
|
elif TOOLKIT == 'gtk':
|
|
# FIXME
|
|
x = self.width - l.XS
|
|
y = self.height - l.YS
|
|
stack = Mahjongg_Foundation(x, y, self)
|
|
if show_removed:
|
|
stack.CARD_XOFFSET = dx
|
|
stack.CARD_YOFFSET = dy
|
|
s.foundations.append(stack)
|
|
|
|
self.texts.info = MfxCanvasText(self.canvas,
|
|
self.width - l.XM - ti_width,
|
|
l.YM + dyy,
|
|
anchor="nw", font=font)
|
|
# the Talon is invisble
|
|
s.talon = InitialDealTalonStack(-l.XS-self.canvas.xmargin,
|
|
self.height-dyy, self)
|
|
|
|
# Define stack groups
|
|
l.defaultStackGroups()
|
|
|
|
#
|
|
# game overrides
|
|
#
|
|
|
|
def _shuffleHook(self, cards):
|
|
if self.app.opt.mahjongg_create_solvable == 0:
|
|
return cards
|
|
# try to create a solvable game
|
|
if self.app.opt.mahjongg_create_solvable == 1:
|
|
# easy
|
|
return self._shuffleHook1(cards[:])
|
|
# hard
|
|
new_cards = self._shuffleHook2(self.s.rows, cards)
|
|
if new_cards is None:
|
|
return cards
|
|
return new_cards
|
|
|
|
def _shuffleHook1(self, cards):
|
|
# old version; it generate a very easy layouts
|
|
old_cards = cards[:]
|
|
rows = self.s.rows
|
|
|
|
def is_blocked(s, new_cards):
|
|
# any of above blocks
|
|
for stack in s.blockmap.above:
|
|
if new_cards[stack.id] is None:
|
|
return True
|
|
# any of left blocks - but we can try right as well
|
|
for stack in s.blockmap.left:
|
|
if new_cards[stack.id] is None:
|
|
break
|
|
else:
|
|
return False
|
|
# any of right blocks
|
|
for stack in s.blockmap.right:
|
|
if new_cards[stack.id] is None:
|
|
return True
|
|
return False
|
|
|
|
def create_solvable(cards, new_cards):
|
|
if not cards:
|
|
return new_cards
|
|
# select two matching cards
|
|
c1 = cards[0]
|
|
del cards[0]
|
|
c2 = None
|
|
for i in range(len(cards)):
|
|
if self.cardsMatch(c1, cards[i]):
|
|
c2 = cards[i]
|
|
del cards[i]
|
|
break
|
|
#
|
|
free_stacks = [] # none-blocked stacks
|
|
for r in rows:
|
|
if new_cards[r.id] is None and not is_blocked(r, new_cards):
|
|
free_stacks.append(r)
|
|
if len(free_stacks) < 2:
|
|
return None # try another way
|
|
#
|
|
i = factorial(len(free_stacks))//2//factorial(len(free_stacks)-2)
|
|
old_pairs = []
|
|
for j in range(i):
|
|
nc = new_cards[:]
|
|
while True:
|
|
# create uniq pair
|
|
r1 = self.random.randrange(0, len(free_stacks))
|
|
r2 = self.random.randrange(0, len(free_stacks)-1)
|
|
if r2 >= r1:
|
|
r2 += 1
|
|
if (r1, r2) not in old_pairs and (r2, r1) not in old_pairs:
|
|
old_pairs.append((r1, r2))
|
|
break
|
|
# add two selected cards to new_cards
|
|
s1 = free_stacks[r1]
|
|
s2 = free_stacks[r2]
|
|
nc[s1.id] = c1
|
|
nc[s2.id] = c2
|
|
# check if this layout is solvable (backtracking)
|
|
nc = create_solvable(cards[:], nc)
|
|
if nc:
|
|
return nc
|
|
return None # try another way
|
|
|
|
new_cards = create_solvable(cards, [None]*len(cards))
|
|
if new_cards:
|
|
new_cards.reverse()
|
|
return new_cards
|
|
print('oops! can\'t create a solvable game')
|
|
return old_cards
|
|
|
|
def _shuffleHook2(self, rows, cards):
|
|
|
|
start_time = time.time()
|
|
iters = [0]
|
|
# limitations
|
|
max_time = 5.0 # seconds
|
|
max_iters = 2*len(cards)
|
|
|
|
def is_suitable(stack, cards):
|
|
for s in stack.blockmap.below:
|
|
if cards[s.id] == 1:
|
|
continue
|
|
# check if below stacks are non-empty
|
|
if cards[s.id] is None:
|
|
return False
|
|
|
|
for s in stack.blockmap.left:
|
|
if cards[s.id] == 1:
|
|
continue
|
|
if cards[s.id] is None:
|
|
for t in s.blockmap.all_left:
|
|
if cards[t.id] == 1:
|
|
continue
|
|
if cards[t.id] is not None:
|
|
# we have empty stack between two non-empty
|
|
return False
|
|
|
|
for s in stack.blockmap.right:
|
|
if cards[s.id] == 1:
|
|
continue
|
|
if cards[s.id] is None:
|
|
for t in s.blockmap.all_right:
|
|
if cards[t.id] == 1:
|
|
continue
|
|
if cards[t.id] is not None:
|
|
# we have empty stack between two non-empty
|
|
return False
|
|
return True
|
|
|
|
def create_solvable(cards, new_cards):
|
|
iters[0] += 1
|
|
if iters[0] > max_iters:
|
|
return None
|
|
if time.time() - start_time > max_time:
|
|
return None
|
|
if not cards:
|
|
return new_cards
|
|
|
|
nc = new_cards[:]
|
|
|
|
# select two matching cards
|
|
c1 = cards[0]
|
|
del cards[0]
|
|
c2 = None
|
|
for i in range(len(cards)):
|
|
if self.cardsMatch(c1, cards[i]):
|
|
c2 = cards[i]
|
|
del cards[i]
|
|
break
|
|
|
|
# find suitable stacks
|
|
# suitable_stacks = []
|
|
# for r in rows:
|
|
# if nc[r.id] is None and is_suitable(r, nc):
|
|
# suitable_stacks.append(r)
|
|
suitable_stacks = [r for r in rows
|
|
if nc[r.id] is None and is_suitable(r, nc)]
|
|
|
|
old_pairs = []
|
|
i = factorial(len(suitable_stacks))//2 \
|
|
// factorial(len(suitable_stacks)-2)
|
|
for j in range(i):
|
|
if iters[0] > max_iters:
|
|
return None
|
|
if time.time() - start_time > max_time:
|
|
return None
|
|
|
|
# select two suitable stacks
|
|
while True:
|
|
# create a uniq pair
|
|
r1 = self.random.randrange(0, len(suitable_stacks))
|
|
r2 = self.random.randrange(0, len(suitable_stacks))
|
|
if r1 == r2:
|
|
continue
|
|
if (r1, r2) not in old_pairs and (r2, r1) not in old_pairs:
|
|
old_pairs.append((r1, r2))
|
|
break
|
|
s1 = suitable_stacks[r1]
|
|
s2 = suitable_stacks[r2]
|
|
# check if s1 don't block s2
|
|
nc[s1.id] = c1
|
|
if not is_suitable(s2, nc):
|
|
nc[s1.id] = None
|
|
continue
|
|
nc[s2.id] = c2
|
|
# check if this layout is solvable (backtracking)
|
|
ret = create_solvable(cards[:], nc)
|
|
if ret:
|
|
ret = [x for x in ret if x != 1]
|
|
return ret
|
|
nc[s1.id] = nc[s2.id] = None # try another way
|
|
|
|
return None
|
|
|
|
new_cards = [None]*len(self.s.rows) # None - empty stack, 1 - non-used
|
|
drows = dict.fromkeys(rows) # optimization
|
|
for r in self.s.rows:
|
|
if r not in drows:
|
|
new_cards[r.id] = 1
|
|
del drows
|
|
|
|
while True:
|
|
ret = create_solvable(cards[:], new_cards)
|
|
if DEBUG:
|
|
print('create_solvable time:', time.time() - start_time)
|
|
if ret:
|
|
ret.reverse()
|
|
return ret
|
|
if time.time() - start_time > max_time or \
|
|
iters[0] <= max_iters:
|
|
print('oops! can\'t create a solvable game')
|
|
return None
|
|
iters = [0]
|
|
print('oops! can\'t create a solvable game')
|
|
return None
|
|
|
|
def _mahjonggShuffle(self):
|
|
talon = self.s.talon
|
|
rows = []
|
|
cards = []
|
|
|
|
for r in self.s.rows:
|
|
if r.cards:
|
|
rows.append(r)
|
|
cards.append(r.cards[0])
|
|
if not rows:
|
|
return
|
|
|
|
if self.app.opt.mahjongg_create_solvable == 0:
|
|
self.playSample('turnwaste')
|
|
old_state = self.enterState(self.S_FILL)
|
|
self.saveSeedMove()
|
|
for r in rows:
|
|
self.moveMove(1, r, talon, frames=0)
|
|
self.shuffleStackMove(talon)
|
|
for r in rows:
|
|
self.moveMove(1, talon, r, frames=0)
|
|
self.leaveState(old_state)
|
|
self.finishMove()
|
|
return
|
|
|
|
self.playSample('turnwaste')
|
|
old_state = self.enterState(self.S_FILL)
|
|
self.saveSeedMove()
|
|
|
|
new_cards = self._shuffleHook2(rows, cards)
|
|
if new_cards is None:
|
|
if TOOLKIT != 'kivy':
|
|
MfxMessageDialog(self.top, title=_('Warning'),
|
|
text=_('''\
|
|
Sorry, I can\'t find
|
|
a solvable configuration.'''),
|
|
bitmap='warning')
|
|
|
|
self.leaveState(old_state)
|
|
# self.finishMove()
|
|
# hack
|
|
am = self.moves.current[0]
|
|
am.undo(self) # restore random
|
|
self.moves.current = []
|
|
return
|
|
|
|
self.stats.shuffle_moves += 1
|
|
# move new_cards to talon
|
|
for c in new_cards:
|
|
for r in rows:
|
|
if r.cards and r.cards[0] is c:
|
|
self.moveMove(1, r, talon, frames=0)
|
|
break
|
|
# deal
|
|
for r in rows:
|
|
self.moveMove(1, talon, r, frames=0)
|
|
|
|
self.leaveState(old_state)
|
|
self.finishMove()
|
|
|
|
def canShuffle(self):
|
|
return True
|
|
|
|
def startGame(self):
|
|
assert len(self.s.talon.cards) == self.NCARDS
|
|
# self.s.talon.dealRow(rows = self.s.rows, frames = 0)
|
|
n = 12
|
|
self.s.talon.dealRow(rows=self.s.rows[:self.NCARDS-n], frames=0)
|
|
self.startDealSample()
|
|
self.s.talon.dealRow(rows=self.s.rows[self.NCARDS-n:])
|
|
assert len(self.s.talon.cards) == 0
|
|
|
|
def isGameWon(self):
|
|
return sum([len(f.cards) for f in self.s.foundations]) == self.NCARDS
|
|
|
|
def shallHighlightMatch(self, stack1, card1, stack2, card2):
|
|
if stack1.basicIsBlocked() or stack2.basicIsBlocked():
|
|
return 0
|
|
return self.cardsMatch(card1, card2)
|
|
|
|
def getAutoStacks(self, event=None):
|
|
return ((), (), ())
|
|
|
|
def updateText(self):
|
|
if self.preview > 1 or self.texts.info is None:
|
|
return
|
|
|
|
# find matching tiles
|
|
stacks = []
|
|
for r in self.s.rows:
|
|
if r.cards and not r.basicIsBlocked():
|
|
stacks.append(r)
|
|
f, i = 0, 0
|
|
for r in stacks:
|
|
n = 0
|
|
for t in stacks[i+1:]:
|
|
if self.cardsMatch(r.cards[0], t.cards[0]):
|
|
n += 1
|
|
# if n == 3: n = 1
|
|
# elif n == 2: n = 0
|
|
n = n % 2
|
|
f += n
|
|
i += 1
|
|
|
|
if f == 0:
|
|
f = _('No Free\nMatching\nPairs')
|
|
else:
|
|
f = ungettext('%d Free\nMatching\nPair',
|
|
'%d Free\nMatching\nPairs',
|
|
f) % f
|
|
t = sum([len(ii.cards) for ii in self.s.foundations])
|
|
r1 = ungettext('%d\nTile\nRemoved\n\n',
|
|
'%d\nTiles\nRemoved\n\n',
|
|
t) % t
|
|
r2 = ungettext('%d\nTile\nRemaining\n\n',
|
|
'%d\nTiles\nRemaining\n\n',
|
|
self.NCARDS - t) % (self.NCARDS - t)
|
|
|
|
t = r1 + r2 + f
|
|
self.texts.info.config(text=t)
|
|
|
|
#
|
|
# Mahjongg special overrides
|
|
#
|
|
|
|
def getHighlightPilesStacks(self):
|
|
# Mahjongg special: highlight all moveable tiles
|
|
return ((self.s.rows, 1),)
|
|
|
|
def _highlightCards(self, info, sleep=1.5, delta=(1, 1, 1, 1)):
|
|
if not Image:
|
|
delta = (-self._delta_x, 0, 0, -self._delta_y)
|
|
return Game._highlightCards(self, info, sleep=sleep, delta=delta)
|
|
|
|
if not info:
|
|
return 0
|
|
if self.pause:
|
|
return 0
|
|
self.stopWinAnimation()
|
|
items = []
|
|
for s, c1, c2, color in info:
|
|
assert c1 is c2
|
|
assert c1 in s.cards
|
|
x, y = s.x, s.y
|
|
img = self.app.images.getHighlightedCard(
|
|
c1.deck, c1.suit, c1.rank, 'black')
|
|
if img is None:
|
|
continue
|
|
img = MfxCanvasImage(self.canvas, x, y, image=img,
|
|
anchor=ANCHOR_NW, group=s.group)
|
|
if self.drag.stack and s is self.drag.stack:
|
|
img.tkraise(self.drag.shade_img)
|
|
else:
|
|
img.tkraise(c1.item)
|
|
items.append(img)
|
|
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 getCardFaceImage(self, deck, suit, rank):
|
|
if suit == 3:
|
|
cs = self.app.cardset
|
|
if len(cs.ranks) >= 12 and len(cs.suits) >= 4:
|
|
# make Mahjongg type games playable with other cardsets
|
|
if rank >= 8: # flower
|
|
suit = 1
|
|
rank = len(cs.ranks) - 2
|
|
elif rank >= 4: # season
|
|
rank = max(10, len(cs.ranks) - 3)
|
|
else: # wind
|
|
suit = rank
|
|
rank = len(cs.ranks) - 1
|
|
return self.app.images.getFace(deck, suit, rank)
|
|
|
|
def getCardBackImage(self, deck, suit, rank):
|
|
# We avoid screen updates caused by flipping cards - all
|
|
# cards are face up anyway. The Talon should be invisible
|
|
# or else the top tile of the Talon will be visible during
|
|
# game start.
|
|
return self.getCardFaceImage(deck, suit, rank)
|
|
|
|
def _createCard(self, id, deck, suit, rank, x, y):
|
|
# if deck >= 1 and suit == 3 and rank >= 4:
|
|
if deck % 4 and suit == 3 and rank >= 4:
|
|
return None
|
|
return Game._createCard(self, id, deck, suit, rank, x, y)
|
|
|
|
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:
|
|
# Mahjongg special: if the stack is very close, do
|
|
# not consider blocked stacks
|
|
if dist > self.check_dist or not stack.basicIsBlocked():
|
|
closest, cdist = stack, dist
|
|
return closest
|
|
|
|
#
|
|
# Mahjongg extras
|
|
#
|
|
|
|
def cardsMatch(self, card1, card2):
|
|
if card1.suit != card2.suit:
|
|
return 0
|
|
if card1.suit == 3:
|
|
if card1.rank >= 8:
|
|
return card2.rank >= 8
|
|
if card1.rank >= 4:
|
|
return 7 >= card2.rank >= 4
|
|
return card1.rank == card2.rank
|
|
|
|
|
|
# mahjongg util
|
|
def comp_cardset(ncards):
|
|
# calc decks, ranks & trumps
|
|
assert ncards % 4 == 0
|
|
assert 0 < ncards <= 288 # ???
|
|
decks = 1
|
|
cards = ncards//4
|
|
if ncards > 144:
|
|
assert ncards % 8 == 0
|
|
decks = 2
|
|
cards = cards//2
|
|
ranks, trumps = divmod(cards, 3)
|
|
if ranks > 10:
|
|
trumps += (ranks-10)*3
|
|
ranks = 10
|
|
if trumps > 4:
|
|
trumps = 4+(trumps-4)*4
|
|
assert 0 <= ranks <= 10 and 0 <= trumps <= 12
|
|
return decks, ranks, trumps
|
|
|
|
# ************************************************************************
|
|
# * register a Mahjongg type game
|
|
# ************************************************************************
|
|
|
|
|
|
def r(id, short_name, name=None, ncards=144, layout=None):
|
|
assert layout
|
|
if not name:
|
|
name = "Mahjongg " + short_name
|
|
classname = re.sub('\\W', '', name)
|
|
# create class
|
|
gameclass = type(classname, (AbstractMahjonggGame,), {})
|
|
gameclass.L = layout
|
|
gameclass.NCARDS = ncards
|
|
decks, ranks, trumps = comp_cardset(ncards)
|
|
gi = GameInfo(id, gameclass, name,
|
|
GI.GT_MAHJONGG, 4*decks, 0, # GI.SL_MOSTLY_SKILL,
|
|
category=GI.GC_MAHJONGG, short_name=short_name,
|
|
suits=list(range(3)), ranks=list(range(ranks)),
|
|
trumps=list(range(trumps)),
|
|
si={"decks": decks, "ncards": ncards})
|
|
gi.ncards = ncards
|
|
gi.rules_filename = "mahjongg.html"
|
|
registerGame(gi)
|
|
return gi
|