From dda8348806b67d502196986c8891bd06c7fbc18c Mon Sep 17 00:00:00 2001 From: Joe R Date: Sun, 17 Dec 2023 21:34:10 -0500 Subject: [PATCH] Added Interlock and Love a Duck games. --- html-src/rules/interlock.html | 35 ++++++ html-src/rules/loveaduck.html | 30 +++++ pysollib/gamedb.py | 4 +- pysollib/games/__init__.py | 1 + pysollib/games/interlock.py | 227 ++++++++++++++++++++++++++++++++++ pysollib/games/klondike.py | 62 ---------- 6 files changed, 295 insertions(+), 64 deletions(-) create mode 100644 html-src/rules/interlock.html create mode 100644 html-src/rules/loveaduck.html create mode 100644 pysollib/games/interlock.py diff --git a/html-src/rules/interlock.html b/html-src/rules/interlock.html new file mode 100644 index 00000000..6d3e34b1 --- /dev/null +++ b/html-src/rules/interlock.html @@ -0,0 +1,35 @@ +

Interlock

+

+Klondike type. 1 deck. Unlimited redeals. + +

Object

+

+Move all cards to the foundations. + +

Rules

+

+The tableau piles are built in a pyramid-esque layout, with eleven cards +in the top layer, ten cards in the middle, and nine on the bottom. +Each card forms a tableau pile which can be built down by alternate +color. The three bottom layer cards can be filled with a king when +empty, but the remaining layers cannot be filled. +

+Though all cards are dealt face-up, cards in the lower layers cannot +be moved until the cards on top of them are moved. However, if the +only card or sequence overlapping a card can legally be played on it, +that sequence can be moved down in a dropdown move. +

+Foundations are built up in suit from Ace to King. +

+Cards are dealt from the stock one at a time, and can be moved to +foundations or exposed tableau piles. You can go through the deck +as many times as needed. +

+The game is won when all cards are moved to the foundations. + +

Notes

+

+Interlock is an original game that's a more straightforward variant +of other interlocking tableau games, such as +Guardian or +Love a Duck. diff --git a/html-src/rules/loveaduck.html b/html-src/rules/loveaduck.html new file mode 100644 index 00000000..dc2677e1 --- /dev/null +++ b/html-src/rules/loveaduck.html @@ -0,0 +1,30 @@ +

Love a Duck

+

+Yukon type. 1 deck. No redeal. + +

Object

+

+Move all cards to the foundations. + +

Rules

+

+The tableau piles are built in a pyramid-esque layout, with eleven cards +in the top layer, ten cards in the middle, and nine on the bottom. +The remaining cards are dealt out to the top layer. Each card forms a +tableau pile which can be built down by alternate color, but cards can be +moved regardless of sequence. The bottom layer cards can be filled with +a king when empty, but the remaining layers cannot be filled. +

+Though all cards are dealt face-up, cards in the lower layers cannot +be moved until the cards on top of them are moved. However, if the +only card or sequence overlapping a card can legally be played on it, +that sequence can be moved down in a dropdown move. +

+Foundations are built up in suit from Ace to King. The game is won when +all cards are moved to the foundations. + +

Notes

+

