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

SOLID Poker - Part 3 - Battle of the Engines

4.95/5 (16 votes)
4 Apr 2017CPOL9 min read 15.9K   185  
Versatile engine to generate Spec Based Poker Hands, which allows for extensive algorithm based testing
The pieces start coming together! Development of the SOLID Poker project continues, and the latest addition is a versatile engine to generate Spec Based Poker Hands, allowing for extensive algorithm based testing.

NOTE: By necessity, the code is somewhat complex, but the power of this latest engine is evidenced by the concision of the Hello World code – all boiler plate code has been eliminated.

Battle of the Engines

Including this latest engine, SOLID Poker has two engines:

  • Spec Based Poker Hand Generator
  • Poker hand Assessor

Essentially, these two engines will be used to test each other. The generator will generate Poker Hands per specifications, and the assessor will test that the generated hands are indeed per the specifications, by determining the hand types and comparing the hands.

Image 1

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.

Prior articles of this series are published here:

Contents

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.

Spec Based Poker Hands

A spec based poker hand is a poker hand generated according to a given specification. This is great for testing purposes:

  • Eliminates Boilerplate Code: Usually a specific type of hand is required. Instead of manually choosing the cards and writing the code to create the hand, the hand can be generated based on specifications by simply calling a function.
  • Extensive Algorithm Based Testing: Hard coded testing is always necessary; however, algorithms can now be written to generate and test every possible poker hand. As a simple example, a loop through the PokerHandType enum can generate a poker hand of every hand type; adding a couple nested loops can generate a very comprehensive set of poker hands.

The spec is comprised of the following parameters:

  • Poker Hand Type: Straight Flush, Straight, Flush, Four Of A Kind, etc.
  • Card Range: The range of cards to be used in generating the poker hand. The card range is specified via the following parameters:
    • Card Range Start: The start of the card range; if Ace is used as the start of the card range, then the Ace is used as a Low Ace. Card Range Start must be smaller than or equal to Card Range End.
    • Card Range End: The end of the card range; if Ace is used as the end of the card range, then the Ace is used as a High Ace. Card Range End must be greater than or equal to Card Range Start.
    • Not Card Suit: The card suit is not used to specify the card range for the following reasons:
      • Relatively Unimportant: Generally, the card suit is not used directly to compare and rank poker hands; as such, it is less important than the card values.
      • Unnecessary Complexity: The goal is not to complicate this functionality unnecessarily.
  • First Card Range: If only the first range is specified, then it applies to all Poker Hand Types and to the whole Poker Hand. If a second range is also specified, then the first range applies to the following:
    • Straight Flush: applies to the whole hand
    • Four Of A Kind: applies to the Four Of A Kind set, not the Kicker (Side Card)
    • Full House: applies to the Three Of A Kind set, not the Pair
    • Flush: applies to the whole hand
    • Straight: applies to the whole hand
    • Three Of A Kind: applies to the Three Of A Kind set, not the two Kickers (Side Cards)
    • Two Pair: applies to the first Pair, not the second Pair
    • Pair: applies to the Pair, not the three Kickers (Side Cards)
    • High Card: applies to the whole hand
  • Second Card Range: applies to the following Poker Hand Types:
    • Four Of A Kind: applies to the Kicker (Side Card)
    • Full House: applies to the Pair of the Full House
    • Three Of A Kind: applies to the two Kickers (Side Cards)
    • Two Pair: applies to the second Pair, and also applies to the Kicker (Side Card) if a third card range is not specified
    • Pair: applies to the three Kickers (Side Cards)
  • Third Card Range: applies to the following Poker Hand Types:
    • Two Pair: applies to the Kicker (Side Card)
  • Poker Hands Already Generated: This functionality may be used to generate several hands to simulate a game, all without duplicating cards; as such, it is necessary to specify which poker hands have already been generated so that cards won't be duplicated when generating a new hand.

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 be 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.

IPokerHandGenerator_SpecBased

IPokerHandGenerator_SpecBased is the contract for Spec Based Poker Hand Generators. The overloads provide subscribers of this functionality great flexibility. Subscribers may choose to provide a less detailed specification, leaving more to the discretion of the generator; alternatively, subscribers may choose to provide a more detailed specification, taking more control over the poker hand generation.

