#!/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/>.
#
# ---------------------------------------------------------------------------##

# Imports
import math

# PySol imports
from pysollib.mygettext import _
from pysollib.gamedb import registerGame, GameInfo, GI
from pysollib.mfxutil import kwdefault
from pysollib.game import Game
from pysollib.layout import Layout
from pysollib.hint import DefaultHint, CautiousDefaultHint
from pysollib.pysoltk import MfxCanvasText

from pysollib.util import ANY_RANK, ANY_SUIT, NO_RANK, UNLIMITED_ACCEPTS, \
        UNLIMITED_MOVES

from pysollib.stack import \
        AC_RowStack, \
        AbstractFoundationStack, \
        InitialDealTalonStack, \
        ReserveStack, \
        SS_FoundationStack, \
        StackWrapper, \
        WasteStack, \
        WasteTalonStack, \
        OpenStack


# ************************************************************************
# * Hex A Deck Foundation Stacks
# ************************************************************************

class HexADeck_FoundationStack(SS_FoundationStack):
    def __init__(self, x, y, game, suit, **cap):
        kwdefault(cap, max_move=0, max_cards=12)
        SS_FoundationStack.__init__(self, x, y, game, suit, **cap)


class HexATrump_Foundation(HexADeck_FoundationStack):
    def acceptsCards(self, from_stack, cards):
        if not self.basicAcceptsCards(from_stack, cards):
            return 0
        for s in self.game.s.foundations[:3]:
            if len(s.cards) != 16:
                return 0
        return 1


class Merlins_Foundation(AbstractFoundationStack):
    def __init__(self, x, y, game, suit, **cap):
        kwdefault(cap, mod=16, dir=0, base_rank=NO_RANK, max_move=0)
        AbstractFoundationStack.__init__(self, x, y, game, suit, **cap)

    def acceptsCards(self, from_stack, cards):
        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
            return 0
        if not self.cards:
            return 1
        stack_dir = self.game.getFoundationDir()
        if stack_dir == 0:
            card_dir = (cards[0].rank - self.cards[-1].rank) % self.cap.mod
            return card_dir in (1, 15)
        else:
            return (self.cards[-1].rank + stack_dir) % self.cap.mod \
                == cards[0].rank


# ************************************************************************
# * Hex A Deck Row Stacks
# ************************************************************************

class HexADeck_OpenStack(OpenStack):

    def __init__(self, x, y, game, yoffset, **cap):
        kwdefault(cap, max_move=UNLIMITED_MOVES, max_accept=UNLIMITED_ACCEPTS,
                  dir=-1)
        OpenStack.__init__(self, x, y, game, **cap)
        self.CARD_YOFFSET = yoffset

    def isRankSequence(self, cards, dir=None):
        if not dir:
            dir = self.cap.dir
        c1 = cards[0]
        for c2 in cards[1:]:
            if not c1.rank + dir == c2.rank:
                return 0
            c1 = c2
        return 1

    def isAlternateColorSequence(self, cards, dir=None):
        if not dir:
            dir = self.cap.dir
        c1 = cards[0]
        for c2 in cards[1:]:
            if (c1.color < 2 and c1.color == c2.color or
                    not c1.rank + dir == c2.rank):
                return 0
            c1 = c2
        return 1

    def isSuitSequence(self, cards, dir=None):
        if not dir:
            dir = self.cap.dir
        c1 = cards[0]
        for c2 in cards[1:]:
            if not ((c1.color == 2 or c2.color == 2 or c1.suit == c2.suit) and
                    c1.rank + dir == c2.rank):
                return 0
            c1 = c2
        return 1


class HexADeck_RK_RowStack(HexADeck_OpenStack):

    def acceptsCards(self, from_stack, cards):
        if (not self.basicAcceptsCards(from_stack, cards) or
                not self.isRankSequence(cards)):
            return 0
        if not self.cards:
            return cards[0].rank == 15 or self.cap.base_rank == ANY_RANK
        return self.isRankSequence([self.cards[-1], cards[0]])

    def canMoveCards(self, cards):
        return (self.basicCanMoveCards(cards) and self.isRankSequence(cards))


