Skip to content

Instantly share code, notes, and snippets.

@jamesmartin
Created May 24, 2010 07:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesmartin/829a163a5135eec612cb to your computer and use it in GitHub Desktop.
Save jamesmartin/829a163a5135eec612cb to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# Run 'rake spec' for examples.
class GameDataParser
attr_reader :rooms
def initialize
@rooms = []
@objects = []
@synonyms = {}
end
def parse_rooms(data)
data.each do |line|
if line.match(/^Room\s+(@\w+):$/)
room = Hash.new
room["name"] = $1
@rooms << room
@parse_multi_lines = false
end
if line.match(/^\s+Title:(.*)$/)
last_room = @rooms.pop
last_room["title"] = $1.lstrip
@rooms << last_room
@parse_multi_lines = false
end
if line.match(/\s+Description:$/)
@parse_multi_lines = true
@ml_token = "description"
next
end
if line.match(/\s+Exits:$/)
@parse_multi_lines = true
@ml_token = "exits"
next
end
if line.match(/\s+Objects:$/)
@parse_multi_lines = true
@ml_token = "objects"
end
if @parse_multi_lines
case @ml_token
when "description"
last_room = @rooms.pop
if last_room[@ml_token]
last_room[@ml_token] = last_room[@ml_token] + " " + line.strip
else
last_room[@ml_token] = line.strip
end
@rooms << last_room
when "exits"
if line.match(/\s+(\w+)\s+to\s+(@\w+)$/)
last_room = @rooms.pop
if last_room[@ml_token]
exit_hash = last_room[@ml_token]
exit_hash[$1] = $2
else
exit_hash = {$1 => $2}
last_room[@ml_token] = exit_hash
end
@rooms << last_room
end
when "objects"
if line.match(/\s+(\$\w+)$/)
last_room = @rooms.pop
if last_room[@ml_token]
last_room[@ml_token] << $1
else
objects_in_room = [$1]
last_room[@ml_token] = objects_in_room
end
@rooms << last_room
end
end
end
end
@rooms
end
def parse_synonyms(data)
@parse_multi_lines = false
data.each do |line|
if line.match(/^Synonyms:$/)
@parse_multi_lines = true
end
if @parse_multi_lines
if line.match(/^\s+(\w+):\s+(\w+)$/)
@synonyms[$2] = $1
end
if line.match(/^\s+(\w+):\s+(\w+,\s+\w+).*$/)
action = $1
synonyms = $2.split ", "
synonyms.each do |synonym|
synonym.strip!
@synonyms[synonym] = action
end
end
end
end
@synonyms
end
def parse_objects(data)
data.each do |line|
if line.match(/^Object\s+(\$\w+):$/)
object = Hash.new
object["name"] = $1
@objects << object
end
if line.match(/\s+Terms:\s+(\w+.*)$/)
object = @objects.pop
terms = $1.split(", ")
unless object.has_key?("terms")
object["terms"] = terms
end
@objects << object
end
if line.match(/\s+Description:\s+(\w+.*)$/)
object = @objects.pop
object["description"] = $1
@objects << object
end
end
@objects
end
end
class GameWorld
def initialize(game_data, options={})
@game_data = game_data
@game_data_parser = options.fetch(:data_parser) { GameDataParser.new() }
@rooms = @game_data_parser.parse_rooms(@game_data)
@synonyms = @game_data_parser.parse_synonyms(@game_data)
@objects = @game_data_parser.parse_objects(@game_data)
@inventory = []
if @rooms
@first_room = @rooms[0]
@current_room = @rooms[0]
end
end
def get_current_room_description
result = @current_room["description"]
if current_room_contains_any_objects?
result += get_description_of_objects_in_current_room
end
return result
end
def get_description_of_objects_in_current_room
result = ""
@current_room["objects"].each do |room_object|
@objects.each do |object|
if room_object == object["name"]
result += "\n\n" + object["description"]
end
end
end
return result
end
def get_current_room_title
@current_room["title"]
end
def current_room_contains_any_objects?
@current_room.has_key?("objects")
end
def current_room_has_an_exit_named?(this_exit)
@current_room["exits"].has_key?(this_exit)
end
def current_room_exit_goes_to(exit_name)
@current_room["exits"][exit_name]
end
def move_to_room_named(room_name)
@rooms.each do |room|
if room["name"] == room_name
@current_room = room
end
end
end
def currently_at_start?
@current_room["name"] == @first_room["name"]
end
def resolve_synonymous_actions(action)
if action.match(/^(\w+)(.*$)/)
verb = $1
noun = $2
end
if @synonyms.has_key?(verb)
return @synonyms[verb] + noun
end
action
end
def invalid_action
"There is no way to go in that direction"
end
def inventory_empty_message
"You're not carrying anything"
end
def item_exists_in_current_room?(item)
@current_room["objects"].each do |object|
return true if object == item
end
false
end
def get_object_description_by_name(object_name)
@objects.each do |object|
if object["name"] == object_name
return object["description"]
end
end
end
def get_object_terms_by_name(object_name)
@objects.each do |object|
if object["name"] == object_name
return object["terms"][0]
end
end
end
def inventory_contents
description = ""
items = []
@inventory.each do |item|
items << get_object_terms_by_name(item)
end
if items.length > 0
description = items.join(", ")
end
description
end
def add_item_to_inventory(item)
if item_exists_in_current_room?(item)
@inventory << item
remove_object_from_current_room(item)
end
end
def add_object_to_current_room(object_name)
if @current_room.has_key?("objects")
@current_room["objects"] << object_name
else
objects = [object_name]
@current_room["objects"] = objects
end
end
def remove_object_from_current_room(object_name)
@current_room["objects"].delete(object_name)
end
def drop_item_from_inventory(item)
@inventory.delete(item)
add_object_to_current_room(item)
end
def resolve_syonymous_objects(item)
@objects.each do |object|
object["terms"].each do |term|
if term =~ /#{item}/i
return object["name"]
end
end
end
return "$" + item
end
def process_action(action)
action = resolve_synonymous_actions(action)
if current_room_has_an_exit_named?(action)
destination_room_name = current_room_exit_goes_to(action)
move_to_room_named(destination_room_name)
if currently_at_start?
return "You're " + get_current_room_title + "."
end
return get_current_room_description
else
case action
when "look"
get_current_room_description
when "inventory"
if @inventory.empty?
inventory_empty_message
else
inventory_contents
end
when /^take\s+(\w+.*)$/
item_to_get = resolve_syonymous_objects($1)
if add_item_to_inventory(item_to_get)
"OK"
end
when /^drop\s+(\w+)$/
item_to_drop = "$" + $1
drop_item_from_inventory(item_to_drop)
when "quit"
"Thanks for playing!"
else
invalid_action
end
end
end
end
class Game
PROMPT = "\n"
def initialize(story_path, options={})
@input = options.fetch(:input) { $stdin }
@output = options.fetch(:output) { $stdout }
@data_reader = options.fetch(:data_reader) { nil }
game_data = get_game_data(story_path)
@world = options.fetch(:game_world) { GameWorld.new(game_data) }
end
def get_game_data(story_path)
if @data_reader != nil
@data_reader.get_data(story_path)
else
data = File.open(story_path)
data.readlines
end
end
def play!
start!
execute_one_command! until ended?
end
def start!
@output << @world.get_current_room_description
@output << PROMPT
end
def execute_one_command!
@action = @input.readline.rstrip
@output << @world.process_action(@action)
@output << PROMPT
end
def ended?
@action == "quit"
end
end
if $PROGRAM_NAME == __FILE__
story_path = ARGV[0]
unless story_path
warn "Usage: #{$PROGRAM_NAME} STORY_FILE"
exit 1
end
game = Game.new(story_path)
game.play!
end
require 'spec_helper'
describe Game do
before(:each) do
@story_path = "/irrelevant/path/to/file.if"
@input = stub('$stdin')
end
describe "#initialize" do
it "delegates data reading responsibility" do
output = stub('$stdout').as_null_object
data_reader = stub('data_reader')
data_reader.should_receive(:get_data).with(@story_path).and_return(
['Irrelevant'])
game_world = stub('GameWorld').as_null_object
game = Game.new(@story_path,
:input => @input,
:output => output,
:data_reader => data_reader,
:game_world => game_world)
end
it "raises an exception if the data file is unavailable"
# Tricky to test this because the supplied Cucumber features require
# that a file on disk be opened by the Game object, leaving little room
# to inject the dependency.
end
describe "#start" do
it "asks the game world for the current room" do
output = stub('$stdout').as_null_object
game_world = stub('GameWorld')
data_reader = stub('DataReader').as_null_object
game = Game.new(@story_path,
:input => @input,
:output => output,
:data_reader => data_reader,
:game_world => game_world)
game_world.should_receive(:get_current_room_description)
game.start!
end
it "prints out the description of the first room" do
output = stub('$stdout')
game_world = stub('GameWorld')
data_reader = stub('DataReader').as_null_object
game = Game.new(@story_path,
:input => @input,
:output => output,
:data_reader => data_reader,
:game_world => game_world)
game_world.should_receive(:get_current_room_description).and_return("Some description")
output.should_receive(:<<).with("Some description")
output.should_receive(:<<).with("\n")
game.start!
end
end
describe "#execute_one_command!" do
it "prompts the user for input" do
# Would be nice to have a prompt indicator for the user (e.g. '>')
# but this breaks one of the cucumber features
output = stub('$stdout')
game_world = stub('GameWorld').as_null_object
data_reader = stub('DataReader').as_null_object
game = Game.new(@story_path,
:input => @input,
:output => output,
:data_reader => data_reader,
:game_world => game_world)
@input.should_receive(:readline).and_return('irrelevant')
game_world.should_receive(:process_action).and_return("Some description")
output.should_receive(:<<).with("Some description")
output.should_receive(:<<).with("\n")
game.execute_one_command!
end
it "asks the input for the current action" do
output = stub('$stdout').as_null_object
game_world = stub('GameWorld').as_null_object
data_reader = stub('DataReader').as_null_object
game = Game.new(@story_path,
:input => @input,
:output => output,
:data_reader => data_reader,
:game_world => game_world)
@input.should_receive(:readline).and_return('irrelevant')
game.execute_one_command!
end
it "asks the GameWorld to process the current action" do
output = stub('$stdout').as_null_object
data_reader = stub('DataReader').as_null_object
game_world = stub('GameWorld')
game = Game.new(@story_path,
:input => @input,
:output => output,
:data_reader => data_reader,
:game_world => game_world)
@input.should_receive(:readline).and_return('east')
game_world.should_receive(:process_action).with('east')
game.execute_one_command!
end
end
describe "#ended?" do
it "signals the end of the game if the user enters the quit command" do
output = stub('$stdout').as_null_object
data_reader = stub('DataReader').as_null_object
game_world = stub('GameWorld')
game = Game.new(@story_path,
:input => @input,
:output => output,
:data_reader => data_reader,
:game_world => game_world)
@input.should_receive(:readline).and_return('quit')
game_world.should_receive(:process_action)
game.execute_one_command!
game.ended?.should == true
end
end
end
describe GameWorld do
describe "#initialize" do
before(:each) do
@game_data = ["Irrelevant\n", "data\n"]
end
it "delegates data parsing responsibility" do
data_parser = stub('GameDataParser')
data_parser.should_receive(:parse_rooms).with(@game_data)
data_parser.should_receive(:parse_synonyms).with(@game_data)
data_parser.should_receive(:parse_objects).with(@game_data)
world = GameWorld.new(@game_data, :data_parser => data_parser)
end
end
describe "#get_current_room_description" do
it "returns the current room description" do
data_parser = stub('GameDataParser')
data_parser.should_receive(:parse_synonyms)
data_parser.should_receive(:parse_objects)
data_parser.should_receive(:parse_rooms).and_return(
[{"name" => "@my_name",
"description" => "Some description"}])
world = GameWorld.new(@game_data, :data_parser => data_parser)
world.get_current_room_description.should == ("Some description")
end
end
describe "#process_action" do
before(:each) do
@data_parser = stub('GameDataParser')
first_room = {"name" => "@first_room",
"title" => "at the First room",
"description" => "First description",
"exits" => {"east" => "@second_room"},
"objects" => ["$lamp"]}
second_room = {"name" => "@second_room",
"title" => "at the Second room",
"description" => "Second description",
"exits" => {"west" => "@first_room", "south" => "@third_room"}}
third_room = {"name" => "@third_room",
"title" => "at the Third room",
"description" => "Third description",
"exits" => {"west" => "@second_room", "north" => "@second_room"},
"objects" => ["$keys"]}
synonyms = {"e" => "east",
"w" => "west",
"get" => "take",
"l" => "look"}
keys = {"name" => "$keys",
"description" => "There are some keys here",
"terms" => ["keys"]}
lamp = {"name" => "$lamp",
"description" => "There is a brass lamp here",
"terms" => ["Brass lantern", "lantern"]}
objects = [keys, lamp]
rooms = [first_room, second_room, third_room]
@data_parser.should_receive(:parse_synonyms).and_return(synonyms)
@data_parser.should_receive(:parse_rooms).and_return(rooms)
@data_parser.should_receive(:parse_objects).and_return(objects)
@world = GameWorld.new(@game_data, :data_parser => @data_parser)
end
describe "movement actions" do
it "returns the description of the location for a valid action" do
@world.process_action("east").should == ("Second description")
end
it "returns a helpful message if a room is revisited" do
@world.process_action("east")
@world.process_action("west").should == ("You're at the First room.")
end
it "displays an error message if a given exit does not exist" do
invalid_room_message = "There is no way to go in that direction"
@world.process_action("invalid").should == (invalid_room_message)
end
end
describe "synonyms for actions" do
it "translates a valid synonym into the related action" do
@world.process_action("e").should == ("Second description")
end
end
describe "looking around" do
it "displays the current room's description" do
@world.process_action("east")
@world.process_action("look").should == ("Second description")
end
end
describe "describing rooms containing objects" do
it "describes a single object inside a room" do
@world.process_action("east").should == ("Second description")
@world.process_action("south").should ==
("Third description\n\nThere are some keys here")
end
end
describe "inventory actions" do
it "informs the user if nothing is in the inventory" do
@world.process_action("inventory").should ==
("You're not carrying anything")
end
it "confirms that the player picked up an item that exists in a room" do
@world.process_action("east")
@world.process_action("south")
@world.process_action("take keys").should == ("OK")
@world.process_action("inventory").should == ("keys")
end
it "confirms that a picked up item has been removed from the room" do
@world.process_action("east")
@world.process_action("south")
@world.process_action("take keys")
@world.process_action("inventory")
@world.process_action("look").should_not =~ /keys/
end
it "displays the contents of the inventory" do
@world.process_action("east")
@world.process_action("south")
@world.process_action("take keys")
@world.process_action("inventory").should == ("keys")
end
it "displays multiple items in the inventory on new lines" do
@world.process_action("take lantern")
@world.process_action("east")
@world.process_action("south")
@world.process_action("take keys")
@world.process_action("inventory").should == ("Brass lantern, keys")
end
it "allows items to be dropped from the inventory" do
@world.process_action("east")
@world.process_action("south")
@world.process_action("take keys")
@world.process_action("drop keys")
@world.process_action("inventory").should_not == ("keys")
end
it "puts dropped items into the current room" do
@world.process_action("east")
@world.process_action("south")
@world.process_action("take keys")
@world.process_action("drop keys")
@world.process_action("look").should =~ /keys/
end
it "puts dropped items into rooms that never contained objects" do
@world.process_action("east")
@world.process_action("south")
@world.process_action("take keys")
@world.process_action("north")
@world.process_action("drop keys")
@world.process_action("look").should =~ /keys/
end
it "allows syonymous terms to be used for getting objects" do
@world.process_action("get lantern").should == "OK"
end
it "handles multiple words as synoymous terms for getting objects" do
@world.process_action("get brass lantern").should == "OK"
end
end
end
end
describe GameDataParser do
before(:each) do
@parser = GameDataParser.new
end
describe "#parse_rooms" do
it "matches a single room with no contents" do
data = ["Room @my_name:\n"]
@parser.parse_rooms(data).should == ([{"name" => "@my_name"}])
end
it "matches a single room with a title" do
data = ["Room @my_name:\n", " Title: Some title\n"]
@parser.parse_rooms(data).should == ([{"name" => "@my_name", "title" => "Some title"}])
end
it "matches a single room with title and description" do
data = ["Room @my_name:\n",
" Title: Some title\n",
" Description:\n"]
data << " Simple description\n"
data << " With two lines\n"
expected_description = "Simple description With two lines"
@parser.parse_rooms(data).should == ([{
"name" => "@my_name",
"title" => "Some title",
"description" => expected_description}])
end
it "matches a single room with exits" do
data = ["Room @my_name:\n",
" Title: Some title\n",
" Description:\n",
" Some description\n",
" Exits:\n",
" east to @some_room\n",
" west to @another_room\n"]
expected = [{"name" => "@my_name",
"title" => "Some title",
"description" => "Some description",
"exits" => {"east" => "@some_room", "west" => "@another_room"}}]
@parser.parse_rooms(data).should == expected
end
it "parses object data from rooms" do
data = ["Room @my_name:\n",
" Title: Some title\n",
" Description:\n",
" Some description\n",
" Exits:\n",
" east to @some_room\n",
" west to @another_room\n",
" Objects:\n",
" $keys\n",
" $lamp\n"]
expected = [{"name" => "@my_name",
"title" => "Some title",
"description" => "Some description",
"exits" => {"east" => "@some_room", "west" => "@another_room"},
"objects" => ["$keys", "$lamp"]}]
@parser.parse_rooms(data).should == expected
end
end
describe "#parse_synonyms" do
it "matches a synonym definition section" do
data = ["Synonyms:\n",
" north: n\n"]
expected = {"n" => "north"}
@parser.parse_synonyms(data).should == expected
end
it "matches multiple synoyms for a single action" do
data = ["Synonyms:\n",
" look: l, examine \n"]
expected = {"l" => "look", "examine" => "look"}
@parser.parse_synonyms(data).should == expected
end
end
describe "#parse_objects" do
it "matches objects" do
data = ["Object $some_keys:\n",
" Terms: Set of keys, keys\n",
" Description: Some description\n"]
expected = [{"name" => "$some_keys",
"terms" => ["Set of keys", "keys"],
"description" => "Some description"}]
@parser.parse_objects(data).should == expected
end
it "handles Description lines that are unrelated" do
data = [" Description:\n",
"Object $some_keys:\n",
" Terms: Set of keys, keys\n",
" Description: Some description\n"]
expected = [{"name" => "$some_keys",
"terms" => ["Set of keys", "keys"],
"description" => "Some description"}]
@parser.parse_objects(data).should == expected
end
end
end
require 'rubygems'
require 'cucumber'
require 'cucumber/rake/task'
require 'spec/rake/spectask'
desc "run all specs"
Spec::Rake::SpecTask.new('spec') do |t|
t.spec_files = FileList['spec/*.rb']
t.spec_opts = ['--format', 'nested', '--color']
end
desc "run all specs with RCov"
Spec::Rake::SpecTask.new('spec_with_rcov') do |t|
t.spec_files = FileList['spec/*.rb']
t.spec_opts = ['--format', 'nested', '--color']
t.rcov = true
t.rcov_opts = ['--exclude', 'spec']
end
namespace :cucumber do
Cucumber::Rake::Task.new(:default) do |t|
t.cucumber_opts = "features --format pretty"
t.profile = "default"
end
Cucumber::Rake::Task.new(:extra_credit) do |t|
t.cucumber_opts = "features --format pretty"
t.profile = "extra_credit"
end
end
desc "Basic challenge acceptance tests"
task :default => ['cucumber:default']
desc "Extra credit tests"
task :extra_credit => ['cucumber:extra_credit']
# Make cucumber.el happy
task :cucumber => ['cucumber:extra_credit']
@jamesmartin
Copy link
Author

This submission is passing the basic cucumber features in 'petite_cave.feature', under Ruby version 1.8.7.

Put 'play_spec.rb' in <project_root>/spec/

Use the modified Rakefile to run the RSpec examples with 'rake spec', or 'rake spec_with_rcov'.

This solution was driven by following TDD as closely as possible. Refactoring has been somewhat neglected, however, every line of production code was written in response to a failing test, which means the code coverage should be fairly high.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment