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)))