1. Computing

Data Visualization

By

Data Visualization
Making a Text Adventure Game in Ruby

Data Visualization

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

In the next article in this series, we'll be finding nodes in the tree and moving them to other locations within the tree. How can we know if this even worked? We can set up some puts statements to see if the node's new parent is correct, but that's tedious. What we need is a general way to output either the entire tree or parts of the tree in an easily readable format.

There are two ways to go about doing this. The first way will iterate over the tree and print it to the terminal, using icons and indentation to preserve the tree structure. This is a simple way, and you don't have to leave the terminal to use it. It also works everywhere, but there's a more powerful way.

The graph gem generates .dot (pronounced "dot dot") files that a program called Graphviz can read. These dot dot files define a graph (think nodes and edges, not bars and pies) that will be rendered to an image file by Graphviz, which can then be displayed on the screen. You'll get a graphical representation of your text adventure game, and you'll be able to see exactly what's going on at a glance. However, this only works if you have the graph gem and Graphviz installed.

ASCII Art

Let's start with the easiest way first. We're going to hijack (or, more accurately, implement) the Node#to_s method. It should iterate over the tree, starting at itself (so if we want the whole tree, we call to_s on the root node) and print the tag of each node. Each time the iteration descends into the tree, increment the indentation so the hierarchical structure of the tree is preserved.

In addition to the tag names, icons will be displayed that represent information about the nodes. For instance, the player's icon is a @ (at sign, usually used to represent players in roguelikes), an open container is O and a closed container is *. This gives the user more information at a glance without bombarding them with text.

However, sometimes you just want to be bombarded by text. You need to know all kinds of things about the game, including the values of one or several keys on the nodes. To do this, we'll include a verbose parameter. After printing the tag for a node, it will print each of the node's keys and values. A few things have to be cleaned up though, since some things (like the children tag) will produce very long strings.


class Node < Hash
  def to_s(verbose=false, indent='')
    bullet = if parent && parent.tag == :root
               '#'
             elsif tag == :player
               '@'
             elsif tag == :root
               '>'
             elsif open == true
               'O'
             else
               '*'
             end

    str = "#{indent}#{bullet} #{tag}\n"
    if verbose
      self.table.each do|k,v|
        if k == :children
          str << "#{indent+'  '}#{k}=#{v.map(&:tag)}\n"
        elsif v.is_a?(Node)
          str << "#{indent+'  '}#{k}=#{v.tag}\n"
        else
          str << "#{indent+'  '}#{k}=#{v}\n"
        end
      end
    end

    children.each do|c|
      str << c.to_s(verbose, indent + '  ')
    end

    return str
  end
end

To use this, you can simply call puts root (where root is the root node of the tree), or puts root.find(:player) to just print the player (with the find methods we implement later). To use the verbose mode, you have to call to_s manually. Normally, that's just something puts does if you give it something that's not a string. So if you wanted to print out detailed information about everything, you could use puts root.to_s(true).

This is also the first time we've iterated over the tree. While iterating over something like an array (a simple 1-dimensional list) is very easy to visualize, iterating over a tree is not so easy. The method chosen here is what is referred to as a depth-first search. First visit the root node (or any other chosen node), then for each of its children repeat the process using recursion (a method that calls itself). So if we start at the root node and print its tag, we then visit the living room node. The living room node's tag is printed, then the same is done for each of the living room node's children and so forth. Recursion does have some disadvantages, it won't work on very deep trees for instance. But our tree won't get very deep, so we don't need to worry about overflowing the stack.

Remember this method of iterating over the tree. It will become very important in the coming articles.

Rendered Graphs

The ASCII art method is OK, but it's still really not easy to read. Two nodes could be siblings (have the same parent), but are separated by 50 lines of text. There's no real easy way to solve that without quite a lot of work, and this is just a method used for debugging purposes. You probably don't want to put very many hours into it.

Thankfully, the graph gem outputs very easy to read and organized images. Though it sometimes looks like spaghetti and meatballs when the graphs get complicated, it's still easy to find your way around. But note that we're not necessarily rendering a "tree" with Graphviz. Graphviz has no real concept of a "tree," only a graph with nodes and edges. All of the hierarchical structure will be there, but the way Graphviz places the nodes may not resemble a tree at all, but more of a cloud. Also, there's no real easy to way get detailed information on such a graph (at least with the graph gem), so if you want detailed information you're going to have to use the to_s method.

Before you get started here, read this article about installing and working with the graph gem and Graphviz. But how this graph is generated is quite simple, we're going to walk the graph (like the to_s method) and define an edge for every node to the node's parent. That's it, more or less. There's a little more code in there to color-code the nodes, but the entire operation is very simple.

I also included an extra step to make the graphs look really good. By default, graphviz doesn't do any sort of antialiasing on its output, so the lines (especially curved lines, which describes all the lines in our graph) look jagged and ugly. So, instead of saving directly to a PNG file, I save to an SVG file and call a program called svg2png to render to a PNG. You don't have to take this extra step if you don't want to install svg2png. Instead, you can render to SVG and use a program that can view SVG to view it, or render directly to PNG. Depending on your needs, you might even want to throw in an `open #{name}.png` so it automatically pops up (at least on OS X, your OS' command for opening files might differ).


class Node < Hash
  def graph(gr=Graph.new)
    gr.edge tag.to_s, parent.tag.to_s unless parent.nil?
    gr.node_attribs << gr.filled

    if tag == :player || tag == :root
      gr.tomato << gr.node(tag.to_s)
    elsif parent && parent.tag == :root
      gr.mediumspringgreen << gr.node(tag.to_s)
    end

    children.each{|c| c.graph(gr) }
    return gr
  end

  def save_graph
    graph.save 'graph', 'svg'
    `svg2png graph.svg graph.png`
    `open graph.png`
  end
end

There one major thing we didn't display on this graph though, and that's the exits. If you start getting a lot of nodes on the graph along with a lot of edges that go between the nodes it starts looking very messy. So we'll output a map in a separate method. Generally speaking, we're not really interested in the exits when we look at the graph anyway, we're interested in the parent relationship. Unless a script changes something, the exits are rather predictable.


class Node < Hash
  def map(gr=Graph.new)
    if parent && parent.tag == :root
      methods.grep(/^exit_[a-z]+(?!=)$/) do|e|
        dir = e.to_s.split(/_/).last.split(//).first
        gr.edge(tag.to_s, send(e).to_s).label(dir)
      end
    end

    children.each{|c| c.map(gr) }
    return gr
  end

  def save_map
    map.save 'map', 'svg'
    `svg2png map.svg map.png`
    `open map.png`
  end
end

Now that we know how to visualize our data, let's start mangling it. In the next section, we'll start finding and moving nodes.

  1. About.com
  2. Computing
  3. Ruby
  4. Tutorials
  5. Text Adventure Games
  6. Data Visualization

©2014 About.com. All rights reserved.