Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game

SOLID Poker - Part 2 - Compare Poker Hands

5.00/5 (15 votes)
7 Apr 2017CPOL4 min read 29.7K   272  
This article continues with the development of the SOLID Poker project, and covers functionality to Compare and Validate Poker Hands.

Image 1

Contents

Series of Articles

This project and documentation will span numerous articles to ensure every part of the project receives the necessary time and attention. This provides good structure and makes the information easier to consume.

Each article builds and depends on the preceding articles, and each article will further develop the code from preceding articles. Preceding articles give necessary background and context to the current article.

Following is the complete list of articles of this series published thus far:

Implementation of Principles, Patterns & Practices

At this early stage of the SOLID Poker Project, only the DRY & SOLID Principles are being implemented.

NOTE: SOLID Principles don't apply to private methods, but the private methods do illustrate the DRY Principle.

Project Code: Commentary

The in-code commentary is extensive and the Variable & Function Names are descriptive. In order not to duplicate content/commentary, additional commentary about the code is only given if there is in fact something to add.

Please comment on the article if something is unclear or needs to be added.

Project Code: Contracts

All the SOLID Poker Contracts are located in the SOLIDPoker.Contract project.

Below is the latest version of IPokerHandAssessor. Care is taken to ensure the functions are cohesive, fulfilling the Single Responsibility of assessing Poker Hands; this is in accordance with the first SOLID Principle, the Single Responsibility Principle.

C#
public interface IPokerHandAssessor
{
    List<PokerHandComparisonItem> ComparePokerHands(params PokerHand[] pokerHands);
    PokerHandType DeterminePokerHandType(PokerHand pokerHand);
    List<PokerHandValidationFault> ValidatePokerHands(params PokerHand[] pokerHands);
}

The below data object is a new addition and is used to provide a comprehensive result when comparing poker hands.

C#
public class PokerHandComparisonItem
{
    public PokerHand Hand { get; set; }
    public PokerHandType HandType { get; set; }
    public int Rank { get; set; }

    public PokerHandComparisonItem()
    { }
    public PokerHandComparisonItem(PokerHand Hand)
    {
        this.Hand = Hand;
    }
    public PokerHandComparisonItem(PokerHand Hand, PokerHandType HandType)
    {
        this.Hand = Hand;
        this.HandType = HandType;
    }
    public PokerHandComparisonItem(PokerHand Hand, PokerHandType HandType, int Rank)
    {
        this.Hand = Hand;
        this.HandType = HandType;
        this.Rank = Rank;
    }
}

The below enum and data object are also new additions and are used to provide a comprehensive result when validating poker hands.

C#
public enum PokerHandValidationFaultDescription
{
    HasDuplicateCards = 1,
    JokersNotAllowed = 2,
    WrongCardCount = 3
}
public class PokerHandValidationFault
{
    public PokerHand Hand { get; set; }
    public PokerHandValidationFaultDescription FaultDescription { get; set; }
}

Project Code: Check Poker Hands For Duplicate Cards

This function is only used by the ValidatePokerHands function; so it is not being reused yet. This function was written separately simply to make the code easier to Read & Maintain.

C#
/// <summary>
/// Checks the poker hands for duplicate cards.
/// Returns the first poker hands found with duplicate cards.
/// If a poker hand contains duplicate cards of itself, 
/// then only that poker hand will be returned.
/// If cards are duplicated between two poker hands, 
/// then both these poker hands will be returned.
/// </summary>
/// <param name="pokerHands">Poker hands to evaluate.</param>
/// <returns>Poker hands that contain duplicate cards.</returns>
PokerHand[] CheckPokerHandsForDuplicateCards(params PokerHand[] pokerHands)
{
    for (int i = 0; i < pokerHands.Length; i++)
    {
        //Check whether the poker hand contains duplicate cards of itself.
        PokerHand pokerHand = pokerHands[i];
        if (pokerHand.GroupBy(card => new { card.Suit, card.Value }).Count() 
                    != pokerHand.Count)
            return new PokerHand[] { pokerHand };

        for (int ii = i + 1; ii < pokerHands.Length; ii++)
        {
            //Check whether cards are duplicated between two poker hands.
            if (new PokerHand[] { pokerHand, pokerHands[ii] }.SelectMany(hand => hand)
                .GroupBy(card => new { card.Suit, card.Value }).Count() !=
                pokerHand.Count + pokerHands[ii].Count)
                return new PokerHand[] { pokerHand, pokerHands[ii] };
        }
    }
    return new PokerHand[0];
}

