logo

 

     
 
Home
Site Map
Search
 
:: Bitwise Courses ::
 
Bitwise Dusty Archives
 
 
 

rss

 
 

ruby in steel

learn aikido in north devon

Learn Aikido in North Devon

 


Section :: Ruby

- Format For Printing...

Adventures In Ruby

The game’s afoot...
Friday 10 August 2007.
 

The Ruby language may be fine and dandy for writing all kinds of serious applications. However, ask most Ruby programmers why they like programming in Ruby, and my guess is that most of them will tell you “because it’s fun.”

In this series we’ll go back to the ‘80s when games were in text and text was in green. In fact, we’ll also be exploring the features of one of the 21st Century’s up and coming languages – Ruby...

- see also: part two of this series

In this series, therefore, I’m going to be using Ruby purely for pleasure. This is not going to be a tutorial in the normal sense – I won’t be starting with concept A and working my way forward in a logical progression through concepts B, C, D and so on. Instead, I am simply going to dip into Ruby, try things out and see where it leads me. Sometimes I may run into a dead end. Other times I may go spinning off at wild tangents. I’m hoping my ultimate destination will be worth the effort – but, then again, since I don’t yet know where that destination will be, there are no guarantees ;-)

If you are new to Ruby you may want to read my beginner’s tutorial in some previous articles for Bitwise. If you want to go a bit deeper into Ruby programming, download my free eBook, The Little Book Of Ruby which provides a more ‘structured’ guide to Ruby than you’ll find in the present series.

My aim is to write a game. It will be a text adventure of the classic variety – similar in essence to the old Infocom games of the 1980s such as Zork, The Hitchhiker’s Guide To The Galaxy and the Leather Goddesses of Phobos. I should say at the outside that, in all probability, my game won’t be as big and complex as this Infocom games and, moreover, it may not have quite so much in the way of tight-fitting leather catsuits. But the principles will be there. If I can inspire one or two people to resurrect the text adventure genre, my happiness will be complete. If not, I’ll a die a sad and bitter old man – my fate, dear reader, is in your hands...

Zip - 2.8 kb

Download The Source Code. You can load the Ruby files (.rb) into any text editor. I also supply a project file which can be loaded into Ruby In Steel Developer. The screenshots in this article all show Ruby In Steel.

The Adventure Begins

Adventure games are full of objects – everything from the locations (‘Rooms’) to the Treasures they contain. Ruby, being strictly object oriented, is, therefore, the perfect language for an adventure game. My first task is to decide on the main classes which will define the objects in my game.

Most game objects – whether they be Rooms, Treasures, Weapons or Monsters – must have at least two properties: a name and a description. I’ll begin by creating a base class, called Thing, from which more specialised classes will descend...

class Thing       
  attr_accessor( :name, :description )

  def initialize( aName, aDescription )
     @name = aName
     @description = aDescription
  end
end

Here, @name and @description are ‘instance variables’ which means that each object (that is, each ‘instance’ of the Thing class) will have its own copies of these variables. In order to get and set the values of @name and @description I need to define a pair of ‘getter’ and ‘setter’ methods (much as you would do when writing ‘properties’ in C# or VB .NET, say). Ruby gives me a shorthand way of doing this by calling the attr_accessor() method and passing to it symbols matching the name of the instance variables to be accessed:

attr_accessor( :name, :description )

In Ruby, instance variables begin with a ‘@’ and symbols begin with a colon ‘:’. A symbol is simply an identifier whose value is itself (so for example, whereas you might set the value of an instance variable like this, @name = “Fred”, you would not set the value of a symbol since the value of :name is :name!

Having created :name and :description accessors I will be able to assign and retrieve the values of the @name and @description variables for a Thing object (I’ll call it t) like this...

t.name
t.name = “A New Name”

The initial values of those variables are assigned in the initialize method which is automatically called when a new object is created:

def initialize( aName, aDescription )
  @name = aName
   @description = aDescription
end
In Ruby, you create objects by calling new...

...if the object’s class has an initialize method, this is called automatically and each object will be initialized with the values passed to it by new. Here I am using the Ruby In Steel debugger to examine the values of the variables inside the room objects which i just created...

Lines Of Descent

OK, now that I have a basic Thing, it’s time to move on to create some more specific descendent classes. Treasure is easy, it’s just a Thing with a value. In Ruby a descendent class is created by placing a < after the class definition followed by its ancestor’s name. My Treasure class defines a new accessor, :value and calls its ancestor class’s initialize method (super takes care of that) to initialize :name and :description...

class Treasure < Thing       
  attr_accessor( :value )

  def initialize( aName, aDescription, aValue )
      super( aName, aDescription )
     @value = aValue
  end
end

The Room class is defined in much the same way but it adds on some ‘exit’ attributes, :n, :s, :w, :e, instead of :value. These attributes will be used to indicate which room, if any, is located at the North, South, West and East exits of the current room...

class Room < Thing       
  attr_accessor( :n, :s, :w, :e )

  def initialize( aName, aDescription, dirN, dirS, dirW, dirE )
     super( aName, aDescription )               
     @n = dirN
     @s = dirS
     @w = dirW
     @e = dirE
  end
end

Any adventure game needs a map (a collection of Rooms) and a Player (to provide the first person perspective as you move through the game). For the sake of simplicity, my Map will simply be a class that contains an array, @rooms. You could, of course, use the default Ruby Array class for this purpose. Or you could make the Map class a descendent of Array. I decided against using a plain Array for the simple reason that I might want to add special behaviour to my Map at a later date and I decided against descending from Array since a) I don’t want my code to have access to the whole range of Array methods when I use a Map object and b) I may decide to change the class of @rooms in a later revision of my code (the Ruby ‘Hash’ class – a key/value ‘dictionary’ - tempts me strangely). Anyway, here in all its glory is my map class...

