Welcome to the fourth and final installment of my adventure game saga. We made it up to Day 15 last time. Let’s finish this up now.
I don’t like that the method to add an item to a room is called
Drop. So let’s fix that. This will allow the later implementation of an actual
Drop command, to give the user the ability to leave behind items currently in the inventory. Now
Drop can be implemented, which makes use of a new
InventoryDropMessage. Again, we should give the item a chance to weigh in whether
Drop is allowed, so we create a
DropCore to be overridden for custom behavior. Now we just need to support “drop” in the sample game and make sure “drop coin” resets the item’s status. One more finishing touch to print a useful message for “drop” with no noun (with corresponding coverage in the sample game walkthrough) and we are done for the day.
Today I notice there is a bug in how “look” is handled. If you pick up an item and it goes into your inventory, you can no longer look at it! Again I’ve made the mistake of using too much imperative code in the
Room.Look handler. I should instead use the ever-faithful MessageBus to send a
LookItemMessage when the “look” verb is detected. Now I can respond to this message in
Inventory. With this and a few more small code updates, “look coin” now works in the sample game.
We have amassed quite a collection of game messages, and it’s time to evaluate them for consistency. I like the pattern of
LookItemMessage, so I go ahead and change
ShowInventoryMessage. While we’re at it, let’s also create a new namespace for all the messages so that they can be grouped together. I hold off on moving
OutputMessage until I add an
Item.Output convenience method — I hate it when you have to import a whole namespace just for one class, so this “façade” will be a nice (albeit, small) improvement for the user experience.
Next on the agenda is some overdue cleanup of
Item. Up to this point, I got away with having a simple constructor for
Item because the
MessageBus instance was always passed to each method. But that’s the problem — passing the same instance to every method means we should just bite the bullet and make an instance field and a new constructor for
Item that requires
MessageBus. Now we can stop accepting the
MessageBus parameter in
Do. Last (and probably least), I make one small change to add an
Output extension method to
MessageBus; I rather like this micro-improvement/simplification to the interface.
Several days ago I added the concept of the
RoomMap and connections between
Room objects but did not implement any ability to actually go anywhere. This changes today! First, a simple warmup change to ensure map points can connect back to themselves (essentially allowing cycles). There’s no reason why this shouldn’t work, and many adventure games explicitly use this mechanic (notably the King’s Quest series with their magical law of containment). Now let’s implement the
Go method in
Room. I already implemented the
GoMessage well in advance, so now I can just use it when processing
Go for a specific direction of travel.
With the groundwork laid for movement, I can implement a secondary room in the sample game, which goes by the autonym
AuxiliaryRoom. To avoid inevitable duplication, I extract the core logic from
MainRoom and push it up to RoomBase. Now all current and future rooms can behave the same way for the standard set of commands (“go”, “look”, etc.). To end the day, I handle a few more error cases for “go.”
The framework for the game is nearly complete, so today I spend time smoothing over a few rough patches. You can’t tell from the commits (which by their nature demonstrate only working functionality) but I kept running into problems when adding new verbs and nouns to the sample game. Invariably I would forget to register the word in the
WordTable. To solve this problem once and for all, I change
Noun to an instance class, do the same for
Verb and then implement a reflection-based registration method for
Noun followed by
Verb as well. Now it is impossible to miss the registration step of a new noun or verb. I make one simple change before ending the day — adding a hint to the “look table” command. It probably isn’t obvious to the user that moving the table is possible, so I want to help them out a bit here.
Can you believe that we’re this far into the development of this project and there is still no way for the player to die? We will have to fix that. It seems logical to leverage the existing
QuitHandler to allow for ending the game, but we can’t use it as is. Thus I start with a new
EndOfGame class. Unlike the (probably flawed) design of
QuitHandler which attempts to parse the “quit” verb directly,
EndOfGame uses a message-based protocol (
EndOfGameMessage) to ultimately signal cancellation of the inner game loop. It also accepts a string of text to display when ending, which could be useful for giving the user a helpful description of how they met their doom. To prepare for the replacement of
QuitHandler, we need to add an
End method to
Room. Now we can delete QuitHandler and use EndOfGame in the sample game instead. To show off this new feature, we add a death scene to the game; it simply involves walking eastward off a cliff in
AuxiliaryRoom. Since this is a terminal condition and a different way to end the game than merely quitting, we need a new
WalkthroughDieTest to pair with our (now renamed)
Looking at some of the other code, I don’t like how the
Coin item has to keep track by itself of whether it has been taken or not. So let’s implement a
Taken flag on the
Item directly. Several TDD cycles later, we can now remove the custom code from Coin and rely on the framework. I’m also a bit annoyed by the need to pass the parent
Room to the
Table item, so I get to work on a
RoomActionMessage protocol to handle sending commands to whatever room the player is in. With the addition of a
SendRoom message on
Item, I can now remove the
Room parameter from the
Table constructor. Loose coupling FTW!
I now identify one last feature before declaring “code complete”: the ability to remove an item. Most adventure games have actions that result in using up an item (e.g. making potions in King’s Quest III). So what we want is a way to delete an item from the
Inventory or a
Room. Let’s start with
Room. There is already a
Take method on
Items that does what we want so I just rename it to
Remove to better pair with
Add. To expose this functionality I now have to add a public
Remove method to
Room, which just calls the corresponding method on
Items. I plan to use this feature in the sample game, so let me set this up now by adding a hole in the wall to the
AuxiliaryRoom. Now if the player looks at the wall, the hint “INSERT COIN” is given.
For completeness I follow up with
Inventory.Remove; again, it is basically a pass-through to the underlying
Items method. As I did before with
Room I also add an
InventoryActionMessage to send generic commands to the
Inventory instance. At this point, I note that the
-ActionMessage classes are functionally identical but for their parameter type, so I use generics to resolve the duplication, creating an
As a final step, I add handling of the “insert coin” action to the sample game. As you might expect, the user can insert the coin into the hole if and only if the
Inventory contains the item. Once the action is processed, the
Coin object is removed, never to be seen again. (In a “real” game, this action might move the plot along, e.g. by causing a special object to appear in a distant room which is necessary to solve the quest.)
Well, that is it! Twenty spare-time days of development has led to a usable, if barebones, text adventure game engine. It employs what we might consider the original object-oriented principles formulated by Alan Kay which place most of the emphasis on messaging. Indeed, almost every time I went down the imperative/procedural route, some time later I was forced to backtrack and create a message/subscriber protocol instead.
Probably the biggest takeaway for me was how message passing helped immensely in avoiding the typical god/context object approach you see in many other frameworks. Since you can never be sure how a user-defined item will interact with its environment, you might be compelled to pass around anything you might ever need to every call site. I largely avoided this by making one trade-off — the use of the single
MessageBus instance in most of the core classes (
Room, etc.). It is not a purely context-independent approach by any means (is such a thing truly possible in any nontrivial system?), but it minimizes the explicit knowledge needed between collaborators and converges most interactions toward one core reusable abstraction.
Thanks for joining me on this journey. See you next year!