Project Code: Validate Poker Hands

This function fulfils part of the IPokerHandAssessor Contract.

C#
/// <summary>
/// Checks that poker hands have 5 cards, no jokers and no duplicate cards.
/// Retuns the first validation faults found. 
/// Does not continue with further validations after validation faults are found.
/// </summary>
/// <param name="pokerHands">The poker hands to validate.</param>
/// <returns>List of Poker Hand Validation Faults</returns>
public List<PokerHandValidationFault> ValidatePokerHands(params PokerHand[] pokerHands)
{
    List<PokerHandValidationFault> faults = new List<PokerHandValidationFault>();

    //Check card count.
    var pokerHandsWithWrongCardCount = 
                pokerHands.Where(hand => hand.Count != 5).ToList();
    if (pokerHandsWithWrongCardCount.Count > 0)
    {
        pokerHandsWithWrongCardCount.ForEach(hand => 
        faults.Add(new PokerHandValidationFault
        {
            FaultDescription = PokerHandValidationFaultDescription.WrongCardCount,
            Hand = hand
        }));
        return faults;
    }

    //Look for jokers.
    foreach (PokerHand hand in pokerHands)
    {
        var jokers = hand.Where(card => card.Suit == CardSuit.Joker);
        if (jokers.Count() > 0)
        {
            faults.Add(new PokerHandValidationFault
            {
                FaultDescription = 
                PokerHandValidationFaultDescription.JokersNotAllowed,
                Hand = hand
            });
            return faults;
        }
    }

    //Look for duplicates.
    List<PokerHand> pokerHandsWithDuplicates = 
                CheckPokerHandsForDuplicateCards(pokerHands).ToList();
    pokerHandsWithDuplicates.ForEach(hand => faults.Add(new PokerHandValidationFault
    {
        FaultDescription = PokerHandValidationFaultDescription.HasDuplicateCards,
        Hand = hand
    }));
    return faults;
}

This function fulfils the DRY Principle as it is used by the two functions:

  • ComparePokerHands
  • DeterminePokerHandType
C#
/// <summary>
/// Validate poker hands and throw an argument exception if validation fails.
/// The public methods of this class expect valid poker hands and an exception must be 
/// thrown in case of an invalid poker hand.
/// Subscribers of this class's functionality can call the ValidatePokerHands function
/// to validate the poker hands without an exception being thrown.
/// The calling method name is automatically detected and included in the exception.
/// </summary> 
/// <param name="pokerHands">Poker hands to validate.</param>
void ValidatePokerHands_private(params PokerHand[] pokerHands)
{
    var validationFaults = ValidatePokerHands(pokerHands);
    if (validationFaults.Count > 0)
    {
        string callingMethodName = 
                    new System.Diagnostics.StackFrame(1).GetMethod().Name;
        throw new ArgumentException(
        "Poker hands failed validation: "+
        Utilities.EnumToTitle(validationFaults[0].FaultDescription)+
        " Call the ValidatePokerHands method for detailed validation feedback.", 
        callingMethodName);
    }
}

Project Code: Compare Poker Hands

This function fulfils part of the IPokerHandAssessor Contract.

