65.9K
CodeProject 正在变化。 阅读更多。
Home

SOLID Poker - 第 3 部分 - 引擎之战

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (16投票s)

2017年4月2日

CPOL

9分钟阅读

viewsIcon

16783

downloadIcon

222

通用的引擎,用于生成基于规格的扑克牌型,可进行广泛的算法测试。

注意:出于必要,代码有些复杂,但最新引擎的强大之处体现在“Hello World”代码的简洁性上——所有样板代码都已消除。

引擎大战

算上这个最新的引擎,SOLID Poker 有两个引擎。

  • 基于规格的扑克牌型生成器
  • 扑克牌型评估器

本质上,这两个引擎将用于互相测试。生成器将根据规格生成扑克牌型,而评估器将通过确定牌型来测试生成的牌型是否符合规格,并进行比较。

系列文章

本项目和文档将跨越多篇文章,以确保项目的每个部分都能得到必要的关注和时间。这提供了良好的结构,使信息更容易理解。

每篇文章都建立在先前的文章之上并依赖于它们,并且每篇文章都将进一步开发先前文章中的代码。先前的文章为当前文章提供了必要的背景和上下文。

本系列的前期文章在此发布。

目录

原则、模式和实践的实现

在 SOLID Poker 项目的早期阶段,只实现了 DRY 和 SOLID 原则。

注意:SOLID 原则不适用于 private 方法,但 private 方法确实说明了 DRY 原则。

基于规格的扑克牌型

基于规格的扑克牌型是根据给定规格生成的扑克牌型。这对于测试非常有用。

  • 消除了样板代码:通常需要特定类型的牌型。无需手动选择牌并编写代码来创建牌型,而是可以通过简单地调用函数来根据规格生成牌型。
  • 广泛的算法测试:硬编码测试始终是必要的;然而,现在可以编写算法来生成和测试所有可能的扑克牌型。作为一个简单的例子,循环遍历 PokerHandType enum 可以生成每种牌型的扑克牌型;添加几个嵌套循环可以生成一套非常全面的扑克牌型。

规格由以下参数组成:

  • 扑克牌型:同花顺、顺子、同花、四条等。
  • 牌值范围:用于生成扑克牌型的牌值范围。牌值范围通过以下参数指定:
    • 牌值范围起始:牌值范围的起始;如果使用 A 作为牌值范围的起始,则 A 作为低 A。牌值范围起始必须小于或等于牌值范围结束。
    • 牌值范围结束:牌值范围的结束;如果使用 A 作为牌值范围的结束,则 A 作为高 A。牌值范围结束必须大于或等于牌值范围起始。
    • 非牌花色:牌花色不用于指定牌值范围的原因如下:
      • 相对不重要:通常,牌花色不直接用于比较和排序扑克牌型;因此,它比牌值更不重要。
      • 不必要的复杂性:目标不是不必要地使此功能复杂化。
  • 第一个牌值范围:如果只指定了第一个范围,则它适用于所有扑克牌型和整个扑克牌。如果还指定了第二个范围,则第一个范围适用于以下情况:
    • 同花顺:适用于整个牌型
    • 四条:适用于四条牌,不适用于 Kicker(边牌)
    • 葫芦:适用于三条牌,不适用于对子
    • 同花:适用于整个牌型
    • 顺子:适用于整个牌型
    • 三条:适用于三条牌,不适用于两个 Kicker(边牌)
    • 两对:适用于第一个对子,不适用于第二个对子
    • 对子:适用于对子,不适用于三个 Kicker(边牌)
    • 高牌:适用于整个牌型
  • 第二个牌值范围:适用于以下扑克牌型:
    • 四条:适用于 Kicker(边牌)
    • 葫芦:适用于葫芦的对子
    • 三条:适用于两个 Kicker(边牌)
    • 两对:适用于第二个对子,如果未指定第三个牌值范围,则也适用于 Kicker(边牌)
    • 对子:适用于三个 Kicker(边牌)
  • 第三个牌值范围:适用于以下扑克牌型:
    • 两对:适用于 Kicker(边牌)
  • 已生成的扑克牌型:此功能可用于生成多手牌以模拟一局游戏,而无需重复牌;因此,有必要指定已生成的扑克牌型,以便在生成新牌时不会重复牌。

项目代码:注释

代码中的注释非常详细,变量和函数名称描述性强。为了避免重复内容/注释,只有在确实有内容要补充时才会提供关于代码的额外注释。

如果文章有不清楚或需要补充的地方,请发表评论。

