1. Computing

The Player

By

The Player
Making a Text Adventure Game in Ruby

The Player

This article is part of the series Making Text Adventure Games in Ruby. The example code for this article can be downloaded here.

Everything is a node. This has been our mantra for this project from the beginning, and the player is no exception. However, the player is a node that can do things. The player can move from room to room, move objects and trigger scripts. The player node can also take user input and act accordingly. And, once we get the player implemented, we'll really be almost done with this project.

So a player is a node that does things. These things are the verbs you type while playing the game. The player can go north or take cheese or even use chainsaw on living room wall. For each of these verbs (the thing the player does), we'll define a do_* method in our new Player class.

The original intent was to simply send the user input to the player instance as a message. To make the player do something, you'd simply do player.send gets.chomp.split(' '). However, there's a bit of checking you'd want to do first. Does the player class implement that verb? What if the user inputs something that accesses one of Node or OpenStruct or Object's methods instead of one of the player verbs? We don't want to accidentally create a full-blown REPL, we just want to send the verb the player can respond to to the player. So, we'll implement a command method first.

This is also the first time we've subclassed the Node class. Until now, we haven't needed to. All nodes more or less act the same, the player just has a few extra methods. But first we'll make a quick modification to the Node#player method, which defines where the player is in the world.


class Node < OpenStruct
  def player(&block)
    Player.new(self, :player, DEFAULTS[:player], &block)
  end
end

class Player < Node
  def command(words)
    verb, *words = words.split(' ')
    verb = "do_#{verb}"

    if respond_to?(verb)
      send(verb, *words)
    else
      puts "I don't know how to do that"
    end
  end
end

The command method doesn't do all that much. Just separate the arguments into verb (always the first word) and other words, prepend do_ to the verb and, if the player really has that method, send it. Otherwise, print an error message to the user.

One thing that may possibly be unfamiliar to you is the use of the splat and soak operator. When the command is split into words, you see *words on the left hand side of the assignment. This will soak up as many arguments as you give it and put them in an array. Later on, we'll send these words using *words. This will splat the array and send them as individual arguments rather than a single array. You'll see them soaked again in the verb implementations.

Moving Around

It's time to start implementing verbs. Let's implement the go verb so we can move between rooms. It's very simple to implement, just find the room you're in, see if there's an exit in that direction and, if there is, move the player to the room indicated by that exit.


class Player < Node
  def do_go(direction, *a)
    dest = get_room.send("exit_#{direction}")

    if dest.nil?
      puts "You can't go that way"
    else
      get_root.move(self, dest)
    end
  end
end

No surprises here, but you will notice one thing. At the end of the argument list, there is an *a argument. This will soak up any extra words the user may input. If the user were to input go north into the kitchen, we need something there so we don't raise an exception because we passed the wrong number of arguments. These extra words aren't used in this method, but in other methods (such as use or put) it will be put to use.

Taking Items

Next up is the take verb. Again, this is very simple to implement. Just find the node the player wants to take from the room (not the root) and try to move it to the player. The move method checks to see if the object is hidden, so if the player can't get to that object right now, it isn't moved. Because we were so careful in implementing our support methods, implementing things like this is just that easy, it's just one line of code.


class Player < Node
  def do_take(*thing)
    get_root.move(thing.join(' '), self)
  end
end

Dropping Items

Dropping items is similarly simple. Just move the object from the player to the player's room. Another one-line method, about the only difference is that we're finding the item from the player, since the item must be in the player's possession.


class Player < Node
  def do_drop(*thing)
    move(thing.join(' '), get_room)
  end
end

Opening and Closing Containers

Since the nodes inside the kitchen drawer are hidden (their parent is a closed container), the only thing preventing the player for taking those items is the open key. So all the player needs to do is open drawer.

To open or close something just find that thing and change the value of the open key. But what about open locked door? Or open unopenable box? This will be implemented with scripting at a later time. For now, all containers can be opened freely. You can even open cat and take dead mouse. Try doing that in real life and you'll find yourself missing your face.


class Player < Node
  def open_close(thing, state)
    container = get_room.find(thing)
    return if container.nil?
    
    if container.open == state
      puts "It's already #{state ? 'open' : 'closed'}"
    else
      container.open = state
    end
  end

  def do_open(*thing)
    open_close(thing, true)
  end

  def do_close(*thing)
    open_close(thing, false)
  end