class HexADeck_AC_RowStack(HexADeck_OpenStack):

    def acceptsCards(self, from_stack, cards):
        if (not self.basicAcceptsCards(from_stack, cards) or
                not self.isAlternateColorSequence(cards)):
            return 0
        if not self.cards:
            return cards[0].rank == 15 or self.cap.base_rank == ANY_RANK
        return self.isAlternateColorSequence([self.cards[-1], cards[0]])

    def canMoveCards(self, cards):
        return (self.basicCanMoveCards(cards) and
                self.isAlternateColorSequence(cards))


class HexADeck_SS_RowStack(HexADeck_OpenStack):

    def acceptsCards(self, from_stack, cards):
        if (not self.basicAcceptsCards(from_stack, cards) or
                not self.isSuitSequence(cards)):
            return 0
        if not self.cards:
            return cards[0].rank == 15 or self.cap.base_rank == ANY_RANK
        return self.isSuitSequence([self.cards[-1], cards[0]])

    def canMoveCards(self, cards):
        return (self.basicCanMoveCards(cards) and self.isSuitSequence(cards))


class Bits_RowStack(ReserveStack):

    def acceptsCards(self, from_stack, cards):
        if not self.basicAcceptsCards(from_stack, cards):
            return 0
        stackcards = self.cards
        if stackcards or cards[0].suit == 4:
            return 0
        i = int(self.id // 4)
        for r in self.game.s.rows[i * 4:self.id]:
            if not r.cards:
                return 0
        return ((self.game.s.foundations[i].cards[-1].rank + 1 >>
                 (self.id % 4)) % 2 == (cards[0].rank + 1) % 2)


class Bytes_RowStack(ReserveStack):
    def acceptsCards(self, from_stack, cards):
        if not self.basicAcceptsCards(from_stack, cards):
            return 0
        stackcards = self.cards
        if stackcards or cards[0].suit == 4:
            return 0
        id = self.id - 16
        i = int(id // 2)
        for r in self.game.s.rows[16 + i * 2:self.id]:
            if not r.cards:
                return 0
        return self.game.s.foundations[i].cards[-1].rank == cards[0].rank


class HexAKlon_RowStack(AC_RowStack):
    def acceptsCards(self, from_stack, cards):
        if not self.basicAcceptsCards(from_stack, cards):
            return 0
        stackcards = self.cards
        if stackcards:
            if (stackcards[-1].suit == 4 or cards[0].suit == 4):
                return 1
        return AC_RowStack.acceptsCards(self, from_stack, cards)


class HexADeck_ACRowStack(AC_RowStack):
    def acceptsCards(self, from_stack, cards):
        if not self.basicAcceptsCards(from_stack, cards):
            return 0
        stackcards = self.cards
        if stackcards:
            if (stackcards[-1].suit == 4 or cards[0].suit == 4):
                return stackcards[-1].rank == cards[0].rank + 1
        return AC_RowStack.acceptsCards(self, from_stack, cards)


class Familiar_ReserveStack(ReserveStack):
    def acceptsCards(self, from_stack, cards):
        if not ReserveStack.acceptsCards(self, from_stack, cards):
            return 0
        # Only take Wizards
        return cards[0].suit == 4

    def getBottomImage(self):
        return self.game.app.images.getSuitBottom(4)


class Merlins_BraidStack(OpenStack):
    def __init__(self, x, y, game):
        OpenStack.__init__(self, x, y, game)
        self.CARD_YOFFSET = self.game.app.images.CARD_YOFFSET
        # use a sine wave for the x offsets
        self.CARD_XOFFSET = []
        j = 1
        for i in range(20):
            self.CARD_XOFFSET.append(int(math.sin(j) * 20))
            j = j + .9


class Merlins_RowStack(ReserveStack):
    def fillStack(self):
        if not self.cards and self.game.s.braid.cards:
            self.game.moveMove(1, self.game.s.braid, self)

    def getBottomImage(self):
        return self.game.app.images.getBraidBottom()


class Merlins_ReserveStack(ReserveStack):
    def acceptsCards(self, from_stack, cards):
        if from_stack is self.game.s.braid or from_stack in self.game.s.rows:
            return 0
        return ReserveStack.acceptsCards(self, from_stack, cards)

    def getBottomImage(self):
        return self.game.app.images.getTalonBottom()


# ************************************************************************
# *
# ************************************************************************

class AbstractHexADeckGame(Game):
    RANKS = (_("Ace"), "2", "3", "4", "5", "6", "7", "8", "9",
             "A", "B", "C", "D", "E", "F", "10")


class Merlins_Hint(DefaultHint):
    pass


# ************************************************************************
# * Bits n Bytes
# ************************************************************************

class BitsNBytes(Game):

    #
    # Game layout
    #

    def createGame(self):
        l, s = Layout(self), self.s
        font = self.app.getFont("canvas_default")

        # Set window size
        self.setSize(l.XM * 4 + l.XS * 8, l.YM + l.YS * 4)

        # Create bit stacks
        self.bit_texts = []
        y = l.YM
        for j in range(4):
            x = l.XM * 4 + l.XS * 7
            for i in range(4):
                stack = Bits_RowStack(x, y, self, max_cards=1, max_accept=1,
                                      base_suit=j, max_move=0)
                s.rows.append(stack)
                stack.texts.misc = MfxCanvasText(self.canvas,
                                                 x + l.CW // 2, y + l.CH // 2,
                                                 anchor="center", font=font)
                x = x - l.XS
            y = y + l.YS

        # Create byte stacks
        y = l.YM
        for j in range(4):
            x = l.XM * 3 + l.XS * 3
            for i in range(2):
                s.rows.append(
                    Bytes_RowStack(
                        x, y, self, max_cards=1,
                        max_accept=1, base_suit=ANY_SUIT, max_move=0))
                x = x - l.XS
            y = y + l.YS

        # Create foundations
        x = l.XM * 2 + l.XS
        y = l.YM
        for i in range(4):
            s.foundations.append(
                SS_FoundationStack(
                    x, y, self, i, mod=1,
                    max_move=0, max_cards=1))
            y = y + l.YS
        self.setRegion(s.rows, (0, 0, 999999, 999999))

        # Create talon
        x = l.XM
        y = l.YM
        s.talon = WasteTalonStack(x, y, self, num_deal=2, max_rounds=2)
        l.createText(s.talon, "s")
        y += l.YS + l.TEXT_HEIGHT
        s.waste = WasteStack(x, y, self)
        l.createText(s.waste, "s")

        # Define stack groups
        l.defaultStackGroups()

    #
    # Game over rides
    #

    def updateText(self):
        if self.preview > 1:
            return
        for j in range(4):
            if not len(self.s.foundations[j].cards):
                break
            s = self.s.foundations[j].cards[-1].rank + 1
            for i in range(4):
                stack = self.s.rows[i + j * 4]
                stack.texts.misc.config(text=str(s % 2))
                s = int(s // 2)

    def _shuffleHook(self, cards):
        topcards, ranks = [None] * 4, [None] * 4
        for c in cards[:]:
            if not c.suit == 4:
                if not topcards[c.suit]:
                    haverank = 0
                    for i in range(4):
                        if c.rank == ranks[i]:
                            haverank = 1
                    if not haverank:
                        topcards[c.suit] = c
                        ranks[c.suit] = c.rank
                        cards.remove(c)
        cards = topcards + cards
        cards.reverse()
        return cards

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        self.startDealSample()
        self.s.talon.dealRow(rows=self.s.foundations)
        self.s.talon.dealCards()

    def isGameWon(self):
        for s in self.s.rows:
            if not s.cards:
                return 0
        return 1

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return 0


# ************************************************************************
# * Hex A Klon
# ************************************************************************

class HexAKlon(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = HexAKlon_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=-1, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=8, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(
            l.s.talon.x, l.s.talon.y, self,
            max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations[:4]:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))
        r = l.s.foundations[4]
        s.foundations.append(
            HexATrump_Foundation(
                r.x, r.y, self, 4, mod=4,
                max_move=0, max_cards=4, base_rank=ANY_RANK))

        # Create rows
        for r in l.s.rows:
            s.rows.append(
                self.RowStack_Class(
                    r.x, r.y, self,
                    suit=ANY_SUIT, base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        for i in range(len(self.s.rows)):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Hex A Klon by Threes
# ************************************************************************

class HexAKlonByThrees(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = HexAKlon_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=-1, num_deal=3, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=8, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(
            l.s.talon.x, l.s.talon.y, self,
            max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations[:4]:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))
        r = l.s.foundations[4]
        s.foundations.append(
            HexATrump_Foundation(
                r.x, r.y, self, 4, mod=4,
                max_move=0, max_cards=4, base_rank=ANY_RANK))

        # Create rows
        for r in l.s.rows:
            s.rows.append(
                self.RowStack_Class(
                    r.x, r.y, self,
                    suit=ANY_SUIT, base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        for i in range(len(self.s.rows)):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * King Only Hex A Klon
# ************************************************************************

class KingOnlyHexAKlon(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = HexAKlon_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=-1, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=8, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations[:4]:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))
        r = l.s.foundations[4]
        s.foundations.append(
            HexATrump_Foundation(
                r.x, r.y, self, 4, mod=4,
                max_move=0, max_cards=4, base_rank=ANY_RANK))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT, base_rank=15))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        for i in range(len(self.s.rows)):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def _shuffleHook(self, cards):
        basecard = [None]
        for c in cards[:]:
            if c.suit == 4:
                if basecard[0] is None:
                    basecard[0] = c
                    cards.remove(c)
        cards = basecard + cards
        cards.reverse()
        return cards

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Klondike Plus 16
# ************************************************************************

class KlondikePlus16(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = HexAKlon_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=2, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=8, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT, base_rank=15))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        for i in range(len(self.s.rows)):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * The Familiar
# ************************************************************************

class TheFamiliar(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = AC_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=2, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=8, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT, base_rank=15))

        # Create reserve
        x, y = l.XM, self.height - l.YS
        s.reserves.append(Familiar_ReserveStack(x, y, self, max_cards=3))
        self.setRegion(
            s.reserves, (-999, y - l.YM, x + l.XS, 999999), priority=1)
        l.createText(s.reserves[0], "se")

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        for i in range(len(self.s.rows)):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Two Familiars
# ************************************************************************

class TwoFamiliars(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = AC_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=2, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=12, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT, base_rank=15))

        # Create reserve
        x, y = l.XM, self.height - l.YS
        s.reserves.append(Familiar_ReserveStack(x, y, self, max_cards=3))
        self.setRegion(
            s.reserves, (-999, y - l.YM, x + l.XS, 999999), priority=1)
        l.createText(s.reserves[0], "se")

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68 * 2
        for i in range(len(self.s.rows)):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Ten by Eight
# ************************************************************************

class TenByEight(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.gypsyLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = AC_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=-1, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=10, waste=1, playcards=30)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT,
                                              base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68 * 2
        frames = 0
        for i in range(8):
            if i == 5:
                frames = -1
                self.startDealSample()
            self.s.talon.dealRow(frames=frames)
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Drawbridge
# ************************************************************************

class Drawbridge(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.harpLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = AC_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=2, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=7, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT,
                                              base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        for i in range(len(self.s.rows) - 1):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Double Drawbridge
# ************************************************************************

class DoubleDrawbridge(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.harpLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = AC_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=2, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=10, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))

        # Create rows
        for r in l.s.rows:
            s.rows.append(
                self.RowStack_Class(
                    r.x, r.y, self,
                    suit=ANY_SUIT, base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68 * 2
        for i in range(len(self.s.rows) - 1):
            self.s.talon.dealRow(rows=self.s.rows[i+1:], flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Hidden Passages
# ************************************************************************

class HiddenPassages(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = AC_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=2, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=7, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations[:4]:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))
        r = l.s.foundations[4]
        s.foundations.append(
            HexATrump_Foundation(
                r.x, r.y, self, 4, mod=4,
                max_move=0, max_cards=4, base_rank=ANY_RANK))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT,
                                              base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #
    def _shuffleHook(self, cards):
        topcards = [None] * 4
        for c in cards[:]:
            if c.rank == 0 and not c.suit == 4:
                topcards[c.suit] = c
                cards.remove(c)
        cards = topcards + cards
        cards.reverse()
        return cards

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        self.s.talon.dealRow(rows=self.s.foundations[:4], frames=0)
        for i in range(2):
            self.s.talon.dealRow(flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Cluitjar's Lair
# ************************************************************************

class CluitjarsLair(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.klondikeLayout)
    Talon_Class = WasteTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = HexADeck_ACRowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=1, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=7, waste=1, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)
        s.waste = WasteStack(l.s.waste.x, l.s.waste.y, self)

        # Create foundations
        for r in l.s.foundations[:4]:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))
        r = l.s.foundations[4]
        s.foundations.append(
            HexATrump_Foundation(
                r.x, r.y, self, 4, mod=4,
                max_move=0, max_cards=4, base_rank=ANY_RANK))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT,
                                              base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #
    def startGame(self):
        assert len(self.s.talon.cards) == 68
        for i in range(2):
            self.s.talon.dealRow(flip=0, frames=0)
        self.startDealSample()
        self.s.talon.dealRow()
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# * Merlin's Meander
# ************************************************************************

class MerlinsMeander(AbstractHexADeckGame):
    Hint_Class = Merlins_Hint
    MERLINS_CARDS = 20

    #
    # game layout
    #

    def createGame(self):
        # create layout
        l, s = Layout(self), self.s

        # set window
        # (piles up to 20 cards are playable - needed for Braid_BraidStack)
        h = max(4*l.YS + 30, l.YS+(self.MERLINS_CARDS-1)*l.YOFFSET)
        self.setSize(10*l.XS+l.XM, l.YM + h)

        # extra settings
        self.base_card = None

        # Create rows, reserves
        s.addattr(braid=None)      # register extra stack variable
        x, y = l.XM, l.YM
        for i in range(2):
            s.rows.append(Merlins_RowStack(x + l.XS * 0.5, y, self))
            s.rows.append(Merlins_RowStack(x + l.XS * 4.5, y, self))
            s.reserves.append(
                Familiar_ReserveStack(x + l.XS * 6.5, y, self, max_cards=3))
            y = y + l.YS * 3
        y = l.YM + l.YS
        for i in range(2):
            s.rows.append(Merlins_ReserveStack(x, y, self))
            s.rows.append(Merlins_ReserveStack(x + l.XS, y, self))
            s.rows.append(Merlins_ReserveStack(x, y + l.YS, self))
            s.rows.append(Merlins_ReserveStack(x + l.XS, y + l.YS, self))
            x = x + l.XS * 4

        # Create braid
        x, y = l.XM + l.XS * 2.2, l.YM
        s.braid = Merlins_BraidStack(x, y, self)

        # Create talon, waste
        x, y = l.XM + l.XS * 7, l.YM + l.YS * 1.5
        s.talon = WasteTalonStack(x, y, self, max_rounds=3)
        l.createText(s.talon, "s")
        s.talon.texts.rounds = MfxCanvasText(
            self.canvas,
            x + l.CW // 2, y - l.YM,
            anchor="s",
            font=self.app.getFont("canvas_default"))
        x -= l.XS
        s.waste = WasteStack(x, y, self)
        l.createText(s.waste, "s")

        # Create foundations
        x, y = l.XM + l.XS * 8, l.YM
        for i in range(4):
            s.foundations.append(
                Merlins_Foundation(
                    x, y, self, i, mod=16,
                    max_cards=16, base_rank=ANY_RANK))
            s.foundations.append(
                Merlins_Foundation(
                    x + l.XS, y, self, i, mod=16,
                    max_cards=16, base_rank=ANY_RANK))
            y = y + l.YS
        self.texts.info = MfxCanvasText(
            self.canvas,
            x + l.CW + l.XM // 2, y,
            anchor="n",
            font=self.app.getFont("canvas_default"))

        # define stack-groups
        self.sg.talonstacks = [s.talon] + [s.waste]
        self.sg.openstacks = s.foundations + s.rows + s.reserves
        self.sg.dropstacks = [s.braid] + s.rows + [s.waste] + s.reserves

    #
    # game overrides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68 * 2
        self.startDealSample()
        self.base_card = None
        self.updateText()
        for i in range(self.MERLINS_CARDS):
            self.s.talon.dealRow(rows=[self.s.braid])
        self.s.talon.dealRow()
        # deal base_card to foundations, update cap.base_rank
        self.base_card = self.s.talon.getCard()
        while self.base_card.suit == 4:
            self.s.talon.cards.remove(self.base_card)
            self.s.talon.cards.insert(0, self.base_card)
            self.base_card = self.s.talon.getCard()
        to_stack = self.s.foundations[2 * self.base_card.suit]
        self.flipMove(self.s.talon)
        self.moveMove(1, self.s.talon, to_stack)
        self.updateText()
        for s in self.s.foundations:
            s.cap.base_rank = self.base_card.rank
        # deal first card to WasteStack
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.suit == card2.suit and
                ((card1.rank + 1) % 16 == card2.rank or
                 (card2.rank + 1) % 16 == card1.rank))

    def getHighlightPilesStacks(self):
        return ()

    def _restoreGameHook(self, game):
        self.base_card = self.cards[game.loadinfo.base_card_id]
        for s in self.s.foundations:
            s.cap.base_rank = self.base_card.rank

    def _loadGameHook(self, p):
        self.loadinfo.addattr(base_card_id=None)    # register extra load var.
        self.loadinfo.base_card_id = p.load()

    def _saveGameHook(self, p):
        p.dump(self.base_card.id)

    #
    # game extras
    #

    def updateText(self):
        if self.preview > 1 or not self.texts.info:
            return
        if not self.base_card:
            t = ""
        else:
            t = self.RANKS[self.base_card.rank]
            dir = self.getFoundationDir() % 16
            if dir == 1:
                t = t + _(" Ascending")
            elif dir == 15:
                t = t + _(" Descending")
        self.texts.info.config(text=t)

    def isGameWon(self):
        for s in self.s.rows:
            if s.cards and s.cards[0].suit != 4:
                return 0
        if not len(self.s.talon.cards) and len(self.s.waste.cards) == 1:
            return self.s.waste.cards[0].suit == 4
        return len(self.s.talon.cards) + len(self.s.waste.cards) == 0


