Using LINQ and C# to Evaluate Poker Hands

The elegance of LINQ (Language Integrated Query) in C# positions it as an effective tool for addressing complex challenges through concise and readable code. Evaluating poker hands serves as an excellent practical application for showcasing LINQ’s capabilities, as it entails the analysis of collections of cards, grouping based on various properties, and identifying patterns.

Representing Cards and Hands

Prior to engaging in LINQ operations, it is essential to establish suitable data structures. A common approach is to represent cards by utilizing enums for suits and ranks:

public enum Suit { Clubs, Diamonds, Hearts, Spades }

public enum Rank { Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace }

public class Card
{
    public Suit Suit { get; }
    public Rank Rank { get; }

    public Card(Suit suit, Rank rank)
    {
        Suit = suit;
        Rank = rank;
    }
}

A poker hand consists of a set of five cards.

public class PokerHand
{
    public List<Card> Cards { get; init; }

    public PokerHand(List<Card> cards)
    {
        if (cards.Count != 5)
        {
            throw new ArgumentException("A poker hand must contain exactly 5 cards");
        }

        Cards = cards;
    }
}

Evaluating Hand Ranks with LINQ

In this section, we will explore how LINQ can be utilized to evaluate various poker hand combinations.

Checking for a Flush

A flush is defined as a hand in which all cards belong to the same suit. This can be effectively determined using LINQ:

     static bool IsFlush() => Cards
        .Select(card => card.Suit)
        .Distinct()
        .Count() == 1;

Verifying a Straight

A straight is defined as a hand containing five cards with consecutive ranks.

    static bool IsStraight()
    {
        List<int> orderedRanks = [.. Cards
            .Select(card => (int)card.Rank)
            .Order()];

        // Handle special case: A-5 straight (Ace counts as 1)
        if (orderedRanks.SequenceEqual([2, 3, 4, 5, 14]))
        {
            return true;
        }

        // Check if ranks form a sequence
        return orderedRanks.Last() - orderedRanks.First() == 4 && orderedRanks.Distinct().Count() == 5;
    }

Identifying Card Combinations

The GroupBy method in LINQ is an excellent tool for recognizing cards of the same rank.

    static Dictionary<string, bool> EvaluateGroups()
    {
        var groups = Cards.GroupBy(card => card.Rank)
                         .Select(g => new { Rank = g.Key, Count = g.Count() })
                         .OrderByDescending(g => g.Count)
                         .ToList();

        return new Dictionary<string, bool>
        {
            ["FourOfAKind"] = groups.Any(g => g.Count == 4),
            ["ThreeOfAKind"] = groups.Any(g => g.Count == 3),
            ["OnePair"] = groups.Count(g => g.Count == 2) == 1,
            ["TwoPair"] = groups.Count(g => g.Count == 2) == 2,
            ["FullHouse"] = groups.Any(g => g.Count == 3) && groups.Any(g => g.Count == 2)
        };
    }

Comprehensive Hand Evaluation

In summary, we can develop a thorough approach to assess and rank poker hands effectively:

    static string GetHandRank()
    {
        bool isFlush = IsFlush();
        bool isStraight = IsStraight();

        Dictionary<string, bool> groupResults = EvaluateGroups();

        return (isFlush, isStraight, groupResults["FourOfAKind"], groupResults["FullHouse"], groupResults["ThreeOfAKind"], groupResults["TwoPair"], groupResults["OnePair"]) switch
        {
            (true, true, _, _, _, _, _) when Cards.Select(c => c.Rank).Order().SequenceEqual([Rank.Ten, Rank.Jack, Rank.Queen, Rank.King, Rank.Ace]) => "Royal Flush",
            (true, true, _, _, _, _, _) => "Straight Flush",
            (_, _, true, _, _, _, _) => "Four of a Kind",
            (_, _, _, true, _, _, _) => "Full House",
            (true, _, _, _, _, _, _) => "Flush",
            (_, true, _, _, _, _, _) => "Straight",
            (_, _, _, _, true, _, _) => "Three of a Kind",
            (_, _, _, _, _, true, _) => "Two Pair",
            (_, _, _, _, _, _, true) => "One Pair",
            _ => "High Card"
        };
    }

Comparing Hands

LINQ excels in the comparison of hands that are of the same rank. For instance, consider the comparison of two “Two Pair” hands:

   static int CompareTwoPairs(PokerHand other)
   {
       var thisGroups = Cards.GroupBy(card => card.Rank)
                            .Select(g => new { Rank = g.Key, Count = g.Count() })
                            .OrderByDescending(g => g.Count)
                            .ThenByDescending(g => g.Rank)
                            .ToList();

       var otherGroups = other.Cards.GroupBy(card => card.Rank)
                                 .Select(g => new { Rank = g.Key, Count = g.Count() })
                                 .OrderByDescending(g => g.Count)
                                 .ThenByDescending(g => g.Rank)
                                 .ToList();

       // Compare higher pair
       int higherPairComparison = ((int)thisGroups[0].Rank).CompareTo((int)otherGroups[0].Rank);
   
       if (higherPairComparison != 0)
       {
           return higherPairComparison;
       }

       // Compare lower pair
       int lowerPairComparison = ((int)thisGroups[1].Rank).CompareTo((int)otherGroups[1].Rank);
 
       if (lowerPairComparison != 0)
       {
           return lowerPairComparison;
       }

       // Compare kicker
       return ((int)thisGroups[2].Rank).CompareTo((int)otherGroups[2].Rank);
   }

Conclusion

The declarative syntax offered by LINQ significantly enhances the clarity and intuitiveness of poker hand evaluation. By utilizing methods such as GroupBy, Select, OrderBy, and Distinct, we can convert what would typically be complex imperative code into succinct expressions that align closely with our conceptual understanding of poker hands.

This methodology not only fosters more maintainable code but also improves the transparency of the underlying logic involved in poker hand evaluation. The integration of LINQ with object-oriented design in C# provides a sophisticated framework for tackling issues related to collections and pattern recognition, making it a highly suitable choice for implementations involving card games.