pcards — Playing Cards Library

__init__.py

"""Playing cards package.

Library Release 1.1

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/

"""

from pcards.card import Card, CardArgumentError, rank_string, suit_string
from pcards.card import get_rank_integer, get_suit_integer
from pcards.card import CLUBS, HEARTS, SPADES, DIAMONDS
from pcards.card import ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN
from pcards.card import EIGHT, NINE, TEN, JACK, QUEEN, KING
from pcards.deck import Deck, EmptyDeckError
from pcards.hand import Hand, NoAssociatedDeckError
from pcards.pokerhand import PokerHand

card.py

"""Playing card module.

Library Release 1.1

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/

"""


from __future__ import division


# Public constants

ACE = 1
TWO = 2
THREE = 3
FOUR = 4
FIVE = 5
SIX = 6
SEVEN = 7
EIGHT = 8
NINE = 9
TEN = 10
JACK = 11
QUEEN = 12
KING = 13

CLUBS = 0
HEARTS = 1
SPADES = 2
DIAMONDS = 3

# Non-public constants

_ALLOWABLE_RANK_STRINGS = {
    "ace": 14, "two": 2, "three": 3, "four": 4, "five": 5,
    "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
    "jack": 11, "queen": 12, "king": 13,
    "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7,
    "8": 8, "9": 9, "10": 10
}
_ALLOWABLE_SUIT_STRINGS = {
    "clubs": 0, "hearts": 1, "spades": 2, "diamonds": 3
}
_RANK_SHORT_STRINGS = {
    1: "A", 2: "2", 3: "3", 4: "4", 5: "5", 6: "6",
    7: "7", 8: "8", 9: "9", 10: "10", 11: "J", 12: "Q",
    13: "K", 14: "A"
}
_RANK_LONG_STRINGS = {
    1: "ace", 2: "two", 3: "three", 4: "four",
    5: "five", 6: "six", 7: "seven", 8: "eight",
    9: "nine", 10: "ten", 11: "jack", 12: "queen",
    13: "king", 14: "ace"
}
_SUIT_LONG_STRINGS = ["clubs", "hearts", "spades", "diamonds"]


# Public functions

def get_rank_integer(rank):

    """Returns a valid integer rank from an input of unspecified type."""

    if rank in range(1, 15):
        return 14 if rank == 1 else rank
    elif isinstance(rank, basestring):
        if not rank:
            raise ValueError("Missing rank value.")

        for key in _ALLOWABLE_RANK_STRINGS.iterkeys():
            if key.startswith(rank.lower()):
                return _ALLOWABLE_RANK_STRINGS[key]

    raise ValueError("Invalid rank value '{0}'".format(rank))


def get_suit_integer(suit):

    """Returns a valid integer suit from an input of unspecified type."""

    if suit in range(0, 4):
        return suit
    elif isinstance(suit, basestring):
        if not suit:
            raise ValueError("Missing suit value.")

        for key in _ALLOWABLE_SUIT_STRINGS.iterkeys():
            if key.startswith(suit.lower()):
                return _ALLOWABLE_SUIT_STRINGS[key]

    raise ValueError("Invalid suit value '{0}'".format(suit))


def rank_string(rank, short=False, capitalize=False):

    """Returns a string representing a specified rank,
    e.g. "ace", "five", "jack".

    Arguments:
    rank -- an integer from 1 to 14, inclusive, representing the
    rank for which a string is to be returned. Aces can be passed
    in either as 1 or 14. 11 is jack, 12 is queen, and 13 is king.
    short -- set to True to output short strings, i.e. "A"
    instead of "ace", "4" instead of "four", "J" instead
    of "jack".
    capitalize -- set to True to capitalize the first letter
    of the word (long strings only).

    """

    if rank not in range(1, 15):
        raise ValueError("Invalid rank value '{0}'".format(rank))

    if short:
        outstr = _RANK_SHORT_STRINGS[rank]
    else:
        outstr = _RANK_LONG_STRINGS[rank]

    if capitalize:
        outstr = outstr.capitalize()

    return outstr


def suit_string(suit, short=False, capitalize=False):

    """Returns a string representing a specified suit,
    e.g. "clubs", "hearts", "diamonds".

    Arguments:
    suit -- an integer from 0 to 3, inclusive, representing the
    suit for which a string is to be returned. 0 is clubs, 1 is
    hearts, 2 is spades, and 3 is diamonds.
    short -- set to True to output short strings, i.e. "C"
    instead of "clubs", "S" instead of "spades"
    capitalize -- set to True to capitalize the first letter
    of the word (long strings only).

    """

    if suit not in range(0, 4):
        raise ValueError("Invalid suit value '{0}'".format(suit))

    if short:
        outstr = _SUIT_LONG_STRINGS[suit][0].upper()
    else:
        outstr = _SUIT_LONG_STRINGS[suit]

    if capitalize:
        outstr = outstr.capitalize()

    return outstr


