Building an adventure game: part 3

Spread the love

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 Entering 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.

One thought on “Building an adventure game: part 3

  1. Pingback: Building an adventure game: part 4 – WriteAsync .NET

Leave a Reply

Your email address will not be published. Required fields are marked *