class Map       
  attr_reader( :rooms )

  def initialize( someRooms )
     @rooms = someRooms
  end                               
end

Note that, since Ruby does not require type definitions for variables, @rooms could be an object of any sort here. It will only magically turn into an Array when I create a new Room object later and call the new constructor with an array as an argument.

I could create a special one-off class for the Player. However, I’ve decided that I may need more than one object with the ability to move through the game (maybe I’ll make it multi-player or maybe I’ll add some ‘characters’ who can move around through the game environment) which is why I’ve created a more generic class called Actor. This class has a :position attribute to indicate which room it is in at any given moment...

class Actor < Thing
  attr_accessor( :position )

  def initialize( aName, aDescription, aPosition )
     super( aName, aDescription )
     @position = aPosition
  end
end

Two more classes round things off – first a Game class which owns the Map and might, in principle (when further developed) store the full state of the game being played; and also the Implementer class. The Implementer is, in effect, the software equivalent of me – the person who programmed the game. It stands above all other objects and can look down upon and manipulate the entire world of the game with a godlike omniscience. Another way to think of the Implementer is as a sort of chess-player moving pieces (the various objects) around on a board (the map).

It would, of course, have been possible to give each individual object the ability to move itself like an automaton - and that might be an interesting programming challenge in itself. I prefer to have the objects ‘unaware’ of their surroundings for the simple reason that ‘aware’ objects would need mutual (two-way) links to one another – for example, a Room would have to know about the People and Monsters which it contains; and the People and Monsters would similarly have to know which Room them were in. By having an omniscient Implementer, I remove this complication – the Rooms and other objects are essentially non-sentient, like chessmen on a chessboard. Which means that only one special object, the Implementer, needs to know where each object is and how to move it from one Room to another.

Here my Implementer starts by initializing the game (to make a more generic ‘game system’ , the data such as the Rooms and other objects might be better saved to a file and read in when the game begins – maybe I’ll do that later on...); then, in response to commands to move the player (or, in principle, any other object of the Actor class) in a specific direction, it looks for an exit in the current Room (given by the player’s position in the map – @game.map.rooms[anActor.position]) and, if it is a positive number, it alters the player’s position to the map index given by the new number, otherwise (if the number is -1) there is no exit in that direction and a reply is returned to say so...

def moveActorTo( anActor, aDirection )
     reply = "No Exit!"
     exit = @game.map.rooms[anActor.position].method(aDirection).call
     if (exit > -1) then
        anActor.position = exit
        reply = "You have entered #{@game.map.rooms[exit].name} which is      
                    #{@game.map.rooms[exit].description}"
     end
     return reply
end

There is one line in the above that needs a bit more explanation. It is this one...

exit = @game.map.rooms[anActor.position].method(aDirection).call

As I explained earlier, the first part of this is straightforward. We get the current position of the player (anActor.position) and use this to index into the array, @game.map.rooms in order to find the Room object which corresponds to the player’s position.

We now need to see if this object has an exit (a positive number representing the number of an ‘adjoining’ room) in a specific direction, aDirection or whether there is no exit (-1). One way of doing this would be to have the various directions (N,S,E or W) represented by constants and then, when a direction argument is passed to the moveActorTo method, perform tests in multiple ifs or a case statement in order to decide which method to call or which attribute or property to look for. For example, in C#, you might write something like this...

switch( direction ) {
        case dir.NORTH : exit = room.N;
                break;
        case dir.SOUTH: exit = room.S;
                break;
        // ...etc
}

Or in Delphi you could write this....

case( direction ) of
        NORTH: exit := room.N;
        SOUTH: exit := room.S;
        // ...etc
end;

And, of course, I could write something very similar in Ruby. In fact, my Ruby code has no such case statement. Rather than testing the aDirection variable and then taking different actions according to its value, it actually calls aDirection as though it were a method!

