I last left off on Day 8 of my adventure game project. We continue from Day 9 today.
Day 9
After warming up with a simple rename, I decide I don’t like how TextConsole
and InputLoop
interact. If I want to go all-in with my message passing design, I should have these classes interact via a message protocol instead. To prepare for this, I rename the existing InputLoop
to OldInputLoop
to avoid breaking everything while I experiment with a new class. I set up the skeletal new version of InputLoop
which will eventually use two messages for handling input requests and detecting the end of input. After filling out the behavior driven by a few tests, I end the day with a Dispose scenario. So far, only these tests are using this new code; I’ll have to wait till the next day to continue.
Day 10
Following the pattern of the previous day, I get to work on TextConsole
by first renaming it to OldTextConsole
. I have to admit I don’t normally follow this workflow of renaming X to OldX, but I rather like it. Again, I sprout the new TextConsole
into existence with a simple initial test. A few tests later, I have a complete enough implementation to move forward. This revamped TextConsole
accepts the InputRequestedMessage
from yesterday to handle reads and the OutputMessage
to handle writes. It also emits InputReceivedMessage
(when a line is read) and InputEndedMessage
(when the stream has ended). I’m now ready to delete the OldX
classes and use the new ones.
The next order of business is to add a description to the room. After all, a text adventure game needs descriptive language to help immerse the user into its fantasy world.
Finally, I want to handle some of the input scenarios a little better. I would rather not respond at all to completely empty input, so I work on the receive side and the send side a bit. Also, the input is lacking a prompt, so I add that functionality. In the final two commits of this day, I go old school and add a ‘>’ prompt to the sample game and validate the empty input scenario.
Day 11
This day is relatively short in commits but long on potential. I decide to work on the ‘look’ verb, a mainstay of the genre. I start by adding a look handler to the sample game. Then I incorporate similar code into the core Room using a Look
method. As a result, I remove the custom game code and use the new Look method directly.
So far, we can look at the room as a whole, but how about looking at an object in the room? I will need to handle a noun for this verb. I start with the unknown case first and then move on to a custom noun case (LookAt
). This functionality suffices to now allow me to add a “table” to the MainRoom in the sample game.
Day 12
Today I decide to work on one of the core features for any adventure game — the room map. The map determines how rooms are connected and allows travel between them. The first step is writing a test to exercise adding two “points” to the map. The map point is essentially a “graph node” which refers to a Room. Another test adds ConnectTo functionality to represent “edges” of the graph. The edges are described in simple terms, using a string
(e.g. "north"
) representing a direction of travel to get to the target point. The next big piece is adding Start
and Go
methods to allow room travel. This change requires a new concept in Room which is represented by the Enter
and Leave
methods. The idea is that entering will activate all the functionality within a room (e.g. by subscribing to the verb handlers) and leave will deactivate (e.g. unsubscribe/unregister). This is used in favor of the Dispose pattern because logically a Room still lives on even if you are not currently standing inside it.
After handling several error cases (e.g. Start called twice, ConnectTo when already connected, etc.), I am ready to incorporate RoomMap into the sample game. It’s fairly unremarkable right now because there is only one room.
I still have some time, so I work on improving the RoomMap.Point
interface to disallow directly calling some of the infrastructure methods. I achieve this with a private interface implementation — a rarely needed feature in my experience but a pretty good use case for this situation.
A few changes later and I realize that Go
should not be directly exposed as a public method. Otherwise, I will have to pass the RoomMap around in an unnatural way to the Rooms themselves. I need to remind myself to instead keep wearing my message-passing hat. So I reimplement Go as a private method which handles the GoMessage
. Now anyone can send the GoMessage
without worrying about who will respond; loose coupling achievement unlocked!
Day 13
On this day, I want to focus on items. Any self-respecting adventure game allows the user to interact with items in the world. I start with some minor cleanup around the unknown look case. (I don’t want the error message to give away any clues about items that the game understands but has not revealed yet.) Then I dive right into one of the fundamental item verbs — Take
. I proceed like I did with Look
, first handling the unknown case and then the custom case. Then I implement ‘take’ handling in the sample game.
Now I’m ready to create the Item
class and its first class collection Items
, starting with one basic test. Many tests later, I go back to the sample game and add a move command. This command will be used with the ‘table’ to reveal a hidden item, the ‘coin.’ Of course, this feature is no good if I can’t actually drop items in a Room, so I work on the Drop command which interacts with the newly added Items
field in Room
. Finally, I can add Coin
, the first custom item to my sample game.
Day 14
I notice that the LookAround
functionality of Room
is not to my liking. It relies on external enumeration of the Items
collection. Let’s maintain encapsulation and instead shift this to a tell-don’t-ask style. To prepare for this I first ensure MessageBus
is passed to Items
. Then it’s a simple matter of removing the enumerator and handling Look
directly, using the reference to MessageBus
to send output messages.
Next up: handling custom actions for items. The general idea here is that we want Items
to be notified when the user sends a command, so that it can redirect it whenever the noun part matches an item in the list. To achieve this, we need to Activate
the Items
instance so that it can receive commands. (The reasoning behind this will become clear in a minute.) To complete this feature, we must implement behavior to skip the action if the item is not recognized and make sure the message is consumed (by returning true
) but only if the action ends up being processed. Of course, we need to allow the Item
to tell us if the action is understood, otherwise it will prevent later processing of this unknown message. In fact, the default behavior of an Item
should be to not handle custom actions — it’s an opt-in feature.
Now to flesh out the activation/deactivation concept. This is necessary because every Room
has its own Items
, but the user can only be present in one Room
at a time. We would not want Room
X’s Items
to remain “in scope” so to speak after leaving Room
X and entering Room
Y. To prepare for this new state, we need to make sure that Items
will not respond to commands before Activate
, nor will it do so after Deactivate
. Statefulness means more ways to go wrong, so we need to handle the various error cases like Activate
twice, Deactivate
twice, and Deactivate
before Activate
.
After a brief interlude to promote Room.Drop
from protected
to public
, let’s start building interactions with Items
via the Room
. As discussed earlier, the main mechanism here is calling Activate
on Enter
ing the room. Of course, we can’t forget to Deactivate
when we Leave
.
Before I end the day, I want to make one small tweak to the unknown verb error message. Now that we support custom actions, it might be too revealing if we have different errors for completely unknown actions (e.g. “frazzle thingamajig”) vs. understood but unsupported actions (e.g. “drop table”). Instead we’ll just be noncommittal and say, “You can’t do that.” As a last step, implement Look
for individual items, so that the user can get a more detailed description of the objects in the game.
Day 15
Now that we have the ability to look at items, let’s describe Coin
in the sample game. And remember that table in the middle of the room? Let’s turn it into a full-fledged Table
class, deriving from Item
. The game is getting more object-oriented by the minute!
For the next big feature, let’s start working on Inventory
, specifically the ability to show the inventory via the InventoryRequestedMessage
. Many games support the concept of an inventory (i.e. the items the player currently has on hand) and the text adventure is no exception. Since Inventory
, like Room
before it, uses the Items
class, we want to take advantage of all the functionality it has. However, showing your inventory is different than listing the contents of a room. For one thing, when you display your inventory, it should say something like, “You are carrying: (SOME ITEM)” instead of, “There is (SOME ITEM) here.” Let’s address this by using a caller-supplied format string for Look
. It would also be nice if it printed a special message when you are carrying nothing instead just an empty string, so let’s return the count of items from the Look
method. Now we can implement the show inventory feature (making sure we also properly unsubscribe after Dispose
).
To allow interacting with items in the Inventory
we need to implement custom action handling just like we did in Room
. Since the user’s main interaction point is still the Room
, we need to complete the protocol between Room
and Inventory
to show the user’s items; the Inventory
method in Room
thus sends the InventoryRequestedMessage
for which we already implemented the response above. Now we can initialize Inventory in the sample game, though it won’t do much yet.
As yet, the user cannot add items to the inventory. Let’s change this by implementing the Take
command in Room
; so far, it just sends the InventoryAddedMessage
which we expect to be handled elsewhere. The inventory side of this equation comes next, which wires up the Inventory.Add
method to this message. Of course, not all items are obtainable. We need a way for each Item
to choose whether it can be taken by giving the option of overriding a TakeCore method. Now the sample game can show examples of both cases via the coin which can be picked up and the table which purports to be too heavy. One final feature addition to the game shows a use case for overriding the take method — detecting whether the item has been picked up or not, to decide how to respond to a verb. Here we have a “read coin” command which only works if you are holding the item.
As of now, the game has the potential of handling multiple rooms in a map and multiple items in a room. The user can also interact with items in the world and even pick them up. It’s all coming together.
Pingback: Building an adventure game: part 4 – WriteAsync .NET