public class Lesson6_GoFish_Pt2

{

void StartingTheGame()

{

This is a continuation of Lesson5_GoFish_Pt1. The full project code can be viewed or downloaded at https://github.com/TimPurdum/GoFish. In the previous post, we created a model of all the pieces needed to play the game, namely a Deck of Cards and Players with Hands and Sets.

 

For this part, we will be describing and programming the action or gameplay. Since the project is short, we will do this all within the Program class, although you could certainly create a Game class and move most of the code there. Let’s start by instantiating two players (one human and one computer) and a deck of cards as class fields that can be accessed from any method. The readonly tag means that these objects cannot be recreated (written to) later, although the methods and properties within are still writable.

        static readonly Deck Deck = new Deck();
        static readonly Player AI = new Player();
        static readonly Player Player = new Player();

 

Now let’s look at our Main method. Of course, you could put the entire logic into this method, but that would be poor design, and harder to read. Instead, we will simply deal with the introduction, shuffling the deck, and starting a while loop to create the game turns.

        static void Main()
        {
            Console.WriteLine("Let's play Go Fish!");
            Console.WriteLine("Type the letter 's' to shuffle the deck.");
            if (Console.ReadLine()?.ToLower() == "s")
            {
                Console.WriteLine("Shuffling...");
                Deck.Shuffle();
                Console.WriteLine();
                Deck.Deal(new List{Player, AI}, 7);

                while (Deck.Count > 0)
                {
                    ShowHand();
                    Guess();
                    AIGuess();
                }

                GameOver();
            }
        }

 

Notice that just like in our ChatBot project, we are using Console.WriteLine and Console.Readline to communicate with the user. Of course, you don’t have to introduce the game at all, or you could make it more involved (get the player’s name, for example).

 

The while loop represents rounds of the game. In each round, first we will ShowHand to the player, so they know what cards they have. Then, they will make a Guess, and we will deal with the results of that guess. Finally, the computer player (AI) will make their AIGuess, and the loop starts over. It ends when the deck is empty.

}
 

void ShowYourCards()

{

Each turn, we want to tell the player what cards they are holding.

        static void ShowHand()
        {
            Console.WriteLine("Here is your hand:");
            foreach (var card in Player.Hand)
            {
                Console.WriteLine($" - {card.Rank} of {card.Suit}");
            }
        }

}
 

void PlayersTurn()

{

Now it’s time for the player to take a turn. For this, we need to use Console.ReadLine() again. The player must have that rank in their hand (and if they have none, they simply draw). If the card is found, it is removed from the AI’s hand and given to the player. If not, the player must draw.

        static void Guess()
        {
            if (Player.Hand.Count == 0)
            {
                Console.WriteLine("No cards, you must draw this turn!");
                Deck.Draw(Player, 1);
                return;
            }
            
            Console.WriteLine();
            Console.WriteLine("Ask me if I have a card...");
            Console.WriteLine("(e.g., Ace, Two, Three, Four, King...)");
            var guess = Console.ReadLine();

            while (Player.Hand.All(c => c.Rank.ToString().ToLower() != guess?.ToLower()))
            {
                Console.WriteLine("You may only guess card numbers that you already have.");
                Console.WriteLine("Ask me if I have a card...");
                guess = Console.ReadLine();
            }
            
            Console.WriteLine();
            var cards = FindCards(guess, AI);
            
            if (cards != null)
            {
                var message = $"You got {cards.Count} {cards.First().Rank}!";
                if (cards.Count > 1)
                {
                    message = $"You got {cards.Count} {cards.First().PluralName}!";
                }
                Console.WriteLine(message);
                
                Player.Hand.AddRange(cards);
                foreach (var c in cards)
                {
                    AI.Hand.Remove(c);
                }
                LayoutSets(Player);
                Guess();
            }
            else
            {
                Console.WriteLine("No! Go Fish!");
                Thread.Sleep(1000);
                Deck.Draw(Player, 1);
                var newCard = Player.Hand.Last();
                Console.WriteLine($"You drew the {newCard.Rank} of {newCard.Suit}");
                LayoutSets(Player);
            }
        }

 

There are two places here where we have called new methods. The first is FindCards, which will search the AI player’s hand for the rank that was guessed. We are making heavy use of LINQ lambda expressions here, which you can learn more about. Any and Where are two LINQ statements that allow you to query a collection, such as a List or Array. Within each one, we declare an internal variable c, which represents each Card in the List, similar to a foreach statement. If the rank isn’t found, we can simply return null, which means no List was found.

        static List FindCards(string guess, Player p)
        {
            var cardGuess = guess.Trim().ToLower();

            if (p.Hand.Any(c => c.Rank.ToString().ToLower() == cardGuess))
            {
                return p.Hand.Where(c => c.Rank.ToString().ToLower() == cardGuess).ToList();
            }
           
            return null;
        }

 

The second new method is LayoutSets. This is where we will check to see if the player has four matching cards, and if so, move those sets to the Sets List.

        static void LayoutSets(Player p)
        {
            Console.WriteLine();
            
            var numberGroups = p.Hand.GroupBy(c => c.Rank);
            foreach (var numberGroup in numberGroups)
            {
                if (numberGroup.Count() == 4)
                {
                    var numberCards = numberGroup.ToList();
                    p.Sets.Add(numberCards);
                    foreach (var c in numberCards)
                    {
                        p.Hand.Remove(c);
                    }
                }
            }
            
            if (p.Sets.Count == 0)
            {
                return;
            }

            ShowSets(p);
        }

 

Notice that both FindCards and LayoutSets take a Player as an argument. This means that we can reuse these classes when it comes to the AI’s turn.

}
 

void ComputersTurn()

{

Once the player has finished, we give the computer (AI) a turn. Using Thread.Sleep() here and there can give the game a more pleasant pace, as modern computers are so fast that they will run through all these commands in less than a second.

 

Of course, in order to guess, we have to have the AI choose a random card rank from its hand. Luckily, .Net has a function called Random() that can do just that!

        static void AIGuess()
        {
            if (AI.Hand.Count == 0)
            {
                Console.WriteLine("No cards, I must draw this turn!");
                Deck.Draw(AI, 1);
                return;
            }
            
            var randomGenerator = new Random();

            var guessCard = AI.Hand[randomGenerator.Next(0, AI.Hand.Count - 1)];
            
            Console.WriteLine("My turn.");
            
            Console.WriteLine($"Do you have any {guessCard.PluralName}?");
            Thread.Sleep(2000);
            Console.WriteLine();
            var cards = FindCards(guessCard.Rank.ToString(), Player);
            
            if (cards != null)
            {
                if (cards.Count == 1)
                {
                    Console.WriteLine($"Yes? I'll take that {guessCard.Rank}!");
                }
                else
                {
                    Console.WriteLine($"Yes? I'll take those {guessCard.PluralName}!");
                }
                
                AI.Hand.AddRange(cards);
                foreach (var c in cards)
                {
                    Player.Hand.Remove(c);
                }
                LayoutSets(AI);
                AIGuess();
            }
            else
            {
                Console.WriteLine("No? I have to draw...");
                Thread.Sleep(1000);
                Deck.Draw(AI, 1);
                LayoutSets(AI);
            }
        }

 

As mentioned above, we re-use FindCards and LayoutSets for both players. Code re-use is an important principal in good programming.

}
 

void GameOver()

{

Let’s wrap up the game by totaling up the sets laid out and declaring a winner!

        static void GameOver()
        {
            Console.WriteLine("Game Over!");
            var playerPoints = 0;
            Player.Sets.ForEach(set => playerPoints += set.Count);
            Console.WriteLine($"Your score is {playerPoints}");
            var aiPoints = 0;
            AI.Sets.ForEach(set => aiPoints += set.Count);
            Console.WriteLine($"My score is {aiPoints}");
            if (playerPoints > aiPoints)
            {
                Console.WriteLine("You Win!");
            }
            else if (aiPoints > playerPoints)
            {
                Console.WriteLine("I Win!");
            }
            else
            {
                Console.WriteLine("Tie Game!");
            }
        }

 

If you have cloned the GitHub GoFish repository, and made changes to your version, feel free to create a pull request, to share your improvements back to our main repo! Or if you have a variation (maybe a new card game), you can share a link in the comments below.

}

}

Leave a Reply

Your email address will not be published. Required fields are marked *