euler/054/poker.py
Correl Roush 71973e170f Removed pointless 5 card limitation from the static factory method
git-svn-id: file:///srv/svn/euler@15 e5f4c3ec-3c0c-11df-b522-21efaa4426b5
2010-04-01 04:44:32 +00:00

309 lines
No EOL
11 KiB
Python

import operator
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 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}",
[str(c.value) + c.suit for c in 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
else:
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