C#
PokerHand GenerateSpecBasedPokerHand(
    PokerHandType pokerHandType
    );
PokerHand GenerateSpecBasedPokerHand(
    PokerHandType pokerHandType,
    List<PokerHand> pokerHandsAlreadyGenerated
    );
PokerHand GenerateSpecBasedPokerHand(
    PokerHandType pokerHandType,
    List<PokerHand> pokerHandsAlreadyGenerated,
    CardValue cardRangeStart,
    CardValue cardRangeEnd
    );
PokerHand GenerateSpecBasedPokerHand(
    PokerHandType pokerHandType,
    List<PokerHand> pokerHandsAlreadyGenerated,
    CardValue cardRange1Start,
    CardValue cardRange1End,
    CardValue cardRange2Start,
    CardValue cardRange2End
    );
PokerHand GenerateSpecBasedPokerHand(
    PokerHandType pokerHandType,
    List<PokerHand> pokerHandsAlreadyGenerated,
    CardValue cardRange1Start,
    CardValue cardRange1End,
    CardValue cardRange2Start,
    CardValue cardRange2End,
    CardValue cardRange3Start,
    CardValue cardRange3End
    );

Single Responsibility & Interface Segregation Implemented

Single Responsibility & Interface Segregation are the first and fourth SOLID Principles. Interestingly, these two principles are very similar:

  • Single Responsibility: a class should fulfill a Single Responsibility
  • Interface Segregation: an interface should fulfill a Single Responsibility

Care is taken to ensure IPokerHandGenerator_SpecBased fulfils a Single Responsibility. Later, there will be two interfaces for Poker Hand Generators, and each will fulfill a Single Responsibility:

  • IPokerHandGenerator_SpecBased: interface for generators of Poker Hands for testing purposes.
  • IPokerHandGenerator_Random: interface for generators of Poker Hands for actual gameplay.

Single Responsibility - Case In Point

The two Poker Hand Generator Interfaces provide good opportunity to discuss Scope Creep of a class or interface. Scope Creep means that the class or interface goes beyond a Single Responsibility.

It may sound reasonable that a single class could generate Poker Hands for various situations; the Single Responsibility of the class is simply a bit more general; however, following are the problems with such a class:

  • Development Work: It makes it more difficult to divide the work between developers or development phases as a single larger class needs to be developed as opposed to two smaller classes.
  • Unnecessary/Unwanted Functionality: Software in the live environment would be using a class that contains functionality meant for the test environment.

Project Code: Extension Methods

The extension methods are in SOLIDPoker.PokerHandMachine.ExtensionMethods.cs. The extension methods are convenient and make the code a bit shorter and easier to read.

The int extension methods make it possible to use ints in the place of Card Value Enums; this is necessary because the Card Value Enum doesn't distinguish between Low Ace & High Ace, but it is possible to distinguish between Low Ace & High Ace with an int.

C#
static class ExtensionMethods
{
    /// <summary>
    /// Checks whether this Card Value is an Ace.
    /// </summary>
    /// <param name="cardValue">Card Value to evaluate.</param>
    /// <returns>Boolean indicating whether this is an Ace.</returns>
    public static bool IsAce(this CardValue cardValue)
    {
        return cardValue == CardValue.Ace;
    }
    /// <summary>
    /// Checks whether this int represents an Ace.
    /// </summary>
    /// <param name="cardValue">Int to evaluate.</param>
    /// <returns>
    /// Boolean indicating whether this int represents an Ace.</returns>
    public static bool IsAce(this int cardValue)
    {
        return cardValue == (int)CardValue.Ace;
    }

    /// <summary>
    /// Checks whether this int represents a High Ace.
    /// </summary>
    /// <param name="cardValue">Int to evaluate.</param>
    /// <returns>
    /// Boolean indicating whether this int represents a High Ace.</returns>
    public static bool IsHighAce(this int cardValue)
    {
        return cardValue == cardValue.ToHighAce();
    }

