import operator
import random

class InvalidCard(Exception):
    """Invalid Card Exception
    
    Thrown if an invalid face value or suit is supplied for a card
    """
    pass
class InvalidHand(Exception):
    """Invalid Hand Exception
    
    Thrown if the hand being created includes zero or more than 5 cards
    """
    pass

def unique_combinations(items, n):
    if n==0: yield []
    else:
        for i in xrange(len(items)):
            for cc in unique_combinations(items[i+1:],n-1):
                yield [items[i]]+cc



class Card:
    """Represents a single poker playing card"""
    
    VALUES = {'T': 10, 'J': 11, 'Q': 12, 'K': 13, 'A': 14}
    SUITS = {'Hearts': 'H', 'Diamonds': 'D', 'Clubs': 'C', 'Spades': 'S'}
    def __init__(self, string):
        self.value = string[0]
        if self.value in '23456789':
            self.value = int(self.value)
        else:
            try:
                self.value = Card.VALUES[self.value]
            except KeyError as e:
                raise InvalidCard('Invalid value: {0}'.format(string))
        self.suit = string[1]
        if not self.suit in Card.SUITS.values():
            raise InvalidCard('Invalid suit: {0}'.format(string))
    def __cmp__(self, other):
        """Compare hand values
        
        Compares this card's value with another. Used for sorting.
        """
        
        return cmp(self.value, other.value)
    def __repr__(self):
        """Builds a string representation of the hand"""
        val = self.value
        for k, v in Card.VALUES.iteritems():
            if self.value == v:
                val = k
        return str.format('{0}{1}', val, self.suit)

class Deck():
    def __init__(self):
        self.__cards = []
        for suit in Card.SUITS.values():
            for i in range(2,15):
                if i >= 10:
                    for k, v in Card.VALUES.iteritems():
                        if i == v: value = k
                else:
                    value = i
                self.__cards.append(Card('{0}{1}'.format(value, suit)))
    def shuffle(self):
        random.shuffle(self.__cards)
    def cards(self):
        return self.__cards
    def deal(self, n=1, players=[]):
        if players:
            for i in range(n):
                for player in players:
                    player.add_card(self.__cards.pop())
        else:
            cards = []
            for i in range(n):
                cards.append(self.__cards.pop())
            return cards

