Chapter 13. The Martian Trail

Well folks, our time together is coming to an end shortly. I hope you’re finding this book as rewarding to read as I did to write. Since it’s nearly the proverbial last day of school, I thought we could have a bit of fun to put everything together. This way, I can show you an example of how a complete functional C# application might look.

When I was young, in the days when we still traveled to school on dinosaurs and had mammoth steak for lunch, I learned to program from a series of books on BASIC.1 These Usborne Publishing books had titles like Computer Battlegames and contained the source code to games you could enter into the computer yourself. They’re all available on the Usborne website if you’re interested. They usually had a sci-fi theme but turned out to be entirely text based and nothing whatsoever like the painted action scene that accompanied them. In that vein, I present to you my own contribution to that rather obscure genre.

I’ve taken inspiration from the 1975 version of Oregon Trail by Don Rawitsch, Bill Heinemann, and Paul Dillenberger in HP Time-Shared BASIC. This is just inspired by it, however; none of the original code or text has been used here.

Story

The year is 2147, and humanity has finally reached the planet Mars. Not only have we traveled there, but settlement of the red planet is well underway. New cities, outposts, and trading posts are starting to spring up everywhere.

You and your family are among the latest batch of settlers to set down at the main travel terminus, which is located in the colossal impact crater known as Hellas Basin. Travel time from Earth to Mars is far faster than it was back in the old days, but it’s still a matter of weeks. You spent all that time planning your route from Hellas Basin to your plot of land up in Amazonis Planitia, which will involve a crossing of Tharsis Rise along the way. It’s going to be a long, difficult, and dangerous journey.

Not only is Mars a harsh environment, requiring everyone to wear atmosphere suits the entire time you’re on the surface, but also it turns out there absolutely are Martians. Writers from the 20th century who took space exploration far less seriously than they should have portrayed Martians as small, green-skinned creatures with no hair and antennae coming out of theirs heads. As it turns out, that’s precisely what they look like. Who’d have thought it?

Most Martians are fairly affable and don’t mind trading with the incoming Earthlings. Humanity could learn a lot from those folks. But some aren’t keen on what they see as trespassers on their land, and those are the ones to look out for on the trail ahead.

For gathering food on the journey (it’ll last weeks, and you can’t carry that much with you), you’ll have the chance to hunt a type of native Martian fauna: Vrolids. They’re short, stocky, and purple, and smell bad but taste good.

For earning money, you can attempt to corner a herd of wild Lophroll, whose long, luxuriant fur is perfect for coats, or ’70s prog-rock style wigs for amateur guitarists and flutists. Prog rock had a resurgence in popularity in 2145, and there are even now alters to rock gods Ian Anderson and Steve Hackett on Earth’s capital city.2 Finally, you’ll periodically be able to trade with outposts along the route for supplies, if you make it that far!

It’s going to take weeks of hard traveling by hover barge to get where you need to be—over 16,000 kilometers away! Best of luck!

Technical Detail

We’ll need a few things to make our game of Martian travel and survival come to life.

First, we’ll need a central game engine, as shown in Figure 13-1. I’m keeping life simple and making this entirely text based in a console app. You can always adapt it however you’d like in your own version and create a graphic interface of some kind. Graphics aren’t a specific feature of the functional paradigm, so I’m considering them outside the scope of this book.

A diagram of how the Martian Trail game engine fits together
Figure 13-1. The Martian Trail game engine design

The game engine itself will be an indefinite loop of some kind. It will prompt the player for a command and then return that command to be processed.

Many small modules will hang off the central game engine, doing all sorts of bits of work based on the command entered.

That’s the central, entirely functional, part of the system. Around it is the nonfunctional shell that provides some essential nonfunctional extension methods and communications with the outside world.

A few external interactions occur in this game. These include a database to allow the player to save their progress, which I’ll simplify to a flat file to reduce the number of necessary steps, and the NASA web API to look up details of current conditions on Mars. It’d be fun to make it a little more accurate.

The game starts with a setup sequence in which the player spends money on the following:

Batteries

For holding solar power. The more batteries you have, the more distance you can travel in a day.

Food

If you don’t know what this is, I’m not sure how you’ve made it past infancy!

Laser charges

For powering laser guns, naturally.

Atmosphere suits

Needed for surviving out on the inhospitable Martian surface.

Medipacks

Standard Kornbluth medical supply packs can cure nearly anything. They come in little black bags.

Terran credits

Easily exchangeable for the local currency of your choice. No one is sure what the Martians use for money. Perhaps they’re too civilized to need it!

Once the initial inventory is set up, the turn sequence consists of the following:

  1. Check for special statuses and prompt the player to do something.

  2. Display the actions for the current turn and record the player’s choice, which can include trading, hunting for food, hunting for Lophroll furs to sell, or just continuing on with the journey.

  3. Update the number of kilometers traveled and food eaten.

  4. Determine which random events occur and inform the player of the results.

  5. Clean up and make everything ready for the next stop.

This carries on until the player has traveled more than 16,000 km, meaning they’ve reached Amazonis Planitia, or an end game condition has occurred (i.e., the death of the player’s character).

Creating the Game

In the sections that follow, I’ll talk you through the process of creating the game with as many notes as possible to help understand my thought process and how I architect it. I’m not going to spell out every single step, but a complete copy of the source code is available in my GitHub account.

The Solution

Before we start, we need to set up a solution and subprojects.

Create a new Solution of type Console Application and call it MartianTrail. This will be our game itself.

You’ll need a unit test project as well, called MartianTrail.Tests. I prefer xUnit. As a matter of personal preference, I usually install the following NuGet dependencies to the test project: Fluent Assertions, Moq, AutoFixture, and AutoFixture.AutoMoq. These aren’t necessary, so I’ll leave those up to you.