This is an example of Ruby’s ‘metaprogramming’ (the ability to treat data as executable code). If you have never done any metaprogramming before, you may find this surprising. Let’s see how my program calls the moveActorTo method. Here is an example...

imp.moveActorTo( thePlayer, :e )

So here you can see that I am passing the symbol :e as the second argument. So when this is assigned to the argument name, aDirection, the code method(aDirection).call has the effect of calling a method named e.

Attributes as Methods...
The attribute accessors defined here (:n, :s, :w, :e) behave just as though I had written eight methods – n, s, w and e which return the values of the instance variables, @, @s, @w and @e – and another four methods n=, s=, w= and e= which assign values to those variables.
Using Ruby In Steel IntelliSense, I can verify that these accessors are available as methods. Note that the Room object has also inherited the name and description methods from its ancestor, Thing, class.

Let’s look at a concrete example. At the start of the game, the player’s position is 0 and we try to move to :e.

When this line executes...

@game.map.rooms[anActor.position].method(aDirection).call

...it has the effect of getting the Room object at index 0 in the rooms array and then calling that object’s e method. The e method, incidentally, was created as an attribute accessor...

attr_accessor( :n, :s, :w, :e )

However, you could equally write methods in the normal way and call them by name (the name may be either a symbol or a string)...

def myMethod
        puts("hello world")
end

method("myMethod").call
method(:myMethod).call

Finally, I have wrapped up my simple Adventure game by simulating user interaction. In a real game, of course, commands would be entered from the keyboard or using a mouse. For testing purposes, though, I’ve entered them in code...

thePlayer = Actor.new("The Player", "You", 0 )
imp = Implementer.new( thePlayer )
puts( imp.moveActorTo( thePlayer, :e ) )
puts( imp.moveActorTo( thePlayer, :w ) )
puts( imp.moveActorTo( thePlayer, :n ) )
puts( imp.moveActorTo( thePlayer, :s ) )
puts( imp.moveActorTo( thePlayer, :e ) )
puts( imp.moveActorTo( thePlayer, :s ) )

So, starting in Room I first move East and end up in Room 1 (you can see that @r0 has 1 at its eastern exit)...

                                                     # Exits:  N,  S,  W,  E
@r0 = Room.new("Treasure Room", "a fabulous golden chamber",  -1,  2, -1,  1)

Next I move west (from my new location in Room 1), back into room 0, which is @r1’s Western exit...

@r1 = Room.new("Dragon's Lair", "a huge and glittering lair", -1, -1,  0, -1)

Now I try to move North but can’t as there is no exit to the North of Room 0 (No Exit is shown as -1)....

@r0 = Room.new("Treasure Room", "a fabulous golden chamber",  -1,  2, -1,  1)

....and so on.

I’ll develop this game further in a future article...

see: part two of this series


More resources...

Ruby

- Introduction To Ruby
- The Little Book Of Ruby

Writing An Adventure Game
- Adventures In C#
- Beginner’s Guide To Smalltalk

Zork
- Download The Zork Trilogy


Huw Collingbourne began programming in the early ‘80s. His first big programming project was an adventure game called The Golden Wombat Of Destiny (download here) which was written in Turbo Pascal. He is now Technology Director of SapphireSteel Software, developers of the Ruby In Steel Ruby On Rails IDE for Visual Studio 2005.

AddThis Social Bookmark Button

Forum

  • Adventures In Ruby
    14 August 2007, by Johnny P

    Reminds me of great times, thanks.

  • Adventures In Ruby
    14 August 2007, by Johnny P

    How about changing this:

    @game.map.rooms[anActor.position].method(aDirection).call

    To

    @game.map.rooms[anActor.position].send(aDirection)

    Probably a little more in the Ruby way given how smalltalk influenced it ;-)

    • Adventures In Ruby
      14 August 2007, by Huw Collingbourne

      Yup, send is fine by me (albeit I’m not sure how all these features are being changed in the migration from Ruby 1.8 -> 1.9 -> 2.0). At any rate, I’ll no doubt change quite a few things in my code as I progress (I hope) in the writing of this game... I don’t promise elegance, efficiency and clever algorithms. But I’m at least hoping that this project may have its moments of fun... :-)

      best wishes

      Huw

  • Adventures In Ruby
    10 August 2007, by zerohalo

    Fun. My 9 year old daughter and I have been writing a similar-style text-based game, as a way for her to learn programming. Ruby’s great for that, she’s catching it without too much difficulty and enjoying it too. Once we’ve finished writing it for the console, we’ll then change it to a GUI based game (probably using Shoes, or wxSugar) so she can learn that.

    • Adventures In Ruby
      6 May 2008, by QBass

      If you haven’t already, I request that you start a simple blog for you and your 9 year old to document your endeavor. It would be great to read a 9 year old perspective on the learning and application of a programming language.


Home