# ************************************************************************
# * Mage's Game
# ************************************************************************

class MagesGame(Game):
    Hint_Class = CautiousDefaultHint
    Layout_Method = staticmethod(Layout.gypsyLayout)
    Talon_Class = InitialDealTalonStack
    Foundation_Class = SS_FoundationStack
    RowStack_Class = AC_RowStack

    #
    # Game layout
    #

    def createGame(self, max_rounds=1, num_deal=1, **layout):
        l, s = Layout(self), self.s
        kwdefault(layout, rows=12, texts=0, playcards=20)
        self.Layout_Method(l, **layout)
        self.setSize(l.size[0], l.size[1])

        # Create talon
        s.talon = self.Talon_Class(l.s.talon.x, l.s.talon.y, self,
                                   max_rounds=max_rounds, num_deal=num_deal)

        # Create foundations
        for r in l.s.foundations:
            s.foundations.append(
                self.Foundation_Class(
                    r.x, r.y, self,
                    r.suit, mod=16, max_cards=16, max_move=1))

        # Create rows
        for r in l.s.rows:
            s.rows.append(self.RowStack_Class(r.x, r.y, self,
                                              suit=ANY_SUIT,
                                              base_rank=ANY_RANK))

        # Define stack groups
        l.defaultAll()

    #
    # Game over rides
    #

    def startGame(self):
        assert len(self.s.talon.cards) == 68
        self._startDealNumRows(4)
        self.s.talon.dealRow()
        self.s.talon.dealRow(rows=self.s.rows[2:10])
        self.s.talon.dealCards()

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        return (card1.color != card2.color and
                (card1.rank + 1 == card2.rank or card2.rank + 1 == card1.rank))