end

Where Am I?

Sometimes you get a bit lost. For now, we don't have detailed descriptions of the rooms, so we'll just print the tag of the room when the player looks. Also, we'll implement an inventory method. The inventory will print the names of all the items in the player's possession.


class Player < Node
  def do_look(*a)
    puts "You are in #{get_room.tag}"
  end

  def do_inventory(*a)
    puts "You are carrying:"

    if children.empty?
      puts " * Nothing"
    else
      children.each do|c|
        puts " * #{c.name} (#{c.words.join(' ')})"
      end
    end
  end
end

You Want to Put What Where?

Put the hamster in the microwave (unless you're on the NES). Put the key in the door. We have containers and a way to get things out of containers, but no way to get thing into containers. The closest we have right now is the drop verb, which puts the item in the room. What we really need is a put verb.

This is the first time we have to do anything non-trivial with the input from the player. The basic form of the command will be put new batteries in remote control. Here we have a verb, two groups of words that describe two items and a preposition. What we want to do is separate the phrase on the preposition, get two lists of words, find the relative items and try to make the move.


class Player < Node
  def do_put(*words)
    prepositions = [' in ', ' on ']

    prep_regex = Regexp.new("(#{prepositions.join('|')})")
    item_words, _, cont_words = words.join(' ').split(prep_regex)

    if cont_words.nil?
      puts "You want to put what where?"
      return
    end

    item = get_room.find(item_words)
    container = get_room.find(cont_words)

    return if item.nil? || container.nil?

    get_room.move(item, container)
  end
end

This works relatively well, but veteran text adventure game players will notice a bug. Imagine this situation (and please excuse the ridiculousness of this situation, there aren't many items in our world yet): go into the living room and open the cat and the remote control. Put the remote in the cat, then put the cat in the remote control. The cat's parent is the remote, and the remote's parent is the cat. Neither of them have a room or an item with a room for a parent, so they've both simply disappeared from the world. To prevent things like this from happening, we need to modify the move method so it will not make the move if one of the ancestors of the destination is equal to the source. We'll also define an ancestors method for convenience.


class Node < OpenStruct
  def ancestors(list=[])
    if parent.nil?
      return list
    else
      list << parent
      return parent.ancestors(list)
    end
  end

  def move(thing, to, check=true)
    item = find(thing)
    dest = find(to)

    return if item.nil?
    if check && item.hidden?
      puts "You can't get to that right now"
      return
    end

    return if dest.nil?
    if check && (dest.hidden? || dest.open == false)
      puts "You can't put that there"
      return
    end

    if dest.ancestors.include?(item)
      puts "Are you trying to destroy the universe?"
      return
    end

    item.parent.children.delete(item)
    dest.children << item
    item.parent = dest
  end
end

Shortcuts

When people play text adventure games they don't often type out commands in full. Why type go north when you can just type n? Common shortcuts are the first letter for each direction, inv and i. We don't need to define all new methods for these, we'll use the the define_method method instead and just iterate over a list of the directions. For the inventory shortcuts, we'll just use the alias_method method. We'll also use this for get in place of take.


class Player < Node
  %w{ north south east west up down }.each do|dir|
    define_method("do_#{dir}") do
      do_go(dir)
    end

    define_method("do_#{dir[0]}") do
      do_go(dir)
    end
  end

  alias_method :do_get, :do_take
  alias_method :do_inv, :do_inventory
  alias_method :do_i, :do_inventory
end

The REPL

REPL means "read, eval, print, loop." You're familiar with this concept if you've ever used IRB, the Rails console, the Python console or anything else along these lines. It's just a program that reads from the user, evaluates the input, prints the result and does all this over and over again. We're going to add a REPL to the Player class that will allow the user to play the game.

In our case, it's extremely simple. All we need to do it print a prompt, read a line of text and feed it to the Player#command method.


class Player < Node
  def play
    loop do
      do_look
      print "What now? "
      command(gets.chomp)
    end
  end
end

However, this method is rather uninteresting. It's intended to be a stub for you to build on. A full text adventure game will have room descriptions and such that need to be printed, and might implement some custom verbs.

In the next article, we'll add scripting to our text adventure game.

  1. About.com
  2. Computing
  3. Ruby
  4. Tutorials
  5. Text Adventure Games
  6. The Player

©2014 About.com. All rights reserved.