I last left off on Day 8 of my adventure game project. We continue from Day 9 today.
After warming up with a simple rename, I decide I don’t like how
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
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.
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.
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.
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
"north") representing a direction of travel to get to the target point. The next big piece is adding
Go methods to allow room travel. This change requires a new concept in Room which is represented by the
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!
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.
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
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
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
Deactivate twice, and
After a brief interlude to promote
public, let’s start building interactions with
Items via the
Room. As discussed earlier, the main mechanism here is calling
Entering the room. Of course, we can’t forget to
Deactivate when we
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.
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
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
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
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.