扩展和使用CardLib

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

    namespace Ch13CardLib
    {
        public event EventHandler LastCardDrawn;

        ...

        public Card GetCard(int cardNum)
        {
            if(cardNum >= 0 && cardNum <= 51)
            {
                if((cardNum == 51) && (LastCardDrawn != null))
                    LastCardDrawn(this, EventArgs.Empty);
                return cards[cardNum];
            }
            else
                throw new CardOutOfRangeException((Cards)cards.Clone());
        }
    }

  这是把事件添加到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的代码如下所示:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Ch13CardLib;

    namespace Ch13CardClient
    {
        public class Player
        {
            public string Name{ get; private set; }
            public Cards PlayHand { get; private set; }

            private Player()
            {
            }

            public Player(string name)
            {
                Name = name;
                PlayHand = new Cards();
            }

            public bool HasWon()
            {
                bool won = true;
                Suit match = PlayHand[0].suit;
                for(int i = 1; i < PlayHand.Count; i++)
                {
                    won &= PlayHand[i].suit == match;
                }
                return won;
            }
        }
    }

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

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

    • playDeck --- Deck类型的变量,包含要使用的一副扑克牌
    • currentCard --- 一个int值,用作下一张要翻开的扑克牌的指针
    • players --- 一个Player对象数组,表示游戏玩家
    • 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文件中):

    using System;
    using System.Collentions.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Ch13CardLib;

    namespace Ch13CardClient
    {
        public class Game
        {
            private int currentCard;
            private Deck playDeck;
            private Player[] players;
            private Cards discardedCards;

            public Game()
            {
                currentCard = 0;
                playDeck = new Deck(true);
                playDeck.LastCardDrawn += Reshuffle;
                playDeck.Shuffle();
                discardedCards = new Cards();
            }

            private void Reshuffle(object source, EventArgs args)
            {
                Console.WriteLine("Discarded cards reshuffled into deck.");
                ((Deck)source).Shuffle();
                discardedCards.Clear();
                currentCard = 0;
            }

            public void SetPlayers(Player[] newPlayers)
            {
                if(newPlayers.Length > 7)
                    throw new ArgumentException(
                        "A maximum of 7 players may play this game.");

                if(newPlayers.Length < 2)
                    throw new ArgumentException(
                        "A minimum of 2 players may play this game.");

                players = newPlayers;
            }

            private void DealHands()
            {
                for(int p = 0; p < players.Length; p++)
                {
                    for(int c = 0; c < 7; c++)
                    {
                        players[p].PlayHand.Add(playDeck.GetCard(currentCard++));
                    }
                }
            }

            public int PlayGame()
            {
                // Code to follow.
            }
        }
    }

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

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

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

    static void Main(string[] args)
    {
        // Display introduction.
        Console.WriteLine("KarliCards: a new and exciting card game.");
        Console.WriteLine("To win you must have 7 cards of the same suit in" +
            " your hand.");
        Console.WriteLine();

        // Prompt for number of players.
        bool inputOK = false;
        int choice = -1;

        do
        {
            Console.WriteLine("How many players(2-7)?");
            string input = Console.ReadLine();

            try
            {
                // Attempt to convert input into a valid number of players.
                choice = Convert.ToInt32(input);
                if((choice >= 2) && (choice <= 7))
                    inputOK = true;
            }
            catch
            {
                // Ignore failed conversions, just continue prompting.
            }

        }while(inputOK == false);

        // Initialize array of Player objects.
        Player[] players = new Player[choice];

        // Get player names.
        for(int p = 0; p < players.Length; p++)
        {
            Console.WriteLine("Player {0}, enter your name:", p + 1);
            string palyerName = Console.ReadLine();
            players[p] = new Player(playerName);
        }

        // Start game.
        Game newGame = new Game();
        newGame.SetPlayers(players);
        int whoWon = newGame.PlayGame();

        // Display winning player.
        Console.WriteLine("{0} has won the game!", players[whoWon].Name);
        Console.ReadKey();
    }

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

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

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

  代码如下所示:

    public int PlayGame()
    {
        // Only play if players exist.
        if(players == null)
            return -1;

        // Deal initial hands.
        DealHands();

               // Initialize game vars, including an initial card to place on the
               // table: playCard.
               bool GameWon = false;
               int currentPlayer;
               Card playCard = playDeck.GetCard(currentCard++);
               discardedCards.Add(playCard);

               // Main game loop, continues until GameWon == true.
               do
               {
                   Console.WriteLine("Press T to take card in play or D to " +
                       "draw:");
                   string input = Console.ReadLine();
                   if(input.ToLower() == "t")
                   {
                       // Add card from table to player hand.
                       Console.WriteLine("Drawn: {0}", playCard);

                       // Remove from discarded cards if possible(if deck
                       // is reshuffled it won't be there any more)
                       if(discardedCards.Contains(playCard))
                       {
                           discardedCards.Remove(playCard);
                       }
                       players[currentPlayer].PlayHand.Add(playCard);
                       inputOK = true;
                   }
                   if(input.ToLower() == "d")
                   {
                       // Add new card from deck to player hand.
                       Card newCard;

                       // Only add card if it isn't already in a player hand
                       // or in the discard pile
                       bool cardIsAvailable;
                       do
                       {
                           newCard = playDeck.GetCard(currentCard++);
                           // Check if card is in discard pile
                           cardIsAvailable = !discardedCards.Contains(newCard);
                           if(cardIsAvailable)
                           {
                               // Loop through all player hands to see if newCard
                               // is already in a hand.
                               foreach(Player testPlayer in players)
                               {
                                   if(testPlayer.PlayHand.Contains(newCard))
                                   {
                                       cardIsAvailable = false;
                                       break;
                                   }
                               }
                           }
                       }while(!cardIsAvailable);
                       // Add the card found to player hand.
                       Console.WriteLine("Drawn: {0}", newCard);
                       players[currentPlayer].PlayHand.Add(newCard);
                       inputOK = true;
                   }
               }while(inputOK == false);

               // Display new hand with cards numbered.
               Console.WriteLine("New hand:");
               for(int i = 0; i < players[currentPlayer].PlayHand.Count; i++)
               {
                  Console.WriteLine("{0}: {1}", i+1, 
                      players[currentPlayer].PlayHand[i]);
               }

               // Prompt player for a card to discard.
               inputOK = false;
               int choice = -1;
               do
               {
                   Console.WriteLine("Choose card to discard:");
                   string input = Console.ReadLine();
                   try
                   {
                        // Attempt to convert input into a valid card number.
                        choice = Convert.ToInt32(input);
                        if((choice > 0) && (choice <= 8))
                            inputOK = true;
                    }
                    catch
                    {
                        // Ignore failed conversions, just continue prompting.
                    }
                }while(inputOK == false);

                // Place reference to removed card in playCard (place the card
                // on the table), then remove card from player hand and add
                // to discarded card pile.
                playCard = players[currentPlayer].PlayHand[choice - 1];
                players[currentPlayer].PlayHand.RemoveAt(choice - 1);
                discardedCards.Add(playCard);
                Console.WriteLine("Discarding: {0}", playCard);

                // Space out text for players
                Console.WriteLine();

                // Check to see if player has won the game, and exit the player
                // loop if so.
                GameWon = players[currentPlayer].HasWon();
                if(GameWon == true)
                    break;

            }
        } while(GameWon == false);

        // End game, noting the winning palyer.
        return currentPlayer;
    }

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

🔚