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.
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.
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.
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.
PokerHand[] CheckPokerHandsForDuplicateCards(params PokerHand[] pokerHands)
{
for (int i = 0; i < pokerHands.Length; i++)
{
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++)
{
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.
public List<PokerHandValidationFault> ValidatePokerHands(params PokerHand[] pokerHands)
{
List<PokerHandValidationFault> faults = new List<PokerHandValidationFault>();
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;
}
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;
}
}
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
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.
public List<PokerHandComparisonItem> ComparePokerHands(params PokerHand[] pokerHands)
{
ValidatePokerHands_private(pokerHands);
var lstPokerHandComparison = new List<PokerHandComparisonItem>();
pokerHands.ToList().ForEach(hand => lstPokerHandComparison.Add(
new PokerHandComparisonItem(hand, DeterminePokerHandType(hand))));
lstPokerHandComparison.Sort((comparisonItem1, comparisonItem2) =>
comparisonItem1.HandType.CompareTo(comparisonItem2.HandType));
var handTypeGroups = lstPokerHandComparison.GroupBy(comparisonItem =>
comparisonItem.HandType).ToList();
int rank = 0;
handTypeGroups.ForEach(handTypeGroup =>
{
var comparisonItemsInGroup = handTypeGroup.ToList();
rank++;
if (comparisonItemsInGroup.Count == 1)
comparisonItemsInGroup[0].Rank = rank;
else
{
comparisonItemsInGroup.Sort((comparisonItem1, comparisonItem2) => -1 *
CompareHandsOfSameType(
comparisonItem1.Hand, comparisonItem2.Hand, comparisonItem1.HandType));
comparisonItemsInGroup[0].Rank = rank;
for (int i = 1; i < comparisonItemsInGroup.Count; i++)
{
var currentComparisonItem = comparisonItemsInGroup[i];
if (CompareHandsOfSameType(comparisonItemsInGroup[i - 1].Hand,
currentComparisonItem.Hand, currentComparisonItem.HandType) == 1)
rank++;
currentComparisonItem.Rank = 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.
int CompareHandsOfSameType(PokerHand pokerHand1, PokerHand pokerHand2,
PokerHandType pokerHandType)
{
ArrangeCards(pokerHand1);
ArrangeCards(pokerHand2);
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;
}
List<Card> hand1SameCardSet1, hand1SameCardSet2;
FindSetsOfCardsWithSameValue(
pokerHand1, out hand1SameCardSet1, out hand1SameCardSet2);
List<Card> hand2SameCardSet1, hand2SameCardSet2;
FindSetsOfCardsWithSameValue(
pokerHand2, out hand2SameCardSet1, out hand2SameCardSet2);
switch (pokerHandType)
{
case PokerHandType.FourOfAKind:
case PokerHandType.FullHouse:
case PokerHandType.ThreeOfAKind:
case PokerHandType.Pair:
return CompareHandsOfSameType_Helper(
hand1SameCardSet1[0], hand2SameCardSet1[0]);
case PokerHandType.TwoPair:
int result = CompareHandsOfSameType_Helper(
hand1SameCardSet1[0], hand2SameCardSet1[0]);
if (result != 0)
return result;
result = CompareHandsOfSameType_Helper(
hand1SameCardSet2[0], hand2SameCardSet2[0]);
if (result != 0)
return result;
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);
}
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.
int CompareHandsOfSameType_Helper(Card pokerHand1_card, Card pokerHand2_card)
{
int pokerHand1_cardIntValue = (int)pokerHand1_card.Value;
int pokerHand2_cardIntValue = (int)pokerHand2_card.Value;
if (pokerHand1_card.Value == CardValue.Ace)
pokerHand1_cardIntValue += (int)CardValue.King;
if (pokerHand2_card.Value == CardValue.Ace)
pokerHand2_cardIntValue += (int)CardValue.King;
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.
Hello World Code
This field and function fulfil the DRY Principle as they are used 2X in the Hello Word Code.
static IPokerHandAssessor assessor = new Assessor_HighRules_NoJoker();
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.
static void Main(string[] args)
{
Console.Title = "♠♥♣♦ Hello World - SOLID Poker";
Console.WriteLine("Testing: Validate Poker Hands");
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)
);
ValidatePokerHands_and_UpdateConsole(hand1);
hand1[1].Suit = CardSuit.Spade;
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);
hand1[0].Suit = CardSuit.Diamond;
ValidatePokerHands_and_UpdateConsole(hand1, hand2);
hand1[0].Suit = CardSuit.Joker;
hand1[0].Value = CardValue.Unspecified;
ValidatePokerHands_and_UpdateConsole(hand1);
hand1.RemoveAt(0);
ValidatePokerHands_and_UpdateConsole(hand1);
Console.WriteLine("");
Console.WriteLine("Testing: Compare Poker Hands");
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)
);
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)
);
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)
);
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)
);
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)
);
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