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

Blackjack - 一个真实的 OOD 示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (41投票s)

2002年10月31日

6分钟阅读

viewsIcon

242749

downloadIcon

9624

通过研究二十一点游戏学习 .NET 中的面向对象设计

引言

2002 年,当我第一次发布这个应用程序时,我向所有人发起挑战,让他们改进它,并提供了一份我认为需要改进的功能列表。没有人接受我的挑战,所以我最终自己添加了一些功能。

其中最重要的是保险。当庄家亮出一张 A 时,玩家有机会购买保险,通常花费原始赌注的一半。然后庄家会看他的底牌,如果他有二十一点,你就投保了,什么都不会输。如果他没有二十一点,你就会输掉保险赌注,游戏照常进行。这个功能在最初的游戏中没有,如果庄家有二十一点,你就会立即输掉。当然,如果庄家底牌是 A,你仍然会立即输掉……抱歉,这是规则。

然后我找到了一篇名为 AquaButton 的文章(有关鸣谢,请参阅 Readme.doc),它子类化了按钮控件,并在它是默认按钮时使其发光。我想如果“正确”移动的按钮发光以向用户展示该做什么,这会很酷。但我也希望我的按钮能够显示图像,所以我编写了自己的按钮版本。请随意在您的应用程序中独立使用此按钮。

如果您仔细查看了代码,您会注意到 Strategy 类中有一个从未使用过的“Draw”方法。此方法可以创建正确移动的图形。去过拉斯维加斯的人可能都见过这种图表;他们甚至会在牌桌上直接分发,并且在游戏过程中参考它是合法的。我的版本可以通过右键单击游戏区域并从弹出菜单中选择“策略窗口”选项来获取。您会注意到,图表会根据所选策略为每个玩家而变化。如果玩家使用高低策略,图表会根据牌数而变化(高低是唯一这样做的策略)。

玩家控件已移至弹出菜单,因为我添加了太多控件,导致小表单窗口变得难以管理。右键单击每个玩家圆圈以获取玩家特定设置。右键单击其他任何地方以获取通用设置,例如策略窗口。

初始玩家设置已从代码移至 app.config 文件,因此您可以设置初始玩家数量、方法和策略

代码也进行了大量清理。我最初编写这个游戏是为了学习 .NET 窗体编程(那时我主要从事 Web 开发)。由于我正在学习,我尝试使用每个功能和细微之处,只是为了看看它是如何工作的。很多东西并不是真正需要的,并且使代码更难理解和维护。所以我对它进行了相当大的简化。

这是原始文章

当程序员开始创建面向对象设计时,总会出现几个问题

  • 我怎么知道要创建什么对象?
  • 我的对象应该有什么属性?
  • 我需要创建哪些方法?
  • 我怎么知道何时重载运算符?
  • 我如何组织我的类以进行继承?
  • 等等。

对象

让我们看一个真实的例子,也是一个有趣的例子。二十一点游戏非常适合面向对象设计,因为它具有可以在面向对象代码中建模的物理对象,即玩家、庄家、牌等。

这些对象之间也有关系。玩家有手牌,手牌有牌。庄家也有手牌,手牌有牌。还有一个牌靴,牌从牌靴中发到手牌中。

public class Player
public class Dealer
public class Hand
public class Card
// A shoe is just many decks of cards, usually 6 in Las Vegas
public class Shoe

对于我们的二十一点游戏,我们将有计算机控制的玩家和真人玩家。为此,我们将需要一个计算机玩家使用的策略。所以我们可以创建另一个对象,尽管不是物理对象,叫做 Strategy,它接受一些输入并就下一步行动提供建议。Strategy 对象将属于 Player 对象,每个玩家将需要一个 Hand 对象数组(玩家可以分牌,所以他们可能有多张手牌)。

public class Player
{
    private Strategy plyrStrategy;
    private Hand[] hands;
    _

一手牌只是一个 Card 对象数组

public class Hand
{
    private Card[] cards;
    _

一个牌靴也只是一个 Card 对象数组

public class Shoe
{
    private Card[] cards;
    …

现在,当我们发牌时,我们只是围着桌子从 Shoe 对象中取牌,并将它们添加到每个玩家和庄家的 Hand 对象中。

for( int k=0; k<2; k++ )
{
    foreach( Player player in players )
    {
        player.GetHands()[0].Add( shoe.Next() );
    }
    dealer.Hand.Add(shoe.Next() );
}

继承接口

当玩家分出一对 A 时,每张 A 只会再收到一张牌

if( CurrentPlayer.CurrentHand[0].FaceValue  == Card.CardType.Ace )
{
    NextCard();
    NextHand();
    NextPlayer();
}

代码不错吧?这是因为在它下面有很多支持代码,尤其是为了实现这一行

if( CurrentPlayer.CurrentHand[0].FaceValue == Card.CardType.Ace )

编译器如何知道 CurrentHand[0] 的含义?要使用这种语法,我们必须实现 IList 接口。这与 ArrayList 类和其他您可能熟悉的类使用的接口相同。通过稍微更改我们的类声明,这很容易做到

public class Hand : IList

现在还有更多工作要做。当您继承一个接口时,您必须为该接口的所有方法提供实现。对于 IList,我们需要添加

IsFixedSize
IsReadOnly
Add
Clear
Contains
IndexOf
Insert
Remove
RemoveAt

但最重要的要实现的方法是 Item,它看起来像这样

Object IList.this[int index]
{
    get { return cards[index]; }
    set { cards[index] = (Card)value; }
}

这允许我们使用数组语法,例如 CurrentHand[0],在我们告诉编译器这表示手牌中牌数组中位置 0 的牌之前,这实际上没有任何意义。如果不实现 IList,我们可能不得不写一些像 CurrentHand.GetCard(0) 这样的东西,这远没有那么酷!

我创建了哪些方法?

请注意,玩家和庄家负责绘制自己的手牌。这使得将代码添加到窗体的 Paint 事件中很方便,例如

dealer.DrawHand( drawingSurface, showDealerDownCard );
foreach( Player player in players 
{
    player.DrawHands( drawingSurface );
}

然后玩家和庄家循环遍历每张手牌,让牌自己绘制

foreach( Hand hand in hands )
{
    foreach( Card card in hand )
    {
        card.Draw( drawingSurface );
    }
}

有时,设想您想要编写的代码,然后建模您的对象以允许它,会更容易。

摘要

回到对象的设计,您可能想知道为什么 DealerPlayer 对象不继承自某个通用对象。嗯,你可以那样做。但我认为庄家和玩家没有足够的共同点来证明这一点。庄家只能有一张手牌,没有银行,没有赌注,没有策略,也没有算牌。这将需要您自行判断,这就是他们付给您高薪的原因。

您可能还会想知道 Deck 对象在哪里。难道 Shoe 不应该由许多 Deck 对象组成,而 Deck 对象又由许多 Card 对象组成吗?在现实世界中可能如此,但这是一个现实世界和面向对象设计可能更好地分道扬镳的例子。Deck 对象只会引入不必要的复杂层。鞋子更容易实现为卡片数组,尽管它必须是牌组中卡片数量(52)的倍数。

看看这篇文章的代码。这是一个功能齐全的二十一点游戏,包含策略、图形甚至算牌。但不要让它吓倒你。它实际上只是归结为上面概述的几个对象,并添加了许多精美的代码,使游戏更具吸引力。

祝您玩得开心。请查看 Readme.doc 文件,了解如何改进此应用程序的想法。

© . All rights reserved.