    /// <summary>
    /// Checks whether this Card Value is a King.
    /// </summary>
    /// <param name="cardValue">Card Value to evaluate.</param>
    /// <returns>Boolean indicating whether this is a King.</returns>
    public static bool IsKing(this CardValue cardValue)
    {
        return cardValue == CardValue.King;
    }

    /// <summary>
    /// Checks whether this int represents an Unspecified Card Value.
    /// </summary>
    /// <param name="cardValue">Int to evaluate.</param>
    /// <returns>
    /// Boolean indicating whether this int represents an Unspecified Card Value.
    /// </returns>
    public static bool IsUnspecifiedCardValue(this int cardValue)
    {
        return cardValue == (int)CardValue.Unspecified;
    }

    /// <summary>
    /// Converts an inv value to a Card Value.
    /// </summary>
    /// <param name="intValue">The int value to convert.</param>
    /// <returns>A Card Value.</returns>
    public static CardValue ToCardValue(this int intValue)
    {
        //High Ace
        if (intValue == CardValue.Ace.ToHighAce())
            return CardValue.Ace;

        //Unspecified
        if (intValue < 0 || intValue > CardValue.Ace.ToHighAce())
            return CardValue.Unspecified;

        //Unspecified, Ace - King
        return (CardValue)intValue;
    }

    /// <summary>
    /// There is no High Ace in the CardValue Enum, but this function
    /// converts an Ace to an int that represents a High Ace.
    /// </summary>
    /// <returns>Int representing a High Ace.</returns>
    public static int ToHighAce(this CardValue cardValue)
    {
        return (int)CardValue.Ace + (int)CardValue.King;
    }

    /// <summary>
    /// There is no High Ace in the CardValue Enum, but this function
    /// returns an int that represents a High Ace.
    /// </summary>
    /// <returns>Int representing a High Ace.</returns>
    public static int ToHighAce(this int cardValue)
    {
        return (int)CardValue.Ace + (int)CardValue.King;
    }
}

Project Code: Generate Spec Based Poker Hand

These functions fulfill the IPokerHandGenerator_SpecBased Contract. Generating the Spec Based Poker Hands is somewhat complex as the following requirements need to be fulfilled:

  • Specification: The hand must be per specification:
    • Poker Hand Type: Straight Flush, Straight, Flush, Four Of A Kind, etc.
    • Card Ranges: must use cards in the card ranges
  • No Duplicate Cards: Cards in the Poker Hands Already Generated can't be reused which basically punches holes in the card ranges, as these cards must be removed from the card ranges.
  • Random: The generated poker hand must be random, random within the given specifications.

NOTE: Private methods use ints instead of Card Value Enums because ints can distinguish between Low Ace & High Ace.

NOTE 2: Only part of the code is in the article as it is impractical to display all the code.

Following is the last overload of the GenerateSpecBasedPokerHand function.