# ************************************************************************
# *
# ************************************************************************

class Convolution(AbstractHexADeckGame):
    RowStack_Class = StackWrapper(HexADeck_RK_RowStack, base_rank=NO_RANK)

    #
    # game layout
    #

    def createGame(self, rows=9, reserves=8):
        # create layout
        l, s = Layout(self), self.s

        # set size
        maxrows = max(rows, reserves)
        self.setSize(l.XM + (maxrows + 2) * l.XS, l.YM + 6 * l.YS)

        #
        playcards = 4 * l.YS // l.YOFFSET
        xoffset, yoffset = [], []
        for i in range(playcards):
            xoffset.append(0)
            yoffset.append(l.YOFFSET)
        for i in range(68 * self.gameinfo.decks - playcards):
            xoffset.append(l.XOFFSET)
            yoffset.append(0)

        # create stacks
        x, y = l.XM + (maxrows - reserves) * l.XS // 2, l.YM
        for i in range(reserves):
            s.reserves.append(ReserveStack(x, y, self))
            x = x + l.XS
        x, y = l.XM + (maxrows - rows) * l.XS // 2, l.YM + l.YS
        self.setRegion(s.reserves, (-999, -999, 999999, y - l.YM // 2))
        for i in range(rows):
            stack = self.RowStack_Class(x, y, self, yoffset=yoffset)
            stack.CARD_XOFFSET = xoffset
            stack.CARD_YOFFSET = yoffset
            s.rows.append(stack)
            x = x + l.XS
        x, y = l.XM + maxrows * l.XS, l.YM
        for i in range(2):
            for suit in range(5):
                s.foundations.append(
                    SS_FoundationStack(x, y, self, suit=suit, max_cards=16))
                y = y + l.YS
            x, y = x + l.XS, l.YM
        self.setRegion(self.s.foundations, (x - l.XS * 2, -999, 999999,
                       self.height - (l.YS + l.YM)), priority=1)
        s.talon = InitialDealTalonStack(
            self.width - 3 * l.XS // 2, self.height - l.YS, self)

        # define stack-groups
        l.defaultStackGroups()

    #
    # game overrides
    #

    def startGame(self):
        self.startDealSample()
        i = 0
        while self.s.talon.cards:
            if self.s.talon.cards[-1].rank == 15:
                if self.s.rows[i].cards:
                    i = i + 1
            self.s.talon.dealRow(rows=[self.s.rows[i]], frames=4)

    # must look at cards
    def _getClosestStack(self, cx, cy, stacks, dragstack):
        closest, cdist = None, 999999999
        for stack in stacks:
            if stack.cards and stack is not dragstack:
                dist = (stack.cards[-1].x - cx)**2 + \
                    (stack.cards[-1].y - cy)**2
            else:
                dist = (stack.x - cx)**2 + (stack.y - cy)**2
            if dist < cdist:
                closest, cdist = stack, dist
        return closest

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        row = self.s.rows[0]
        sequence = row.isRankSequence
        return (sequence([card1, card2]) or sequence([card2, card1]))


# ************************************************************************
# *
# ************************************************************************

class Labyrinth(Convolution):
    RowStack_Class = StackWrapper(HexADeck_AC_RowStack, base_rank=NO_RANK)

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        row = self.s.rows[0]
        sequence = row.isAlternateColorSequence
        return (sequence([card1, card2]) or sequence([card2, card1]))


# ************************************************************************
# *
# ************************************************************************

class Snakestone(Convolution):
    RowStack_Class = StackWrapper(HexADeck_SS_RowStack, base_rank=NO_RANK)

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        row = self.s.rows[0]
        sequence = row.isSuitSequence
        return (sequence([card1, card2]) or sequence([card2, card1]))


# ************************************************************************
# *
# ************************************************************************

def r(id, gameclass, name, game_type, decks, redeals, skill_level):
    game_type = game_type | GI.GT_HEXADECK
    gi = GameInfo(id, gameclass, name, game_type, decks, redeals, skill_level,
                  suits=list(range(4)), ranks=list(range(16)),
                  trumps=list(range(4)))
    registerGame(gi)
    return gi


r(165, BitsNBytes, 'Bits n Bytes', GI.GT_HEXADECK, 1, 1, GI.SL_BALANCED)
r(166, HexAKlon, 'Hex A Klon', GI.GT_HEXADECK, 1, -1, GI.SL_BALANCED)
r(16666, KlondikePlus16, 'Klondike Plus 16', GI.GT_HEXADECK, 1, 1,
  GI.SL_BALANCED)
r(16667, HexAKlonByThrees, 'Hex A Klon by Threes', GI.GT_HEXADECK, 1, -1,
  GI.SL_BALANCED)
r(16668, KingOnlyHexAKlon, 'King Only Hex A Klon', GI.GT_HEXADECK, 1, -1,
  GI.SL_BALANCED)
r(16669, TheFamiliar, 'The Familiar', GI.GT_HEXADECK, 1, 1, GI.SL_BALANCED)
r(16670, TwoFamiliars, 'Two Familiars', GI.GT_HEXADECK, 2, 1, GI.SL_BALANCED)
r(16671, TenByEight, '10 x 8', GI.GT_HEXADECK, 2, -1, GI.SL_BALANCED)
r(16672, Drawbridge, 'Drawbridge', GI.GT_HEXADECK, 1, 1, GI.SL_BALANCED)
r(16673, DoubleDrawbridge, 'Double Drawbridge', GI.GT_HEXADECK, 2, 1,
  GI.SL_BALANCED)
r(16674, HiddenPassages, 'Hidden Passages', GI.GT_HEXADECK, 1, 1,
  GI.SL_MOSTLY_LUCK)
r(16675, CluitjarsLair, 'Cluitjar\'s Lair', GI.GT_HEXADECK, 1, 0,
  GI.SL_BALANCED)
r(16676, MerlinsMeander, 'Merlin\'s Meander', GI.GT_HEXADECK, 2, 2,
  GI.SL_BALANCED)
r(16677, MagesGame, 'Mage\'s Game', GI.GT_HEXADECK, 1, 0, GI.SL_BALANCED)
r(16678, Convolution, 'Convolution', GI.GT_HEXADECK, 2, 0, GI.SL_MOSTLY_SKILL)
r(16679, Labyrinth, 'Hex Labyrinth', GI.GT_HEXADECK, 2, 0, GI.SL_MOSTLY_SKILL)
r(16680, Snakestone, 'Snakestone', GI.GT_HEXADECK, 2, 0, GI.SL_MOSTLY_SKILL)

del r
