diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07b4a9f3d..7d4d52eca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: python-version: 3.7 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index 0657b3299..5eaef84fd 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -10,16 +10,16 @@ on: jobs: build: - runs-on: macos-14 + runs-on: macos-15 steps: - - uses: actions/checkout@v6 - - name: Set up python + - uses: actions/checkout@v7 + - name: "Set up python (universal2: arm64 + x86_64)" env: - PYVER: '3.9.7' - PYVER_SHORT: '3.9' + PYVER: '3.11.9' + PYVER_SHORT: '3.11' run: | - wget -O python.pkg "https://www.python.org/ftp/python/${PYVER}/python-${PYVER}-macosx10.9.pkg" + wget -O python.pkg "https://www.python.org/ftp/python/${PYVER}/python-${PYVER}-macos11.pkg" sudo installer -pkg python.pkg -target / echo "/Library/Frameworks/Python.framework/Versions/${PYVER_SHORT}/bin" >> $GITHUB_PATH - name: Install dependencies @@ -67,6 +67,21 @@ jobs: PYTHONPATH="." python3 setup_osx.py py2app test -d dist/PySolFC.app zip -q -r -X PySolFC-app.zip dist + - name: Verify the app is universal2 (arm64 + x86_64) + run: | + set -euo pipefail + check_universal2() { + archs="$(lipo -archs "$1" 2>/dev/null || true)" + case "$archs" in + *arm64*x86_64*|*x86_64*arm64*) ;; + *) echo "ERROR: $1 is not universal2 (archs: ${archs:-none})"; exit 1 ;; + esac + } + check_universal2 "dist/PySolFC.app/Contents/MacOS/PySolFC" + pygame_ext=$(find dist/PySolFC.app -name 'base.cpython-*-darwin.so' -path '*/pygame/*' | head -1) + test -n "$pygame_ext" + check_universal2 "$pygame_ext" + echo "OK: app and pygame-ce are both universal2." - name: Make the .dmg for easy installation run: create-dmg --volname "Install PySolFC" diff --git a/Makefile b/Makefile index fb99baf16..fc55a2b3d 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ rules: pot: ./scripts/all_games.py gettext > po/games.pot - xgettext --keyword=n_ --add-comments=TRANSLATORS: -o po/pysol.pot \ + xgettext --keyword=n_ --keyword=pgettext:1c,2 --add-comments=TRANSLATORS: -o po/pysol.pot \ pysollib/*.py pysollib/*/*.py pysollib/*/*/*.py data/pysolfc.glade set -e; \ for lng in de fr pl it ru pt_BR; do \ diff --git a/NEWS.asciidoc b/NEWS.asciidoc index bcdc90f88..f321ac384 100644 --- a/NEWS.asciidoc +++ b/NEWS.asciidoc @@ -1,5 +1,10 @@ [[news]] === News +* _19 July, 2026:_ There is a new stable release +https://sourceforge.net/projects/pysolfc/files/PySolFC/PySolFC-3.6.1/[PySolFC +v3.6.1]. New in this release: +** Improved Python 3.14 support. +** Many bugfixes. * _13 June, 2026:_ There is a new stable release https://sourceforge.net/projects/pysolfc/files/PySolFC/PySolFC-3.6.0/[PySolFC v3.6.0]. New in this release: diff --git a/html-src/howtoplay.html b/html-src/howtoplay.html index b94e40dc8..6bd79e9bd 100644 --- a/html-src/howtoplay.html +++ b/html-src/howtoplay.html @@ -106,4 +106,6 @@
-There is no need to compile anything since the whole program is just -a Python script. Just run it, and that's all. -
-PySol requires Python 2.3 and Tcl/Tk 8.4 or better. Both packages are -freely available for Unix, Windows and Macintosh platforms. -
-PySol is free Open Source software distributed under the terms of the -GNU GPL. - +PySolFC is free and open source software, released under the GNU GPL. It will run on Windows, Mac, Linux, Android, etc. The easiest way to run is to use a self-contained installer, which is available for most supported operating systems. -
-If you want to modify the PySol source code or write your own -Python programs you can get the development system from -http://www.python.org/download/download_windows.html +You can download the latest Windows installer from SourceForge. The installer includes the app, Python and all of the requirements to run PySolFC. A basic and a full installer are available - the full installer includes the app, plus the full set of cardsets and all of the extras. +
+PySolFC is available from Flathub (includes the full cardsets collection). Your Linux distribution may also provide their own package, though note that these packages may not always be fully up-to-date. Additionally, you can download a tarball from SourceForge. -
+You can download a .DMG package from SourceForge, and install it like any Mac application. Like with the Windows installer, a basic and full package are both available.
-There are good chances that your system already ships with Python and Tcl/Tk.
-Otherwise visit
-http://www.python.org/download/
-for full source code.
+
-Also, installable packages exist for all major Linux distributions, -FreeBSD and HPUX. +An Android package is available to install from F-Droid. +
+Additional cardsets, music, and settings are stored under ~/.PySolFC/ (or %APPDATA%\PySolFC\ on Windows), organized as follows:
+
cardsets/ - cardsets and card imagesmusic/ - background music trackstiles/ - table tile backgroundsplugins/ - app plugins+The app's install directory also contains bundled cardsets and data; the folders above are where per-user content and settings are stored. +
+You can download additional content from SourceForge, though the full collection is already included if you downloaded a full Windows/Mac package or the Flathub version: +
-
+You can check for new versions on the PySolFC website, the news page in the online documentation, along with the SourceForge/Flathub/F-Droid sites. Statistics, options, and user data in your PySolFC user folder will be retained across updates.
-Self installing exectuables for Python and Tcl/Tk are available from
-http://www.python.org/download/download_mac.html
+
-As I don't have access to a Mac I'd appreciate any detailed feedback on
-installation and look & feel. "Porting" from X11 to Windows only required some
-minor changes in the default font settings, so I hope the situation on Macs is
-similar.
+Developers can also run PySolFC from the source code. You can clone the GitHub repository and run
+python3 pysol.py after installing Python 3.7+, Tkinter, and the pysol-cards package. See the repository's README for the full details.
+
+
-[ I have been told that PySol works fine on a Mac - just drop "pysol.py"
- on the Python interpreter and that's it. But for some reason you must
- assign a large amount of memory to the Python interpreter. ]
+A number of command line options are available for special run options, i.e. pysol.py --game="FreeCell". Run with the --help command parameter for more details on the different options.
+
+The following errors might occur during installation: +
19 July, 2026: There is a new stable release +PySolFC +v3.6.1. New in this release:
++Improved Python 3.14 support. +
++Many bugfixes. +
+13 June, 2026: There is a new stable release PySolFC v3.6.0. New in this release:
diff --git a/html-src/rules/antares.html b/html-src/rules/antares.html index 62b9a074d..176278328 100644 --- a/html-src/rules/antares.html +++ b/html-src/rules/antares.html @@ -25,5 +25,6 @@-Demons and Thieves is a combination of FreeCell-type and Scorpion-type +Antares is a combination of FreeCell type +and Scorpion type games. It was invented by Thomas Warfield. diff --git a/html-src/rules/demonsandthieves.html b/html-src/rules/demonsandthieves.html index e8e6ae5c6..dc8dc3107 100644 --- a/html-src/rules/demonsandthieves.html +++ b/html-src/rules/demonsandthieves.html @@ -30,5 +30,6 @@
-Demons and Thieves is a combination of Canfield-type and Forty Thieves-type
+Demons and Thieves is a combination of Canfield type
+and Forty Thieves type
games. It was invented by Thomas Warfield.
diff --git a/po/de_pysol.po b/po/de_pysol.po
index dda63fa86..22b077b36 100644
--- a/po/de_pysol.po
+++ b/po/de_pysol.po
@@ -4030,6 +4030,25 @@ msgstr "Extra große Karten"
msgid "Hi-Res cardsets"
msgstr ""
+msgid "Tiny"
+msgstr ""
+
+msgid "Small"
+msgstr ""
+
+msgctxt "size"
+msgid "Medium"
+msgstr ""
+
+msgid "Large"
+msgstr ""
+
+msgid "Extra Large"
+msgstr ""
+
+msgid "Hi-Res"
+msgstr ""
+
#: pysollib\pysolgtk\selectcardset.py:171 pysollib\tile\selectcardset.py:85
#: pysollib\tile\selectcardset.py:184 pysollib\tk\selectcardset.py:81
#: pysollib\tk\selectcardset.py:178
diff --git a/po/fr_pysol.po b/po/fr_pysol.po
index 737cc77b4..15393121d 100644
--- a/po/fr_pysol.po
+++ b/po/fr_pysol.po
@@ -4075,6 +4075,25 @@ msgstr "Cartes XL"
msgid "Hi-Res cardsets"
msgstr ""
+msgid "Tiny"
+msgstr ""
+
+msgid "Small"
+msgstr ""
+
+msgctxt "size"
+msgid "Medium"
+msgstr ""
+
+msgid "Large"
+msgstr ""
+
+msgid "Extra Large"
+msgstr ""
+
+msgid "Hi-Res"
+msgstr ""
+
#: pysollib\pysolgtk\selectcardset.py:171 pysollib\tile\selectcardset.py:85
#: pysollib\tile\selectcardset.py:184 pysollib\tk\selectcardset.py:81
#: pysollib\tk\selectcardset.py:178
diff --git a/po/it_pysol.po b/po/it_pysol.po
index 04e0468d4..9caa0167b 100644
--- a/po/it_pysol.po
+++ b/po/it_pysol.po
@@ -4133,6 +4133,25 @@ msgstr "Grandissime"
msgid "Hi-Res cardsets"
msgstr ""
+msgid "Tiny"
+msgstr ""
+
+msgid "Small"
+msgstr ""
+
+msgctxt "size"
+msgid "Medium"
+msgstr ""
+
+msgid "Large"
+msgstr ""
+
+msgid "Extra Large"
+msgstr ""
+
+msgid "Hi-Res"
+msgstr ""
+
#: pysollib\pysolgtk\selectcardset.py:171 pysollib\tile\selectcardset.py:85
#: pysollib\tile\selectcardset.py:184 pysollib\tk\selectcardset.py:81
#: pysollib\tk\selectcardset.py:178
diff --git a/po/pl_pysol.po b/po/pl_pysol.po
index 9d590deb2..72fea0396 100644
--- a/po/pl_pysol.po
+++ b/po/pl_pysol.po
@@ -4101,6 +4101,25 @@ msgstr "Bardzo duże zestawy kart"
msgid "Hi-Res cardsets"
msgstr "Wysoka rozdzielczość zestawu kart"
+msgid "Tiny"
+msgstr ""
+
+msgid "Small"
+msgstr ""
+
+msgctxt "size"
+msgid "Medium"
+msgstr ""
+
+msgid "Large"
+msgstr ""
+
+msgid "Extra Large"
+msgstr ""
+
+msgid "Hi-Res"
+msgstr ""
+
#: pysollib\pysolgtk\selectcardset.py:171 pysollib\tile\selectcardset.py:85
#: pysollib\tile\selectcardset.py:184 pysollib\tk\selectcardset.py:81
#: pysollib\tk\selectcardset.py:178
diff --git a/po/pt_BR_pysol.po b/po/pt_BR_pysol.po
index 60cce0061..69bdf7986 100644
--- a/po/pt_BR_pysol.po
+++ b/po/pt_BR_pysol.po
@@ -4105,6 +4105,25 @@ msgstr "Sets de cartas extra grandes"
msgid "Hi-Res cardsets"
msgstr "Sets de cartas em alta resolução"
+msgid "Tiny"
+msgstr ""
+
+msgid "Small"
+msgstr ""
+
+msgctxt "size"
+msgid "Medium"
+msgstr ""
+
+msgid "Large"
+msgstr ""
+
+msgid "Extra Large"
+msgstr ""
+
+msgid "Hi-Res"
+msgstr ""
+
#: pysollib\pysolgtk\selectcardset.py:171 pysollib\tile\selectcardset.py:85
#: pysollib\tile\selectcardset.py:184 pysollib\tk\selectcardset.py:81
#: pysollib\tk\selectcardset.py:178
diff --git a/po/pysol.pot b/po/pysol.pot
index 03b54a550..b2958ad3f 100644
--- a/po/pysol.pot
+++ b/po/pysol.pot
@@ -4436,6 +4436,25 @@ msgstr ""
msgid "Hi-Res cardsets"
msgstr ""
+msgid "Tiny"
+msgstr ""
+
+msgid "Small"
+msgstr ""
+
+msgctxt "size"
+msgid "Medium"
+msgstr ""
+
+msgid "Large"
+msgstr ""
+
+msgid "Extra Large"
+msgstr ""
+
+msgid "Hi-Res"
+msgstr ""
+
#: pysollib\pysolgtk\selectcardset.py:171
#: pysollib\tile\selectcardset.py:85
#: pysollib\tile\selectcardset.py:184
diff --git a/po/ru_pysol.po b/po/ru_pysol.po
index d798adae9..0167e796f 100644
--- a/po/ru_pysol.po
+++ b/po/ru_pysol.po
@@ -4143,6 +4143,25 @@ msgstr "Очень большие колоды"
msgid "Hi-Res cardsets"
msgstr ""
+msgid "Tiny"
+msgstr ""
+
+msgid "Small"
+msgstr ""
+
+msgctxt "size"
+msgid "Medium"
+msgstr ""
+
+msgid "Large"
+msgstr ""
+
+msgid "Extra Large"
+msgstr ""
+
+msgid "Hi-Res"
+msgstr ""
+
#: pysollib\pysolgtk\selectcardset.py:171 pysollib\tile\selectcardset.py:85
#: pysollib\tile\selectcardset.py:184 pysollib\tk\selectcardset.py:81
#: pysollib\tk\selectcardset.py:178
diff --git a/pysollib/actions.py b/pysollib/actions.py
index 4978513d2..708b1c298 100644
--- a/pysollib/actions.py
+++ b/pysollib/actions.py
@@ -82,6 +82,7 @@ def __init__(self, app, top, progress=None):
shuffle=0,
autodeal=0,
quickplay=0,
+ cardbackground=0,
demo=0,
highlight_piles=0,
autoscale=0,
@@ -162,6 +163,8 @@ def _updateMenuState(self):
ms.autodeal = 1
if autostacks[2]:
ms.quickplay = 1
+ if len(game.app.images.getCardbacks()) > 1:
+ ms.cardbackground = 1
if opt.highlight_piles and game.getHighlightPilesStacks():
ms.highlight_piles = 1
if opt.auto_scale:
@@ -220,6 +223,7 @@ def _updateMenus(self):
self.setMenuState(ms.autodrop, "options.automaticplay.autodrop")
self.setMenuState(ms.autodeal, "options.automaticplay.autodeal")
self.setMenuState(ms.quickplay, "options.automaticplay.quickplay")
+ self.setMenuState(ms.cardbackground, "options.cardbackground")
if USE_PIL:
self.setMenuState(ms.autoscale,
"options.cardsize.preserveaspectratio")
diff --git a/pysollib/app.py b/pysollib/app.py
index 910b37b06..3f71851d7 100644
--- a/pysollib/app.py
+++ b/pysollib/app.py
@@ -995,14 +995,15 @@ def isCardCountRelevant(self, gi):
return (gi.category == GI.GC_TRUMP_ONLY or
gi.category == GI.GC_MATCHING)
- def selectCardset(self, title, key):
+ def selectCardset(self, title, key, initial_tab=None):
wasPaused = False
if not self.game.pause:
self.game.doPause()
wasPaused = True
d = SelectCardsetDialogWithPreview(
self.top, title=title, app=self,
- manager=self.cardset_manager, key=key)
+ manager=self.cardset_manager, key=key,
+ initial_tab=initial_tab)
if self.game.pause:
if wasPaused:
self.game.doPause()
diff --git a/pysollib/game/__init__.py b/pysollib/game/__init__.py
index 08670542f..7a2998836 100644
--- a/pysollib/game/__init__.py
+++ b/pysollib/game/__init__.py
@@ -37,7 +37,7 @@
from pysollib.game.dump import pysolDumpGame
from pysollib.gamedb import GI
from pysollib.help import help_about
-from pysollib.hint import DefaultHint
+from pysollib.hint import DefaultHint, HINT_LEVEL_SOLVER, HINT_LEVEL_STUCK
from pysollib.mfxutil import Image, ImageTk, USE_PIL
from pysollib.mfxutil import Struct, SubclassResponsibility, destruct
from pysollib.mfxutil import format_time, print_err
@@ -666,7 +666,7 @@ def create(self, app):
self.showHelp() # just in case
hint_class = self.getHintClass()
if hint_class is not None:
- self.Stuck_Class = hint_class(self, 0)
+ self.Stuck_Class = hint_class(self, HINT_LEVEL_STUCK)
self.busy = old_busy
def _checkGame(self):
@@ -1733,17 +1733,6 @@ def animatedMoveTo(self, from_stack, to_stack, cards, x, y,
if self.app.opt.animations == 0 or frames == 0:
return
- if TOOLKIT == 'kivy':
- c0 = cards[0]
- dx, dy = (x - c0.x), (y - c0.y)
- base = float(self.app.opt.animations)
- duration = base*base/30.0 + 0.05
- for card in cards:
- card.animatedMove(dx, dy, duration)
- # self.top.waitAnimation(swallow=True, pickup=True)
- # synchronise: ev. per option ?
- return
-
# init timer - need a high resolution for this to work
clock, delay, skip = None, 1, 1
if self.app.opt.animations >= 2:
@@ -1767,73 +1756,89 @@ def animatedMoveTo(self, from_stack, to_stack, cards, x, y,
# if self.moves.state == self.S_INIT and frames > 4:
# frames //= 2
return
- if shadow < 0:
- shadow = self.app.opt.shadow
- shadows = ()
-
- # start animation
- if tkraise:
+ if TOOLKIT == 'kivy':
+ c0 = cards[0]
+ dx, dy = (x - c0.x), (y - c0.y)
for card in cards:
- card.tkraise()
+ card.animatedMove(dx, dy, SPF * frames)
+ # self.top.waitAnimation(swallow=True, pickup=True)
+ # synchronise: ev. per option ?
+ return
- # Recalculate every frame so the animation adapts
- c0 = cards[0]
- if to_stack:
+ # Block input while animating: canvas.update() in the loop below
+ # can re-enter mouse handlers and corrupt game state (#576).
+ old_busy = self.busy
+ self.busy = 1
+ try:
+ if shadow < 0:
+ shadow = self.app.opt.shadow
+ shadows = ()
- def _dest_func():
- return to_stack.getPositionForNextCard()
- else:
- _static_dest = (x, y)
+ # start animation
+ if tkraise:
+ for card in cards:
+ card.tkraise()
- def _dest_func():
- return _static_dest
+ # Recalculate every frame so the animation adapts
+ c0 = cards[0]
+ if to_stack:
- i = 1
- if clock:
- starttime = clock()
- while i < frames:
- # Determine current destination for this frame
- dest_x, dest_y = _dest_func()
- dx_remain = dest_x - c0.x
- dy_remain = dest_y - c0.y
- remaining_frames = frames - i + 1
- if remaining_frames <= 0:
- remaining_frames = 1
- # Move a fraction of the remaining distance
- mx = int(round(dx_remain / remaining_frames))
- my = int(round(dy_remain / remaining_frames))
- 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)
+ def _dest_func():
+ return to_stack.getPositionForNextCard()
+ else:
+ _static_dest = (x, y)
+
+ def _dest_func():
+ return _static_dest
+
+ i = 1
+ if clock:
+ starttime = clock()
+ while i < frames:
+ # Determine current destination for this frame
+ dest_x, dest_y = _dest_func()
+ dx_remain = dest_x - c0.x
+ dy_remain = dest_y - c0.y
+ remaining_frames = frames - i + 1
+ if remaining_frames <= 0:
+ remaining_frames = 1
+ # Move a fraction of the remaining distance
+ mx = int(round(dx_remain / remaining_frames))
+ my = int(round(dy_remain / remaining_frames))
+ 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)
+ # process events so resize handlers run
+ self.canvas.update()
+ step = 1
+ if clock:
+ endtime = starttime + i * SPF
+ sleep = endtime - clock()
+ if delay and sleep >= 0.005:
+ usleep(sleep)
+ elif skip and sleep <= -0.75 * SPF:
+ step += 1
+ if frames > 4 and sleep < -1.5 * SPF:
+ step += 1
+ i += step
+
+ # last frame: delete shadows and move cards to final destination
for s in shadows:
- s.move(mx, my)
+ s.delete()
+ dest_x, dest_y = _dest_func()
+ dx_final = dest_x - c0.x
+ dy_final = dest_y - c0.y
for card in cards:
- card.moveBy(mx, my)
- # process events so resize handlers run
+ card.moveBy(dx_final, dy_final)
self.canvas.update()
- step = 1
- if clock:
- endtime = starttime + i * SPF
- sleep = endtime - clock()
- if delay and sleep >= 0.005:
- usleep(sleep)
- elif skip and sleep <= -0.75 * SPF:
- step += 1
- if frames > 4 and sleep < -1.5 * SPF:
- step += 1
- i += step
-
- # last frame: delete shadows and move cards to final destination
- for s in shadows:
- s.delete()
- dest_x, dest_y = _dest_func()
- dx_final = dest_x - c0.x
- dy_final = dest_y - c0.y
- for card in cards:
- card.moveBy(dx_final, dy_final)
- self.canvas.update()
+ finally:
+ self.busy = old_busy
def doAnimatedFlipAndMove(self, from_stack, to_stack=None, frames=-1):
if self.app.opt.animations == 0 or frames == 0:
@@ -2821,7 +2826,7 @@ def getGameBalance(self):
# 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 level == HINT_LEVEL_SOLVER:
# if self.solver is None:
# return None
return self.solver.getHints(taken_hint)
@@ -3215,14 +3220,34 @@ def createDemoLogo(self):
self.demo_logo = self.app.gimages.demo[int(n)]
self.canvas.setTopImage(self.demo_logo)
+ def _iterMovablePiles(self, stack):
+ # Yield every face-up suffix pile the stack may move (longest first).
+ cards = stack.cards
+ if not cards:
+ return
+ start = 0
+ for i, c in enumerate(cards):
+ if c.face_up:
+ start = i
+ break
+ piles = []
+ for i in range(start, len(cards)):
+ pile = cards[i:]
+ if stack.canMoveCards(pile):
+ piles.append(pile)
+ for pile in reversed(piles):
+ yield pile
+
def getStuck(self):
- h = self.Stuck_Class.getHints(None)
+ h = self.Stuck_Class.getHints(None) or []
if h:
self.failed_snapshots = []
return True
if not self.canDealCards():
return False
- # can deal cards: do we have any hints in previous deals ?
+ # No table/waste moves, but dealing (or a waste redeal) is still
+ # possible. Remember this layout; if we return here after dealing
+ # through the talon/waste with no progress, treat as stuck.
sn = self.getSnapshot()
if sn in self.failed_snapshots:
return False
diff --git a/pysollib/gamedb.py b/pysollib/gamedb.py
index 79c2a12ab..65dc849e2 100644
--- a/pysollib/gamedb.py
+++ b/pysollib/gamedb.py
@@ -21,13 +21,12 @@
#
# ---------------------------------------------------------------------------##
-import re
from importlib import util
import pysollib.settings
from pysollib.mfxutil import Struct, print_err
from pysollib.mygettext import _, n_
-from pysollib.resource import CSI
+from pysollib.resource import CSI, getNaturalSortKey
# ************************************************************************
@@ -886,13 +885,6 @@ def register(self, gi):
self.callback()
self._num_games += 1
- def getNaturalSortKey(self, text):
- # Sort numbers numerically, and strings alphabetically
- def convert(text):
- return int(text) if text.isdigit() else text.lower()
-
- return [convert(c) for c in re.split(r'(\d+)', text)]
-
#
# access games database - we do not expose hidden games
#
@@ -910,14 +902,13 @@ def getGamesIdSortedByName(self):
if self.__games_by_name is None:
l1, l2, l3 = [], [], []
for id, gi in self.__games.items():
- name = gi.name.lower()
- l1.append((self.getNaturalSortKey(name), id))
+ l1.append((getNaturalSortKey(gi.name), id))
if gi.name != gi.short_name:
- name = gi.short_name.lower()
- l2.append((self.getNaturalSortKey(name), id))
+ l2.append((getNaturalSortKey(gi.short_name), id))
+ else:
+ l2.append((getNaturalSortKey(gi.name), id))
for n in gi.altnames:
- name = n.lower()
- l3.append((self.getNaturalSortKey(name), id, n))
+ l3.append((getNaturalSortKey(n), id, n))
l1.sort(key=lambda x: x[0])
l2.sort(key=lambda x: x[0])
l3.sort(key=lambda x: x[0])
diff --git a/pysollib/games/acesup.py b/pysollib/games/acesup.py
index 0fb675b6a..da59428d7 100644
--- a/pysollib/games/acesup.py
+++ b/pysollib/games/acesup.py
@@ -52,6 +52,9 @@
class AcesUp_Foundation(AbstractFoundationStack):
def acceptsCards(self, from_stack, cards):
+ # Uncomment this if needed for testing - allows all cards to be
+ # moved to the foundation, due to the difficulty in winning.
+ # return cards[0].rank != ACE
if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
return False
c = cards[0]
@@ -101,7 +104,8 @@ def createGame(self, rows=4, reserve=False, **layout):
x = x + l.XS
x = x + l.XS//2
stack = self.Foundation_Class(x, y, self, suit=ANY_SUIT, max_move=0,
- dir=0, base_rank=ANY_RANK, max_cards=48)
+ dir=0, base_rank=ANY_RANK,
+ max_cards=self.gameinfo.ncards - 4)
l.createText(stack, "s")
s.foundations.append(stack)
@@ -120,7 +124,7 @@ def startGame(self):
self._startAndDealRow()
def isGameWon(self):
- if len(self.s.foundations[0].cards) != 48:
+ if len(self.s.foundations[0].cards) != self.gameinfo.ncards - 4:
return False
for s in self.s.rows:
if len(s.cards) != 1 or s.cards[0].rank != ACE:
diff --git a/pysollib/games/golf.py b/pysollib/games/golf.py
index b1bff52cd..7f816a03e 100644
--- a/pysollib/games/golf.py
+++ b/pysollib/games/golf.py
@@ -210,11 +210,20 @@ def getHighlightPilesStacks(self):
def getAutoStacks(self, event=None):
if event is None:
- # disable auto drop - this would ruin the whole gameplay
- return (self.sg.dropstacks, (), ())
+ # disable auto drop and auto flip
+ # auto drop would ruin the gameplay
+ # auto flip causes issues with undo when multiple cards are flipped
+ return ([], (), ())
# rightclickHandler
return (self.sg.dropstacks, self.sg.dropstacks, ())
+ def fillStack(self, stack):
+ old_state = self.enterState(self.S_FILL)
+ for r in self.s.rows:
+ if r.canFlipCard():
+ r.flipMove(animation=True)
+ self.leaveState(old_state)
+
# ************************************************************************
# * Double Golf
diff --git a/pysollib/games/interlock.py b/pysollib/games/interlock.py
index 20cfa751b..2f985421e 100644
--- a/pysollib/games/interlock.py
+++ b/pysollib/games/interlock.py
@@ -23,7 +23,7 @@
from pysollib.game import Game
from pysollib.gamedb import GI, GameInfo, registerGame
-from pysollib.hint import Yukon_Hint
+from pysollib.hint import Yukon_Hint, hint_level_is_stuck
from pysollib.layout import Layout
from pysollib.mygettext import _
from pysollib.stack import \
@@ -67,6 +67,12 @@ def _shouldSkipWholePileToEmptyRow(self, r, t, lp, lr):
return True
return False
+ def step010b_getPiles(self, stack):
+ if hint_level_is_stuck(self.level):
+ p = stack.getPile()
+ return (p, ) if p else ()
+ return Yukon_Hint.step010b_getPiles(self, stack)
+
class Interlock_StackMethods:
STEP = ((9, 10), (9, 10), (9, 10), (9, 10), (9, 10),
@@ -209,6 +215,18 @@ def _getClosestStack(self, cx, cy, stacks, dragstack):
closest, cdist = stack, dist
return closest
+ def getAutoStacks(self, event=None):
+ if event is None:
+ return ([], self.sg.dropstacks, self.sg.dropstacks)
+ return Game.getAutoStacks(self, event)
+
+ def fillStack(self, stack):
+ old_state = self.enterState(self.S_FILL)
+ for r in self.s.rows:
+ if r.canFlipCard():
+ r.flipMove(animation=True)
+ self.leaveState(old_state)
+
# ************************************************************************
# * Love A Duck
diff --git a/pysollib/games/special/matchthree.py b/pysollib/games/special/matchthree.py
index f52ec194e..ae561e299 100644
--- a/pysollib/games/special/matchthree.py
+++ b/pysollib/games/special/matchthree.py
@@ -23,6 +23,7 @@
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 kwdefault
from pysollib.mygettext import _
@@ -96,7 +97,26 @@ def getAdjacent(self, playSpace):
return adjacentRows
+class MatchThree_Hint(AbstractHint):
+ def computeHints(self):
+ game = self.game
+ seen = set()
+ for r in game.s.rows:
+ if not r.cards:
+ continue
+ for t in r.getAdjacent(r.id):
+ if not t.cards:
+ continue
+ pair = (min(r.id, t.id), max(r.id, t.id))
+ if pair in seen:
+ continue
+ if t.acceptsCards(r, r.cards):
+ seen.add(pair)
+ self.addHint(1000, 1, r, t)
+
+
class AbstractMatchThreeGame(Game):
+ Hint_Class = MatchThree_Hint
RowStack_Class = MatchThree_RowStack
NCOLORS = 4
diff --git a/pysollib/games/spider.py b/pysollib/games/spider.py
index 8f685c4c7..2f1630b08 100644
--- a/pysollib/games/spider.py
+++ b/pysollib/games/spider.py
@@ -26,7 +26,12 @@
from pysollib.gamedb import GI, GameInfo, registerGame
from pysollib.hint import CautiousDefaultHint
from pysollib.hint import FreeCellSolverWrapper
-from pysollib.hint import SpiderType_Hint, YukonType_Hint
+from pysollib.hint import (
+ HINT_LEVEL_DEBUG,
+ SpiderType_Hint,
+ YukonType_Hint,
+ hint_level_is_stuck,
+)
from pysollib.layout import Layout
from pysollib.mfxutil import kwdefault
from pysollib.stack import \
@@ -90,7 +95,11 @@ def shallMovePile(self, r, t, pile, rpile):
# The pile will get moved onto the correct suit
if len(rpile) == 0 or pile[0].suit != rpile[-1].suit:
return True
- if self.level <= 1 and len(rpile) == 0:
+ # Stuck must not accept reversible whole-pile shuffles, even
+ # though player hints (level <= 1) are more lenient.
+ if hint_level_is_stuck(self.level):
+ return False
+ if self.level <= HINT_LEVEL_DEBUG and len(rpile) == 0:
return True
return False
return True
diff --git a/pysollib/games/threepeaks.py b/pysollib/games/threepeaks.py
index 4a5a2e9a6..ad2feb3a5 100644
--- a/pysollib/games/threepeaks.py
+++ b/pysollib/games/threepeaks.py
@@ -100,7 +100,22 @@ def canSelect(self):
# * Three Peaks Game
# ************************************************************************
-class ThreePeaks(Game):
+
+class ThreePeaksExposeFlip:
+ def getAutoStacks(self, event=None):
+ if event is None:
+ return ([], self.sg.dropstacks, self.sg.dropstacks)
+ return Game.getAutoStacks(self, event)
+
+ def fillStack(self, stack):
+ old_state = self.enterState(self.S_FILL)
+ for r in self.s.rows:
+ if r.canFlipCard():
+ r.flipMove(animation=True)
+ self.leaveState(old_state)
+
+
+class ThreePeaks(ThreePeaksExposeFlip, Game):
Waste_Class = StackWrapper(Golf_Waste, mod=13)
Hint_Class = Golf_Hint
@@ -365,7 +380,7 @@ def moveMove(self, ncards, to_stack, frames=-1, shadow=-1):
self.game.leaveState(old_state)
-class Ricochet(Game):
+class Ricochet(ThreePeaksExposeFlip, Game):
Waste_Class = StackWrapper(Ricochet_Waste, mod=13)
Hint_Class = Golf_Hint
diff --git a/pysollib/hint.py b/pysollib/hint.py
index 40d94e539..405c85a6a 100644
--- a/pysollib/hint.py
+++ b/pysollib/hint.py
@@ -35,6 +35,31 @@
FCS_VERSION = None
+# Hint levels form an ordinal capability ladder (except HINT_LEVEL_STUCK):
+# >= HINT_LEVEL_DEBUG step020/030 (split-pile setup moves)
+# >= HINT_LEVEL_DEMO flip, deal, demo loop guards
+# HINT_LEVEL_STUCK is outside that ladder and used only for stuck checks.
+HINT_LEVEL_STUCK = -1
+HINT_LEVEL_PLAYER = 0
+HINT_LEVEL_DEBUG = 1
+HINT_LEVEL_DEMO = 2
+HINT_LEVEL_SOLVER = 3
+
+
+def hint_level_is_stuck(level):
+ return level == HINT_LEVEL_STUCK
+
+
+def hint_level_is_demo_or_above(level):
+ return level >= HINT_LEVEL_DEMO
+
+
+def hint_level_avoids_move_loops(level):
+ # Demo avoids reversible shuffles so it can stop; stuck must do the same
+ # or it will treat ping-pong tableau moves as "not stuck".
+ return hint_level_is_demo_or_above(level) or hint_level_is_stuck(level)
+
+
# ************************************************************************
# * HintInterface is an abstract class that defines the public
# * interface - it only consists of the constructor
@@ -45,9 +70,11 @@
class HintInterface:
- # level == 0: show hint (key `H')
- # level == 1: show hint and display score value (key `Ctrl-H')
- # level == 2: demo
+ # HINT_LEVEL_PLAYER: show hint (key `H')
+ # HINT_LEVEL_DEBUG: show hint and display score value (key `Ctrl-H')
+ # HINT_LEVEL_DEMO: demo
+ # HINT_LEVEL_SOLVER: solver dialog
+ # HINT_LEVEL_STUCK: stuck check only (not used by getHints())
def __init__(self, game, level):
pass
@@ -65,7 +92,8 @@ def __init__(self, game, level):
# A forced_move is the next move that must be taken after this move
# in order to avoid endless loops during demo play.
#
- # Deal and flip may only happen if self.level >= 2 (i.e. demo).
+ # Deal may only happen if self.level >= HINT_LEVEL_DEMO (i.e. demo).
+ # Flip may happen at HINT_LEVEL_DEMO or HINT_LEVEL_STUCK.
#
# See Game.showHint() for more information.
def getHints(self, taken_hint=None):
@@ -83,7 +111,7 @@ def __init__(self, game, level):
self.game = game
self.level = level
self.score_flatten_value = 0
- if self.level == 0:
+ if self.level == HINT_LEVEL_PLAYER:
self.score_flatten_value = 10000
# temporaries within getHints()
self.bonus_color = None
@@ -178,7 +206,8 @@ def getHints(self, taken_hint=None):
if taken_hint and taken_hint[6]:
return [taken_hint[6]]
# 2) try if we can flip a card
- if self.level >= 2:
+ if hint_level_is_demo_or_above(self.level) or \
+ hint_level_is_stuck(self.level):
for r in game.allstacks:
if r.canFlipCard():
self.addHint(self.SCORE_FLIP, 1, r, r)
@@ -187,7 +216,7 @@ def getHints(self, taken_hint=None):
# 3) ask subclass to do something useful
self.computeHints()
# 4) try if we can deal cards
- if self.level >= 2:
+ if hint_level_is_demo_or_above(self.level):
if game.canDealCards():
self.addHint(self.SCORE_DEAL, 0, game.s.talon, None)
# A few games have multiple waste stacks. In these games,
@@ -239,12 +268,12 @@ def _cautiousShallMovePile(self, from_stack, to_stack, pile, rpile):
return 0
return 1
- # same, but only check for loops only when in demo mode
+ # same, but only check for loops in demo / stuck checks
def _cautiousDemoShallMovePile(self, from_stack, to_stack, pile, rpile):
if from_stack is to_stack or not \
to_stack.acceptsCards(from_stack, pile):
return 0
- if self.level >= 2:
+ if hint_level_avoids_move_loops(self.level):
#
if len(rpile) == 0:
return 1
@@ -454,27 +483,42 @@ def _getDropCardScore(self, score, color, r, t, ncards):
# compute hints - main hint intelligence
#
+ def shallMovePile(self, from_stack, to_stack, pile, rpile):
+ # Klondike-family stuck must ignore reversible shuffles (same idea as
+ # CautiousDefaultHint / demo loop avoidance), or dead deals never get
+ # marked stuck while Autopilot still stops.
+ if hint_level_is_stuck(self.level):
+ return AbstractHint._cautiousShallMovePile(
+ self, from_stack, to_stack, pile, rpile)
+ return AbstractHint._defaultShallMovePile(
+ self, from_stack, to_stack, pile, rpile)
+
def computeHints(self):
game = self.game
+ stuck = hint_level_is_stuck(self.level)
# 1) check Tableau piles
self.step010(game.sg.dropstacks, game.s.rows)
# 2) try if we can move part of a pile within the RowStacks
# so that we can drop a card afterwards
- if not self.hints and self.level >= 1:
+ # Stuck mirrors demo/debug gating (only when nothing found yet).
+ # Forcing these steps invented pointless moves and hid real stuck.
+ if not self.hints and (stuck or self.level >= HINT_LEVEL_DEBUG):
self.step020(game.s.rows, game.s.foundations)
# 3) try if we should move a card from a Foundation to a RowStack
- if not self.hints and self.level >= 1:
+ if (not stuck and not self.hints and
+ self.level >= HINT_LEVEL_DEBUG):
self.step030(game.s.foundations, game.s.rows, game.sg.dropstacks)
- # 4) try if we can move a card from a RowStack to a ReserveStack
- if not self.hints or self.level == 0:
+ # 4/5) reserves: player always; stuck/demo only if still empty
+ if stuck:
+ if not self.hints:
+ self.step040(game.s.rows, game.sg.reservestacks)
+ self.step050(game.sg.reservestacks, game.s.rows)
+ elif not self.hints or self.level == HINT_LEVEL_PLAYER:
self.step040(game.s.rows, game.sg.reservestacks)
-
- # 5) try if we should move a card from a ReserveStack to a RowStack
- if not self.hints or self.level == 0:
self.step050(game.sg.reservestacks, game.s.rows)
# Don't be too clever and give up ;-)
@@ -495,7 +539,7 @@ def step010(self, dropstacks, rows):
score, color = self._getDropCardScore(
score, color, r, t, ncards)
self.addHint(score, ncards, r, t, color)
- if score >= 90000 and self.level >= 1:
+ if score >= 90000 and self.level >= HINT_LEVEL_DEBUG:
break
# 1b) try if we can move cards to one of the RowStacks
for pile in self.step010b_getPiles(r):
@@ -504,7 +548,8 @@ def step010(self, dropstacks, rows):
def step010b_getPiles(self, stack):
# return all moveable piles for this stack, longest one first
- return (stack.getPile(), )
+ p = stack.getPile()
+ return (p, ) if p else ()
def _shouldSkipWholePileToEmptyRow(self, r, t, lp, lr):
# Some games may need to allow moving piles between empty rows.
@@ -641,11 +686,12 @@ def step040(self, rows, reservestacks):
for t in reservestacks:
if t is r or not t.acceptsCards(r, pile):
continue
- if rr.acceptsCards(t, pile):
- # the pile we are going to move from r to t
- # could be moved back from t ro r - this is
- # dangerous as we can create loops...
- continue
+ if hint_level_avoids_move_loops(self.level):
+ if rr.acceptsCards(t, pile):
+ # the pile we are going to move from r to t
+ # could be moved back from t ro r - this is
+ # dangerous as we can create loops...
+ continue
score = 10000
score, color = self._getMovePileScore(
score, None, r, t, pile, rpile)
@@ -657,7 +703,23 @@ def step040(self, rows, reservestacks):
def step050(self, reservestacks, rows):
if not reservestacks:
return
- # FIXME
+ for r in reservestacks:
+ card = r.getCard()
+ if not card or not r.canMoveCards([card]):
+ continue
+ pile = [card]
+ for t in rows:
+ if t is r or not t.acceptsCards(r, pile):
+ continue
+ if hint_level_avoids_move_loops(self.level):
+ tt = self.ClonedStack(t, stackcards=t.cards[:])
+ if tt.acceptsCards(r, pile):
+ continue
+ score = 10000
+ score, color = self._getMovePileScore(
+ score, None, r, t, pile, [])
+ self.addHint(score, len(pile), r, t, color)
+ break
# ************************************************************************
@@ -665,8 +727,8 @@ def step050(self, reservestacks, rows):
# ************************************************************************
class CautiousDefaultHint(DefaultHint):
- # shallMovePile = DefaultHint._cautiousShallMovePile
- shallMovePile = DefaultHint._cautiousDemoShallMovePile
+ shallMovePile = DefaultHint._cautiousShallMovePile
+ # shallMovePile = DefaultHint._cautiousDemoShallMovePile
def _preferHighRankMoves(self):
return 1
diff --git a/pysollib/init.py b/pysollib/init.py
index e86b49cad..c9842ac4e 100644
--- a/pysollib/init.py
+++ b/pysollib/init.py
@@ -39,6 +39,31 @@
# ************************************************************************
+def _find_locale_dir():
+ # Locate the locale directory containing pysol.mo translations.
+ locale_locations = (
+ # locale/ next to the pysol.py script
+ sys.path[0],
+ # locale/ next to library.zip (py2exe)
+ os.path.dirname(sys.path[0]),
+ # locale/ in curdir (works for e.g. py2app)
+ os.curdir)
+ if getattr(sys, 'frozen', False):
+ # PyInstaller: sys.path[0] is often empty, and locale/ is installed
+ # next to the executable (see setup.py install_data in AppVeyor).
+ frozen_dirs = [os.path.dirname(os.path.abspath(sys.executable))]
+ meipass = getattr(sys, '_MEIPASS', None)
+ if meipass:
+ frozen_dirs.extend((meipass, os.path.dirname(meipass)))
+ locale_locations = tuple(frozen_dirs) + locale_locations
+
+ for par in locale_locations:
+ locale_dir = os.path.abspath(os.path.join(par, 'locale'))
+ if os.path.isdir(locale_dir):
+ return locale_dir
+ return None
+
+
def init():
if 'LANG' not in os.environ:
@@ -52,20 +77,10 @@ def init():
locale.setlocale(locale.LC_ALL, '')
# install gettext
- locale_locations = (
- # locale/ next to the pysol.py script
- sys.path[0],
- # locale/ next to library.zip (py2exe)
- os.path.dirname(sys.path[0]),
- # locale/ in curdir (works for e.g. py2app)
- os.curdir)
# leaving the domain unbound means sys.prefix+'/share/locale'
-
- for par in locale_locations:
- locale_dir = os.path.join(par, 'locale')
- if os.path.isdir(locale_dir):
- gettext.bindtextdomain('pysol', locale_dir)
- break
+ locale_dir = _find_locale_dir()
+ if locale_dir:
+ gettext.bindtextdomain('pysol', locale_dir)
gettext.textdomain('pysol')
diff --git a/pysollib/kivy/LApp.py b/pysollib/kivy/LApp.py
index cbd9f4a54..aa09d8bc5 100644
--- a/pysollib/kivy/LApp.py
+++ b/pysollib/kivy/LApp.py
@@ -763,6 +763,14 @@ def get_image_type(self):
LB241111.
'''
+ def _is_paused(self):
+ if self.game is not None:
+ return bool(self.game.pause)
+ lapp = App.get_running_app()
+ app = getattr(lapp, 'app', None)
+ game = getattr(app, 'game', None)
+ return bool(game and game.pause)
+
def send_event_pressed_n(self, event, n):
r = EVENT_PROPAGATE
if self.group and n in self.group.bindings:
@@ -788,6 +796,9 @@ def send_event_pressed(self, touch, event):
def on_touch_down(self, touch):
+ if self._is_paused():
+ return False
+
# print('LCardImage: size = %s' % self.size)
if self.collide_point(*touch.pos):
@@ -847,6 +858,9 @@ def on_touch_up(self, touch):
touch.ungrab(self)
return True
+ if self._is_paused():
+ return False
+
if self.collide_point(*touch.pos):
if (self.game):
diff --git a/pysollib/kivy/menubar.py b/pysollib/kivy/menubar.py
index e1372aaec..4902cf99c 100644
--- a/pysollib/kivy/menubar.py
+++ b/pysollib/kivy/menubar.py
@@ -847,7 +847,7 @@ def buildTree(self, tv, node):
# -------------------------------------------
# Cardsets and card backside options
- from pysollib.resource import CSI
+ from pysollib.resource import CSI, getNaturalSortKey
rg = tv.add_node(
LTreeNode(text=_('Cardsets')))
@@ -861,6 +861,7 @@ def buildTree(self, tv, node):
for k in sorted(cdict.keys()):
name = CSI.TYPE_NAME[k]
csl = cdict[k]
+ csl.sort(key=lambda cst: getNaturalSortKey(cst[1].name))
rg1 = tv.add_node(LTreeNode(text=name), rg)
for cst in csl:
@@ -1014,13 +1015,10 @@ def buildTree(self, tv, node):
if rg1 or rg2:
tm = self.app.tabletile_manager
- # cnt = tm.len()
- i = 1
- while True:
- ti = tm.get(i)
-
- if ti is None:
- break
+ for ti in tm.getAllSortedByName():
+ if ti.index == 0:
+ continue
+ i = ti.index
if ti.save_aspect == 0 and ti.stretch == 0 and rg1:
self.addRadioNode(tv, rg1,
ti.name,
@@ -1031,7 +1029,6 @@ def buildTree(self, tv, node):
ti.name,
self.menubar.tkopt.tabletile, i,
self.menubar.mOptTileSet)
- i += 1
yield
# -------------------------------------------
diff --git a/pysollib/kivy/tkcanvas.py b/pysollib/kivy/tkcanvas.py
index 3550ab3fd..c24a7b273 100644
--- a/pysollib/kivy/tkcanvas.py
+++ b/pysollib/kivy/tkcanvas.py
@@ -921,14 +921,9 @@ def setTopImage(self, image, cw=0, ch=0):
def hideAllItems(self):
print('MfxCanvas: hideAllItems')
- # TBD
- # Wir lassen das. Das TopImage deckt alles ab. Spielen ist
- # nicht möglich.
def showAllItems(self):
print('MfxCanvas: showAllItems')
- # TBD
- # Brauchts darum auch nicht.
# Erweiterungen fuer Tk Canvas (prüfen was noch nötig!!).
diff --git a/pysollib/mygettext.py b/pysollib/mygettext.py
index a009a4fb5..391bd07a2 100644
--- a/pysollib/mygettext.py
+++ b/pysollib/mygettext.py
@@ -63,3 +63,9 @@ def fix_gettext():
_ = gettext.ugettext
ungettext = gettext.ungettext
+
+
+def pgettext(context, message):
+ key = "%s\x04%s" % (context, message)
+ translated = _(key)
+ return message if translated == key else translated
diff --git a/pysollib/resource.py b/pysollib/resource.py
index ea90af615..7ff6a97e0 100644
--- a/pysollib/resource.py
+++ b/pysollib/resource.py
@@ -23,10 +23,11 @@
import glob
import os
+import re
import traceback
from pysollib.mfxutil import Image, KwStruct, Struct, USE_PIL
-from pysollib.mygettext import _
+from pysollib.mygettext import _, pgettext
from pysollib.settings import DEBUG
# ************************************************************************
@@ -34,6 +35,14 @@
# ************************************************************************
+def getNaturalSortKey(text):
+ # Sort numbers numerically, and strings alphabetically
+ def convert(part):
+ return int(part) if part.isdigit() else part.lower()
+
+ return [convert(c) for c in re.split(r'(\d+)', text)]
+
+
class Resource(Struct):
def __init__(self, **kw):
kw = KwStruct(
@@ -49,7 +58,7 @@ def __init__(self, **kw):
Struct.__init__(self, **kw.getKw())
def getSortKey(self):
- return self.name.lower()
+ return getNaturalSortKey(self.name)
class ResourceManager:
@@ -298,15 +307,15 @@ class CSI:
# cardset styles
STYLE = {
35: _("Abstract"), #
- 1: _("Adult"), #
- 2: _("Animals"), #
- 3: _("Anime"), #
- 4: _("Art"), #
- 5: _("Cartoons"), #
- 6: _("Children"), #
- 7: _("Classic Look"), #
- 8: _("Collectors"), # scanned collectors cardsets
- 9: _("Computers"), #
+ 1: _("Adult"), #
+ 2: _("Animals"), #
+ 3: _("Anime"), #
+ 4: _("Art"), #
+ 5: _("Cartoons"), #
+ 6: _("Children"), #
+ 7: _("Classic Look"), #
+ 8: _("Collectors"), # scanned collectors cardsets
+ 9: _("Computers"), #
36: _("Divination"), # e.g. fortunetelling decks
10: _("Engines"), #
11: _("Fantasy"), #
@@ -380,6 +389,15 @@ class CSI:
22: "2200 - 2299",
}
+ SIZE_NAME = {
+ 1: _("Tiny"),
+ 2: _("Small"),
+ 3: pgettext("size", "Medium"),
+ 4: _("Large"),
+ 5: _("Extra Large"),
+ 6: _("Hi-Res"),
+ }
+
class CardsetConfig(Struct):
# see config.txt and _readCardsetConfig()
@@ -524,36 +542,20 @@ def _check(self, cs):
elif s == CSI.TYPE_MUGHAL_GANJIFA:
cs.nbottoms = 11
elif s == CSI.TYPE_NAVAGRAHA_GANJIFA:
- # ???return 0 ## FIXME
cs.nbottoms = 12
elif s == CSI.TYPE_DASHAVATARA_GANJIFA:
cs.nbottoms = 13
elif s == CSI.TYPE_TRUMP_ONLY:
- # ???return 0 ## FIXME
- # cs.nbottoms = 7
- # cs.ranks = ()
- # cs.suits = ""
- # cs.trumps = range(cs.ncards)
cs.nbottoms = 1
cs.nletters = 0
cs.nshadows = 0
cs.trumps = list(range(cs.ncards))
elif s == CSI.TYPE_MATCHING:
- # ???return 0 ## FIXME
- # cs.nbottoms = 7
- # cs.ranks = ()
- # cs.suits = ""
- # cs.trumps = range(cs.ncards)
cs.nbottoms = 1
cs.nletters = 0
cs.nshadows = 0
cs.trumps = list(range(cs.ncards))
elif s == CSI.TYPE_PUZZLE:
- # ???return 0 ## FIXME
- # cs.nbottoms = 7
- # cs.ranks = ()
- # cs.suits = ""
- # cs.trumps = range(cs.ncards)
cs.nbottoms = 1
cs.nletters = 0
cs.nshadows = 0
diff --git a/pysollib/settings.py b/pysollib/settings.py
index f7a44e275..a5897f593 100644
--- a/pysollib/settings.py
+++ b/pysollib/settings.py
@@ -32,7 +32,7 @@ def n_(x): # for gettext
TITLE = 'PySol'
PACKAGE_URL = 'http://pysolfc.sourceforge.io/'
-VERSION_TUPLE = (3, 6, 0)
+VERSION_TUPLE = (3, 6, 1)
VERSION = '.'.join(map(str, VERSION_TUPLE))
# Tk windowing system (auto set up in init.py)
diff --git a/pysollib/stack.py b/pysollib/stack.py
index 44f676aeb..7e1927988 100644
--- a/pysollib/stack.py
+++ b/pysollib/stack.py
@@ -643,25 +643,38 @@ def closeStack(self):
#
def playFlipMove(self, sound=True, animation=False):
- if sound:
- self.game.playSample("flip", 5)
- self.flipMove(animation=animation)
- if not self.game.checkForWin():
- self.game.autoPlay()
- self.game.finishMove()
+ # Block input for the whole turn (move + autoplay + finishMove).
+ # Drag moves use frames=-2 (no animation) so animatedMoveTo's busy
+ # guard does not apply; autoPlay may still call canvas.update()
+ old_busy = self.game.busy
+ self.game.busy = 1
+ try:
+ if sound:
+ self.game.playSample("flip", 5)
+ self.flipMove(animation=animation)
+ if not self.game.checkForWin():
+ self.game.autoPlay()
+ self.game.finishMove()
+ finally:
+ self.game.busy = old_busy
def playMoveMove(self, ncards, to_stack, frames=-1, shadow=-1, sound=True):
- if sound:
- if to_stack in self.game.s.foundations:
- self.game.playSample("drop", priority=30)
- else:
- self.game.playSample("move", priority=10)
- self.moveMove(ncards, to_stack, frames=frames, shadow=shadow)
- if not self.game.checkForWin():
- # let the player put cards back from the foundations
- if self not in self.game.s.foundations:
- self.game.autoPlay()
- self.game.finishMove()
+ old_busy = self.game.busy
+ self.game.busy = 1
+ try:
+ if sound:
+ if to_stack in self.game.s.foundations:
+ self.game.playSample("drop", priority=30)
+ else:
+ self.game.playSample("move", priority=10)
+ self.moveMove(ncards, to_stack, frames=frames, shadow=shadow)
+ if not self.game.checkForWin():
+ # let the player put cards back from the foundations
+ if self not in self.game.s.foundations:
+ self.game.autoPlay()
+ self.game.finishMove()
+ finally:
+ self.game.busy = old_busy
#
# Appearance {view}
diff --git a/pysollib/tile/menubar.py b/pysollib/tile/menubar.py
index 773822dee..be7f1772a 100644
--- a/pysollib/tile/menubar.py
+++ b/pysollib/tile/menubar.py
@@ -93,11 +93,12 @@ def _calc_MfxMessageDialog(self):
# create the menubar
#
- def mSelectCardsetDialog(self, *event):
+ def mSelectCardsetDialog(self, *event, initial_tab=None):
if self._cancelDrag(break_pause=False):
return
key = self.app.nextgame.cardset.index
- cs = self.app.selectCardset(_("Select ")+CARDSET, key)
+ cs = self.app.selectCardset(
+ _("Select ")+CARDSET, key, initial_tab=initial_tab)
if not cs:
return
self.app.nextgame.cardset = cs
diff --git a/pysollib/tile/selectcardset.py b/pysollib/tile/selectcardset.py
index 5fbbc4f9d..d93d19daf 100644
--- a/pysollib/tile/selectcardset.py
+++ b/pysollib/tile/selectcardset.py
@@ -27,7 +27,7 @@
from pysollib.mfxutil import KwStruct, USE_PIL
from pysollib.mygettext import _
-from pysollib.resource import CSI
+from pysollib.resource import CSI, getNaturalSortKey
from pysollib.speech import Speech
from pysollib.ui.tktile.selecttree import SelectDialogTreeData
from pysollib.ui.tktile.tkcanvas import MfxCanvasImage
@@ -209,7 +209,8 @@ class SelectCardsetDialogWithPreview(MfxDialog):
TreeDataHolder_Class = SelectCardsetTree
TreeData_Class = SelectCardsetData
- def __init__(self, parent, title, app, manager, key=None, **kw):
+ def __init__(self, parent, title, app, manager, key=None,
+ initial_tab=None, **kw):
kw = self.initKw(kw)
MfxDialog.__init__(self, parent, title, kw.resizable, kw.default)
top_frame, bottom_frame = self.createFrames(kw)
@@ -301,6 +302,10 @@ def __init__(self, parent, title, app, manager, key=None, **kw):
self.list.config(yscrollcommand=self.list_scrollbar.set)
self.list_scrollbar.config(command=self.list.yview)
+ self.notebook = notebook
+ search_focus = notebook.apply_initial_tab(
+ initial_tab, search_frame, self.list_searchtext)
+
if USE_PIL:
size_frame = ttk.Frame(notebook)
notebook.add(size_frame, text=_('Card Size'))
@@ -406,7 +411,7 @@ def __init__(self, parent, title, app, manager, key=None, **kw):
self.updatePreview(key, overrideScale=True)
#
focus = self.createButtons(bottom_frame, kw)
- focus = self.tree.frame
+ focus = search_focus or self.tree.frame
self.mainloop(focus, kw.timeout, geometry=geometry)
def destroy(self):
@@ -562,7 +567,7 @@ def performSearch(self):
if self.app.checkSearchString(self.criteria.name,
cardset.name):
results.append(cardset.name)
- results.sort(key=lambda x: x.lower())
+ results.sort(key=getNaturalSortKey)
pos = 0
for result in results:
self.list.insert(pos, result)
@@ -751,7 +756,8 @@ def __init__(self, parent, title, cardset, images, **kw):
(_('Nationality:'), nationalities),
(_('Year:'), year),
(_('Num. cards:'), str(cardset.ncards)),
- (_('Size:'), '%d x %d' % (cardset.CARDW, cardset.CARDH)),
+ (_('Size:'), '%d x %d (%s)' % (cardset.CARDW, cardset.CARDH,
+ CSI.SIZE_NAME.get(cardset.si.size)))
):
if t is not None:
label = ttk.Label(info_frame, text=n,
@@ -785,7 +791,7 @@ def __init__(self, parent, title, cardset, images, **kw):
# bg = top_frame["bg"]
bg = 'white'
text_w = PysolText(frame, bd=1, relief="sunken", wrap="word",
- padx=4, width=64, height=8, bg=bg)
+ padx=4, width=66, height=8, bg=bg)
text_w.grid(row=row, column=0, sticky='nsew')
sb = ttk.Scrollbar(frame)
sb.grid(row=row, column=1, sticky='ns')
diff --git a/pysollib/tile/selectgame.py b/pysollib/tile/selectgame.py
index 83e25bd1e..bb4456271 100644
--- a/pysollib/tile/selectgame.py
+++ b/pysollib/tile/selectgame.py
@@ -31,7 +31,7 @@
from pysollib.mfxutil import KwStruct, Struct, destruct
from pysollib.mfxutil import format_time
from pysollib.mygettext import _
-from pysollib.resource import CSI
+from pysollib.resource import CSI, getNaturalSortKey
from pysollib.ui.tktile.selecttree import SelectDialogTreeData
from pysollib.ui.tktile.tkutil import bind, unbind_destroy
@@ -349,7 +349,8 @@ def mDone(self, button):
class SelectGameDialogWithPreview(SelectGameDialog):
Tree_Class = SelectGameTreeWithPreview
- def __init__(self, parent, title, app, gameid, bookmark=None, **kw):
+ def __init__(self, parent, title, app, gameid, bookmark=None,
+ initial_tab=None, **kw):
kw = self.initKw(kw)
MfxDialog.__init__(self, parent, title, kw.resizable, kw.default)
top_frame, bottom_frame = self.createFrames(kw)
@@ -446,6 +447,10 @@ def __init__(self, parent, title, app, gameid, bookmark=None, **kw):
self.list.config(yscrollcommand=self.list_scrollbar.set)
self.list_scrollbar.config(command=self.list.yview)
+ self.notebook = notebook
+ search_focus = notebook.apply_initial_tab(
+ initial_tab, search_frame, self.list_searchtext)
+
# LabelFrame
info_frame = ttk.LabelFrame(right_frame, text=_('About game'))
info_frame.grid(row=0, column=0, padx=padx, pady=pady,
@@ -498,7 +503,8 @@ def __init__(self, parent, title, app, gameid, bookmark=None, **kw):
self.preview_game = None
self.preview_app = None
self.updatePreview(gameid, animations=0)
- # focus = self.tree.frame
+ if search_focus is not None:
+ focus = search_focus
self.mainloop(focus, kw.timeout, geometry=geometry)
def initKw(self, kw):
@@ -676,7 +682,7 @@ def performSearch(self):
for altname in game.altnames:
if self.app.checkSearchString(self.criteria.name, altname):
results.append(altname)
- results.sort(key=self.app.gdb.getNaturalSortKey)
+ results.sort(key=getNaturalSortKey)
pos = 0
for result in results:
self.list.insert(pos, result)
diff --git a/pysollib/tile/selecttile.py b/pysollib/tile/selecttile.py
index 4306d487a..3f0fa2e45 100644
--- a/pysollib/tile/selecttile.py
+++ b/pysollib/tile/selecttile.py
@@ -30,7 +30,7 @@
from pysollib.mfxutil import KwStruct, USE_PIL
from pysollib.mygettext import _
-from pysollib.resource import TTI, Tile
+from pysollib.resource import TTI, Tile, getNaturalSortKey
from pysollib.ui.tktile.selecttree import SelectDialogTreeData
from pysollib.ui.tktile.tkutil import bind
from pysollib.util import IMAGE_EXTENSIONS
@@ -133,7 +133,8 @@ class SelectTileDialogWithPreview(MfxDialog):
TreeDataHolder_Class = SelectTileTree
TreeData_Class = SelectTileData
- def __init__(self, parent, title, app, manager, key=None, **kw):
+ def __init__(self, parent, title, app, manager, key=None,
+ initial_tab=None, **kw):
kw = self.initKw(kw)
MfxDialog.__init__(self, parent, title, kw.resizable, kw.default)
top_frame, bottom_frame = self.createFrames(kw)
@@ -227,6 +228,10 @@ def __init__(self, parent, title, app, manager, key=None, **kw):
self.list.config(yscrollcommand=self.list_scrollbar.set)
self.list_scrollbar.config(command=self.list.yview)
+ self.notebook = notebook
+ search_focus = notebook.apply_initial_tab(
+ initial_tab, search_frame, self.list_searchtext)
+
if USE_PIL:
self.scaleOptions = {"Default": 0,
"Tile": 1,
@@ -366,7 +371,7 @@ def SetupPreview():
self.updatePreview(key, app.opt.tabletile_scale_method)
#
focus = self.createButtons(bottom_frame, kw)
- focus = self.tree.frame
+ focus = search_focus or self.tree.frame
self.mainloop(focus, kw.timeout, geometry=geometry)
@@ -471,7 +476,7 @@ def performSearch(self):
if self.app.checkSearchString(self.criteria.name,
tile.name):
results.append(tile.name)
- results.sort(key=lambda x: x.lower())
+ results.sort(key=getNaturalSortKey)
pos = 0
for result in results:
self.list.insert(pos, result)
diff --git a/pysollib/tile/tkwidget.py b/pysollib/tile/tkwidget.py
index b3fbd7f89..734509f7a 100644
--- a/pysollib/tile/tkwidget.py
+++ b/pysollib/tile/tkwidget.py
@@ -34,7 +34,7 @@
from pysollib.settings import WIN_SYSTEM
from pysollib.speech import Speech
from pysollib.ui.tktile.tkcanvas import MfxCanvas
-from pysollib.ui.tktile.tkutil import after, after_cancel
+from pysollib.ui.tktile.tkutil import after, after_cancel, after_idle
from pysollib.ui.tktile.tkutil import bind, unbind_destroy
from pysollib.ui.tktile.tkutil import makeToplevel, setTransient
@@ -71,8 +71,6 @@ def __init__(self, parent, title="", resizable=False, default=-1):
def mainloop(self, focus=None, timeout=0, transient=True, geometry=""):
bind(self.top, "