C#
/// <summary>
/// Generates a random poker hand of the specified poker hand type.
/// Poker hand generation is limited by the ranges of cards specified by
/// the Range Start Card Values and Range End Card Values. 
/// For Example: Ace as the Range Start and Three as the Range End specifies
/// the following range: ♣A,♦A,♥A,♠A, ♣2,♦2,♥2,♠2, ♣3,♦3,♥3,♠3.
/// RANGE 1 applies to the following Poker Hand Types: Straight Flush,
/// Four Of A Kind, Full House (Three of A Kind), Flush, Straight, 
/// Three Of A Kind, Two Pair (First Pair), Pair, High Card.
/// RANGE 2 applies to the following Poker Hand Types: 
/// Four Of A Kind (Kicker), Full House (Pair), 
/// Three Of A Kind (2 Kickers), Two Pair (2nd Pair and Kicker),
/// Pair (3 Kickers).
/// RANGE 3 applies only to the Kicker of the Two Pair. Essentially, this overload 
/// is only useful for Two Pair Poker Hands.
/// Poker hand generation is also limited by availability of cards,
/// based on the poker hands that have already been generated.
/// </summary>
/// <param name="pokerHandType">Type of poker hand to generate.</param>
/// <param name="cardRange1Start">
/// The Card Value that serves as the start of the range. 
/// The Range Start Value must be smaller than or equal to the Range End Value.
/// Ace will be treated as a Low Ace. 
/// </param>
/// <param name="cardRange1End">
/// The Card Value that serves as the end of the range. 
/// The Range End Value must be greater than or equal to the Range Start Value.
/// Ace will be treated as a High Ace. 
/// If Range Start is an Ace and Range End is an Ace, then both will be treated
/// as High Aces.
/// </param>
/// <param name="cardRange2Start">Same as cardRange1Start.</param>
/// <param name="cardRange2End">Same as cardRange1End.</param>
/// <param name="cardRange3Start">Same as cardRange1Start.</param>
/// <param name="cardRange3End">Same as cardRange1End.</param>
/// <param name="pokerHandsAlreadyGenerated">
/// Determines available cards with which to generate a poker hand.</param>
/// <returns>
/// Generated poker hand if required cards are available, otherwise null.</returns>
public PokerHand GenerateSpecBasedPokerHand(
    PokerHandType pokerHandType,
    List<PokerHand> pokerHandsAlreadyGenerated,
    CardValue cardRange1Start,
    CardValue cardRange1End,
    CardValue cardRange2Start,
    CardValue cardRange2End,
    CardValue cardRange3Start,
    CardValue cardRange3End
    )
{
    return GenerateSpecBasedPokerHand_actual(
    pokerHandType,
    pokerHandsAlreadyGenerated,
    (int)cardRange1Start,
    (int)cardRange1End,
    (int)cardRange2Start,
    (int)cardRange2End,
    (int)cardRange2Start,
    (int)cardRange2End
    );
}

This function is long but it adheres to the DRY Principle and the Single Responsibility Principle. Numerous pieces of logic have been separated into functions to simplify the code and to prevent duplication of logic. An argument can be made that this function could/should be broken down further; however this will increase the code and won't really simplify it. In the future, the code may be refactored to make it more customizable via dependency injection.

