rpcfn-interactive-fiction

Ruby Programming Challenge for Newbies: Interactive Fiction

Stars
28
  • Challenge: Interactive Fiction
    *** Solution Notes
    Thanks to everyone who entered my Interactive Fiction challenge! Here are some notes on the solutions.

***** Misc. I found it interesting that all but one entrant chose to use a combination of linewise parsing and string matching methods to parse the .if file.

***** Aldric Giacomoni

Good use of String#scan for the parsing.

This solution is tied to the petite_cave.if file - it doesn't generalize to other adventure files.

The use of File.open could be simplified.

: 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

The 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

***** Benoit Daloze

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 #<< operator for adding to inventory and room contents. I'm a little dubious about the use of #>> 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!

***** James Martin

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))

***** Tanzeeb Khalili

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 reguolar 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]]}

***** Vojto Rinik

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.

*** Introduction [[http://en.wikipedia.org/wiki/Interactive_fiction][Interactive fiction]] (IF) games, also known as text adventures, are computer games in which you must rely on your imagination to provide the visuals. They represent one of the longest-lived forms of computer entertainment. Originating in the 1970s, they reached their zenith in the 1980s, with classic Infocom games such as Zork and The Hitchiker's Guide to the Galaxy. At their best, interactive fiction games offer rich interaction, engrossing storylines, and phenomenal writing. While most gamers have moved on to more graphically rich games, there remains a [[http://www.ifarchive.org/][strong community]] of interactive fiction writers and players to this day.

I've always loved interactive fiction. Like most people who got into
programming young, as a teenager I first taught myself to program in order
to write my own games. The very first program I ever wrote was a tiny text
adventure game, written in the [[http://en.wikipedia.org/wiki/REXX][REXX]] programming language.

While I've since moved on to writing other kinds of software, I still think
writing interactive fiction engines is a great way to get a feel for a new
language. Unlike many canned programming challenges which primarily test
your knowledge of pure computer science concepts, writing a text adventure
game exercises many skills which translate directly to typical real-world
applications.

In order to write a successful IF engine, you must deal with challenges such
as:
- Modeling the interactions of real-world objects (such as rooms, items,
  and players) in software.
- Interpreting a Domain-Specific Language (DSL) in order to load games.
- Dealing with unpredictable user input.

Writing an IF engine is a fun way to learn how to tackle these problems in a
new language, and the skills you come away with can be applied directly to
a wide array of applications.

*** The Challenge In this challenge, you'll implement an interactive fiction game which mimics the first few rooms of the grandaddy of all text adventures, Collossal Cave. In order to succeed, your program will need to read in a "story" in the form of a simple DSL, interpret user commands, and track the player's progress and inventory. If you get all that working without too much trouble, I've also included an "extra credit" challenge to implement basic "puzzle" functionality in the game.

Here's a sample interaction with a finished implementation of the challenge:

: $ 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 food here. : There is a shiny brass lamp nearby. : There is a bottle of water here. : > get keys : OK : > get food : OK : > get lantern : OK : > get water : OK : > inventory : You are currently holding the following: : Set of keys : Tasty food : Brass lantern : Small bottle : > 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 str eambed is bare rock. : > s : You are in a 20-foot depression floored with bare dirt. Set into the dirt is : a strong steel grate mo unted in concrete. A dry streambed leads into the : depression. : > unlock grate : The grate is now unlocked : > enter : You are in a small chamber beneath a 3x3 steel grate to the surface. A low : crawl over cobbles leads inward to the west.

Here's a sample of the story DSL which defines the adventure:

: Room @end_of_road: : Title: at end of road again : Description: : 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. : Exits: : east to @building : enter to @building : south to @valley : : Room @building: : Title: inside building : Description: : You are inside a building, a well house for a large spring. : Exits: : west to @end_of_road : exit to @end_of_road : Objects: : $keys : $lamp : $food : $water_bottle : : Object $keys: : Terms: Set of keys, keys : Description: There are some keys on the ground here. : : Object $lamp: : Terms: Brass lantern, brass lamp, lamp, lantern : Description: There is a shiny brass lamp nearby.

The full story file can be found at =data/petite_cave.if=. This format is
one I've invented for this challenge. There is no formal specification for
it. Your program is only required to be able to parse the provided file
petite_cave.if in order to satisfy the challlenge.

*** Getting Started Here are steps for getting started on your entry:

  1. Clone the Github project avdi/rpcfn-interactive-fiction: =git clone git://github.com/avdi/rpcfn-interactive-fiction.git=

  2. Install Cucumber, if you don't have it already: =gem install cucumber=

  3. Run the acceptance tests by running Rake: =cd rpcfn-interactive-fiction; rake=

    You should see failure messages. That's because the implementation hasn't been written yet! Making the tests pass is up to you.

  4. I've provided a skeleton =bin/play.rb= to start you off. Edit that file to implement your interactive fiction engine.

  5. Drive your development by running =rake= periodically to see what's left to implement.

  6. Make sure to manually test your implementation by running it standalone: =ruby bin/play.rb data/petite_cave.if=

*** Extra Credit If you want an extra challenge, run : rake extra_credit and write code to make those tests pass as well. In order to make the extra credit features work, your engine will have to evaluate arbitrary scripts from the story file in order to implement guard conditions and custom actions.

The code executed by the guard/action part of the story file expects a simple API to be made available by your implementation:

  • =#blackboard= should return a hash. The blackboard is a place for story
    scripts to stow arbitrary story-specific values.
  • =#player_in?(room_id)= should return whether the player is in the specified
    room.
  • =#player_has?(object_id)= should return whether the player has the
    specified item in their inventory.
  • Exit guard clauses return an =Array= of [ALLOW, MESSAGE]. ALLOW is a
    boolean indicating whether the player's attempt to exit the room was
    allowed. MESSAGE must be shown to the user if provided.
  • Action scripts return an =Array= of [MESSAGE, BLACKBOARD]. Message must be
    shown to the user if non-nil. The values in BLACKBOARD should be merged
    into the =Hash= returned by =#blackboard=.

You may find it helpful to define these methods in the class =Game= and then execute the story scripts in the context of your Game object using =#instance_eval=.

The reason story scripts do not directly set values in the blackboard is so that it is possible to implement story script execution inside of [[http://www.ruby-doc.org/docs/ProgrammingRuby/taint.html][$SAFE jails]]. For extra, extra credit, write your implementation so that all story scripts are executed under =$SAFE= level 4.

*** Requirements - You must use only Ruby standard libraries in your implementation. - Your entry must at minimum pass the tests in =features/petite_cave.feature= - Your entry must be capable of running as a standalone executable. It must accept a single argument, the path of the story file. E.g.: : ruby bin/play.rb data/petite_cave.if

- Your entry must run under Ruby 1.8.7. If it runs under 1.9 as well, all
  the better.

*** Hints

To get an idea of how the finished product should behave, spend a few minutes playing the original Colossal Cave Adventure. If you are on Ubuntu you can install it with:
: apt-get install bsdgames

Or you can play it online here: [[http://www.ifiction.org/games/play.phpz?cat=&game=1&mode=html]]

There are a number of potential ways to go about parsing the story DSL:
- You could write a basic [[http://en.wikipedia.org/wiki/Recursive_descent_parser][recursive-descent parser]].
- You could use regular expression methods, like [[http://ruby-doc.org/core/classes/String.html#M000812][=String#scan=]]
- You could use Ruby's standard [[http://ruby-doc.org/core/classes/StringScanner.html][StringScanner]] library
- You could use regular expression substitutions to convert the text into
  valid Ruby code, and then [[http://ruby-doc.org/core/classes/Kernel.html#M005922][=#eval()=]] the story definition.

*** Conclusion Feel free to [[mailto:[email protected]][contact me]] if something about the challenge is unclear. Good Luck, and happy hacking!