Communications

As we start building out the game, the first thing we need is the ability for the player and game to communicate. For this, create a new folder called UserInteraction, and in it a new code file containing a couple of DUs that represent interactions with the player and the possible consequences.

These are as follows:

UserInteraction

Information provided by the user via the console. This information has these possible states:

IntegerInput

The player entered a numeric value. We can use this for determining which choice from a selection the player made, or validating amounts of money spent.

TextInput

The player entered text that wasn’t numeric.

EmptyInput

The player just pressed the Enter key without any input. This is a form of error state.

UserInputError

An exception was raised from the console.

Operation

An interaction with the user in which we aren’t expecting any data back. This occurs in situations such as writing a message to the console, but not expecting anything to be typed back. Here are its possible states:

Success

The operation completed without error.

Failure

The operation resulted in an exception being thrown. The exception is captured in this object.

Details of how to implement these unions, along with a ConsoleShim and User​Inter⁠action client class can be found in Chapter 6.

Now that we’re able to swap data two ways between the game and player, we need something to say.

Want to Learn How to Play?

The first task of the game upon loading is to ask the player whether they’d like instructions on how to play.

For that, we need to combine our existing abilities to take input from the player and to send messages, and have the function that results from the combination also return a calculated Boolean value that will determine whether the message should be sent. This removes the need to have if statements in the purely functional area of the codebase.

Add these two functions to both the IPlayerInteraction interface and the Player​In⁠teraction class that implements it:

public Operation WriteMessage(params string[] prompt) =>
    console.WriteLine(prompt);

public Operation WriteMessageConditional(
    bool condition,
    params string[] prompt) =>
    condition
        ? WriteMessage(prompt)
        : new Success();

Now, back in the root of the project, we can create a new folder called Instruction that contains an interface called IDisplayInstructions and its implementation DisplayInstructions.cs. These represent the operation to ask the player if they want instructions on how to play, and the message that’s written to screen if they do.

The interface is as simple as it gets. We don’t necessarily care how the operation went, so a void return type is fine:

public interface IDisplayInstructions
{
    void DisplayInstructions();
}

I’m not going to present the whole of the code for the DisplayInstructions class here, because the instructions are fairly lengthy. Instead, I’ll show a few choice extracts.

First, a UserInteraction instance needs to be injected via the constructor, to allow us to test it, as well as to provide a method for communication with the player:

private readonly IPlayerInteraction userInteraction;

public DisplayInstructions(IPlayerInteraction userInteraction)
{
    this.userInteraction = userInteraction;
}

For determining whether the user has said some variation on yes, a collection-based approach tends to keep things simple:

