1
0
Fork 0
mirror of https://github.com/shlomif/PySolFC.git synced 2025-04-05 00:02:29 -04:00

Added Interlock and Love a Duck games.

This commit is contained in:
Joe R 2023-12-17 21:34:10 -05:00
parent 7bc56fc101
commit dda8348806
6 changed files with 295 additions and 64 deletions

View file

@ -0,0 +1,35 @@
<h1>Interlock</h1>
<p>
Klondike type. 1 deck. Unlimited redeals.
<h3>Object</h3>
<p>
Move all cards to the foundations.
<h3>Rules</h3>
<p>
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.
<p>
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.
<p>
Foundations are built up in suit from Ace to King.
<p>
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.
<p>
The game is won when all cards are moved to the foundations.
<h3>Notes</h3>
<p>
Interlock is an original game that's a more straightforward variant
of other interlocking tableau games, such as
<a href="guardian.html">Guardian</a> or
<a href="loveaduck.html">Love a Duck</a>.

View file

@ -0,0 +1,30 @@
<h1>Love a Duck</h1>
<p>
Yukon type. 1 deck. No redeal.
<h3>Object</h3>
<p>
Move all cards to the foundations.
<h3>Rules</h3>
<p>
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.
<p>
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.
<p>
Foundations are built up in suit from Ace to King. The game is won when
all cards are moved to the foundations.
<h3>Notes</h3>
<p>
Love a Duck was invented by Jan Wolter, and first appeared in his app
<a href="https://politaire.com">Politaire</a>. The name came from
"Lord Love a Duck", an expression of surprise from early 1900s London.

View file

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

View file

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

227
pysollib/games/interlock.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
#
# ---------------------------------------------------------------------------##
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))

View file

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