With our players fully implemented and our game ready to play, we only have a few things left to do. In this final part of our Ticket to Ride C# Modeling Practice series, we will:

  • Structure the game, including creating players, taking turns, and finding the endgame condition AND
  • Calculate player scores.

We might also use the fancy new board I found on the internet!

Apparently Apple's design philosophy got to Alan R Moon's brain.

Or, maybe not.  Let's get going!

The Sample Project

As always with my code-heavy posts, there's a sample project on GitHub that you can use to follow along with this one. Check it out!

exceptionnotfound/TicketToRideModelingPractice
Contribute to exceptionnotfound/TicketToRideModelingPractice development by creating an account on GitHub.

Game Structure

In order to play a game of Ticket to Ride, we need to do the following things:

  1. Setup the board and card decks.
  2. Create players, and deal them each their starter cards.
  3. Start the players' turns.
  4. Watch for the endgame condition (e.g. that one player has 2 or less train cars remaining)
  5. End the game after one additional turn after the endgame condition.

As of yet, we haven't dealt with the Program.cs file; now we will:

class Program
{
    static void Main(string[] args)
    {
        //Create and setup the game board
        var board = new Board();

        //Create the players
        var player1 = new Player("Alice", PlayerColor.Red, board);
        var player2 = new Player("Bob", PlayerColor.Blue, board);
        var player3 = new Player("Charlie", PlayerColor.Black, board);
        var player4 = new Player("Danielle", PlayerColor.Yellow, board);

        //For each player, deal them 4 train cards
        for(int i = 0; i<4; i++)
        {
            var cards = board.Deck.Pop(4);
            player1.Hand.Add(cards[0]);
            player2.Hand.Add(cards[1]);
            player3.Hand.Add(cards[2]);
            player4.Hand.Add(cards[3]);
        }

        //Populate the Shown Cards collection
        board.PopulateShownCards();

        //Now that each player has calculated their desired routes and colors,
        //output that information to the command line.
        player1.OutputPlayerSummary();
        player2.OutputPlayerSummary();
        player3.OutputPlayerSummary();
        player4.OutputPlayerSummary();

        int remainingTurns = -1;
        bool inFinalTurn = false;

        //While we are not in the final turn, have each player take turns.
        while(remainingTurns == -1 || remainingTurns > 0)
        {
            if (remainingTurns > 0) remainingTurns--;
            else if (remainingTurns == 0) break;

            player1.TakeTurn();

            if (!inFinalTurn && player1.RemainingTrainCars <= 2)
            {
                inFinalTurn = true;
                remainingTurns = 3;
                    
                Console.WriteLine("FINAL TURN");
            }

            if (remainingTurns > 0) remainingTurns--;
            else if (remainingTurns == 0) break;

            player2.TakeTurn();

            if (!inFinalTurn && player2.RemainingTrainCars <= 2)
            {
                inFinalTurn = true;
                remainingTurns = 3;
                Console.WriteLine("FINAL TURN");
            }

            if (remainingTurns > 0) remainingTurns--;
            else if (remainingTurns == 0) break;

            player3.TakeTurn();

            if (!inFinalTurn && player3.RemainingTrainCars <= 2)
            {
                inFinalTurn = true;
                remainingTurns = 3;
                Console.WriteLine("FINAL TURN");
            }

            if (remainingTurns > 0) remainingTurns--;
            else if (remainingTurns == 0) break;

            player4.TakeTurn();

            if (!inFinalTurn && player4.RemainingTrainCars <= 2)
            {
                inFinalTurn = true;
                remainingTurns = 3;
                Console.WriteLine("FINAL TURN");
            }

            Console.WriteLine(Environment.NewLine);

            //Console.ReadLine();
        }

        Console.WriteLine("GAME OVER!");

        //This method will be implemented later in this post.
        player1.CalculateScore();
        player2.CalculateScore();
        player3.CalculateScore();
        player4.CalculateScore();

        Console.ReadLine();
    }
}

Notice the complicated while() loop in this code. I am open to suggestions on how to improve it, but given the rules of Ticket to Ride (that when a player has 2 or less train cars remaining, each other player gets one more turn, and then play stops) this was the best implementation I could determine that was still easy to read.

Also, here's the example output for the OutputPlayerSummary() method, so you have an idea of what we are showing:

Bob is going to have a hell of a time trying to complete those destinations.

We are outputting the Player's name, current Destination Cards, Targeted Routes, and Desired Colors. Note that this is all before play begins; these values change as the game progresses.

Calculating Scores

At the end of the game, we need to calculate the score for each player. Lucky for us, the structure we've been using makes this fairly easy.

