Note: This post is the third in a three-part series which attempts to model the card game UNO as a C# application. Here's Part One and Part Two. You may want to use the GitHub repository to follow along.

In the previous parts of this series we first saw how to play UNO and how to model the cards and moved on to how to model the player behavior. In this post, the last part of the series, we're going to take the results from the first two parts and combine them together to make a fully-working UNO-playing robot.

Well, UNO-playing C# application anyway. What? I can dream.

The Game Manager

The most critical component that we haven't yet built is a class called GameManager. In a real UNO game, the players themselves are responsible for keeping track of everybody else following the rules. However, in our model, we'll need a non-player class to do this, as well as control the game flow and keep track of which player's turn is next.

Here's the skeleton of the GameManager class. The next step will be to establish what each of these methods actually do.

public class GameManager
{
    public List<Player> Players { get; set; }
    public CardDeck DrawPile { get; set; }
    public List<Card> DiscardPile { get; set; }

    public GameManager(int numPlayers) { }
    public void PlayGame() { }
    private void AddToDiscardPile(PlayerTurn currentTurn) { }
}

Creating the Game

The first method is just the GameManager class's constructor, which we will use to set up the game being played. The only input to this method is an int numPlayers, which is the number of players that will be playing the game.

Given the number of players, the GameManager must set up the game, which consists of:

  1. Creating the deck of cards.
  2. Dealing seven cards to each player.
  3. Placing a single card from the draw pile into the discard pile.

Here's how our constructor does this:

public GameManager(int numPlayers)
{
    Players = new List<Player>();
    DrawPile = new CardDeck();
    DrawPile.Shuffle();

    //Create the players
    for (int i = 1; i <= numPlayers; i++)
    {
        Players.Add(new Player()
        {
            Position = i
        });
    }

    int maxCards = 7 * Players.Count;
    int dealtCards = 0;

    //Deal 7 cards to each player
    while(dealtCards < maxCards)
    {
        for(int i = 0; i < numPlayers; i ++)
        {
            Players[i].Hand.Add(DrawPile.Cards.First());
            DrawPile.Cards.RemoveAt(0);
            dealtCards++;
        }
    }

    //Add a single card to the discard pile
    DiscardPile = new List<Card>();
    DiscardPile.Add(DrawPile.Cards.First());
    DrawPile.Cards.RemoveAt(0);

    //Game rules do not allow the first discard to be a wild.
    while(DiscardPile.First().Value == CardValue.Wild || DiscardPile.First().Value == CardValue.DrawFour)
    {
        DiscardPile.Insert(0, DrawPile.Cards.First());
        DrawPile.Cards.RemoveAt(0);
    }

    //And now we're ready to play!
}

With all that setup out of the way, we can finally let GameManager start an actual game!

Playing the Game

GameManager kicks off the game by telling Player 1 to take his turn. Play must then proceed (to Player 2, then Player 3, so on) until somebody plays a Reverse card. At that point, we need GameManager to note that a Reverse card was played and reverse the turn order.

GameManager also needs to stop the game when a player no longer has any cards in his/her hand.

We can implement the actual playing of the game using the PlayGame() and AddToDiscardPile() methods like so:

