SOLID Poker - 第 2 部分 - 比较扑克牌





5.00/5 (15投票s)
本文继续SOLID Poker项目的开发,涵盖了比较和验证扑克牌型的功能。
目录
- 系列文章
- 原则、模式和实践的实现
- 项目代码:注释
- 项目代码:契约
- 项目代码:检查扑克牌是否有重复的牌
- 项目代码:验证扑克牌型
- 项目代码:比较扑克牌型
- 单元测试
- Hello World
- Hello World 代码
- 摘要
系列文章
本项目和文档将跨越多篇文章,以确保项目的每个部分都能得到必要的关注和时间。这提供了良好的结构,使信息更容易理解。
每篇文章都建立在先前的文章之上并依赖于它们,并且每篇文章都将进一步开发先前文章中的代码。先前的文章为当前文章提供了必要的背景和上下文。
以下是迄今为止已发布的本系列文章的完整列表
原则、模式和实践的实现
在 SOLID Poker 项目的早期阶段,只实现了 DRY 和 SOLID 原则。
注意:SOLID原则不适用于private
方法,但private
方法确实说明了DRY原则。
项目代码:注释
代码中的注释非常丰富,变量和函数名称也具有描述性。为了避免重复内容/注释,只有在确实有内容可以补充时,才会提供关于代码的其他注释。
如果文章有不清楚或需要补充的地方,请发表评论。
项目代码:契约
所有SOLID Poker的契约都位于SOLIDPoker.Contract
项目中。
下面是IPokerHandAssessor
的最新版本。我们精心确保函数的内聚性,履行评估扑克牌型的单一职责;这符合第一个SOLID原则,即单一职责原则。
public interface IPokerHandAssessor
{
List<PokerHandComparisonItem> ComparePokerHands(params PokerHand[] pokerHands);
PokerHandType DeterminePokerHandType(PokerHand pokerHand);
List<PokerHandValidationFault> ValidatePokerHands(params PokerHand[] pokerHands);
}
下面的数据对象是一个新增项,用于在比较扑克牌型时提供全面的结果。
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;
}
}
下面的enum
和数据对象也是新增项,用于在验证扑克牌型时提供全面的结果。
public enum PokerHandValidationFaultDescription
{
HasDuplicateCards = 1,
JokersNotAllowed = 2,
WrongCardCount = 3
}
public class PokerHandValidationFault
{
public PokerHand Hand { get; set; }
public PokerHandValidationFaultDescription FaultDescription { get; set; }
}
项目代码:检查扑克牌是否有重复的牌
此函数仅由ValidatePokerHands
函数使用,因此尚未被重用。编写此函数是为了使代码更易于阅读和维护。
/// <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];
}
项目代码:验证扑克牌型
此函数履行了IPokerHandAssessor
契约的一部分。
/// <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;
}
此函数履行了DRY原则,因为它被两个函数使用
ComparePokerHands
DeterminePokerHandType
/// <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);
}
}
项目代码:比较扑克牌型
此函数履行了IPokerHandAssessor
契约的一部分。
/// <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;
}
此函数履行了DRY原则,因为它被ComparePokerHands
函数使用2次。将此代码编写为单独的函数也有助于使ComparePokerHands
代码更易于阅读和维护。
/// <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.");
}
此函数履行了DRY原则,因为它被CompareHandsOfSameType
函数使用多次。
/// <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;
}
单元测试
到目前为止,还没有进行单元测试;已开发的功能仅用于演示。
SOLID项目非常适合单元测试,而单元测试非常重要;以至于本系列的第四篇文章(第三部分)将专门用于单元测试。
Hello World
这个小型应用程序的唯一目的是测试和演示最新的功能
ValidatePokerHands
:测试按以下顺序进行- 重复的牌:前两个测试检测重复的牌,第三个测试检测没有重复的牌。
- 不允许使用Joker:只进行了一项测试,检测了Joker。
- 错误的牌数:只进行了一项测试,检测扑克牌型中的牌数是否错误。
ComparePokerHands
:比较了5手扑克牌,并列出了比较结果。有两手牌的等级为1,意味着这两手牌并列第一。
Hello World 代码
这个字段和函数履行了DRY原则,因为它们在Hello Word代码中被使用了2次。
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)));
}
以下代码有太多的样板代码;这个问题将在本系列的第三部分通过开发一个扑克牌型生成器来解决。
/// <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();
}
看见就说
目标是提供清晰、无错误的内容,对此您的帮助将不胜感激。如果您发现错误或潜在的改进之处,请务必发表评论。欢迎所有反馈。
摘要
SOLID Poker - 第二部分涵盖了相当多的代码,项目开了一个好头!下一篇文章将介绍扑克牌型生成器,它将允许进行详细的测试,并消除样板化的扑克牌型创建代码。
希望你喜欢Linq的工作;Linq得到了广泛使用!
历史
- 2017年3月26日:初始版本