lib/gamefic/active.rb in gamefic-1.7.0 vs lib/gamefic/active.rb in gamefic-2.0.0

- old
+ new

@@ -1,280 +1,325 @@ -module Gamefic - class NotConclusionError < Exception - end - - # The Active module gives entities the ability to perform actions and - # participate in scenes. - # - module Active - # @return [Gamefic::Action] - attr_reader :last_action - - # @return [Gamefic::User::Base] - attr_reader :user - - # @return [Gamefic::Scene::Base] - attr_reader :scene - - attr_reader :next_scene - - # @return [Gamefic::Plot::Playbook] - #attr_accessor :playbook - - # @return [Array<Gamefic::Plot::Playbook>] - def playbooks - @playbooks ||= [] - end - - def connect user - @user = user - end - - # An array of actions waiting to be performed. - # - # @return [Array<String>] - def queue - @queue ||= [] - end - - # A hash of values representing the state of a performing entity. - # - # @return [Hash] - def state - @state = {} - @state.merge! scene.state unless scene.nil? - @state[:output] = messages - @state - end - - # Send a message to the entity. - # This method will automatically wrap the message in HTML paragraphs. - # To send a message without paragraph formatting, use #stream instead. - # - # @param message [String] - def tell(message) - if buffer_stack > 0 - append_buffer message - else - super - end - end - - # Send a message to the Character as raw text. - # Unlike #tell, this method will not wrap the message in HTML paragraphs. - # - # @param message [String] - def stream(message) - if buffer_stack > 0 - append_buffer message - else - super - end - end - - # Perform a command. - # The command can be specified as a String or a verb with a list of - # parameters. Either form should yield the same result, but the - # verb/parameter form can yield better performance since it bypasses the - # parser. - # - # The command will be executed immediately regardless of the entity's - # state. - # - # @example Send a command as a string - # character.perform "take the key" - # - # @example Send a command as a verb with parameters - # character.perform :take, @key - # - # @return [Gamefic::Action] - def perform(*command) - actions = [] - playbooks.reverse.each { |p| actions.concat p.dispatch(self, *command) } - execute_stack actions - end - - # Quietly perform a command. - # This method executes the command exactly as #perform does, except it - # buffers the resulting output instead of sending it to the user. - # - # @return [String] The output that resulted from performing the command. - def quietly(*command) - if buffer_stack == 0 - clear_buffer - end - set_buffer_stack buffer_stack + 1 - self.perform *command - set_buffer_stack buffer_stack - 1 - buffer - end - - # Perform an action. - # This is functionally identical to the `perform` method, except the - # action must be declared as a verb with a list of parameters. Use - # `perform` if you need to parse a string as a command. - # - # The command will be executed immediately regardless of the entity's - # state. - # - # @example - # character.execute :take, @key - # - # @return [Gamefic::Action] - def execute(verb, *params, quietly: false) - actions = [] - playbooks.reverse.each { |p| actions.concat p.dispatch_from_params(self, verb, params) } - execute_stack actions, quietly: quietly - end - - # Proceed to the next Action in the current stack. - # This method is typically used in Action blocks to cascade through - # multiple implementations of the same verb. - # - # @example Proceed through two implementations of a verb - # introduction do |actor| - # actor[:has_eaten] = false # Initial value - # end - # respond :eat do |actor| - # actor.tell "You eat something." - # actor[:has_eaten] = true - # end - # respond :eat do |actor| - # # This version will be executed first because it was implemented last - # if actor[:has_eaten] - # actor.tell "You already ate." - # else - # actor.proceed # Execute the previous implementation - # end - # end - # - def proceed quietly: false - #Director::Delegate.proceed_for self - return if performance_stack.empty? - a = performance_stack.last.shift - unless a.nil? - if quietly - if @buffer_stack == 0 - @buffer = "" - end - @buffer_stack += 1 - end - a.execute - if quietly - @buffer_stack -= 1 - @buffer - end - end - end - - # Immediately start a new scene for the character. - # Use #prepare if you want to declare a scene to be started at the - # beginning of the next turn. - # - def cue new_scene - @next_scene = nil - if new_scene.nil? - @scene = nil - else - @scene = new_scene.new(self) - @scene.start - end - end - - # Prepare a scene to be started for this character at the beginning of the - # next turn. - # - def prepare s - @next_scene = s - end - - # Return true if the character is expected to be in the specified scene on - # the next turn. - # - # @return [Boolean] - def will_cue? scene - (@scene.class == scene and @next_scene.nil?) or @next_scene == scene - end - - # Cue a conclusion. This method works like #cue, except it will raise a - # NotConclusionError if the scene is not a Scene::Conclusion. - # - def conclude scene - raise NotConclusionError unless scene <= Scene::Conclusion - cue scene - end - - # True if the character is in a conclusion. - # - # @return [Boolean] - def concluded? - !scene.nil? and scene.kind_of?(Scene::Conclusion) - end - - def performed order - order.freeze - @last_action = order - end - - def accessible? - false - end - - def inspect - to_s - end - - private - - def execute_stack actions, quietly: false - return nil if actions.empty? - a = actions.first - okay = true - unless a.meta? - playbooks.reverse.each do |playbook| - okay = validate_playbook playbook, a - break unless okay - end - end - if okay - performance_stack.push actions - proceed quietly: quietly - performance_stack.pop - end - a - end - - def validate_playbook playbook, action - okay = true - playbook.validators.each { |v| - result = v.call(self, action.verb, action.parameters) - okay = (result != false) - break unless okay - } - okay - end - - def buffer_stack - @buffer_stack ||= 0 - end - - def set_buffer_stack num - @buffer_stack = num - end - - def buffer - @buffer ||= '' - end - - def append_buffer str - @buffer += str - end - - def clear_buffer - @buffer = '' unless @buffer.empty? - end - - def performance_stack - @performance_stack ||= [] - end - end -end +module Gamefic + class NotConclusionError < RuntimeError; end + + # The Active module gives entities the ability to perform actions and + # participate in scenes. The Actor class, for example, is an Entity + # subclass that includes this module. + # + module Active + # The last action executed by the entity, as reported by the + # Active#performed method. + # + # @return [Gamefic::Action] + attr_reader :last_action + + # The scene in which the entity is currently participating. + # + # @return [Gamefic::Scene::Base] + attr_reader :scene + + # The scene class that will be cued for this entity on the next turn. + # Usually set with the #prepare method. + # + # @return [Class<Gamefic::Scene::Base>] + attr_reader :next_scene + + attr_reader :next_options + + # The prompt for the previous scene. + # + # @return [String] + attr_accessor :last_prompt + + # The input for the previous scene. + # + # @return [String] + attr_accessor :last_input + + # The playbooks that will be used to perform commands. + # + # @return [Array<Gamefic::World::Playbook>] + def playbooks + @playbooks ||= [] + end + + def syntaxes + playbooks.map(&:syntaxes).flatten + end + + # An array of actions waiting to be performed. + # + # @return [Array<String>] + def queue + @queue ||= [] + end + + # A hash of values representing the state of a performing entity. + # + # @return [Hash{Symbol => Object}] + def state + @state ||= {} + end + + def output + @output ||= {} + end + + # Send a message to the entity. + # This method will automatically wrap the message in HTML paragraphs. + # To send a message without paragraph formatting, use #stream instead. + # + # @param message [String] + def tell(message) + if buffer_stack > 0 + append_buffer format(message) + else + super + end + end + + # Send a message to the Character as raw text. + # Unlike #tell, this method will not wrap the message in HTML paragraphs. + # + # @param message [String] + def stream(message) + if buffer_stack > 0 + append_buffer message + else + super + end + end + + # Perform a command. + # The command can be specified as a String or a verb with a list of + # parameters. Either form should yield the same result, but the + # verb/parameter form can yield better performance since it bypasses the + # parser. + # + # The command will be executed immediately regardless of the entity's + # state. + # + # @example Send a command as a string + # character.perform "take the key" + # + # @example Send a command as a verb with parameters + # character.perform :take, @key + # + # @return [Gamefic::Action] + def perform(*command) + actions = [] + playbooks.reverse.each { |p| actions.concat p.dispatch(self, *command) } + execute_stack actions + end + + # Quietly perform a command. + # This method executes the command exactly as #perform does, except it + # buffers the resulting output instead of sending it to the user. + # + # @return [String] The output that resulted from performing the command. + def quietly(*command) + if buffer_stack == 0 + clear_buffer + end + set_buffer_stack buffer_stack + 1 + self.perform *command + set_buffer_stack buffer_stack - 1 + buffer + end + + # Perform an action. + # This is functionally identical to the `perform` method, except the + # action must be declared as a verb with a list of parameters. Use + # `perform` if you need to parse a string as a command. + # + # The command will be executed immediately regardless of the entity's + # state. + # + # @example + # character.execute :take, @key + # + # @return [Gamefic::Action] + def execute(verb, *params, quietly: false) + actions = [] + playbooks.reverse.each { |p| actions.concat p.dispatch_from_params(self, verb, params) } + execute_stack actions, quietly: quietly + end + + # Proceed to the next Action in the current stack. + # This method is typically used in Action blocks to cascade through + # multiple implementations of the same verb. + # + # @example Proceed through two implementations of a verb + # introduction do |actor| + # actor[:has_eaten] = false # Initial value + # end + # respond :eat do |actor| + # actor.tell "You eat something." + # actor[:has_eaten] = true + # end + # respond :eat do |actor| + # # This version will be executed first because it was implemented last + # if actor[:has_eaten] + # actor.tell "You already ate." + # else + # actor.proceed # Execute the previous implementation + # end + # end + # + def proceed quietly: false + return if performance_stack.empty? + a = performance_stack.last.shift + unless a.nil? + if quietly + if buffer_stack == 0 + @buffer = "" + end + set_buffer_stack(buffer_stack + 1) + end + a.execute + if quietly + set_buffer_stack(buffer_stack - 1) + @buffer + end + end + end + + # Immediately start a new scene for the character. + # Use #prepare if you want to declare a scene to be started at the + # beginning of the next turn. + # + # @param new_scene [Class] + def cue new_scene, **options + @next_scene = nil + if new_scene.nil? + @scene = nil + else + @scene = new_scene.new(self, **options) + @scene.start + end + end + + # Prepare a scene to be started for this character at the beginning of the + # next turn. As opposed to #cue, a prepared scene will not start until the + # current scene finishes. + # + # @param new_scene [Class] + def prepare new_scene, **options + @next_scene = new_scene + @next_options = options + end + + # Return true if the character is expected to be in the specified scene on + # the next turn. + # + # @return [Boolean] + def will_cue? scene + (@scene.class == scene and @next_scene.nil?) || @next_scene == scene + end + + # Cue a conclusion. This method works like #cue, except it will raise a + # NotConclusionError if the scene is not a Scene::Conclusion. + # + def conclude scene + raise NotConclusionError unless scene <= Scene::Conclusion + cue scene + end + + # True if the character is in a conclusion. + # + # @return [Boolean] + def concluded? + !scene.nil? && scene.kind_of?(Scene::Conclusion) + end + + # Record the last action the entity executed. This method is typically + # called when the entity performs an action in response to user input. + # + def performed action + action.freeze + @last_action = action + end + + def accessible? + false + end + + def inspect + to_s + end + + # Track the entity's performance of a scene. + # + def entered scene + klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene) + entered_scenes.push klass unless entered_scenes.include?(klass) + end + + # Determine whether the entity has performed the specified scene. + # + # @return [Boolean] + def entered? scene + klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene) + entered_scenes.include?(klass) + end + + private + + # @return [Array<Gamefic::Scene::Base>] + def entered_scenes + @entered_scenes ||= [] + end + + def execute_stack actions, quietly: false + return nil if actions.empty? + a = actions.first + okay = true + unless a.meta? + playbooks.reverse.each do |playbook| + okay = validate_playbook playbook, a + break unless okay + end + end + if okay + performance_stack.push actions + proceed quietly: quietly + performance_stack.pop + end + a + end + + def validate_playbook playbook, action + okay = true + playbook.validators.each { |v| + result = v.call(self, action.verb, action.parameters) + okay = (result != false) + break unless okay + } + okay + end + + def buffer_stack + @buffer_stack ||= 0 + end + + def set_buffer_stack num + @buffer_stack = num + end + + # @return [String] + def buffer + @buffer ||= '' + end + + def append_buffer str + @buffer += str + end + + def clear_buffer + @buffer = '' + end + + def performance_stack + @performance_stack ||= [] + end + end +end