项目代码:契约

所有 SOLID Poker 契约都位于 SOLIDPoker.Contract 项目中。

IPokerHandGenerator_SpecBased

IPokerHandGenerator_SpecBased 是基于规格的扑克牌型生成器的契约。重载为该功能的用户提供了极大的灵活性。用户可以选择提供不太详细的规格,将更多自由裁量权留给生成器;或者,用户可以选择提供更详细的规格,从而更严格地控制扑克牌型的生成。

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
    );

单一职责与接口隔离已实现

单一职责和接口隔离是 SOLID 原则的第一个和第四个原则。有趣的是,这两个原则非常相似。

  • 单一职责:一个类应该只承担一项职责。
  • 接口隔离:一个接口应该只承担一项职责。

我们注意确保 IPokerHandGenerator_SpecBased 只承担单一职责。稍后,将有两个用于扑克牌型生成器的接口,每个接口都将承担单一职责。

  • IPokerHandGenerator_SpecBased:用于测试目的的扑克牌型生成器的接口。
  • IPokerHandGenerator_Random:用于实际游戏扑克牌型生成器的接口。

单一职责 - 实例

两个扑克牌型生成器接口提供了讨论类或接口范围蔓延(Scope Creep)的绝佳机会。范围蔓延意味着类或接口超出了单一职责。

由一个类生成用于各种情况的扑克牌型似乎是合理的;类的单一职责只是更宽泛一些;然而,以下是此类的问题:

  • 开发工作:这使得在开发人员或开发阶段之间分配工作更加困难,因为需要开发一个较大的类,而不是两个较小的类。
  • 不必要的/不需要的功能:在生产环境中运行的软件将使用一个包含测试环境所需功能的类。

项目代码:扩展方法

扩展方法位于 SOLIDPoker.PokerHandMachine.ExtensionMethods.cs。扩展方法很方便,可以使代码更简短易读。

int 扩展方法使得可以使用 int 代替牌值枚举;这是必需的,因为牌值枚举无法区分低 A 和高 A,但使用 int 可以区分低 A 和高 A。

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;
    }
}

项目代码:生成基于规格的扑克牌型

这些函数满足 IPokerHandGenerator_SpecBased 契约。生成基于规格的扑克牌型有些复杂,因为需要满足以下要求:

  • 规格:牌型必须符合规格。
    • 扑克牌型:同花顺、顺子、同花、四条等。
    • 牌值范围:必须使用牌值范围内的牌。
  • 无重复牌:已生成扑克牌型中的牌不能重复使用,这基本上会在牌值范围上打孔,因为这些牌必须从牌值范围中移除。
  • 随机性:生成的扑克牌型必须是随机的,在给定规格内随机。

注意Private 方法使用 int 而非牌值枚举,因为 int 可以区分低 A 和高 A。

注意 2:文章中只包含部分代码,因为显示所有代码是不切实际的。

以下是 GenerateSpecBasedPokerHand 函数的最后一个重载。

/// <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
    );
}

此函数很长,但它遵循 DRY 原则和单一职责原则。大量的逻辑被分离到函数中,以简化代码并防止逻辑重复。有人认为此函数可以/应该进一步细分;然而,这会增加代码量,而不会真正简化它。将来,代码可能会通过依赖注入进行重构,以使其更具可定制性。

/// <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;
}

单元测试

到目前为止,还没有进行单元测试;已开发的功能只是在演示。

SOLID 项目非常适合单元测试,而单元测试非常重要;如此重要,以至于本系列的第四篇文章(第 4 部分)将完全用于单元测试。

Hello World

这个小应用程序的唯一目的是测试和演示最新功能。

注意:测试一张高牌型是否能击败相同牌型的一张低牌型,是一种简单的方法来测试扑克牌型是否确实是使用指定范围内的牌值生成的。

Hello World 代码

基于规格的扑克牌型生成器消除了所有样板代码,并使得编写基于算法的演示代码成为可能。

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();
}

看见就说

目标是提供清晰、无错误的内容,对此您的帮助将不胜感激。如果您发现错误或潜在的改进之处,请务必发表评论。欢迎所有反馈。

结论

这个基于规格的扑克牌型生成器花费了大量的工作,但它将节省单元测试中的大量时间和代码。此生成器提供的自动化程度将使得对扑克牌型评估器类执行所有可能的单元测试相对容易,从而确保代码的健壮性。

历史

  • 2017 年 4 月 2 日:初始版本
© . All rights reserved.