Permalink
Browse files

+ multilevel dialog example

  • Loading branch information...
1 parent 072bfdc commit da59a689f6053ba676ff2830aed1100fd60d9c3e @floere committed Jun 8, 2011
View
@@ -6,7 +6,7 @@ class CLI
def execute *patterns
options = extract_options patterns
-
+
dialogs = find_dialogs_for patterns
puts "James: I haven't found anything to talk about (No files found). Exiting." or exit!(1) if dialogs.empty?
@@ -16,7 +16,7 @@ def execute *patterns
James.listen options
end
-
+
# Defines default options and extracts options from
# command line.
#
@@ -26,21 +26,21 @@ def extract_options patterns
silent = patterns.delete '-s'
silent_input = patterns.delete '-si'
silent_output = patterns.delete '-so'
-
+
options = {}
options[:input] = Inputs::Terminal if silent || silent_input
options[:output] = Outputs::Terminal if silent || silent_output
-
+
options
end
-
+
#
#
def find_dialogs_for patterns
patterns = ["**/*_dialog{,ue}.rb"] if patterns.empty?
Dir[*patterns]
end
-
+
#
#
def load_all dialogs
@@ -0,0 +1,129 @@
+# If using the gem, replace with:
+#
+# require 'rubygems'
+# require 'james'
+require File.expand_path '../../../lib/james', __FILE__
+
+# This is an example of a complex multilevel dialog not using the API.
+#
+
+# It is structured like this:
+#
+# Initially in Here ("talking to you")
+# Away <-> Here -> C <-> D -> E <-> F
+# -> E <-> F
+#
+# States which are chainable – like B, or D – can have dialogs attached to them.
+# The chainable states' phrases are always available.
+#
+# So if you came to F through B and D, you will always be able
+# to go to Away directly (from B), and to C, from D.
+#
+# What, why? (You might ask)
+# I suggest that conversations work a little this way.
+# People talk about A, leading them to B, then C.
+# At some point, people might want to go back or exit
+# the conversation, bringing them back to A.
+# (A could be Hello/Bye, with states :here, :away, and
+# :away -> "hello" -> :here, :here -> "bye" -> :away,
+# and :here being chainable, thus able to append dialogs
+# to it)
+#
+# So if you attached two dialogs to our simple hi/bye...
+# This is what it'd look like.
+#
+
+class Initial
+
+ include James::Dialog
+
+ initially :here
+
+ state :away do
+ hear 'Stay away'
+ hear 'Come here' => :here
+ hear ['Exit', 'Bye bye'] => :exit
+ into do
+ "Bye."
+ end
+ end
+ state :here do
+ chainable # Starting point for other dialogs.
+
+ hear 'Stay here'
+ hear 'Go away' => :away
+ into do
+ "Hi there. From here you can continue to C and E, or I can go away."
+ end
+ end
+ state :exit do
+ into { puts "Bye bye!"; exit! }
+ end
+
+end
+
+class ComplexCD
+
+ include James::Dialog
+
+ hear 'Go to C' => :c
+
+ state :c do
+ hear 'Go to D' => :d
+ into do
+ "Welcome to C. From here you can continue on to D, or go back to A."
+ end
+ end
+ state :d do
+ chainable
+
+ hear 'Stay in D'
+ hear 'Go to C' => :c
+ into do
+ "Welcome to D. From here you can continue to E, or back to C, or even A."
+ end
+ end
+
+end
+
+class ComplexEF
+
+ include James::Dialog
+
+ hear 'Go to E' => :e
+
+ state :e do
+ hear 'Go to F' => :f
+ into do
+ "Welcome to E. From here you can continue on to F, or go back to C, or even A."
+ end
+ end
+ state :f do
+ chainable
+
+ hear 'Stay in F'
+ hear 'Go to E' => :e
+ into do
+ "Welcome to F. From here you can go back to "
+ end
+ end
+
+end
+
+# Create the dialogs.
+#
+initial = Initial.new
+cd = ComplexCD.new
+ef = ComplexEF.new
+
+# Attach them to chainable states (an an explicit one, just as an example)
+#
+initial << cd
+initial.here << ef
+cd << ef
+
+# Create a controller which listens/speaks to the terminal.
+#
+controller = James::Controller.new initial
+controller.listen input: James::Inputs::Terminal,
+ output: James::Outputs::Terminal
View
@@ -13,6 +13,7 @@ module James; end
require File.expand_path '../james/dialogs', __FILE__
+require File.expand_path '../james/inputs', __FILE__
require File.expand_path '../james/inputs/base', __FILE__
require File.expand_path '../james/inputs/audio', __FILE__
require File.expand_path '../james/inputs/terminal', __FILE__
@@ -78,6 +78,9 @@ def hear definition
def state name, &block
@states ||= {}
@states[name] ||= block if block_given?
+ define_method name do
+ state_for name
+ end unless instance_methods.include? name
end
#
View
@@ -0,0 +1 @@
+module Inputs; end
@@ -6,16 +6,15 @@ module Markers
#
class Current < Marker
- attr_reader :initial
-
# Hear a phrase.
#
- # Returns a new marker if it crossed a boundary.
+ # Returns a new marker and self if it crossed a boundary.
# Returns itself if not.
#
def hear phrase, &block
+ return [self] unless hears? phrase
last = current
- process(phrase, &block) ? Memory.new(last) : self
+ process(phrase, &block) ? [Memory.new(last), self] : [self]
end
# Expects all phrases, not just internal.
@@ -24,6 +23,10 @@ def expects
current.expects
end
+ def current?
+ true
+ end
+
end
end
@@ -55,15 +55,18 @@ def transition phrase
#
#
def check
- reset && yield("Whoops. That led nowhere. Perhaps you didn't define the target state?") unless self.current
+ yield("Whoops. That led nowhere. Perhaps you didn't define the target state?") unless self.current
end
+ # Returns falsy if it stays the same.
+ #
def process phrase, &block
- return unless hears? phrase
exit_text = exit &block
+ last_context = current.context
transition phrase
check &block
into_text = enter &block
+ last_context != current.context
end
#
@@ -15,13 +15,19 @@ class Memory < Marker
# Returns itself if not.
#
def hear phrase, &block
- process(phrase, &block) ? Current.new(current) : self
+ return [self] unless hears? phrase
+ last = current
+ process(phrase, &block) ? [Memory.new(last), Current.new(current)] : [Current.new(current)]
end
# A marker does not care about phrases that cross dialog boundaries.
#
def expects
- current.internal
+ current.internal_expects
+ end
+
+ def current?
+ false
end
end
View
@@ -24,10 +24,7 @@ def initialize name, context
@name = name
@context = context
- # Transitions.
- #
- @external = {}
- @internal = {}
+ @transitions = {}
instance_eval(&Proc.new) if block_given?
end
@@ -46,7 +43,7 @@ def initialize name, context
#
def hear transitions
transitions = { transitions => name } unless transitions.respond_to?(:to_hash)
- @internal = expand(transitions).merge @internal
+ @transitions = expand(transitions).merge @transitions
end
# Execute this block or say the text when entering this state.
@@ -89,7 +86,7 @@ def chainable
# Description of self using name and transitions.
#
def to_s
- "#{self.class.name}(#{name}, #{context}, Internal: #{internal})"
+ "#{self.class.name}(#{name}, #{context}, #{expects})"
end
# The naughty privates of this class.
@@ -8,10 +8,19 @@ def transitions
@transitions
end
+ #
+ #
def expects
transitions.keys
end
+ #
+ #
+ def internal_expects
+ transitions.select { |phrase, target| target.respond_to?(:to_sym) || target == self.context }.keys
+ end
+
+
# Returns the next state for the given phrase.
#
# It accesses the context (aka Dialog) to get a full object state.
View
@@ -18,8 +18,6 @@ module James
#
class Visitors
- require 'set'
-
attr_reader :visitors
# A Visitors keeps a stack of visitors.
@@ -42,8 +40,10 @@ def initialize initial
#
def hear phrase, &block
@visitors = visitors.inject([]) do |remaining, visitor|
- new_visitor = visitor.hear phrase, &block
- remaining << new_visitor
+ markers = visitor.hear phrase, &block
+ remaining = remaining + markers
+ break remaining if remaining.last.current?
+ remaining
end
end

0 comments on commit da59a68

Please sign in to comment.