This is the solution for the third "Mega Challenge" called MegaChallengeWar. The goal of this challenge was for you to create an application that simulates the computer playing a game of War. This solution will go into detail on all the steps required to solve this challenge in a particular manner. This is by no means the only solution to the challenge, but hopefully it will help you to understand the thought process behind completing it successfully. Read on as far as you need to in order to understand the problem you're facing, or if you solved it and want to see how it compares, feel free to compare and leave a comment with your solution!
To begin with, create a new Project called MegaChallengeWar and add to it a Default.apsx page as we've been doing throughout this series. In the Design view, add the following text and server controls to the form, with the matching programmatic ID's:
That is all we will add to this page for now, as the Default.aspx will dependent on the other classes to provide it the information it needs. It will primarily concern itself with displaying information back to the user. So, let's begin adding the other classes that will generate information and perform the game logic. Begin by right-clicking on your Project in the Solution Explorer and select Add > New Class... then name the new class Game.cs:
Repeat this process to add three more classes:
We'll begin working with these classes by first creating the deck, because the cards contained within it are required for every other part of the game. In Deck.cs, initialize a List<Card> called _deck as a private property for the class. Then, create a constructor method for the Deck, initializing the _deck to a new List<Card>:
In order to populate _deck with Cards, we need to populate the Card class with the appropriate properties needed. We'll give it two string properties, one for Suit, and one for Kind. Suit would refer to either Clubs, Spades, Diamonds or Hearts, whereas Kind refers to either the number or Jack, Queen, King or Ace:
Now that we have properties for the cards, let's go back and populate _deck with cards. Now, one way we could do this is by using a Collection Initializer on the _deck, like so:
However, you can see that the downfall of this is that you would need to create an entry for each Card, 52 in total. That would be a lot of typing and clutter, and opens up the possibility for mistyped data. If you choose to do it this way, that's fine. However, for the purposes of this solution, we'll do it in a different manner.
Create two new string arrays called 'suits' and 'kinds'. These array will hold their respective values, suits with the suits and kinds with the kinds:
Now, we can use foreach() statements to iterate through the arrays:
This will ensure that for each suit, each kind is evaluated, and then a new Card with the current suit and kind is added to _deck. That way, each Suit receives all the cards it needs and they are added to _deck without needing to list every single card.
At this point, it would be helpful to make sure that the code we've written for the Deck class works, as the rest of our code will depend on it. It's usually a bad idea to write a lot of code without testing, because if you encounter a bug, you won't know where it's located. So, let's test this code by temporarily making the _deck property public:
What this will allows us to do is initialize a deck in the Default.aspx.cs when the user presses the playButton. Make sure to generate the playButton_Click by double-clicking the playButton in the Default page. Inside this code block, initialize a new Deck called deck. Then, using a foreach() statement, we'll want to iterate through each card in the _deck property of deck and display it's information to the resultLabel:
If done successfully, you should see your deck's suits and kinds listen when the playButton is pressed:
Now that we know the code works, delete the code in the playButton_Click and reset _deck to a private property within the Deck class.
Now that we have initialized a starting deck, we need to actually deal out the cards to the players. The first player will be dealt a random card, followed by the second player being dealt a random card, then returning to the first and so on, until all the cards in the deck have been dealt. This way, no one player has the advantage or disadvantage in terms of which cards they receive.
Before cards can be dealt to the players, the Player class needs to be populated with properties that can be worked with. Navigate to Player.cs and create the following properties:
Now that the Player is expecting a List<Card> to populate their hand, we can begin to deal cards to them. To do this, return to the Deck class and create a new method called Deal() that takes in two Players:
Because the process of dealing cards will be done randomly, an instance of Random will need to be created for this class and initialized inside the constructor:
The process of dealing cards to players will consist of three main steps:
Notice that, while these steps all pertain to some aspect of dealing cards, performing it all in the Deal() method would result in a loss of cohesion. Deal() shouldn't know how to locate or remove cards from _deck, nor should it be a multi-step process within the method. Deal() should be focused on simply giving cards to each Player it is passed. This is a good indication that the responsibility of retrieving a Card and then removing it should be delegated to a new helper method.
Create a new private method called dealCard() that takes a Player as a parameter:
Now, inside this method we will perform the same basic logic as we would have in the Deal() method, accessing a random member of _deck, assigning it to player, then removing that member of _deck:
Finally, call the dealCard() method for each player in the Deal() method:
Now that we've created the methods for dealing cards to the players, we need to test them in order to ensure it functions properly. We can do this by displaying the result of the deal to the screen. To do this, create a new private StringBuilder field in the Deck class called _sb and initialize it in the Deck() constructor:
Note: When using the StringBuilder class, you may need to add a using statement for System.Text. You can do this manually, or by pressing Ctrl + '.' when you type in 'StringBuilder', and generate the using statement from there.
Inside the dealCard() method, we'll use this StringBuilder instance to piece together the name of the player, the suit and kind of the card they were dealt and text to format it for display:
Because _sb is a private field within this class, we can reference it directly from the Deal() method located in the same class. Change the return type of Deal() from void to string, so that it will return the StringBuilder text to the caller after the while() loop is completed:
This will work properly, but there is no call to the Deal() method that this string can be returned to. The caller to this method should be the same caller that is responsible for coordinating the calls to the other main methods within our game. This is the explicit purpose of the Game class, to act as a manager for all the elements of the game.
Before we can do this, the Game class needs to be populated with the properties it needs in order to function. Navigate to Game.cs to create private fields and create a constructor for the class with the following code to initialize the players:
Next, create a public method called Play() that initializes an instance of Deck and calls the deck.Deal(), passing in the two players:
Next, navigate to Player.cs to create a constructor for Player that initializes the Player's cards. This will allow us to retrieve that List<Card> representing each player's hand whenever the Play() method is called:
Return to the playButton_Click in Default.aspx.cs. Write the following code to call the Play() method and display the results to screen:
Save and run your project to test and see the results:
Now that each player has a hand of cards that they can battle with, we can begin to fill out the actual battle logic. The battle will be handled in three basic steps:
We will tackle these steps individually to ensure that our concerns are separated effectively. To begin with, we'll need to make a way to determine the value of each card being compared in within battle. For comparison's sake, we'll assign a numeric value to each card that corresponds to its rank in the deck.
In the Card class, create a new public int method called CardValue() and write in the following code to determine each Card's given value:
This code might look a bit confusing at this point, but we'll talk about the switch() statement in more detail in a later lesson. This essentially works as several compounded if() statements. It begins by checking if the current Card's Kind is equal to any of the given string values, then assigning it the appropriate numeric value. If none of the cases apply, it defaults to a TryParse() to convert the string value (i.e. "2") to an integer (literal 2). Finally, the method returns the integer value to the caller.
Return to the Game class' Play() method, and create a while() loop that ensures that neither player has exhausted their cards:
The next step is to determine which card has a greater value. We'll do this by creating an if() statement to determine if player1's card value is greater than player2's by calling the CardValue() method:
The player who wins will take both cards being evaluated and add them to the bottom of their deck. However, this concern is separate from the Play() method and should comprise a method of its own. Create the following private method that returns type Card:
This removes the provided player's card from their hand, but does not yet add the card to the winning player's deck. Because there will be two separate calls to this method (one for each player), we will need to create a List<Card> that will hold the retrieved card from each method call. Create a new private field of type List<Card> to hold the bounty of cards that will be added to the winning player's deck and initialize it within the constructor:
Return to getCard() and add the card to the _bounty List:
Returning to the while() loop within the Play() method, rewrite the player1Card and player2Card variables, setting them each equal to the returned value of the getCard() method:
Instead of housing the comparison logic within the Play() method (which should be coordinating method calls rather than performing logic), we'll create a new private method called performEvaluation(). Move the evaluation code into the method, so that it looks like this:
If player1's card value is greater than player2's, we'll add the _bounty to their deck, or else if player2's card value is greater, they will receive the bounty.
Note: The else if() is used here instead of else, because the else() will be used later on to account for a tie leading to a War scenario.
Return to the Play() method and create a string result variable, assigned to the value of deck.Deal(). This will allow the method to continue on to the while() loop instead of returning out of the method at that point:
Next, call performEvaluation() inside the while() loop, passing in the players and their cards. Finally, we need to satisfy one of the requirements for this challenge, to terminate the game after 20 rounds of play. To do this, we'll create a new integer variable called round and increment it at the end of each loop. Then, create an if() statement to break out of the loop if round is greater than 20:
Finally, within the performEvaluation() method, use the Clear() method to remove all the cards from _bounty after they've been assigned to a Player:
Now that the comparison has been performed, and we've broken out of the battle loop, we need to make a way to display the winner to the screen. Create a new private method called determineWinner() that evaluates which player ended up with the greater number of cards:
We'll fill in the logic, adding to result based upon which player won, then also adding both player's card count and returning the result to the caller:
Call determineWinner() beneath the while() loop in the Play() method:
Save and run your project to see the result:
If you review the code we've written so far, specifically in the Game class, it becomes apparent that it needs to be refactored and cleaned up. The methods and responsibilities of the Game class have expanded beyond what they should. Not only is it responsible for coordinating the play of the Game, it houses all the logic for the battles themselves. In order to separate our concerns out and keep our classes cohesive, let's create a new class called Battle that will be responsible for the battle logic. Once created, begin by moving the code for _bounty over to the new Class, making sure to remove that code from Game.cs:
Next, transfer the getCard() and performEvaluation() methods into the new Battle class as well:
Next, create a new public method within Battle called PerformBattle(), using the following code from the Play() method:
In order to access the PerformBattle method in the Game class, a new instance of Battle will need to be initialized. Return to the Play() method's while() loop to create a new instance of Battle, and call its PerformBattle() method, passing in the required players:
You may notice when testing your code that sometimes the result doesn't include all of the cards in the deck. The reason for this is found in the performEvaluation() method. We created if() and else if() statements in the event that the cards had unequal values, but never created a case for a tie. We're going to evaluate that case now, as we return to the Battle class to create a new helper method to deal with this war scenario.
In Battle.cs, create a new private method called war(), passing in both player1 and player2:
A War will play out in the following manner:
We'll begin by adding the three cards for player1 into _bounty by calling the getCard() method three times. The second card, however, will be assigned to a new variable called warCard1:
Repeat this process for player2's cards as well, so that all the cards are added to the bounty. After that process is complete, call performEvaluation() within this method, passing in both players and both warCards, so that the new cards perform a battle:
Now, call the war() method from within performEvaluation() under the else() clause:
We've now successfully implemented the logic for the war() method and called it, but we have no way of ensuring that it functions properly. We need to display to the screen that the war has taken place, which cards are being compared, and which are in the bounty.
To do this, we'll create two new helper methods in the Battle class: displayBattleCards() and displayBountyCards():
Because it is evaluating the two cards being compared in the war, displayBattleCards() has an input parameter expectation for two cards. In contrast, the displayBountyCards() method will look at the private field _bounty, so it doesn't need any input parameters.
In order to actually format the text for display to the user, create a private StringBuilder field within the Battle class and name it _sb. Then, initialize this field in the constructor:
Next, return to displayBattleCards() and type in the following code to append the information to the StringBuilder:
The displayBountyCards() method will implement the same logic, but in a different way because we won't know at the onset how many cards are contained within _bounty. What we can do is iterate through the List<Card> and print out a result for each card in the collection:
Now that we have a way to display both the evaluation and results of the battle, we need to call the appropriate methods from performEvaluation(). However, when you look at performEvalution(), notice how many responsibilities it is manages after calling these methods:
This method is not only performing the evaluation of cards and determining the winner, but it's displaying the cards, managing _bounty, adding to it and divvying it out. At the very least, the process of managing _bounty should be handled in a different helper method. Within Battle.cs, create a new private void awardWinner() method with the following code transferred from performEvaluation():
Now make the following changes to the performEvaluation() method to call awardWinner():
Return to the awardWinner() method and modify it in order to append the winning player's name to the StringBuilder:
Save and run your project to see the result at this point:
Now that our entire game logic is firmly in place, the final step is to go through and finish up the formatting for the game, making it easily readable to the user. We'll begin in the Play() method of the Game class by creating headers for when dealing cards and when the battle begins:
We'll also need to indicate to the user that a war scenario is playing out. To do this, return to the war() method in Battle.cs and append the following to the StringBuilder to identify a war:
Save and run your project to see the result:
This completes the solution for MegaChallengeWar. This is simply one of many possible ways to complete the challenge, and it is certainly far from the best. While there are many better ways to handle displaying the results to the user, we have not yet covered them, which meant we had to stitch a lot together. Sometimes the process can be messy when getting a particular result, but it does work. As always, if you solved the challenge in a different way, that is perfectly acceptable. If you didn't complete the challenge, try again and reinforce these concepts in your mind. A lot of this process is trial and error, refactoring and modifying. Coding isn't like chiseling a statue out of stone; it's akin to molding a statue from clay. It changes and evolves throughout the process of creating it. And that's good. It gets better, more precise and more cohesive through that process. Good job on completing this challenge, keep it up!
Solution - Mega Challenge War