# Non-public functions

def _get_index_integer(index):

    """Returns a valid index integer from an input of unspecified type."""

    if index in range(0, 52):
        return index
    else:
        raise ValueError("Invalid index value '{0}'".format(index))


def _get_rank_and_suit_from_index(index):

    """Returns a two-element tuple containing a valid
    rank and suit integer from a provided valid index integer.

    """

    rank = index % 13 + 1
    if rank == 1:
        rank = 14
    return (rank, index // 13)


def _get_index_from_rank_and_suit(rank, suit):

    """Returns an integer representing a valid index
    from provided valid rank and suit integers.

    """

    return suit * 13 + ((rank - 1) if rank != 14 else 0)


def _get_rank_and_suit_from_name(name):

    """Returns a two-element tuple representing a valid rank integer
    and a valid suit integer from a provided short name.

    """

    return (get_rank_integer(name[0:-1]), get_suit_integer(name[-1]))


def _get_index_from_name(name):

    """Returns an integer representing a valid index
    from a provided short name.

    """

    rank = get_rank_integer(name[0:-1])
    suit = get_suit_integer(name[-1])

    return _get_index_from_rank_and_suit(rank, suit)


# Exceptions

class CardArgumentError(Exception):

    """Exception raised when arguments to the Card class
    initializer are mismatched or missing.

    """

    pass


# Class

class Card(object):

    """Playing card class.

    Public methods:
    __init__(rank, suit, index)
    copy()
    rank()
    suit()
    index()
    rank_string(short, capitalized)
    suit_string(short, capitalized)
    name_string(short, capitalized)

    Comparison operators:
    All are overloaded. Card instances are compared by rank only,
    i.e. suits are irrelevant. Aces are always treated as high by
    the comparison operators.

    Conversion operators:
    __str__ -- returns the result from name_string(capitalized=True)
    __int__ -- returns the card rank, with aces always one.

    """

    def __init__(self, rank=None, suit=None, index=None, name=None):

        """Instance initialization function.

        Arguments:
        rank -- rank of card, integer from 1 to 14 inclusive. Jack is 11,
        queen is 12, king is 13, and ace is 14. An ace can also
        be passed in with a value of 1. The names can also be
        passed in, e.g. "ace", "three", "king", or any shorter
        starting substring, e.g. "a", "e", "j", "k". The result is
        uncertain in the case of ambiguity, i.e. "t" may resolve to
        "two" or it may resolve to "ten", so use strings long enough
        to avoid ambiguity unless you want unpredictable results.
        suit -- suit of card, an integer from 0 to 3 inclusive representing
        clubs, hearts, spades or diamonds, in that order. The names
        can also be passed in, similar to the rank, e.g. "clubs",
        "sp", "hear", "d".
        index -- an integer from 0 to 51 inclusive, where 0 is the
        ace of clubs, 1 is the two of clubs, 2 is the three of clubs,
        and so on, progressing from ace to king through clubs, hearts,
        spades, and then diamonds, so that 51 is the king of diamonds.
        name -- a string of the form "AC" or "10D", corresponding to the
        type of string returned from name_string(short=True)

        Exceptions raised:
        CardArgumentError -- if all arguments are missing, or if an
        index is provided in addition to a rank and a suit, or if
        one but not both of a rank and a suit are provided.
        ValueError -- if any of the provided values are invalid.

        """

        if (suit is not None and rank is not None and
                 index is None and name is None):
            self._rank = get_rank_integer(rank)
            self._suit = get_suit_integer(suit)
            self._index = _get_index_from_rank_and_suit(self._rank, self._suit)
        elif (index is not None and rank is None and
                 suit is None and name is None):
            self._index = _get_index_integer(index)
            self._rank, self._suit = _get_rank_and_suit_from_index(index)
        elif (name is not None and rank is None and
                 suit is None and index is None):
            self._rank, self._suit = _get_rank_and_suit_from_name(name)
            self._index = _get_index_from_rank_and_suit(self._rank, self._suit)
        else:
            raise CardArgumentError("Missing or mismatched card arguments.")

    # Public methods

    def copy(self):

        """Returns a copy of the card."""

        return self.__class__(index=self.index())

    def rank(self):

        """Returns the integer rank of a card.

        Aces are always returned as 14, kings as 13,
        queens as 12, and jacks as 11.

        """

        return self._rank

    def suit(self):

        """Returns the integer suit of a card.

        0 represents clubs, 1 represents hearts, 2 represents
        spades, and 3 represents diamonds.

        """

        return self._suit

    def index(self):

        """Returns the integer index of a card.

        The index is an integer from 0 to 51 inclusive, where 0 is the
        ace of clubs, 1 is the two of clubs, 2 is the three of clubs,
        and so on, progressing from ace to king through clubs, hearts,
        spades, and then diamonds, so that 51 is the king of diamonds.

        """

        return self._index

    def rank_string(self, short=False, capitalize=False):

        """Returns a string representing the rank of the card,
        e.g. "ace", "five", "jack".

        Arguments:
        short -- set to True to output short strings, i.e. "A"
        instead of "ace", "4" instead of "four", "J" instead
        of "jack".
        capitalize -- set to True to capitalize the first letter
        of the word (long strings only).

        """

        return rank_string(self._rank, short=short, capitalize=capitalize)

    def suit_string(self, short=False, capitalize=False):

        """Returns a string representing the suit of the card,
         e.g. "clubs", "hearts", "diamonds".

        Arguments:
        short -- set to True to output short strings, i.e. "C"
        instead of "clubs", "S" instead of "spades"
        capitalize -- set to True to capitalize the first letter
        of the word (long strings only).

        """

        return suit_string(self._suit, short=short, capitalize=capitalize)

    def name_string(self, short=False, capitalize=False):

        """Returns a string representing the name of the card,
         e.g. "ace of spades", "four of hearts", "ten of diamonds".

        Arguments:
        short -- set to True to output short strings, i.e. "AS"
        instead of "ace of spades", "4H" instead of "four of hearts"
        capitalize -- set to True to capitalize the first letter
        of the rank and the suit (long strings only).

        """

        if short:
            ofstr = ""
        else:
            ofstr = " of "

        return (self.rank_string(short=short, capitalize=capitalize) + ofstr +
                self.suit_string(short=short, capitalize=capitalize))

    # Non-public methods

    def _sort_index(self):

        """Returns an alternate index suitable for sorting, where cards
        are ordered first by rank, and then by suit order.

        """

        rank = 0 if self._rank == 14 else self._rank - 1
        return self._suit + rank * 4

    # Conversion operators

    def __str__(self):

        """Override string conversion operator to return representation
        of card in long capitalize "Ace of Spades" format.

        """

        return self.name_string(capitalize=True)

    def __int__(self):

        """Override integer conversion operator to return card rank,
        with aces always returned as 1, kings as 13, queens as
        12, and jacks as 11.

        Note that this deliberately returns a different result from
        the rank() method, where aces are always returned as 14,
        not 1. The rank() method would normally be used in preference
        to converting to an integer when evaluating a hand, since
        in most cases the ace is treated as the highest card. For
        a plain integer representation, however, representing it
        as 1 is more natural than representing it as 14.

        """

        return self._rank if self._rank != 14 else 1

    # Comparison operators

    # Disable pylint message for access to protected other._rank,
    # usage is safe when comparing two types of the same class
    # by an instance method.
    #
    # pylint: disable=W0212

    def __eq__(self, other):

        """Override == operator to compare based on card rank only."""

        return self._rank == other._rank

    def __ne__(self, other):

        """Override != operator to compare based on card rank only."""

        return self._rank != other._rank

    def __gt__(self, other):

        """Override > operator to compare based on card rank only.

        Aces are always treated as high.

        """

        return self._rank > other._rank

    def __lt__(self, other):

        """Override < operator to compare based on card rank only.

        Aces are always treated as high.

        """

        return self._rank < other._rank

    def __ge__(self, other):

        """Override >= operator to compare based on card rank only.

        Aces are always treated as high.

        """

        return self._rank >= other._rank

    def __le__(self, other):

        """Override <= operator to compare based on card rank only.

        Aces are always treated as high.

        """

        return self._rank <= other._rank

    # pylint: enable=W0212

deck.py

"""Card deck module.

Library Release 1.1

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/

"""


import random

from pcards import Card


class EmptyDeckError(Exception):

    """Exception class for trying to draw a card from an empty deck.

    """

    pass


class Deck(object):

    """Implements a deck of cards, using Card instances.

    Public methods:
    __init__(packs)
    discard()
    discard_size()
    draw()
    get_card_list()
    get_discard_list()
    replace_discards()
    shuffle()

    """

    def __init__(self, packs=1):

        """Initializes a Deck instance.

        Creates a deck of cards, and an empty discard pile.

        Arguments:
        packs -- the number of packs to put into the deck, set to
        something greater than 1 to create a deck consisting of
        multiple packs.

        """

        if not isinstance(packs, int) or not packs > 0:
            raise ValueError("Argument 'packs' must be a positive integer.")

        self._cards = [Card(index=idx) for pack in range(packs)
                                       for idx in range(51, -1, -1)]
        self._discards = []

    # Public methods

    def discard(self, cards):

        """Adds a list of cards to the discard pile.

        Arguments:
        card -- the Card instance list to be added. Under normal
        circumstances, this list will have originally been taken
        from the same deck using the draw() method, but this is
        not necessarily so.

        """

        self._discards.extend(cards)

    def discard_size(self):

        """Returns the number of cards in the discard pile."""

        return len(self._discards)

    def draw(self, number=1):

        """Draws one or more cards from the top of the deck and returns
        it or them in a list.

        Arguments:
        number -- the number of cards to draw.

        """

        if number > len(self._cards):
            raise EmptyDeckError
        else:
            drawn_cards = self._cards[-number:]

            # Call reverse() to simulate cards being popped
            # off the top of the deck in order

            drawn_cards.reverse()
            self._cards = self._cards[0:-number]
            return drawn_cards

    def get_card_list(self):

        """Returns a copy of the card list."""

        return self._cards[:]

    def get_discard_list(self):

        """Returns a copy of the discard list."""

        return self._discards[:]

    def replace_discards(self):

        """Replaces the discard pile at the bottom of the deck."""

        self._discards.extend(self._cards)
        self._cards = self._discards
        self._discards = []

    def shuffle(self, return_discards=True):

        """Shuffles the deck.

        Arguments:
        return_discards -- If set to 'True', the discard pile is added
        back to the deck before shuffling, otherwise the discard pile
        remains intact and only the remaining cards in the deck
        are shuffled.

        """

        if return_discards and self._discards:
            self.replace_discards()

        random.shuffle(self._cards)

    # Indexing and iteration methods

    def __len__(self):

        """Returns the number of cards remaining in the deck.

        Excludes the cards in the discard pile.

        """

        return len(self._cards)

    def __getitem__(self, key):

        """Returns the card at the specified index."""

        return self._cards[key]

    def __setitem__(self, key, value):

        """Sets the card at the specified index."""

        if not isinstance(value, Card):
            raise TypeError("Only Card instances can be assigned.")
        else:
            self._cards[key] = value

    def __delitem__(self, key):

        """Deletes the card at the specified index."""

        return self._cards.pop(key)

    def __iter__(self):

        """Returns an iterator object of the cards list."""

        return iter(self._cards)

    def __contains__(self, item):

        """Returns true if item is in the cards list."""

        if not isinstance(item, Card):
            return False
        else:
            for card in self._cards:
                if card.index() == item.index():
                    return True
            else:
                return False

hand.py

"""Generic hand module.

Library Release 1.1

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/

"""


from collections import defaultdict

from pcards import Card, get_rank_integer


# Exceptions

class NoAssociatedDeckError(Exception):

    """Exception for when an instance method requiring
    an associated deck is called, but no deck is
    associated.

    """

    pass


# Class

class Hand(object):

    """Implements a generic card hand class.

    Implemented as a full sequence container class.

    Public methods:
    __init__(deck, numcards, namelist, cardlist, nocopy)
    copy()
    discard()
    draw(numcards)
    exchange(chg)
    get_list()
    index_list()

    Comparison operators:
    All are overloaded, comparison evaluation follows normal
    rules for comparing poker hands.

    Indexing and iteration operators:
    All are overloaded (__getitem__, __setitem__, __delitem__,
    __iter__, __contains__, __len__) and slicing is supported.

    Sequence concatenation and multiplication methods:
    All are overloaded (__add__, __radd__, __iadd__, __mul__,
    __rmul__, __imul__)

    Sequence container methods:
    append(value)
    count(value)
    extend(hand)
    index(value)
    insert(idx, value)
    pop(idx)
    remove(value)
    sort(reverse)
    reverse()

    Conversion operators:
    Only __str__ is overloaded, returning a string showing the
    short values (e.g. "4H") of all five cards.
    """

    def __init__(self, deck=None, numcards=0,
                 namelist=None, cardlist=None, nocopy=False):

        """Initializes a Hand instance.

        Arguments:
        deck -- the Deck instance to draw the cards from
        numcards -- an optional number of cards to draw immediately
        from the deck, if a deck is provided
        namelist -- a list of short names (e.g. "AS", "10C", "7H") to
        populate the hand
        cardlist -- a list of Card instances to populate the hand
        nocopy -- set to True to add the provided cardlist list as the
        internal list of cards, rather than a copy of it which is the
        default behavior.

        """

        self._score = []
        self._cards = []
        self._deck = deck

        if numcards and deck and not namelist:
            self.draw(numcards)
            self._cards_changed()
        elif namelist:
            for name in namelist:
                self._cards.append(Card(name=name))
                self._cards_changed()
        elif cardlist:
            if nocopy:
                self._cards = cardlist
            else:
                for card in cardlist:
                    self._cards.append(card.copy())
                    self._cards_changed()

    # Public methods

    def copy(self):

        """Returns a new Hand instance which is a copy of the
        original Hand instance. Copies are made of the Card
        instances comprising the original card list (copies
        are made by the initializer method), as well as of
        the list itself.

        """

        return self.__class__(cardlist=self._cards)

    def discard(self):

        """Discards all cards in the hand and returns them
        to the associated deck.

        """

        if self._deck == None:
            raise NoAssociatedDeckError()

        self._deck.discard(self._cards)
        self._cards = []
        self._cards_changed()

    def draw(self, numcards):

        """Draws a specific number of cards from the deck.

        Arguments:
        numcards -- the number of cards to draw.

        """

        if self._deck is None:
            raise NoAssociatedDeckError
        else:
            self._cards.extend(self._deck.draw(numcards))
            self._cards_changed()

    def exchange(self, chg):

        """Exchanges selected cards for new cards drawn from the deck.

        Arguments:
        chg -- a string of digits containing the positions of the
        cards to be exchanged, starting at 1, e.g. "125", or "3", or
        "14".

        """

        discards = []
        for idx in chg:
            if int(idx) in range(len(self._cards)):
                discards.append(self._cards[int(idx) - 1])
                self._cards[int(idx) - 1] = self._deck.draw()[0]
        self._deck.discard(discards)
        self._cards_changed()

    def get_list(self):

        """Returns a copy of the card list."""

        copied_cards = []
        for original_card in self._cards:
            copied_cards.append(original_card.copy())
        return copied_cards

    def index_list(self):

        """Returns a list of card indices for the current hand.

        Primarily intended to be used for comparing lists, since
        the usual card comparison only compares by rank, so a royal
        flush of clubs and a royal flush of spades would compare as
        identical when equating the output of get_list().

        """

        return [card.index() for card in self._cards]

    # Conversion operators

    def __str__(self):

        """Override string conversion operator to return a
        representation of the hand in short "6D" format.

        """

        return ''.join([" {0:>3}".format(c.name_string(short=True))
                                         for c in self._cards])

    # Comparison operators

    # Disable pylint message for access to protected other._score,
    # usage is safe when comparing two types of the same class
    # by an instance method.
    #
    # pylint: disable=W0212

    def __eq__(self, other):

        """Override == operator based on standard poker hand evaluation."""

        if not isinstance(other, Hand):
            raise NotImplementedError("Only other Hands can be compared " +
                                      "to Hands.")
        if self._score == other._score:
            return True
        else:
            return False

    def __ne__(self, other):

        """Override != operator based on standard poker hand evaluation."""

        return not self == other

    def __gt__(self, other):

        """Override > operator based on standard poker hand evaluation."""

        if not isinstance(other, Hand):
            raise NotImplementedError("Only other Hands can be compared " +
                                      "to Hands.")
        if self._score > other._score:
            return True
        else:
            return False

    def __le__(self, other):

        """Override <= operator based on standard poker hand evaluation."""

        return not self > other

    def __lt__(self, other):

        """Override < operator based on standard poker hand evaluation."""

        if not isinstance(other, Hand):
            raise NotImplementedError("Only other Hands can be compared " +
                                      "to Hands.")
        if self._score < other._score:
            return True
        else:
            return False

    def __ge__(self, other):

        """Override >= operator based on standard poker hand evaluation."""

        return not self < other

    # pylint: enable=W0212

    # Concatenation and repetition operators

    def __add__(self, other):

        """Adds the cards in one Hand to another."""

        if not isinstance(other, Hand):
            raise NotImplementedError("You can only add another " +
                                      "Hand to a Hand.")
        else:
            new_hand = self.copy()
            new_hand.extend(other)
            return new_hand

    def __mul__(self, other):

        """Multiples the cards in a Hand by an integer."""

        if not isinstance(other, int) or other < 1:
            raise NotImplementedError("You can only multiply a Hand " +
                                      "by a positive integer")
        else:
            new_cards = []
            for copies in range(other):
                new_cards.extend(self.get_list())
            return self.__class__(cardlist=new_cards, nocopy=True)

    def __radd__(self, other):

        """Adds the cards in one Hand to another."""

        if not isinstance(other, Hand):
            raise NotImplementedError("You can only add another " +
                                      "Hand to a Hand.")
        else:
            new_hand = self.copy()
            new_hand.extend(other)
            return new_hand

    def __rmul__(self, other):

        """Multiples the cards in a Hand by an integer."""

        if not isinstance(other, int) or other < 1:
            raise NotImplementedError("You can only multiply a " +
                                      "Hand by a positive integer")
        else:
            new_cards = []
            for copies in range(other):
                new_cards.extend(self.get_list())
            return self.__class__(cardlist=new_cards, nocopy=True)

    def __iadd__(self, other):

        """Adds the cards in one Hand to another."""

        if not isinstance(other, Hand):
            raise NotImplementedError("You can only add another " +
                                      "Hand to a Hand.")
        else:
            self.extend(other)
            self._cards_changed()
            return self

    def __imul__(self, other):

        """Multiples the cards in a Hand by an integer."""

        if not isinstance(other, int) or other < 1:
            raise NotImplementedError("You can only multiply a Hand by " +
                                      "a positive integer")
        else:
            new_cards = []
            for copies in range(other - 1):
                new_cards.extend(self.get_list())
            self._cards.extend(new_cards)
            self._cards_changed()
            return self

    # Indexing and iteration methods

    def __len__(self):

        """Returns the number of cards in the hand."""

        return len(self._cards)

    def __getitem__(self, key):

        """Returns a copy of the card at the specified index.

        Note that, since __getitem__ (deliberately) returns a copy,
        statements such as myhand[1]._index = 0 will not modify myhand,
        as 0 will be assigned to _index in the copy, not to _index in
        the actual card at myhand[1].

        """

        if isinstance(key, slice):
            new_list = []
            for original_card in self._cards[key]:
                new_list.append(original_card.copy())
            return Hand(cardlist=new_list, nocopy=True)
        else:
            return self._cards[key].copy()

    def __setitem__(self, key, value):

        """Sets the card at the specified index."""

        if isinstance(key, slice):
            if not isinstance(value, Hand):
                raise TypeError("Only other Hand instances can be " +
                                "assigned via slicing.")
            self._cards[key] = value.get_list()
            self._cards_changed()
        elif not isinstance(value, Card):
            raise TypeError("Only Card instances can be assigned.")
        else:
            self._cards[key] = value.copy()
            self._cards_changed()

    def __delitem__(self, key):

        """Deletes the card at the specified index."""

        del(self._cards[key])
        self._cards_changed()

    def __iter__(self):

        """Returns a copy of the card list as an iterator object."""

        return iter(self.get_list())

    def __contains__(self, item):

        """Returns true if item is in the card list."""

        if not isinstance(item, Card):
            return False
        else:
            for card in self._cards:
                if card.index() == item.index():
                    return True
            else:
                return False

    # Container methods

    def append(self, value):

        """Appends a card to the card list."""

        if not isinstance(value, Card):
            raise TypeError("Only Card instances can be appended.")
        else:
            self._cards.append(value.copy())
            self._cards_changed()

    def count(self, value):

        """Returns the count of a card or integer rank in the card list."""

        if isinstance(value, Card):
            cnt, index = 0, value.index()
            for card in self._cards:
                if card.index() == index:
                    cnt += 1
            return cnt
        elif isinstance(value, int) or isinstance(value, basestring):
            cnt, value = 0, get_rank_integer(value)
            for card in self._cards:
                if card.rank() == value:
                    cnt += 1
            return cnt
        else:
            return 0

    def extend(self, hand):

        """Extends the card list with that of another Hand."""

        if not isinstance(hand, Hand):
            raise TypeError("Hand instances may only be extended with " +
                            "other Hand instances.")
        else:
            self._cards.extend(hand.get_list())
            self._cards_changed()

    def index(self, value):

        """Returns the index in the card list of the first item whose
        value is 'value'.

        """

        if isinstance(value, Card):
            for idx, card in enumerate(self._cards):
                if card.index() == value.index():
                    return idx
            else:
                raise ValueError("Card.index(x): x not in Card")
        elif isinstance(value, int) or isinstance(value, basestring):
            value = get_rank_integer(value)
            for idx, card in enumerate(self._cards):
                if card.rank() == value:
                    return idx
            else:
                raise ValueError("Card.index(x): x not in Card")
        else:
            raise ValueError("Card.index(c): x not in Card")

    def insert(self, idx, value):

        """Inserts a card at an index in the card list."""

        if not isinstance(value, Card):
            raise TypeError("Only Card instances may be inserted into " +
                            "Hand instances.")
        else:
            self._cards.insert(idx, value.copy())
            self._cards_changed()

    def pop(self, idx=None):

        """Pops a card at a specified index (or from the end, if no
        index is provided) of the card list, and returns it.

        """

        if idx != None:
            if not isinstance(idx, int):
                raise TypeError("index must be an integer")
            ret_card = self._cards.pop(idx)
        else:
            ret_card = self._cards.pop()
        self._cards_changed()
        return ret_card

    def remove(self, value):

        """Removes the first Card from the card list whose value is
        'value'.

        """

        if not isinstance(value, Card):
            raise TypeError("Only Cards may be removed from a Hand.")
        else:
            self._cards.remove(value)
            self._cards_changed()

    def sort(self, key=None, reverse=False):        # pylint: disable=W0613

        """Sorts the Cards, in place."""

        # pylint: disable=W0212

        self._cards.sort(key=Card._sort_index, reverse=reverse)

        # pylint: enable=W0212

    def reverse(self):

        """Reverses the order of the cards, in place."""

        self._cards.reverse()

    # Non-public methods

    def _cards_changed(self):

        """Called when the card list changes, will normally
        be override by subclasses.

        """

        pass

    def _ranks(self, sort=False, reverse=False):

        """Returns a list of card ranks.

        Arguments:
        sort -- set to True to sort the list in ascending order
        reverse -- set to True to sort the list in descending order.

        """

        rank_list = [card.rank() for card in self._cards]
        if sort or reverse:
            rank_list.sort(reverse=reverse)
        return rank_list

    def _suits(self):

        """Returns a list of card suits."""

        return [card.suit() for card in self._cards]

    def _get_rank_counts(self):

        """Returns a dictionary of rank counts."""

        rank_counts = defaultdict(int)
        for rank in self._ranks():
            rank_counts[rank] += 1
        return rank_counts

    def _get_suit_counts(self):

        """Returns a dictionary of suit counts."""

        suit_counts = defaultdict(int)
        for suit in self._suits():
            suit_counts[suit] += 1
        return suit_counts

pokerhand.py

"""Poker hand module.

Library Release 1.1

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/

"""


from collections import namedtuple

from pcards import rank_string, Hand


# Non-public named tuples

_HSLF = namedtuple("HSLF", ["fstr", "fargs"])

# pylint raises a convention warning for _HandInfo
# rather than _HANDINFO, but we use named tuples
# in a similar way to classes, so we follow that
# naming convention instead and disable the message.
#
# pylint: disable=C0103

_HandInfo = namedtuple("HandInfo", ["high_card", "low_pair", "high_pair",
                                    "three", "four", "flush", "straight",
                                    "straight_flush", "royal_flush"])

# pylint: enable=C0103


# Non-public constants

_HIGH_CARD = 0
_LOW_PAIR = 1
_HIGH_PAIR = 2
_THREE = 3
_FOUR = 4
_FLUSH = 5
_STRAIGHT = 6
_STRAIGHTFLUSH = 7
_ROYALFLUSH = 8

_HAND_STRINGS_SHORT = [
    "HI", "PR", "TP", "TK", "ST",
    "FL", "FH", "FK", "SF", "RF"
]
_HAND_STRINGS_NORMAL = [
    "High card", "Pair", "Two pair", "Three of a kind",
    "Straight", "Flush", "Full House", "Four of a kind",
    "Straight flush", "Royal flush"
]
_HAND_STRINGS_LONG = [
    _HSLF("{0} high", (_HIGH_CARD,)),
    _HSLF("Pair of {0}s", (_LOW_PAIR,)),
    _HSLF("Two pair, {0}s over {1}s", (_HIGH_PAIR, _LOW_PAIR)),
    _HSLF("Three of a kind", None),
    _HSLF("Straight", None),
    _HSLF("Flush", None),
    _HSLF("Full house, {0}s full of {1}s", (_THREE, _LOW_PAIR)),
    _HSLF("Four of a kind", None),
    _HSLF("Straight flush", None),
    _HSLF("Royal flush", None)
]


# Class

class PokerHand(Hand):

    """Implements a five card regular poker hand class.

    Public methods:
    __init__(deck, numcards, namelist)
    show_value(short, full)
    video_winnings(bet, easy)

    """

    _vp_returns_normal = [0, 1, 2, 3, 4, 6, 9, 25, 50, 800]
    _vp_returns_easy = [0, 2, 3, 4, 15, 20, 50, 100, 250, 2500]

    def __init__(self, deck=None, numcards=5, namelist=None, cardlist=None):

        """Initializes a PokerHand instance.

        Arguments:
        deck -- the Deck instance to draw the cards from.

        """

        self._singles = []
        self._hand_info = None

        Hand.__init__(self, deck, numcards, namelist, cardlist)

    # Public methods

    def show_value(self, short=False, full=True):

        """Returns a string containing the name of, and
        sometimes more information about, a poker hand.

        Arguments:
        short -- set to True to receive two character value
        full -- set to True to include other information, e.g.
        when full is True "Full house" becomes "Full house, aces
        full of threes" or similar.

        Returns: string object containing hand evaluation.

        """

        if short:
            return _HAND_STRINGS_SHORT[self._score[0]]
        elif full:
            hsl = _HAND_STRINGS_LONG[self._score[0]]
            if hsl.fargs:
                arg_list = [rank_string(self._hand_info_item(item))
                            for item in hsl.fargs]
                return hsl.fstr.format(*arg_list).capitalize()
            else:
                return hsl.fstr.format()
        else:
            return _HAND_STRINGS_NORMAL[self._score[0]]

    def video_winnings(self, bet, easy=False):

        """Returns video poker winnings for a given bet.

        Arguments:
        bet -- the amount of the bet.
        easy -- if set to 'True', higher winnings are awarded,
        otherwise default winnings are awarded. Default is 'False'.

        """

        if easy:
            rtns = PokerHand._vp_returns_easy
        else:
            rtns = PokerHand._vp_returns_normal

        if self._score[0] == 1 and self._hand_info.low_pair < 11:
            return 0        # Pairs only win if Jacks or better
        else:
            return rtns[self._score[0]] * bet

    # Non-public methods

    def _hand_info_item(self, item_index):

        """Returns the value of a hand info item."""

        item_dict = {
            _HIGH_CARD: 0 if not self._singles else self._singles[0],
            _LOW_PAIR: self._hand_info.low_pair,
            _HIGH_PAIR: self._hand_info.high_pair,
            _THREE: self._hand_info.three,
            _FOUR: self._hand_info.four,
            _FLUSH: self._hand_info.flush,
            _STRAIGHT: self._hand_info.straight,
            _STRAIGHTFLUSH: self._hand_info.straight_flush,
            _ROYALFLUSH: self._hand_info.royal_flush
        }

        return item_dict[item_index]

    def _evaluate(self):

        """Evaluates a poker hand and stores information
        necessary for later comparison.

        """

        # Identify singles, pairs, threes and fours

        (self._singles, low_pair,
         high_pair, three, four) = self._get_rank_matches()

        # Check for a straight, and set the high card

        straight, high_card = self._check_straight()

        # Check for a flush

        flush = self._check_flush()

        # Check for a straight and royal flush

        straightflush = False
        royal = False

        if flush and straight:
            straightflush = True
            if high_card == 14:
                royal = True

        # Populate HandInfo instance attribute and set score

        self._hand_info = _HandInfo(high_card, low_pair,
                                    high_pair, three,
                                    four, flush, straight,
                                    straightflush, royal)
        self._set_score()

    def _set_score(self):

        """Stores a score for the hand, to enable us to
        compare them, later. The score is a list,
        representing - from left to right - the things
        that determine a winning poker hand. For example,
        for a three of a kind, first compare the overall
        category (e.g. a three of a kind (4) always beats
        a pair (2) but never beats a flush (6)). If we
        have two threes of a kind, then compare the ranks
        of the threes (self.three). If the ranks of the
        threes are the same (this is possible in reality
        if wild cards or community cards are used) then
        look through the remaining cards (self._singles)
        for the hightest card. Because of the way Python
        compares lists, we can set up the score like this
        and have Python do almost all of the dirty work
        for us just using comparison operators.

        """

        if self._hand_info.royal_flush:
            self._score = [9]
        elif self._hand_info.straight_flush:
            self._score = [8, self._hand_info.high_card]
        elif self._hand_info.four:
            self._score = [7, self._hand_info.four, self._singles]
        elif self._hand_info.three and self._hand_info.low_pair:
            self._score = [6, self._hand_info.three, self._hand_info.low_pair]
        elif self._hand_info.flush:
            self._score = [5, self._ranks(reverse=True)]
        elif self._hand_info.straight:
            self._score = [4, self._hand_info.high_card]
        elif self._hand_info.three:
            self._score = [3, self._hand_info.three, self._singles]
        elif self._hand_info.high_pair:
            self._score = [2, self._hand_info.high_pair,
                           self._hand_info.low_pair, self._singles]
        elif self._hand_info.low_pair:
            self._score = [1, self._hand_info.low_pair, self._singles]
        else:
            self._score = [0, self._singles]

    def _get_rank_matches(self):

        """Returns information about rank properties."""

        rank_counts = self._get_rank_counts()

        singles = []
        low_pair = 0
        high_pair = 0
        three = 0
        four = 0

        for val in sorted(rank_counts):
            if rank_counts[val] == 1:
                singles.append(val)
            elif rank_counts[val] == 2:
                if not low_pair:
                    low_pair = val
                else:
                    high_pair = val
            elif rank_counts[val] == 3:
                three = val
            elif rank_counts[val] == 4:
                four = val
            elif rank_counts[val] == 5:
                raise NotImplementedError("Five of a kind not implemented.")

        singles.reverse()

        return [singles, low_pair, high_pair, three, four]

    def _check_straight(self):

        """Checks for a straight.

        Returns a two-element tuple. The first element is
        True if a straight is found. The second element
        represents the high card if a straight is found, or
        None if a straight is not found. This information
        is important because an A-2-3-4-5 (a "wheel straight")
        is the only time in a standard poker hand where the
        ace is treated as the low card, so we'd need to know
        that the high card was five, in this instance.

        """

        sorted_ranks = self._ranks(sort=True)
        high_card = sorted_ranks[-1]
        straight = False

        if len(set(sorted_ranks)) == len(sorted_ranks):
            if sorted_ranks[4] - sorted_ranks[0] == 4:
                straight = True
            elif sorted_ranks[4] - sorted_ranks[3] == 9:
                straight = True
                high_card = 5

        return (straight, high_card)

    def _check_flush(self):

        """Checks if we have a flush, returns True if we
        do and false if we don't.

        """

        if 5 in self._get_suit_counts().values():
            return True
        else:
            return False

    def _cards_changed(self):

        """Override superclass function and evaluate hand."""

        Hand._cards_changed(self)
        if len(self._cards) == 5:
            self._evaluate()