2 of 5
added mroe explanation
Mathieu Guindon
  • 75k
  • 18
  • 189
  • 461

Poker Hand Evaluator, take 2

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?

Mathieu Guindon
  • 75k
  • 18
  • 189
  • 461