+Love a Duck was invented by Jan Wolter, and first appeared in his app +Politaire. The name came from +"Lord Love a Duck", an expression of surprise from early 1900s London. diff --git a/pysollib/gamedb.py b/pysollib/gamedb.py index 8175807b..be7a4452 100644 --- a/pysollib/gamedb.py +++ b/pysollib/gamedb.py @@ -509,7 +509,7 @@ class GI: 415, 427, 458, 495, 496, 497, 508, 800, 814, 820, 825, 889, 911, 926)), ("Mary Whitmore Jones", (421, 624,)), - ("Jan Wolter", (917,)), + ("Jan Wolter", (917, 939,)), ) GAMES_BY_PYSOL_VERSION = ( @@ -592,7 +592,7 @@ class GI: ('fc-2.20', tuple(range(855, 897))), ('fc-2.21', tuple(range(897, 900)) + tuple(range(11014, 11017)) + tuple(range(13160, 13163)) + (16682,)), - ('dev', tuple(range(906, 938)) + tuple(range(11017, 11020)) + + ('dev', tuple(range(906, 940)) + tuple(range(11017, 11020)) + tuple(range(5600, 5624)) + tuple(range(18000, 18005)) + tuple(range(22303, 22311)) + tuple(range(22353, 22361))), ) diff --git a/pysollib/games/__init__.py b/pysollib/games/__init__.py index ae65163a..73138be5 100644 --- a/pysollib/games/__init__.py +++ b/pysollib/games/__init__.py @@ -56,6 +56,7 @@ from . import gypsy # noqa: F401 from . import harp # noqa: F401 from . import headsandtails # noqa: F401 from . import hitormiss # noqa: F401 +from . import interlock # noqa: F401 from . import katzenschwanz # noqa: F401 from . import klondike # noqa: F401 from . import knockout # noqa: F401 diff --git a/pysollib/games/interlock.py b/pysollib/games/interlock.py new file mode 100644 index 00000000..aba00632 --- /dev/null +++ b/pysollib/games/interlock.py @@ -0,0 +1,227 @@ +#!/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 . +# +# ---------------------------------------------------------------------------## + +from pysollib.game import Game +from pysollib.gamedb import GI, GameInfo, registerGame +from pysollib.hint import Yukon_Hint +from pysollib.layout import Layout +from pysollib.stack import \ + AC_RowStack, \ + InitialDealTalonStack, \ + SS_FoundationStack, \ + StackWrapper, \ + WasteStack, \ + WasteTalonStack, \ + Yukon_AC_RowStack +from pysollib.util import ANY_RANK, KING + + +# ************************************************************************ +# * Interlock +# ************************************************************************ + +class Interlock_StackMethods: + STEP = (9, 9, 9, 9, 9, 9, 9, 9, 9, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10) + + def basicIsBlocked(self): + r, step = self.game.s.rows, self.STEP + i, n, mylen = self.id, 1, len(step) + while i < mylen: + i = i + step[i] + n = n + 1 + for j in range(i, i + n): + if r[j].cards: + return True + return False + + # TODO: The dropdown move logic can be done cleaner - too much duplication. + def isDropdownMove(self, other_stack): + if other_stack not in self.game.s.rows or len(self.cards) == 0 \ + or not self.cards[0].face_up: + return False + r, step = self.game.s.rows, self.STEP + i, n, mylen = self.id, 1, len(step) + while i < mylen: + i = i + step[i] + n = n + 1 + for j in range(i, i + n): + if r[j].cards: + if r[j] != other_stack: + return False + return True + + # Use this for dropdown moves, as they are an exception + # to the normal accept cards logic. + def dropdownAcceptsCards(self, cards): + # cards must be an acceptable sequence + if not self._isAcceptableSequence(cards): + return False + # [topcard + cards] must be an acceptable sequence + if (self.cards and not + self._isAcceptableSequence([self.cards[-1]] + cards)): + return False + return True + + +class Interlock_RowStack(Interlock_StackMethods, AC_RowStack): + def acceptsCards(self, from_stack, cards): + if len(self.cards) == 0 and self.id > self.STEP[0] - 1: + return False + if (self.isDropdownMove(from_stack) and + len(cards) == len(from_stack.cards)): + return self.dropdownAcceptsCards(cards) + + return AC_RowStack.acceptsCards(self, from_stack, cards) + + +class Interlock(Game): + RowStack_Class = StackWrapper(Interlock_RowStack, base_rank=KING) + Talon_Class = StackWrapper(WasteTalonStack, num_deal=1, max_rounds=-1) + + MAX_ROWS = 11 + PLAYCARDS = 13 + + TEXT = True + WASTE = True + + def createGame(self): + lay, s = Layout(self), self.s + self.setSize((max(self.MAX_ROWS, 7) * lay.XS) + lay.XM, + (2.5 * lay.YS) + (self.PLAYCARDS * lay.YOFFSET) + lay.YM) + + self.min_rows = self.MAX_ROWS - 2 + gap = max(7, self.MAX_ROWS) - self.min_rows + # create stacks + for i in range(3): + x = lay.XM + (gap - i) * lay.XS // 2 + y = lay.YM + lay.TEXT_HEIGHT + lay.YS + i * lay.YS // 4 + for j in range(i + self.min_rows): + s.rows.append(self.RowStack_Class(x, y, self)) + x = x + lay.XS + + x, y = lay.XM, lay.YM + s.talon = self.Talon_Class(x, y, self) + if self.TEXT: + lay.createText(s.talon, "s") + if self.WASTE: + x += lay.XS + s.waste = WasteStack(x, y, self) + lay.createText(s.waste, "s") + x += lay.XS * max(1, (self.MAX_ROWS - 6) // 2) + for i in range(4): + x += lay.XS + s.foundations.append(SS_FoundationStack(x, y, self, i, + mod=13, max_move=0)) + + lay.defaultStackGroups() + + def startGame(self): + self.startDealSample() + self.s.talon.dealRow(rows=self.s.rows[:19], flip=1, frames=0) + self.s.talon.dealRow(rows=self.s.rows[19:]) + self.s.talon.dealCards() # deal first card to WasteStack + + 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: + # Interlock special: do not consider stacks + # outside the bottom row that have been emptied. + if len(stack.cards) == 0 and stack in self.s.rows[self.min_rows:]: + continue + dist = (stack.x - cx)**2 + (stack.y - cy)**2 + if dist < cdist: + closest, cdist = stack, dist + return closest + + +# ************************************************************************ +# * Love A Duck +# ************************************************************************ + +class LoveADuck_RowStack(Interlock_StackMethods, Yukon_AC_RowStack): + def acceptsCards(self, from_stack, cards): + if len(self.cards) == 0 and self.id > self.STEP[0] - 1: + return False + if (self.isDropdownMove(from_stack) and + len(cards) == len(from_stack.cards)): + return self.dropdownAcceptsCards(cards) + + return Yukon_AC_RowStack.acceptsCards(self, from_stack, cards) + + def dropdownAcceptsCards(self, cards): + if self.cards and not self._isYukonSequence(self.cards[-1], cards[0]): + return False + return True + + +class LoveADuck(Interlock): + RowStack_Class = StackWrapper(LoveADuck_RowStack, base_rank=KING) + Talon_Class = InitialDealTalonStack + Waste_Class = None + Hint_Class = Yukon_Hint + + PLAYCARDS = 25 + + TEXT = False + WASTE = False + + def startGame(self): + self.startDealSample() + self.s.talon.dealRow(rows=self.s.rows[:19], flip=1, frames=0) + for i in range(2): + self.s.talon.dealRow(rows=self.s.rows[19:], flip=1, frames=0) + self.s.talon.dealRow(rows=self.s.rows[19:]) + self.s.talon.dealCards() # deal first card to WasteStack + + +# ************************************************************************ +# * Guardian +# ************************************************************************ + +class Guardian_RowStack(Interlock_RowStack): + STEP = (3, 3, 3, 4, 4, 4, 4) + + +class Guardian(Interlock): + RowStack_Class = StackWrapper(Guardian_RowStack, base_rank=ANY_RANK) + Talon_Class = StackWrapper(WasteTalonStack, num_deal=3, max_rounds=-1) + + MAX_ROWS = 5 + + def startGame(self): + self.startDealSample() + self.s.talon.dealRow(rows=self.s.rows[:7], flip=0, frames=0) + self.s.talon.dealRow(rows=self.s.rows[7:]) + self.s.talon.dealCards() # deal first card to WasteStack + + +# register the game +registerGame(GameInfo(852, Guardian, "Guardian", + GI.GT_KLONDIKE, 1, -1, GI.SL_BALANCED)) +registerGame(GameInfo(938, Interlock, "Interlock", + GI.GT_KLONDIKE | GI.GT_ORIGINAL, 1, -1, GI.SL_BALANCED)) +registerGame(GameInfo(939, LoveADuck, "Love a Duck", + GI.GT_YUKON | GI.GT_OPEN, 1, 0, GI.SL_BALANCED)) diff --git a/pysollib/games/klondike.py b/pysollib/games/klondike.py index 7ed8a2a2..e184b7ce 100644 --- a/pysollib/games/klondike.py +++ b/pysollib/games/klondike.py @@ -1555,66 +1555,6 @@ class EightSages(Klondike): self.s.talon.dealCards() -# ************************************************************************ -# * Guardian -# ************************************************************************ - -class Guardian_RowStack(AC_RowStack): - STEP = (3, 3, 3, 4, 4, 4, 4) - - def basicIsBlocked(self): - r, step = self.game.s.rows, self.STEP - i, n, mylen = self.id, 1, len(step) - while i < mylen: - i = i + step[i] - n = n + 1 - for j in range(i, i + n): - if r[j].cards: - return True - return False - - def acceptsCards(self, from_stack, cards): - if len(self.cards) == 0 and self.id > 2: - return False - return AC_RowStack.acceptsCards(self, from_stack, cards) - - -class Guardian(Game): - - def createGame(self): - lay, s = Layout(self), self.s - self.setSize((7 * lay.XS) + lay.XM, - (2.5 * lay.YS) + (13 * lay.YOFFSET) + lay.YM) - - # create stacks - for i in range(3): - x = lay.XM + (4 - i) * lay.XS // 2 - y = lay.YM + lay.TEXT_HEIGHT + lay.YS + i * lay.YS // 4 - for j in range(i + 3): - s.rows.append(Guardian_RowStack(x, y, self)) - x = x + lay.XS - - x, y = lay.XM, lay.YM - s.talon = WasteTalonStack(x, y, self, - max_rounds=-1, num_deal=3) - lay.createText(s.talon, "s") - x += lay.XS - s.waste = WasteStack(x, y, self) - lay.createText(s.waste, "s") - x += lay.XS - for i in range(4): - x += lay.XS - s.foundations.append(SS_FoundationStack(x, y, self, i, - mod=13, max_move=0)) - lay.defaultStackGroups() - - def startGame(self): - self.startDealSample() - self.s.talon.dealRow(rows=self.s.rows[:7], flip=0) - self.s.talon.dealRow(rows=self.s.rows[7:]) - self.s.talon.dealCards() # deal first card to WasteStack - - # register the game registerGame(GameInfo(2, Klondike, "Klondike", GI.GT_KLONDIKE, 1, -1, GI.SL_BALANCED, @@ -1770,8 +1710,6 @@ registerGame(GameInfo(821, Trigon, "Trigon", registerGame(GameInfo(849, RelaxedRaglan, "Relaxed Raglan", GI.GT_RAGLAN | GI.GT_RELAXED | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL)) -registerGame(GameInfo(852, Guardian, "Guardian", - GI.GT_KLONDIKE, 1, -1, GI.SL_BALANCED)) registerGame(GameInfo(855, HalfKlondike, "Half Klondike", GI.GT_KLONDIKE | GI.GT_STRIPPED, 1, -1, GI.SL_BALANCED, suits=(1, 2)))