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.
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.
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 int
s 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
.
static class ExtensionMethods
{
public static bool IsAce(this CardValue cardValue)
{
return cardValue == CardValue.Ace;
}
public static bool IsAce(this int cardValue)
{
return cardValue == (int)CardValue.Ace;
}
public static bool IsHighAce(this int cardValue)
{
return cardValue == cardValue.ToHighAce();
}
public static bool IsKing(this CardValue cardValue)
{
return cardValue == CardValue.King;
}
public static bool IsUnspecifiedCardValue(this int cardValue)
{
return cardValue == (int)CardValue.Unspecified;
}
public static CardValue ToCardValue(this int intValue)
{
if (intValue == CardValue.Ace.ToHighAce())
return CardValue.Ace;
if (intValue < 0 || intValue > CardValue.Ace.ToHighAce())
return CardValue.Unspecified;
return (CardValue)intValue;
}
public static int ToHighAce(this CardValue cardValue)
{
return (int)CardValue.Ace + (int)CardValue.King;
}
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 int
s instead of Card Value Enums because int
s 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.
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.
PokerHand GenerateSpecBasedPokerHand_actual(
PokerHandType pokerHandType,
List<PokerHand> pokerHandsAlreadyGenerated,
int cardRange1Start,
int cardRange1End,
int cardRange2Start,
int cardRange2End,
int cardRange3Start,
int cardRange3End
)
{
if (pokerHandType == PokerHandType.FiveOfAKind)
throw new Exception(
"Spec Based Poker Generator does not cater for the following Poker Hand Type: "
+ Utilities.EnumToTitle(pokerHandType));
ConfigureRangeStartAndEnd(ref cardRange1Start, ref cardRange1End);
ConfigureRangeStartAndEnd(ref cardRange2Start, ref cardRange2End);
ConfigureRangeStartAndEnd(ref cardRange3Start, ref cardRange3End);
ValidateCardRange(cardRange1Start, cardRange1End, 1);
ValidateCardRange(cardRange2Start, cardRange2End, 2);
ValidateCardRange(cardRange3Start, cardRange3End, 3);
HashSet<int>
range1 =
PrepareCardRange(cardRange1Start, cardRange1End, pokerHandsAlreadyGenerated),
range2 =
PrepareCardRange(cardRange2Start, cardRange2End, pokerHandsAlreadyGenerated),
range3 = null;
HashSet<CardSuit> cardSuitExclusions;
HashSet<int> cardValueExclusions;
int randomCardValue_int,
direction;
PokerHand hand;
CardSuit randomSuit;
Card card;
CardSuit cardSuitExclusion;
int[] setRequiredLengths = null;
switch (pokerHandType)
{
case PokerHandType.StraightFlush:
cardSuitExclusions = new HashSet<CardSuit>();
while ((randomSuit = GetRandomCardSuit(cardSuitExclusions))
!= CardSuit.Unspecified)
{
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)
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:
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)
{
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
);
cardSuitExclusions = new HashSet<CardSuit>();
while ((randomSuit = GetRandomCardSuit(cardSuitExclusions))
!= CardSuit.Unspecified)
{
hand = new PokerHand();
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);
cardValueExclusions.Add(randomCardValue_int);
}
if (hand.Count == 5)
return hand;
cardSuitExclusions.Add(randomSuit);
cardValueExclusions = new HashSet<int>();
}
return null;
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:
setRequiredLengths = new int[1];
AvoidStraightTrap(
cardRange1Start,
cardRange1End,
cardRange2Start,
cardRange2End,
range1,
range2
);
break;
}
int[] cardRangeStarts = new int[] {
cardRange1Start,cardRange2Start,cardRange3Start};
int[] cardRangeEnds = new int[] { cardRange1End, cardRange2End, cardRange3End };
HashSet<int>[] ranges = new HashSet<int>[] { range1, range2, range3 };
hand = new PokerHand();
List<Card> set;
for (int i = 0; i < setRequiredLengths.Length; i++)
{
int setRequiredLength = setRequiredLengths[i];
bool addedSet = false;
cardValueExclusions = new HashSet<int>();
cardSuitExclusion = GetExclusionsToMaintainPokerHandType(
hand,
cardValueExclusions);
while (!(randomCardValue_int = GetRandomCardValue(
cardRangeStarts[i],
cardRangeEnds[i],
cardValueExclusions))
.IsUnspecifiedCardValue())
{
set = new List<Card>();
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)
{
hand.AddRange(set);
addedSet = true;
break;
}
cardValueExclusions.Add(randomCardValue_int);
}
if (!addedSet)
return null;
}
int previousLoopsHandLength = -1;
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;
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)))
{
hand.Add(card);
addedCard = true;
break;
}
cardSuitExclusions.Add(randomSuit);
}
cardValueExclusions.Add(randomCardValue_int);
if (addedCard)
break;
}
}
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.
Hello World Code
The Spec Based Poker Hand Generator has eliminated all boilerplate code and made it possible to write algorithm based demo code.
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();
Enum.GetValues(typeof(PokerHandType)).Cast<PokerHandType>()
.Where(handType => handType != PokerHandType.FiveOfAKind).ToList()
.ForEach(handType =>
{
var lowHand = generator.GenerateSpecBasedPokerHand(
handType,
null,
CardValue.Two,
CardValue.Eight);
var highHand = generator.GenerateSpecBasedPokerHand(
handType,
new PokerHand[] { lowHand }.ToList(),
CardValue.Eight,
CardValue.Ace);
var comparisonItems = assessor.ComparePokerHands(lowHand, highHand);
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