C#
/// <summary>
/// Compares poker hands.
/// </summary>
/// <param name="pokerHands">Poker hands to compare.</param>
/// <returns>
/// A list of Poker Hand Comparison Items ordered ascending by poker hand rank. 
/// Each comparison item represents a poker hand and provides its Hand Type & Rank.
/// Winning hands have rank: 1, and will be at the top of the list.
/// </returns>
public List<PokerHandComparisonItem> ComparePokerHands(params PokerHand[] pokerHands)
{
    ValidatePokerHands_private(pokerHands);

    //NOTE: 
    //For better understanding of this code, remember:
    //A PokerHandComparisonItem is a Poker Hand with comparison data.

    //Prepare hand comparison list, including poker hand type.
    //The rank will be calculated later in this function.
    var lstPokerHandComparison = new List<PokerHandComparisonItem>();
    pokerHands.ToList().ForEach(hand => lstPokerHandComparison.Add(
    new PokerHandComparisonItem(hand, DeterminePokerHandType(hand))));

    //Sort ascending by poker hand type.
    lstPokerHandComparison.Sort((comparisonItem1, comparisonItem2) =>
    comparisonItem1.HandType.CompareTo(comparisonItem2.HandType));

    //Group by hand type.
    var handTypeGroups = lstPokerHandComparison.GroupBy(comparisonItem => 
    comparisonItem.HandType).ToList();

    //Compare hands in groups.
    int rank = 0;
    handTypeGroups.ForEach(handTypeGroup =>
    {
        //Get comparison items in this group.
        var comparisonItemsInGroup = handTypeGroup.ToList();

        //Rank must be incremented for every group.
        rank++;

        //Process single hand group.
        if (comparisonItemsInGroup.Count == 1)
            comparisonItemsInGroup[0].Rank = rank;

        //Process multi hand group.
        else
        {
            //Sort descending by winning hand. Winning hands are listed first.
            comparisonItemsInGroup.Sort((comparisonItem1, comparisonItem2) => -1 *
            CompareHandsOfSameType(
            comparisonItem1.Hand, comparisonItem2.Hand, comparisonItem1.HandType));

            //Assign current rank to first hand in group.
            comparisonItemsInGroup[0].Rank = rank;

            //Determine rank for subsequent hands in group.
            //It is helpful that the items are already sorted by winning hand; 
            //however:
            //the hands must be compared again to check which hands are equal.
            //Equal hands must have same rank.
            for (int i = 1; i < comparisonItemsInGroup.Count; i++)
            {
                //Compare current hand with previous hand.
                var currentComparisonItem = comparisonItemsInGroup[i];
                if (CompareHandsOfSameType(comparisonItemsInGroup[i - 1].Hand, 
                    currentComparisonItem.Hand, currentComparisonItem.HandType) == 1)
                    rank++;//Increment rank if previous hand wins current hand.

                //Assign current rank to current hand in group.
                currentComparisonItem.Rank = rank;
            }
        }
    });

    //Sort ascending by rank.
    lstPokerHandComparison.Sort((comparisonItem1, comparisonItem2) =>
    comparisonItem1.Rank.CompareTo(comparisonItem2.Rank));

    return lstPokerHandComparison;
}

This function fulfils the DRY Principle as it is used 2X by the ComparePokerHands function. Writing this code as a separate function also goes a long way in making the ComparePokerHands code easier to Read & Maintain.

C#
/// <summary>
/// Compares two poker hands of the same poker hand type;
/// for example: 2 poker hands with hand type Four Of A Kind.
/// </summary>
/// <param name="pokerHand1">First poker hand to compare.</param>
/// <param name="pokerHand2">Second poker hand to compare.</param>
/// <param name="pokerHandType">Poker Hand Type of the 2 poker hands.</param>
/// <returns>
/// Int value indicating the winning hand. 
/// 1: Hand 1 is the winning hand, 
/// 0: The two hands are equal, 
/// -1: Hand 2 is the winning hand.
/// </returns>
int CompareHandsOfSameType(PokerHand pokerHand1, PokerHand pokerHand2, 
                           PokerHandType pokerHandType)
{
    //Arrange cards
    ArrangeCards(pokerHand1);
    ArrangeCards(pokerHand2);

    //Compare the hands.
    switch (pokerHandType)
    {
        case PokerHandType.StraightFlush:
        case PokerHandType.Straight:
            return CompareHandsOfSameType_Helper(pokerHand1[4], pokerHand2[4]);
        case PokerHandType.Flush:
        case PokerHandType.HighCard:
            for (int i = 4; i >= 0; i--)
            {
                int result = 
                CompareHandsOfSameType_Helper(pokerHand1[i], pokerHand2[i]);
                if (result != 0)
                    return result;
            }
            return 0;
    }

    //Find sets of cards with same value: KK QQQ.
    List<Card> hand1SameCardSet1, hand1SameCardSet2;
    FindSetsOfCardsWithSameValue(
    pokerHand1, out hand1SameCardSet1, out hand1SameCardSet2);

    List<Card> hand2SameCardSet1, hand2SameCardSet2;
    FindSetsOfCardsWithSameValue(
    pokerHand2, out hand2SameCardSet1, out hand2SameCardSet2);

    //Continue comparing the hands.
    switch (pokerHandType)
    {
        case PokerHandType.FourOfAKind:
        case PokerHandType.FullHouse:
        case PokerHandType.ThreeOfAKind:
        case PokerHandType.Pair:
            return CompareHandsOfSameType_Helper(
                hand1SameCardSet1[0], hand2SameCardSet1[0]);
        case PokerHandType.TwoPair:
            //Compare first pair
            int result = CompareHandsOfSameType_Helper(
                hand1SameCardSet1[0], hand2SameCardSet1[0]);
            if (result != 0)
                return result;

            //Compare second pair
            result = CompareHandsOfSameType_Helper(
                hand1SameCardSet2[0], hand2SameCardSet2[0]);
            if (result != 0)
                return result;

            //Compare kickers (side cards)
            var kicker1 = pokerHand1.Where(card =>
                !hand1SameCardSet1.Contains(card) && 
                !hand1SameCardSet2.Contains(card)).ToList()[0];
            var kicker2 = pokerHand2.Where(card =>
                !hand2SameCardSet1.Contains(card) && 
                !hand2SameCardSet2.Contains(card)).ToList()[0];
            return CompareHandsOfSameType_Helper(kicker1, kicker2);
    }

    //This area of code should not be reached.
    throw new Exception("Hand comparison failed. Check code integrity.");
}

