#!/usr/bin/env python
"""

 Poker hand implementation, 
 an extended version of exercise 14, page 380 of Zelle's "Intro Python"

 Prints lots of tests if run from the command line,
 including median 'winning' hand of four hands dealt from one deck
 (which is about a pair of queens); see the end of this file for the output.

 Here's what you can do with the classes defined in this file.

    # -- PokerCard --
    card = PokerCard(rank, suit) # rank=1..13, suit='c', 'd', 's', or 'h'
         = PokerCard()           # or this way for a random card
    print card.getRank()         # returns 2 (duece) ... 14 (ace); or card.rank
    print card.getSuit()         # returns 'c', 'd', 'h', or 's'; or card.suit
    print card                   # e.g. 'Three of Clubs'
    card1 < card2                # ordering by (i) rank, (ii) suit
    sort(cards)                  # sort a list of cards, low to high, in place

    # -- PokerDeck --
    deck = PokerDeck()           # a 52-card deck
    print len(deck)              # number of cards left in the deck
    card = deck.deal_a_card()    # return and remove a random card from deck
    list = deck.get_n_cards(n)   # ditto for a list of cards

    # -- PokerHand --
    hand = PokerHand(string)     # make hand from e.g. '1 s, 2 h, 3 d, 4 c, 5 s'
         = PokerHand(cards=list) #  ... from a list of PokerCard's
         = PokerHand(deck=d)     #  ... from given deck (and remove 'em from deck)
    hand = PokerHand()           #  ... from a new deck
    hand1 < hand2                # using (losing hand < winning hand)
    sort(hands)                  # sort a list of hands, low to high, in place
    print hand                   # e.g. 'Straight Flush :
                                         Queen of Hearts, Jack of Hearts, ...'
    print hand.category          # e.g. 'Two Pair'
    print hand.description       # e.g. 'Sixes over Fours'

 Jim Mahoney, Nov 11 2010, GPL
"""

from random import randint

# These definitions are used by several
# of the classes so I've left 'em here outside of any one class.
suits = ('d', 'c', 'h', 's')   # i.e. Diamonds, Clubs, Hearts, Spades
ranks = range(2, 15)           # 2 ... 10, Jack=11, Queen=12, King=13, Ace=14 (high)
suit_names = { 'd' : 'Diamonds',
               'c' : 'Clubs',
               'h' : 'Hearts',
               's' : 'Spades', }
