Permalink
Browse files

Implemented actions

  • Loading branch information...
1 parent 495325b commit bb1701842ee75a68afdfd741dd3b801d5b4ced95 Avdi Grimm & Chris Strom committed Apr 4, 2010
Showing with 195 additions and 13 deletions.
  1. +91 −12 bin/play.rb
  2. +47 −1 data/petite_cave.if
  3. +47 −0 features/petite_cave.feature
  4. +10 −0 features/step_definitions/interactive_fiction_steps.rb
View
@@ -24,9 +24,9 @@ def logger
end
- attr_reader :story, :input, :output, :player_inventory
+ attr_reader :story, :input, :output, :player_inventory, :blackboard
- def_delegators :story, :synonyms, :object
+ def_delegators :story, :synonyms, :object, :actions
def_delegator :player_location, :objects, :objects_here
def initialize(story_path, options={})
@@ -38,6 +38,7 @@ def initialize(story_path, options={})
@ended = false
@player_room_name = :unset
@player_inventory = Set.new
+ @blackboard = {}
@logger = options.fetch(:logger) { lambda{} }
end
@@ -70,11 +71,27 @@ def execute_one_command!
when *story.exits
move_player!(command)
say_location
+ when *actions.keys
+ message, blackboard = safe_eval(actions[command].code)
+ if message then say message end
+ if blackboard && blackboard.is_a?(Hash)
+ self.blackboard.merge!(blackboard)
+ end
else
say "I don't know that word."
end
end
+ # check if the player is in the given room ID
+ def player_in?(room)
+ @player_room_name == room
+ end
+
+ # check to see if player has the given item ID in inventory
+ def player_has?(object)
+ player_inventory.include?(object)
+ end
+
def ended?
@ended
end
@@ -89,6 +106,14 @@ def move_player!(direction)
Game.log "Valid exits: #{player_location.exits.inspect}"
return false
end
+ if guard = player_location.exit_guards[direction]
+ allowed, message = safe_eval(guard)
+ Game.log "Guard result: #{allowed.inspect}, #{message.inspect}"
+ if !allowed
+ say message
+ return false
+ end
+ end
@player_room_name = new_room
Game.log "Now in #{new_room}: #{player_location.inspect}"
true
@@ -167,6 +192,14 @@ def expand_synonyms(command)
end
command.gsub("FNORD", "")
end
+
+ def safe_eval(code)
+ Game.log "Evaluating #{code}"
+ Thread.start do
+ $SAFE = 4
+ instance_eval(code)
+ end.join.value
+ end
end
class Story
@@ -177,27 +210,29 @@ def self.load(text)
end
- attr_reader :scanner, :starting_room, :rooms, :objects, :synonyms
+ attr_reader :scanner, :starting_room, :rooms, :objects, :synonyms, :actions
def initialize(text)
@text = text
@scanner = StringScanner.new(@text)
@rooms = {}
@objects = {}
+ @actions = {}
@starting_room = :unset
@synonyms = {}
end
# Load a Story from the provided text
def load
- while scanner.scan_until(/^(Room|Object|Synonyms)(\s+([@$]\w+))?:\s*\n/)
+ while scanner.scan_until(/^(Room|Object|Synonyms|Action)(\s+([@$!]\w+))?:\s*\n/)
type = scanner[1]
identifier = scanner[3]
Game.log "Processing #{type} definition for #{identifier}"
case type
when "Room" then add_room!(identifier)
when "Object" then add_object!(identifier)
when "Synonyms" then scan_synonyms!
+ when "Action" then add_action!(identifier)
else raise "Unrecognized definition of '#{type}'"
end
end
@@ -216,8 +251,9 @@ def add_room!(identifier)
room.description = scan_description!
Game.log "Set room #{identifier} description to #{room.description}"
when "Exits"
- scan_exits! do |direction, target_room|
+ scan_exits! do |direction, target_room, guard|
room.exits[direction] = target_room
+ room.exit_guards[direction] = guard
Game.log "Added #{identifier} exit #{direction} to #{target_room}"
end
when "Objects"
@@ -236,7 +272,7 @@ def add_object!(identifier)
scan_attributes! do |attribute|
case attribute
when "Terms"
- object.terms = scanner.scan_until(/\n/).strip.split(/\s*,\s*/)
+ object.terms = scan_terms!
object.title = object.terms.first
object.terms.each do |term|
synonyms[term.downcase] = identifier
@@ -251,6 +287,25 @@ def add_object!(identifier)
objects[identifier] = object
end
+ def add_action!(identifier)
+ Game.log "Adding action #{identifier}"
+ action = Action.new
+ scan_attributes! do |attribute|
+ case attribute
+ when "Terms" then
+ action.terms = scan_terms!
+ action.terms.each do |term|
+ synonyms[term.downcase] = identifier
+ end
+ when "Code"
+ action.code = scan_code!
+ Game.log "Code for action #{identifier}: #{action.code}"
+ else raise "Unrecognized attribute '#{attribute}'"
+ end
+ end
+ self.actions[identifier] = action
+ end
+
def scan_synonyms!
while scanner.scan(/\s+(\w+):(.*)\n/)
word = scanner[1]
@@ -268,12 +323,32 @@ def scan_description!
description
end
+ def scan_terms!
+ scanner.scan_until(/\n/).strip.split(/\s*,\s*/)
+ end
+
+ def scan_code!
+ if scanner.scan(/\s*\{\{\{(.*?)\}\}\}/m)
+ code = scanner[1]
+ scanner.scan_until(/\n/)
+ end
+ code
+ end
+
def scan_exits!
next_line!
- while scanner.scan(/\s+(\w+)\s+to\s+(@\w+).*\n/)
+ while scanner.scan(/\s+(\w+)\s+to\s+(@\w+)(\s+guarded by:)?/)
direction = scanner[1]
target = scanner[2]
- yield direction, target
+ guarded = !!scanner[3]
+ guard = if guarded
+ scan_code!
+ else
+ scanner.scan_until(/\n/)
+ nil
+ end
+ Game.log "Exit guard for #{direction}->#{target} is #{guard}"
+ yield direction, target, guard
end
end
@@ -307,12 +382,13 @@ def scan_attributes!
end
end
-class Room < Struct.new(:title, :description, :exits, :objects, :visited)
+class Room < Struct.new(:title, :description, :exits, :exit_guards, :objects, :visited)
def initialize(*args)
super(*args)
- self.exits ||= {}
- self.objects ||= Set.new
- self.visited ||= false
+ self.exits ||= {}
+ self.exit_guards ||= {}
+ self.objects ||= Set.new
+ self.visited ||= false
end
def visited?
@@ -323,6 +399,9 @@ def visited?
class GameObject < Struct.new(:title, :description, :terms)
end
+class Action < Struct.new(:terms, :code)
+end
+
if $PROGRAM_NAME == __FILE__
if ARGV.delete('-d') || ARGV.delete('--debug')
Game.logger.level = Logger::DEBUG
View
@@ -45,10 +45,56 @@ Room @valley:
rocky bed.
Exits:
north to @end_of_road
+ south to @slit
+Room @slit:
+ Title: at slit in streambed
+ Description:
+ At your feet all the water of the stream splashes into a 2-inch slit
+ in the rock. Downstream the streambed is bare rock.
+ Exits:
+ north to @valley
+ south to @depression
+
+Room @depression:
+ Title: outside grate
+ Description:
+ 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.
+ Exits:
+ north to @slit
+ enter to @grate_chamber guarded by:
+ {{{
+ if blackboard[:grate_unlocked]
+ [true, ""]
+ else
+ [false, "You can't go through a locked steel grate!"]
+ end
+ }}}
+
+Room @grate_chamber:
+ Title: below the grate
+ Description:
+ You are in a small chamber beneath a 3x3 steel grate to the surface.
+ A low crawl over cobbles leads inward to the west.
+
+Action !unlock:
+ Terms: unlock grate, open grate
+ Code:
+ {{{
+ if !player_in?('@depression')
+ ["There is no grate here", {}]
+ elsif !player_has?('$keys')
+ ["You have no keys!", {}]
+ else
+ ["The grate is now unlocked", {:grate_unlocked => true}]
+ end
+ }}}
+
Synonyms:
north: n
- sount: s
+ south: s
east: e
west: w
look: l, examine
@@ -25,6 +25,25 @@ Feature: Lead the user on an epic adventure
"""
You're at end of road again.
"""
+ When I enter "south"
+ Then I should see:
+ """
+ You are in a valley in the forest beside a stream tumbling along a
+ rocky bed.
+ """
+ When I enter "south"
+ Then I should see:
+ """
+ At your feet all the water of the stream splashes into a 2-inch slit
+ in the rock. Downstream the streambed is bare rock.
+ """
+ When I enter "south"
+ Then I should see:
+ """
+ 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.
+ """
Scenario: Blocked movement
When I enter "east"
@@ -108,3 +127,31 @@ Feature: Lead the user on an epic adventure
| Small bottle | water |
| Small bottle | small bottle |
| Tasty food | food |
+
+ Scenario: Try to enter locked grate
+ Given I am at the grate
+ When I enter "enter"
+ Then I should see "You can't go through a locked steel grate!"
+
+ Scenario: Try to unlock grate in wrong room
+ Given I am in the building
+ When I enter "unlock grate"
+ Then I should see "There is no grate here"
+
+ Scenario: Try to unlock grate without key
+ Given I am at the grate
+ When I enter "drop keys"
+ When I enter "unlock grate"
+ Then I should see "You have no keys!"
+
+ Scenario: Unlock grate
+ Given I am at the grate
+ When I enter "unlock grate"
+ Then I should see "The grate is now unlocked"
+ When I enter "enter"
+ Then I should see:
+ """
+ You are in a small chamber beneath a 3x3 steel grate to the surface.
+ A low crawl over cobbles leads inward to the west.
+ """
+
@@ -53,4 +53,14 @@
When "I enter \"east\""
end
+Given /^I am at the grate$/ do
+ Given "I am in the building"
+ When "I pick up the keys"
+ And "I pick up the bottle"
+ And "I pick up the food"
+ And "I enter \"west\""
+ And "I enter \"south\""
+ And "I enter \"south\""
+ And "I enter \"south\""
+end

0 comments on commit bb17018

Please sign in to comment.