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.
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!
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.
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:
For holding solar power. The more batteries you have, the more distance you can travel in a day.
If you don’t know what this is, I’m not sure how you’ve made it past infancy!
For powering laser guns, naturally.
Needed for surviving out on the inhospitable Martian surface.
Standard Kornbluth medical supply packs can cure nearly anything. They come in little black bags.
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:
Check for special statuses and prompt the player to do something.
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.
Update the number of kilometers traveled and food eaten.
Determine which random events occur and inform the player of the results.
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).
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.
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.
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 UserInteraction
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.
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 PlayerInteraction
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.
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 UserInteraction
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.
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!
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.
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.
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:
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.
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
)
:
0
M
;
var
timeTaken
=
(
timeEnd
-
timeStart
).
TotalSeconds
;
var
timeAccuracy
=
1
M
-
(
0.1
M
*
(
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.9
M
=>
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.
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.
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!