public void PlayGame()
{
    int i = 0;
    bool isAscending = true;

    //First, let's show what each player starts with
    foreach (var player in Players)
    {
        player.ShowHand();
    }
 
    //Game won't start until user presses Enter
    Console.ReadLine();

    //We need a "mock" PlayerTurn representing the first discard
    PlayerTurn currentTurn = new PlayerTurn()
    {
        Result = TurnResult.GameStart,
        Card = DiscardPile.First(),
        DeclaredColor = DiscardPile.First().Color
    };

    Console.WriteLine("First card is a " + currentTurn.Card.DisplayValue + ".");

    //Game continues until somebody has no cards in their hand
    while(!Players.Any(x => !x.Hand.Any()))
    {
        //If the draw pile is getting low, shuffle the discard pile into the draw pile
        if(DrawPile.Cards.Count < 4)
        {
            var currentCard = DiscardPile.First();
                    
            //Take the discarded cards, shuffle them, and make them the new draw pile.
            DrawPile.Cards = DiscardPile.Skip(1).ToList();
            DrawPile.Shuffle();

            //Reset the discard pile to only have the current card.
            DiscardPile = new List<Card>();
            DiscardPile.Add(currentCard);
                    
            Console.WriteLine("Shuffling cards!");
        }
 
        //Now the current player can take their turn
        var currentPlayer = Players[i];
        currentTurn = Players[i].PlayTurn(currentTurn, DrawPile);
 
        //We must add the current player's discarded card to the discard pile.
        AddToDiscardPile(currentTurn);
         
        //When somebody plays a reverse card, we need to reverse the turn order
        if (currentTurn.Result == TurnResult.Reversed)
        {
            isAscending = !isAscending;
        }
        
        //Now we figure out who has the next turn.
        if (isAscending)
        {
            i++;
            if (i >= Players.Count) //Reset player counter
            {
                i = 0;
            }
        }
        else
        {
            i--;
            if (i < 0)
            {
                i = Players.Count - 1;
            }
        }        
    }

    //Let's see who won the game!
    var winningPlayer = Players.Where(x => !x.Hand.Any()).First();
    Console.WriteLine("Player " + winningPlayer.Position.ToString() + " wins!!");

    //Finally, calculate and display each player's score
    foreach(var player in Players)
    {
        Console.WriteLine("Player " + player.Position.ToString() + " has " + player.Hand.Sum(x => x.Score).ToString() + " points in his hand.");
    }
}

private void AddToDiscardPile(PlayerTurn currentTurn)
{
    if (currentTurn.Result == TurnResult.PlayedCard
            || currentTurn.Result == TurnResult.DrawTwo
            || currentTurn.Result == TurnResult.Skip
            || currentTurn.Result == TurnResult.WildCard
            || currentTurn.Result == TurnResult.WildDrawFour
            || currentTurn.Result == TurnResult.Reversed)
    {
        DiscardPile.Insert(0, currentTurn.Card);
    }
}

Whew! We are finally done with our code. All that's left to do now is to run a sample game!

Running a Sample Game

With all the code in place, let's run the app a few times to make sure it works the way we think it does.

The first time we boot the app (there's a complete working version over on GitHub), we'll see something like this:

Well well, looks like Player 3 has a pretty good hand, what with all the wilds. But, let's run the app to see how everyone does.

And, sure enough, Player 3 ends up winning the game. Those wilds help.

Drawbacks of This Model

As I've said many times throughout this series, the point of those posts is not to model UNO precisely, the point is to take a complex real-world problem and break it down into smaller, more manageable little problems.

That said, I can identify a few ways in which, given unlimited time, I might improve this model:

  • Different player "personalities": Not every player is going to be a stupid jackass. I'd like to model different kinds of player strategy (e.g. offensive vs. defensive, hold wilds vs play them, etc.).
  • A GUI: I mean, I know the hardcore programmers among us LOVE them some command line, but really this could use a GUI to make it pop.
  • Rules modification: Different UNO sets use different kinds of rules, and I love to find a way to model lots of different rules and have the players react to them.

That said, I'm pretty darn happy with how this turned out.

Summary

The point of modeling practice is to practice. Sounds obvious, I know, but I firmly believe that the difficulty in creating complex software programs is not writing the code, but in getting the correct requirements. Modeling practice helps us consider all possibilities, and when we do it against a known game like UNO (or Candy Land or Minesweeper) we have a distinct set of rules to work against, something we often lack in real-world projects.

As always, feel free to leave any comments you may have (good or bad) in the comments section below, and check out the GitHub repository and maybe even run the app a few times. I'm quite proud of how it turned out.

Happy Coding!