class Hand:
    HIGH_CARD = 0
    ONE_PAIR = 1
    TWO_PAIRS = 2
    THREE_OF_A_KIND = 3
    STRAIGHT = 4
    FLUSH = 5
    FULL_HOUSE = 6
    FOUR_OF_A_KIND = 7
    STRAIGHT_FLUSH = 8
    ROYAL_FLUSH = 9
    RANKS = [
        'High Card',
        'One Pair',
        'Two Pairs',
        'Three of a Kind',
        'Straight',
        'Flush',
        'Full House',
        'Four of a Kind',
        'Straight Flush',
        'Royal Flush'
    ]
    def __init__(self, cards):
        if len(cards) < 1 or len(cards) > 5:
            raise InvalidHand(cards)
        self.__rank = None
        self.__cards = sorted([Card(c) for c in cards], reverse=True)
        self.__values = []
        self.rank()
    def __repr__(self):
        """Builds a string representation of the hand"""
        return str.format("Cards: {0} Rank: '{1}' Values: {2}",
            self.__cards,
            Hand.RANKS[self.rank()],
            self.values())
    def rank(self):
        """Get the hand rank
        
        Determines the rank of the poker hand if it has not already been
        computed, and returns it.
        """
        
        if self.__rank:
            return self.__rank
        flush = True
        straight = False
        last = None
        merged = {}
        for c in self.__cards:
            if last:
                if flush and c.suit != last.suit:
                    flush = False
            last = c
            if c.value in merged:
                merged[c.value] = merged[c.value] + 1
            else:
                merged[c.value] = 1
        if (len(merged)) == 5:
            # All unique cards, check for a straight
            if self.__cards[0].value - self.__cards[4].value == 4:
                straight = True
            if self.__cards[4].value == 2 and self.__cards[1].value == 5 and self.__cards[0].value == 14:
                straight = True
                # Set the value of the ace to 1 and resort so hand comparisons work correctly
                self.__cards[0].value = 1
                self.__cards = sorted(self.__cards, reverse=True)
            if straight and flush:
                if self.__cards[0].value == 14:
                    self.__rank = Hand.ROYAL_FLUSH
                else:
                    self.__rank = Hand.STRAIGHT_FLUSH
            elif flush:
                self.__rank = Hand.FLUSH
            elif straight:
                self.__rank = Hand.STRAIGHT
            else:
                self.__rank = Hand.HIGH_CARD
            self.__values = [c.value for c in self.__cards]
        else:
            multiples = [m for m in sorted(merged.items(), key = operator.itemgetter(1), reverse = True) if m[1] > 1]
            if len(multiples) > 1:
                if multiples[0][1] == multiples[1][1]:
                    self.__rank = Hand.TWO_PAIRS
                else:
                    self.__rank = Hand.FULL_HOUSE 
            elif multiples:
                if multiples[0][1] > 3:
                    self.__rank = Hand.FOUR_OF_A_KIND
                elif multiples[0][1] == 3:
                    self.__rank = Hand.THREE_OF_A_KIND
                else:
                    self.__rank = Hand.ONE_PAIR
            mvalues = [m[0] for m in multiples]
            self.__values = mvalues + [c.value for c in self.__cards if c.value not in mvalues]

        return self.__rank
    def values(self):
        """Returns a list of card values for the hand
        
        Values for sets are provided first in order of importance and descending
        strength, followed by all additional card values in order of descending
        strength. Used for comparison and sorting.
        
        Examples:
            Full House  ['5S', '5D', '5H', '8S', '8C']
            Values = [5, 8]
            
            Two Pair    ['9D', '9S', '5H', '5C', 'KH']
            Values = [9, 5, 13]
            
            Straight    ['AS', '2H', '3C', '4C', '5H']
            Values = [5, 4, 3, 2, 1]
        """
        if not self.__values:
            self.rank()
        return self.__values
    @staticmethod
    def create_best_hand(cards):
        """Create the strongest possible poker hand
        
        Using the supplied cards, this will build the strongest possible five-
        card poker hand.
        
        Uses smart hand creation if more than five cards are specified.
        """
        
        if len(cards) <= 5:
            return Hand(cards)
        else:
            return Hand.create_best_hand_smart(cards)
        return false
    @staticmethod
    def create_best_hand_bruteforce(cards):
        """Create the strongest possible poker hand
        
        Builds every possible poker hand from the supplied set of cards, and
        returns the strongest result.
        """
        
        combos = unique_combinations(cards, 5)
        hands = [Hand(combo) for combo in combos]
        hands = sorted(hands, reverse=True)
        return hands[0]
    @staticmethod
    def create_best_hand_smart(cards):
        """Create the strongest possible poker hand
        
        Intelligently seeks out the strongest possible poker hand from the
        supplied set of cards using the following steps:
        
        * Find all flushes
        * Find all straights
        ** Return best hand present in both flushes and straights, if applicable
        * Find all sets
        ** Return best quads with top remaining card
        ** Find and return best full house
        ** Find and return best three of a kind with top remaining cards
        ** Find and return best two pair with top remaining cards
        ** Find and return best single pair with top remaining cards
        ** Return top 5 cards
        """
        cards = sorted([Card(c) for c in cards], reverse=True)
        
        # Get all flushes
        flushes = []
        for suit in Card.SUITS.values():
            suited = [str(c) for c in cards if c.suit == suit]
            if len(suited) >= 5:
                combos = unique_combinations(suited, 5)
                for combo in combos: flushes.append(Hand(combo))
        flushes = sorted(flushes, reverse=True)
        if (flushes and flushes[0].rank() >= Hand.STRAIGHT_FLUSH):
            # Straight flush! No need to check anything else
            return flushes[0]
        
        #Get all sets
        merged = {}
        for c in cards:
            if c.value in merged:
                merged[c.value] = merged[c.value] + 1
            else:
                merged[c.value] = 1
        multiples = [m for m in sorted(merged.items(), key = operator.itemgetter(1), reverse = True) if m[1] > 1]
        quads = [c[0] for c in multiples if c[1] == 4]
        quads = [c for c in cards if c.value in quads]
        trips = [c[0] for c in multiples if c[1] == 3]
        trips = [c for c in cards if c.value in trips]
        pairs = [c[0] for c in multiples if c[1] == 2]
        pairs = [c for c in cards if c.value in pairs]
        remaining = [c for c in cards if c.value not in [m[0] for m in multiples]]
        
        if quads:
            h = quads[:4]
            remaining = [c for c in cards if c.value not in [cc.value for cc in h]][:1]
            for r in remaining: h.append(r)
            return Hand([str(c) for c in h])
        if trips and pairs:
            # Get a full house together
            h = trips[:3]
            remaining = pairs[:2]
            for r in remaining: h.append(r)
            return Hand([str(c) for c in h])
        if flushes:
            # We've already got a flush, return it!
            return flushes[0]
        # Look for a straight!
        mvals = sorted(merged.keys(), reverse=True)
        for i in range(0, len(mvals) -4, 1):
            if (mvals[i] - mvals[i + 4]) == 4:
                # Regular straight
                h = [[c for c in cards if c.value == v][0] for v in mvals[i:i + 5]]
                return Hand([str(c) for c in h])
            elif 14 in [c.value for c in cards] and mvals[i + 1] == 5 and mvals[i + 4] == 2:
                # Ace low straight
                h = [[c for c in cards if c.value == v][0] for v in mvals[i + 1:i + 5]]
                h.append([c for c in cards if c.value == 14][0])
                return Hand([str(c) for c in h])
        
        if trips:
            h = trips[:3]
            remaining = [c for c in cards if c.value not in [cc.value for cc in h]][:2]
            for r in remaining: h.append(r)
            return Hand([str(c) for c in h])
        if pairs:
            if len(pairs) > 2:
                h = pairs[:4]
                remaining = [c for c in cards if c.value not in [cc.value for cc in h]][:1]
                for r in remaining: h.append(r)
                return Hand([str(c) for c in h])
            else:
                h = pairs
                remaining = [c for c in cards if c.value not in [cc.value for cc in h]][:3]
                for r in remaining: h.append(r)
                return Hand([str(c) for c in h])
        
        # High card, send the top 5 reverse-sorted cards
        return Hand([str(c) for c in cards[:5]])
    def __cmp__(self, other):
        """Compare hand rankings
        
        Compares this hand with another by first checking rank, and if they are
        equal in that regard, by their card values. Used for sorting.
        """
        
        result = cmp(self.rank(), other.rank())
        if (result == 0):
            # Compare hand values
            for i in range(len(self.values())):
                result = cmp(self.values()[i], other.values()[i])
                if (result != 0):
                    return result
        return result

class Player:
    def __init__(self, name):
        self.__name = name
        self.__hand = None
        self.__cards = []
        self.__community_cards = []
    def add_card(self, card, community=False):
        if community:
            self.__community_cards.append(card)
        else:
            self.__cards.append(card)
        
        # Rebuild and re-evaluate hand
        self.__hand = Hand.create_best_hand([str(c) for c in self.__community_cards + self.__cards])
    def hand(self):
        return self.__hand
    def __repr__(self):
        return '"{0}"\n\tCards: {1}\n\tHand: {2}'.format(self.__name, self.__cards, self.__hand)
    def __cmp__(self, other):
        return cmp(self.hand(), other.hand())
    
if __name__ == '__main__':
    deck = Deck()
    deck.shuffle()
    players = []
    for i in range(1,10):
        players.append(Player('Player {0}'.format(i)))
    community_cards = []
    deck.deal(2, players)
    deck.deal()
    community_cards.extend(deck.deal(3))
    deck.deal()
    community_cards.extend(deck.deal())
    deck.deal()
    community_cards.extend(deck.deal())
    for player in players:
        for card in community_cards:
            player.add_card(card, True)
    players = sorted(players, reverse=True)
    print 'Community', community_cards
    for player in players:
        print player