RPCFN: Interactive Fiction (#9)

by Satish Talim on April 29, 2010

Ruby Programming Challenge For Newbies

RPCFN: Interactive Fiction (#9)

By Avdi Grimm

About Avdi Grimm

Avdi GrimmAvdi Grimm is a husband, father, software cultivator living in southern Pennsylvania, USA. He has been been working with the Ruby language for almost ten years, and is still finding new reasons to love it. He is the author of NullDB, Hammertime, AlterEgo, HookR and numerous other Rubygems and has contributed code to Gemcutter, UtilityBelt, the DataMapper/SimpleDB adapter, and other projects. He writes about Ruby and software development at his blog Virtuous Code.

Avdi has this to say about the challenge:

One of the hardest parts of getting started with a new programming language is picking problems to practice on. The problems need to be difficult enough to give you an opportunity to apply the features of the language, but small enough to complete in a reasonable amount of time. RPCFN is a great way to find practice problems which have been scoped to that “just right” size by experienced Rubyists. Complete these challenges and you will be sure to gain confidence and proficiency in the Ruby language.

Our Awesome Sponsors

This monthly programming challenge is co-sponsored by ELC Technologies and Backup My App.

A leading software development firm based in Santa Barbara, California

Founded in 1991, ELC Technologies delivers the value of next-generation Web technologies to today’s businesses, harnessing the power of mobile applications, cloud computing, and software as a service (SaaS). ELC Technologies’ leadership in agile software development processes has brought success to business-critical implementations for clients ranging from Cisco to Tribune Interactive to LiveNation. The company is a worldwide leader in Ruby on Rails development and has pioneered dynamic language development across multiple platforms. Short iterations, demos early and often, and constant client communication regarding vision and progress are all part of the ELC promise and what makes them such a valuable development partner for Global 2000 companies.

ELC Technologies is headquartered in Santa Barbara, California.

Backup My App

Backup My App is an automatic backup service for Ruby on Rails applications. You simply install the plugin to your Rails application and they handle the rest of the process. They store backup history for several weeks and you can restore any of them automatically. Try out their 1 GB plan for free. Backup My App has co-sponsored this challenge and is proud to make this contribution to the Ruby community.

Prizes

  • The participant with the best Ruby solution (if there is a tie between answers, then the one who posted first will be the winner) will be awarded any one of PeepCode’s Ruby on Rails screencasts and a free 10 GB account for a year from Backup My App.
  • From the remaining working Ruby solutions, three participants would be selected randomly and each one would be awarded any one of Pragmatic’s The Ruby Object Model and Metaprogramming screencasts.
  • All the participants in this challenge (except the participant with the best Ruby solution) will get a free 5 GB account for 6 months from Backup My App.

The four persons who win, can’t win again in the next immediate challenge but can still participate.

The Ruby Challenge

RPCFN

The Challenge

The entire challenge details are available at: http://github.com/avdi/rpcfn-interactive-fiction.

How to Enter the Challenge

Read the Challenge Rules. By participating in this challenge, you agree to be bound by these Challenge Rules. It’s free and registration is optional. You can enter the challenge just by posting the following as a comment to this blog post:

  1. Your name:
  2. Country of Residence:
  3. GIST URL of your Solution (i.e. Ruby code) with explanation and / or test cases:
  4. Code works with Ruby 1.8 / 1.9 / Both:
  5. Email address (will not be published):
  6. Brief description of what you do (will not be published):

Note:

  • As soon as we receive your GIST URL, we will fork your submission. This means that your solution is frozen and accepted. Please be sure that is the solution you want, as it is now recorded in time and is the version that will be evaluated.
  • All solutions posted would be hidden to allow participants to come up with their own solutions.
  • You should post your entries before midnight of 24th May 2010 (Indian Standard Time). No new solutions will be accepted from 25th May onwards.
  • On 25th May 2010 all the solutions will be thrown open for everyone to see and comment upon.
  • The winning entries will be announced on this blog before 30th May 2010. The winners will be sent their prizes by email.

More details on the RPCFN?

Please refer to the RPCFN FAQ for answers to the following questions:

Donations

RPCFN is entirely financed by RubyLearning and sometimes sponsors, so if you enjoy solving Ruby problems and would like to give something back by helping with the running costs then any donations are gratefully received.

Click here to lend your support to: Support RubyLearning With Some Love and make a donation at www.pledgie.com !

Acknowledgements

Special thanks to:

  • Avdi Grimm.
  • Sponsors ELC Technologies and Backup My App.
  • GitHub, for giving us access to a private repository on GitHub to store all the submitted solutions.
  • The RubyLearning team.

Questions?

Contact Satish Talim at satish [dot] talim [at] gmail.com OR if you have any doubts / questions about the challenge (the current problem statement), please post them as comments to this post and the author will reply asap.

The Participants

There are two categories of participants. Some are vying for the prizes and some are participating for the fun of it.

In the competition

  1. Aldric Giacomoni, USA
  2. Benoit Daloze, Belgium
  3. James Martin, Australia

Just for Fun

  1. Tanzeeb Khalili, Canada
  2. Vojto Rinik, Slovakia

The Winners

Winners

Congratulations to the winners of this Ruby Challenge. They are:

Previous Challenge

RPCFN: XML Transformer (#8) by Jamie van Dyke.

Note: All the previous challenges, sponsors and winners can be seen on the Ruby Programming Challenge for Newbies page.

Update

  • This challenge is now closed.
  • The (#10) challenge by Ryan Bates, USA is scheduled for 1st June 2010.

Technorati Tags: , , , , ,

Posted by Satish Talim

{ 28 comments… read them below or add one }

Aldric Giacomoni May 14, 2010 at 11:16 pm

I am having the hardest time just getting the output to fit! I am getting odd spaces where I don’t expect them, a trailing last space which ‘strip’ doesn’t take care of, duplications of spaces which ‘squeeze’ doesn’t remove …

Does anybody have any hints on this?

Reply

Avdi Grimm May 15, 2010 at 8:49 pm

Aldric, is your code online somewhere I could review it and maybe give you a hint or two?

Reply

Aldric Giacomoni May 17, 2010 at 10:30 pm

Hi Avdi,
Nice avatar. :)
Here’s what I have so far: http://gist.github.com/401343

Reply

Avdi Grtimm May 18, 2010 at 6:57 am

Aldric, when I run your code all the specs fail on missing method #we_know_this(). Did you post the complete code?

Reply

Aldric Giacomoni May 19, 2010 at 7:17 am

‘Complete’ yes. I haven’t started parsing commands, so — indeed — #we_know_this fails, because it is not implemented. It suffices to throw an error instead of attempting to parse anything to see the actual failures. My apologies.

Aldric Giacomoni May 20, 2010 at 2:15 am

Soo… I just checked out your code again, and copied over my code where applicable.
The first test passed. Apparently it’s some kind of quirk, either on Windows or whatever version of Ruby I was running on that Windows machine. Sadly, I don’t have access to that machine anymore so I can’t troubleshoot this further.

Aldric Giacomoni May 22, 2010 at 7:16 am

Solution (#1):

Aldric Giacomoni, USA
Code works with 1.8 and, after very brief testing, it seems like 1.9, too.
Solution: http://gist.github.com/401343
No extra credit; I will probably attempt to implement that to learn some more about things like instance_eval, but I won’t have time before the deadline — it will involve some tweaking.

Reply

Satoshi Asakawa May 25, 2010 at 5:42 am

Ran his code with ruby 1.8.6 (2010-02-04 patchlevel 398) [i386-mingw32] and confirmed author’s sample interaction. The following is the log:

————–
C:rpcfn-interactive-fiction>ruby bin/play.rb data/petite_cave.if
bin/play.rb:27: warning: parenthesize argument(s) for future version
You are standing at the end of a road before a small brick building. Around you
is a forest. A small stream flows out of the building and down a gully.
north
There is no way to go in that direction
east
You are inside a building, a well house for a large spring.
There are some keys on the ground here.
There is a shiny brass lamp nearby.
There is food here.
There is a bottle of water here.
get keys
OK
get food
OK
get lantern
OK
get water
OK
inventory
Small bottle
bottle
water
water bottle
Tasty food
food
Set of keys
keys
Brass lantern
brass lamp
lamp
lantern
west
You’re at end of road again.
s
You are in a valley in the forest beside a stream tumbling along a rocky bed.
s
At your feet all the water of the stream splashes into a 2-inch slit in the rock
. Downstream the streambed is bare rock.
s
You are in a 20-foot depression floored with bare dirt. Set into the dirt is a s
trong steel grate mounted in concrete. A dry streambed leads into the depression
.

C:rpcfn-interactive-fiction>

————–

He writes `No extra credit`, so his code works well. :)
My trivial comment is,

- it’s better to show a prompt for user input.
- it’s better to revise the `inventory` command output. No need to show all synonyms.

And his code also works well with Ruby 1.9.1, if he edits line 130 like this:

commands = commands.split “\n”

Reply

Avdi Grimm May 30, 2010 at 12:31 am

Aldric, thank you for your entry. Here are some comments:

Nice use of String#scan for parsing.

Unfortunately, it looks like your solution is tied to specific lines in the petite_cave.if file – it won’t generalize to other adventures.

You could simplify your use of the File class:

  def open file
    raw_data = ""
    begin
      File.open(file, 'r') { |f| raw_data << f.read }
    rescue
      throw ArgumentError, "File not found or impossible to open."
    end
    raw_data
  end

Could instead be:

  def open file
    File.open(file, 'r') { |f| f.read }
  rescue
    throw ArgumentError, "File not found or impossible to open."
  end

Or simply:

  def open file
    File.read(file)
  rescue
    throw ArgumentError, "File not found or impossible to open."
  end

Your method we_know_this:

  def we_know_this command
    # Alright; this is kinda hackish. I'm lazy. Sue me.
    # I mean, don't. Jeez. Where's your sense of humor?
    if @commands.keys.include? command
      return @commands[command]
    else
      return false
    end
  end

Could just be:

@commands[command]

Since it will return nil if no command is found, and nil is a “falsy” value.

Try to avoid the use of local variables when you can:

def find_casecmp array, item
     found = false
     array.each do |x|
       if x.casecmp(item) == 0
         found = true
         break
       end
     end
     found
   end

Could be:

def find_casecmp array, item
     array.each do |x|
       return true if x.casecmp(item) == 0
     end
   end

Or even better:

def find_casecmp array, item
   array.any? { |x| x.casecmp(item) == 0 }
end

Nice job, and happy hacking!

Note: Because the comment forms on the RubyLearning site don’t seem to support much in the way of formatting for code samples, etc. I’ve instead pout my comments in the challenge README at
http://github.com/avdi/rpcfn-interactive-fiction

Reply

Benoit Daloze May 22, 2010 at 6:45 pm

Solution (#2):

Benoit Daloze, Belgium
GIST URL of your Solution (i.e. Ruby code) with explanation and / or test cases: https://gist.github.com/56114e98b2109df8865a
Code works with Ruby 1.8 / 1.9 / Both: both, helped with backports
This solution pass all the test, use a simple parser using the indentation.
The abstract parsing is done in parser.rb, and all objects parse these results to fit the needs.

Reply

Satoshi Asakawa May 25, 2010 at 5:43 am

I don’t have Ruby 1.9.2. So, I added `require ‘backport’` in his play.rb and ran with Ruby 1.9.1. Then his code worked very well. It also supported Extra Credit! Awesome!

This is a log:

——————
C:\rpcfn-interactive-fiction>ruby bin/play.rb data/petite_cave.if
You are standing at the end of a road before a small brick building.
Around you is a forest. A small stream flows out of the building and
down a gully.
> n
There is no way to go in that direction
> e
You are inside a building, a well house for a large spring.
There are some keys on the ground here.
There is a shiny brass lamp nearby.
There is food here.
There is a bottle of water here.
> get keys
OK
> get lamp
OK
> i
Set of keys
Brass lantern
> w
You’re at end of road again.
> s
You are in a valley in the forest beside a stream tumbling along a
rocky bed.
> s
At your feet all the water of the stream splashes into a 2-inch slit
in the rock. Downstream the streambed is bare rock.
> s
You are in a 20-foot depression floored with bare dirt. Set into the
dirt is a strong steel grate mounted in concrete. A dry streambed
leads into the depression.
> s
There is no way to go in that direction
> enter
You can’t go through a locked steel grate!
> unlock grate
The grate is now unlocked
> e
There is no way to go in that direction
> enter
You are in a small chamber beneath a 3×3 steel grate to the surface.
A low crawl over cobbles leads inward to the west.

> q

C:\rpcfn-interactive-fiction>

Reply

Benoit Daloze May 25, 2010 at 6:06 am

> I don’t have Ruby 1.9.2. So, I added `require ‘backport’` in his play.rb and ran with Ruby 1.9.1.
Sorry, I just forgot a “.2″ somewhere to compare in main.rb
(git and gist updated !)

> Then his code worked very well. It also supported Extra Credit! Awesome!
:D

Reply

Benoit Daloze May 25, 2010 at 5:55 am
Avdi Grimm May 30, 2010 at 5:47 am

Writing a prompt (“>”) only when run standalone is a nice touch.

This solution is very nicely factored out into small classes and files.

Impressive use of many different built-in String, Enumerable, and Array methods. Although this makes the some of the parser code a little too dense to easily follow.

I like the use of the #<> to remove objects from a room – it’s nonobvious, and using #delete would match Ruby standard libraries better.

While for the most part the code neatly divides responsibilities between classes, it seems like the parsing code is split between the parser and the individual class initializers.

Yay extra credit!

Reply

Benoit Daloze May 30, 2010 at 10:02 pm

> While for the most part the code neatly divides responsibilities between classes, it seems like the parsing code is split between the parser and the individual class initializers.

So I changed that and now classes like Action are really short and nice. (See the git or the gist).
It was not that easy to move the code, but now I got rid of the specific constructors and they really use their superclass: AbstractObject.

I think my code is even better now :)
(except probably a bit of headache in Parser)

Reply

James Martin May 24, 2010 at 1:39 pm

Solution (#3):

James Martin, Australia
Gist URL: https://gist.github.com/829a163a5135eec612cb/60b796f2cc70547d5c69c9f79740582193fcde65
Code works with: version 1.8.7 of Ruby.
Email address: jimmymartin@gmail.com

Reply

Satoshi Asakawa May 25, 2010 at 5:44 am

Ran his code with ruby 1.9.1p376 (2009-12-07 revision 26041) [i386-mswin32]. It works well. I guess that his code doesn’t support Extra Credit.

Log is here:

——————-

C:\rpcfn-interactive-fiction>ruby bin/play.rb data/petite_cave.if
You are standing at the end of a road before a small brick building. Around you
is a forest. A small stream flows out of the building and down a gully.
north
There is no way to go in that direction
east
You are inside a building, a well house for a large spring.

There are some keys on the ground here.

There is a shiny brass lamp nearby.

There is food here.

There is a bottle of water here.
get keys
OK
get lamp
OK
get food
OK
get water
OK
i
Set of keys, Brass lantern, Tasty food, Small bottle
w
You’re at end of road again.
s
You are in a valley in the forest beside a stream tumbling along a rocky bed.
s
At your feet all the water of the stream splashes into a 2-inch slit in the rock
. Downstream the streambed is bare rock.
s
You are in a 20-foot depression floored with bare dirt. Set into the dirt is a
strong steel grate mounted in concrete. A dry streambed leads into the depressi
on.
s
There is no way to go in that direction
unlock grate
There is no way to go in that direction
open grate
There is no way to go in that direction
enter
There is no way to go in that direction
quit
Thanks for playing!

C:\rpcfn-interactive-fiction>

Reply

Avdi Grimm May 30, 2010 at 5:50 am

Nice use of meaningfully-named predicates like #current_room_has_an_exit_named?.

Some of the methods and classes are very long, and could have benefitted from being factored into smaller units.

When creating an empty Hash it’s a bit more conventional to use {} instead of Hash.new.

Prefixing reader methods with “get_” is a Java-style convention. In Ruby, we just name the method after what we are getting. E.g. Instead of:

get_description_of_objects_in_current_room, just description_of_objects_in_current_room

Try to use more specific Enumerable methods when applicable. E.g.

@rooms.each do |room|
if room["name"] == room_name
@current_room = room
end
end

Has the same semantics as:

if room = @rooms.detect{|room| room["name"] == room_name }
@current_room = room if room
end

And:

items = []
@inventory.each do |item|
items << get_object_terms_by_name(item)
end

becomes:

items = @inventory.map { |item| get_object_terms_by_name(item) }

or even:

items = @inventory.map(&method(:get_object_terms_by_name))

Reply

Tanzeeb Khalili May 24, 2010 at 10:55 pm

Solution (#4):

Tanzeeb Khalili, Canada
Just for Fun entry: http://gist.github.com/412189
Fairly simple solution, all files can go under bin/. Uses regular expressions to transform *.if files into executable code. No hope of running under $SAFE, but should qualify for extra credit.

Reply

Tanzeeb Khalili May 26, 2010 at 10:17 am

Would like to extend a special thanks to Satish, Satoshi and Avdi for this month’s contest.

Very creative and fun, and also quite the challenge!

Reply

Satoshi Asakawa May 26, 2010 at 2:32 pm

Ran his code with Ruby 1.9.1. It worked very well. It also supported Extra Credit! Awesome!

This is a log:

C:\rpcfn-interactive-fiction>ruby bin/play.rb data/petite_cave.if
You are standing at the end of a road before a small brick building. Around you
is a forest. A small stream flows out of the building and down a gully.
> north
There is no way to go in that direction
> east
You are inside a building, a well house for a large spring.
There are some keys on the ground here.
There is a shiny brass lamp nearby.
There is food here.
There is a bottle of water here.
> get lamp
OK
> get food
OK
> i
Brass lantern
Tasty food
> w
You’re at end of road again.
> s
You are in a valley in the forest beside a stream tumbling along a rocky bed.
> s
At your feet all the water of the stream splashes into a 2-inch slit in the rock
. Downstream the streambed is bare rock.
> s
You are in a 20-foot depression floored with bare dirt. Set into the dirt is a
strong steel grate mounted in concrete. A dry streambed leads into the depressi
on.
> s
There is no way to go in that direction
> enter
You can’t go through a locked steel grate!
> unlock grate
You have no keys!
> n
You’re at slit in streambed.
> n
You’re in valley.
> n
You’re at end of road again.
> e
You’re inside building.
> look
You are inside a building, a well house for a large spring.
There are some keys on the ground here.
There is a bottle of water here.
> get keys
OK
> w
You’re at end of road again.
> s
You’re in valley.
> s
You’re at slit in streambed.
> s
You’re outside grate.
> open grate
The grate is now unlocked
> enter
You are in a small chamber beneath a 3×3 steel grate to the surface. A low crawl
over cobbles leads inward to the west.
> exit
You’re outside grate.

C:\rpcfn-interactive-fiction>

Reply

Avdi Grimm May 30, 2010 at 5:52 am

This entry is a work of art. It should be mounted on the wall of a museum of beautiful code.

The code is neatly broken down into small classes and very short methods.

It makes good use of String/Enumerable built-in methods.

I love the pattern where Player#do_* methods become the commands available at the command line.

Using regular expressions to turn the story definition into executable code makes for an astonishingly succinct parser. I’m glad someone chose to go this route, because I think it’s a great, pragmatic technique for parsing DSLs.

One nitpick: instead of:

@exits[direction] || [nil, GUARDS[:none]]

use:

@exits.fetch(direction) {[nil, GUARDS[:none]]}

Reply

Tanzeeb Khalili May 31, 2010 at 7:53 pm

Wow, thank you Avdi!

Also, thanks again for the challenge. Very well executed, by far my favourite .

Reply

Vojto May 25, 2010 at 2:30 am

Hey there,

now I know I didn’t make it on time, I just now realized it’s *indian time* (wtf).

Anyway. I don’t know if this is a bug or what. But my tests are getting passed when I run

cucumber features/petite_cave.feature:105

When I run cucumber with everything, this test (drop keys) fails.

I don’t know why Cuke acts like this, the Background task should reset everything. But I found out it’s failing, because and the beginning of “Drop an object” scenario, there are already no keys in @building.

I have modified this scenario, so that error would be more obvious:

Scenario: Drop an object
When I enter “look”
Then I should not see “keys”
When I enter “east”
Then I should see “keys” <– here it fails
And I enter "get keys"
And I enter "west"
And I enter "drop keys"
And I enter "look"
Then I should see "There are some keys on the ground here."
When I enter "inventory"
Then I should not see "keys"

Again, it only happens when you run the whole thing at once, and I don't know — maybe this is happening only for me?

I'll post my solution (no gist, I'll just fork it, I can't participate anyway). I think it's pretty neat, I built a parser that I can use to parse pretty much any tree-structured DSL.

Reply

Vojto Rinik May 25, 2010 at 3:07 am

Solution (#5):

Um, so yeah, just for fun, but anyway I think I can post a little info about me:

Vojto Rinik, Slovakia
http://github.com/vojto/rpcfn-interactive-fiction (sorry guys, I don’t want to fit it into one gist, I really like my file structure)
Yay no tests. Vojto, stop being lazy and write tests!
Works with 1.8

^ If my previous comment got approved:
The problem was that I used Singleton class for describing world. The state of world persisted between tests.

Now here’s my solution: http://github.com/vojto/rpcfn-interactive-fiction

I’m late, I know, here in Europe it’s midnight right now, but whatever it’s not about winning, it’s about the fun.

Also, any comments to my code are welcome.

Reply

Satoshi Asakawa May 26, 2010 at 2:32 pm

Ran his code with Ruby 1.9.1. It worked and got the following log.
It doesn’t seem to support Extra Credit.

—————–

C:\rpcfn-interactive-fiction>ruby bin/play.rb data/petite_cave.if
You’re at end of road again.
["You are standing at the end of a road before a small brick building.\n", "Arou
nd you is a forest. A small stream flows out of the building and\n", "down a gu
lly.\n"]
north
There is no way to go in that direction
east
You’re inside building.
["You are inside a building, a well house for a large spring.\n"]
There are some keys on the ground here.
There is a shiny brass lamp nearby.
There is food here.
There is a bottle of water here.
get keys
OK
get brass lamp
OK
get water
OK
i
Set of keys
Brass lantern
Small bottle
w
You’re at end of road again.
["You are standing at the end of a road before a small brick building.\n", "Arou
nd you is a forest. A small stream flows out of the building and\n", "down a gu
lly.\n"]
s
You’re in valley.
["You are in a valley in the forest beside a stream tumbling along a\n", "rocky
bed.\n"]
s
You’re at slit in streambed.
["At your feet all the water of the stream splashes into a 2-inch slit\n", "in t
he rock. Downstream the streambed is bare rock.\n"]
s
You’re outside grate.
["You are in a 20-foot depression floored with bare dirt. Set into the\n", "dir
t is a strong steel grate mounted in concrete. A dry streambed\n", "leads into
the depression.\n"]
enter
Unknown command enter
unlock grate
Unknown command unlock
s
There is no way to go in that direction
exit
Unknown command exit
bin/play.rb:139:in `eof?’: Interrupt
from bin/play.rb:139:in `ended?’
from bin/play.rb:16:in `play!’
from bin/play.rb:150:in `’

C:\rpcfn-interactive-fiction>
—————–

His code looks almost good for accepting basic commands. But it’s better to improve output format and need quit command.

Reply

Avdi Grimm May 30, 2010 at 5:53 am

The parser tracks offsets (indentation) in the story definition file in order to determine the current scope. I like this!

Different responsibilities are nicely factored out into separate classes and methods. My only caveat is that, like some of the other solutions, the responsibility for parsing the story file seems to be partly split between the parser and the classes representing Rooms, etc.

This is an easily-understandable, straightforward solution.

Reply

Charles June 6, 2010 at 10:08 am

Forgot to add my just-for-fun entry – http://gist.github.com/427312.
Regexps the data into a YAML document easy indentation-aware parsing. The rest of the code is brief but messy – first thing that worked.
Don’t think I really understand ruby’s SAFE levels (first time I ever used them), but had a crack at getting that to work too.
Nice challenge!

Reply

Leave a Comment

{ 24 trackbacks }

Previous post:

Next post: