NOTE: This is Part 2 of a three-part series demonstrating how we might model the card game War as a C# program. Part 1 is over here. You might want to use the sample project over on GitHub to follow along with this post. Also, check out my other posts in the Modeling Practice series!
Now that we've got our Objects, observations, and other rules in place, it's time to start building them!
The Card
Let's start with the simplest object in this model: the card itself.
In our model, each Card has three properties:
- A Suit (Hearts, Clubs, Diamonds, or Spades)
- A Value
- A Display Name (e.g. "10D" for 10 of diamonds, "AC" for Ace of Clubs, etc.)
I ended up using a C# enum to represent the Suit of the cards, so here's that enum plus the Card object:
public enum Suit
{
Clubs,
Diamonds,
Spades,
Hearts
}
public class Card
{
public string DisplayName { get; set; }
public Suit Suit { get; set; }
public int Value { get; set; }
}
Player
Now let's start with a very simple definition for the Player
object. A player, in our model, has the following properties:
- A name
- A collection of cards (this is the player's deck).
So, our player object looks like this:
public class Player
{
public string Name { get; set; }
public Queue<Card> Deck { get; set; }
}
Now before you go yelling at me, let me explain why there is no Deck
object...
Deck of Cards
Here's the first secret in our model: there actually isn't an object called Deck
. Rather, we implement a deck of cards using the built-in Queue<>
object in C#. That collection has almost all the functionality we need to implement a deck of cards, so why not use it?
Key word: Almost. We need one other method to make our code simpler: a method which can "enqueue", or place into the collection, a collection of Cards rather than just a single one. Here's that extension method:
public static class Extensions
{
public static void Enqueue(this Queue<Card> cards, Queue<Card> newCards)
{
foreach(var card in newCards)
{
cards.Enqueue(card);
}
}
}
The Deck of Cards
Even though there is no Deck
object, we will still need to create a standard 52-card playing card deck and shuffle it.
A standard deck has 13 cards for each of the four suits: an Ace, cards numbered 2-10, a Jack, a Queen, and a King.
Here's a static class DeckCreator
which will give us a standard deck of cards:
public static class DeckCreator
{
public static Queue<Card> CreateCards()
{
Queue<Card> cards = new Queue<Card>();
for(int i = 2; i <= 14; i++)
{
foreach(Suit suit in Enum.GetValues(typeof(Suit)))
{
cards.Enqueue(new Card()
{
Suit = suit,
Value = i,
DisplayName = GetShortName(i, suit)
});
}
}
return Shuffle(cards);
}
}
Note the unusual for
declaration. We are going to make 13 cards per suit, but each card will have a value from 2 to 14. This is because Aces are high in this game (higher than Kings), and we want to compare values easily.
Also, note the Shuffle()
method that we haven't implemented yet. I previously wrote a post on the Fisher-Yates card shuffling algorithm and that's what we're going to use to shuffle the cards.
public static class DeckCreator
{
public static Queue<Card> CreateCards() { ... }
private static Queue<Card> Shuffle(Queue<Card> cards)
{
//Shuffle the existing cards using Fisher-Yates Modern
List<Card> transformedCards = cards.ToList();
Random r = new Random(DateTime.Now.Millisecond);
for (int n = transformedCards.Count - 1; n > 0; --n)
{
//Step 2: Randomly pick a card which has not been shuffled
int k = r.Next(n + 1);
//Step 3: Swap the selected item
// with the last "unselected" card in the collection
Card temp = transformedCards[n];
transformedCards[n] = transformedCards[k];
transformedCards[k] = temp;
}
Queue<Card> shuffledCards = new Queue<Card>();
foreach(var card in transformedCards)
{
shuffledCards.Enqueue(card);
}
return shuffledCards;
}
}
Finally, we need to handle the GetShortName()
method. We don't want to be displaying "Ace of Spades" every time that card appears, rather we want to show the short name "AS" (or "5C" for 5 of Clubs, or "KD" for King of Diamonds, and so on). So we need a method to assign this short name to each card, like so:
public static class DeckCreator
{
public static Queue<Card> CreateCards() { ... }
private static Queue<Card> Shuffle(Queue<Card> cards) { ... }
private static string GetShortName(int value, Suit suit)
{
string valueDisplay = "";
if (value >= 2 && value <= 10)
{
valueDisplay = value.ToString();
}
else if (value == 11)
{
valueDisplay = "J";
}
else if (value == 12)
{
valueDisplay = "Q";
}
else if (value == 13)
{
valueDisplay = "K";
}
else if (value == 14)
{
valueDisplay = "A";
}
return valueDisplay + Enum.GetName(typeof(Suit), suit)[0];
}
}
With the card, player, and "deck" in place, we can finally create the last and most complex object in our model: the game itself.
The Game
In this modeling practice, like several of the others, the game itself is represented by an object. The properties of this object are the things required to play a game. In real life, what we would need to play a game is simple: two players, and a deck of cards.
However, we also need to end games if they become infinite, so we need to keep track of the number of turns elapsed.
public class Game
{
private Player Player1;
private Player Player2;
private int TurnCount; //Number of turns elapsed
}
Remember that our model makes the deck a property of the Player
object, and so it will not be declared in the Game
object.
Now we need to think about the steps involved in playing a turn in War. Here's what I came up with.
- The first step is to create the players, shuffle the cards, and pass a deck to each player.
- The players play turns until one player is left without any cards.
- There is an "end of game" check which sees if either player is out of cards.
The first step is relatively easy, so let's code that up:
public class Game
{
private Player Player1;
private Player Player2;
public Game(string player1name, string player2name)
{
Player1 = new Player(player1name);
Player2 = new Player(player2name);
var cards = DeckCreator.CreateCards(); //Returns a shuffled set of cards
var deck = Player1.Deal(cards); //Returns Player2's deck. Player1 keeps his.
Player2.Deck = deck;
}
}
The end-of-game step is also relatively easy, so that's next. In the real world, end-of-game happens whenever a player is out of cards.
Decision Point: In Part 1, we discussed the possibility of infinite games of War. In our model, we want to avoid said infinite games, and so we'll forcibly end the game after 1000 turns have elapsed (the reasoning for this particular number will be in Part 3 of this series).
Here's that end-of-game check method in the Game
object:
public bool IsEndOfGame()
{
if(!Player1.Deck.Any())
{
Console.WriteLine(Player1.Name + " is out of cards! " + Player2.Name + " WINS!");
return true;
}
else if(!Player2.Deck.Any())
{
Console.WriteLine(Player2.Name + " is out of cards! " + Player1.Name + " WINS!");
return true;
}
else if(TurnCount > 1000)
{
Console.WriteLine("Infinite game! Let's call the whole thing off.");
return true;
}
return false;
}
Now we have to deal with the most difficult of the three steps: playing a turn.
Playing a Turn
Playing a turn in War would be simple, if it weren't for the whole War mechanic. Flip two cards, higher card gets both. This sounds simple, but the War mechanic makes this more difficult than it sounds.
Before we show the code for this part, let's walk through the logic involved.
- Each player flips a card. If one card is bigger than the other, that player gets both cards.
- If the two cards are the same value, we start a War. Each player lays down three cards, and flips the fourth one. If one of the flipped cards is bigger than the other, that player gets all the cards on the table.
- If both flipped cards are the same, repeat the process (place 3, flip fourth) until one player has a bigger card than the other
- If a player runs out of cards during this process, they automatically lose.
Decision Point: In Part 1, I mentioned that the "official" rules of War do not say what happens if a player runs out of cards during a War. In this model, I am making that scenario an immediate end-of-game, as it simplifies the system as a whole.
There's one trick we're going to employ: for the face-down cards during a War, we're going to put them in a common pool. At that point, it doesn't matter who placed them, it just matters that they go to the winner of the War.
With all this in mind, here's the code:
public void PlayTurn()
{
Queue<Card> pool = new Queue<Card>();
//Step 1: Each player flips a card
var player1card = Player1.Deck.Dequeue();
var player2card = Player2.Deck.Dequeue();
pool.Add(player1card);
pool.Add(player2card);
Console.WriteLine(Player1.Name + " plays "
+ player1card.DisplayName + ", "
+ Player2.Name + " plays " + player2card.DisplayName);
//Step 2: If the cards have the same value, we have a War!
//IMPORTANT: We CONTINUE to have a war
// as long as the flipped cards are the same value.
while (player1card.Value == player2card.Value)
{
Console.WriteLine("WAR!");
//If either player doesn't have enough cards for the War, they lose.
if (Player1.Deck.Count < 4)
{
Player1.Deck.Clear();
return;
}
if(Player2.Deck.Count < 4)
{
Player2.Deck.Clear();
return;
}
//Add three "face-down" cards from each player to a common pool
pool.Add(Player1.Deck.Dequeue());
pool.Add(Player1.Deck.Dequeue());
pool.Add(Player1.Deck.Dequeue());
pool.Add(Player2.Deck.Dequeue());
pool.Add(Player2.Deck.Dequeue());
pool.Add(Player2.Deck.Dequeue());
//Pop the fourth card from each player's deck
player1card = Player1.Deck.Dequeue();
player2card = Player2.Deck.Dequeue();
pool.Enqueue(player1card);
pool.Enqueue(player2card);
Console.WriteLine(Player1.Name + " plays "
+ player1card.DisplayName + ", "
+ Player2.Name + " plays "
+ player2card.DisplayName);
}
//Add the won cards to the winning player's deck,
//and display which player won that hand.
//This uses our custom extension method from earlier.
if(player1card.Value < player2card.Value)
{
Player2.Deck.Enqueue(pool);
Console.WriteLine(Player2.Name + " takes the hand!");
}
else
{
Player1.Deck.Enqueue(pool);
Console.WriteLine(Player1.Name + " takes the hand!");
}
TurnCount++;
}
Decision Point: In Part 1, we discussed that War in real-life is not deterministic, meaning that the outcome cannot be known after the cards are shuffled because they will be added to players decks in random order. Our model, almost by accident, has made War deterministic; cards are always added to player decks in a known order. Therefore, if we wanted to, we could "know" after dealing which player will win. It's up to you, dear readers, to decide what to do with this information.
Guess what? We now have a simple C# application that can play games of War! But we're not done yet. In the final part of this series, we'll run this system thousands of times, to see whether or not we accidentally biased it and how we might improve.
Don't forget to check out the sample project on GitHub!. As always, constructive criticism and tips to improve this project are welcome. Share your thoughts in the comments!
Happy Coding!