C#
/// <summary>
/// The function commentary of GenerateSpecBasedPokerHand's last overload
/// applies to this function.
/// This function is written seperately so that the Range Starts and Ends
/// may be of type int.
/// This is necessary as it allows distinction between a Low Ace and a High Ace.
/// </summary>
PokerHand GenerateSpecBasedPokerHand_actual(
PokerHandType pokerHandType,
List<PokerHand> pokerHandsAlreadyGenerated,
int cardRange1Start,
int cardRange1End,
int cardRange2Start,
int cardRange2End,
int cardRange3Start,
int cardRange3End
)
{
    //Validate Poker Hand Type
    if (pokerHandType == PokerHandType.FiveOfAKind)
        throw new Exception(
        "Spec Based Poker Generator does not cater for the following Poker Hand Type: "
        + Utilities.EnumToTitle(pokerHandType));

    //Configure Range Starts & Ends
    ConfigureRangeStartAndEnd(ref cardRange1Start, ref cardRange1End);
    ConfigureRangeStartAndEnd(ref cardRange2Start, ref cardRange2End);
    ConfigureRangeStartAndEnd(ref cardRange3Start, ref cardRange3End);

    //Validate the card ranges
    ValidateCardRange(cardRange1Start, cardRange1End, 1);
    ValidateCardRange(cardRange2Start, cardRange2End, 2);
    ValidateCardRange(cardRange3Start, cardRange3End, 3);

    //Prepare Card Ranges.
    //Range 3 is only required by the following Poker Hand Type: Two Pair;
    //as such, it will be created in the Two Pair code section.
    //Note: CardToInt was used to convert the cards to ints.
    HashSet<int>
    range1 =
    PrepareCardRange(cardRange1Start, cardRange1End, pokerHandsAlreadyGenerated),
    range2 =
    PrepareCardRange(cardRange2Start, cardRange2End, pokerHandsAlreadyGenerated),
    range3 = null;

    //These exclusions ensure the same card suits & values are not retried.
    HashSet<CardSuit> cardSuitExclusions;
    HashSet<int> cardValueExclusions;

    //Variables used by the algorithms of the different Poker Hand Types.
    int randomCardValue_int,
        direction;
    PokerHand hand;
    CardSuit randomSuit;
    Card card;
    CardSuit cardSuitExclusion;

    //Sets required to generate a poker hand; for example, a Full House is 
    //comprised of two sets of the following lengths: 3 ,2.
    //A set is cards of the same value; for example: ♣A,♦A,♥A or ♠K,♥K
    int[] setRequiredLengths = null;

    //Generate hands.
    switch (pokerHandType)
    {
        case PokerHandType.StraightFlush:
            //Loop while there are Card Suits available to test/process.
            cardSuitExclusions = new HashSet<CardSuit>();
            while ((randomSuit = GetRandomCardSuit(cardSuitExclusions))
            != CardSuit.Unspecified)
            {
                cardValueExclusions = new HashSet<int>();

                //Loop while there are Card Values available to test/process.
                while (!(randomCardValue_int = GetRandomCardValue(
                cardRange1Start,
                cardRange1End,
                cardValueExclusions))
                .IsUnspecifiedCardValue())
                {
                    direction = DetermineDirection(
                    cardRange1Start,
                    cardRange1End,
                    randomCardValue_int);

                    hand = new PokerHand();
                    for (int cardValue = randomCardValue_int;
                        ((
                        direction == 1 ?
                        cardValue <= cardRange1End :
                        cardValue >= cardRange1Start)
                        && hand.Count < 5
                        )
                        ; cardValue += direction)
                        hand.Add(new Card(
                        (CardSuit)randomSuit,
                        cardValue.ToCardValue()));

                    if (ValidatePokerHandPart(hand, 5, range1))
                        return hand;
                    cardValueExclusions.Add(randomCardValue_int);
                }
                cardSuitExclusions.Add(randomSuit);
            }
            return null;
        case PokerHandType.Straight:
            //Loop while there are Card Values available to test/process.
            cardValueExclusions = new HashSet<int>();
            while (!(randomCardValue_int = GetRandomCardValue(
            cardRange1Start,
            cardRange1End,
            cardValueExclusions))
            .IsUnspecifiedCardValue())
            {
                direction = DetermineDirection(
                cardRange1Start,
                cardRange1End,
                randomCardValue_int);
                hand = new PokerHand();
                for (int cardValue = randomCardValue_int;
                ((
                direction == 1 ?
                cardValue <= cardRange1End :
                cardValue >= cardRange1Start)
                && hand.Count < 5
                )
                ; cardValue += direction)
                {
                    //Loop while there are Card Suits available to test/process.
                    cardSuitExclusions = new HashSet<CardSuit>(
                    new CardSuit[] { GetExclusionsToPreventFlush(hand) });
                    while ((randomSuit = GetRandomCardSuit(cardSuitExclusions))
                    != CardSuit.Unspecified)
                    {
                        card = new Card(randomSuit, cardValue.ToCardValue());
                        if (range1.Contains(CardToInt(card)))
                        {
                            hand.Add(card);
                            break;
                        }
                        else
                            cardSuitExclusions.Add(randomSuit);
                    }
                }
                if (ValidatePokerHandPart(hand, 5, range1))
                    return hand;
                cardValueExclusions.Add(randomCardValue_int);
            }
            return null;
        case PokerHandType.Flush:
            AvoidStraightTrap(
            cardRange1Start,
            cardRange1End,
            cardRange2Start,
            cardRange2End,
            range1,
            range2
            );

            //Loop while there are Card Suits available to test/process.
            cardSuitExclusions = new HashSet<CardSuit>();
            while ((randomSuit = GetRandomCardSuit(cardSuitExclusions))
            != CardSuit.Unspecified)
            {
                hand = new PokerHand();
                //Loop while there are Card Values available to test/process.
                cardValueExclusions = new HashSet<int>();
                while (!(randomCardValue_int = GetRandomCardValue(
                cardRange1Start,
                cardRange1End,
                cardValueExclusions))
                .IsUnspecifiedCardValue() && hand.Count < 5)
                {
                    card = new Card(randomSuit, randomCardValue_int.ToCardValue());
                    if (range1.Contains(CardToInt(card)))
                        hand.Add(card);
                    GetExclusionsToPreventStraight(hand, cardValueExclusions);

                    //Current Card Value must be excluded 
                    //irrespective of whether or not it was added to the hand.
                    cardValueExclusions.Add(randomCardValue_int);
                }
                if (hand.Count == 5)
                    return hand;

                cardSuitExclusions.Add(randomSuit);
                cardValueExclusions = new HashSet<int>();
            }
            return null;

        //The following Poker Hand Types are processed from this point onward:
        //FourOfAKind, FullHouse, ThreeOfAKind, TwoPair, Pair, HighCard.
        //These Poker Hand Types follow the same pattern: Sets + Kickers (Side Cards).
        //As such, they are generated with the same algorithm/code.
        case PokerHandType.FourOfAKind:
            setRequiredLengths = new int[] { 4 };
            break;
        case PokerHandType.FullHouse:
            setRequiredLengths = new int[] { 3, 2 };
            break;
        case PokerHandType.ThreeOfAKind:
            setRequiredLengths = new int[] { 3 };
            break;
        case PokerHandType.TwoPair:
            setRequiredLengths = new int[] { 2, 2 };
            range3 =
            PrepareCardRange(
            cardRange3Start,
            cardRange3End,
            pokerHandsAlreadyGenerated);
            break;
        case PokerHandType.Pair:
            setRequiredLengths = new int[] { 2 };
            break;
        case PokerHandType.HighCard:
            //The High Card comes from Range1, and the 4 Kickers (Side Cards) come 
            //from Range2.
            setRequiredLengths = new int[1];

            AvoidStraightTrap(
                cardRange1Start,
                cardRange1End,
                cardRange2Start,
                cardRange2End,
                range1,
                range2
                );
            break;
    }

    //Prepare Range Arrays
    int[] cardRangeStarts = new int[] {
    cardRange1Start,cardRange2Start,cardRange3Start};
    int[] cardRangeEnds = new int[] { cardRange1End, cardRange2End, cardRange3End };
    HashSet<int>[] ranges = new HashSet<int>[] { range1, range2, range3 };

    //ADD SETS
    hand = new PokerHand();
    List<Card> set;
    for (int i = 0; i < setRequiredLengths.Length; i++)
    {
        int setRequiredLength = setRequiredLengths[i];
        bool addedSet = false;

        //Loop while there are Card Values available to test/process.
        cardValueExclusions = new HashSet<int>();
        cardSuitExclusion = GetExclusionsToMaintainPokerHandType(
        hand,
        cardValueExclusions);
        while (!(randomCardValue_int = GetRandomCardValue(
        cardRangeStarts[i],
        cardRangeEnds[i],
        cardValueExclusions))
        .IsUnspecifiedCardValue())
        {
            set = new List<Card>();

            //Loop while there are Card Suits available to test/process,
            //and while the set has not reached its required length.
            cardSuitExclusions = new HashSet<CardSuit>(
            new CardSuit[] { cardSuitExclusion });
            while ((randomSuit = GetRandomCardSuit(cardSuitExclusions))
            != CardSuit.Unspecified && set.Count < setRequiredLength)
            {
                card = new Card(randomSuit, randomCardValue_int.ToCardValue());
                if (ranges[i].Contains(CardToInt(card)))
                    set.Add(card);
                cardSuitExclusions.Add(randomSuit);
            }

            if (set.Count == setRequiredLength)
            {
                //Successfully created set; now add to hand.
                hand.AddRange(set);
                addedSet = true;
                break;
            }

            cardValueExclusions.Add(randomCardValue_int);
        }

        //Unable to generate the Poker Hand with the available cards in the ranges.
        if (!addedSet)
            return null;
    }

    //ADD KICKERS (SIDE CARDS)
    int previousLoopsHandLength = -1;

    //Loop while hand is too short & while cards are being added.
    //If no cards are being added, then it means the hand can't 
    //be generated with the available cards in the ranges.
    int rangeToUse = setRequiredLengths.Length;
    while (hand.Count < 5 && hand.Count != previousLoopsHandLength)
    {
        previousLoopsHandLength = hand.Count;
        cardValueExclusions = new HashSet<int>();
        cardSuitExclusion = GetExclusionsToMaintainPokerHandType(
        hand,
        cardValueExclusions);
        while (!(randomCardValue_int = GetRandomCardValue(
        cardRangeStarts[rangeToUse],
        cardRangeEnds[rangeToUse],
        cardValueExclusions))
        .IsUnspecifiedCardValue())
        {
            bool addedCard = false;

            //Loop while there are Card Suits available to test/process,
            //and while the set has not reached its required length.
            cardSuitExclusions = new HashSet<CardSuit>(
            new CardSuit[] { cardSuitExclusion });
            while ((randomSuit = GetRandomCardSuit(cardSuitExclusions))
            != CardSuit.Unspecified)
            {
                card = new Card(randomSuit, randomCardValue_int.ToCardValue());
                if (ranges[rangeToUse].Contains(CardToInt(card)))
                {
                    //Successfully created set; now add to hand.
                    hand.Add(card);
                    addedCard = true;
                    break;
                }
                cardSuitExclusions.Add(randomSuit);
            }

            cardValueExclusions.Add(randomCardValue_int);
            if (addedCard)
                break;
        }
    }

    //Return Hand!
    if (hand.Count != 5)
        return null;
    return hand;
}

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.

