扩展和使用CardLib

  前面介绍了事件的定义和使用,现在就可以在Ch13CardLib中使用它们了。在库中需要添加一个LastCardDrawn事件,当使用GetCard获得Deck对象中的最后一个Card对象时,就将引发该事件。这个事件允许订阅者(subscriber)自动重新洗牌,减少需要在客户端完成的处理。这个事件将使用EventHandler委托类型,并传递一个Deck对象的应用作为事件源,这样无论处理程序在什么地方,都可以访问Shuffle()方法。在Deck.cs中添加以下代码以定义并引发事件(这段代码包含在Ch13CardLib\Deck.cs文件中):

  1. namespace Ch13CardLib
  2. {
  3. public event EventHandler LastCardDrawn;
  4. ...
  5. public Card GetCard(int cardNum)
  6. {
  7. if(cardNum >= 0 && cardNum <= 51)
  8. {
  9. if((cardNum == 51) && (LastCardDrawn != null))
  10. LastCardDrawn(this, EventArgs.Empty);
  11. return cards[cardNum];
  12. }
  13. else
  14. throw new CardOutOfRangeException((Cards)cards.Clone());
  15. }
  16. }

  这是把事件添加到Deck类定义需要的所有代码。

CardLib的扑克牌游戏客户程序

  在开发了CardLib库后,就可以使用它了。在结束讲述C#和.NET Framework中OOP技术的这个部分前,我们将编写扑克牌应用程序的基本代码,其中将使用我们熟悉的扑克牌类。

  与前面的章节一样,我们将在Ch13CardLib解决方案中添加一个客户控制台应用程序,添加一个Ch13CardLib项目的引用,使其成为启动项目。这个应用程序称为Ch13CardClient。

  首先在Ch13CardClient的一个新文件Player.cs中创建一个新类Player,相应代码可在本章下载代码的Ch13CardClient\Player.cs文件中找到。这个类包含两个自动属性:Name(字符串)和PlayerHand(Cards类型)。这些属性有私有的set访问器。但是PlayHand属性仍可以对其内容进行写入访问,这样就可以修改玩家手中的扑克牌。

  我们还把默认的构造函数设置为私有,以隐藏它,并提供了一个公共的非默认构造函数,该函数接受Player实例中属性Name的初始值。

  最后提供一个bool类型的方法HasWon()。如果玩家手中的扑克牌花色都相同(一个简单的取胜条件,但并没有什么意义),该方法就返回true。

  Player.cs的代码如下所示:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Ch13CardLib;
  7. namespace Ch13CardClient
  8. {
  9. public class Player
  10. {
  11. public string Name{ get; private set; }
  12. public Cards PlayHand { get; private set; }
  13. private Player()
  14. {
  15. }
  16. public Player(string name)
  17. {
  18. Name = name;
  19. PlayHand = new Cards();
  20. }
  21. public bool HasWon()
  22. {
  23. bool won = true;
  24. Suit match = PlayHand[0].suit;
  25. for(int i = 1; i < PlayHand.Count; i++)
  26. {
  27. won &= PlayHand[i].suit == match;
  28. }
  29. return won;
  30. }
  31. }
  32. }

  接着定义一个处理扑克牌游戏的类Game,这个类在Ch13CardClient项目的Game.cs文件中。

  这个类有4个私有成员字段:

  1. playDeck --- Deck类型的变量,包含要使用的一副扑克牌
  2. currentCard --- 一个int值,用作下一张要翻开的扑克牌的指针
  3. players --- 一个Player对象数组,表示游戏玩家
  4. discardedCards --- Cards集合,表示玩家扔掉的扑克牌,但还没有放回整副牌中。

  这个类的默认构造函数初始化了存储在playDeck中的Deck,并洗牌,把currentCard指针变量设置为0(playDeck中的第一张牌),并关联了playDeck.LastCardDrawn事件的处理程序Reshuffle()。这个处理程序将洗牌,初始化discardedCards集合,并将currentCard重置为0,准备从新的一副牌中读取扑克牌。

  Game类还包含两个实用方法:SetPlayers()可以设置游戏的玩家(Player对象数组),DealHands()给玩家发牌(每个玩家有7张牌)。玩家的数量限制为2~7人,确保每个玩家有足够多的牌。

  最后,PlayGame()方法包含游戏逻辑。在分析了Program.cs中的代码后介绍这个方法,Game.cs的剩余代码如下所示(这段代码包含在Ch13CardClient\Game.cs文件中):

  1. using System;
  2. using System.Collentions.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Ch13CardLib;
  7. namespace Ch13CardClient
  8. {
  9. public class Game
  10. {
  11. private int currentCard;
  12. private Deck playDeck;
  13. private Player[] players;
  14. private Cards discardedCards;
  15. public Game()
  16. {
  17. currentCard = 0;
  18. playDeck = new Deck(true);
  19. playDeck.LastCardDrawn += Reshuffle;
  20. playDeck.Shuffle();
  21. discardedCards = new Cards();
  22. }
  23. private void Reshuffle(object source, EventArgs args)
  24. {
  25. Console.WriteLine("Discarded cards reshuffled into deck.");
  26. ((Deck)source).Shuffle();
  27. discardedCards.Clear();
  28. currentCard = 0;
  29. }
  30. public void SetPlayers(Player[] newPlayers)
  31. {
  32. if(newPlayers.Length > 7)
  33. throw new ArgumentException(
  34. "A maximum of 7 players may play this game.");
  35. if(newPlayers.Length < 2)
  36. throw new ArgumentException(
  37. "A minimum of 2 players may play this game.");
  38. players = newPlayers;
  39. }
  40. private void DealHands()
  41. {
  42. for(int p = 0; p < players.Length; p++)
  43. {
  44. for(int c = 0; c < 7; c++)
  45. {
  46. players[p].PlayHand.Add(playDeck.GetCard(currentCard++));
  47. }
  48. }
  49. }
  50. public int PlayGame()
  51. {
  52. // Code to follow.
  53. }
  54. }
  55. }

  Program.cs中包含Main()方法,它初始化并运行游戏。这个方法执行以下步骤:

  1. 1)显示引导画面。
  2. 2)提示用户输入玩家数(27)。
  3. 3)根据玩家数建立一个Player对象数组。
  4. 4)给每个玩家取名,用于初始化数组中的一个PLayer对象。
  5. 5)创建一个Game对象,使用SetPlayers()方法指定玩家。
  6. 6)使用PlayGame()方法启动游戏。
  7. 7PlayGame()的int返回值用于显示一条获胜消息(返回的值是Player对象数组中获胜的玩家的索引)。

  这个方法的代码(为清晰起见,加了一些注释)如下所示(这段代码包含在Ch13CardClient\Program.cs文件中):

  1. static void Main(string[] args)
  2. {
  3. // Display introduction.
  4. Console.WriteLine("KarliCards: a new and exciting card game.");
  5. Console.WriteLine("To win you must have 7 cards of the same suit in" +
  6. " your hand.");
  7. Console.WriteLine();
  8. // Prompt for number of players.
  9. bool inputOK = false;
  10. int choice = -1;
  11. do
  12. {
  13. Console.WriteLine("How many players(2-7) ");
  14. string input = Console.ReadLine();
  15. try
  16. {
  17. // Attempt to convert input into a valid number of players.
  18. choice = Convert.ToInt32(input);
  19. if((choice >= 2) && (choice <= 7))
  20. inputOK = true;
  21. }
  22. catch
  23. {
  24. // Ignore failed conversions, just continue prompting.
  25. }
  26. }while(inputOK == false);
  27. // Initialize array of Player objects.
  28. Player[] players = new Player[choice];
  29. // Get player names.
  30. for(int p = 0; p < players.Length; p++)
  31. {
  32. Console.WriteLine("Player {0}, enter your name:", p + 1);
  33. string palyerName = Console.ReadLine();
  34. players[p] = new Player(playerName);
  35. }
  36. // Start game.
  37. Game newGame = new Game();
  38. newGame.SetPlayers(players);
  39. int whoWon = newGame.PlayGame();
  40. // Display winning player.
  41. Console.WriteLine("{0} has won the game!", players[whoWon].Name);
  42. Console.ReadKey();
  43. }

  接着分析一下应用程序的主体PlayGame()。由于篇幅所限,这里不准备详细讲解这个方法,而只是加注了一些注释,使其更容易理解。实际上,这些代码都不复杂,仅是较多而已。

  每个玩家都可以查看手中的牌和桌面上的那张牌,或者翻开一张新牌。在拾取一张牌后,玩家必须扔掉一张牌,如果他们拾取了桌面上的那张牌,就必须用另一张牌替换桌面上的那张牌,或者把扔掉的那张牌放在桌面上那张牌的上面(把扔掉的那张牌添加到discardedCards集合中)。

  在分析这段代码时,一个关键问题在于Card对象的处理方式。必须清楚,这些对象定义为引用类型,而不是值类型(使用结构)。给定的Card对象似乎同时存在于多个地方,因为引用可以存在于Deck对象、Player对象的hand字段、discardedCards集合和playCard对象(桌面上的当前牌)中。这样便于跟踪扑克牌,特别是可以用于从一副牌中拾取一张新牌。如果牌不在任何玩家的手中,也不在discardedCards集合中,才能接受该牌。

  代码如下所示:

  1. public int PlayGame()
  2. {
  3. // Only play if players exist.
  4. if(players == null)
  5. return -1;
  6. // Deal initial hands.
  7. DealHands();
  8. // Initialize game vars, including an initial card to place on the
  9. // table: playCard.
  10. bool GameWon = false;
  11. int currentPlayer;
  12. Card playCard = playDeck.GetCard(currentCard++);
  13. discardedCards.Add(playCard);
  14. // Main game loop, continues until GameWon == true.
  15. do
  16. {
  17. Console.WriteLine("Press T to take card in play or D to " +
  18. "draw:");
  19. string input = Console.ReadLine();
  20. if(input.ToLower() == "t")
  21. {
  22. // Add card from table to player hand.
  23. Console.WriteLine("Drawn: {0}", playCard);
  24. // Remove from discarded cards if possible(if deck
  25. // is reshuffled it won't be there any more)
  26. if(discardedCards.Contains(playCard))
  27. {
  28. discardedCards.Remove(playCard);
  29. }
  30. players[currentPlayer].PlayHand.Add(playCard);
  31. inputOK = true;
  32. }
  33. if(input.ToLower() == "d")
  34. {
  35. // Add new card from deck to player hand.
  36. Card newCard;
  37. // Only add card if it isn't already in a player hand
  38. // or in the discard pile
  39. bool cardIsAvailable;
  40. do
  41. {
  42. newCard = playDeck.GetCard(currentCard++);
  43. // Check if card is in discard pile
  44. cardIsAvailable = !discardedCards.Contains(newCard);
  45. if(cardIsAvailable)
  46. {
  47. // Loop through all player hands to see if newCard
  48. // is already in a hand.
  49. foreach(Player testPlayer in players)
  50. {
  51. if(testPlayer.PlayHand.Contains(newCard))
  52. {
  53. cardIsAvailable = false;
  54. break;
  55. }
  56. }
  57. }
  58. }while(!cardIsAvailable);
  59. // Add the card found to player hand.
  60. Console.WriteLine("Drawn: {0}", newCard);
  61. players[currentPlayer].PlayHand.Add(newCard);
  62. inputOK = true;
  63. }
  64. }while(inputOK == false);
  65. // Display new hand with cards numbered.
  66. Console.WriteLine("New hand:");
  67. for(int i = 0; i < players[currentPlayer].PlayHand.Count; i++)
  68. {
  69. Console.WriteLine("{0}: {1}", i+1,
  70. players[currentPlayer].PlayHand[i]);
  71. }
  72. // Prompt player for a card to discard.
  73. inputOK = false;
  74. int choice = -1;
  75. do
  76. {
  77. Console.WriteLine("Choose card to discard:");
  78. string input = Console.ReadLine();
  79. try
  80. {
  81. // Attempt to convert input into a valid card number.
  82. choice = Convert.ToInt32(input);
  83. if((choice > 0) && (choice <= 8))
  84. inputOK = true;
  85. }
  86. catch
  87. {
  88. // Ignore failed conversions, just continue prompting.
  89. }
  90. }while(inputOK == false);
  91. // Place reference to removed card in playCard (place the card
  92. // on the table), then remove card from player hand and add
  93. // to discarded card pile.
  94. playCard = players[currentPlayer].PlayHand[choice - 1];
  95. players[currentPlayer].PlayHand.RemoveAt(choice - 1);
  96. discardedCards.Add(playCard);
  97. Console.WriteLine("Discarding: {0}", playCard);
  98. // Space out text for players
  99. Console.WriteLine();
  100. // Check to see if player has won the game, and exit the player
  101. // loop if so.
  102. GameWon = players[currentPlayer].HasWon();
  103. if(GameWon == true)
  104. break;
  105. }
  106. } while(GameWon == false);
  107. // End game, noting the winning palyer.
  108. return currentPlayer;
  109. }

  玩这个游戏,确保花一些时间去仔细研究它。应尝试在Reshuffle()方法中设置一个断点,由7个玩家来玩这个游戏。如果在拾取了扑克牌后,马上扔掉它,不需要太长的时间就要重新洗牌了,因为7个玩家玩这个游戏时,就只富余3张牌。这样就可以注意3张牌何时重新翻开,验证程序是否正常执行。