This function fulfils the DRY Principle as it is used numerous times by the CompareHandsOfSameType function.

C#
/// <summary>
/// This function eliminates boilerplate code when comparing poker cards,
/// and returns an int value indicating the winning hand.
/// </summary>
/// <param name="pokerHand1_card">Poker hand 1's card.</param>
/// <param name="pokerHand2_card">Poker hand 2's card.</param>
/// <returns>Int value indicating the winning hand. 
/// 1: Hand 1 is the winning hand, 
/// 0: The two hands are equal, 
/// -1: Hand 2 is the winning hand.</returns>
int CompareHandsOfSameType_Helper(Card pokerHand1_card, Card pokerHand2_card)
{
    //Get card int values.
    //This is convenient for use in this function.
    //This is also necessary to ensure the actual card's value remains unchanged.
    int pokerHand1_cardIntValue = (int)pokerHand1_card.Value;
    int pokerHand2_cardIntValue = (int)pokerHand2_card.Value;

    //Aces are always treated as high aces in this function.
    //Low aces are never passed to this function. 
    if (pokerHand1_card.Value == CardValue.Ace)
        pokerHand1_cardIntValue += (int)CardValue.King;
    if (pokerHand2_card.Value == CardValue.Ace)
        pokerHand2_cardIntValue += (int)CardValue.King;

    //Compare and return result.
    return pokerHand1_cardIntValue > pokerHand2_cardIntValue ? 1 :
        pokerHand1_cardIntValue == pokerHand2_cardIntValue ? 0 : -1;
}

Unit Testing

Thus far no Unit Testing has been done; developed functionality is only being demo'd.

SOLID projects are great for Unit Testing and Unit Testing is very important; so much so, that the entire 4th article (Part 4) of this series will be devoted to Unit Testing.

Hello World

The sole purpose of this little application is to test and illustrate the latest functionality:

  • ValidatePokerHands: The tests are done in the following order:
    • Duplicate cards: The first two tests detect duplicate cards and the third test detects no duplicate cards.
    • Jokers Not Allowed: Only one test is done on this, and it detects the joker.
    • Wrong Card Count: Only one test is done on this and it detects that the poker hand has the wrong number of cards.
  • ComparePokerHands: 5 poker hands are compared and the comparison results are listed. There are two hands with Rank 1, meaning the two hands draw for first place.

Image 2

Hello World Code

This field and function fulfil the DRY Principle as they are used 2X in the Hello Word Code.

C#
static IPokerHandAssessor assessor = new Assessor_HighRules_NoJoker();

/// <summary>
/// Validate poker hands and update console with results.
/// </summary>
/// <param name="pokerHands">Poker hands to validate.</param>
static void ValidatePokerHands_and_UpdateConsole(params PokerHand[] pokerHands)
{
    var faults = assessor.ValidatePokerHands(pokerHands);
    Console.WriteLine("");
    Console.WriteLine(
        "Validating: " + Utilities.PokerHandsToShortString(pokerHands) + ":");
    Console.WriteLine((faults.Count == 0 ? "Valid" : "Validation Fault: "
        + Utilities.EnumToTitle(faults[0].FaultDescription)));
}

The following code has too much boilerplate code; this problem will be solved in Part 3 of this series with the development of a Poker Hand Generator.

