Yesterday, Lachlan and I finally got a chance to sit down for a little while and work on our game a bit more. As I mentioned in my previous post in this series, we left off with the ability to launch the program, which outputs a description of the character’s starting location and then immediately exits. The next feature we are implementing is the ability for the player to issue the look and quit commands.

In our code from last time, we created a Universe object that ultimately holds the state of the running game, and the command-line script invokes the Universe#run method to set things in action. We also inject a PlayerInterface object into the Universe, which allows the game to send its output to the user and, eventually, to receive input from the user.

At this point, our Universe#run method looks like this:

def run
  active_character.inspect_location(tell: player_interface)

And the specs covering it:

RSpec.describe PortalAdventure::Universe do
  subject { player_interface,
                        starting_location: starting_location,
                        default_character: default_character)

  let(:player_interface) {
                    handle_location_description: nil)

  let(:starting_location) { instance_double('PortalAdventure::Location') }

  let(:room_description) { "You find yourself in a vast, empty space." }

  let(:first_player_character) {
                    inspect_location: nil)

  let(:default_character) { ->(location:) { first_player_character } }

  describe '#run' do
    it 'describes the first room to the player' do
      allow(first_player_character).to receive(:inspect_location) do |args|
        args[:tell].handle_location_description room_description
      expect(player_interface).to have_received(:handle_location_description)

Before we even got into writing any code to implement command input, I realized I didn’t really care for that spec on the #run method. I think that it must have evolved from something else—possibly a version of the code before we introduced the PlayerInterface and we were just hitting STDOUT directly—but its current form is hiding the signal in a bunch of noise. All we really want to know here is that it tells the active character to inspect its current location, and that it provides a callback object along with that message. I rewrote that spec as such:

describe '#run' do
  it 'tells the active character to inspect its location' do
    expect(first_player_character).to have_received(:inspect_location)
      .with(tell: player_interface)

Much simpler! (Note that I have RSpec configured to verify doubles, so that it will complain if, for example, the PlayerCharacter class does not actually implement the #inspect_location instance method in the way that it is used here.)

With that change out of the way, I added the next spec to describe our #run method:

it 'tells the player interface to process the next command' do
  expect(player_interface).to have_received(:process_next_command)

This is simplistic, and it should be immediately obvious (to those of us with experience) that this won’t be sufficient to deal with setting up a program loop that processes multiple commands or to tell us what to actually do with those commands. It turns out that sometimes the small steps we need to take while teaching a beginner are a good reminder to the rest of us that those same small steps can help us reason about a problem without having to hold the entire solution in our heads at once.

The truth is, I did first start by trying to reason about how the main loop of the program might be written to process commands and know when to tell the player character to act versus knowing when to exit the game, but there were a lot of different possibilities there. I was floundering a bit and realized I was also quickly losing Lachlan’s attention. Pausing for a moment I realized that—while we may eventually need to deal with that problem—our immediate need was to process even a single command. If we focused on that first, we could make some visible forward progress and may also learn a bit more about what kind of things we will need in order to process multiple commands in a loop. So, the new spec above is what we decided the next smallest step was that we could take.

At this point, I ran the specs, and we got an error message about the PlayerInterface class not implementing the #process_next_command method (thanks to that verifying double). So I slid the keyboard over in front of Lachlan, and said, “Yours.” :-)

In our previous session, I had done all of the typing (making sure to be quite verbose as to why I was doing everything), because I wanted to be sure we made enough progress to have a functional first step. But one generally learns more by doing rather than watching, so it was time for Lachlan to start writing the code, too. And yes, I am indoctrinating him young into the practice of ping-pong pairing. :-)

The main area we focused on today was the concept of message-passing in object-oriented programming, and how it can be useful to thing of code such as object.do_a_thing as sending a message, do_a_thing, to the object as opposed to calling a method. Even though you will hear both interchangeably, the former encourages thinking about objects in terms of their responsibility rather than allowing them to just become a dumping ground for methods that seem to use the same set of data.

Due to our short time available last night and the large amount of explaining I had to do, we didn’t make a ton of progress; but after a few passes of the keyboard (at each change in the error message produced by the tests), we got back to a point where all of our specs were passing and we could run the program. Of course, because we took the smallest steps necessary to get that spec to pass, all we ended up with was a noop method definition in the PlayerInterface object:

class PlayerInterface
  def process_next_command

You can see the code as of this point in our GitHub repository for the project.

Next time we will work on getting that new method to actually process user input and execute the look command in the game.