NOTE: Testing that a High Poker Hand beats a Low Poker Hand of the same Poker Hand Type, is a simple way to test that the Poker Hand was indeed created using only the Card Values in the specified ranges.

Image 2

Hello World Code

The Spec Based Poker Hand Generator has eliminated all boilerplate code and made it possible to write algorithm based demo code.

C#
static void Main(string[] args)
{
    Console.Title = "♠♥♣♦ Hello World - SOLID Poker";
    Console.WriteLine("BATTLE OF THE ENGINES -- GENERATOR vs. ASSESSOR.");
    Console.WriteLine("");
    Console.WriteLine("BATTLE DEFINED:");
    Console.WriteLine(
    "GENERATOR: Generates a high & low Poker Hand of each Poker Hand Type.");
    Console.WriteLine(
    "ASSESSOR: Checks that each Poker Hand is according to specification:");
    Console.WriteLine("1) That each Poker Hand is of the correct Poker Hand Type.");
    Console.WriteLine("2) That the high Poker Hands beat the low Poker Hands.");
    Console.WriteLine("");
    Console.WriteLine("BATTLE RESULTS:");
    Console.WriteLine(
    "BATTLE          | CORRECT POKER HAND TYPE GENERATED | HIGH BEATS LOW");

    IPokerHandAssessor assessor = new Assessor_HighRules_NoJoker();
    IPokerHandGenerator_SpecBased generator = new Generator_HighRules_NoJoker();
    //Loop through the Poker Hand Types.
    Enum.GetValues(typeof(PokerHandType)).Cast<PokerHandType>()
    .Where(handType => handType != PokerHandType.FiveOfAKind).ToList()
    .ForEach(handType =>
    {
        //Generate the Poker Hands.
        var lowHand = generator.GenerateSpecBasedPokerHand(
            handType, 
            null, 
            CardValue.Two, 
            CardValue.Eight);
        var highHand = generator.GenerateSpecBasedPokerHand(
            handType,
            new PokerHand[] { lowHand }.ToList(), 
            CardValue.Eight, 
            CardValue.Ace);

        //Compare the Poker Hands.
        var comparisonItems = assessor.ComparePokerHands(lowHand, highHand);

        //Display the results.
        Console.WriteLine(
        Utilities.EnumToTitle(handType).PadRight(18, ' ') +
        (comparisonItems.Where(item => item.HandType == handType).Count() == 2 ? 
        "Success" : "Fail").PadRight(36, ' ') +
        (comparisonItems[0].Hand == highHand ? "Success" : "Fail")
        );
    });

    Console.WriteLine("");
    Console.WriteLine("BATTLE OF THE ENGINES - WINNER: GENERATOR & ASSESSOR.");
    Console.WriteLine("As iron sharpens iron, so one engine perfects another.");
    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.

Conclusion

This Spec Based Poker Hand Generator was a fair amount of work, but it will save a lot of time and code in the Unit Testing. The degree of automation provided by this generator will make it relatively easy to perform every possible unit test against Poker Hand Assessor class, ensuring the code is robust.

History

  • 2nd April, 2017: Initial version

License

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