C#
/// <summary>
/// Hello World Constructor
/// </summary>
static void Main(string[] args)
{
    Console.Title = "♠♥♣♦ Hello World - SOLID Poker";
    Console.WriteLine("Testing: Validate Poker Hands");

    //Create hand 1 with duplicate.
    PokerHand hand1 = new PokerHand(
        new Card(CardSuit.Spade, CardValue.Two),
        new Card(CardSuit.Club, CardValue.Seven),
        new Card(CardSuit.Club, CardValue.Seven),
        new Card(CardSuit.Diamond, CardValue.Seven),
        new Card(CardSuit.Heart, CardValue.Seven)
        );

    //Validate hand 1 & Validate.
    ValidatePokerHands_and_UpdateConsole(hand1);
    hand1[1].Suit = CardSuit.Spade;

    //Create hand 2 and duplicate a card from hand 1 & Validate.
    PokerHand hand2 = new PokerHand(
        new Card(CardSuit.Spade, CardValue.Two),
        new Card(CardSuit.Club, CardValue.Queen),
        new Card(CardSuit.Spade, CardValue.King),
        new Card(CardSuit.Diamond, CardValue.Jack),
        new Card(CardSuit.Heart, CardValue.Ace)
        );
    ValidatePokerHands_and_UpdateConsole(hand1, hand2);

    //Change card that was duplicated between the two hands & Validate.
    hand1[0].Suit = CardSuit.Diamond;
    ValidatePokerHands_and_UpdateConsole(hand1, hand2);

    //Place joker in hand 1 & Validate.
    hand1[0].Suit = CardSuit.Joker;
    hand1[0].Value = CardValue.Unspecified;
    ValidatePokerHands_and_UpdateConsole(hand1);

    //Remove a card from hand 1 & Validate.
    hand1.RemoveAt(0);
    ValidatePokerHands_and_UpdateConsole(hand1);


    Console.WriteLine("");
    Console.WriteLine("Testing: Compare Poker Hands");

    //Prepare hands to compare
    //Two Pair
    hand1 = new PokerHand(
        new Card(CardSuit.Spade, CardValue.Two),
        new Card(CardSuit.Club, CardValue.Two),
        new Card(CardSuit.Spade, CardValue.Four),
        new Card(CardSuit.Club, CardValue.Four),
        new Card(CardSuit.Heart, CardValue.Seven)
        );
    //Two Pair
    hand2 = new PokerHand(
        new Card(CardSuit.Diamond, CardValue.Two),
        new Card(CardSuit.Heart, CardValue.Two),
        new Card(CardSuit.Diamond, CardValue.Four),
        new Card(CardSuit.Heart, CardValue.Four),
        new Card(CardSuit.Heart, CardValue.Six)
        );
    //flush
    PokerHand hand3 = new PokerHand(
        new Card(CardSuit.Spade, CardValue.Ace),
        new Card(CardSuit.Spade, CardValue.Three),
        new Card(CardSuit.Spade, CardValue.Queen),
        new Card(CardSuit.Spade, CardValue.King),
        new Card(CardSuit.Spade, CardValue.Ten)
        );
    //flush
    PokerHand hand4 = new PokerHand(
        new Card(CardSuit.Diamond, CardValue.Ace),
        new Card(CardSuit.Diamond, CardValue.Three),
        new Card(CardSuit.Diamond, CardValue.Queen),
        new Card(CardSuit.Diamond, CardValue.King),
        new Card(CardSuit.Diamond, CardValue.Ten)
        );
    //flush
    PokerHand hand5 = new PokerHand(
        new Card(CardSuit.Heart, CardValue.Five),
        new Card(CardSuit.Heart, CardValue.Three),
        new Card(CardSuit.Heart, CardValue.Queen),
        new Card(CardSuit.Heart, CardValue.King),
        new Card(CardSuit.Heart, CardValue.Ten)
        );

    //Compare hands.
    var comparisonItems = assessor.ComparePokerHands(
    hand1, hand2, hand3, hand4, hand5);
    comparisonItems.ForEach(item =>
    Console.WriteLine(
    "Rank: " + item.Rank +
    ", Poker Hand: " + Utilities.PokerHandsToShortString(item.Hand) +
    ", Hand Type: " + Utilities.EnumToTitle(item.HandType)));

    Console.Read();
}

See Something - Say Something

The goal is to have clear, error free content and your help in this regard is much appreciated. Be sure to comment if you see an error or potential improvement. All feedback is welcome.

Summary

SOLID Poker - Part 2 has covered a decent amount of code and the project is off to a good start! A Poker Hand Generator is coming in the next article of this series and will allow for elaborate testing, and will eliminate the boilerplate poker hand creation code.

Hopefully you enjoyed the Linq work; Linq was used extensively!

History

  • 26th March, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)