This is following up on my previous attempt, which was admittedly done fast and not-so-well.
This one is more tested, and abstracts the dirt away into an interface:
public interface IPokerHandEvaluator
{
bool IsPair(IEnumerable<PlayingCard> cards);
bool IsTwoPair(IEnumerable<PlayingCard> cards);
bool IsThreeOfKind(IEnumerable<PlayingCard> cards);
bool IsFourOfKind(IEnumerable<PlayingCard> cards);
bool IsFlush(IEnumerable<PlayingCard> cards);
bool IsFullHouse(IEnumerable<PlayingCard> cards);
bool IsStraight(IEnumerable<PlayingCard> cards);
bool IsStraightFlush(IEnumerable<PlayingCard> cards);
bool IsRoyalFlush(IEnumerable<PlayingCard> cards);
}
This is my current implementation - it correctly evaluates all 5-card hands, and I've tested it correctly evaluates a Straight Flush (ace low) with a 7-card hand as well:
public class PokerHandEvaluator : IPokerHandEvaluator
{
private IEnumerable<IGrouping<CardNominalValue, PlayingCard>> GroupByNominalValue(IEnumerable<PlayingCard> cards)
{
return cards.GroupBy(card => card.NominalValue);
}
private IEnumerable<IGrouping<CardNominalValue, PlayingCard>> Pairs(IEnumerable<PlayingCard> cards)
{
return GroupByNominalValue(cards).Where(group => group.Count() == 2);
}
private IEnumerable<IGrouping<CardNominalValue, PlayingCard>> Triplets(IEnumerable<PlayingCard> cards)
{
return GroupByNominalValue(cards).Where(group => group.Count() == 3);
}
private IEnumerable<IGrouping<CardSuit, PlayingCard>> Suits(IEnumerable<PlayingCard> cards)
{
return cards.GroupBy(card => card.Suit);
}
public bool IsPair(IEnumerable<PlayingCard> cards)
{
return Pairs(cards).Count() == 1 && !Triplets(cards).Any();
}
public bool IsTwoPair(IEnumerable<PlayingCard> cards)
{
return Pairs(cards).Count() == 2;
}
public bool IsThreeOfKind(IEnumerable<PlayingCard> cards)
{
return Triplets(cards).Count() == 1 && !Pairs(cards).Any();
}
public bool IsFourOfKind(IEnumerable<PlayingCard> cards)
{
return GroupByNominalValue(cards).Any(group => group.Count() >= 4);
}
public bool IsFlush(IEnumerable<PlayingCard> cards)
{
return Suits(cards).Any(suit => suit.Count() >= 5);
}
public bool IsFullHouse(IEnumerable<PlayingCard> cards)
{
return Pairs(cards).Any() && Triplets(cards).Any();
}
public bool IsStraight(IEnumerable<PlayingCard> cards)
{
var distinctValues = GroupByNominalValue(cards);
return IsStraightNoAce(distinctValues) || IsStraightWithAce(distinctValues).Item1;
}
private enum AceKind
{
AceLow,
AceHigh
}
private Tuple<bool, AceKind?> IsStraightWithAce(IEnumerable<IGrouping<CardNominalValue, PlayingCard>> distinctValues)
{
// if has no ace, don't bother
if (!distinctValues.Any(group => group.Key == CardNominalValue.Ace)) return new Tuple<bool, AceKind?>(false, null);
var aceRemoved = distinctValues.Where(group => group.Key != CardNominalValue.Ace)
.Select(group => (int)group.Key);
if (aceRemoved.OrderBy(value => value).Take(4).Max() == (int)CardNominalValue.Five) return new Tuple<bool, AceKind?>(true, AceKind.AceLow);
if (aceRemoved.OrderByDescending(value => value).Take(4).Min() == (int)CardNominalValue.Ten) return new Tuple<bool, AceKind?>(true, AceKind.AceHigh);
return new Tuple<bool, AceKind?>(false, null);
}
private Tuple<bool, AceKind?> IsStraightWithAce(IEnumerable<PlayingCard> cards)
{
return IsStraightWithAce(GroupByNominalValue(cards));
}
private bool IsStraightNoAce(IEnumerable<IGrouping<CardNominalValue, PlayingCard>> distinctValues)
{
// if has ace, don't bother
if (distinctValues.Any(group => group.Key == CardNominalValue.Ace)) return false;
var sortedValues = distinctValues.OrderBy(group => (int)group.Key);
var possibleLows = sortedValues.Take(5 - sortedValues.Count() + 1);
var skippedCards = 0;
foreach (var possibleLow in possibleLows)
{
var isStraight = sortedValues.Skip(skippedCards)
.Take(5)
.Max(group => (int)group.Key) == (int)possibleLow.Key + 4;
if (isStraight) return true;
skippedCards++;
}
return false;
}
public bool IsStraightFlush(IEnumerable<PlayingCard> cards)
{
return IsFlush(cards) && IsStraight(cards);
}
public bool IsRoyalFlush(IEnumerable<PlayingCard> cards)
{
var straightWithAceResult = IsStraightWithAce(cards);
var result = IsFlush(cards) && straightWithAceResult.Item1 && straightWithAceResult.Item2 == AceKind.AceHigh;
return result;
}
}
So the PokerGame
class is now much more focused at knowing what the rules are and in which order they're evaluated; the dictionary of rules/hands has turned into a private field, there's no need to expose it at all - given the new PokerHand EvaluateHand()
method:
public class PokerGame
{
private readonly IPokerHandEvaluator _evaluator;
private readonly IDictionary<PokerHand, Func<IEnumerable<PlayingCard>, bool>> _hands;
public PokerGame(IPokerHandEvaluator evaluator)
{
_evaluator = evaluator;
_hands = new Dictionary<PokerHand, Func<IEnumerable<PlayingCard>, bool>>
{
{ PokerHand.HighCard, cards => true },
{ PokerHand.Pair, _evaluator.IsPair },
{ PokerHand.TwoPair, _evaluator.IsTwoPair },
{ PokerHand.ThreeOfKind, _evaluator.IsThreeOfKind },
{ PokerHand.Straight, _evaluator.IsStraight },
{ PokerHand.Flush, _evaluator.IsFlush },
{ PokerHand.FullHouse, _evaluator.IsFullHouse },
{ PokerHand.FourOfKind, _evaluator.IsFourOfKind },
{ PokerHand.StraightFlush, _evaluator.IsStraightFlush },
{ PokerHand.RoyalFlush, _evaluator.IsRoyalFlush }
};
}
public PokerHand EvaluateHand(IEnumerable<PlayingCard> cards)
{
return _hands.OrderByDescending(hand => (int)hand.Key)
.First(hand => hand.Value.Invoke(cards)).Key;
}
}
For this to work I had to add PokerHand.HighCard
:
public enum PokerHand
{
HighCard,
Pair,
TwoPair,
ThreeOfKind,
Straight,
Flush,
FullHouse,
FourOfKind,
StraightFlush,
RoyalFlush
}
The CardSuit
enum was reordered, in prevision for when I want to modify this code to compare two hands - the suit would be a tiebreaker:
public enum CardSuit
{
Clubs,
Diamonds,
Hearts,
Spades
}
Definitely cleaner, except I must admit getting the IsStraight
method ..straight with a hand of not necessarily 5 cards got messier than I wanted it to be. What do you think?