Playing Cards with Snakes

Another random discussion in our regular coding dojo led me to think about representing individual playing cards and decks of cards in python. Playing cards with Snakes rather than Sharks, if you will :)

Card

Here's a card class I came up with to model the suit and rank of an instance along with string conversion and comparison operators - which seem to make sense for cards so that they can be sorted or we can tell if they aren't in order.



class Card(object):

    suit_names = ["Clubs", "Hearts", "Spades", "Diamonds"]
    rank_names = ["Ace", "2", "3", "4", "5", "6", "7",
              "8", "9", "10", "Jack", "Queen", "King"]

    def __init__(self, suit=0, rank=2):
        self.suit = suit
        self.rank = rank

    def __str__(self):
        return '%s of %s' % (Card.rank_names[self.rank],
                             Card.suit_names[self.suit])

    def __cmp__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return cmp(t1, t2)

    def __lt__(self, other):
        if self.suit < other.suit:
            return True
        elif self.suit > other.suit:
            return False
        else:
            return self.rank < other.rank

Deck

Next we need to collect the cards into a deck so we can order, shuffle, add and remove cards from the collection.


import random

class Deck(object):

    def __init__(self):
        self.cards = []
        for suit in range(len(Card.suit_names)):
            for rank in range(len(Card.rank_names)):
                card = Card(suit, rank)
                self.cards.append(card)

    def __str__(self):
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)

    def add_card(self, card):
        self.cards.append(card)

    # take card from the deck - default to last.
    def pop_card(self, i=-1):
        return self.cards.pop(i)

    def shuffle(self):
        random.shuffle(self.cards)

    def sort(self):
        self.cards.sort()


if __name__ == '__main__':
    deck = Deck()
    print(deck)
    deck.shuffle()
    print(deck)

Magic

In the dojo we talked about card tricks and ways in which magician's use maths and probability in their favour to give the illusion that cards are randomly arranged but they can predict a card you chose.

One example of this is I had never heard of before is "Si Stebbins Order". This is a way of making the cards look like they are shuffled in random order to the casual observer but follow a repeatable pattern. A magician can ask you to select a card "at random" and if they can get a glimpse of the card before or after it in the order, they can use a simple pattern to work out exactly what the card will be.

See this link for a booklet on magic tricks using this system.

Here's the ordering of the cards for this system.

booklet page 1 booklet page 2

One of the magical properties of this ordering is that the pattern rolls over at the edges so if you can see the last card in the deck, you can know what the first card is. Cutting the cards also preserves the pattern so we can do some really cool tricks just with this simple ordering.

Cutting

For our deck of cards we want to be able to cut the cards into two stacks and place the bottom stack onto the top stack.



class Deck(object):

    def cut(self, index):

        if index <= 0 or index > len(self.cards):
            return

        top = []

        for card in range(0, index): top.append(self.cards[card])

        bottom = []
        for card in range(index, len(self.cards)): bottom.append(self.cards[card])

        self.cards = bottom + top


if __name__ == '__main__':
    deck = Deck()
    print(deck)
    deck.cut(2)
    print(deck)

Si Stebbins Order

Now if we implement the Si Stebbins ordering algorithm, we can cut the cards and demonstrate that the ordering still works. Magic!



class Deck(object):

    def si_stebbins_order(self):
        self.cards = []

        suits = len(Card.suit_names)
        cards = len(Card.rank_names)

        number = 5
        suit = 1

        for i in range(cards * suits):
            card = Card(suit, number)
            print(card)
            self.cards.append(card)
            suit += 1
            if suit >= suits:
                suit = 0
            number += 3
            if number >= cards:
                number = number - cards


if __name__ == '__main__':
    deck = Deck()
    print(deck)
    deck.si_stebbins_order()
    print(deck)
    deck.cut(2)
    print(deck)