void IDisplayInstructions.DisplayInstructions()
{
    var displayInstructionsAnswer = this.userInteraction.GetInput(
     "Would you like to learn how to play this game?");

    var positiveResponses = new []
    {
        "YES",
        "Y",
        "YEAH",
        "SURE",
        "WHY NOT"
    }

    var displayInstructions = displayInstructionsAnswer switch
    {
        TextInput ti when positiveResponses.contains(ti.TextFromUser.ToUpper() =>
         true,
        _ => false
    };

    this.userInteraction.WriteMessageConditional(displayInstructions,
            "Martian Trail - Instructions"
            string.Empty,
            "Welcome to the Planet Mars, brave explorer.  Here are the",
            "things you need to know in order to survive here, on your new",
            "homeworld..."
            // Insert the rest of the instructions here...
        );
    }

Now that the player hopefully knows what they’re doing, the next step is to give them their initial bank balance and ask them to buy things for the journey ahead.

We could put that positiveResponses logic into a shared class somewhere, but as it happens there isn’t anywhere else in this codebase that needs to know whether a response was positive, so that logic can simply sit here by itself.

The Inventory Setup

Writing a function to set up the player’s Inventory is going to call for a series of wheels within wheels. We require not only a loop to move from inventory item to item, but also a loop within each item to validate the player’s input. In addition, a bit of overarching logic determines whether the player has overspent.

We’re going to move from inventory item to item, and in each case ask the player what they’d like to spend on it. If their choice is invalid, we’ll ask them to try again until we get an answer we like.

The player has a total of 1,000 Terran credits to play with.3 The only rules for each iteration of the inventory selection are that the spending must be 0 or more, and that it can’t be greater than the current number of credits remaining.

At the end of the entire sequence, we’ll prompt the player with a list of their choices, and ask whether they’re happy. If they are, we can move on. Otherwise, it’s time to loop back to the beginning and try the whole thing again.

The GameState object will have a section for inventory, but this section has its own metadata (a bool that records whether the player is happy with their selection) which is of no use later, so let’s create a state record specifically for this section:

public record InventorySelectionState
{
    public int NumberOfBatteries { get; set; }
    public int Food { get; set; }
    public int LaserCharges { get; set; }
    public int AtmosphereSuits { get; set; }
    public int MediPacks { get; set; }
    public int Credits { get; set; }
    public bool PlayerIsHappyWithSelection { get; set; }
}

We’ll also need a cross-application domain version that doesn’t contain the additional metadata and can be passed around between modules:

public record InventoryState
{
    public int NumberOfBatteries { get; set; }
    public int Food { get; set; }
    public int LaserCharges { get; set; }
    public int AtmosphereSuits { get; set; }
    public int MediPacks { get; set; }
    public int Credits { get; set; }
}

The next step is to set up an indefinite loop that is looking for a happy player to allow the loop to complete. Strictly speaking, the concept of classes isn’t a thing in FP, but in the C# world, we have a few choices.

If you want to go down the more purely functional route, create a static class called InventorySelection and have a static function within that to return an Inventory record. This will be done only after the final selections have been made.

This isn’t conducive to good unit testing, though. It’d need all sorts of User​Interac⁠tion mock setups to be created for every unit test that touches the main game module. It’s less purely functional, but given this is C#, I’d rather continue to use classes and interfaces so that we can more easily provide mocks during unit tests.

The interface for the inventory selection module looks like this:

public interface IInventorySelection
{
 InventoryState SelectInitialInventory(IPlayerInteraction playerInteraction);
}

Next, put the InventorySelectionState record, this interface, and its implementation all together in a folder called InventorySelection in the project. This way, we’re keeping everything grouped together logically.

This approach also allows the InventorySelectionState to be expanded later, if we think of some other metadata we want to throw in after improving the game, but the cross-domain version InventoryState can stay as it is, not needing to know what’s happened internally within this module.

Create a new class in the InventorySelection folder called SelectInitialInventoryClient, which should implement IInventorySelection.

To represent the outcome of each attempt by the player to select a usable value for the current inventory item, let’s create a DU to represent every eventuality:

public InventoryState SelectInitialInventory(IPlayerInteraction pInteract)
{
    throw new NotImplementedException();
}

public abstract class InventorySelectionResult { }

public class InventorySelectionInvalidInput { }

public class InventorySelectionValueTooLow { }

public class InventorySelectionValueTooHigh { }

public class InventorySelectionValid
{
    public int QuantitySelected { get; set; }
    public int UpdatedCreditsAmount { get; set; }
}

We’ve defined this within the SelectInitialInventoryClient class, since it’ll never be used anywhere else. I wouldn’t blame you for wanting to use less verbose class names, but I like my code to be descriptive.

To save effort, we can create a generic function to handle all the inventory selections. We’d just need to pass it the parts of the logic that would change every time:

  • The name of the inventory item

  • The place within InventorySelectionState to update

  • The cost in credits of the items being bought

It might look something like this:

private InventorySelectionState MakeInventorySelection(
    IPlayerInteraction playerInteraction,
    InventorySelectionState oldState,
    string name,
    int costPerItem,
    Func<int, InventorySelectionState, InventorySelectionState> updateFunc)
{
    var numberAffordable = oldState.Credits / costPerItem;
    var validateUserChoice = (int x) => x >= 0 && x <= numberAffordable;

        var userAttempt = playerInteraction.GetInput(
        name + " Selection.  They cost " +
        costPerItem + "per item.  How many would you like? " +
         " You can't afford more than " +
         numberAffordable);

    var validUserInput = userAttempt.IterateUntil(
        x =>
        {
            var userMessage = userAttempt switch
            {
                IntegerInput i when i.IntegerFromUser < 0 =>
                 "That was less than zero",
                IntegerInput i when
                 (i.IntegerFromUser * costPerItem) > oldState.Credits =>
                  "You can't accord that many!",
                IntegerInput _ => "Thank you",
                EmptyInput => "You have to enter a value",
                TextInput => "That wasn't an integer value",
                UserInputError e => "An error occurred: " +
                 e.ExceptionRaised.Message
            };

            playerInteraction.WriteMessage(userMessage);

            return x is IntegerInput ii && validateUserChoice(ii.IntegerFromUser)
                ? x
                : playerInteraction.GetInput("Please try again...");
        }, x => x is IntegerInput ii && validateUserChoice(ii.IntegerFromUser));

    var numberOfItemsBought = (validUserInput as IntegerInput).IntegerFromUser;

    var updatedInventory = updateFunc(numberOfItemsBought, oldState) with
    {
        Credits = oldState.Credits - (numberOfItemsBought * costPerItem)
    };

    return updatedInventory;
}

Let’s consider for a few minutes what this function is doing.

First, we’re including everything it needs in the parameters list to keep it pure. If you want, you could have the constructor to this class contain the IPlayerInteraction instance, and reference that as a property of the class. It would save a bit of code noise, and there’s nothing wrong with doing it that way. I’ll leave the choice to you.

Next, we’re making an initial attempt at getting the player’s choice, and then iterating on that indefinitely until we’re certain it’s a valid choice. We’ve held the logic to validate the number of items bought as a Func delegate, so we can reference it multiple times without repeating the same code.

Inside the indefinite iteration, we’re determining exactly what was entered by the player, determining what to say back to them, and then choosing whether to iterate again.

Figure 13-2 shows what this process looks like in diagram form.

A diagram of validating an inventory item
Figure 13-2. The initial purchases process

An interesting phenomenon to note: Visual Studio will complain about the casting of validUserInput into an IntegerInput as a possible null reference exception, even though there is nothing else it could ever logically be. I’d guess that Visual Studio simply can’t see deeply enough into the code to understand that no null value is possible. The compiler warning can be ignored safely in this instance.

Sadly, so far as I’m aware, it’s not currently possible to have lambda expressions in tuples as of .NET 7, so we can create a quick struct to wrap the inventory configurations instead:

public struct InventoryConfiguration
{
    public string Name { get; set; }
    public int CostPerItem { get; set; }
    public Func<int, InventorySelectionState, InventorySelectionState>
     UpdateFunc { get; set; }

    public InventoryConfiguration(
     string name,
     int costPerItem,
     Func<int, InventorySelectionState, InventorySelectionState> updateFunc)
    {
        Name = name;
        CostPerItem = costPerItem;
        UpdateFunc = updateFunc;
    }

}

This is how I created the inventory configurations. The prices I’ve chosen here are a little arbitrary. Feel free to tinker around with them if you want to try developing this game yourself:

private readonly IEnumerable<InventoryConfiguration> _inventorySelections = new[]
{
    new InventoryConfiguration("Batteries", 50, (q, oldState) =>
        oldState with { NumberOfBatteries = q}),
    new InventoryConfiguration("Food Packs", 10, (q, oldState) =>
        oldState with { Food = q}),
    new InventoryConfiguration("Laser Charges ", 40, (q, oldState) =>
        oldState with { LaserCharges = q}),
    new InventoryConfiguration("Atmosphere Suits", 15, (q, oldState) =>
        oldState with { AtmosphereSuits = q}),
    new InventoryConfiguration("MediPacks", 30, (q, oldState) =>
        oldState with { MediPacks = q})
};

I’m aware it’s probably not all that much work to do something clever with reflection and remove this entire array with a few lines of code, but I’m not sure what the benefit is going to be. The code is unlikely to be updated often, if at all, and reflection has a performance cost, as well as potentially giving rise to problems if things don’t match up at runtime.

Here’s a function to display the current state of the inventory:

private void DisplayInventory(IPlayerInteraction playerInteraction,
 InventorySelectionState state) =>
    playerInteraction.WriteMessage(
        "Batteries: " + state.NumberOfBatteries,
        "Food Packs: " + state.Food,
        "Laser Charges: " + state.LaserCharges,
        "Atmosphere Suits: " + state.AtmosphereSuits,
        "MediPacks: " + state.MediPacks,
        "Remaining Credits: " + state.Credits
    );

We’ll use this here and there so the player can make informed choices.

Here’s how we’d code a request to the player to confirm whether they’re happy with the selection of inventory purchases:

private InventorySelectionState UpdateUserIsHappyStatus(
 IPlayerInteraction playerInteraction,
    InventorySelectionState oldState)
{
    var yes = new[]
    {
        "Y",
        "YES",
        "YEP",
        "WHY NOT",
    };

    var no = new[]
    {
        "N",
        "NO",
        "NOPE",
        "ARE YOU JOKING??!??",
    };

    this.DisplayInventory(playerInteraction, oldState);

    bool GetPlayerResponse(string message)
    {
        var playerResponse = playerInteraction.GetInput(message);
        var validatedPlayerResponse = playerResponse switch
        {
            TextInput ti when yes.Contains(ti.TextFromUser.ToUpper()) => true,
            TextInput ti when no.Contains(ti.TextFromUser.ToUpper()) => false,
            _ => GetPlayerResponse("Sorry, could you try again?")
        };
        return validatedPlayerResponse;
    };

    return (oldState with
    {
        PlayerIsHappyWithSelection = GetPlayerResponse(
         "Are you happy with these purchases?")
    }).Map(x => x with
    {
        Credits = x.PlayerIsHappyWithSelection ? x.Credits : 1000
    });
}

Note the use of a recursive function here. It was the simpler choice in this situation, and there are unlikely to be that many wrong attempts at entering some variation on yes or no, so it’s a fairly safe thing to use here.

Finally, we need to make a public implementation of the SelectInitialInventory() function that puts everything together:

public InventoryState SelectInitialInventory(IPlayerInteraction playerInteract)
{
    var initialState = new InventorySelectionState
    {
        Credits = 1000
    };

    var finalState = initialState.IterateUntil(x =>
            this._inventorySelections.Aggregate(x, (acc, y) =>
                    this.MakeInventorySelection(
                     playerInteract, acc, y.Name, y.CostPerItem, y.UpdateFunc)
            ).Map(y => this.UpdateUserIsHappyStatus(playerInteract, y))
        , x => x.PlayerIsHappyWithSelection);

    var returnValue = new InventoryState
    {
        NumberOfBatteries = finalState.NumberOfBatteries,
        Food = finalState.Food,
        LaserCharges = finalState.LaserCharges,
        AtmosphereSuits = finalState.AtmosphereSuits,
        MediPacks = finalState.MediPacks,
        Credits = finalState.Credits
    };
    return returnValue;
}

All done. We now have all the code required to request the player to select how many of each item of inventory they want, along with various bits of validation logic. An overarching loop will allow the player to validate their entire set of choices and decide whether to move on to playing the game.

If you want to give this code a test run quickly now, try changing your program.cs file to the following:

using MartianTrail.InventorySelection;
using MartianTrail.PlayerInteraction;

var inventory = new SelectInitialInventoryClient();
inventory.SelectInitialInventory(new PlayerInteractionClient(new ConsoleShim()));

This will create the class that will create the initial inventory for the player, and also pass in all the real implementations of the interfaces it depends on.

This game is small and simple enough that there’s no real point in creating an IoC container—unless you really want to. It’s your code!

The Game Loop

Now that we have some of the basic structure set, the first thing needed for an actual playable game is a basic loop that represents the player’s turn. These turns consist of a message from the game, prompting the player to make a choice, and the player’s choice and its consequences.

We need an indefinite loop that continues until the game has ended, one way or another. For that, we’ll also need a GameState record. Let’s create it now with a couple of properties:

public record GameState
{
    public bool PlayerIsDead { get; set; }
    public bool ReachedDestination { get; set; }
}

To drive the indefinite loop, use the IterateUntil() extension method from Chapter 9, which we can place in a file called FunctionalExtensions.cs.

Finally, for the loop, we want to keep the code nice and neat when we’re defining the flow of the game turn, so let’s also create an extension method to continue the progress of the turn that will internalize the logic that checks to see whether the game has ended. It’s a technique inspired by the Bind() function attached to some monads:

public static GameState ContinueTurn(
    this GameState @this,
    Func<GameState, GameState> f) =>
    @this.ReachedDestination || @this.PlayerIsDead
        ? @this
        : f(@this);

This means we can chain together many functions that create new instances of the GameState record, but we won’t be required to make a check after every update to see whether the game has ended already. This will act somewhat like a two-state Maybe monad and not execute any functions provided in the event of the game ending.

Each game module after this point takes the form of a function that takes the current instance of the GameState record and returns a new, modified GameState to replace it with.

In fact, it’s easy enough to create a generic interface that represents any given phase of the game. So that’s just what we’ll do:

public interface IGamePhase
{
    GameState DoPhase(IPlayerInteraction playerInteraction, GameState oldState);
}

Consequently, the entirety of the loop that powers the game engine can now be written in a fairly simple bit of code:

public class Game
{
    public GameState Play(
     GameState initialState,
     IPlayerInteraction playerInteraction,
     params IGamePhase[] gamePhases)
    {
        var gp = gamePhases.ToArray();

        var finalState = initialState.IterateUntil(x =>
            gp.Aggregate(x, (acc, y) =>
             acc.ContinueTurn( z => y.DoPhase(playerInteraction, z))),
            x => x.PlayerIsDead || x.ReachedDestination
        );

        return finalState;
    }
}

This class takes the initial state, and a list of all the phases of the game that will update that state. We use Aggregate() to apply the phases one after the other. Note the use of the ContinueTurn() extension method. This has the monad-like short-circuit built in that’ll stop future phases from being executed after the game has already ended.

This indefinite loop will continue to run the turn sequence again and again until either the player dies or the final destination of the expedition is reached. Either would trigger the end of the game.

Let’s define a few game phases now. Then the last job will be to reference the Game class in program.cs and pass it all the phases we’ve defined.

Creating a weather report

I’m British, and as I’ve said previously, there’s nothing more British than discussing the weather, which is exactly what I’ll begin this game with, even if we are now on Mars. I’d do it by getting some real Martian data. Give this a sense of authenticity. We can do that by creating a web API call to NASA’s Mars API. Since that’s a call to an external system, we’ll also need to wrap that in a Maybe, since any manner of things can go wrong. See Chapter 7 for more details on how to implement a Maybe monad in C#.

This is a super simple implementation of a set of classes to provide a mechanism to download data from a web API endpoint. Feel free to make this as complicated as you like, but I’m keeping it simple since this is just an example, not a chapter on web communication.

First, we need a shim class for the built-in HttpClient class, which has no usable interface to inject into dependent classes:

public interface IHttpClient
{
    Task<HttpResponseMessage> GetAsync(string url);
}

public class HttpClientShim : IHttpClient
{
    private readonly HttpClient _httpClient;

    public HttpClientShim(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public Task<HttpResponseMessage> GetAsync(string url) =>
        _httpClient.GetAsync(url);
}

We’re still using the built-in HttpResponseMessage for now, but if you want to do this, you’ll probably need to provide your own shim implementation of each of the subclasses.

Here’s a class that uses async bind calls to various HttpClient methods to convert ultimately from a URI to usable data:

public interface IFetchWebApiData
{
    Task<Maybe<T>> FetchData<T>(string url);
}

public async Task<Maybe<T>> FetchData<T>(string url)
{
    try
    {
        var response = await this._httpClient.GetAsync(url);
        Maybe<string> data = response.IsSuccessStatusCode
            ? new Something<string>(await response.Content.ReadAsStringAsync())
            : new Nothing<string>();

        var contentStream = await data.BindAsync(x =>
         response.Content.ReadAsStreamAsync());
        var returnValue = await contentStream.BindAsync(x =>
         JsonSerializer.DeserializeAsync<T>(x));
        return returnValue;
    }
    catch (Exception e)
    {
        return new Error<T>(e);
    }
}

Now that we got that, we can make a call to the NASA Mars API to display the current weather on Mars. The Martian day is measured in sols, which is the amount of time required for Mars to rotate once on its axis. It’s just short of 40 minutes longer than an Earth day. There are 668 sols in a Martian year (that’s how long it takes for Mars to complete an orbit around the sun).

The NASA API call returns a set of historical data for the current sol, and a large number of sols previous to it. We’re going to treat each day as a sol, so we’ll start with the lowest sol on record, then count up by one on each turn, using an integer field in the game state object to keep track of the player’s location.

The weather information should give a bit of real Martian flavor to our game, as well as providing a practical example of the use of the Maybe monad in real code.

First, we need a class to store the NASA data. The API contains a lot more than this, but this code is restricted to just the data items we’re interested in:

public class NasaMarsData
{
    public IEnumerable<NasaSolData> soles { get; set; }
}

public class NasaSolData
{
    public string id { get; set; }
    public string sol { get; set; }
    public string max_temp { get; set; }
    public string min_temp { get; set; }
    public string local_uv_irradiance_index { get; set; }

}

Create a folder called GamePhases to store all these code classes we’re about to make.

This is the code to display today’s Martian weather:

public class DisplayMartianWeather : IGamePhase
{
    private readonly IFetchWebApiData _webApiClient;

    public DisplayMartianWeather(IFetchWebApiData webApiClient)
    {
        _webApiClient = webApiClient;
    }

    private string FormatMarsData(NasaSolData sol) =>
        "Mars Sol " + sol.sol + Environment.NewLine +
        "	Min Temp: " + sol.min_temp + Environment.NewLine +
        "	Max Temp: " + sol.max_temp + Environment.NewLine +
        "	UV Irradiance Index: " + sol.local_uv_irradiance_index +
         Environment.NewLine;


    public GameState DoPhase(
        IPlayerInteraction playerInteraction,
        GameState oldState)
    {

    // I'm calling Result here, which forces this to be synchronous.
    // This isn't a web app, there is only a single user, so I'm not
    // really very concerned.
        var data =
            this._webApiClient.FetchData<NasaMarsData>(
            "https://mars.nasa.gov/rss/api/?" +
            "feed=weather&category=msl&feedtype=json")
             .Result;

        var currentSolData = data.Bind(x => oldState.CurrentSol == 0
            ? x.soles.MaxBy(y => int.Parse(y.sol))
            : x.soles.SingleOrDefault(y =>
                y.sol == oldState.CurrentSol.ToString())
        );

        var formattedData = currentSolData.Bind(FormatMarsData);

        var message = formattedData switch
        {
            Something<string> s => s.Value,
            _ => string.Empty
        };

        playerInteraction.WriteMessage(message);

        return oldState with
        {
            CurrentSol = currentSolData is Something<NasaSolData> s1
             ? int.Parse(s1.Value.sol) + 1
             : 0
        };
    }
}

The point of this code is to grab the data from NASA, which contains a list of recent sols and their respective weather reports. If we’ve already determined the current sol and stored it in GameState, we use that sol; otherwise, we use the oldest sol in the dataset.

If we were doing this for real, we’d probably also implement a caching system to prevent the need to fetch a fresh dataset from NASA every turn. I’ll leave you to work out how to include that, since it’s not really what this book is about.

The first step of the game is now finished. Next up is to decide which actions are available and to ask the player to select what they want to do.

Choosing what to do this turn

In our version of Mars, there are two possible areas to explore: relatively settled areas with buildings and fortifications, and wilderness (where anything is possible). Different choices will be available to the player, depending on whether they’re in wilderness or near a settlement.

There’s a roughly 33% chance that the current turn takes place near a settlement, and a 66% chance it’s in the wilderness. All sorts of enhancements are possible; the probabilities could vary depending on which region of Mars the player is passing through. Let’s keep it simple for now, though.

The first thing we need is the capability to select something randomly from a list of possibilities. We can’t use the built-in .NET Random class, as that would mean adding unpredictable side effects into our functions, rendering them impure. Instead, we’ll need to inject a dependency of some kind.

A purer language like Haskell does these jobs with one of its number of available monads. In a hybrid language like C#, I don’t see any issue with simply using OOP-style dependency injection and adding in another shim class with an interface:

public interface IRandomNumberGenerator
{
    int BetweenZeroAnd(int input);
}

public class RandomNumberGenerator : IRandomNumberGenerator
{
    public int BetweenZeroAnd(int input) =>
        new Random().Next(0, input);
}

Injecting this, we can create a new class to handle the selection of available actions.

First, create an enum of actions, because these will be used now to make a selection, and then later again to make calculations on how far the player has traveled in this sol:

public enum PlayerActions
{
    Unavailable,
    TradeAtOutpost,
    HuntForFood,
    HuntForFurs,
    PushOn
}

The next step is to scaffold out a new game phase for selecting an action:

public class SelectAction : IGamePhase
{
    private readonly IRandomNumberGenerator _rnd;
    private readonly IPlayerInteraction _playerInteraction;



    public SelectAction(
        IRandomNumberGenerator rnd,
        IPlayerInteraction playerInteraction)
    {
        _rnd = rnd;
        _playerInteraction = playerInteraction;
    }

    public GameState DoPhase(
        IPlayerInteraction playerInteraction,
        GameState oldState)
    {
        // TODO
    }
}

The DoPhase() function here will be used to select which actions are available and which the player wants to perform.

I’ve decided to make the choices all based on more probability curves, with hunting actions being more likely to be available in the wilderness, and trading actions being more likely near settlements.

Bearing in mind that FP is structured more like the individual steps required to solve a mathematical problem, with no if statements or variables changed after they’re created, we can write out this section as a series of Boolean flags:

var isWilderness = this._rnd.BetweenZeroAnd(100) > 33;
var isTradingOutpost = this._rnd.BetweenZeroAnd(100) > (isWilderness ? 90 : 10);
var isHuntingArea = this._rnd.BetweenZeroAnd(100) > (isWilderness ? 10 : 20);

var canHuntForFurs = isHuntingArea && this._rnd.BetweenZeroAnd(100) > (33);
var canHuntForFood = isHuntingArea && this._rnd.BetweenZeroAnd(100) > (33);

To be able to make lists of options in messages to the player, we need an array of everything that’s possible to do this sol. I don’t fancy having nested if statements to append into a list, so we need to do this in one. My solution is to have each of the available options with a ternary-style if, which either stores the option or an “unavailable” state, which we can use to filter by:

var options = new[]
    {
        isTradingOutpost
            ? PlayerActions.TradeAtOutpost
            : PlayerActions.Unavailable,
        canHuntForFood ? PlayerActions.HuntForFood : PlayerActions.Unavailable,
        canHuntForFurs ? PlayerActions.HuntForFurs : PlayerActions.Unavailable,
        PlayerActions.PushOn
    }.Where(x => x != PlayerActions.Unavailable)
    .Select((x, i) => (
        Action: x,
        ChoiceNumber: i + 1
    )).ToArray();

We’ve finished by selecting the list of available choices into a tuple, with the array index value used to give the user an integer to select options by. The advantage of doing it this way is that both the options and integer values associated with them are generated at runtime. This approach facilitates customizing the action list in any way, while still having a dynamically generated list of options (each with an integer ID) presented to the player.

We do need to actually send the message to the player, along with a bit of preamble, which we can do with a string.join() and a Concat(), which is a LINQ method that merges two arrays into a single array:

var messageToPlayer = string.Join(Environment.NewLine,
    new[]
    {
        "The area you are passing through is " + (isWilderness
         ? " wilderness"
         : "a small settlement"),
        "Here are your options for what you can do:"
    }.Concat(
        options.Select(x => "	" + x.ChoiceNumber + " - " + x.Action switch
        {
            PlayerActions.TradeAtOutpost => "Trade at the nearby outpost",
            PlayerActions.HuntForFood => "Hunt for food",
            PlayerActions.HuntForFurs => "Hunt for Lophroll furs to sell later",
            PlayerActions.PushOn => "Just push on to travel faster"
        })
    )
);

this._playerInteraction.WriteMessage(messageToPlayer);

It’s also necessary to ask the player what they want, and to validate that input with an indefinite loop to ensure they’ve entered something correct:

var playerChoice = this._playerInteraction.GetInput(
     "What would you like to do? ");
var validatedPlayerChoice = playerChoice.IterateUntil(
    x => this._playerInteraction.GetInput(
    "That's not a valid choice.  Please try again."),
    x => x is IntegerInput i && options.Any(y =>
    y.ChoiceNumber == i.IntegerFromUser));

var playerChoiceInt = (validatedPlayerChoice as IntegerInput).IntegerFromUser;
var actionToDo = options.Single(
 x => x.ChoiceNumber == playerChoiceInt).Action;

Finally, now that we have a validated player action in an enum type variable, we can apply it by selecting from a list of private functions in this class (which we’ll create shortly):

Func<GameState, GameState> actionFunc = actionToDo switch
{
    PlayerActions.TradeAtOutpost => DoTrading,
    PlayerActions.HuntForFood => DoHuntingForFood,
    PlayerActions.HuntForFurs => DoHuntingForFurs,
    PlayerActions.PushOn => DoPushOn
};

var updatedState = actionFunc(oldState);

return updatedState with
{
  UserActionSelectedThisTurn = actionToDo
};

Let’s start with the easiest: push on without doing anything. Honestly, that means that nothing is done, so no state change occurs (but mileage will be calculated differently later):

private GameState DoPushOn(GameState state) => state;

We’ll handle the Hunting options in one go. For that, we need a way to represent the difficulty of hunting. My answer is to prompt the player with four randomly selected letters that they need to type in order. Their accuracy and the speed with which they type are then used as a multiplication factor to determine success.

That success factor is then used to modify the player’s inventory. Better success means more gains and fewer losses.

The random letter mini-game is one that’s likely to come up repeatedly whenever things like this happen, so we’ll make that a module of its own, separate from the game phases. Place it in a folder called MiniGame.

Here’s the interface:

namespace MartianTrail.MiniGame
{
    public interface IPlayMiniGame
    {
        decimal PlayMiniGameForSuccessFactor();
    }
}

And here’s an implementation, which takes an IRandomNumberGenerator and IPlayerInteraction in its constructor, as well as another new shim, this time for DateTime.Now:

public interface ITimeService
{
    DateTime Now();
}

public class TimeService : ITimeService
{
    public DateTime Now() => DateTime.Now;
}

The MiniGame class has a single public function that generates the four random letters and then rates the player between 1 and 0 on two factors:

Text accuracy

Was each character accurately entered? The player earns 25% for each correct character. A score of 0 results if the length of text entered was wrong, or it wasn’t text. An error on the console results in a retry.

Time accuracy

This starts at 1 and then reduces by 10% (0.1) for each additional second the player takes.

These two factors are then multiplied together. Here are two versions of the calculation.

Say the player is prompted to enter the text CXTD, and does so with 100% accuracy in 4 seconds. That would be a text accuracy of 1, and a time accuracy of 1 – (0.1 × 4), which is 0.6. Multiplying 1 × 0.6 gives a final accuracy of 0.6.

On the other hand, say the player is prompted to enter the text EFSU but incorrectly enters EFSY in 3 seconds. That would be a text accuracy of 0.75 (calculated from 0.25 × the 3 correct letters) and a time accuracy of 0.7 (calculated from 1 – (0.1 × 3)), giving a final accuracy of 0.525 (calculated by multiplying the two factors, 0.75 × 0.7).

As a final example, if the player panics and simply presses the Enter key instead of any text, they’d get a text accuracy of 0, and it wouldn’t matter how much time they took, because the two factors are multiplied together and anything multiplied by 0 is also 0:

private static decimal RateAccuracy(string expected, string actual)
{
    var charByCharComparison = expected.Zip(actual,
        (x, y) => char.ToUpper(x) == char.ToUpper(y));
    var numberCorrect = charByCharComparison.Sum(x => x ? 1 : 0);
    var accuracyScore = (decimal)numberCorrect / 4;
    return accuracyScore;
}

public decimal PlayMiniGameForSuccessFactor()
{
    // I don't care what the user enters, I'm just getting them ready to
    // play.
    _ = this._playerInteraction.GetInput(
        "Get ready, the mini-game is about to begin.",
        "Press enter to begin....");

    var lettersToSelect = Enumerable.Repeat(0, 4)
        .Select(_ => this._rnd.BetweenZeroAnd(25))
        .Select(x => (char)('A' + x))
        .ToArray();

    var textToSelect = string.Join("", lettersToSelect);

    var timeStart = this._timeService.Now();

    var userAttempt = this._playerInteraction.GetInput(
        "Please enter the following as accurately as you can: " +
        textToSelect);

    var nonErrorInput = userAttempt is not UserInputError
        ? userAttempt
        : userAttempt.IterateUntil(
        x => this._playerInteraction.GetInput(
            "Please enter the following as accurately as you can: " +
            textToSelect),
        x => x is not UserInputError
    );

    var timeEnd = this._timeService.Now();

    var textAccuracy =
        nonErrorInput is TextInput { TextFromUser.Length: 4 } ti
            ? RateAccuracy(textToSelect, ti.TextFromUser)
            : 0M;

    var timeTaken = (timeEnd - timeStart).TotalSeconds;
    var timeAccuracy = 1M - (0.1M * (decimal)timeTaken);

    return textAccuracy * timeAccuracy;
}

With this all done, we can now inject an instance of the MiniGameClient into our SelectAction game phase class and use it for determining whether the player was able to hunt successfully. Then, based on that accuracy, the number of laser charges needs to be reduced, and the number of one of the inventory items needs to be increased.

Here’s the hunting for food mini-game. I won’t bother adding the furs version here, as it’s effectively the same but with a different inventory item to update and different flavor text. You can still check out the full source code on my GitHub site.

private GameState DoHuntingForFood(GameState state)
{
    this._playerInteraction.WriteMessage("You're hunting Vrolids for food.",
        "For that you'll have to play the mini-game...");

    var accuracy = this._playMiniGame.PlayMiniGameForSuccessFactor();

    var message = accuracy switch
    {
       >= 0.9M => new[]
       {
           "Great shot!  You brought down a whole load of the things!",
           "Vrolid burgers are on you today!"
       },
       0 => new[]
       {
           "You missed.  Were you taking a nap?"
       },
       _ => new []
       {
           "Not a bad shot",
           "You brought down at least a couple",
           "Don't go too crazy eating tonight"
       }
    };

    this._playerInteraction.WriteMessage(message);

    var laserChargesUsed = 50 * (1 - accuracy);
    var foodGained = 100 * accuracy;

    return state with
    {
        Inventory = state.Inventory with
        {
            LaserCharges = state.Inventory.LaserCharges - (int)laserChargesUsed,
            Food = state.Inventory.Food + (int)foodGained
        }
    };
}

I’ll skip trading for now too. It’s basically the same idea as the initial choice of actions. List a set of things the player can do: buy food, laser packs, batteries, etc., or else sell something they already have. Based on the selection made, update the inventory. Use an indefinite loop to keep presenting the player with things to do in the outpost until the player selects the “Leave the trading outpost” option.

Updating progress

This phase of the game doesn’t involve any user choices. It’s the phase that updates the game state based on the current supplies and conditions affecting the player.

Keeping the code fairly simple for now, it looks like this:

public GameState DoPhase(
    IPlayerInteraction playerInteraction,
    GameState oldState)
{
    playerInteraction.WriteMessage("End of Sol " + oldState.CurrentSol);
    var distanceTraveled = oldState.Inventory.NumberOfBatteries *
        (oldState.UserActionSelectedThisTurn == PlayerActions.PushOn ? 100 : 50);

    var batteriesUsedUp = this._rnd.BetweenZeroAnd(4);

    var foodUsedUp = this._rnd.BetweenZeroAnd(5) * 20;

    var newState = oldState with
    {
        DistanceTraveled = oldState.DistanceTraveled + distanceTraveled,
        Inventory = oldState.Inventory with
        {
            NumberOfBatteries = (oldState.Inventory.NumberOfBatteries -
             batteriesUsedUp)
                .Map(x => x >= 0 ? x : 0),
            Food = (oldState.Inventory.Food - foodUsedUp)
                .Map(x => x >= 0 ? x : 0)
        }
    };

    playerInteraction.WriteMessage("You have traveled " + distanceTraveled +
        " this Sol.",
    "That's a total distance of " + newState.DistanceTraveled);

    playerInteraction.WriteMessageConditional(batteriesUsedUp > 0,
        "You have " + newState.Inventory.NumberOfBatteries + " remaining");

    playerInteraction.WriteMessageConditional(foodUsedUp > 0,
        "You have " + newState.Inventory.Food + " remaining");

    return newState;
}

This is where it made a difference if the player stopped to do something or just rushed ahead. They’re also burning up food and battery packs, which is why hunting, selling goods, and buying replacement supplies is essential to win the game.

Summary

I haven’t given you code for a lot of the game. That’s largely because it’s fairly repetitive, and I just wanted to include pieces illustrating any interesting additional bits of functional structure. You can either visit my GitHub page to see the current version of the code, or you could be really daring and finish it yourself.

The main piece that’s left is a random-event-generator module. All that essentially consists of is a call to the random-number generator and then selecting a function out of a long list, triggering a random event that affects the player.

This is where you can really let your imagination go wild! Here are a few ideas for positive events:

  • The player finds a crashed speeder that has a stash of credits inside.

  • A stampede of Vrolids occurs. Distance traveled is reduced, but there’s extra food to be had! Perhaps a round of a mini-game can control how much of an effect this has.

  • The player encounters some settlers having a yard sale. They’re selling off their old batteries and atmosphere suits extremely cheaply.

  • Friendly Martians appear and guide the player to a food source.

Here are a few ideas for negative events:

  • Sand storm! Atmosphere suits are needed to survive. A number are used up. If the player is out of atmosphere suits, they die, lost to the storm.

  • Dangerous predators attack during the night. Perhaps a mini-game is needed to fend them off. Failure of the player to defend themselves means death, as does running out of laser charges!

  • Bandits are on the trail. They need to be fought off if any laser charges remain, or given all the credits the player has left.

  • The player falls ill with some sort of awful, and slightly embarrassing, Martian illness. Medical supplies should be used, or the player dies.

Hopefully, this has given you plenty to go on and many ideas to try out. Feel free to add as many of your own as you can think of.

This game also could be expanded in plenty of ways. The Martian landscape could even be split into areas, with some having higher and lower probabilities of certain events occurring. This can be as complicated or as simple as you like.

The rest of it, though, I leave to you. It only remains for me to wish you a safe journey and happy trails ahead!

1 BASIC stands for Beginner’s All-purpose Symbolic Instruction Code, a programming language popular in the ’70s and ’80s and now only really interesting to hobbyists like myself.

2 Would you believe, so much arguing arose over where to make the capital that after nearly 500 vetoes, it turned out to be British seaside resort Bognor Regis by default. Now political debates can be done on the beach with an ice-cream cone.

3 “Terra” is the Latin word for “the Earth.” A lot of old sci-fi stories call us “Terrans” and I rather like it!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.26.152