public void CalculateScore()
{
    //First, we must get combined score of all claimed routes
    var claimedRouteScore = Board.Routes
                                 .Routes
                                 .Where(x => x.IsOccupied 
                                          && x.OccupyingPlayerColor == Color)
                                 .Sum(x => x.PointValue);
                                 
    var routes = Board.Routes
                      .Routes
                      .Where(x => x.IsOccupied 
                                  && x.OccupyingPlayerColor == Color)
                      .ToList();

    foreach(var route in routes)
    {
        Console.WriteLine(Name + " claimed the route " 
                          + route.Origin + " to " + route.Destination 
                          + ", worth " + route.PointValue + " points!");
    }

    //Now, for each destination card, 
    // we add the point value if the card is complete,
    // and subtract if it is not.
    foreach(var card in DestinationCards)
    {
        string output = Name + " has the destination card " 
                        + card.Origin + " to " + card.Destination;

        var isConnected = Board.Routes
                               .IsAlreadyConnected(card.Origin,
                                                   card.Destination,
                                                   Color);

        if (isConnected)
        {
            claimedRouteScore += card.PointValue;
            output += " (SUCCEEDED)";
        }
        else
        {
                    
            claimedRouteScore -= card.PointValue;
            output += " (FAILED)";
        }

        Console.WriteLine(output);

        Board.Routes.AlreadyCheckedCities.Clear();
    }

    Console.WriteLine(Name + " scored " 
                      + claimedRouteScore.ToString() + " points!");
}

That's it! The output looks like this:

Playing the Game

Now our simulation is ready! Let's see some example output, and follow the player named Charlie, who is playing the black trains.

Here's Charlie's situation at the beginning of the game:

Unfortunately for him, none of his routes correspond very well. Helena to Los Angeles is a north-south route on the western side of the board, Seattle to New York is a east-west route along the northern side, and Dallas to New York is a southwest-to-northeast route from the Midwest section to the eastern seaboard. He'll have to work hard to win this game.

Here's a screenshot of the players taking their first turn.

Alice, Bob, and Danielle all start the game by claiming one of the routes they need. Charlie is a bit unlucky here, but not too bad, since two of the shown cards are colors he needs.

You might be asking: "why did Charlie take a blue card?" This is because he needs a grey-colored route, and already has a blue card in his hand.

Let's see the next turn.

On this turn, all players draw. Charlie is cursing Alice here, because she took the Blue card that he also needs. Because of this, Charlie doesn't see the colors he need in the shown cards collection, so he just draws from the deck.

Here's the next turn.

Uh oh, Charlie is falling behind. Bob and Danielle are both claiming another route!

In fact, in this simulation, it is not until the 11th turn that Charlie claims a route, though in his defense, it is a big one:

Winnipeg to Sault Ste. Marie is 15 points all by itself, so that might have been worth the wait. Or else, Charlie simply isn't getting the colors he needs.

After this point, the floodgates open, and Charlie claims routes on 4 of his next 6 turns. He catches up, and desperately tries to finish his destination cards.

At the end of the game, here is his situation:

Charlie failed to complete any of his destination cards! Oh no! But he also claimed a whopping 86 points in routes, and in fact, he won the game!

This is just one of the simulations of Ticket to Ride I have run using the result of our modeling practice. Try some out for yourself!

Drawbacks

There are a couple of drawbacks to our implementation.

If you run enough of these simulations, you would come to the conclusion that our Players are really, really bad at actually completing their destination cards.

Here, Bob has claimed 15 different routes but only completed one of his five destination cards.

In my (admittedly anecdotal) experience, having played this game in real life for almost eight years, people rarely fail to complete their destination cards in Ticket to Ride, at least not nearly as often as our players do.

So, how mad will you be if I take Oklahoma City to Little Rock?

Because the players are so bad at finishing their destination cards, the final scores are also very low. My family's games often end with at least one player breaking 100 points; I've seen that happen exactly one time in the 30+ simulations I've run using this codebase. Obviously, this could be improved.

Something else that is bugging me: I shouldn't need to put limits on the number of train cards or destination cards the players can have. In a real game, we NEVER run out of destination cards or train cards to draw, but in the simulation without those limits it happens all the time.

I believe there are also improvements that we could make to our ideal route finder algorithm. Right now, it's possible (though unlikely) that the algorithm could just loop endlessly, and that's not efficient. In early forms of this simulation, running it would slow my application to a crawl. I have since located and fixed some of these issues, but I would bet it can still be made better.

All that said, I'm pretty proud of this simulation. This is the result of three weeks (nights and weekends) work, and it works pretty damn well if I do say so myself.

Red better start catching up!

But above all, I hope this project is useful to you, my dear readers. Along the way, I hope you've discovered something that will help you in your normal lives, whether that's a better way to model a problem you are currently facing, or merely a small change you can make so future you won't be so mad at present you. Whatever it is, I hope this series (and, indeed, all my blog posts) helps you find it.

Summary

Our C# simulation of Ticket to Ride is complete! Check out the sample project and run it for yourself to see!

Maybe next time, I'll have to try out the European version, where you can dig tunnels, sail on ferries, and use train stations to get into cities you otherwise couldn't!

Well how am I going to get from Brest to Smolensk now?!

On second thought, maybe not. Maybe it's just more fun to play that game instead.

If you enjoyed this series, please check out the other Modeling Practice series I've done in the past: Candy Land, Minesweeper, UNO, Battleship, War, and Connect Four in Blazor.

Thanks for reading, and happy coding!