rank_names = { 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',   }

class PokerCard:
    """ A playing card. Ace is high. """
    def __init__(self, rank=False, suit=False):
        """ PokerCard(rank, suit)
             rank is 1..10 for Ace...10, and (11,12,13) for jack, queen, king.
             suits are single lower case letters, ('c', 'd', 'h', 's').
             Aces may also be input as rank=14 (one higher than king).
            PokerCard() returns a random card."""
        if rank == 1:     # If an ace is input as 1, then
            rank = 14     # turn it into 14 so it's bigger than king=13
        if not rank:
            rank = randint(2,14)
        if not suit:
            suit = suits[randint(0,3)]
        if rank not in ranks:
            raise Exception("illegal card rank '"+str(rank)+"'")
        if suit not in suits:
            raise Exception("illegal suit '"+str(suit)+"'")
        self.rank = rank
        self.suit = suit
    def getRank(self):
        """ return rank of card as integer n, 2<=n<=14 """
        return self.rank
    def getSuit(self):
        """ return suit of card as a letter in ('d', 'c', 'h', 's')"""
        return self.suit
    def __str__(self):
        """ Display card as a string, e.g. 'Ace of Spades'. """
        return rank_names[int(self.rank)] + ' of ' + suit_names[self.suit]
    def __cmp__(card1, card2):
        """ Compare cards by rank, and within rank by suit.
            Note that this function automatically enables cards.sort()
            and all the comparison operators like <, >, ==, etc. """
        (rank1, rank2) = (card1.getRank(), card2.getRank())
        if rank1 != rank2:
            return cmp(rank1, rank2)
        else:
            return cmp(card1.getSuit(), card2.getSuit())

class PokerDeck:
    """ 52 playing cards """
    def __init__(self):
        self.cards = []
        for rank in ranks:
            for suit in suits:
                self.cards.append(PokerCard(rank, suit))
        # terse version:
        # self.cards = [PokerCard(rank,suit) for rank in ranks for suit in suits]
    def deal_a_card(self):
        how_many_in_deck = len(self.cards)
        random_index = randint(0, how_many_in_deck - 1)
        card = self.cards.pop(random_index)   # get it and remove it from list
        return card
        # terse version:
        # return self.cards.pop(randint(0,len(self.cards)-1))
    def deal_n_cards(self, n_cards):
        cards = []
        for n in range(n_cards):
            cards.append( self.deal_a_card() )
        return cards
        # terse version:
        # return [self.deal_a_card() for i in range(n_cards)]
    def __len__(self):
        return len(self.cards)

class PokerHand:
    types = {
        'Royal Flush' :    10,   # name : value ; higher is better
        'Straight Flush' :  9,
        'Four of a Kind' :  8,
        'Full House' :      7,
        'Flush' :           6,
        'Straight' :        5,
        'Three of a Kind' : 4,
        'Two Pair' :        3,
        'Pair' :            2,
        'High' :            1    # e.g. 'King high'
        }
    """ Five playing cards with sorting by which hand wins at poker. """
    def __init__(self, string='', cards=[], deck=False):
        """ Create a hand from either
              1) a string like '1 s, 1 d, 1 c', or
              2) a list of PokerCard's, or
              3) 5 cards taken from the given deck
              3) create a random hand if none of those specified. """
        self.cards = []
        if string:
            for rank_suit in string.split(','):      # e.g. rank_suit = '1 s'
                rank_suit_tuple = rank_suit.split()  # e.g. ('1', 's')
                if len(rank_suit_tuple)==2:
                    (rank, suit) = rank_suit_tuple   # e.g. rank='1', suit='s'
                    self.cards.append( PokerCard(int(rank), suit) )
        elif cards:
            for card in cards:
                self.cards.append(card)
        elif deck:
            for i in range(5):
                self.cards.append( deck.deal_a_card() )
        else:
            deck = PokerDeck()
            for i in range(5):
                self.cards.append( deck.deal_a_card() )
        self.cards.sort()     # low to high
        self.cards.reverse()  # high to low
        self.calculate_frequencies()
        self.calculate_type()
    def __str__(self):
        result = ''
        for card in self.cards:
           result += str(card) + ", "
        return self.description() + ' : ' + result[:-2]
    def is_flush(self):
        suit = self.cards[0].getSuit()
        for card in self.cards:
            if card.getSuit() != suit:
                return False
        return True
    def is_straight(self):
        high = self.cards[0].getRank()
        if (high == 14 and self.cards[1].getRank() != 13):
            high = 6
        for i in range(1,5):
            if self.cards[i].getRank() != high - i:
                return False
        return True
    def pluralFreq(self, which):
        """ Return plural form of a card rank, e.g. 'Sixes' or 'Aces'.
            which is 0, 1, ... for rank with most duplicates, 2nd most, etc """
        name = rank_names[self.freq[which][1]]
        if name == 'Six':
            return 'Sixes'
        else:
            return name + 's'
    def description(self):
        """ Return string description given self.type and self.freq """
        if self.type == 'Four of a Kind':
            return 'Four ' + self.pluralFreq(0)
        elif self.type == 'Three of a Kind':
            return 'Three ' + self.pluralFreq(0)
        elif self.type == 'Pair':
            return 'Two ' + self.pluralFreq(0)
        elif self.type == 'Two Pair':
            return self.pluralFreq(0) + ' over ' + self.pluralFreq(1)
        elif self.type == 'High':
            return rank_names[self.cards[0].getRank()] + ' High'
        else:
            return self.type
    def hand_value(self):
        return PokerHand.types[self.type]
    def calculate_type(self):
        """ Return and store self.type string describing type of hand.
            self.freq must be already set via calculate_frequencies. """
        is_flush = self.is_flush();
        is_straight = self.is_straight();
        if (is_flush and is_straight):
            self.type = 'Royal Flush'
        elif self.freq[0][0] == 4:
            self.type = 'Four of a Kind'
        elif self.freq[0][0] == 3 and self.freq[1][0] == 2:
            self.type = 'Full House'
        elif is_flush:
            self.type = 'Flush'
        elif is_straight:
            self.type = 'Straight'
        elif self.freq[0][0] == 3:
            self.type = 'Three of a Kind'
        elif self.freq[0][0] == 2 and self.freq[1][0] == 2:
            self.type = 'Two Pair'
        elif self.freq[0][0] == 2:
            self.type = 'Pair'
        else:
            self.type = 'High'
        return self.type
    def calculate_frequencies(self):
        """ Compute (frequency, rank) pairs, sorted by highest frequency.
            Save result in self.freq and return it. """
        # Here's how it works:
        # A five, a ten, and three aces will get grouped into
        #  (a) frequencies = { 10:1, 5:1, 1:3 }  #  rank: frequency
        #  (b) freq.items converts to [ (10,1), (5,1), (1,3) ]
        #  (c) swapping each tuple becomes [ (1,10), (1,5), (3,1) ]
        #  (d) sorting low-to-high and reversing : [ (3,1), (1,5), (1,10) ]
        # which means "3 aces, 1 five, 1 ten".
        freq = {}                                  
        for card in self.cards:
            rank = card.getRank()
            freq[rank] = freq.get(rank,0) + 1
        self.freq = freq.items()
        for i in range(len(self.freq)):
            self.freq[i] = ( self.freq[i][1], self.freq[i][0] )
        self.freq.sort()
        self.freq.reverse()
        return self.freq
    def __cmp__(self, other):
        # See http://en.wikipedia.org/wiki/Rank_of_hands_%28poker%29
        if self.hand_value() != other.hand_value():
            return cmp(self.hand_value(), other.hand_value())
        else:
            for i in range(len(self.freq)):
                if self.freq[i][1] != other.freq[i][1]:
                    return cmp(self.freq[i][1], other.freq[i][1])
            return 0

def run_tests():
    """ Print out stuff to see if all this works. """
    print "\n-- test hand printing from each type  --"
    hands = [ '1 s, 13 s, 12 s, 11 s, 10 s',
              '8 h, 9 h, 10 h, 11 h, 12 h',
              '2 s, 3 s, 4 c, 5 c, 6 d',
              '2 d, 4 d, 6 d, 8 d, 10 d',
              '3 d, 3 c, 3 h, 12 s, 12 d',
              '3 c, 4 h, 7 h, 7 d, 7 c',
              '10 h, 10 d, 12 h, 12 s, 1 c',
              '6 h, 6 d, 10 c, 12 s, 7 h',
              '1 d, 2 d, 4 h, 5 d, 8 c',
              '1 d, 5 d, 4 c, 3 h, 2 d',
              ]
    for hand in hands:
        print PokerHand(string=hand)
    #
    print "\n-- test hand ordering --"
    two_hands = [ ('6 h, 6 d, 10 s, 11 c, 12 d', '4 h, 4 d, 10 c, 11 d, 1 d' ),
                  ('6 h, 6 d, 4 h, 4 d, 3 s', '6 s, 6 c, 4 s, 4 c, 2 s' ),
                  ('6 h, 5 c, 4 h, 3 h, 2 h', '6 d, 5 d, 4 d, 3 c, 2 d' ),
                  ('1 h, 2 h, 3 c, 4 h, 5 h', '6 h, 6 d, 6 c, 6 s, 10 c'),
                  ('1 h, 2 h, 3 h, 4 h, 5 h', '6 h, 6 d, 6 c, 6 s, 10 c'),
                  ]
    for two_hand in two_hands:
        first = PokerHand(string=two_hand[0])
        second = PokerHand(string=two_hand[1])
        if first > second:
            comparison = "\n  beats\n"
        elif first < second:
            comparison = "\n  loses to\n"
        else:
            comparison = "\n  ties\n"            
        print str(first)+comparison+"    "+str(second)
    #
    n_random = 10
    print "\n-- test", n_random, "random hands --"
    for i in range(n_random):
        print PokerHand()
    #
    print "\n-- test 10 hands from the same deck --"
    deck = PokerDeck()
    hands = []
    for i in range(10):
        hands.append(PokerHand(deck=deck))
    hands.sort()
    hands.reverse()
    print "After dealing there are", len(deck), "cards left in the deck."
    print "Best to worst hands:"
    for hand in hands:
        print hand
    #
    n_many = 10000
    print "\n-- Generating", n_many, "hands and four-hand games ..."
    hands = []
    winning_hands = []
    for i in range(n_many):
        hands.append(PokerHand())
        four_hands = []
        deck = PokerDeck()
        for j in range(4):
            four_hands.append(PokerHand(deck=deck))
        four_hands.sort()
        winning_hands.append(four_hands[-1])
        if i % 1000 == 0:
            print "  i = ",i
    print "done.  Sorting..."
    hands.sort()
    hands.reverse()
    print "best:", hands[0]
    print "median:", hands[n_many/2]
    print "worst:", hands[-1]    
    winning_hands.sort()
    winning_hands.reverse()
    print "median winner of four hands:"
    print "  ", winning_hands[n_many/2]

if __name__ == '__main__':
    run_tests()

"""
 ========= output =====================================

$ ./poker.py 

-- test hand printing from each type  --
Royal Flush : Ace of Spades, King of Spades, Queen of Spades, Jack of Spades, Ten of Spades
Straight Flush : Queen of Hearts, Jack of Hearts, Ten of Hearts, Nine of Hearts, Eight of Hearts
Straight : Six of Diamonds, Five of Clubs, Four of Clubs, Three of Spades, Two of Spades
Flush : Ten of Diamonds, Eight of Diamonds, Six of Diamonds, Four of Diamonds, Two of Diamonds
Full House : Queen of Spades, Queen of Diamonds, Three of Hearts, Three of Diamonds, Three of Clubs
Three Threes : Seven of Hearts, Seven of Diamonds, Seven of Clubs, Four of Hearts, Three of Clubs
Queens over Tens : Ace of Clubs, Queen of Spades, Queen of Hearts, Ten of Hearts, Ten of Diamonds
Pair of Sixes : Queen of Spades, Ten of Clubs, Seven of Hearts, Six of Hearts, Six of Diamonds
Ace High : Ace of Diamonds, Eight of Clubs, Five of Diamonds, Four of Hearts, Two of Diamonds
Straight : Ace of Diamonds, Five of Diamonds, Four of Clubs, Three of Hearts, Two of Diamonds

-- test hand ordering --
Pair of Sixes : Queen of Diamonds, Jack of Clubs, Ten of Spades, Six of Hearts, Six of Diamonds
  beats
    Pair of Fours : Ace of Diamonds, Jack of Diamonds, Ten of Clubs, Four of Hearts, Four of Diamonds
Sixs over Fours : Six of Hearts, Six of Diamonds, Four of Hearts, Four of Diamonds, Three of Spades
  beats
    Sixs over Fours : Six of Spades, Six of Clubs, Four of Spades, Four of Clubs, Two of Spades
Straight : Six of Hearts, Five of Clubs, Four of Hearts, Three of Hearts, Two of Hearts
  ties
    Straight : Six of Diamonds, Five of Diamonds, Four of Diamonds, Three of Clubs, Two of Diamonds
Straight : Ace of Hearts, Five of Hearts, Four of Hearts, Three of Clubs, Two of Hearts
  loses to
    Four of a Kind : Ten of Clubs, Six of Spades, Six of Hearts, Six of Diamonds, Six of Clubs
Straight Flush : Ace of Hearts, Five of Hearts, Four of Hearts, Three of Hearts, Two of Hearts
  beats
    Four of a Kind : Ten of Clubs, Six of Spades, Six of Hearts, Six of Diamonds, Six of Clubs

-- test 10 random hands --
Queen High : Queen of Spades, Jack of Hearts, Nine of Spades, Five of Hearts, Two of Clubs
Pair of Nines : Nine of Hearts, Nine of Diamonds, Eight of Spades, Four of Diamonds, Three of Clubs
Pair of Sixes : King of Diamonds, Queen of Clubs, Six of Spades, Six of Diamonds, Four of Diamonds
Queen High : Queen of Clubs, Ten of Hearts, Nine of Spades, Five of Spades, Three of Hearts
King High : King of Hearts, Jack of Clubs, Ten of Diamonds, Five of Spades, Two of Clubs
Ace High : Ace of Clubs, King of Clubs, Queen of Hearts, Nine of Spades, Eight of Clubs
Pair of Jacks : King of Hearts, Jack of Diamonds, Jack of Clubs, Ten of Spades, Four of Hearts
King High : King of Clubs, Queen of Spades, Seven of Diamonds, Five of Clubs, Four of Diamonds
Jack High : Jack of Spades, Ten of Hearts, Nine of Spades, Five of Hearts, Four of Diamonds
Jack High : Jack of Hearts, Ten of Hearts, Six of Diamonds, Three of Spades, Two of Spades

-- test 10 hands from the same deck --
After dealing there are 2 cards left in the deck.
Best to worst hands:
Pair of Jacks : Queen of Hearts, Jack of Hearts, Jack of Diamonds, Seven of Clubs, Four of Spades
Pair of Nines : Ace of Clubs, Nine of Hearts, Nine of Diamonds, Eight of Spades, Five of Hearts
Pair of Fours : Ace of Spades, Ten of Spades, Four of Diamonds, Four of Clubs, Two of Hearts
Ace High : Ace of Hearts, King of Spades, Jack of Spades, Seven of Spades, Five of Diamonds
Ace High : Ace of Diamonds, Seven of Hearts, Four of Hearts, Three of Spades, Two of Spades
King High : King of Clubs, Queen of Clubs, Eight of Clubs, Seven of Diamonds, Three of Hearts
King High : King of Diamonds, Ten of Hearts, Eight of Hearts, Six of Diamonds, Two of Diamonds
King High : King of Hearts, Nine of Clubs, Eight of Diamonds, Six of Spades, Three of Clubs
Queen High : Queen of Diamonds, Ten of Clubs, Nine of Spades, Six of Clubs, Five of Clubs
Queen High : Queen of Spades, Ten of Diamonds, Six of Hearts, Five of Spades, Three of Diamonds

-- Generating 10000 hands and four-hand games ...
  i =  0
  i =  1000
  i =  2000
  i =  3000
  i =  4000
  i =  5000
  i =  6000
  i =  7000
  i =  8000
  i =  9000
done.  Sorting...
best: Four of a Kind : Ace of Spades, Ace of Hearts, Ace of Diamonds, Ace of Clubs, Jack of Clubs
median: Ace High : Ace of Spades, King of Hearts, Queen of Clubs, Jack of Spades, Seven of Diamonds
worst: Seven High : Seven of Clubs, Five of Hearts, Four of Spades, Three of Clubs, Two of Clubs
median winner of four hands:
   Pair of Queens : Queen of Hearts, Queen of Diamonds, Jack of Spades, Seven of Hearts, Six of Diamonds
 
"""

syntax highlighted by Code2HTML, v. 0.93pm6